@ews-admin/global-design-system 1.37.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.esm.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
2
  import * as React from 'react';
3
3
  import React__default, { useId, useState, useRef, useEffect, forwardRef, useCallback, createContext, useContext } from 'react';
4
- import { Stethoscope, Heart, User, ChevronDown, EyeOff, Eye, X, Search, Check, AlertCircle, CheckCircle, AlertTriangle, Pencil, Trash, Lock, Delete, Mic, SendHorizontal, ShieldCheck, SquarePen, MessageSquare, UserRound, Loader2, RotateCcw, Sparkles, ArrowRight, History, AudioLines, ArrowUp, Minimize2, Maximize2 } from 'lucide-react';
4
+ import { Stethoscope, Heart, User, ChevronDown, EyeOff, Eye, X, Search, Check, AlertCircle, CheckCircle, AlertTriangle, Pencil, Trash, Lock, Delete, ArrowUp, AudioLines, Mic, SendHorizontal, ShieldCheck, SquarePen, MessageSquare, UserRound, Loader2, RotateCcw, Sparkles, ArrowRight, MicOff, History, Minimize2, Maximize2 } from 'lucide-react';
5
5
  export * from 'lucide-react';
6
6
  import { useController, Controller } from 'react-hook-form';
7
7
 
@@ -1011,13 +1011,21 @@ function useSelectField({ name, control, options: _options, rules, defaultValue,
1011
1011
  };
1012
1012
  }
1013
1013
 
1014
- const BCP47 = { fr: "fr-FR", en: "en-US", ar: "ar-SA" };
1014
+ const BCP47$2 = { fr: "fr-FR", en: "en-US", ar: "ar-SA" };
1015
1015
  function getSpeechRecognition() {
1016
1016
  if (typeof window === "undefined")
1017
1017
  return undefined;
1018
1018
  const w = window;
1019
1019
  return w.SpeechRecognition ?? w.webkitSpeechRecognition;
1020
1020
  }
