mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-09 11:06:02 +08:00
feat(flightlab): sidebar, progress bars, skip speech, SimBridge telemetry & auth
- Add collapsible sidebar with phase stepper (jump between phases) - Add SimBridge conditions panel in sidebar (live values, progress bars, targets) - Add global progress bar (top edge, glowing) + phase-local TTS progress bar - Add skip button to skip TTS speech while ATC is speaking - Add skipSpeech() to audio composable (stops current Pizzicato sound) - Wire up bridge data.post.ts with user auth (JWT) + example payload - Add server-side telemetry store with pub/sub for Bridge→WS relay - Extend WS handler with subscribe-telemetry message + userId tracking - Extend sync composable with subscribeTelemetry() + onTelemetry() callback - Add require-auth middleware to all flightlab pages - Fix instructor station ECONNREFUSED via import.meta.client guard - Add animations: phase transitions, button lists, fade-scale, check-pop, pulse Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ export function useFlightLabAudio() {
|
||||
const soundBuffers = ref<Map<string, AudioBuffer>>(new Map())
|
||||
let speechQueue: Promise<void> = Promise.resolve()
|
||||
let pizzicato: PizzicatoLiteType | null = null
|
||||
let currentSpeechReject: (() => void) | null = null
|
||||
|
||||
// --- Replay Cache ---
|
||||
const lastSpokenAudio = ref<{ base64: string; mime: string; readability: number; text: string } | null>(null)
|
||||
@@ -151,6 +152,9 @@ export function useFlightLabAudio() {
|
||||
})
|
||||
}
|
||||
|
||||
let currentPizzicatoSound: any = null
|
||||
let currentNoiseStoppers: Array<() => void> = []
|
||||
|
||||
async function playWithRadioEffects(base64: string, _mime: string, readability: number) {
|
||||
const pz = await loadPizzicato()
|
||||
if (!pz) return
|
||||
@@ -158,6 +162,7 @@ export function useFlightLabAudio() {
|
||||
const profile = getReadabilityProfile(readability)
|
||||
|
||||
const sound = await pz.createSoundFromBase64(ctx, base64)
|
||||
currentPizzicatoSound = sound
|
||||
|
||||
// Apply radio filter chain
|
||||
sound.addEffect(new pz.Effects.HighPassFilter(ctx, { frequency: profile.eq.highpass, q: profile.eq.highpassQ }))
|
||||
@@ -185,9 +190,28 @@ export function useFlightLabAudio() {
|
||||
sound.setVolume(profile.gain)
|
||||
|
||||
const stopNoise = createNoiseGenerators(ctx, sound.duration, profile, readability)
|
||||
currentNoiseStoppers = stopNoise
|
||||
|
||||
await sound.play()
|
||||
stopNoise.forEach((fn: () => void) => fn())
|
||||
currentPizzicatoSound = null
|
||||
currentNoiseStoppers = []
|
||||
}
|
||||
|
||||
/** Skip currently playing TTS speech immediately */
|
||||
function skipSpeech() {
|
||||
if (!isSpeaking.value) return
|
||||
// Stop the pizzicato sound
|
||||
if (currentPizzicatoSound) {
|
||||
try { currentPizzicatoSound.stop() } catch {}
|
||||
currentPizzicatoSound = null
|
||||
}
|
||||
// Stop noise generators
|
||||
currentNoiseStoppers.forEach((fn) => { try { fn() } catch {} })
|
||||
currentNoiseStoppers = []
|
||||
isSpeaking.value = false
|
||||
// Reset the speech queue so next speech can start fresh
|
||||
speechQueue = Promise.resolve()
|
||||
}
|
||||
|
||||
async function replayLastMessage(): Promise<void> {
|
||||
@@ -248,6 +272,7 @@ export function useFlightLabAudio() {
|
||||
crossfadeSound,
|
||||
stopAllSounds,
|
||||
setMasterVolume,
|
||||
skipSpeech,
|
||||
dispose,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ export function useFlightLabSync() {
|
||||
onInstructorMessage: [] as Array<(text: string, withRadioEffect: boolean) => void>,
|
||||
onPeerJoined: [] as Array<(peerRole: FlightLabRole) => void>,
|
||||
onPeerLeft: [] as Array<(peerRole: FlightLabRole) => void>,
|
||||
onTelemetry: [] as Array<(data: any) => void>,
|
||||
onError: [] as Array<(msg: string) => void>,
|
||||
}
|
||||
|
||||
@@ -70,6 +71,9 @@ export function useFlightLabSync() {
|
||||
case 'peer-left':
|
||||
callbacks.onPeerLeft.forEach(cb => cb(data.role))
|
||||
break
|
||||
case 'telemetry':
|
||||
callbacks.onTelemetry.forEach(cb => cb(data.data))
|
||||
break
|
||||
case 'error':
|
||||
callbacks.onError.forEach(cb => cb(data.message))
|
||||
break
|
||||
@@ -92,6 +96,11 @@ export function useFlightLabSync() {
|
||||
send({ type: 'join-session', code: code.toUpperCase(), role: joinRole })
|
||||
}
|
||||
|
||||
/** Subscribe this session to receive telemetry for the given userId */
|
||||
function subscribeTelemetry(userId: string) {
|
||||
send({ type: 'subscribe-telemetry', userId })
|
||||
}
|
||||
|
||||
function sendParticipantAction(phaseId: string, buttonId: string, nextPhaseId: string) {
|
||||
send({ type: 'participant-action', phaseId, buttonId, nextPhaseId })
|
||||
}
|
||||
@@ -117,6 +126,7 @@ export function useFlightLabSync() {
|
||||
function onInstructorMessage(cb: (text: string, withRadioEffect: boolean) => void) { callbacks.onInstructorMessage.push(cb) }
|
||||
function onPeerJoined(cb: (role: FlightLabRole) => void) { callbacks.onPeerJoined.push(cb) }
|
||||
function onPeerLeft(cb: (role: FlightLabRole) => void) { callbacks.onPeerLeft.push(cb) }
|
||||
function onTelemetry(cb: (data: any) => void) { callbacks.onTelemetry.push(cb) }
|
||||
function onError(cb: (msg: string) => void) { callbacks.onError.push(cb) }
|
||||
|
||||
return {
|
||||
@@ -126,6 +136,7 @@ export function useFlightLabSync() {
|
||||
remoteState,
|
||||
createSession,
|
||||
joinSession,
|
||||
subscribeTelemetry,
|
||||
sendParticipantAction,
|
||||
sendInstructorCommand,
|
||||
sendInstructorMessage,
|
||||
@@ -134,6 +145,7 @@ export function useFlightLabSync() {
|
||||
onInstructorMessage,
|
||||
onPeerJoined,
|
||||
onPeerLeft,
|
||||
onTelemetry,
|
||||
onError,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user