@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,367 @@
1
+ import { useCallback, useSyncExternalStore } from "react";
2
+ import { useStore } from "zustand";
3
+ import { useShallow } from "zustand/react/shallow";
4
+ import { useEphiaContext, useOptionalEphiaContext } from "../provider/EphiaContext";
5
+ import { useEphiaInternal } from "../provider/EphiaInternalContext";
6
+ import type { EphiaError, EphiaStatus, EphiaPartial } from "../store/types";
7
+ import type { EphiaConnectionState } from "../../core/connection/connection-state";
8
+ import type { EphiaAudioState } from "../../shared/state/audio-state";
9
+ import type { EphiaStartOptions, EphiaStartResult } from "../../core/client/client-options";
10
+ import type { EphiaServerEvent, ResetReason } from "ephia-protocol";
11
+ import { getMessage } from "../../shared/errors/messages";
12
+ import { toEphiaError } from "../../shared/errors/EphiaSdkError";
13
+
14
+ const FINALIZATION_TIMEOUT_MS = 30_000;
15
+
16
+ // ─── Hook principal ───────────────────────────────────────────────────────────
17
+
18
+ export interface UseEphiaReturn {
19
+ // État
20
+ status: EphiaStatus;
21
+ sessionId: string | null;
22
+ activeTargetId: string | null;
23
+ error: { code: string; message: string } | null;
24
+
25
+ // Booléens commodité
26
+ isIdle: boolean;
27
+ isRecording: boolean;
28
+ isProcessing: boolean;
29
+ isBusy: boolean;
30
+ hasError: boolean;
31
+
32
+ // Actions
33
+ start: (targetId?: string, options?: EphiaStartOptions) => Promise<EphiaStartResult>;
34
+ stop: () => Promise<void>;
35
+ toggle: (targetId?: string, options?: EphiaStartOptions) => Promise<void>;
36
+
37
+ pause: () => Promise<void>;
38
+ resume: () => Promise<void>;
39
+ endSession: () => Promise<void>;
40
+ warmupMic: () => Promise<void>;
41
+
42
+ setActiveTarget: (id: string) => void;
43
+ resetTarget: (id?: string, reason?: ResetReason) => Promise<void>;
44
+ resetSession: (reason?: ResetReason) => Promise<void>;
45
+ sendEditorContext: (id?: string) => Promise<void>;
46
+ applyServerEvent: (event: EphiaServerEvent) => void;
47
+ }
48
+
49
+ export function useEphia(): UseEphiaReturn {
50
+ const { store, applyServerEvent } = useEphiaContext();
51
+ const internal = useEphiaInternal();
52
+
53
+ const status = useStore(store, (s) => s.status);
54
+ const sessionId = useStore(store, (s) => s.sessionId);
55
+ const activeTargetId = useStore(store, (s) => s.activeTargetId);
56
+ const error = useStore(store, useShallow((s) => s.error));
57
+
58
+ const start = useCallback(
59
+ async (targetId?: string, options?: EphiaStartOptions): Promise<EphiaStartResult> => {
60
+ const client = internal.clientRef.current;
61
+ const targetManager = internal.targetManagerRef.current;
62
+ if (!client || !targetManager) {
63
+ throw new Error(getMessage("provider.not_initialized"));
64
+ }
65
+
66
+ const currentStatus = store.getState().status;
67
+ if (currentStatus !== "idle" && currentStatus !== "error") {
68
+ return { sessionId: store.getState().sessionId };
69
+ }
70
+
71
+ const clickTs = typeof window !== "undefined" ? window.__ephia_record_click_ts : undefined;
72
+ const startCallTs = Date.now();
73
+ const deltaFromClick = clickTs ? startCallTs - clickTs : undefined;
74
+
75
+ window.dispatchEvent(
76
+ new CustomEvent("ephia:sdk-debug", {
77
+ detail: {
78
+ type: "sdk.record.start_called",
79
+ sessionId: null,
80
+ payload: {
81
+ message: `start() appelé${deltaFromClick !== undefined ? ` (${deltaFromClick}ms après clic)` : ""}`,
82
+ deltaFromClick,
83
+ ts: startCallTs,
84
+ },
85
+ },
86
+ })
87
+ );
88
+
89
+ if (targetId) {
90
+ targetManager.setActive(targetId);
91
+ }
92
+
93
+ const activeTargetId = targetId ?? targetManager.getActiveTargetId() ?? undefined;
94
+ targetManager.prepareForDictation(activeTargetId);
95
+
96
+ store.getState()._setStatus("processing");
97
+
98
+ try {
99
+ const result = await client.start(
100
+ targetId ? { ...options, initialTargetId: targetId } : options
101
+ );
102
+ targetManager.syncActiveTarget();
103
+ targetManager.flushEditorContext(
104
+ targetId ?? targetManager.getActiveTargetId() ?? undefined
105
+ );
106
+ store.getState()._setStatus("recording");
107
+ store.getState()._setSessionId(result.sessionId);
108
+ return result;
109
+ } catch (err) {
110
+ store.getState()._setError(toEphiaError(err));
111
+ store.getState()._setStatus("error");
112
+ throw err;
113
+ }
114
+ },
115
+ [store, internal]
116
+ );
117
+
118
+ const stop = useCallback(async () => {
119
+ const client = internal.clientRef.current;
120
+ if (!client) return;
121
+
122
+ const current = store.getState().status;
123
+ // Permettre l'arrêt pendant le démarrage (processing) pour éviter qu'un
124
+ // push-to-talk très court laisse le micro ouvert (race start/stop).
125
+ if (current === "idle" || current === "error") return;
126
+
127
+ store.getState()._setStatus("processing");
128
+
129
+ try {
130
+ await client.stop();
131
+
132
+ const clientState = client.getState();
133
+
134
+ if (
135
+ clientState.status === "idle" ||
136
+ clientState.status === "paused" ||
137
+ clientState.status === "ended"
138
+ ) {
139
+ store.getState()._setStatus("idle");
140
+ } else if (store.getState().status === "processing") {
141
+ store.getState()._setStatus("idle");
142
+ }
143
+ } catch (err) {
144
+ store.getState()._setError(toEphiaError(err));
145
+ store.getState()._setStatus("error");
146
+ throw err;
147
+ }
148
+ }, [store, internal]);
149
+
150
+ const toggle = useCallback(
151
+ async (targetId?: string, options?: EphiaStartOptions) => {
152
+ const { status } = store.getState();
153
+ if (status === "recording") {
154
+ await stop();
155
+ } else {
156
+ await start(targetId, options);
157
+ }
158
+ },
159
+ [store, start, stop]
160
+ );
161
+
162
+ const pause = useCallback(async () => {
163
+ // V2 initial: pause is equivalent to stop (session stays alive on the backend/client).
164
+ await stop();
165
+ }, [stop]);
166
+
167
+ const resume = useCallback(
168
+ async (options?: EphiaStartOptions) => {
169
+ const client = internal.clientRef.current;
170
+ const targetManager = internal.targetManagerRef.current;
171
+ if (!client) return;
172
+
173
+ const clientStatus = client.getState().status;
174
+ if (clientStatus === "paused") {
175
+ targetManager?.prepareForDictation(
176
+ store.getState().activeTargetId ?? targetManager.getActiveTargetId() ?? undefined,
177
+ );
178
+ await client.start(options);
179
+ return;
180
+ }
181
+
182
+ // Fallback: if the client is not paused, start a fresh session.
183
+ await start(undefined, options);
184
+ },
185
+ [internal, start, store]
186
+ );
187
+
188
+ const endSession = useCallback(async () => {
189
+ const client = internal.clientRef.current;
190
+ if (!client) return;
191
+
192
+ const current = store.getState().status;
193
+ if (current !== "recording" && current !== "processing") return;
194
+
195
+ store.getState()._setStatus("processing");
196
+
197
+ const timeout = setTimeout(() => {
198
+ store.getState()._setStatus("error");
199
+ store.getState()._setError({
200
+ code: "client.finalization_timeout",
201
+ message: getMessage("client.finalization_timeout"),
202
+ });
203
+ }, FINALIZATION_TIMEOUT_MS);
204
+
205
+ try {
206
+ await client.endSession();
207
+ } catch (err) {
208
+ store.getState()._setError(toEphiaError(err));
209
+ store.getState()._setStatus("error");
210
+ throw err;
211
+ } finally {
212
+ clearTimeout(timeout);
213
+ }
214
+
215
+ const clientState = client.getState();
216
+ if (clientState.status === "idle") {
217
+ store.getState()._setStatus("idle");
218
+ return;
219
+ }
220
+
221
+ if (clientState.status === "error" || clientState.error) {
222
+ if (clientState.error) {
223
+ store.getState()._setError(clientState.error);
224
+ }
225
+ store.getState()._setStatus("error");
226
+ return;
227
+ }
228
+
229
+ if (store.getState().status === "processing") {
230
+ store.getState()._setStatus("idle");
231
+ }
232
+ }, [store, internal]);
233
+
234
+ const warmupMic = useCallback(async () => {
235
+ const client = internal.clientRef.current;
236
+ if (!client) return;
237
+ await client.warmupMic().catch(() => {});
238
+ }, [internal]);
239
+
240
+ const setActiveTarget = useCallback(
241
+ (id: string) => {
242
+ const targetManager = internal.targetManagerRef.current;
243
+ if (!targetManager) return;
244
+ targetManager.setActive(id);
245
+ },
246
+ [internal]
247
+ );
248
+
249
+ const sendEditorContext = useCallback(
250
+ async (id?: string) => {
251
+ const targetManager = internal.targetManagerRef.current;
252
+ if (!targetManager) return;
253
+ targetManager.flushEditorContext(id);
254
+ },
255
+ [internal]
256
+ );
257
+
258
+ const resetTarget = useCallback(
259
+ async (id?: string, reason: ResetReason = "user_explicit") => {
260
+ const targetManager = internal.targetManagerRef.current;
261
+ if (!targetManager) return;
262
+
263
+ const targetId = id ?? store.getState().activeTargetId;
264
+ if (!targetId) return;
265
+
266
+ targetManager.reset("target", targetId, reason);
267
+ },
268
+ [store, internal]
269
+ );
270
+
271
+ const resetSession = useCallback(
272
+ async (reason: ResetReason = "user_explicit") => {
273
+ const targetManager = internal.targetManagerRef.current;
274
+ if (!targetManager) return;
275
+ targetManager.reset("global", undefined, reason);
276
+ },
277
+ [internal]
278
+ );
279
+
280
+ return {
281
+ status,
282
+ sessionId,
283
+ activeTargetId,
284
+ error,
285
+ isIdle: status === "idle",
286
+ isRecording: status === "recording",
287
+ isProcessing: status === "processing",
288
+ isBusy: status !== "idle" && status !== "error",
289
+ hasError: status === "error",
290
+ start,
291
+ stop,
292
+ toggle,
293
+ pause,
294
+ resume,
295
+ endSession,
296
+ warmupMic,
297
+ setActiveTarget,
298
+ resetTarget,
299
+ resetSession,
300
+ sendEditorContext,
301
+ applyServerEvent,
302
+ };
303
+ }
304
+
305
+ // ─── Hooks sélecteurs légers (anti re-render) ─────────────────────────────────
306
+
307
+ /** Re-rend uniquement sur changement de status. */
308
+ export function useEphiaStatus(): EphiaStatus {
309
+ const { store } = useEphiaContext();
310
+ return useStore(store, (s) => s.status);
311
+ }
312
+
313
+ /** Comme useEphiaStatus mais retourne undefined en dehors d'un EphiaProvider. */
314
+ export function useOptionalEphiaStatus(): EphiaStatus | undefined {
315
+ const ctx = useOptionalEphiaContext();
316
+ return useSyncExternalStore(
317
+ (callback) => {
318
+ if (!ctx) return () => {};
319
+ return ctx.store.subscribe(callback);
320
+ },
321
+ () => (ctx ? ctx.store.getState().status : undefined),
322
+ () => undefined,
323
+ );
324
+ }
325
+
326
+ /** Re-rend uniquement sur changement d'erreur session (ex. session.error STT). */
327
+ export function useEphiaError(): EphiaError | null {
328
+ const { store } = useEphiaContext();
329
+ return useStore(store, (s) => s.error);
330
+ }
331
+
332
+ /** Re-rend sur chaque partial — à utiliser uniquement dans les composants d'affichage. */
333
+ export function useEphiaPartial(): EphiaPartial | null {
334
+ const { store } = useEphiaContext();
335
+ return useStore(store, useShallow((s) => s.partial));
336
+ }
337
+
338
+ /** Re-rend sur changement de connexion. */
339
+ export function useEphiaConnection(): EphiaConnectionState {
340
+ const { store } = useEphiaContext();
341
+ return useStore(store, useShallow((s) => s.connection));
342
+ }
343
+
344
+ /** Comme useEphiaConnection mais retourne undefined en dehors d'un EphiaProvider. */
345
+ export function useOptionalEphiaConnection(): EphiaConnectionState | undefined {
346
+ const ctx = useOptionalEphiaContext();
347
+ return useSyncExternalStore(
348
+ (callback) => {
349
+ if (!ctx) return () => {};
350
+ return ctx.store.subscribe(callback);
351
+ },
352
+ () => (ctx ? ctx.store.getState().connection : undefined),
353
+ () => undefined,
354
+ );
355
+ }
356
+
357
+ /** Re-rend sur changement de niveau audio. */
358
+ export function useEphiaAudioLevel(): EphiaAudioState {
359
+ const { store } = useEphiaContext();
360
+ return useStore(store, useShallow((s) => s.audio));
361
+ }
362
+
363
+ /** Re-rend uniquement sur changement d'état parole (VAD gate). */
364
+ export function useEphiaSpeaking(): boolean {
365
+ const { store } = useEphiaContext();
366
+ return useStore(store, (s) => s.audio.speaking);
367
+ }
@@ -0,0 +1,53 @@
1
+ import { useEffect, useMemo } from "react";
2
+ import type { EditorContext } from "ephia-protocol";
3
+ import type { EphiaBinding } from "../../core/bindings/EphiaBinding";
4
+ import { useEphiaContext } from "../provider/EphiaContext";
5
+
6
+ function createDiscardBinding(): EphiaBinding {
7
+ return {
8
+ kind: "discard",
9
+ attach() {},
10
+ detach() {},
11
+ getText() {
12
+ return "";
13
+ },
14
+ getEditorContext(targetId = ""): EditorContext {
15
+ return {
16
+ targetId,
17
+ documentEmpty: true,
18
+ insertionMode: "insert_at_cursor",
19
+ leftContext: "",
20
+ rightContext: "",
21
+ selectedText: null,
22
+ cursorOffset: 0,
23
+ };
24
+ },
25
+ previewSegment() {},
26
+ upsertSegment() {},
27
+ removeSegment() {},
28
+ removeSegments() {},
29
+ };
30
+ }
31
+
32
+ export interface UseEphiaDiscardTargetOptions {
33
+ enabled?: boolean;
34
+ }
35
+
36
+ /**
37
+ * Enregistre une cible no-op (segments ignorés côté éditeur).
38
+ * Utile en mode « event-only » : l'app affiche le transcript via useEphiaServerEvent
39
+ * sans textarea/TipTap, mais le backend route quand même vers un targetId.
40
+ */
41
+ export function useEphiaDiscardTarget(
42
+ id: string,
43
+ options: UseEphiaDiscardTargetOptions = {},
44
+ ): void {
45
+ const { registerTarget } = useEphiaContext();
46
+ const { enabled = true } = options;
47
+ const binding = useMemo(() => createDiscardBinding(), []);
48
+
49
+ useEffect(() => {
50
+ if (!enabled) return;
51
+ return registerTarget(id, binding, { mode: "write" });
52
+ }, [binding, enabled, id, registerTarget]);
53
+ }
@@ -0,0 +1,33 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef } from "react";
4
+ import type { EphiaServerEvent } from "ephia-protocol";
5
+ import { useEphiaInternal } from "../provider/EphiaInternalContext";
6
+
7
+ /**
8
+ * Subscribe to a specific server event emitted by the underlying SDK client.
9
+ *
10
+ * @example
11
+ * useEphiaServerEvent("segment.operation", (event) => {
12
+ * console.log("segment", event.payload.segmentId, event.payload.text);
13
+ * });
14
+ */
15
+ export function useEphiaServerEvent<T extends EphiaServerEvent["type"]>(
16
+ type: T,
17
+ handler: (event: Extract<EphiaServerEvent, { type: T }>) => void
18
+ ): void {
19
+ const { clientRef, clientEpoch } = useEphiaInternal();
20
+ const handlerRef = useRef(handler);
21
+ handlerRef.current = handler;
22
+
23
+ useEffect(() => {
24
+ const client = clientRef.current;
25
+ if (!client) return;
26
+
27
+ return client.onServerEvent((event) => {
28
+ if (event.type === type) {
29
+ handlerRef.current(event as Extract<EphiaServerEvent, { type: T }>);
30
+ }
31
+ });
32
+ }, [clientRef, clientEpoch, type]);
33
+ }
@@ -0,0 +1,47 @@
1
+ import { useEffect } from "react";
2
+ import { useEphiaContext } from "../provider/EphiaContext";
3
+ import { useEphiaInternal } from "../provider/EphiaInternalContext";
4
+ import { NativeBinding } from "../../core/bindings/native/NativeBinding";
5
+
6
+ export interface UseEphiaNativeTargetOptions {
7
+ id: string;
8
+ mode?: string;
9
+ label?: string;
10
+ enabled?: boolean;
11
+ }
12
+
13
+ /** @deprecated Use `UseEphiaNativeTargetOptions` instead. */
14
+ export type UseEphiaTargetOptions = UseEphiaNativeTargetOptions;
15
+
16
+ /**
17
+ * Registers a textarea or input element as a V2 Ephia target.
18
+ * Creates a NativeBinding and wires it to the TargetManager via context.
19
+ *
20
+ * @example
21
+ * const ref = useRef<HTMLTextAreaElement>(null)
22
+ * useEphiaNativeTarget(ref, { id: 'report', label: 'Compte rendu' })
23
+ * return <textarea ref={ref} />
24
+ */
25
+ export function useEphiaNativeTarget<T extends HTMLInputElement | HTMLTextAreaElement>(
26
+ ref: React.RefObject<T | null>,
27
+ options: UseEphiaNativeTargetOptions,
28
+ ): void {
29
+ const { registerTarget } = useEphiaContext();
30
+ const { clientEpoch } = useEphiaInternal();
31
+ const { id, mode, enabled } = options;
32
+
33
+ useEffect(() => {
34
+ if (enabled === false) return;
35
+ const el = ref.current;
36
+ if (!el) return;
37
+ return registerTarget(id, new NativeBinding(el), {
38
+ mode: mode ?? "write",
39
+ element: el,
40
+ });
41
+ }, [ref, id, mode, enabled, registerTarget, clientEpoch]);
42
+ }
43
+
44
+ /**
45
+ * @deprecated Use `useEphiaNativeTarget` for textarea/input fields.
46
+ */
47
+ export const useEphiaTarget = useEphiaNativeTarget;
@@ -0,0 +1,22 @@
1
+ import { useEffect, useReducer } from "react";
2
+ import { liveTranscriptReducer } from "../../core/transcript/transcript.reducer";
3
+ import type { LiveTranscriptAction } from "../../core/transcript/transcript.reducer";
4
+ import { initialLiveTranscriptState } from "../../core/transcript/transcript.state";
5
+ import type { LiveTranscriptState } from "../../core/transcript/transcript.state";
6
+
7
+ export function useEphiaTranscript(): {
8
+ state: LiveTranscriptState;
9
+ dispatch: (action: LiveTranscriptAction) => void;
10
+ } {
11
+ const [state, dispatch] = useReducer(liveTranscriptReducer, initialLiveTranscriptState);
12
+
13
+ useEffect(() => {
14
+ if (state.diagnostics.processingChunks === 0) return;
15
+ const id = setInterval(() => {
16
+ dispatch({ type: "tick" });
17
+ }, 200);
18
+ return () => clearInterval(id);
19
+ }, [dispatch, state.diagnostics.processingChunks]);
20
+
21
+ return { state, dispatch };
22
+ }
@@ -0,0 +1,58 @@
1
+ // Provider & Context
2
+ export { EphiaProvider } from "./provider/EphiaProvider";
3
+ export { EphiaAuto } from "./EphiaAuto";
4
+ export type { EphiaProviderProps } from "./provider/EphiaProvider";
5
+
6
+ // Hooks
7
+ export {
8
+ useEphia,
9
+ useEphiaStatus,
10
+ useOptionalEphiaStatus,
11
+ useEphiaError,
12
+ useEphiaPartial,
13
+ useEphiaConnection,
14
+ useOptionalEphiaConnection,
15
+ useEphiaAudioLevel,
16
+ useEphiaSpeaking,
17
+ } from "./hooks/useEphia";
18
+ export type { UseEphiaReturn } from "./hooks/useEphia";
19
+ export type { EphiaStartResult } from "../core/client/client-options";
20
+ export {
21
+ useEphiaNativeTarget,
22
+ useEphiaTarget,
23
+ } from "./hooks/useEphiaTarget";
24
+ export type {
25
+ UseEphiaNativeTargetOptions,
26
+ UseEphiaTargetOptions,
27
+ } from "./hooks/useEphiaTarget";
28
+ export { useEphiaTiptap } from "./hooks/targets/useEphiaTiptap";
29
+ export { useEphiaServerEvent } from "./hooks/useEphiaServerEvent";
30
+ export {
31
+ useEphiaDiscardTarget,
32
+ } from "./hooks/useEphiaDiscardTarget";
33
+ export type { UseEphiaDiscardTargetOptions } from "./hooks/useEphiaDiscardTarget";
34
+ export { EphiaV2PreviewMark, EphiaV2CommittedMark } from "../core/bindings/tiptap/TiptapBinding";
35
+ export {
36
+ EphiaSpeechMikeProvider,
37
+ useEphiaSpeechMike,
38
+ } from "../devices/speechmike";
39
+ export type {
40
+ EphiaSpeechMikeAction,
41
+ EphiaSpeechMikeAudioStatus,
42
+ EphiaSpeechMikeButton,
43
+ EphiaSpeechMikeDeviceInfo,
44
+ EphiaSpeechMikeError,
45
+ EphiaSpeechMikeHidStatus,
46
+ EphiaSpeechMikeLedIntent,
47
+ EphiaSpeechMikeProviderProps,
48
+ EphiaSpeechMikeState,
49
+ EphiaSpeechMikeStatus,
50
+ } from "../devices/speechmike";
51
+
52
+ // Components
53
+ export { EphiaDictationButton } from "./components/EphiaDictationButton";
54
+ export { EphiaStatusBar } from "./components/EphiaStatusBar";
55
+ export { EphiaTextarea } from "./components/EphiaTextarea";
56
+
57
+ // Store
58
+ export type { EphiaStatus } from "./store/types";
@@ -0,0 +1,63 @@
1
+ "use client";
2
+
3
+ import { createContext, useContext } from "react";
4
+ import type { EphiaServerEvent } from "ephia-protocol";
5
+ import type { EphiaStore } from "../store/create-ephia-store";
6
+ import type { EphiaBinding } from "../../core/bindings/EphiaBinding";
7
+
8
+ export interface EphiaProviderConfig {
9
+ apiUrl?: string;
10
+ apiKey?: string;
11
+ bearerToken?: string;
12
+ clientType?: string;
13
+ language?: string;
14
+ medicalDomain?: string;
15
+ }
16
+
17
+ export interface RegisterTargetOptions {
18
+ mode?: string;
19
+ element?: HTMLElement;
20
+ }
21
+
22
+ /**
23
+ * Public context value exposed by <EphiaProvider>.
24
+ *
25
+ * V2 rules:
26
+ * - no direct client access (getClient / clientRef removed)
27
+ * - no legacy registry (registryRef removed)
28
+ * - no voice-formatting config (handled backend-side)
29
+ *
30
+ * Consumers receive the reactive store and a target registration helper.
31
+ */
32
+ export interface EphiaContextValue {
33
+ store: EphiaStore;
34
+ /**
35
+ * Registers a V2 EphiaBinding with the TargetManager.
36
+ * Returns a cleanup function that unregisters and detaches the binding.
37
+ */
38
+ registerTarget: (id: string, binding: EphiaBinding, opts?: RegisterTargetOptions) => () => void;
39
+ /**
40
+ * Réapplique un événement serveur (replay backend / événements manqués).
41
+ * Délègue au TranscriptApplier interne du provider.
42
+ */
43
+ applyServerEvent: (event: EphiaServerEvent) => void;
44
+ config: EphiaProviderConfig;
45
+ }
46
+
47
+ export const EphiaContext = createContext<EphiaContextValue | null>(null);
48
+
49
+ export function useEphiaContext(): EphiaContextValue {
50
+ const ctx = useContext(EphiaContext);
51
+ if (!ctx) {
52
+ throw new Error(
53
+ "[ephia-audio] useEphia() doit être utilisé dans un <EphiaProvider>. " +
54
+ "Vérifiez que <EphiaProvider> est bien dans votre layout racine."
55
+ );
56
+ }
57
+ return ctx;
58
+ }
59
+
60
+ /** Returns the context value without throwing — null when outside a Provider. */
61
+ export function useOptionalEphiaContext(): EphiaContextValue | null {
62
+ return useContext(EphiaContext);
63
+ }
@@ -0,0 +1,32 @@
1
+ "use client";
2
+
3
+ import { createContext, useContext } from "react";
4
+ import type { EphiaStore } from "../store/create-ephia-store";
5
+ import type { EphiaAudioClient } from "../../core/client/EphiaAudioClient";
6
+ import type { TargetManager } from "../../core/targets/TargetManager";
7
+
8
+ /**
9
+ * Internal context used by SDK hooks that need direct access to the audio client
10
+ * and target manager. This context is intentionally NOT exported from the public
11
+ * package API so that consumers cannot bypass the V2 orchestration layer.
12
+ */
13
+ export interface EphiaInternalContextValue {
14
+ store: EphiaStore;
15
+ clientRef: React.MutableRefObject<EphiaAudioClient | null>;
16
+ targetManagerRef: React.MutableRefObject<TargetManager | null>;
17
+ /** Incrémenté à chaque (re)création du client audio — pour ré-abonner les hooks tardifs. */
18
+ clientEpoch: number;
19
+ }
20
+
21
+ export const EphiaInternalContext = createContext<EphiaInternalContextValue | null>(null);
22
+
23
+ export function useEphiaInternal(): EphiaInternalContextValue {
24
+ const ctx = useContext(EphiaInternalContext);
25
+ if (!ctx) {
26
+ throw new Error(
27
+ "[ephia-audio] useEphiaInternal() doit être utilisé dans un <EphiaProvider>. " +
28
+ "Vérifiez que <EphiaProvider> est bien dans votre layout racine."
29
+ );
30
+ }
31
+ return ctx;
32
+ }