@ews-admin/global-design-system 1.37.0 → 1.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/components/AssistantChat/ActionTooltip.d.ts +12 -0
  2. package/dist/components/AssistantChat/ActionTooltip.d.ts.map +1 -0
  3. package/dist/components/AssistantChat/AssistantChat.d.ts.map +1 -1
  4. package/dist/components/AssistantChat/AssistantChat.types.d.ts +35 -1
  5. package/dist/components/AssistantChat/AssistantChat.types.d.ts.map +1 -1
  6. package/dist/components/AssistantChat/AssistantComposer.d.ts +16 -3
  7. package/dist/components/AssistantChat/AssistantComposer.d.ts.map +1 -1
  8. package/dist/components/AssistantChat/AssistantDictationBar.d.ts +28 -0
  9. package/dist/components/AssistantChat/AssistantDictationBar.d.ts.map +1 -0
  10. package/dist/components/AssistantChat/AssistantSuggestionCard.d.ts +1 -1
  11. package/dist/components/AssistantChat/AssistantSuggestionCard.d.ts.map +1 -1
  12. package/dist/components/AssistantChat/AssistantVoiceMode.d.ts +37 -0
  13. package/dist/components/AssistantChat/AssistantVoiceMode.d.ts.map +1 -0
  14. package/dist/components/AssistantChat/assistantMarkdown.d.ts.map +1 -1
  15. package/dist/components/AssistantChat/index.d.ts +4 -0
  16. package/dist/components/AssistantChat/index.d.ts.map +1 -1
  17. package/dist/components/AssistantLauncher/AssistantLauncher.d.ts.map +1 -1
  18. package/dist/hooks/index.d.ts +1 -1
  19. package/dist/hooks/index.d.ts.map +1 -1
  20. package/dist/hooks/useSpeechRecognition.d.ts +2 -0
  21. package/dist/hooks/useSpeechRecognition.d.ts.map +1 -1
  22. package/dist/index.css +1 -1
  23. package/dist/index.d.ts +116 -7
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.esm.css +1 -1
  26. package/dist/index.esm.js +431 -57
  27. package/dist/index.esm.js.map +1 -1
  28. package/dist/index.js +433 -55
  29. package/dist/index.js.map +1 -1
  30. package/package.json +1 -1
  31. package/src/components/AssistantChat/ActionTooltip.tsx +31 -0
  32. package/src/components/AssistantChat/AssistantChat.tsx +36 -4
  33. package/src/components/AssistantChat/AssistantChat.types.ts +25 -1
  34. package/src/components/AssistantChat/AssistantComposer.tsx +106 -68
  35. package/src/components/AssistantChat/AssistantDictationBar.tsx +268 -0
  36. package/src/components/AssistantChat/AssistantSuggestionCard.tsx +7 -6
  37. package/src/components/AssistantChat/AssistantVoiceMode.tsx +304 -0
  38. package/src/components/AssistantChat/assistantMarkdown.tsx +4 -1
  39. package/src/components/AssistantChat/index.ts +4 -0
  40. package/src/components/AssistantLauncher/AssistantLauncher.tsx +106 -77
  41. package/src/hooks/index.ts +1 -1
  42. package/src/hooks/useSpeechRecognition.ts +9 -0
  43. package/src/index.ts +8 -0
@@ -1,7 +1,9 @@
1
1
  import { ArrowUp, AudioLines, Maximize2, Mic, Minimize2, X } from "lucide-react";
2
2
  import React, { useCallback, useEffect, useId, useRef, useState } from "react";
3
3
  import { listAssistantConversations, saveAssistantConversation } from "../../assistant/store";
4
- import { useSpeechRecognition } from "../../hooks/useSpeechRecognition";
4
+ import { useSpeechSupported } from "../../hooks/useSpeechRecognition";
5
+ import { ActionTooltip } from "../AssistantChat/ActionTooltip";
6
+ import { AssistantDictationBar } from "../AssistantChat/AssistantDictationBar";
5
7
  import { cn, generateId } from "../../utils";
6
8
  import { AssistantChat } from "../AssistantChat/AssistantChat";
