@cubos/agent-sdk-react-dom 0.0.1136563
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/README.md +265 -0
- package/dist/agent-markdown.d.ts +53 -0
- package/dist/colors.d.ts +24 -0
- package/dist/currency.d.ts +9 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +678 -0
- package/dist/index.js.map +16 -0
- package/dist/recording-wave.d.ts +26 -0
- package/dist/styles.css +496 -0
- package/dist/use-recorder.d.ts +56 -0
- package/dist/use-transcript-anchor.d.ts +67 -0
- package/dist/voice-message.d.ts +53 -0
- package/package.json +59 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
// src/agent-markdown.tsx
|
|
2
|
+
import { memo, useMemo } from "react";
|
|
3
|
+
import Markdown from "react-markdown";
|
|
4
|
+
import rehypeHighlight from "rehype-highlight";
|
|
5
|
+
import rehypeKatex from "rehype-katex";
|
|
6
|
+
import remarkGfm from "remark-gfm";
|
|
7
|
+
import remarkMath from "remark-math";
|
|
8
|
+
|
|
9
|
+
// src/colors.ts
|
|
10
|
+
function colorStyle(colors) {
|
|
11
|
+
if (!colors)
|
|
12
|
+
return;
|
|
13
|
+
const style = {};
|
|
14
|
+
for (const [token, value] of Object.entries(colors)) {
|
|
15
|
+
if (value)
|
|
16
|
+
style[`--cubos-agent-sdk-react-dom-${token}`] = value;
|
|
17
|
+
}
|
|
18
|
+
return style;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/currency.ts
|
|
22
|
+
var CURRENCY_PREFIX = /(?:R|US|CA|AU|NZ|HK|NT|SG|Cz|C|A|S|B|J|Z|Mex|Arg|CLP|COP)$/;
|
|
23
|
+
var AMOUNT_AFTER = /^[ ]{0,1}\d/;
|
|
24
|
+
function escapeCurrency(markdown) {
|
|
25
|
+
let out = "";
|
|
26
|
+
let i = 0;
|
|
27
|
+
while (i < markdown.length) {
|
|
28
|
+
const char = markdown[i];
|
|
29
|
+
if (char === "`" && markdown.startsWith("```", i)) {
|
|
30
|
+
const end = markdown.indexOf("```", i + 3);
|
|
31
|
+
const stop = end === -1 ? markdown.length : end + 3;
|
|
32
|
+
out += markdown.slice(i, stop);
|
|
33
|
+
i = stop;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (char === "`") {
|
|
37
|
+
let ticks = 0;
|
|
38
|
+
while (markdown[i + ticks] === "`")
|
|
39
|
+
ticks += 1;
|
|
40
|
+
const fence = "`".repeat(ticks);
|
|
41
|
+
const end = markdown.indexOf(fence, i + ticks);
|
|
42
|
+
const stop = end === -1 ? markdown.length : end + ticks;
|
|
43
|
+
out += markdown.slice(i, stop);
|
|
44
|
+
i = stop;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (char === "\\" && i + 1 < markdown.length) {
|
|
48
|
+
out += markdown.slice(i, i + 2);
|
|
49
|
+
i += 2;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (char === "$") {
|
|
53
|
+
if (markdown.startsWith("$$", i)) {
|
|
54
|
+
const end = markdown.indexOf("$$", i + 2);
|
|
55
|
+
const stop = end === -1 ? markdown.length : end + 2;
|
|
56
|
+
out += markdown.slice(i, stop);
|
|
57
|
+
i = stop;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (isCurrency(markdown, i, out)) {
|
|
61
|
+
out += "\\$";
|
|
62
|
+
i += 1;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
out += char;
|
|
67
|
+
i += 1;
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
function isCurrency(text, index, emitted) {
|
|
72
|
+
if (CURRENCY_PREFIX.test(emitted))
|
|
73
|
+
return true;
|
|
74
|
+
return AMOUNT_AFTER.test(text.slice(index + 1, index + 3));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// src/agent-markdown.tsx
|
|
78
|
+
import"./styles.css";
|
|
79
|
+
import { jsx } from "react/jsx-runtime";
|
|
80
|
+
var ROOT_CLASS = "cubos-agent-sdk-react-dom cubos-agent-sdk-react-dom--markdown";
|
|
81
|
+
var DEFAULT_COMPONENTS = {
|
|
82
|
+
a: ({ node: _node, ...props }) => /* @__PURE__ */ jsx("a", {
|
|
83
|
+
...props,
|
|
84
|
+
target: "_blank",
|
|
85
|
+
rel: "noopener noreferrer"
|
|
86
|
+
}),
|
|
87
|
+
table: ({ node: _node, ...props }) => /* @__PURE__ */ jsx("div", {
|
|
88
|
+
className: "cubos-agent-sdk-react-dom__table",
|
|
89
|
+
children: /* @__PURE__ */ jsx("table", {
|
|
90
|
+
...props
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
};
|
|
94
|
+
var AgentMarkdown = memo(function AgentMarkdown2({
|
|
95
|
+
children,
|
|
96
|
+
gfm = true,
|
|
97
|
+
math = true,
|
|
98
|
+
highlight = true,
|
|
99
|
+
components,
|
|
100
|
+
colors,
|
|
101
|
+
className
|
|
102
|
+
}) {
|
|
103
|
+
const remarkPlugins = useMemo(() => [...gfm ? [remarkGfm] : [], ...math ? [remarkMath] : []], [gfm, math]);
|
|
104
|
+
const rehypePlugins = useMemo(() => [
|
|
105
|
+
...math ? [rehypeKatex] : [],
|
|
106
|
+
...highlight ? [[rehypeHighlight, { detect: true, ignoreMissing: true }]] : []
|
|
107
|
+
], [math, highlight]);
|
|
108
|
+
const merged = useMemo(() => ({ ...DEFAULT_COMPONENTS, ...components }), [components]);
|
|
109
|
+
const source = useMemo(() => math ? escapeCurrency(children) : children, [children, math]);
|
|
110
|
+
return /* @__PURE__ */ jsx("div", {
|
|
111
|
+
className: ROOT_CLASS + (className ? ` ${className}` : ""),
|
|
112
|
+
style: colorStyle(colors),
|
|
113
|
+
children: /* @__PURE__ */ jsx(Markdown, {
|
|
114
|
+
remarkPlugins,
|
|
115
|
+
rehypePlugins,
|
|
116
|
+
components: merged,
|
|
117
|
+
children: source
|
|
118
|
+
})
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
// src/recording-wave.tsx
|
|
122
|
+
import"./styles.css";
|
|
123
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
124
|
+
function RecordingWave({ levels, silent = false, colors, className }) {
|
|
125
|
+
return /* @__PURE__ */ jsx2("div", {
|
|
126
|
+
className: `cubos-agent-sdk-react-dom cubos-agent-sdk-react-dom__meter${silent ? " cubos-agent-sdk-react-dom__meter--silent" : ""}${className ? ` ${className}` : ""}`,
|
|
127
|
+
style: colorStyle(colors),
|
|
128
|
+
"data-silent": silent ? "true" : undefined,
|
|
129
|
+
"aria-hidden": "true",
|
|
130
|
+
children: levels.map((level, index) => /* @__PURE__ */ jsx2("span", {
|
|
131
|
+
className: "cubos-agent-sdk-react-dom__meter-bar",
|
|
132
|
+
style: { height: `${Math.max(6, Math.round(level * 100))}%` }
|
|
133
|
+
}, index))
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
// src/use-recorder.ts
|
|
137
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
138
|
+
var CANDIDATE_TYPES = [
|
|
139
|
+
"audio/webm;codecs=opus",
|
|
140
|
+
"audio/webm",
|
|
141
|
+
"audio/mp4",
|
|
142
|
+
"audio/ogg;codecs=opus"
|
|
143
|
+
];
|
|
144
|
+
function pickMimeType() {
|
|
145
|
+
if (typeof MediaRecorder === "undefined")
|
|
146
|
+
return;
|
|
147
|
+
return CANDIDATE_TYPES.find((type) => MediaRecorder.isTypeSupported(type));
|
|
148
|
+
}
|
|
149
|
+
var LEVEL_BARS = 40;
|
|
150
|
+
var LEVEL_INTERVAL_MS = 50;
|
|
151
|
+
var LEVEL_GAIN = 5;
|
|
152
|
+
var SILENCE_FLOOR = 0.012;
|
|
153
|
+
var SILENCE_AFTER_MS = 1500;
|
|
154
|
+
function levelOf(samples) {
|
|
155
|
+
if (samples.length === 0)
|
|
156
|
+
return 0;
|
|
157
|
+
let sum = 0;
|
|
158
|
+
for (const sample of samples) {
|
|
159
|
+
const centred = (sample - 128) / 128;
|
|
160
|
+
sum += centred * centred;
|
|
161
|
+
}
|
|
162
|
+
return Math.sqrt(sum / samples.length);
|
|
163
|
+
}
|
|
164
|
+
var NO_LEVELS = Array.from({ length: LEVEL_BARS }, () => 0);
|
|
165
|
+
function isSupported() {
|
|
166
|
+
return typeof MediaRecorder !== "undefined" && typeof navigator !== "undefined" && navigator.mediaDevices?.getUserMedia !== undefined;
|
|
167
|
+
}
|
|
168
|
+
function useRecorder() {
|
|
169
|
+
const [state, setState] = useState("idle");
|
|
170
|
+
const [seconds, setSeconds] = useState(0);
|
|
171
|
+
const [error, setError] = useState(null);
|
|
172
|
+
const [levels, setLevels] = useState(NO_LEVELS);
|
|
173
|
+
const [silent, setSilent] = useState(false);
|
|
174
|
+
const recorderRef = useRef(null);
|
|
175
|
+
const chunksRef = useRef([]);
|
|
176
|
+
const streamRef = useRef(null);
|
|
177
|
+
const analyserRef = useRef(null);
|
|
178
|
+
const audioRef = useRef(null);
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
setState(isSupported() ? "idle" : "unsupported");
|
|
181
|
+
}, []);
|
|
182
|
+
const release = useCallback(() => {
|
|
183
|
+
for (const track of streamRef.current?.getTracks() ?? [])
|
|
184
|
+
track.stop();
|
|
185
|
+
audioRef.current?.close().catch(() => {});
|
|
186
|
+
audioRef.current = null;
|
|
187
|
+
analyserRef.current = null;
|
|
188
|
+
streamRef.current = null;
|
|
189
|
+
recorderRef.current = null;
|
|
190
|
+
chunksRef.current = [];
|
|
191
|
+
setLevels(NO_LEVELS);
|
|
192
|
+
setSilent(false);
|
|
193
|
+
}, []);
|
|
194
|
+
useEffect(() => release, [release]);
|
|
195
|
+
useEffect(() => {
|
|
196
|
+
if (state !== "recording")
|
|
197
|
+
return;
|
|
198
|
+
const started = Date.now();
|
|
199
|
+
setSeconds(0);
|
|
200
|
+
const timer = setInterval(() => setSeconds(Math.floor((Date.now() - started) / 1000)), 500);
|
|
201
|
+
return () => clearInterval(timer);
|
|
202
|
+
}, [state]);
|
|
203
|
+
useEffect(() => {
|
|
204
|
+
const analyser = analyserRef.current;
|
|
205
|
+
if (state !== "recording" || analyser === null)
|
|
206
|
+
return;
|
|
207
|
+
const samples = new Uint8Array(analyser.fftSize);
|
|
208
|
+
let lastSound = Date.now();
|
|
209
|
+
const timer = setInterval(() => {
|
|
210
|
+
analyser.getByteTimeDomainData(samples);
|
|
211
|
+
const level = levelOf(samples);
|
|
212
|
+
if (level > SILENCE_FLOOR)
|
|
213
|
+
lastSound = Date.now();
|
|
214
|
+
setLevels((held) => [...held.slice(1), Math.min(1, level * LEVEL_GAIN)]);
|
|
215
|
+
setSilent(Date.now() - lastSound > SILENCE_AFTER_MS);
|
|
216
|
+
}, LEVEL_INTERVAL_MS);
|
|
217
|
+
return () => clearInterval(timer);
|
|
218
|
+
}, [state]);
|
|
219
|
+
const start = useCallback(async () => {
|
|
220
|
+
if (!isSupported()) {
|
|
221
|
+
setState("unsupported");
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
setError(null);
|
|
225
|
+
setState("requesting");
|
|
226
|
+
try {
|
|
227
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
228
|
+
streamRef.current = stream;
|
|
229
|
+
const mimeType = pickMimeType();
|
|
230
|
+
const recorder = new MediaRecorder(stream, mimeType ? { mimeType } : undefined);
|
|
231
|
+
chunksRef.current = [];
|
|
232
|
+
recorder.ondataavailable = (event) => {
|
|
233
|
+
if (event.data.size > 0)
|
|
234
|
+
chunksRef.current.push(event.data);
|
|
235
|
+
};
|
|
236
|
+
recorderRef.current = recorder;
|
|
237
|
+
try {
|
|
238
|
+
const Ctx = window.AudioContext ?? window.webkitAudioContext;
|
|
239
|
+
if (Ctx) {
|
|
240
|
+
const audio = new Ctx;
|
|
241
|
+
const analyser = audio.createAnalyser();
|
|
242
|
+
analyser.fftSize = 1024;
|
|
243
|
+
analyser.smoothingTimeConstant = 0;
|
|
244
|
+
audio.createMediaStreamSource(stream).connect(analyser);
|
|
245
|
+
audioRef.current = audio;
|
|
246
|
+
analyserRef.current = analyser;
|
|
247
|
+
}
|
|
248
|
+
} catch {
|
|
249
|
+
analyserRef.current = null;
|
|
250
|
+
}
|
|
251
|
+
setLevels(NO_LEVELS);
|
|
252
|
+
setSilent(false);
|
|
253
|
+
recorder.start();
|
|
254
|
+
setState("recording");
|
|
255
|
+
} catch (err) {
|
|
256
|
+
release();
|
|
257
|
+
setState("idle");
|
|
258
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
259
|
+
}
|
|
260
|
+
}, [release]);
|
|
261
|
+
const stop = useCallback(async () => {
|
|
262
|
+
const recorder = recorderRef.current;
|
|
263
|
+
if (!recorder || recorder.state === "inactive") {
|
|
264
|
+
release();
|
|
265
|
+
setState("idle");
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
const clip = await new Promise((resolve) => {
|
|
269
|
+
recorder.onstop = () => {
|
|
270
|
+
const chunks = chunksRef.current;
|
|
271
|
+
resolve(chunks.length ? new Blob(chunks, { type: recorder.mimeType }) : null);
|
|
272
|
+
};
|
|
273
|
+
recorder.stop();
|
|
274
|
+
});
|
|
275
|
+
release();
|
|
276
|
+
setState("idle");
|
|
277
|
+
setSeconds(0);
|
|
278
|
+
return clip;
|
|
279
|
+
}, [release]);
|
|
280
|
+
const cancel = useCallback(() => {
|
|
281
|
+
const recorder = recorderRef.current;
|
|
282
|
+
if (recorder && recorder.state !== "inactive") {
|
|
283
|
+
recorder.onstop = null;
|
|
284
|
+
recorder.stop();
|
|
285
|
+
}
|
|
286
|
+
release();
|
|
287
|
+
setState("idle");
|
|
288
|
+
setSeconds(0);
|
|
289
|
+
}, [release]);
|
|
290
|
+
return { state, seconds, levels, silent, error, start, stop, cancel };
|
|
291
|
+
}
|
|
292
|
+
// src/use-transcript-anchor.ts
|
|
293
|
+
import { useCallback as useCallback2, useEffect as useEffect2, useLayoutEffect, useRef as useRef2, useState as useState2 } from "react";
|
|
294
|
+
function useTranscriptAnchor(options) {
|
|
295
|
+
const { tailKey, hasOlder = false, loadOlder, atBottomPx = 24, loadOlderPx = 200 } = options;
|
|
296
|
+
const scrollRef = useRef2(null);
|
|
297
|
+
const following = useRef2(true);
|
|
298
|
+
const [isAtBottom, setIsAtBottom] = useState2(true);
|
|
299
|
+
const heightBeforePrepend = useRef2(null);
|
|
300
|
+
const loadingOlder = useRef2(false);
|
|
301
|
+
const toEnd = useCallback2(() => {
|
|
302
|
+
const box = scrollRef.current;
|
|
303
|
+
if (box)
|
|
304
|
+
box.scrollTop = box.scrollHeight;
|
|
305
|
+
}, []);
|
|
306
|
+
const scrollToEnd = useCallback2(() => {
|
|
307
|
+
following.current = true;
|
|
308
|
+
setIsAtBottom(true);
|
|
309
|
+
toEnd();
|
|
310
|
+
}, [toEnd]);
|
|
311
|
+
const paging = useRef2({ hasOlder, loadOlder });
|
|
312
|
+
paging.current = { hasOlder, loadOlder };
|
|
313
|
+
const requestOlder = useCallback2(() => {
|
|
314
|
+
const box = scrollRef.current;
|
|
315
|
+
const { hasOlder: more, loadOlder: fetchOlder } = paging.current;
|
|
316
|
+
if (!box || !more || !fetchOlder || loadingOlder.current)
|
|
317
|
+
return;
|
|
318
|
+
loadingOlder.current = true;
|
|
319
|
+
heightBeforePrepend.current = box.scrollHeight;
|
|
320
|
+
fetchOlder().then((added) => {
|
|
321
|
+
if (added === 0)
|
|
322
|
+
heightBeforePrepend.current = null;
|
|
323
|
+
}).catch(() => {
|
|
324
|
+
heightBeforePrepend.current = null;
|
|
325
|
+
}).finally(() => {
|
|
326
|
+
loadingOlder.current = false;
|
|
327
|
+
});
|
|
328
|
+
}, []);
|
|
329
|
+
const onScroll = useCallback2(() => {
|
|
330
|
+
const box = scrollRef.current;
|
|
331
|
+
if (!box)
|
|
332
|
+
return;
|
|
333
|
+
const atBottom = box.scrollHeight - box.scrollTop - box.clientHeight < atBottomPx;
|
|
334
|
+
following.current = atBottom;
|
|
335
|
+
setIsAtBottom((held) => held === atBottom ? held : atBottom);
|
|
336
|
+
if (box.scrollTop < loadOlderPx)
|
|
337
|
+
requestOlder();
|
|
338
|
+
}, [atBottomPx, loadOlderPx, requestOlder]);
|
|
339
|
+
useLayoutEffect(() => {
|
|
340
|
+
const box = scrollRef.current;
|
|
341
|
+
const before = heightBeforePrepend.current;
|
|
342
|
+
if (!box || before === null)
|
|
343
|
+
return;
|
|
344
|
+
const added = box.scrollHeight - before;
|
|
345
|
+
if (added <= 0)
|
|
346
|
+
return;
|
|
347
|
+
heightBeforePrepend.current = null;
|
|
348
|
+
box.scrollTop += added;
|
|
349
|
+
});
|
|
350
|
+
useLayoutEffect(() => {
|
|
351
|
+
if (heightBeforePrepend.current !== null)
|
|
352
|
+
return;
|
|
353
|
+
if (following.current)
|
|
354
|
+
toEnd();
|
|
355
|
+
}, [tailKey, toEnd]);
|
|
356
|
+
const pullWhileShort = useCallback2(() => {
|
|
357
|
+
const box = scrollRef.current;
|
|
358
|
+
if (!box || !paging.current.hasOlder)
|
|
359
|
+
return;
|
|
360
|
+
if (box.scrollHeight <= box.clientHeight)
|
|
361
|
+
requestOlder();
|
|
362
|
+
}, [requestOlder]);
|
|
363
|
+
const observer = useRef2(null);
|
|
364
|
+
const contentRef = useCallback2((node) => {
|
|
365
|
+
observer.current?.disconnect();
|
|
366
|
+
observer.current = null;
|
|
367
|
+
if (node === null)
|
|
368
|
+
return;
|
|
369
|
+
let previous = node.scrollHeight;
|
|
370
|
+
const watch = new ResizeObserver(() => {
|
|
371
|
+
const height = node.scrollHeight;
|
|
372
|
+
if (height === previous)
|
|
373
|
+
return;
|
|
374
|
+
previous = height;
|
|
375
|
+
if (heightBeforePrepend.current !== null)
|
|
376
|
+
return;
|
|
377
|
+
if (following.current)
|
|
378
|
+
toEnd();
|
|
379
|
+
pullWhileShort();
|
|
380
|
+
});
|
|
381
|
+
watch.observe(node);
|
|
382
|
+
observer.current = watch;
|
|
383
|
+
}, [toEnd, pullWhileShort]);
|
|
384
|
+
useEffect2(() => {
|
|
385
|
+
pullWhileShort();
|
|
386
|
+
}, [pullWhileShort, hasOlder]);
|
|
387
|
+
useEffect2(() => () => observer.current?.disconnect(), []);
|
|
388
|
+
return { scrollRef, contentRef, onScroll, isAtBottom, scrollToEnd };
|
|
389
|
+
}
|
|
390
|
+
// src/voice-message.tsx
|
|
391
|
+
import { ChevronDown, ChevronRight, Pause, Play } from "lucide-react";
|
|
392
|
+
import { useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
|
|
393
|
+
import"./styles.css";
|
|
394
|
+
import { jsx as jsx3, jsxs, Fragment } from "react/jsx-runtime";
|
|
395
|
+
var DEFAULT_LABELS = {
|
|
396
|
+
play: "Play voice message",
|
|
397
|
+
pause: "Pause voice message",
|
|
398
|
+
transcript: "Transcript",
|
|
399
|
+
transcribing: "Transcribing…",
|
|
400
|
+
noSpeech: "No speech in this recording.",
|
|
401
|
+
loadFailed: "Could not load the recording."
|
|
402
|
+
};
|
|
403
|
+
var ROOT_CLASS2 = "cubos-agent-sdk-react-dom cubos-agent-sdk-react-dom--voice";
|
|
404
|
+
var PEAK_COUNT = 40;
|
|
405
|
+
var MAX_DECODE_BYTES = 8 * 1024 * 1024;
|
|
406
|
+
async function extractPeaks(blob) {
|
|
407
|
+
if (blob.size > MAX_DECODE_BYTES)
|
|
408
|
+
return null;
|
|
409
|
+
const Ctx = window.OfflineAudioContext ?? window.webkitOfflineAudioContext;
|
|
410
|
+
if (!Ctx)
|
|
411
|
+
return null;
|
|
412
|
+
try {
|
|
413
|
+
const bytes = await blob.arrayBuffer();
|
|
414
|
+
const decoded = await new Ctx(1, 1, 8000).decodeAudioData(bytes);
|
|
415
|
+
const samples = decoded.getChannelData(0);
|
|
416
|
+
const bucket = Math.floor(samples.length / PEAK_COUNT) || 1;
|
|
417
|
+
const peaks = [];
|
|
418
|
+
let loudest = 0;
|
|
419
|
+
for (let i = 0;i < PEAK_COUNT; i++) {
|
|
420
|
+
let peak = 0;
|
|
421
|
+
for (let j = i * bucket;j < (i + 1) * bucket && j < samples.length; j++) {
|
|
422
|
+
const value = Math.abs(samples[j]);
|
|
423
|
+
if (value > peak)
|
|
424
|
+
peak = value;
|
|
425
|
+
}
|
|
426
|
+
peaks.push(peak);
|
|
427
|
+
if (peak > loudest)
|
|
428
|
+
loudest = peak;
|
|
429
|
+
}
|
|
430
|
+
if (loudest === 0)
|
|
431
|
+
return null;
|
|
432
|
+
return peaks.map((peak) => Math.max(0.12, peak / loudest));
|
|
433
|
+
} catch {
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
function VoiceMessage({
|
|
438
|
+
loadBytes,
|
|
439
|
+
transcript,
|
|
440
|
+
bytes,
|
|
441
|
+
defaultTranscriptOpen = false,
|
|
442
|
+
labels,
|
|
443
|
+
colors,
|
|
444
|
+
className
|
|
445
|
+
}) {
|
|
446
|
+
const text = { ...DEFAULT_LABELS, ...labels };
|
|
447
|
+
const rootRef = useRef3(null);
|
|
448
|
+
const audioRef = useRef3(null);
|
|
449
|
+
const objectUrlRef = useRef3(null);
|
|
450
|
+
const abortRef = useRef3(null);
|
|
451
|
+
const inflightRef = useRef3(null);
|
|
452
|
+
const [isPlaying, setIsPlaying] = useState3(false);
|
|
453
|
+
const [isLoading, setIsLoading] = useState3(false);
|
|
454
|
+
const [failed, setFailed] = useState3(false);
|
|
455
|
+
const [position, setPosition] = useState3(0);
|
|
456
|
+
const [duration, setDuration] = useState3(0);
|
|
457
|
+
const [peaks, setPeaks] = useState3(null);
|
|
458
|
+
const [open, setOpen] = useState3(defaultTranscriptOpen);
|
|
459
|
+
useEffect3(() => () => {
|
|
460
|
+
abortRef.current?.abort();
|
|
461
|
+
audioRef.current?.pause();
|
|
462
|
+
if (objectUrlRef.current)
|
|
463
|
+
URL.revokeObjectURL(objectUrlRef.current);
|
|
464
|
+
}, []);
|
|
465
|
+
const ensureLoadedRef = useRef3(() => {});
|
|
466
|
+
ensureLoadedRef.current = () => void ensureLoaded();
|
|
467
|
+
useEffect3(() => {
|
|
468
|
+
const node = rootRef.current;
|
|
469
|
+
if (node === null || typeof IntersectionObserver === "undefined")
|
|
470
|
+
return;
|
|
471
|
+
const observer = new IntersectionObserver((entries) => {
|
|
472
|
+
if (!entries.some((entry) => entry.isIntersecting))
|
|
473
|
+
return;
|
|
474
|
+
observer.disconnect();
|
|
475
|
+
ensureLoadedRef.current();
|
|
476
|
+
}, { rootMargin: "600px" });
|
|
477
|
+
observer.observe(node);
|
|
478
|
+
return () => observer.disconnect();
|
|
479
|
+
}, []);
|
|
480
|
+
function ensureLoaded() {
|
|
481
|
+
if (audioRef.current)
|
|
482
|
+
return Promise.resolve(audioRef.current);
|
|
483
|
+
inflightRef.current ??= load();
|
|
484
|
+
return inflightRef.current;
|
|
485
|
+
}
|
|
486
|
+
async function load() {
|
|
487
|
+
const controller = new AbortController;
|
|
488
|
+
abortRef.current = controller;
|
|
489
|
+
setFailed(false);
|
|
490
|
+
try {
|
|
491
|
+
const blob = await loadBytes(controller.signal);
|
|
492
|
+
if (controller.signal.aborted)
|
|
493
|
+
return null;
|
|
494
|
+
const url = URL.createObjectURL(blob);
|
|
495
|
+
objectUrlRef.current = url;
|
|
496
|
+
const element = new Audio(url);
|
|
497
|
+
element.preload = "metadata";
|
|
498
|
+
element.onended = () => {
|
|
499
|
+
setIsPlaying(false);
|
|
500
|
+
setPosition(0);
|
|
501
|
+
};
|
|
502
|
+
element.ontimeupdate = () => setPosition(element.currentTime);
|
|
503
|
+
element.ondurationchange = () => {
|
|
504
|
+
if (Number.isFinite(element.duration))
|
|
505
|
+
setDuration(element.duration);
|
|
506
|
+
};
|
|
507
|
+
element.onloadedmetadata = () => {
|
|
508
|
+
if (Number.isFinite(element.duration)) {
|
|
509
|
+
setDuration(element.duration);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
element.currentTime = 100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000;
|
|
513
|
+
element.addEventListener("durationchange", () => {
|
|
514
|
+
element.currentTime = 0;
|
|
515
|
+
setPosition(0);
|
|
516
|
+
}, { once: true });
|
|
517
|
+
};
|
|
518
|
+
audioRef.current = element;
|
|
519
|
+
extractPeaks(blob).then((found) => {
|
|
520
|
+
if (!controller.signal.aborted && found)
|
|
521
|
+
setPeaks(found);
|
|
522
|
+
});
|
|
523
|
+
return element;
|
|
524
|
+
} catch {
|
|
525
|
+
if (!controller.signal.aborted)
|
|
526
|
+
setFailed(true);
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
async function toggle() {
|
|
531
|
+
setIsLoading(audioRef.current === null);
|
|
532
|
+
const element = await ensureLoaded().finally(() => setIsLoading(false));
|
|
533
|
+
if (!element)
|
|
534
|
+
return;
|
|
535
|
+
if (element.paused) {
|
|
536
|
+
try {
|
|
537
|
+
await element.play();
|
|
538
|
+
setIsPlaying(true);
|
|
539
|
+
} catch {
|
|
540
|
+
setFailed(true);
|
|
541
|
+
setIsPlaying(false);
|
|
542
|
+
}
|
|
543
|
+
} else {
|
|
544
|
+
element.pause();
|
|
545
|
+
setIsPlaying(false);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
function seek(to) {
|
|
549
|
+
setPosition(to);
|
|
550
|
+
if (audioRef.current)
|
|
551
|
+
audioRef.current.currentTime = to;
|
|
552
|
+
}
|
|
553
|
+
const progress = duration > 0 ? Math.min(1, position / duration) : 0;
|
|
554
|
+
const clock = duration > 0 ? formatClock(position > 0 ? position : duration) : formatSize(bytes);
|
|
555
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
556
|
+
ref: rootRef,
|
|
557
|
+
className: className ? `${ROOT_CLASS2} ${className}` : ROOT_CLASS2,
|
|
558
|
+
style: colorStyle(colors),
|
|
559
|
+
children: [
|
|
560
|
+
/* @__PURE__ */ jsxs("div", {
|
|
561
|
+
className: "cubos-agent-sdk-react-dom__voice-row",
|
|
562
|
+
children: [
|
|
563
|
+
/* @__PURE__ */ jsx3("button", {
|
|
564
|
+
type: "button",
|
|
565
|
+
className: "cubos-agent-sdk-react-dom__voice-play",
|
|
566
|
+
onClick: () => void toggle(),
|
|
567
|
+
disabled: isLoading,
|
|
568
|
+
"aria-label": isPlaying ? text.pause : text.play,
|
|
569
|
+
children: /* @__PURE__ */ jsx3("span", {
|
|
570
|
+
className: "cubos-agent-sdk-react-dom__voice-face",
|
|
571
|
+
children: isPlaying ? /* @__PURE__ */ jsx3(Pause, {
|
|
572
|
+
size: 13,
|
|
573
|
+
fill: "currentColor",
|
|
574
|
+
"aria-hidden": "true"
|
|
575
|
+
}) : /* @__PURE__ */ jsx3(Play, {
|
|
576
|
+
size: 13,
|
|
577
|
+
fill: "currentColor",
|
|
578
|
+
style: { marginLeft: 1 },
|
|
579
|
+
"aria-hidden": "true"
|
|
580
|
+
})
|
|
581
|
+
})
|
|
582
|
+
}),
|
|
583
|
+
/* @__PURE__ */ jsxs("div", {
|
|
584
|
+
className: "cubos-agent-sdk-react-dom__voice-track",
|
|
585
|
+
children: [
|
|
586
|
+
peaks ? /* @__PURE__ */ jsx3("div", {
|
|
587
|
+
className: "cubos-agent-sdk-react-dom__voice-wave",
|
|
588
|
+
"aria-hidden": "true",
|
|
589
|
+
children: peaks.map((height, index) => /* @__PURE__ */ jsx3("span", {
|
|
590
|
+
className: index / peaks.length <= progress ? "cubos-agent-sdk-react-dom__voice-bar cubos-agent-sdk-react-dom__voice-bar--played" : "cubos-agent-sdk-react-dom__voice-bar",
|
|
591
|
+
style: { height: `${Math.round(height * 100)}%` }
|
|
592
|
+
}, index))
|
|
593
|
+
}) : /* @__PURE__ */ jsx3("div", {
|
|
594
|
+
className: "cubos-agent-sdk-react-dom__voice-line",
|
|
595
|
+
"aria-hidden": "true",
|
|
596
|
+
children: /* @__PURE__ */ jsx3("span", {
|
|
597
|
+
className: "cubos-agent-sdk-react-dom__voice-line-fill",
|
|
598
|
+
style: { width: `${progress * 100}%` }
|
|
599
|
+
})
|
|
600
|
+
}),
|
|
601
|
+
duration > 0 && /* @__PURE__ */ jsx3("span", {
|
|
602
|
+
className: "cubos-agent-sdk-react-dom__voice-head",
|
|
603
|
+
style: { left: `${progress * 100}%` },
|
|
604
|
+
"aria-hidden": "true"
|
|
605
|
+
}),
|
|
606
|
+
/* @__PURE__ */ jsx3("input", {
|
|
607
|
+
type: "range",
|
|
608
|
+
className: "cubos-agent-sdk-react-dom__voice-seek",
|
|
609
|
+
min: 0,
|
|
610
|
+
max: duration || 0,
|
|
611
|
+
step: "any",
|
|
612
|
+
value: position,
|
|
613
|
+
disabled: duration === 0,
|
|
614
|
+
"aria-label": text.play,
|
|
615
|
+
"aria-valuetext": formatClock(position),
|
|
616
|
+
onChange: (event) => seek(Number(event.target.value))
|
|
617
|
+
})
|
|
618
|
+
]
|
|
619
|
+
}),
|
|
620
|
+
/* @__PURE__ */ jsx3("span", {
|
|
621
|
+
className: "cubos-agent-sdk-react-dom__voice-time",
|
|
622
|
+
children: clock
|
|
623
|
+
})
|
|
624
|
+
]
|
|
625
|
+
}),
|
|
626
|
+
failed && /* @__PURE__ */ jsx3("p", {
|
|
627
|
+
className: "cubos-agent-sdk-react-dom__voice-error",
|
|
628
|
+
children: text.loadFailed
|
|
629
|
+
}),
|
|
630
|
+
transcript !== undefined && transcript !== "" ? /* @__PURE__ */ jsxs(Fragment, {
|
|
631
|
+
children: [
|
|
632
|
+
/* @__PURE__ */ jsxs("button", {
|
|
633
|
+
type: "button",
|
|
634
|
+
className: "cubos-agent-sdk-react-dom__voice-disclosure",
|
|
635
|
+
onClick: () => setOpen((was) => !was),
|
|
636
|
+
"aria-expanded": open,
|
|
637
|
+
children: [
|
|
638
|
+
open ? /* @__PURE__ */ jsx3(ChevronDown, {
|
|
639
|
+
size: 12,
|
|
640
|
+
"aria-hidden": "true"
|
|
641
|
+
}) : /* @__PURE__ */ jsx3(ChevronRight, {
|
|
642
|
+
size: 12,
|
|
643
|
+
"aria-hidden": "true"
|
|
644
|
+
}),
|
|
645
|
+
text.transcript
|
|
646
|
+
]
|
|
647
|
+
}),
|
|
648
|
+
open && /* @__PURE__ */ jsx3("p", {
|
|
649
|
+
className: "cubos-agent-sdk-react-dom__voice-transcript",
|
|
650
|
+
children: transcript
|
|
651
|
+
})
|
|
652
|
+
]
|
|
653
|
+
}) : /* @__PURE__ */ jsx3("p", {
|
|
654
|
+
className: "cubos-agent-sdk-react-dom__voice-pending",
|
|
655
|
+
children: transcript === undefined ? text.transcribing : text.noSpeech
|
|
656
|
+
})
|
|
657
|
+
]
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
function formatClock(seconds) {
|
|
661
|
+
const total = Math.max(0, Math.round(seconds));
|
|
662
|
+
return `${Math.floor(total / 60)}:${String(total % 60).padStart(2, "0")}`;
|
|
663
|
+
}
|
|
664
|
+
function formatSize(bytes) {
|
|
665
|
+
if (bytes === undefined)
|
|
666
|
+
return "--:--";
|
|
667
|
+
return bytes >= 1024 * 1024 ? `${(bytes / (1024 * 1024)).toFixed(1)} MB` : `${Math.max(1, Math.round(bytes / 1024))} KB`;
|
|
668
|
+
}
|
|
669
|
+
export {
|
|
670
|
+
useTranscriptAnchor,
|
|
671
|
+
useRecorder,
|
|
672
|
+
VoiceMessage,
|
|
673
|
+
RecordingWave,
|
|
674
|
+
AgentMarkdown
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
//# debugId=D616CDC56985922A64756E2164756E21
|
|
678
|
+
//# sourceMappingURL=index.js.map
|