@djangocfg/ui-tools 2.1.382 → 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 (62) hide show
  1. package/dist/DictationField-U25MEYAL.mjs +4 -0
  2. package/dist/{DictationField-2ZLQWLYV.mjs.map → DictationField-U25MEYAL.mjs.map} +1 -1
  3. package/dist/DictationField-XWR5VOID.cjs +13 -0
  4. package/dist/{DictationField-IPPJ54CU.cjs.map → DictationField-XWR5VOID.cjs.map} +1 -1
  5. package/dist/{chunk-KMSBGNVC.cjs → chunk-4PFW7MIJ.cjs} +4 -2
  6. package/dist/chunk-4PFW7MIJ.cjs.map +1 -0
  7. package/dist/{chunk-4LXG3NBV.mjs → chunk-C2YN6WEO.mjs} +3 -3
  8. package/dist/chunk-C2YN6WEO.mjs.map +1 -0
  9. package/dist/index.cjs +139 -2
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.cts +68 -1
  12. package/dist/index.d.ts +68 -1
  13. package/dist/index.mjs +141 -6
  14. package/dist/index.mjs.map +1 -1
  15. package/package.json +6 -13
  16. package/src/tools/Chat/index.ts +15 -0
  17. package/dist/DictationField-2ZLQWLYV.mjs +0 -4
  18. package/dist/DictationField-IPPJ54CU.cjs +0 -13
  19. package/dist/chunk-4LXG3NBV.mjs.map +0 -1
  20. package/dist/chunk-KMSBGNVC.cjs.map +0 -1
  21. package/src/components/markdown/MarkdownMessage/MarkdownMessage.story.tsx +0 -771
  22. package/src/stories/index.ts +0 -63
  23. package/src/tools/AudioPlayer/AudioPlayer.story.tsx +0 -481
  24. package/src/tools/Chat/stories/01-basic.story.tsx +0 -64
  25. package/src/tools/Chat/stories/02-bubbles.story.tsx +0 -21
  26. package/src/tools/Chat/stories/03-tool-calls.story.tsx +0 -59
  27. package/src/tools/Chat/stories/04-personas.story.tsx +0 -78
  28. package/src/tools/Chat/stories/05-launcher.story.tsx +0 -321
  29. package/src/tools/Chat/stories/06-header.story.tsx +0 -147
  30. package/src/tools/Chat/stories/07-audio-actions.story.tsx +0 -112
  31. package/src/tools/Chat/stories/shared/Frame.tsx +0 -21
  32. package/src/tools/Chat/stories/shared/index.ts +0 -5
  33. package/src/tools/Chat/stories/shared/messages.ts +0 -39
  34. package/src/tools/Chat/stories/shared/personas.ts +0 -13
  35. package/src/tools/Chat/stories/shared/seeds.ts +0 -92
  36. package/src/tools/Chat/stories/shared/transports.ts +0 -36
  37. package/src/tools/CodeEditor/CodeEditor.story.tsx +0 -202
  38. package/src/tools/CronScheduler/CronScheduler.story.tsx +0 -300
  39. package/src/tools/Gallery/Gallery.story.tsx +0 -237
  40. package/src/tools/ImageViewer/ImageViewer.story.tsx +0 -85
  41. package/src/tools/JsonForm/JsonForm.story.tsx +0 -350
  42. package/src/tools/JsonTree/JsonTree.story.tsx +0 -141
  43. package/src/tools/LottiePlayer/LottiePlayer.story.tsx +0 -95
  44. package/src/tools/Map/Map.story.tsx +0 -458
  45. package/src/tools/MarkdownEditor/MarkdownEditor.story.tsx +0 -225
  46. package/src/tools/Mermaid/Mermaid.story.tsx +0 -251
  47. package/src/tools/OpenapiViewer/OpenapiViewer.story.tsx +0 -230
  48. package/src/tools/PrettyCode/PrettyCode.story.tsx +0 -304
  49. package/src/tools/SpeechRecognition/stories/01-basic.story.tsx +0 -32
  50. package/src/tools/SpeechRecognition/stories/02-dictation-field.story.tsx +0 -32
  51. package/src/tools/SpeechRecognition/stories/03-push-to-talk.story.tsx +0 -27
  52. package/src/tools/SpeechRecognition/stories/04-mic-meter.story.tsx +0 -35
  53. package/src/tools/SpeechRecognition/stories/05-custom-engine-http.story.tsx +0 -40
  54. package/src/tools/SpeechRecognition/stories/06-custom-engine-ws.story.tsx +0 -48
  55. package/src/tools/SpeechRecognition/stories/07-language-device.story.tsx +0 -57
  56. package/src/tools/SpeechRecognition/stories/08-errors-permissions.story.tsx +0 -25
  57. package/src/tools/SpeechRecognition/stories/09-chat-voice.story.tsx +0 -90
  58. package/src/tools/SpeechRecognition/stories/shared.tsx +0 -123
  59. package/src/tools/Tour/Tour.story.tsx +0 -279
  60. package/src/tools/Tree/Tree.story.tsx +0 -620
  61. package/src/tools/Uploader/Uploader.story.tsx +0 -415
  62. package/src/tools/VideoPlayer/VideoPlayer.story.tsx +0 -87
