@ews-admin/global-design-system 1.37.0 → 1.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/AssistantChat/ActionTooltip.d.ts +12 -0
- package/dist/components/AssistantChat/ActionTooltip.d.ts.map +1 -0
- package/dist/components/AssistantChat/AssistantChat.d.ts.map +1 -1
- package/dist/components/AssistantChat/AssistantChat.types.d.ts +37 -1
- package/dist/components/AssistantChat/AssistantChat.types.d.ts.map +1 -1
- package/dist/components/AssistantChat/AssistantComposer.d.ts +16 -3
- package/dist/components/AssistantChat/AssistantComposer.d.ts.map +1 -1
- package/dist/components/AssistantChat/AssistantDictationBar.d.ts +28 -0
- package/dist/components/AssistantChat/AssistantDictationBar.d.ts.map +1 -0
- package/dist/components/AssistantChat/AssistantSuggestionCard.d.ts +1 -1
- package/dist/components/AssistantChat/AssistantSuggestionCard.d.ts.map +1 -1
- package/dist/components/AssistantChat/AssistantVoiceMode.d.ts +37 -0
- package/dist/components/AssistantChat/AssistantVoiceMode.d.ts.map +1 -0
- package/dist/components/AssistantChat/assistantMarkdown.d.ts.map +1 -1
- package/dist/components/AssistantChat/index.d.ts +4 -0
- package/dist/components/AssistantChat/index.d.ts.map +1 -1
- package/dist/components/AssistantLauncher/AssistantLauncher.d.ts +15 -0
- package/dist/components/AssistantLauncher/AssistantLauncher.d.ts.map +1 -1
- package/dist/components/AssistantLauncher/index.d.ts +1 -1
- package/dist/components/AssistantLauncher/index.d.ts.map +1 -1
- package/dist/hooks/index.d.ts +1 -1
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/useSpeechRecognition.d.ts +2 -0
- package/dist/hooks/useSpeechRecognition.d.ts.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.ts +133 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.css +1 -1
- package/dist/index.esm.js +435 -58
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +437 -56
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/AssistantChat/ActionTooltip.tsx +31 -0
- package/src/components/AssistantChat/AssistantChat.tsx +43 -5
- package/src/components/AssistantChat/AssistantChat.types.ts +27 -1
- package/src/components/AssistantChat/AssistantComposer.tsx +106 -68
- package/src/components/AssistantChat/AssistantDictationBar.tsx +268 -0
- package/src/components/AssistantChat/AssistantSuggestionCard.tsx +7 -6
- package/src/components/AssistantChat/AssistantVoiceMode.tsx +304 -0
- package/src/components/AssistantChat/assistantMarkdown.tsx +4 -1
- package/src/components/AssistantChat/index.ts +4 -0
- package/src/components/AssistantLauncher/AssistantLauncher.tsx +156 -79
- package/src/components/AssistantLauncher/index.ts +1 -1
- package/src/hooks/index.ts +1 -1
- package/src/hooks/useSpeechRecognition.ts +9 -0
- package/src/index.ts +9 -1
package/dist/index.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,
|
|
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, ArrowLeft, 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,
|
|
3463
|
-
*
|
|
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
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
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
|
|
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: [
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
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
|
-
|
|
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 (
|
|
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(
|
|
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, heroLogo, 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",
|
|
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-20 pt-10" : "px-6 pb-24 pt-14"), children: [heroBackground && jsx("div", { className: "absolute inset-0", "aria-hidden": "true", children: heroBackground }), jsxs("div", { className: "relative", children: [heroLogo && (jsx("div", { className: "mb-4 flex justify-center", "data-testid": "assistant-hero-logo", children: heroLogo })), 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-4 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
|
/**
|
|
@@ -3870,7 +4233,7 @@ const DIRECTION = {
|
|
|
3870
4233
|
* goes to the composer on open and back to the bar on close. In Arabic the
|
|
3871
4234
|
* panel slides from the left (`end-0` follows `dir`).
|
|
3872
4235
|
*/
|
|
3873
|
-
const AssistantLauncher = ({ launcherTexts, open, defaultOpen = false, onOpenChange, expanded, defaultExpanded = false, onExpandedChange, bottomOffset = 16, hidden = false, className, panelClassName, headerAction, voice = true, conversations: hostConversations, activeConversationId: hostActiveId, initialMessages: hostInitialMessages, onConversationCreated, onSelectConversation, onNewConversation, ...chatProps }) => {
|
|
4236
|
+
const AssistantLauncher = ({ launcherTexts, open, defaultOpen = false, onOpenChange, expanded, defaultExpanded = false, onExpandedChange, bottomOffset = 16, hidden = false, className, panelClassName, embed, onEmbedClose, headerAction, voice = true, conversations: hostConversations, activeConversationId: hostActiveId, initialMessages: hostInitialMessages, onConversationCreated, onSelectConversation, onNewConversation, ...chatProps }) => {
|
|
3874
4237
|
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
|
|
3875
4238
|
const isOpen = open ?? uncontrolledOpen;
|
|
3876
4239
|
const [uncontrolledExpanded, setUncontrolledExpanded] = useState(defaultExpanded);
|
|
@@ -3912,11 +4275,18 @@ const AssistantLauncher = ({ launcherTexts, open, defaultOpen = false, onOpenCha
|
|
|
3912
4275
|
setDraft("");
|
|
3913
4276
|
setOpen(true);
|
|
3914
4277
|
}, [setOpen]);
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
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,38 @@ 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 &&
|
|
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(
|
|
3984
|
-
|
|
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
|
-
},
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
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" }), jsxs("section", { ref: panelRef, id: panelId, role: "dialog", "aria-modal": "true", "aria-label": launcherTexts.open, "data-expanded": isExpanded, className: cn("absolute inset-y-0 end-0 flex w-full flex-col bg-white shadow-2xl",
|
|
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)]",
|
|
4376
|
+
// Only fixed widths and heights, so expanding animates from one size to the other (a
|
|
4377
|
+
// max-width switching to none jumped to full width first).
|
|
4378
|
+
"sm:max-w-[calc(100vw-3rem)] sm:transition-[width,height] sm:duration-300 sm:ease-out", isExpanded ? "sm:h-[calc(100vh-3rem)]" : "sm:h-[calc(100vh-6rem)]",
|
|
4002
4379
|
// Expanded: a dashboard reaching the middle of the screen (never full width), tablets and up.
|
|
4003
|
-
isExpanded ? "sm:
|
|
4380
|
+
isExpanded ? "sm:w-[32rem] md:w-[max(50vw,40rem)]" : "sm:w-[32rem]", panelClassName), children: [embed && (jsxs("div", { className: "absolute inset-0 z-20 flex flex-col bg-white", "data-testid": "assistant-launcher-embed", children: [jsxs("div", { className: "flex items-center gap-2 border-b border-ews-gray-100 px-3 py-2", children: [jsx("button", { type: "button", onClick: onEmbedClose, "aria-label": launcherTexts.backToConversation ?? launcherTexts.close, title: launcherTexts.backToConversation ?? launcherTexts.close, className: "rounded-full p-2 text-ews-gray-500 hover:bg-ews-gray-100 hover:text-ews-gray-900 focus:outline-none focus:ring-2 focus:ring-ews-primary", "data-testid": "assistant-launcher-embed-back", children: jsx(ArrowLeft, { className: "h-5 w-5 rtl:rotate-180", "aria-hidden": "true" }) }), jsx("h2", { className: "min-w-0 flex-1 truncate text-base font-semibold text-ews-gray-900", children: embed.title }), jsx("button", { type: "button", onClick: () => setOpen(false), "aria-label": launcherTexts.close, title: launcherTexts.close, className: "rounded-full p-2 text-ews-gray-500 hover:bg-ews-gray-100 hover:text-ews-gray-900 focus:outline-none focus:ring-2 focus:ring-ews-primary", children: jsx(X, { className: "h-5 w-5", "aria-hidden": "true" }) })] }), jsx("iframe", { title: embed.title, src: embed.url, className: "block w-full flex-1 border-0" })] })), 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
4381
|
};
|
|
4005
4382
|
|
|
4006
4383
|
/** Turns one SSE text stream into assistant events; frames may arrive split across chunks. */
|
|
@@ -4362,5 +4739,5 @@ const SpecialtySearchAutocomplete = ({ selectedSpecialties = [], onSpecialtiesCh
|
|
|
4362
4739
|
: `${selectedSpecialties.length} specialty${selectedSpecialties.length !== 1 ? "ies" : ""} selected` }), maxSelections && (jsxs("span", { className: "text-gray-400", children: [selectedSpecialties.length, "/", maxSelections] }))] }))] }));
|
|
4363
4740
|
};
|
|
4364
4741
|
|
|
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 };
|
|
4742
|
+
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
4743
|
//# sourceMappingURL=index.esm.js.map
|