@k2b/ui 0.39.0 → 0.41.0
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/dist/browser/chat/ChatComposer.js +759 -570
- package/dist/browser/chat/ChatComposer.js.map +1 -1
- package/dist/browser/inputs/ChoiceGroups.js +30 -18
- package/dist/browser/inputs/ChoiceGroups.js.map +1 -1
- package/dist/browser/inputs/Select.js +32 -19
- package/dist/browser/inputs/Select.js.map +1 -1
- package/dist/browser/inputs/SpecialInputs.js +54 -25
- package/dist/browser/inputs/SpecialInputs.js.map +1 -1
- package/dist/browser/inputs/choice.js +4 -1
- package/dist/browser/inputs/choice.js.map +1 -1
- package/dist/browser/inputs/dictation/Dictation.js +546 -0
- package/dist/browser/inputs/dictation/Dictation.js.map +1 -0
- package/dist/browser/inputs/dictation/create-dictation.js +324 -0
- package/dist/browser/inputs/dictation/create-dictation.js.map +1 -0
- package/dist/browser/inputs/dictation/dictation-messages.js +115 -0
- package/dist/browser/inputs/dictation/dictation-messages.js.map +1 -0
- package/dist/browser/inputs/dictation/text-dictation.js +152 -0
- package/dist/browser/inputs/dictation/text-dictation.js.map +1 -0
- package/dist/browser/inputs/dictation/types.js +2 -0
- package/dist/browser/inputs/dictation/types.js.map +1 -0
- package/dist/browser/inputs/icon-messages.js +29 -2
- package/dist/browser/inputs/icon-messages.js.map +1 -1
- package/dist/browser/inputs/icon-texts.js +172 -0
- package/dist/browser/inputs/icon-texts.js.map +1 -0
- package/dist/browser/inputs/index.js +2 -0
- package/dist/browser/inputs/index.js.map +1 -1
- package/dist/browser/layout/AnchoredPopover.js +1 -1
- package/dist/browser/layout/AnchoredPopover.js.map +1 -1
- package/dist/browser/layout/AppWorkspace.js +31 -17
- package/dist/browser/layout/AppWorkspace.js.map +1 -1
- package/dist/browser/layout/SettingsModal.js +10 -5
- package/dist/browser/layout/SettingsModal.js.map +1 -1
- package/dist/browser/layout/app-workspace-controller.js +49 -5
- package/dist/browser/layout/app-workspace-controller.js.map +1 -1
- package/dist/browser/text-editor/TextEditor.js +74 -25
- package/dist/browser/text-editor/TextEditor.js.map +1 -1
- package/dist/browser/text-editor/dictation.js +166 -0
- package/dist/browser/text-editor/dictation.js.map +1 -0
- package/dist/browser/text-editor.js +1 -0
- package/dist/browser/text-editor.js.map +1 -1
- package/dist/ssr/chat/ChatComposer.js +271 -99
- package/dist/ssr/chat/ChatComposer.js.map +1 -1
- package/dist/ssr/inputs/ChoiceGroups.js +12 -3
- package/dist/ssr/inputs/ChoiceGroups.js.map +1 -1
- package/dist/ssr/inputs/Select.js +8 -2
- package/dist/ssr/inputs/Select.js.map +1 -1
- package/dist/ssr/inputs/SpecialInputs.js +54 -25
- package/dist/ssr/inputs/SpecialInputs.js.map +1 -1
- package/dist/ssr/inputs/choice.js +4 -1
- package/dist/ssr/inputs/choice.js.map +1 -1
- package/dist/ssr/inputs/dictation/Dictation.js +386 -0
- package/dist/ssr/inputs/dictation/Dictation.js.map +1 -0
- package/dist/ssr/inputs/dictation/create-dictation.js +324 -0
- package/dist/ssr/inputs/dictation/create-dictation.js.map +1 -0
- package/dist/ssr/inputs/dictation/dictation-messages.js +115 -0
- package/dist/ssr/inputs/dictation/dictation-messages.js.map +1 -0
- package/dist/ssr/inputs/dictation/text-dictation.js +152 -0
- package/dist/ssr/inputs/dictation/text-dictation.js.map +1 -0
- package/dist/ssr/inputs/dictation/types.js +2 -0
- package/dist/ssr/inputs/dictation/types.js.map +1 -0
- package/dist/ssr/inputs/icon-messages.js +29 -2
- package/dist/ssr/inputs/icon-messages.js.map +1 -1
- package/dist/ssr/inputs/icon-texts.js +172 -0
- package/dist/ssr/inputs/icon-texts.js.map +1 -0
- package/dist/ssr/inputs/index.js +2 -0
- package/dist/ssr/inputs/index.js.map +1 -1
- package/dist/ssr/layout/AnchoredPopover.js +1 -1
- package/dist/ssr/layout/AnchoredPopover.js.map +1 -1
- package/dist/ssr/layout/AppWorkspace.js +31 -17
- package/dist/ssr/layout/AppWorkspace.js.map +1 -1
- package/dist/ssr/layout/SettingsModal.js +10 -5
- package/dist/ssr/layout/SettingsModal.js.map +1 -1
- package/dist/ssr/layout/app-workspace-controller.js +49 -5
- package/dist/ssr/layout/app-workspace-controller.js.map +1 -1
- package/dist/ssr/text-editor/TextEditor.js +74 -25
- package/dist/ssr/text-editor/TextEditor.js.map +1 -1
- package/dist/ssr/text-editor/dictation.js +166 -0
- package/dist/ssr/text-editor/dictation.js.map +1 -0
- package/dist/ssr/text-editor.js +1 -0
- package/dist/ssr/text-editor.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/types/chat/ChatComposer.d.ts +9 -0
- package/dist/types/inputs/SpecialInputs.d.ts +8 -0
- package/dist/types/inputs/dictation/Dictation.d.ts +26 -0
- package/dist/types/inputs/dictation/create-dictation.d.ts +77 -0
- package/dist/types/inputs/dictation/dictation-messages.d.ts +54 -0
- package/dist/types/inputs/dictation/text-dictation.d.ts +36 -0
- package/dist/types/inputs/dictation/types.d.ts +74 -0
- package/dist/types/inputs/icon-messages.d.ts +14 -0
- package/dist/types/inputs/icon-texts.d.ts +13 -0
- package/dist/types/inputs/index.d.ts +5 -0
- package/dist/types/layout/AnchoredPopover.d.ts +5 -0
- package/dist/types/layout/AppWorkspace.d.ts +14 -0
- package/dist/types/layout/app-workspace-controller.d.ts +2 -0
- package/dist/types/text-editor/TextEditor.d.ts +10 -1
- package/dist/types/text-editor/dictation.d.ts +20 -0
- package/dist/types/text-editor.d.ts +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { createSignal, getOwner, onCleanup } from "solid-js";
|
|
2
|
+
import { isServer } from "solid-js/web";
|
|
3
|
+
import { announce } from "../../feedback/announce";
|
|
4
|
+
import { useLocale } from "../../intl/locale";
|
|
5
|
+
import { dictationMessages } from "./dictation-messages";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* What stays visible after a session could not start or ended on its own. `interrupted` and `ended` replace the bar,
|
|
9
|
+
* so "Continue" writes on at the same mark; the others are cards at the microphone.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const deviceKey = "k2b.dictation.deviceId";
|
|
13
|
+
const levelCount = 160;
|
|
14
|
+
/** A stop waits this long for the source's last words before it gives up the connection. */
|
|
15
|
+
const stopGraceMs = 8_000;
|
|
16
|
+
const defaultMaxMs = 600_000;
|
|
17
|
+
const readDevice = () => {
|
|
18
|
+
try {
|
|
19
|
+
return localStorage.getItem(deviceKey) ?? undefined;
|
|
20
|
+
} catch {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
const writeDevice = id => {
|
|
25
|
+
try {
|
|
26
|
+
localStorage.setItem(deviceKey, id);
|
|
27
|
+
} catch {
|
|
28
|
+
// A private window may refuse storage; the choice then lasts for this page.
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
const noticeFor = (code, resetsAt) => {
|
|
32
|
+
switch (code) {
|
|
33
|
+
case "microphone_blocked":
|
|
34
|
+
return {
|
|
35
|
+
kind: "blocked"
|
|
36
|
+
};
|
|
37
|
+
case "microphone_unavailable":
|
|
38
|
+
return {
|
|
39
|
+
kind: "unavailable"
|
|
40
|
+
};
|
|
41
|
+
case "not_configured":
|
|
42
|
+
case "no_access":
|
|
43
|
+
return {
|
|
44
|
+
kind: "not_available"
|
|
45
|
+
};
|
|
46
|
+
case "daily_limit":
|
|
47
|
+
return {
|
|
48
|
+
kind: "quota",
|
|
49
|
+
resetsAt
|
|
50
|
+
};
|
|
51
|
+
case "overloaded":
|
|
52
|
+
return {
|
|
53
|
+
kind: "overloaded"
|
|
54
|
+
};
|
|
55
|
+
case "busy_elsewhere":
|
|
56
|
+
return {
|
|
57
|
+
kind: "elsewhere"
|
|
58
|
+
};
|
|
59
|
+
default:
|
|
60
|
+
return {
|
|
61
|
+
kind: "interrupted"
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** Bar notices stand where the bar was; card notices open at the microphone. */
|
|
67
|
+
export const isBarNotice = notice => notice?.kind === "interrupted" || notice?.kind === "ended";
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The state of one dictation place: which session runs, what its bar shows, and the notice it leaves. Render it with
|
|
71
|
+
* `DictationButton` and `DictationBar`; the target decides where text lands. One controller runs one session at a time.
|
|
72
|
+
*/
|
|
73
|
+
export function createDictation(options) {
|
|
74
|
+
const [status, setStatus] = createSignal("idle");
|
|
75
|
+
const [notice, setNotice] = createSignal(null);
|
|
76
|
+
const [levels, setLevels] = createSignal([]);
|
|
77
|
+
const [elapsedMs, setElapsedMs] = createSignal(0);
|
|
78
|
+
const [maxMs, setMaxMs] = createSignal(defaultMaxMs);
|
|
79
|
+
const [device, setDevice] = createSignal();
|
|
80
|
+
const [deviceId, setDeviceId] = createSignal(isServer ? undefined : readDevice());
|
|
81
|
+
const [devices, setDevices] = createSignal([]);
|
|
82
|
+
const [offline, setOffline] = createSignal(!isServer && typeof navigator !== "undefined" && navigator.onLine === false);
|
|
83
|
+
const elements = {};
|
|
84
|
+
let session;
|
|
85
|
+
let abort;
|
|
86
|
+
/** The target's mark stays open while a notice offers to continue. */
|
|
87
|
+
let open = false;
|
|
88
|
+
let timer;
|
|
89
|
+
let liveSince = 0;
|
|
90
|
+
let disposed = false;
|
|
91
|
+
let grace;
|
|
92
|
+
const locale = useLocale();
|
|
93
|
+
const t = () => dictationMessages.resolve([locale()]).t;
|
|
94
|
+
/** Set while a new microphone takes over, so the mark stays where the words go on. */
|
|
95
|
+
let restarting = false;
|
|
96
|
+
const focusLater = role => queueMicrotask(() => requestAnimationFrame(() => elements[role]?.focus({
|
|
97
|
+
preventScroll: true
|
|
98
|
+
})));
|
|
99
|
+
const clearTimer = () => {
|
|
100
|
+
if (timer) clearInterval(timer);
|
|
101
|
+
timer = undefined;
|
|
102
|
+
};
|
|
103
|
+
const close = () => {
|
|
104
|
+
if (!open) return;
|
|
105
|
+
open = false;
|
|
106
|
+
options.target.end();
|
|
107
|
+
};
|
|
108
|
+
const settle = (next, message) => {
|
|
109
|
+
clearTimer();
|
|
110
|
+
if (grace) clearTimeout(grace);
|
|
111
|
+
grace = undefined;
|
|
112
|
+
session = undefined;
|
|
113
|
+
abort = undefined;
|
|
114
|
+
setStatus("idle");
|
|
115
|
+
setNotice(next);
|
|
116
|
+
if (restarting && !next) {
|
|
117
|
+
// A new microphone takes over at the same mark.
|
|
118
|
+
queueMicrotask(start);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
restarting = false;
|
|
122
|
+
if (!next) close();
|
|
123
|
+
if (message) announce(message);
|
|
124
|
+
// A card takes the focus itself and returns it to the microphone when it closes.
|
|
125
|
+
if (!next || isBarNotice(next)) focusLater(next ? "continue" : "microphone");
|
|
126
|
+
};
|
|
127
|
+
const run = async (current, signal) => {
|
|
128
|
+
let finished = false;
|
|
129
|
+
try {
|
|
130
|
+
for await (const event of current.events) {
|
|
131
|
+
if (signal.aborted || disposed) return;
|
|
132
|
+
switch (event.type) {
|
|
133
|
+
case "state":
|
|
134
|
+
if (event.state === "connecting") setStatus("starting");else if (event.state === "live") {
|
|
135
|
+
setMaxMs(event.maxMs || defaultMaxMs);
|
|
136
|
+
if (event.device) setDevice(event.device);
|
|
137
|
+
liveSince = performance.now();
|
|
138
|
+
setElapsedMs(0);
|
|
139
|
+
clearTimer();
|
|
140
|
+
timer = setInterval(() => setElapsedMs(performance.now() - liveSince), 250);
|
|
141
|
+
const resumed = restarting;
|
|
142
|
+
restarting = false;
|
|
143
|
+
setStatus("live");
|
|
144
|
+
if (!resumed) {
|
|
145
|
+
announce(t().running);
|
|
146
|
+
focusLater("stop");
|
|
147
|
+
}
|
|
148
|
+
} else if (event.state === "finishing") setStatus("finishing");else {
|
|
149
|
+
finished = true;
|
|
150
|
+
if (event.reason === "daily_limit") settle({
|
|
151
|
+
kind: "quota"
|
|
152
|
+
}, t().ended);else if (event.reason) settle({
|
|
153
|
+
kind: "ended",
|
|
154
|
+
reason: event.reason,
|
|
155
|
+
maxMs: maxMs()
|
|
156
|
+
}, t().ended);else settle(null, t().ended);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
break;
|
|
160
|
+
case "level":
|
|
161
|
+
setLevels(previous => [...previous.slice(1 - levelCount), Math.max(0, Math.min(1, event.level))]);
|
|
162
|
+
break;
|
|
163
|
+
case "text":
|
|
164
|
+
if (event.text) options.target.insert(event.text);
|
|
165
|
+
break;
|
|
166
|
+
case "error":
|
|
167
|
+
finished = true;
|
|
168
|
+
settle(noticeFor(event.code, event.resetsAt));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
} catch {
|
|
173
|
+
if (signal.aborted || disposed) return;
|
|
174
|
+
finished = true;
|
|
175
|
+
settle({
|
|
176
|
+
kind: "interrupted"
|
|
177
|
+
});
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (!finished && !signal.aborted && !disposed) settle({
|
|
181
|
+
kind: "interrupted"
|
|
182
|
+
});
|
|
183
|
+
};
|
|
184
|
+
const start = () => {
|
|
185
|
+
if (disposed || offline() || options.disabled?.() || status() !== "idle") return;
|
|
186
|
+
setNotice(null);
|
|
187
|
+
if (!restarting) setDevice(undefined);
|
|
188
|
+
options.target.begin();
|
|
189
|
+
open = true;
|
|
190
|
+
setLevels([]);
|
|
191
|
+
setElapsedMs(0);
|
|
192
|
+
setStatus("starting");
|
|
193
|
+
const controller = new AbortController();
|
|
194
|
+
abort = controller;
|
|
195
|
+
try {
|
|
196
|
+
session = options.source.start({
|
|
197
|
+
deviceId: deviceId(),
|
|
198
|
+
signal: controller.signal
|
|
199
|
+
});
|
|
200
|
+
} catch {
|
|
201
|
+
settle({
|
|
202
|
+
kind: "interrupted"
|
|
203
|
+
});
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
void run(session, controller.signal);
|
|
207
|
+
};
|
|
208
|
+
const stop = async () => {
|
|
209
|
+
const current = session;
|
|
210
|
+
const controller = abort;
|
|
211
|
+
if (!current || !controller || status() !== "live" && status() !== "starting") return;
|
|
212
|
+
if (status() === "starting") {
|
|
213
|
+
// Nothing was said yet: end it like a discard, without removing anything.
|
|
214
|
+
controller.abort();
|
|
215
|
+
settle(null);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
setStatus("finishing");
|
|
219
|
+
clearTimer();
|
|
220
|
+
// The deadline stays until the session settled, also when stop() itself fails, so "Finishing" always ends.
|
|
221
|
+
grace = setTimeout(() => {
|
|
222
|
+
if (abort !== controller) return;
|
|
223
|
+
controller.abort();
|
|
224
|
+
settle({
|
|
225
|
+
kind: "interrupted"
|
|
226
|
+
});
|
|
227
|
+
}, stopGraceMs);
|
|
228
|
+
try {
|
|
229
|
+
await current.stop();
|
|
230
|
+
} catch {
|
|
231
|
+
// The event stream reports how it ended; the deadline covers a stream that stalls.
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
const discard = () => {
|
|
235
|
+
restarting = false;
|
|
236
|
+
abort?.abort();
|
|
237
|
+
if (open) options.target.discard();
|
|
238
|
+
settle(null, t().discarded);
|
|
239
|
+
};
|
|
240
|
+
if (!isServer && typeof window !== "undefined") {
|
|
241
|
+
const online = () => setOffline(false);
|
|
242
|
+
const lost = () => setOffline(true);
|
|
243
|
+
window.addEventListener("online", online);
|
|
244
|
+
window.addEventListener("offline", lost);
|
|
245
|
+
if (getOwner()) onCleanup(() => {
|
|
246
|
+
window.removeEventListener("online", online);
|
|
247
|
+
window.removeEventListener("offline", lost);
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
if (getOwner()) onCleanup(() => {
|
|
251
|
+
disposed = true;
|
|
252
|
+
clearTimer();
|
|
253
|
+
abort?.abort();
|
|
254
|
+
if (open) {
|
|
255
|
+
open = false;
|
|
256
|
+
options.target.end();
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
return {
|
|
260
|
+
status,
|
|
261
|
+
active: () => status() !== "idle",
|
|
262
|
+
notice,
|
|
263
|
+
levels,
|
|
264
|
+
elapsedMs,
|
|
265
|
+
maxMs,
|
|
266
|
+
offline,
|
|
267
|
+
device,
|
|
268
|
+
deviceId,
|
|
269
|
+
devices,
|
|
270
|
+
loadDevices: async () => {
|
|
271
|
+
try {
|
|
272
|
+
setDevices(await options.source.devices());
|
|
273
|
+
} catch {
|
|
274
|
+
setDevices([]);
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
chooseDevice: id => {
|
|
278
|
+
writeDevice(id);
|
|
279
|
+
setDeviceId(id);
|
|
280
|
+
const chosen = devices().find(item => item.id === id);
|
|
281
|
+
if (chosen) setDevice(chosen);
|
|
282
|
+
// A running session keeps its microphone; the next words come from the new one after a short finish.
|
|
283
|
+
if (status() !== "live") return;
|
|
284
|
+
restarting = true;
|
|
285
|
+
void stop();
|
|
286
|
+
},
|
|
287
|
+
get alternative() {
|
|
288
|
+
return options.alternative;
|
|
289
|
+
},
|
|
290
|
+
start: () => start(),
|
|
291
|
+
stop,
|
|
292
|
+
discard,
|
|
293
|
+
dismiss: () => {
|
|
294
|
+
const card = notice() && !isBarNotice(notice());
|
|
295
|
+
setNotice(null);
|
|
296
|
+
close();
|
|
297
|
+
// A closed card gives the focus back to the microphone, unless the person moved it elsewhere meanwhile.
|
|
298
|
+
if (card) queueMicrotask(() => requestAnimationFrame(() => {
|
|
299
|
+
const active = document.activeElement;
|
|
300
|
+
if (!active || active === document.body || !active.isConnected) elements.microphone?.focus({
|
|
301
|
+
preventScroll: true
|
|
302
|
+
});
|
|
303
|
+
}));
|
|
304
|
+
},
|
|
305
|
+
end: () => {
|
|
306
|
+
restarting = false;
|
|
307
|
+
if (status() === "idle" && !notice() && !open) return;
|
|
308
|
+
abort?.abort();
|
|
309
|
+
clearTimer();
|
|
310
|
+
if (grace) clearTimeout(grace);
|
|
311
|
+
grace = undefined;
|
|
312
|
+
session = undefined;
|
|
313
|
+
abort = undefined;
|
|
314
|
+
setStatus("idle");
|
|
315
|
+
setNotice(null);
|
|
316
|
+
close();
|
|
317
|
+
},
|
|
318
|
+
disabled: () => Boolean(options.disabled?.()),
|
|
319
|
+
bind: (role, element) => {
|
|
320
|
+
if (element) elements[role] = element;else delete elements[role];
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
//# sourceMappingURL=create-dictation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":[],"sources":["inputs/dictation/create-dictation.ts"],"sourcesContent":["import { type Accessor, createSignal, getOwner, onCleanup } from \"solid-js\";\nimport { isServer } from \"solid-js/web\";\nimport { announce } from \"../../feedback/announce\";\nimport { useLocale } from \"../../intl/locale\";\nimport { dictationMessages } from \"./dictation-messages\";\nimport type {\n DictationAlternative,\n DictationDevice,\n DictationErrorCode,\n DictationEvent,\n DictationSession,\n DictationSource,\n DictationTarget,\n} from \"./types\";\n\nexport type DictationStatus = \"idle\" | \"starting\" | \"live\" | \"finishing\";\n\n/**\n * What stays visible after a session could not start or ended on its own. `interrupted` and `ended` replace the bar,\n * so \"Continue\" writes on at the same mark; the others are cards at the microphone.\n */\nexport type DictationNotice =\n | { kind: \"blocked\" }\n | { kind: \"unavailable\" }\n | { kind: \"not_available\" }\n | { kind: \"quota\"; resetsAt?: string }\n | { kind: \"overloaded\" }\n | { kind: \"elsewhere\" }\n | { kind: \"interrupted\" }\n | { kind: \"ended\"; reason: \"max_duration\" | \"silence\"; maxMs: number };\n\nexport type DictationOptions = {\n source: DictationSource;\n target: DictationTarget;\n /** Offered beside \"Try again\" when live dictation is busy. */\n alternative?: DictationAlternative;\n /** While true, dictation cannot start, such as in a read-only field; the host calls `end()` when it turns true. */\n disabled?: () => boolean;\n};\n\nexport type DictationController = {\n status: Accessor<DictationStatus>;\n /** Dictating, starting, or finishing: the bar stands in for the host's controls. */\n active: Accessor<boolean>;\n notice: Accessor<DictationNotice | null>;\n /** Recent levels from 0 to 1, newest last. */\n levels: Accessor<readonly number[]>;\n elapsedMs: Accessor<number>;\n maxMs: Accessor<number>;\n offline: Accessor<boolean>;\n device: Accessor<DictationDevice | undefined>;\n deviceId: Accessor<string | undefined>;\n devices: Accessor<readonly DictationDevice[]>;\n loadDevices(): Promise<void>;\n chooseDevice(id: string): void;\n alternative?: DictationAlternative;\n start(): void;\n stop(): Promise<void>;\n /** Ends the session at once and removes only the text it inserted. */\n discard(): void;\n /** Closes a notice; the text stays. */\n dismiss(): void;\n /** Ends any session at once and closes the dictation; the text stays. For a field that changes or turns read-only. */\n end(): void;\n /** Whether dictation may start now. */\n disabled: Accessor<boolean>;\n /** Elements that receive focus: the microphone after the end, the stop button after the start. */\n bind(role: \"microphone\" | \"stop\" | \"continue\", element: HTMLElement | undefined): void;\n};\n\nconst deviceKey = \"k2b.dictation.deviceId\";\nconst levelCount = 160;\n/** A stop waits this long for the source's last words before it gives up the connection. */\nconst stopGraceMs = 8_000;\nconst defaultMaxMs = 600_000;\n\nconst readDevice = () => {\n try {\n return localStorage.getItem(deviceKey) ?? undefined;\n } catch {\n return undefined;\n }\n};\nconst writeDevice = (id: string) => {\n try {\n localStorage.setItem(deviceKey, id);\n } catch {\n // A private window may refuse storage; the choice then lasts for this page.\n }\n};\n\nconst noticeFor = (code: DictationErrorCode, resetsAt?: string): DictationNotice => {\n switch (code) {\n case \"microphone_blocked\":\n return { kind: \"blocked\" };\n case \"microphone_unavailable\":\n return { kind: \"unavailable\" };\n case \"not_configured\":\n case \"no_access\":\n return { kind: \"not_available\" };\n case \"daily_limit\":\n return { kind: \"quota\", resetsAt };\n case \"overloaded\":\n return { kind: \"overloaded\" };\n case \"busy_elsewhere\":\n return { kind: \"elsewhere\" };\n default:\n return { kind: \"interrupted\" };\n }\n};\n\n/** Bar notices stand where the bar was; card notices open at the microphone. */\nexport const isBarNotice = (notice: DictationNotice | null) => notice?.kind === \"interrupted\" || notice?.kind === \"ended\";\n\n/**\n * The state of one dictation place: which session runs, what its bar shows, and the notice it leaves. Render it with\n * `DictationButton` and `DictationBar`; the target decides where text lands. One controller runs one session at a time.\n */\nexport function createDictation(options: DictationOptions): DictationController {\n const [status, setStatus] = createSignal<DictationStatus>(\"idle\");\n const [notice, setNotice] = createSignal<DictationNotice | null>(null);\n const [levels, setLevels] = createSignal<readonly number[]>([]);\n const [elapsedMs, setElapsedMs] = createSignal(0);\n const [maxMs, setMaxMs] = createSignal(defaultMaxMs);\n const [device, setDevice] = createSignal<DictationDevice>();\n const [deviceId, setDeviceId] = createSignal<string | undefined>(isServer ? undefined : readDevice());\n const [devices, setDevices] = createSignal<readonly DictationDevice[]>([]);\n const [offline, setOffline] = createSignal(!isServer && typeof navigator !== \"undefined\" && navigator.onLine === false);\n const elements: Partial<Record<\"microphone\" | \"stop\" | \"continue\", HTMLElement>> = {};\n let session: DictationSession | undefined;\n let abort: AbortController | undefined;\n /** The target's mark stays open while a notice offers to continue. */\n let open = false;\n let timer: ReturnType<typeof setInterval> | undefined;\n let liveSince = 0;\n let disposed = false;\n let grace: ReturnType<typeof setTimeout> | undefined;\n\n const locale = useLocale();\n const t = () => dictationMessages.resolve([locale()]).t;\n /** Set while a new microphone takes over, so the mark stays where the words go on. */\n let restarting = false;\n const focusLater = (role: \"microphone\" | \"stop\" | \"continue\") =>\n queueMicrotask(() => requestAnimationFrame(() => elements[role]?.focus({ preventScroll: true })));\n const clearTimer = () => {\n if (timer) clearInterval(timer);\n timer = undefined;\n };\n const close = () => {\n if (!open) return;\n open = false;\n options.target.end();\n };\n const settle = (next: DictationNotice | null, message?: string) => {\n clearTimer();\n if (grace) clearTimeout(grace);\n grace = undefined;\n session = undefined;\n abort = undefined;\n setStatus(\"idle\");\n setNotice(next);\n if (restarting && !next) {\n // A new microphone takes over at the same mark.\n queueMicrotask(start);\n return;\n }\n restarting = false;\n if (!next) close();\n if (message) announce(message);\n // A card takes the focus itself and returns it to the microphone when it closes.\n if (!next || isBarNotice(next)) focusLater(next ? \"continue\" : \"microphone\");\n };\n\n const run = async (current: DictationSession, signal: AbortSignal) => {\n let finished = false;\n try {\n for await (const event of current.events as AsyncIterable<DictationEvent>) {\n if (signal.aborted || disposed) return;\n switch (event.type) {\n case \"state\":\n if (event.state === \"connecting\") setStatus(\"starting\");\n else if (event.state === \"live\") {\n setMaxMs(event.maxMs || defaultMaxMs);\n if (event.device) setDevice(event.device);\n liveSince = performance.now();\n setElapsedMs(0);\n clearTimer();\n timer = setInterval(() => setElapsedMs(performance.now() - liveSince), 250);\n const resumed = restarting;\n restarting = false;\n setStatus(\"live\");\n if (!resumed) {\n announce(t().running);\n focusLater(\"stop\");\n }\n } else if (event.state === \"finishing\") setStatus(\"finishing\");\n else {\n finished = true;\n if (event.reason === \"daily_limit\") settle({ kind: \"quota\" }, t().ended);\n else if (event.reason) settle({ kind: \"ended\", reason: event.reason, maxMs: maxMs() }, t().ended);\n else settle(null, t().ended);\n return;\n }\n break;\n case \"level\":\n setLevels((previous) => [...previous.slice(1 - levelCount), Math.max(0, Math.min(1, event.level))]);\n break;\n case \"text\":\n if (event.text) options.target.insert(event.text);\n break;\n case \"error\":\n finished = true;\n settle(noticeFor(event.code, event.resetsAt));\n return;\n }\n }\n } catch {\n if (signal.aborted || disposed) return;\n finished = true;\n settle({ kind: \"interrupted\" });\n return;\n }\n if (!finished && !signal.aborted && !disposed) settle({ kind: \"interrupted\" });\n };\n\n const start = () => {\n if (disposed || offline() || options.disabled?.() || status() !== \"idle\") return;\n setNotice(null);\n if (!restarting) setDevice(undefined);\n options.target.begin();\n open = true;\n setLevels([]);\n setElapsedMs(0);\n setStatus(\"starting\");\n const controller = new AbortController();\n abort = controller;\n try {\n session = options.source.start({ deviceId: deviceId(), signal: controller.signal });\n } catch {\n settle({ kind: \"interrupted\" });\n return;\n }\n void run(session, controller.signal);\n };\n\n const stop = async () => {\n const current = session;\n const controller = abort;\n if (!current || !controller || (status() !== \"live\" && status() !== \"starting\")) return;\n if (status() === \"starting\") {\n // Nothing was said yet: end it like a discard, without removing anything.\n controller.abort();\n settle(null);\n return;\n }\n setStatus(\"finishing\");\n clearTimer();\n // The deadline stays until the session settled, also when stop() itself fails, so \"Finishing\" always ends.\n grace = setTimeout(() => {\n if (abort !== controller) return;\n controller.abort();\n settle({ kind: \"interrupted\" });\n }, stopGraceMs);\n try {\n await current.stop();\n } catch {\n // The event stream reports how it ended; the deadline covers a stream that stalls.\n }\n };\n\n const discard = () => {\n restarting = false;\n abort?.abort();\n if (open) options.target.discard();\n settle(null, t().discarded);\n };\n\n if (!isServer && typeof window !== \"undefined\") {\n const online = () => setOffline(false);\n const lost = () => setOffline(true);\n window.addEventListener(\"online\", online);\n window.addEventListener(\"offline\", lost);\n if (getOwner())\n onCleanup(() => {\n window.removeEventListener(\"online\", online);\n window.removeEventListener(\"offline\", lost);\n });\n }\n if (getOwner())\n onCleanup(() => {\n disposed = true;\n clearTimer();\n abort?.abort();\n if (open) {\n open = false;\n options.target.end();\n }\n });\n\n return {\n status,\n active: () => status() !== \"idle\",\n notice,\n levels,\n elapsedMs,\n maxMs,\n offline,\n device,\n deviceId,\n devices,\n loadDevices: async () => {\n try {\n setDevices(await options.source.devices());\n } catch {\n setDevices([]);\n }\n },\n chooseDevice: (id) => {\n writeDevice(id);\n setDeviceId(id);\n const chosen = devices().find((item) => item.id === id);\n if (chosen) setDevice(chosen);\n // A running session keeps its microphone; the next words come from the new one after a short finish.\n if (status() !== \"live\") return;\n restarting = true;\n void stop();\n },\n get alternative() {\n return options.alternative;\n },\n start: () => start(),\n stop,\n discard,\n dismiss: () => {\n const card = notice() && !isBarNotice(notice());\n setNotice(null);\n close();\n // A closed card gives the focus back to the microphone, unless the person moved it elsewhere meanwhile.\n if (card)\n queueMicrotask(() =>\n requestAnimationFrame(() => {\n const active = document.activeElement;\n if (!active || active === document.body || !active.isConnected) elements.microphone?.focus({ preventScroll: true });\n }),\n );\n },\n end: () => {\n restarting = false;\n if (status() === \"idle\" && !notice() && !open) return;\n abort?.abort();\n clearTimer();\n if (grace) clearTimeout(grace);\n grace = undefined;\n session = undefined;\n abort = undefined;\n setStatus(\"idle\");\n setNotice(null);\n close();\n },\n disabled: () => Boolean(options.disabled?.()),\n bind: (role, element) => {\n if (element) elements[role] = element;\n else delete elements[role];\n },\n };\n}\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { i18n } from "@k2b/stdlib";
|
|
2
|
+
export const dictationMessages = i18n.define({
|
|
3
|
+
baseLocale: "en",
|
|
4
|
+
messages: {
|
|
5
|
+
en: {
|
|
6
|
+
dictate: "Dictate",
|
|
7
|
+
dictateOffline: "Dictate, not available offline",
|
|
8
|
+
offline: "Offline. Dictation needs a connection.",
|
|
9
|
+
discard: "Discard dictation",
|
|
10
|
+
stop: "Stop dictation",
|
|
11
|
+
running: "Dictating",
|
|
12
|
+
finishingGroup: "Finishing dictation",
|
|
13
|
+
finishing: "Finishing …",
|
|
14
|
+
starting: "Starting …",
|
|
15
|
+
ended: "Dictation ended",
|
|
16
|
+
discarded: "Dictation discarded",
|
|
17
|
+
microphone: "Microphone",
|
|
18
|
+
microphoneNamed: ({
|
|
19
|
+
name
|
|
20
|
+
}) => `Microphone: ${name}`,
|
|
21
|
+
systemDefault: "Default",
|
|
22
|
+
interrupted: "Dictation interrupted. The text so far stays.",
|
|
23
|
+
interruptedShort: "Interrupted. Text stays.",
|
|
24
|
+
maxDone: ({
|
|
25
|
+
minutes
|
|
26
|
+
}) => `Ended after ${minutes} minutes. The text stays.`,
|
|
27
|
+
maxDoneShort: ({
|
|
28
|
+
minutes
|
|
29
|
+
}) => `Ended after ${minutes} min.`,
|
|
30
|
+
silenceDone: "Ended after a long silence. The text stays.",
|
|
31
|
+
silenceDoneShort: "Ended after silence.",
|
|
32
|
+
continue: "Continue dictating",
|
|
33
|
+
dismiss: "Close notice",
|
|
34
|
+
blockedTitle: "Microphone blocked",
|
|
35
|
+
blockedBody: "The browser does not let this page use the microphone. To allow it:",
|
|
36
|
+
blockedStep1: "Tap the lock on the left of the address bar.",
|
|
37
|
+
blockedStep2: "Choose “Allow” for “Microphone”.",
|
|
38
|
+
retry: "Try again",
|
|
39
|
+
quotaTitle: "Your dictation allowance for today is used up",
|
|
40
|
+
quotaBody: ({
|
|
41
|
+
time
|
|
42
|
+
}) => `You can dictate again from ${time}. Typing works as always.`,
|
|
43
|
+
quotaBodyTomorrow: ({
|
|
44
|
+
time
|
|
45
|
+
}) => `You can dictate again from tomorrow, ${time}. Typing works as always.`,
|
|
46
|
+
overloadedTitle: "Live dictation is busy right now",
|
|
47
|
+
overloadedBody: "Try again in a moment.",
|
|
48
|
+
elsewhereTitle: "You are dictating in another tab",
|
|
49
|
+
elsewhereBody: "End it there, then you can dictate here.",
|
|
50
|
+
unavailableTitle: "No microphone found",
|
|
51
|
+
unavailableBody: "Connect a microphone or choose another one, then try again.",
|
|
52
|
+
notAvailableTitle: "Dictation is not available here",
|
|
53
|
+
notAvailableBody: "Typing works as always.",
|
|
54
|
+
ok: "OK",
|
|
55
|
+
timeLeft: ({
|
|
56
|
+
time
|
|
57
|
+
}) => `${time} left`
|
|
58
|
+
},
|
|
59
|
+
de: {
|
|
60
|
+
dictate: "Diktieren",
|
|
61
|
+
dictateOffline: "Diktieren, offline nicht verfügbar",
|
|
62
|
+
offline: "Offline. Diktieren braucht eine Verbindung.",
|
|
63
|
+
discard: "Diktat verwerfen",
|
|
64
|
+
stop: "Diktat beenden",
|
|
65
|
+
running: "Diktat läuft",
|
|
66
|
+
finishingGroup: "Diktat wird abgeschlossen",
|
|
67
|
+
finishing: "Wird abgeschlossen …",
|
|
68
|
+
starting: "Wird gestartet …",
|
|
69
|
+
ended: "Diktat beendet",
|
|
70
|
+
discarded: "Diktat verworfen",
|
|
71
|
+
microphone: "Mikrofon",
|
|
72
|
+
microphoneNamed: ({
|
|
73
|
+
name
|
|
74
|
+
}) => `Mikrofon: ${name}`,
|
|
75
|
+
systemDefault: "Standard",
|
|
76
|
+
interrupted: "Diktat unterbrochen. Der Text bis hier bleibt.",
|
|
77
|
+
interruptedShort: "Unterbrochen. Text bleibt.",
|
|
78
|
+
maxDone: ({
|
|
79
|
+
minutes
|
|
80
|
+
}) => `Nach ${minutes} Minuten beendet. Der Text bleibt.`,
|
|
81
|
+
maxDoneShort: ({
|
|
82
|
+
minutes
|
|
83
|
+
}) => `Nach ${minutes} Min. beendet.`,
|
|
84
|
+
silenceDone: "Nach langer Stille beendet. Der Text bleibt.",
|
|
85
|
+
silenceDoneShort: "Nach Stille beendet.",
|
|
86
|
+
continue: "Weiter diktieren",
|
|
87
|
+
dismiss: "Hinweis schließen",
|
|
88
|
+
blockedTitle: "Mikrofon blockiert",
|
|
89
|
+
blockedBody: "Der Browser lässt diese Seite nicht an das Mikrofon. So erlaubst du es:",
|
|
90
|
+
blockedStep1: "Links in der Adressleiste auf das Schloss tippen.",
|
|
91
|
+
blockedStep2: "Bei „Mikrofon“ „Erlauben“ wählen.",
|
|
92
|
+
retry: "Erneut versuchen",
|
|
93
|
+
quotaTitle: "Dein Diktat-Kontingent für heute ist aufgebraucht",
|
|
94
|
+
quotaBody: ({
|
|
95
|
+
time
|
|
96
|
+
}) => `Ab ${time} kannst du wieder diktieren. Tippen geht natürlich weiter.`,
|
|
97
|
+
quotaBodyTomorrow: ({
|
|
98
|
+
time
|
|
99
|
+
}) => `Ab morgen, ${time}, kannst du wieder diktieren. Tippen geht natürlich weiter.`,
|
|
100
|
+
overloadedTitle: "Live-Diktat ist gerade ausgelastet",
|
|
101
|
+
overloadedBody: "Versuch es in einem Moment noch mal.",
|
|
102
|
+
elsewhereTitle: "Du diktierst gerade in einem anderen Tab",
|
|
103
|
+
elsewhereBody: "Beende es dort, dann kannst du hier diktieren.",
|
|
104
|
+
unavailableTitle: "Kein Mikrofon gefunden",
|
|
105
|
+
unavailableBody: "Schließ ein Mikrofon an oder wähl ein anderes, dann versuch es noch mal.",
|
|
106
|
+
notAvailableTitle: "Diktieren ist hier nicht verfügbar",
|
|
107
|
+
notAvailableBody: "Tippen geht natürlich weiter.",
|
|
108
|
+
ok: "OK",
|
|
109
|
+
timeLeft: ({
|
|
110
|
+
time
|
|
111
|
+
}) => `noch ${time}`
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
//# sourceMappingURL=dictation-messages.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":[],"sources":["inputs/dictation/dictation-messages.ts"],"sourcesContent":["import { i18n } from \"@k2b/stdlib\";\n\nexport const dictationMessages = i18n.define({\n baseLocale: \"en\",\n messages: {\n en: {\n dictate: \"Dictate\",\n dictateOffline: \"Dictate, not available offline\",\n offline: \"Offline. Dictation needs a connection.\",\n discard: \"Discard dictation\",\n stop: \"Stop dictation\",\n running: \"Dictating\",\n finishingGroup: \"Finishing dictation\",\n finishing: \"Finishing …\",\n starting: \"Starting …\",\n ended: \"Dictation ended\",\n discarded: \"Dictation discarded\",\n microphone: \"Microphone\",\n microphoneNamed: ({ name }: { name: string }) => `Microphone: ${name}`,\n systemDefault: \"Default\",\n interrupted: \"Dictation interrupted. The text so far stays.\",\n interruptedShort: \"Interrupted. Text stays.\",\n maxDone: ({ minutes }: { minutes: number }) => `Ended after ${minutes} minutes. The text stays.`,\n maxDoneShort: ({ minutes }: { minutes: number }) => `Ended after ${minutes} min.`,\n silenceDone: \"Ended after a long silence. The text stays.\",\n silenceDoneShort: \"Ended after silence.\",\n continue: \"Continue dictating\",\n dismiss: \"Close notice\",\n blockedTitle: \"Microphone blocked\",\n blockedBody: \"The browser does not let this page use the microphone. To allow it:\",\n blockedStep1: \"Tap the lock on the left of the address bar.\",\n blockedStep2: \"Choose “Allow” for “Microphone”.\",\n retry: \"Try again\",\n quotaTitle: \"Your dictation allowance for today is used up\",\n quotaBody: ({ time }: { time: string }) => `You can dictate again from ${time}. Typing works as always.`,\n quotaBodyTomorrow: ({ time }: { time: string }) => `You can dictate again from tomorrow, ${time}. Typing works as always.`,\n overloadedTitle: \"Live dictation is busy right now\",\n overloadedBody: \"Try again in a moment.\",\n elsewhereTitle: \"You are dictating in another tab\",\n elsewhereBody: \"End it there, then you can dictate here.\",\n unavailableTitle: \"No microphone found\",\n unavailableBody: \"Connect a microphone or choose another one, then try again.\",\n notAvailableTitle: \"Dictation is not available here\",\n notAvailableBody: \"Typing works as always.\",\n ok: \"OK\",\n timeLeft: ({ time }: { time: string }) => `${time} left`,\n },\n de: {\n dictate: \"Diktieren\",\n dictateOffline: \"Diktieren, offline nicht verfügbar\",\n offline: \"Offline. Diktieren braucht eine Verbindung.\",\n discard: \"Diktat verwerfen\",\n stop: \"Diktat beenden\",\n running: \"Diktat läuft\",\n finishingGroup: \"Diktat wird abgeschlossen\",\n finishing: \"Wird abgeschlossen …\",\n starting: \"Wird gestartet …\",\n ended: \"Diktat beendet\",\n discarded: \"Diktat verworfen\",\n microphone: \"Mikrofon\",\n microphoneNamed: ({ name }: { name: string }) => `Mikrofon: ${name}`,\n systemDefault: \"Standard\",\n interrupted: \"Diktat unterbrochen. Der Text bis hier bleibt.\",\n interruptedShort: \"Unterbrochen. Text bleibt.\",\n maxDone: ({ minutes }: { minutes: number }) => `Nach ${minutes} Minuten beendet. Der Text bleibt.`,\n maxDoneShort: ({ minutes }: { minutes: number }) => `Nach ${minutes} Min. beendet.`,\n silenceDone: \"Nach langer Stille beendet. Der Text bleibt.\",\n silenceDoneShort: \"Nach Stille beendet.\",\n continue: \"Weiter diktieren\",\n dismiss: \"Hinweis schließen\",\n blockedTitle: \"Mikrofon blockiert\",\n blockedBody: \"Der Browser lässt diese Seite nicht an das Mikrofon. So erlaubst du es:\",\n blockedStep1: \"Links in der Adressleiste auf das Schloss tippen.\",\n blockedStep2: \"Bei „Mikrofon“ „Erlauben“ wählen.\",\n retry: \"Erneut versuchen\",\n quotaTitle: \"Dein Diktat-Kontingent für heute ist aufgebraucht\",\n quotaBody: ({ time }: { time: string }) => `Ab ${time} kannst du wieder diktieren. Tippen geht natürlich weiter.`,\n quotaBodyTomorrow: ({ time }: { time: string }) => `Ab morgen, ${time}, kannst du wieder diktieren. Tippen geht natürlich weiter.`,\n overloadedTitle: \"Live-Diktat ist gerade ausgelastet\",\n overloadedBody: \"Versuch es in einem Moment noch mal.\",\n elsewhereTitle: \"Du diktierst gerade in einem anderen Tab\",\n elsewhereBody: \"Beende es dort, dann kannst du hier diktieren.\",\n unavailableTitle: \"Kein Mikrofon gefunden\",\n unavailableBody: \"Schließ ein Mikrofon an oder wähl ein anderes, dann versuch es noch mal.\",\n notAvailableTitle: \"Diktieren ist hier nicht verfügbar\",\n notAvailableBody: \"Tippen geht natürlich weiter.\",\n ok: \"OK\",\n timeLeft: ({ time }: { time: string }) => `noch ${time}`,\n },\n },\n});\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dictation into plain text, such as a textarea: a mark where the next words land and the ranges dictation inserted.
|
|
3
|
+
* Both follow edits the person makes anywhere else, so typing elsewhere never moves the dictated text.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const beginTextDictation = mark => ({
|
|
7
|
+
mark,
|
|
8
|
+
ranges: [],
|
|
9
|
+
fresh: true
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
/** Where `offset` lands after `previous` became `next`; `after` keeps it behind text inserted exactly at it. */
|
|
13
|
+
export const mapTextOffset = (previous, next, offset, after) => {
|
|
14
|
+
if (previous === next) return offset;
|
|
15
|
+
const limit = Math.min(previous.length, next.length);
|
|
16
|
+
let start = 0;
|
|
17
|
+
while (start < limit && previous[start] === next[start]) start++;
|
|
18
|
+
let tail = 0;
|
|
19
|
+
while (tail < limit - start && previous[previous.length - 1 - tail] === next[next.length - 1 - tail]) tail++;
|
|
20
|
+
const oldEnd = previous.length - tail;
|
|
21
|
+
const newEnd = next.length - tail;
|
|
22
|
+
// Before the change, or exactly at its start and asked to stay before it.
|
|
23
|
+
if (offset < start || offset === start && !after) return offset;
|
|
24
|
+
if (offset >= oldEnd) return offset + (newEnd - oldEnd);
|
|
25
|
+
// Inside a replaced stretch: to its new start or end.
|
|
26
|
+
return after ? newEnd : start;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** Follow an edit that dictation did not make. Ranges that the edit removed entirely disappear. */
|
|
30
|
+
export const mapTextDictation = (state, previous, next) => {
|
|
31
|
+
if (previous === next) return state;
|
|
32
|
+
const ranges = [];
|
|
33
|
+
for (const [from, to] of state.ranges) {
|
|
34
|
+
const start = mapTextOffset(previous, next, from, true);
|
|
35
|
+
const end = Math.max(start, mapTextOffset(previous, next, to, false));
|
|
36
|
+
if (end > start) ranges.push([start, end]);
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
...state,
|
|
40
|
+
mark: mapTextOffset(previous, next, state.mark, true),
|
|
41
|
+
ranges
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
const space = /\s/;
|
|
45
|
+
|
|
46
|
+
/** Append `text` at the mark. The first words of a session get one space from earlier text, never two. */
|
|
47
|
+
export const insertTextDictation = (state, value, text) => {
|
|
48
|
+
const mark = Math.min(state.mark, value.length);
|
|
49
|
+
let chunk = text;
|
|
50
|
+
/** A space after the first words when they land right before existing text; the mark stays before it. */
|
|
51
|
+
let gap = "";
|
|
52
|
+
if (state.fresh) {
|
|
53
|
+
const before = value[mark - 1];
|
|
54
|
+
if (before === undefined || space.test(before)) chunk = chunk.replace(/^[^\S\n]+/, "");else if (chunk && !space.test(chunk[0])) chunk = ` ${chunk}`;
|
|
55
|
+
const after = value[mark];
|
|
56
|
+
if (chunk && after !== undefined && !space.test(after) && !space.test(chunk[chunk.length - 1])) gap = " ";
|
|
57
|
+
}
|
|
58
|
+
if (!chunk) return {
|
|
59
|
+
state: {
|
|
60
|
+
...state,
|
|
61
|
+
fresh: state.fresh && !text
|
|
62
|
+
},
|
|
63
|
+
value
|
|
64
|
+
};
|
|
65
|
+
const length = chunk.length + gap.length;
|
|
66
|
+
const ranges = [];
|
|
67
|
+
let joined = false;
|
|
68
|
+
for (const [from, to] of state.ranges) {
|
|
69
|
+
if (from > mark) ranges.push([from + length, to + length]);else if (to >= mark && !joined) {
|
|
70
|
+
// The range around the mark grows by the new words.
|
|
71
|
+
ranges.push([from, to + length]);
|
|
72
|
+
joined = true;
|
|
73
|
+
} else ranges.push([from, to]);
|
|
74
|
+
}
|
|
75
|
+
if (!joined) {
|
|
76
|
+
ranges.push([mark, mark + length]);
|
|
77
|
+
ranges.sort((a, b) => a[0] - b[0]);
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
state: {
|
|
81
|
+
mark: mark + chunk.length,
|
|
82
|
+
ranges,
|
|
83
|
+
fresh: false
|
|
84
|
+
},
|
|
85
|
+
value: value.slice(0, mark) + chunk + gap + value.slice(mark)
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** Remove every dictated range; text that was there before stays. */
|
|
90
|
+
export const discardTextDictation = (state, value) => {
|
|
91
|
+
let next = value;
|
|
92
|
+
let caret = state.mark;
|
|
93
|
+
for (const [from, to] of [...state.ranges].sort((a, b) => b[0] - a[0])) {
|
|
94
|
+
next = next.slice(0, from) + next.slice(to);
|
|
95
|
+
if (caret >= to) caret -= to - from;else if (caret > from) caret = from;
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
value: next,
|
|
99
|
+
caret: Math.min(caret, next.length)
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Split highlight segments of a field's text at the newest dictated words and the mark, for an overlay that tints the
|
|
105
|
+
* fresh words and shows where the next ones land. A mark arrives as an empty segment.
|
|
106
|
+
*/
|
|
107
|
+
export const splitDictationSegments = (segments, fresh, mark) => {
|
|
108
|
+
const result = [];
|
|
109
|
+
let offset = 0;
|
|
110
|
+
let marked = mark === undefined;
|
|
111
|
+
for (const segment of segments) {
|
|
112
|
+
const end = offset + segment.text.length;
|
|
113
|
+
const cuts = new Set([offset, end]);
|
|
114
|
+
for (const [from, to] of fresh) {
|
|
115
|
+
if (from > offset && from < end) cuts.add(from);
|
|
116
|
+
if (to > offset && to < end) cuts.add(to);
|
|
117
|
+
}
|
|
118
|
+
if (!marked && mark > offset && mark < end) cuts.add(mark);
|
|
119
|
+
const points = [...cuts].sort((a, b) => a - b);
|
|
120
|
+
for (let index = 0; index < points.length; index++) {
|
|
121
|
+
const from = points[index];
|
|
122
|
+
if (!marked && mark === from) {
|
|
123
|
+
result.push({
|
|
124
|
+
...segment,
|
|
125
|
+
text: "",
|
|
126
|
+
mark: true
|
|
127
|
+
});
|
|
128
|
+
marked = true;
|
|
129
|
+
}
|
|
130
|
+
const to = points[index + 1];
|
|
131
|
+
if (to === undefined) break;
|
|
132
|
+
const isFresh = fresh.some(([start, stop]) => start <= from && to <= stop);
|
|
133
|
+
result.push({
|
|
134
|
+
...segment,
|
|
135
|
+
text: segment.text.slice(from - offset, to - offset),
|
|
136
|
+
...(isFresh ? {
|
|
137
|
+
fresh: true
|
|
138
|
+
} : {})
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
offset = end;
|
|
142
|
+
}
|
|
143
|
+
if (!marked) result.push({
|
|
144
|
+
...(segments[segments.length - 1] ?? {
|
|
145
|
+
text: ""
|
|
146
|
+
}),
|
|
147
|
+
text: "",
|
|
148
|
+
mark: true
|
|
149
|
+
});
|
|
150
|
+
return result;
|
|
151
|
+
};
|
|
152
|
+
//# sourceMappingURL=text-dictation.js.map
|