anentrypoint-design 0.0.385 → 0.0.387

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/247420.css +33 -0
  2. package/dist/247420.js +53 -51
  3. package/package.json +1 -1
  4. package/src/components/agent-chat/controls.js +143 -0
  5. package/src/components/agent-chat/empty-state.js +63 -0
  6. package/src/components/agent-chat/message-rows.js +141 -0
  7. package/src/components/agent-chat/surface.js +206 -0
  8. package/src/components/agent-chat/thread-behaviour.js +50 -0
  9. package/src/components/agent-chat.js +6 -546
  10. package/src/components/chat/composer-affordances.js +109 -0
  11. package/src/components/chat/composer.js +256 -0
  12. package/src/components/chat/threads.js +105 -0
  13. package/src/components/chat-message-parts/agent-nodes.js +103 -0
  14. package/src/components/chat-message-parts/inline.js +106 -0
  15. package/src/components/chat-message-parts/prose-nodes.js +133 -0
  16. package/src/components/chat-message-parts/renderers.js +89 -0
  17. package/src/components/chat-message-parts.js +12 -408
  18. package/src/components/chat-minimap/minimap.js +167 -0
  19. package/src/components/chat-minimap/paint.js +73 -0
  20. package/src/components/chat-minimap/preview.js +41 -0
  21. package/src/components/chat-minimap.js +7 -263
  22. package/src/components/chat.js +20 -628
  23. package/src/components/community/chrome.js +63 -0
  24. package/src/components/community/navigation.js +167 -0
  25. package/src/components/community/presence.js +106 -0
  26. package/src/components/community/shell.js +17 -0
  27. package/src/components/community/views.js +131 -0
  28. package/src/components/community.js +18 -447
  29. package/src/components/files/chrome.js +140 -0
  30. package/src/components/files/entries.js +156 -0
  31. package/src/components/files/grid-controls.js +63 -0
  32. package/src/components/files/grid.js +177 -0
  33. package/src/components/files/types.js +69 -0
  34. package/src/components/files-modals/dialogs.js +118 -0
  35. package/src/components/files-modals/modal-shell.js +153 -0
  36. package/src/components/files-modals/preview-bodies.js +129 -0
  37. package/src/components/files-modals/preview-containers.js +96 -0
  38. package/src/components/files-modals.js +14 -475
  39. package/src/components/files.js +15 -564
  40. package/src/components/freddie/pages-config.js +3 -94
  41. package/src/components/freddie/pages-models.js +97 -0
  42. package/src/components/freddie.js +2 -1
  43. package/src/components/interaction-primitives/mobile.js +25 -0
  44. package/src/components/interaction-primitives/pointer.js +214 -0
  45. package/src/components/interaction-primitives/reorderable.js +47 -0
  46. package/src/components/interaction-primitives/shortcuts.js +119 -0
  47. package/src/components/interaction-primitives.js +16 -388
  48. package/src/components/sessions/conversation-list.js +230 -0
  49. package/src/components/sessions/dashboard.js +202 -0
  50. package/src/components/sessions/detail-bits.js +49 -0
  51. package/src/components/sessions/format.js +42 -0
  52. package/src/components/sessions/session-card.js +92 -0
  53. package/src/components/sessions.js +16 -579
  54. package/src/components/voice/audio-cue.js +39 -0
  55. package/src/components/voice/capture.js +90 -0
  56. package/src/components/voice/playback.js +75 -0
  57. package/src/components/voice/settings-modal.js +104 -0
  58. package/src/components/voice.js +16 -293
  59. package/src/css/app-shell/base.css +23 -0
  60. package/src/css/app-shell/states-interactions.css +10 -0
  61. package/src/kits/os/freddie/chat-protocol.js +32 -0
  62. package/src/kits/os/freddie/chat-transport.js +178 -0
  63. package/src/kits/os/freddie/pages-chat.js +10 -180
  64. package/src/kits/os/shell-chrome.js +163 -0
  65. package/src/kits/os/shell-geometry.js +59 -0
  66. package/src/kits/os/shell.js +178 -335
  67. package/src/page-html/client-script.js +151 -0
  68. package/src/page-html/head-tags.js +59 -0
  69. package/src/page-html/markdown.js +68 -0
  70. package/src/page-html/page-styles.js +44 -0
  71. package/src/page-html.js +14 -291
