@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/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",
|
|
@@ -49,12 +50,15 @@ export const AssistantChat: React.FC<AssistantChatProps> = ({
|
|
|
49
50
|
headerAction,
|
|
50
51
|
disclaimer,
|
|
51
52
|
heroBackground,
|
|
53
|
+
heroLogo,
|
|
52
54
|
voice = true,
|
|
53
55
|
pendingConfirmation = null,
|
|
54
56
|
onConfirm,
|
|
55
57
|
onCancelConfirmation,
|
|
56
58
|
layout = "page",
|
|
57
59
|
queuedPrompt = null,
|
|
60
|
+
voiceRequest = null,
|
|
61
|
+
composerDraft = null,
|
|
58
62
|
onNavigate,
|
|
59
63
|
historyOpen = false,
|
|
60
64
|
onHistoryOpenChange,
|
|
@@ -179,6 +183,15 @@ export const AssistantChat: React.FC<AssistantChatProps> = ({
|
|
|
179
183
|
void send(queuedPrompt.content);
|
|
180
184
|
}, [busy, queuedPrompt, send]);
|
|
181
185
|
|
|
186
|
+
// Voice conversation screen, opened by the mic (here or in the launcher bar).
|
|
187
|
+
const [voiceOpen, setVoiceOpen] = useState(false);
|
|
188
|
+
const handledVoiceRef = useRef<string | null>(null);
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
if (!voiceRequest || voiceRequest.id === handledVoiceRef.current) return;
|
|
191
|
+
handledVoiceRef.current = voiceRequest.id;
|
|
192
|
+
setVoiceOpen(true);
|
|
193
|
+
}, [voiceRequest]);
|
|
194
|
+
|
|
182
195
|
/** Retry re-sends the user message that preceded the failed answer. */
|
|
183
196
|
const retry = useCallback(
|
|
184
197
|
(failed: AssistantMessage) => {
|
|
@@ -211,7 +224,7 @@ export const AssistantChat: React.FC<AssistantChatProps> = ({
|
|
|
211
224
|
dir={dir}
|
|
212
225
|
lang={language}
|
|
213
226
|
className={cn(
|
|
214
|
-
"overflow-hidden bg-white",
|
|
227
|
+
"relative overflow-hidden bg-white",
|
|
215
228
|
layout === "page" && "grid min-h-[36rem] grid-cols-1 rounded-2xl border border-ews-gray-200 shadow-sm md:grid-cols-[16rem_1fr]",
|
|
216
229
|
layout === "wide" && "grid h-full min-h-0 grid-cols-1 md:grid-cols-[16rem_1fr]",
|
|
217
230
|
compact && "relative flex h-full min-h-0 flex-col",
|
|
@@ -257,7 +270,7 @@ export const AssistantChat: React.FC<AssistantChatProps> = ({
|
|
|
257
270
|
</div>
|
|
258
271
|
)}
|
|
259
272
|
|
|
260
|
-
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
|
|
273
|
+
<div className="relative flex min-h-0 min-w-0 flex-1 flex-col">
|
|
261
274
|
<header className="flex items-center justify-between gap-3 border-b border-ews-gray-200 px-4 py-3">
|
|
262
275
|
<div className="flex min-w-0 items-center gap-2">
|
|
263
276
|
<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,21 +322,31 @@ export const AssistantChat: React.FC<AssistantChatProps> = ({
|
|
|
309
322
|
<section
|
|
310
323
|
className={cn(
|
|
311
324
|
"relative overflow-hidden bg-ews-primary-light text-center",
|
|
312
|
-
|
|
325
|
+
// Extra bottom room: the white sheet of cards overlaps the hero (mockup tools).
|
|
326
|
+
compact ? "px-4 pb-20 pt-10" : "px-6 pb-24 pt-14"
|
|
313
327
|
)}
|
|
314
328
|
>
|
|
315
329
|
{heroBackground && <div className="absolute inset-0" aria-hidden="true">{heroBackground}</div>}
|
|
316
330
|
<div className="relative">
|
|
331
|
+
{heroLogo && (
|
|
332
|
+
<div className="mb-4 flex justify-center" data-testid="assistant-hero-logo">
|
|
333
|
+
{heroLogo}
|
|
334
|
+
</div>
|
|
335
|
+
)}
|
|
317
336
|
<h2 className={cn("font-bold text-ews-gray-900", compact ? "text-xl" : "text-2xl sm:text-3xl")}>
|
|
318
337
|
{texts.heroTitle}
|
|
319
338
|
</h2>
|
|
320
|
-
<p className={cn("mx-auto mt-
|
|
339
|
+
<p className={cn("mx-auto mt-4 max-w-xl text-ews-gray-700", compact && "text-sm")}>
|
|
321
340
|
{texts.heroSubtitle}
|
|
322
341
|
</p>
|
|
323
342
|
</div>
|
|
324
343
|
</section>
|
|
325
344
|
<div
|
|
326
|
-
|
|
345
|
+
// A white sheet rounded on top, slid over the bottom of the hero, holding the cards.
|
|
346
|
+
className={cn(
|
|
347
|
+
"relative -mt-6 grid flex-1 content-start gap-3 rounded-t-3xl bg-white px-4 py-5",
|
|
348
|
+
compact ? "grid-cols-2" : "grid-cols-2 lg:grid-cols-4"
|
|
349
|
+
)}
|
|
327
350
|
data-testid="assistant-suggestions"
|
|
328
351
|
>
|
|
329
352
|
{suggestions.map((suggestion) => (
|
|
@@ -374,7 +397,11 @@ export const AssistantChat: React.FC<AssistantChatProps> = ({
|
|
|
374
397
|
onSend={(content) => void send(content)}
|
|
375
398
|
disabled={busy}
|
|
376
399
|
voice={voice}
|
|
400
|
+
onVoice={() => setVoiceOpen(true)}
|
|
401
|
+
draft={composerDraft}
|
|
377
402
|
texts={{
|
|
403
|
+
dictate: texts.dictate,
|
|
404
|
+
dictation: texts.dictation,
|
|
378
405
|
placeholder: texts.composerPlaceholder,
|
|
379
406
|
hint: texts.composerHint,
|
|
380
407
|
send: texts.send,
|
|
@@ -384,6 +411,17 @@ export const AssistantChat: React.FC<AssistantChatProps> = ({
|
|
|
384
411
|
/>
|
|
385
412
|
{disclaimer && <div className="mt-2 px-2 text-xs text-ews-gray-500">{disclaimer}</div>}
|
|
386
413
|
</div>
|
|
414
|
+
{/* In the thread column, so the conversation list stays visible beside it in the wide layouts. */}
|
|
415
|
+
{voiceOpen && (
|
|
416
|
+
<AssistantVoiceMode
|
|
417
|
+
language={language}
|
|
418
|
+
messages={messages}
|
|
419
|
+
busy={busy}
|
|
420
|
+
onSend={(content) => void send(content)}
|
|
421
|
+
onClose={() => setVoiceOpen(false)}
|
|
422
|
+
texts={texts.voiceMode}
|
|
423
|
+
/>
|
|
424
|
+
)}
|
|
387
425
|
</div>
|
|
388
426
|
</div>
|
|
389
427
|
);
|
|
@@ -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 {
|
|
@@ -140,6 +157,8 @@ export interface AssistantChatProps {
|
|
|
140
157
|
disclaimer?: ReactNode;
|
|
141
158
|
/** Decorative image or gradient behind the hero. */
|
|
142
159
|
heroBackground?: ReactNode;
|
|
160
|
+
/** A small logo (e.g. the app favicon) above the hero title. */
|
|
161
|
+
heroLogo?: ReactNode;
|
|
143
162
|
/** Push-to-talk through the browser speech API. Default true; hidden anyway where unsupported. */
|
|
144
163
|
voice?: boolean;
|
|
145
164
|
/** Phase 2: a confirmation the assistant asks before a write. */
|
|
@@ -161,6 +180,13 @@ export interface AssistantChatProps {
|
|
|
161
180
|
* soon as possible. Each `id` is sent once; a new id sends again.
|
|
162
181
|
*/
|
|
163
182
|
queuedPrompt?: { id: string; content: string } | null;
|
|
183
|
+
/**
|
|
184
|
+
* Opens the voice conversation screen from outside (the launcher bar mic).
|
|
185
|
+
* Each `id` opens it once; a new id opens it again.
|
|
186
|
+
*/
|
|
187
|
+
voiceRequest?: { id: string } | null;
|
|
188
|
+
/** A text to put in the composer from outside (a dictation done in the launcher bar). Each id once. */
|
|
189
|
+
composerDraft?: { id: string; content: string } | null;
|
|
164
190
|
/**
|
|
165
191
|
* Called when a link in an answer is clicked. Return true when the host
|
|
166
192
|
* 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
|
);
|