mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 03:14:00 +00:00
feat(voice): render the voice control from a presenter
createVoiceControl now returns a presenter instead of building the control with innerHTML: the session, cost, input, card and panel owners write its state by part and listen for interactions, and VoiceControl.vue renders it in the command dock, between the location bar and the visual presets as before. Focus moves wait for the render; the waveform's bars publish apart from the control and only when they change; the minutes field keeps a draft until its owner writes it. Owner tests use a real presenter on their own store, and view tests render the control.
This commit is contained in:
1 parent
2ffd7a4afd
commit
1de4f635ca
47 files changed
+1691
-1645
No files matched your search
+31
-36
@@ -11,8 +11,8 @@ import {
|
||||
resolveVoiceVisualizerSpeaker,
|
||||
resolveVoiceControlHint,
|
||||
gateVoiceVisualizerLevel,
|
||||
resetVoiceVisualizerBars,
|
||||
} from './realtimeInputPolicy.js';
|
||||
import { VOICE_VISUALIZER_BARS } from './control.js';
|
||||
import { shouldPauseRadioForVoice } from './realtimeProtocol.js';
|
||||
|
||||
/** Own physical push-to-talk gestures, microphone controls and audio meters. */
|
||||
@@ -116,7 +116,7 @@ export class RealtimeInput {
|
||||
// off if it is talking. Short taps never reach this point.
|
||||
if (mayInterrupt) this.bargeIn?.();
|
||||
this.beginPointerTurn?.('keydown');
|
||||
this.ui.root.dataset.pushToTalk = 'held';
|
||||
this.ui.update('root', { pushToTalk: 'held' });
|
||||
this.setMicrophoneEnabled(true);
|
||||
if (this.status === 'listening')
|
||||
this.setStatus('listening', t('Release Space to send'));
|
||||
@@ -124,7 +124,8 @@ export class RealtimeInput {
|
||||
this.start({ pushToTalk: true });
|
||||
// start() performs a controlled stop() before connecting. Restore the
|
||||
// marker it clears so the UI reflects this still-held gesture.
|
||||
if (this.pushToTalkKeyHeld) this.ui.root.dataset.pushToTalk = 'held';
|
||||
if (this.pushToTalkKeyHeld)
|
||||
this.ui.update('root', { pushToTalk: 'held' });
|
||||
}
|
||||
}, PUSH_TO_TALK_HOLD_DELAY_MS);
|
||||
};
|
||||
@@ -191,7 +192,7 @@ export class RealtimeInput {
|
||||
this.releasePointerHold?.();
|
||||
if (!this.pushToTalkKeyHeld) return;
|
||||
this.pushToTalkKeyHeld = false;
|
||||
delete this.ui.root.dataset.pushToTalk;
|
||||
this.ui.update('root', { pushToTalk: null });
|
||||
if (!this.pushToTalkMode) return;
|
||||
this.setMicrophoneEnabled(false);
|
||||
if (this.status === 'listening')
|
||||
@@ -205,8 +206,7 @@ export class RealtimeInput {
|
||||
* @returns {void}
|
||||
*/
|
||||
setMicrophoneEnabled(enabled) {
|
||||
if (this.ui?.root)
|
||||
this.ui.root.dataset.microphone = enabled ? 'active' : 'muted';
|
||||
this.ui?.update('root', { microphone: enabled ? 'active' : 'muted' });
|
||||
this.stream?.getAudioTracks?.().forEach((track) => {
|
||||
track.enabled = Boolean(enabled);
|
||||
});
|
||||
@@ -220,10 +220,7 @@ export class RealtimeInput {
|
||||
startVoiceVisualizer(stream) {
|
||||
this.stopVoiceVisualizer();
|
||||
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
|
||||
const bars = Array.from(
|
||||
this.ui.root.querySelectorAll('.gev-voice-visualizer span'),
|
||||
);
|
||||
if (!AudioContextClass || !stream || !bars.length) return;
|
||||
if (!AudioContextClass || !stream || !this.ui) return;
|
||||
try {
|
||||
const context = new AudioContextClass();
|
||||
this.visualizerAudioContext = context;
|
||||
@@ -252,17 +249,18 @@ export class RealtimeInput {
|
||||
},
|
||||
);
|
||||
if (!signal) {
|
||||
resetVoiceVisualizerBars(bars);
|
||||
this.restVoiceVisualizer();
|
||||
this.visualizerFrame = requestAnimationFrame(render);
|
||||
return;
|
||||
}
|
||||
signal.analyser.getByteFrequencyData(signal.data);
|
||||
const binCount = signal.data.length;
|
||||
bars.forEach((bar, index) => {
|
||||
const start = Math.floor((index / bars.length) * binCount);
|
||||
const count = VOICE_VISUALIZER_BARS;
|
||||
const bars = Array.from({ length: count }, (_, index) => {
|
||||
const start = Math.floor((index / count) * binCount);
|
||||
const end = Math.max(
|
||||
start + 1,
|
||||
Math.floor(((index + 1) / bars.length) * binCount),
|
||||
Math.floor(((index + 1) / count) * binCount),
|
||||
);
|
||||
let energy = 0;
|
||||
for (let bin = start; bin < end; bin++) energy += signal.data[bin];
|
||||
@@ -275,15 +273,12 @@ export class RealtimeInput {
|
||||
gateVoiceVisualizerLevel(normalized, gate),
|
||||
0.72,
|
||||
);
|
||||
bar.style.setProperty(
|
||||
'--audio-level',
|
||||
`${Math.round(5 + shaped * 29)}px`,
|
||||
);
|
||||
bar.style.setProperty(
|
||||
'--audio-opacity',
|
||||
`${(0.5 + shaped * 0.5).toFixed(2)}`,
|
||||
);
|
||||
return {
|
||||
level: `${Math.round(5 + shaped * 29)}px`,
|
||||
opacity: `${(0.5 + shaped * 0.5).toFixed(2)}`,
|
||||
};
|
||||
});
|
||||
this.ui?.setBars(bars);
|
||||
this.visualizerFrame = requestAnimationFrame(render);
|
||||
};
|
||||
render();
|
||||
@@ -353,9 +348,12 @@ export class RealtimeInput {
|
||||
this.visualizerAudioContext.close().catch(() => {});
|
||||
this.visualizerAudioContext = null;
|
||||
}
|
||||
resetVoiceVisualizerBars(
|
||||
this.ui?.root?.querySelectorAll('.gev-voice-visualizer span'),
|
||||
);
|
||||
this.restVoiceVisualizer();
|
||||
}
|
||||
|
||||
/** Return the waveform to its CSS standby baseline. */
|
||||
restVoiceVisualizer() {
|
||||
this.ui?.setBars(null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -363,15 +361,15 @@ export class RealtimeInput {
|
||||
* @returns {void}
|
||||
*/
|
||||
updateVoiceButtonLabel() {
|
||||
if (!this.ui.buttonLabel) return;
|
||||
if (!this.ui) return;
|
||||
// The mic button's caption; the first-run tip names it the same way.
|
||||
this.ui.buttonLabel.textContent = t('MIC');
|
||||
if (this.ui.helpDetail) {
|
||||
this.ui.helpDetail.textContent = resolveVoiceControlHint(
|
||||
this.ui.update('mic', { text: t('MIC') });
|
||||
this.ui.update('help', {
|
||||
text: resolveVoiceControlHint(
|
||||
this.pushToTalkMode,
|
||||
this.pushToTalkKeyHeld,
|
||||
);
|
||||
}
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
setVoiceSpeaker(speaker, { keepVisualizerSpeaker = false } = {}) {
|
||||
@@ -382,7 +380,7 @@ export class RealtimeInput {
|
||||
nextSpeaker,
|
||||
keepVisualizerSpeaker,
|
||||
);
|
||||
this.ui.root.dataset.speaker = nextSpeaker;
|
||||
this.ui.update('root', { speaker: nextSpeaker });
|
||||
if (shouldPauseRadioForVoice({ speaker: nextSpeaker }))
|
||||
this.pauseRadioForVoice();
|
||||
}
|
||||
@@ -392,10 +390,7 @@ export class RealtimeInput {
|
||||
this.pushToTalkKeyHeld = false;
|
||||
this.spaceKeyHeld = false;
|
||||
this.resetPushToTalkGesture();
|
||||
if (this.ui?.root) {
|
||||
delete this.ui.root.dataset.pushToTalk;
|
||||
delete this.ui.root.dataset.microphone;
|
||||
}
|
||||
this.ui?.update('root', { pushToTalk: null, microphone: null });
|
||||
}
|
||||
|
||||
detachBindings() {
|
||||
|
||||
Reference in new issue
Block a user