@ews-admin/global-design-system 1.36.0 → 1.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/components/AssistantChat/ActionTooltip.d.ts +12 -0
  2. package/dist/components/AssistantChat/ActionTooltip.d.ts.map +1 -0
  3. package/dist/components/AssistantChat/AssistantChat.d.ts.map +1 -1
  4. package/dist/components/AssistantChat/AssistantChat.types.d.ts +35 -1
  5. package/dist/components/AssistantChat/AssistantChat.types.d.ts.map +1 -1
  6. package/dist/components/AssistantChat/AssistantComposer.d.ts +16 -3
  7. package/dist/components/AssistantChat/AssistantComposer.d.ts.map +1 -1
  8. package/dist/components/AssistantChat/AssistantDictationBar.d.ts +28 -0
  9. package/dist/components/AssistantChat/AssistantDictationBar.d.ts.map +1 -0
  10. package/dist/components/AssistantChat/AssistantSuggestionCard.d.ts +1 -1
  11. package/dist/components/AssistantChat/AssistantSuggestionCard.d.ts.map +1 -1
  12. package/dist/components/AssistantChat/AssistantVoiceMode.d.ts +37 -0
  13. package/dist/components/AssistantChat/AssistantVoiceMode.d.ts.map +1 -0
  14. package/dist/components/AssistantChat/assistantMarkdown.d.ts.map +1 -1
  15. package/dist/components/AssistantChat/index.d.ts +4 -0
  16. package/dist/components/AssistantChat/index.d.ts.map +1 -1
  17. package/dist/components/AssistantLauncher/AssistantLauncher.d.ts.map +1 -1
  18. package/dist/hooks/index.d.ts +1 -1
  19. package/dist/hooks/index.d.ts.map +1 -1
  20. package/dist/hooks/useSpeechRecognition.d.ts +2 -0
  21. package/dist/hooks/useSpeechRecognition.d.ts.map +1 -1
  22. package/dist/index.css +1 -1
  23. package/dist/index.d.ts +116 -7
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.esm.css +1 -1
  26. package/dist/index.esm.js +431 -57
  27. package/dist/index.esm.js.map +1 -1
  28. package/dist/index.js +433 -55
  29. package/dist/index.js.map +1 -1
  30. package/package.json +1 -1
  31. package/src/components/AssistantChat/ActionTooltip.tsx +31 -0
  32. package/src/components/AssistantChat/AssistantChat.tsx +36 -4
  33. package/src/components/AssistantChat/AssistantChat.types.ts +25 -1
  34. package/src/components/AssistantChat/AssistantComposer.tsx +106 -68
  35. package/src/components/AssistantChat/AssistantDictationBar.tsx +268 -0
  36. package/src/components/AssistantChat/AssistantSuggestionCard.tsx +7 -6
  37. package/src/components/AssistantChat/AssistantVoiceMode.tsx +304 -0
  38. package/src/components/AssistantChat/assistantMarkdown.tsx +4 -1
  39. package/src/components/AssistantChat/index.ts +4 -0
  40. package/src/components/AssistantLauncher/AssistantLauncher.tsx +106 -77
  41. package/src/hooks/index.ts +1 -1
  42. package/src/hooks/useSpeechRecognition.ts +9 -0
  43. package/src/index.ts +8 -0
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, push-to-talk mic (hold to
3482
- * record, release to send) and a send button. Enter sends, Shift+Enter breaks.
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 speech = useSpeechRecognition({
3495
- language,
3496
- onInterim: (text) => setValue(text),
3497
- onResult: (text) => submit(text),
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 showMic = voice && speech.supported;
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: [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: speech.listening ? texts.listening : 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" }), showMic && (jsxRuntime.jsx("button", { type: "button", "aria-label": texts.holdToTalk, "aria-pressed": speech.listening, title: texts.holdToTalk, disabled: disabled, onMouseDown: speech.start, onMouseUp: speech.stop, onMouseLeave: speech.listening ? speech.stop : undefined, onTouchStart: speech.start, onTouchEnd: speech.stop, onKeyDown: (event) => {
3510
- if (event.key === " " || event.key === "Enter") {
3511
- event.preventDefault();
3512
- if (speech.listening)
3513
- speech.stop();
3514
- else
3515
- speech.start();
3516
- }
3517
- }, className: cn("rounded-full p-2 transition-colors focus:outline-none focus:ring-2 focus:ring-ews-primary", speech.listening
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
- nodes.push(jsxRuntime.jsx("strong", { children: token.slice(2, -2) }, `${keyPrefix}-b${index}`));
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 (mockup med1): title, description, sent as a prompt on click. */
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("flex h-full w-full flex-col items-start gap-2 rounded-xl border border-ews-gray-200 bg-white p-4 text-start shadow-sm transition-colors", "hover:border-ews-primary hover:bg-ews-gray-50 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: "text-ews-gray-400", "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 text-ews-gray-900", children: suggestion.title }), suggestion.description && (jsxRuntime.jsx("span", { className: "text-sm text-ews-gray-600", children: suggestion.description })), isExample && (jsxRuntime.jsx("span", { className: "mt-auto text-xs uppercase tracking-wide text-ews-gray-400", children: exampleLabel }))] }));
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, 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", compact ? "px-4 py-6" : "px-6 py-10"), children: [heroBackground && jsxRuntime.jsx("div", { className: "absolute inset-0", "aria-hidden": "true", children: heroBackground }), jsxRuntime.jsxs("div", { className: "relative", children: [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-2 max-w-xl text-ews-gray-700", compact && "text-sm"), children: texts.heroSubtitle })] })] }), jsxRuntime.jsx("div", { className: cn("grid gap-3 px-4 py-5", compact ? "grid-cols-1" : "sm: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
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-12 pt-8" : "px-6 pb-16 pt-12"), children: [heroBackground && jsxRuntime.jsx("div", { className: "absolute inset-0", "aria-hidden": "true", children: heroBackground }), jsxRuntime.jsxs("div", { className: "relative", children: [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-2 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
  /**
@@ -3931,11 +4294,18 @@ const AssistantLauncher = ({ launcherTexts, open, defaultOpen = false, onOpenCha
3931
4294
  setDraft("");
3932
4295
  setOpen(true);
3933
4296
  }, [setOpen]);
3934
- const speech = useSpeechRecognition({
3935
- language: chatProps.language,
3936
- onInterim: (text) => setDraft(text),
3937
- onResult: (text) => submitFromBar(text),
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,35 @@ 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 && speech.supported;
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("input", { type: "text", readOnly: true, value: draft, onMouseDown: (event) => {
4003
- event.preventDefault();
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
- }, onFocus: () => setOpen(true), onKeyDown: (event) => {
4006
- if (event.key === "Enter" || event.key === " ") {
4007
- event.preventDefault();
4008
- setOpen(true);
4009
- }
4010
- }, placeholder: speech.listening ? chatProps.texts.listening : 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("button", { type: "button", "aria-label": chatProps.texts.holdToTalk, "aria-pressed": speech.listening, title: chatProps.texts.holdToTalk, onMouseDown: speech.start, onMouseUp: speech.stop, onMouseLeave: speech.listening ? speech.stop : undefined, onTouchStart: speech.start, onTouchEnd: speech.stop, onKeyDown: (event) => {
4011
- if (event.key === " " || event.key === "Enter") {
4012
- event.preventDefault();
4013
- if (speech.listening)
4014
- speech.stop();
4015
- else
4016
- speech.start();
4017
- }
4018
- }, className: cn("flex h-10 w-10 shrink-0 items-center justify-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-ews-primary", speech.listening
4019
- ? "bg-ews-error text-white animate-pulse"
4020
- : "text-ews-gray-600 hover:bg-ews-gray-100 hover:text-ews-primary"), "data-testid": "assistant-launcher-mic", children: jsxRuntime.jsx(lucideReact.Mic, { className: "h-5 w-5", "aria-hidden": "true" }) })), jsxRuntime.jsx("button", { type: "button", onClick: () => setOpen(true), "aria-label": launcherTexts.voice ?? launcherTexts.open, title: launcherTexts.voice ?? launcherTexts.open, 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", { className: "absolute inset-0 bg-black/40", onClick: () => setOpen(false), "data-testid": "assistant-launcher-overlay" }), jsxRuntime.jsx("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 transition-[width,max-width] duration-200",
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.jsx("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 transition-[width,max-width,height] duration-200",
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)]", isExpanded ? "sm:h-[calc(100vh-3rem)]" : "sm:h-[calc(100vh-6rem)]",
4021
4395
  // Expanded: a dashboard reaching the middle of the screen (never full width), tablets and up.
4022
- isExpanded ? "sm:max-w-md md:w-[max(50vw,40rem)] md:max-w-none" : "sm:max-w-md", panelClassName), children: jsxRuntime.jsx(AssistantChat, { ...chatProps, texts: texts, voice: voice, layout: isExpanded ? "wide" : "panel", queuedPrompt: queuedPrompt, 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" }) })] }) }) })] })] }));
4396
+ isExpanded ? "sm:max-w-md md:w-[max(50vw,40rem)] md:max-w-none" : "sm:max-w-md", panelClassName), children: 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
4397
  };
4024
4398
 
4025
4399
  /** Turns one SSE text stream into assistant events; frames may arrive split across chunks. */
@@ -4388,9 +4762,11 @@ exports.AssistantChat = AssistantChat;
4388
4762
  exports.AssistantComposer = AssistantComposer;
4389
4763
  exports.AssistantConfirmationCard = AssistantConfirmationCard;
4390
4764
  exports.AssistantConversationList = AssistantConversationList;
4765
+ exports.AssistantDictationBar = AssistantDictationBar;
4391
4766
  exports.AssistantLauncher = AssistantLauncher;
4392
4767
  exports.AssistantMessages = AssistantMessages;
4393
4768
  exports.AssistantSuggestionCard = AssistantSuggestionCard;
4769
+ exports.AssistantVoiceMode = AssistantVoiceMode;
4394
4770
  exports.BLOOD_TYPES = BLOOD_TYPES;
4395
4771
  exports.Button = Button;
4396
4772
  exports.COUNTRY_CODES = COUNTRY_CODES;
@@ -4445,12 +4821,14 @@ exports.parseAssistantEventStream = parseAssistantEventStream;
4445
4821
  exports.parseDateSafe = parseDateSafe;
4446
4822
  exports.renderAssistantMarkdown = renderAssistantMarkdown;
4447
4823
  exports.saveAssistantConversation = saveAssistantConversation;
4824
+ exports.toSpokenText = toSpokenText;
4448
4825
  exports.useDebounce = useDebounce;
4449
4826
  exports.useDebouncedCallback = useDebouncedCallback;
4450
4827
  exports.useIdleLock = useIdleLock;
4451
4828
  exports.useMediaQuery = useMediaQuery;
4452
4829
  exports.useSelectField = useSelectField;
4453
4830
  exports.useSpeechRecognition = useSpeechRecognition;
4831
+ exports.useSpeechSupported = useSpeechSupported;
4454
4832
  exports.useTheme = useTheme;
4455
4833
  Object.keys(lucideReact).forEach(function (k) {
4456
4834
  if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {