@unctad-ai/voice-agent-ui 1.0.0 → 1.0.1

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.
@@ -0,0 +1,1238 @@
1
+ // src/components/VoiceSettingsView.tsx
2
+ import { useState as useState3 } from "react";
3
+ import { motion, AnimatePresence } from "motion/react";
4
+ import {
5
+ ArrowLeft,
6
+ RotateCcw,
7
+ Volume2,
8
+ Gauge,
9
+ AudioLines,
10
+ Sparkles,
11
+ Mic,
12
+ Timer,
13
+ MessageSquare,
14
+ Activity,
15
+ Cpu,
16
+ EyeOff,
17
+ Ear,
18
+ Clock,
19
+ Zap,
20
+ Minimize2,
21
+ Signal,
22
+ History,
23
+ ChevronDown,
24
+ User,
25
+ MessageCircle,
26
+ Headphones,
27
+ SlidersHorizontal,
28
+ Wrench
29
+ } from "lucide-react";
30
+
31
+ // src/contexts/VoiceSettingsContext.tsx
32
+ import { createContext, useContext, useState, useRef, useCallback } from "react";
33
+ import {
34
+ DEFAULT_VOLUME,
35
+ DEFAULT_PLAYBACK_SPEED,
36
+ DEFAULT_TTS_ENABLED,
37
+ DEFAULT_AUTO_LISTEN,
38
+ DEFAULT_IDLE_TIMEOUT_MS,
39
+ DEFAULT_EXPRESSIVENESS,
40
+ DEFAULT_RESPONSE_LENGTH,
41
+ DEFAULT_SHOW_PIPELINE_METRICS,
42
+ DEFAULT_PIPELINE_METRICS_AUTO_HIDE_MS,
43
+ DEFAULT_SPEECH_THRESHOLD,
44
+ DEFAULT_PAUSE_TOLERANCE_MS,
45
+ DEFAULT_BARGE_IN_THRESHOLD,
46
+ DEFAULT_PANEL_COLLAPSE_TIMEOUT_MS,
47
+ DEFAULT_STT_TIMEOUT_MS,
48
+ DEFAULT_TTS_TIMEOUT_MS,
49
+ DEFAULT_LLM_TIMEOUT_MS,
50
+ DEFAULT_MIN_AUDIO_RMS,
51
+ DEFAULT_MAX_HISTORY_MESSAGES
52
+ } from "@unctad-ai/voice-agent-core";
53
+ import { jsx } from "react/jsx-runtime";
54
+ var DEFAULTS = {
55
+ volume: DEFAULT_VOLUME,
56
+ playbackSpeed: DEFAULT_PLAYBACK_SPEED,
57
+ ttsEnabled: DEFAULT_TTS_ENABLED,
58
+ autoListen: DEFAULT_AUTO_LISTEN,
59
+ idleTimeoutMs: DEFAULT_IDLE_TIMEOUT_MS,
60
+ expressiveness: DEFAULT_EXPRESSIVENESS,
61
+ responseLength: DEFAULT_RESPONSE_LENGTH,
62
+ showPipelineMetrics: DEFAULT_SHOW_PIPELINE_METRICS,
63
+ pipelineMetricsAutoHideMs: DEFAULT_PIPELINE_METRICS_AUTO_HIDE_MS,
64
+ speechThreshold: DEFAULT_SPEECH_THRESHOLD,
65
+ pauseToleranceMs: DEFAULT_PAUSE_TOLERANCE_MS,
66
+ bargeInThreshold: DEFAULT_BARGE_IN_THRESHOLD,
67
+ panelCollapseTimeoutMs: DEFAULT_PANEL_COLLAPSE_TIMEOUT_MS,
68
+ sttTimeoutMs: DEFAULT_STT_TIMEOUT_MS,
69
+ ttsTimeoutMs: DEFAULT_TTS_TIMEOUT_MS,
70
+ llmTimeoutMs: DEFAULT_LLM_TIMEOUT_MS,
71
+ minAudioRms: DEFAULT_MIN_AUDIO_RMS,
72
+ maxHistoryMessages: DEFAULT_MAX_HISTORY_MESSAGES
73
+ };
74
+ var STORAGE_KEY = "voice-settings";
75
+ function loadSettings() {
76
+ try {
77
+ const raw = localStorage.getItem(STORAGE_KEY);
78
+ if (!raw) return { ...DEFAULTS };
79
+ const parsed = JSON.parse(raw);
80
+ return { ...DEFAULTS, ...parsed };
81
+ } catch {
82
+ return { ...DEFAULTS };
83
+ }
84
+ }
85
+ function persistSettings(settings) {
86
+ try {
87
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
88
+ } catch {
89
+ }
90
+ }
91
+ var VoiceSettingsContext = createContext(void 0);
92
+ function VoiceSettingsProvider({ children }) {
93
+ const [settings, setSettings] = useState(loadSettings);
94
+ const volumeRef = useRef(settings.volume);
95
+ const speedRef = useRef(settings.playbackSpeed);
96
+ const updateSetting = useCallback(
97
+ (key, value) => {
98
+ setSettings((prev) => {
99
+ const next = { ...prev, [key]: value };
100
+ persistSettings(next);
101
+ if (key === "volume") volumeRef.current = value;
102
+ if (key === "playbackSpeed") speedRef.current = value;
103
+ return next;
104
+ });
105
+ },
106
+ []
107
+ );
108
+ const resetSettings = useCallback(() => {
109
+ const defaults = { ...DEFAULTS };
110
+ setSettings(defaults);
111
+ persistSettings(defaults);
112
+ volumeRef.current = defaults.volume;
113
+ speedRef.current = defaults.playbackSpeed;
114
+ }, []);
115
+ return /* @__PURE__ */ jsx(
116
+ VoiceSettingsContext.Provider,
117
+ {
118
+ value: { settings, volumeRef, speedRef, updateSetting, resetSettings },
119
+ children
120
+ }
121
+ );
122
+ }
123
+ function useVoiceSettings() {
124
+ const context = useContext(VoiceSettingsContext);
125
+ if (context) return context;
126
+ return {
127
+ settings: DEFAULTS,
128
+ volumeRef: { current: DEFAULTS.volume },
129
+ speedRef: { current: DEFAULTS.playbackSpeed },
130
+ updateSetting: () => {
131
+ },
132
+ resetSettings: () => {
133
+ }
134
+ };
135
+ }
136
+
137
+ // src/components/VoiceSettingsView.tsx
138
+ import { VAD, useSiteConfig as useSiteConfig2 } from "@unctad-ai/voice-agent-core";
139
+
140
+ // src/components/PersonaSettings.tsx
141
+ import { useState as useState2, useRef as useRef2, useEffect } from "react";
142
+ import { usePersonaContext, useSiteConfig } from "@unctad-ai/voice-agent-core";
143
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
144
+ function PersonaSettings() {
145
+ const config = useSiteConfig();
146
+ if (!config.personaEndpoint) return null;
147
+ return /* @__PURE__ */ jsx2(PersonaSettingsInner, {});
148
+ }
149
+ function PersonaSettingsInner() {
150
+ const config = useSiteConfig();
151
+ const persona = usePersonaContext();
152
+ if (!persona) return null;
153
+ const { persona: data, isLoaded, updateName, uploadAvatar, uploadVoice, deleteVoice, setActiveVoice, previewVoice } = persona;
154
+ if (!isLoaded) {
155
+ return /* @__PURE__ */ jsx2("div", { style: { padding: 16, fontSize: 13, color: "#9ca3af", fontFamily: "inherit" }, children: "Loading persona settings..." });
156
+ }
157
+ return /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 12, fontFamily: "inherit" }, children: [
158
+ /* @__PURE__ */ jsx2(AvatarSection, { avatarUrl: config.avatarUrl, name: config.copilotName, onUpload: uploadAvatar }),
159
+ /* @__PURE__ */ jsx2(NameSection, { name: config.copilotName, onSave: updateName, primaryColor: config.colors.primary }),
160
+ /* @__PURE__ */ jsx2(
161
+ VoiceSection,
162
+ {
163
+ voices: data?.voices ?? [],
164
+ activeVoiceId: data?.activeVoiceId ?? "",
165
+ onUpload: uploadVoice,
166
+ onDelete: deleteVoice,
167
+ onSelect: setActiveVoice,
168
+ onPreview: previewVoice,
169
+ primaryColor: config.colors.primary
170
+ }
171
+ )
172
+ ] });
173
+ }
174
+ function AvatarSection({ avatarUrl, name, onUpload }) {
175
+ const [uploading, setUploading] = useState2(false);
176
+ const [hovered, setHovered] = useState2(false);
177
+ const [imgError, setImgError] = useState2(false);
178
+ const inputRef = useRef2(null);
179
+ const initial = (name || "?")[0].toUpperCase();
180
+ const handleFile = async (e) => {
181
+ const file = e.target.files?.[0];
182
+ if (!file) return;
183
+ setUploading(true);
184
+ try {
185
+ await onUpload(file);
186
+ setImgError(false);
187
+ } catch (err) {
188
+ console.error("Avatar upload failed:", err);
189
+ } finally {
190
+ setUploading(false);
191
+ }
192
+ };
193
+ const showImage = avatarUrl && !imgError;
194
+ return /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 12, paddingTop: 4, paddingBottom: 4 }, children: [
195
+ /* @__PURE__ */ jsxs(
196
+ "div",
197
+ {
198
+ onClick: () => !uploading && inputRef.current?.click(),
199
+ onMouseEnter: () => setHovered(true),
200
+ onMouseLeave: () => setHovered(false),
201
+ style: {
202
+ position: "relative",
203
+ width: 44,
204
+ height: 44,
205
+ borderRadius: 9999,
206
+ overflow: "hidden",
207
+ backgroundColor: "#e5e7eb",
208
+ flexShrink: 0,
209
+ cursor: uploading ? "wait" : "pointer"
210
+ },
211
+ children: [
212
+ showImage ? /* @__PURE__ */ jsx2(
213
+ "img",
214
+ {
215
+ src: avatarUrl,
216
+ alt: "",
217
+ onError: () => setImgError(true),
218
+ style: { width: "100%", height: "100%", objectFit: "cover" }
219
+ }
220
+ ) : /* @__PURE__ */ jsx2("div", { style: {
221
+ width: "100%",
222
+ height: "100%",
223
+ display: "flex",
224
+ alignItems: "center",
225
+ justifyContent: "center",
226
+ color: "#6b7280",
227
+ fontSize: 16,
228
+ fontWeight: 600
229
+ }, children: initial }),
230
+ hovered && !uploading && /* @__PURE__ */ jsx2("div", { style: {
231
+ position: "absolute",
232
+ inset: 0,
233
+ backgroundColor: "rgba(0,0,0,0.35)",
234
+ display: "flex",
235
+ alignItems: "center",
236
+ justifyContent: "center",
237
+ color: "#fff",
238
+ fontSize: 9,
239
+ fontWeight: 600,
240
+ letterSpacing: "0.02em"
241
+ }, children: "Edit" })
242
+ ]
243
+ }
244
+ ),
245
+ /* @__PURE__ */ jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [
246
+ /* @__PURE__ */ jsx2("div", { style: { fontSize: 13, fontWeight: 500, color: "#111827" }, children: "Avatar" }),
247
+ /* @__PURE__ */ jsx2("div", { style: { fontSize: 11, color: "#6b7280" }, children: uploading ? "Uploading..." : "Click to change" })
248
+ ] }),
249
+ /* @__PURE__ */ jsx2("input", { ref: inputRef, type: "file", accept: "image/png,image/jpeg,image/webp", onChange: handleFile, style: { display: "none" } })
250
+ ] });
251
+ }
252
+ function NameSection({ name, onSave, primaryColor }) {
253
+ const [value, setValue] = useState2(name);
254
+ const [saving, setSaving] = useState2(false);
255
+ const [inputFocused, setInputFocused] = useState2(false);
256
+ const [saveHovered, setSaveHovered] = useState2(false);
257
+ const dirty = value !== name;
258
+ useEffect(() => {
259
+ setValue(name);
260
+ }, [name]);
261
+ const handleSave = async () => {
262
+ setSaving(true);
263
+ try {
264
+ await onSave(value);
265
+ } catch (err) {
266
+ console.error("Name update failed:", err);
267
+ } finally {
268
+ setSaving(false);
269
+ }
270
+ };
271
+ return /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 6, paddingTop: 4, paddingBottom: 4 }, children: [
272
+ /* @__PURE__ */ jsx2("span", { style: { fontSize: 13, fontWeight: 500, color: "#111827" }, children: "Name" }),
273
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8 }, children: [
274
+ /* @__PURE__ */ jsx2(
275
+ "input",
276
+ {
277
+ type: "text",
278
+ value,
279
+ onChange: (e) => setValue(e.target.value),
280
+ onFocus: () => setInputFocused(true),
281
+ onBlur: () => setInputFocused(false),
282
+ maxLength: 30,
283
+ style: {
284
+ flex: 1,
285
+ fontSize: 13,
286
+ paddingLeft: 10,
287
+ paddingRight: 10,
288
+ paddingTop: 6,
289
+ paddingBottom: 6,
290
+ borderRadius: 8,
291
+ border: `1px solid ${inputFocused ? "#9ca3af" : "#e5e7eb"}`,
292
+ backgroundColor: "#fff",
293
+ outline: "none",
294
+ fontFamily: "inherit",
295
+ transition: "border-color 0.15s"
296
+ }
297
+ }
298
+ ),
299
+ dirty && /* @__PURE__ */ jsx2(
300
+ "button",
301
+ {
302
+ onClick: handleSave,
303
+ disabled: saving,
304
+ onMouseEnter: () => setSaveHovered(true),
305
+ onMouseLeave: () => setSaveHovered(false),
306
+ style: {
307
+ fontSize: 12,
308
+ fontWeight: 500,
309
+ paddingLeft: 12,
310
+ paddingRight: 12,
311
+ paddingTop: 6,
312
+ paddingBottom: 6,
313
+ borderRadius: 8,
314
+ border: "none",
315
+ backgroundColor: saveHovered ? primaryColor : "#1f2937",
316
+ color: "#fff",
317
+ cursor: saving ? "default" : "pointer",
318
+ opacity: saving ? 0.5 : 1,
319
+ transition: "background-color 0.15s",
320
+ fontFamily: "inherit"
321
+ },
322
+ children: saving ? "Saving..." : "Save"
323
+ }
324
+ )
325
+ ] })
326
+ ] });
327
+ }
328
+ function VoiceSection({ voices, activeVoiceId, onUpload, onDelete, onSelect, onPreview, primaryColor }) {
329
+ const [uploading, setUploading] = useState2(false);
330
+ const [uploadName, setUploadName] = useState2("");
331
+ const [showUpload, setShowUpload] = useState2(false);
332
+ const [previewing, setPreviewing] = useState2(null);
333
+ const inputRef = useRef2(null);
334
+ const fileRef = useRef2(null);
335
+ const handleFileSelect = (e) => {
336
+ const file = e.target.files?.[0];
337
+ if (!file) return;
338
+ fileRef.current = file;
339
+ setUploadName(file.name.replace(/\.wav$/i, ""));
340
+ setShowUpload(true);
341
+ };
342
+ const handleUpload = async () => {
343
+ if (!fileRef.current || !uploadName) return;
344
+ setUploading(true);
345
+ try {
346
+ await onUpload(fileRef.current, uploadName);
347
+ setShowUpload(false);
348
+ setUploadName("");
349
+ fileRef.current = null;
350
+ } catch (err) {
351
+ console.error("Voice upload failed:", err);
352
+ } finally {
353
+ setUploading(false);
354
+ }
355
+ };
356
+ const handlePreview = async (voiceId) => {
357
+ setPreviewing(voiceId);
358
+ try {
359
+ const buffer = await onPreview(voiceId, "Hello, I am your AI assistant. How can I help you today?");
360
+ const blob = new Blob([buffer], { type: "audio/wav" });
361
+ const url = URL.createObjectURL(blob);
362
+ const audio = new Audio(url);
363
+ audio.onended = () => {
364
+ URL.revokeObjectURL(url);
365
+ setPreviewing(null);
366
+ };
367
+ audio.play();
368
+ } catch (err) {
369
+ console.error("Preview failed:", err);
370
+ setPreviewing(null);
371
+ }
372
+ };
373
+ return /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 8, paddingTop: 4, paddingBottom: 4 }, children: [
374
+ /* @__PURE__ */ jsx2("span", { style: { fontSize: 13, fontWeight: 500, color: "#111827" }, children: "Voice" }),
375
+ voices.length === 0 && /* @__PURE__ */ jsx2("p", { style: { fontSize: 11, color: "#9ca3af", margin: 0 }, children: "No voices configured. Upload a WAV sample to enable voice cloning." }),
376
+ voices.map((v) => /* @__PURE__ */ jsx2(
377
+ VoiceRow,
378
+ {
379
+ voice: v,
380
+ isActive: v.id === activeVoiceId,
381
+ isPreviewing: previewing === v.id,
382
+ onSelect: () => onSelect(v.id),
383
+ onPreview: () => handlePreview(v.id),
384
+ onDelete: () => onDelete(v.id),
385
+ primaryColor
386
+ },
387
+ v.id
388
+ )),
389
+ showUpload ? /* @__PURE__ */ jsx2(
390
+ UploadForm,
391
+ {
392
+ uploadName,
393
+ uploading,
394
+ onNameChange: setUploadName,
395
+ onUpload: handleUpload,
396
+ onCancel: () => {
397
+ setShowUpload(false);
398
+ fileRef.current = null;
399
+ },
400
+ primaryColor
401
+ }
402
+ ) : /* @__PURE__ */ jsx2(
403
+ UploadButton,
404
+ {
405
+ disabled: voices.length >= 10,
406
+ onClick: () => inputRef.current?.click()
407
+ }
408
+ ),
409
+ /* @__PURE__ */ jsx2("input", { ref: inputRef, type: "file", accept: "audio/wav", onChange: handleFileSelect, style: { display: "none" } })
410
+ ] });
411
+ }
412
+ function VoiceRow({ voice, isActive, isPreviewing, onSelect, onPreview, onDelete, primaryColor }) {
413
+ const [previewHovered, setPreviewHovered] = useState2(false);
414
+ const [deleteHovered, setDeleteHovered] = useState2(false);
415
+ return /* @__PURE__ */ jsxs("div", { style: {
416
+ display: "flex",
417
+ alignItems: "center",
418
+ gap: 8,
419
+ paddingLeft: 10,
420
+ paddingRight: 10,
421
+ paddingTop: 8,
422
+ paddingBottom: 8,
423
+ borderRadius: 8,
424
+ border: `1px solid ${isActive ? "#9ca3af" : "#e5e7eb"}`,
425
+ backgroundColor: isActive ? "#f9fafb" : "#fff",
426
+ fontSize: 13
427
+ }, children: [
428
+ /* @__PURE__ */ jsx2(
429
+ "input",
430
+ {
431
+ type: "radio",
432
+ name: "active-voice",
433
+ checked: isActive,
434
+ onChange: onSelect,
435
+ style: { accentColor: primaryColor }
436
+ }
437
+ ),
438
+ /* @__PURE__ */ jsx2("span", { style: { flex: 1 }, children: voice.name }),
439
+ /* @__PURE__ */ jsx2(
440
+ "button",
441
+ {
442
+ onClick: onPreview,
443
+ disabled: isPreviewing,
444
+ onMouseEnter: () => setPreviewHovered(true),
445
+ onMouseLeave: () => setPreviewHovered(false),
446
+ style: {
447
+ fontSize: 11,
448
+ color: previewHovered ? "#374151" : "#6b7280",
449
+ background: "none",
450
+ border: "none",
451
+ cursor: isPreviewing ? "default" : "pointer",
452
+ opacity: isPreviewing ? 0.5 : 1,
453
+ fontFamily: "inherit",
454
+ transition: "color 0.15s"
455
+ },
456
+ children: isPreviewing ? "Playing..." : "Preview"
457
+ }
458
+ ),
459
+ /* @__PURE__ */ jsx2(
460
+ "button",
461
+ {
462
+ onClick: onDelete,
463
+ onMouseEnter: () => setDeleteHovered(true),
464
+ onMouseLeave: () => setDeleteHovered(false),
465
+ style: {
466
+ fontSize: 11,
467
+ color: deleteHovered ? "#b91c1c" : "#ef4444",
468
+ background: "none",
469
+ border: "none",
470
+ cursor: "pointer",
471
+ fontFamily: "inherit",
472
+ transition: "color 0.15s"
473
+ },
474
+ children: "Delete"
475
+ }
476
+ )
477
+ ] });
478
+ }
479
+ function UploadForm({ uploadName, uploading, onNameChange, onUpload, onCancel, primaryColor }) {
480
+ const [uploadHovered, setUploadHovered] = useState2(false);
481
+ const [cancelHovered, setCancelHovered] = useState2(false);
482
+ return /* @__PURE__ */ jsxs("div", { style: {
483
+ display: "flex",
484
+ flexDirection: "column",
485
+ gap: 8,
486
+ padding: 10,
487
+ borderRadius: 6,
488
+ border: "1px solid #e5e7eb",
489
+ backgroundColor: "#f9fafb"
490
+ }, children: [
491
+ /* @__PURE__ */ jsx2(
492
+ "input",
493
+ {
494
+ type: "text",
495
+ value: uploadName,
496
+ onChange: (e) => onNameChange(e.target.value),
497
+ placeholder: "Voice name",
498
+ style: {
499
+ fontSize: 13,
500
+ paddingLeft: 10,
501
+ paddingRight: 10,
502
+ paddingTop: 6,
503
+ paddingBottom: 6,
504
+ borderRadius: 6,
505
+ border: "1px solid #e5e7eb",
506
+ outline: "none",
507
+ fontFamily: "inherit"
508
+ }
509
+ }
510
+ ),
511
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 8 }, children: [
512
+ /* @__PURE__ */ jsx2(
513
+ "button",
514
+ {
515
+ onClick: onUpload,
516
+ disabled: uploading || !uploadName,
517
+ onMouseEnter: () => setUploadHovered(true),
518
+ onMouseLeave: () => setUploadHovered(false),
519
+ style: {
520
+ fontSize: 11,
521
+ paddingLeft: 10,
522
+ paddingRight: 10,
523
+ paddingTop: 4,
524
+ paddingBottom: 4,
525
+ borderRadius: 6,
526
+ border: "none",
527
+ backgroundColor: uploadHovered ? primaryColor : "#1f2937",
528
+ color: "#fff",
529
+ cursor: uploading || !uploadName ? "default" : "pointer",
530
+ opacity: uploading || !uploadName ? 0.5 : 1,
531
+ fontFamily: "inherit",
532
+ transition: "background-color 0.15s"
533
+ },
534
+ children: uploading ? "Processing (~8s)..." : "Upload"
535
+ }
536
+ ),
537
+ /* @__PURE__ */ jsx2(
538
+ "button",
539
+ {
540
+ onClick: onCancel,
541
+ onMouseEnter: () => setCancelHovered(true),
542
+ onMouseLeave: () => setCancelHovered(false),
543
+ style: {
544
+ fontSize: 11,
545
+ paddingLeft: 10,
546
+ paddingRight: 10,
547
+ paddingTop: 4,
548
+ paddingBottom: 4,
549
+ borderRadius: 6,
550
+ border: "1px solid #e5e7eb",
551
+ backgroundColor: cancelHovered ? "#f9fafb" : "transparent",
552
+ cursor: "pointer",
553
+ fontFamily: "inherit",
554
+ transition: "background-color 0.15s"
555
+ },
556
+ children: "Cancel"
557
+ }
558
+ )
559
+ ] })
560
+ ] });
561
+ }
562
+ function UploadButton({ disabled, onClick }) {
563
+ const [hovered, setHovered] = useState2(false);
564
+ return /* @__PURE__ */ jsx2(
565
+ "button",
566
+ {
567
+ onClick,
568
+ disabled,
569
+ onMouseEnter: () => setHovered(true),
570
+ onMouseLeave: () => setHovered(false),
571
+ style: {
572
+ fontSize: 11,
573
+ paddingLeft: 10,
574
+ paddingRight: 10,
575
+ paddingTop: 6,
576
+ paddingBottom: 6,
577
+ borderRadius: 8,
578
+ border: "1px dashed #d1d5db",
579
+ backgroundColor: hovered && !disabled ? "#f9fafb" : "#fff",
580
+ cursor: disabled ? "not-allowed" : "pointer",
581
+ opacity: disabled ? 0.5 : 1,
582
+ transition: "background-color 0.15s",
583
+ fontFamily: "inherit"
584
+ },
585
+ children: "+ Upload voice sample (WAV, max 30s)"
586
+ }
587
+ );
588
+ }
589
+
590
+ // src/components/VoiceSettingsView.tsx
591
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
592
+ function expressivenessLabel(v) {
593
+ if (v <= 0.15) return "Low";
594
+ if (v <= 0.4) return "Medium";
595
+ return "High";
596
+ }
597
+ function speechThresholdLabel(v) {
598
+ if (v >= 0.75) return "Strict";
599
+ if (v >= 0.5) return "Balanced";
600
+ return "Sensitive";
601
+ }
602
+ function bargeInLabel(v) {
603
+ if (v >= 0.8) return "Hard";
604
+ if (v >= 0.6) return "Normal";
605
+ return "Easy";
606
+ }
607
+ var sliderStylesInjected = false;
608
+ function ensureSliderStyles() {
609
+ if (sliderStylesInjected || typeof document === "undefined") return;
610
+ sliderStylesInjected = true;
611
+ const style = document.createElement("style");
612
+ style.textContent = `
613
+ input[type="range"].voice-slider {
614
+ -webkit-appearance: none;
615
+ appearance: none;
616
+ width: 100%;
617
+ height: 4px;
618
+ border-radius: 2px;
619
+ background: #e5e7eb;
620
+ outline: none;
621
+ cursor: pointer;
622
+ margin: 6px 0;
623
+ }
624
+ input[type="range"].voice-slider::-webkit-slider-thumb {
625
+ -webkit-appearance: none;
626
+ appearance: none;
627
+ width: 16px;
628
+ height: 16px;
629
+ border-radius: 50%;
630
+ background: var(--voice-settings-accent, #DB2129);
631
+ border: 2px solid #fff;
632
+ box-shadow: 0 1px 3px rgba(0,0,0,0.15);
633
+ cursor: pointer;
634
+ transition: transform 0.1s ease;
635
+ margin-top: -6px;
636
+ }
637
+ input[type="range"].voice-slider::-webkit-slider-thumb:hover {
638
+ transform: scale(1.15);
639
+ }
640
+ input[type="range"].voice-slider::-moz-range-thumb {
641
+ width: 16px;
642
+ height: 16px;
643
+ border-radius: 50%;
644
+ background: var(--voice-settings-accent, #DB2129);
645
+ border: 2px solid #fff;
646
+ box-shadow: 0 1px 3px rgba(0,0,0,0.15);
647
+ cursor: pointer;
648
+ }
649
+ input[type="range"].voice-slider::-webkit-slider-runnable-track {
650
+ height: 4px;
651
+ border-radius: 2px;
652
+ display: flex;
653
+ align-items: center;
654
+ }
655
+ input[type="range"].voice-slider::-moz-range-track {
656
+ height: 4px;
657
+ border-radius: 2px;
658
+ background: #e5e7eb;
659
+ }
660
+ `;
661
+ document.head.appendChild(style);
662
+ }
663
+ function SliderSetting({
664
+ icon,
665
+ label,
666
+ value,
667
+ displayValue,
668
+ min,
669
+ max,
670
+ step,
671
+ onChange
672
+ }) {
673
+ ensureSliderStyles();
674
+ const pct = (value - min) / (max - min) * 100;
675
+ return /* @__PURE__ */ jsxs2("div", { style: { paddingTop: 12, paddingBottom: 12, display: "flex", flexDirection: "column", gap: 10 }, children: [
676
+ /* @__PURE__ */ jsxs2("div", { style: { display: "flex", alignItems: "center", gap: 12 }, children: [
677
+ icon,
678
+ /* @__PURE__ */ jsx3("span", { style: { flex: 1, fontSize: 13, fontWeight: 500, color: "#111827" }, children: label }),
679
+ /* @__PURE__ */ jsx3("span", { style: { fontSize: 11, fontWeight: 500, fontVariantNumeric: "tabular-nums", color: "var(--voice-settings-accent, #DB2129)" }, children: displayValue })
680
+ ] }),
681
+ /* @__PURE__ */ jsx3(
682
+ "input",
683
+ {
684
+ type: "range",
685
+ className: "voice-slider",
686
+ value,
687
+ min,
688
+ max,
689
+ step,
690
+ onChange: (e) => onChange(Number(e.target.value)),
691
+ style: {
692
+ background: `linear-gradient(to right, var(--voice-settings-accent, #DB2129) 0%, var(--voice-settings-accent, #DB2129) ${pct}%, #e5e7eb ${pct}%, #e5e7eb 100%)`
693
+ }
694
+ }
695
+ )
696
+ ] });
697
+ }
698
+ function ToggleSetting({
699
+ icon,
700
+ label,
701
+ description,
702
+ checked,
703
+ onChange
704
+ }) {
705
+ return /* @__PURE__ */ jsxs2("label", { style: { paddingTop: 12, paddingBottom: 12, display: "flex", alignItems: "center", gap: 12, cursor: "pointer" }, children: [
706
+ icon,
707
+ /* @__PURE__ */ jsxs2("div", { style: { flex: 1, minWidth: 0 }, children: [
708
+ /* @__PURE__ */ jsx3("div", { style: { fontSize: 13, fontWeight: 500, color: "#111827" }, children: label }),
709
+ /* @__PURE__ */ jsx3("div", { style: { fontSize: 11, color: "#6b7280" }, children: description })
710
+ ] }),
711
+ /* @__PURE__ */ jsx3(
712
+ "button",
713
+ {
714
+ role: "switch",
715
+ "aria-checked": checked,
716
+ onClick: () => onChange(!checked),
717
+ style: {
718
+ position: "relative",
719
+ display: "inline-flex",
720
+ alignItems: "center",
721
+ width: 44,
722
+ height: 24,
723
+ borderRadius: 9999,
724
+ backgroundColor: checked ? "var(--voice-settings-accent, #DB2129)" : "#d1d5db",
725
+ border: "none",
726
+ padding: 0,
727
+ cursor: "pointer",
728
+ transition: "background-color 0.2s",
729
+ flexShrink: 0
730
+ },
731
+ children: /* @__PURE__ */ jsx3(
732
+ "span",
733
+ {
734
+ style: {
735
+ display: "inline-block",
736
+ width: 20,
737
+ height: 20,
738
+ borderRadius: 9999,
739
+ backgroundColor: "#fff",
740
+ boxShadow: "0 1px 3px rgba(0,0,0,0.2)",
741
+ transition: "transform 0.2s",
742
+ transform: checked ? "translateX(22px)" : "translateX(2px)"
743
+ }
744
+ }
745
+ )
746
+ }
747
+ )
748
+ ] });
749
+ }
750
+ function SelectSetting({
751
+ icon,
752
+ label,
753
+ value,
754
+ onChange,
755
+ options
756
+ }) {
757
+ return /* @__PURE__ */ jsxs2("div", { style: { paddingTop: 12, paddingBottom: 12, display: "flex", alignItems: "center", gap: 12 }, children: [
758
+ icon,
759
+ /* @__PURE__ */ jsx3("span", { style: { flex: 1, fontSize: 13, fontWeight: 500, color: "#111827" }, children: label }),
760
+ /* @__PURE__ */ jsx3(
761
+ "select",
762
+ {
763
+ value,
764
+ onChange: (e) => onChange(e.target.value),
765
+ onMouseEnter: (e) => {
766
+ e.currentTarget.style.borderColor = "#d1d5db";
767
+ e.currentTarget.style.backgroundColor = "#f3f4f6";
768
+ },
769
+ onMouseLeave: (e) => {
770
+ e.currentTarget.style.borderColor = "#e5e7eb";
771
+ e.currentTarget.style.backgroundColor = "#f9fafb";
772
+ },
773
+ onFocus: (e) => {
774
+ e.currentTarget.style.borderColor = "#9ca3af";
775
+ },
776
+ onBlur: (e) => {
777
+ e.currentTarget.style.borderColor = "#e5e7eb";
778
+ },
779
+ style: {
780
+ height: 32,
781
+ fontSize: 12,
782
+ fontWeight: 500,
783
+ color: "#374151",
784
+ borderRadius: 9999,
785
+ border: "1px solid #e5e7eb",
786
+ backgroundColor: "#f9fafb",
787
+ paddingLeft: 12,
788
+ paddingRight: 28,
789
+ outline: "none",
790
+ WebkitAppearance: "none",
791
+ appearance: "none",
792
+ cursor: "pointer",
793
+ transition: "border-color 0.15s, background-color 0.15s",
794
+ backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23999' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E")`,
795
+ backgroundRepeat: "no-repeat",
796
+ backgroundPosition: "right 8px center"
797
+ },
798
+ children: options.map((opt) => /* @__PURE__ */ jsx3("option", { value: opt.value, children: opt.label }, opt.value))
799
+ }
800
+ )
801
+ ] });
802
+ }
803
+ function VoiceSettingsView({ onBack, onVolumeChange }) {
804
+ const { settings, updateSetting, resetSettings } = useVoiceSettings();
805
+ const config = useSiteConfig2();
806
+ const { colors } = config;
807
+ const [openSection, setOpenSection] = useState3(null);
808
+ const iconStyle = { width: 16, height: 16, flexShrink: 0, color: colors.primary };
809
+ const sectionIconStyle = { width: 16, height: 16, flexShrink: 0, color: colors.primary };
810
+ const sectionProps = (id) => ({
811
+ open: openSection === id,
812
+ onToggle: () => setOpenSection(openSection === id ? null : id)
813
+ });
814
+ return /* @__PURE__ */ jsxs2(
815
+ motion.div,
816
+ {
817
+ initial: { opacity: 0, x: 20 },
818
+ animate: { opacity: 1, x: 0 },
819
+ exit: { opacity: 0, x: 20 },
820
+ transition: { duration: 0.2, ease: [0.22, 1, 0.36, 1] },
821
+ style: {
822
+ position: "absolute",
823
+ inset: 0,
824
+ display: "flex",
825
+ flexDirection: "column",
826
+ zIndex: 10,
827
+ borderRadius: "inherit",
828
+ backgroundColor: "#f9fafb",
829
+ fontFamily: "inherit",
830
+ "--voice-settings-accent": colors.primary
831
+ },
832
+ children: [
833
+ /* @__PURE__ */ jsxs2(
834
+ "div",
835
+ {
836
+ style: { display: "flex", alignItems: "center", gap: 10, flexShrink: 0, paddingLeft: 16, paddingRight: 16, height: 56, borderBottom: "1px solid #e5e7eb" },
837
+ children: [
838
+ /* @__PURE__ */ jsx3(
839
+ "button",
840
+ {
841
+ onClick: onBack,
842
+ style: { width: 32, height: 32, borderRadius: 9999, display: "flex", alignItems: "center", justifyContent: "center", cursor: "pointer", transition: "color 0.15s", color: "#6b7280", background: "none", border: "none" },
843
+ onMouseEnter: (e) => {
844
+ e.currentTarget.style.color = "#111827";
845
+ },
846
+ onMouseLeave: (e) => {
847
+ e.currentTarget.style.color = "#6b7280";
848
+ },
849
+ "aria-label": "Back to conversation",
850
+ children: /* @__PURE__ */ jsx3(ArrowLeft, { style: { width: 16, height: 16 } })
851
+ }
852
+ ),
853
+ /* @__PURE__ */ jsx3("span", { style: { flex: 1, fontSize: 13, fontWeight: 600, color: "#111827" }, children: "Settings" }),
854
+ /* @__PURE__ */ jsx3(
855
+ "button",
856
+ {
857
+ onClick: resetSettings,
858
+ style: { width: 32, height: 32, borderRadius: 9999, display: "flex", alignItems: "center", justifyContent: "center", cursor: "pointer", transition: "color 0.15s", color: "#9ca3af", background: "none", border: "none" },
859
+ onMouseEnter: (e) => {
860
+ e.currentTarget.style.color = colors.primary;
861
+ },
862
+ onMouseLeave: (e) => {
863
+ e.currentTarget.style.color = "#9ca3af";
864
+ },
865
+ "aria-label": "Reset all settings",
866
+ title: "Reset to defaults",
867
+ children: /* @__PURE__ */ jsx3(RotateCcw, { style: { width: 14, height: 14 } })
868
+ }
869
+ )
870
+ ]
871
+ }
872
+ ),
873
+ /* @__PURE__ */ jsxs2("div", { style: { flex: 1, minHeight: 0, overflowY: "auto" }, children: [
874
+ config.personaEndpoint && /* @__PURE__ */ jsx3(SettingsSection, { title: "Persona", icon: /* @__PURE__ */ jsx3(User, { style: sectionIconStyle }), ...sectionProps("persona"), children: /* @__PURE__ */ jsx3(PersonaSettings, {}) }),
875
+ /* @__PURE__ */ jsxs2(SettingsSection, { title: "Conversation", icon: /* @__PURE__ */ jsx3(MessageCircle, { style: sectionIconStyle }), ...sectionProps("conversation"), children: [
876
+ /* @__PURE__ */ jsx3(
877
+ SelectSetting,
878
+ {
879
+ icon: /* @__PURE__ */ jsx3(MessageSquare, { style: iconStyle }),
880
+ label: "Response length",
881
+ value: String(settings.responseLength),
882
+ onChange: (v) => updateSetting("responseLength", Number(v)),
883
+ options: [
884
+ { value: "30", label: "Brief" },
885
+ { value: "60", label: "Normal" },
886
+ { value: "100", label: "Detailed" }
887
+ ]
888
+ }
889
+ ),
890
+ /* @__PURE__ */ jsx3(Divider, {}),
891
+ /* @__PURE__ */ jsx3(
892
+ SelectSetting,
893
+ {
894
+ icon: /* @__PURE__ */ jsx3(History, { style: iconStyle }),
895
+ label: "Chat memory",
896
+ value: String(settings.maxHistoryMessages),
897
+ onChange: (v) => updateSetting("maxHistoryMessages", Number(v)),
898
+ options: [
899
+ { value: "10", label: "10 msgs" },
900
+ { value: "20", label: "20 msgs" },
901
+ { value: "30", label: "30 msgs" },
902
+ { value: "40", label: "40 msgs" }
903
+ ]
904
+ }
905
+ )
906
+ ] }),
907
+ /* @__PURE__ */ jsxs2(SettingsSection, { title: "Listening", icon: /* @__PURE__ */ jsx3(Headphones, { style: sectionIconStyle }), ...sectionProps("listening"), children: [
908
+ /* @__PURE__ */ jsx3(
909
+ ToggleSetting,
910
+ {
911
+ icon: /* @__PURE__ */ jsx3(Mic, { style: iconStyle }),
912
+ label: "Auto-listen",
913
+ description: "Start mic when panel opens",
914
+ checked: settings.autoListen,
915
+ onChange: (v) => updateSetting("autoListen", v)
916
+ }
917
+ ),
918
+ /* @__PURE__ */ jsx3(Divider, {}),
919
+ /* @__PURE__ */ jsx3(
920
+ SliderSetting,
921
+ {
922
+ icon: /* @__PURE__ */ jsx3(Ear, { style: iconStyle }),
923
+ label: "Speech threshold",
924
+ value: settings.speechThreshold * 100,
925
+ displayValue: speechThresholdLabel(settings.speechThreshold),
926
+ min: 30,
927
+ max: 90,
928
+ step: 5,
929
+ onChange: (v) => updateSetting("speechThreshold", v / 100)
930
+ }
931
+ ),
932
+ /* @__PURE__ */ jsx3(Divider, {}),
933
+ /* @__PURE__ */ jsx3(
934
+ SelectSetting,
935
+ {
936
+ icon: /* @__PURE__ */ jsx3(Clock, { style: iconStyle }),
937
+ label: "Pause tolerance",
938
+ value: String(settings.pauseToleranceMs),
939
+ onChange: (v) => updateSetting("pauseToleranceMs", Number(v)),
940
+ options: [
941
+ { value: "400", label: "Fast" },
942
+ { value: "600", label: "Default" },
943
+ { value: "800", label: "Relaxed" },
944
+ { value: "1000", label: "Patient" }
945
+ ]
946
+ }
947
+ ),
948
+ /* @__PURE__ */ jsx3(Divider, {}),
949
+ /* @__PURE__ */ jsx3(
950
+ SliderSetting,
951
+ {
952
+ icon: /* @__PURE__ */ jsx3(Zap, { style: iconStyle }),
953
+ label: "Barge-in threshold",
954
+ value: settings.bargeInThreshold * 100,
955
+ displayValue: bargeInLabel(settings.bargeInThreshold),
956
+ min: 40,
957
+ max: 90,
958
+ step: 5,
959
+ onChange: (v) => updateSetting("bargeInThreshold", v / 100)
960
+ }
961
+ )
962
+ ] }),
963
+ /* @__PURE__ */ jsxs2(SettingsSection, { title: "Speaking", icon: /* @__PURE__ */ jsx3(Volume2, { style: sectionIconStyle }), ...sectionProps("speaking"), children: [
964
+ /* @__PURE__ */ jsx3(
965
+ ToggleSetting,
966
+ {
967
+ icon: /* @__PURE__ */ jsx3(AudioLines, { style: iconStyle }),
968
+ label: "Text-to-speech",
969
+ description: "Speak responses aloud",
970
+ checked: settings.ttsEnabled,
971
+ onChange: (v) => updateSetting("ttsEnabled", v)
972
+ }
973
+ ),
974
+ /* @__PURE__ */ jsx3(Divider, {}),
975
+ /* @__PURE__ */ jsx3(
976
+ SliderSetting,
977
+ {
978
+ icon: /* @__PURE__ */ jsx3(Volume2, { style: iconStyle }),
979
+ label: "Volume",
980
+ value: settings.volume * 100,
981
+ displayValue: `${Math.round(settings.volume * 100)}%`,
982
+ min: 0,
983
+ max: 100,
984
+ step: 1,
985
+ onChange: (v) => {
986
+ updateSetting("volume", v / 100);
987
+ onVolumeChange?.(v / 100);
988
+ }
989
+ }
990
+ ),
991
+ /* @__PURE__ */ jsx3(Divider, {}),
992
+ /* @__PURE__ */ jsx3(
993
+ SliderSetting,
994
+ {
995
+ icon: /* @__PURE__ */ jsx3(Gauge, { style: iconStyle }),
996
+ label: "Speed",
997
+ value: settings.playbackSpeed * 100,
998
+ displayValue: `${settings.playbackSpeed.toFixed(2)}x`,
999
+ min: 75,
1000
+ max: 150,
1001
+ step: 5,
1002
+ onChange: (v) => updateSetting("playbackSpeed", v / 100)
1003
+ }
1004
+ ),
1005
+ /* @__PURE__ */ jsx3(Divider, {}),
1006
+ /* @__PURE__ */ jsx3(
1007
+ SliderSetting,
1008
+ {
1009
+ icon: /* @__PURE__ */ jsx3(Sparkles, { style: iconStyle }),
1010
+ label: "Expressiveness",
1011
+ value: settings.expressiveness * 100,
1012
+ displayValue: expressivenessLabel(settings.expressiveness),
1013
+ min: 10,
1014
+ max: 60,
1015
+ step: 5,
1016
+ onChange: (v) => updateSetting("expressiveness", v / 100)
1017
+ }
1018
+ )
1019
+ ] }),
1020
+ /* @__PURE__ */ jsxs2(SettingsSection, { title: "Behavior", icon: /* @__PURE__ */ jsx3(SlidersHorizontal, { style: sectionIconStyle }), ...sectionProps("behavior"), children: [
1021
+ /* @__PURE__ */ jsx3(
1022
+ SelectSetting,
1023
+ {
1024
+ icon: /* @__PURE__ */ jsx3(Timer, { style: iconStyle }),
1025
+ label: "Idle timeout",
1026
+ value: String(settings.idleTimeoutMs),
1027
+ onChange: (v) => updateSetting("idleTimeoutMs", Number(v)),
1028
+ options: [
1029
+ { value: "30000", label: "30s" },
1030
+ { value: "60000", label: "1 min" },
1031
+ { value: "120000", label: "2 min" },
1032
+ { value: "300000", label: "5 min" }
1033
+ ]
1034
+ }
1035
+ ),
1036
+ /* @__PURE__ */ jsx3(Divider, {}),
1037
+ /* @__PURE__ */ jsx3(
1038
+ SelectSetting,
1039
+ {
1040
+ icon: /* @__PURE__ */ jsx3(Minimize2, { style: iconStyle }),
1041
+ label: "Auto-collapse",
1042
+ value: String(settings.panelCollapseTimeoutMs),
1043
+ onChange: (v) => updateSetting("panelCollapseTimeoutMs", Number(v)),
1044
+ options: [
1045
+ { value: "120000", label: "2 min" },
1046
+ { value: "300000", label: "5 min" },
1047
+ { value: "600000", label: "10 min" },
1048
+ { value: "0", label: "Never" }
1049
+ ]
1050
+ }
1051
+ )
1052
+ ] }),
1053
+ /* @__PURE__ */ jsxs2(SettingsSection, { title: "Developer", icon: /* @__PURE__ */ jsx3(Wrench, { style: sectionIconStyle }), ...sectionProps("developer"), last: true, children: [
1054
+ /* @__PURE__ */ jsx3(
1055
+ ToggleSetting,
1056
+ {
1057
+ icon: /* @__PURE__ */ jsx3(Activity, { style: iconStyle }),
1058
+ label: "Pipeline metrics",
1059
+ description: "Show STT / LLM / TTS timings",
1060
+ checked: settings.showPipelineMetrics,
1061
+ onChange: (v) => updateSetting("showPipelineMetrics", v)
1062
+ }
1063
+ ),
1064
+ /* @__PURE__ */ jsx3(Divider, {}),
1065
+ /* @__PURE__ */ jsx3(
1066
+ SelectSetting,
1067
+ {
1068
+ icon: /* @__PURE__ */ jsx3(EyeOff, { style: iconStyle }),
1069
+ label: "Auto-hide metrics",
1070
+ value: String(settings.pipelineMetricsAutoHideMs),
1071
+ onChange: (v) => updateSetting("pipelineMetricsAutoHideMs", Number(v)),
1072
+ options: [
1073
+ { value: "5000", label: "5s" },
1074
+ { value: "8000", label: "8s" },
1075
+ { value: "15000", label: "15s" },
1076
+ { value: "0", label: "Never" }
1077
+ ]
1078
+ }
1079
+ ),
1080
+ /* @__PURE__ */ jsx3(Divider, {}),
1081
+ /* @__PURE__ */ jsx3(
1082
+ SelectSetting,
1083
+ {
1084
+ icon: /* @__PURE__ */ jsx3(Mic, { style: iconStyle }),
1085
+ label: "STT timeout",
1086
+ value: String(settings.sttTimeoutMs),
1087
+ onChange: (v) => updateSetting("sttTimeoutMs", Number(v)),
1088
+ options: [
1089
+ { value: "10000", label: "10s" },
1090
+ { value: "15000", label: "15s" },
1091
+ { value: "30000", label: "30s" }
1092
+ ]
1093
+ }
1094
+ ),
1095
+ /* @__PURE__ */ jsx3(Divider, {}),
1096
+ /* @__PURE__ */ jsx3(
1097
+ SelectSetting,
1098
+ {
1099
+ icon: /* @__PURE__ */ jsx3(AudioLines, { style: iconStyle }),
1100
+ label: "TTS timeout",
1101
+ value: String(settings.ttsTimeoutMs),
1102
+ onChange: (v) => updateSetting("ttsTimeoutMs", Number(v)),
1103
+ options: [
1104
+ { value: "30000", label: "30s" },
1105
+ { value: "55000", label: "55s" },
1106
+ { value: "90000", label: "90s" }
1107
+ ]
1108
+ }
1109
+ ),
1110
+ /* @__PURE__ */ jsx3(Divider, {}),
1111
+ /* @__PURE__ */ jsx3(
1112
+ SelectSetting,
1113
+ {
1114
+ icon: /* @__PURE__ */ jsx3(Cpu, { style: iconStyle }),
1115
+ label: "LLM timeout",
1116
+ value: String(settings.llmTimeoutMs),
1117
+ onChange: (v) => updateSetting("llmTimeoutMs", Number(v)),
1118
+ options: [
1119
+ { value: "10000", label: "10s" },
1120
+ { value: "20000", label: "20s" },
1121
+ { value: "30000", label: "30s" },
1122
+ { value: "60000", label: "60s" }
1123
+ ]
1124
+ }
1125
+ ),
1126
+ /* @__PURE__ */ jsx3(Divider, {}),
1127
+ /* @__PURE__ */ jsx3(
1128
+ SelectSetting,
1129
+ {
1130
+ icon: /* @__PURE__ */ jsx3(Signal, { style: iconStyle }),
1131
+ label: "Min audio level",
1132
+ value: String(settings.minAudioRms),
1133
+ onChange: (v) => updateSetting("minAudioRms", Number(v)),
1134
+ options: [
1135
+ { value: "0.01", label: "Sensitive" },
1136
+ { value: "0.02", label: "Default" },
1137
+ { value: "0.035", label: "Moderate" },
1138
+ { value: "0.05", label: "Strict" }
1139
+ ]
1140
+ }
1141
+ ),
1142
+ /* @__PURE__ */ jsx3(Divider, {}),
1143
+ /* @__PURE__ */ jsxs2("div", { style: { paddingTop: 8, paddingBottom: 4, fontSize: 11, color: "#9ca3af" }, children: [
1144
+ "VAD Threshold: ",
1145
+ /* @__PURE__ */ jsx3("span", { style: { fontWeight: 500, color: "#6b7280" }, children: VAD.positiveSpeechThreshold })
1146
+ ] })
1147
+ ] })
1148
+ ] })
1149
+ ]
1150
+ }
1151
+ );
1152
+ }
1153
+ function SettingsSection({
1154
+ title,
1155
+ icon,
1156
+ children,
1157
+ last,
1158
+ open = false,
1159
+ onToggle
1160
+ }) {
1161
+ const [hovered, setHovered] = useState3(false);
1162
+ return /* @__PURE__ */ jsxs2("div", { style: { borderBottom: last ? "none" : "1px solid #e5e7eb" }, children: [
1163
+ /* @__PURE__ */ jsxs2(
1164
+ "button",
1165
+ {
1166
+ onClick: onToggle,
1167
+ onMouseEnter: () => setHovered(true),
1168
+ onMouseLeave: () => setHovered(false),
1169
+ style: {
1170
+ width: "100%",
1171
+ display: "flex",
1172
+ alignItems: "center",
1173
+ gap: 10,
1174
+ paddingLeft: 16,
1175
+ paddingRight: 16,
1176
+ paddingTop: 14,
1177
+ paddingBottom: 14,
1178
+ backgroundColor: hovered ? "#edf0f3" : open ? "#f0f2f5" : "transparent",
1179
+ border: "none",
1180
+ cursor: "pointer",
1181
+ fontFamily: "inherit",
1182
+ transition: "background-color 0.15s"
1183
+ },
1184
+ children: [
1185
+ icon,
1186
+ /* @__PURE__ */ jsx3("span", { style: {
1187
+ flex: 1,
1188
+ textAlign: "left",
1189
+ fontSize: 13,
1190
+ fontWeight: 600,
1191
+ color: "#374151"
1192
+ }, children: title }),
1193
+ /* @__PURE__ */ jsx3(ChevronDown, { style: {
1194
+ width: 14,
1195
+ height: 14,
1196
+ color: "#9ca3af",
1197
+ transition: "transform 0.2s ease",
1198
+ transform: open ? "rotate(0deg)" : "rotate(-90deg)"
1199
+ } })
1200
+ ]
1201
+ }
1202
+ ),
1203
+ /* @__PURE__ */ jsx3(AnimatePresence, { initial: false, children: open && /* @__PURE__ */ jsx3(
1204
+ motion.div,
1205
+ {
1206
+ initial: { height: 0, opacity: 0 },
1207
+ animate: { height: "auto", opacity: 1 },
1208
+ exit: { height: 0, opacity: 0 },
1209
+ transition: { duration: 0.2, ease: [0.22, 1, 0.36, 1] },
1210
+ style: { overflow: "hidden" },
1211
+ children: /* @__PURE__ */ jsx3("div", { style: {
1212
+ paddingLeft: 16,
1213
+ paddingRight: 16,
1214
+ paddingTop: 4,
1215
+ paddingBottom: 12,
1216
+ backgroundColor: "#fff",
1217
+ borderTop: "1px solid #e5e7eb"
1218
+ }, children })
1219
+ }
1220
+ ) })
1221
+ ] });
1222
+ }
1223
+ function Divider() {
1224
+ return /* @__PURE__ */ jsx3("div", { style: { height: 1, backgroundColor: "#f3f4f6" } });
1225
+ }
1226
+
1227
+ export {
1228
+ VoiceSettingsProvider,
1229
+ useVoiceSettings,
1230
+ PersonaSettings,
1231
+ SliderSetting,
1232
+ ToggleSetting,
1233
+ SelectSetting,
1234
+ VoiceSettingsView,
1235
+ SettingsSection,
1236
+ Divider
1237
+ };
1238
+ //# sourceMappingURL=chunk-BFDEQ5XG.js.map