@djangocfg/ui-tools 2.1.381 → 2.1.383

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 (178) hide show
  1. package/README.md +132 -899
  2. package/dist/ChatRoot-6IZFM5HM.mjs +5 -0
  3. package/dist/{ChatRoot-EJC5Y2YM.cjs.map → ChatRoot-6IZFM5HM.mjs.map} +1 -1
  4. package/dist/ChatRoot-LW4XNIKP.cjs +14 -0
  5. package/dist/{ChatRoot-QOSKJPM6.mjs.map → ChatRoot-LW4XNIKP.cjs.map} +1 -1
  6. package/dist/DictationField-U25MEYAL.mjs +4 -0
  7. package/dist/DictationField-U25MEYAL.mjs.map +1 -0
  8. package/dist/DictationField-XWR5VOID.cjs +13 -0
  9. package/dist/DictationField-XWR5VOID.cjs.map +1 -0
  10. package/dist/{DocsLayout-2YKPXZYO.mjs → DocsLayout-2P3ONDWJ.mjs} +3 -3
  11. package/dist/{DocsLayout-2YKPXZYO.mjs.map → DocsLayout-2P3ONDWJ.mjs.map} +1 -1
  12. package/dist/{DocsLayout-Q4KS3QWW.cjs → DocsLayout-2YZNS5VK.cjs} +8 -8
  13. package/dist/{DocsLayout-Q4KS3QWW.cjs.map → DocsLayout-2YZNS5VK.cjs.map} +1 -1
  14. package/dist/chunk-4PFW7MIJ.cjs +837 -0
  15. package/dist/chunk-4PFW7MIJ.cjs.map +1 -0
  16. package/dist/chunk-C2YN6WEO.mjs +833 -0
  17. package/dist/chunk-C2YN6WEO.mjs.map +1 -0
  18. package/dist/{chunk-XACCHZH2.cjs → chunk-FIRK5CEH.cjs} +42 -4
  19. package/dist/chunk-FIRK5CEH.cjs.map +1 -0
  20. package/dist/{chunk-NWUT327A.mjs → chunk-HIK6BPL7.mjs} +38 -5
  21. package/dist/chunk-HIK6BPL7.mjs.map +1 -0
  22. package/dist/chunk-OZAU3QWD.cjs +2493 -0
  23. package/dist/chunk-OZAU3QWD.cjs.map +1 -0
  24. package/dist/chunk-UWVP6LCW.mjs +2447 -0
  25. package/dist/chunk-UWVP6LCW.mjs.map +1 -0
  26. package/dist/index.cjs +1668 -99
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts +1215 -107
  29. package/dist/index.d.ts +1215 -107
  30. package/dist/index.mjs +1555 -50
  31. package/dist/index.mjs.map +1 -1
  32. package/package.json +16 -15
  33. package/src/audio-assets.d.ts +8 -0
  34. package/src/components/markdown/MarkdownMessage/CollapseToggle.tsx +3 -1
  35. package/src/components/markdown/MarkdownMessage/components.tsx +2 -5
  36. package/src/tools/Chat/README.md +347 -530
  37. package/src/tools/Chat/components/Attachments.tsx +6 -1
  38. package/src/tools/Chat/components/ChatRoot.tsx +30 -2
  39. package/src/tools/Chat/components/Composer.tsx +20 -3
  40. package/src/tools/Chat/components/ErrorBanner.tsx +7 -3
  41. package/src/tools/Chat/components/MessageActions.tsx +3 -1
  42. package/src/tools/Chat/components/MessageBubble.tsx +6 -5
  43. package/src/tools/Chat/components/MessageList.tsx +87 -1
  44. package/src/tools/Chat/components/ToolCalls.tsx +21 -3
  45. package/src/tools/Chat/context/ChatProvider.tsx +21 -3
  46. package/src/tools/Chat/core/audio/audioBus.ts +10 -163
  47. package/src/tools/Chat/core/audio/defaults.ts +43 -0
  48. package/src/tools/Chat/core/audio/index.ts +1 -0
  49. package/src/tools/Chat/core/audio/preferences.ts +5 -59
  50. package/src/tools/Chat/core/audio/sounds/error.mp3 +0 -0
  51. package/src/tools/Chat/core/audio/sounds/mention.mp3 +0 -0
  52. package/src/tools/Chat/core/audio/sounds/notification.mp3 +0 -0
  53. package/src/tools/Chat/core/audio/sounds/received.mp3 +0 -0
  54. package/src/tools/Chat/core/audio/sounds/sent.mp3 +0 -0
  55. package/src/tools/Chat/core/audio/sounds/start.mp3 +0 -0
  56. package/src/tools/Chat/core/audio/types.ts +28 -0
  57. package/src/tools/Chat/core/reducer.ts +33 -0
  58. package/src/tools/Chat/core/transport/index.ts +13 -0
  59. package/src/tools/Chat/core/transport/mappers/index.ts +6 -0
  60. package/src/tools/Chat/core/transport/mappers/pydantic-ai.ts +142 -0
  61. package/src/tools/Chat/core/transport/pydantic-ai-transport.ts +208 -0
  62. package/src/tools/Chat/core/transport/sse.ts +18 -5
  63. package/src/tools/Chat/hooks/index.ts +25 -0
  64. package/src/tools/Chat/hooks/useAutoFocusOnStreamEnd.ts +5 -3
  65. package/src/tools/Chat/hooks/useChat.ts +28 -0
  66. package/src/tools/Chat/hooks/useChatAudio.ts +59 -180
  67. package/src/tools/Chat/hooks/useChatDockPrefs.ts +74 -0
  68. package/src/tools/Chat/hooks/useChatReset.ts +70 -0
  69. package/src/tools/Chat/hooks/useChatUnread.ts +87 -0
  70. package/src/tools/Chat/hooks/useFocusOnEmptyClick.ts +111 -0
  71. package/src/tools/Chat/hooks/useVisitorFingerprint.ts +48 -0
  72. package/src/tools/Chat/index.ts +84 -1
  73. package/src/tools/Chat/launcher/ChatDock.tsx +263 -0
  74. package/src/tools/Chat/launcher/ChatFAB.tsx +349 -0
  75. package/src/tools/Chat/launcher/ChatGreeting.tsx +200 -0
  76. package/src/tools/Chat/launcher/ChatHeader.tsx +76 -0
  77. package/src/tools/Chat/launcher/ChatHeaderActionButton.tsx +87 -0
  78. package/src/tools/Chat/launcher/ChatHeaderAudioToggle.tsx +47 -0
  79. package/src/tools/Chat/launcher/ChatHeaderLanguageButton.tsx +179 -0
  80. package/src/tools/Chat/launcher/ChatHeaderModeToggle.tsx +57 -0
  81. package/src/tools/Chat/launcher/ChatHeaderResetButton.tsx +93 -0
  82. package/src/tools/Chat/launcher/ChatLauncher.tsx +321 -0
  83. package/src/tools/Chat/launcher/ChatUnreadPreview.tsx +197 -0
  84. package/src/tools/Chat/launcher/index.ts +46 -0
  85. package/src/tools/Chat/launcher/useChatPresence.ts +44 -0
  86. package/src/tools/Chat/styles/bubbleTokens.ts +71 -0
  87. package/src/tools/Chat/styles/index.ts +16 -0
  88. package/src/tools/Chat/styles/useChatStyles.ts +101 -0
  89. package/src/tools/Chat/types/attachment.ts +25 -0
  90. package/src/tools/Chat/types/config.ts +48 -0
  91. package/src/tools/Chat/types/events.ts +35 -0
  92. package/src/tools/Chat/types/index.ts +34 -0
  93. package/src/tools/Chat/types/labels.ts +38 -0
  94. package/src/tools/Chat/types/message.ts +32 -0
  95. package/src/tools/Chat/types/persona.ts +31 -0
  96. package/src/tools/Chat/types/session.ts +43 -0
  97. package/src/tools/Chat/types/tool-call.ts +17 -0
  98. package/src/tools/Chat/types/transport.ts +28 -0
  99. package/src/tools/Chat/types.ts +5 -240
  100. package/src/tools/MarkdownEditor/MarkdownEditor.tsx +50 -14
  101. package/src/tools/MarkdownEditor/index.ts +1 -1
  102. package/src/tools/SpeechRecognition/README.md +336 -0
  103. package/src/tools/SpeechRecognition/__tests__/ids.test.ts +15 -0
  104. package/src/tools/SpeechRecognition/__tests__/language.test.ts +59 -0
  105. package/src/tools/SpeechRecognition/__tests__/reducer.test.ts +71 -0
  106. package/src/tools/SpeechRecognition/__tests__/transcript.test.ts +52 -0
  107. package/src/tools/SpeechRecognition/components/DevicePicker.tsx +49 -0
  108. package/src/tools/SpeechRecognition/components/DictationButton.tsx +93 -0
  109. package/src/tools/SpeechRecognition/components/EngineBadge.tsx +30 -0
  110. package/src/tools/SpeechRecognition/components/ErrorBanner.tsx +52 -0
  111. package/src/tools/SpeechRecognition/components/LanguagePicker.tsx +63 -0
  112. package/src/tools/SpeechRecognition/components/MicMeter.tsx +63 -0
  113. package/src/tools/SpeechRecognition/components/PushToTalkHint.tsx +51 -0
  114. package/src/tools/SpeechRecognition/components/TranscriptView.tsx +55 -0
  115. package/src/tools/SpeechRecognition/components/index.ts +16 -0
  116. package/src/tools/SpeechRecognition/context/SpeechRecognitionProvider.tsx +47 -0
  117. package/src/tools/SpeechRecognition/context/index.ts +6 -0
  118. package/src/tools/SpeechRecognition/core/audio/defaults.ts +24 -0
  119. package/src/tools/SpeechRecognition/core/engine/external.ts +222 -0
  120. package/src/tools/SpeechRecognition/core/engine/http.ts +147 -0
  121. package/src/tools/SpeechRecognition/core/engine/index.ts +52 -0
  122. package/src/tools/SpeechRecognition/core/engine/mediarecorder.ts +105 -0
  123. package/src/tools/SpeechRecognition/core/engine/websocket.ts +211 -0
  124. package/src/tools/SpeechRecognition/core/engine/webspeech.ts +188 -0
  125. package/src/tools/SpeechRecognition/core/ids.ts +11 -0
  126. package/src/tools/SpeechRecognition/core/index.ts +14 -0
  127. package/src/tools/SpeechRecognition/core/language.ts +78 -0
  128. package/src/tools/SpeechRecognition/core/languages-catalog.ts +229 -0
  129. package/src/tools/SpeechRecognition/core/logger.ts +3 -0
  130. package/src/tools/SpeechRecognition/core/reducer.ts +105 -0
  131. package/src/tools/SpeechRecognition/core/transcript.ts +36 -0
  132. package/src/tools/SpeechRecognition/hooks/index.ts +14 -0
  133. package/src/tools/SpeechRecognition/hooks/useDictation.ts +59 -0
  134. package/src/tools/SpeechRecognition/hooks/useEnginePrefs.ts +15 -0
  135. package/src/tools/SpeechRecognition/hooks/useMicDevices.ts +57 -0
  136. package/src/tools/SpeechRecognition/hooks/useMicLevel.ts +52 -0
  137. package/src/tools/SpeechRecognition/hooks/usePushToTalk.ts +85 -0
  138. package/src/tools/SpeechRecognition/hooks/useResolvedLanguage.ts +28 -0
  139. package/src/tools/SpeechRecognition/hooks/useSpeechLanguageInfo.ts +108 -0
  140. package/src/tools/SpeechRecognition/hooks/useSpeechRecognition.ts +188 -0
  141. package/src/tools/SpeechRecognition/hooks/useVoiceSupport.ts +78 -0
  142. package/src/tools/SpeechRecognition/index.ts +82 -0
  143. package/src/tools/SpeechRecognition/lazy.tsx +19 -0
  144. package/src/tools/SpeechRecognition/store/index.ts +2 -0
  145. package/src/tools/SpeechRecognition/store/prefsStore.ts +54 -0
  146. package/src/tools/SpeechRecognition/types.ts +133 -0
  147. package/src/tools/SpeechRecognition/widgets/DictationField.tsx +105 -0
  148. package/src/tools/SpeechRecognition/widgets/VoiceComposerSlot.tsx +305 -0
  149. package/src/tools/SpeechRecognition/widgets/VoiceMessageRecorder.tsx +88 -0
  150. package/src/tools/SpeechRecognition/widgets/index.ts +6 -0
  151. package/dist/ChatRoot-EJC5Y2YM.cjs +0 -14
  152. package/dist/ChatRoot-QOSKJPM6.mjs +0 -5
  153. package/dist/chunk-NWUT327A.mjs.map +0 -1
  154. package/dist/chunk-QLMKCSR6.mjs +0 -2420
  155. package/dist/chunk-QLMKCSR6.mjs.map +0 -1
  156. package/dist/chunk-SI5RD2GD.cjs +0 -2460
  157. package/dist/chunk-SI5RD2GD.cjs.map +0 -1
  158. package/dist/chunk-XACCHZH2.cjs.map +0 -1
  159. package/src/components/markdown/MarkdownMessage/MarkdownMessage.story.tsx +0 -771
  160. package/src/stories/index.ts +0 -33
  161. package/src/tools/AudioPlayer/AudioPlayer.story.tsx +0 -481
  162. package/src/tools/Chat/Chat.story.tsx +0 -1457
  163. package/src/tools/CodeEditor/CodeEditor.story.tsx +0 -202
  164. package/src/tools/CronScheduler/CronScheduler.story.tsx +0 -300
  165. package/src/tools/Gallery/Gallery.story.tsx +0 -237
  166. package/src/tools/ImageViewer/ImageViewer.story.tsx +0 -85
  167. package/src/tools/JsonForm/JsonForm.story.tsx +0 -350
  168. package/src/tools/JsonTree/JsonTree.story.tsx +0 -141
  169. package/src/tools/LottiePlayer/LottiePlayer.story.tsx +0 -95
  170. package/src/tools/Map/Map.story.tsx +0 -458
  171. package/src/tools/MarkdownEditor/MarkdownEditor.story.tsx +0 -225
  172. package/src/tools/Mermaid/Mermaid.story.tsx +0 -251
  173. package/src/tools/OpenapiViewer/OpenapiViewer.story.tsx +0 -230
  174. package/src/tools/PrettyCode/PrettyCode.story.tsx +0 -304
  175. package/src/tools/Tour/Tour.story.tsx +0 -279
  176. package/src/tools/Tree/Tree.story.tsx +0 -620
  177. package/src/tools/Uploader/Uploader.story.tsx +0 -415
  178. package/src/tools/VideoPlayer/VideoPlayer.story.tsx +0 -87
