mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 22:41:09 +02:00
feat(writing): token-usage in version history + draft drag-source
Two small UX wins. Version-history shows generation cost - VersionHistory takes a generations[] prop (DetailView already pulls one via useGenerationsForDraft) and looks up each AI version's linked Generation by id. When found, renders a monospace cost line under the version's wordcount: "1234 → 567 Tokens · 1.4s · ollama/gemma3:4b". - Skips silently when the generation row isn't there (e.g. older drafts before the field was tracked, or a generation that was reverted). - Lets the user see what each draft cost without digging into the Workbench audit timeline. Drafts as drag source - DraftCard wires `use:dragSource` with type='draft' + a payload carrying id / title / kind / content / wordCount / topic. Cards in the Writing list view are now drag origins for any drop target that declares acceptsDropFrom: ['draft']. - App-registry entry for 'writing' gets the matching collection / paramKey / dragType / getDisplayData fields so the workbench layer treats drafts as full first-class drag-citizens (sibling navigation, display fallbacks). - @mana/shared-ui DragType union extended with 'draft'. No drop-target wiring yet — articles' acceptsDropFrom can pick up 'draft' as a follow-up, but the M10 ExportMenu's "Als Artikel speichern" already covers that flow from the editor side. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b7a54ccd10
commit
2e9ec76d60
5 changed files with 68 additions and 4 deletions
|
|
@ -1354,6 +1354,16 @@ registerApp({
|
|||
),
|
||||
},
|
||||
],
|
||||
collection: 'writingDrafts',
|
||||
paramKey: 'draftId',
|
||||
dragType: 'draft',
|
||||
getDisplayData: (item) => ({
|
||||
title: (item.title as string) || (item.topic as string) || 'Draft',
|
||||
subtitle:
|
||||
typeof item.wordCount === 'number' && item.wordCount > 0
|
||||
? `${item.wordCount} Wörter`
|
||||
: undefined,
|
||||
}),
|
||||
});
|
||||
|
||||
registerApp({
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { formatDate } from '$lib/i18n/format';
|
||||
import { dragSource } from '@mana/shared-ui/dnd';
|
||||
import StatusBadge from './StatusBadge.svelte';
|
||||
import { KIND_LABELS } from '../constants';
|
||||
import type { Draft, DraftVersion } from '../types';
|
||||
|
|
@ -41,7 +42,22 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<button type="button" class="card" onclick={open}>
|
||||
<button
|
||||
type="button"
|
||||
class="card"
|
||||
onclick={open}
|
||||
use:dragSource={{
|
||||
type: 'draft',
|
||||
data: () => ({
|
||||
id: draft.id,
|
||||
title: draft.title || draft.briefing.topic || 'Unbenannt',
|
||||
kind: draft.kind,
|
||||
content: currentVersion?.content ?? '',
|
||||
wordCount: currentVersion?.wordCount ?? 0,
|
||||
topic: draft.briefing.topic,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<header>
|
||||
<span class="kind" title={kind.de}>
|
||||
<span aria-hidden="true">{kind.emoji}</span>
|
||||
|
|
|
|||
|
|
@ -3,17 +3,26 @@
|
|||
"Wiederherstellen" flips the draft's `currentVersionId` back via the
|
||||
store. Versions are immutable snapshots so Restore is a pointer change,
|
||||
not a destructive revert.
|
||||
|
||||
AI-generated versions also show their generation cost (token usage +
|
||||
duration) when the linked Generation row is available, so the user can
|
||||
see what each draft cost without digging into Workbench audit views.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { draftsStore } from '../stores/drafts.svelte';
|
||||
import type { DraftVersion } from '../types';
|
||||
import type { DraftVersion, Generation } from '../types';
|
||||
import { formatDate as formatLocaleDate } from '$lib/i18n/format';
|
||||
|
||||
let {
|
||||
versions,
|
||||
generations = [],
|
||||
currentVersionId,
|
||||
draftId,
|
||||
}: {
|
||||
versions: DraftVersion[];
|
||||
/** Generations for this draft. Used to show token-usage / duration
|
||||
* next to AI-generated versions; harmless to omit. */
|
||||
generations?: Generation[];
|
||||
currentVersionId: string | null;
|
||||
draftId: string;
|
||||
} = $props();
|
||||
|
|
@ -21,6 +30,8 @@
|
|||
// Newest on top.
|
||||
const sorted = $derived([...versions].sort((a, b) => b.versionNumber - a.versionNumber));
|
||||
|
||||
const generationById = $derived(new Map(generations.map((g) => [g.id, g])));
|
||||
|
||||
async function restore(versionId: string) {
|
||||
await draftsStore.restoreVersion(draftId, versionId);
|
||||
}
|
||||
|
|
@ -28,18 +39,34 @@
|
|||
function formatDate(iso: string): string {
|
||||
const d = new Date(iso);
|
||||
if (Number.isNaN(d.getTime())) return '';
|
||||
return d.toLocaleString('de-DE', {
|
||||
return formatLocaleDate(d, {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
}
|
||||
|
||||
function genCostLine(version: DraftVersion): string | null {
|
||||
if (!version.generationId) return null;
|
||||
const gen = generationById.get(version.generationId);
|
||||
if (!gen) return null;
|
||||
const parts: string[] = [];
|
||||
if (gen.tokenUsage) {
|
||||
parts.push(`${gen.tokenUsage.input} → ${gen.tokenUsage.output} Tokens`);
|
||||
}
|
||||
if (gen.durationMs) {
|
||||
parts.push(`${(gen.durationMs / 1000).toFixed(1)}s`);
|
||||
}
|
||||
if (gen.model) parts.push(gen.model);
|
||||
return parts.length > 0 ? parts.join(' · ') : null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<ul class="history">
|
||||
{#each sorted as version (version.id)}
|
||||
{@const isCurrent = version.id === currentVersionId}
|
||||
{@const costLine = genCostLine(version)}
|
||||
<li class="version" class:current={isCurrent}>
|
||||
<div class="meta">
|
||||
<strong>v{version.versionNumber}</strong>
|
||||
|
|
@ -54,6 +81,9 @@
|
|||
<span>{version.wordCount} Wörter</span>
|
||||
<span class="date">{formatDate(version.createdAt)}</span>
|
||||
</div>
|
||||
{#if costLine}
|
||||
<div class="cost" title="Verbrauch + Modell der zugehörigen Generation">{costLine}</div>
|
||||
{/if}
|
||||
{#if version.summary}
|
||||
<p class="summary">{version.summary}</p>
|
||||
{/if}
|
||||
|
|
@ -114,6 +144,12 @@
|
|||
font-size: 0.75rem;
|
||||
color: var(--color-text-muted, rgba(0, 0, 0, 0.55));
|
||||
}
|
||||
.cost {
|
||||
font-size: 0.7rem;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
color: var(--color-text-muted, rgba(0, 0, 0, 0.55));
|
||||
opacity: 0.8;
|
||||
}
|
||||
.summary {
|
||||
margin: 0;
|
||||
font-size: 0.8rem;
|
||||
|
|
|
|||
|
|
@ -510,6 +510,7 @@
|
|||
<h2>Versionen</h2>
|
||||
<VersionHistory
|
||||
versions={versions ?? []}
|
||||
generations={generations ?? []}
|
||||
currentVersionId={draft.currentVersionId}
|
||||
draftId={draft.id}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -27,7 +27,8 @@ export type DragType =
|
|||
| 'dream'
|
||||
| 'journal-entry'
|
||||
| 'first'
|
||||
| 'wish';
|
||||
| 'wish'
|
||||
| 'draft';
|
||||
|
||||
export interface DragPayload<T = Record<string, unknown>> {
|
||||
type: DragType;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue