@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.
Files changed (247) hide show
  1. package/README.md +89 -0
  2. package/dist/EphiaBinding-BvRmlqqC.d.ts +36 -0
  3. package/dist/EphiaFloatingButton-CxiF86VW.d.ts +65 -0
  4. package/dist/EphiaTextarea-B4_CAVUg.d.ts +183 -0
  5. package/dist/NativeBinding-ChG0GeSz.d.ts +53 -0
  6. package/dist/TargetBinding-BKGQwUMc.d.ts +89 -0
  7. package/dist/TiptapBinding-B-agfV2H.d.ts +45 -0
  8. package/dist/Transport-zdeA4Pou.d.ts +63 -0
  9. package/dist/audio-state-kZ3KSvux.d.ts +39 -0
  10. package/dist/chunk-35AJK2IO.js +1 -0
  11. package/dist/chunk-35AJK2IO.js.map +1 -0
  12. package/dist/chunk-3LXZODL4.js +886 -0
  13. package/dist/chunk-3LXZODL4.js.map +1 -0
  14. package/dist/chunk-5IK5TLSK.js +67 -0
  15. package/dist/chunk-5IK5TLSK.js.map +1 -0
  16. package/dist/chunk-7E43RY75.js +9 -0
  17. package/dist/chunk-7E43RY75.js.map +1 -0
  18. package/dist/chunk-A5UEXJ5R.js +183 -0
  19. package/dist/chunk-A5UEXJ5R.js.map +1 -0
  20. package/dist/chunk-AEE554FT.js +51 -0
  21. package/dist/chunk-AEE554FT.js.map +1 -0
  22. package/dist/chunk-DIEWY3IT.js +1332 -0
  23. package/dist/chunk-DIEWY3IT.js.map +1 -0
  24. package/dist/chunk-EGIAN7FH.js +18 -0
  25. package/dist/chunk-EGIAN7FH.js.map +1 -0
  26. package/dist/chunk-EMOEAPVU.js +486 -0
  27. package/dist/chunk-EMOEAPVU.js.map +1 -0
  28. package/dist/chunk-IDC7FHIZ.js +40 -0
  29. package/dist/chunk-IDC7FHIZ.js.map +1 -0
  30. package/dist/chunk-ITJFN3VM.js +601 -0
  31. package/dist/chunk-ITJFN3VM.js.map +1 -0
  32. package/dist/chunk-K24GNU27.js +22 -0
  33. package/dist/chunk-K24GNU27.js.map +1 -0
  34. package/dist/chunk-LXMCRXXF.js +778 -0
  35. package/dist/chunk-LXMCRXXF.js.map +1 -0
  36. package/dist/chunk-MJCEOOLW.js +122 -0
  37. package/dist/chunk-MJCEOOLW.js.map +1 -0
  38. package/dist/chunk-N7U5M3VZ.js +33 -0
  39. package/dist/chunk-N7U5M3VZ.js.map +1 -0
  40. package/dist/chunk-PSYX674B.js +27 -0
  41. package/dist/chunk-PSYX674B.js.map +1 -0
  42. package/dist/chunk-RFQRV7ML.js +33 -0
  43. package/dist/chunk-RFQRV7ML.js.map +1 -0
  44. package/dist/chunk-THNHRV2B.js +18 -0
  45. package/dist/chunk-THNHRV2B.js.map +1 -0
  46. package/dist/chunk-VSLGR64U.js +62 -0
  47. package/dist/chunk-VSLGR64U.js.map +1 -0
  48. package/dist/chunk-W2ZP674X.js +346 -0
  49. package/dist/chunk-W2ZP674X.js.map +1 -0
  50. package/dist/chunk-YWZUMUYE.js +695 -0
  51. package/dist/chunk-YWZUMUYE.js.map +1 -0
  52. package/dist/client-options-Uo6jXO8k.d.ts +64 -0
  53. package/dist/connection-state-Bk33YprE.d.ts +32 -0
  54. package/dist/core/bindings/index.d.ts +24 -0
  55. package/dist/core/bindings/index.js +1025 -0
  56. package/dist/core/bindings/index.js.map +1 -0
  57. package/dist/core/index.d.ts +383 -0
  58. package/dist/core/index.js +1284 -0
  59. package/dist/core/index.js.map +1 -0
  60. package/dist/createEphiaClient-BhdZ183V.d.ts +69 -0
  61. package/dist/devices/speechmike/index.d.ts +148 -0
  62. package/dist/devices/speechmike/index.js +40 -0
  63. package/dist/devices/speechmike/index.js.map +1 -0
  64. package/dist/headless/index.d.ts +10 -0
  65. package/dist/headless/index.js +25 -0
  66. package/dist/headless/index.js.map +1 -0
  67. package/dist/index.d.ts +18 -0
  68. package/dist/index.js +119 -0
  69. package/dist/index.js.map +1 -0
  70. package/dist/react/index.d.ts +38 -0
  71. package/dist/react/index.js +70 -0
  72. package/dist/react/index.js.map +1 -0
  73. package/dist/rich-editor/index.d.ts +46 -0
  74. package/dist/rich-editor/index.js +13 -0
  75. package/dist/rich-editor/index.js.map +1 -0
  76. package/dist/schema-B2ycPlNB.d.ts +87 -0
  77. package/dist/session-APaXR48R.d.ts +12 -0
  78. package/dist/shared/index.d.ts +16 -0
  79. package/dist/shared/index.js +30 -0
  80. package/dist/shared/index.js.map +1 -0
  81. package/dist/style.css +1093 -0
  82. package/dist/testing/index.d.ts +84 -0
  83. package/dist/testing/index.js +36 -0
  84. package/dist/testing/index.js.map +1 -0
  85. package/dist/types-D5SXPSwR.d.ts +32 -0
  86. package/dist/ui/index.d.ts +30 -0
  87. package/dist/ui/index.js +34 -0
  88. package/dist/ui/index.js.map +1 -0
  89. package/dist/useEphiaSpeechMike-CjD7DWnh.d.ts +64 -0
  90. package/package.json +110 -0
  91. package/src/core/audio/audio-worklet-source.ts +30 -0
  92. package/src/core/audio/index.ts +3 -0
  93. package/src/core/audio/voice-level-meter.test.ts +27 -0
  94. package/src/core/audio/voice-level-meter.ts +270 -0
  95. package/src/core/bindings/EphiaBinding.ts +41 -0
  96. package/src/core/bindings/SegmentBindingBridge.test.ts +422 -0
  97. package/src/core/bindings/SegmentBindingBridge.ts +377 -0
  98. package/src/core/bindings/TargetBinding.ts +142 -0
  99. package/src/core/bindings/adapters/NativeAdapter.test.ts +85 -0
  100. package/src/core/bindings/adapters/NativeAdapter.ts +216 -0
  101. package/src/core/bindings/adapters/ProseMirrorAdapter.ts +231 -0
  102. package/src/core/bindings/adapters/index.ts +2 -0
  103. package/src/core/bindings/binding-factory.ts +78 -0
  104. package/src/core/bindings/detect-editor-type.ts +87 -0
  105. package/src/core/bindings/index.ts +13 -0
  106. package/src/core/bindings/insertion-boundary.test.ts +38 -0
  107. package/src/core/bindings/insertion-boundary.ts +26 -0
  108. package/src/core/bindings/native/NativeBinding.test.ts +277 -0
  109. package/src/core/bindings/native/NativeBinding.ts +239 -0
  110. package/src/core/bindings/resolver.ts +18 -0
  111. package/src/core/bindings/targets/codemirror.binding.ts +293 -0
  112. package/src/core/bindings/targets/contenteditable.binding.ts +452 -0
  113. package/src/core/bindings/targets/index.ts +10 -0
  114. package/src/core/bindings/targets/monaco.binding.ts +315 -0
  115. package/src/core/bindings/targets/tiptap.binding.test.ts +417 -0
  116. package/src/core/bindings/targets/tiptap.binding.ts +1192 -0
  117. package/src/core/bindings/tiptap/TiptapBinding.test.ts +63 -0
  118. package/src/core/bindings/tiptap/TiptapBinding.ts +464 -0
  119. package/src/core/bindings/types.ts +41 -0
  120. package/src/core/client/EphiaAudioClient.ts +654 -0
  121. package/src/core/client/audio-capture.ts +263 -0
  122. package/src/core/client/client-options.ts +39 -0
  123. package/src/core/client/client-state.ts +18 -0
  124. package/src/core/client/constants.ts +23 -0
  125. package/src/core/client/session-api.ts +415 -0
  126. package/src/core/connection/connection-state.ts +78 -0
  127. package/src/core/connection/index.ts +6 -0
  128. package/src/core/index.ts +47 -0
  129. package/src/core/operations/textToDocumentOperations.test.ts +69 -0
  130. package/src/core/operations/textToDocumentOperations.ts +92 -0
  131. package/src/core/runtime/DictationRuntime.test.ts +578 -0
  132. package/src/core/runtime/DictationRuntime.ts +434 -0
  133. package/src/core/runtime/TranscriptApplier.test.ts +355 -0
  134. package/src/core/runtime/TranscriptApplier.ts +229 -0
  135. package/src/core/runtime/index.ts +18 -0
  136. package/src/core/session/index.ts +2 -0
  137. package/src/core/session/session-machine.test.ts +16 -0
  138. package/src/core/session/session-machine.ts +59 -0
  139. package/src/core/targets/EditorContextCollector.ts +71 -0
  140. package/src/core/targets/TargetManager.test.ts +194 -0
  141. package/src/core/targets/TargetManager.ts +194 -0
  142. package/src/core/targets/index.ts +10 -0
  143. package/src/core/text-processing/index.ts +11 -0
  144. package/src/core/text-processing/overlap.test.ts +35 -0
  145. package/src/core/text-processing/overlap.ts +101 -0
  146. package/src/core/text-processing/voice-formatting.normalizer.test.ts +132 -0
  147. package/src/core/text-processing/voice-formatting.normalizer.ts +284 -0
  148. package/src/core/transcript/client-transcript.reducer.ts +366 -0
  149. package/src/core/transcript/client-transcript.state.ts +25 -0
  150. package/src/core/transcript/index.ts +19 -0
  151. package/src/core/transcript/transcript.assembler.test.ts +205 -0
  152. package/src/core/transcript/transcript.assembler.ts +152 -0
  153. package/src/core/transcript/transcript.reducer.test.ts +199 -0
  154. package/src/core/transcript/transcript.reducer.ts +771 -0
  155. package/src/core/transcript/transcript.state.ts +123 -0
  156. package/src/core/transport/LiveKitTransport.publish.test.ts +226 -0
  157. package/src/core/transport/LiveKitTransport.ts +459 -0
  158. package/src/core/transport/MockTransport.ts +231 -0
  159. package/src/core/transport/Transport.ts +82 -0
  160. package/src/debug/sdk-debug-collector.ts +79 -0
  161. package/src/devices/index.ts +2 -0
  162. package/src/devices/speechmike/__tests__/EphiaSpeechMikeProvider.test.tsx +99 -0
  163. package/src/devices/speechmike/__tests__/speechmike-audio-resolver.test.ts +96 -0
  164. package/src/devices/speechmike/__tests__/speechmike-button-router.test.ts +66 -0
  165. package/src/devices/speechmike/__tests__/speechmike-device-manager.test.ts +201 -0
  166. package/src/devices/speechmike/__tests__/speechmike-led-controller.test.ts +68 -0
  167. package/src/devices/speechmike/browser.ts +80 -0
  168. package/src/devices/speechmike/constants.ts +74 -0
  169. package/src/devices/speechmike/dictation-support-loader.ts +81 -0
  170. package/src/devices/speechmike/index.ts +11 -0
  171. package/src/devices/speechmike/react/EphiaSpeechMikeContext.ts +34 -0
  172. package/src/devices/speechmike/react/EphiaSpeechMikeProvider.tsx +287 -0
  173. package/src/devices/speechmike/react/useEphiaSpeechMike.ts +26 -0
  174. package/src/devices/speechmike/speechmike-audio-resolver.ts +58 -0
  175. package/src/devices/speechmike/speechmike-button-router.ts +73 -0
  176. package/src/devices/speechmike/speechmike-device-manager.ts +461 -0
  177. package/src/devices/speechmike/speechmike-led-controller.ts +78 -0
  178. package/src/devices/speechmike/types.ts +96 -0
  179. package/src/dictation_support.d.ts +31 -0
  180. package/src/global.d.ts +10 -0
  181. package/src/headless/createEphiaClient.ts +220 -0
  182. package/src/headless/index.ts +18 -0
  183. package/src/index.ts +89 -0
  184. package/src/react/EphiaAuto.tsx +87 -0
  185. package/src/react/components/EphiaDictationButton.tsx +88 -0
  186. package/src/react/components/EphiaStatusBar.tsx +59 -0
  187. package/src/react/components/EphiaTextarea.tsx +295 -0
  188. package/src/react/ephia-react.css +318 -0
  189. package/src/react/hooks/targets/index.ts +3 -0
  190. package/src/react/hooks/targets/useEphiaCodemirror.ts +35 -0
  191. package/src/react/hooks/targets/useEphiaMonaco.ts +35 -0
  192. package/src/react/hooks/targets/useEphiaTiptap.ts +23 -0
  193. package/src/react/hooks/useEphia.lifecycle.test.tsx +389 -0
  194. package/src/react/hooks/useEphia.ts +367 -0
  195. package/src/react/hooks/useEphiaDiscardTarget.ts +53 -0
  196. package/src/react/hooks/useEphiaServerEvent.ts +33 -0
  197. package/src/react/hooks/useEphiaTarget.ts +47 -0
  198. package/src/react/hooks/useEphiaTranscript.ts +22 -0
  199. package/src/react/index.ts +58 -0
  200. package/src/react/provider/EphiaContext.ts +63 -0
  201. package/src/react/provider/EphiaInternalContext.ts +32 -0
  202. package/src/react/provider/EphiaProvider.tsx +373 -0
  203. package/src/react/registry/binding-factory.ts +7 -0
  204. package/src/react/registry/detect-editor-type.ts +2 -0
  205. package/src/react/registry/events.ts +37 -0
  206. package/src/react/registry/registries/CodeMirrorInstanceRegistry.ts +24 -0
  207. package/src/react/registry/registries/MonacoInstanceRegistry.ts +23 -0
  208. package/src/react/registry/registries/TargetRegistry.ts +327 -0
  209. package/src/react/registry/registries/TiptapInstanceRegistry.ts +43 -0
  210. package/src/react/registry/registries/index.ts +5 -0
  211. package/src/react/store/create-ephia-store.ts +36 -0
  212. package/src/react/store/types.ts +41 -0
  213. package/src/react/utils/flash-range.ts +24 -0
  214. package/src/react/utils/index.ts +1 -0
  215. package/src/rich-editor/adapters/tiptap.test.ts +86 -0
  216. package/src/rich-editor/adapters/tiptap.ts +23 -0
  217. package/src/rich-editor/index.ts +3 -0
  218. package/src/rich-editor/types.ts +24 -0
  219. package/src/rich-editor/use-ephia-rich-editor.test.tsx +202 -0
  220. package/src/rich-editor/use-ephia-rich-editor.ts +47 -0
  221. package/src/shared/config/endpoint.test.ts +45 -0
  222. package/src/shared/config/endpoint.ts +39 -0
  223. package/src/shared/config/schema.ts +32 -0
  224. package/src/shared/effective-text.ts +13 -0
  225. package/src/shared/errors/EphiaSdkError.ts +54 -0
  226. package/src/shared/errors/messages.ts +40 -0
  227. package/src/shared/index.ts +27 -0
  228. package/src/shared/state/audio-state.ts +45 -0
  229. package/src/shared/state/index.ts +2 -0
  230. package/src/shared/store/document-store.ts +32 -0
  231. package/src/shared/store/index.ts +2 -0
  232. package/src/shared/types/editors.ts +28 -0
  233. package/src/shared/types/session.ts +12 -0
  234. package/src/style.css +2 -0
  235. package/src/testing/index.tsx +60 -0
  236. package/src/ui/assets/ephia-logo.svg +4 -0
  237. package/src/ui/components/EphiaLogo.tsx +77 -0
  238. package/src/ui/index.ts +24 -0
  239. package/src/ui/primitives/Button.tsx +53 -0
  240. package/src/ui/primitives/Spinner.tsx +21 -0
  241. package/src/ui/primitives/index.ts +5 -0
  242. package/src/ui/recorder/EphiaFloatingButton.tsx +489 -0
  243. package/src/ui/recorder/MinimalProcessingBars.tsx +122 -0
  244. package/src/ui/recorder/StandardIntensityVisualizer.tsx +148 -0
  245. package/src/ui/recorder/appearance.ts +9 -0
  246. package/src/ui/recorder/index.ts +8 -0
  247. package/src/ui/theme.css +775 -0
