@opengeni/react-native 1.4.5-canary.37615713321001
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -0
- package/README.md +74 -0
- package/dist/adapters.d.ts +53 -0
- package/dist/adapters.js +3 -0
- package/dist/adapters.js.map +1 -0
- package/dist/attachments.d.ts +75 -0
- package/dist/chunk-3VSZAQ7G.js +391 -0
- package/dist/chunk-3VSZAQ7G.js.map +1 -0
- package/dist/chunk-5ZVK7WKA.js +88 -0
- package/dist/chunk-5ZVK7WKA.js.map +1 -0
- package/dist/chunk-EEDIMY3L.js +240 -0
- package/dist/chunk-EEDIMY3L.js.map +1 -0
- package/dist/chunk-M23SDI4R.js +83 -0
- package/dist/chunk-M23SDI4R.js.map +1 -0
- package/dist/chunk-PHJBSAR3.js +2006 -0
- package/dist/chunk-PHJBSAR3.js.map +1 -0
- package/dist/environment.d.ts +19 -0
- package/dist/expo.d.ts +25 -0
- package/dist/expo.js +267 -0
- package/dist/expo.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +3345 -0
- package/dist/index.js.map +1 -0
- package/dist/presentation.d.ts +262 -0
- package/dist/read-acknowledgement.d.ts +39 -0
- package/dist/realtime/call-events.d.ts +11 -0
- package/dist/realtime/call-view.d.ts +26 -0
- package/dist/realtime/expo-call.d.ts +11 -0
- package/dist/realtime/native-call.d.ts +104 -0
- package/dist/realtime/native-realtime.d.ts +35 -0
- package/dist/realtime/use-native-session-realtime.d.ts +25 -0
- package/dist/session-view.d.ts +87 -0
- package/dist/timeline/activity.d.ts +34 -0
- package/dist/timeline/composer.d.ts +86 -0
- package/dist/timeline/controls.d.ts +27 -0
- package/dist/timeline/decisions.d.ts +29 -0
- package/dist/timeline/feedback.d.ts +26 -0
- package/dist/timeline/icon-nodes.generated.d.ts +738 -0
- package/dist/timeline/icon.d.ts +11 -0
- package/dist/timeline/index.d.ts +32 -0
- package/dist/timeline/index.js +5419 -0
- package/dist/timeline/index.js.map +1 -0
- package/dist/timeline/markdown.d.ts +11 -0
- package/dist/timeline/markdown.js +415 -0
- package/dist/timeline/markdown.js.map +1 -0
- package/dist/timeline/message-attachments.d.ts +13 -0
- package/dist/timeline/message-timeline.d.ts +36 -0
- package/dist/timeline/model-picker.d.ts +17 -0
- package/dist/timeline/primitives.d.ts +85 -0
- package/dist/timeline/queue-dock.d.ts +19 -0
- package/dist/timeline/session-actions.d.ts +34 -0
- package/dist/timeline/session-list.d.ts +50 -0
- package/dist/timeline/session-screen.d.ts +35 -0
- package/dist/timeline/session-signals.d.ts +37 -0
- package/dist/timeline/sheet.d.ts +14 -0
- package/dist/timeline/theme.d.ts +58 -0
- package/dist/timeline/thinking-orb.d.ts +13 -0
- package/dist/timeline/turn.d.ts +80 -0
- package/dist/ui/attention.d.ts +14 -0
- package/dist/ui/cards.d.ts +21 -0
- package/dist/ui/composer.d.ts +21 -0
- package/dist/ui/format.d.ts +13 -0
- package/dist/ui/icons.d.ts +9 -0
- package/dist/ui/index.d.ts +12 -0
- package/dist/ui/index.js +1757 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/steps-sheet.d.ts +10 -0
- package/dist/ui/theme.d.ts +63 -0
- package/dist/ui/timeline.d.ts +48 -0
- package/dist/ui/web-tokens.gen.d.ts +90 -0
- package/dist/use-native-session.d.ts +40 -0
- package/dist/view-model.d.ts +52 -0
- package/dist/voice-input.d.ts +102 -0
- package/dist/webrtc.d.ts +9 -0
- package/dist/webrtc.js +19 -0
- package/dist/webrtc.js.map +1 -0
- package/expo-module.config.json +7 -0
- package/ios/OpenGeniCall.podspec +22 -0
- package/ios/OpenGeniCallAppDelegateSubscriber.swift +29 -0
- package/ios/OpenGeniCallCenter.swift +189 -0
- package/ios/OpenGeniCallModule.swift +64 -0
- package/package.json +93 -0
- package/src/adapters.ts +150 -0
- package/src/attachments.ts +396 -0
- package/src/environment.tsx +214 -0
- package/src/expo.ts +295 -0
- package/src/index.ts +98 -0
- package/src/presentation.ts +285 -0
- package/src/read-acknowledgement.ts +72 -0
- package/src/realtime/call-events.ts +41 -0
- package/src/realtime/call-view.tsx +220 -0
- package/src/realtime/expo-call.ts +49 -0
- package/src/realtime/native-call.ts +266 -0
- package/src/realtime/native-realtime.ts +115 -0
- package/src/realtime/use-native-session-realtime.ts +29 -0
- package/src/session-view.tsx +2251 -0
- package/src/timeline/activity.tsx +736 -0
- package/src/timeline/composer.tsx +686 -0
- package/src/timeline/controls.tsx +151 -0
- package/src/timeline/decisions.tsx +651 -0
- package/src/timeline/feedback.tsx +251 -0
- package/src/timeline/icon-nodes.generated.ts +1830 -0
- package/src/timeline/icon.tsx +44 -0
- package/src/timeline/index.ts +67 -0
- package/src/timeline/markdown.tsx +510 -0
- package/src/timeline/message-attachments.tsx +155 -0
- package/src/timeline/message-timeline.tsx +1042 -0
- package/src/timeline/model-picker.tsx +450 -0
- package/src/timeline/primitives.tsx +561 -0
- package/src/timeline/queue-dock.tsx +331 -0
- package/src/timeline/session-actions.tsx +379 -0
- package/src/timeline/session-list.tsx +338 -0
- package/src/timeline/session-screen.tsx +488 -0
- package/src/timeline/session-signals.tsx +312 -0
- package/src/timeline/sheet.tsx +153 -0
- package/src/timeline/theme.tsx +128 -0
- package/src/timeline/thinking-orb.tsx +93 -0
- package/src/timeline/turn.tsx +569 -0
- package/src/ui/attention.tsx +220 -0
- package/src/ui/cards.tsx +267 -0
- package/src/ui/composer.tsx +296 -0
- package/src/ui/format.ts +86 -0
- package/src/ui/icons.tsx +131 -0
- package/src/ui/index.ts +18 -0
- package/src/ui/steps-sheet.tsx +71 -0
- package/src/ui/theme.ts +261 -0
- package/src/ui/timeline.tsx +837 -0
- package/src/ui/web-tokens.gen.ts +94 -0
- package/src/use-native-session.ts +219 -0
- package/src/view-model.ts +485 -0
- package/src/voice-input.ts +382 -0
- package/src/webrtc.ts +30 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
// src/voice-input.ts
|
|
4
|
+
function appendDictation(draft, transcript) {
|
|
5
|
+
const final = transcript.trim();
|
|
6
|
+
if (!final) return draft;
|
|
7
|
+
if (!draft) return final;
|
|
8
|
+
return /\s$/u.test(draft) ? `${draft}${final}` : `${draft} ${final}`;
|
|
9
|
+
}
|
|
10
|
+
var NativeTranscriptionError = class extends Error {
|
|
11
|
+
status;
|
|
12
|
+
constructor(message, status) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "NativeTranscriptionError";
|
|
15
|
+
this.status = status;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
function isRetryableTranscriptionError(cause) {
|
|
19
|
+
const status = cause instanceof NativeTranscriptionError ? cause.status : null;
|
|
20
|
+
if (status === null) return true;
|
|
21
|
+
return status === 408 || status === 425 || status === 429 || status === 499 || status >= 500;
|
|
22
|
+
}
|
|
23
|
+
var TRANSCRIPTION_RETRY_DELAYS_MS = [1e3, 3e3, 8e3];
|
|
24
|
+
function dictationSegmentSeconds(maxDurationSeconds) {
|
|
25
|
+
if (maxDurationSeconds === null || !(maxDurationSeconds > 0)) return null;
|
|
26
|
+
return Math.max(1, maxDurationSeconds - Math.min(3, maxDurationSeconds * 0.1));
|
|
27
|
+
}
|
|
28
|
+
var wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
29
|
+
function useNativeVoiceInput(input) {
|
|
30
|
+
const [status, setStatus] = useState("idle");
|
|
31
|
+
const [error, setError] = useState(null);
|
|
32
|
+
const [durationSeconds, setDurationSeconds] = useState(0);
|
|
33
|
+
const [level, setLevel] = useState(null);
|
|
34
|
+
const [hasSavedRecording, setHasSavedRecording] = useState(false);
|
|
35
|
+
const generation = useRef(0);
|
|
36
|
+
const statusRef = useRef("idle");
|
|
37
|
+
const segments = useRef([]);
|
|
38
|
+
const inFlight = useRef(/* @__PURE__ */ new Map());
|
|
39
|
+
const capturedBefore = useRef(0);
|
|
40
|
+
const rolling = useRef(false);
|
|
41
|
+
const latest = useRef(input);
|
|
42
|
+
latest.current = input;
|
|
43
|
+
const segmentSeconds = dictationSegmentSeconds(input.maxDurationSeconds ?? null);
|
|
44
|
+
const update = useCallback((next) => {
|
|
45
|
+
statusRef.current = next;
|
|
46
|
+
setStatus(next);
|
|
47
|
+
}, []);
|
|
48
|
+
const discardAll = useCallback(() => {
|
|
49
|
+
const recorder = latest.current.recorder;
|
|
50
|
+
for (const segment of segments.current) recorder?.discard?.(segment.recording);
|
|
51
|
+
segments.current = [];
|
|
52
|
+
inFlight.current.clear();
|
|
53
|
+
capturedBefore.current = 0;
|
|
54
|
+
setHasSavedRecording(false);
|
|
55
|
+
}, []);
|
|
56
|
+
const transcribeSegment = useCallback((segment) => {
|
|
57
|
+
if (segment.text !== null) return Promise.resolve();
|
|
58
|
+
const running = inFlight.current.get(segment);
|
|
59
|
+
if (running) return running;
|
|
60
|
+
const work = (async () => {
|
|
61
|
+
const delays = latest.current.retryDelaysMs ?? TRANSCRIPTION_RETRY_DELAYS_MS;
|
|
62
|
+
const limit = latest.current.maxDurationSeconds ?? null;
|
|
63
|
+
const recording = limit !== null && segment.recording.durationSeconds > limit ? { ...segment.recording, durationSeconds: limit } : segment.recording;
|
|
64
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
65
|
+
try {
|
|
66
|
+
segment.text = await latest.current.transcribe(recording);
|
|
67
|
+
return;
|
|
68
|
+
} catch (cause) {
|
|
69
|
+
if (attempt >= delays.length || !isRetryableTranscriptionError(cause)) throw cause;
|
|
70
|
+
await wait(delays[attempt]);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
})().finally(() => inFlight.current.delete(segment));
|
|
74
|
+
inFlight.current.set(segment, work);
|
|
75
|
+
return work;
|
|
76
|
+
}, []);
|
|
77
|
+
const complete = useCallback(
|
|
78
|
+
async (ticket) => {
|
|
79
|
+
update("transcribing");
|
|
80
|
+
setError(null);
|
|
81
|
+
try {
|
|
82
|
+
for (const segment of [...segments.current]) {
|
|
83
|
+
await transcribeSegment(segment);
|
|
84
|
+
if (ticket !== generation.current) return;
|
|
85
|
+
}
|
|
86
|
+
const text = segments.current.map((segment) => segment.text?.trim() ?? "").filter(Boolean).join(" ");
|
|
87
|
+
latest.current.setValue(appendDictation(latest.current.getValue(), text));
|
|
88
|
+
discardAll();
|
|
89
|
+
update("idle");
|
|
90
|
+
} catch (cause) {
|
|
91
|
+
if (ticket !== generation.current) return;
|
|
92
|
+
setHasSavedRecording(true);
|
|
93
|
+
setError(
|
|
94
|
+
isRetryableTranscriptionError(cause) ? "Couldn't reach transcription. Your recording is saved. Retry when you're back online." : `Couldn't transcribe this recording${cause instanceof Error && cause.message ? ` (${cause.message})` : ""}. It's saved, so you can retry.`
|
|
95
|
+
);
|
|
96
|
+
update("error");
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
[discardAll, transcribeSegment, update]
|
|
100
|
+
);
|
|
101
|
+
const finish = useCallback(async () => {
|
|
102
|
+
const recorder = latest.current.recorder;
|
|
103
|
+
if (!recorder || statusRef.current !== "recording") return;
|
|
104
|
+
const ticket = generation.current;
|
|
105
|
+
latest.current.onFeedback?.("stop");
|
|
106
|
+
update("transcribing");
|
|
107
|
+
setLevel(null);
|
|
108
|
+
while (rolling.current) await wait(20);
|
|
109
|
+
try {
|
|
110
|
+
const recording = await recorder.stop();
|
|
111
|
+
if (ticket !== generation.current) return;
|
|
112
|
+
if (recording && recording.durationSeconds >= 0.3) {
|
|
113
|
+
segments.current.push({ recording, text: null });
|
|
114
|
+
} else if (recording) {
|
|
115
|
+
recorder.discard?.(recording);
|
|
116
|
+
}
|
|
117
|
+
} catch {
|
|
118
|
+
}
|
|
119
|
+
if (ticket !== generation.current) return;
|
|
120
|
+
if (segments.current.length === 0) {
|
|
121
|
+
capturedBefore.current = 0;
|
|
122
|
+
update("idle");
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
await complete(ticket);
|
|
126
|
+
}, [complete, update]);
|
|
127
|
+
const rollover = useCallback(async () => {
|
|
128
|
+
const recorder = latest.current.recorder;
|
|
129
|
+
if (!recorder || rolling.current || statusRef.current !== "recording") return;
|
|
130
|
+
const ticket = generation.current;
|
|
131
|
+
rolling.current = true;
|
|
132
|
+
try {
|
|
133
|
+
const recording = await recorder.stop();
|
|
134
|
+
if (recording) {
|
|
135
|
+
capturedBefore.current += recording.durationSeconds;
|
|
136
|
+
const segment = { recording, text: null };
|
|
137
|
+
segments.current.push(segment);
|
|
138
|
+
void transcribeSegment(segment).catch(() => void 0);
|
|
139
|
+
}
|
|
140
|
+
if (ticket !== generation.current || statusRef.current !== "recording") return;
|
|
141
|
+
await recorder.start();
|
|
142
|
+
} catch {
|
|
143
|
+
if (ticket !== generation.current) return;
|
|
144
|
+
rolling.current = false;
|
|
145
|
+
if (segments.current.length > 0) await complete(ticket);
|
|
146
|
+
else update("idle");
|
|
147
|
+
return;
|
|
148
|
+
} finally {
|
|
149
|
+
rolling.current = false;
|
|
150
|
+
}
|
|
151
|
+
}, [complete, transcribeSegment, update]);
|
|
152
|
+
const start = useCallback(() => {
|
|
153
|
+
const recorder = latest.current.recorder;
|
|
154
|
+
if (!recorder || !latest.current.available) return;
|
|
155
|
+
if (statusRef.current === "recording" || statusRef.current === "transcribing") return;
|
|
156
|
+
if (segments.current.length > 0) {
|
|
157
|
+
void complete(++generation.current);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const ticket = ++generation.current;
|
|
161
|
+
setError(null);
|
|
162
|
+
setDurationSeconds(0);
|
|
163
|
+
capturedBefore.current = 0;
|
|
164
|
+
update("requesting-permission");
|
|
165
|
+
void (async () => {
|
|
166
|
+
try {
|
|
167
|
+
const granted = await recorder.requestPermission();
|
|
168
|
+
if (ticket !== generation.current) return;
|
|
169
|
+
if (!granted) {
|
|
170
|
+
setError("Allow microphone access in Settings to dictate.");
|
|
171
|
+
update("error");
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
await recorder.start();
|
|
175
|
+
if (ticket !== generation.current) {
|
|
176
|
+
await recorder.cancel();
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
latest.current.onFeedback?.("start");
|
|
180
|
+
update("recording");
|
|
181
|
+
} catch {
|
|
182
|
+
if (ticket !== generation.current) return;
|
|
183
|
+
setError("Couldn't start the microphone. Try again.");
|
|
184
|
+
update("error");
|
|
185
|
+
}
|
|
186
|
+
})();
|
|
187
|
+
}, [complete, update]);
|
|
188
|
+
const cancel = useCallback(() => {
|
|
189
|
+
generation.current += 1;
|
|
190
|
+
const recorder = latest.current.recorder;
|
|
191
|
+
const wasRecording = statusRef.current === "recording";
|
|
192
|
+
update("idle");
|
|
193
|
+
setError(null);
|
|
194
|
+
setLevel(null);
|
|
195
|
+
setDurationSeconds(0);
|
|
196
|
+
if (wasRecording) latest.current.onFeedback?.("cancel");
|
|
197
|
+
if (recorder && wasRecording) void recorder.cancel().catch(() => void 0);
|
|
198
|
+
discardAll();
|
|
199
|
+
}, [discardAll, update]);
|
|
200
|
+
const retry = useCallback(() => {
|
|
201
|
+
if (segments.current.length === 0 || statusRef.current === "transcribing") return;
|
|
202
|
+
const ticket = ++generation.current;
|
|
203
|
+
void complete(ticket);
|
|
204
|
+
}, [complete]);
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
if (status !== "recording") return;
|
|
207
|
+
const timer = setInterval(() => {
|
|
208
|
+
const recorder = latest.current.recorder;
|
|
209
|
+
if (!recorder || rolling.current) return;
|
|
210
|
+
const seconds = recorder.durationSeconds();
|
|
211
|
+
setDurationSeconds(capturedBefore.current + seconds);
|
|
212
|
+
setLevel(recorder.level());
|
|
213
|
+
if (segmentSeconds !== null && seconds >= segmentSeconds) void rollover();
|
|
214
|
+
}, 100);
|
|
215
|
+
return () => clearInterval(timer);
|
|
216
|
+
}, [rollover, segmentSeconds, status]);
|
|
217
|
+
const scope = input.scope;
|
|
218
|
+
useEffect(() => () => cancel(), [cancel, scope]);
|
|
219
|
+
return {
|
|
220
|
+
available: Boolean(input.recorder) && input.available,
|
|
221
|
+
status,
|
|
222
|
+
error,
|
|
223
|
+
durationSeconds,
|
|
224
|
+
level,
|
|
225
|
+
maxDurationSeconds: null,
|
|
226
|
+
hasSavedRecording,
|
|
227
|
+
start,
|
|
228
|
+
stop: () => void finish(),
|
|
229
|
+
cancel,
|
|
230
|
+
retry,
|
|
231
|
+
clearError: () => {
|
|
232
|
+
setError(null);
|
|
233
|
+
if (statusRef.current === "error" && segments.current.length === 0) update("idle");
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export { NativeTranscriptionError, appendDictation, isRetryableTranscriptionError, useNativeVoiceInput };
|
|
239
|
+
//# sourceMappingURL=chunk-EEDIMY3L.js.map
|
|
240
|
+
//# sourceMappingURL=chunk-EEDIMY3L.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/voice-input.ts"],"names":[],"mappings":";;;AAoEO,SAAS,eAAA,CAAgB,OAAe,UAAA,EAA4B;AACzE,EAAA,MAAM,KAAA,GAAQ,WAAW,IAAA,EAAK;AAC9B,EAAA,IAAI,CAAC,OAAO,OAAO,KAAA;AACnB,EAAA,IAAI,CAAC,OAAO,OAAO,KAAA;AACnB,EAAA,OAAO,MAAA,CAAO,IAAA,CAAK,KAAK,CAAA,GAAI,CAAA,EAAG,KAAK,CAAA,EAAG,KAAK,CAAA,CAAA,GAAK,CAAA,EAAG,KAAK,CAAA,CAAA,EAAI,KAAK,CAAA,CAAA;AACpE;AAGO,IAAM,wBAAA,GAAN,cAAuC,KAAA,CAAM;AAAA,EACzC,MAAA;AAAA,EACT,WAAA,CAAY,SAAiB,MAAA,EAAuB;AAClD,IAAA,KAAA,CAAM,OAAO,CAAA;AACb,IAAA,IAAA,CAAK,IAAA,GAAO,0BAAA;AACZ,IAAA,IAAA,CAAK,MAAA,GAAS,MAAA;AAAA,EAChB;AACF;AAOO,SAAS,8BAA8B,KAAA,EAAyB;AACrE,EAAA,MAAM,MAAA,GAAS,KAAA,YAAiB,wBAAA,GAA2B,KAAA,CAAM,MAAA,GAAS,IAAA;AAC1E,EAAA,IAAI,MAAA,KAAW,MAAM,OAAO,IAAA;AAC5B,EAAA,OAAO,MAAA,KAAW,OAAO,MAAA,KAAW,GAAA,IAAO,WAAW,GAAA,IAAO,MAAA,KAAW,OAAO,MAAA,IAAU,GAAA;AAC3F;AAGO,IAAM,6BAAA,GAAgC,CAAC,GAAA,EAAO,GAAA,EAAO,GAAK,CAAA;AAM1D,SAAS,wBAAwB,kBAAA,EAAkD;AACxF,EAAA,IAAI,kBAAA,KAAuB,IAAA,IAAQ,EAAE,kBAAA,GAAqB,IAAI,OAAO,IAAA;AACrE,EAAA,OAAO,IAAA,CAAK,IAAI,CAAA,EAAG,kBAAA,GAAqB,KAAK,GAAA,CAAI,CAAA,EAAG,kBAAA,GAAqB,GAAG,CAAC,CAAA;AAC/E;AAIA,IAAM,IAAA,GAAO,CAAC,EAAA,KAAe,IAAI,OAAA,CAAc,CAAC,OAAA,KAAY,UAAA,CAAW,OAAA,EAAS,EAAE,CAAC,CAAA;AAY5E,SAAS,oBAAoB,KAAA,EAef;AACnB,EAAA,MAAM,CAAC,MAAA,EAAQ,SAAS,CAAA,GAAI,SAAiC,MAAM,CAAA;AACnE,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,CAAA,GAAI,SAAwB,IAAI,CAAA;AACtD,EAAA,MAAM,CAAC,eAAA,EAAiB,kBAAkB,CAAA,GAAI,SAAS,CAAC,CAAA;AACxD,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,CAAA,GAAI,SAAwB,IAAI,CAAA;AACtD,EAAA,MAAM,CAAC,iBAAA,EAAmB,oBAAoB,CAAA,GAAI,SAAS,KAAK,CAAA;AAChE,EAAA,MAAM,UAAA,GAAa,OAAO,CAAC,CAAA;AAC3B,EAAA,MAAM,SAAA,GAAY,OAA+B,MAAM,CAAA;AACvD,EAAA,MAAM,QAAA,GAAW,MAAA,CAAkB,EAAE,CAAA;AACrC,EAAA,MAAM,QAAA,GAAW,MAAA,iBAAO,IAAI,GAAA,EAA6B,CAAA;AACzD,EAAA,MAAM,cAAA,GAAiB,OAAO,CAAC,CAAA;AAC/B,EAAA,MAAM,OAAA,GAAU,OAAO,KAAK,CAAA;AAC5B,EAAA,MAAM,MAAA,GAAS,OAAO,KAAK,CAAA;AAC3B,EAAA,MAAA,CAAO,OAAA,GAAU,KAAA;AACjB,EAAA,MAAM,cAAA,GAAiB,uBAAA,CAAwB,KAAA,CAAM,kBAAA,IAAsB,IAAI,CAAA;AAE/E,EAAA,MAAM,MAAA,GAAS,WAAA,CAAY,CAAC,IAAA,KAAiC;AAC3D,IAAA,SAAA,CAAU,OAAA,GAAU,IAAA;AACpB,IAAA,SAAA,CAAU,IAAI,CAAA;AAAA,EAChB,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,MAAM,UAAA,GAAa,YAAY,MAAM;AACnC,IAAA,MAAM,QAAA,GAAW,OAAO,OAAA,CAAQ,QAAA;AAChC,IAAA,KAAA,MAAW,WAAW,QAAA,CAAS,OAAA,EAAS,QAAA,EAAU,OAAA,GAAU,QAAQ,SAAS,CAAA;AAC7E,IAAA,QAAA,CAAS,UAAU,EAAC;AACpB,IAAA,QAAA,CAAS,QAAQ,KAAA,EAAM;AACvB,IAAA,cAAA,CAAe,OAAA,GAAU,CAAA;AACzB,IAAA,oBAAA,CAAqB,KAAK,CAAA;AAAA,EAC5B,CAAA,EAAG,EAAE,CAAA;AAGL,EAAA,MAAM,iBAAA,GAAoB,WAAA,CAAY,CAAC,OAAA,KAAoC;AACzE,IAAA,IAAI,OAAA,CAAQ,IAAA,KAAS,IAAA,EAAM,OAAO,QAAQ,OAAA,EAAQ;AAClD,IAAA,MAAM,OAAA,GAAU,QAAA,CAAS,OAAA,CAAQ,GAAA,CAAI,OAAO,CAAA;AAC5C,IAAA,IAAI,SAAS,OAAO,OAAA;AACpB,IAAA,MAAM,QAAQ,YAAY;AACxB,MAAA,MAAM,MAAA,GAAS,MAAA,CAAO,OAAA,CAAQ,aAAA,IAAiB,6BAAA;AAC/C,MAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,OAAA,CAAQ,kBAAA,IAAsB,IAAA;AAGnD,MAAA,MAAM,SAAA,GACJ,KAAA,KAAU,IAAA,IAAQ,OAAA,CAAQ,UAAU,eAAA,GAAkB,KAAA,GAClD,EAAE,GAAG,OAAA,CAAQ,SAAA,EAAW,eAAA,EAAiB,KAAA,KACzC,OAAA,CAAQ,SAAA;AACd,MAAA,KAAA,IAAS,OAAA,GAAU,CAAA,IAAK,OAAA,IAAW,CAAA,EAAG;AACpC,QAAA,IAAI;AACF,UAAA,OAAA,CAAQ,IAAA,GAAO,MAAM,MAAA,CAAO,OAAA,CAAQ,WAAW,SAAS,CAAA;AACxD,UAAA;AAAA,QACF,SAAS,KAAA,EAAO;AACd,UAAA,IAAI,WAAW,MAAA,CAAO,MAAA,IAAU,CAAC,6BAAA,CAA8B,KAAK,GAAG,MAAM,KAAA;AAC7E,UAAA,MAAM,IAAA,CAAK,MAAA,CAAO,OAAO,CAAE,CAAA;AAAA,QAC7B;AAAA,MACF;AAAA,IACF,CAAA,IAAK,OAAA,CAAQ,MAAM,SAAS,OAAA,CAAQ,MAAA,CAAO,OAAO,CAAC,CAAA;AACnD,IAAA,QAAA,CAAS,OAAA,CAAQ,GAAA,CAAI,OAAA,EAAS,IAAI,CAAA;AAClC,IAAA,OAAO,IAAA;AAAA,EACT,CAAA,EAAG,EAAE,CAAA;AAGL,EAAA,MAAM,QAAA,GAAW,WAAA;AAAA,IACf,OAAO,MAAA,KAAmB;AACxB,MAAA,MAAA,CAAO,cAAc,CAAA;AACrB,MAAA,QAAA,CAAS,IAAI,CAAA;AACb,MAAA,IAAI;AACF,QAAA,KAAA,MAAW,OAAA,IAAW,CAAC,GAAG,QAAA,CAAS,OAAO,CAAA,EAAG;AAC3C,UAAA,MAAM,kBAAkB,OAAO,CAAA;AAC/B,UAAA,IAAI,MAAA,KAAW,WAAW,OAAA,EAAS;AAAA,QACrC;AACA,QAAA,MAAM,OAAO,QAAA,CAAS,OAAA,CACnB,GAAA,CAAI,CAAC,YAAY,OAAA,CAAQ,IAAA,EAAM,IAAA,EAAK,IAAK,EAAE,CAAA,CAC3C,MAAA,CAAO,OAAO,CAAA,CACd,KAAK,GAAG,CAAA;AACX,QAAA,MAAA,CAAO,OAAA,CAAQ,SAAS,eAAA,CAAgB,MAAA,CAAO,QAAQ,QAAA,EAAS,EAAG,IAAI,CAAC,CAAA;AACxE,QAAA,UAAA,EAAW;AACX,QAAA,MAAA,CAAO,MAAM,CAAA;AAAA,MACf,SAAS,KAAA,EAAO;AACd,QAAA,IAAI,MAAA,KAAW,WAAW,OAAA,EAAS;AAEnC,QAAA,oBAAA,CAAqB,IAAI,CAAA;AACzB,QAAA,QAAA;AAAA,UACE,6BAAA,CAA8B,KAAK,CAAA,GAC/B,uFAAA,GACA,CAAA,kCAAA,EACE,KAAA,YAAiB,KAAA,IAAS,KAAA,CAAM,OAAA,GAAU,CAAA,EAAA,EAAK,KAAA,CAAM,OAAO,MAAM,EACpE,CAAA,+BAAA;AAAA,SACN;AACA,QAAA,MAAA,CAAO,OAAO,CAAA;AAAA,MAChB;AAAA,IACF,CAAA;AAAA,IACA,CAAC,UAAA,EAAY,iBAAA,EAAmB,MAAM;AAAA,GACxC;AAEA,EAAA,MAAM,MAAA,GAAS,YAAY,YAAY;AACrC,IAAA,MAAM,QAAA,GAAW,OAAO,OAAA,CAAQ,QAAA;AAChC,IAAA,IAAI,CAAC,QAAA,IAAY,SAAA,CAAU,OAAA,KAAY,WAAA,EAAa;AACpD,IAAA,MAAM,SAAS,UAAA,CAAW,OAAA;AAC1B,IAAA,MAAA,CAAO,OAAA,CAAQ,aAAa,MAAM,CAAA;AAClC,IAAA,MAAA,CAAO,cAAc,CAAA;AACrB,IAAA,QAAA,CAAS,IAAI,CAAA;AAEb,IAAA,OAAO,OAAA,CAAQ,OAAA,EAAS,MAAM,IAAA,CAAK,EAAE,CAAA;AACrC,IAAA,IAAI;AACF,MAAA,MAAM,SAAA,GAAY,MAAM,QAAA,CAAS,IAAA,EAAK;AACtC,MAAA,IAAI,MAAA,KAAW,WAAW,OAAA,EAAS;AACnC,MAAA,IAAI,SAAA,IAAa,SAAA,CAAU,eAAA,IAAmB,GAAA,EAAK;AACjD,QAAA,QAAA,CAAS,QAAQ,IAAA,CAAK,EAAE,SAAA,EAAW,IAAA,EAAM,MAAM,CAAA;AAAA,MACjD,WAAW,SAAA,EAAW;AACpB,QAAA,QAAA,CAAS,UAAU,SAAS,CAAA;AAAA,MAC9B;AAAA,IACF,CAAA,CAAA,MAAQ;AAAA,IAER;AACA,IAAA,IAAI,MAAA,KAAW,WAAW,OAAA,EAAS;AACnC,IAAA,IAAI,QAAA,CAAS,OAAA,CAAQ,MAAA,KAAW,CAAA,EAAG;AACjC,MAAA,cAAA,CAAe,OAAA,GAAU,CAAA;AACzB,MAAA,MAAA,CAAO,MAAM,CAAA;AACb,MAAA;AAAA,IACF;AACA,IAAA,MAAM,SAAS,MAAM,CAAA;AAAA,EACvB,CAAA,EAAG,CAAC,QAAA,EAAU,MAAM,CAAC,CAAA;AAGrB,EAAA,MAAM,QAAA,GAAW,YAAY,YAAY;AACvC,IAAA,MAAM,QAAA,GAAW,OAAO,OAAA,CAAQ,QAAA;AAChC,IAAA,IAAI,CAAC,QAAA,IAAY,OAAA,CAAQ,OAAA,IAAW,SAAA,CAAU,YAAY,WAAA,EAAa;AACvE,IAAA,MAAM,SAAS,UAAA,CAAW,OAAA;AAC1B,IAAA,OAAA,CAAQ,OAAA,GAAU,IAAA;AAClB,IAAA,IAAI;AACF,MAAA,MAAM,SAAA,GAAY,MAAM,QAAA,CAAS,IAAA,EAAK;AACtC,MAAA,IAAI,SAAA,EAAW;AACb,QAAA,cAAA,CAAe,WAAW,SAAA,CAAU,eAAA;AACpC,QAAA,MAAM,OAAA,GAAmB,EAAE,SAAA,EAAW,IAAA,EAAM,IAAA,EAAK;AACjD,QAAA,QAAA,CAAS,OAAA,CAAQ,KAAK,OAAO,CAAA;AAE7B,QAAA,KAAK,iBAAA,CAAkB,OAAO,CAAA,CAAE,KAAA,CAAM,MAAM,KAAA,CAAS,CAAA;AAAA,MACvD;AACA,MAAA,IAAI,MAAA,KAAW,UAAA,CAAW,OAAA,IAAW,SAAA,CAAU,YAAY,WAAA,EAAa;AACxE,MAAA,MAAM,SAAS,KAAA,EAAM;AAAA,IACvB,CAAA,CAAA,MAAQ;AACN,MAAA,IAAI,MAAA,KAAW,WAAW,OAAA,EAAS;AAEnC,MAAA,OAAA,CAAQ,OAAA,GAAU,KAAA;AAClB,MAAA,IAAI,SAAS,OAAA,CAAQ,MAAA,GAAS,CAAA,EAAG,MAAM,SAAS,MAAM,CAAA;AAAA,kBAC1C,MAAM,CAAA;AAClB,MAAA;AAAA,IACF,CAAA,SAAE;AACA,MAAA,OAAA,CAAQ,OAAA,GAAU,KAAA;AAAA,IACpB;AAAA,EACF,CAAA,EAAG,CAAC,QAAA,EAAU,iBAAA,EAAmB,MAAM,CAAC,CAAA;AAExC,EAAA,MAAM,KAAA,GAAQ,YAAY,MAAM;AAC9B,IAAA,MAAM,QAAA,GAAW,OAAO,OAAA,CAAQ,QAAA;AAChC,IAAA,IAAI,CAAC,QAAA,IAAY,CAAC,MAAA,CAAO,QAAQ,SAAA,EAAW;AAC5C,IAAA,IAAI,SAAA,CAAU,OAAA,KAAY,WAAA,IAAe,SAAA,CAAU,YAAY,cAAA,EAAgB;AAE/E,IAAA,IAAI,QAAA,CAAS,OAAA,CAAQ,MAAA,GAAS,CAAA,EAAG;AAC/B,MAAA,KAAK,QAAA,CAAS,EAAE,UAAA,CAAW,OAAO,CAAA;AAClC,MAAA;AAAA,IACF;AACA,IAAA,MAAM,MAAA,GAAS,EAAE,UAAA,CAAW,OAAA;AAC5B,IAAA,QAAA,CAAS,IAAI,CAAA;AACb,IAAA,kBAAA,CAAmB,CAAC,CAAA;AACpB,IAAA,cAAA,CAAe,OAAA,GAAU,CAAA;AACzB,IAAA,MAAA,CAAO,uBAAuB,CAAA;AAC9B,IAAA,KAAA,CAAM,YAAY;AAChB,MAAA,IAAI;AACF,QAAA,MAAM,OAAA,GAAU,MAAM,QAAA,CAAS,iBAAA,EAAkB;AACjD,QAAA,IAAI,MAAA,KAAW,WAAW,OAAA,EAAS;AACnC,QAAA,IAAI,CAAC,OAAA,EAAS;AACZ,UAAA,QAAA,CAAS,iDAAiD,CAAA;AAC1D,UAAA,MAAA,CAAO,OAAO,CAAA;AACd,UAAA;AAAA,QACF;AACA,QAAA,MAAM,SAAS,KAAA,EAAM;AACrB,QAAA,IAAI,MAAA,KAAW,WAAW,OAAA,EAAS;AACjC,UAAA,MAAM,SAAS,MAAA,EAAO;AACtB,UAAA;AAAA,QACF;AACA,QAAA,MAAA,CAAO,OAAA,CAAQ,aAAa,OAAO,CAAA;AACnC,QAAA,MAAA,CAAO,WAAW,CAAA;AAAA,MACpB,CAAA,CAAA,MAAQ;AACN,QAAA,IAAI,MAAA,KAAW,WAAW,OAAA,EAAS;AAEnC,QAAA,QAAA,CAAS,2CAA2C,CAAA;AACpD,QAAA,MAAA,CAAO,OAAO,CAAA;AAAA,MAChB;AAAA,IACF,CAAA,GAAG;AAAA,EACL,CAAA,EAAG,CAAC,QAAA,EAAU,MAAM,CAAC,CAAA;AAErB,EAAA,MAAM,MAAA,GAAS,YAAY,MAAM;AAC/B,IAAA,UAAA,CAAW,OAAA,IAAW,CAAA;AACtB,IAAA,MAAM,QAAA,GAAW,OAAO,OAAA,CAAQ,QAAA;AAChC,IAAA,MAAM,YAAA,GAAe,UAAU,OAAA,KAAY,WAAA;AAC3C,IAAA,MAAA,CAAO,MAAM,CAAA;AACb,IAAA,QAAA,CAAS,IAAI,CAAA;AACb,IAAA,QAAA,CAAS,IAAI,CAAA;AACb,IAAA,kBAAA,CAAmB,CAAC,CAAA;AACpB,IAAA,IAAI,YAAA,EAAc,MAAA,CAAO,OAAA,CAAQ,UAAA,GAAa,QAAQ,CAAA;AACtD,IAAA,IAAI,QAAA,IAAY,cAAc,KAAK,QAAA,CAAS,QAAO,CAAE,KAAA,CAAM,MAAM,MAAS,CAAA;AAC1E,IAAA,UAAA,EAAW;AAAA,EACb,CAAA,EAAG,CAAC,UAAA,EAAY,MAAM,CAAC,CAAA;AAEvB,EAAA,MAAM,KAAA,GAAQ,YAAY,MAAM;AAC9B,IAAA,IAAI,SAAS,OAAA,CAAQ,MAAA,KAAW,CAAA,IAAK,SAAA,CAAU,YAAY,cAAA,EAAgB;AAC3E,IAAA,MAAM,MAAA,GAAS,EAAE,UAAA,CAAW,OAAA;AAC5B,IAAA,KAAK,SAAS,MAAM,CAAA;AAAA,EACtB,CAAA,EAAG,CAAC,QAAQ,CAAC,CAAA;AAIb,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,WAAW,WAAA,EAAa;AAC5B,IAAA,MAAM,KAAA,GAAQ,YAAY,MAAM;AAC9B,MAAA,MAAM,QAAA,GAAW,OAAO,OAAA,CAAQ,QAAA;AAChC,MAAA,IAAI,CAAC,QAAA,IAAY,OAAA,CAAQ,OAAA,EAAS;AAClC,MAAA,MAAM,OAAA,GAAU,SAAS,eAAA,EAAgB;AACzC,MAAA,kBAAA,CAAmB,cAAA,CAAe,UAAU,OAAO,CAAA;AACnD,MAAA,QAAA,CAAS,QAAA,CAAS,OAAO,CAAA;AACzB,MAAA,IAAI,cAAA,KAAmB,IAAA,IAAQ,OAAA,IAAW,cAAA,OAAqB,QAAA,EAAS;AAAA,IAC1E,GAAG,GAAG,CAAA;AACN,IAAA,OAAO,MAAM,cAAc,KAAK,CAAA;AAAA,EAClC,CAAA,EAAG,CAAC,QAAA,EAAU,cAAA,EAAgB,MAAM,CAAC,CAAA;AAGrC,EAAA,MAAM,QAAQ,KAAA,CAAM,KAAA;AACpB,EAAA,SAAA,CAAU,MAAM,MAAM,MAAA,IAAU,CAAC,MAAA,EAAQ,KAAK,CAAC,CAAA;AAE/C,EAAA,OAAO;AAAA,IACL,SAAA,EAAW,OAAA,CAAQ,KAAA,CAAM,QAAQ,KAAK,KAAA,CAAM,SAAA;AAAA,IAC5C,MAAA;AAAA,IACA,KAAA;AAAA,IACA,eAAA;AAAA,IACA,KAAA;AAAA,IACA,kBAAA,EAAoB,IAAA;AAAA,IACpB,iBAAA;AAAA,IACA,KAAA;AAAA,IACA,IAAA,EAAM,MAAM,KAAK,MAAA,EAAO;AAAA,IACxB,MAAA;AAAA,IACA,KAAA;AAAA,IACA,YAAY,MAAM;AAChB,MAAA,QAAA,CAAS,IAAI,CAAA;AACb,MAAA,IAAI,SAAA,CAAU,YAAY,OAAA,IAAW,QAAA,CAAS,QAAQ,MAAA,KAAW,CAAA,SAAU,MAAM,CAAA;AAAA,IACnF;AAAA,GACF;AACF","file":"chunk-EEDIMY3L.js","sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\n\n/** A finished recording on the device. */\nexport interface NativeVoiceRecording {\n uri: string;\n mimeType: string;\n durationSeconds: number;\n}\n\n/**\n * The platform recorder behind dictation. The Expo implementation lives in\n * `@opengeni/react-native/expo` (`useExpoVoiceRecorder`).\n */\nexport interface NativeVoiceRecorder {\n /** Ask for microphone access; resolves false when the person declines. */\n requestPermission(): Promise<boolean>;\n start(): Promise<void>;\n /**\n * Stop and keep the recording; null when nothing was captured. The file must\n * survive a following `start()`: dictation keeps it until its text is in the\n * draft, then calls `discard`.\n */\n stop(): Promise<NativeVoiceRecording | null>;\n /** Stop and throw the current recording away. */\n cancel(): Promise<void>;\n /** Delete a kept recording file once it is no longer needed. */\n discard?(recording: NativeVoiceRecording): void;\n /** Seconds captured so far while recording. */\n durationSeconds(): number;\n /** The current input level from 0 to 1 while recording, when the platform meters it. */\n level(): number | null;\n}\n\nexport type NativeVoiceInputStatus =\n | \"idle\"\n | \"requesting-permission\"\n | \"recording\"\n | \"transcribing\"\n | \"error\";\n\nexport interface NativeVoiceInput {\n /** Dictation is offered: a recorder exists and the deployment can transcribe. */\n available: boolean;\n status: NativeVoiceInputStatus;\n error: string | null;\n /** Total seconds captured in this dictation, across segments. */\n durationSeconds: number;\n /** Input level from 0 to 1 while recording (null when not metered). */\n level: number | null;\n /**\n * Always null: long dictation rolls over into segments under the server's\n * per-request limit, so there is no recording cap to count down to.\n */\n maxDurationSeconds: number | null;\n /** A finished recording is kept on the device after a failed transcription. */\n hasSavedRecording: boolean;\n start(): void;\n /** Stop, transcribe and append the text to the draft. */\n stop(): void;\n /** Throw the current dictation away (recording or saved). */\n cancel(): void;\n /** Transcribe a saved recording again. */\n retry(): void;\n /** Hide the error message; a saved recording stays available to retry. */\n clearError(): void;\n}\n\n/** Append dictated text to the draft, as the web composer does. */\nexport function appendDictation(draft: string, transcript: string): string {\n const final = transcript.trim();\n if (!final) return draft;\n if (!draft) return final;\n return /\\s$/u.test(draft) ? `${draft}${final}` : `${draft} ${final}`;\n}\n\n/** An error from the transcription request, with the HTTP status when there was one. */\nexport class NativeTranscriptionError extends Error {\n readonly status: number | null;\n constructor(message: string, status: number | null) {\n super(message);\n this.name = \"NativeTranscriptionError\";\n this.status = status;\n }\n}\n\n/**\n * Whether trying the same audio again can succeed: network failures, timeouts,\n * rate limits and server errors. Rejections of the audio itself (bad request,\n * too large, unsupported, forbidden) do not change on retry.\n */\nexport function isRetryableTranscriptionError(cause: unknown): boolean {\n const status = cause instanceof NativeTranscriptionError ? cause.status : null;\n if (status === null) return true;\n return status === 408 || status === 425 || status === 429 || status === 499 || status >= 500;\n}\n\n/** Delays between automatic attempts; the recording is kept whatever happens. */\nexport const TRANSCRIPTION_RETRY_DELAYS_MS = [1_000, 3_000, 8_000] as const;\n\n/**\n * Where a long recording rolls over into a new segment: a little under the\n * server's per-request limit, so a segment is never rejected as too long.\n */\nexport function dictationSegmentSeconds(maxDurationSeconds: number | null): number | null {\n if (maxDurationSeconds === null || !(maxDurationSeconds > 0)) return null;\n return Math.max(1, maxDurationSeconds - Math.min(3, maxDurationSeconds * 0.1));\n}\n\ntype Segment = { recording: NativeVoiceRecording; text: string | null };\n\nconst wait = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));\n\n/**\n * Dictation for a native composer: record on the device, send the audio to the\n * deployment's transcription API, and append the text to the draft. The draft\n * is read at completion, so typing while transcribing is preserved.\n *\n * Nothing said is lost: a long recording rolls over into segments under the\n * per-request limit (transcribed in the background while recording goes on),\n * failed requests are retried, and a recording whose text could not be fetched\n * stays on the device until it is transcribed or the person discards it.\n */\nexport function useNativeVoiceInput(input: {\n recorder: NativeVoiceRecorder | null;\n /** Whether the deployment offers transcription in this workspace. */\n available: boolean;\n /** The server's per-request duration limit; long dictation is segmented under it. */\n maxDurationSeconds?: number | null | undefined;\n transcribe: (recording: NativeVoiceRecording) => Promise<string>;\n getValue: () => string;\n setValue: (value: string) => void;\n /** Host feedback (haptics) when recording starts and stops. */\n onFeedback?: ((event: \"start\" | \"stop\" | \"cancel\") => void) | undefined;\n /** Scope key; changing it cancels a running capture (another session or workspace). */\n scope?: string | undefined;\n /** Test seam for the retry delays. */\n retryDelaysMs?: readonly number[] | undefined;\n}): NativeVoiceInput {\n const [status, setStatus] = useState<NativeVoiceInputStatus>(\"idle\");\n const [error, setError] = useState<string | null>(null);\n const [durationSeconds, setDurationSeconds] = useState(0);\n const [level, setLevel] = useState<number | null>(null);\n const [hasSavedRecording, setHasSavedRecording] = useState(false);\n const generation = useRef(0);\n const statusRef = useRef<NativeVoiceInputStatus>(\"idle\");\n const segments = useRef<Segment[]>([]);\n const inFlight = useRef(new Map<Segment, Promise<void>>());\n const capturedBefore = useRef(0);\n const rolling = useRef(false);\n const latest = useRef(input);\n latest.current = input;\n const segmentSeconds = dictationSegmentSeconds(input.maxDurationSeconds ?? null);\n\n const update = useCallback((next: NativeVoiceInputStatus) => {\n statusRef.current = next;\n setStatus(next);\n }, []);\n\n const discardAll = useCallback(() => {\n const recorder = latest.current.recorder;\n for (const segment of segments.current) recorder?.discard?.(segment.recording);\n segments.current = [];\n inFlight.current.clear();\n capturedBefore.current = 0;\n setHasSavedRecording(false);\n }, []);\n\n /** Transcribe one segment, retrying transient failures; resolves either way. */\n const transcribeSegment = useCallback((segment: Segment): Promise<void> => {\n if (segment.text !== null) return Promise.resolve();\n const running = inFlight.current.get(segment);\n if (running) return running;\n const work = (async () => {\n const delays = latest.current.retryDelaysMs ?? TRANSCRIPTION_RETRY_DELAYS_MS;\n const limit = latest.current.maxDurationSeconds ?? null;\n // The reported length never exceeds the limit: a recording stopped a\n // moment after the cap must not be rejected as too long.\n const recording =\n limit !== null && segment.recording.durationSeconds > limit\n ? { ...segment.recording, durationSeconds: limit }\n : segment.recording;\n for (let attempt = 0; ; attempt += 1) {\n try {\n segment.text = await latest.current.transcribe(recording);\n return;\n } catch (cause) {\n if (attempt >= delays.length || !isRetryableTranscriptionError(cause)) throw cause;\n await wait(delays[attempt]!);\n }\n }\n })().finally(() => inFlight.current.delete(segment));\n inFlight.current.set(segment, work);\n return work;\n }, []);\n\n /** Transcribe every kept segment in order and append the text once all are in. */\n const complete = useCallback(\n async (ticket: number) => {\n update(\"transcribing\");\n setError(null);\n try {\n for (const segment of [...segments.current]) {\n await transcribeSegment(segment);\n if (ticket !== generation.current) return;\n }\n const text = segments.current\n .map((segment) => segment.text?.trim() ?? \"\")\n .filter(Boolean)\n .join(\" \");\n latest.current.setValue(appendDictation(latest.current.getValue(), text));\n discardAll();\n update(\"idle\");\n } catch (cause) {\n if (ticket !== generation.current) return;\n // The recording stays on the device; Retry sends the same audio again.\n setHasSavedRecording(true);\n setError(\n isRetryableTranscriptionError(cause)\n ? \"Couldn't reach transcription. Your recording is saved. Retry when you're back online.\"\n : `Couldn't transcribe this recording${\n cause instanceof Error && cause.message ? ` (${cause.message})` : \"\"\n }. It's saved, so you can retry.`,\n );\n update(\"error\");\n }\n },\n [discardAll, transcribeSegment, update],\n );\n\n const finish = useCallback(async () => {\n const recorder = latest.current.recorder;\n if (!recorder || statusRef.current !== \"recording\") return;\n const ticket = generation.current;\n latest.current.onFeedback?.(\"stop\");\n update(\"transcribing\");\n setLevel(null);\n // A rollover in progress hands over its segment before the final stop.\n while (rolling.current) await wait(20);\n try {\n const recording = await recorder.stop();\n if (ticket !== generation.current) return;\n if (recording && recording.durationSeconds >= 0.3) {\n segments.current.push({ recording, text: null });\n } else if (recording) {\n recorder.discard?.(recording);\n }\n } catch {\n // The platform could not hand over the last segment; earlier ones are kept.\n }\n if (ticket !== generation.current) return;\n if (segments.current.length === 0) {\n capturedBefore.current = 0;\n update(\"idle\");\n return;\n }\n await complete(ticket);\n }, [complete, update]);\n\n /** Close the current segment and keep recording into a new one. */\n const rollover = useCallback(async () => {\n const recorder = latest.current.recorder;\n if (!recorder || rolling.current || statusRef.current !== \"recording\") return;\n const ticket = generation.current;\n rolling.current = true;\n try {\n const recording = await recorder.stop();\n if (recording) {\n capturedBefore.current += recording.durationSeconds;\n const segment: Segment = { recording, text: null };\n segments.current.push(segment);\n // Start transcribing now; failures are retried again at the end.\n void transcribeSegment(segment).catch(() => undefined);\n }\n if (ticket !== generation.current || statusRef.current !== \"recording\") return;\n await recorder.start();\n } catch {\n if (ticket !== generation.current) return;\n // The microphone could not restart: keep what was captured and transcribe it.\n rolling.current = false;\n if (segments.current.length > 0) await complete(ticket);\n else update(\"idle\");\n return;\n } finally {\n rolling.current = false;\n }\n }, [complete, transcribeSegment, update]);\n\n const start = useCallback(() => {\n const recorder = latest.current.recorder;\n if (!recorder || !latest.current.available) return;\n if (statusRef.current === \"recording\" || statusRef.current === \"transcribing\") return;\n // A saved recording is transcribed first; a new dictation never replaces it.\n if (segments.current.length > 0) {\n void complete(++generation.current);\n return;\n }\n const ticket = ++generation.current;\n setError(null);\n setDurationSeconds(0);\n capturedBefore.current = 0;\n update(\"requesting-permission\");\n void (async () => {\n try {\n const granted = await recorder.requestPermission();\n if (ticket !== generation.current) return;\n if (!granted) {\n setError(\"Allow microphone access in Settings to dictate.\");\n update(\"error\");\n return;\n }\n await recorder.start();\n if (ticket !== generation.current) {\n await recorder.cancel();\n return;\n }\n latest.current.onFeedback?.(\"start\");\n update(\"recording\");\n } catch {\n if (ticket !== generation.current) return;\n // The platform's reason (no input device, audio session busy) is not actionable.\n setError(\"Couldn't start the microphone. Try again.\");\n update(\"error\");\n }\n })();\n }, [complete, update]);\n\n const cancel = useCallback(() => {\n generation.current += 1;\n const recorder = latest.current.recorder;\n const wasRecording = statusRef.current === \"recording\";\n update(\"idle\");\n setError(null);\n setLevel(null);\n setDurationSeconds(0);\n if (wasRecording) latest.current.onFeedback?.(\"cancel\");\n if (recorder && wasRecording) void recorder.cancel().catch(() => undefined);\n discardAll();\n }, [discardAll, update]);\n\n const retry = useCallback(() => {\n if (segments.current.length === 0 || statusRef.current === \"transcribing\") return;\n const ticket = ++generation.current;\n void complete(ticket);\n }, [complete]);\n\n // Duration and level tick while recording; near the per-request limit the\n // recording rolls over into a new segment instead of stopping.\n useEffect(() => {\n if (status !== \"recording\") return;\n const timer = setInterval(() => {\n const recorder = latest.current.recorder;\n if (!recorder || rolling.current) return;\n const seconds = recorder.durationSeconds();\n setDurationSeconds(capturedBefore.current + seconds);\n setLevel(recorder.level());\n if (segmentSeconds !== null && seconds >= segmentSeconds) void rollover();\n }, 100);\n return () => clearInterval(timer);\n }, [rollover, segmentSeconds, status]);\n\n // Leaving the scope (another session or workspace) drops a running capture.\n const scope = input.scope;\n useEffect(() => () => cancel(), [cancel, scope]);\n\n return {\n available: Boolean(input.recorder) && input.available,\n status,\n error,\n durationSeconds,\n level,\n maxDurationSeconds: null,\n hasSavedRecording,\n start,\n stop: () => void finish(),\n cancel,\n retry,\n clearError: () => {\n setError(null);\n if (statusRef.current === \"error\" && segments.current.length === 0) update(\"idle\");\n },\n };\n}\n"]}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { useNativeTimelineTheme } from './chunk-PHJBSAR3.js';
|
|
2
|
+
import { resolvePreset, MODE_FRAMES } from 'thinking-orbs/engine';
|
|
3
|
+
import { useMemo, useState, useEffect } from 'react';
|
|
4
|
+
import { AppState, AccessibilityInfo } from 'react-native';
|
|
5
|
+
import Svg, { Line, Circle } from 'react-native-svg';
|
|
6
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
7
|
+
|
|
8
|
+
var FRAME_MS = 33;
|
|
9
|
+
function ink(white, alpha, dark) {
|
|
10
|
+
const level = Math.round((dark ? 1 - Math.min(1, Math.max(0, white)) : white) * 255);
|
|
11
|
+
return `rgba(${level},${level},${level},${alpha ?? 1})`;
|
|
12
|
+
}
|
|
13
|
+
function ThinkingOrb({
|
|
14
|
+
state = "searching",
|
|
15
|
+
size = 64,
|
|
16
|
+
speed = 0.8,
|
|
17
|
+
displaySize
|
|
18
|
+
}) {
|
|
19
|
+
const theme = useNativeTimelineTheme();
|
|
20
|
+
const dark = theme.scheme === "dark";
|
|
21
|
+
const preset = useMemo(() => resolvePreset(state, size), [state, size]);
|
|
22
|
+
const [time, setTime] = useState(0.6);
|
|
23
|
+
const [reduceMotion, setReduceMotion] = useState(false);
|
|
24
|
+
const [active, setActive] = useState(AppState.currentState === "active");
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
void AccessibilityInfo.isReduceMotionEnabled().then(setReduceMotion);
|
|
27
|
+
const motion = AccessibilityInfo.addEventListener("reduceMotionChanged", setReduceMotion);
|
|
28
|
+
const app = AppState.addEventListener("change", (next) => setActive(next === "active"));
|
|
29
|
+
return () => {
|
|
30
|
+
motion.remove();
|
|
31
|
+
app.remove();
|
|
32
|
+
};
|
|
33
|
+
}, []);
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (reduceMotion || !active) return;
|
|
36
|
+
const started = Date.now();
|
|
37
|
+
const timer = setInterval(() => setTime((Date.now() - started) / 1e3), FRAME_MS);
|
|
38
|
+
return () => clearInterval(timer);
|
|
39
|
+
}, [active, reduceMotion]);
|
|
40
|
+
const frame = MODE_FRAMES[preset.mode](
|
|
41
|
+
size,
|
|
42
|
+
reduceMotion ? 0.6 : time * preset.speed * speed,
|
|
43
|
+
preset.opts
|
|
44
|
+
);
|
|
45
|
+
return /* @__PURE__ */ jsxs(
|
|
46
|
+
Svg,
|
|
47
|
+
{
|
|
48
|
+
width: displaySize ?? size,
|
|
49
|
+
height: displaySize ?? size,
|
|
50
|
+
viewBox: `0 0 ${size} ${size}`,
|
|
51
|
+
accessibilityElementsHidden: true,
|
|
52
|
+
importantForAccessibility: "no",
|
|
53
|
+
children: [
|
|
54
|
+
frame.lines.map((line, index) => /* @__PURE__ */ jsx(
|
|
55
|
+
Line,
|
|
56
|
+
{
|
|
57
|
+
x1: line.x1,
|
|
58
|
+
y1: line.y1,
|
|
59
|
+
x2: line.x2,
|
|
60
|
+
y2: line.y2,
|
|
61
|
+
stroke: ink(line.white, line.a, dark),
|
|
62
|
+
strokeWidth: line.w
|
|
63
|
+
},
|
|
64
|
+
`l${index}`
|
|
65
|
+
)),
|
|
66
|
+
frame.dots.map((dot, index) => /* @__PURE__ */ jsx(
|
|
67
|
+
Circle,
|
|
68
|
+
{
|
|
69
|
+
cx: dot.x,
|
|
70
|
+
cy: dot.y,
|
|
71
|
+
r: dot.r,
|
|
72
|
+
fill: ink(dot.white, dot.a, dark)
|
|
73
|
+
},
|
|
74
|
+
`d${index}`
|
|
75
|
+
))
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export { ThinkingOrb };
|
|
82
|
+
//# sourceMappingURL=chunk-M23SDI4R.js.map
|
|
83
|
+
//# sourceMappingURL=chunk-M23SDI4R.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/timeline/thinking-orb.tsx"],"names":[],"mappings":"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","file":"chunk-M23SDI4R.js","sourcesContent":["import { MODE_FRAMES, resolvePreset, type OrbState } from \"thinking-orbs/engine\";\nimport { useEffect, useMemo, useState } from \"react\";\nimport { AccessibilityInfo, AppState } from \"react-native\";\nimport Svg, { Circle, Line } from \"react-native-svg\";\nimport { useNativeTimelineTheme } from \"./theme\";\n\n/** About 30 frames a second: smooth at 64pt without spending a frame budget. */\nconst FRAME_MS = 33;\n\nfunction ink(white: number, alpha: number | undefined, dark: boolean): string {\n // The engine's convention: 0 is the darkest ink on paper, mirrored on dark.\n const level = Math.round((dark ? 1 - Math.min(1, Math.max(0, white)) : white) * 255);\n return `rgba(${level},${level},${level},${alpha ?? 1})`;\n}\n\n/**\n * The web's ThinkingOrb on native: the same `thinking-orbs` geometry (one\n * frame of dots and edges per instant) drawn with SVG. Pauses in the\n * background and holds a still frame under Reduce Motion, as the web does.\n */\nexport function ThinkingOrb({\n state = \"searching\",\n size = 64,\n speed = 0.8,\n displaySize,\n}: {\n state?: OrbState;\n size?: 64 | 20;\n speed?: number;\n /** Draw the tuned preset larger (for example a call screen) without losing sharpness. */\n displaySize?: number;\n}) {\n const theme = useNativeTimelineTheme();\n const dark = theme.scheme === \"dark\";\n const preset = useMemo(() => resolvePreset(state, size), [state, size]);\n const [time, setTime] = useState(0.6);\n const [reduceMotion, setReduceMotion] = useState(false);\n const [active, setActive] = useState(AppState.currentState === \"active\");\n useEffect(() => {\n void AccessibilityInfo.isReduceMotionEnabled().then(setReduceMotion);\n const motion = AccessibilityInfo.addEventListener(\"reduceMotionChanged\", setReduceMotion);\n const app = AppState.addEventListener(\"change\", (next) => setActive(next === \"active\"));\n return () => {\n motion.remove();\n app.remove();\n };\n }, []);\n useEffect(() => {\n if (reduceMotion || !active) return;\n const started = Date.now();\n const timer = setInterval(() => setTime((Date.now() - started) / 1000), FRAME_MS);\n return () => clearInterval(timer);\n }, [active, reduceMotion]);\n const frame = MODE_FRAMES[preset.mode](\n size,\n reduceMotion ? 0.6 : time * preset.speed * speed,\n preset.opts,\n );\n return (\n <Svg\n width={displaySize ?? size}\n height={displaySize ?? size}\n viewBox={`0 0 ${size} ${size}`}\n accessibilityElementsHidden\n importantForAccessibility=\"no\"\n >\n {frame.lines.map((line, index) => (\n <Line\n // Stateless drawing slots retain their keys as coordinates animate.\n // oxlint-disable-next-line react/no-array-index-key\n key={`l${index}`}\n x1={line.x1}\n y1={line.y1}\n x2={line.x2}\n y2={line.y2}\n stroke={ink(line.white, line.a, dark)}\n strokeWidth={line.w}\n />\n ))}\n {frame.dots.map((dot, index) => (\n <Circle\n // Stateless drawing slots retain their keys as coordinates animate.\n // oxlint-disable-next-line react/no-array-index-key\n key={`d${index}`}\n cx={dot.x}\n cy={dot.y}\n r={dot.r}\n fill={ink(dot.white, dot.a, dark)}\n />\n ))}\n </Svg>\n );\n}\n"]}
|