@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,295 @@
1
+ "use client";
2
+
3
+ import React, { useCallback, useEffect, useRef, useState } from "react";
4
+
5
+ const ASCII_CURSOR_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] as const;
6
+ const ASCII_CURSOR_INTERVAL_MS = 90;
7
+
8
+ export interface EphiaTextareaProps
9
+ extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
10
+ targetId: string;
11
+ mode?: string;
12
+ insertion?: "preview-inline" | "preview-floating" | "final-only" | "none";
13
+ }
14
+
15
+ /**
16
+ * Textarea avec overlay de preview du partial en streaming.
17
+ *
18
+ * Stratégie (Dragon Medical-style) :
19
+ * - La `<textarea>` contient uniquement le texte committed (color: transparent).
20
+ * - L'overlay div par-dessous affiche : texte committed + partial grisé.
21
+ */
22
+ export const EphiaTextarea = React.forwardRef<
23
+ HTMLTextAreaElement,
24
+ EphiaTextareaProps
25
+ >(function EphiaTextarea(
26
+ { targetId, mode = "write", insertion = "preview-inline", className = "", style, ...rest },
27
+ forwardedRef
28
+ ) {
29
+ const wrapperRef = useRef<HTMLDivElement>(null);
30
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
31
+ const overlayRef = useRef<HTMLDivElement>(null);
32
+ const partialRef = useRef<{ text: string; insertAt: number } | null>(null);
33
+ const insertionRef = useRef(insertion);
34
+ insertionRef.current = insertion;
35
+ const anchorEndRef = useRef<number | null>(null);
36
+ const asciiFrameRef = useRef(0);
37
+ const asciiTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
38
+
39
+ const appendAsciiCursor = (ov: HTMLDivElement) => {
40
+ const span = document.createElement("span");
41
+ span.className = "ephia-cursor";
42
+ span.setAttribute("aria-hidden", "true");
43
+ span.textContent = ASCII_CURSOR_FRAMES[asciiFrameRef.current] ?? "⠋";
44
+ ov.appendChild(span);
45
+ };
46
+
47
+ const setTextareaRef = useCallback(
48
+ (node: HTMLTextAreaElement | null) => {
49
+ (textareaRef as React.MutableRefObject<HTMLTextAreaElement | null>).current = node;
50
+ if (typeof forwardedRef === "function") {
51
+ forwardedRef(node);
52
+ } else if (forwardedRef) {
53
+ (forwardedRef as React.MutableRefObject<HTMLTextAreaElement | null>).current = node;
54
+ }
55
+ },
56
+ [forwardedRef]
57
+ );
58
+
59
+ const [isActive, setIsActive] = useState(false);
60
+
61
+ // ─── Helpers DOM overlay (évite innerHTML + escapeHtml à chaque frame) ───
62
+ const clearOverlay = (ov: HTMLDivElement) => {
63
+ while (ov.firstChild) ov.removeChild(ov.firstChild);
64
+ };
65
+
66
+ const appendText = (ov: HTMLDivElement, text: string) => {
67
+ if (text) ov.appendChild(document.createTextNode(text));
68
+ };
69
+
70
+ const appendBrNbsp = (ov: HTMLDivElement) => {
71
+ ov.appendChild(document.createElement("br"));
72
+ ov.appendChild(document.createTextNode("\u00A0"));
73
+ };
74
+
75
+ // ─── Mise à jour impérative de l'overlay ─────────────────────────────────
76
+ const updateOverlay = () => {
77
+ const ov = overlayRef.current;
78
+ const ta = textareaRef.current;
79
+ if (!ov || !ta) return;
80
+
81
+ const rawText = ta.value;
82
+ const partial = partialRef.current;
83
+ const isRecording = ta.hasAttribute("data-ephia-recording");
84
+ const active = isRecording;
85
+
86
+ clearOverlay(ov);
87
+
88
+ // ── Recording / idle : texte committed + partial ───────────────────────
89
+ if (!partial || insertionRef.current === "none") {
90
+ if (active) {
91
+ const cursorPos = Math.min(
92
+ anchorEndRef.current ?? rawText.length,
93
+ rawText.length
94
+ );
95
+ appendText(ov, rawText.slice(0, cursorPos));
96
+ appendAsciiCursor(ov);
97
+ appendText(ov, rawText.slice(cursorPos));
98
+ appendBrNbsp(ov);
99
+ return;
100
+ }
101
+ appendText(ov, rawText);
102
+ appendBrNbsp(ov);
103
+ return;
104
+ }
105
+
106
+ const insertAt = Math.min(partial.insertAt, rawText.length);
107
+ const textAtInsert = rawText.slice(insertAt, insertAt + partial.text.length);
108
+ const isAbsorbed = textAtInsert === partial.text;
109
+
110
+ appendText(ov, rawText.slice(0, insertAt));
111
+ if (!isAbsorbed) {
112
+ const span = document.createElement("span");
113
+ span.className = "ephia-text--streaming";
114
+ span.textContent = partial.text;
115
+ ov.appendChild(span);
116
+ } else if (active) {
117
+ appendAsciiCursor(ov);
118
+ }
119
+ appendText(ov, rawText.slice(insertAt));
120
+ appendBrNbsp(ov);
121
+ };
122
+
123
+ // ─── MutationObserver : synchroniser l'état actif ────────────────────────
124
+ useEffect(() => {
125
+ const ta = textareaRef.current;
126
+ const wr = wrapperRef.current;
127
+ if (!ta) return;
128
+
129
+ const onAttributeChange = () => {
130
+ const isRecording = ta.hasAttribute("data-ephia-recording");
131
+ setIsActive(isRecording);
132
+ if (!isRecording) {
133
+ anchorEndRef.current = null;
134
+ if (asciiTimerRef.current !== null) {
135
+ clearInterval(asciiTimerRef.current);
136
+ asciiTimerRef.current = null;
137
+ }
138
+ } else if (asciiTimerRef.current === null) {
139
+ asciiTimerRef.current = setInterval(() => {
140
+ asciiFrameRef.current = (asciiFrameRef.current + 1) % ASCII_CURSOR_FRAMES.length;
141
+ updateOverlay();
142
+ }, ASCII_CURSOR_INTERVAL_MS);
143
+ }
144
+ updateOverlay();
145
+ };
146
+
147
+ const observer = new MutationObserver(onAttributeChange);
148
+ observer.observe(ta, {
149
+ attributes: true,
150
+ attributeFilter: ["data-ephia-recording"],
151
+ });
152
+
153
+ return () => {
154
+ observer.disconnect();
155
+ if (asciiTimerRef.current !== null) {
156
+ clearInterval(asciiTimerRef.current);
157
+ asciiTimerRef.current = null;
158
+ }
159
+ };
160
+ // eslint-disable-next-line react-hooks/exhaustive-deps
161
+ }, []);
162
+
163
+ // ─── Écoute des events DOM du binding ────────────────────────────────────
164
+ useEffect(() => {
165
+ const ta = textareaRef.current;
166
+ if (!ta) return;
167
+
168
+ const onPartial = (e: Event) => {
169
+ const { text, insertAt } = (e as CustomEvent<{ text: string; insertAt: number }>).detail;
170
+ partialRef.current = { text, insertAt };
171
+ updateOverlay();
172
+ };
173
+
174
+ const onPartialCleared = () => {
175
+ partialRef.current = null;
176
+ updateOverlay();
177
+ };
178
+
179
+ const onCursorPosition = (e: Event) => {
180
+ const { position } = (e as CustomEvent<{ position: number }>).detail;
181
+ anchorEndRef.current = position;
182
+ updateOverlay();
183
+ };
184
+
185
+ ta.addEventListener("ephia:partial", onPartial);
186
+ ta.addEventListener("ephia:partial-cleared", onPartialCleared);
187
+ ta.addEventListener("ephia:cursor-position", onCursorPosition);
188
+ return () => {
189
+ ta.removeEventListener("ephia:partial", onPartial);
190
+ ta.removeEventListener("ephia:partial-cleared", onPartialCleared);
191
+ ta.removeEventListener("ephia:cursor-position", onCursorPosition);
192
+ };
193
+ // eslint-disable-next-line react-hooks/exhaustive-deps
194
+ }, []);
195
+
196
+ // ─── Sync scroll overlay ↔ textarea ──────────────────────────────────────
197
+ useEffect(() => {
198
+ const ta = textareaRef.current;
199
+ const ov = overlayRef.current;
200
+ if (!ta || !ov) return;
201
+ const syncScroll = () => {
202
+ ov.scrollTop = ta.scrollTop;
203
+ ov.scrollLeft = ta.scrollLeft;
204
+ };
205
+ ta.addEventListener("scroll", syncScroll, { passive: true });
206
+ return () => ta.removeEventListener("scroll", syncScroll);
207
+ }, []);
208
+
209
+ // ─── Sync styles computed → overlay (font, padding, etc.) ─────────────────
210
+ useEffect(() => {
211
+ const ta = textareaRef.current;
212
+ const ov = overlayRef.current;
213
+ if (!ta || !ov) return;
214
+
215
+ const applyStyles = () => {
216
+ const cs = window.getComputedStyle(ta);
217
+ const s = ov.style;
218
+ s.fontFamily = cs.fontFamily;
219
+ s.fontSize = cs.fontSize;
220
+ s.fontWeight = cs.fontWeight;
221
+ s.fontStyle = cs.fontStyle;
222
+ s.lineHeight = cs.lineHeight;
223
+ s.letterSpacing = cs.letterSpacing;
224
+ s.wordSpacing = cs.wordSpacing;
225
+ s.textTransform = cs.textTransform;
226
+ s.textIndent = cs.textIndent;
227
+ s.paddingTop = cs.paddingTop;
228
+ s.paddingRight = cs.paddingRight;
229
+ s.paddingBottom = cs.paddingBottom;
230
+ s.paddingLeft = cs.paddingLeft;
231
+ s.borderTopWidth = cs.borderTopWidth;
232
+ s.borderRightWidth = cs.borderRightWidth;
233
+ s.borderBottomWidth = cs.borderBottomWidth;
234
+ s.borderLeftWidth = cs.borderLeftWidth;
235
+ s.boxSizing = cs.boxSizing;
236
+ s.width = cs.width;
237
+ s.height = cs.height;
238
+ };
239
+
240
+ applyStyles();
241
+ let ro: ResizeObserver | null = null;
242
+ if (typeof ResizeObserver !== "undefined") {
243
+ ro = new ResizeObserver(applyStyles);
244
+ ro.observe(ta);
245
+ }
246
+ return () => ro?.disconnect();
247
+ }, []);
248
+
249
+ // ─── Mise à jour overlay sur input utilisateur ────────────────────────────
250
+ useEffect(() => {
251
+ const ta = textareaRef.current;
252
+ if (!ta) return;
253
+ const onInput = () => updateOverlay();
254
+ ta.addEventListener("input", onInput);
255
+ return () => ta.removeEventListener("input", onInput);
256
+ // eslint-disable-next-line react-hooks/exhaustive-deps
257
+ }, []);
258
+
259
+ // ─── Initialisation overlay au montage ───────────────────────────────────
260
+ useEffect(() => {
261
+ updateOverlay();
262
+ // eslint-disable-next-line react-hooks/exhaustive-deps
263
+ }, []);
264
+
265
+ // ─── Sync overlay quand value change via React (controlled component) ─────
266
+ useEffect(() => {
267
+ const ta = textareaRef.current;
268
+ if (!ta) return;
269
+ requestAnimationFrame(() => updateOverlay());
270
+ // eslint-disable-next-line react-hooks/exhaustive-deps
271
+ }, [rest.value]);
272
+
273
+ const inlineStyle = isActive
274
+ ? { ...style, caretColor: "transparent" as const }
275
+ : style;
276
+
277
+ return (
278
+ <div ref={wrapperRef} className="ephia-textarea-wrapper" style={{ position: "relative" }}>
279
+ <div
280
+ ref={overlayRef}
281
+ className="ephia-textarea-overlay"
282
+ aria-hidden="true"
283
+ />
284
+ <textarea
285
+ ref={setTextareaRef}
286
+ data-ephia-target={targetId}
287
+ data-ephia-mode={mode}
288
+ data-ephia-insertion={insertion}
289
+ className={`ephia-textarea-input ${className}`}
290
+ style={inlineStyle}
291
+ {...rest}
292
+ />
293
+ </div>
294
+ );
295
+ });
@@ -0,0 +1,318 @@
1
+ /* ephia-audio React SDK — visual feedback for active target, recording state & text states.
2
+ * Import: `import "ephia-audio/react/ephia-react.css"` (ou via styles.css du package).
3
+ * Tout est dérivé de variables CSS pour être surchargeable.
4
+ *
5
+ * Système de text states :
6
+ * .ephia-text--streaming → texte en cours d'arrivée STT (partial)
7
+ * .ephia-text--committed → transition de validation (streaming → normal)
8
+ * .ephia-text--revised → texte corrigé par le review pipeline
9
+ */
10
+
11
+ :root {
12
+ /* ── Couleurs sémantiques ─────────────────────────────────────────────── */
13
+ --ephia-color-active: #6366f1;
14
+ --ephia-color-recording: #ef4444;
15
+ --ephia-color-streaming: #6366f1;
16
+ --ephia-color-committed: #22c55e;
17
+ --ephia-color-revised: #f59e0b;
18
+
19
+ /* ── Épaisseurs / radius ──────────────────────────────────────────────── */
20
+ --ephia-ring-width: 2px;
21
+ --ephia-ring-radius: 6px;
22
+ --ephia-transition: 180ms ease;
23
+ }
24
+
25
+ /* ═══════════════════════════════════════════════════════════════════════════
26
+ HALOS — états du target (container)
27
+ ═══════════════════════════════════════════════════════════════════════════ */
28
+
29
+ /* ─── Session active : griser les non-cibles pendant une dictée ──────────── */
30
+ /* Cible les descendants directs du body ET les enfants directs des containers
31
+ pour éviter d'affecter des composants imbriqués non liés à Ephia. */
32
+ [data-ephia-session-active="true"] > [data-ephia-target]:not([data-ephia-recording="true"]),
33
+ [data-ephia-session-active="true"] [data-ephia-target]:not([data-ephia-recording="true"]) {
34
+ opacity: 0.55;
35
+ filter: grayscale(0.6);
36
+ transition: opacity 0.2s ease, filter 0.2s ease;
37
+ }
38
+
39
+ /* ─── Target actif : halo discret (uniquement en session active) ────────── */
40
+ body[data-ephia-session-active="true"] [data-ephia-target-active="true"] {
41
+ outline: var(--ephia-ring-width) solid
42
+ color-mix(in srgb, var(--ephia-color-active) 35%, transparent);
43
+ outline-offset: 1px;
44
+ border-radius: var(--ephia-ring-radius);
45
+ transition: outline-color var(--ephia-transition),
46
+ box-shadow var(--ephia-transition);
47
+ }
48
+
49
+ /* ─── Target en enregistrement : border + glow ───────────────────────────── */
50
+ [data-ephia-recording="true"] {
51
+ position: relative;
52
+ outline: none !important;
53
+ border: 2px solid color-mix(in srgb, var(--ephia-color-recording) 55%, transparent);
54
+ border-radius: var(--ephia-ring-radius);
55
+ }
56
+
57
+ [data-ephia-recording="true"]::before {
58
+ content: "";
59
+ position: absolute;
60
+ inset: -3px;
61
+ border-radius: calc(var(--ephia-ring-radius) + 3px);
62
+ pointer-events: none;
63
+ z-index: 50;
64
+ box-shadow: 0 0 0 1px color-mix(in srgb, var(--ephia-color-recording) 30%, transparent),
65
+ 0 0 18px color-mix(in srgb, var(--ephia-color-recording) 25%, transparent);
66
+ animation: ephia-recording-pulse 1.8s ease-in-out infinite;
67
+ }
68
+
69
+ @keyframes ephia-recording-pulse {
70
+ 0%, 100% { opacity: 0.8; transform: scale(1); }
71
+ 50% { opacity: 1; transform: scale(1.01); }
72
+ }
73
+
74
+ /* ─── Indicateur public processing : discret, piloté par EphiaStatusBar ─── */
75
+ [data-ephia-state="processing"] {
76
+ display: inline-flex;
77
+ align-items: center;
78
+ gap: 3px;
79
+ padding: 2px 6px;
80
+ }
81
+
82
+ [data-ephia-state="processing"]::before,
83
+ [data-ephia-state="processing"]::after {
84
+ content: "";
85
+ display: inline-block;
86
+ width: 6px;
87
+ height: 6px;
88
+ border-radius: 999px;
89
+ background: currentColor;
90
+ opacity: 0.4;
91
+ animation: ephia-processing-pulse 1.2s ease-in-out infinite;
92
+ }
93
+
94
+ [data-ephia-state="processing"]::after {
95
+ animation-delay: 0.4s;
96
+ margin-left: 3px;
97
+ }
98
+
99
+ @keyframes ephia-processing-pulse {
100
+ 0%, 80%, 100% { opacity: 0.2; transform: scale(0.85); }
101
+ 40% { opacity: 0.8; transform: scale(1); }
102
+ }
103
+
104
+ /* ═══════════════════════════════════════════════════════════════════════════
105
+ FLASHES — overlays temporaires (position: fixed)
106
+ ═══════════════════════════════════════════════════════════════════════════ */
107
+
108
+ /* ─── Flash sur plage révisée ────────────────────────────────────────────── */
109
+ .ephia-reformat-flash {
110
+ position: fixed;
111
+ pointer-events: none;
112
+ z-index: 100;
113
+ border-radius: 3px;
114
+ background-color: color-mix(in srgb, var(--ephia-color-active) 22%, transparent);
115
+ box-shadow: 0 0 0 1px color-mix(in srgb, var(--ephia-color-active) 40%, transparent);
116
+ animation: ephia-reformat-fade 1.2s ease-out forwards;
117
+ }
118
+
119
+ @keyframes ephia-reformat-fade {
120
+ 0% { opacity: 0; }
121
+ 15% { opacity: 1; }
122
+ 100% { opacity: 0; }
123
+ }
124
+
125
+ /* ─── Flash chunk committed : bleu → vert ────────────────────────────────── */
126
+ .ephia-committed-flash {
127
+ position: fixed;
128
+ pointer-events: none;
129
+ z-index: 100;
130
+ border-radius: 3px;
131
+ background-color: color-mix(in srgb, #3b82f6 22%, transparent);
132
+ box-shadow: 0 0 0 1px color-mix(in srgb, #3b82f6 40%, transparent);
133
+ animation: ephia-committed-fade 1s ease-out forwards;
134
+ }
135
+
136
+ @keyframes ephia-committed-fade {
137
+ 0% { opacity: 0; background-color: color-mix(in srgb, #3b82f6 30%, transparent); }
138
+ 30% { opacity: 1; background-color: color-mix(in srgb, #3b82f6 25%, transparent); }
139
+ 60% { background-color: color-mix(in srgb, #22c55e 20%, transparent); }
140
+ 100% { opacity: 0; background-color: color-mix(in srgb, #22c55e 15%, transparent); }
141
+ }
142
+
143
+ /* ═══════════════════════════════════════════════════════════════════════════
144
+ TEXT STATES — styles appliqués au texte lui-même
145
+ ═══════════════════════════════════════════════════════════════════════════ */
146
+
147
+ /* ─── 1. STREAMING — texte en cours d'arrivée STT ──────────────────────────
148
+ Visuel : fond lavande "respirant" + border-bottom dotted + italique léger.
149
+ Le texte est vivant, il peut encore changer. */
150
+ .ephia-streaming,
151
+ .ephia-text--streaming {
152
+ background-color: color-mix(in srgb, var(--ephia-color-streaming) 8%, transparent);
153
+ border-bottom: 1.5px dotted color-mix(in srgb, var(--ephia-color-streaming) 30%, transparent);
154
+ border-radius: 2px;
155
+ padding: 0 2px;
156
+ margin: 0 -2px;
157
+ font-style: italic;
158
+ animation: ephia-streaming-breathe 2.2s ease-in-out infinite;
159
+ transition: background-color 350ms ease, border-bottom-color 350ms ease, opacity 350ms ease;
160
+ }
161
+
162
+ @keyframes ephia-streaming-breathe {
163
+ 0%, 100% { background-color: color-mix(in srgb, var(--ephia-color-streaming) 6%, transparent); }
164
+ 50% { background-color: color-mix(in srgb, var(--ephia-color-streaming) 11%, transparent); }
165
+ }
166
+
167
+ /* ─── 1b. PLACEHOLDER — texte interim au démarrage (avant premier partial) ─
168
+ Visuel : points de suspension avec un shimmer horizontal qui simule
169
+ une onde traversant le texte en attendant l'arrivée du STT. */
170
+ .ephia-text--placeholder {
171
+ background: linear-gradient(
172
+ 90deg,
173
+ rgba(99, 102, 241, 0.2) 0%,
174
+ rgba(99, 102, 241, 0.7) 40%,
175
+ rgba(99, 102, 241, 0.2) 80%
176
+ );
177
+ background-size: 250% 100%;
178
+ -webkit-background-clip: text;
179
+ -webkit-text-fill-color: transparent;
180
+ background-clip: text;
181
+ animation: ephia-placeholder-shimmer 1.6s ease-in-out infinite;
182
+ font-style: italic;
183
+ }
184
+
185
+ @keyframes ephia-placeholder-shimmer {
186
+ 0% { background-position: 250% 0; }
187
+ 100% { background-position: -250% 0; }
188
+ }
189
+
190
+ /* ─── 2. COMMITTED — transition streaming → normal ─────────────────────────
191
+ Quand le backend valide le texte, le fond lavande glisse vers le vert
192
+ puis disparaît complètement. Feedback de confirmation visuelle. */
193
+ .ephia-committed,
194
+ .ephia-text--committed {
195
+ animation: ephia-committed-settle 1.2s ease-out forwards;
196
+ font-style: normal;
197
+ }
198
+
199
+ @keyframes ephia-committed-settle {
200
+ 0% {
201
+ background-color: color-mix(in srgb, var(--ephia-color-streaming) 8%, transparent);
202
+ border-bottom-color: color-mix(in srgb, var(--ephia-color-streaming) 30%, transparent);
203
+ }
204
+ 25% {
205
+ background-color: color-mix(in srgb, var(--ephia-color-committed) 18%, transparent);
206
+ border-bottom-color: color-mix(in srgb, var(--ephia-color-committed) 45%, transparent);
207
+ }
208
+ 100% {
209
+ background-color: transparent;
210
+ border-bottom-color: transparent;
211
+ }
212
+ }
213
+
214
+ /* ─── 3. REVISED — texte corrigé par le review pipeline ────────────────────
215
+ Visuel : fond ambre + ligne ondulée (style suggestion). S'efface après 2s.
216
+ L'utilisateur remarque que le backend a modifié quelque chose. */
217
+ .ephia-revised,
218
+ .ephia-text--revised {
219
+ background-color: color-mix(in srgb, var(--ephia-color-revised) 14%, transparent);
220
+ border-bottom: 2px wavy color-mix(in srgb, var(--ephia-color-revised) 45%, transparent);
221
+ border-radius: 2px;
222
+ padding: 0 2px;
223
+ margin: 0 -2px;
224
+ animation: ephia-revised-settle 2s ease-out forwards;
225
+ }
226
+
227
+ @keyframes ephia-revised-settle {
228
+ 0% {
229
+ background-color: color-mix(in srgb, var(--ephia-color-revised) 22%, transparent);
230
+ border-bottom-color: color-mix(in srgb, var(--ephia-color-revised) 55%, transparent);
231
+ }
232
+ 50% {
233
+ background-color: color-mix(in srgb, var(--ephia-color-revised) 10%, transparent);
234
+ border-bottom-color: color-mix(in srgb, var(--ephia-color-revised) 35%, transparent);
235
+ }
236
+ 100% {
237
+ background-color: transparent;
238
+ border-bottom-color: transparent;
239
+ }
240
+ }
241
+
242
+ /* ─── 5. ERROR — erreur de transcription ─────────────────────────────────── */
243
+ .ephia-error {
244
+ color: #dc2626;
245
+ text-decoration: line-through;
246
+ }
247
+
248
+ /* ═══════════════════════════════════════════════════════════════════════════
249
+ UTILITAIRES & A11Y
250
+ ═══════════════════════════════════════════════════════════════════════════ */
251
+
252
+ /* ─── Lock cursor TipTap / ProseMirror ───────────────────────────────────── */
253
+ [data-ephia-recording="true"] .ProseMirror {
254
+ caret-color: transparent !important;
255
+ }
256
+
257
+ /* ─── Curseur d'insertion ─────────────────────────────────────────────────── */
258
+ .ephia-insertion-cursor {
259
+ display: flex;
260
+ align-items: center;
261
+ justify-content: center;
262
+ }
263
+
264
+ @keyframes ephia-pulse {
265
+ 0%, 100% {
266
+ transform: translateX(-50%) scale(0.9);
267
+ opacity: 0.7;
268
+ }
269
+ 50% {
270
+ transform: translateX(-50%) scale(1.15);
271
+ opacity: 1;
272
+ }
273
+ }
274
+
275
+ /* ─── A11y : pas d'animations si reduce-motion ────────────────────────────── */
276
+ @media (prefers-reduced-motion: reduce) {
277
+ [data-ephia-recording="true"] {
278
+ animation: none;
279
+ }
280
+ [data-ephia-recording="true"]::before {
281
+ animation: none !important;
282
+ }
283
+ .ephia-insertion-cursor > * {
284
+ animation: none !important;
285
+ }
286
+ .ephia-reformat-flash,
287
+ .ephia-committed-flash {
288
+ animation: none;
289
+ opacity: 0.5;
290
+ }
291
+ .ephia-streaming,
292
+ .ephia-text--streaming {
293
+ animation: none;
294
+ background-color: color-mix(in srgb, var(--ephia-color-streaming) 8%, transparent);
295
+ }
296
+ .ephia-text--placeholder {
297
+ animation: none;
298
+ background: transparent;
299
+ -webkit-text-fill-color: color-mix(in srgb, var(--ephia-color-streaming) 50%, transparent);
300
+ color: color-mix(in srgb, var(--ephia-color-streaming) 50%, transparent);
301
+ }
302
+ .ephia-committed,
303
+ .ephia-text--committed {
304
+ animation: none;
305
+ background-color: transparent;
306
+ border-bottom-color: transparent;
307
+ }
308
+ .ephia-revised,
309
+ .ephia-text--revised {
310
+ animation: none;
311
+ background-color: color-mix(in srgb, var(--ephia-color-revised) 10%, transparent);
312
+ border-bottom-color: color-mix(in srgb, var(--ephia-color-revised) 40%, transparent);
313
+ }
314
+ .ephia-error {
315
+ color: #dc2626;
316
+ text-decoration: line-through;
317
+ }
318
+ }
@@ -0,0 +1,3 @@
1
+ export { useEphiaCodemirror } from "./useEphiaCodemirror";
2
+ export { useEphiaMonaco } from "./useEphiaMonaco";
3
+ export { useEphiaTiptap } from "./useEphiaTiptap";
@@ -0,0 +1,35 @@
1
+ import { useEffect, useRef } from "react";
2
+ import type { CMEditorView } from "../../../shared/types/editors";
3
+ import { CodeMirrorInstanceRegistry } from "../../registry/registries/CodeMirrorInstanceRegistry";
4
+ import type { UseEphiaTargetOptions } from "../useEphiaTarget";
5
+
6
+ /**
7
+ * Enregistre une instance CodeMirror 6 (`EditorView`) comme cible de dictée Ephia.
8
+ *
9
+ * @example
10
+ * const [view, setView] = useState<EditorView | null>(null);
11
+ * const ref = useEphiaCodemirror(view, { id: 'rapport', mode: 'write' });
12
+ *
13
+ * return <div ref={ref} />; // CodeMirror sera monté dans ce div
14
+ */
15
+ export function useEphiaCodemirror(
16
+ view: CMEditorView | null,
17
+ options: UseEphiaTargetOptions
18
+ ): React.RefObject<HTMLDivElement | null> {
19
+ const ref = useRef<HTMLDivElement>(null);
20
+
21
+ useEffect(() => {
22
+ if (!view || !ref.current) return;
23
+ const el = ref.current;
24
+ CodeMirrorInstanceRegistry.set(el, view);
25
+ return () => {
26
+ CodeMirrorInstanceRegistry.delete(el);
27
+ };
28
+ }, [view]);
29
+
30
+ // V1 registration removed — useEphiaCodemirror is deprecated in V2.
31
+ // Migrate to useEphiaTiptap or useEphiaTarget with a textarea.
32
+ void options;
33
+
34
+ return ref;
35
+ }
@@ -0,0 +1,35 @@
1
+ import { useEffect, useRef } from "react";
2
+ import type { MonacoEditorInstance } from "../../../shared/types/editors";
3
+ import { MonacoInstanceRegistry } from "../../registry/registries/MonacoInstanceRegistry";
4
+ import type { UseEphiaTargetOptions } from "../useEphiaTarget";
5
+
6
+ /**
7
+ * Enregistre une instance Monaco Editor comme cible de dictée Ephia.
8
+ *
9
+ * @example
10
+ * const editorRef = useRef<MonacoEditorInstance | null>(null);
11
+ * const containerRef = useEphiaMonaco(editorRef.current, { id: 'code', mode: 'write' });
12
+ *
13
+ * return <div ref={containerRef} style={{ height: 400 }} />;
14
+ */
15
+ export function useEphiaMonaco(
16
+ editor: MonacoEditorInstance | null,
17
+ options: UseEphiaTargetOptions
18
+ ): React.RefObject<HTMLDivElement | null> {
19
+ const ref = useRef<HTMLDivElement>(null);
20
+
21
+ useEffect(() => {
22
+ if (!editor || !ref.current) return;
23
+ const el = ref.current;
24
+ MonacoInstanceRegistry.set(el, editor);
25
+ return () => {
26
+ MonacoInstanceRegistry.delete(el);
27
+ };
28
+ }, [editor]);
29
+
30
+ // V1 registration removed — useEphiaMonaco is deprecated in V2.
31
+ // Migrate to useEphiaTiptap or useEphiaTarget with a textarea.
32
+ void options;
33
+
34
+ return ref;
35
+ }