@ews-admin/global-design-system 1.37.0 → 1.39.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/dist/components/AssistantChat/ActionTooltip.d.ts +12 -0
- package/dist/components/AssistantChat/ActionTooltip.d.ts.map +1 -0
- package/dist/components/AssistantChat/AssistantChat.d.ts.map +1 -1
- package/dist/components/AssistantChat/AssistantChat.types.d.ts +37 -1
- package/dist/components/AssistantChat/AssistantChat.types.d.ts.map +1 -1
- package/dist/components/AssistantChat/AssistantComposer.d.ts +16 -3
- package/dist/components/AssistantChat/AssistantComposer.d.ts.map +1 -1
- package/dist/components/AssistantChat/AssistantDictationBar.d.ts +28 -0
- package/dist/components/AssistantChat/AssistantDictationBar.d.ts.map +1 -0
- package/dist/components/AssistantChat/AssistantSuggestionCard.d.ts +1 -1
- package/dist/components/AssistantChat/AssistantSuggestionCard.d.ts.map +1 -1
- package/dist/components/AssistantChat/AssistantVoiceMode.d.ts +37 -0
- package/dist/components/AssistantChat/AssistantVoiceMode.d.ts.map +1 -0
- package/dist/components/AssistantChat/assistantMarkdown.d.ts.map +1 -1
- package/dist/components/AssistantChat/index.d.ts +4 -0
- package/dist/components/AssistantChat/index.d.ts.map +1 -1
- package/dist/components/AssistantLauncher/AssistantLauncher.d.ts +15 -0
- package/dist/components/AssistantLauncher/AssistantLauncher.d.ts.map +1 -1
- package/dist/components/AssistantLauncher/index.d.ts +1 -1
- package/dist/components/AssistantLauncher/index.d.ts.map +1 -1
- package/dist/hooks/index.d.ts +1 -1
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/useSpeechRecognition.d.ts +2 -0
- package/dist/hooks/useSpeechRecognition.d.ts.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.ts +133 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.css +1 -1
- package/dist/index.esm.js +435 -58
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +437 -56
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/AssistantChat/ActionTooltip.tsx +31 -0
- package/src/components/AssistantChat/AssistantChat.tsx +43 -5
- package/src/components/AssistantChat/AssistantChat.types.ts +27 -1
- package/src/components/AssistantChat/AssistantComposer.tsx +106 -68
- package/src/components/AssistantChat/AssistantDictationBar.tsx +268 -0
- package/src/components/AssistantChat/AssistantSuggestionCard.tsx +7 -6
- package/src/components/AssistantChat/AssistantVoiceMode.tsx +304 -0
- package/src/components/AssistantChat/assistantMarkdown.tsx +4 -1
- package/src/components/AssistantChat/index.ts +4 -0
- package/src/components/AssistantLauncher/AssistantLauncher.tsx +156 -79
- package/src/components/AssistantLauncher/index.ts +1 -1
- package/src/hooks/index.ts +1 -1
- package/src/hooks/useSpeechRecognition.ts +9 -0
- package/src/index.ts +9 -1
package/dist/index.js
CHANGED
|
@@ -1030,13 +1030,21 @@ function useSelectField({ name, control, options: _options, rules, defaultValue,
|
|
|
1030
1030
|
};
|
|
1031
1031
|
}
|
|
1032
1032
|
|
|
1033
|
-
const BCP47 = { fr: "fr-FR", en: "en-US", ar: "ar-SA" };
|
|
1033
|
+
const BCP47$2 = { fr: "fr-FR", en: "en-US", ar: "ar-SA" };
|
|
1034
1034
|
function getSpeechRecognition() {
|
|
1035
1035
|
if (typeof window === "undefined")
|
|
1036
1036
|
return undefined;
|
|
1037
1037
|
const w = window;
|
|
1038
1038
|
return w.SpeechRecognition ?? w.webkitSpeechRecognition;
|
|
1039
1039
|
}
|
|
1040
|
+
/** True once mounted in a browser that has the speech recognition API (false on the server). */
|
|
1041
|
+
function useSpeechSupported() {
|
|
1042
|
+
const [supported, setSupported] = React.useState(false);
|
|
1043
|
+
React.useEffect(() => {
|
|
1044
|
+
setSupported(getSpeechRecognition() !== undefined);
|
|
1045
|
+
}, []);
|
|
1046
|
+
return supported;
|
|
1047
|
+
}
|
|
1040
1048
|
/**
|
|
1041
1049
|
* Push-to-talk on the browser speech API (MED-1783). `supported` is false on
|
|
1042
1050
|
* the server and in browsers without the API, so the app can hide the mic.
|
|
@@ -1060,7 +1068,7 @@ function useSpeechRecognition({ language, onResult, onInterim, }) {
|
|
|
1060
1068
|
if (!Ctor || recognitionRef.current)
|
|
1061
1069
|
return;
|
|
1062
1070
|
const recognition = new Ctor();
|
|
1063
|
-
recognition.lang = BCP47[language] ?? language;
|
|
1071
|
+
recognition.lang = BCP47$2[language] ?? language;
|
|
1064
1072
|
recognition.interimResults = true;
|
|
1065
1073
|
recognition.continuous = true;
|
|
1066
1074
|
transcriptRef.current = "";
|
|
@@ -3476,14 +3484,192 @@ const PinCodeInput = ({ length = 4, onComplete, onChange, masked = true, autoCom
|
|
|
3476
3484
|
}, type: "text", inputMode: "numeric", pattern: "[0-9]*", autoComplete: index === 0 ? autoComplete : "off", autoCorrect: "off", spellCheck: false, value: digit && masked ? MASK : digit, disabled: disabled, "aria-label": digitLabel(index + 1, length), "aria-invalid": invalid || undefined, "data-filled": !!digit, onChange: (event) => handleChange(index, event), onKeyDown: (event) => handleKeyDown(index, event), onPaste: (event) => handlePaste(index, event), onFocus: (event) => event.target.select(), className: cn("h-14 w-12 rounded-lg border bg-white text-center text-2xl font-medium text-ews-gray-900 outline-none", "transition-colors focus:border-ews-primary focus:ring-2 focus:ring-ews-primary", "disabled:cursor-not-allowed disabled:opacity-60", invalid ? "border-ews-error" : digit ? "border-ews-primary" : "border-ews-gray-300", disabled && digit && "animate-pulse") }, index))) }));
|
|
3477
3485
|
};
|
|
3478
3486
|
|
|
3487
|
+
/**
|
|
3488
|
+
* A visible tooltip for an icon button (hover and keyboard focus), so users
|
|
3489
|
+
* know what the mic and the dictation buttons do. The button keeps its own
|
|
3490
|
+
* aria-label; the tooltip is tied to it with aria-describedby.
|
|
3491
|
+
*/
|
|
3492
|
+
const ActionTooltip = ({ label, children, className }) => {
|
|
3493
|
+
const id = React.useId();
|
|
3494
|
+
return (jsxRuntime.jsxs("span", { className: cn("group relative inline-flex", className), children: [children(id), jsxRuntime.jsx("span", { id: id, role: "tooltip", className: cn("pointer-events-none absolute bottom-full start-1/2 z-50 mb-2 -translate-x-1/2 whitespace-nowrap rounded-md", "bg-ews-gray-900 px-2 py-1 text-xs font-medium text-white opacity-0 shadow transition-opacity duration-150", "group-hover:opacity-100 group-focus-within:opacity-100 rtl:translate-x-1/2"), children: label })] }));
|
|
3495
|
+
};
|
|
3496
|
+
|
|
3497
|
+
const DEFAULT_TEXTS$1 = {
|
|
3498
|
+
cancel: "Annuler la dictée",
|
|
3499
|
+
done: "Terminer et modifier le texte",
|
|
3500
|
+
send: "Envoyer",
|
|
3501
|
+
unavailable: "Micro indisponible : autorisez-le (page https requise).",
|
|
3502
|
+
};
|
|
3503
|
+
const BCP47$1 = { fr: "fr-FR", en: "en-US", ar: "ar-SA" };
|
|
3504
|
+
const BARS = 28;
|
|
3505
|
+
const BLOCKING_ERRORS$1 = new Set(["not-allowed", "service-not-allowed", "audio-capture"]);
|
|
3506
|
+
const formatElapsed = (seconds) => `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, "0")}`;
|
|
3507
|
+
/**
|
|
3508
|
+
* The recording bar of the mockup (audio3): cancel, a live waveform, the
|
|
3509
|
+
* elapsed time, done and send. Recording starts as soon as it shows. The
|
|
3510
|
+
* waveform follows the mic level when the browser gives it, and moves while
|
|
3511
|
+
* words come in otherwise.
|
|
3512
|
+
*/
|
|
3513
|
+
const AssistantDictationBar = ({ language, onCancel, onDone, onSend, texts, className, }) => {
|
|
3514
|
+
const t = { ...DEFAULT_TEXTS$1, ...texts };
|
|
3515
|
+
const [levels, setLevels] = React.useState(() => Array(BARS).fill(0.15));
|
|
3516
|
+
const [elapsed, setElapsed] = React.useState(0);
|
|
3517
|
+
const [unavailable, setUnavailable] = React.useState(() => typeof window !== "undefined" && window.isSecureContext === false);
|
|
3518
|
+
const transcriptRef = React.useRef("");
|
|
3519
|
+
const recognitionRef = React.useRef(null);
|
|
3520
|
+
const actionRef = React.useRef(null);
|
|
3521
|
+
const callbacks = React.useRef({ onCancel, onDone, onSend });
|
|
3522
|
+
callbacks.current = { onCancel, onDone, onSend };
|
|
3523
|
+
const pushLevel = React.useCallback((level) => {
|
|
3524
|
+
setLevels((current) => [...current.slice(1), Math.max(0.12, Math.min(1, level))]);
|
|
3525
|
+
}, []);
|
|
3526
|
+
const finish = React.useCallback(() => {
|
|
3527
|
+
const text = transcriptRef.current.trim();
|
|
3528
|
+
const action = actionRef.current;
|
|
3529
|
+
if (action === "send" && text)
|
|
3530
|
+
callbacks.current.onSend(text);
|
|
3531
|
+
else if (action === "done")
|
|
3532
|
+
callbacks.current.onDone(text);
|
|
3533
|
+
else
|
|
3534
|
+
callbacks.current.onCancel();
|
|
3535
|
+
}, []);
|
|
3536
|
+
// Speech recognition, from the first render.
|
|
3537
|
+
React.useEffect(() => {
|
|
3538
|
+
if (unavailable)
|
|
3539
|
+
return;
|
|
3540
|
+
const Ctor = getSpeechRecognition();
|
|
3541
|
+
if (!Ctor) {
|
|
3542
|
+
setUnavailable(true);
|
|
3543
|
+
return;
|
|
3544
|
+
}
|
|
3545
|
+
const recognition = new Ctor();
|
|
3546
|
+
recognition.lang = BCP47$1[language] ?? language;
|
|
3547
|
+
recognition.interimResults = true;
|
|
3548
|
+
recognition.continuous = true;
|
|
3549
|
+
recognition.onresult = (event) => {
|
|
3550
|
+
let text = "";
|
|
3551
|
+
for (let i = 0; i < event.results.length; i++)
|
|
3552
|
+
text += event.results[i][0]?.transcript ?? "";
|
|
3553
|
+
if (text.length !== transcriptRef.current.length)
|
|
3554
|
+
pushLevel(0.4 + Math.random() * 0.6);
|
|
3555
|
+
transcriptRef.current = text;
|
|
3556
|
+
};
|
|
3557
|
+
recognition.onerror = (event) => {
|
|
3558
|
+
if (event.error && BLOCKING_ERRORS$1.has(event.error)) {
|
|
3559
|
+
recognition.onend = null;
|
|
3560
|
+
recognitionRef.current = null;
|
|
3561
|
+
setUnavailable(true);
|
|
3562
|
+
}
|
|
3563
|
+
};
|
|
3564
|
+
recognition.onend = () => {
|
|
3565
|
+
if (recognitionRef.current !== recognition)
|
|
3566
|
+
return;
|
|
3567
|
+
recognitionRef.current = null;
|
|
3568
|
+
if (actionRef.current)
|
|
3569
|
+
finish();
|
|
3570
|
+
else {
|
|
3571
|
+
// The browser stopped after a silence: keep recording.
|
|
3572
|
+
try {
|
|
3573
|
+
recognition.start();
|
|
3574
|
+
recognitionRef.current = recognition;
|
|
3575
|
+
}
|
|
3576
|
+
catch {
|
|
3577
|
+
/* ignore: the user can still press done */
|
|
3578
|
+
}
|
|
3579
|
+
}
|
|
3580
|
+
};
|
|
3581
|
+
recognitionRef.current = recognition;
|
|
3582
|
+
recognition.start();
|
|
3583
|
+
return () => {
|
|
3584
|
+
recognition.onend = null;
|
|
3585
|
+
recognitionRef.current = null;
|
|
3586
|
+
recognition.abort();
|
|
3587
|
+
};
|
|
3588
|
+
}, [finish, language, pushLevel, unavailable]);
|
|
3589
|
+
// Live mic level for the waveform, where the browser allows it.
|
|
3590
|
+
React.useEffect(() => {
|
|
3591
|
+
if (unavailable || typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia)
|
|
3592
|
+
return;
|
|
3593
|
+
const w = window;
|
|
3594
|
+
const AudioCtx = w.AudioContext ?? w.webkitAudioContext;
|
|
3595
|
+
if (!AudioCtx)
|
|
3596
|
+
return;
|
|
3597
|
+
let stream = null;
|
|
3598
|
+
let context = null;
|
|
3599
|
+
let frame = 0;
|
|
3600
|
+
let cancelled = false;
|
|
3601
|
+
navigator.mediaDevices
|
|
3602
|
+
.getUserMedia({ audio: true })
|
|
3603
|
+
.then((media) => {
|
|
3604
|
+
if (cancelled) {
|
|
3605
|
+
media.getTracks().forEach((track) => track.stop());
|
|
3606
|
+
return;
|
|
3607
|
+
}
|
|
3608
|
+
stream = media;
|
|
3609
|
+
context = new AudioCtx();
|
|
3610
|
+
const analyser = context.createAnalyser();
|
|
3611
|
+
analyser.fftSize = 256;
|
|
3612
|
+
context.createMediaStreamSource(media).connect(analyser);
|
|
3613
|
+
const data = new Uint8Array(analyser.frequencyBinCount);
|
|
3614
|
+
let last = 0;
|
|
3615
|
+
const tick = (now) => {
|
|
3616
|
+
if (now - last > 80) {
|
|
3617
|
+
analyser.getByteTimeDomainData(data);
|
|
3618
|
+
let peak = 0;
|
|
3619
|
+
for (const value of data)
|
|
3620
|
+
peak = Math.max(peak, Math.abs(value - 128));
|
|
3621
|
+
pushLevel(peak / 64);
|
|
3622
|
+
last = now;
|
|
3623
|
+
}
|
|
3624
|
+
frame = requestAnimationFrame(tick);
|
|
3625
|
+
};
|
|
3626
|
+
frame = requestAnimationFrame(tick);
|
|
3627
|
+
return undefined;
|
|
3628
|
+
})
|
|
3629
|
+
.catch(() => undefined);
|
|
3630
|
+
return () => {
|
|
3631
|
+
cancelled = true;
|
|
3632
|
+
cancelAnimationFrame(frame);
|
|
3633
|
+
stream?.getTracks().forEach((track) => track.stop());
|
|
3634
|
+
void context?.close();
|
|
3635
|
+
};
|
|
3636
|
+
}, [pushLevel, unavailable]);
|
|
3637
|
+
// Elapsed time.
|
|
3638
|
+
React.useEffect(() => {
|
|
3639
|
+
if (unavailable)
|
|
3640
|
+
return;
|
|
3641
|
+
const timer = setInterval(() => setElapsed((value) => value + 1), 1000);
|
|
3642
|
+
return () => clearInterval(timer);
|
|
3643
|
+
}, [unavailable]);
|
|
3644
|
+
const end = (action) => {
|
|
3645
|
+
actionRef.current = action;
|
|
3646
|
+
const recognition = recognitionRef.current;
|
|
3647
|
+
if (!recognition || action === "cancel") {
|
|
3648
|
+
if (recognition) {
|
|
3649
|
+
recognition.onend = null;
|
|
3650
|
+
recognitionRef.current = null;
|
|
3651
|
+
recognition.abort();
|
|
3652
|
+
}
|
|
3653
|
+
finish();
|
|
3654
|
+
return;
|
|
3655
|
+
}
|
|
3656
|
+
// stop() delivers the last words, then onend calls finish().
|
|
3657
|
+
recognition.stop();
|
|
3658
|
+
};
|
|
3659
|
+
return (jsxRuntime.jsxs("div", { className: cn("flex w-full items-center gap-2 rounded-full bg-ews-gray-100 px-3 py-1.5", className), "data-testid": "assistant-dictation", children: [jsxRuntime.jsx("button", { type: "button", onClick: () => end("cancel"), "aria-label": t.cancel, title: t.cancel, className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-full text-ews-gray-600 hover:bg-ews-gray-200 focus:outline-none focus:ring-2 focus:ring-ews-primary", "data-testid": "assistant-dictation-cancel", children: jsxRuntime.jsx(lucideReact.X, { className: "h-5 w-5", "aria-hidden": "true" }) }), unavailable ? (jsxRuntime.jsx("p", { className: "flex-1 truncate text-sm text-ews-error", role: "alert", "data-testid": "assistant-dictation-unavailable", children: t.unavailable })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: "flex h-8 flex-1 items-center justify-center gap-[3px]", "aria-hidden": "true", "data-testid": "assistant-dictation-wave", children: levels.map((level, index) => (jsxRuntime.jsx("span", { className: "w-[3px] rounded-full bg-ews-gray-700 transition-[height] duration-100", style: { height: `${Math.round(level * 100)}%` } }, index))) }), jsxRuntime.jsx("span", { className: "w-10 shrink-0 text-end text-sm tabular-nums text-ews-gray-600", "data-testid": "assistant-dictation-time", children: formatElapsed(elapsed) })] })), jsxRuntime.jsx("button", { type: "button", onClick: () => end("done"), disabled: unavailable, "aria-label": t.done, title: t.done, className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-full text-ews-gray-700 hover:bg-ews-gray-200 focus:outline-none focus:ring-2 focus:ring-ews-primary disabled:opacity-40", "data-testid": "assistant-dictation-done", children: jsxRuntime.jsx(lucideReact.Check, { className: "h-5 w-5", "aria-hidden": "true" }) }), jsxRuntime.jsx("button", { type: "button", onClick: () => end("send"), disabled: unavailable, "aria-label": t.send, title: t.send, className: "flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-ews-primary text-white shadow-sm hover:bg-ews-primary-hover focus:outline-none focus:ring-2 focus:ring-ews-primary focus:ring-offset-1 disabled:opacity-40", "data-testid": "assistant-dictation-send", children: jsxRuntime.jsx(lucideReact.ArrowUp, { className: "h-5 w-5", "aria-hidden": "true" }) })] }));
|
|
3660
|
+
};
|
|
3661
|
+
|
|
3479
3662
|
const MAX_MESSAGE_LENGTH = 2000;
|
|
3480
3663
|
/**
|
|
3481
|
-
* The composer of the mockup: textarea, hint,
|
|
3482
|
-
*
|
|
3664
|
+
* The composer of the mockup: textarea, hint, a dictation button (waveform:
|
|
3665
|
+
* a recording bar transcribes into the field), a mic (the voice conversation)
|
|
3666
|
+
* and send. Both voice buttons show a tooltip. Enter sends, Shift+Enter breaks.
|
|
3483
3667
|
*/
|
|
3484
|
-
const AssistantComposer = ({ language, onSend, disabled = false, voice = true, texts, className, }) => {
|
|
3668
|
+
const AssistantComposer = ({ language, onSend, disabled = false, voice = true, onVoice, draft = null, texts, className, }) => {
|
|
3485
3669
|
const [value, setValue] = React.useState("");
|
|
3670
|
+
const [dictating, setDictating] = React.useState(false);
|
|
3486
3671
|
const textareaRef = React.useRef(null);
|
|
3672
|
+
const speechSupported = useSpeechSupported();
|
|
3487
3673
|
const submit = React.useCallback((content) => {
|
|
3488
3674
|
const trimmed = content.trim();
|
|
3489
3675
|
if (!trimmed || disabled)
|
|
@@ -3491,32 +3677,34 @@ const AssistantComposer = ({ language, onSend, disabled = false, voice = true, t
|
|
|
3491
3677
|
onSend(trimmed.slice(0, MAX_MESSAGE_LENGTH));
|
|
3492
3678
|
setValue("");
|
|
3493
3679
|
}, [disabled, onSend]);
|
|
3494
|
-
const
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3680
|
+
const handledDraftRef = React.useRef(null);
|
|
3681
|
+
React.useEffect(() => {
|
|
3682
|
+
if (!draft || draft.id === handledDraftRef.current)
|
|
3683
|
+
return;
|
|
3684
|
+
handledDraftRef.current = draft.id;
|
|
3685
|
+
setValue(draft.content.slice(0, MAX_MESSAGE_LENGTH));
|
|
3686
|
+
textareaRef.current?.focus();
|
|
3687
|
+
}, [draft]);
|
|
3499
3688
|
const onKeyDown = (event) => {
|
|
3500
3689
|
if (event.key === "Enter" && !event.shiftKey) {
|
|
3501
3690
|
event.preventDefault();
|
|
3502
3691
|
submit(value);
|
|
3503
3692
|
}
|
|
3504
3693
|
};
|
|
3505
|
-
const
|
|
3694
|
+
const showVoice = voice && speechSupported;
|
|
3695
|
+
const dictateLabel = texts.dictate ?? "Dicter un message";
|
|
3506
3696
|
// A plain div rather than a <form>: Enter and the send button submit, and a
|
|
3507
3697
|
// form followed by a sibling trips React hydration under happy-dom (the apps'
|
|
3508
3698
|
// server-rendering tests) although browsers are fine.
|
|
3509
|
-
return (jsxRuntime.jsxs("div", { className: cn("w-full", className), children: [
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
? "bg-ews-error text-white animate-pulse"
|
|
3519
|
-
: "text-ews-gray-500 hover:bg-ews-gray-100 hover:text-ews-primary", "disabled:opacity-50"), "data-testid": "assistant-mic", children: jsxRuntime.jsx(lucideReact.Mic, { className: "h-5 w-5", "aria-hidden": "true" }) })), jsxRuntime.jsx("button", { type: "button", onClick: () => submit(value), "aria-label": texts.send, title: texts.send, disabled: disabled || value.trim().length === 0, className: "rounded-full bg-ews-primary p-2 text-white transition-colors hover:bg-ews-primary-hover focus:outline-none focus:ring-2 focus:ring-ews-primary focus:ring-offset-1 disabled:opacity-40", "data-testid": "assistant-send", children: jsxRuntime.jsx(lucideReact.SendHorizontal, { className: "h-5 w-5 rtl:-scale-x-100", "aria-hidden": "true" }) })] }), jsxRuntime.jsx("p", { className: "mt-1.5 px-2 text-xs text-ews-gray-400", children: texts.hint })] }));
|
|
3699
|
+
return (jsxRuntime.jsxs("div", { className: cn("w-full", className), children: [dictating ? (jsxRuntime.jsx(AssistantDictationBar, { language: language, texts: texts.dictation, onCancel: () => setDictating(false), onDone: (text) => {
|
|
3700
|
+
setDictating(false);
|
|
3701
|
+
if (text)
|
|
3702
|
+
setValue((current) => (current ? `${current} ${text}` : text).slice(0, MAX_MESSAGE_LENGTH));
|
|
3703
|
+
setTimeout(() => textareaRef.current?.focus(), 0);
|
|
3704
|
+
}, onSend: (text) => {
|
|
3705
|
+
setDictating(false);
|
|
3706
|
+
submit(value ? `${value} ${text}` : text);
|
|
3707
|
+
} })) : (jsxRuntime.jsxs("div", { className: cn("flex items-end gap-2 rounded-2xl border border-ews-gray-200 bg-white px-3 py-2 shadow-sm", "focus-within:border-ews-primary focus-within:ring-2 focus-within:ring-ews-primary/20"), children: [jsxRuntime.jsx("textarea", { ref: textareaRef, value: value, onChange: (event) => setValue(event.target.value), onKeyDown: onKeyDown, rows: 1, maxLength: MAX_MESSAGE_LENGTH, placeholder: texts.placeholder, "aria-label": texts.placeholder, disabled: disabled, className: "max-h-40 min-h-[2.5rem] flex-1 resize-none bg-transparent py-2 text-base text-ews-gray-900 placeholder:text-ews-gray-400 focus:outline-none disabled:opacity-60" }), showVoice && (jsxRuntime.jsx(ActionTooltip, { label: dictateLabel, children: (describedBy) => (jsxRuntime.jsx("button", { type: "button", "aria-label": dictateLabel, "aria-describedby": describedBy, disabled: disabled, onClick: () => setDictating(true), className: "rounded-full p-2 text-ews-gray-500 transition-colors hover:bg-ews-gray-100 hover:text-ews-primary focus:outline-none focus:ring-2 focus:ring-ews-primary disabled:opacity-50", "data-testid": "assistant-dictate", children: jsxRuntime.jsx(lucideReact.AudioLines, { className: "h-5 w-5", "aria-hidden": "true" }) })) })), showVoice && onVoice && (jsxRuntime.jsx(ActionTooltip, { label: texts.holdToTalk, children: (describedBy) => (jsxRuntime.jsx("button", { type: "button", "aria-label": texts.holdToTalk, "aria-describedby": describedBy, disabled: disabled, onClick: onVoice, className: "rounded-full p-2 text-ews-gray-500 transition-colors hover:bg-ews-gray-100 hover:text-ews-primary focus:outline-none focus:ring-2 focus:ring-ews-primary disabled:opacity-50", "data-testid": "assistant-mic", children: jsxRuntime.jsx(lucideReact.Mic, { className: "h-5 w-5", "aria-hidden": "true" }) })) })), jsxRuntime.jsx("button", { type: "button", onClick: () => submit(value), "aria-label": texts.send, title: texts.send, disabled: disabled || value.trim().length === 0, className: "rounded-full bg-ews-primary p-2 text-white transition-colors hover:bg-ews-primary-hover focus:outline-none focus:ring-2 focus:ring-ews-primary focus:ring-offset-1 disabled:opacity-40", "data-testid": "assistant-send", children: jsxRuntime.jsx(lucideReact.SendHorizontal, { className: "h-5 w-5 rtl:-scale-x-100", "aria-hidden": "true" }) })] })), jsxRuntime.jsx("p", { className: "mt-1.5 px-2 text-xs text-ews-gray-400", children: texts.hint })] }));
|
|
3520
3708
|
};
|
|
3521
3709
|
|
|
3522
3710
|
/**
|
|
@@ -3566,7 +3754,8 @@ function renderInline(text, keyPrefix, options) {
|
|
|
3566
3754
|
nodes.push(text.slice(last, start));
|
|
3567
3755
|
const token = match[0];
|
|
3568
3756
|
if (token.startsWith("**")) {
|
|
3569
|
-
|
|
3757
|
+
// MED-1801: a bold run can hold a link, e.g. **[Dr Ba](/doctors/8)**.
|
|
3758
|
+
nodes.push(jsxRuntime.jsx("strong", { children: renderInline(token.slice(2, -2), `${keyPrefix}-b${index}`, options) }, `${keyPrefix}-b${index}`));
|
|
3570
3759
|
}
|
|
3571
3760
|
else {
|
|
3572
3761
|
const close = token.indexOf("](");
|
|
@@ -3615,10 +3804,169 @@ const AssistantMessages = ({ messages, activity, onRetry, texts, linkOptions, cl
|
|
|
3615
3804
|
}), jsxRuntime.jsx("div", { ref: endRef })] }));
|
|
3616
3805
|
};
|
|
3617
3806
|
|
|
3618
|
-
/** One of the cards under the hero (
|
|
3807
|
+
/** One of the cards under the hero (mockups med1 and tools): icon, title, description, sent as a prompt on click. */
|
|
3619
3808
|
const AssistantSuggestionCard = ({ suggestion, onSelect, exampleLabel, disabled = false, className, }) => {
|
|
3620
3809
|
const isExample = suggestion.kind === "example";
|
|
3621
|
-
return (jsxRuntime.jsxs("button", { type: "button", disabled: disabled, onClick: () => onSelect(suggestion), className: cn(
|
|
3810
|
+
return (jsxRuntime.jsxs("button", { type: "button", disabled: disabled, onClick: () => onSelect(suggestion), className: cn(
|
|
3811
|
+
// A soft grey card without border (mockup tools); it lifts with a shadow on hover.
|
|
3812
|
+
"flex h-full w-full flex-col items-start gap-2 rounded-2xl bg-ews-gray-100 p-4 text-start transition-[background-color,box-shadow]", "hover:bg-ews-gray-50 hover:shadow-lg focus:outline-none focus:ring-2 focus:ring-ews-primary focus:ring-offset-1", "disabled:cursor-not-allowed disabled:opacity-60", className), "data-testid": `assistant-suggestion-${suggestion.id}`, children: [jsxRuntime.jsx("span", { className: "mb-2 text-ews-gray-600", "aria-hidden": "true", children: suggestion.icon ?? (isExample ? jsxRuntime.jsx(lucideReact.Sparkles, { className: "h-4 w-4" }) : jsxRuntime.jsx(lucideReact.ArrowRight, { className: "h-4 w-4" })) }), jsxRuntime.jsx("span", { className: "font-semibold leading-snug text-ews-gray-900", children: suggestion.title }), suggestion.description && (jsxRuntime.jsx("span", { className: "text-sm leading-snug text-ews-gray-400", children: suggestion.description })), isExample && (jsxRuntime.jsx("span", { className: "mt-auto text-xs uppercase tracking-wide text-ews-gray-400", children: exampleLabel }))] }));
|
|
3813
|
+
};
|
|
3814
|
+
|
|
3815
|
+
const DEFAULT_TEXTS = {
|
|
3816
|
+
title: "Conversation vocale avec Medecine360",
|
|
3817
|
+
listening: "À l'écoute…",
|
|
3818
|
+
thinking: "Réflexion…",
|
|
3819
|
+
speaking: "Réponse…",
|
|
3820
|
+
muted: "Micro coupé",
|
|
3821
|
+
mute: "Couper le micro",
|
|
3822
|
+
unmute: "Réactiver le micro",
|
|
3823
|
+
end: "Terminer",
|
|
3824
|
+
unavailable: "Le micro n'est pas accessible. Autorisez-le dans le navigateur ; il ne fonctionne que sur une page sécurisée (https).",
|
|
3825
|
+
};
|
|
3826
|
+
/** Errors after which listening again cannot work until the user acts. */
|
|
3827
|
+
const BLOCKING_ERRORS = new Set(["not-allowed", "service-not-allowed", "audio-capture"]);
|
|
3828
|
+
const BCP47 = { fr: "fr-FR", en: "en-US", ar: "ar-SA" };
|
|
3829
|
+
/** Markdown and links read aloud as plain words. */
|
|
3830
|
+
function toSpokenText(markdown) {
|
|
3831
|
+
return markdown
|
|
3832
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
|
3833
|
+
.replace(/https?:\/\/\S+/g, "")
|
|
3834
|
+
.replace(/\*\*/g, "")
|
|
3835
|
+
.replace(/^\s*[-*•]\s+/gm, "")
|
|
3836
|
+
.replace(/\s+/g, " ")
|
|
3837
|
+
.trim();
|
|
3838
|
+
}
|
|
3839
|
+
/**
|
|
3840
|
+
* MED-1801 (voice): the voice conversation screen of the mockup. Listening
|
|
3841
|
+
* starts as soon as it opens; a finished sentence is sent to the same
|
|
3842
|
+
* conversation; the mic pauses while the assistant answers (read aloud when
|
|
3843
|
+
* the browser can) and resumes after. Mute stops the mic; Terminer closes.
|
|
3844
|
+
*/
|
|
3845
|
+
const AssistantVoiceMode = ({ language, messages, busy, onSend, onClose, texts, speak = true, }) => {
|
|
3846
|
+
const t = { ...DEFAULT_TEXTS, ...texts };
|
|
3847
|
+
const [muted, setMuted] = React.useState(false);
|
|
3848
|
+
const [speaking, setSpeaking] = React.useState(false);
|
|
3849
|
+
const [interim, setInterim] = React.useState("");
|
|
3850
|
+
// The mic cannot work: insecure page (http) or permission refused.
|
|
3851
|
+
const [unavailable, setUnavailable] = React.useState(() => typeof window !== "undefined" && window.isSecureContext === false);
|
|
3852
|
+
const recognitionRef = React.useRef(null);
|
|
3853
|
+
const restartTimerRef = React.useRef(null);
|
|
3854
|
+
const sentRef = React.useRef(false);
|
|
3855
|
+
// Only the turns of this voice session are shown.
|
|
3856
|
+
const [firstIndex] = React.useState(messages.length);
|
|
3857
|
+
const spokenRef = React.useRef(null);
|
|
3858
|
+
const callbacks = React.useRef({ onSend });
|
|
3859
|
+
callbacks.current = { onSend };
|
|
3860
|
+
const stopListening = React.useCallback(() => {
|
|
3861
|
+
if (restartTimerRef.current)
|
|
3862
|
+
clearTimeout(restartTimerRef.current);
|
|
3863
|
+
restartTimerRef.current = null;
|
|
3864
|
+
const recognition = recognitionRef.current;
|
|
3865
|
+
recognitionRef.current = null;
|
|
3866
|
+
if (recognition) {
|
|
3867
|
+
recognition.onend = null;
|
|
3868
|
+
recognition.abort();
|
|
3869
|
+
}
|
|
3870
|
+
}, []);
|
|
3871
|
+
const startListening = React.useCallback(() => {
|
|
3872
|
+
const Ctor = getSpeechRecognition();
|
|
3873
|
+
if (!Ctor || recognitionRef.current)
|
|
3874
|
+
return;
|
|
3875
|
+
const recognition = new Ctor();
|
|
3876
|
+
recognition.lang = BCP47[language] ?? language;
|
|
3877
|
+
recognition.interimResults = true;
|
|
3878
|
+
recognition.continuous = true;
|
|
3879
|
+
sentRef.current = false;
|
|
3880
|
+
recognition.onresult = (event) => {
|
|
3881
|
+
let text = "";
|
|
3882
|
+
let final = false;
|
|
3883
|
+
for (let i = 0; i < event.results.length; i++) {
|
|
3884
|
+
text += event.results[i][0]?.transcript ?? "";
|
|
3885
|
+
if (event.results[i].isFinal)
|
|
3886
|
+
final = true;
|
|
3887
|
+
}
|
|
3888
|
+
const trimmed = text.trim();
|
|
3889
|
+
setInterim(trimmed);
|
|
3890
|
+
if (final && trimmed && !sentRef.current) {
|
|
3891
|
+
sentRef.current = true;
|
|
3892
|
+
setInterim("");
|
|
3893
|
+
recognitionRef.current = null;
|
|
3894
|
+
recognition.onend = null;
|
|
3895
|
+
recognition.stop();
|
|
3896
|
+
callbacks.current.onSend(trimmed);
|
|
3897
|
+
}
|
|
3898
|
+
};
|
|
3899
|
+
recognition.onerror = (event) => {
|
|
3900
|
+
// Permission refused or no mic: stop here and say so, never retry in a loop.
|
|
3901
|
+
if (event.error && BLOCKING_ERRORS.has(event.error)) {
|
|
3902
|
+
recognitionRef.current = null;
|
|
3903
|
+
recognition.onend = null;
|
|
3904
|
+
setUnavailable(true);
|
|
3905
|
+
}
|
|
3906
|
+
/* other errors (no-speech, network): onend follows and listens again */
|
|
3907
|
+
};
|
|
3908
|
+
recognition.onend = () => {
|
|
3909
|
+
// The browser ends recognition after a silence: listen again, a moment later.
|
|
3910
|
+
if (recognitionRef.current === recognition) {
|
|
3911
|
+
recognitionRef.current = null;
|
|
3912
|
+
restartTimerRef.current = setTimeout(() => startListeningRef.current(), 250);
|
|
3913
|
+
}
|
|
3914
|
+
};
|
|
3915
|
+
recognitionRef.current = recognition;
|
|
3916
|
+
recognition.start();
|
|
3917
|
+
}, [language]);
|
|
3918
|
+
const startListeningRef = React.useRef(startListening);
|
|
3919
|
+
startListeningRef.current = startListening;
|
|
3920
|
+
const last = messages[messages.length - 1];
|
|
3921
|
+
const canSpeak = speak && typeof window !== "undefined" && "speechSynthesis" in window;
|
|
3922
|
+
// Read the finished answer aloud, once.
|
|
3923
|
+
React.useEffect(() => {
|
|
3924
|
+
if (busy || !last || last.role !== "assistant" || last.status === "streaming")
|
|
3925
|
+
return;
|
|
3926
|
+
if (messages.length <= firstIndex || spokenRef.current === last.id)
|
|
3927
|
+
return;
|
|
3928
|
+
spokenRef.current = last.id;
|
|
3929
|
+
const text = toSpokenText(last.content);
|
|
3930
|
+
if (!canSpeak || !text)
|
|
3931
|
+
return;
|
|
3932
|
+
const utterance = new SpeechSynthesisUtterance(text);
|
|
3933
|
+
utterance.lang = BCP47[language] ?? language;
|
|
3934
|
+
utterance.onend = () => setSpeaking(false);
|
|
3935
|
+
utterance.onerror = () => setSpeaking(false);
|
|
3936
|
+
setSpeaking(true);
|
|
3937
|
+
window.speechSynthesis.speak(utterance);
|
|
3938
|
+
}, [busy, canSpeak, firstIndex, language, last, messages.length]);
|
|
3939
|
+
// Listen whenever nobody else is talking.
|
|
3940
|
+
const shouldListen = !muted && !busy && !speaking && !unavailable;
|
|
3941
|
+
React.useEffect(() => {
|
|
3942
|
+
if (shouldListen)
|
|
3943
|
+
startListening();
|
|
3944
|
+
else
|
|
3945
|
+
stopListening();
|
|
3946
|
+
}, [shouldListen, startListening, stopListening]);
|
|
3947
|
+
React.useEffect(() => () => {
|
|
3948
|
+
stopListening();
|
|
3949
|
+
if (typeof window !== "undefined" && "speechSynthesis" in window)
|
|
3950
|
+
window.speechSynthesis.cancel();
|
|
3951
|
+
}, [stopListening]);
|
|
3952
|
+
const phase = unavailable
|
|
3953
|
+
? "unavailable"
|
|
3954
|
+
: muted
|
|
3955
|
+
? "muted"
|
|
3956
|
+
: busy
|
|
3957
|
+
? "thinking"
|
|
3958
|
+
: speaking
|
|
3959
|
+
? "speaking"
|
|
3960
|
+
: "listening";
|
|
3961
|
+
const status = {
|
|
3962
|
+
listening: t.listening,
|
|
3963
|
+
thinking: t.thinking,
|
|
3964
|
+
speaking: t.speaking,
|
|
3965
|
+
muted: t.muted,
|
|
3966
|
+
unavailable: t.unavailable,
|
|
3967
|
+
}[phase];
|
|
3968
|
+
const turns = messages.slice(firstIndex).filter((message) => message.content.trim().length > 0);
|
|
3969
|
+
return (jsxRuntime.jsxs("div", { dir: language === "ar" ? "rtl" : "ltr", className: "absolute inset-0 z-30 flex flex-col bg-white", role: "dialog", "aria-label": t.title, "data-phase": phase, "data-testid": "assistant-voice-mode", children: [jsxRuntime.jsxs("div", { className: "flex items-center gap-2 px-5 pt-4 text-sm text-ews-gray-600", children: [jsxRuntime.jsx("span", { className: "h-2 w-2 rounded-full bg-emerald-500", "aria-hidden": "true" }), t.title] }), jsxRuntime.jsxs("div", { className: "flex flex-col items-center gap-3 pt-8", children: [jsxRuntime.jsx("div", { className: cn("h-32 w-32 rounded-full shadow-[0_0_60px_rgba(33,89,108,0.35)] transition-transform duration-700", "bg-[radial-gradient(circle_at_30%_25%,#ffffff_0%,#c0d0d4_35%,#21596c_75%,#1a4756_100%)]", phase === "listening" && "animate-pulse", phase === "speaking" && "scale-110", (phase === "muted" || phase === "unavailable") && "opacity-50"), "aria-hidden": "true", "data-testid": "assistant-voice-orb" }), jsxRuntime.jsx("p", { className: cn("max-w-xs px-4 text-center text-sm", phase === "unavailable" ? "text-ews-error" : "text-ews-gray-500"), role: phase === "unavailable" ? "alert" : undefined, "aria-live": "polite", "data-testid": "assistant-voice-status", children: status })] }), jsxRuntime.jsxs("div", { className: "flex-1 space-y-3 overflow-y-auto px-5 py-6", "data-testid": "assistant-voice-transcript", children: [turns.map((message) => (jsxRuntime.jsx("div", { className: cn("flex", message.role === "user" ? "justify-end" : "justify-start"), children: jsxRuntime.jsx("p", { className: cn("max-w-[85%] rounded-2xl px-4 py-2 text-sm", message.role === "user" ? "bg-ews-gray-100 text-ews-gray-800" : "bg-ews-primary-light/40 text-ews-gray-900"), children: message.role === "assistant" ? toSpokenText(message.content) : message.content }) }, message.id))), interim && (jsxRuntime.jsx("div", { className: "flex justify-end", children: jsxRuntime.jsx("p", { className: "max-w-[85%] rounded-2xl bg-ews-gray-100 px-4 py-2 text-sm italic text-ews-gray-500", "data-testid": "assistant-voice-interim", children: interim }) }))] }), jsxRuntime.jsxs("div", { className: "flex items-center justify-center gap-3 pb-6", children: [jsxRuntime.jsx("button", { type: "button", disabled: unavailable, onClick: () => setMuted((value) => !value), "aria-pressed": muted, "aria-label": muted ? t.unmute : t.mute, title: muted ? t.unmute : t.mute, className: "flex h-11 w-11 items-center justify-center rounded-full border border-ews-gray-200 text-ews-gray-600 hover:bg-ews-gray-50 disabled:opacity-40", "data-testid": "assistant-voice-mute", children: muted ? jsxRuntime.jsx(lucideReact.MicOff, { className: "h-5 w-5", "aria-hidden": "true" }) : jsxRuntime.jsx(lucideReact.Mic, { className: "h-5 w-5", "aria-hidden": "true" }) }), jsxRuntime.jsx("button", { type: "button", onClick: onClose, className: "rounded-full bg-red-600 px-6 py-2.5 text-sm font-semibold text-white shadow hover:bg-red-700", "data-testid": "assistant-voice-end", children: t.end })] })] }));
|
|
3622
3970
|
};
|
|
3623
3971
|
|
|
3624
3972
|
const DIRECTION$1 = {
|
|
@@ -3640,7 +3988,7 @@ function conversationTitle(firstMessage, max = 60) {
|
|
|
3640
3988
|
* Server-rendering safe: nothing touches `window` at render time, the
|
|
3641
3989
|
* speech API is detected after mount.
|
|
3642
3990
|
*/
|
|
3643
|
-
const AssistantChat = ({ transport, language, texts, suggestions, conversations = [], activeConversationId = null, initialMessages, onConversationCreated, onSelectConversation, onNewConversation, signInHref, headerAction, disclaimer, heroBackground, voice = true, pendingConfirmation = null, onConfirm, onCancelConfirmation, layout = "page", queuedPrompt = null, onNavigate, historyOpen = false, onHistoryOpenChange, className, }) => {
|
|
3991
|
+
const AssistantChat = ({ transport, language, texts, suggestions, conversations = [], activeConversationId = null, initialMessages, onConversationCreated, onSelectConversation, onNewConversation, signInHref, headerAction, disclaimer, heroBackground, heroLogo, voice = true, pendingConfirmation = null, onConfirm, onCancelConfirmation, layout = "page", queuedPrompt = null, voiceRequest = null, composerDraft = null, onNavigate, historyOpen = false, onHistoryOpenChange, className, }) => {
|
|
3644
3992
|
const [messages, setMessages] = React.useState(initialMessages ?? []);
|
|
3645
3993
|
const [activity, setActivity] = React.useState(null);
|
|
3646
3994
|
const [busy, setBusy] = React.useState(false);
|
|
@@ -3751,6 +4099,15 @@ const AssistantChat = ({ transport, language, texts, suggestions, conversations
|
|
|
3751
4099
|
handledPromptRef.current = queuedPrompt.id;
|
|
3752
4100
|
void send(queuedPrompt.content);
|
|
3753
4101
|
}, [busy, queuedPrompt, send]);
|
|
4102
|
+
// Voice conversation screen, opened by the mic (here or in the launcher bar).
|
|
4103
|
+
const [voiceOpen, setVoiceOpen] = React.useState(false);
|
|
4104
|
+
const handledVoiceRef = React.useRef(null);
|
|
4105
|
+
React.useEffect(() => {
|
|
4106
|
+
if (!voiceRequest || voiceRequest.id === handledVoiceRef.current)
|
|
4107
|
+
return;
|
|
4108
|
+
handledVoiceRef.current = voiceRequest.id;
|
|
4109
|
+
setVoiceOpen(true);
|
|
4110
|
+
}, [voiceRequest]);
|
|
3754
4111
|
/** Retry re-sends the user message that preceded the failed answer. */
|
|
3755
4112
|
const retry = React.useCallback((failed) => {
|
|
3756
4113
|
const index = messages.findIndex((message) => message.id === failed.id);
|
|
@@ -3773,13 +4130,17 @@ const AssistantChat = ({ transport, language, texts, suggestions, conversations
|
|
|
3773
4130
|
const compact = layout === "panel";
|
|
3774
4131
|
const withList = layout !== "panel";
|
|
3775
4132
|
const historyLabel = texts.history ?? texts.recentConversations;
|
|
3776
|
-
return (jsxRuntime.jsxs("div", { dir: dir, lang: language, className: cn("overflow-hidden bg-white", layout === "page" && "grid min-h-[36rem] grid-cols-1 rounded-2xl border border-ews-gray-200 shadow-sm md:grid-cols-[16rem_1fr]", layout === "wide" && "grid h-full min-h-0 grid-cols-1 md:grid-cols-[16rem_1fr]", compact && "relative flex h-full min-h-0 flex-col", className), "data-layout": layout, "data-testid": "assistant-chat", children: [withList && (jsxRuntime.jsx(AssistantConversationList, { conversations: conversations, activeConversationId: activeConversationId, onSelect: onSelectConversation, onNew: startNewConversation, texts: texts, signInHref: signInHref, className: "hidden border-e border-ews-gray-200 md:flex" })), compact && historyOpen && (jsxRuntime.jsxs("div", { className: "absolute inset-0 z-10 flex", "data-testid": "assistant-history", children: [jsxRuntime.jsx(AssistantConversationList, { conversations: conversations, activeConversationId: activeConversationId, onSelect: (id) => {
|
|
4133
|
+
return (jsxRuntime.jsxs("div", { dir: dir, lang: language, className: cn("relative overflow-hidden bg-white", layout === "page" && "grid min-h-[36rem] grid-cols-1 rounded-2xl border border-ews-gray-200 shadow-sm md:grid-cols-[16rem_1fr]", layout === "wide" && "grid h-full min-h-0 grid-cols-1 md:grid-cols-[16rem_1fr]", compact && "relative flex h-full min-h-0 flex-col", className), "data-layout": layout, "data-testid": "assistant-chat", children: [withList && (jsxRuntime.jsx(AssistantConversationList, { conversations: conversations, activeConversationId: activeConversationId, onSelect: onSelectConversation, onNew: startNewConversation, texts: texts, signInHref: signInHref, className: "hidden border-e border-ews-gray-200 md:flex" })), compact && historyOpen && (jsxRuntime.jsxs("div", { className: "absolute inset-0 z-10 flex", "data-testid": "assistant-history", children: [jsxRuntime.jsx(AssistantConversationList, { conversations: conversations, activeConversationId: activeConversationId, onSelect: (id) => {
|
|
3777
4134
|
onSelectConversation?.(id);
|
|
3778
4135
|
onHistoryOpenChange?.(false);
|
|
3779
4136
|
}, onNew: () => {
|
|
3780
4137
|
startNewConversation();
|
|
3781
4138
|
onHistoryOpenChange?.(false);
|
|
3782
|
-
}, texts: texts, signInHref: signInHref, className: "w-72 max-w-full border-e border-ews-gray-200 shadow-xl" }), jsxRuntime.jsx("div", { className: "flex-1 bg-black/20", onClick: () => onHistoryOpenChange?.(false), "data-testid": "assistant-history-overlay" })] })), jsxRuntime.jsxs("div", { className: "flex min-h-0 min-w-0 flex-1 flex-col", children: [jsxRuntime.jsxs("header", { className: "flex items-center justify-between gap-3 border-b border-ews-gray-200 px-4 py-3", children: [jsxRuntime.jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [jsxRuntime.jsx("span", { className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-ews-primary text-sm font-bold text-white", "aria-hidden": "true", children: texts.brandName.charAt(0) }), jsxRuntime.jsx("span", { className: "truncate font-semibold text-ews-gray-900", children: texts.brandName }), jsxRuntime.jsx("span", { className: "rounded-full bg-ews-gray-100 px-2 py-0.5 text-xs font-medium text-ews-gray-600", children: "AI" }), jsxRuntime.jsxs("span", { className: "flex items-center gap-1 text-xs text-ews-gray-500", children: [jsxRuntime.jsx("span", { className: "h-2 w-2 rounded-full bg-ews-success", "aria-hidden": "true" }), texts.online] })] }), jsxRuntime.jsxs("div", { className: "flex shrink-0 items-center gap-1", children: [compact && onHistoryOpenChange && (jsxRuntime.jsx("button", { type: "button", onClick: () => onHistoryOpenChange(!historyOpen), "aria-label": historyLabel, "aria-pressed": historyOpen, title: historyLabel, className: cn("rounded-full p-2 hover:bg-ews-gray-100 hover:text-ews-primary focus:outline-none focus:ring-2 focus:ring-ews-primary", historyOpen ? "bg-ews-gray-100 text-ews-primary" : "text-ews-gray-500"), "data-testid": "assistant-history-toggle", children: jsxRuntime.jsx(lucideReact.History, { className: "h-4 w-4", "aria-hidden": "true" }) })), !withList && !empty && (jsxRuntime.jsx("button", { type: "button", onClick: startNewConversation, "aria-label": texts.newConversation, title: texts.newConversation, className: "rounded-full p-2 text-ews-gray-500 hover:bg-ews-gray-100 hover:text-ews-primary focus:outline-none focus:ring-2 focus:ring-ews-primary", "data-testid": "assistant-new-conversation", children: jsxRuntime.jsx(lucideReact.SquarePen, { className: "h-4 w-4", "aria-hidden": "true" }) })), headerAction] })] }), jsxRuntime.jsx("div", { className: "flex flex-1 flex-col overflow-y-auto", children: empty ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("section", { className: cn("relative overflow-hidden bg-ews-primary-light text-center",
|
|
4139
|
+
}, texts: texts, signInHref: signInHref, className: "w-72 max-w-full border-e border-ews-gray-200 shadow-xl" }), jsxRuntime.jsx("div", { className: "flex-1 bg-black/20", onClick: () => onHistoryOpenChange?.(false), "data-testid": "assistant-history-overlay" })] })), jsxRuntime.jsxs("div", { className: "relative flex min-h-0 min-w-0 flex-1 flex-col", children: [jsxRuntime.jsxs("header", { className: "flex items-center justify-between gap-3 border-b border-ews-gray-200 px-4 py-3", children: [jsxRuntime.jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [jsxRuntime.jsx("span", { className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-ews-primary text-sm font-bold text-white", "aria-hidden": "true", children: texts.brandName.charAt(0) }), jsxRuntime.jsx("span", { className: "truncate font-semibold text-ews-gray-900", children: texts.brandName }), jsxRuntime.jsx("span", { className: "rounded-full bg-ews-gray-100 px-2 py-0.5 text-xs font-medium text-ews-gray-600", children: "AI" }), jsxRuntime.jsxs("span", { className: "flex items-center gap-1 text-xs text-ews-gray-500", children: [jsxRuntime.jsx("span", { className: "h-2 w-2 rounded-full bg-ews-success", "aria-hidden": "true" }), texts.online] })] }), jsxRuntime.jsxs("div", { className: "flex shrink-0 items-center gap-1", children: [compact && onHistoryOpenChange && (jsxRuntime.jsx("button", { type: "button", onClick: () => onHistoryOpenChange(!historyOpen), "aria-label": historyLabel, "aria-pressed": historyOpen, title: historyLabel, className: cn("rounded-full p-2 hover:bg-ews-gray-100 hover:text-ews-primary focus:outline-none focus:ring-2 focus:ring-ews-primary", historyOpen ? "bg-ews-gray-100 text-ews-primary" : "text-ews-gray-500"), "data-testid": "assistant-history-toggle", children: jsxRuntime.jsx(lucideReact.History, { className: "h-4 w-4", "aria-hidden": "true" }) })), !withList && !empty && (jsxRuntime.jsx("button", { type: "button", onClick: startNewConversation, "aria-label": texts.newConversation, title: texts.newConversation, className: "rounded-full p-2 text-ews-gray-500 hover:bg-ews-gray-100 hover:text-ews-primary focus:outline-none focus:ring-2 focus:ring-ews-primary", "data-testid": "assistant-new-conversation", children: jsxRuntime.jsx(lucideReact.SquarePen, { className: "h-4 w-4", "aria-hidden": "true" }) })), headerAction] })] }), jsxRuntime.jsx("div", { className: "flex flex-1 flex-col overflow-y-auto", children: empty ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsxs("section", { className: cn("relative overflow-hidden bg-ews-primary-light text-center",
|
|
4140
|
+
// Extra bottom room: the white sheet of cards overlaps the hero (mockup tools).
|
|
4141
|
+
compact ? "px-4 pb-20 pt-10" : "px-6 pb-24 pt-14"), children: [heroBackground && jsxRuntime.jsx("div", { className: "absolute inset-0", "aria-hidden": "true", children: heroBackground }), jsxRuntime.jsxs("div", { className: "relative", children: [heroLogo && (jsxRuntime.jsx("div", { className: "mb-4 flex justify-center", "data-testid": "assistant-hero-logo", children: heroLogo })), jsxRuntime.jsx("h2", { className: cn("font-bold text-ews-gray-900", compact ? "text-xl" : "text-2xl sm:text-3xl"), children: texts.heroTitle }), jsxRuntime.jsx("p", { className: cn("mx-auto mt-4 max-w-xl text-ews-gray-700", compact && "text-sm"), children: texts.heroSubtitle })] })] }), jsxRuntime.jsx("div", {
|
|
4142
|
+
// A white sheet rounded on top, slid over the bottom of the hero, holding the cards.
|
|
4143
|
+
className: cn("relative -mt-6 grid flex-1 content-start gap-3 rounded-t-3xl bg-white px-4 py-5", compact ? "grid-cols-2" : "grid-cols-2 lg:grid-cols-4"), "data-testid": "assistant-suggestions", children: suggestions.map((suggestion) => (jsxRuntime.jsx(AssistantSuggestionCard, { suggestion: suggestion, onSelect: (selected) => void send(selected.prompt), exampleLabel: texts.exampleLabel, disabled: busy }, suggestion.id))) })] })) : (jsxRuntime.jsxs("div", { className: "flex-1 px-4 py-5", children: [jsxRuntime.jsx(AssistantMessages, { messages: messages, activity: activity, onRetry: retry, texts: texts, linkOptions: onNavigate
|
|
3783
4144
|
? {
|
|
3784
4145
|
onLinkClick: (href, event) => {
|
|
3785
4146
|
if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey)
|
|
@@ -3788,13 +4149,15 @@ const AssistantChat = ({ transport, language, texts, suggestions, conversations
|
|
|
3788
4149
|
event.preventDefault();
|
|
3789
4150
|
},
|
|
3790
4151
|
}
|
|
3791
|
-
: undefined }), pendingConfirmation && onConfirm && onCancelConfirmation && (jsxRuntime.jsx(AssistantConfirmationCard, { confirmation: pendingConfirmation, onConfirm: onConfirm, onCancel: onCancelConfirmation, texts: texts, className: "mt-4" }))] })) }), jsxRuntime.jsxs("div", { className: "border-t border-ews-gray-200 px-4 pb-3 pt-3", children: [jsxRuntime.jsx(AssistantComposer, { language: language, onSend: (content) => void send(content), disabled: busy, voice: voice, texts: {
|
|
4152
|
+
: undefined }), pendingConfirmation && onConfirm && onCancelConfirmation && (jsxRuntime.jsx(AssistantConfirmationCard, { confirmation: pendingConfirmation, onConfirm: onConfirm, onCancel: onCancelConfirmation, texts: texts, className: "mt-4" }))] })) }), jsxRuntime.jsxs("div", { className: "border-t border-ews-gray-200 px-4 pb-3 pt-3", children: [jsxRuntime.jsx(AssistantComposer, { language: language, onSend: (content) => void send(content), disabled: busy, voice: voice, onVoice: () => setVoiceOpen(true), draft: composerDraft, texts: {
|
|
4153
|
+
dictate: texts.dictate,
|
|
4154
|
+
dictation: texts.dictation,
|
|
3792
4155
|
placeholder: texts.composerPlaceholder,
|
|
3793
4156
|
hint: texts.composerHint,
|
|
3794
4157
|
send: texts.send,
|
|
3795
4158
|
holdToTalk: texts.holdToTalk,
|
|
3796
4159
|
listening: texts.listening,
|
|
3797
|
-
} }), disclaimer && jsxRuntime.jsx("div", { className: "mt-2 px-2 text-xs text-ews-gray-500", children: disclaimer })] })] })] }));
|
|
4160
|
+
} }), disclaimer && jsxRuntime.jsx("div", { className: "mt-2 px-2 text-xs text-ews-gray-500", children: disclaimer })] }), voiceOpen && (jsxRuntime.jsx(AssistantVoiceMode, { language: language, messages: messages, busy: busy, onSend: (content) => void send(content), onClose: () => setVoiceOpen(false), texts: texts.voiceMode }))] })] }));
|
|
3798
4161
|
};
|
|
3799
4162
|
|
|
3800
4163
|
/**
|
|
@@ -3889,7 +4252,7 @@ const DIRECTION = {
|
|
|
3889
4252
|
* goes to the composer on open and back to the bar on close. In Arabic the
|
|
3890
4253
|
* panel slides from the left (`end-0` follows `dir`).
|
|
3891
4254
|
*/
|
|
3892
|
-
const AssistantLauncher = ({ launcherTexts, open, defaultOpen = false, onOpenChange, expanded, defaultExpanded = false, onExpandedChange, bottomOffset = 16, hidden = false, className, panelClassName, headerAction, voice = true, conversations: hostConversations, activeConversationId: hostActiveId, initialMessages: hostInitialMessages, onConversationCreated, onSelectConversation, onNewConversation, ...chatProps }) => {
|
|
4255
|
+
const AssistantLauncher = ({ launcherTexts, open, defaultOpen = false, onOpenChange, expanded, defaultExpanded = false, onExpandedChange, bottomOffset = 16, hidden = false, className, panelClassName, embed, onEmbedClose, headerAction, voice = true, conversations: hostConversations, activeConversationId: hostActiveId, initialMessages: hostInitialMessages, onConversationCreated, onSelectConversation, onNewConversation, ...chatProps }) => {
|
|
3893
4256
|
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen);
|
|
3894
4257
|
const isOpen = open ?? uncontrolledOpen;
|
|
3895
4258
|
const [uncontrolledExpanded, setUncontrolledExpanded] = React.useState(defaultExpanded);
|
|
@@ -3931,11 +4294,18 @@ const AssistantLauncher = ({ launcherTexts, open, defaultOpen = false, onOpenCha
|
|
|
3931
4294
|
setDraft("");
|
|
3932
4295
|
setOpen(true);
|
|
3933
4296
|
}, [setOpen]);
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
4297
|
+
// One click on the bar mic (or the voice button) opens the panel straight in
|
|
4298
|
+
// the voice conversation, listening at once.
|
|
4299
|
+
const [voiceRequest, setVoiceRequest] = React.useState(null);
|
|
4300
|
+
const openVoice = React.useCallback(() => {
|
|
4301
|
+
setVoiceRequest({ id: generateId("v") });
|
|
4302
|
+
setOpen(true);
|
|
4303
|
+
}, [setOpen]);
|
|
4304
|
+
const speechSupported = useSpeechSupported();
|
|
4305
|
+
// The waveform button records a dictation in the bar itself (recording bar).
|
|
4306
|
+
const [dictating, setDictating] = React.useState(false);
|
|
4307
|
+
const [composerDraft, setComposerDraft] = React.useState(null);
|
|
4308
|
+
const dictateLabel = chatProps.texts.dictate ?? launcherTexts.voice ?? "Dicter un message";
|
|
3939
4309
|
const handleConversationCreated = React.useCallback((conversation) => {
|
|
3940
4310
|
if (!hostManaged) {
|
|
3941
4311
|
setStoredConversations(saveAssistantConversation(conversation));
|
|
@@ -3995,31 +4365,38 @@ const AssistantLauncher = ({ launcherTexts, open, defaultOpen = false, onOpenCha
|
|
|
3995
4365
|
if (hidden)
|
|
3996
4366
|
return null;
|
|
3997
4367
|
const dir = DIRECTION[chatProps.language];
|
|
3998
|
-
const showMic = voice &&
|
|
4368
|
+
const showMic = voice && speechSupported;
|
|
3999
4369
|
const expandLabel = launcherTexts.expand ?? launcherTexts.open;
|
|
4000
4370
|
const collapseLabel = launcherTexts.collapse ?? launcherTexts.close;
|
|
4001
4371
|
const texts = launcherTexts.history ? { ...chatProps.texts, history: launcherTexts.history } : chatProps.texts;
|
|
4002
|
-
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [!isOpen && (jsxRuntime.jsx("div", { dir: dir, className: "pointer-events-none fixed inset-x-0 z-40 flex justify-center px-4", style: { bottom: bottomOffset }, "data-testid": "assistant-launcher-bar", children: jsxRuntime.jsxs("div", { role: "group", "aria-label": launcherTexts.open, className: cn("pointer-events-auto flex w-full max-w-2xl items-center gap-2 rounded-2xl border border-ews-gray-100 bg-white py-2 pe-2 ps-3", "shadow-[0_8px_30px_rgba(15,23,42,0.14)]", className), children: [jsxRuntime.jsx("button", { ref: barRef, type: "button", onClick: () => setOpen(true), "aria-label": launcherTexts.open, "aria-haspopup": "dialog", "aria-expanded": false, "aria-controls": panelId, className: "flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-ews-primary to-ews-primary-hover text-base font-bold text-white shadow-sm focus:outline-none focus:ring-2 focus:ring-ews-primary/40", "data-testid": "assistant-launcher", children: chatProps.texts.brandName.charAt(0) }), jsxRuntime.jsx(
|
|
4003
|
-
|
|
4372
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [!isOpen && (jsxRuntime.jsx("div", { dir: dir, className: "pointer-events-none fixed inset-x-0 z-40 flex justify-center px-4", style: { bottom: bottomOffset }, "data-testid": "assistant-launcher-bar", children: jsxRuntime.jsxs("div", { role: "group", "aria-label": launcherTexts.open, className: cn("pointer-events-auto flex w-full max-w-2xl items-center gap-2 rounded-2xl border border-ews-gray-100 bg-white py-2 pe-2 ps-3", "shadow-[0_8px_30px_rgba(15,23,42,0.14)]", className), children: [jsxRuntime.jsx("button", { ref: barRef, type: "button", onClick: () => setOpen(true), "aria-label": launcherTexts.open, "aria-haspopup": "dialog", "aria-expanded": false, "aria-controls": panelId, className: "flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-ews-primary to-ews-primary-hover text-base font-bold text-white shadow-sm focus:outline-none focus:ring-2 focus:ring-ews-primary/40", "data-testid": "assistant-launcher", children: chatProps.texts.brandName.charAt(0) }), dictating ? (jsxRuntime.jsx(AssistantDictationBar, { language: chatProps.language, texts: chatProps.texts.dictation, className: "min-w-0 flex-1", onCancel: () => setDictating(false), onDone: (text) => {
|
|
4373
|
+
setDictating(false);
|
|
4374
|
+
if (text)
|
|
4375
|
+
setComposerDraft({ id: generateId("d"), content: text });
|
|
4004
4376
|
setOpen(true);
|
|
4005
|
-
},
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4377
|
+
}, onSend: (text) => {
|
|
4378
|
+
setDictating(false);
|
|
4379
|
+
submitFromBar(text);
|
|
4380
|
+
} })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("input", { type: "text", readOnly: true, value: draft, onMouseDown: (event) => {
|
|
4381
|
+
event.preventDefault();
|
|
4382
|
+
setOpen(true);
|
|
4383
|
+
}, onFocus: () => setOpen(true), onKeyDown: (event) => {
|
|
4384
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
4385
|
+
event.preventDefault();
|
|
4386
|
+
setOpen(true);
|
|
4387
|
+
}
|
|
4388
|
+
}, placeholder: launcherTexts.open, "aria-label": launcherTexts.open, "aria-haspopup": "dialog", "aria-controls": panelId, className: "min-w-0 flex-1 cursor-text bg-transparent py-2 text-lg text-ews-gray-800 placeholder:text-ews-gray-400 focus:outline-none", "data-testid": "assistant-launcher-input" }), showMic && (jsxRuntime.jsx(ActionTooltip, { label: chatProps.texts.holdToTalk, children: (describedBy) => (jsxRuntime.jsx("button", { type: "button", "aria-label": chatProps.texts.holdToTalk, "aria-describedby": describedBy, onClick: openVoice, className: "flex h-10 w-10 shrink-0 items-center justify-center rounded-full text-ews-gray-600 transition-colors hover:bg-ews-gray-100 hover:text-ews-primary focus:outline-none focus:ring-2 focus:ring-ews-primary", "data-testid": "assistant-launcher-mic", children: jsxRuntime.jsx(lucideReact.Mic, { className: "h-5 w-5", "aria-hidden": "true" }) })) })), jsxRuntime.jsx(ActionTooltip, { label: dictateLabel, children: (describedBy) => (jsxRuntime.jsx("button", { type: "button", onClick: speechSupported ? () => setDictating(true) : () => setOpen(true), "aria-label": dictateLabel, "aria-describedby": describedBy, className: "flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-ews-gray-100 text-ews-gray-700 transition-colors hover:bg-ews-gray-200 focus:outline-none focus:ring-2 focus:ring-ews-primary", "data-testid": "assistant-launcher-voice", children: jsxRuntime.jsx(lucideReact.AudioLines, { className: "h-5 w-5", "aria-hidden": "true" }) })) }), jsxRuntime.jsx("button", { type: "button", onClick: () => submitFromBar(draft), "aria-label": chatProps.texts.send, title: chatProps.texts.send, className: "flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-ews-primary text-white shadow-sm transition-colors hover:bg-ews-primary-hover focus:outline-none focus:ring-2 focus:ring-ews-primary focus:ring-offset-1", "data-testid": "assistant-launcher-send", children: jsxRuntime.jsx(lucideReact.ArrowUp, { className: "h-5 w-5", "aria-hidden": "true" }) })] }))] }) })), jsxRuntime.jsxs("div", { dir: dir, className: cn("fixed inset-0 z-[90]", !isOpen && "hidden"), "aria-hidden": !isOpen, "data-testid": "assistant-launcher-panel", children: [jsxRuntime.jsx("div", {
|
|
4389
|
+
// Mobile: a dimmed page under a full-screen panel. From tablets up the panel floats over the
|
|
4390
|
+
// page, which stays visible (no dimming); a click outside still closes it.
|
|
4391
|
+
className: "absolute inset-0 bg-black/40 sm:bg-transparent", onClick: () => setOpen(false), "data-testid": "assistant-launcher-overlay" }), jsxRuntime.jsxs("section", { ref: panelRef, id: panelId, role: "dialog", "aria-modal": "true", "aria-label": launcherTexts.open, "data-expanded": isExpanded, className: cn("absolute inset-y-0 end-0 flex w-full flex-col bg-white shadow-2xl",
|
|
4392
|
+
// From tablets up: a floating card (MED-1801, like the Notiwa widget), clear of the top,
|
|
4393
|
+
// the bottom and the side of the screen, rounded, with a soft border and shadow.
|
|
4394
|
+
"sm:inset-y-auto sm:bottom-6 sm:end-6 sm:overflow-hidden sm:rounded-3xl sm:border sm:border-ews-gray-100", "sm:shadow-[0_24px_60px_rgba(15,23,42,0.22)]",
|
|
4395
|
+
// Only fixed widths and heights, so expanding animates from one size to the other (a
|
|
4396
|
+
// max-width switching to none jumped to full width first).
|
|
4397
|
+
"sm:max-w-[calc(100vw-3rem)] sm:transition-[width,height] sm:duration-300 sm:ease-out", isExpanded ? "sm:h-[calc(100vh-3rem)]" : "sm:h-[calc(100vh-6rem)]",
|
|
4021
4398
|
// Expanded: a dashboard reaching the middle of the screen (never full width), tablets and up.
|
|
4022
|
-
isExpanded ? "sm:
|
|
4399
|
+
isExpanded ? "sm:w-[32rem] md:w-[max(50vw,40rem)]" : "sm:w-[32rem]", panelClassName), children: [embed && (jsxRuntime.jsxs("div", { className: "absolute inset-0 z-20 flex flex-col bg-white", "data-testid": "assistant-launcher-embed", children: [jsxRuntime.jsxs("div", { className: "flex items-center gap-2 border-b border-ews-gray-100 px-3 py-2", children: [jsxRuntime.jsx("button", { type: "button", onClick: onEmbedClose, "aria-label": launcherTexts.backToConversation ?? launcherTexts.close, title: launcherTexts.backToConversation ?? launcherTexts.close, className: "rounded-full p-2 text-ews-gray-500 hover:bg-ews-gray-100 hover:text-ews-gray-900 focus:outline-none focus:ring-2 focus:ring-ews-primary", "data-testid": "assistant-launcher-embed-back", children: jsxRuntime.jsx(lucideReact.ArrowLeft, { className: "h-5 w-5 rtl:rotate-180", "aria-hidden": "true" }) }), jsxRuntime.jsx("h2", { className: "min-w-0 flex-1 truncate text-base font-semibold text-ews-gray-900", children: embed.title }), jsxRuntime.jsx("button", { type: "button", onClick: () => setOpen(false), "aria-label": launcherTexts.close, title: launcherTexts.close, className: "rounded-full p-2 text-ews-gray-500 hover:bg-ews-gray-100 hover:text-ews-gray-900 focus:outline-none focus:ring-2 focus:ring-ews-primary", children: jsxRuntime.jsx(lucideReact.X, { className: "h-5 w-5", "aria-hidden": "true" }) })] }), jsxRuntime.jsx("iframe", { title: embed.title, src: embed.url, className: "block w-full flex-1 border-0" })] })), jsxRuntime.jsx(AssistantChat, { ...chatProps, texts: texts, voice: voice, layout: isExpanded ? "wide" : "panel", queuedPrompt: queuedPrompt, voiceRequest: voiceRequest, composerDraft: composerDraft, conversations: hostManaged ? hostConversations : storedConversations, activeConversationId: hostManaged ? hostActiveId : managedActiveId, initialMessages: hostManaged ? hostInitialMessages : managedMessages, onConversationCreated: handleConversationCreated, onSelectConversation: handleSelectConversation, onNewConversation: handleNewConversation, historyOpen: historyOpen, onHistoryOpenChange: setHistoryOpen, className: "flex-1", headerAction: jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [headerAction, jsxRuntime.jsx("button", { type: "button", onClick: () => setExpanded(!isExpanded), "aria-label": isExpanded ? collapseLabel : expandLabel, "aria-pressed": isExpanded, title: isExpanded ? collapseLabel : expandLabel, className: "hidden rounded-full p-2 text-ews-gray-500 hover:bg-ews-gray-100 hover:text-ews-gray-900 focus:outline-none focus:ring-2 focus:ring-ews-primary md:inline-flex", "data-testid": "assistant-launcher-expand", children: isExpanded ? (jsxRuntime.jsx(lucideReact.Minimize2, { className: "h-5 w-5", "aria-hidden": "true" })) : (jsxRuntime.jsx(lucideReact.Maximize2, { className: "h-5 w-5", "aria-hidden": "true" })) }), jsxRuntime.jsx("button", { type: "button", onClick: () => setOpen(false), "aria-label": launcherTexts.close, title: launcherTexts.close, className: "rounded-full p-2 text-ews-gray-500 hover:bg-ews-gray-100 hover:text-ews-gray-900 focus:outline-none focus:ring-2 focus:ring-ews-primary", "data-testid": "assistant-launcher-close", children: jsxRuntime.jsx(lucideReact.X, { className: "h-5 w-5", "aria-hidden": "true" }) })] }) })] })] })] }));
|
|
4023
4400
|
};
|
|
4024
4401
|
|
|
4025
4402
|
/** Turns one SSE text stream into assistant events; frames may arrive split across chunks. */
|
|
@@ -4388,9 +4765,11 @@ exports.AssistantChat = AssistantChat;
|
|
|
4388
4765
|
exports.AssistantComposer = AssistantComposer;
|
|
4389
4766
|
exports.AssistantConfirmationCard = AssistantConfirmationCard;
|
|
4390
4767
|
exports.AssistantConversationList = AssistantConversationList;
|
|
4768
|
+
exports.AssistantDictationBar = AssistantDictationBar;
|
|
4391
4769
|
exports.AssistantLauncher = AssistantLauncher;
|
|
4392
4770
|
exports.AssistantMessages = AssistantMessages;
|
|
4393
4771
|
exports.AssistantSuggestionCard = AssistantSuggestionCard;
|
|
4772
|
+
exports.AssistantVoiceMode = AssistantVoiceMode;
|
|
4394
4773
|
exports.BLOOD_TYPES = BLOOD_TYPES;
|
|
4395
4774
|
exports.Button = Button;
|
|
4396
4775
|
exports.COUNTRY_CODES = COUNTRY_CODES;
|
|
@@ -4445,12 +4824,14 @@ exports.parseAssistantEventStream = parseAssistantEventStream;
|
|
|
4445
4824
|
exports.parseDateSafe = parseDateSafe;
|
|
4446
4825
|
exports.renderAssistantMarkdown = renderAssistantMarkdown;
|
|
4447
4826
|
exports.saveAssistantConversation = saveAssistantConversation;
|
|
4827
|
+
exports.toSpokenText = toSpokenText;
|
|
4448
4828
|
exports.useDebounce = useDebounce;
|
|
4449
4829
|
exports.useDebouncedCallback = useDebouncedCallback;
|
|
4450
4830
|
exports.useIdleLock = useIdleLock;
|
|
4451
4831
|
exports.useMediaQuery = useMediaQuery;
|
|
4452
4832
|
exports.useSelectField = useSelectField;
|
|
4453
4833
|
exports.useSpeechRecognition = useSpeechRecognition;
|
|
4834
|
+
exports.useSpeechSupported = useSpeechSupported;
|
|
4454
4835
|
exports.useTheme = useTheme;
|
|
4455
4836
|
Object.keys(lucideReact).forEach(function (k) {
|
|
4456
4837
|
if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
|