1021
+ /** True once mounted in a browser that has the speech recognition API (false on the server). */
1022
+ function useSpeechSupported() {
1023
+ const [supported, setSupported] = useState(false);
1024
+ useEffect(() => {
1025
+ setSupported(getSpeechRecognition() !== undefined);
1026
+ }, []);
1027
+ return supported;
1028
+ }
1021
1029
  /**
1022
1030
  * Push-to-talk on the browser speech API (MED-1783). `supported` is false on
1023
1031
  * the server and in browsers without the API, so the app can hide the mic.
@@ -1041,7 +1049,7 @@ function useSpeechRecognition({ language, onResult, onInterim, }) {
1041
1049
  if (!Ctor || recognitionRef.current)
1042
1050
  return;
1043
1051
  const recognition = new Ctor();
1044
- recognition.lang = BCP47[language] ?? language;
1052
+ recognition.lang = BCP47$2[language] ?? language;
1045
1053
  recognition.interimResults = true;
1046
1054
  recognition.continuous = true;
1047
1055
  transcriptRef.current = "";
@@ -3457,14 +3465,192 @@ const PinCodeInput = ({ length = 4, onComplete, onChange, masked = true, autoCom
3457
3465
  }, 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))) }));
3458
3466
  };
3459
3467
 
3468
+ /**
3469
+ * A visible tooltip for an icon button (hover and keyboard focus), so users
3470
+ * know what the mic and the dictation buttons do. The button keeps its own
3471
+ * aria-label; the tooltip is tied to it with aria-describedby.
3472
+ */
3473
+ const ActionTooltip = ({ label, children, className }) => {
3474
+ const id = useId();
3475
+ return (jsxs("span", { className: cn("group relative inline-flex", className), children: [children(id), 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 })] }));
3476
+ };
3477
+
3478
+ const DEFAULT_TEXTS$1 = {
3479
+ cancel: "Annuler la dictée",
3480
+ done: "Terminer et modifier le texte",
3481
+ send: "Envoyer",
3482
+ unavailable: "Micro indisponible : autorisez-le (page https requise).",
3483
+ };
3484
+ const BCP47$1 = { fr: "fr-FR", en: "en-US", ar: "ar-SA" };
3485
+ const BARS = 28;
3486
+ const BLOCKING_ERRORS$1 = new Set(["not-allowed", "service-not-allowed", "audio-capture"]);
3487
+ const formatElapsed = (seconds) => `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, "0")}`;
3488
+ /**
3489
+ * The recording bar of the mockup (audio3): cancel, a live waveform, the
3490
+ * elapsed time, done and send. Recording starts as soon as it shows. The
3491
+ * waveform follows the mic level when the browser gives it, and moves while
3492
+ * words come in otherwise.
3493
+ */
3494
+ const AssistantDictationBar = ({ language, onCancel, onDone, onSend, texts, className, }) => {
3495
+ const t = { ...DEFAULT_TEXTS$1, ...texts };
3496
+ const [levels, setLevels] = useState(() => Array(BARS).fill(0.15));
3497
+ const [elapsed, setElapsed] = useState(0);
3498
+ const [unavailable, setUnavailable] = useState(() => typeof window !== "undefined" && window.isSecureContext === false);
3499
+ const transcriptRef = useRef("");
3500
+ const recognitionRef = useRef(null);
3501
+ const actionRef = useRef(null);
3502
+ const callbacks = useRef({ onCancel, onDone, onSend });
3503
+ callbacks.current = { onCancel, onDone, onSend };
3504
+ const pushLevel = useCallback((level) => {
3505
+ setLevels((current) => [...current.slice(1), Math.max(0.12, Math.min(1, level))]);
3506
+ }, []);
3507
+ const finish = useCallback(() => {
3508
+ const text = transcriptRef.current.trim();
3509
+ const action = actionRef.current;
3510
+ if (action === "send" && text)
3511
+ callbacks.current.onSend(text);
3512
+ else if (action === "done")
3513
+ callbacks.current.onDone(text);
3514
+ else
3515
+ callbacks.current.onCancel();
3516
+ }, []);
3517
+ // Speech recognition, from the first render.
3518
+ useEffect(() => {
3519
+ if (unavailable)
3520
+ return;
3521
+ const Ctor = getSpeechRecognition();
3522
+ if (!Ctor) {
3523
+ setUnavailable(true);
3524
+ return;
3525
+ }
3526
+ const recognition = new Ctor();
3527
+ recognition.lang = BCP47$1[language] ?? language;
3528
+ recognition.interimResults = true;
3529
+ recognition.continuous = true;
3530
+ recognition.onresult = (event) => {
3531
+ let text = "";
3532
+ for (let i = 0; i < event.results.length; i++)
3533
+ text += event.results[i][0]?.transcript ?? "";
3534
+ if (text.length !== transcriptRef.current.length)
3535
+ pushLevel(0.4 + Math.random() * 0.6);
3536
+ transcriptRef.current = text;
3537
+ };
3538
+ recognition.onerror = (event) => {
3539
+ if (event.error && BLOCKING_ERRORS$1.has(event.error)) {
3540
+ recognition.onend = null;
3541
+ recognitionRef.current = null;
3542
+ setUnavailable(true);
3543
+ }
3544
+ };
3545
+ recognition.onend = () => {
3546
+ if (recognitionRef.current !== recognition)
3547
+ return;
3548
+ recognitionRef.current = null;
3549
+ if (actionRef.current)
3550
+ finish();
3551
+ else {
3552
+ // The browser stopped after a silence: keep recording.
3553
+ try {
3554
+ recognition.start();
3555
+ recognitionRef.current = recognition;
3556
+ }
3557
+ catch {
3558
+ /* ignore: the user can still press done */
3559
+ }
3560
+ }
3561
+ };
3562
+ recognitionRef.current = recognition;
3563
+ recognition.start();
3564
+ return () => {
3565
+ recognition.onend = null;
3566
+ recognitionRef.current = null;
3567
+ recognition.abort();
3568
+ };
3569
+ }, [finish, language, pushLevel, unavailable]);
3570
+ // Live mic level for the waveform, where the browser allows it.
3571
+ useEffect(() => {
3572
+ if (unavailable || typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia)
3573
+ return;
3574
+ const w = window;
3575
+ const AudioCtx = w.AudioContext ?? w.webkitAudioContext;
3576
+ if (!AudioCtx)
3577
+ return;
3578
+ let stream = null;
3579
+ let context = null;
3580
+ let frame = 0;
3581
+ let cancelled = false;
3582
+ navigator.mediaDevices
3583
+ .getUserMedia({ audio: true })
3584
+ .then((media) => {
3585
+ if (cancelled) {
3586
+ media.getTracks().forEach((track) => track.stop());
3587
+ return;
3588
+ }
3589
+ stream = media;
3590
+ context = new AudioCtx();
3591
+ const analyser = context.createAnalyser();
3592
+ analyser.fftSize = 256;
3593
+ context.createMediaStreamSource(media).connect(analyser);
3594
+ const data = new Uint8Array(analyser.frequencyBinCount);
3595
+ let last = 0;
3596
+ const tick = (now) => {
3597
+ if (now - last > 80) {
3598
+ analyser.getByteTimeDomainData(data);
3599
+ let peak = 0;
3600
+ for (const value of data)
3601
+ peak = Math.max(peak, Math.abs(value - 128));
3602
+ pushLevel(peak / 64);
3603
+ last = now;
3604
+ }
3605
+ frame = requestAnimationFrame(tick);
3606
+ };
3607
+ frame = requestAnimationFrame(tick);
3608
+ return undefined;
3609
+ })
3610
+ .catch(() => undefined);
3611
+ return () => {
3612
+ cancelled = true;
3613
+ cancelAnimationFrame(frame);
3614
+ stream?.getTracks().forEach((track) => track.stop());
3615
+ void context?.close();
3616
+ };
3617
+ }, [pushLevel, unavailable]);
3618
+ // Elapsed time.
3619
+ useEffect(() => {
3620
+ if (unavailable)
3621
+ return;
3622
+ const timer = setInterval(() => setElapsed((value) => value + 1), 1000);
3623
+ return () => clearInterval(timer);
3624
+ }, [unavailable]);
3625
+ const end = (action) => {
3626
+ actionRef.current = action;
3627
+ const recognition = recognitionRef.current;
3628
+ if (!recognition || action === "cancel") {
3629
+ if (recognition) {
3630
+ recognition.onend = null;
3631
+ recognitionRef.current = null;
3632
+ recognition.abort();
3633
+ }
3634
+ finish();
3635
+ return;
3636
+ }
3637
+ // stop() delivers the last words, then onend calls finish().
3638
+ recognition.stop();
3639
+ };
3640
+ return (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: [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: jsx(X, { className: "h-5 w-5", "aria-hidden": "true" }) }), unavailable ? (jsx("p", { className: "flex-1 truncate text-sm text-ews-error", role: "alert", "data-testid": "assistant-dictation-unavailable", children: t.unavailable })) : (jsxs(Fragment, { children: [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) => (jsx("span", { className: "w-[3px] rounded-full bg-ews-gray-700 transition-[height] duration-100", style: { height: `${Math.round(level * 100)}%` } }, index))) }), 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) })] })), 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: jsx(Check, { className: "h-5 w-5", "aria-hidden": "true" }) }), 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: jsx(ArrowUp, { className: "h-5 w-5", "aria-hidden": "true" }) })] }));
3641
+ };
3642
+
3460
3643
  const MAX_MESSAGE_LENGTH = 2000;
3461
3644
  /**
3462
- * The composer of the mockup: textarea, hint, push-to-talk mic (hold to
3463
- * record, release to send) and a send button. Enter sends, Shift+Enter breaks.
3645
+ * The composer of the mockup: textarea, hint, a dictation button (waveform:
3646
+ * a recording bar transcribes into the field), a mic (the voice conversation)
3647
+ * and send. Both voice buttons show a tooltip. Enter sends, Shift+Enter breaks.
3464
3648
  */
