@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,202 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import React, { createContext } from "react";
4
+ import { describe, expect, it, vi, afterEach } from "vitest";
5
+ import { cleanup, render } from "@testing-library/react";
6
+ import { EphiaContext } from "../react/provider/EphiaContext";
7
+ import type { EphiaContextValue } from "../react/provider/EphiaContext";
8
+ import type { EphiaRichEditorAdapter } from "./types";
9
+ import { useEphiaRichEditor } from "./use-ephia-rich-editor";
10
+
11
+ afterEach(cleanup);
12
+
13
+ function makeAdapter(identity: object): EphiaRichEditorAdapter {
14
+ return {
15
+ kind: "test",
16
+ identity,
17
+ createBinding: () => ({
18
+ kind: "test",
19
+ attach: vi.fn(),
20
+ detach: vi.fn(),
21
+ getText: () => "",
22
+ getEditorContext: () => ({
23
+ targetId: "",
24
+ documentEmpty: true,
25
+ insertionMode: "append" as const,
26
+ leftContext: "",
27
+ rightContext: "",
28
+ selectedText: null,
29
+ cursorOffset: 0,
30
+ }),
31
+ upsertSegment: vi.fn(),
32
+ removeSegment: vi.fn(),
33
+ removeSegments: vi.fn(),
34
+ }),
35
+ };
36
+ }
37
+
38
+ function makeContext(registerTarget: EphiaContextValue["registerTarget"]): EphiaContextValue {
39
+ return {
40
+ store: {} as any,
41
+ config: {},
42
+ registerTarget,
43
+ applyServerEvent: vi.fn(),
44
+ };
45
+ }
46
+
47
+ function Wrapper({
48
+ ctx,
49
+ children,
50
+ }: {
51
+ ctx: EphiaContextValue;
52
+ children: React.ReactNode;
53
+ }) {
54
+ return (
55
+ <EphiaContext.Provider value={ctx}>{children}</EphiaContext.Provider>
56
+ );
57
+ }
58
+
59
+ describe("useEphiaRichEditor", () => {
60
+ it("calls registerTarget once on mount", () => {
61
+ const unregister = vi.fn();
62
+ const registerTarget = vi.fn(() => unregister);
63
+ const ctx = makeContext(registerTarget);
64
+ const identity = {};
65
+ const adapter = makeAdapter(identity);
66
+
67
+ function Test() {
68
+ useEphiaRichEditor(adapter, { id: "report", label: "Report" });
69
+ return null;
70
+ }
71
+
72
+ render(
73
+ <Wrapper ctx={ctx}>
74
+ <Test />
75
+ </Wrapper>,
76
+ );
77
+
78
+ expect(registerTarget).toHaveBeenCalledTimes(1);
79
+ });
80
+
81
+ it("does not re-register when adapter object is recreated with same identity", () => {
82
+ const unregister = vi.fn();
83
+ const registerTarget = vi.fn(() => unregister);
84
+ const ctx = makeContext(registerTarget);
85
+ const identity = {};
86
+
87
+ function Test() {
88
+ // Simulate inline creation: new adapter object each render, same identity
89
+ const adapter = makeAdapter(identity);
90
+ useEphiaRichEditor(adapter, { id: "report", label: "Report" });
91
+ return null;
92
+ }
93
+
94
+ const { rerender } = render(
95
+ <Wrapper ctx={ctx}>
96
+ <Test />
97
+ </Wrapper>,
98
+ );
99
+ rerender(
100
+ <Wrapper ctx={ctx}>
101
+ <Test />
102
+ </Wrapper>,
103
+ );
104
+ rerender(
105
+ <Wrapper ctx={ctx}>
106
+ <Test />
107
+ </Wrapper>,
108
+ );
109
+
110
+ expect(registerTarget).toHaveBeenCalledTimes(1);
111
+ });
112
+
113
+ it("re-registers when identity changes", () => {
114
+ const unregister = vi.fn();
115
+ const registerTarget = vi.fn(() => unregister);
116
+ const ctx = makeContext(registerTarget);
117
+
118
+ const identityA = {};
119
+ const identityB = {};
120
+
121
+ function Test({ which }: { which: "a" | "b" }) {
122
+ const identity = which === "a" ? identityA : identityB;
123
+ useEphiaRichEditor(makeAdapter(identity), { id: "report" });
124
+ return null;
125
+ }
126
+
127
+ const { rerender } = render(
128
+ <Wrapper ctx={ctx}>
129
+ <Test which="a" />
130
+ </Wrapper>,
131
+ );
132
+
133
+ expect(registerTarget).toHaveBeenCalledTimes(1);
134
+
135
+ rerender(
136
+ <Wrapper ctx={ctx}>
137
+ <Test which="b" />
138
+ </Wrapper>,
139
+ );
140
+
141
+ expect(registerTarget).toHaveBeenCalledTimes(2);
142
+ });
143
+
144
+ it("calls cleanup (unregister) on unmount", () => {
145
+ const unregister = vi.fn();
146
+ const registerTarget = vi.fn(() => unregister);
147
+ const ctx = makeContext(registerTarget);
148
+ const adapter = makeAdapter({});
149
+
150
+ function Test() {
151
+ useEphiaRichEditor(adapter, { id: "report" });
152
+ return null;
153
+ }
154
+
155
+ const { unmount } = render(
156
+ <Wrapper ctx={ctx}>
157
+ <Test />
158
+ </Wrapper>,
159
+ );
160
+
161
+ expect(unregister).not.toHaveBeenCalled();
162
+ unmount();
163
+ expect(unregister).toHaveBeenCalledTimes(1);
164
+ });
165
+
166
+ it("does not register when enabled is false", () => {
167
+ const registerTarget = vi.fn(() => vi.fn());
168
+ const ctx = makeContext(registerTarget);
169
+ const adapter = makeAdapter({});
170
+
171
+ function Test() {
172
+ useEphiaRichEditor(adapter, { id: "report", enabled: false });
173
+ return null;
174
+ }
175
+
176
+ render(
177
+ <Wrapper ctx={ctx}>
178
+ <Test />
179
+ </Wrapper>,
180
+ );
181
+
182
+ expect(registerTarget).not.toHaveBeenCalled();
183
+ });
184
+
185
+ it("does not register when adapter is null", () => {
186
+ const registerTarget = vi.fn(() => vi.fn());
187
+ const ctx = makeContext(registerTarget);
188
+
189
+ function Test() {
190
+ useEphiaRichEditor(null, { id: "report" });
191
+ return null;
192
+ }
193
+
194
+ render(
195
+ <Wrapper ctx={ctx}>
196
+ <Test />
197
+ </Wrapper>,
198
+ );
199
+
200
+ expect(registerTarget).not.toHaveBeenCalled();
201
+ });
202
+ });
@@ -0,0 +1,47 @@
1
+ import { useEffect, useRef } from "react";
2
+ import { useEphiaContext } from "../react/provider/EphiaContext";
3
+ import { useEphiaInternal } from "../react/provider/EphiaInternalContext";
4
+ import type { EphiaRichEditorAdapter, UseEphiaRichEditorOptions } from "./types";
5
+
6
+ /**
7
+ * Registers a rich text editor via an adapter as a V2 Ephia target.
8
+ *
9
+ * @example
10
+ * const ephiaRef = useEphiaRichEditor(
11
+ * createTiptapEphiaAdapter(editor),
12
+ * { id: "report", label: "Compte rendu" },
13
+ * );
14
+ *
15
+ * return (
16
+ * <div ref={ephiaRef}>
17
+ * <EditorContent editor={editor} />
18
+ * </div>
19
+ * );
20
+ */
21
+ export function useEphiaRichEditor(
22
+ adapter: EphiaRichEditorAdapter | null,
23
+ options: UseEphiaRichEditorOptions,
24
+ ): React.RefObject<HTMLDivElement | null> {
25
+ const ref = useRef<HTMLDivElement>(null);
26
+ const { registerTarget } = useEphiaContext();
27
+ const { clientEpoch } = useEphiaInternal();
28
+ const { id, mode, label, enabled } = options;
29
+
30
+ const adapterKind = adapter?.kind ?? null;
31
+ // Use identity if provided (stable per editor instance). Fall back to the
32
+ // adapter object itself so custom adapters without identity still work.
33
+ const adapterIdentity = adapter?.identity ?? adapter ?? null;
34
+
35
+ useEffect(() => {
36
+ if (enabled === false) return;
37
+ if (!adapter) return;
38
+
39
+ const binding = adapter.createBinding({ targetId: id, label });
40
+ return registerTarget(id, binding, {
41
+ mode: mode ?? "write",
42
+ element: adapter.getElement?.() ?? ref.current ?? undefined,
43
+ });
44
+ }, [adapterKind, adapterIdentity, enabled, id, label, mode, registerTarget, clientEpoch]);
45
+
46
+ return ref;
47
+ }
@@ -0,0 +1,45 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import {
3
+ DEFAULT_EPHIA_SDK_API_URL,
4
+ resolveEphiaSdkApiUrl,
5
+ } from "./endpoint";
6
+
7
+ describe("resolveEphiaSdkApiUrl", () => {
8
+ afterEach(() => {
9
+ vi.unstubAllEnvs();
10
+ });
11
+
12
+ it("uses explicit apiUrl when provided", () => {
13
+ expect(resolveEphiaSdkApiUrl("http://localhost:8000")).toBe(
14
+ "http://localhost:8000"
15
+ );
16
+ });
17
+
18
+ it("normalizes trailing slashes and /api/v* suffix on explicit url", () => {
19
+ expect(resolveEphiaSdkApiUrl("https://api.test/api/v1/")).toBe(
20
+ "https://api.test"
21
+ );
22
+ });
23
+
24
+ it("falls back to EPHIA_SDK_ENDPOINT when no explicit url", () => {
25
+ vi.stubEnv("EPHIA_SDK_ENDPOINT", "https://api.ephiak.app");
26
+ expect(resolveEphiaSdkApiUrl()).toBe("https://api.ephiak.app");
27
+ });
28
+
29
+ it("falls back to NEXT_PUBLIC_EPHIA_SDK_ENDPOINT when EPHIA_SDK_ENDPOINT is absent", () => {
30
+ vi.stubEnv("NEXT_PUBLIC_EPHIA_SDK_ENDPOINT", "https://preview.test");
31
+ expect(resolveEphiaSdkApiUrl()).toBe("https://preview.test");
32
+ });
33
+
34
+ it("prefers explicit apiUrl over env", () => {
35
+ vi.stubEnv("EPHIA_SDK_ENDPOINT", "https://api.ephiak.app");
36
+ expect(resolveEphiaSdkApiUrl("http://localhost:8000")).toBe(
37
+ "http://localhost:8000"
38
+ );
39
+ });
40
+
41
+ it("defaults to production api.ephia.app", () => {
42
+ expect(resolveEphiaSdkApiUrl()).toBe(DEFAULT_EPHIA_SDK_API_URL);
43
+ expect(DEFAULT_EPHIA_SDK_API_URL).toBe("https://api.ephia.app");
44
+ });
45
+ });
@@ -0,0 +1,39 @@
1
+ /** URL API Ephia par défaut (production). */
2
+ export const DEFAULT_EPHIA_SDK_API_URL = "https://api.ephia.app";
3
+
4
+ function normalizeApiUrl(raw: string): string {
5
+ return raw.trim().replace(/\/+$/, "").replace(/\/api\/v\d+$/, "");
6
+ }
7
+
8
+ function readEnvEndpoint(): string | undefined {
9
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
+ const env = (typeof globalThis !== "undefined" && (globalThis as any).process?.env) as Record<string, string | undefined> | undefined;
11
+ if (!env) return undefined;
12
+
13
+ const fromEnv = env["EPHIA_SDK_ENDPOINT"]?.trim();
14
+ if (fromEnv) return fromEnv;
15
+
16
+ // Next.js n'expose côté client que les variables NEXT_PUBLIC_*.
17
+ const fromNextPublic = env["NEXT_PUBLIC_EPHIA_SDK_ENDPOINT"]?.trim();
18
+ if (fromNextPublic) return fromNextPublic;
19
+
20
+ return undefined;
21
+ }
22
+
23
+ /**
24
+ * Résout l'URL API utilisée par le SDK.
25
+ *
26
+ * Priorité :
27
+ * 1. `apiUrl` explicite (ex. localhost en dev)
28
+ * 2. `EPHIA_SDK_ENDPOINT` ou `NEXT_PUBLIC_EPHIA_SDK_ENDPOINT`
29
+ * 3. {@link DEFAULT_EPHIA_SDK_API_URL}
30
+ */
31
+ export function resolveEphiaSdkApiUrl(explicitApiUrl?: string | null): string {
32
+ const explicit = explicitApiUrl?.trim();
33
+ if (explicit) return normalizeApiUrl(explicit);
34
+
35
+ const fromEnv = readEnvEndpoint();
36
+ if (fromEnv) return normalizeApiUrl(fromEnv);
37
+
38
+ return DEFAULT_EPHIA_SDK_API_URL;
39
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Schéma Zod de validation de la configuration du SDK.
3
+ * SSR-safe : pas de z.instanceof(Element) qui plante en Node/SSR.
4
+ */
5
+
6
+ import { z } from "zod";
7
+
8
+ const elementSchema = z.custom<Element>(
9
+ (value) =>
10
+ typeof Element !== "undefined" && value instanceof Element,
11
+ { message: "Expected DOM Element" }
12
+ );
13
+
14
+ export const ephiaSessionOptionsSchema = z.object({
15
+ language: z.enum(["fr", "en"]).optional(),
16
+ mode: z.enum(["fast_xs", "fast_s", "smart_xs", "smart_s"]).optional(),
17
+ debugChunks: z.boolean().optional(),
18
+ });
19
+
20
+ export const ephiaClientConfigSchema = z.object({
21
+ apiUrl: z.string().url(),
22
+ apiKey: z.string().optional(),
23
+ bearerToken: z.string().optional(),
24
+ clientType: z.string().optional(),
25
+ session: ephiaSessionOptionsSchema.optional(),
26
+ debug: z.boolean().optional(),
27
+ noiseFilter: z.boolean().optional(),
28
+ target: z.union([z.string(), elementSchema, z.null()]).optional(),
29
+ });
30
+
31
+ export type EphiaClientConfig = z.infer<typeof ephiaClientConfigSchema>;
32
+ export type EphiaSessionOptionsConfig = z.infer<typeof ephiaSessionOptionsSchema>;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Returns the text to apply to DOM/state after a finalized or committed
3
+ * transcript event. An empty delta is intentional and must not fall back to
4
+ * the raw provider text.
5
+ */
6
+ export function pickEffectiveText(payload: {
7
+ text: string;
8
+ delta?: string | null;
9
+ }): string {
10
+ return payload.delta !== undefined && payload.delta !== null
11
+ ? payload.delta
12
+ : payload.text;
13
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Erreurs typées du SDK Ephia.
3
+ */
4
+
5
+ export type EphiaSdkErrorCode =
6
+ | "config.invalid"
7
+ | "session.create_failed"
8
+ | "session.token_missing"
9
+ | "transport.connect_failed"
10
+ | "transport.reconnecting"
11
+ | "transport.disconnected"
12
+ | "transport.not_connected"
13
+ | "audio.permission_denied"
14
+ | "audio.no_input_device"
15
+ | "audio.device_in_use"
16
+ | "audio.track_publish_failed"
17
+ | "audio.silence_detected"
18
+ | "protocol.invalid_event"
19
+ | "protocol.seq_gap"
20
+ | "protocol.seq_gap_sync_failed"
21
+ | "client.invalid_state"
22
+ | "client.start_failed"
23
+ | "client.finalization_timeout"
24
+ | "binding.target_not_found"
25
+ | "binding.unsupported_editor";
26
+
27
+ export class EphiaSdkError extends Error {
28
+ constructor(
29
+ public code: EphiaSdkErrorCode,
30
+ message: string,
31
+ public details?: Record<string, unknown>
32
+ ) {
33
+ super(message);
34
+ this.name = "EphiaSdkError";
35
+ }
36
+
37
+ toJSON(): Record<string, unknown> {
38
+ return {
39
+ code: this.code,
40
+ message: this.message,
41
+ details: this.details,
42
+ };
43
+ }
44
+ }
45
+
46
+ export function toEphiaError(error: unknown): { code: string; message: string } {
47
+ if (error instanceof EphiaSdkError) {
48
+ return { code: error.code, message: error.message };
49
+ }
50
+ if (error instanceof Error) {
51
+ return { code: "client.start_failed", message: error.message };
52
+ }
53
+ return { code: "client.start_failed", message: String(error) };
54
+ }
@@ -0,0 +1,40 @@
1
+ export type EphiaLang = "fr" | "en";
2
+
3
+ const MESSAGES: Record<string, Record<EphiaLang, string>> = {
4
+ "client.start_failed": {
5
+ fr: "Échec du démarrage",
6
+ en: "Start failed",
7
+ },
8
+ "client.finalization_timeout": {
9
+ fr: "Timeout pendant la finalisation",
10
+ en: "Finalization timeout",
11
+ },
12
+ "client.stop_failed": {
13
+ fr: "Échec de l'arrêt",
14
+ en: "Stop failed",
15
+ },
16
+ "audio.no_input_device": {
17
+ fr: "getUserMedia n'est pas disponible dans ce contexte",
18
+ en: "getUserMedia is not available in this browser/context",
19
+ },
20
+ "session.create_failed": {
21
+ fr: "Échec de création de session",
22
+ en: "Failed to create session",
23
+ },
24
+ "protocol.invalid_event": {
25
+ fr: "Événement invalide du serveur",
26
+ en: "Invalid event from server",
27
+ },
28
+ "provider.not_initialized": {
29
+ fr: "Provider non initialisé",
30
+ en: "Provider not initialized",
31
+ },
32
+ "transport.disconnect_timeout": {
33
+ fr: "Déconnexion trop lente, abandon",
34
+ en: "Disconnect too slow, giving up",
35
+ },
36
+ };
37
+
38
+ export function getMessage(code: string, lang: EphiaLang = "fr"): string {
39
+ return MESSAGES[code]?.[lang] ?? code;
40
+ }
@@ -0,0 +1,27 @@
1
+ // Errors
2
+ export { EphiaSdkError } from "./errors/EphiaSdkError";
3
+ export type { EphiaSdkErrorCode } from "./errors/EphiaSdkError";
4
+ export { getMessage } from "./errors/messages";
5
+ export type { EphiaLang } from "./errors/messages";
6
+
7
+ // Config / Schema
8
+ export {
9
+ DEFAULT_EPHIA_SDK_API_URL,
10
+ resolveEphiaSdkApiUrl,
11
+ } from "./config/endpoint";
12
+ export {
13
+ ephiaClientConfigSchema,
14
+ ephiaSessionOptionsSchema,
15
+ } from "./config/schema";
16
+ export type {
17
+ EphiaClientConfig,
18
+ EphiaSessionOptionsConfig,
19
+ } from "./config/schema";
20
+
21
+ // Types
22
+ export type { Mode, EphiaSessionOptions } from "./types/session";
23
+ export { pickEffectiveText } from "./effective-text";
24
+
25
+ // State
26
+ export type { EphiaAudioPermission, EphiaAudioState } from "./state/audio-state";
27
+ export { initialAudioState } from "./state/audio-state";
@@ -0,0 +1,45 @@
1
+ /**
2
+ * État audio local (micro, niveau, silence, permission).
3
+ */
4
+
5
+ export type EphiaAudioPermission = "unknown" | "prompt" | "granted" | "denied";
6
+
7
+ export type EphiaAudioState = {
8
+ permission: EphiaAudioPermission;
9
+
10
+ inputDeviceId?: string;
11
+ inputDeviceLabel?: string;
12
+ hasInputDevice: boolean;
13
+
14
+ level: number; // 0..1
15
+ rms: number;
16
+ peak: number;
17
+
18
+ isSilent: boolean;
19
+ silenceDurationMs: number;
20
+
21
+ muted: boolean;
22
+ localAudioPublished: boolean;
23
+ /** true si le VADGate détecte de la parole active */
24
+ speaking: boolean;
25
+ /** True dès que getUserMedia a réussi, même si le transport n'est pas encore connecté. */
26
+ micReady?: boolean;
27
+
28
+ error?: {
29
+ code: string;
30
+ message: string;
31
+ };
32
+ };
33
+
34
+ export const initialAudioState: EphiaAudioState = {
35
+ permission: "unknown",
36
+ hasInputDevice: false,
37
+ level: 0,
38
+ rms: 0,
39
+ peak: 0,
40
+ isSilent: false,
41
+ silenceDurationMs: 0,
42
+ muted: false,
43
+ localAudioPublished: false,
44
+ speaking: false,
45
+ };
@@ -0,0 +1,2 @@
1
+ export { initialAudioState } from "./audio-state";
2
+ export type { EphiaAudioPermission, EphiaAudioState } from "./audio-state";
@@ -0,0 +1,32 @@
1
+ import { create } from "zustand";
2
+ import { subscribeWithSelector } from "zustand/middleware";
3
+
4
+ export interface DocumentSection {
5
+ id: string;
6
+ title: string;
7
+ text: string;
8
+ level: number;
9
+ start: number;
10
+ end: number;
11
+ }
12
+
13
+ export interface DocumentStoreState {
14
+ content: string;
15
+ segments: DocumentSection[];
16
+ selectedText: string | null;
17
+ cursorSection: string | null;
18
+
19
+ syncFromEditor: (content: string, segments: DocumentSection[]) => void;
20
+ setSelection: (text: string | null, section: string | null) => void;
21
+ }
22
+
23
+ export const documentStore = create<DocumentStoreState>()(
24
+ subscribeWithSelector((set) => ({
25
+ content: "",
26
+ segments: [],
27
+ selectedText: null,
28
+ cursorSection: null,
29
+ syncFromEditor: (content, segments) => set({ content, segments }),
30
+ setSelection: (text, section) => set({ selectedText: text, cursorSection: section }),
31
+ }))
32
+ );
@@ -0,0 +1,2 @@
1
+ export { documentStore } from "./document-store";
2
+ export type { DocumentSection, DocumentStoreState } from "./document-store";
@@ -0,0 +1,28 @@
1
+ // Duck-type minimal — pas d'import @codemirror/view pour ne pas créer de dépendance.
2
+ export interface CMEditorView {
3
+ state: {
4
+ doc: { toString(): string; length: number };
5
+ selection: { main: { head: number; anchor: number; from: number; to: number; empty: boolean } };
6
+ };
7
+ dispatch(tr: object): void;
8
+ dom: HTMLElement;
9
+ contentDOM: HTMLElement;
10
+ }
11
+
12
+ // Duck-type minimal — pas d'import monaco-editor pour ne pas créer de dépendance.
13
+ export interface MonacoEditorInstance {
14
+ getValue(): string;
15
+ getModel(): {
16
+ applyEdits(edits: Array<{ range: { startLineNumber: number; startColumn: number; endLineNumber: number; endColumn: number }; text: string }>): void;
17
+ getOffsetAt(position: { lineNumber: number; column: number }): number;
18
+ getPositionAt(offset: number): { lineNumber: number; column: number };
19
+ getValueLength(): number;
20
+ } | null;
21
+ getPosition(): { lineNumber: number; column: number } | null;
22
+ setPosition(position: { lineNumber: number; column: number }): void;
23
+ getSelection(): { isEmpty(): boolean; startLineNumber: number; startColumn: number; endLineNumber: number; endColumn: number } | null;
24
+ getDomNode(): HTMLElement | null;
25
+ getScrolledVisiblePosition(position: { lineNumber: number; column: number }): { top: number; left: number; height: number } | null;
26
+ executeEdits(source: string, edits: Array<{ range: { startLineNumber: number; startColumn: number; endLineNumber: number; endColumn: number }; text: string }>): boolean;
27
+ deltaDecorations(oldDecorations: string[], newDecorations: Array<{ range: { startLineNumber: number; startColumn: number; endLineNumber: number; endColumn: number }; options: { inlineClassName?: string; isWholeLine?: boolean } }>): string[];
28
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Types pour la configuration d'une session de transcription.
3
+ */
4
+
5
+ export type Mode = "fast_xs" | "fast_s" | "smart_xs" | "smart_s";
6
+
7
+ export type EphiaSessionOptions = {
8
+ language?: string;
9
+ /** Mode global : fast_xs | fast_s | smart_xs | smart_s */
10
+ mode?: Mode;
11
+ debugChunks?: boolean;
12
+ };
package/src/style.css ADDED
@@ -0,0 +1,2 @@
1
+ @import "./ui/theme.css";
2
+ @import "./react/ephia-react.css";