@@ -0,0 +1,39 @@
1
+ // playCompletionCue — short two-tone audio cue (e.g. "agent turn finished"),
2
+ // ported from pi-web's useAudio. The voice surface otherwise covers voice-call
3
+ // UI (PTT/VAD/webcam/queue), not simple one-shot notification tones, so this is
4
+ // the missing piece rather than a duplicate. A single AudioContext is reused
5
+ // (module-scoped) so a browser autoplay-suspended context can be resumed
6
+ // instead of leaking a fresh context per call.
7
+
8
+ let _cueCtx = null;
9
+ function getCueCtx() {
10
+ if (_cueCtx && _cueCtx.state !== 'closed') return _cueCtx;
11
+ try { _cueCtx = new (window.AudioContext || window.webkitAudioContext)(); } catch { return null; }
12
+ return _cueCtx;
13
+ }
14
+
15
+ export function playCompletionCue() {
16
+ const ctx = getCueCtx();
17
+ if (!ctx) return;
18
+ const play = () => {
19
+ try {
20
+ const now = ctx.currentTime;
21
+ [523.25, 659.25].forEach((freq, i) => {
22
+ const osc = ctx.createOscillator();
23
+ const gain = ctx.createGain();
24
+ osc.connect(gain);
25
+ gain.connect(ctx.destination);
26
+ osc.type = 'sine';
27
+ osc.frequency.value = freq;
28
+ const t = now + i * 0.18;
29
+ gain.gain.setValueAtTime(0, t);
30
+ gain.gain.linearRampToValueAtTime(0.18, t + 0.02);
31
+ gain.gain.exponentialRampToValueAtTime(0.001, t + 0.45);
32
+ osc.start(t);
33
+ osc.stop(t + 0.45);
34
+ });
35
+ } catch { /* swallow: AudioContext/oscillator unsupported, cue is best-effort */ }
36
+ };
37
+ if (ctx.state === 'suspended') { ctx.resume().then(play).catch(() => {}); return; }
38
+ play();
39
+ }
@@ -0,0 +1,90 @@
1
+ // Capture-side voice surfaces — what the local user speaks and shows through:
2
+ // the push-to-talk button, the voice-activity level meter with its threshold
3
+ // handle, and the webcam preview with resolution/fps pickers.
4
+
5
+ import * as webjsx from '../../../vendor/webjsx/index.js';
6
+ import { Icon } from '../shell.js';
7
+ const h = webjsx.createElement;
8
+
9
+ export function PttButton({ state = 'idle', mode = 'ptt', onHoldStart, onHoldEnd, onClick, label = 'Hold to talk' } = {}) {
10
+ const active = state === 'live' || state === 'recording' || state === 'vad';
11
+ const start = (e) => { onHoldStart && onHoldStart(e); };
12
+ const end = (e) => { onHoldEnd && onHoldEnd(e); };
13
+ return h('button', {
14
+ type: 'button',
15
+ class: 'vx-ptt vx-ptt-' + state + ' vx-ptt-mode-' + mode,
16
+ 'data-state': state,
17
+ 'data-mode': mode,
18
+ 'aria-pressed': active ? 'true' : 'false',
19
+ 'aria-label': label,
20
+ onclick: onClick ? (e) => onClick(e) : null,
21
+ onpointerdown: (e) => { e.preventDefault(); start(e); },
22
+ onpointerup: (e) => { e.preventDefault(); end(e); },
23
+ onpointerleave: (e) => end(e),
24
+ oncontextmenu: (e) => e.preventDefault(),
25
+ ontouchstart: (e) => start(e),
26
+ ontouchend: (e) => { e.preventDefault(); end(e); }
27
+ },
28
+ h('span', { class: 'vx-ptt-glow', 'aria-hidden': 'true' }),
29
+ h('span', { class: 'vx-ptt-icon', 'aria-hidden': 'true' }, state === 'idle' ? Icon('mic') : h('span', { class: 'ds-dot ds-dot-on', 'aria-hidden': 'true' })),
30
+ h('span', { class: 'vx-ptt-label' }, label)
31
+ );
32
+ }
33
+
34
+ export function VadMeter({ level = 0, threshold = 0.5, onThresholdChange } = {}) {
35
+ const lvl = Math.max(0, Math.min(1, Number(level) || 0));
36
+ const thr = Math.max(0, Math.min(1, Number(threshold) || 0));
37
+ const over = lvl >= thr;
38
+ return h('div', { class: 'vx-vad', role: 'group', 'aria-label': 'voice activity meter' },
39
+ h('div', { class: 'vx-vad-track' },
40
+ h('div', { class: 'vx-vad-fill' + (over ? ' vx-vad-fill-over' : ''), style: 'width:' + (lvl * 100).toFixed(1) + '%' }),
41
+ h('div', { class: 'vx-vad-marker', style: 'left:' + (thr * 100).toFixed(1) + '%', 'aria-hidden': 'true' }),
42
+ h('input', {
43
+ class: 'vx-vad-range',
44
+ type: 'range', min: '0', max: '1', step: '0.01',
45
+ value: String(thr),
46
+ 'aria-label': 'VAD threshold',
47
+ oninput: onThresholdChange ? (e) => onThresholdChange(parseFloat(e.target.value)) : null
48
+ })
49
+ ),
50
+ h('div', { class: 'vx-vad-readout' },
51
+ h('span', {}, 'lvl ' + Math.round(lvl * 100)),
52
+ h('span', {}, 'thr ' + Math.round(thr * 100))
53
+ )
54
+ );
55
+ }
56
+
57
+ export function WebcamPreview({ videoStream = null, resolution = '640x480', fps = 30, enabled = true, resolutions = [], fpsOptions = [], onResolutionChange, onFpsChange, onToggle } = {}) {
58
+ const videoRef = (el) => {
59
+ if (!el) return;
60
+ if (el.srcObject !== videoStream) el.srcObject = videoStream || null;
61
+ };
62
+ const resOpts = (resolutions.length ? resolutions : [resolution]).map(r =>
63
+ h('option', { key: 'r-' + r, value: r, selected: r === resolution }, r));
64
+ const fpsList = fpsOptions.length ? fpsOptions : [fps];
65
+ const fpsOpts = fpsList.map(f =>
66
+ h('option', { key: 'f-' + f, value: String(f), selected: Number(f) === Number(fps) }, f + ' fps'));
67
+ return h('div', { class: 'vx-cam' + (enabled ? '' : ' vx-cam-off') },
68
+ h('div', { class: 'vx-cam-stage' },
69
+ enabled
70
+ ? h('video', { class: 'vx-cam-video', ref: videoRef, autoplay: true, muted: true, playsinline: true })
71
+ : h('div', { class: 'vx-cam-placeholder' }, h('span', {}, Icon('camera')), h('span', {}, 'Camera off'))
72
+ ),
73
+ h('div', { class: 'vx-cam-controls' },
74
+ h('select', {
75
+ class: 'vx-select', 'aria-label': 'resolution',
76
+ onchange: onResolutionChange ? (e) => onResolutionChange(e.target.value) : null
77
+ }, ...resOpts),
78
+ h('select', {
79
+ class: 'vx-select', 'aria-label': 'frame rate',
80
+ onchange: onFpsChange ? (e) => onFpsChange(Number(e.target.value)) : null
81
+ }, ...fpsOpts),
82
+ h('button', {
83
+ type: 'button',
84
+ class: 'vx-btn' + (enabled ? ' vx-btn-on' : ''),
85
+ 'aria-pressed': enabled ? 'true' : 'false',
86
+ onclick: onToggle ? () => onToggle() : null
87
+ }, enabled ? 'Disable' : 'Enable')
88
+ )
89
+ );
90
+ }
@@ -0,0 +1,75 @@
1
+ // In-call playback surfaces: the mic/deafen/camera/screen/settings/leave
2
+ // control toolbar, and the per-speaker audio queue strip with its
3
+ // pause/resume/skip transport.
4
+
5
+ import * as webjsx from '../../../vendor/webjsx/index.js';
6
+ import { Icon } from '../shell.js';
7
+ const h = webjsx.createElement;
8
+
9
+ function fmtDur(s) {
10
+ s = Math.max(0, Math.round(Number(s) || 0));
11
+ if (s < 60) return s + 's';
12
+ const m = Math.floor(s / 60);
13
+ const r = s % 60;
14
+ return m + ':' + String(r).padStart(2, '0');
15
+ }
16
+
17
+ export function VoiceControls({ muted = false, deafened = false, cameraOn = false, screenShareOn = false, onMic, onDeafen, onCamera, onScreenShare, onSettings, onLeave } = {}) {
18
+ const btn = (cls, on, label, glyph, handler) => h('button', {
19
+ type: 'button',
20
+ class: 'vx-vc-btn ' + cls + (on ? ' vx-vc-on' : '') + (handler ? '' : ' vx-vc-disabled'),
21
+ 'aria-pressed': on ? 'true' : 'false',
22
+ 'aria-label': label,
23
+ title: label,
24
+ disabled: handler ? null : true,
25
+ onclick: handler ? (e) => handler(e) : null
26
+ },
27
+ h('span', { class: 'vx-vc-glyph', 'aria-hidden': 'true' }, glyph)
28
+ );
29
+ return h('div', { class: 'vx-vc', role: 'toolbar', 'aria-label': 'voice controls' },
30
+ btn('vx-vc-mic', !muted, muted ? 'Unmute' : 'Mute', Icon(muted ? 'mic-off' : 'mic'), onMic),
31
+ btn('vx-vc-deafen', !deafened, deafened ? 'Undeafen' : 'Deafen', Icon(deafened ? 'speaker-off' : 'speaker'), onDeafen),
32
+ btn('vx-vc-camera', cameraOn, cameraOn ? 'Stop camera' : 'Start camera', Icon('camera'), onCamera),
33
+ btn('vx-vc-screen', screenShareOn, screenShareOn ? 'Stop sharing' : 'Share screen', Icon('screen'), onScreenShare),
34
+ btn('vx-vc-settings', false, 'Voice settings', Icon('settings'), onSettings),
35
+ h('button', {
36
+ type: 'button', class: 'vx-vc-btn vx-vc-leave', 'aria-label': 'Leave voice', title: 'Leave voice',
37
+ onclick: onLeave ? (e) => onLeave(e) : null
38
+ }, h('span', { class: 'vx-vc-glyph', 'aria-hidden': 'true' }, Icon('phone')))
39
+ );
40
+ }
41
+
42
+ export function AudioQueue({ segments = [], currentSegmentId = null, paused = false, onReplay, onSkip, onResume, onPause } = {}) {
43
+ if (!segments || !segments.length) {
44
+ return h('div', { class: 'vx-queue vx-queue-empty' },
45
+ h('span', { class: 'vx-queue-empty-text' }, 'No audio queued'));
46
+ }
47
+ return h('div', { class: 'vx-queue', role: 'group', 'aria-label': 'audio queue' },
48
+ h('div', { class: 'vx-queue-ctrls' },
49
+ h('button', {
50
+ type: 'button', class: 'vx-queue-btn',
51
+ 'aria-label': paused ? 'resume' : 'pause',
52
+ onclick: () => paused ? (onResume && onResume()) : (onPause && onPause())
53
+ }, paused ? Icon('play') : Icon('pause')),
54
+ h('button', {
55
+ type: 'button', class: 'vx-queue-btn',
56
+ 'aria-label': 'skip', onclick: () => onSkip && onSkip()
57
+ }, Icon('skip-forward'))
58
+ ),
59
+ h('div', { class: 'vx-queue-strip' },
60
+ ...segments.map(s => h('button', {
61
+ key: 'q-' + s.id,
62
+ type: 'button',
63
+ class: 'vx-chip' + (s.id === currentSegmentId ? ' vx-chip-current' : '') + (s.isLive ? ' vx-chip-live' : ''),
64
+ 'data-id': s.id,
65
+ onclick: () => onReplay && onReplay(s.id)
66
+ },
67
+ h('span', { class: 'vx-chip-dot', style: s.color ? 'background:' + s.color : null, 'aria-hidden': 'true' }),
68
+ h('span', { class: 'vx-chip-name' }, s.speaker || '—'),
69
+ s.isLive
70
+ ? h('span', { class: 'vx-chip-tag' }, 'LIVE')
71
+ : h('span', { class: 'vx-chip-dur' }, fmtDur(s.duration))
72
+ ))
73
+ )
74
+ );
75
+ }
@@ -0,0 +1,104 @@
1
+ // VoiceSettingsModal — mode (PTT / VAD / live), input+output device pickers,
2
+ // VAD threshold, processing toggles, bitrate and master volume, plus the small
3
+ // section / device-select / toggle-row builders it composes from.
4
+
5
+ import * as webjsx from '../../../vendor/webjsx/index.js';
6
+ import { Icon } from '../shell.js';
7
+ const h = webjsx.createElement;
8
+
9
+ function seg({ label, children, className = '' }) {
10
+ return h('div', { class: 'vx-section ' + className },
11
+ label != null ? h('div', { class: 'vx-section-label' }, label) : null,
12
+ ...(Array.isArray(children) ? children : [children])
13
+ );
14
+ }
15
+
16
+ function devSelect(value, devices, onChange, aria) {
17
+ return h('select', {
18
+ class: 'vx-select', 'aria-label': aria,
19
+ onchange: onChange ? (e) => onChange(e.target.value) : null
20
+ }, ...(devices || []).map(d =>
21
+ h('option', { key: 'd-' + d.value, value: d.value, selected: d.value === value }, d.label)));
22
+ }
23
+
24
+ function toggleRow(label, checked, onToggle) {
25
+ return h('label', { class: 'vx-toggle-row' },
26
+ h('span', {}, label),
27
+ h('input', {
28
+ type: 'checkbox', class: 'vx-toggle',
29
+ checked: checked ? true : null,
30
+ onchange: onToggle ? (e) => onToggle(e.target.checked) : null
31
+ })
32
+ );
33
+ }
34
+
35
+ export function VoiceSettingsModal({ open = false, mode = 'ptt', inputId, outputId, inputDevices = [], outputDevices = [], vadThreshold = 0.5, rnnoise = false, autoGain = false, forceTurn = false, bitrate = 64, volume, onChange, onSave, onCancel, onClose } = {}) {
36
+ if (!open) return null;
37
+ const patch = (p) => onChange && onChange(p);
38
+ const modes = ['ptt', 'vad', 'live'];
39
+ const vol = volume == null ? 1 : volume;
40
+ return h('div', {
41
+ class: 'vx-modal-backdrop',
42
+ onclick: (e) => { if (e.target === e.currentTarget) onClose && onClose(); },
43
+ onkeydown: (e) => { if (e.key === 'Escape') { e.preventDefault(); onClose && onClose(); } }
44
+ },
45
+ h('div', { class: 'vx-modal', role: 'dialog', 'aria-modal': 'true', 'aria-label': 'Voice settings' },
46
+ h('div', { class: 'vx-modal-head' },
47
+ h('h2', { class: 'vx-modal-title' }, 'Voice settings'),
48
+ h('button', { type: 'button', class: 'vx-modal-x', 'aria-label': 'close', onclick: () => onClose && onClose() }, Icon('x'))
49
+ ),
50
+ h('div', { class: 'vx-modal-body' },
51
+ seg({ label: 'Mode', children:
52
+ h('div', { class: 'vx-segmented', role: 'group', 'aria-label': 'mode' },
53
+ ...modes.map(m => h('button', {
54
+ key: 'm-' + m, type: 'button',
55
+ class: 'vx-seg' + (m === mode ? ' vx-seg-on' : ''),
56
+ 'aria-pressed': m === mode ? 'true' : 'false',
57
+ onclick: () => patch({ mode: m })
58
+ }, m.toUpperCase())))
59
+ }),
60
+ seg({ label: 'Input device', children: devSelect(inputId, inputDevices, (v) => patch({ inputId: v }), 'input device') }),
61
+ seg({ label: 'Output device', children: devSelect(outputId, outputDevices, (v) => patch({ outputId: v }), 'output device') }),
62
+ mode === 'vad' ? seg({ label: 'VAD threshold', children:
63
+ h('div', { class: 'vx-range-row' },
64
+ h('input', {
65
+ type: 'range', class: 'vx-range', min: '0', max: '1', step: '0.01',
66
+ value: String(vadThreshold), 'aria-label': 'VAD threshold',
67
+ oninput: (e) => patch({ vadThreshold: parseFloat(e.target.value) })
68
+ }),
69
+ h('span', { class: 'vx-range-val' }, Math.round((Number(vadThreshold) || 0) * 100) + '%')
70
+ )
71
+ }) : null,
72
+ seg({ label: 'Processing', children: [
73
+ toggleRow('RNNoise', rnnoise, (v) => patch({ rnnoise: v })),
74
+ toggleRow('Auto gain', autoGain, (v) => patch({ autoGain: v })),
75
+ toggleRow('Force TURN', forceTurn, (v) => patch({ forceTurn: v }))
76
+ ]}),
77
+ seg({ label: 'Bitrate', children:
78
+ h('div', { class: 'vx-range-row' },
79
+ h('input', {
80
+ type: 'range', class: 'vx-range', min: '8', max: '256', step: '8',
81
+ value: String(bitrate), 'aria-label': 'bitrate',
82
+ oninput: (e) => patch({ bitrate: parseInt(e.target.value, 10) })
83
+ }),
84
+ h('span', { class: 'vx-range-val' }, (Number(bitrate) || 0) + ' kbps')
85
+ )
86
+ }),
87
+ seg({ label: 'Master volume', children:
88
+ h('div', { class: 'vx-range-row' },
89
+ h('input', {
90
+ type: 'range', class: 'vx-range', min: '0', max: '1', step: '0.01',
91
+ value: String(vol), 'aria-label': 'master volume',
92
+ oninput: (e) => patch({ volume: parseFloat(e.target.value) })
93
+ }),
94
+ h('span', { class: 'vx-range-val' }, Math.round(vol * 100) + '%')
95
+ )
96
+ })
97
+ ),
98
+ h('div', { class: 'vx-modal-foot' },
99
+ h('button', { type: 'button', class: 'vx-btn', onclick: () => onCancel && onCancel() }, 'Cancel'),
100
+ h('button', { type: 'button', class: 'vx-btn vx-btn-primary', onclick: () => onSave && onSave() }, 'Save')
101
+ )
102
+ )
103
+ );
104
+ }
@@ -1,295 +1,18 @@
1
1
  // Voice surfaces — PTT, VAD meter, webcam preview, voice settings, audio queue.
