@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.
- 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 +35 -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.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 +116 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.css +1 -1
- package/dist/index.esm.js +431 -57
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +433 -55
- 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 +36 -4
- package/src/components/AssistantChat/AssistantChat.types.ts +25 -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 +106 -77
- package/src/hooks/index.ts +1 -1
- package/src/hooks/useSpeechRecognition.ts +9 -0
- package/src/index.ts +8 -0
package/package.json
CHANGED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React, { useId } from "react";
|
|
2
|
+
import { cn } from "../../utils";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A visible tooltip for an icon button (hover and keyboard focus), so users
|
|
6
|
+
* know what the mic and the dictation buttons do. The button keeps its own
|
|
7
|
+
* aria-label; the tooltip is tied to it with aria-describedby.
|
|
8
|
+
*/
|
|
9
|
+
export const ActionTooltip: React.FC<{
|
|
10
|
+
label: string;
|
|
11
|
+
children: (describedBy: string) => React.ReactNode;
|
|
12
|
+
className?: string;
|
|
13
|
+
}> = ({ label, children, className }) => {
|
|
14
|
+
const id = useId();
|
|
15
|
+
return (
|
|
16
|
+
<span className={cn("group relative inline-flex", className)}>
|
|
17
|
+
{children(id)}
|
|
18
|
+
<span
|
|
19
|
+
id={id}
|
|
20
|
+
role="tooltip"
|
|
21
|
+
className={cn(
|
|
22
|
+
"pointer-events-none absolute bottom-full start-1/2 z-50 mb-2 -translate-x-1/2 whitespace-nowrap rounded-md",
|
|
23
|
+
"bg-ews-gray-900 px-2 py-1 text-xs font-medium text-white opacity-0 shadow transition-opacity duration-150",
|
|
24
|
+
"group-hover:opacity-100 group-focus-within:opacity-100 rtl:translate-x-1/2"
|
|
25
|
+
)}
|
|
26
|
+
>
|
|
27
|
+
{label}
|
|
28
|
+
</span>
|
|
29
|
+
</span>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
@@ -12,6 +12,7 @@ import { AssistantConfirmationCard } from "./AssistantConfirmationCard";
|
|
|
12
12
|
import { AssistantConversationList } from "./AssistantConversationList";
|
|
13
13
|
import { AssistantMessages } from "./AssistantMessages";
|
|
14
14
|
import { AssistantSuggestionCard } from "./AssistantSuggestionCard";
|
|
15
|
+
import { AssistantVoiceMode } from "./AssistantVoiceMode";
|
|
15
16
|
|
|
16
17
|
const DIRECTION: Record<AssistantChatProps["language"], "ltr" | "rtl"> = {
|
|
17
18
|
fr: "ltr",
|
|
@@ -55,6 +56,8 @@ export const AssistantChat: React.FC<AssistantChatProps> = ({
|
|
|
55
56
|
onCancelConfirmation,
|
|
56
57
|
layout = "page",
|
|
57
58
|
queuedPrompt = null,
|
|
59
|
+
voiceRequest = null,
|
|
60
|
+
composerDraft = null,
|
|
58
61
|
onNavigate,
|
|
59
62
|
historyOpen = false,
|
|
60
63
|
onHistoryOpenChange,
|
|
@@ -179,6 +182,15 @@ export const AssistantChat: React.FC<AssistantChatProps> = ({
|
|
|
179
182
|
void send(queuedPrompt.content);
|
|
180
183
|
}, [busy, queuedPrompt, send]);
|
|
181
184
|
|
|
185
|
+
// Voice conversation screen, opened by the mic (here or in the launcher bar).
|
|
186
|
+
const [voiceOpen, setVoiceOpen] = useState(false);
|
|
187
|
+
const handledVoiceRef = useRef<string | null>(null);
|
|
188
|
+
useEffect(() => {
|
|
189
|
+
if (!voiceRequest || voiceRequest.id === handledVoiceRef.current) return;
|
|
190
|
+
handledVoiceRef.current = voiceRequest.id;
|
|
191
|
+
setVoiceOpen(true);
|
|
192
|
+
}, [voiceRequest]);
|
|
193
|
+
|
|
182
194
|
/** Retry re-sends the user message that preceded the failed answer. */
|
|
183
195
|
const retry = useCallback(
|
|
184
196
|
(failed: AssistantMessage) => {
|
|
@@ -211,7 +223,7 @@ export const AssistantChat: React.FC<AssistantChatProps> = ({
|
|
|
211
223
|
dir={dir}
|
|
212
224
|
lang={language}
|
|
213
225
|
className={cn(
|
|
214
|
-
"overflow-hidden bg-white",
|
|
226
|
+
"relative overflow-hidden bg-white",
|
|
215
227
|
layout === "page" && "grid min-h-[36rem] grid-cols-1 rounded-2xl border border-ews-gray-200 shadow-sm md:grid-cols-[16rem_1fr]",
|
|
216
228
|
layout === "wide" && "grid h-full min-h-0 grid-cols-1 md:grid-cols-[16rem_1fr]",
|
|
217
229
|
compact && "relative flex h-full min-h-0 flex-col",
|
|
@@ -257,7 +269,7 @@ export const AssistantChat: React.FC<AssistantChatProps> = ({
|
|
|
257
269
|
</div>
|
|
258
270
|
)}
|
|
259
271
|
|
|
260
|
-
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
|
|
272
|
+
<div className="relative flex min-h-0 min-w-0 flex-1 flex-col">
|
|
261
273
|
<header className="flex items-center justify-between gap-3 border-b border-ews-gray-200 px-4 py-3">
|
|
262
274
|
<div className="flex min-w-0 items-center gap-2">
|
|
263
275
|
<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">
|
|
@@ -309,7 +321,8 @@ export const AssistantChat: React.FC<AssistantChatProps> = ({
|
|
|
309
321
|
<section
|
|
310
322
|
className={cn(
|
|
311
323
|
"relative overflow-hidden bg-ews-primary-light text-center",
|
|
312
|
-
|
|
324
|
+
// Extra bottom room: the white sheet of cards overlaps the hero (mockup tools).
|
|
325
|
+
compact ? "px-4 pb-12 pt-8" : "px-6 pb-16 pt-12"
|
|
313
326
|
)}
|
|
314
327
|
>
|
|
315
328
|
{heroBackground && <div className="absolute inset-0" aria-hidden="true">{heroBackground}</div>}
|
|
@@ -323,7 +336,11 @@ export const AssistantChat: React.FC<AssistantChatProps> = ({
|
|
|
323
336
|
</div>
|
|
324
337
|
</section>
|
|
325
338
|
<div
|
|
326
|
-
|
|
339
|
+
// A white sheet rounded on top, slid over the bottom of the hero, holding the cards.
|
|
340
|
+
className={cn(
|
|
341
|
+
"relative -mt-6 grid flex-1 content-start gap-3 rounded-t-3xl bg-white px-4 py-5",
|
|
342
|
+
compact ? "grid-cols-2" : "grid-cols-2 lg:grid-cols-4"
|
|
343
|
+
)}
|
|
327
344
|
data-testid="assistant-suggestions"
|
|
328
345
|
>
|
|
329
346
|
{suggestions.map((suggestion) => (
|
|
@@ -374,7 +391,11 @@ export const AssistantChat: React.FC<AssistantChatProps> = ({
|
|
|
374
391
|
onSend={(content) => void send(content)}
|
|
375
392
|
disabled={busy}
|
|
376
393
|
voice={voice}
|
|
394
|
+
onVoice={() => setVoiceOpen(true)}
|
|
395
|
+
draft={composerDraft}
|
|
377
396
|
texts={{
|
|
397
|
+
dictate: texts.dictate,
|
|
398
|
+
dictation: texts.dictation,
|
|
378
399
|
placeholder: texts.composerPlaceholder,
|
|
379
400
|
hint: texts.composerHint,
|
|
380
401
|
send: texts.send,
|
|
@@ -384,6 +405,17 @@ export const AssistantChat: React.FC<AssistantChatProps> = ({
|
|
|
384
405
|
/>
|
|
385
406
|
{disclaimer && <div className="mt-2 px-2 text-xs text-ews-gray-500">{disclaimer}</div>}
|
|
386
407
|
</div>
|
|
408
|
+
{/* In the thread column, so the conversation list stays visible beside it in the wide layouts. */}
|
|
409
|
+
{voiceOpen && (
|
|
410
|
+
<AssistantVoiceMode
|
|
411
|
+
language={language}
|
|
412
|
+
messages={messages}
|
|
413
|
+
busy={busy}
|
|
414
|
+
onSend={(content) => void send(content)}
|
|
415
|
+
onClose={() => setVoiceOpen(false)}
|
|
416
|
+
texts={texts.voiceMode}
|
|
417
|
+
/>
|
|
418
|
+
)}
|
|
387
419
|
</div>
|
|
388
420
|
</div>
|
|
389
421
|
);
|
|
@@ -98,7 +98,7 @@ export interface AssistantChatTexts {
|
|
|
98
98
|
composerPlaceholder: string;
|
|
99
99
|
composerHint: string;
|
|
100
100
|
send: string;
|
|
101
|
-
/** Mic button:
|
|
101
|
+
/** Mic button label: opens the voice conversation (MED-1801 voice mode). */
|
|
102
102
|
holdToTalk: string;
|
|
103
103
|
listening: string;
|
|
104
104
|
thinking: string;
|
|
@@ -111,6 +111,23 @@ export interface AssistantChatTexts {
|
|
|
111
111
|
cancel: string;
|
|
112
112
|
/** The history button of the panel layout; falls back to `recentConversations`. */
|
|
113
113
|
history?: string;
|
|
114
|
+
/** The dictation (waveform) button: label and tooltip. French default. */
|
|
115
|
+
dictate?: string;
|
|
116
|
+
/** Texts of the dictation recording bar; French defaults when absent. */
|
|
117
|
+
dictation?: { cancel?: string; done?: string; send?: string; unavailable?: string };
|
|
118
|
+
/** Texts of the voice conversation screen; French defaults when absent. */
|
|
119
|
+
voiceMode?: {
|
|
120
|
+
title?: string;
|
|
121
|
+
listening?: string;
|
|
122
|
+
thinking?: string;
|
|
123
|
+
speaking?: string;
|
|
124
|
+
muted?: string;
|
|
125
|
+
mute?: string;
|
|
126
|
+
unmute?: string;
|
|
127
|
+
end?: string;
|
|
128
|
+
/** Shown when the browser refuses the mic (permission denied, or the page is not https). */
|
|
129
|
+
unavailable?: string;
|
|
130
|
+
};
|
|
114
131
|
}
|
|
115
132
|
|
|
116
133
|
export interface AssistantConfirmation {
|
|
@@ -161,6 +178,13 @@ export interface AssistantChatProps {
|
|
|
161
178
|
* soon as possible. Each `id` is sent once; a new id sends again.
|
|
162
179
|
*/
|
|
163
180
|
queuedPrompt?: { id: string; content: string } | null;
|
|
181
|
+
/**
|
|
182
|
+
* Opens the voice conversation screen from outside (the launcher bar mic).
|
|
183
|
+
* Each `id` opens it once; a new id opens it again.
|
|
184
|
+
*/
|
|
185
|
+
voiceRequest?: { id: string } | null;
|
|
186
|
+
/** A text to put in the composer from outside (a dictation done in the launcher bar). Each id once. */
|
|
187
|
+
composerDraft?: { id: string; content: string } | null;
|
|
164
188
|
/**
|
|
165
189
|
* Called when a link in an answer is clicked. Return true when the host
|
|
166
190
|
* navigated itself (e.g. its router): the browser then does not follow the
|
|
@@ -1,21 +1,31 @@
|
|
|
1
|
-
import { Mic, SendHorizontal } from "lucide-react";
|
|
2
|
-
import React, { useCallback, useRef, useState } from "react";
|
|
3
|
-
import {
|
|
1
|
+
import { AudioLines, Mic, SendHorizontal } from "lucide-react";
|
|
2
|
+
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
import { useSpeechSupported } from "../../hooks/useSpeechRecognition";
|
|
4
4
|
import { cn } from "../../utils";
|
|
5
|
+
import { ActionTooltip } from "./ActionTooltip";
|
|
6
|
+
import { AssistantDictationBar, type AssistantDictationTexts } from "./AssistantDictationBar";
|
|
5
7
|
import type { AssistantLanguage } from "./AssistantChat.types";
|
|
6
8
|
|
|
7
9
|
export interface AssistantComposerProps {
|
|
8
10
|
language: AssistantLanguage;
|
|
9
11
|
onSend: (content: string) => void;
|
|
10
12
|
disabled?: boolean;
|
|
11
|
-
/** Mic
|
|
13
|
+
/** Mic and dictation buttons; hidden anyway where the browser has no speech API. */
|
|
12
14
|
voice?: boolean;
|
|
15
|
+
/** Mic click: opens the voice conversation. Without it the mic is not shown. */
|
|
16
|
+
onVoice?: () => void;
|
|
17
|
+
/** A text to put in the field from outside (e.g. a dictation done in the launcher bar). Each id once. */
|
|
18
|
+
draft?: { id: string; content: string } | null;
|
|
13
19
|
texts: {
|
|
14
20
|
placeholder: string;
|
|
15
21
|
hint: string;
|
|
16
22
|
send: string;
|
|
23
|
+
/** Mic: voice conversation (label and tooltip). */
|
|
17
24
|
holdToTalk: string;
|
|
18
25
|
listening: string;
|
|
26
|
+
/** Waveform button: dictation (label and tooltip). French default. */
|
|
27
|
+
dictate?: string;
|
|
28
|
+
dictation?: AssistantDictationTexts;
|
|
19
29
|
};
|
|
20
30
|
className?: string;
|
|
21
31
|
}
|
|
@@ -23,19 +33,24 @@ export interface AssistantComposerProps {
|
|
|
23
33
|
export const MAX_MESSAGE_LENGTH = 2000;
|
|
24
34
|
|
|
25
35
|
/**
|
|
26
|
-
* The composer of the mockup: textarea, hint,
|
|
27
|
-
*
|
|
36
|
+
* The composer of the mockup: textarea, hint, a dictation button (waveform:
|
|
37
|
+
* a recording bar transcribes into the field), a mic (the voice conversation)
|
|
38
|
+
* and send. Both voice buttons show a tooltip. Enter sends, Shift+Enter breaks.
|
|
28
39
|
*/
|
|
29
40
|
export const AssistantComposer: React.FC<AssistantComposerProps> = ({
|
|
30
41
|
language,
|
|
31
42
|
onSend,
|
|
32
43
|
disabled = false,
|
|
33
44
|
voice = true,
|
|
45
|
+
onVoice,
|
|
46
|
+
draft = null,
|
|
34
47
|
texts,
|
|
35
48
|
className,
|
|
36
49
|
}) => {
|
|
37
50
|
const [value, setValue] = useState("");
|
|
51
|
+
const [dictating, setDictating] = useState(false);
|
|
38
52
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
53
|
+
const speechSupported = useSpeechSupported();
|
|
39
54
|
|
|
40
55
|
const submit = useCallback(
|
|
41
56
|
(content: string) => {
|
|
@@ -47,11 +62,13 @@ export const AssistantComposer: React.FC<AssistantComposerProps> = ({
|
|
|
47
62
|
[disabled, onSend]
|
|
48
63
|
);
|
|
49
64
|
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
65
|
+
const handledDraftRef = useRef<string | null>(null);
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (!draft || draft.id === handledDraftRef.current) return;
|
|
68
|
+
handledDraftRef.current = draft.id;
|
|
69
|
+
setValue(draft.content.slice(0, MAX_MESSAGE_LENGTH));
|
|
70
|
+
textareaRef.current?.focus();
|
|
71
|
+
}, [draft]);
|
|
55
72
|
|
|
56
73
|
const onKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
57
74
|
if (event.key === "Enter" && !event.shiftKey) {
|
|
@@ -60,74 +77,95 @@ export const AssistantComposer: React.FC<AssistantComposerProps> = ({
|
|
|
60
77
|
}
|
|
61
78
|
};
|
|
62
79
|
|
|
63
|
-
const
|
|
80
|
+
const showVoice = voice && speechSupported;
|
|
81
|
+
const dictateLabel = texts.dictate ?? "Dicter un message";
|
|
64
82
|
|
|
65
83
|
// A plain div rather than a <form>: Enter and the send button submit, and a
|
|
66
84
|
// form followed by a sibling trips React hydration under happy-dom (the apps'
|
|
67
85
|
// server-rendering tests) although browsers are fine.
|
|
68
86
|
return (
|
|
69
87
|
<div className={cn("w-full", className)}>
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
aria-label={texts.placeholder}
|
|
85
|
-
disabled={disabled}
|
|
86
|
-
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"
|
|
88
|
+
{dictating ? (
|
|
89
|
+
<AssistantDictationBar
|
|
90
|
+
language={language}
|
|
91
|
+
texts={texts.dictation}
|
|
92
|
+
onCancel={() => setDictating(false)}
|
|
93
|
+
onDone={(text) => {
|
|
94
|
+
setDictating(false);
|
|
95
|
+
if (text) setValue((current) => (current ? `${current} ${text}` : text).slice(0, MAX_MESSAGE_LENGTH));
|
|
96
|
+
setTimeout(() => textareaRef.current?.focus(), 0);
|
|
97
|
+
}}
|
|
98
|
+
onSend={(text) => {
|
|
99
|
+
setDictating(false);
|
|
100
|
+
submit(value ? `${value} ${text}` : text);
|
|
101
|
+
}}
|
|
87
102
|
/>
|
|
88
|
-
|
|
103
|
+
) : (
|
|
104
|
+
<div
|
|
105
|
+
className={cn(
|
|
106
|
+
"flex items-end gap-2 rounded-2xl border border-ews-gray-200 bg-white px-3 py-2 shadow-sm",
|
|
107
|
+
"focus-within:border-ews-primary focus-within:ring-2 focus-within:ring-ews-primary/20"
|
|
108
|
+
)}
|
|
109
|
+
>
|
|
110
|
+
<textarea
|
|
111
|
+
ref={textareaRef}
|
|
112
|
+
value={value}
|
|
113
|
+
onChange={(event) => setValue(event.target.value)}
|
|
114
|
+
onKeyDown={onKeyDown}
|
|
115
|
+
rows={1}
|
|
116
|
+
maxLength={MAX_MESSAGE_LENGTH}
|
|
117
|
+
placeholder={texts.placeholder}
|
|
118
|
+
aria-label={texts.placeholder}
|
|
119
|
+
disabled={disabled}
|
|
120
|
+
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"
|
|
121
|
+
/>
|
|
122
|
+
{showVoice && (
|
|
123
|
+
<ActionTooltip label={dictateLabel}>
|
|
124
|
+
{(describedBy) => (
|
|
125
|
+
<button
|
|
126
|
+
type="button"
|
|
127
|
+
aria-label={dictateLabel}
|
|
128
|
+
aria-describedby={describedBy}
|
|
129
|
+
disabled={disabled}
|
|
130
|
+
onClick={() => setDictating(true)}
|
|
131
|
+
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"
|
|
132
|
+
data-testid="assistant-dictate"
|
|
133
|
+
>
|
|
134
|
+
<AudioLines className="h-5 w-5" aria-hidden="true" />
|
|
135
|
+
</button>
|
|
136
|
+
)}
|
|
137
|
+
</ActionTooltip>
|
|
138
|
+
)}
|
|
139
|
+
{showVoice && onVoice && (
|
|
140
|
+
<ActionTooltip label={texts.holdToTalk}>
|
|
141
|
+
{(describedBy) => (
|
|
142
|
+
<button
|
|
143
|
+
type="button"
|
|
144
|
+
aria-label={texts.holdToTalk}
|
|
145
|
+
aria-describedby={describedBy}
|
|
146
|
+
disabled={disabled}
|
|
147
|
+
onClick={onVoice}
|
|
148
|
+
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"
|
|
149
|
+
data-testid="assistant-mic"
|
|
150
|
+
>
|
|
151
|
+
<Mic className="h-5 w-5" aria-hidden="true" />
|
|
152
|
+
</button>
|
|
153
|
+
)}
|
|
154
|
+
</ActionTooltip>
|
|
155
|
+
)}
|
|
89
156
|
<button
|
|
90
157
|
type="button"
|
|
91
|
-
|
|
92
|
-
aria-
|
|
93
|
-
title={texts.
|
|
94
|
-
disabled={disabled}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
onMouseLeave={speech.listening ? speech.stop : undefined}
|
|
98
|
-
onTouchStart={speech.start}
|
|
99
|
-
onTouchEnd={speech.stop}
|
|
100
|
-
onKeyDown={(event) => {
|
|
101
|
-
if (event.key === " " || event.key === "Enter") {
|
|
102
|
-
event.preventDefault();
|
|
103
|
-
if (speech.listening) speech.stop();
|
|
104
|
-
else speech.start();
|
|
105
|
-
}
|
|
106
|
-
}}
|
|
107
|
-
className={cn(
|
|
108
|
-
"rounded-full p-2 transition-colors focus:outline-none focus:ring-2 focus:ring-ews-primary",
|
|
109
|
-
speech.listening
|
|
110
|
-
? "bg-ews-error text-white animate-pulse"
|
|
111
|
-
: "text-ews-gray-500 hover:bg-ews-gray-100 hover:text-ews-primary",
|
|
112
|
-
"disabled:opacity-50"
|
|
113
|
-
)}
|
|
114
|
-
data-testid="assistant-mic"
|
|
158
|
+
onClick={() => submit(value)}
|
|
159
|
+
aria-label={texts.send}
|
|
160
|
+
title={texts.send}
|
|
161
|
+
disabled={disabled || value.trim().length === 0}
|
|
162
|
+
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"
|
|
163
|
+
data-testid="assistant-send"
|
|
115
164
|
>
|
|
116
|
-
<
|
|
165
|
+
<SendHorizontal className="h-5 w-5 rtl:-scale-x-100" aria-hidden="true" />
|
|
117
166
|
</button>
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
type="button"
|
|
121
|
-
onClick={() => submit(value)}
|
|
122
|
-
aria-label={texts.send}
|
|
123
|
-
title={texts.send}
|
|
124
|
-
disabled={disabled || value.trim().length === 0}
|
|
125
|
-
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"
|
|
126
|
-
data-testid="assistant-send"
|
|
127
|
-
>
|
|
128
|
-
<SendHorizontal className="h-5 w-5 rtl:-scale-x-100" aria-hidden="true" />
|
|
129
|
-
</button>
|
|
130
|
-
</div>
|
|
167
|
+
</div>
|
|
168
|
+
)}
|
|
131
169
|
<p className="mt-1.5 px-2 text-xs text-ews-gray-400">{texts.hint}</p>
|
|
132
170
|
</div>
|
|
133
171
|
);
|