diff --git a/apps/mana/apps/web/src/lib/modules/todo/ListView.svelte b/apps/mana/apps/web/src/lib/modules/todo/ListView.svelte
index 8dc4a8804..42d7e0b3d 100644
--- a/apps/mana/apps/web/src/lib/modules/todo/ListView.svelte
+++ b/apps/mana/apps/web/src/lib/modules/todo/ListView.svelte
@@ -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 @@
+
+
{#each filtered() as task (task.id)}
{@const taskTagIds = getTaskTagIds(task)}
diff --git a/apps/mana/apps/web/src/lib/modules/todo/stores/tasks.svelte.ts b/apps/mana/apps/web/src/lib/modules/todo/stores/tasks.svelte.ts
index 778cd0c8d..c7dd7ff55 100644
--- a/apps/mana/apps/web/src/lib/modules/todo/stores/tasks.svelte.ts
+++ b/apps/mana/apps/web/src/lib/modules/todo/stores/tasks.svelte.ts
@@ -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 {
+ 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) {
const raw = await taskTable.get(id);
if (!raw) return;