@@ -0,0 +1,93 @@
1
+ 'use client';
2
+
3
+ import type * as React from 'react';
4
+
5
+ import { Loader2, Mic, MicOff } from 'lucide-react';
6
+ import type { CSSProperties, ReactNode } from 'react';
7
+
8
+ import { cn } from '@djangocfg/ui-core/lib';
9
+
10
+ import type { RecognitionStatus } from '../types';
11
+
12
+ export interface DictationButtonProps {
13
+ status: RecognitionStatus;
14
+ onClick: () => void;
15
+ isSupported?: boolean;
16
+ size?: 'sm' | 'md' | 'lg';
17
+ className?: string;
18
+ style?: CSSProperties;
19
+ ariaLabel?: string;
20
+ /** Override icon for the idle state. */
21
+ idleIcon?: ReactNode;
22
+ /** Override icon for the listening state. */
23
+ listeningIcon?: ReactNode;
24
+ /** Disable without unmounting. */
25
+ disabled?: boolean;
26
+ }
27
+
28
+ const SIZE_CLS: Record<NonNullable<DictationButtonProps['size']>, string> = {
29
+ sm: 'h-8 w-8 [&_svg]:h-4 [&_svg]:w-4',
30
+ md: 'h-10 w-10 [&_svg]:h-5 [&_svg]:w-5',
31
+ lg: 'h-12 w-12 [&_svg]:h-6 [&_svg]:w-6',
32
+ };
33
+
34
+ /**
35
+ * Round microphone button. Cycles icon by status; shows a soft pulse
36
+ * ring when listening. ARIA-correct so screen readers announce
37
+ * "recording" vs "start dictation".
38
+ */
39
+ export function DictationButton({
40
+ status,
41
+ onClick,
42
+ isSupported = true,
43
+ size = 'md',
44
+ className,
45
+ style,
46
+ ariaLabel,
47
+ idleIcon,
48
+ listeningIcon,
49
+ disabled,
50
+ }: DictationButtonProps): React.ReactElement {
51
+ const listening = status === 'listening' || status === 'starting';
52
+ const stopping = status === 'stopping';
53
+ const off = !isSupported;
54
+
55
+ return (
56
+ <button
57
+ type="button"
58
+ onClick={onClick}
59
+ disabled={disabled || off}
60
+ aria-pressed={listening}
61
+ aria-label={
62
+ ariaLabel ?? (listening ? 'Stop dictation' : off ? 'Dictation not supported' : 'Start dictation')
63
+ }
64
+ className={cn(
65
+ 'relative inline-flex items-center justify-center rounded-full transition-colors',
66
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
67
+ 'disabled:cursor-not-allowed disabled:opacity-50',
68
+ SIZE_CLS[size],
69
+ listening
70
+ ? 'bg-destructive text-destructive-foreground hover:bg-destructive/90'
71
+ : 'bg-primary text-primary-foreground hover:bg-primary/90',
72
+ className,
73
+ )}
74
+ style={style}
75
+ >
76
+ {listening && (
77
+ <span
78
+ aria-hidden
79
+ className="absolute inset-0 rounded-full bg-destructive/40 animate-ping"
80
+ />
81
+ )}
82
+ {stopping ? (
83
+ <Loader2 className="animate-spin" />
84
+ ) : off ? (
85
+ listeningIcon ?? <MicOff />
86
+ ) : listening ? (
87
+ listeningIcon ?? <Mic />
88
+ ) : (
89
+ idleIcon ?? <Mic />
90
+ )}
91
+ </button>
92
+ );
93
+ }
@@ -0,0 +1,30 @@
1
+ 'use client';
2
+
3
+ import type * as React from 'react';
4
+
5
+ import { cn } from '@djangocfg/ui-core/lib';
6
+
7
+ export interface EngineBadgeProps {
8
+ engineId: string;
9
+ className?: string;
10
+ }
11
+
12
+ const LABELS: Record<string, string> = {
13
+ webspeech: 'Web Speech',
14
+ http: 'Custom HTTP',
15
+ websocket: 'WebSocket',
16
+ };
17
+
18
+ export function EngineBadge({ engineId, className }: EngineBadgeProps): React.ReactElement {
19
+ const label = LABELS[engineId] ?? engineId;
20
+ return (
21
+ <span
22
+ className={cn(
23
+ 'inline-flex items-center rounded-full border border-border bg-muted/60 px-2 py-0.5 text-[10px] uppercase tracking-wide text-muted-foreground',
24
+ className,
25
+ )}
26
+ >
27
+ {label}
28
+ </span>
29
+ );
30
+ }
@@ -0,0 +1,52 @@
1
+ 'use client';
2
+
3
+ import type * as React from 'react';
4
+
5
+ import { AlertTriangle } from 'lucide-react';
6
+
7
+ import { cn } from '@djangocfg/ui-core/lib';
8
+
9
+ import type { RecognitionError } from '../types';
10
+
11
+ const FRIENDLY: Record<RecognitionError['code'], string> = {
12
+ unsupported: 'Speech recognition isn\'t available in this browser.',
13
+ 'permission-denied': 'Microphone access was denied. Allow it in your browser settings to dictate.',
14
+ 'no-microphone': 'No microphone found.',
15
+ network: 'Network error talking to the speech service.',
16
+ aborted: 'Recognition was interrupted.',
17
+ 'no-speech': 'No speech was detected. Try again.',
18
+ language: 'The requested language isn\'t supported by the engine.',
19
+ engine: 'The speech engine reported an error.',
20
+ unknown: 'Something went wrong with the microphone.',
21
+ };
22
+
23
+ export interface ErrorBannerProps {
24
+ error: RecognitionError | null;
25
+ className?: string;
26
+ onDismiss?: () => void;
27
+ }
28
+
29
+ export function ErrorBanner({ error, className, onDismiss }: ErrorBannerProps): React.ReactElement | null {
30
+ if (!error) return null;
31
+ return (
32
+ <div
33
+ role="alert"
34
+ className={cn(
35
+ 'flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-xs text-destructive',
36
+ className,
37
+ )}
38
+ >
39
+ <AlertTriangle className="mt-0.5 h-3.5 w-3.5 shrink-0" />
40
+ <div className="flex-1">{FRIENDLY[error.code] ?? error.message}</div>
41
+ {onDismiss && (
42
+ <button
43
+ type="button"
44
+ onClick={onDismiss}
45
+ className="text-destructive hover:underline"
46
+ >
47
+ Dismiss
48
+ </button>
49
+ )}
50
+ </div>
51
+ );
52
+ }
@@ -0,0 +1,63 @@
1
+ 'use client';
2
+
3
+ import type * as React from 'react';
4
+
5
+ import { cn } from '@djangocfg/ui-core/lib';
6
+
7
+ export interface LanguageOption {
8
+ value: string;
9
+ label: string;
10
+ }
11
+
12
+ const DEFAULT_LANGUAGES: LanguageOption[] = [
13
+ { value: 'en-US', label: 'English (US)' },
14
+ { value: 'en-GB', label: 'English (UK)' },
15
+ { value: 'ru-RU', label: 'Русский' },
16
+ { value: 'ko-KR', label: '한국어' },
17
+ { value: 'ja-JP', label: '日本語' },
18
+ { value: 'zh-CN', label: '中文 (简体)' },
19
+ { value: 'es-ES', label: 'Español' },
20
+ { value: 'fr-FR', label: 'Français' },
21
+ { value: 'de-DE', label: 'Deutsch' },
22
+ ];
23
+
24
+ export interface LanguagePickerProps {
25
+ value: string;
26
+ onChange: (value: string) => void;
27
+ options?: LanguageOption[];
28
+ className?: string;
29
+ disabled?: boolean;
30
+ ariaLabel?: string;
31
+ }
32
+
33
+ export function LanguagePicker({
34
+ value,
35
+ onChange,
36
+ options = DEFAULT_LANGUAGES,
37
+ className,
38
+ disabled,
39
+ ariaLabel = 'Recognition language',
40
+ }: LanguagePickerProps): React.ReactElement {
41
+ return (
42
+ <select
43
+ value={value}
44
+ onChange={(e) => onChange(e.target.value)}
45
+ disabled={disabled}
46
+ aria-label={ariaLabel}
47
+ className={cn(
48
+ 'h-8 rounded-md border border-input bg-background px-2 text-xs text-foreground',
49
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
50
+ 'disabled:cursor-not-allowed disabled:opacity-50',
51
+ className,
52
+ )}
53
+ >
54
+ {options.map((o) => (
55
+ <option key={o.value} value={o.value}>
56
+ {o.label}
57
+ </option>
58
+ ))}
59
+ </select>
60
+ );
61
+ }
62
+
63
+ export { DEFAULT_LANGUAGES };
@@ -0,0 +1,63 @@
1
+ 'use client';
2
+
3
+ import type * as React from 'react';
4
+
5
+ import { cn } from '@djangocfg/ui-core/lib';
6
+
7
+ export interface MicMeterProps {
8
+ /** RMS level 0..1 (use the value returned by `useMicLevel`). */
9
+ level: number;
10
+ /** Number of bars rendered. @default 12 */
11
+ bars?: number;
12
+ /** Bar gap in px. @default 2 */
13
+ gap?: number;
14
+ /** Bar width in px. @default 3 */
15
+ barWidth?: number;
16
+ /** Container height in px. @default 24 */
17
+ height?: number;
18
+ className?: string;
19
+ }
20
+
21
+ /**
22
+ * Discrete-bar VU meter — small, dependency-free, no canvas. Each bar
23
+ * lights up when the current `level` exceeds its threshold. Centre bars
24
+ * are tallest so the meter reads like a classic mic level indicator.
25
+ */
26
+ export function MicMeter({
27
+ level,
28
+ bars = 12,
29
+ gap = 2,
30
+ barWidth = 3,
31
+ height = 24,
32
+ className,
33
+ }: MicMeterProps): React.ReactElement {
34
+ const clamped = Math.min(1, Math.max(0, level));
35
+ return (
36
+ <div
37
+ role="meter"
38
+ aria-valuemin={0}
39
+ aria-valuemax={1}
40
+ aria-valuenow={clamped}
41
+ className={cn('inline-flex items-center', className)}
42
+ style={{ gap, height }}
43
+ >
44
+ {Array.from({ length: bars }).map((_, i) => {
45
+ const ratio = (i + 1) / bars;
46
+ const active = clamped >= ratio - 0.5 / bars;
47
+ // taller in the middle, shorter at edges
48
+ const heightRatio = 1 - Math.abs(i - (bars - 1) / 2) / ((bars - 1) / 2 || 1);
49
+ const barH = Math.max(2, height * (0.35 + heightRatio * 0.65));
50
+ return (
51
+ <span
52
+ key={i}
53
+ className={cn(
54
+ 'rounded-sm transition-colors',
55
+ active ? 'bg-primary' : 'bg-border',
56
+ )}
57
+ style={{ width: barWidth, height: barH }}
58
+ />
59
+ );
60
+ })}
61
+ </div>
62
+ );
63
+ }
@@ -0,0 +1,51 @@
1
+ 'use client';
2
+
3
+ import type * as React from 'react';
4
+
5
+ import { cn } from '@djangocfg/ui-core/lib';
6
+
7
+ export interface PushToTalkHintProps {
8
+ /** Same chord string passed to `usePushToTalk`. */
9
+ chord: string;
10
+ className?: string;
11
+ }
12
+
13
+ /**
14
+ * Renders "Hold ⌥ to talk" with the chord pretty-printed. Tiny helper
15
+ * meant to live next to a `DictationButton`.
16
+ */
17
+ export function PushToTalkHint({ chord, className }: PushToTalkHintProps): React.ReactElement {
18
+ const formatted = chord
19
+ .split('+')
20
+ .map((p) => p.trim().toLowerCase())
21
+ .map((p) => {
22
+ switch (p) {
23
+ case 'mod':
24
+ case 'meta':
25
+ return '⌘';
26
+ case 'alt':
27
+ return '⌥';
28
+ case 'shift':
29
+ return '⇧';
30
+ case 'ctrl':
31
+ return '⌃';
32
+ default:
33
+ return p.length === 1 ? p.toUpperCase() : p;
34
+ }
35
+ })
36
+ .join('');
37
+ return (
38
+ <span
39
+ className={cn(
40
+ 'inline-flex items-center gap-1 text-[11px] text-muted-foreground',
41
+ className,
42
+ )}
43
+ >
44
+ Hold
45
+ <kbd className="rounded border border-border bg-muted px-1 py-0.5 font-mono text-[10px] text-foreground">
46
+ {formatted}
47
+ </kbd>
48
+ to talk
49
+ </span>
50
+ );
51
+ }
@@ -0,0 +1,55 @@
1
+ 'use client';
2
+
3
+ import type * as React from 'react';
4
+
5
+ import { cn } from '@djangocfg/ui-core/lib';
6
+
7
+ import type { Transcript } from '../types';
8
+
9
+ export interface TranscriptViewProps {
10
+ transcript: Transcript;
11
+ /** Render the empty state when there is nothing yet. */
12
+ emptyState?: React.ReactNode;
13
+ /** Tone the interim text down so users can tell it's still "wet". */
14
+ dimInterim?: boolean;
15
+ className?: string;
16
+ }
17
+
18
+ /**
19
+ * Two-tone transcript: final segments in foreground, the trailing
20
+ * interim chunk dimmed so users see the model "thinking". Pure
21
+ * presentational — pair with `useSpeechRecognition().transcript`.
22
+ */
23
+ export function TranscriptView({
24
+ transcript,
25
+ emptyState,
26
+ dimInterim = true,
27
+ className,
28
+ }: TranscriptViewProps): React.ReactElement {
29
+ const { final, interim } = transcript;
30
+ if (!final && !interim) {
31
+ return (
32
+ <div className={cn('text-sm text-muted-foreground', className)}>
33
+ {emptyState ?? 'Press the mic to start dictating…'}
34
+ </div>
35
+ );
36
+ }
37
+ return (
38
+ <div className={cn('text-sm leading-relaxed', className)}>
39
+ <span className="text-foreground whitespace-pre-wrap">{final}</span>
40
+ {interim && (
41
+ <>
42
+ {' '}
43
+ <span
44
+ className={cn(
45
+ 'whitespace-pre-wrap',
46
+ dimInterim ? 'text-muted-foreground' : 'text-foreground',
47
+ )}
48
+ >
49
+ {interim}
50
+ </span>
51
+ </>
52
+ )}
53
+ </div>
54
+ );
55
+ }
@@ -0,0 +1,16 @@
1
+ export { DictationButton } from './DictationButton';
2
+ export type { DictationButtonProps } from './DictationButton';
3
+ export { MicMeter } from './MicMeter';
4
+ export type { MicMeterProps } from './MicMeter';
5
+ export { TranscriptView } from './TranscriptView';
6
+ export type { TranscriptViewProps } from './TranscriptView';
7
+ export { LanguagePicker, DEFAULT_LANGUAGES } from './LanguagePicker';
8
+ export type { LanguagePickerProps, LanguageOption } from './LanguagePicker';
9
+ export { DevicePicker } from './DevicePicker';
10
+ export type { DevicePickerProps } from './DevicePicker';
11
+ export { EngineBadge } from './EngineBadge';
12
+ export type { EngineBadgeProps } from './EngineBadge';
13
+ export { ErrorBanner } from './ErrorBanner';
14
+ export type { ErrorBannerProps } from './ErrorBanner';
15
+ export { PushToTalkHint } from './PushToTalkHint';
16
+ export type { PushToTalkHintProps } from './PushToTalkHint';
@@ -0,0 +1,47 @@
1
+ 'use client';
2
+
3
+ import type * as React from 'react';
4
+ import { createContext, useContext, useMemo } from 'react';
5
+
6
+ import type { UseSpeechRecognitionConfig, UseSpeechRecognitionReturn } from '../types';
7
+ import { useSpeechRecognition } from '../hooks/useSpeechRecognition';
8
+
9
+ const Ctx = createContext<UseSpeechRecognitionReturn | null>(null);
10
+
11
+ export interface SpeechRecognitionProviderProps extends UseSpeechRecognitionConfig {
12
+ children?: React.ReactNode;
13
+ }
14
+
15
+ /**
16
+ * Lifts a single `useSpeechRecognition` instance into a context so any
17
+ * descendant — composer slot, header badge, transcript overlay — can
18
+ * read the same status / transcript / level without each component
19
+ * spinning up its own engine. Mount it once per Chat (or per
20
+ * dictation-aware screen).
21
+ */
22
+ export function SpeechRecognitionProvider({
23
+ children,
24
+ ...config
25
+ }: SpeechRecognitionProviderProps): React.ReactElement {
26
+ const rec = useSpeechRecognition(config);
27
+ // Memo prevents context-value churn from leaking through to every
28
+ // useContext consumer when only one of {status, level, transcript}
29
+ // changes — React already triggers them via the inner state, the
30
+ // outer object identity should stay stable across renders.
31
+ const value = useMemo(() => rec, [rec]);
32
+ return <Ctx.Provider value={value}>{children}</Ctx.Provider>;
33
+ }
34
+
35
+ export function useSpeechRecognitionContext(): UseSpeechRecognitionReturn {
36
+ const ctx = useContext(Ctx);
37
+ if (!ctx) {
38
+ throw new Error(
39
+ 'useSpeechRecognitionContext must be used inside a <SpeechRecognitionProvider>.',
40
+ );
41
+ }
42
+ return ctx;
43
+ }
44
+
45
+ export function useSpeechRecognitionContextOptional(): UseSpeechRecognitionReturn | null {
46
+ return useContext(Ctx);
47
+ }
@@ -0,0 +1,6 @@
1
+ export {
2
+ SpeechRecognitionProvider,
3
+ useSpeechRecognitionContext,
4
+ useSpeechRecognitionContextOptional,
5
+ } from './SpeechRecognitionProvider';
6
+ export type { SpeechRecognitionProviderProps } from './SpeechRecognitionProvider';
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Default earcons for speech sessions. We reuse two of the bundled
3
+ * Chat notification sounds rather than ship new MP3s — same lazy chunk
4
+ * already pays for them, and the tonal palette stays consistent across
5
+ * the chat + dictation UI.
6
+ *
7
+ * - `start` → the chat "stream start" chime (calm low-key tone).
8
+ * - `stop` → the chat "sent" tick (2KB, very short — feels like
9
+ * "captured" rather than a notification ping).
10
+ * Previously used `notification.mp3` (30KB, long, loud) which was
11
+ * too attention-grabbing for a self-initiated action.
12
+ *
13
+ * Loaded as `dataurl` by tsup (see `tsup.config.ts`), so consumers get
14
+ * working audio with zero asset setup.
15
+ */
16
+ import start from '../../../Chat/core/audio/sounds/start.mp3';
17
+ import sent from '../../../Chat/core/audio/sounds/sent.mp3';
18
+
19
+ export type VoiceSoundEvent = 'start' | 'stop';
20
+
21
+ export const DEFAULT_VOICE_SOUNDS: Record<VoiceSoundEvent, string> = {
22
+ start,
23
+ stop: sent,
24
+ };