From 2f00d9c5d3fd4e2b497ce882a199b64f67f1cf9e Mon Sep 17 00:00:00 2001 From: Till JS Date: Thu, 9 Apr 2026 14:19:59 +0200 Subject: [PATCH] feat(memoro): show title source label below the title input MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 `
` 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) --- .../lib/modules/memoro/llm-watcher.svelte.ts | 12 +++++ .../lib/modules/memoro/stores/memos.svelte.ts | 17 +++++++ .../modules/memoro/views/DetailView.svelte | 46 +++++++++++++++++++ 3 files changed, 75 insertions(+) diff --git a/apps/mana/apps/web/src/lib/modules/memoro/llm-watcher.svelte.ts b/apps/mana/apps/web/src/lib/modules/memoro/llm-watcher.svelte.ts index a4fda3a17..6a3164137 100644 --- a/apps/mana/apps/web/src/lib/modules/memoro/llm-watcher.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/memoro/llm-watcher.svelte.ts @@ -158,9 +158,21 @@ async function applyRow(row: QueuedTask): Promise { }); } + // 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 | null) ?? {}; + const diff: Partial = { title: titleToWrite, updatedAt: new Date().toISOString(), + metadata: { + ...existingMetadata, + titleSource: row.source, + }, }; await encryptRecord('memos', diff); await memoTable.update(row.refId, diff); diff --git a/apps/mana/apps/web/src/lib/modules/memoro/stores/memos.svelte.ts b/apps/mana/apps/web/src/lib/modules/memoro/stores/memos.svelte.ts index ad7484866..ee3ef610a 100644 --- a/apps/mana/apps/web/src/lib/modules/memoro/stores/memos.svelte.ts +++ b/apps/mana/apps/web/src/lib/modules/memoro/stores/memos.svelte.ts @@ -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 | null) ?? {}; + if ('titleSource' in existingMetadata) { + const { titleSource: _omit, ...rest } = existingMetadata; + void _omit; + diff.metadata = rest; + } + } + await encryptRecord('memos', diff); await memoTable.update(id, diff); }, diff --git a/apps/mana/apps/web/src/lib/modules/memoro/views/DetailView.svelte b/apps/mana/apps/web/src/lib/modules/memoro/views/DetailView.svelte index 088cf2773..b412fedde 100644 --- a/apps/mana/apps/web/src/lib/modules/memoro/views/DetailView.svelte +++ b/apps/mana/apps/web/src/lib/modules/memoro/views/DetailView.svelte @@ -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 = { + 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 | null) ?? {}; + const source = metadata.titleSource; + return isLlmTier(source) ? TITLE_SOURCE_LABELS[source] : null; + });
+ {#if titleSourceLabel} +
{titleSourceLabel}
+ {/if} +
Status @@ -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; + }