@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.
Files changed (47) hide show
  1. package/dist/components/AssistantChat/ActionTooltip.d.ts +12 -0
  2. package/dist/components/AssistantChat/ActionTooltip.d.ts.map +1 -0
  3. package/dist/components/AssistantChat/AssistantChat.d.ts.map +1 -1
  4. package/dist/components/AssistantChat/AssistantChat.types.d.ts +37 -1
  5. package/dist/components/AssistantChat/AssistantChat.types.d.ts.map +1 -1
  6. package/dist/components/AssistantChat/AssistantComposer.d.ts +16 -3
  7. package/dist/components/AssistantChat/AssistantComposer.d.ts.map +1 -1
  8. package/dist/components/AssistantChat/AssistantDictationBar.d.ts +28 -0
  9. package/dist/components/AssistantChat/AssistantDictationBar.d.ts.map +1 -0
  10. package/dist/components/AssistantChat/AssistantSuggestionCard.d.ts +1 -1
  11. package/dist/components/AssistantChat/AssistantSuggestionCard.d.ts.map +1 -1
  12. package/dist/components/AssistantChat/AssistantVoiceMode.d.ts +37 -0
  13. package/dist/components/AssistantChat/AssistantVoiceMode.d.ts.map +1 -0
  14. package/dist/components/AssistantChat/assistantMarkdown.d.ts.map +1 -1
  15. package/dist/components/AssistantChat/index.d.ts +4 -0
  16. package/dist/components/AssistantChat/index.d.ts.map +1 -1
  17. package/dist/components/AssistantLauncher/AssistantLauncher.d.ts +15 -0
  18. package/dist/components/AssistantLauncher/AssistantLauncher.d.ts.map +1 -1
  19. package/dist/components/AssistantLauncher/index.d.ts +1 -1
  20. package/dist/components/AssistantLauncher/index.d.ts.map +1 -1
  21. package/dist/hooks/index.d.ts +1 -1
  22. package/dist/hooks/index.d.ts.map +1 -1
  23. package/dist/hooks/useSpeechRecognition.d.ts +2 -0
  24. package/dist/hooks/useSpeechRecognition.d.ts.map +1 -1
  25. package/dist/index.css +1 -1
  26. package/dist/index.d.ts +133 -7
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.esm.css +1 -1
  29. package/dist/index.esm.js +435 -58
  30. package/dist/index.esm.js.map +1 -1
  31. package/dist/index.js +437 -56
  32. package/dist/index.js.map +1 -1
  33. package/package.json +1 -1
  34. package/src/components/AssistantChat/ActionTooltip.tsx +31 -0
  35. package/src/components/AssistantChat/AssistantChat.tsx +43 -5
  36. package/src/components/AssistantChat/AssistantChat.types.ts +27 -1
  37. package/src/components/AssistantChat/AssistantComposer.tsx +106 -68
  38. package/src/components/AssistantChat/AssistantDictationBar.tsx +268 -0
  39. package/src/components/AssistantChat/AssistantSuggestionCard.tsx +7 -6
  40. package/src/components/AssistantChat/AssistantVoiceMode.tsx +304 -0
  41. package/src/components/AssistantChat/assistantMarkdown.tsx +4 -1
  42. package/src/components/AssistantChat/index.ts +4 -0
  43. package/src/components/AssistantLauncher/AssistantLauncher.tsx +156 -79
  44. package/src/components/AssistantLauncher/index.ts +1 -1
  45. package/src/hooks/index.ts +1 -1
  46. package/src/hooks/useSpeechRecognition.ts +9 -0
  47. package/src/index.ts +9 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ews-admin/global-design-system",
3
- "version": "1.37.0",
3
+ "version": "1.39.0",
4
4
  "description": "EWS Global Design System - Reusable components for EWS applications",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.esm.js",
@@ -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
- compact ? "px-4 py-6" : "px-6 py-10"
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-2 max-w-xl text-ews-gray-700", compact && "text-sm")}>
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
- className={cn("grid gap-3 px-4 py-5", compact ? "grid-cols-1" : "sm:grid-cols-2 lg:grid-cols-4")}
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: hold to talk. */
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 { useSpeechRecognition } from "../../hooks/useSpeechRecognition";
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 button; hidden anyway where the browser has no speech API. */
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, push-to-talk mic (hold to
27
- * record, release to send) and a send button. Enter sends, Shift+Enter breaks.
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 speech = useSpeechRecognition({
51
- language,
52
- onInterim: (text) => setValue(text),
53
- onResult: (text) => submit(text),
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 showMic = voice && speech.supported;
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
- <div
71
- className={cn(
72
- "flex items-end gap-2 rounded-2xl border border-ews-gray-200 bg-white px-3 py-2 shadow-sm",
73
- "focus-within:border-ews-primary focus-within:ring-2 focus-within:ring-ews-primary/20"
74
- )}
75
- >
76
- <textarea
77
- ref={textareaRef}
78
- value={value}
79
- onChange={(event) => setValue(event.target.value)}
80
- onKeyDown={onKeyDown}
81
- rows={1}
82
- maxLength={MAX_MESSAGE_LENGTH}
83
- placeholder={speech.listening ? texts.listening : texts.placeholder}
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
- {showMic && (
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
- aria-label={texts.holdToTalk}
92
- aria-pressed={speech.listening}
93
- title={texts.holdToTalk}
94
- disabled={disabled}
95
- onMouseDown={speech.start}
96
- onMouseUp={speech.stop}
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
- <Mic className="h-5 w-5" aria-hidden="true" />
165
+ <SendHorizontal className="h-5 w-5 rtl:-scale-x-100" aria-hidden="true" />
117
166
  </button>
118
- )}
119
- <button
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
  );