@@ -0,0 +1,1025 @@
1
+ import {
2
+ NativeBinding
3
+ } from "../../chunk-A5UEXJ5R.js";
4
+ import {
5
+ EphiaV2CommittedMark,
6
+ EphiaV2PreviewMark,
7
+ TiptapBinding
8
+ } from "../../chunk-W2ZP674X.js";
9
+ import {
10
+ textToDocumentOperations
11
+ } from "../../chunk-VSLGR64U.js";
12
+
13
+ // src/core/bindings/TargetBinding.ts
14
+ function pickJoiner(before, next) {
15
+ if (!before || !next) return "";
16
+ const lastChar = before[before.length - 1];
17
+ const firstChar = next[0];
18
+ if (/\s/.test(lastChar)) return "";
19
+ if (/\s/.test(firstChar)) return "";
20
+ if (/^[,.;:!?)\]]/.test(firstChar)) return "";
21
+ if (/[(\[]/.test(lastChar)) return "";
22
+ return " ";
23
+ }
24
+
25
+ // src/core/bindings/targets/tiptap.binding.ts
26
+ import { Mark } from "@tiptap/core";
27
+ import { Plugin, PluginKey, TextSelection } from "@tiptap/pm/state";
28
+
29
+ // src/shared/store/document-store.ts
30
+ import { create } from "zustand";
31
+ import { subscribeWithSelector } from "zustand/middleware";
32
+ var documentStore = create()(
33
+ subscribeWithSelector((set) => ({
34
+ content: "",
35
+ segments: [],
36
+ selectedText: null,
37
+ cursorSection: null,
38
+ syncFromEditor: (content, segments) => set({ content, segments }),
39
+ setSelection: (text, section) => set({ selectedText: text, cursorSection: section })
40
+ }))
41
+ );
42
+
43
+ // src/core/text-processing/overlap.ts
44
+ var TOKEN_RE = /\S+/g;
45
+ var PUNCT_EDGE_RE = /^[.,;:!?"'«»“”‘’()[\]{}]+|[.,;:!?"'«»“”‘’()[\]{}]+$/g;
46
+ var ELISION_RE = /^(?:qu['’]|[dlsnmtcj]['’])/i;
47
+ function normalizeToken(token) {
48
+ return token.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(PUNCT_EDGE_RE, "").replace(ELISION_RE, "");
49
+ }
50
+ function tokenize(text) {
51
+ const out = [];
52
+ for (const match of text.matchAll(TOKEN_RE)) {
53
+ const raw = match[0];
54
+ const start = match.index ?? 0;
55
+ const norm = normalizeToken(raw);
56
+ if (!norm) continue;
57
+ out.push({ raw, norm, start, end: start + raw.length });
58
+ }
59
+ return out;
60
+ }
61
+ function prefixCharsForNormalizedPrefix(raw, normalizedPrefix) {
62
+ if (!normalizedPrefix) return 0;
63
+ for (let i = 1; i <= raw.length; i++) {
64
+ const norm = normalizeToken(raw.slice(0, i));
65
+ if (norm.length >= normalizedPrefix.length && norm.startsWith(normalizedPrefix)) {
66
+ return i;
67
+ }
68
+ }
69
+ return 0;
70
+ }
71
+ function stripLeadingOverlapFromTextWithInfo(before, incoming, maxTokens = 24) {
72
+ const left = tokenize(before);
73
+ const right = tokenize(incoming);
74
+ if (!left.length || !right.length) {
75
+ return { text: incoming, partialWord: false, overlapTokens: 0 };
76
+ }
77
+ const limit = Math.min(left.length, right.length, maxTokens);
78
+ for (let size = limit; size > 0; size--) {
79
+ const leftSlice = left.slice(left.length - size);
80
+ const rightSlice = right.slice(0, size);
81
+ let partialLast = false;
82
+ let matches = true;
83
+ for (let i = 0; i < size; i++) {
84
+ const l = leftSlice[i].norm;
85
+ const r = rightSlice[i].norm;
86
+ if (l === r) continue;
87
+ if (i === size - 1 && l.length >= 3 && r.startsWith(l)) {
88
+ partialLast = true;
89
+ continue;
90
+ }
91
+ matches = false;
92
+ break;
93
+ }
94
+ if (!matches) continue;
95
+ const lastRight = rightSlice[rightSlice.length - 1];
96
+ let cut = lastRight.end;
97
+ if (partialLast) {
98
+ cut = lastRight.start + prefixCharsForNormalizedPrefix(lastRight.raw, leftSlice[size - 1].norm);
99
+ }
100
+ return {
101
+ text: incoming.slice(cut).replace(/^[ \t]+/, ""),
102
+ partialWord: partialLast,
103
+ overlapTokens: size
104
+ };
105
+ }
106
+ return { text: incoming, partialWord: false, overlapTokens: 0 };
107
+ }
108
+
109
+ // src/core/bindings/targets/tiptap.binding.ts
110
+ var ephiaTiptapKey = new PluginKey("ephiaTiptap");
111
+ function createEphiaTiptapPlugin() {
112
+ return new Plugin({
113
+ key: ephiaTiptapKey,
114
+ state: {
115
+ init() {
116
+ return { segments: {}, anchor: null };
117
+ },
118
+ apply(tr, value) {
119
+ const docSize = tr.doc.content.size;
120
+ const mappedSegments = {};
121
+ for (const [id, pos] of Object.entries(value.segments)) {
122
+ const mappedFrom = Math.min(Math.max(tr.mapping.map(pos.from), 0), docSize);
123
+ const mappedTo = Math.min(Math.max(tr.mapping.map(pos.to), 0), docSize);
124
+ if (mappedFrom < 0 || mappedTo < 0 || mappedFrom > docSize || mappedTo > docSize || mappedFrom > mappedTo) {
125
+ continue;
126
+ }
127
+ mappedSegments[id] = {
128
+ from: mappedFrom,
129
+ to: mappedTo,
130
+ revision: pos.revision,
131
+ joinerLength: pos.joinerLength
132
+ };
133
+ }
134
+ let mappedAnchor = value.anchor ? {
135
+ initialStart: Math.min(Math.max(tr.mapping.map(value.anchor.initialStart), 0), docSize),
136
+ initialEnd: Math.min(Math.max(tr.mapping.map(value.anchor.initialEnd), 0), docSize),
137
+ hadSelection: value.anchor.hadSelection,
138
+ end: Math.min(Math.max(tr.mapping.map(value.anchor.end), 0), docSize),
139
+ selectionPendingDelete: value.anchor.selectionPendingDelete
140
+ } : null;
141
+ if (mappedAnchor && (mappedAnchor.initialStart > docSize || mappedAnchor.initialEnd > docSize || mappedAnchor.end > docSize)) {
142
+ mappedAnchor = null;
143
+ }
144
+ const meta = tr.getMeta(ephiaTiptapKey);
145
+ if (meta) {
146
+ if (meta.segments) {
147
+ for (const [id, pos] of Object.entries(meta.segments)) {
148
+ if (pos === null) delete mappedSegments[id];
149
+ else mappedSegments[id] = pos;
150
+ }
151
+ }
152
+ if (meta.anchor) {
153
+ if (meta.anchor.kind === "clear") {
154
+ mappedAnchor = null;
155
+ } else if (meta.anchor.kind === "set") {
156
+ mappedAnchor = meta.anchor.value;
157
+ } else if (meta.anchor.kind === "patch" && mappedAnchor) {
158
+ mappedAnchor = {
159
+ ...mappedAnchor,
160
+ end: meta.anchor.end ?? mappedAnchor.end
161
+ };
162
+ }
163
+ }
164
+ }
165
+ return { segments: mappedSegments, anchor: mappedAnchor };
166
+ }
167
+ }
168
+ });
169
+ }
170
+ function getPluginState(editor) {
171
+ return ephiaTiptapKey.getState(editor.state) ?? { segments: {}, anchor: null };
172
+ }
173
+ function setSegmentMeta(tr, segmentId, pos) {
174
+ const current = tr.getMeta(ephiaTiptapKey) ?? {};
175
+ current.segments = { ...current.segments ?? {}, [segmentId]: pos };
176
+ tr.setMeta(ephiaTiptapKey, current);
177
+ }
178
+ function setAnchorMeta(tr, update) {
179
+ const current = tr.getMeta(ephiaTiptapKey) ?? {};
180
+ current.anchor = update;
181
+ tr.setMeta(ephiaTiptapKey, current);
182
+ }
183
+ function getCommonPrefix(a, b) {
184
+ let i = 0;
185
+ while (i < a.length && i < b.length && a[i] === b[i]) i++;
186
+ return i;
187
+ }
188
+ function getCommonSuffix(a, b, prefixLen) {
189
+ let i = 0;
190
+ while (i < a.length - prefixLen && i < b.length - prefixLen && a[a.length - 1 - i] === b[b.length - 1 - i]) {
191
+ i++;
192
+ }
193
+ return i;
194
+ }
195
+ var SCROLL_THROTTLE_MS = 300;
196
+ var USER_SCROLL_QUIET_MS = 500;
197
+ var PARTIAL_DEBOUNCE_MS = 60;
198
+ var SKIP_STORE_SYNC_META = "ephiaSkipStoreSync";
199
+ function isPosVisible(editor, pos) {
200
+ try {
201
+ const coords = editor.view.coordsAtPos(pos);
202
+ const rect = editor.view.dom.getBoundingClientRect();
203
+ return coords.top >= rect.top && coords.bottom <= rect.bottom;
204
+ } catch {
205
+ return true;
206
+ }
207
+ }
208
+ function textBetweenWithHardBreaks(doc, from, to, blockSeparator) {
209
+ return doc.textBetween(from, to, blockSeparator, "\n");
210
+ }
211
+ function getTextFromDoc(doc) {
212
+ return textBetweenWithHardBreaks(doc, 0, doc.content.size, "\n\n");
213
+ }
214
+ function resolveInsertPosition(editor, position) {
215
+ if (typeof position === "number") {
216
+ return Math.min(Math.max(position, 0), editor.state.doc.content.size);
217
+ }
218
+ if (position === "start") return 1;
219
+ if (position === "end") return Math.max(1, editor.state.doc.content.size - 1);
220
+ return editor.state.selection.from;
221
+ }
222
+ function insertOperationsAt(editor, tr, pos, operations, mark) {
223
+ let cursor = pos;
224
+ const fallbackSteps = [];
225
+ const hardBreak = editor.schema.nodes.hardBreak;
226
+ const marks = mark ? [mark] : [];
227
+ const insertText = (text) => {
228
+ if (!text) return;
229
+ const node = editor.schema.text(text, marks);
230
+ tr.insert(cursor, node);
231
+ cursor += node.nodeSize;
232
+ };
233
+ const insertHardBreakOrFallback = () => {
234
+ if (hardBreak) {
235
+ const node2 = hardBreak.create();
236
+ tr.insert(cursor, node2);
237
+ cursor += node2.nodeSize;
238
+ return;
239
+ }
240
+ const node = editor.schema.text("\n", marks);
241
+ tr.insert(cursor, node);
242
+ cursor += node.nodeSize;
243
+ fallbackSteps.push("fallback_plain_newline_no_hardbreak_node");
244
+ };
245
+ for (const operation of operations) {
246
+ if (operation.type === "insert_text" || operation.type === "insert") {
247
+ insertText(operation.text);
248
+ } else if (operation.type === "line_break") {
249
+ insertHardBreakOrFallback();
250
+ } else if (operation.type === "paragraph_break") {
251
+ insertHardBreakOrFallback();
252
+ insertHardBreakOrFallback();
253
+ }
254
+ }
255
+ return { from: pos, to: cursor, fallbackSteps };
256
+ }
257
+ function measureOperationsSize(editor, operations) {
258
+ const hardBreak = editor.schema.nodes.hardBreak;
259
+ let size = 0;
260
+ for (const operation of operations) {
261
+ if (operation.type === "insert_text" || operation.type === "insert") {
262
+ if (operation.text) {
263
+ size += editor.schema.text(operation.text).nodeSize;
264
+ }
265
+ } else if (operation.type === "line_break") {
266
+ size += hardBreak ? hardBreak.create().nodeSize : editor.schema.text("\n").nodeSize;
267
+ } else if (operation.type === "paragraph_break") {
268
+ const breakSize = hardBreak ? hardBreak.create().nodeSize : editor.schema.text("\n").nodeSize;
269
+ size += breakSize * 2;
270
+ }
271
+ }
272
+ return size;
273
+ }
274
+ function measureTextAsOperationsSize(editor, text) {
275
+ return measureOperationsSize(editor, textToDocumentOperations(text));
276
+ }
277
+ function parseSegmentsFromDoc(doc) {
278
+ const segments = [];
279
+ let currentSection = null;
280
+ let offset = 0;
281
+ doc.descendants((node, _pos) => {
282
+ if (node.type.name === "heading") {
283
+ if (currentSection) currentSection.end = offset;
284
+ currentSection = {
285
+ id: `sec_${segments.length}`,
286
+ title: node.textContent,
287
+ text: "",
288
+ level: node.attrs.level || 1,
289
+ start: offset,
290
+ end: offset
291
+ };
292
+ segments.push(currentSection);
293
+ } else if (currentSection && node.isTextblock) {
294
+ currentSection.text += (currentSection.text ? "\n" : "") + node.textContent;
295
+ }
296
+ offset += node.nodeSize;
297
+ return true;
298
+ });
299
+ if (currentSection) {
300
+ currentSection.end = offset;
301
+ }
302
+ return segments;
303
+ }
304
+ function parseSegmentsFromEditor(editor) {
305
+ return parseSegmentsFromDoc(editor.state.doc);
306
+ }
307
+ function toPublicAnchor(anchor) {
308
+ return {
309
+ initialStart: anchor.initialStart,
310
+ initialEnd: anchor.initialEnd,
311
+ hadSelection: anchor.hadSelection,
312
+ end: anchor.end,
313
+ selectionPendingDelete: anchor.selectionPendingDelete
314
+ };
315
+ }
316
+ var EphiaPreviewMark = Mark.create({
317
+ name: "ephiaPreview",
318
+ parseHTML() {
319
+ return [{ tag: "span[data-ephia-streaming]" }];
320
+ },
321
+ renderHTML() {
322
+ return [
323
+ "span",
324
+ {
325
+ "data-ephia-streaming": "true",
326
+ class: "ephia-text--streaming"
327
+ },
328
+ 0
329
+ ];
330
+ }
331
+ });
332
+ var EphiaCommittedMark = Mark.create({
333
+ name: "ephiaCommitted",
334
+ parseHTML() {
335
+ return [{ tag: "span[data-ephia-committed]" }];
336
+ },
337
+ renderHTML() {
338
+ return [
339
+ "span",
340
+ {
341
+ "data-ephia-committed": "true",
342
+ class: "ephia-text--committed"
343
+ },
344
+ 0
345
+ ];
346
+ }
347
+ });
348
+ var EphiaRevisedMark = Mark.create({
349
+ name: "ephiaRevised",
350
+ parseHTML() {
351
+ return [{ tag: "span[data-ephia-revised]" }];
352
+ },
353
+ renderHTML() {
354
+ return [
355
+ "span",
356
+ {
357
+ "data-ephia-revised": "true",
358
+ class: "ephia-text--revised"
359
+ },
360
+ 0
361
+ ];
362
+ }
363
+ });
364
+ var EphiaPlaceholderMark = Mark.create({
365
+ name: "ephiaPlaceholder",
366
+ parseHTML() {
367
+ return [{ tag: "span[data-ephia-placeholder]" }];
368
+ },
369
+ renderHTML() {
370
+ return [
371
+ "span",
372
+ {
373
+ "data-ephia-placeholder": "true",
374
+ class: "ephia-text--placeholder"
375
+ },
376
+ 0
377
+ ];
378
+ }
379
+ });
380
+ function createTiptapBinding(editor, wrapperEl) {
381
+ const plugin = createEphiaTiptapPlugin();
382
+ editor.registerPlugin(plugin);
383
+ let lastScrollTime = 0;
384
+ let lastUserScrollTime = 0;
385
+ let lastDictationEnd = null;
386
+ let cachedSectionsDoc = editor.state.doc;
387
+ let cachedSections = parseSegmentsFromEditor(editor);
388
+ const markUserScroll = () => {
389
+ lastUserScrollTime = Date.now();
390
+ };
391
+ const maybeScrollIntoView = (tr, ed, pos) => {
392
+ const now = Date.now();
393
+ if (now - lastUserScrollTime < USER_SCROLL_QUIET_MS) return;
394
+ if (now - lastScrollTime < SCROLL_THROTTLE_MS) return;
395
+ if (isPosVisible(ed, pos)) return;
396
+ tr.scrollIntoView();
397
+ lastScrollTime = now;
398
+ };
399
+ editor.view.dom.addEventListener("scroll", markUserScroll, { passive: true });
400
+ const committedTimeouts = /* @__PURE__ */ new Map();
401
+ const knownSegmentIds = /* @__PURE__ */ new Set();
402
+ const clearCommittedTimeout = (segmentId) => {
403
+ const timeoutId = committedTimeouts.get(segmentId);
404
+ if (timeoutId !== void 0) {
405
+ window.clearTimeout(timeoutId);
406
+ committedTimeouts.delete(segmentId);
407
+ }
408
+ };
409
+ let syncRaf = null;
410
+ let pendingSyncDoc = null;
411
+ const getCachedSections = () => cachedSections;
412
+ const syncToStore = (event) => {
413
+ if (event?.transaction?.getMeta(SKIP_STORE_SYNC_META)) return;
414
+ pendingSyncDoc = event?.transaction?.doc ?? editor.state.doc;
415
+ if (syncRaf !== null) return;
416
+ syncRaf = (typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame : (cb) => setTimeout(cb, 16))(
417
+ () => {
418
+ syncRaf = null;
419
+ const doc = pendingSyncDoc ?? editor.state.doc;
420
+ pendingSyncDoc = null;
421
+ const text = getTextFromDoc(doc);
422
+ cachedSectionsDoc = doc;
423
+ cachedSections = parseSegmentsFromDoc(doc);
424
+ const segments = cachedSections;
425
+ documentStore.getState().syncFromEditor(text, segments);
426
+ }
427
+ );
428
+ };
429
+ const syncSelection = () => {
430
+ const { from, to } = editor.state.selection;
431
+ const selectedText = from !== to ? textBetweenWithHardBreaks(editor.state.doc, from, to) : null;
432
+ if (cachedSectionsDoc !== editor.state.doc && cachedSections.length === 0) {
433
+ cachedSectionsDoc = editor.state.doc;
434
+ cachedSections = parseSegmentsFromEditor(editor);
435
+ }
436
+ const cursor = editor.state.selection.from;
437
+ const section = getCachedSections().find((seg) => cursor >= seg.start && cursor <= seg.end)?.title ?? null;
438
+ documentStore.getState().setSelection(selectedText, section);
439
+ };
440
+ editor.on("update", syncToStore);
441
+ editor.on("selectionUpdate", syncSelection);
442
+ syncToStore();
443
+ syncSelection();
444
+ const _applyPartial = (segmentId, text, revision) => {
445
+ const state = getPluginState(editor);
446
+ const existing = state.segments[segmentId];
447
+ const isPlaceholder = segmentId === "ephia-startup-placeholder";
448
+ const mark = isPlaceholder ? editor.schema.marks.ephiaPlaceholder?.create() : editor.schema.marks.ephiaPreview?.create();
449
+ if (existing) {
450
+ if (revision <= existing.revision) return;
451
+ const oldText = textBetweenWithHardBreaks(editor.state.doc, existing.from, existing.to);
452
+ if (oldText === text) return;
453
+ const tr = editor.state.tr;
454
+ tr.setMeta("addToHistory", false);
455
+ tr.setMeta(SKIP_STORE_SYNC_META, true);
456
+ const prefixLen = getCommonPrefix(oldText, text);
457
+ const suffixLen = getCommonSuffix(oldText, text, prefixLen);
458
+ if (prefixLen > 0 || suffixLen > 0) {
459
+ const deleteFrom = existing.from + prefixLen;
460
+ const deleteTo = existing.to - suffixLen;
461
+ const insertText = text.slice(prefixLen, text.length - suffixLen);
462
+ if (deleteFrom < deleteTo) tr.delete(deleteFrom, deleteTo);
463
+ if (insertText) {
464
+ insertOperationsAt(
465
+ editor,
466
+ tr,
467
+ deleteFrom,
468
+ textToDocumentOperations(insertText),
469
+ mark
470
+ );
471
+ }
472
+ const newEnd = existing.from + measureTextAsOperationsSize(editor, text);
473
+ setSegmentMeta(tr, segmentId, {
474
+ from: existing.from,
475
+ to: newEnd,
476
+ revision,
477
+ joinerLength: existing.joinerLength
478
+ });
479
+ } else {
480
+ tr.delete(existing.from, existing.to);
481
+ const inserted = insertOperationsAt(
482
+ editor,
483
+ tr,
484
+ existing.from,
485
+ textToDocumentOperations(text),
486
+ mark
487
+ );
488
+ setSegmentMeta(tr, segmentId, {
489
+ from: existing.from,
490
+ to: inserted.to,
491
+ revision,
492
+ joinerLength: existing.joinerLength
493
+ });
494
+ }
495
+ maybeScrollIntoView(tr, editor, existing.to);
496
+ editor.view.dispatch(tr);
497
+ knownSegmentIds.add(segmentId);
498
+ } else {
499
+ if (!state.anchor) {
500
+ return;
501
+ }
502
+ const tr = editor.state.tr;
503
+ tr.setMeta("addToHistory", false);
504
+ tr.setMeta(SKIP_STORE_SYNC_META, true);
505
+ let pos = state.anchor.end;
506
+ if (state.anchor.selectionPendingDelete && state.anchor.initialEnd > state.anchor.initialStart) {
507
+ tr.delete(state.anchor.initialStart, state.anchor.initialEnd);
508
+ pos = state.anchor.initialStart;
509
+ setAnchorMeta(tr, {
510
+ kind: "set",
511
+ value: {
512
+ ...state.anchor,
513
+ selectionPendingDelete: false,
514
+ initialEnd: state.anchor.initialStart,
515
+ end: state.anchor.initialStart
516
+ }
517
+ });
518
+ }
519
+ const before = textBetweenWithHardBreaks(editor.state.doc, Math.max(0, pos - 500), pos);
520
+ const overlap = stripLeadingOverlapFromTextWithInfo(before, text);
521
+ text = overlap.text;
522
+ if (!text) return;
523
+ const joiner = overlap.partialWord ? "" : pickJoiner(before, text);
524
+ const fullText = joiner + text;
525
+ const inserted = insertOperationsAt(
526
+ editor,
527
+ tr,
528
+ pos,
529
+ textToDocumentOperations(fullText),
530
+ mark
531
+ );
532
+ const segFrom = pos + measureTextAsOperationsSize(editor, joiner);
533
+ const segTo = inserted.to;
534
+ setSegmentMeta(tr, segmentId, { from: segFrom, to: segTo, revision, joinerLength: joiner.length });
535
+ setAnchorMeta(tr, { kind: "patch", end: segTo });
536
+ maybeScrollIntoView(tr, editor, segTo);
537
+ editor.view.dispatch(tr);
538
+ knownSegmentIds.add(segmentId);
539
+ }
540
+ };
541
+ const pendingPartials = /* @__PURE__ */ new Map();
542
+ const flushPendingPartial = (segmentId) => {
543
+ const pending = pendingPartials.get(segmentId);
544
+ if (!pending) return;
545
+ clearTimeout(pending.timer);
546
+ pendingPartials.delete(segmentId);
547
+ _applyPartial(segmentId, pending.text, pending.revision);
548
+ };
549
+ const cancelPendingPartial = (segmentId) => {
550
+ const pending = pendingPartials.get(segmentId);
551
+ if (!pending) return;
552
+ clearTimeout(pending.timer);
553
+ pendingPartials.delete(segmentId);
554
+ };
555
+ const cancelAllPendingPartials = () => {
556
+ for (const { timer } of pendingPartials.values()) {
557
+ clearTimeout(timer);
558
+ }
559
+ pendingPartials.clear();
560
+ };
561
+ const schedulePartial = (segmentId, text, revision) => {
562
+ const existing = pendingPartials.get(segmentId);
563
+ if (existing) {
564
+ if (revision < existing.revision) return;
565
+ clearTimeout(existing.timer);
566
+ }
567
+ const timer = setTimeout(() => flushPendingPartial(segmentId), PARTIAL_DEBOUNCE_MS);
568
+ pendingPartials.set(segmentId, { text, revision, timer });
569
+ };
570
+ const trimOverlappingPreviewSegments = (tr, state, committedSegmentId, committedText) => {
571
+ const previewMark = editor.schema.marks.ephiaPreview?.create();
572
+ for (const [otherId, other] of Object.entries(state.segments)) {
573
+ if (otherId === committedSegmentId || other.revision === Number.MAX_SAFE_INTEGER) {
574
+ continue;
575
+ }
576
+ const previewText = textBetweenWithHardBreaks(editor.state.doc, other.from, other.to);
577
+ const overlap = stripLeadingOverlapFromTextWithInfo(committedText, previewText);
578
+ const trimmed = overlap.text;
579
+ if (trimmed === previewText) continue;
580
+ let from = tr.mapping.map(other.from);
581
+ let to = tr.mapping.map(other.to);
582
+ if (overlap.partialWord && from > 0 && /\s/.test(textBetweenWithHardBreaks(tr.doc, from - 1, from))) {
583
+ tr.delete(from - 1, from);
584
+ from -= 1;
585
+ to -= 1;
586
+ }
587
+ if (trimmed) {
588
+ tr.delete(from, to);
589
+ const inserted = insertOperationsAt(
590
+ editor,
591
+ tr,
592
+ from,
593
+ textToDocumentOperations(trimmed),
594
+ previewMark
595
+ );
596
+ setSegmentMeta(tr, otherId, {
597
+ from,
598
+ to: inserted.to,
599
+ revision: other.revision + 1
600
+ });
601
+ } else {
602
+ tr.delete(from, to);
603
+ setSegmentMeta(tr, otherId, null);
604
+ }
605
+ }
606
+ };
607
+ return {
608
+ kind: "tiptap",
609
+ attach() {
610
+ },
611
+ detach() {
612
+ editor.view.dom.removeEventListener("scroll", markUserScroll);
613
+ editor.unregisterPlugin(ephiaTiptapKey);
614
+ editor.off("update", syncToStore);
615
+ editor.off("selectionUpdate", syncSelection);
616
+ cancelAllPendingPartials();
617
+ if (syncRaf !== null && typeof cancelAnimationFrame !== "undefined") {
618
+ cancelAnimationFrame(syncRaf);
619
+ syncRaf = null;
620
+ }
621
+ for (const timeoutId of committedTimeouts.values()) {
622
+ clearTimeout(timeoutId);
623
+ }
624
+ committedTimeouts.clear();
625
+ knownSegmentIds.clear();
626
+ },
627
+ // ─── Session lifecycle ──────────────────────────────────────────────────
628
+ beginSession(opts = {}) {
629
+ const replaceSelection = opts.replaceSelection ?? true;
630
+ const isFocused = editor.isFocused;
631
+ let from = editor.state.selection.from;
632
+ let to = editor.state.selection.to;
633
+ if (!isFocused && from === to) {
634
+ const docSize = editor.state.doc.content.size;
635
+ const resumePos = lastDictationEnd !== null ? Math.min(lastDictationEnd, Math.max(1, docSize - 1)) : Math.max(1, docSize - 1);
636
+ from = resumePos;
637
+ to = resumePos;
638
+ }
639
+ const hadSelection = from !== to;
640
+ const selectionPendingDelete = hadSelection && replaceSelection;
641
+ const tr = editor.state.tr;
642
+ tr.setMeta("addToHistory", false);
643
+ tr.setMeta(SKIP_STORE_SYNC_META, true);
644
+ const anchorValue = {
645
+ initialStart: from,
646
+ initialEnd: to,
647
+ // conserver la vraie fin de sélection
648
+ hadSelection,
649
+ end: from,
650
+ // point d'insertion = début de sélection
651
+ selectionPendingDelete
652
+ };
653
+ try {
654
+ const resolved = tr.doc.resolve(from);
655
+ tr.setSelection(TextSelection.near(resolved));
656
+ } catch {
657
+ }
658
+ setAnchorMeta(tr, { kind: "set", value: anchorValue });
659
+ editor.view.dispatch(tr);
660
+ if (!isFocused) {
661
+ try {
662
+ editor.view.focus();
663
+ } catch {
664
+ }
665
+ }
666
+ return toPublicAnchor(anchorValue);
667
+ },
668
+ endSession() {
669
+ cancelAllPendingPartials();
670
+ const anchor = getPluginState(editor).anchor;
671
+ if (anchor) lastDictationEnd = anchor.end;
672
+ const tr = editor.state.tr;
673
+ tr.setMeta("addToHistory", false);
674
+ tr.setMeta(SKIP_STORE_SYNC_META, true);
675
+ setAnchorMeta(tr, { kind: "clear" });
676
+ const committedMark = editor.schema.marks.ephiaCommitted;
677
+ if (committedMark) {
678
+ tr.removeMark(0, editor.state.doc.content.size, committedMark);
679
+ }
680
+ editor.view.dispatch(tr);
681
+ syncToStore();
682
+ },
683
+ getSessionAnchor() {
684
+ const anchor = getPluginState(editor).anchor;
685
+ return anchor ? toPublicAnchor(anchor) : null;
686
+ },
687
+ getCursorRect() {
688
+ const anchor = getPluginState(editor).anchor;
689
+ const pos = anchor ? anchor.end : editor.state.selection.from;
690
+ try {
691
+ const coords = editor.view.coordsAtPos(pos);
692
+ return new DOMRect(
693
+ coords.left,
694
+ coords.top,
695
+ coords.right - coords.left,
696
+ coords.bottom - coords.top
697
+ );
698
+ } catch {
699
+ const dom = editor.view.dom;
700
+ return dom.getBoundingClientRect();
701
+ }
702
+ },
703
+ getRangeRect(start, end) {
704
+ if (end <= start) return null;
705
+ try {
706
+ const a = editor.view.coordsAtPos(start);
707
+ const b = editor.view.coordsAtPos(end);
708
+ const top = Math.min(a.top, b.top);
709
+ const bottom = Math.max(a.bottom, b.bottom);
710
+ const left = Math.min(a.left, b.left);
711
+ const right = Math.max(a.right, b.right);
712
+ return new DOMRect(left, top, Math.max(2, right - left), bottom - top);
713
+ } catch {
714
+ return null;
715
+ }
716
+ },
717
+ // ─── Streaming insertion ────────────────────────────────────────────────
718
+ insertPartial(segmentId, text, revision) {
719
+ schedulePartial(segmentId, text, revision);
720
+ },
721
+ commitFinal(segmentId, text, options) {
722
+ clearCommittedTimeout(segmentId);
723
+ flushPendingPartial(segmentId);
724
+ const state = getPluginState(editor);
725
+ const existing = state.segments[segmentId];
726
+ if (!existing && knownSegmentIds.has(segmentId)) {
727
+ console.warn("[ephia:tiptap] known segmentId lost range; refusing append", {
728
+ segmentId,
729
+ text
730
+ });
731
+ return;
732
+ }
733
+ if (existing) {
734
+ const tr = editor.state.tr;
735
+ const oldText = textBetweenWithHardBreaks(editor.state.doc, existing.from, existing.to);
736
+ let newEnd = existing.from + measureTextAsOperationsSize(editor, text);
737
+ if (oldText !== text) {
738
+ const prefixLen = getCommonPrefix(oldText, text);
739
+ const suffixLen = getCommonSuffix(oldText, text, prefixLen);
740
+ if (prefixLen > 0 || suffixLen > 0) {
741
+ const deleteFrom = existing.from + prefixLen;
742
+ const deleteTo = existing.to - suffixLen;
743
+ const insertText = text.slice(prefixLen, text.length - suffixLen);
744
+ if (deleteFrom < deleteTo) tr.delete(deleteFrom, deleteTo);
745
+ if (insertText) {
746
+ insertOperationsAt(
747
+ editor,
748
+ tr,
749
+ deleteFrom,
750
+ textToDocumentOperations(insertText)
751
+ );
752
+ }
753
+ newEnd = existing.from + measureTextAsOperationsSize(editor, text);
754
+ } else {
755
+ tr.delete(existing.from, existing.to);
756
+ const inserted = insertOperationsAt(
757
+ editor,
758
+ tr,
759
+ existing.from,
760
+ textToDocumentOperations(text)
761
+ );
762
+ newEnd = inserted.to;
763
+ }
764
+ }
765
+ tr.removeMark(existing.from, newEnd, editor.schema.marks.ephiaPreview);
766
+ const committedMark = editor.schema.marks.ephiaCommitted?.create();
767
+ if (committedMark) {
768
+ tr.addMark(existing.from, newEnd, committedMark);
769
+ }
770
+ setSegmentMeta(tr, segmentId, {
771
+ from: existing.from,
772
+ to: newEnd,
773
+ revision: Number.MAX_SAFE_INTEGER,
774
+ joinerLength: existing.joinerLength
775
+ });
776
+ if (state.anchor) {
777
+ setAnchorMeta(tr, { kind: "patch", end: newEnd });
778
+ }
779
+ lastDictationEnd = newEnd;
780
+ trimOverlappingPreviewSegments(tr, state, segmentId, text);
781
+ for (const absorbedId of options?.absorbedSegmentIds ?? []) {
782
+ if (absorbedId === segmentId) continue;
783
+ cancelPendingPartial(absorbedId);
784
+ const absorbedSeg = state.segments[absorbedId];
785
+ if (!absorbedSeg) continue;
786
+ const aFrom = tr.mapping.map(absorbedSeg.from - (absorbedSeg.joinerLength ?? 0));
787
+ const aTo = tr.mapping.map(absorbedSeg.to);
788
+ if (aFrom < aTo) tr.delete(aFrom, aTo);
789
+ setSegmentMeta(tr, absorbedId, null);
790
+ }
791
+ maybeScrollIntoView(tr, editor, newEnd);
792
+ editor.view.dispatch(tr);
793
+ knownSegmentIds.add(segmentId);
794
+ if (committedMark) {
795
+ const timeoutId = window.setTimeout(() => {
796
+ committedTimeouts.delete(segmentId);
797
+ const freshState = getPluginState(editor);
798
+ const seg = freshState.segments[segmentId];
799
+ if (seg && editor.schema.marks.ephiaCommitted) {
800
+ const tr2 = editor.state.tr;
801
+ tr2.setMeta("addToHistory", false);
802
+ tr2.setMeta(SKIP_STORE_SYNC_META, true);
803
+ tr2.removeMark(seg.from, seg.to, editor.schema.marks.ephiaCommitted);
804
+ editor.view.dispatch(tr2);
805
+ }
806
+ }, 1300);
807
+ committedTimeouts.set(segmentId, timeoutId);
808
+ }
809
+ } else {
810
+ const tr = editor.state.tr;
811
+ let pos = state.anchor ? state.anchor.end : editor.state.selection.from;
812
+ if (state.anchor?.selectionPendingDelete && state.anchor.initialEnd > state.anchor.initialStart) {
813
+ tr.delete(state.anchor.initialStart, state.anchor.initialEnd);
814
+ pos = state.anchor.initialStart;
815
+ setAnchorMeta(tr, {
816
+ kind: "set",
817
+ value: {
818
+ ...state.anchor,
819
+ selectionPendingDelete: false,
820
+ initialEnd: state.anchor.initialStart,
821
+ end: state.anchor.initialStart
822
+ }
823
+ });
824
+ }
825
+ let joiner = "";
826
+ if (state.anchor) {
827
+ const before = textBetweenWithHardBreaks(editor.state.doc, Math.max(0, pos - 500), pos);
828
+ const overlap = stripLeadingOverlapFromTextWithInfo(before, text);
829
+ text = overlap.text;
830
+ if (!text) return;
831
+ joiner = overlap.partialWord ? "" : pickJoiner(before, text);
832
+ }
833
+ const fullText = joiner + text;
834
+ const inserted = insertOperationsAt(
835
+ editor,
836
+ tr,
837
+ pos,
838
+ textToDocumentOperations(fullText)
839
+ );
840
+ const segFrom = pos + measureTextAsOperationsSize(editor, joiner);
841
+ const segTo = inserted.to;
842
+ setSegmentMeta(tr, segmentId, {
843
+ from: segFrom,
844
+ to: segTo,
845
+ revision: Number.MAX_SAFE_INTEGER,
846
+ joinerLength: joiner.length
847
+ });
848
+ if (state.anchor) {
849
+ setAnchorMeta(tr, { kind: "patch", end: segTo });
850
+ }
851
+ lastDictationEnd = segTo;
852
+ trimOverlappingPreviewSegments(tr, state, segmentId, text);
853
+ for (const absorbedId of options?.absorbedSegmentIds ?? []) {
854
+ if (absorbedId === segmentId) continue;
855
+ cancelPendingPartial(absorbedId);
856
+ const absorbedSeg = state.segments[absorbedId];
857
+ if (!absorbedSeg) continue;
858
+ const aFrom = tr.mapping.map(absorbedSeg.from - (absorbedSeg.joinerLength ?? 0));
859
+ const aTo = tr.mapping.map(absorbedSeg.to);
860
+ if (aFrom < aTo) tr.delete(aFrom, aTo);
861
+ setSegmentMeta(tr, absorbedId, null);
862
+ }
863
+ maybeScrollIntoView(tr, editor, segTo);
864
+ editor.view.dispatch(tr);
865
+ knownSegmentIds.add(segmentId);
866
+ }
867
+ },
868
+ clearPartial(segmentId) {
869
+ cancelPendingPartial(segmentId);
870
+ const state = getPluginState(editor);
871
+ const existing = state.segments[segmentId];
872
+ if (existing) {
873
+ if (existing.revision === Number.MAX_SAFE_INTEGER) {
874
+ return;
875
+ }
876
+ const tr = editor.state.tr;
877
+ tr.setMeta("addToHistory", false);
878
+ tr.setMeta(SKIP_STORE_SYNC_META, true);
879
+ tr.delete(existing.from - (existing.joinerLength ?? 0), existing.to);
880
+ setSegmentMeta(tr, segmentId, null);
881
+ editor.view.dispatch(tr);
882
+ }
883
+ },
884
+ clearAll() {
885
+ cancelAllPendingPartials();
886
+ const state = getPluginState(editor);
887
+ const previewEntries = Object.entries(state.segments).filter(
888
+ ([, seg]) => seg.revision !== Number.MAX_SAFE_INTEGER
889
+ );
890
+ if (previewEntries.length === 0) return;
891
+ const tr = editor.state.tr;
892
+ tr.setMeta("addToHistory", false);
893
+ tr.setMeta(SKIP_STORE_SYNC_META, true);
894
+ for (const [segmentId, seg] of previewEntries.sort(
895
+ (a, b) => b[1].from - a[1].from
896
+ )) {
897
+ const from = tr.mapping.map(seg.from - (seg.joinerLength ?? 0));
898
+ const to = tr.mapping.map(seg.to);
899
+ if (from < to) tr.delete(from, to);
900
+ setSegmentMeta(tr, segmentId, null);
901
+ }
902
+ editor.view.dispatch(tr);
903
+ },
904
+ getText() {
905
+ return getTextFromDoc(editor.state.doc);
906
+ },
907
+ getSelection() {
908
+ const { from, to } = editor.state.selection;
909
+ if (from === to) return null;
910
+ return {
911
+ text: textBetweenWithHardBreaks(editor.state.doc, from, to),
912
+ range: { start: from, end: to }
913
+ };
914
+ },
915
+ getCursorOffset() {
916
+ return editor.state.selection.from;
917
+ },
918
+ applyOperation(operation) {
919
+ switch (operation.type) {
920
+ case "replace": {
921
+ let start;
922
+ let end;
923
+ if (operation.range) {
924
+ start = operation.range.start;
925
+ end = operation.range.end;
926
+ } else if (operation.targetText) {
927
+ const text = editor.getText();
928
+ const idx = text.indexOf(operation.targetText);
929
+ if (idx === -1) {
930
+ console.warn(
931
+ "[tiptapBinding] replace ignored: targetText not found"
932
+ );
933
+ return;
934
+ }
935
+ start = idx;
936
+ end = idx + operation.targetText.length;
937
+ } else {
938
+ console.warn(
939
+ "[tiptapBinding] replace ignored: missing range or targetText"
940
+ );
941
+ return;
942
+ }
943
+ editor.chain().deleteRange({ from: start, to: end }).insertContentAt(start, [{ type: "text", text: operation.replacement }]).run();
944
+ break;
945
+ }
946
+ case "insert":
947
+ case "insert_text":
948
+ case "line_break":
949
+ case "paragraph_break": {
950
+ const pos = resolveInsertPosition(editor, operation.position);
951
+ const tr = editor.state.tr;
952
+ insertOperationsAt(editor, tr, pos, [operation]);
953
+ editor.view.dispatch(tr);
954
+ break;
955
+ }
956
+ case "delete": {
957
+ let start;
958
+ let end;
959
+ if (operation.range) {
960
+ start = operation.range.start;
961
+ end = operation.range.end;
962
+ } else if (operation.targetText) {
963
+ const text = editor.getText();
964
+ const idx = text.indexOf(operation.targetText);
965
+ if (idx === -1) {
966
+ console.warn(
967
+ "[tiptapBinding] delete ignored: targetText not found"
968
+ );
969
+ return;
970
+ }
971
+ start = idx;
972
+ end = idx + operation.targetText.length;
973
+ } else {
974
+ console.warn(
975
+ "[tiptapBinding] delete ignored: missing range or targetText"
976
+ );
977
+ return;
978
+ }
979
+ editor.chain().deleteRange({ from: start, to: end }).run();
980
+ break;
981
+ }
982
+ case "replace_all": {
983
+ editor.chain().setContent([{ type: "paragraph", content: [{ type: "text", text: operation.replacement }] }]).run();
984
+ break;
985
+ }
986
+ default:
987
+ console.warn(
988
+ "[tiptapBinding] Operation not supported:",
989
+ operation.type
990
+ );
991
+ }
992
+ },
993
+ applyOperations(operations) {
994
+ if (operations.length === 0) return;
995
+ const firstInsertOperation = operations.find(
996
+ (operation) => operation.type === "insert" || operation.type === "insert_text" || operation.type === "line_break" || operation.type === "paragraph_break"
997
+ );
998
+ if (!firstInsertOperation) {
999
+ for (const operation of operations) {
1000
+ console.warn(
1001
+ "[tiptapBinding] Operation not supported in applyOperations:",
1002
+ operation.type
1003
+ );
1004
+ }
1005
+ return;
1006
+ }
1007
+ const tr = editor.state.tr;
1008
+ const pos = resolveInsertPosition(editor, firstInsertOperation.position);
1009
+ insertOperationsAt(editor, tr, pos, operations);
1010
+ editor.view.dispatch(tr);
1011
+ }
1012
+ };
1013
+ }
1014
+ export {
1015
+ EphiaCommittedMark,
1016
+ EphiaPlaceholderMark,
1017
+ EphiaPreviewMark,
1018
+ EphiaRevisedMark,
1019
+ EphiaV2CommittedMark,
1020
+ EphiaV2PreviewMark,
1021
+ NativeBinding,
1022
+ TiptapBinding,
1023
+ createTiptapBinding as createLegacyTiptapBinding
1024
+ };
1025
+ //# sourceMappingURL=index.js.map