3465
- const AssistantComposer = ({ language, onSend, disabled = false, voice = true, texts, className, }) => {
3649
+ const AssistantComposer = ({ language, onSend, disabled = false, voice = true, onVoice, draft = null, texts, className, }) => {
3466
3650
  const [value, setValue] = useState("");
3651
+ const [dictating, setDictating] = useState(false);
3467
3652
  const textareaRef = useRef(null);
3653
+ const speechSupported = useSpeechSupported();
3468
3654
  const submit = useCallback((content) => {
3469
3655
  const trimmed = content.trim();
3470
3656
  if (!trimmed || disabled)
@@ -3472,32 +3658,34 @@ const AssistantComposer = ({ language, onSend, disabled = false, voice = true, t
3472
3658
  onSend(trimmed.slice(0, MAX_MESSAGE_LENGTH));
3473
3659
  setValue("");
3474
3660
  }, [disabled, onSend]);
3475
- const speech = useSpeechRecognition({
3476
- language,
3477
- onInterim: (text) => setValue(text),
3478
- onResult: (text) => submit(text),
3479
- });
3661
+ const handledDraftRef = useRef(null);
3662
+ useEffect(() => {
3663
+ if (!draft || draft.id === handledDraftRef.current)
3664
+ return;
3665
+ handledDraftRef.current = draft.id;
3666
+ setValue(draft.content.slice(0, MAX_MESSAGE_LENGTH));
3667
+ textareaRef.current?.focus();
3668
+ }, [draft]);
3480
3669
  const onKeyDown = (event) => {
3481
3670
  if (event.key === "Enter" && !event.shiftKey) {
3482
3671
  event.preventDefault();
3483
3672
  submit(value);
3484
3673
  }
3485
3674
  };
3486
- const showMic = voice && speech.supported;
3675
+ const showVoice = voice && speechSupported;
3676
+ const dictateLabel = texts.dictate ?? "Dicter un message";
3487
3677
  // A plain div rather than a <form>: Enter and the send button submit, and a
3488
3678
  // form followed by a sibling trips React hydration under happy-dom (the apps'
3489
3679
  // server-rendering tests) although browsers are fine.
3490
- return (jsxs("div", { className: cn("w-full", className), children: [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: [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 && (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) => {
3491
- if (event.key === " " || event.key === "Enter") {
3492
- event.preventDefault();
3493
- if (speech.listening)
3494
- speech.stop();
3495
- else
3496
- speech.start();
3497
- }
3498
- }, className: cn("rounded-full p-2 transition-colors focus:outline-none focus:ring-2 focus:ring-ews-primary", speech.listening
3499
- ? "bg-ews-error text-white animate-pulse"
3500
- : "text-ews-gray-500 hover:bg-ews-gray-100 hover:text-ews-primary", "disabled:opacity-50"), "data-testid": "assistant-mic", children: jsx(Mic, { className: "h-5 w-5", "aria-hidden": "true" }) })), 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: jsx(SendHorizontal, { className: "h-5 w-5 rtl:-scale-x-100", "aria-hidden": "true" }) })] }), jsx("p", { className: "mt-1.5 px-2 text-xs text-ews-gray-400", children: texts.hint })] }));
3680
+ return (jsxs("div", { className: cn("w-full", className), children: [dictating ? (jsx(AssistantDictationBar, { language: language, texts: texts.dictation, onCancel: () => setDictating(false), onDone: (text) => {
3681
+ setDictating(false);
3682
+ if (text)
3683
+ setValue((current) => (current ? `${current} ${text}` : text).slice(0, MAX_MESSAGE_LENGTH));
3684
+ setTimeout(() => textareaRef.current?.focus(), 0);
3685
+ }, onSend: (text) => {
3686
+ setDictating(false);
3687
+ submit(value ? `${value} ${text}` : text);
3688
+ } })) : (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: [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 && (jsx(ActionTooltip, { label: dictateLabel, children: (describedBy) => (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: jsx(AudioLines, { className: "h-5 w-5", "aria-hidden": "true" }) })) })), showVoice && onVoice && (jsx(ActionTooltip, { label: texts.holdToTalk, children: (describedBy) => (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: jsx(Mic, { className: "h-5 w-5", "aria-hidden": "true" }) })) })), 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: jsx(SendHorizontal, { className: "h-5 w-5 rtl:-scale-x-100", "aria-hidden": "true" }) })] })), jsx("p", { className: "mt-1.5 px-2 text-xs text-ews-gray-400", children: texts.hint })] }));
3501
3689
  };
3502
3690
 
