mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-17 11:49:39 +02:00
feat(todo): voice quick-add in workbench ListView via shared <VoiceCaptureBar>
Speak a task and it lands in the list as a placeholder while mana-stt transcribes it; the title swaps in once the transcript returns. No date/priority/label parsing yet — that's a follow-up that needs an LLM pass over the transcript. For now the whole transcript becomes the task title and the user can edit inline. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9b3d7c7325
commit
b841a24e73
2 changed files with 60 additions and 0 deletions
|
|
@ -20,6 +20,7 @@
|
|||
import { dropTarget, dragSource } from '@mana/shared-ui/dnd';
|
||||
import type { TagDragData } from '@mana/shared-ui/dnd';
|
||||
import { useAllTags, getTagsByIds } from '@mana/shared-stores';
|
||||
import VoiceCaptureBar from '$lib/components/voice/VoiceCaptureBar.svelte';
|
||||
|
||||
let { navigate, goBack, params }: ViewProps = $props();
|
||||
|
||||
|
|
@ -68,6 +69,10 @@
|
|||
newTitle = '';
|
||||
}
|
||||
|
||||
async function handleVoiceComplete(blob: Blob, durationMs: number) {
|
||||
await tasksStore.createFromVoice(blob, durationMs, 'de');
|
||||
}
|
||||
|
||||
// Context menu
|
||||
let ctxMenu = $state<{
|
||||
visible: boolean;
|
||||
|
|
@ -160,6 +165,13 @@
|
|||
<input bind:value={newTitle} placeholder="Neue Aufgabe..." class="add-input" />
|
||||
</form>
|
||||
|
||||
<VoiceCaptureBar
|
||||
idleLabel="Aufgabe sprechen"
|
||||
feature="todo-voice-capture"
|
||||
reason="Aufgaben werden verschlüsselt gespeichert. Dafür brauchst du ein Mana-Konto."
|
||||
onComplete={handleVoiceComplete}
|
||||
/>
|
||||
|
||||
<div class="task-list">
|
||||
{#each filtered() as task (task.id)}
|
||||
{@const taskTagIds = getTaskTagIds(task)}
|
||||
|
|
|
|||
|
|
@ -83,6 +83,54 @@ export const tasksStore = {
|
|||
return plaintextSnapshot;
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a task from a voice recording. Inserts a placeholder task
|
||||
* immediately so the user sees instant feedback in the list, then
|
||||
* fills in the real title once mana-stt returns the transcript.
|
||||
*
|
||||
* No date/priority parsing yet — that needs an LLM pass and is its
|
||||
* own follow-up. The user can edit the task inline like any other.
|
||||
*/
|
||||
async createFromVoice(blob: Blob, _durationMs: number, language = 'de') {
|
||||
const placeholder = await this.createTask({ title: 'Sprachaufgabe wird transkribiert…' });
|
||||
void this.transcribeIntoTask(placeholder.id, blob, language);
|
||||
return placeholder;
|
||||
},
|
||||
|
||||
/**
|
||||
* Upload an audio blob to /api/v1/voice/transcribe and write the
|
||||
* transcript into an existing task as the new title. On failure,
|
||||
* surfaces the error inline so the user isn't left with the
|
||||
* "wird transkribiert…" placeholder forever.
|
||||
*/
|
||||
async transcribeIntoTask(taskId: string, blob: Blob, language?: string): Promise<void> {
|
||||
try {
|
||||
const form = new FormData();
|
||||
const ext = blob.type.includes('webm')
|
||||
? '.webm'
|
||||
: blob.type.includes('mp4')
|
||||
? '.m4a'
|
||||
: '.audio';
|
||||
form.append('file', blob, `task${ext}`);
|
||||
if (language) form.append('language', language);
|
||||
|
||||
const response = await fetch('/api/v1/voice/transcribe', {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
});
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error(text || `HTTP ${response.status}`);
|
||||
}
|
||||
const result = (await response.json()) as { text: string };
|
||||
const transcript = (result.text ?? '').trim() || 'Sprachaufgabe';
|
||||
await this.updateTask(taskId, { title: transcript });
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
await this.updateTask(taskId, { title: `Sprachaufgabe (Fehler: ${msg})` });
|
||||
}
|
||||
},
|
||||
|
||||
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