@djangocfg/ui-tools 2.1.382 → 2.1.384
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.
- package/dist/ChatRoot-JVR3M3H2.mjs +5 -0
- package/dist/{ChatRoot-6IZFM5HM.mjs.map → ChatRoot-JVR3M3H2.mjs.map} +1 -1
- package/dist/ChatRoot-LXIUBOXF.cjs +14 -0
- package/dist/{ChatRoot-LW4XNIKP.cjs.map → ChatRoot-LXIUBOXF.cjs.map} +1 -1
- package/dist/DictationField-U25MEYAL.mjs +4 -0
- package/dist/{DictationField-2ZLQWLYV.mjs.map → DictationField-U25MEYAL.mjs.map} +1 -1
- package/dist/DictationField-XWR5VOID.cjs +13 -0
- package/dist/{DictationField-IPPJ54CU.cjs.map → DictationField-XWR5VOID.cjs.map} +1 -1
- package/dist/{chunk-KMSBGNVC.cjs → chunk-4PFW7MIJ.cjs} +4 -2
- package/dist/chunk-4PFW7MIJ.cjs.map +1 -0
- package/dist/{chunk-4LXG3NBV.mjs → chunk-C2YN6WEO.mjs} +3 -3
- package/dist/chunk-C2YN6WEO.mjs.map +1 -0
- package/dist/{chunk-OZAU3QWD.cjs → chunk-HPK3EWBF.cjs} +8 -8
- package/dist/chunk-HPK3EWBF.cjs.map +1 -0
- package/dist/{chunk-UWVP6LCW.mjs → chunk-PEKBT75W.mjs} +8 -8
- package/dist/chunk-PEKBT75W.mjs.map +1 -0
- package/dist/index.cjs +192 -55
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +78 -6
- package/dist/index.d.ts +78 -6
- package/dist/index.mjs +143 -8
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -13
- package/src/tools/Chat/core/audio/defaults.ts +16 -11
- package/src/tools/Chat/core/audio/sounds/error.ts +3 -0
- package/src/tools/Chat/core/audio/sounds/mention.ts +3 -0
- package/src/tools/Chat/core/audio/sounds/notification.ts +3 -0
- package/src/tools/Chat/core/audio/sounds/received.ts +3 -0
- package/src/tools/Chat/core/audio/sounds/sent.ts +3 -0
- package/src/tools/Chat/core/audio/sounds/start.ts +3 -0
- package/src/tools/Chat/index.ts +15 -0
- package/src/tools/SpeechRecognition/core/audio/defaults.ts +4 -4
- package/dist/ChatRoot-6IZFM5HM.mjs +0 -5
- package/dist/ChatRoot-LW4XNIKP.cjs +0 -14
- package/dist/DictationField-2ZLQWLYV.mjs +0 -4
- package/dist/DictationField-IPPJ54CU.cjs +0 -13
- package/dist/chunk-4LXG3NBV.mjs.map +0 -1
- package/dist/chunk-KMSBGNVC.cjs.map +0 -1
- package/dist/chunk-OZAU3QWD.cjs.map +0 -1
- package/dist/chunk-UWVP6LCW.mjs.map +0 -1
- package/src/audio-assets.d.ts +0 -8
- package/src/components/markdown/MarkdownMessage/MarkdownMessage.story.tsx +0 -771
- package/src/stories/index.ts +0 -63
- package/src/tools/AudioPlayer/AudioPlayer.story.tsx +0 -481
- package/src/tools/Chat/core/audio/sounds/error.mp3 +0 -0
- package/src/tools/Chat/core/audio/sounds/mention.mp3 +0 -0
- package/src/tools/Chat/core/audio/sounds/notification.mp3 +0 -0
- package/src/tools/Chat/core/audio/sounds/received.mp3 +0 -0
- package/src/tools/Chat/core/audio/sounds/sent.mp3 +0 -0
- package/src/tools/Chat/core/audio/sounds/start.mp3 +0 -0
- package/src/tools/Chat/stories/01-basic.story.tsx +0 -64
- package/src/tools/Chat/stories/02-bubbles.story.tsx +0 -21
- package/src/tools/Chat/stories/03-tool-calls.story.tsx +0 -59
- package/src/tools/Chat/stories/04-personas.story.tsx +0 -78
- package/src/tools/Chat/stories/05-launcher.story.tsx +0 -321
- package/src/tools/Chat/stories/06-header.story.tsx +0 -147
- package/src/tools/Chat/stories/07-audio-actions.story.tsx +0 -112
- package/src/tools/Chat/stories/shared/Frame.tsx +0 -21
- package/src/tools/Chat/stories/shared/index.ts +0 -5
- package/src/tools/Chat/stories/shared/messages.ts +0 -39
- package/src/tools/Chat/stories/shared/personas.ts +0 -13
- package/src/tools/Chat/stories/shared/seeds.ts +0 -92
- package/src/tools/Chat/stories/shared/transports.ts +0 -36
- package/src/tools/CodeEditor/CodeEditor.story.tsx +0 -202
- package/src/tools/CronScheduler/CronScheduler.story.tsx +0 -300
- package/src/tools/Gallery/Gallery.story.tsx +0 -237
- package/src/tools/ImageViewer/ImageViewer.story.tsx +0 -85
- package/src/tools/JsonForm/JsonForm.story.tsx +0 -350
- package/src/tools/JsonTree/JsonTree.story.tsx +0 -141
- package/src/tools/LottiePlayer/LottiePlayer.story.tsx +0 -95
- package/src/tools/Map/Map.story.tsx +0 -458
- package/src/tools/MarkdownEditor/MarkdownEditor.story.tsx +0 -225
- package/src/tools/Mermaid/Mermaid.story.tsx +0 -251
- package/src/tools/OpenapiViewer/OpenapiViewer.story.tsx +0 -230
- package/src/tools/PrettyCode/PrettyCode.story.tsx +0 -304
- package/src/tools/SpeechRecognition/stories/01-basic.story.tsx +0 -32
- package/src/tools/SpeechRecognition/stories/02-dictation-field.story.tsx +0 -32
- package/src/tools/SpeechRecognition/stories/03-push-to-talk.story.tsx +0 -27
- package/src/tools/SpeechRecognition/stories/04-mic-meter.story.tsx +0 -35
- package/src/tools/SpeechRecognition/stories/05-custom-engine-http.story.tsx +0 -40
- package/src/tools/SpeechRecognition/stories/06-custom-engine-ws.story.tsx +0 -48
- package/src/tools/SpeechRecognition/stories/07-language-device.story.tsx +0 -57
- package/src/tools/SpeechRecognition/stories/08-errors-permissions.story.tsx +0 -25
- package/src/tools/SpeechRecognition/stories/09-chat-voice.story.tsx +0 -90
- package/src/tools/SpeechRecognition/stories/shared.tsx +0 -123
- package/src/tools/Tour/Tour.story.tsx +0 -279
- package/src/tools/Tree/Tree.story.tsx +0 -620
- package/src/tools/Uploader/Uploader.story.tsx +0 -415
- 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="card" (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="plain"</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
|
-
);
|