3503
3691
  /**
@@ -3547,7 +3735,8 @@ function renderInline(text, keyPrefix, options) {
3547
3735
  nodes.push(text.slice(last, start));
3548
3736
  const token = match[0];
3549
3737
  if (token.startsWith("**")) {
3550
- nodes.push(jsx("strong", { children: token.slice(2, -2) }, `${keyPrefix}-b${index}`));
3738
+ // MED-1801: a bold run can hold a link, e.g. **[Dr Ba](/doctors/8)**.
3739
+ nodes.push(jsx("strong", { children: renderInline(token.slice(2, -2), `${keyPrefix}-b${index}`, options) }, `${keyPrefix}-b${index}`));
3551
3740
  }
3552
3741
  else {
3553
3742
  const close = token.indexOf("](");
@@ -3596,10 +3785,169 @@ const AssistantMessages = ({ messages, activity, onRetry, texts, linkOptions, cl
3596
3785
  }), jsx("div", { ref: endRef })] }));
3597
3786
  };
3598
3787
 
3599
- /** One of the cards under the hero (mockup med1): title, description, sent as a prompt on click. */
3788
+ /** One of the cards under the hero (mockups med1 and tools): icon, title, description, sent as a prompt on click. */
3600
3789
  const AssistantSuggestionCard = ({ suggestion, onSelect, exampleLabel, disabled = false, className, }) => {
3601
3790
  const isExample = suggestion.kind === "example";
3602
- return (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: [jsx("span", { className: "text-ews-gray-400", "aria-hidden": "true", children: suggestion.icon ?? (isExample ? jsx(Sparkles, { className: "h-4 w-4" }) : jsx(ArrowRight, { className: "h-4 w-4" })) }), jsx("span", { className: "font-semibold text-ews-gray-900", children: suggestion.title }), suggestion.description && (jsx("span", { className: "text-sm text-ews-gray-600", children: suggestion.description })), isExample && (jsx("span", { className: "mt-auto text-xs uppercase tracking-wide text-ews-gray-400", children: exampleLabel }))] }));
3791
+ return (jsxs("button", { type: "button", disabled: disabled, onClick: () => onSelect(suggestion), className: cn(
3792
+ // A soft grey card without border (mockup tools); it lifts with a shadow on hover.
3793
+ "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: [jsx("span", { className: "mb-2 text-ews-gray-600", "aria-hidden": "true", children: suggestion.icon ?? (isExample ? jsx(Sparkles, { className: "h-4 w-4" }) : jsx(ArrowRight, { className: "h-4 w-4" })) }), jsx("span", { className: "font-semibold leading-snug text-ews-gray-900", children: suggestion.title }), suggestion.description && (jsx("span", { className: "text-sm leading-snug text-ews-gray-400", children: suggestion.description })), isExample && (jsx("span", { className: "mt-auto text-xs uppercase tracking-wide text-ews-gray-400", children: exampleLabel }))] }));
3794
+ };
3795
+
3796
+ const DEFAULT_TEXTS = {
3797
+ title: "Conversation vocale avec Medecine360",
3798
+ listening: "À l'écoute…",
3799
+ thinking: "Réflexion…",
3800
+ speaking: "Réponse…",
3801
+ muted: "Micro coupé",
3802
+ mute: "Couper le micro",
3803
+ unmute: "Réactiver le micro",
3804
+ end: "Terminer",
3805
+ unavailable: "Le micro n'est pas accessible. Autorisez-le dans le navigateur ; il ne fonctionne que sur une page sécurisée (https).",
3806
+ };
3807
+ /** Errors after which listening again cannot work until the user acts. */
3808
+ const BLOCKING_ERRORS = new Set(["not-allowed", "service-not-allowed", "audio-capture"]);
3809
+ const BCP47 = { fr: "fr-FR", en: "en-US", ar: "ar-SA" };
3810
+ /** Markdown and links read aloud as plain words. */
3811
+ function toSpokenText(markdown) {
3812
+ return markdown
3813
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
3814
+ .replace(/https?:\/\/\S+/g, "")
3815
+ .replace(/\*\*/g, "")
3816
+ .replace(/^\s*[-*•]\s+/gm, "")
3817
+ .replace(/\s+/g, " ")
3818
+ .trim();
3819
+ }
3820
+ /**
3821
+ * MED-1801 (voice): the voice conversation screen of the mockup. Listening
3822
+ * starts as soon as it opens; a finished sentence is sent to the same
3823
+ * conversation; the mic pauses while the assistant answers (read aloud when
3824
+ * the browser can) and resumes after. Mute stops the mic; Terminer closes.
3825
+ */
3826
+ const AssistantVoiceMode = ({ language, messages, busy, onSend, onClose, texts, speak = true, }) => {
3827
+ const t = { ...DEFAULT_TEXTS, ...texts };
3828
+ const [muted, setMuted] = useState(false);
3829
+ const [speaking, setSpeaking] = useState(false);
3830
+ const [interim, setInterim] = useState("");
3831
+ // The mic cannot work: insecure page (http) or permission refused.
3832
+ const [unavailable, setUnavailable] = useState(() => typeof window !== "undefined" && window.isSecureContext === false);
3833
+ const recognitionRef = useRef(null);
3834
+ const restartTimerRef = useRef(null);
3835
+ const sentRef = useRef(false);
3836
+ // Only the turns of this voice session are shown.
3837
+ const [firstIndex] = useState(messages.length);
3838
+ const spokenRef = useRef(null);
3839
+ const callbacks = useRef({ onSend });
3840
+ callbacks.current = { onSend };
3841
+ const stopListening = useCallback(() => {
3842
+ if (restartTimerRef.current)
3843
+ clearTimeout(restartTimerRef.current);
3844
+ restartTimerRef.current = null;
3845
+ const recognition = recognitionRef.current;
3846
+ recognitionRef.current = null;
3847
+ if (recognition) {
3848
+ recognition.onend = null;
3849
+ recognition.abort();
3850
+ }
3851
+ }, []);
3852
+ const startListening = useCallback(() => {
3853
+ const Ctor = getSpeechRecognition();
3854
+ if (!Ctor || recognitionRef.current)
3855
+ return;
3856
+ const recognition = new Ctor();
3857
+ recognition.lang = BCP47[language] ?? language;
3858
+ recognition.interimResults = true;
3859
+ recognition.continuous = true;
3860
+ sentRef.current = false;
3861
+ recognition.onresult = (event) => {
3862
+ let text = "";
3863
+ let final = false;
3864
+ for (let i = 0; i < event.results.length; i++) {
3865
+ text += event.results[i][0]?.transcript ?? "";
3866
+ if (event.results[i].isFinal)
3867
+ final = true;
3868
+ }
3869
+ const trimmed = text.trim();
3870
+ setInterim(trimmed);
3871
+ if (final && trimmed && !sentRef.current) {
3872
+ sentRef.current = true;
3873
+ setInterim("");
3874
+ recognitionRef.current = null;
3875
+ recognition.onend = null;
3876
+ recognition.stop();
3877
+ callbacks.current.onSend(trimmed);
3878
+ }
3879
+ };
3880
+ recognition.onerror = (event) => {
3881
+ // Permission refused or no mic: stop here and say so, never retry in a loop.
3882
+ if (event.error && BLOCKING_ERRORS.has(event.error)) {
3883
+ recognitionRef.current = null;
3884
+ recognition.onend = null;
3885
+ setUnavailable(true);
3886
+ }
3887
+ /* other errors (no-speech, network): onend follows and listens again */
3888
+ };
3889
+ recognition.onend = () => {
3890
+ // The browser ends recognition after a silence: listen again, a moment later.
3891
+ if (recognitionRef.current === recognition) {
3892
+ recognitionRef.current = null;
3893
+ restartTimerRef.current = setTimeout(() => startListeningRef.current(), 250);
3894
+ }
3895
+ };
3896
+ recognitionRef.current = recognition;
3897
+ recognition.start();
3898
+ }, [language]);
3899
+ const startListeningRef = useRef(startListening);
3900
+ startListeningRef.current = startListening;
3901
+ const last = messages[messages.length - 1];
3902
+ const canSpeak = speak && typeof window !== "undefined" && "speechSynthesis" in window;
3903
+ // Read the finished answer aloud, once.
3904
+ useEffect(() => {
3905
+ if (busy || !last || last.role !== "assistant" || last.status === "streaming")
3906
+ return;
3907
+ if (messages.length <= firstIndex || spokenRef.current === last.id)
3908
+ return;
3909
+ spokenRef.current = last.id;
3910
+ const text = toSpokenText(last.content);
3911
+ if (!canSpeak || !text)
3912
+ return;
3913
+ const utterance = new SpeechSynthesisUtterance(text);
3914
+ utterance.lang = BCP47[language] ?? language;
3915
+ utterance.onend = () => setSpeaking(false);
3916
+ utterance.onerror = () => setSpeaking(false);
3917
+ setSpeaking(true);
3918
+ window.speechSynthesis.speak(utterance);
3919
+ }, [busy, canSpeak, firstIndex, language, last, messages.length]);
3920
+ // Listen whenever nobody else is talking.
3921
+ const shouldListen = !muted && !busy && !speaking && !unavailable;
3922
+ useEffect(() => {
3923
+ if (shouldListen)
3924
+ startListening();
3925
+ else
3926
+ stopListening();
3927
+ }, [shouldListen, startListening, stopListening]);
3928
+ useEffect(() => () => {
3929
+ stopListening();
3930
+ if (typeof window !== "undefined" && "speechSynthesis" in window)
3931
+ window.speechSynthesis.cancel();
3932
+ }, [stopListening]);
3933
+ const phase = unavailable
3934
+ ? "unavailable"
3935
+ : muted
3936
+ ? "muted"
3937
+ : busy
3938
+ ? "thinking"
3939
+ : speaking
3940
+ ? "speaking"
3941
+ : "listening";
3942
+ const status = {
3943
+ listening: t.listening,
3944
+ thinking: t.thinking,
3945
+ speaking: t.speaking,
3946
+ muted: t.muted,
3947
+ unavailable: t.unavailable,
3948
+ }[phase];
3949
+ const turns = messages.slice(firstIndex).filter((message) => message.content.trim().length > 0);
3950
+ return (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: [jsxs("div", { className: "flex items-center gap-2 px-5 pt-4 text-sm text-ews-gray-600", children: [jsx("span", { className: "h-2 w-2 rounded-full bg-emerald-500", "aria-hidden": "true" }), t.title] }), jsxs("div", { className: "flex flex-col items-center gap-3 pt-8", children: [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" }), 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 })] }), jsxs("div", { className: "flex-1 space-y-3 overflow-y-auto px-5 py-6", "data-testid": "assistant-voice-transcript", children: [turns.map((message) => (jsx("div", { className: cn("flex", message.role === "user" ? "justify-end" : "justify-start"), children: 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 && (jsx("div", { className: "flex justify-end", children: 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 }) }))] }), jsxs("div", { className: "flex items-center justify-center gap-3 pb-6", children: [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 ? jsx(MicOff, { className: "h-5 w-5", "aria-hidden": "true" }) : jsx(Mic, { className: "h-5 w-5", "aria-hidden": "true" }) }), 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 })] })] }));
3603
3951
  };
3604
3952
 
3605
3953
  const DIRECTION$1 = {
@@ -3621,7 +3969,7 @@ function conversationTitle(firstMessage, max = 60) {
3621
3969
  * Server-rendering safe: nothing touches `window` at render time, the
3622
3970
  * speech API is detected after mount.
3623
3971
  */
3624
- 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, }) => {
3972
+ 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, }) => {
3625
3973
  const [messages, setMessages] = useState(initialMessages ?? []);
3626
3974
  const [activity, setActivity] = useState(null);
3627
3975
  const [busy, setBusy] = useState(false);
@@ -3732,6 +4080,15 @@ const AssistantChat = ({ transport, language, texts, suggestions, conversations
3732
4080
  handledPromptRef.current = queuedPrompt.id;
3733
4081
  void send(queuedPrompt.content);
3734
4082
  }, [busy, queuedPrompt, send]);
4083
+ // Voice conversation screen, opened by the mic (here or in the launcher bar).
4084
+ const [voiceOpen, setVoiceOpen] = useState(false);
4085
+ const handledVoiceRef = useRef(null);
4086
+ useEffect(() => {
4087
+ if (!voiceRequest || voiceRequest.id === handledVoiceRef.current)
4088
+ return;
4089
+ handledVoiceRef.current = voiceRequest.id;
4090
+ setVoiceOpen(true);
4091
+ }, [voiceRequest]);
3735
4092
  /** Retry re-sends the user message that preceded the failed answer. */
3736
4093
  const retry = useCallback((failed) => {
3737
4094
  const index = messages.findIndex((message) => message.id === failed.id);
@@ -3754,13 +4111,17 @@ const AssistantChat = ({ transport, language, texts, suggestions, conversations
3754
4111
  const compact = layout === "panel";
3755
4112
  const withList = layout !== "panel";
3756
4113
  const historyLabel = texts.history ?? texts.recentConversations;
3757
- return (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 && (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 && (jsxs("div", { className: "absolute inset-0 z-10 flex", "data-testid": "assistant-history", children: [jsx(AssistantConversationList, { conversations: conversations, activeConversationId: activeConversationId, onSelect: (id) => {
4114
+ return (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 && (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 && (jsxs("div", { className: "absolute inset-0 z-10 flex", "data-testid": "assistant-history", children: [jsx(AssistantConversationList, { conversations: conversations, activeConversationId: activeConversationId, onSelect: (id) => {
3758
4115
  onSelectConversation?.(id);
3759
4116
  onHistoryOpenChange?.(false);
3760
4117
  }, onNew: () => {
3761
4118
  startNewConversation();
3762
4119
  onHistoryOpenChange?.(false);
3763
- }, texts: texts, signInHref: signInHref, className: "w-72 max-w-full border-e border-ews-gray-200 shadow-xl" }), jsx("div", { className: "flex-1 bg-black/20", onClick: () => onHistoryOpenChange?.(false), "data-testid": "assistant-history-overlay" })] })), jsxs("div", { className: "flex min-h-0 min-w-0 flex-1 flex-col", children: [jsxs("header", { className: "flex items-center justify-between gap-3 border-b border-ews-gray-200 px-4 py-3", children: [jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [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) }), jsx("span", { className: "truncate font-semibold text-ews-gray-900", children: texts.brandName }), jsx("span", { className: "rounded-full bg-ews-gray-100 px-2 py-0.5 text-xs font-medium text-ews-gray-600", children: "AI" }), jsxs("span", { className: "flex items-center gap-1 text-xs text-ews-gray-500", children: [jsx("span", { className: "h-2 w-2 rounded-full bg-ews-success", "aria-hidden": "true" }), texts.online] })] }), jsxs("div", { className: "flex shrink-0 items-center gap-1", children: [compact && onHistoryOpenChange && (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: jsx(History, { className: "h-4 w-4", "aria-hidden": "true" }) })), !withList && !empty && (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: jsx(SquarePen, { className: "h-4 w-4", "aria-hidden": "true" }) })), headerAction] })] }), jsx("div", { className: "flex flex-1 flex-col overflow-y-auto", children: empty ? (jsxs(Fragment, { children: [jsxs("section", { className: cn("relative overflow-hidden bg-ews-primary-light text-center", compact ? "px-4 py-6" : "px-6 py-10"), children: [heroBackground && jsx("div", { className: "absolute inset-0", "aria-hidden": "true", children: heroBackground }), jsxs("div", { className: "relative", children: [jsx("h2", { className: cn("font-bold text-ews-gray-900", compact ? "text-xl" : "text-2xl sm:text-3xl"), children: texts.heroTitle }), jsx("p", { className: cn("mx-auto mt-2 max-w-xl text-ews-gray-700", compact && "text-sm"), children: texts.heroSubtitle })] })] }), 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) => (jsx(AssistantSuggestionCard, { suggestion: suggestion, onSelect: (selected) => void send(selected.prompt), exampleLabel: texts.exampleLabel, disabled: busy }, suggestion.id))) })] })) : (jsxs("div", { className: "flex-1 px-4 py-5", children: [jsx(AssistantMessages, { messages: messages, activity: activity, onRetry: retry, texts: texts, linkOptions: onNavigate
4120
+ }, texts: texts, signInHref: signInHref, className: "w-72 max-w-full border-e border-ews-gray-200 shadow-xl" }), jsx("div", { className: "flex-1 bg-black/20", onClick: () => onHistoryOpenChange?.(false), "data-testid": "assistant-history-overlay" })] })), jsxs("div", { className: "relative flex min-h-0 min-w-0 flex-1 flex-col", children: [jsxs("header", { className: "flex items-center justify-between gap-3 border-b border-ews-gray-200 px-4 py-3", children: [jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [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) }), jsx("span", { className: "truncate font-semibold text-ews-gray-900", children: texts.brandName }), jsx("span", { className: "rounded-full bg-ews-gray-100 px-2 py-0.5 text-xs font-medium text-ews-gray-600", children: "AI" }), jsxs("span", { className: "flex items-center gap-1 text-xs text-ews-gray-500", children: [jsx("span", { className: "h-2 w-2 rounded-full bg-ews-success", "aria-hidden": "true" }), texts.online] })] }), jsxs("div", { className: "flex shrink-0 items-center gap-1", children: [compact && onHistoryOpenChange && (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: jsx(History, { className: "h-4 w-4", "aria-hidden": "true" }) })), !withList && !empty && (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: jsx(SquarePen, { className: "h-4 w-4", "aria-hidden": "true" }) })), headerAction] })] }), jsx("div", { className: "flex flex-1 flex-col overflow-y-auto", children: empty ? (jsxs(Fragment, { children: [jsxs("section", { className: cn("relative overflow-hidden bg-ews-primary-light text-center",
4121
+ // Extra bottom room: the white sheet of cards overlaps the hero (mockup tools).
4122
+ compact ? "px-4 pb-12 pt-8" : "px-6 pb-16 pt-12"), children: [heroBackground && jsx("div", { className: "absolute inset-0", "aria-hidden": "true", children: heroBackground }), jsxs("div", { className: "relative", children: [jsx("h2", { className: cn("font-bold text-ews-gray-900", compact ? "text-xl" : "text-2xl sm:text-3xl"), children: texts.heroTitle }), jsx("p", { className: cn("mx-auto mt-2 max-w-xl text-ews-gray-700", compact && "text-sm"), children: texts.heroSubtitle })] })] }), jsx("div", {
4123
+ // A white sheet rounded on top, slid over the bottom of the hero, holding the cards.
4124
+ 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) => (jsx(AssistantSuggestionCard, { suggestion: suggestion, onSelect: (selected) => void send(selected.prompt), exampleLabel: texts.exampleLabel, disabled: busy }, suggestion.id))) })] })) : (jsxs("div", { className: "flex-1 px-4 py-5", children: [jsx(AssistantMessages, { messages: messages, activity: activity, onRetry: retry, texts: texts, linkOptions: onNavigate
3764
4125
  ? {
3765
4126
  onLinkClick: (href, event) => {
3766
4127
  if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey)
@@ -3769,13 +4130,15 @@ const AssistantChat = ({ transport, language, texts, suggestions, conversations
3769
4130
  event.preventDefault();
3770
4131
  },
3771
4132
  }
3772
- : undefined }), pendingConfirmation && onConfirm && onCancelConfirmation && (jsx(AssistantConfirmationCard, { confirmation: pendingConfirmation, onConfirm: onConfirm, onCancel: onCancelConfirmation, texts: texts, className: "mt-4" }))] })) }), jsxs("div", { className: "border-t border-ews-gray-200 px-4 pb-3 pt-3", children: [jsx(AssistantComposer, { language: language, onSend: (content) => void send(content), disabled: busy, voice: voice, texts: {
4133
+ : undefined }), pendingConfirmation && onConfirm && onCancelConfirmation && (jsx(AssistantConfirmationCard, { confirmation: pendingConfirmation, onConfirm: onConfirm, onCancel: onCancelConfirmation, texts: texts, className: "mt-4" }))] })) }), jsxs("div", { className: "border-t border-ews-gray-200 px-4 pb-3 pt-3", children: [jsx(AssistantComposer, { language: language, onSend: (content) => void send(content), disabled: busy, voice: voice, onVoice: () => setVoiceOpen(true), draft: composerDraft, texts: {
4134
+ dictate: texts.dictate,
4135
+ dictation: texts.dictation,
3773
4136
  placeholder: texts.composerPlaceholder,
3774
4137
  hint: texts.composerHint,
3775
4138
  send: texts.send,
3776
4139
  holdToTalk: texts.holdToTalk,
3777
4140
  listening: texts.listening,
3778
- } }), disclaimer && jsx("div", { className: "mt-2 px-2 text-xs text-ews-gray-500", children: disclaimer })] })] })] }));
4141
+ } }), disclaimer && jsx("div", { className: "mt-2 px-2 text-xs text-ews-gray-500", children: disclaimer })] }), voiceOpen && (jsx(AssistantVoiceMode, { language: language, messages: messages, busy: busy, onSend: (content) => void send(content), onClose: () => setVoiceOpen(false), texts: texts.voiceMode }))] })] }));
3779
4142
  };
