mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:41:09 +02:00
feat(todo): typed quick-add gets the same LLM enrichment as voice
Press Enter on "Steuererklärung morgen 14 Uhr hoch" and the task lands instantly with your exact text as the title — then a background pass through /api/v1/voice/parse-task swaps in dueDate + priority once mana-llm answers. The title only gets rewritten when the LLM actually finds structured info (dueDate or priority); for plain titles like "Mülltonnen rausstellen" the typed text is left alone, since silently "cleaning up" perfectly fine input is more annoying than helpful. Pulled the parse + STT-then-parse plumbing apart so both flows share parseTaskText() and only differ in policy: voice always applies the LLM title (raw transcripts are noisy), typed only when there's structured payoff. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c32a5a57de
commit
d8da11a4ff
2 changed files with 70 additions and 18 deletions
|
|
@ -65,8 +65,13 @@
|
|||
if (!title) return;
|
||||
const data: Record<string, unknown> = { title };
|
||||
if (filter === 'today') data.dueDate = new Date().toISOString();
|
||||
await tasksStore.createTask(data as { title: string; dueDate?: string });
|
||||
const task = await tasksStore.createTask(data as { title: string; dueDate?: string });
|
||||
newTitle = '';
|
||||
// Background LLM enrichment: if the user typed something like
|
||||
// "Steuererklärung morgen 14 Uhr hoch", swap in dueDate + priority
|
||||
// once mana-llm answers. The task is already in the list with
|
||||
// the user's exact title, so this only ever adds detail.
|
||||
void tasksStore.enrichTaskFromText(task.id, title);
|
||||
}
|
||||
|
||||
async function handleVoiceComplete(blob: Blob, durationMs: number) {
|
||||
|
|
|
|||
|
|
@ -133,23 +133,11 @@ export const tasksStore = {
|
|||
return;
|
||||
}
|
||||
|
||||
// Step 2: structured extraction. parse-task gracefully falls
|
||||
// back to { title: transcript, dueDate: null, ... } if mana-llm
|
||||
// is unreachable, so we don't wrap this in another try/catch.
|
||||
const parseResponse = await fetch('/api/v1/voice/parse-task', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ transcript, language }),
|
||||
});
|
||||
const parsed = parseResponse.ok
|
||||
? ((await parseResponse.json()) as {
|
||||
title: string;
|
||||
dueDate: string | null;
|
||||
priority: 'low' | 'medium' | 'high' | null;
|
||||
labels: string[];
|
||||
})
|
||||
: { title: transcript, dueDate: null, priority: null as null, labels: [] as string[] };
|
||||
|
||||
// Step 2: structured extraction. For voice we always apply the
|
||||
// LLM's title since the raw transcript ("erinnere mich morgen
|
||||
// daran die steuererklärung zu machen") is much noisier than
|
||||
// what the user actually wants to see in the list.
|
||||
const parsed = await this.parseTaskText(transcript, language);
|
||||
const update: Record<string, unknown> = { title: parsed.title };
|
||||
if (parsed.dueDate) update.dueDate = parsed.dueDate;
|
||||
if (parsed.priority) update.priority = parsed.priority;
|
||||
|
|
@ -164,6 +152,65 @@ export const tasksStore = {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Background enrichment for typed quick-add. Runs the same LLM
|
||||
* parser the voice flow uses, but with a stricter rule: only update
|
||||
* the task if the LLM actually found structured info (dueDate or
|
||||
* priority). For typed input the user already sees their exact text
|
||||
* as the title — silently rewriting it to a "cleaner" version when
|
||||
* the LLM didn't find a date/priority would be surprising and
|
||||
* occasionally wrong, so we leave it alone in that case.
|
||||
*/
|
||||
async enrichTaskFromText(taskId: string, text: string, language = 'de'): Promise<void> {
|
||||
const trimmed = text.trim();
|
||||
if (!trimmed) return;
|
||||
try {
|
||||
const parsed = await this.parseTaskText(trimmed, language);
|
||||
if (!parsed.dueDate && !parsed.priority) return;
|
||||
|
||||
const update: Record<string, unknown> = {};
|
||||
if (parsed.title && parsed.title !== trimmed) update.title = parsed.title;
|
||||
if (parsed.dueDate) update.dueDate = parsed.dueDate;
|
||||
if (parsed.priority) update.priority = parsed.priority;
|
||||
if (Object.keys(update).length === 0) return;
|
||||
await this.updateTask(taskId, update);
|
||||
} catch {
|
||||
// Silent — typed quick-add already gave the user a usable
|
||||
// task; an LLM failure should never undo that.
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* POST a transcript or typed text to the parse-task proxy and
|
||||
* return the structured result. The proxy already falls back to
|
||||
* { title: text, dueDate: null, ... } when mana-llm is unreachable
|
||||
* or returns garbage, so callers can use the result unconditionally.
|
||||
*/
|
||||
async parseTaskText(
|
||||
text: string,
|
||||
language = 'de'
|
||||
): Promise<{
|
||||
title: string;
|
||||
dueDate: string | null;
|
||||
priority: 'low' | 'medium' | 'high' | null;
|
||||
labels: string[];
|
||||
}> {
|
||||
const response = await fetch('/api/v1/voice/parse-task', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ transcript: text, language }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
return { title: text, dueDate: null, priority: null, labels: [] };
|
||||
}
|
||||
return (await response.json()) as {
|
||||
title: string;
|
||||
dueDate: string | null;
|
||||
priority: 'low' | 'medium' | 'high' | null;
|
||||
labels: string[];
|
||||
};
|
||||
},
|
||||
|
||||
async updateTask(id: string, data: Record<string, unknown>) {
|
||||
const raw = await taskTable.get(id);
|
||||
if (!raw) return;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue