@ephia/dova-sdk 1.0.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/README.md +89 -0
- package/dist/EphiaBinding-BvRmlqqC.d.ts +36 -0
- package/dist/EphiaFloatingButton-CxiF86VW.d.ts +65 -0
- package/dist/EphiaTextarea-B4_CAVUg.d.ts +183 -0
- package/dist/NativeBinding-ChG0GeSz.d.ts +53 -0
- package/dist/TargetBinding-BKGQwUMc.d.ts +89 -0
- package/dist/TiptapBinding-B-agfV2H.d.ts +45 -0
- package/dist/Transport-zdeA4Pou.d.ts +63 -0
- package/dist/audio-state-kZ3KSvux.d.ts +39 -0
- package/dist/chunk-35AJK2IO.js +1 -0
- package/dist/chunk-35AJK2IO.js.map +1 -0
- package/dist/chunk-3LXZODL4.js +886 -0
- package/dist/chunk-3LXZODL4.js.map +1 -0
- package/dist/chunk-5IK5TLSK.js +67 -0
- package/dist/chunk-5IK5TLSK.js.map +1 -0
- package/dist/chunk-7E43RY75.js +9 -0
- package/dist/chunk-7E43RY75.js.map +1 -0
- package/dist/chunk-A5UEXJ5R.js +183 -0
- package/dist/chunk-A5UEXJ5R.js.map +1 -0
- package/dist/chunk-AEE554FT.js +51 -0
- package/dist/chunk-AEE554FT.js.map +1 -0
- package/dist/chunk-DIEWY3IT.js +1332 -0
- package/dist/chunk-DIEWY3IT.js.map +1 -0
- package/dist/chunk-EGIAN7FH.js +18 -0
- package/dist/chunk-EGIAN7FH.js.map +1 -0
- package/dist/chunk-EMOEAPVU.js +486 -0
- package/dist/chunk-EMOEAPVU.js.map +1 -0
- package/dist/chunk-IDC7FHIZ.js +40 -0
- package/dist/chunk-IDC7FHIZ.js.map +1 -0
- package/dist/chunk-ITJFN3VM.js +601 -0
- package/dist/chunk-ITJFN3VM.js.map +1 -0
- package/dist/chunk-K24GNU27.js +22 -0
- package/dist/chunk-K24GNU27.js.map +1 -0
- package/dist/chunk-LXMCRXXF.js +778 -0
- package/dist/chunk-LXMCRXXF.js.map +1 -0
- package/dist/chunk-MJCEOOLW.js +122 -0
- package/dist/chunk-MJCEOOLW.js.map +1 -0
- package/dist/chunk-N7U5M3VZ.js +33 -0
- package/dist/chunk-N7U5M3VZ.js.map +1 -0
- package/dist/chunk-PSYX674B.js +27 -0
- package/dist/chunk-PSYX674B.js.map +1 -0
- package/dist/chunk-RFQRV7ML.js +33 -0
- package/dist/chunk-RFQRV7ML.js.map +1 -0
- package/dist/chunk-THNHRV2B.js +18 -0
- package/dist/chunk-THNHRV2B.js.map +1 -0
- package/dist/chunk-VSLGR64U.js +62 -0
- package/dist/chunk-VSLGR64U.js.map +1 -0
- package/dist/chunk-W2ZP674X.js +346 -0
- package/dist/chunk-W2ZP674X.js.map +1 -0
- package/dist/chunk-YWZUMUYE.js +695 -0
- package/dist/chunk-YWZUMUYE.js.map +1 -0
- package/dist/client-options-Uo6jXO8k.d.ts +64 -0
- package/dist/connection-state-Bk33YprE.d.ts +32 -0
- package/dist/core/bindings/index.d.ts +24 -0
- package/dist/core/bindings/index.js +1025 -0
- package/dist/core/bindings/index.js.map +1 -0
- package/dist/core/index.d.ts +383 -0
- package/dist/core/index.js +1284 -0
- package/dist/core/index.js.map +1 -0
- package/dist/createEphiaClient-BhdZ183V.d.ts +69 -0
- package/dist/devices/speechmike/index.d.ts +148 -0
- package/dist/devices/speechmike/index.js +40 -0
- package/dist/devices/speechmike/index.js.map +1 -0
- package/dist/headless/index.d.ts +10 -0
- package/dist/headless/index.js +25 -0
- package/dist/headless/index.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +119 -0
- package/dist/index.js.map +1 -0
- package/dist/react/index.d.ts +38 -0
- package/dist/react/index.js +70 -0
- package/dist/react/index.js.map +1 -0
- package/dist/rich-editor/index.d.ts +46 -0
- package/dist/rich-editor/index.js +13 -0
- package/dist/rich-editor/index.js.map +1 -0
- package/dist/schema-B2ycPlNB.d.ts +87 -0
- package/dist/session-APaXR48R.d.ts +12 -0
- package/dist/shared/index.d.ts +16 -0
- package/dist/shared/index.js +30 -0
- package/dist/shared/index.js.map +1 -0
- package/dist/style.css +1093 -0
- package/dist/testing/index.d.ts +84 -0
- package/dist/testing/index.js +36 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/types-D5SXPSwR.d.ts +32 -0
- package/dist/ui/index.d.ts +30 -0
- package/dist/ui/index.js +34 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/useEphiaSpeechMike-CjD7DWnh.d.ts +64 -0
- package/package.json +110 -0
- package/src/core/audio/audio-worklet-source.ts +30 -0
- package/src/core/audio/index.ts +3 -0
- package/src/core/audio/voice-level-meter.test.ts +27 -0
- package/src/core/audio/voice-level-meter.ts +270 -0
- package/src/core/bindings/EphiaBinding.ts +41 -0
- package/src/core/bindings/SegmentBindingBridge.test.ts +422 -0
- package/src/core/bindings/SegmentBindingBridge.ts +377 -0
- package/src/core/bindings/TargetBinding.ts +142 -0
- package/src/core/bindings/adapters/NativeAdapter.test.ts +85 -0
- package/src/core/bindings/adapters/NativeAdapter.ts +216 -0
- package/src/core/bindings/adapters/ProseMirrorAdapter.ts +231 -0
- package/src/core/bindings/adapters/index.ts +2 -0
- package/src/core/bindings/binding-factory.ts +78 -0
- package/src/core/bindings/detect-editor-type.ts +87 -0
- package/src/core/bindings/index.ts +13 -0
- package/src/core/bindings/insertion-boundary.test.ts +38 -0
- package/src/core/bindings/insertion-boundary.ts +26 -0
- package/src/core/bindings/native/NativeBinding.test.ts +277 -0
- package/src/core/bindings/native/NativeBinding.ts +239 -0
- package/src/core/bindings/resolver.ts +18 -0
- package/src/core/bindings/targets/codemirror.binding.ts +293 -0
- package/src/core/bindings/targets/contenteditable.binding.ts +452 -0
- package/src/core/bindings/targets/index.ts +10 -0
- package/src/core/bindings/targets/monaco.binding.ts +315 -0
- package/src/core/bindings/targets/tiptap.binding.test.ts +417 -0
- package/src/core/bindings/targets/tiptap.binding.ts +1192 -0
- package/src/core/bindings/tiptap/TiptapBinding.test.ts +63 -0
- package/src/core/bindings/tiptap/TiptapBinding.ts +464 -0
- package/src/core/bindings/types.ts +41 -0
- package/src/core/client/EphiaAudioClient.ts +654 -0
- package/src/core/client/audio-capture.ts +263 -0
- package/src/core/client/client-options.ts +39 -0
- package/src/core/client/client-state.ts +18 -0
- package/src/core/client/constants.ts +23 -0
- package/src/core/client/session-api.ts +415 -0
- package/src/core/connection/connection-state.ts +78 -0
- package/src/core/connection/index.ts +6 -0
- package/src/core/index.ts +47 -0
- package/src/core/operations/textToDocumentOperations.test.ts +69 -0
- package/src/core/operations/textToDocumentOperations.ts +92 -0
- package/src/core/runtime/DictationRuntime.test.ts +578 -0
- package/src/core/runtime/DictationRuntime.ts +434 -0
- package/src/core/runtime/TranscriptApplier.test.ts +355 -0
- package/src/core/runtime/TranscriptApplier.ts +229 -0
- package/src/core/runtime/index.ts +18 -0
- package/src/core/session/index.ts +2 -0
- package/src/core/session/session-machine.test.ts +16 -0
- package/src/core/session/session-machine.ts +59 -0
- package/src/core/targets/EditorContextCollector.ts +71 -0
- package/src/core/targets/TargetManager.test.ts +194 -0
- package/src/core/targets/TargetManager.ts +194 -0
- package/src/core/targets/index.ts +10 -0
- package/src/core/text-processing/index.ts +11 -0
- package/src/core/text-processing/overlap.test.ts +35 -0
- package/src/core/text-processing/overlap.ts +101 -0
- package/src/core/text-processing/voice-formatting.normalizer.test.ts +132 -0
- package/src/core/text-processing/voice-formatting.normalizer.ts +284 -0
- package/src/core/transcript/client-transcript.reducer.ts +366 -0
- package/src/core/transcript/client-transcript.state.ts +25 -0
- package/src/core/transcript/index.ts +19 -0
- package/src/core/transcript/transcript.assembler.test.ts +205 -0
- package/src/core/transcript/transcript.assembler.ts +152 -0
- package/src/core/transcript/transcript.reducer.test.ts +199 -0
- package/src/core/transcript/transcript.reducer.ts +771 -0
- package/src/core/transcript/transcript.state.ts +123 -0
- package/src/core/transport/LiveKitTransport.publish.test.ts +226 -0
- package/src/core/transport/LiveKitTransport.ts +459 -0
- package/src/core/transport/MockTransport.ts +231 -0
- package/src/core/transport/Transport.ts +82 -0
- package/src/debug/sdk-debug-collector.ts +79 -0
- package/src/devices/index.ts +2 -0
- package/src/devices/speechmike/__tests__/EphiaSpeechMikeProvider.test.tsx +99 -0
- package/src/devices/speechmike/__tests__/speechmike-audio-resolver.test.ts +96 -0
- package/src/devices/speechmike/__tests__/speechmike-button-router.test.ts +66 -0
- package/src/devices/speechmike/__tests__/speechmike-device-manager.test.ts +201 -0
- package/src/devices/speechmike/__tests__/speechmike-led-controller.test.ts +68 -0
- package/src/devices/speechmike/browser.ts +80 -0
- package/src/devices/speechmike/constants.ts +74 -0
- package/src/devices/speechmike/dictation-support-loader.ts +81 -0
- package/src/devices/speechmike/index.ts +11 -0
- package/src/devices/speechmike/react/EphiaSpeechMikeContext.ts +34 -0
- package/src/devices/speechmike/react/EphiaSpeechMikeProvider.tsx +287 -0
- package/src/devices/speechmike/react/useEphiaSpeechMike.ts +26 -0
- package/src/devices/speechmike/speechmike-audio-resolver.ts +58 -0
- package/src/devices/speechmike/speechmike-button-router.ts +73 -0
- package/src/devices/speechmike/speechmike-device-manager.ts +461 -0
- package/src/devices/speechmike/speechmike-led-controller.ts +78 -0
- package/src/devices/speechmike/types.ts +96 -0
- package/src/dictation_support.d.ts +31 -0
- package/src/global.d.ts +10 -0
- package/src/headless/createEphiaClient.ts +220 -0
- package/src/headless/index.ts +18 -0
- package/src/index.ts +89 -0
- package/src/react/EphiaAuto.tsx +87 -0
- package/src/react/components/EphiaDictationButton.tsx +88 -0
- package/src/react/components/EphiaStatusBar.tsx +59 -0
- package/src/react/components/EphiaTextarea.tsx +295 -0
- package/src/react/ephia-react.css +318 -0
- package/src/react/hooks/targets/index.ts +3 -0
- package/src/react/hooks/targets/useEphiaCodemirror.ts +35 -0
- package/src/react/hooks/targets/useEphiaMonaco.ts +35 -0
- package/src/react/hooks/targets/useEphiaTiptap.ts +23 -0
- package/src/react/hooks/useEphia.lifecycle.test.tsx +389 -0
- package/src/react/hooks/useEphia.ts +367 -0
- package/src/react/hooks/useEphiaDiscardTarget.ts +53 -0
- package/src/react/hooks/useEphiaServerEvent.ts +33 -0
- package/src/react/hooks/useEphiaTarget.ts +47 -0
- package/src/react/hooks/useEphiaTranscript.ts +22 -0
- package/src/react/index.ts +58 -0
- package/src/react/provider/EphiaContext.ts +63 -0
- package/src/react/provider/EphiaInternalContext.ts +32 -0
- package/src/react/provider/EphiaProvider.tsx +373 -0
- package/src/react/registry/binding-factory.ts +7 -0
- package/src/react/registry/detect-editor-type.ts +2 -0
- package/src/react/registry/events.ts +37 -0
- package/src/react/registry/registries/CodeMirrorInstanceRegistry.ts +24 -0
- package/src/react/registry/registries/MonacoInstanceRegistry.ts +23 -0
- package/src/react/registry/registries/TargetRegistry.ts +327 -0
- package/src/react/registry/registries/TiptapInstanceRegistry.ts +43 -0
- package/src/react/registry/registries/index.ts +5 -0
- package/src/react/store/create-ephia-store.ts +36 -0
- package/src/react/store/types.ts +41 -0
- package/src/react/utils/flash-range.ts +24 -0
- package/src/react/utils/index.ts +1 -0
- package/src/rich-editor/adapters/tiptap.test.ts +86 -0
- package/src/rich-editor/adapters/tiptap.ts +23 -0
- package/src/rich-editor/index.ts +3 -0
- package/src/rich-editor/types.ts +24 -0
- package/src/rich-editor/use-ephia-rich-editor.test.tsx +202 -0
- package/src/rich-editor/use-ephia-rich-editor.ts +47 -0
- package/src/shared/config/endpoint.test.ts +45 -0
- package/src/shared/config/endpoint.ts +39 -0
- package/src/shared/config/schema.ts +32 -0
- package/src/shared/effective-text.ts +13 -0
- package/src/shared/errors/EphiaSdkError.ts +54 -0
- package/src/shared/errors/messages.ts +40 -0
- package/src/shared/index.ts +27 -0
- package/src/shared/state/audio-state.ts +45 -0
- package/src/shared/state/index.ts +2 -0
- package/src/shared/store/document-store.ts +32 -0
- package/src/shared/store/index.ts +2 -0
- package/src/shared/types/editors.ts +28 -0
- package/src/shared/types/session.ts +12 -0
- package/src/style.css +2 -0
- package/src/testing/index.tsx +60 -0
- package/src/ui/assets/ephia-logo.svg +4 -0
- package/src/ui/components/EphiaLogo.tsx +77 -0
- package/src/ui/index.ts +24 -0
- package/src/ui/primitives/Button.tsx +53 -0
- package/src/ui/primitives/Spinner.tsx +21 -0
- package/src/ui/primitives/index.ts +5 -0
- package/src/ui/recorder/EphiaFloatingButton.tsx +489 -0
- package/src/ui/recorder/MinimalProcessingBars.tsx +122 -0
- package/src/ui/recorder/StandardIntensityVisualizer.tsx +148 -0
- package/src/ui/recorder/appearance.ts +9 -0
- package/src/ui/recorder/index.ts +8 -0
- package/src/ui/theme.css +775 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { NativeAdapter } from "./adapters/NativeAdapter";
|
|
5
|
+
import { SegmentBindingBridge } from "./SegmentBindingBridge";
|
|
6
|
+
import type { BindingAdapter, SegmentStatus, TextSegment } from "./types";
|
|
7
|
+
import { pickEffectiveText } from "../../shared/effective-text";
|
|
8
|
+
|
|
9
|
+
describe("SegmentBindingBridge", () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
vi.useFakeTimers();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
vi.useRealTimers();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("updates the session anchor end after final commits", () => {
|
|
19
|
+
const textarea = document.createElement("textarea");
|
|
20
|
+
document.body.appendChild(textarea);
|
|
21
|
+
|
|
22
|
+
const bridge = new SegmentBindingBridge(new NativeAdapter(textarea), textarea);
|
|
23
|
+
const anchor = bridge.beginSession();
|
|
24
|
+
|
|
25
|
+
bridge.insertPartial("s1", "\nComment ça va", 1);
|
|
26
|
+
bridge.commitFinal("s1", "\nComment ça va");
|
|
27
|
+
|
|
28
|
+
expect(textarea.value).toBe("\nComment ça va");
|
|
29
|
+
expect(anchor.end).toBe(textarea.value.length);
|
|
30
|
+
expect(anchor.end).toBeGreaterThan(anchor.initialStart);
|
|
31
|
+
|
|
32
|
+
bridge.endSession();
|
|
33
|
+
textarea.remove();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it.skip("trims an open segment after the previous segment commits — overlap trimming removed (Replace Model, backend-enforced)", () => {
|
|
37
|
+
const textarea = document.createElement("textarea");
|
|
38
|
+
document.body.appendChild(textarea);
|
|
39
|
+
|
|
40
|
+
const adapter = new NativeAdapter(textarea);
|
|
41
|
+
const bridge = new SegmentBindingBridge(adapter, textarea);
|
|
42
|
+
bridge.beginSession();
|
|
43
|
+
|
|
44
|
+
bridge.insertPartial("a", "Scanner cérébral", 0);
|
|
45
|
+
bridge.insertPartial("b", "Scanner cérébral. Indication", 0);
|
|
46
|
+
bridge.commitFinal("a", "Scanner cérébral.");
|
|
47
|
+
|
|
48
|
+
expect(textarea.value).toBe("Scanner cérébral. Indication");
|
|
49
|
+
expect(adapter.getSegmentText("b")).toBe("Indication");
|
|
50
|
+
|
|
51
|
+
bridge.endSession();
|
|
52
|
+
textarea.remove();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it.skip("trims every open overlapping segment on commit — overlap trimming removed (Replace Model, backend-enforced)", () => {
|
|
56
|
+
const textarea = document.createElement("textarea");
|
|
57
|
+
document.body.appendChild(textarea);
|
|
58
|
+
|
|
59
|
+
const adapter = new NativeAdapter(textarea);
|
|
60
|
+
const bridge = new SegmentBindingBridge(adapter, textarea);
|
|
61
|
+
bridge.beginSession();
|
|
62
|
+
|
|
63
|
+
bridge.insertPartial("a", "Scanner cérébral", 0);
|
|
64
|
+
bridge.insertPartial("b", "Scanner cérébral. Indication", 0);
|
|
65
|
+
bridge.insertPartial("c", "Scanner cérébral. Conclusion", 0);
|
|
66
|
+
bridge.commitFinal("a", "Scanner cérébral.");
|
|
67
|
+
|
|
68
|
+
expect(adapter.getSegmentText("b")).toBe("Indication");
|
|
69
|
+
expect(adapter.getSegmentText("c")).toBe("Conclusion");
|
|
70
|
+
expect(textarea.value).toBe("Scanner cérébral. Indication Conclusion");
|
|
71
|
+
|
|
72
|
+
bridge.endSession();
|
|
73
|
+
textarea.remove();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("keeps an explicit empty delta as the effective committed text", () => {
|
|
77
|
+
expect(pickEffectiveText({ text: "raw context", delta: "" })).toBe("");
|
|
78
|
+
expect(pickEffectiveText({ text: "raw context" })).toBe("raw context");
|
|
79
|
+
|
|
80
|
+
const textarea = document.createElement("textarea");
|
|
81
|
+
document.body.appendChild(textarea);
|
|
82
|
+
|
|
83
|
+
const bridge = new SegmentBindingBridge(new NativeAdapter(textarea), textarea);
|
|
84
|
+
bridge.beginSession();
|
|
85
|
+
bridge.insertPartial("a", "raw context", 0);
|
|
86
|
+
bridge.commitFinal("a", "");
|
|
87
|
+
|
|
88
|
+
expect(textarea.value).toBe("");
|
|
89
|
+
|
|
90
|
+
bridge.endSession();
|
|
91
|
+
textarea.remove();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("inserts native dictation at the cursor and keeps the suffix intact", () => {
|
|
95
|
+
const textarea = document.createElement("textarea");
|
|
96
|
+
textarea.value = "foo baz";
|
|
97
|
+
document.body.appendChild(textarea);
|
|
98
|
+
textarea.focus();
|
|
99
|
+
textarea.setSelectionRange(4, 4);
|
|
100
|
+
|
|
101
|
+
const bridge = new SegmentBindingBridge(new NativeAdapter(textarea), textarea);
|
|
102
|
+
const anchor = bridge.beginSession();
|
|
103
|
+
|
|
104
|
+
bridge.insertPartial("s1", "bar", 1);
|
|
105
|
+
vi.advanceTimersByTime(120);
|
|
106
|
+
expect(textarea.value).toBe("foo bar baz");
|
|
107
|
+
|
|
108
|
+
bridge.commitFinal("s1", "bar.");
|
|
109
|
+
expect(textarea.value).toBe("foo bar. baz");
|
|
110
|
+
expect(anchor.end).toBe("foo bar.".length);
|
|
111
|
+
|
|
112
|
+
bridge.endSession();
|
|
113
|
+
textarea.remove();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("replaces the native selection on first insert", () => {
|
|
117
|
+
const textarea = document.createElement("textarea");
|
|
118
|
+
textarea.value = "hello old world";
|
|
119
|
+
document.body.appendChild(textarea);
|
|
120
|
+
textarea.focus();
|
|
121
|
+
textarea.setSelectionRange(6, 9);
|
|
122
|
+
|
|
123
|
+
const bridge = new SegmentBindingBridge(new NativeAdapter(textarea), textarea);
|
|
124
|
+
bridge.beginSession();
|
|
125
|
+
|
|
126
|
+
bridge.insertPartial("s1", "new", 1);
|
|
127
|
+
vi.advanceTimersByTime(120);
|
|
128
|
+
expect(textarea.value).toBe("hello new world");
|
|
129
|
+
|
|
130
|
+
bridge.commitFinal("s1", "new");
|
|
131
|
+
expect(textarea.value).toBe("hello new world");
|
|
132
|
+
|
|
133
|
+
bridge.endSession();
|
|
134
|
+
textarea.remove();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("clears only native streaming segments on clearAll", () => {
|
|
138
|
+
const textarea = document.createElement("textarea");
|
|
139
|
+
document.body.appendChild(textarea);
|
|
140
|
+
|
|
141
|
+
const bridge = new SegmentBindingBridge(new NativeAdapter(textarea), textarea);
|
|
142
|
+
bridge.beginSession();
|
|
143
|
+
bridge.insertPartial("placeholder", "...", 0);
|
|
144
|
+
bridge.clearAll();
|
|
145
|
+
|
|
146
|
+
expect(textarea.value).toBe("");
|
|
147
|
+
|
|
148
|
+
bridge.insertPartial("s1", "texte final", 1);
|
|
149
|
+
bridge.commitFinal("s1", "texte final");
|
|
150
|
+
bridge.insertPartial("s2", "preview", 1);
|
|
151
|
+
bridge.clearAll();
|
|
152
|
+
|
|
153
|
+
expect(textarea.value).toBe("texte final");
|
|
154
|
+
|
|
155
|
+
bridge.endSession();
|
|
156
|
+
textarea.remove();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("skips trimming for adapters that do not expose getSegmentText", () => {
|
|
160
|
+
const adapter = new RecordingAdapter({ exposeText: false });
|
|
161
|
+
const element = document.createElement("div");
|
|
162
|
+
const bridge = new SegmentBindingBridge(adapter, element);
|
|
163
|
+
|
|
164
|
+
bridge.beginSession();
|
|
165
|
+
bridge.insertPartial("a", "Scanner cérébral", 0);
|
|
166
|
+
bridge.insertPartial("b", "Scanner cérébral. Indication", 0);
|
|
167
|
+
vi.advanceTimersByTime(120);
|
|
168
|
+
bridge.commitFinal("a", "Scanner cérébral.");
|
|
169
|
+
|
|
170
|
+
expect(adapter.getInternalText("b")).toBe("Scanner cérébral. Indication");
|
|
171
|
+
expect(adapter.updates.filter((u) => u.id === "b")).toHaveLength(0);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it.skip("uses adapter-side text when trimming another open segment — overlap trimming removed (Replace Model, backend-enforced)", () => {
|
|
175
|
+
const adapter = new RecordingAdapter();
|
|
176
|
+
const element = document.createElement("div");
|
|
177
|
+
const bridge = new SegmentBindingBridge(adapter, element);
|
|
178
|
+
|
|
179
|
+
bridge.beginSession();
|
|
180
|
+
bridge.insertPartial("a", "Bonjour docteur", 0);
|
|
181
|
+
bridge.insertPartial("b", "Bonjour docteur le monde", 0);
|
|
182
|
+
adapter.simulateManualDomText("b", "Bonjour docteur mes amis");
|
|
183
|
+
bridge.commitFinal("a", "Bonjour docteur.");
|
|
184
|
+
|
|
185
|
+
expect(adapter.getInternalText("b")).toBe("le monde");
|
|
186
|
+
expect(adapter.getDomText("b")).toBe("Bonjour docteur mes amis");
|
|
187
|
+
expect(adapter.updates.some((u) => u.id === "b" && u.text === "le monde")).toBe(true);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("does not trim a one-token overlap in the binding DOM path", () => {
|
|
191
|
+
const textarea = document.createElement("textarea");
|
|
192
|
+
document.body.appendChild(textarea);
|
|
193
|
+
|
|
194
|
+
const adapter = new NativeAdapter(textarea);
|
|
195
|
+
const bridge = new SegmentBindingBridge(adapter, textarea);
|
|
196
|
+
bridge.beginSession();
|
|
197
|
+
|
|
198
|
+
bridge.insertPartial("a", "le", 0);
|
|
199
|
+
bridge.insertPartial("b", "le chien", 0);
|
|
200
|
+
vi.advanceTimersByTime(120);
|
|
201
|
+
bridge.commitFinal("a", "le");
|
|
202
|
+
|
|
203
|
+
expect(adapter.getSegmentText("b")).toBe("le chien");
|
|
204
|
+
expect(textarea.value).toBe("le le chien");
|
|
205
|
+
|
|
206
|
+
bridge.endSession();
|
|
207
|
+
textarea.remove();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("revises a committed native segment with a later Small final", () => {
|
|
211
|
+
const textarea = document.createElement("textarea");
|
|
212
|
+
document.body.appendChild(textarea);
|
|
213
|
+
|
|
214
|
+
const bridge = new SegmentBindingBridge(new NativeAdapter(textarea), textarea);
|
|
215
|
+
bridge.beginSession();
|
|
216
|
+
|
|
217
|
+
bridge.insertPartial("s1", "bonjour", 1);
|
|
218
|
+
vi.advanceTimersByTime(120);
|
|
219
|
+
expect(textarea.value).toBe("bonjour");
|
|
220
|
+
|
|
221
|
+
bridge.commitFinal("s1", "bonjour"); // realtime final
|
|
222
|
+
expect(textarea.value).toBe("bonjour");
|
|
223
|
+
|
|
224
|
+
bridge.commitFinal("s1", "Bonjour."); // small revision
|
|
225
|
+
expect(textarea.value).toBe("Bonjour.");
|
|
226
|
+
|
|
227
|
+
bridge.endSession();
|
|
228
|
+
textarea.remove();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("ignores identical duplicate commits but still accepts later revisions", () => {
|
|
232
|
+
const textarea = document.createElement("textarea");
|
|
233
|
+
document.body.appendChild(textarea);
|
|
234
|
+
const onInput = vi.fn();
|
|
235
|
+
textarea.addEventListener("input", onInput);
|
|
236
|
+
|
|
237
|
+
const bridge = new SegmentBindingBridge(new NativeAdapter(textarea), textarea);
|
|
238
|
+
bridge.beginSession();
|
|
239
|
+
|
|
240
|
+
bridge.insertPartial("s1", "bonjour", 1);
|
|
241
|
+
vi.advanceTimersByTime(120);
|
|
242
|
+
bridge.commitFinal("s1", "bonjour");
|
|
243
|
+
expect(textarea.value).toBe("bonjour");
|
|
244
|
+
expect(onInput).toHaveBeenCalledTimes(1);
|
|
245
|
+
|
|
246
|
+
bridge.commitFinal("s1", "bonjour");
|
|
247
|
+
expect(textarea.value).toBe("bonjour");
|
|
248
|
+
expect(onInput).toHaveBeenCalledTimes(1);
|
|
249
|
+
|
|
250
|
+
bridge.commitFinal("s1", "Bonjour.");
|
|
251
|
+
expect(textarea.value).toBe("Bonjour.");
|
|
252
|
+
expect(onInput).toHaveBeenCalledTimes(2);
|
|
253
|
+
|
|
254
|
+
bridge.endSession();
|
|
255
|
+
textarea.remove();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("removes absorbed native segments idempotently on commit", () => {
|
|
259
|
+
const textarea = document.createElement("textarea");
|
|
260
|
+
document.body.appendChild(textarea);
|
|
261
|
+
|
|
262
|
+
const bridge = new SegmentBindingBridge(new NativeAdapter(textarea), textarea);
|
|
263
|
+
bridge.beginSession();
|
|
264
|
+
|
|
265
|
+
bridge.insertPartial("s1", "avec aspect inflammatoire", 1);
|
|
266
|
+
bridge.insertPartial("s2", "amincissement des parois adjacentes", 1);
|
|
267
|
+
vi.advanceTimersByTime(120);
|
|
268
|
+
expect(textarea.value).toBe("avec aspect inflammatoire amincissement des parois adjacentes");
|
|
269
|
+
|
|
270
|
+
bridge.commitFinal("s1", "avec aspect inflammatoire et amincissement des parois adjacentes.", {
|
|
271
|
+
absorbedSegmentIds: ["s2", "missing"],
|
|
272
|
+
});
|
|
273
|
+
expect(textarea.value).toBe("avec aspect inflammatoire et amincissement des parois adjacentes.");
|
|
274
|
+
|
|
275
|
+
bridge.commitFinal("s1", "avec aspect inflammatoire et amincissement des parois adjacentes.", {
|
|
276
|
+
absorbedSegmentIds: ["s2"],
|
|
277
|
+
});
|
|
278
|
+
expect(textarea.value).toBe("avec aspect inflammatoire et amincissement des parois adjacentes.");
|
|
279
|
+
|
|
280
|
+
bridge.endSession();
|
|
281
|
+
textarea.remove();
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("cancels pending native partials for absorbed segments", () => {
|
|
285
|
+
const textarea = document.createElement("textarea");
|
|
286
|
+
document.body.appendChild(textarea);
|
|
287
|
+
|
|
288
|
+
const bridge = new SegmentBindingBridge(new NativeAdapter(textarea), textarea);
|
|
289
|
+
bridge.beginSession();
|
|
290
|
+
|
|
291
|
+
bridge.insertPartial("s2", "segment absorbé", 1);
|
|
292
|
+
bridge.commitFinal("s1", "segment fusionné", { absorbedSegmentIds: ["s2"] });
|
|
293
|
+
vi.advanceTimersByTime(120);
|
|
294
|
+
|
|
295
|
+
expect(textarea.value).toBe("segment fusionné");
|
|
296
|
+
|
|
297
|
+
bridge.endSession();
|
|
298
|
+
textarea.remove();
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("debounces native partials with a 120ms default and accepts an override", () => {
|
|
302
|
+
const textarea = document.createElement("textarea");
|
|
303
|
+
document.body.appendChild(textarea);
|
|
304
|
+
|
|
305
|
+
const bridge = new SegmentBindingBridge(new NativeAdapter(textarea), textarea);
|
|
306
|
+
bridge.beginSession();
|
|
307
|
+
|
|
308
|
+
bridge.insertPartial("s1", "bo", 1);
|
|
309
|
+
bridge.insertPartial("s1", "bonjour", 2);
|
|
310
|
+
vi.advanceTimersByTime(119);
|
|
311
|
+
expect(textarea.value).toBe("");
|
|
312
|
+
|
|
313
|
+
vi.advanceTimersByTime(1);
|
|
314
|
+
expect(textarea.value).toBe("bonjour");
|
|
315
|
+
|
|
316
|
+
bridge.endSession();
|
|
317
|
+
|
|
318
|
+
const fastBridge = new SegmentBindingBridge(new NativeAdapter(textarea), textarea, {
|
|
319
|
+
partialDebounceMs: 10,
|
|
320
|
+
});
|
|
321
|
+
fastBridge.beginSession();
|
|
322
|
+
fastBridge.insertPartial("s2", " vite", 1);
|
|
323
|
+
vi.advanceTimersByTime(10);
|
|
324
|
+
|
|
325
|
+
expect(textarea.value).toBe("bonjour vite");
|
|
326
|
+
|
|
327
|
+
fastBridge.endSession();
|
|
328
|
+
textarea.remove();
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it("cancels pending native partials on clearAll, endSession, and detach", () => {
|
|
332
|
+
const textarea = document.createElement("textarea");
|
|
333
|
+
document.body.appendChild(textarea);
|
|
334
|
+
|
|
335
|
+
const bridge = new SegmentBindingBridge(new NativeAdapter(textarea), textarea);
|
|
336
|
+
bridge.beginSession();
|
|
337
|
+
bridge.insertPartial("s1", "preview", 1);
|
|
338
|
+
bridge.clearAll();
|
|
339
|
+
vi.advanceTimersByTime(120);
|
|
340
|
+
expect(textarea.value).toBe("");
|
|
341
|
+
|
|
342
|
+
bridge.insertPartial("s2", "preview", 1);
|
|
343
|
+
bridge.endSession();
|
|
344
|
+
vi.advanceTimersByTime(120);
|
|
345
|
+
expect(textarea.value).toBe("");
|
|
346
|
+
|
|
347
|
+
bridge.beginSession();
|
|
348
|
+
bridge.insertPartial("s3", "preview", 1);
|
|
349
|
+
bridge.detach();
|
|
350
|
+
vi.advanceTimersByTime(120);
|
|
351
|
+
expect(textarea.value).toBe("");
|
|
352
|
+
|
|
353
|
+
textarea.remove();
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
class RecordingAdapter implements BindingAdapter {
|
|
358
|
+
private segments = new Map<string, { text: string; status: SegmentStatus }>();
|
|
359
|
+
private domText = new Map<string, string>();
|
|
360
|
+
readonly updates: Array<{ id: string; text: string; status: SegmentStatus }> = [];
|
|
361
|
+
|
|
362
|
+
constructor(private readonly opts: { exposeText?: boolean } = {}) {
|
|
363
|
+
if (opts.exposeText === false) {
|
|
364
|
+
(this as { getSegmentText?: (id: string) => string | null }).getSegmentText = undefined;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
attach(): void {}
|
|
369
|
+
|
|
370
|
+
detach(): void {
|
|
371
|
+
this.segments.clear();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
insertSegment(seg: TextSegment): void {
|
|
375
|
+
this.segments.set(seg.id, { text: seg.text, status: seg.status });
|
|
376
|
+
this.domText.set(seg.id, seg.text);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
updateSegment(id: string, text: string, status: SegmentStatus): void {
|
|
380
|
+
const seg = this.segments.get(id);
|
|
381
|
+
if (!seg) return;
|
|
382
|
+
this.segments.set(id, { text, status });
|
|
383
|
+
this.updates.push({ id, text, status });
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
commitSegment(id: string): void {
|
|
387
|
+
this.segments.delete(id);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
removeSegment(id: string): void {
|
|
391
|
+
this.segments.delete(id);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
getSegmentText(id: string): string | null {
|
|
395
|
+
if (this.opts.exposeText === false) return null;
|
|
396
|
+
const seg = this.segments.get(id);
|
|
397
|
+
if (!seg || seg.status === "committed") return null;
|
|
398
|
+
return seg.text;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
getTextContent(): string {
|
|
402
|
+
return [...this.segments.values()].map((s) => s.text).join("");
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
getCursorOffset(): number {
|
|
406
|
+
return this.getTextContent().length;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
applyOperation(): void {}
|
|
410
|
+
|
|
411
|
+
getInternalText(id: string): string | null {
|
|
412
|
+
return this.segments.get(id)?.text ?? null;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
simulateManualDomText(id: string, text: string): void {
|
|
416
|
+
this.domText.set(id, text);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
getDomText(id: string): string | null {
|
|
420
|
+
return this.domText.get(id) ?? null;
|
|
421
|
+
}
|
|
422
|
+
}
|