3780
4143
 
3781
4144
  /**
@@ -3912,11 +4275,18 @@ const AssistantLauncher = ({ launcherTexts, open, defaultOpen = false, onOpenCha
3912
4275
  setDraft("");
3913
4276
  setOpen(true);
3914
4277
  }, [setOpen]);
3915
- const speech = useSpeechRecognition({
3916
- language: chatProps.language,
3917
- onInterim: (text) => setDraft(text),
3918
- onResult: (text) => submitFromBar(text),
3919
- });
4278
+ // One click on the bar mic (or the voice button) opens the panel straight in
4279
+ // the voice conversation, listening at once.
4280
+ const [voiceRequest, setVoiceRequest] = useState(null);
4281
+ const openVoice = useCallback(() => {
4282
+ setVoiceRequest({ id: generateId("v") });
4283
+ setOpen(true);
4284
+ }, [setOpen]);
4285
+ const speechSupported = useSpeechSupported();
4286
+ // The waveform button records a dictation in the bar itself (recording bar).
4287
+ const [dictating, setDictating] = useState(false);
4288
+ const [composerDraft, setComposerDraft] = useState(null);
4289
+ const dictateLabel = chatProps.texts.dictate ?? launcherTexts.voice ?? "Dicter un message";
3920
4290
  const handleConversationCreated = useCallback((conversation) => {
3921
4291
  if (!hostManaged) {
3922
4292
  setStoredConversations(saveAssistantConversation(conversation));
@@ -3976,31 +4346,35 @@ const AssistantLauncher = ({ launcherTexts, open, defaultOpen = false, onOpenCha
3976
4346
  if (hidden)
3977
4347
  return null;
3978
4348
  const dir = DIRECTION[chatProps.language];
3979
- const showMic = voice && speech.supported;
4349
+ const showMic = voice && speechSupported;
3980
4350
  const expandLabel = launcherTexts.expand ?? launcherTexts.open;
3981
4351
  const collapseLabel = launcherTexts.collapse ?? launcherTexts.close;
3982
4352
  const texts = launcherTexts.history ? { ...chatProps.texts, history: launcherTexts.history } : chatProps.texts;
3983
- return (jsxs(Fragment, { children: [!isOpen && (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: 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: [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) }), jsx("input", { type: "text", readOnly: true, value: draft, onMouseDown: (event) => {
3984
- event.preventDefault();
4353
+ return (jsxs(Fragment, { children: [!isOpen && (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: 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: [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 ? (jsx(AssistantDictationBar, { language: chatProps.language, texts: chatProps.texts.dictation, className: "min-w-0 flex-1", onCancel: () => setDictating(false), onDone: (text) => {
4354
+ setDictating(false);
4355
+ if (text)
4356
+ setComposerDraft({ id: generateId("d"), content: text });
3985
4357
  setOpen(true);
3986
- }, onFocus: () => setOpen(true), onKeyDown: (event) => {
3987
- if (event.key === "Enter" || event.key === " ") {
3988
- event.preventDefault();
3989
- setOpen(true);
3990
- }
3991
- }, 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 && (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) => {
3992
- if (event.key === " " || event.key === "Enter") {
3993
- event.preventDefault();
3994
- if (speech.listening)
3995
- speech.stop();
3996
- else
3997
- speech.start();
3998
- }
3999
- }, 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
4000
- ? "bg-ews-error text-white animate-pulse"
4001
- : "text-ews-gray-600 hover:bg-ews-gray-100 hover:text-ews-primary"), "data-testid": "assistant-launcher-mic", children: jsx(Mic, { className: "h-5 w-5", "aria-hidden": "true" }) })), 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: jsx(AudioLines, { className: "h-5 w-5", "aria-hidden": "true" }) }), 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: jsx(ArrowUp, { className: "h-5 w-5", "aria-hidden": "true" }) })] }) })), jsxs("div", { dir: dir, className: cn("fixed inset-0 z-[90]", !isOpen && "hidden"), "aria-hidden": !isOpen, "data-testid": "assistant-launcher-panel", children: [jsx("div", { className: "absolute inset-0 bg-black/40", onClick: () => setOpen(false), "data-testid": "assistant-launcher-overlay" }), 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",
4358
+ }, onSend: (text) => {
4359
+ setDictating(false);
4360
+ submitFromBar(text);
4361
+ } })) : (jsxs(Fragment, { children: [jsx("input", { type: "text", readOnly: true, value: draft, onMouseDown: (event) => {
4362
+ event.preventDefault();
4363
+ setOpen(true);
4364
+ }, onFocus: () => setOpen(true), onKeyDown: (event) => {
4365
+ if (event.key === "Enter" || event.key === " ") {
4366
+ event.preventDefault();
4367
+ setOpen(true);
4368
+ }
4369
+ }, 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 && (jsx(ActionTooltip, { label: chatProps.texts.holdToTalk, children: (describedBy) => (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: jsx(Mic, { className: "h-5 w-5", "aria-hidden": "true" }) })) })), jsx(ActionTooltip, { label: dictateLabel, children: (describedBy) => (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: jsx(AudioLines, { className: "h-5 w-5", "aria-hidden": "true" }) })) }), 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: jsx(ArrowUp, { className: "h-5 w-5", "aria-hidden": "true" }) })] }))] }) })), jsxs("div", { dir: dir, className: cn("fixed inset-0 z-[90]", !isOpen && "hidden"), "aria-hidden": !isOpen, "data-testid": "assistant-launcher-panel", children: [jsx("div", {
4370
+ // Mobile: a dimmed page under a full-screen panel. From tablets up the panel floats over the
4371
+ // page, which stays visible (no dimming); a click outside still closes it.
4372
+ className: "absolute inset-0 bg-black/40 sm:bg-transparent", onClick: () => setOpen(false), "data-testid": "assistant-launcher-overlay" }), 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",
4373
+ // From tablets up: a floating card (MED-1801, like the Notiwa widget), clear of the top,
4374
+ // the bottom and the side of the screen, rounded, with a soft border and shadow.
4375
+ "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)]",
4002
4376
  // Expanded: a dashboard reaching the middle of the screen (never full width), tablets and up.
4003
- isExpanded ? "sm:max-w-md md:w-[max(50vw,40rem)] md:max-w-none" : "sm:max-w-md", panelClassName), children: 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: jsxs(Fragment, { children: [headerAction, 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 ? (jsx(Minimize2, { className: "h-5 w-5", "aria-hidden": "true" })) : (jsx(Maximize2, { className: "h-5 w-5", "aria-hidden": "true" })) }), 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: jsx(X, { className: "h-5 w-5", "aria-hidden": "true" }) })] }) }) })] })] }));
4377
+ isExpanded ? "sm:max-w-md md:w-[max(50vw,40rem)] md:max-w-none" : "sm:max-w-md", panelClassName), children: 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: jsxs(Fragment, { children: [headerAction, 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 ? (jsx(Minimize2, { className: "h-5 w-5", "aria-hidden": "true" })) : (jsx(Maximize2, { className: "h-5 w-5", "aria-hidden": "true" })) }), 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: jsx(X, { className: "h-5 w-5", "aria-hidden": "true" }) })] }) }) })] })] }));
4004
4378
  };
4005
4379
 
4006
4380
  /** Turns one SSE text stream into assistant events; frames may arrive split across chunks. */
