1import {
2 VoiceInputController,
3 voiceInputBlocksSubmission,
4 type VoiceDraftSnapshot,
5 type VoiceInputControllerDependencies,
6} from "@t3tools/client-runtime/voice-input";
7
8export type VoiceInputTarget = {
9 readonly ownerKey: string;
10 /** Names the draft being dictated into while its composer is off screen. */
11 readonly label?: string;
12 readonly readDraft: () => VoiceDraftSnapshot | null;
13 readonly commitDraft: VoiceInputControllerDependencies["commitDraft"];
14 readonly subscribe: () => () => void;
15};
16
17export function createVoiceInputTarget(
18 ownerKey: string,
19 readText: () => string | null,
20 commitDraft: VoiceInputTarget["commitDraft"],
21 selection: VoiceDraftSnapshot["selection"],
22 subscribeToChanges: (onChange: () => void) => () => void,
23): VoiceInputTarget {
24 let revision = 0;
25 return {
26 ownerKey,
27 readDraft: () => {
28 const text = readText();
29 if (text === null) return null;
30 return { ownerKey, text, selection, revision };
31 },
32 commitDraft,
33 subscribe: () => {
34 let previousText = readText();
35 return subscribeToChanges(() => {
36 const text = readText();
37 if (text !== previousText) {
38 previousText = text;
39 revision += 1;
40 }
41 });
42 },
43 };
44}
45
46export class VoiceInputSession {
47 readonly controller: VoiceInputController;
48 private target: VoiceInputTarget | null = null;
49 private unsubscribe: (() => void) | null = null;
50
51 constructor(dependencies: Omit<VoiceInputControllerDependencies, "readDraft" | "commitDraft">) {
52 this.controller = new VoiceInputController({
53 ...dependencies,
54 onStateChange: (state) => {
55 if (!voiceInputBlocksSubmission(state)) {
56 this.unsubscribe?.();
57 this.unsubscribe = null;
58 }
59 dependencies.onStateChange(state);
60 },
61 readDraft: () => this.target?.readDraft() ?? null,
62 commitDraft: (text, selection) => this.target?.commitDraft(text, selection),
63 });
64 }
65
66 get ownerKey(): string | null {
67 return this.target?.ownerKey ?? null;
68 }
69
70 get label(): string | null {
71 return this.target?.label ?? null;
72 }
73
74 cancel(ownerKey: string | null): void {
75 if (ownerKey !== null && this.ownerKey === ownerKey) this.controller.cancel();
76 }
77
78 retry(): Promise<void> {
79 return this.target ? this.start(this.target) : Promise.resolve();
80 }
81
82 start(target: VoiceInputTarget): Promise<void> {
83 if (voiceInputBlocksSubmission(this.controller.currentState)) return Promise.resolve();
84 this.target = target;
85 this.unsubscribe = target.subscribe();
86 return this.controller.start();
87 }
88}