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