@ephia/dova-sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +89 -0
- package/dist/EphiaBinding-BvRmlqqC.d.ts +36 -0
- package/dist/EphiaFloatingButton-CxiF86VW.d.ts +65 -0
- package/dist/EphiaTextarea-B4_CAVUg.d.ts +183 -0
- package/dist/NativeBinding-ChG0GeSz.d.ts +53 -0
- package/dist/TargetBinding-BKGQwUMc.d.ts +89 -0
- package/dist/TiptapBinding-B-agfV2H.d.ts +45 -0
- package/dist/Transport-zdeA4Pou.d.ts +63 -0
- package/dist/audio-state-kZ3KSvux.d.ts +39 -0
- package/dist/chunk-35AJK2IO.js +1 -0
- package/dist/chunk-35AJK2IO.js.map +1 -0
- package/dist/chunk-3LXZODL4.js +886 -0
- package/dist/chunk-3LXZODL4.js.map +1 -0
- package/dist/chunk-5IK5TLSK.js +67 -0
- package/dist/chunk-5IK5TLSK.js.map +1 -0
- package/dist/chunk-7E43RY75.js +9 -0
- package/dist/chunk-7E43RY75.js.map +1 -0
- package/dist/chunk-A5UEXJ5R.js +183 -0
- package/dist/chunk-A5UEXJ5R.js.map +1 -0
- package/dist/chunk-AEE554FT.js +51 -0
- package/dist/chunk-AEE554FT.js.map +1 -0
- package/dist/chunk-DIEWY3IT.js +1332 -0
- package/dist/chunk-DIEWY3IT.js.map +1 -0
- package/dist/chunk-EGIAN7FH.js +18 -0
- package/dist/chunk-EGIAN7FH.js.map +1 -0
- package/dist/chunk-EMOEAPVU.js +486 -0
- package/dist/chunk-EMOEAPVU.js.map +1 -0
- package/dist/chunk-IDC7FHIZ.js +40 -0
- package/dist/chunk-IDC7FHIZ.js.map +1 -0
- package/dist/chunk-ITJFN3VM.js +601 -0
- package/dist/chunk-ITJFN3VM.js.map +1 -0
- package/dist/chunk-K24GNU27.js +22 -0
- package/dist/chunk-K24GNU27.js.map +1 -0
- package/dist/chunk-LXMCRXXF.js +778 -0
- package/dist/chunk-LXMCRXXF.js.map +1 -0
- package/dist/chunk-MJCEOOLW.js +122 -0
- package/dist/chunk-MJCEOOLW.js.map +1 -0
- package/dist/chunk-N7U5M3VZ.js +33 -0
- package/dist/chunk-N7U5M3VZ.js.map +1 -0
- package/dist/chunk-PSYX674B.js +27 -0
- package/dist/chunk-PSYX674B.js.map +1 -0
- package/dist/chunk-RFQRV7ML.js +33 -0
- package/dist/chunk-RFQRV7ML.js.map +1 -0
- package/dist/chunk-THNHRV2B.js +18 -0
- package/dist/chunk-THNHRV2B.js.map +1 -0
- package/dist/chunk-VSLGR64U.js +62 -0
- package/dist/chunk-VSLGR64U.js.map +1 -0
- package/dist/chunk-W2ZP674X.js +346 -0
- package/dist/chunk-W2ZP674X.js.map +1 -0
- package/dist/chunk-YWZUMUYE.js +695 -0
- package/dist/chunk-YWZUMUYE.js.map +1 -0
- package/dist/client-options-Uo6jXO8k.d.ts +64 -0
- package/dist/connection-state-Bk33YprE.d.ts +32 -0
- package/dist/core/bindings/index.d.ts +24 -0
- package/dist/core/bindings/index.js +1025 -0
- package/dist/core/bindings/index.js.map +1 -0
- package/dist/core/index.d.ts +383 -0
- package/dist/core/index.js +1284 -0
- package/dist/core/index.js.map +1 -0
- package/dist/createEphiaClient-BhdZ183V.d.ts +69 -0
- package/dist/devices/speechmike/index.d.ts +148 -0
- package/dist/devices/speechmike/index.js +40 -0
- package/dist/devices/speechmike/index.js.map +1 -0
- package/dist/headless/index.d.ts +10 -0
- package/dist/headless/index.js +25 -0
- package/dist/headless/index.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +119 -0
- package/dist/index.js.map +1 -0
- package/dist/react/index.d.ts +38 -0
- package/dist/react/index.js +70 -0
- package/dist/react/index.js.map +1 -0
- package/dist/rich-editor/index.d.ts +46 -0
- package/dist/rich-editor/index.js +13 -0
- package/dist/rich-editor/index.js.map +1 -0
- package/dist/schema-B2ycPlNB.d.ts +87 -0
- package/dist/session-APaXR48R.d.ts +12 -0
- package/dist/shared/index.d.ts +16 -0
- package/dist/shared/index.js +30 -0
- package/dist/shared/index.js.map +1 -0
- package/dist/style.css +1093 -0
- package/dist/testing/index.d.ts +84 -0
- package/dist/testing/index.js +36 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/types-D5SXPSwR.d.ts +32 -0
- package/dist/ui/index.d.ts +30 -0
- package/dist/ui/index.js +34 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/useEphiaSpeechMike-CjD7DWnh.d.ts +64 -0
- package/package.json +110 -0
- package/src/core/audio/audio-worklet-source.ts +30 -0
- package/src/core/audio/index.ts +3 -0
- package/src/core/audio/voice-level-meter.test.ts +27 -0
- package/src/core/audio/voice-level-meter.ts +270 -0
- package/src/core/bindings/EphiaBinding.ts +41 -0
- package/src/core/bindings/SegmentBindingBridge.test.ts +422 -0
- package/src/core/bindings/SegmentBindingBridge.ts +377 -0
- package/src/core/bindings/TargetBinding.ts +142 -0
- package/src/core/bindings/adapters/NativeAdapter.test.ts +85 -0
- package/src/core/bindings/adapters/NativeAdapter.ts +216 -0
- package/src/core/bindings/adapters/ProseMirrorAdapter.ts +231 -0
- package/src/core/bindings/adapters/index.ts +2 -0
- package/src/core/bindings/binding-factory.ts +78 -0
- package/src/core/bindings/detect-editor-type.ts +87 -0
- package/src/core/bindings/index.ts +13 -0
- package/src/core/bindings/insertion-boundary.test.ts +38 -0
- package/src/core/bindings/insertion-boundary.ts +26 -0
- package/src/core/bindings/native/NativeBinding.test.ts +277 -0
- package/src/core/bindings/native/NativeBinding.ts +239 -0
- package/src/core/bindings/resolver.ts +18 -0
- package/src/core/bindings/targets/codemirror.binding.ts +293 -0
- package/src/core/bindings/targets/contenteditable.binding.ts +452 -0
- package/src/core/bindings/targets/index.ts +10 -0
- package/src/core/bindings/targets/monaco.binding.ts +315 -0
- package/src/core/bindings/targets/tiptap.binding.test.ts +417 -0
- package/src/core/bindings/targets/tiptap.binding.ts +1192 -0
- package/src/core/bindings/tiptap/TiptapBinding.test.ts +63 -0
- package/src/core/bindings/tiptap/TiptapBinding.ts +464 -0
- package/src/core/bindings/types.ts +41 -0
- package/src/core/client/EphiaAudioClient.ts +654 -0
- package/src/core/client/audio-capture.ts +263 -0
- package/src/core/client/client-options.ts +39 -0
- package/src/core/client/client-state.ts +18 -0
- package/src/core/client/constants.ts +23 -0
- package/src/core/client/session-api.ts +415 -0
- package/src/core/connection/connection-state.ts +78 -0
- package/src/core/connection/index.ts +6 -0
- package/src/core/index.ts +47 -0
- package/src/core/operations/textToDocumentOperations.test.ts +69 -0
- package/src/core/operations/textToDocumentOperations.ts +92 -0
- package/src/core/runtime/DictationRuntime.test.ts +578 -0
- package/src/core/runtime/DictationRuntime.ts +434 -0
- package/src/core/runtime/TranscriptApplier.test.ts +355 -0
- package/src/core/runtime/TranscriptApplier.ts +229 -0
- package/src/core/runtime/index.ts +18 -0
- package/src/core/session/index.ts +2 -0
- package/src/core/session/session-machine.test.ts +16 -0
- package/src/core/session/session-machine.ts +59 -0
- package/src/core/targets/EditorContextCollector.ts +71 -0
- package/src/core/targets/TargetManager.test.ts +194 -0
- package/src/core/targets/TargetManager.ts +194 -0
- package/src/core/targets/index.ts +10 -0
- package/src/core/text-processing/index.ts +11 -0
- package/src/core/text-processing/overlap.test.ts +35 -0
- package/src/core/text-processing/overlap.ts +101 -0
- package/src/core/text-processing/voice-formatting.normalizer.test.ts +132 -0
- package/src/core/text-processing/voice-formatting.normalizer.ts +284 -0
- package/src/core/transcript/client-transcript.reducer.ts +366 -0
- package/src/core/transcript/client-transcript.state.ts +25 -0
- package/src/core/transcript/index.ts +19 -0
- package/src/core/transcript/transcript.assembler.test.ts +205 -0
- package/src/core/transcript/transcript.assembler.ts +152 -0
- package/src/core/transcript/transcript.reducer.test.ts +199 -0
- package/src/core/transcript/transcript.reducer.ts +771 -0
- package/src/core/transcript/transcript.state.ts +123 -0
- package/src/core/transport/LiveKitTransport.publish.test.ts +226 -0
- package/src/core/transport/LiveKitTransport.ts +459 -0
- package/src/core/transport/MockTransport.ts +231 -0
- package/src/core/transport/Transport.ts +82 -0
- package/src/debug/sdk-debug-collector.ts +79 -0
- package/src/devices/index.ts +2 -0
- package/src/devices/speechmike/__tests__/EphiaSpeechMikeProvider.test.tsx +99 -0
- package/src/devices/speechmike/__tests__/speechmike-audio-resolver.test.ts +96 -0
- package/src/devices/speechmike/__tests__/speechmike-button-router.test.ts +66 -0
- package/src/devices/speechmike/__tests__/speechmike-device-manager.test.ts +201 -0
- package/src/devices/speechmike/__tests__/speechmike-led-controller.test.ts +68 -0
- package/src/devices/speechmike/browser.ts +80 -0
- package/src/devices/speechmike/constants.ts +74 -0
- package/src/devices/speechmike/dictation-support-loader.ts +81 -0
- package/src/devices/speechmike/index.ts +11 -0
- package/src/devices/speechmike/react/EphiaSpeechMikeContext.ts +34 -0
- package/src/devices/speechmike/react/EphiaSpeechMikeProvider.tsx +287 -0
- package/src/devices/speechmike/react/useEphiaSpeechMike.ts +26 -0
- package/src/devices/speechmike/speechmike-audio-resolver.ts +58 -0
- package/src/devices/speechmike/speechmike-button-router.ts +73 -0
- package/src/devices/speechmike/speechmike-device-manager.ts +461 -0
- package/src/devices/speechmike/speechmike-led-controller.ts +78 -0
- package/src/devices/speechmike/types.ts +96 -0
- package/src/dictation_support.d.ts +31 -0
- package/src/global.d.ts +10 -0
- package/src/headless/createEphiaClient.ts +220 -0
- package/src/headless/index.ts +18 -0
- package/src/index.ts +89 -0
- package/src/react/EphiaAuto.tsx +87 -0
- package/src/react/components/EphiaDictationButton.tsx +88 -0
- package/src/react/components/EphiaStatusBar.tsx +59 -0
- package/src/react/components/EphiaTextarea.tsx +295 -0
- package/src/react/ephia-react.css +318 -0
- package/src/react/hooks/targets/index.ts +3 -0
- package/src/react/hooks/targets/useEphiaCodemirror.ts +35 -0
- package/src/react/hooks/targets/useEphiaMonaco.ts +35 -0
- package/src/react/hooks/targets/useEphiaTiptap.ts +23 -0
- package/src/react/hooks/useEphia.lifecycle.test.tsx +389 -0
- package/src/react/hooks/useEphia.ts +367 -0
- package/src/react/hooks/useEphiaDiscardTarget.ts +53 -0
- package/src/react/hooks/useEphiaServerEvent.ts +33 -0
- package/src/react/hooks/useEphiaTarget.ts +47 -0
- package/src/react/hooks/useEphiaTranscript.ts +22 -0
- package/src/react/index.ts +58 -0
- package/src/react/provider/EphiaContext.ts +63 -0
- package/src/react/provider/EphiaInternalContext.ts +32 -0
- package/src/react/provider/EphiaProvider.tsx +373 -0
- package/src/react/registry/binding-factory.ts +7 -0
- package/src/react/registry/detect-editor-type.ts +2 -0
- package/src/react/registry/events.ts +37 -0
- package/src/react/registry/registries/CodeMirrorInstanceRegistry.ts +24 -0
- package/src/react/registry/registries/MonacoInstanceRegistry.ts +23 -0
- package/src/react/registry/registries/TargetRegistry.ts +327 -0
- package/src/react/registry/registries/TiptapInstanceRegistry.ts +43 -0
- package/src/react/registry/registries/index.ts +5 -0
- package/src/react/store/create-ephia-store.ts +36 -0
- package/src/react/store/types.ts +41 -0
- package/src/react/utils/flash-range.ts +24 -0
- package/src/react/utils/index.ts +1 -0
- package/src/rich-editor/adapters/tiptap.test.ts +86 -0
- package/src/rich-editor/adapters/tiptap.ts +23 -0
- package/src/rich-editor/index.ts +3 -0
- package/src/rich-editor/types.ts +24 -0
- package/src/rich-editor/use-ephia-rich-editor.test.tsx +202 -0
- package/src/rich-editor/use-ephia-rich-editor.ts +47 -0
- package/src/shared/config/endpoint.test.ts +45 -0
- package/src/shared/config/endpoint.ts +39 -0
- package/src/shared/config/schema.ts +32 -0
- package/src/shared/effective-text.ts +13 -0
- package/src/shared/errors/EphiaSdkError.ts +54 -0
- package/src/shared/errors/messages.ts +40 -0
- package/src/shared/index.ts +27 -0
- package/src/shared/state/audio-state.ts +45 -0
- package/src/shared/state/index.ts +2 -0
- package/src/shared/store/document-store.ts +32 -0
- package/src/shared/store/index.ts +2 -0
- package/src/shared/types/editors.ts +28 -0
- package/src/shared/types/session.ts +12 -0
- package/src/style.css +2 -0
- package/src/testing/index.tsx +60 -0
- package/src/ui/assets/ephia-logo.svg +4 -0
- package/src/ui/components/EphiaLogo.tsx +77 -0
- package/src/ui/index.ts +24 -0
- package/src/ui/primitives/Button.tsx +53 -0
- package/src/ui/primitives/Spinner.tsx +21 -0
- package/src/ui/primitives/index.ts +5 -0
- package/src/ui/recorder/EphiaFloatingButton.tsx +489 -0
- package/src/ui/recorder/MinimalProcessingBars.tsx +122 -0
- package/src/ui/recorder/StandardIntensityVisualizer.tsx +148 -0
- package/src/ui/recorder/appearance.ts +9 -0
- package/src/ui/recorder/index.ts +8 -0
- package/src/ui/theme.css +775 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/devices/speechmike/constants.ts","../src/devices/speechmike/dictation-support-loader.ts","../src/devices/speechmike/speechmike-audio-resolver.ts","../src/devices/speechmike/speechmike-button-router.ts","../src/devices/speechmike/browser.ts","../src/devices/speechmike/speechmike-led-controller.ts","../src/devices/speechmike/speechmike-device-manager.ts","../src/devices/speechmike/react/EphiaSpeechMikeProvider.tsx","../src/devices/speechmike/react/EphiaSpeechMikeContext.ts","../src/devices/speechmike/react/useEphiaSpeechMike.ts"],"sourcesContent":["import type { EphiaSpeechMikeAction, EphiaSpeechMikeLedIntent } from \"./types\";\n\nexport const SPEECHMIKE_VENDOR_IDS = {\n PHILIPS: 0x0911,\n} as const;\n\nexport const SPEECHMIKE_CONFIG = {\n BUTTON_DEBOUNCE_MS: 150,\n WATCHDOG_INTERVAL_MS: 10_000,\n DEVICE_OPERATION_TIMEOUT_MS: 2_000,\n REQUEST_DEVICE_TIMEOUT_MS: 15_000,\n LED_OPERATION_TIMEOUT_MS: 500,\n} as const;\n\nexport const LED_MODE = {\n OFF: 0,\n ON: 1,\n ON_BRIGHT: 3,\n} as const;\n\nexport const DEFAULT_BUTTON_MAPPING: Record<string, EphiaSpeechMikeAction> = {\n RECORD: \"record.toggle\",\n PLAY: \"play.toggle\",\n FORWARD: \"seek.forward\",\n REWIND: \"seek.backward\",\n\n F1: \"audio.discard\",\n F1_A: \"audio.discard\",\n F2: \"audio.discard\",\n F2_B: \"audio.discard\",\n\n F3: \"audio.submit\",\n F3_C: \"audio.submit\",\n F4: \"audio.submit\",\n F4_D: \"audio.submit\",\n\n INS: \"agent.open\",\n OVR: \"exam.new\",\n INS_OVR: \"exam.new\",\n\n EOL: \"resources.toggle\",\n EOL_PRIO: \"resources.toggle\",\n};\n\nconst ALL_LEDS_OFF: Record<number, number> = {\n 0: 0,\n 1: 0,\n 2: 0,\n 3: 0,\n 4: 0,\n 5: 0,\n 6: 0,\n 7: 0,\n 8: 0,\n 9: 0,\n};\n\nexport const DEFAULT_LED_INTENTS: Record<EphiaSpeechMikeLedIntent, Record<number, number>> = {\n off: { ...ALL_LEDS_OFF },\n idle: { ...ALL_LEDS_OFF, 2: LED_MODE.ON_BRIGHT },\n recording: { ...ALL_LEDS_OFF, 1: LED_MODE.ON_BRIGHT },\n processing: { ...ALL_LEDS_OFF, 2: LED_MODE.ON, 4: LED_MODE.ON },\n audio_available: {\n ...ALL_LEDS_OFF,\n 6: LED_MODE.ON_BRIGHT,\n 7: LED_MODE.ON_BRIGHT,\n 8: LED_MODE.ON_BRIGHT,\n 9: LED_MODE.ON_BRIGHT,\n },\n success: { ...ALL_LEDS_OFF, 2: 2 },\n error: { ...ALL_LEDS_OFF, 3: 2 },\n cancelled: { ...ALL_LEDS_OFF, 3: LED_MODE.ON_BRIGHT },\n};\n\n","export type DictationDevice = unknown;\n\nexport type DictationButtonCallback = (\n device: DictationDevice,\n bitMask: number,\n) => void;\n\nexport type DictationDeviceCallback = (device: DictationDevice) => void;\n\nexport type DictationMotionCallback = (\n device: DictationDevice,\n motion: number,\n) => void;\n\nexport type DictationDeviceManagerLike = {\n init?: () => Promise<void>;\n requestDevice: () => Promise<DictationDevice[]>;\n getDeviceInfo?: (device: DictationDevice) => Promise<unknown>;\n getDevices?: () => DictationDevice[];\n\n addButtonEventListener?: (cb: DictationButtonCallback) => void;\n removeButtonEventListener?: (cb: DictationButtonCallback) => void;\n\n addDeviceConnectedEventListener?: (cb: DictationDeviceCallback) => void;\n removeDeviceConnectedEventListener?: (cb: DictationDeviceCallback) => void;\n\n addDeviceDisconnectedEventListener?: (cb: DictationDeviceCallback) => void;\n removeDeviceDisconnectedEventListener?: (cb: DictationDeviceCallback) => void;\n\n addMotionEventListener?: (cb: DictationMotionCallback) => void;\n removeMotionEventListener?: (cb: DictationMotionCallback) => void;\n\n disconnect?: () => void;\n};\n\nexport type DictationSupportEnum = Record<number, string> & Record<string, number>;\n\nexport type DictationSupportRuntime = {\n DictationDeviceManager: new () => DictationDeviceManagerLike;\n ButtonEvent: DictationSupportEnum;\n MotionEvent?: DictationSupportEnum;\n};\n\nlet runtimePromise: Promise<DictationSupportRuntime> | null = null;\n\ntype DictationSupportModule = {\n DictationDeviceManager?: new () => DictationDeviceManagerLike;\n ButtonEvent?: DictationSupportEnum;\n MotionEvent?: DictationSupportEnum;\n};\n\nexport function resetDictationSupportLoaderForTests(): void {\n runtimePromise = null;\n}\n\nexport function loadDictationSupport(): Promise<DictationSupportRuntime> {\n if (runtimePromise) return runtimePromise;\n\n const specifier = \"dictation_support\";\n runtimePromise = import(/* @vite-ignore */ specifier)\n .then((mod: DictationSupportModule) => {\n if (!mod.DictationDeviceManager || !mod.ButtonEvent) {\n throw new Error(\"dictation_support exports are incomplete\");\n }\n\n return {\n DictationDeviceManager: mod.DictationDeviceManager,\n ButtonEvent: mod.ButtonEvent,\n MotionEvent: mod.MotionEvent,\n };\n })\n .catch((error: unknown) => {\n runtimePromise = null;\n throw Object.assign(new Error(\"dictation_support unavailable\"), {\n code: \"speechmike.driver_missing\",\n cause: error,\n });\n });\n\n return runtimePromise;\n}\n","export type SpeechMikeAudioMatch = {\n deviceId: string;\n label: string;\n score: number;\n raw: MediaDeviceInfo;\n};\n\nexport function scoreSpeechMikeAudioDevice(device: MediaDeviceInfo): number {\n if (device.kind !== \"audioinput\") return 0;\n\n const label = (device.label ?? \"\").toLowerCase();\n let score = 0;\n\n if (label.includes(\"speechmike\")) score += 100;\n if (label.includes(\"philips\")) score += 60;\n if (label.includes(\"dictation\")) score += 40;\n if (label.includes(\"microphone\")) score += 10;\n\n return score;\n}\n\nexport async function listAudioInputDevices(): Promise<MediaDeviceInfo[]> {\n if (typeof navigator === \"undefined\" || !navigator.mediaDevices?.enumerateDevices) {\n return [];\n }\n\n const devices = await navigator.mediaDevices.enumerateDevices();\n return devices.filter((device) => device.kind === \"audioinput\");\n}\n\nexport async function resolveSpeechMikeAudioInput(options?: {\n requestPermission?: boolean;\n}): Promise<SpeechMikeAudioMatch | null> {\n if (typeof navigator === \"undefined\" || !navigator.mediaDevices) return null;\n\n let permissionStream: MediaStream | null = null;\n if (options?.requestPermission) {\n permissionStream = await navigator.mediaDevices.getUserMedia({ audio: true });\n }\n\n try {\n const inputs = await listAudioInputDevices();\n const matches = inputs\n .map((device) => ({\n deviceId: device.deviceId,\n label: device.label,\n score: scoreSpeechMikeAudioDevice(device),\n raw: device,\n }))\n .filter((match) => match.score > 0)\n .sort((a, b) => b.score - a.score);\n\n return matches[0] ?? null;\n } finally {\n permissionStream?.getTracks().forEach((track) => track.stop());\n }\n}\n\n","import { DEFAULT_BUTTON_MAPPING, SPEECHMIKE_CONFIG } from \"./constants\";\nimport type { DictationSupportEnum } from \"./dictation-support-loader\";\nimport type { EphiaSpeechMikeAction } from \"./types\";\n\nexport type ButtonRouterEvent = {\n buttons: Set<string>;\n actions: EphiaSpeechMikeAction[];\n rawBitMask: number;\n};\n\nexport class SpeechMikeButtonRouter {\n private lastEventTime = 0;\n private readonly processedEvents = new Set<string>();\n private mapping: Record<string, EphiaSpeechMikeAction>;\n\n constructor(\n private readonly buttonEnum: DictationSupportEnum,\n mapping: Partial<Record<string, EphiaSpeechMikeAction>> = {},\n private readonly debounceMs = SPEECHMIKE_CONFIG.BUTTON_DEBOUNCE_MS,\n ) {\n this.mapping = mergeButtonMapping(mapping);\n }\n\n setMapping(mapping: Partial<Record<string, EphiaSpeechMikeAction>>): void {\n this.mapping = mergeButtonMapping(mapping);\n }\n\n parse(bitMask: number): ButtonRouterEvent | null {\n const now = Date.now();\n const buttons = new Set<string>();\n\n for (const value of Object.values(this.buttonEnum)) {\n const numeric = Number(value);\n if (Number.isNaN(numeric)) continue;\n\n if (bitMask & numeric) {\n const name = this.buttonEnum[numeric];\n if (name) buttons.add(name);\n }\n }\n\n if (buttons.size === 0) {\n return { buttons, actions: [], rawBitMask: bitMask };\n }\n\n if (now - this.lastEventTime < this.debounceMs) return null;\n\n const eventKey = `${[...buttons].join(\",\")}_${Math.floor(now / this.debounceMs)}`;\n if (this.processedEvents.has(eventKey)) return null;\n\n this.processedEvents.add(eventKey);\n if (typeof window !== \"undefined\") {\n window.setTimeout(() => this.processedEvents.delete(eventKey), 1000);\n }\n this.lastEventTime = now;\n\n const actions = [...buttons]\n .map((button) => this.mapping[button] ?? \"none\")\n .filter((action): action is Exclude<EphiaSpeechMikeAction, \"none\"> => action !== \"none\");\n\n return { buttons, actions, rawBitMask: bitMask };\n }\n}\n\nfunction mergeButtonMapping(\n mapping: Partial<Record<string, EphiaSpeechMikeAction>>,\n): Record<string, EphiaSpeechMikeAction> {\n const merged: Record<string, EphiaSpeechMikeAction> = { ...DEFAULT_BUTTON_MAPPING };\n for (const [button, action] of Object.entries(mapping)) {\n if (action !== undefined) merged[button] = action;\n }\n return merged;\n}\n","export type HidDeviceLike = {\n productName?: string;\n manufacturerName?: string;\n vendorId?: number;\n productId?: number;\n};\n\nexport type HidConnectionEventLike = Event & {\n device?: HidDeviceLike;\n};\n\nexport type HidLike = {\n getDevices: () => Promise<HidDeviceLike[]>;\n addEventListener: (\n type: \"connect\" | \"disconnect\",\n listener: (event: Event) => void,\n ) => void;\n removeEventListener: (\n type: \"connect\" | \"disconnect\",\n listener: (event: Event) => void,\n ) => void;\n};\n\ntype NavigatorWithHid = Navigator & {\n hid?: HidLike;\n};\n\nexport function isBrowser(): boolean {\n return typeof window !== \"undefined\" && typeof navigator !== \"undefined\";\n}\n\nexport function getNavigatorHid(): HidLike | null {\n if (!isBrowser()) return null;\n return (navigator as NavigatorWithHid).hid ?? null;\n}\n\nexport function isWebHidAvailable(): boolean {\n return getNavigatorHid() !== null;\n}\n\nexport function createTimeoutError(operation: string, timeoutMs: number): Error {\n return new Error(`${operation} timeout after ${timeoutMs}ms`);\n}\n\nexport function withTimeout<T>(\n promise: Promise<T>,\n timeoutMs: number,\n operation: string,\n): Promise<T> {\n if (typeof window === \"undefined\") return promise;\n\n let timeoutId: number | null = null;\n\n return Promise.race([\n promise.finally(() => {\n if (timeoutId !== null) window.clearTimeout(timeoutId);\n }),\n new Promise<T>((_, reject) => {\n timeoutId = window.setTimeout(() => {\n reject(createTimeoutError(operation, timeoutMs));\n }, timeoutMs);\n }),\n ]);\n}\n\nexport function matchesSpeechMikeIdentity(device: {\n productName?: string;\n manufacturerName?: string;\n vendorId?: number;\n productId?: number;\n}): boolean {\n const productName = device.productName?.toLowerCase() ?? \"\";\n const manufacturerName = device.manufacturerName?.toLowerCase() ?? \"\";\n\n return (\n productName.includes(\"speechmike\") ||\n productName.includes(\"philips\") ||\n manufacturerName.includes(\"philips\")\n );\n}\n","import { DEFAULT_LED_INTENTS, SPEECHMIKE_CONFIG } from \"./constants\";\nimport { withTimeout } from \"./browser\";\nimport type { EphiaSpeechMikeLedIntent } from \"./types\";\n\ntype LedCapableDevice = {\n setLed?: (index: number, mode: number) => Promise<void>;\n setLeds?: (states: Record<number, number>) => Promise<void>;\n};\n\nfunction isLedCapableDevice(device: unknown): device is LedCapableDevice {\n if (!device || typeof device !== \"object\") return false;\n return \"setLed\" in device || \"setLeds\" in device;\n}\n\nexport class SpeechMikeLedController {\n private queue: Promise<void> = Promise.resolve();\n\n constructor(private readonly getDevice: () => unknown | null) {}\n\n setIntent(intent: EphiaSpeechMikeLedIntent): void {\n this.setBatchQueued(DEFAULT_LED_INTENTS[intent]);\n }\n\n setBatchQueued(states: Record<number, number>): void {\n this.queue = this.queue\n .catch(() => undefined)\n .then(() => this.setBatch(states))\n .catch(() => undefined);\n }\n\n async setBatch(states: Record<number, number>): Promise<void> {\n const device = this.getLedDevice();\n if (!device) return;\n\n if (typeof device.setLeds === \"function\") {\n try {\n await withTimeout(\n device.setLeds(states),\n SPEECHMIKE_CONFIG.LED_OPERATION_TIMEOUT_MS,\n \"setLeds\",\n );\n return;\n } catch {\n // Fall back to individual LED calls.\n }\n }\n\n await Promise.allSettled(\n Object.entries(states).map(([index, mode]) => this.setLed(Number(index), mode)),\n );\n }\n\n async setLed(index: number, mode: number): Promise<boolean> {\n const device = this.getLedDevice();\n if (!device?.setLed) return false;\n\n try {\n await withTimeout(\n device.setLed(index, mode),\n SPEECHMIKE_CONFIG.LED_OPERATION_TIMEOUT_MS,\n `setLed(${index})`,\n );\n return true;\n } catch {\n return false;\n }\n }\n\n clear(): void {\n this.setIntent(\"off\");\n }\n\n private getLedDevice(): LedCapableDevice | null {\n const device = this.getDevice();\n return isLedCapableDevice(device) ? device : null;\n }\n}\n\n","import {\n getNavigatorHid,\n matchesSpeechMikeIdentity,\n withTimeout,\n type HidConnectionEventLike,\n type HidDeviceLike,\n} from \"./browser\";\nimport { SPEECHMIKE_CONFIG } from \"./constants\";\nimport {\n loadDictationSupport,\n type DictationButtonCallback,\n type DictationDevice,\n type DictationDeviceCallback,\n type DictationDeviceManagerLike,\n type DictationMotionCallback,\n type DictationSupportRuntime,\n} from \"./dictation-support-loader\";\nimport { SpeechMikeButtonRouter, type ButtonRouterEvent } from \"./speechmike-button-router\";\nimport type {\n EphiaSpeechMikeAction,\n EphiaSpeechMikeDeviceInfo,\n EphiaSpeechMikeError,\n EphiaSpeechMikeHidStatus,\n} from \"./types\";\n\nexport type SpeechMikeDeviceManagerEvents = {\n status: (status: EphiaSpeechMikeHidStatus) => void;\n device: (device: unknown | null) => void;\n deviceInfo: (info: EphiaSpeechMikeDeviceInfo | null) => void;\n presence: (present: boolean) => void;\n button: (event: ButtonRouterEvent) => void;\n error: (error: EphiaSpeechMikeError) => void;\n};\n\nexport type SpeechMikeDeviceManagerOptions = {\n buttonMapping?: Partial<Record<string, EphiaSpeechMikeAction>>;\n loadRuntime?: () => Promise<DictationSupportRuntime>;\n};\n\ntype EventListenerMap = {\n [K in keyof SpeechMikeDeviceManagerEvents]: Set<SpeechMikeDeviceManagerEvents[K]>;\n};\n\ntype DeviceWithHid = {\n hidDevice?: HidDeviceLike;\n};\n\nfunction toError(error: unknown, fallbackCode: string): EphiaSpeechMikeError {\n if (error && typeof error === \"object\" && \"code\" in error) {\n const code = String((error as { code?: unknown }).code ?? fallbackCode);\n const message = error instanceof Error ? error.message : String(error);\n return { code, message };\n }\n\n return {\n code: fallbackCode,\n message: error instanceof Error ? error.message : String(error),\n };\n}\n\nfunction toRecord(value: unknown): Record<string, unknown> | null {\n return value && typeof value === \"object\" ? (value as Record<string, unknown>) : null;\n}\n\nfunction getDeviceHidDevice(device: unknown): HidDeviceLike | null {\n const maybeDevice = toRecord(device) as DeviceWithHid | null;\n return maybeDevice?.hidDevice ?? null;\n}\n\nfunction sameHidDevice(a: HidDeviceLike | null | undefined, b: HidDeviceLike | null | undefined): boolean {\n if (!a || !b) return false;\n return a === b;\n}\n\nfunction extractDeviceInfo(value: unknown): EphiaSpeechMikeDeviceInfo | null {\n const record = toRecord(value);\n if (!record) return null;\n\n const productName = typeof record.productName === \"string\" ? record.productName : undefined;\n const manufacturerName =\n typeof record.manufacturerName === \"string\" ? record.manufacturerName : undefined;\n const vendorId = typeof record.vendorId === \"number\" ? record.vendorId : undefined;\n const productId = typeof record.productId === \"number\" ? record.productId : undefined;\n\n if (\n productName === undefined &&\n manufacturerName === undefined &&\n vendorId === undefined &&\n productId === undefined\n ) {\n return null;\n }\n\n return { productName, manufacturerName, vendorId, productId };\n}\n\nexport class SpeechMikeDeviceManager {\n private readonly listeners: EventListenerMap = {\n status: new Set(),\n device: new Set(),\n deviceInfo: new Set(),\n presence: new Set(),\n button: new Set(),\n error: new Set(),\n };\n\n private readonly loadRuntime: () => Promise<DictationSupportRuntime>;\n private readonly initialButtonMapping: Partial<Record<string, EphiaSpeechMikeAction>>;\n private manager: DictationDeviceManagerLike | null = null;\n private router: SpeechMikeButtonRouter | null = null;\n private currentDevice: DictationDevice | null = null;\n private currentDeviceInfo: EphiaSpeechMikeDeviceInfo | null = null;\n private status: EphiaSpeechMikeHidStatus = \"idle\";\n private physicallyPresent = false;\n private initialized = false;\n private disposed = false;\n private reconnectLock = false;\n private watchdogInterval: number | null = null;\n\n private readonly onButtonEvent: DictationButtonCallback = (_device, bitMask) => {\n const event = this.router?.parse(bitMask);\n if (event) this.emit(\"button\", event);\n };\n\n private readonly onDeviceConnected: DictationDeviceCallback = (device) => {\n void this.attachDevice(device, \"sdk-connect\");\n };\n\n private readonly onDeviceDisconnected: DictationDeviceCallback = () => {\n this.detachDevice(\"sdk-disconnect\");\n };\n\n private readonly onMotionEvent: DictationMotionCallback = () => {};\n\n private readonly onHidConnect = (event: Event) => {\n void this.handleHidConnect(event as HidConnectionEventLike);\n };\n\n private readonly onHidDisconnect = (event: Event) => {\n void this.handleHidDisconnect(event as HidConnectionEventLike);\n };\n\n constructor(options: SpeechMikeDeviceManagerOptions = {}) {\n this.loadRuntime = options.loadRuntime ?? loadDictationSupport;\n this.initialButtonMapping = options.buttonMapping ?? {};\n }\n\n async initialize(): Promise<void> {\n if (this.disposed) return;\n if (this.initialized) return;\n this.initialized = true;\n\n const hid = getNavigatorHid();\n if (!hid) {\n this.setStatus(\"unsupported\");\n return;\n }\n\n this.setStatus(\"initializing\");\n\n try {\n const runtime = await this.loadRuntime();\n if (this.disposed) return;\n\n this.router = new SpeechMikeButtonRouter(runtime.ButtonEvent, this.initialButtonMapping);\n const manager = new runtime.DictationDeviceManager();\n this.manager = manager;\n\n manager.addButtonEventListener?.(this.onButtonEvent);\n manager.addDeviceConnectedEventListener?.(this.onDeviceConnected);\n manager.addDeviceDisconnectedEventListener?.(this.onDeviceDisconnected);\n manager.addMotionEventListener?.(this.onMotionEvent);\n\n hid.addEventListener(\"connect\", this.onHidConnect);\n hid.addEventListener(\"disconnect\", this.onHidDisconnect);\n\n if (manager.init) {\n await withTimeout(\n manager.init(),\n SPEECHMIKE_CONFIG.DEVICE_OPERATION_TIMEOUT_MS,\n \"speechmike.init\",\n );\n }\n\n await this.reconnect();\n this.startWatchdog();\n } catch (error) {\n const err = toError(error, \"speechmike.initialize_failed\");\n this.emit(\"error\", err);\n this.setStatus(err.code === \"speechmike.driver_missing\" ? \"driver_missing\" : \"error\");\n }\n }\n\n async requestAuthorization(): Promise<void> {\n await this.initialize();\n if (this.disposed) return;\n if (!this.manager) {\n const error = {\n code: \"speechmike.manager_unavailable\",\n message: \"SpeechMike manager is not initialized\",\n };\n this.emit(\"error\", error);\n throw new Error(error.message);\n }\n\n this.setStatus(\"authorization_required\");\n\n try {\n await withTimeout(\n this.manager.requestDevice(),\n SPEECHMIKE_CONFIG.REQUEST_DEVICE_TIMEOUT_MS,\n \"speechmike.requestDevice\",\n );\n const device = this.getManagerDevices()[0] ?? null;\n if (!device) {\n this.setStatus(\"error\");\n throw new Error(\"No SpeechMike device returned after authorization\");\n }\n await this.attachDevice(device, \"user-grant\");\n } catch (error) {\n const err = toError(error, \"speechmike.authorization_failed\");\n this.emit(\"error\", err);\n this.setStatus(\"present_unauthorized\");\n throw error;\n }\n }\n\n async reconnect(): Promise<void> {\n if (this.reconnectLock || this.disposed) return;\n this.reconnectLock = true;\n this.setStatus(this.currentDevice ? \"recovering\" : \"restoring\");\n\n try {\n const device = this.getManagerDevices()[0] ?? null;\n if (device) {\n await this.attachDevice(device, \"reconnect\");\n return;\n }\n\n const present = await this.detectPhysicalPresence();\n this.setPhysicallyPresent(present);\n this.detachDevice(present ? \"present-unauthorized\" : \"not-present\", {\n status: present ? \"present_unauthorized\" : \"not_present\",\n });\n } catch (error) {\n const err = toError(error, \"speechmike.reconnect_failed\");\n this.emit(\"error\", err);\n this.setStatus(\"error\");\n } finally {\n this.reconnectLock = false;\n }\n }\n\n dispose(): void {\n this.disposed = true;\n if (this.watchdogInterval !== null && typeof window !== \"undefined\") {\n window.clearInterval(this.watchdogInterval);\n this.watchdogInterval = null;\n }\n\n const hid = getNavigatorHid();\n hid?.removeEventListener(\"connect\", this.onHidConnect);\n hid?.removeEventListener(\"disconnect\", this.onHidDisconnect);\n\n this.manager?.removeButtonEventListener?.(this.onButtonEvent);\n this.manager?.removeDeviceConnectedEventListener?.(this.onDeviceConnected);\n this.manager?.removeDeviceDisconnectedEventListener?.(this.onDeviceDisconnected);\n this.manager?.removeMotionEventListener?.(this.onMotionEvent);\n this.manager?.disconnect?.();\n\n this.currentDevice = null;\n this.currentDeviceInfo = null;\n this.manager = null;\n this.router = null;\n this.initialized = false;\n this.emit(\"device\", null);\n this.emit(\"deviceInfo\", null);\n }\n\n getCurrentDevice(): unknown | null {\n return this.currentDevice;\n }\n\n getStatus(): EphiaSpeechMikeHidStatus {\n return this.status;\n }\n\n getPhysicallyPresent(): boolean {\n return this.physicallyPresent;\n }\n\n getDeviceInfo(): EphiaSpeechMikeDeviceInfo | null {\n return this.currentDeviceInfo;\n }\n\n setButtonMapping(mapping: Partial<Record<string, EphiaSpeechMikeAction>>): void {\n this.router?.setMapping(mapping);\n }\n\n on<K extends keyof SpeechMikeDeviceManagerEvents>(\n event: K,\n cb: SpeechMikeDeviceManagerEvents[K],\n ): () => void {\n this.listeners[event].add(cb);\n return () => {\n this.listeners[event].delete(cb);\n };\n }\n\n private async attachDevice(device: DictationDevice, _cause: string): Promise<void> {\n if (this.disposed) return;\n\n this.currentDevice = device;\n this.setPhysicallyPresent(true);\n this.currentDeviceInfo = await this.resolveDeviceInfo(device);\n this.emit(\"device\", device);\n this.emit(\"deviceInfo\", this.currentDeviceInfo);\n this.setStatus(\"connected\");\n }\n\n private detachDevice(\n _cause: string,\n options: { status?: EphiaSpeechMikeHidStatus } = {},\n ): void {\n this.currentDevice = null;\n this.currentDeviceInfo = null;\n this.emit(\"device\", null);\n this.emit(\"deviceInfo\", null);\n this.setStatus(options.status ?? \"disconnected\");\n }\n\n private async handleHidConnect(event: HidConnectionEventLike): Promise<void> {\n if (this.disposed) return;\n const present = await this.detectPhysicalPresence();\n this.setPhysicallyPresent(present);\n if (!present) return;\n\n const devices = this.getManagerDevices();\n const connectedHidDevice = event.device;\n const match =\n devices.find((device) => sameHidDevice(getDeviceHidDevice(device), connectedHidDevice)) ??\n devices[0] ??\n null;\n\n if (match) {\n await this.attachDevice(match, \"hid-connect\");\n } else {\n this.setStatus(\"present_unauthorized\");\n }\n }\n\n private async handleHidDisconnect(event: HidConnectionEventLike): Promise<void> {\n if (this.disposed) return;\n const currentHidDevice = getDeviceHidDevice(this.currentDevice);\n if (sameHidDevice(currentHidDevice, event.device)) {\n this.detachDevice(\"hid-disconnect\");\n }\n this.setPhysicallyPresent(await this.detectPhysicalPresence());\n }\n\n private startWatchdog(): void {\n if (this.watchdogInterval !== null || typeof window === \"undefined\") return;\n this.watchdogInterval = window.setInterval(() => {\n void this.watchdogTick();\n }, SPEECHMIKE_CONFIG.WATCHDOG_INTERVAL_MS);\n }\n\n private async watchdogTick(): Promise<void> {\n if (this.reconnectLock || this.disposed) return;\n this.reconnectLock = true;\n\n try {\n const present = await this.detectPhysicalPresence();\n this.setPhysicallyPresent(present);\n\n if (!this.currentDevice && present) {\n const device = this.getManagerDevices()[0] ?? null;\n if (device) {\n await this.attachDevice(device, \"watchdog-restore\");\n } else {\n this.setStatus(\"present_unauthorized\");\n }\n } else if (this.currentDevice && !present) {\n this.detachDevice(\"watchdog-absent\", { status: \"disconnected\" });\n }\n } finally {\n this.reconnectLock = false;\n }\n }\n\n private async detectPhysicalPresence(): Promise<boolean> {\n const hid = getNavigatorHid();\n if (hid) {\n try {\n const devices = await hid.getDevices();\n if (devices.some(matchesSpeechMikeIdentity)) return true;\n } catch {\n // Fall through to MediaDevices.\n }\n }\n\n try {\n const devices = await navigator.mediaDevices?.enumerateDevices?.();\n return !!devices?.some((device) => {\n const label = (device.label ?? \"\").toLowerCase();\n return (\n device.kind === \"audioinput\" &&\n (label.includes(\"speechmike\") ||\n label.includes(\"philips\") ||\n label.includes(\"dictation\"))\n );\n });\n } catch {\n return false;\n }\n }\n\n private async resolveDeviceInfo(\n device: DictationDevice,\n ): Promise<EphiaSpeechMikeDeviceInfo | null> {\n const fromDevice = extractDeviceInfo(device);\n if (fromDevice) return fromDevice;\n\n const hidDevice = getDeviceHidDevice(device);\n const fromHid = extractDeviceInfo(hidDevice);\n if (fromHid) return fromHid;\n\n try {\n const info = await this.manager?.getDeviceInfo?.(device);\n return extractDeviceInfo(info);\n } catch {\n return null;\n }\n }\n\n private getManagerDevices(): DictationDevice[] {\n const devices = this.manager?.getDevices?.() ?? [];\n return Array.isArray(devices) ? devices : [];\n }\n\n private setStatus(status: EphiaSpeechMikeHidStatus): void {\n if (this.status === status) return;\n this.status = status;\n this.emit(\"status\", status);\n }\n\n private setPhysicallyPresent(present: boolean): void {\n if (this.physicallyPresent === present) return;\n this.physicallyPresent = present;\n this.emit(\"presence\", present);\n }\n\n private emit<K extends keyof SpeechMikeDeviceManagerEvents>(\n event: K,\n payload: Parameters<SpeechMikeDeviceManagerEvents[K]>[0],\n ): void {\n for (const listener of this.listeners[event]) {\n (listener as (value: typeof payload) => void)(payload);\n }\n }\n}\n","\"use client\";\n\nimport React, { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { useEphiaInternal } from \"../../../react/provider/EphiaInternalContext\";\nimport { SpeechMikeDeviceManager } from \"../speechmike-device-manager\";\nimport { SpeechMikeLedController } from \"../speechmike-led-controller\";\nimport { resolveSpeechMikeAudioInput } from \"../speechmike-audio-resolver\";\nimport {\n EphiaSpeechMikeContext,\n type EphiaSpeechMikeContextValue,\n} from \"./EphiaSpeechMikeContext\";\nimport type {\n EphiaSpeechMikeAction,\n EphiaSpeechMikeAudioStatus,\n EphiaSpeechMikeError,\n EphiaSpeechMikeLedIntent,\n EphiaSpeechMikeState,\n} from \"../types\";\n\nexport type EphiaSpeechMikeProviderProps = {\n children: React.ReactNode;\n enabled?: boolean;\n autoConnect?: boolean;\n preferAudioInput?: boolean;\n requestAudioPermissionOnInit?: boolean;\n ledSync?: boolean;\n initialLedIntent?: EphiaSpeechMikeLedIntent;\n buttonMapping?: Partial<Record<string, EphiaSpeechMikeAction>>;\n onAction?: (action: EphiaSpeechMikeAction) => void;\n onError?: (error: EphiaSpeechMikeError) => void;\n onStatusChange?: (state: EphiaSpeechMikeState) => void;\n};\n\nconst INITIAL_STATE: EphiaSpeechMikeState = {\n status: { hid: \"idle\", audio: \"idle\" },\n device: null,\n deviceInfo: null,\n physicallyPresent: false,\n audioInputDeviceId: null,\n audioInputDeviceLabel: null,\n pressedButtons: new Set<string>(),\n lastButton: null,\n lastAction: null,\n error: null,\n};\n\ntype StatePatch =\n | Partial<EphiaSpeechMikeState>\n | ((prev: EphiaSpeechMikeState) => EphiaSpeechMikeState);\n\nexport function EphiaSpeechMikeProvider({\n children,\n enabled = true,\n autoConnect = true,\n preferAudioInput = true,\n requestAudioPermissionOnInit = false,\n ledSync = true,\n initialLedIntent = \"idle\",\n buttonMapping,\n onAction,\n onError,\n onStatusChange,\n}: EphiaSpeechMikeProviderProps): React.ReactElement {\n const internal = useEphiaInternal();\n const managerRef = useRef<SpeechMikeDeviceManager | null>(null);\n const ledRef = useRef<SpeechMikeLedController | null>(null);\n const onActionRef = useRef(onAction);\n const onErrorRef = useRef(onError);\n const onStatusChangeRef = useRef(onStatusChange);\n const ledSyncRef = useRef(ledSync);\n const initialLedIntentRef = useRef(initialLedIntent);\n const buttonMappingRef = useRef(buttonMapping);\n\n const [state, setState] = useState<EphiaSpeechMikeState>(INITIAL_STATE);\n\n onActionRef.current = onAction;\n onErrorRef.current = onError;\n onStatusChangeRef.current = onStatusChange;\n ledSyncRef.current = ledSync;\n initialLedIntentRef.current = initialLedIntent;\n buttonMappingRef.current = buttonMapping;\n\n const patchState = useCallback((patch: StatePatch): void => {\n setState((prev) => {\n const next =\n typeof patch === \"function\"\n ? patch(prev)\n : {\n ...prev,\n ...patch,\n };\n onStatusChangeRef.current?.(next);\n return next;\n });\n }, []);\n\n const setAudioStatus = useCallback(\n (audio: EphiaSpeechMikeAudioStatus): void => {\n patchState((prev) => ({\n ...prev,\n status: { ...prev.status, audio },\n }));\n },\n [patchState],\n );\n\n const resolveAndApplyAudioInput = useCallback(async (): Promise<void> => {\n if (!preferAudioInput) return;\n\n setAudioStatus(\"resolving\");\n\n try {\n const match = await resolveSpeechMikeAudioInput({\n requestPermission: requestAudioPermissionOnInit,\n });\n\n if (!match) {\n patchState((prev) => ({\n ...prev,\n status: { ...prev.status, audio: \"not_found\" },\n audioInputDeviceId: null,\n audioInputDeviceLabel: null,\n }));\n return;\n }\n\n internal.clientRef.current?.setPreferredAudioInputDeviceId(match.deviceId);\n patchState((prev) => ({\n ...prev,\n status: { ...prev.status, audio: \"ready\" },\n audioInputDeviceId: match.deviceId,\n audioInputDeviceLabel: match.label,\n error: null,\n }));\n } catch (error) {\n const err = {\n code: \"speechmike.audio_resolve_failed\",\n message: error instanceof Error ? error.message : String(error),\n };\n patchState((prev) => ({\n ...prev,\n status: { ...prev.status, audio: \"error\" },\n error: err,\n }));\n onErrorRef.current?.(err);\n }\n }, [\n internal.clientRef,\n patchState,\n preferAudioInput,\n requestAudioPermissionOnInit,\n setAudioStatus,\n ]);\n\n useEffect(() => {\n if (!enabled) {\n patchState(INITIAL_STATE);\n return;\n }\n\n const manager = new SpeechMikeDeviceManager({\n buttonMapping: buttonMappingRef.current,\n });\n const led = new SpeechMikeLedController(() => manager.getCurrentDevice());\n managerRef.current = manager;\n ledRef.current = led;\n\n const unsubs = [\n manager.on(\"status\", (hidStatus) => {\n patchState((prev) => ({\n ...prev,\n status: { ...prev.status, hid: hidStatus },\n }));\n }),\n manager.on(\"presence\", (physicallyPresent) => {\n patchState({ physicallyPresent });\n }),\n manager.on(\"deviceInfo\", (deviceInfo) => {\n patchState({ deviceInfo });\n }),\n manager.on(\"device\", (device) => {\n patchState({ device });\n if (device && ledSyncRef.current) {\n led.setIntent(initialLedIntentRef.current);\n }\n if (device && preferAudioInput) {\n void resolveAndApplyAudioInput();\n }\n }),\n manager.on(\"button\", ({ buttons, actions }) => {\n const buttonList = [...buttons];\n const lastButton = buttonList[buttonList.length - 1] ?? null;\n const lastAction = actions[actions.length - 1] ?? null;\n\n patchState({\n pressedButtons: new Set(buttons),\n lastButton,\n lastAction,\n });\n\n for (const action of actions) {\n onActionRef.current?.(action);\n }\n }),\n manager.on(\"error\", (error) => {\n patchState({ error });\n onErrorRef.current?.(error);\n }),\n ];\n\n if (autoConnect) {\n manager.initialize().catch((error: unknown) => {\n const err = {\n code: \"speechmike.initialize_failed\",\n message: error instanceof Error ? error.message : String(error),\n };\n patchState({ error: err });\n onErrorRef.current?.(err);\n });\n }\n\n return () => {\n unsubs.forEach((unsub) => unsub());\n led.clear();\n manager.dispose();\n managerRef.current = null;\n ledRef.current = null;\n };\n }, [\n autoConnect,\n enabled,\n patchState,\n preferAudioInput,\n resolveAndApplyAudioInput,\n ]);\n\n useEffect(() => {\n managerRef.current?.setButtonMapping(buttonMapping ?? {});\n }, [buttonMapping]);\n\n useEffect(() => {\n if (!enabled || !preferAudioInput) return;\n if (typeof navigator === \"undefined\" || !navigator.mediaDevices?.addEventListener) return;\n\n const handler = (): void => {\n void resolveAndApplyAudioInput();\n };\n\n navigator.mediaDevices.addEventListener(\"devicechange\", handler);\n return () => {\n navigator.mediaDevices.removeEventListener(\"devicechange\", handler);\n };\n }, [enabled, preferAudioInput, resolveAndApplyAudioInput]);\n\n const value = useMemo<EphiaSpeechMikeContextValue>(\n () => ({\n ...state,\n isConnected: state.status.hid === \"connected\",\n isReady: state.status.hid === \"connected\" && state.status.audio === \"ready\",\n requestAuthorization: async () => {\n if (!enabled) {\n throw new Error(\"EphiaSpeechMikeProvider is disabled\");\n }\n await managerRef.current?.requestAuthorization();\n await resolveAndApplyAudioInput();\n },\n reconnect: async () => {\n if (!enabled) return;\n await managerRef.current?.reconnect();\n await resolveAndApplyAudioInput();\n },\n setLedIntent: (intent) => {\n ledRef.current?.setIntent(intent);\n },\n setButtonMapping: (mapping) => {\n managerRef.current?.setButtonMapping(mapping);\n },\n }),\n [enabled, resolveAndApplyAudioInput, state],\n );\n\n return (\n <EphiaSpeechMikeContext.Provider value={value}>\n {children}\n </EphiaSpeechMikeContext.Provider>\n );\n}\n","\"use client\";\n\nimport { createContext } from \"react\";\nimport type {\n EphiaSpeechMikeAction,\n EphiaSpeechMikeLedIntent,\n EphiaSpeechMikeState,\n} from \"../types\";\n\nexport type EphiaSpeechMikeContextValue = EphiaSpeechMikeState & {\n isConnected: boolean;\n isReady: boolean;\n requestAuthorization: () => Promise<void>;\n reconnect: () => Promise<void>;\n setLedIntent: (intent: EphiaSpeechMikeLedIntent) => void;\n setButtonMapping: (mapping: Partial<Record<string, EphiaSpeechMikeAction>>) => void;\n};\n\nexport const FALLBACK_EPHIA_SPEECHMIKE_STATE: EphiaSpeechMikeState = {\n status: { hid: \"unsupported\", audio: \"idle\" },\n device: null,\n deviceInfo: null,\n physicallyPresent: false,\n audioInputDeviceId: null,\n audioInputDeviceLabel: null,\n pressedButtons: new Set<string>(),\n lastButton: null,\n lastAction: null,\n error: null,\n};\n\nexport const EphiaSpeechMikeContext =\n createContext<EphiaSpeechMikeContextValue | null>(null);\n\n","\"use client\";\n\nimport { useContext } from \"react\";\nimport {\n EphiaSpeechMikeContext,\n FALLBACK_EPHIA_SPEECHMIKE_STATE,\n type EphiaSpeechMikeContextValue,\n} from \"./EphiaSpeechMikeContext\";\n\nexport function useEphiaSpeechMike(): EphiaSpeechMikeContextValue {\n const ctx = useContext(EphiaSpeechMikeContext);\n if (ctx) return ctx;\n\n return {\n ...FALLBACK_EPHIA_SPEECHMIKE_STATE,\n isConnected: false,\n isReady: false,\n requestAuthorization: async () => {\n throw new Error(\"EphiaSpeechMikeProvider not mounted\");\n },\n reconnect: async () => {},\n setLedIntent: () => {},\n setButtonMapping: () => {},\n };\n}\n\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// src/shared/config/schema.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
var elementSchema = z.custom(
|
|
4
|
+
(value) => typeof Element !== "undefined" && value instanceof Element,
|
|
5
|
+
{ message: "Expected DOM Element" }
|
|
6
|
+
);
|
|
7
|
+
var ephiaSessionOptionsSchema = z.object({
|
|
8
|
+
language: z.enum(["fr", "en"]).optional(),
|
|
9
|
+
mode: z.enum(["fast_xs", "fast_s", "smart_xs", "smart_s"]).optional(),
|
|
10
|
+
debugChunks: z.boolean().optional()
|
|
11
|
+
});
|
|
12
|
+
var ephiaClientConfigSchema = z.object({
|
|
13
|
+
apiUrl: z.string().url(),
|
|
14
|
+
apiKey: z.string().optional(),
|
|
15
|
+
bearerToken: z.string().optional(),
|
|
16
|
+
clientType: z.string().optional(),
|
|
17
|
+
session: ephiaSessionOptionsSchema.optional(),
|
|
18
|
+
debug: z.boolean().optional(),
|
|
19
|
+
noiseFilter: z.boolean().optional(),
|
|
20
|
+
target: z.union([z.string(), elementSchema, z.null()]).optional()
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// src/shared/errors/messages.ts
|
|
24
|
+
var MESSAGES = {
|
|
25
|
+
"client.start_failed": {
|
|
26
|
+
fr: "\xC9chec du d\xE9marrage",
|
|
27
|
+
en: "Start failed"
|
|
28
|
+
},
|
|
29
|
+
"client.finalization_timeout": {
|
|
30
|
+
fr: "Timeout pendant la finalisation",
|
|
31
|
+
en: "Finalization timeout"
|
|
32
|
+
},
|
|
33
|
+
"client.stop_failed": {
|
|
34
|
+
fr: "\xC9chec de l'arr\xEAt",
|
|
35
|
+
en: "Stop failed"
|
|
36
|
+
},
|
|
37
|
+
"audio.no_input_device": {
|
|
38
|
+
fr: "getUserMedia n'est pas disponible dans ce contexte",
|
|
39
|
+
en: "getUserMedia is not available in this browser/context"
|
|
40
|
+
},
|
|
41
|
+
"session.create_failed": {
|
|
42
|
+
fr: "\xC9chec de cr\xE9ation de session",
|
|
43
|
+
en: "Failed to create session"
|
|
44
|
+
},
|
|
45
|
+
"protocol.invalid_event": {
|
|
46
|
+
fr: "\xC9v\xE9nement invalide du serveur",
|
|
47
|
+
en: "Invalid event from server"
|
|
48
|
+
},
|
|
49
|
+
"provider.not_initialized": {
|
|
50
|
+
fr: "Provider non initialis\xE9",
|
|
51
|
+
en: "Provider not initialized"
|
|
52
|
+
},
|
|
53
|
+
"transport.disconnect_timeout": {
|
|
54
|
+
fr: "D\xE9connexion trop lente, abandon",
|
|
55
|
+
en: "Disconnect too slow, giving up"
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
function getMessage(code, lang = "fr") {
|
|
59
|
+
return MESSAGES[code]?.[lang] ?? code;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export {
|
|
63
|
+
ephiaSessionOptionsSchema,
|
|
64
|
+
ephiaClientConfigSchema,
|
|
65
|
+
getMessage
|
|
66
|
+
};
|
|
67
|
+
//# sourceMappingURL=chunk-5IK5TLSK.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/shared/config/schema.ts","../src/shared/errors/messages.ts"],"sourcesContent":["/**\n * Schéma Zod de validation de la configuration du SDK.\n * SSR-safe : pas de z.instanceof(Element) qui plante en Node/SSR.\n */\n\nimport { z } from \"zod\";\n\nconst elementSchema = z.custom<Element>(\n (value) =>\n typeof Element !== \"undefined\" && value instanceof Element,\n { message: \"Expected DOM Element\" }\n);\n\nexport const ephiaSessionOptionsSchema = z.object({\n language: z.enum([\"fr\", \"en\"]).optional(),\n mode: z.enum([\"fast_xs\", \"fast_s\", \"smart_xs\", \"smart_s\"]).optional(),\n debugChunks: z.boolean().optional(),\n});\n\nexport const ephiaClientConfigSchema = z.object({\n apiUrl: z.string().url(),\n apiKey: z.string().optional(),\n bearerToken: z.string().optional(),\n clientType: z.string().optional(),\n session: ephiaSessionOptionsSchema.optional(),\n debug: z.boolean().optional(),\n noiseFilter: z.boolean().optional(),\n target: z.union([z.string(), elementSchema, z.null()]).optional(),\n});\n\nexport type EphiaClientConfig = z.infer<typeof ephiaClientConfigSchema>;\nexport type EphiaSessionOptionsConfig = z.infer<typeof ephiaSessionOptionsSchema>;\n","export type EphiaLang = \"fr\" | \"en\";\n\nconst MESSAGES: Record<string, Record<EphiaLang, string>> = {\n \"client.start_failed\": {\n fr: \"Échec du démarrage\",\n en: \"Start failed\",\n },\n \"client.finalization_timeout\": {\n fr: \"Timeout pendant la finalisation\",\n en: \"Finalization timeout\",\n },\n \"client.stop_failed\": {\n fr: \"Échec de l'arrêt\",\n en: \"Stop failed\",\n },\n \"audio.no_input_device\": {\n fr: \"getUserMedia n'est pas disponible dans ce contexte\",\n en: \"getUserMedia is not available in this browser/context\",\n },\n \"session.create_failed\": {\n fr: \"Échec de création de session\",\n en: \"Failed to create session\",\n },\n \"protocol.invalid_event\": {\n fr: \"Événement invalide du serveur\",\n en: \"Invalid event from server\",\n },\n \"provider.not_initialized\": {\n fr: \"Provider non initialisé\",\n en: \"Provider not initialized\",\n },\n \"transport.disconnect_timeout\": {\n fr: \"Déconnexion trop lente, abandon\",\n en: \"Disconnect too slow, giving up\",\n },\n};\n\nexport function getMessage(code: string, lang: EphiaLang = \"fr\"): string {\n return MESSAGES[code]?.[lang] ?? code;\n}\n"],"mappings":";AAKA,SAAS,SAAS;AAElB,IAAM,gBAAgB,EAAE;AAAA,EACtB,CAAC,UACC,OAAO,YAAY,eAAe,iBAAiB;AAAA,EACrD,EAAE,SAAS,uBAAuB;AACpC;AAEO,IAAM,4BAA4B,EAAE,OAAO;AAAA,EAChD,UAAU,EAAE,KAAK,CAAC,MAAM,IAAI,CAAC,EAAE,SAAS;AAAA,EACxC,MAAM,EAAE,KAAK,CAAC,WAAW,UAAU,YAAY,SAAS,CAAC,EAAE,SAAS;AAAA,EACpE,aAAa,EAAE,QAAQ,EAAE,SAAS;AACpC,CAAC;AAEM,IAAM,0BAA0B,EAAE,OAAO;AAAA,EAC9C,QAAQ,EAAE,OAAO,EAAE,IAAI;AAAA,EACvB,QAAQ,EAAE,OAAO,EAAE,SAAS;AAAA,EAC5B,aAAa,EAAE,OAAO,EAAE,SAAS;AAAA,EACjC,YAAY,EAAE,OAAO,EAAE,SAAS;AAAA,EAChC,SAAS,0BAA0B,SAAS;AAAA,EAC5C,OAAO,EAAE,QAAQ,EAAE,SAAS;AAAA,EAC5B,aAAa,EAAE,QAAQ,EAAE,SAAS;AAAA,EAClC,QAAQ,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,eAAe,EAAE,KAAK,CAAC,CAAC,EAAE,SAAS;AAClE,CAAC;;;AC1BD,IAAM,WAAsD;AAAA,EAC1D,uBAAuB;AAAA,IACrB,IAAI;AAAA,IACJ,IAAI;AAAA,EACN;AAAA,EACA,+BAA+B;AAAA,IAC7B,IAAI;AAAA,IACJ,IAAI;AAAA,EACN;AAAA,EACA,sBAAsB;AAAA,IACpB,IAAI;AAAA,IACJ,IAAI;AAAA,EACN;AAAA,EACA,yBAAyB;AAAA,IACvB,IAAI;AAAA,IACJ,IAAI;AAAA,EACN;AAAA,EACA,yBAAyB;AAAA,IACvB,IAAI;AAAA,IACJ,IAAI;AAAA,EACN;AAAA,EACA,0BAA0B;AAAA,IACxB,IAAI;AAAA,IACJ,IAAI;AAAA,EACN;AAAA,EACA,4BAA4B;AAAA,IAC1B,IAAI;AAAA,IACJ,IAAI;AAAA,EACN;AAAA,EACA,gCAAgC;AAAA,IAC9B,IAAI;AAAA,IACJ,IAAI;AAAA,EACN;AACF;AAEO,SAAS,WAAW,MAAc,OAAkB,MAAc;AACvE,SAAO,SAAS,IAAI,IAAI,IAAI,KAAK;AACnC;","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/shared/effective-text.ts"],"sourcesContent":["/**\n * Returns the text to apply to DOM/state after a finalized or committed\n * transcript event. An empty delta is intentional and must not fall back to\n * the raw provider text.\n */\nexport function pickEffectiveText(payload: {\n text: string;\n delta?: string | null;\n}): string {\n return payload.delta !== undefined && payload.delta !== null\n ? payload.delta\n : payload.text;\n}\n"],"mappings":";AAKO,SAAS,kBAAkB,SAGvB;AACT,SAAO,QAAQ,UAAU,UAAa,QAAQ,UAAU,OACpD,QAAQ,QACR,QAAQ;AACd;","names":[]}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ensureMinimalAppendBoundary
|
|
3
|
+
} from "./chunk-W2ZP674X.js";
|
|
4
|
+
|
|
5
|
+
// src/core/bindings/native/NativeBinding.ts
|
|
6
|
+
function setNativeValue(el, value) {
|
|
7
|
+
const proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
|
|
8
|
+
const setter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
|
|
9
|
+
setter?.call(el, value);
|
|
10
|
+
}
|
|
11
|
+
function dispatchInputEvent(el) {
|
|
12
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
13
|
+
}
|
|
14
|
+
var NativeBinding = class {
|
|
15
|
+
kind = "native";
|
|
16
|
+
identity;
|
|
17
|
+
el;
|
|
18
|
+
warn;
|
|
19
|
+
// Text before/after the dictation zone, locked at attach().
|
|
20
|
+
prefix = "";
|
|
21
|
+
suffix = "";
|
|
22
|
+
// Committed segment texts, keyed by id, in insertion order (Map preserves insertion order).
|
|
23
|
+
segmentTexts = /* @__PURE__ */ new Map();
|
|
24
|
+
// Tracks all segment ids ever seen (for idempotent range-lost guard).
|
|
25
|
+
knownSegmentIds = /* @__PURE__ */ new Set();
|
|
26
|
+
// Boundary prefix computed once per segment at first insertion (preview or upsert).
|
|
27
|
+
segmentBoundaryPrefixes = /* @__PURE__ */ new Map();
|
|
28
|
+
// Current streaming preview (displayed but not committed).
|
|
29
|
+
previewSegmentId = null;
|
|
30
|
+
previewText = "";
|
|
31
|
+
constructor(el, options) {
|
|
32
|
+
this.el = el;
|
|
33
|
+
this.identity = el;
|
|
34
|
+
this.warn = options?.warn ?? ((msg, d) => console.warn(msg, d));
|
|
35
|
+
}
|
|
36
|
+
attach() {
|
|
37
|
+
this._syncInsertionAnchor();
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Resync prefix/suffix and clear segment memory from the current DOM value.
|
|
41
|
+
* Called before each dictation so manual edits between sessions are preserved.
|
|
42
|
+
*/
|
|
43
|
+
prepareForDictation() {
|
|
44
|
+
this._syncInsertionAnchor();
|
|
45
|
+
}
|
|
46
|
+
detach() {
|
|
47
|
+
this.segmentTexts.clear();
|
|
48
|
+
this.knownSegmentIds.clear();
|
|
49
|
+
this.segmentBoundaryPrefixes.clear();
|
|
50
|
+
this.previewSegmentId = null;
|
|
51
|
+
this.previewText = "";
|
|
52
|
+
}
|
|
53
|
+
getText() {
|
|
54
|
+
return this.prefix + this._committedText() + this.suffix;
|
|
55
|
+
}
|
|
56
|
+
getEditorContext(targetId = "") {
|
|
57
|
+
const committed = this._committedText();
|
|
58
|
+
const value = this.prefix + committed + this.suffix;
|
|
59
|
+
const cursorOffset = this.prefix.length + committed.length;
|
|
60
|
+
return {
|
|
61
|
+
targetId,
|
|
62
|
+
documentEmpty: value.trim().length === 0,
|
|
63
|
+
insertionMode: this.suffix.length > 0 ? "middle_of_sentence" : "append",
|
|
64
|
+
leftContext: value.slice(Math.max(0, cursorOffset - 400), cursorOffset),
|
|
65
|
+
rightContext: value.slice(cursorOffset, cursorOffset + 200),
|
|
66
|
+
selectedText: null,
|
|
67
|
+
cursorOffset
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
previewSegment(input) {
|
|
71
|
+
if (!this.segmentBoundaryPrefixes.has(input.id)) {
|
|
72
|
+
const leftText = this.prefix + this._committedText();
|
|
73
|
+
const withBoundary = ensureMinimalAppendBoundary(leftText, input.text);
|
|
74
|
+
const prefix = withBoundary.length > input.text.length ? withBoundary.slice(0, withBoundary.length - input.text.length) : "";
|
|
75
|
+
this.segmentBoundaryPrefixes.set(input.id, prefix);
|
|
76
|
+
}
|
|
77
|
+
this.previewSegmentId = input.id;
|
|
78
|
+
const boundaryPrefix = this.segmentBoundaryPrefixes.get(input.id) ?? "";
|
|
79
|
+
this.previewText = boundaryPrefix + input.text;
|
|
80
|
+
this._render();
|
|
81
|
+
}
|
|
82
|
+
upsertSegment(input) {
|
|
83
|
+
if (this.previewSegmentId === input.id) {
|
|
84
|
+
this.previewSegmentId = null;
|
|
85
|
+
this.previewText = "";
|
|
86
|
+
}
|
|
87
|
+
if (this.knownSegmentIds.has(input.id) && !this.segmentTexts.has(input.id)) {
|
|
88
|
+
this.warn("[ephia:native] segment range lost; upsert ignored", { segmentId: input.id });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (!this.segmentBoundaryPrefixes.has(input.id)) {
|
|
92
|
+
const leftText = this.prefix + this._committedText();
|
|
93
|
+
const withBoundary = ensureMinimalAppendBoundary(leftText, input.text);
|
|
94
|
+
const prefix = withBoundary.length > input.text.length ? withBoundary.slice(0, withBoundary.length - input.text.length) : "";
|
|
95
|
+
this.segmentBoundaryPrefixes.set(input.id, prefix);
|
|
96
|
+
} else if (input.text.length > 0 && /^\s/.test(input.text)) {
|
|
97
|
+
this.segmentBoundaryPrefixes.set(input.id, "");
|
|
98
|
+
}
|
|
99
|
+
this.segmentTexts.set(input.id, input.text);
|
|
100
|
+
this.knownSegmentIds.add(input.id);
|
|
101
|
+
if (input.stage === "canonical") {
|
|
102
|
+
this._reconcileCanonicalSegmentInsertion(input.id, input.text);
|
|
103
|
+
}
|
|
104
|
+
this._render();
|
|
105
|
+
}
|
|
106
|
+
removeSegment(id) {
|
|
107
|
+
if (this.previewSegmentId === id) {
|
|
108
|
+
this.previewSegmentId = null;
|
|
109
|
+
this.previewText = "";
|
|
110
|
+
}
|
|
111
|
+
this.segmentTexts.delete(id);
|
|
112
|
+
this.segmentBoundaryPrefixes.delete(id);
|
|
113
|
+
this.knownSegmentIds.add(id);
|
|
114
|
+
this._render();
|
|
115
|
+
}
|
|
116
|
+
removeSegments(ids) {
|
|
117
|
+
for (const id of ids) this.removeSegment(id);
|
|
118
|
+
}
|
|
119
|
+
_syncInsertionAnchor() {
|
|
120
|
+
const start = this.el.selectionStart ?? this.el.value.length;
|
|
121
|
+
const end = this.el.selectionEnd ?? start;
|
|
122
|
+
this.prefix = this.el.value.slice(0, start);
|
|
123
|
+
this.suffix = this.el.value.slice(end);
|
|
124
|
+
this.segmentTexts.clear();
|
|
125
|
+
this.knownSegmentIds.clear();
|
|
126
|
+
this.segmentBoundaryPrefixes.clear();
|
|
127
|
+
this.previewSegmentId = null;
|
|
128
|
+
this.previewText = "";
|
|
129
|
+
}
|
|
130
|
+
// ------------------------------------------------------------------
|
|
131
|
+
// Internals
|
|
132
|
+
// ------------------------------------------------------------------
|
|
133
|
+
_committedText() {
|
|
134
|
+
return [...this.segmentTexts.keys()].map((id) => {
|
|
135
|
+
const prefix = this.segmentBoundaryPrefixes.get(id) ?? "";
|
|
136
|
+
return prefix + (this.segmentTexts.get(id) ?? "");
|
|
137
|
+
}).join("");
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Canonical upserts (incl. absorb/merge) may carry the full corrected phrase.
|
|
141
|
+
* Drop segment/binding prefixes that would duplicate its opening.
|
|
142
|
+
*/
|
|
143
|
+
_reconcileCanonicalSegmentInsertion(segmentId, text) {
|
|
144
|
+
const boundaryPrefix = this.segmentBoundaryPrefixes.get(segmentId);
|
|
145
|
+
if (boundaryPrefix) {
|
|
146
|
+
const boundaryCore = boundaryPrefix.trimEnd();
|
|
147
|
+
if (text.startsWith(boundaryPrefix) || boundaryCore.length > 0 && text.startsWith(boundaryCore)) {
|
|
148
|
+
this.segmentBoundaryPrefixes.set(segmentId, "");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
this._trimBindingPrefixOverlap(text);
|
|
152
|
+
}
|
|
153
|
+
/** Trim binding.prefix when canonical text already opens with its trailing words. */
|
|
154
|
+
_trimBindingPrefixOverlap(text) {
|
|
155
|
+
if (!this.prefix || !text) return;
|
|
156
|
+
for (let overlap = Math.min(this.prefix.length, text.length); overlap > 0; overlap--) {
|
|
157
|
+
const suffix = this.prefix.slice(this.prefix.length - overlap);
|
|
158
|
+
if (suffix.trim().length === 0) continue;
|
|
159
|
+
if (text.startsWith(suffix)) {
|
|
160
|
+
this.prefix = this.prefix.slice(0, this.prefix.length - overlap);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
_render() {
|
|
166
|
+
const committed = this._committedText();
|
|
167
|
+
const next = this.prefix + committed + this.previewText + this.suffix;
|
|
168
|
+
if (this.el.value !== next) {
|
|
169
|
+
setNativeValue(this.el, next);
|
|
170
|
+
dispatchInputEvent(this.el);
|
|
171
|
+
}
|
|
172
|
+
const insertionPos = this.prefix.length + committed.length;
|
|
173
|
+
try {
|
|
174
|
+
this.el.setSelectionRange(insertionPos, insertionPos);
|
|
175
|
+
} catch {
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
export {
|
|
181
|
+
NativeBinding
|
|
182
|
+
};
|
|
183
|
+
//# sourceMappingURL=chunk-A5UEXJ5R.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/bindings/native/NativeBinding.ts"],"sourcesContent":["import type { EditorContext } from \"ephia-protocol\";\nimport type { EphiaBinding, PreviewSegmentInput, UpsertSegmentInput } from \"../EphiaBinding\";\nimport { ensureMinimalAppendBoundary } from \"../insertion-boundary\";\n\n// Bypass React controlled components — use native setter to avoid onChange fights.\nfunction setNativeValue(el: HTMLInputElement | HTMLTextAreaElement, value: string): void {\n const proto =\n el instanceof HTMLTextAreaElement\n ? HTMLTextAreaElement.prototype\n : HTMLInputElement.prototype;\n const setter = Object.getOwnPropertyDescriptor(proto, \"value\")?.set;\n setter?.call(el, value);\n}\n\nfunction dispatchInputEvent(el: HTMLInputElement | HTMLTextAreaElement): void {\n el.dispatchEvent(new Event(\"input\", { bubbles: true }));\n}\n\n/**\n * Binding V2 pour <textarea> et <input type=\"text\">.\n *\n * Modèle : prefix | segments dictés | preview | suffix\n *\n * Le prefix et le suffix sont verrouillés à l'attach() depuis la sélection\n * courante. Tous les segments dictés sont insérés entre les deux.\n * Même segmentId = mise à jour en place (jamais d'append).\n */\nexport class NativeBinding implements EphiaBinding {\n readonly kind = \"native\";\n readonly identity: HTMLInputElement | HTMLTextAreaElement;\n\n private readonly el: HTMLInputElement | HTMLTextAreaElement;\n private readonly warn: (msg: string, details?: Record<string, unknown>) => void;\n\n // Text before/after the dictation zone, locked at attach().\n private prefix = \"\";\n private suffix = \"\";\n\n // Committed segment texts, keyed by id, in insertion order (Map preserves insertion order).\n private readonly segmentTexts = new Map<string, string>();\n // Tracks all segment ids ever seen (for idempotent range-lost guard).\n private readonly knownSegmentIds = new Set<string>();\n // Boundary prefix computed once per segment at first insertion (preview or upsert).\n private readonly segmentBoundaryPrefixes = new Map<string, string>();\n\n // Current streaming preview (displayed but not committed).\n private previewSegmentId: string | null = null;\n private previewText = \"\";\n\n constructor(\n el: HTMLInputElement | HTMLTextAreaElement,\n options?: { warn?: (msg: string, details?: Record<string, unknown>) => void }\n ) {\n this.el = el;\n this.identity = el;\n this.warn = options?.warn ?? ((msg, d) => console.warn(msg, d));\n }\n\n attach(): void {\n this._syncInsertionAnchor();\n }\n\n /**\n * Resync prefix/suffix and clear segment memory from the current DOM value.\n * Called before each dictation so manual edits between sessions are preserved.\n */\n prepareForDictation(): void {\n this._syncInsertionAnchor();\n }\n\n detach(): void {\n this.segmentTexts.clear();\n this.knownSegmentIds.clear();\n this.segmentBoundaryPrefixes.clear();\n this.previewSegmentId = null;\n this.previewText = \"\";\n }\n\n getText(): string {\n return this.prefix + this._committedText() + this.suffix;\n }\n\n getEditorContext(targetId = \"\"): EditorContext {\n const committed = this._committedText();\n const value = this.prefix + committed + this.suffix;\n const cursorOffset = this.prefix.length + committed.length;\n\n return {\n targetId,\n documentEmpty: value.trim().length === 0,\n insertionMode: this.suffix.length > 0 ? \"middle_of_sentence\" : \"append\",\n leftContext: value.slice(Math.max(0, cursorOffset - 400), cursorOffset),\n rightContext: value.slice(cursorOffset, cursorOffset + 200),\n selectedText: null,\n cursorOffset,\n };\n }\n\n previewSegment(input: PreviewSegmentInput): void {\n // Compute boundary prefix once per segment (first preview for this id).\n if (!this.segmentBoundaryPrefixes.has(input.id)) {\n const leftText = this.prefix + this._committedText();\n const withBoundary = ensureMinimalAppendBoundary(leftText, input.text);\n const prefix = withBoundary.length > input.text.length\n ? withBoundary.slice(0, withBoundary.length - input.text.length)\n : \"\";\n this.segmentBoundaryPrefixes.set(input.id, prefix);\n }\n this.previewSegmentId = input.id;\n const boundaryPrefix = this.segmentBoundaryPrefixes.get(input.id) ?? \"\";\n this.previewText = boundaryPrefix + input.text;\n this._render();\n }\n\n upsertSegment(input: UpsertSegmentInput): void {\n if (this.previewSegmentId === input.id) {\n this.previewSegmentId = null;\n this.previewText = \"\";\n }\n\n if (this.knownSegmentIds.has(input.id) && !this.segmentTexts.has(input.id)) {\n this.warn(\"[ephia:native] segment range lost; upsert ignored\", { segmentId: input.id });\n return;\n }\n\n if (!this.segmentBoundaryPrefixes.has(input.id)) {\n // First upsert for this segment with no prior preview — compute boundary from left context.\n const leftText = this.prefix + this._committedText();\n const withBoundary = ensureMinimalAppendBoundary(leftText, input.text);\n const prefix = withBoundary.length > input.text.length\n ? withBoundary.slice(0, withBoundary.length - input.text.length)\n : \"\";\n this.segmentBoundaryPrefixes.set(input.id, prefix);\n } else if (input.text.length > 0 && /^\\s/.test(input.text)) {\n // Backend canonical provides its own boundary — clear ours to avoid double spacing.\n this.segmentBoundaryPrefixes.set(input.id, \"\");\n }\n\n this.segmentTexts.set(input.id, input.text);\n this.knownSegmentIds.add(input.id);\n\n if (input.stage === \"canonical\") {\n this._reconcileCanonicalSegmentInsertion(input.id, input.text);\n }\n\n this._render();\n }\n\n removeSegment(id: string): void {\n if (this.previewSegmentId === id) {\n this.previewSegmentId = null;\n this.previewText = \"\";\n }\n\n this.segmentTexts.delete(id);\n this.segmentBoundaryPrefixes.delete(id);\n this.knownSegmentIds.add(id);\n this._render();\n }\n\n removeSegments(ids: string[]): void {\n for (const id of ids) this.removeSegment(id);\n }\n\n private _syncInsertionAnchor(): void {\n const start = this.el.selectionStart ?? this.el.value.length;\n const end = this.el.selectionEnd ?? start;\n\n this.prefix = this.el.value.slice(0, start);\n this.suffix = this.el.value.slice(end);\n\n this.segmentTexts.clear();\n this.knownSegmentIds.clear();\n this.segmentBoundaryPrefixes.clear();\n this.previewSegmentId = null;\n this.previewText = \"\";\n }\n\n // ------------------------------------------------------------------\n // Internals\n // ------------------------------------------------------------------\n\n private _committedText(): string {\n return [...this.segmentTexts.keys()].map((id) => {\n const prefix = this.segmentBoundaryPrefixes.get(id) ?? \"\";\n return prefix + (this.segmentTexts.get(id) ?? \"\");\n }).join(\"\");\n }\n\n /**\n * Canonical upserts (incl. absorb/merge) may carry the full corrected phrase.\n * Drop segment/binding prefixes that would duplicate its opening.\n */\n private _reconcileCanonicalSegmentInsertion(segmentId: string, text: string): void {\n const boundaryPrefix = this.segmentBoundaryPrefixes.get(segmentId);\n if (boundaryPrefix) {\n const boundaryCore = boundaryPrefix.trimEnd();\n if (\n text.startsWith(boundaryPrefix)\n || (boundaryCore.length > 0 && text.startsWith(boundaryCore))\n ) {\n this.segmentBoundaryPrefixes.set(segmentId, \"\");\n }\n }\n\n this._trimBindingPrefixOverlap(text);\n }\n\n /** Trim binding.prefix when canonical text already opens with its trailing words. */\n private _trimBindingPrefixOverlap(text: string): void {\n if (!this.prefix || !text) return;\n\n for (let overlap = Math.min(this.prefix.length, text.length); overlap > 0; overlap--) {\n const suffix = this.prefix.slice(this.prefix.length - overlap);\n if (suffix.trim().length === 0) continue;\n if (text.startsWith(suffix)) {\n this.prefix = this.prefix.slice(0, this.prefix.length - overlap);\n return;\n }\n }\n }\n\n private _render(): void {\n const committed = this._committedText();\n const next = this.prefix + committed + this.previewText + this.suffix;\n\n if (this.el.value !== next) {\n setNativeValue(this.el, next);\n dispatchInputEvent(this.el);\n }\n\n const insertionPos = this.prefix.length + committed.length;\n try {\n this.el.setSelectionRange(insertionPos, insertionPos);\n } catch {\n /* noop */\n }\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useEphiaInternal
|
|
3
|
+
} from "./chunk-THNHRV2B.js";
|
|
4
|
+
import {
|
|
5
|
+
useEphiaContext
|
|
6
|
+
} from "./chunk-K24GNU27.js";
|
|
7
|
+
import {
|
|
8
|
+
TiptapBinding
|
|
9
|
+
} from "./chunk-W2ZP674X.js";
|
|
10
|
+
|
|
11
|
+
// src/rich-editor/adapters/tiptap.ts
|
|
12
|
+
var tiptapAdapterCache = /* @__PURE__ */ new WeakMap();
|
|
13
|
+
function createTiptapEphiaAdapter(editor) {
|
|
14
|
+
if (!editor) return null;
|
|
15
|
+
const cached = tiptapAdapterCache.get(editor);
|
|
16
|
+
if (cached) return cached;
|
|
17
|
+
const adapter = {
|
|
18
|
+
kind: "tiptap",
|
|
19
|
+
identity: editor,
|
|
20
|
+
createBinding: () => new TiptapBinding(editor)
|
|
21
|
+
};
|
|
22
|
+
tiptapAdapterCache.set(editor, adapter);
|
|
23
|
+
return adapter;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/rich-editor/use-ephia-rich-editor.ts
|
|
27
|
+
import { useEffect, useRef } from "react";
|
|
28
|
+
function useEphiaRichEditor(adapter, options) {
|
|
29
|
+
const ref = useRef(null);
|
|
30
|
+
const { registerTarget } = useEphiaContext();
|
|
31
|
+
const { clientEpoch } = useEphiaInternal();
|
|
32
|
+
const { id, mode, label, enabled } = options;
|
|
33
|
+
const adapterKind = adapter?.kind ?? null;
|
|
34
|
+
const adapterIdentity = adapter?.identity ?? adapter ?? null;
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (enabled === false) return;
|
|
37
|
+
if (!adapter) return;
|
|
38
|
+
const binding = adapter.createBinding({ targetId: id, label });
|
|
39
|
+
return registerTarget(id, binding, {
|
|
40
|
+
mode: mode ?? "write",
|
|
41
|
+
element: adapter.getElement?.() ?? ref.current ?? void 0
|
|
42
|
+
});
|
|
43
|
+
}, [adapterKind, adapterIdentity, enabled, id, label, mode, registerTarget, clientEpoch]);
|
|
44
|
+
return ref;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export {
|
|
48
|
+
createTiptapEphiaAdapter,
|
|
49
|
+
useEphiaRichEditor
|
|
50
|
+
};
|
|
51
|
+
//# sourceMappingURL=chunk-AEE554FT.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/rich-editor/adapters/tiptap.ts","../src/rich-editor/use-ephia-rich-editor.ts"],"sourcesContent":["import type { Editor } from \"@tiptap/core\";\nimport { TiptapBinding } from \"../../core/bindings/tiptap/TiptapBinding\";\nimport type { EphiaRichEditorAdapter } from \"../types\";\n\nconst tiptapAdapterCache = new WeakMap<Editor, EphiaRichEditorAdapter>();\n\nexport function createTiptapEphiaAdapter(\n editor: Editor | null,\n): EphiaRichEditorAdapter | null {\n if (!editor) return null;\n\n const cached = tiptapAdapterCache.get(editor);\n if (cached) return cached;\n\n const adapter: EphiaRichEditorAdapter = {\n kind: \"tiptap\",\n identity: editor,\n createBinding: () => new TiptapBinding(editor),\n };\n\n tiptapAdapterCache.set(editor, adapter);\n return adapter;\n}\n","import { useEffect, useRef } from \"react\";\nimport { useEphiaContext } from \"../react/provider/EphiaContext\";\nimport { useEphiaInternal } from \"../react/provider/EphiaInternalContext\";\nimport type { EphiaRichEditorAdapter, UseEphiaRichEditorOptions } from \"./types\";\n\n/**\n * Registers a rich text editor via an adapter as a V2 Ephia target.\n *\n * @example\n * const ephiaRef = useEphiaRichEditor(\n * createTiptapEphiaAdapter(editor),\n * { id: \"report\", label: \"Compte rendu\" },\n * );\n *\n * return (\n * <div ref={ephiaRef}>\n * <EditorContent editor={editor} />\n * </div>\n * );\n */\nexport function useEphiaRichEditor(\n adapter: EphiaRichEditorAdapter | null,\n options: UseEphiaRichEditorOptions,\n): React.RefObject<HTMLDivElement | null> {\n const ref = useRef<HTMLDivElement>(null);\n const { registerTarget } = useEphiaContext();\n const { clientEpoch } = useEphiaInternal();\n const { id, mode, label, enabled } = options;\n\n const adapterKind = adapter?.kind ?? null;\n // Use identity if provided (stable per editor instance). Fall back to the\n // adapter object itself so custom adapters without identity still work.\n const adapterIdentity = adapter?.identity ?? adapter ?? null;\n\n useEffect(() => {\n if (enabled === false) return;\n if (!adapter) return;\n\n const binding = adapter.createBinding({ targetId: id, label });\n return registerTarget(id, binding, {\n mode: mode ?? \"write\",\n element: adapter.getElement?.() ?? ref.current ?? undefined,\n });\n }, [adapterKind, adapterIdentity, enabled, id, label, mode, registerTarget, clientEpoch]);\n\n return ref;\n}\n"],"mappings":";;;;;;;;;;;AAIA,IAAM,qBAAqB,oBAAI,QAAwC;AAEhE,SAAS,yBACd,QAC+B;AAC/B,MAAI,CAAC,OAAQ,QAAO;AAEpB,QAAM,SAAS,mBAAmB,IAAI,MAAM;AAC5C,MAAI,OAAQ,QAAO;AAEnB,QAAM,UAAkC;AAAA,IACtC,MAAM;AAAA,IACN,UAAU;AAAA,IACV,eAAe,MAAM,IAAI,cAAc,MAAM;AAAA,EAC/C;AAEA,qBAAmB,IAAI,QAAQ,OAAO;AACtC,SAAO;AACT;;;ACtBA,SAAS,WAAW,cAAc;AAoB3B,SAAS,mBACd,SACA,SACwC;AACxC,QAAM,MAAM,OAAuB,IAAI;AACvC,QAAM,EAAE,eAAe,IAAI,gBAAgB;AAC3C,QAAM,EAAE,YAAY,IAAI,iBAAiB;AACzC,QAAM,EAAE,IAAI,MAAM,OAAO,QAAQ,IAAI;AAErC,QAAM,cAAc,SAAS,QAAQ;AAGrC,QAAM,kBAAkB,SAAS,YAAY,WAAW;AAExD,YAAU,MAAM;AACd,QAAI,YAAY,MAAO;AACvB,QAAI,CAAC,QAAS;AAEd,UAAM,UAAU,QAAQ,cAAc,EAAE,UAAU,IAAI,MAAM,CAAC;AAC7D,WAAO,eAAe,IAAI,SAAS;AAAA,MACjC,MAAM,QAAQ;AAAA,MACd,SAAS,QAAQ,aAAa,KAAK,IAAI,WAAW;AAAA,IACpD,CAAC;AAAA,EACH,GAAG,CAAC,aAAa,iBAAiB,SAAS,IAAI,OAAO,MAAM,gBAAgB,WAAW,CAAC;AAExF,SAAO;AACT;","names":[]}
|