mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 21:01:08 +02:00
feat(memoro): show title source label below the title input
Mirror the "Voxtral via mana-stt" label that already sits under the
transcript: a small italic line directly below the title input
showing which tier (and roughly which model) generated the title.
This way the user can see at a glance whether the title came from
the local rules engine, from Gemma 4 in their browser, from
gemma3:4b on the Mana server, or from Google Gemini — and can
decide whether to keep it or rewrite manually.
Storage:
apps/mana/apps/web/src/lib/modules/memoro/llm-watcher.svelte.ts
- When applying a completed title task, the watcher now also
stamps memo.metadata.titleSource with the LlmTier string
('none' | 'browser' | 'mana-server' | 'cloud') from the queue
row's `source` field. Stored in the existing plaintext metadata
object — no encryption needed (the tier name isn't sensitive
and the encryption registry for memos only covers
title/intro/transcript). Existing metadata fields are
spread-preserved so we don't accidentally wipe STT failure
markers etc.
Manual override clears the marker:
apps/mana/apps/web/src/lib/modules/memoro/stores/memos.svelte.ts
- memosStore.update() now detects when `title` is in the diff
and clears `metadata.titleSource` so the DetailView stops
showing "via Mana-Server (gemma3:4b)" for a title the user
typed themselves. Only fires when title is actually present
in the update payload — non-title updates leave metadata alone
so we don't blow away other markers.
Display:
apps/mana/apps/web/src/lib/modules/memoro/views/DetailView.svelte
- New TITLE_SOURCE_LABELS map gives each tier a human-readable
label that surfaces the actual model name where known:
none → "Lokal (regelbasiert)"
browser → "Auf deinem Gerät (Gemma 4)"
mana-server → "Mana-Server (gemma3:4b)"
cloud → "Google Gemini"
We deliberately don't reuse @mana/shared-llm's tierLabel()
because the model name is more informative than the abstract
tier in this UX context.
- $derived `titleSourceLabel` reads memo.metadata.titleSource
and validates it via an isLlmTier type guard. Returns null
(→ no label rendered) when:
* the entity hasn't loaded yet
* a title task is currently in flight (titleIsGenerating)
* the title input is currently focused (user is editing)
* the metadata field is missing or not a known tier value
- New `<div class="source-label title-source-label">` slot
between the title-row and the properties block, with a small
CSS override (.title-source-label) for a tighter top gap and
a slight left indent so it visually lines up under the input
text rather than under the input border.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7fa3afcdc7
commit
2f00d9c5d3
3 changed files with 75 additions and 0 deletions
|
|
@ -158,9 +158,21 @@ async function applyRow(row: QueuedTask): Promise<void> {
|
|||
});
|
||||
}
|
||||
|
||||
// Stamp the title source on the memo's metadata so the DetailView can
|
||||
// render a "via Mana-Server" / "Auf deinem Gerät" / "Lokal (Regeln)"
|
||||
// label under the title — the same UX we already have under the
|
||||
// transcript ("Voxtral via mana-stt"). Stored as plaintext metadata
|
||||
// because the tier name isn't sensitive and the encryption registry
|
||||
// for memos only covers title/intro/transcript.
|
||||
const existingMetadata = (memo.metadata as Record<string, unknown> | null) ?? {};
|
||||
|
||||
const diff: Partial<LocalMemo> = {
|
||||
title: titleToWrite,
|
||||
updatedAt: new Date().toISOString(),
|
||||
metadata: {
|
||||
...existingMetadata,
|
||||
titleSource: row.source,
|
||||
},
|
||||
};
|
||||
await encryptRecord('memos', diff);
|
||||
await memoTable.update(row.refId, diff);
|
||||
|
|
|
|||
|
|
@ -158,6 +158,23 @@ export const memosStore = {
|
|||
...data,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// If the user is overwriting the title manually, clear the
|
||||
// auto-generated titleSource marker so the DetailView stops
|
||||
// showing "via Mana-Server" — the title is now the user's, not
|
||||
// the LLM's. We only touch metadata when title was actually in
|
||||
// the diff so we don't accidentally wipe other metadata fields
|
||||
// (e.g. STT failure markers) on a non-title update.
|
||||
if ('title' in data) {
|
||||
const existing = await memoTable.get(id);
|
||||
const existingMetadata = (existing?.metadata as Record<string, unknown> | null) ?? {};
|
||||
if ('titleSource' in existingMetadata) {
|
||||
const { titleSource: _omit, ...rest } = existingMetadata;
|
||||
void _omit;
|
||||
diff.metadata = rest;
|
||||
}
|
||||
}
|
||||
|
||||
await encryptRecord('memos', diff);
|
||||
await memoTable.update(id, diff);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -8,10 +8,27 @@
|
|||
import DetailViewShell from '$lib/components/DetailViewShell.svelte';
|
||||
import { memosStore } from '../stores/memos.svelte';
|
||||
import { llmQueueDb } from '$lib/llm-queue';
|
||||
import type { LlmTier } from '@mana/shared-llm';
|
||||
import { PushPin } from '@mana/shared-icons';
|
||||
import type { ViewProps } from '$lib/app-registry';
|
||||
import type { LocalMemo, ProcessingStatus } from '../types';
|
||||
|
||||
// Human-readable labels for the title-source badge below the title
|
||||
// input. We use these specific strings (not @mana/shared-llm's
|
||||
// generic tierLabel) so we can surface the actual model name where
|
||||
// known — "gemma3:4b" for mana-server, "Gemma 4" for browser tier
|
||||
// — rather than the abstract tier name.
|
||||
const TITLE_SOURCE_LABELS: Record<LlmTier, string> = {
|
||||
none: 'Lokal (regelbasiert)',
|
||||
browser: 'Auf deinem Gerät (Gemma 4)',
|
||||
'mana-server': 'Mana-Server (gemma3:4b)',
|
||||
cloud: 'Google Gemini',
|
||||
};
|
||||
|
||||
function isLlmTier(value: unknown): value is LlmTier {
|
||||
return value === 'none' || value === 'browser' || value === 'mana-server' || value === 'cloud';
|
||||
}
|
||||
|
||||
let { params, goBack }: ViewProps = $props();
|
||||
let memoId = $derived(params.memoId as string);
|
||||
|
||||
|
|
@ -97,6 +114,23 @@
|
|||
const titleIsGenerating = $derived(
|
||||
titleQueueRow.value?.state === 'pending' || titleQueueRow.value?.state === 'running'
|
||||
);
|
||||
|
||||
// Source label for the title — read from memo.metadata.titleSource
|
||||
// (set by the memoro LLM watcher when it applies an auto-generated
|
||||
// title, cleared by memosStore.update() when the user types over it).
|
||||
// Returns a label string or null if the title was manually entered.
|
||||
const titleSourceLabel = $derived.by(() => {
|
||||
const memo = detail.entity;
|
||||
if (!memo) return null;
|
||||
// Don't show a source label while we're still mid-generation.
|
||||
if (titleIsGenerating) return null;
|
||||
// Don't show a source label if the user has typed into the field
|
||||
// and edits haven't been saved yet — they're about to overwrite.
|
||||
if (detail.focused) return null;
|
||||
const metadata = (memo.metadata as Record<string, unknown> | null) ?? {};
|
||||
const source = metadata.titleSource;
|
||||
return isLlmTier(source) ? TITLE_SOURCE_LABELS[source] : null;
|
||||
});
|
||||
</script>
|
||||
|
||||
<DetailViewShell
|
||||
|
|
@ -128,6 +162,10 @@
|
|||
</button>
|
||||
</div>
|
||||
|
||||
{#if titleSourceLabel}
|
||||
<div class="source-label title-source-label">{titleSourceLabel}</div>
|
||||
{/if}
|
||||
|
||||
<div class="properties">
|
||||
<div class="prop-row">
|
||||
<span class="prop-label">Status</span>
|
||||
|
|
@ -271,4 +309,12 @@
|
|||
opacity: 0.7;
|
||||
font-style: italic;
|
||||
}
|
||||
.title-source-label {
|
||||
/* Sit visually right under the title input rather than the
|
||||
transcript box — needs a tighter top gap and a small left
|
||||
indent so it lines up with the text inside the input. */
|
||||
margin-top: 0.125rem;
|
||||
margin-bottom: 0.5rem;
|
||||
padding-left: 0.125rem;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue