@djangocfg/ui-tools 2.1.381 → 2.1.382

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 (183) 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-2ZLQWLYV.mjs +4 -0
  7. package/dist/DictationField-2ZLQWLYV.mjs.map +1 -0
  8. package/dist/DictationField-IPPJ54CU.cjs +13 -0
  9. package/dist/DictationField-IPPJ54CU.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-4LXG3NBV.mjs +833 -0
  15. package/dist/chunk-4LXG3NBV.mjs.map +1 -0
  16. package/dist/{chunk-XACCHZH2.cjs → chunk-FIRK5CEH.cjs} +42 -4
  17. package/dist/chunk-FIRK5CEH.cjs.map +1 -0
  18. package/dist/{chunk-NWUT327A.mjs → chunk-HIK6BPL7.mjs} +38 -5
  19. package/dist/chunk-HIK6BPL7.mjs.map +1 -0
  20. package/dist/chunk-KMSBGNVC.cjs +835 -0
  21. package/dist/chunk-KMSBGNVC.cjs.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 +1532 -100
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts +1148 -107
  29. package/dist/index.d.ts +1148 -107
  30. package/dist/index.mjs +1421 -51
  31. package/dist/index.mjs.map +1 -1
  32. package/package.json +16 -8
  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/stories/index.ts +32 -2
  37. package/src/tools/Chat/README.md +347 -530
  38. package/src/tools/Chat/components/Attachments.tsx +6 -1
  39. package/src/tools/Chat/components/ChatRoot.tsx +30 -2
  40. package/src/tools/Chat/components/Composer.tsx +20 -3
  41. package/src/tools/Chat/components/ErrorBanner.tsx +7 -3
  42. package/src/tools/Chat/components/MessageActions.tsx +3 -1
  43. package/src/tools/Chat/components/MessageBubble.tsx +6 -5
  44. package/src/tools/Chat/components/MessageList.tsx +87 -1
  45. package/src/tools/Chat/components/ToolCalls.tsx +21 -3
  46. package/src/tools/Chat/context/ChatProvider.tsx +21 -3
  47. package/src/tools/Chat/core/audio/audioBus.ts +10 -163
  48. package/src/tools/Chat/core/audio/defaults.ts +43 -0
  49. package/src/tools/Chat/core/audio/index.ts +1 -0
  50. package/src/tools/Chat/core/audio/preferences.ts +5 -59
  51. package/src/tools/Chat/core/audio/sounds/error.mp3 +0 -0
  52. package/src/tools/Chat/core/audio/sounds/mention.mp3 +0 -0
  53. package/src/tools/Chat/core/audio/sounds/notification.mp3 +0 -0
  54. package/src/tools/Chat/core/audio/sounds/received.mp3 +0 -0
  55. package/src/tools/Chat/core/audio/sounds/sent.mp3 +0 -0
  56. package/src/tools/Chat/core/audio/sounds/start.mp3 +0 -0
  57. package/src/tools/Chat/core/audio/types.ts +28 -0
  58. package/src/tools/Chat/core/reducer.ts +33 -0
  59. package/src/tools/Chat/core/transport/index.ts +13 -0
  60. package/src/tools/Chat/core/transport/mappers/index.ts +6 -0
  61. package/src/tools/Chat/core/transport/mappers/pydantic-ai.ts +142 -0
  62. package/src/tools/Chat/core/transport/pydantic-ai-transport.ts +208 -0
  63. package/src/tools/Chat/core/transport/sse.ts +18 -5
  64. package/src/tools/Chat/hooks/index.ts +25 -0
  65. package/src/tools/Chat/hooks/useAutoFocusOnStreamEnd.ts +5 -3
  66. package/src/tools/Chat/hooks/useChat.ts +28 -0
  67. package/src/tools/Chat/hooks/useChatAudio.ts +59 -180
  68. package/src/tools/Chat/hooks/useChatDockPrefs.ts +74 -0
  69. package/src/tools/Chat/hooks/useChatReset.ts +70 -0
  70. package/src/tools/Chat/hooks/useChatUnread.ts +87 -0
  71. package/src/tools/Chat/hooks/useFocusOnEmptyClick.ts +111 -0
  72. package/src/tools/Chat/hooks/useVisitorFingerprint.ts +48 -0
  73. package/src/tools/Chat/index.ts +69 -1
  74. package/src/tools/Chat/launcher/ChatDock.tsx +263 -0
  75. package/src/tools/Chat/launcher/ChatFAB.tsx +349 -0
  76. package/src/tools/Chat/launcher/ChatGreeting.tsx +200 -0
  77. package/src/tools/Chat/launcher/ChatHeader.tsx +76 -0
  78. package/src/tools/Chat/launcher/ChatHeaderActionButton.tsx +87 -0
  79. package/src/tools/Chat/launcher/ChatHeaderAudioToggle.tsx +47 -0
  80. package/src/tools/Chat/launcher/ChatHeaderLanguageButton.tsx +179 -0
  81. package/src/tools/Chat/launcher/ChatHeaderModeToggle.tsx +57 -0
  82. package/src/tools/Chat/launcher/ChatHeaderResetButton.tsx +93 -0
  83. package/src/tools/Chat/launcher/ChatLauncher.tsx +321 -0
  84. package/src/tools/Chat/launcher/ChatUnreadPreview.tsx +197 -0
  85. package/src/tools/Chat/launcher/index.ts +46 -0
  86. package/src/tools/Chat/launcher/useChatPresence.ts +44 -0
  87. package/src/tools/Chat/stories/01-basic.story.tsx +64 -0
  88. package/src/tools/Chat/stories/02-bubbles.story.tsx +21 -0
  89. package/src/tools/Chat/stories/03-tool-calls.story.tsx +59 -0
  90. package/src/tools/Chat/stories/04-personas.story.tsx +78 -0
  91. package/src/tools/Chat/stories/05-launcher.story.tsx +321 -0
  92. package/src/tools/Chat/stories/06-header.story.tsx +147 -0
  93. package/src/tools/Chat/stories/07-audio-actions.story.tsx +112 -0
  94. package/src/tools/Chat/stories/shared/Frame.tsx +21 -0
  95. package/src/tools/Chat/stories/shared/index.ts +5 -0
  96. package/src/tools/Chat/stories/shared/messages.ts +39 -0
  97. package/src/tools/Chat/stories/shared/personas.ts +13 -0
  98. package/src/tools/Chat/stories/shared/seeds.ts +92 -0
  99. package/src/tools/Chat/stories/shared/transports.ts +36 -0
  100. package/src/tools/Chat/styles/bubbleTokens.ts +71 -0
  101. package/src/tools/Chat/styles/index.ts +16 -0
  102. package/src/tools/Chat/styles/useChatStyles.ts +101 -0
  103. package/src/tools/Chat/types/attachment.ts +25 -0
  104. package/src/tools/Chat/types/config.ts +48 -0
  105. package/src/tools/Chat/types/events.ts +35 -0
  106. package/src/tools/Chat/types/index.ts +34 -0
  107. package/src/tools/Chat/types/labels.ts +38 -0
  108. package/src/tools/Chat/types/message.ts +32 -0
  109. package/src/tools/Chat/types/persona.ts +31 -0
  110. package/src/tools/Chat/types/session.ts +43 -0
  111. package/src/tools/Chat/types/tool-call.ts +17 -0
  112. package/src/tools/Chat/types/transport.ts +28 -0
  113. package/src/tools/Chat/types.ts +5 -240
  114. package/src/tools/MarkdownEditor/MarkdownEditor.tsx +50 -14
  115. package/src/tools/MarkdownEditor/index.ts +1 -1
  116. package/src/tools/SpeechRecognition/README.md +336 -0
  117. package/src/tools/SpeechRecognition/__tests__/ids.test.ts +15 -0
  118. package/src/tools/SpeechRecognition/__tests__/language.test.ts +59 -0
  119. package/src/tools/SpeechRecognition/__tests__/reducer.test.ts +71 -0
  120. package/src/tools/SpeechRecognition/__tests__/transcript.test.ts +52 -0
  121. package/src/tools/SpeechRecognition/components/DevicePicker.tsx +49 -0
  122. package/src/tools/SpeechRecognition/components/DictationButton.tsx +93 -0
  123. package/src/tools/SpeechRecognition/components/EngineBadge.tsx +30 -0
  124. package/src/tools/SpeechRecognition/components/ErrorBanner.tsx +52 -0
  125. package/src/tools/SpeechRecognition/components/LanguagePicker.tsx +63 -0
  126. package/src/tools/SpeechRecognition/components/MicMeter.tsx +63 -0
  127. package/src/tools/SpeechRecognition/components/PushToTalkHint.tsx +51 -0
  128. package/src/tools/SpeechRecognition/components/TranscriptView.tsx +55 -0
  129. package/src/tools/SpeechRecognition/components/index.ts +16 -0
  130. package/src/tools/SpeechRecognition/context/SpeechRecognitionProvider.tsx +47 -0
  131. package/src/tools/SpeechRecognition/context/index.ts +6 -0
  132. package/src/tools/SpeechRecognition/core/audio/defaults.ts +24 -0
  133. package/src/tools/SpeechRecognition/core/engine/external.ts +222 -0
  134. package/src/tools/SpeechRecognition/core/engine/http.ts +147 -0
  135. package/src/tools/SpeechRecognition/core/engine/index.ts +52 -0
  136. package/src/tools/SpeechRecognition/core/engine/mediarecorder.ts +105 -0
  137. package/src/tools/SpeechRecognition/core/engine/websocket.ts +211 -0
  138. package/src/tools/SpeechRecognition/core/engine/webspeech.ts +188 -0
  139. package/src/tools/SpeechRecognition/core/ids.ts +11 -0
  140. package/src/tools/SpeechRecognition/core/index.ts +14 -0
  141. package/src/tools/SpeechRecognition/core/language.ts +78 -0
  142. package/src/tools/SpeechRecognition/core/languages-catalog.ts +229 -0
  143. package/src/tools/SpeechRecognition/core/logger.ts +3 -0
  144. package/src/tools/SpeechRecognition/core/reducer.ts +105 -0
  145. package/src/tools/SpeechRecognition/core/transcript.ts +36 -0
  146. package/src/tools/SpeechRecognition/hooks/index.ts +14 -0
  147. package/src/tools/SpeechRecognition/hooks/useDictation.ts +59 -0
  148. package/src/tools/SpeechRecognition/hooks/useEnginePrefs.ts +15 -0
  149. package/src/tools/SpeechRecognition/hooks/useMicDevices.ts +57 -0
  150. package/src/tools/SpeechRecognition/hooks/useMicLevel.ts +52 -0
  151. package/src/tools/SpeechRecognition/hooks/usePushToTalk.ts +85 -0
  152. package/src/tools/SpeechRecognition/hooks/useResolvedLanguage.ts +28 -0
  153. package/src/tools/SpeechRecognition/hooks/useSpeechLanguageInfo.ts +108 -0
  154. package/src/tools/SpeechRecognition/hooks/useSpeechRecognition.ts +188 -0
  155. package/src/tools/SpeechRecognition/hooks/useVoiceSupport.ts +78 -0
  156. package/src/tools/SpeechRecognition/index.ts +82 -0
  157. package/src/tools/SpeechRecognition/lazy.tsx +19 -0
  158. package/src/tools/SpeechRecognition/store/index.ts +2 -0
  159. package/src/tools/SpeechRecognition/store/prefsStore.ts +54 -0
  160. package/src/tools/SpeechRecognition/stories/01-basic.story.tsx +32 -0
  161. package/src/tools/SpeechRecognition/stories/02-dictation-field.story.tsx +32 -0
  162. package/src/tools/SpeechRecognition/stories/03-push-to-talk.story.tsx +27 -0
  163. package/src/tools/SpeechRecognition/stories/04-mic-meter.story.tsx +35 -0
  164. package/src/tools/SpeechRecognition/stories/05-custom-engine-http.story.tsx +40 -0
  165. package/src/tools/SpeechRecognition/stories/06-custom-engine-ws.story.tsx +48 -0
  166. package/src/tools/SpeechRecognition/stories/07-language-device.story.tsx +57 -0
  167. package/src/tools/SpeechRecognition/stories/08-errors-permissions.story.tsx +25 -0
  168. package/src/tools/SpeechRecognition/stories/09-chat-voice.story.tsx +90 -0
  169. package/src/tools/SpeechRecognition/stories/shared.tsx +123 -0
  170. package/src/tools/SpeechRecognition/types.ts +133 -0
  171. package/src/tools/SpeechRecognition/widgets/DictationField.tsx +105 -0
  172. package/src/tools/SpeechRecognition/widgets/VoiceComposerSlot.tsx +305 -0
  173. package/src/tools/SpeechRecognition/widgets/VoiceMessageRecorder.tsx +88 -0
  174. package/src/tools/SpeechRecognition/widgets/index.ts +6 -0
  175. package/dist/ChatRoot-EJC5Y2YM.cjs +0 -14
  176. package/dist/ChatRoot-QOSKJPM6.mjs +0 -5
  177. package/dist/chunk-NWUT327A.mjs.map +0 -1
  178. package/dist/chunk-QLMKCSR6.mjs +0 -2420
  179. package/dist/chunk-QLMKCSR6.mjs.map +0 -1
  180. package/dist/chunk-SI5RD2GD.cjs +0 -2460
  181. package/dist/chunk-SI5RD2GD.cjs.map +0 -1
  182. package/dist/chunk-XACCHZH2.cjs.map +0 -1
  183. package/src/tools/Chat/Chat.story.tsx +0 -1457
@@ -0,0 +1,54 @@
1
+ 'use client';
2
+
3
+ import { create } from 'zustand';
4
+ import { persist, createJSONStorage } from 'zustand/middleware';
5
+
6
+ export interface SpeechPrefs {
7
+ /**
8
+ * BCP-47 tag the user explicitly picked (via `<LanguagePicker>` or
9
+ * programmatically). `null` means "no override" — `useResolvedLanguage`
10
+ * then falls through to the app i18n locale / `navigator.language`.
11
+ * Storing the picker default as `null` is what lets a host's i18n
12
+ * locale take effect when the user never touched the picker.
13
+ */
14
+ language: string | null;
15
+ deviceId: string | null;
16
+ engineId: string | null;
17
+ earcons: boolean;
18
+ }
19
+
20
+ const DEFAULTS: SpeechPrefs = {
21
+ language: null,
22
+ deviceId: null,
23
+ engineId: null,
24
+ earcons: false,
25
+ };
26
+
27
+ interface PrefsStore extends SpeechPrefs {
28
+ setLanguage: (v: string | null) => void;
29
+ setDeviceId: (v: string | null) => void;
30
+ setEngineId: (v: string | null) => void;
31
+ setEarcons: (v: boolean) => void;
32
+ reset: () => void;
33
+ }
34
+
35
+ export const useSpeechPrefs = create<PrefsStore>()(
36
+ persist(
37
+ (set) => ({
38
+ ...DEFAULTS,
39
+ setLanguage: (language) => set({ language }),
40
+ setDeviceId: (deviceId) => set({ deviceId }),
41
+ setEngineId: (engineId) => set({ engineId }),
42
+ setEarcons: (earcons) => set({ earcons }),
43
+ reset: () => set({ ...DEFAULTS }),
44
+ }),
45
+ {
46
+ name: 'djangocfg-stt:prefs',
47
+ storage: createJSONStorage(() =>
48
+ typeof window === 'undefined'
49
+ ? (undefined as unknown as Storage)
50
+ : window.localStorage,
51
+ ),
52
+ },
53
+ ),
54
+ );
@@ -0,0 +1,32 @@
1
+ import { useMemo } from 'react';
2
+
3
+ import { defineStory } from '@djangocfg/playground';
4
+
5
+ import { DictationButton } from '../components/DictationButton';
6
+ import { ErrorBanner } from '../components/ErrorBanner';
7
+ import { TranscriptView } from '../components/TranscriptView';
8
+ import { useSpeechRecognition } from '../hooks/useSpeechRecognition';
9
+ import { Frame, createMockEngine } from './shared';
10
+
11
+ export default defineStory({
12
+ title: 'Tools/SpeechRecognition/Basic',
13
+ component: DictationButton,
14
+ description:
15
+ 'Headless `useSpeechRecognition` with a mock engine — deterministic transcript, no mic prompt.',
16
+ });
17
+
18
+ export const MockTranscript = () => {
19
+ const engine = useMemo(() => createMockEngine(), []);
20
+ const rec = useSpeechRecognition({ engine });
21
+ return (
22
+ <Frame>
23
+ <div className="flex items-start gap-3">
24
+ <DictationButton status={rec.status} onClick={() => void rec.toggle()} />
25
+ <div className="flex-1">
26
+ <TranscriptView transcript={rec.transcript} />
27
+ <ErrorBanner error={rec.error} className="mt-2" />
28
+ </div>
29
+ </div>
30
+ </Frame>
31
+ );
32
+ };
@@ -0,0 +1,32 @@
1
+ import { useMemo, useState } from 'react';
2
+
3
+ import { defineStory } from '@djangocfg/playground';
4
+
5
+ import { DictationField } from '../widgets/DictationField';
6
+ import { Frame, createMockEngine } from './shared';
7
+
8
+ export default defineStory({
9
+ title: 'Tools/SpeechRecognition/DictationField',
10
+ component: DictationField,
11
+ description:
12
+ 'Opinionated textarea + mic button assembly. Final segments are appended to the controlled `value`. `BrowserNative` uses the real Web Speech API (mic prompt on first press); `MockedDemo` types a scripted phrase without touching the microphone.',
13
+ });
14
+
15
+ export const BrowserNative = () => {
16
+ const [value, setValue] = useState('');
17
+ return (
18
+ <Frame>
19
+ <DictationField value={value} onChange={setValue} />
20
+ </Frame>
21
+ );
22
+ };
23
+
24
+ export const MockedDemo = () => {
25
+ const engine = useMemo(() => createMockEngine(), []);
26
+ const [value, setValue] = useState('');
27
+ return (
28
+ <Frame>
29
+ <DictationField value={value} onChange={setValue} engine={engine} />
30
+ </Frame>
31
+ );
32
+ };
@@ -0,0 +1,27 @@
1
+ import { useMemo, useState } from 'react';
2
+
3
+ import { defineStory } from '@djangocfg/playground';
4
+
5
+ import { DictationField } from '../widgets/DictationField';
6
+ import { Frame, createMockEngine } from './shared';
7
+
8
+ export default defineStory({
9
+ title: 'Tools/SpeechRecognition/PushToTalk',
10
+ component: DictationField,
11
+ description: 'Hold ⌥ (alt) to dictate. Release to stop and commit.',
12
+ });
13
+
14
+ export const HoldAlt = () => {
15
+ const engine = useMemo(() => createMockEngine({ partialIntervalMs: 80 }), []);
16
+ const [value, setValue] = useState('');
17
+ return (
18
+ <Frame>
19
+ <DictationField
20
+ value={value}
21
+ onChange={setValue}
22
+ engine={engine}
23
+ pushToTalk={{ key: 'alt' }}
24
+ />
25
+ </Frame>
26
+ );
27
+ };
@@ -0,0 +1,35 @@
1
+ import { useEffect, useState } from 'react';
2
+
3
+ import { defineStory } from '@djangocfg/playground';
4
+
5
+ import { MicMeter } from '../components/MicMeter';
6
+ import { Frame } from './shared';
7
+
8
+ export default defineStory({
9
+ title: 'Tools/SpeechRecognition/MicMeter',
10
+ component: MicMeter,
11
+ description:
12
+ 'RMS level meter — animates from a synthetic sine here so you can see the bars without granting mic access.',
13
+ });
14
+
15
+ export const Animated = () => {
16
+ const [level, setLevel] = useState(0);
17
+ useEffect(() => {
18
+ let t = 0;
19
+ const id = window.setInterval(() => {
20
+ t += 0.1;
21
+ setLevel((Math.sin(t) + 1) / 2);
22
+ }, 80);
23
+ return () => window.clearInterval(id);
24
+ }, []);
25
+ return (
26
+ <Frame w={320}>
27
+ <div className="flex items-center gap-3">
28
+ <MicMeter level={level} bars={16} height={32} />
29
+ <span className="font-mono text-xs text-muted-foreground">
30
+ level: {level.toFixed(2)}
31
+ </span>
32
+ </div>
33
+ </Frame>
34
+ );
35
+ };
@@ -0,0 +1,40 @@
1
+ import { defineStory } from '@djangocfg/playground';
2
+
3
+ import { EngineBadge } from '../components/EngineBadge';
4
+ import { Frame } from './shared';
5
+
6
+ export default defineStory({
7
+ title: 'Tools/SpeechRecognition/CustomEngine: HTTP',
8
+ component: EngineBadge,
9
+ description:
10
+ 'Plug `createHttpEngine` into `useSpeechRecognition` to forward audio to any REST endpoint (Whisper, custom Django, …). This story is documentation only — it would need a real backend to play sound.',
11
+ });
12
+
13
+ export const Snippet = () => (
14
+ <Frame>
15
+ <div className="space-y-2 text-xs">
16
+ <div className="flex items-center gap-2">
17
+ <EngineBadge engineId="http" />
18
+ <span className="text-muted-foreground">— consumer-owned backend</span>
19
+ </div>
20
+ <pre className="overflow-auto rounded-md border border-border bg-muted/40 p-3 font-mono text-[11px] leading-snug">
21
+ {`import {
22
+ createHttpEngine,
23
+ useSpeechRecognition,
24
+ } from '@djangocfg/ui-tools/speech-recognition';
25
+
26
+ const engine = createHttpEngine({
27
+ url: '/api/stt/transcribe',
28
+ headers: async () => ({ Authorization: \`Bearer \${token}\` }),
29
+ chunkMs: 750,
30
+ parse: async (resp) => {
31
+ const { text, final } = await resp.json();
32
+ return { text, isFinal: final };
33
+ },
34
+ });
35
+
36
+ const rec = useSpeechRecognition({ engine });`}
37
+ </pre>
38
+ </div>
39
+ </Frame>
40
+ );
@@ -0,0 +1,48 @@
1
+ import { defineStory } from '@djangocfg/playground';
2
+
3
+ import { EngineBadge } from '../components/EngineBadge';
4
+ import { Frame } from './shared';
5
+
6
+ export default defineStory({
7
+ title: 'Tools/SpeechRecognition/CustomEngine: WebSocket',
8
+ component: EngineBadge,
9
+ description:
10
+ '`createWebSocketEngine` pushes audio frames over a persistent socket — Deepgram / AssemblyAI / custom realtime gateways. Documentation snippet, no live backend.',
11
+ });
12
+
13
+ export const Snippet = () => (
14
+ <Frame>
15
+ <div className="space-y-2 text-xs">
16
+ <div className="flex items-center gap-2">
17
+ <EngineBadge engineId="websocket" />
18
+ <span className="text-muted-foreground">— low-latency streaming</span>
19
+ </div>
20
+ <pre className="overflow-auto rounded-md border border-border bg-muted/40 p-3 font-mono text-[11px] leading-snug">
21
+ {`import {
22
+ createWebSocketEngine,
23
+ useSpeechRecognition,
24
+ } from '@djangocfg/ui-tools/speech-recognition';
25
+
26
+ const engine = createWebSocketEngine({
27
+ url: async () => {
28
+ const { token } = await fetch('/api/stt/ticket').then((r) => r.json());
29
+ return \`wss://stt.example.com/listen?token=\${token}\`;
30
+ },
31
+ chunkMs: 250,
32
+ parseMessage: (data) => {
33
+ if (typeof data !== 'string') return { kind: 'ignore' };
34
+ const msg = JSON.parse(data);
35
+ if (msg.type === 'Results') {
36
+ return msg.is_final
37
+ ? { kind: 'final', text: msg.channel.alternatives[0].transcript }
38
+ : { kind: 'partial', text: msg.channel.alternatives[0].transcript };
39
+ }
40
+ return { kind: 'ignore' };
41
+ },
42
+ });
43
+
44
+ const rec = useSpeechRecognition({ engine });`}
45
+ </pre>
46
+ </div>
47
+ </Frame>
48
+ );
@@ -0,0 +1,57 @@
1
+ import { useMemo, useState } from 'react';
2
+
3
+ import { defineStory } from '@djangocfg/playground';
4
+
5
+ import { DevicePicker } from '../components/DevicePicker';
6
+ import { EngineBadge } from '../components/EngineBadge';
7
+ import { LanguagePicker } from '../components/LanguagePicker';
8
+ import { TranscriptView } from '../components/TranscriptView';
9
+ import { useMicDevices } from '../hooks/useMicDevices';
10
+ import { useSpeechPrefs } from '../store/prefsStore';
11
+ import { useSpeechRecognition } from '../hooks/useSpeechRecognition';
12
+ import { Frame, createMockEngine } from './shared';
13
+
14
+ export default defineStory({
15
+ title: 'Tools/SpeechRecognition/Language & Device',
16
+ component: LanguagePicker,
17
+ description:
18
+ 'Persisted language / mic preferences via `useSpeechPrefs` + `useMicDevices`. Mock engine here so the toolbar renders without a real mic.',
19
+ });
20
+
21
+ export const LanguageAndDevice = () => {
22
+ const engine = useMemo(() => createMockEngine(), []);
23
+ const rec = useSpeechRecognition({ engine });
24
+ const prefs = useSpeechPrefs();
25
+ const { devices } = useMicDevices();
26
+ const [language, setLanguage] = useState(prefs.language ?? 'en-US');
27
+
28
+ return (
29
+ <Frame>
30
+ <div className="space-y-3">
31
+ <div className="flex items-center gap-2">
32
+ <LanguagePicker
33
+ value={language}
34
+ onChange={(v) => {
35
+ setLanguage(v);
36
+ prefs.setLanguage(v);
37
+ }}
38
+ />
39
+ <DevicePicker
40
+ devices={devices}
41
+ value={prefs.deviceId}
42
+ onChange={prefs.setDeviceId}
43
+ />
44
+ <EngineBadge engineId={engine.id} className="ml-auto" />
45
+ </div>
46
+ <button
47
+ type="button"
48
+ onClick={() => void rec.toggle()}
49
+ className="rounded-md border border-border px-2 py-1 text-xs hover:bg-muted"
50
+ >
51
+ {rec.status === 'listening' ? 'Stop' : 'Start'}
52
+ </button>
53
+ <TranscriptView transcript={rec.transcript} />
54
+ </div>
55
+ </Frame>
56
+ );
57
+ };
@@ -0,0 +1,25 @@
1
+ import { defineStory } from '@djangocfg/playground';
2
+
3
+ import { ErrorBanner } from '../components/ErrorBanner';
4
+ import { Frame } from './shared';
5
+
6
+ export default defineStory({
7
+ title: 'Tools/SpeechRecognition/Errors',
8
+ component: ErrorBanner,
9
+ description:
10
+ 'All error codes the engine can surface — render `<ErrorBanner>` to translate them into friendly copy.',
11
+ });
12
+
13
+ export const AllCodes = () => (
14
+ <Frame>
15
+ <div className="space-y-2">
16
+ <ErrorBanner error={{ code: 'unsupported', message: '' }} />
17
+ <ErrorBanner error={{ code: 'permission-denied', message: '' }} />
18
+ <ErrorBanner error={{ code: 'no-microphone', message: '' }} />
19
+ <ErrorBanner error={{ code: 'network', message: '' }} />
20
+ <ErrorBanner error={{ code: 'no-speech', message: '' }} />
21
+ <ErrorBanner error={{ code: 'language', message: '' }} />
22
+ <ErrorBanner error={{ code: 'engine', message: 'Engine X said no.' }} />
23
+ </div>
24
+ </Frame>
25
+ );
@@ -0,0 +1,90 @@
1
+ import { useMemo } from 'react';
2
+
3
+ import { defineStory } from '@djangocfg/playground';
4
+
5
+ import { Composer } from '../../Chat/components/Composer';
6
+ import { EmptyState } from '../../Chat/components/EmptyState';
7
+ import { MessageList } from '../../Chat/components/MessageList';
8
+ import { ChatProvider, useChatContext } from '../../Chat/context';
9
+ import { useChatComposer } from '../../Chat/hooks/useChatComposer';
10
+ import { ChatHeader } from '../../Chat/launcher/ChatHeader';
11
+ import { ChatHeaderLanguageButton } from '../../Chat/launcher/ChatHeaderLanguageButton';
12
+ import { SEED_BASIC, makeBasicTransport } from '../../Chat/stories/shared';
13
+ import { Frame as ChatFrame } from '../../Chat/stories/shared/Frame';
14
+ import { VoiceComposerSlot } from '../widgets/VoiceComposerSlot';
15
+ import type { RecognitionEngine } from '../types';
16
+ import { createMockEngine } from './shared';
17
+
18
+ export default defineStory({
19
+ title: 'Tools/Chat/Voice composer',
20
+ component: VoiceComposerSlot,
21
+ description:
22
+ 'Live dictation slot wired into the Chat composer. `InsideComposer` uses the real browser Web Speech API + a kebab settings menu in the header for picking the speech-recognition language (persists in `useSpeechPrefs`). `MockedDemo` uses a deterministic mock engine so screenshots / CI never prompt for microphone access. The slot auto-hides on Firefox / in-app browsers / missing getUserMedia.',
23
+ });
24
+
25
+ function VoiceChat({ engine }: { engine?: RecognitionEngine }) {
26
+ const chat = useChatContext();
27
+ const composer = useChatComposer({
28
+ onSubmit: (content, attachments) => chat.sendMessage(content, attachments),
29
+ disabled: chat.isStreaming,
30
+ });
31
+
32
+ return (
33
+ <div className="flex h-full min-h-0 flex-col">
34
+ <ChatHeader
35
+ title="Assistant"
36
+ actions={<ChatHeaderLanguageButton />}
37
+ />
38
+ <MessageList
39
+ renderEmpty={() => (
40
+ <EmptyState greeting="Open the settings menu (⋯) to pick the speech language, then press the mic." />
41
+ )}
42
+ />
43
+ <Composer
44
+ composer={composer}
45
+ placeholder="Type or press the mic…"
46
+ toolbarEnd={
47
+ <VoiceComposerSlot
48
+ value={composer.value}
49
+ onChange={composer.setValue}
50
+ engine={engine}
51
+ />
52
+ }
53
+ />
54
+ </div>
55
+ );
56
+ }
57
+
58
+ /**
59
+ * Real browser Web Speech API + kebab settings menu with the language
60
+ * picker. The choice persists in `useSpeechPrefs` (localStorage), so it
61
+ * survives reloads. Defaults are resolved in order: prop → prefs → i18n
62
+ * → `navigator.language` → `en-US`.
63
+ */
64
+ export const InsideComposer = () => {
65
+ const transport = useMemo(() => makeBasicTransport(SEED_BASIC), []);
66
+ return (
67
+ <ChatFrame>
68
+ <ChatProvider transport={transport}>
69
+ <VoiceChat />
70
+ </ChatProvider>
71
+ </ChatFrame>
72
+ );
73
+ };
74
+
75
+ /**
76
+ * Deterministic mock engine — types a scripted phrase into the composer
77
+ * without touching the microphone. Useful for screenshots, CI, and
78
+ * environments where granting mic permission is not desired.
79
+ */
80
+ export const MockedDemo = () => {
81
+ const transport = useMemo(() => makeBasicTransport(SEED_BASIC), []);
82
+ const engine = useMemo(() => createMockEngine({ partialIntervalMs: 90 }), []);
83
+ return (
84
+ <ChatFrame>
85
+ <ChatProvider transport={transport}>
86
+ <VoiceChat engine={engine} />
87
+ </ChatProvider>
88
+ </ChatFrame>
89
+ );
90
+ };
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Deterministic mock engine — emits scripted partials/finals on a timer
3
+ * so stories render the same thing every load and don't beg the browser
4
+ * for microphone permission.
5
+ */
6
+
7
+ import { createEngineBus } from '../core/engine';
8
+ import { newSegmentId } from '../core/ids';
9
+ import type {
10
+ EngineStartOptions,
11
+ RecognitionEngine,
12
+ Unsub,
13
+ } from '../types';
14
+
15
+ export interface MockEngineOptions {
16
+ /** Final phrases the mock will emit, one after another. */
17
+ phrases?: string[];
18
+ /** ms between partial chunks. Default 120. */
19
+ partialIntervalMs?: number;
20
+ /** ms between final segments. Default 1500. */
21
+ finalIntervalMs?: number;
22
+ /** Override id (default `'mock'`). */
23
+ id?: string;
24
+ }
25
+
26
+ const DEFAULT_PHRASES = [
27
+ 'Hello and welcome to the dictation demo.',
28
+ 'This text is being typed by the mock engine.',
29
+ 'Try the buttons on the right to control the session.',
30
+ ];
31
+
32
+ export function createMockEngine(
33
+ opts: MockEngineOptions = {},
34
+ ): RecognitionEngine {
35
+ const bus = createEngineBus();
36
+ const phrases = opts.phrases ?? DEFAULT_PHRASES;
37
+ const partialMs = opts.partialIntervalMs ?? 120;
38
+ const finalMs = opts.finalIntervalMs ?? 1500;
39
+
40
+ let timers: ReturnType<typeof setTimeout>[] = [];
41
+ let running = false;
42
+
43
+ function clearTimers(): void {
44
+ timers.forEach(clearTimeout);
45
+ timers = [];
46
+ }
47
+
48
+ function run(): void {
49
+ let phraseIdx = 0;
50
+ let charIdx = 0;
51
+ let segmentId = newSegmentId();
52
+ let baseTime = 0;
53
+
54
+ const tick = (): void => {
55
+ if (!running) return;
56
+ const phrase = phrases[phraseIdx % phrases.length];
57
+ charIdx = Math.min(charIdx + 2, phrase.length);
58
+ bus.emit('partial', phrase.slice(0, charIdx), segmentId);
59
+ if (charIdx >= phrase.length) {
60
+ const tFinal = setTimeout(() => {
61
+ if (!running) return;
62
+ bus.emit('final', phrase, segmentId);
63
+ phraseIdx += 1;
64
+ charIdx = 0;
65
+ segmentId = newSegmentId();
66
+ baseTime += finalMs;
67
+ const tNext = setTimeout(tick, partialMs);
68
+ timers.push(tNext);
69
+ }, finalMs - partialMs);
70
+ timers.push(tFinal);
71
+ return;
72
+ }
73
+ const tNext = setTimeout(tick, partialMs);
74
+ timers.push(tNext);
75
+ };
76
+ tick();
77
+ }
78
+
79
+ return {
80
+ id: opts.id ?? 'mock',
81
+ isSupported: true,
82
+ on(event, cb): Unsub {
83
+ return bus.on(event, cb);
84
+ },
85
+ async start(_opts: EngineStartOptions): Promise<void> {
86
+ if (running) return;
87
+ running = true;
88
+ bus.emit('state', 'listening');
89
+ run();
90
+ },
91
+ async stop(): Promise<void> {
92
+ running = false;
93
+ clearTimers();
94
+ bus.emit('state', 'closed');
95
+ },
96
+ abort(): void {
97
+ running = false;
98
+ clearTimers();
99
+ bus.emit('state', 'closed');
100
+ },
101
+ };
102
+ }
103
+
104
+ import type { ReactNode } from 'react';
105
+
106
+ export function Frame({
107
+ children,
108
+ w = 480,
109
+ h = 'auto' as number | string,
110
+ }: {
111
+ children: ReactNode;
112
+ w?: number | string;
113
+ h?: number | string;
114
+ }) {
115
+ return (
116
+ <div
117
+ className="rounded-lg border border-border bg-background p-4 shadow-sm"
118
+ style={{ width: w, height: h }}
119
+ >
120
+ {children}
121
+ </div>
122
+ );
123
+ }