7
9
  import type {
@@ -147,11 +149,18 @@ export const AssistantLauncher: React.FC<AssistantLauncherProps> = ({
147
149
  [setOpen]
148
150
  );
149
151
 
150
- const speech = useSpeechRecognition({
151
- language: chatProps.language,
152
- onInterim: (text) => setDraft(text),
153
- onResult: (text) => submitFromBar(text),
154
- });
152
+ // One click on the bar mic (or the voice button) opens the panel straight in
153
+ // the voice conversation, listening at once.
154
+ const [voiceRequest, setVoiceRequest] = useState<{ id: string } | null>(null);
155
+ const openVoice = useCallback(() => {
156
+ setVoiceRequest({ id: generateId("v") });
157
+ setOpen(true);
158
+ }, [setOpen]);
159
+ const speechSupported = useSpeechSupported();
160
+ // The waveform button records a dictation in the bar itself (recording bar).
161
+ const [dictating, setDictating] = useState(false);
162
+ const [composerDraft, setComposerDraft] = useState<{ id: string; content: string } | null>(null);
163
+ const dictateLabel = chatProps.texts.dictate ?? launcherTexts.voice ?? "Dicter un message";
155
164
 
156
165
  const handleConversationCreated = useCallback(
157
166
  (conversation: AssistantConversationSummary) => {
@@ -218,7 +227,7 @@ export const AssistantLauncher: React.FC<AssistantLauncherProps> = ({
218
227
  if (hidden) return null;
219
228
 
220
229
  const dir = DIRECTION[chatProps.language];
221
- const showMic = voice && speech.supported;
230
+ const showMic = voice && speechSupported;
222
231
  const expandLabel = launcherTexts.expand ?? launcherTexts.open;
223
232
  const collapseLabel = launcherTexts.collapse ?? launcherTexts.close;
224
233
  const texts = launcherTexts.history ? { ...chatProps.texts, history: launcherTexts.history } : chatProps.texts;
@@ -254,78 +263,89 @@ export const AssistantLauncher: React.FC<AssistantLauncherProps> = ({
254
263
  >
255
264
  {chatProps.texts.brandName.charAt(0)}
256
265
  </button>
257
- {/* The field opens the panel: typing happens in the panel composer, which gets the focus. */}
258
- <input
259
- type="text"
260
- readOnly
261
- value={draft}
262
- onMouseDown={(event) => {
263
- event.preventDefault();
264
- setOpen(true);
265
- }}
266
- onFocus={() => setOpen(true)}
267
- onKeyDown={(event) => {
268
- if (event.key === "Enter" || event.key === " ") {
269
- event.preventDefault();
266
+ {dictating ? (
267
+ <AssistantDictationBar
268
+ language={chatProps.language}
269
+ texts={chatProps.texts.dictation}
270
+ className="min-w-0 flex-1"
271
+ onCancel={() => setDictating(false)}
272
+ onDone={(text) => {
273
+ setDictating(false);
274
+ if (text) setComposerDraft({ id: generateId("d"), content: text });
270
275
  setOpen(true);
271
- }
272
- }}
273
- placeholder={speech.listening ? chatProps.texts.listening : launcherTexts.open}
274
- aria-label={launcherTexts.open}
275
- aria-haspopup="dialog"
276
- aria-controls={panelId}
277
- 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"
278
- data-testid="assistant-launcher-input"
279
- />
280
- {showMic && (
281
- <button
282
- type="button"
283
- aria-label={chatProps.texts.holdToTalk}
284
- aria-pressed={speech.listening}
285
- title={chatProps.texts.holdToTalk}
286
- onMouseDown={speech.start}
287
- onMouseUp={speech.stop}
288
- onMouseLeave={speech.listening ? speech.stop : undefined}
289
- onTouchStart={speech.start}
290
- onTouchEnd={speech.stop}
291
- onKeyDown={(event) => {
292
- if (event.key === " " || event.key === "Enter") {
293
- event.preventDefault();
294
- if (speech.listening) speech.stop();
295
- else speech.start();
296
- }
297
276
  }}
298
- className={cn(
299
- "flex h-10 w-10 shrink-0 items-center justify-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-ews-primary",
300
- speech.listening
301
- ? "bg-ews-error text-white animate-pulse"
302
- : "text-ews-gray-600 hover:bg-ews-gray-100 hover:text-ews-primary"
277
+ onSend={(text) => {
278
+ setDictating(false);
279
+ submitFromBar(text);
280
+ }}
281
+ />
282
+ ) : (
283
+ <>
284
+ {/* The field opens the panel: typing happens in the panel composer, which gets the focus. */}
285
+ <input
286
+ type="text"
287
+ readOnly
288
+ value={draft}
289
+ onMouseDown={(event) => {
290
+ event.preventDefault();
291
+ setOpen(true);
292
+ }}
293
+ onFocus={() => setOpen(true)}
294
+ onKeyDown={(event) => {
295
+ if (event.key === "Enter" || event.key === " ") {
296
+ event.preventDefault();
297
+ setOpen(true);
298
+ }
299
+ }}
300
+ placeholder={launcherTexts.open}
301
+ aria-label={launcherTexts.open}
302
+ aria-haspopup="dialog"
303
+ aria-controls={panelId}
304
+ 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"
305
+ data-testid="assistant-launcher-input"
306
+ />
307
+ {showMic && (
308
+ <ActionTooltip label={chatProps.texts.holdToTalk}>
309
+ {(describedBy) => (
310
+ <button
311
+ type="button"
312
+ aria-label={chatProps.texts.holdToTalk}
313
+ aria-describedby={describedBy}
314
+ onClick={openVoice}
315
+ 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"
316
+ data-testid="assistant-launcher-mic"
317
+ >
318
+ <Mic className="h-5 w-5" aria-hidden="true" />
319
+ </button>
320
+ )}
321
+ </ActionTooltip>
303
322
  )}
304
- data-testid="assistant-launcher-mic"
305
- >
306
- <Mic className="h-5 w-5" aria-hidden="true" />
307
- </button>
323
+ <ActionTooltip label={dictateLabel}>
324
+ {(describedBy) => (
325
+ <button
326
+ type="button"
327
+ onClick={speechSupported ? () => setDictating(true) : () => setOpen(true)}
328
+ aria-label={dictateLabel}
329
+ aria-describedby={describedBy}
330
+ 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"
331
+ data-testid="assistant-launcher-voice"
332
+ >
333
+ <AudioLines className="h-5 w-5" aria-hidden="true" />
334
+ </button>
335
+ )}
336
+ </ActionTooltip>
337
+ <button
338
+ type="button"
339
+ onClick={() => submitFromBar(draft)}
340
+ aria-label={chatProps.texts.send}
341
+ title={chatProps.texts.send}
342
+ 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"
343
+ data-testid="assistant-launcher-send"
344
+ >
345
+ <ArrowUp className="h-5 w-5" aria-hidden="true" />
346
+ </button>
347
+ </>
308
348
  )}
309
- <button
310
- type="button"
311
- onClick={() => setOpen(true)}
312
- aria-label={launcherTexts.voice ?? launcherTexts.open}
313
- title={launcherTexts.voice ?? launcherTexts.open}
314
- 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"
315
- data-testid="assistant-launcher-voice"
316
- >
317
- <AudioLines className="h-5 w-5" aria-hidden="true" />
318
- </button>
319
- <button
320
- type="button"
321
- onClick={() => submitFromBar(draft)}
322
- aria-label={chatProps.texts.send}
323
- title={chatProps.texts.send}
324
- 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"
325
- data-testid="assistant-launcher-send"
326
- >
327
- <ArrowUp className="h-5 w-5" aria-hidden="true" />
328
- </button>
329
349
  </div>
330
350
  </div>
331
351
  )}
@@ -337,7 +357,9 @@ export const AssistantLauncher: React.FC<AssistantLauncherProps> = ({
337
357
  data-testid="assistant-launcher-panel"
338
358
  >
339
359
  <div
340
- className="absolute inset-0 bg-black/40"
360
+ // Mobile: a dimmed page under a full-screen panel. From tablets up the panel floats over the
361
+ // page, which stays visible (no dimming); a click outside still closes it.
362
+ className="absolute inset-0 bg-black/40 sm:bg-transparent"
341
363
  onClick={() => setOpen(false)}
342
364
  data-testid="assistant-launcher-overlay"
343
365
  />
@@ -349,7 +371,12 @@ export const AssistantLauncher: React.FC<AssistantLauncherProps> = ({
349
371
  aria-label={launcherTexts.open}
350
372
  data-expanded={isExpanded}
351
373
  className={cn(
352
- "absolute inset-y-0 end-0 flex w-full flex-col bg-white shadow-2xl transition-[width,max-width] duration-200",
374
+ "absolute inset-y-0 end-0 flex w-full flex-col bg-white shadow-2xl transition-[width,max-width,height] duration-200",
375
+ // From tablets up: a floating card (MED-1801, like the Notiwa widget), clear of the top,
376
+ // the bottom and the side of the screen, rounded, with a soft border and shadow.
377
+ "sm:inset-y-auto sm:bottom-6 sm:end-6 sm:overflow-hidden sm:rounded-3xl sm:border sm:border-ews-gray-100",
378
+ "sm:shadow-[0_24px_60px_rgba(15,23,42,0.22)]",
379
+ isExpanded ? "sm:h-[calc(100vh-3rem)]" : "sm:h-[calc(100vh-6rem)]",
353
380
  // Expanded: a dashboard reaching the middle of the screen (never full width), tablets and up.
354
381
  isExpanded ? "sm:max-w-md md:w-[max(50vw,40rem)] md:max-w-none" : "sm:max-w-md",
355
382
  panelClassName
@@ -361,6 +388,8 @@ export const AssistantLauncher: React.FC<AssistantLauncherProps> = ({
361
388
  voice={voice}
362
389
  layout={isExpanded ? "wide" : "panel"}
363
390
  queuedPrompt={queuedPrompt}
391
+ voiceRequest={voiceRequest}
392
+ composerDraft={composerDraft}
364
393
  conversations={hostManaged ? hostConversations : storedConversations}
365
394
  activeConversationId={hostManaged ? hostActiveId : managedActiveId}
366
395
  initialMessages={hostManaged ? hostInitialMessages : managedMessages}
@@ -4,5 +4,5 @@ export { useIdleLock } from "./useIdleLock";
4
4
  export type { UseIdleLockOptions, UseIdleLockResult } from "./useIdleLock";
5
5
  export { useSelectField } from "./useSelectField";
6
6
  export type { UseSelectFieldProps } from "./useSelectField";
7
- export { useSpeechRecognition, getSpeechRecognition } from "./useSpeechRecognition";
7
+ export { useSpeechRecognition, useSpeechSupported, getSpeechRecognition } from "./useSpeechRecognition";
8
8
  export type { UseSpeechRecognitionOptions } from "./useSpeechRecognition";
@@ -26,6 +26,15 @@ export function getSpeechRecognition(): SpeechRecognitionCtor | undefined {
26
26
  return w.SpeechRecognition ?? w.webkitSpeechRecognition;
27
27
  }
28
28
 
29
+ /** True once mounted in a browser that has the speech recognition API (false on the server). */
30
+ export function useSpeechSupported(): boolean {
31
+ const [supported, setSupported] = useState(false);
32
+ useEffect(() => {
33
+ setSupported(getSpeechRecognition() !== undefined);
34
+ }, []);
35
+ return supported;
36
+ }
37
+
29
38
  export interface UseSpeechRecognitionOptions {
30
39
  /** `fr`, `en`, `ar` or a BCP 47 tag. */
31
40
  language: string;
package/src/index.ts CHANGED
@@ -65,6 +65,9 @@ export type { PinCodeInputProps } from "./components/PinCodeInput";
65
65
  export {
66
66
  AssistantChat,
67
67
  AssistantComposer,
68
+ AssistantVoiceMode,
69
+ AssistantDictationBar,
70
+ toSpokenText,
68
71
  AssistantConfirmationCard,
69
72
  AssistantConversationList,
70
73
  AssistantMessages,
@@ -90,6 +93,10 @@ export type {
90
93
  AssistantMessagesProps,
91
94
  AssistantSuggestion,
92
95
  AssistantSuggestionCardProps,
96
+ AssistantVoiceModeProps,
97
+ AssistantDictationBarProps,
98
+ AssistantDictationTexts,
99
+ AssistantVoiceModeTexts,
93
100
  AssistantTransport,
94
101
  } from "./components/AssistantChat";
95
102
 
@@ -162,6 +169,7 @@ export {
162
169
  useMediaQuery,
163
170
  useSelectField,
164
171
  useSpeechRecognition,
172
+ useSpeechSupported,
165
173
  getSpeechRecognition,
166
174
  } from "./hooks";
167
175
  export type {