@@ -4362,5 +4736,5 @@ const SpecialtySearchAutocomplete = ({ selectedSpecialties = [], onSpecialtiesCh
4362
4736
  : `${selectedSpecialties.length} specialty${selectedSpecialties.length !== 1 ? "ies" : ""} selected` }), maxSelections && (jsxs("span", { className: "text-gray-400", children: [selectedSpecialties.length, "/", maxSelections] }))] }))] }));
4363
4737
  };
4364
4738
 
4365
- export { ASSISTANT_CLIENT_ID_KEY, ASSISTANT_CONVERSATIONS_KEY, ASSISTANT_MAX_CONVERSATIONS, AssistantChat, AssistantComposer, AssistantConfirmationCard, AssistantConversationList, AssistantLauncher, AssistantMessages, AssistantSuggestionCard, BLOOD_TYPES, BloodType, Button, COUNTRY_CODES, DoctorIcon, DropdownMultiSelect, Icon, ImageCropModal, Input, LockScreen, Logo, MAX_MESSAGE_LENGTH, Modal, MultiSearchAutocomplete, PatientIcon, PaymentDueBanner, PhoneInput, PinCodeInput, PinPad, ProfileImageUpload, RegionSelect, SearchAutocomplete, SecureLoader, Select, SpecialtySearchAutocomplete, SupportedCountry, Tag, TagList, ThemeDebugger, ThemeProvider, ThemeToggle, UserIcon, cn, conversationTitle, createAssistantTransport, createEnvConfig, debounce, extractCountryCodeFromPhoneNumber, formatCurrency, formatDate, formatNumeric, formatPhoneNumberForDisplay, formatPhoneNumberWithCountryCode, generateId, getAssistantClientId, getDefaultCountryCode, getRegionOptions, getRegionsByCountry, getSpeechRecognition, isExternalLink, isValidPhoneNumber, listAssistantConversations, parseAssistantEventStream, parseDateSafe, renderAssistantMarkdown, saveAssistantConversation, useDebounce, useDebouncedCallback, useIdleLock, useMediaQuery, useSelectField, useSpeechRecognition, useTheme };
4739
+ export { ASSISTANT_CLIENT_ID_KEY, ASSISTANT_CONVERSATIONS_KEY, ASSISTANT_MAX_CONVERSATIONS, AssistantChat, AssistantComposer, AssistantConfirmationCard, AssistantConversationList, AssistantDictationBar, AssistantLauncher, AssistantMessages, AssistantSuggestionCard, AssistantVoiceMode, BLOOD_TYPES, BloodType, Button, COUNTRY_CODES, DoctorIcon, DropdownMultiSelect, Icon, ImageCropModal, Input, LockScreen, Logo, MAX_MESSAGE_LENGTH, Modal, MultiSearchAutocomplete, PatientIcon, PaymentDueBanner, PhoneInput, PinCodeInput, PinPad, ProfileImageUpload, RegionSelect, SearchAutocomplete, SecureLoader, Select, SpecialtySearchAutocomplete, SupportedCountry, Tag, TagList, ThemeDebugger, ThemeProvider, ThemeToggle, UserIcon, cn, conversationTitle, createAssistantTransport, createEnvConfig, debounce, extractCountryCodeFromPhoneNumber, formatCurrency, formatDate, formatNumeric, formatPhoneNumberForDisplay, formatPhoneNumberWithCountryCode, generateId, getAssistantClientId, getDefaultCountryCode, getRegionOptions, getRegionsByCountry, getSpeechRecognition, isExternalLink, isValidPhoneNumber, listAssistantConversations, parseAssistantEventStream, parseDateSafe, renderAssistantMarkdown, saveAssistantConversation, toSpokenText, useDebounce, useDebouncedCallback, useIdleLock, useMediaQuery, useSelectField, useSpeechRecognition, useSpeechSupported, useTheme };
4366
4740
  //# sourceMappingURL=index.esm.js.map