@@ -1,304 +0,0 @@
1
- import { defineStory, useSelect, useBoolean } from '@djangocfg/playground';
2
- import PrettyCode from './index';
3
-
4
- export default defineStory({
5
- title: 'Tools/Pretty Code',
6
- component: PrettyCode,
7
- description: 'Syntax highlighted code block with copy button.',
8
- });
9
-
10
- // ─── Samples ──────────────────────────────────────────────────────────────────
11
-
12
- const LONG_CODE_SAMPLES = {
13
- typescript: `import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
14
-
15
- export type Status = 'idle' | 'loading' | 'success' | 'error';
16
-
17
- export interface PaginationParams {
18
- page: number;
19
- pageSize: number;
20
- sortBy?: string;
21
- sortOrder?: 'asc' | 'desc';
22
- }
23
-
24
- export function useVehicles(initialParams?: Partial<PaginationParams>) {
25
- const [status, setStatus] = useState<Status>('idle');
26
- const [data, setData] = useState<unknown[]>([]);
27
- const [error, setError] = useState<Error | null>(null);
28
- const abortRef = useRef<AbortController | null>(null);
29
-
30
- const [params, setParams] = useState<PaginationParams>({
31
- page: 1,
32
- pageSize: 20,
33
- ...initialParams,
34
- });
35
-
36
- const fetch = useCallback(async (p: PaginationParams) => {
37
- abortRef.current?.abort();
38
- abortRef.current = new AbortController();
39
- setStatus('loading');
40
- try {
41
- const query = new URLSearchParams({
42
- page: String(p.page),
43
- pageSize: String(p.pageSize),
44
- });
45
- const res = await window.fetch(\`/api/vehicles?\${query}\`, {
46
- signal: abortRef.current.signal,
47
- });
48
- if (!res.ok) throw new Error(\`HTTP \${res.status}\`);
49
- const json = await res.json();
50
- setData(json.data);
51
- setStatus('success');
52
- } catch (err) {
53
- if ((err as Error).name === 'AbortError') return;
54
- setError(err as Error);
55
- setStatus('error');
56
- }
57
- }, []);
58
-
59
- useEffect(() => {
60
- fetch(params);
61
- return () => abortRef.current?.abort();
62
- }, [params, fetch]);
63
-
64
- return { status, data, error, params, setParams };
65
- }`,
66
-
67
- python: `from __future__ import annotations
68
-
69
- import asyncio
70
- import logging
71
- from dataclasses import dataclass, field
72
- from datetime import datetime
73
- from enum import StrEnum
74
-
75
- import httpx
76
- from pydantic import BaseModel, ConfigDict, Field, field_validator
77
-
78
- logger = logging.getLogger(__name__)
79
-
80
-
81
- class FuelType(StrEnum):
82
- GASOLINE = "gasoline"
83
- DIESEL = "diesel"
84
- ELECTRIC = "electric"
85
-
86
-
87
- class Vehicle(BaseModel):
88
- model_config = ConfigDict(from_attributes=True)
89
-
90
- id: str
91
- make: str
92
- model: str
93
- year: int
94
- fuel_type: FuelType
95
- created_at: datetime = Field(default_factory=datetime.utcnow)
96
-
97
- @field_validator("year")
98
- @classmethod
99
- def validate_year(cls, v: int) -> int:
100
- if not (1900 <= v <= datetime.utcnow().year + 1):
101
- raise ValueError(f"Invalid year: {v}")
102
- return v
103
-
104
-
105
- @dataclass
106
- class VehicleRepository:
107
- base_url: str
108
-
109
- async def list(self, page: int = 1, page_size: int = 20) -> list[Vehicle]:
110
- async with httpx.AsyncClient(base_url=self.base_url) as client:
111
- response = await client.get(
112
- "/vehicles",
113
- params={"page": page, "page_size": page_size},
114
- )
115
- response.raise_for_status()
116
- payload = response.json()
117
- return [Vehicle.model_validate(v) for v in payload["data"]]`,
118
-
119
- javascript: `class EventBus {
120
- #listeners = new Map();
121
- #wildcards = new Set();
122
-
123
- on(event, handler) {
124
- if (event === '*') {
125
- this.#wildcards.add(handler);
126
- return () => this.#wildcards.delete(handler);
127
- }
128
- if (!this.#listeners.has(event)) {
129
- this.#listeners.set(event, new Set());
130
- }
131
- this.#listeners.get(event).add(handler);
132
- return () => this.#listeners.get(event)?.delete(handler);
133
- }
134
-
135
- once(event, handler) {
136
- const unsub = this.on(event, (...args) => {
137
- unsub();
138
- handler(...args);
139
- });
140
- return unsub;
141
- }
142
-
143
- emit(event, payload) {
144
- this.#listeners.get(event)?.forEach(h => h(payload));
145
- this.#wildcards.forEach(h => h(event, payload));
146
- }
147
- }
148
-
149
- const bus = new EventBus();
150
- bus.on('*', (event, payload) => console.debug(\`[bus] \${event}\`, payload));
151
- bus.emit('vehicle:created', { id: 'v_001', make: 'BMW' });`,
152
-
153
- css: `:root {
154
- --radius-md: 0.5rem;
155
- --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
156
- --transition-base: 200ms cubic-bezier(0.4, 0, 0.2, 1);
157
- }
158
-
159
- .card {
160
- border-radius: var(--radius-md);
161
- border: 1px solid hsl(var(--border));
162
- background: hsl(var(--card));
163
- box-shadow: var(--shadow-sm);
164
- transition: box-shadow var(--transition-base);
165
-
166
- &:hover {
167
- box-shadow: var(--shadow-md);
168
- transform: translateY(-1px);
169
- }
170
- }
171
-
172
- @keyframes shimmer {
173
- from { background-position: -200% 0; }
174
- to { background-position: 200% 0; }
175
- }
176
-
177
- .card--skeleton .card-title {
178
- background: linear-gradient(90deg, hsl(var(--muted)) 25%, hsl(var(--muted-foreground) / 0.1) 50%, hsl(var(--muted)) 75%);
179
- background-size: 200% 100%;
180
- animation: shimmer 1.5s infinite;
181
- }`,
182
-
183
- html: `<!DOCTYPE html>
184
- <html lang="en">
185
- <head>
186
- <meta charset="UTF-8" />
187
- <title>Sample</title>
188
- </head>
189
- <body>
190
- <div id="root"></div>
191
- <script type="module" src="/entry.tsx"></script>
192
- </body>
193
- </html>`,
194
- };
195
-
196
- // ─── Stories ──────────────────────────────────────────────────────────────────
197
-
198
- /** Interactive — language, variant, compact, scrollIsolation all toggleable.
199
- * Replaces the old per-language / per-mode / per-flag stories which were
200
- * just this component with different defaults. */
201
- export const Interactive = () => {
202
- const [language] = useSelect('language', {
203
- options: ['typescript', 'javascript', 'python', 'css', 'html'] as const,
204
- defaultValue: 'typescript',
205
- label: 'Language',
206
- });
207
-
208
- const [variant] = useSelect('variant', {
209
- options: ['card', 'plain'] as const,
210
- defaultValue: 'card',
211
- label: 'Variant',
212
- description: 'card = full chrome, plain = bare highlighted code',
213
- });
214
-
215
- const [isCompact] = useBoolean('isCompact', {
216
- defaultValue: false,
217
- label: 'Compact',
218
- });
219
-
220
- const [scrollIsolation] = useBoolean('scrollIsolation', {
221
- defaultValue: true,
222
- label: 'Scroll isolation',
223
- description: 'Require click before the block captures scroll (card only)',
224
- });
225
-
226
- return (
227
- <PrettyCode
228
- data={LONG_CODE_SAMPLES[language]}
229
- language={language}
230
- variant={variant}
231
- isCompact={isCompact}
232
- scrollIsolation={scrollIsolation}
233
- />
234
- );
235
- };
236
-
237
- /** Variants — ``card`` vs ``plain`` side by side. The card has its own
238
- * border, hover toolbar, and can internally scroll when a ``maxLines``
239
- * cap is hit. The plain variant is chrome-less for embedding inside
240
- * an existing scroll container (e.g. a response panel), where the
241
- * parent surface owns chrome and scroll. */
242
- export const Variants = () => (
243
- <div className="grid grid-cols-1 lg:grid-cols-2 gap-4 max-w-6xl">
244
- <section className="space-y-2">
245
- <h3 className="text-sm font-semibold">variant=&quot;card&quot; (default)</h3>
246
- <p className="text-xs text-muted-foreground">
247
- Border, background, hover toolbar with Copy + language tag.
248
- </p>
249
- <PrettyCode data={LONG_CODE_SAMPLES.typescript} language="typescript" variant="card" />
250
- </section>
251
- <section className="space-y-2">
252
- <h3 className="text-sm font-semibold">variant=&quot;plain&quot;</h3>
253
- <p className="text-xs text-muted-foreground">
254
- Chrome-less. No border, no background, no toolbar. Flows into the
255
- parent scroll container.
256
- </p>
257
- <PrettyCode data={LONG_CODE_SAMPLES.typescript} language="typescript" variant="plain" />
258
- </section>
259
- </div>
260
- );
261
-
262
- /** LongCode — stress test for multi-column layouts and long snippets. */
263
- export const LongCode = () => (
264
- <div className="flex flex-col gap-6 max-w-4xl">
265
- <div className="h-96">
266
- <PrettyCode data={LONG_CODE_SAMPLES.typescript} language="typescript" maxLines={30} />
267
- </div>
268
- <div className="h-96">
269
- <PrettyCode data={LONG_CODE_SAMPLES.python} language="python" maxLines={30} />
270
- </div>
271
- <div className="h-96">
272
- <PrettyCode data={LONG_CODE_SAMPLES.javascript} language="javascript" maxLines={30} />
273
- </div>
274
- </div>
275
- );
276
-
277
- /** ScrollIsolation — both states side-by-side so the difference is
278
- * obvious without toggling. The isolation flag matters most when the
279
- * block is embedded in a scrolling page: without it, wheel-scrolling
280
- * anywhere over the block "captures" the wheel and the page itself
281
- * stops scrolling — click-to-unlock avoids that surprise. */
282
- export const ScrollIsolation = () => (
283
- <div className="flex flex-col gap-8 max-w-4xl">
284
- <section className="space-y-2">
285
- <h3 className="text-sm font-semibold">scrollIsolation=true (default)</h3>
286
- <p className="text-xs text-muted-foreground">
287
- Hover to see the hint; click to unlock wheel-scroll inside the block.
288
- </p>
289
- <div className="h-64">
290
- <PrettyCode data={LONG_CODE_SAMPLES.typescript} language="typescript" maxLines={15} scrollIsolation />
291
- </div>
292
- </section>
293
- <section className="space-y-2">
294
- <h3 className="text-sm font-semibold">scrollIsolation=false</h3>
295
- <p className="text-xs text-muted-foreground">
296
- Wheel-scroll captured immediately — use when the block has no
297
- surrounding page-level scroll to compete with.
298
- </p>
299
- <div className="h-64">
300
- <PrettyCode data={LONG_CODE_SAMPLES.typescript} language="typescript" maxLines={15} scrollIsolation={false} />
301
- </div>
302
- </section>
303
- </div>
304
- );
@@ -1,32 +0,0 @@
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
- };
@@ -1,32 +0,0 @@
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
- };
@@ -1,27 +0,0 @@
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
- };
@@ -1,35 +0,0 @@
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
- };
@@ -1,40 +0,0 @@
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
- );
@@ -1,48 +0,0 @@
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
- );
@@ -1,57 +0,0 @@
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
- };
@@ -1,25 +0,0 @@
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
- );