2
2
  // Pure factories returning webjsx vnodes. Class prefix: vx-*.
3
-
4
- import * as webjsx from '../../vendor/webjsx/index.js';
5
- import { Icon } from './shell.js';
6
- const h = webjsx.createElement;
7
-
8
- // ---------------------------------------------------------------------------
9
- // playCompletionCue short two-tone audio cue (e.g. "agent turn finished"),
10
- // ported from pi-web's useAudio. This file otherwise covers voice-call UI
11
- // (PTT/VAD/webcam/queue), not simple one-shot notification tones, so this is
12
- // the missing piece rather than a duplicate. A single AudioContext is reused
13
- // (module-scoped) so a browser autoplay-suspended context can be resumed
14
- // instead of leaking a fresh context per call.
15
- // ---------------------------------------------------------------------------
16
- let _cueCtx = null;
17
- function getCueCtx() {
18
- if (_cueCtx && _cueCtx.state !== 'closed') return _cueCtx;
19
- try { _cueCtx = new (window.AudioContext || window.webkitAudioContext)(); } catch { return null; }
20
- return _cueCtx;
21
- }
22
-
23
- export function playCompletionCue() {
24
- const ctx = getCueCtx();
25
- if (!ctx) return;
26
- const play = () => {
27
- try {
28
- const now = ctx.currentTime;
29
- [523.25, 659.25].forEach((freq, i) => {
30
- const osc = ctx.createOscillator();
31
- const gain = ctx.createGain();
32
- osc.connect(gain);
33
- gain.connect(ctx.destination);
34
- osc.type = 'sine';
35
- osc.frequency.value = freq;
36
- const t = now + i * 0.18;
37
- gain.gain.setValueAtTime(0, t);
38
- gain.gain.linearRampToValueAtTime(0.18, t + 0.02);
39
- gain.gain.exponentialRampToValueAtTime(0.001, t + 0.45);
40
- osc.start(t);
41
- osc.stop(t + 0.45);
42
- });
43
- } catch { /* swallow: AudioContext/oscillator unsupported, cue is best-effort */ }
44
- };
45
- if (ctx.state === 'suspended') { ctx.resume().then(play).catch(() => {}); return; }
46
- play();
47
- }
48
-
49
- function fmtDur(s) {
50
- s = Math.max(0, Math.round(Number(s) || 0));
51
- if (s < 60) return s + 's';
52
- const m = Math.floor(s / 60);
53
- const r = s % 60;
54
- return m + ':' + String(r).padStart(2, '0');
55
- }
56
-
57
- export function PttButton({ state = 'idle', mode = 'ptt', onHoldStart, onHoldEnd, onClick, label = 'Hold to talk' } = {}) {
58
- const active = state === 'live' || state === 'recording' || state === 'vad';
59
- const start = (e) => { onHoldStart && onHoldStart(e); };
60
- const end = (e) => { onHoldEnd && onHoldEnd(e); };
61
- return h('button', {
62
- type: 'button',
63
- class: 'vx-ptt vx-ptt-' + state + ' vx-ptt-mode-' + mode,
64
- 'data-state': state,
65
- 'data-mode': mode,
66
- 'aria-pressed': active ? 'true' : 'false',
67
- 'aria-label': label,
68
- onclick: onClick ? (e) => onClick(e) : null,
69
- onpointerdown: (e) => { e.preventDefault(); start(e); },
70
- onpointerup: (e) => { e.preventDefault(); end(e); },
71
- onpointerleave: (e) => end(e),
72
- oncontextmenu: (e) => e.preventDefault(),
73
- ontouchstart: (e) => start(e),
74
- ontouchend: (e) => { e.preventDefault(); end(e); }
75
- },
76
- h('span', { class: 'vx-ptt-glow', 'aria-hidden': 'true' }),
77
- h('span', { class: 'vx-ptt-icon', 'aria-hidden': 'true' }, state === 'idle' ? Icon('mic') : h('span', { class: 'ds-dot ds-dot-on', 'aria-hidden': 'true' })),
78
- h('span', { class: 'vx-ptt-label' }, label)
79
- );
80
- }
81
-
82
- export function VadMeter({ level = 0, threshold = 0.5, onThresholdChange } = {}) {
83
- const lvl = Math.max(0, Math.min(1, Number(level) || 0));
84
- const thr = Math.max(0, Math.min(1, Number(threshold) || 0));
85
- const over = lvl >= thr;
86
- return h('div', { class: 'vx-vad', role: 'group', 'aria-label': 'voice activity meter' },
87
- h('div', { class: 'vx-vad-track' },
88
- h('div', { class: 'vx-vad-fill' + (over ? ' vx-vad-fill-over' : ''), style: 'width:' + (lvl * 100).toFixed(1) + '%' }),
89
- h('div', { class: 'vx-vad-marker', style: 'left:' + (thr * 100).toFixed(1) + '%', 'aria-hidden': 'true' }),
90
- h('input', {
91
- class: 'vx-vad-range',
92
- type: 'range', min: '0', max: '1', step: '0.01',
93
- value: String(thr),
94
- 'aria-label': 'VAD threshold',
95
- oninput: onThresholdChange ? (e) => onThresholdChange(parseFloat(e.target.value)) : null
96
- })
97
- ),
98
- h('div', { class: 'vx-vad-readout' },
99
- h('span', {}, 'lvl ' + Math.round(lvl * 100)),
100
- h('span', {}, 'thr ' + Math.round(thr * 100))
101
- )
102
- );
103
- }
104
-
105
- export function WebcamPreview({ videoStream = null, resolution = '640x480', fps = 30, enabled = true, resolutions = [], fpsOptions = [], onResolutionChange, onFpsChange, onToggle } = {}) {
106
- const videoRef = (el) => {
107
- if (!el) return;
108
- if (el.srcObject !== videoStream) el.srcObject = videoStream || null;
109
- };
110
- const resOpts = (resolutions.length ? resolutions : [resolution]).map(r =>
111
- h('option', { key: 'r-' + r, value: r, selected: r === resolution }, r));
112
- const fpsList = fpsOptions.length ? fpsOptions : [fps];
113
- const fpsOpts = fpsList.map(f =>
114
- h('option', { key: 'f-' + f, value: String(f), selected: Number(f) === Number(fps) }, f + ' fps'));
115
- return h('div', { class: 'vx-cam' + (enabled ? '' : ' vx-cam-off') },
116
- h('div', { class: 'vx-cam-stage' },
117
- enabled
118
- ? h('video', { class: 'vx-cam-video', ref: videoRef, autoplay: true, muted: true, playsinline: true })
119
- : h('div', { class: 'vx-cam-placeholder' }, h('span', {}, Icon('camera')), h('span', {}, 'Camera off'))
120
- ),
121
- h('div', { class: 'vx-cam-controls' },
122
- h('select', {
123
- class: 'vx-select', 'aria-label': 'resolution',
124
- onchange: onResolutionChange ? (e) => onResolutionChange(e.target.value) : null
125
- }, ...resOpts),
126
- h('select', {
127
- class: 'vx-select', 'aria-label': 'frame rate',
128
- onchange: onFpsChange ? (e) => onFpsChange(Number(e.target.value)) : null
129
- }, ...fpsOpts),
130
- h('button', {
131
- type: 'button',
132
- class: 'vx-btn' + (enabled ? ' vx-btn-on' : ''),
133
- 'aria-pressed': enabled ? 'true' : 'false',
134
- onclick: onToggle ? () => onToggle() : null
135
- }, enabled ? 'Disable' : 'Enable')
136
- )
137
- );
138
- }
139
-
140
- function seg({ label, children, className = '' }) {
141
- return h('div', { class: 'vx-section ' + className },
142
- label != null ? h('div', { class: 'vx-section-label' }, label) : null,
143
- ...(Array.isArray(children) ? children : [children])
144
- );
145
- }
146
-
147
- function devSelect(value, devices, onChange, aria) {
148
- return h('select', {
149
- class: 'vx-select', 'aria-label': aria,
150
- onchange: onChange ? (e) => onChange(e.target.value) : null
151
- }, ...(devices || []).map(d =>
152
- h('option', { key: 'd-' + d.value, value: d.value, selected: d.value === value }, d.label)));
153
- }
154
-
155
- function toggleRow(label, checked, onToggle) {
156
- return h('label', { class: 'vx-toggle-row' },
157
- h('span', {}, label),
158
- h('input', {
159
- type: 'checkbox', class: 'vx-toggle',
160
- checked: checked ? true : null,
161
- onchange: onToggle ? (e) => onToggle(e.target.checked) : null
162
- })
163
- );
164
- }
165
-
166
- export function VoiceSettingsModal({ open = false, mode = 'ptt', inputId, outputId, inputDevices = [], outputDevices = [], vadThreshold = 0.5, rnnoise = false, autoGain = false, forceTurn = false, bitrate = 64, volume, onChange, onSave, onCancel, onClose } = {}) {
167
- if (!open) return null;
168
- const patch = (p) => onChange && onChange(p);
169
- const modes = ['ptt', 'vad', 'live'];
170
- const vol = volume == null ? 1 : volume;
171
- return h('div', {
172
- class: 'vx-modal-backdrop',
173
- onclick: (e) => { if (e.target === e.currentTarget) onClose && onClose(); },
174
- onkeydown: (e) => { if (e.key === 'Escape') { e.preventDefault(); onClose && onClose(); } }
175
- },
176
- h('div', { class: 'vx-modal', role: 'dialog', 'aria-modal': 'true', 'aria-label': 'Voice settings' },
177
- h('div', { class: 'vx-modal-head' },
178
- h('h2', { class: 'vx-modal-title' }, 'Voice settings'),
179
- h('button', { type: 'button', class: 'vx-modal-x', 'aria-label': 'close', onclick: () => onClose && onClose() }, Icon('x'))
180
- ),
181
- h('div', { class: 'vx-modal-body' },
182
- seg({ label: 'Mode', children:
183
- h('div', { class: 'vx-segmented', role: 'group', 'aria-label': 'mode' },
184
- ...modes.map(m => h('button', {
185
- key: 'm-' + m, type: 'button',
186
- class: 'vx-seg' + (m === mode ? ' vx-seg-on' : ''),
187
- 'aria-pressed': m === mode ? 'true' : 'false',
188
- onclick: () => patch({ mode: m })
189
- }, m.toUpperCase())))
190
- }),
191
- seg({ label: 'Input device', children: devSelect(inputId, inputDevices, (v) => patch({ inputId: v }), 'input device') }),
192
- seg({ label: 'Output device', children: devSelect(outputId, outputDevices, (v) => patch({ outputId: v }), 'output device') }),
193
- mode === 'vad' ? seg({ label: 'VAD threshold', children:
194
- h('div', { class: 'vx-range-row' },
195
- h('input', {
196
- type: 'range', class: 'vx-range', min: '0', max: '1', step: '0.01',
197
- value: String(vadThreshold), 'aria-label': 'VAD threshold',
198
- oninput: (e) => patch({ vadThreshold: parseFloat(e.target.value) })
199
- }),
200
- h('span', { class: 'vx-range-val' }, Math.round((Number(vadThreshold) || 0) * 100) + '%')
201
- )
202
- }) : null,
203
- seg({ label: 'Processing', children: [
204
- toggleRow('RNNoise', rnnoise, (v) => patch({ rnnoise: v })),
205
- toggleRow('Auto gain', autoGain, (v) => patch({ autoGain: v })),
206
- toggleRow('Force TURN', forceTurn, (v) => patch({ forceTurn: v }))
207
- ]}),
208
- seg({ label: 'Bitrate', children:
209
- h('div', { class: 'vx-range-row' },
210
- h('input', {
211
- type: 'range', class: 'vx-range', min: '8', max: '256', step: '8',
212
- value: String(bitrate), 'aria-label': 'bitrate',
213
- oninput: (e) => patch({ bitrate: parseInt(e.target.value, 10) })
214
- }),
215
- h('span', { class: 'vx-range-val' }, (Number(bitrate) || 0) + ' kbps')
216
- )
217
- }),
218
- seg({ label: 'Master volume', children:
219
- h('div', { class: 'vx-range-row' },
220
- h('input', {
221
- type: 'range', class: 'vx-range', min: '0', max: '1', step: '0.01',
222
- value: String(vol), 'aria-label': 'master volume',
223
- oninput: (e) => patch({ volume: parseFloat(e.target.value) })
224
- }),
225
- h('span', { class: 'vx-range-val' }, Math.round(vol * 100) + '%')
226
- )
227
- })
228
- ),
229
- h('div', { class: 'vx-modal-foot' },
230
- h('button', { type: 'button', class: 'vx-btn', onclick: () => onCancel && onCancel() }, 'Cancel'),
231
- h('button', { type: 'button', class: 'vx-btn vx-btn-primary', onclick: () => onSave && onSave() }, 'Save')
232
- )
233
- )
234
- );
235
- }
236
-
237
- export function VoiceControls({ muted = false, deafened = false, cameraOn = false, screenShareOn = false, onMic, onDeafen, onCamera, onScreenShare, onSettings, onLeave } = {}) {
238
- const btn = (cls, on, label, glyph, handler) => h('button', {
239
- type: 'button',
240
- class: 'vx-vc-btn ' + cls + (on ? ' vx-vc-on' : '') + (handler ? '' : ' vx-vc-disabled'),
241
- 'aria-pressed': on ? 'true' : 'false',
242
- 'aria-label': label,
243
- title: label,
244
- disabled: handler ? null : true,
245
- onclick: handler ? (e) => handler(e) : null
246
- },
247
- h('span', { class: 'vx-vc-glyph', 'aria-hidden': 'true' }, glyph)
248
- );
249
- return h('div', { class: 'vx-vc', role: 'toolbar', 'aria-label': 'voice controls' },
250
- btn('vx-vc-mic', !muted, muted ? 'Unmute' : 'Mute', Icon(muted ? 'mic-off' : 'mic'), onMic),
251
- btn('vx-vc-deafen', !deafened, deafened ? 'Undeafen' : 'Deafen', Icon(deafened ? 'speaker-off' : 'speaker'), onDeafen),
252
- btn('vx-vc-camera', cameraOn, cameraOn ? 'Stop camera' : 'Start camera', Icon('camera'), onCamera),
253
- btn('vx-vc-screen', screenShareOn, screenShareOn ? 'Stop sharing' : 'Share screen', Icon('screen'), onScreenShare),
254
- btn('vx-vc-settings', false, 'Voice settings', Icon('settings'), onSettings),
255
- h('button', {
256
- type: 'button', class: 'vx-vc-btn vx-vc-leave', 'aria-label': 'Leave voice', title: 'Leave voice',
257
- onclick: onLeave ? (e) => onLeave(e) : null
258
- }, h('span', { class: 'vx-vc-glyph', 'aria-hidden': 'true' }, Icon('phone')))
259
- );
260
- }
261
-
262
- export function AudioQueue({ segments = [], currentSegmentId = null, paused = false, onReplay, onSkip, onResume, onPause } = {}) {
263
- if (!segments || !segments.length) {
264
- return h('div', { class: 'vx-queue vx-queue-empty' },
265
- h('span', { class: 'vx-queue-empty-text' }, 'No audio queued'));
266
- }
267
- return h('div', { class: 'vx-queue', role: 'group', 'aria-label': 'audio queue' },
268
- h('div', { class: 'vx-queue-ctrls' },
269
- h('button', {
270
- type: 'button', class: 'vx-queue-btn',
271
- 'aria-label': paused ? 'resume' : 'pause',
272
- onclick: () => paused ? (onResume && onResume()) : (onPause && onPause())
273
- }, paused ? Icon('play') : Icon('pause')),
274
- h('button', {
275
- type: 'button', class: 'vx-queue-btn',
276
- 'aria-label': 'skip', onclick: () => onSkip && onSkip()
277
- }, Icon('skip-forward'))
278
- ),
279
- h('div', { class: 'vx-queue-strip' },
280
- ...segments.map(s => h('button', {
281
- key: 'q-' + s.id,
282
- type: 'button',
283
- class: 'vx-chip' + (s.id === currentSegmentId ? ' vx-chip-current' : '') + (s.isLive ? ' vx-chip-live' : ''),
284
- 'data-id': s.id,
285
- onclick: () => onReplay && onReplay(s.id)
286
- },
287
- h('span', { class: 'vx-chip-dot', style: s.color ? 'background:' + s.color : null, 'aria-hidden': 'true' }),
288
- h('span', { class: 'vx-chip-name' }, s.speaker || '—'),
289
- s.isLive
290
- ? h('span', { class: 'vx-chip-tag' }, 'LIVE')
291
- : h('span', { class: 'vx-chip-dur' }, fmtDur(s.duration))
292
- ))
293
- )
294
- );
295
- }
3
+ //
4
+ // This module is a barrel: every component lives in a single-responsibility
5
+ // submodule under ./voice/, and the public export surface here is unchanged
6
+ // no consumer import needs to move.
7
+
8
+ import { playCompletionCue } from './voice/audio-cue.js';
9
+ import { PttButton, VadMeter, WebcamPreview } from './voice/capture.js';
10
+ import { VoiceSettingsModal } from './voice/settings-modal.js';
11
+ import { VoiceControls, AudioQueue } from './voice/playback.js';
12
+
13
+ export {
14
+ playCompletionCue,
15
+ PttButton, VadMeter, WebcamPreview,
16
+ VoiceSettingsModal,
17
+ VoiceControls, AudioQueue,
18
+ };