mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-01 06:06:05 +08:00
refactor(live-atc): extract debug simulation and trace inspectors to useDebugSimulation
Moves debugMode, the scripted end-to-end simulation (simulationPilotSteps, simulationDecisions, runFullSimulation and its helpers), the decision-trace computeds the debug drawer renders, and debugState/debugNextStates out of live-atc.vue. The page keeps only the destructured bindings its debug template still reads; once DebugPanel.vue exists it constructs this composable itself and the page stops touching it entirely. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
438
app/composables/useDebugSimulation.ts
Normal file
438
app/composables/useDebugSimulation.ts
Normal file
@@ -0,0 +1,438 @@
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
import useCommunicationsEngine from '../../shared/utils/communicationsEngine'
|
||||
|
||||
export interface DebugSimulationDeps {
|
||||
setLastTransmission: (text: string) => void
|
||||
clearLog: () => void
|
||||
startDemoFlight: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug drawer state: the scripted end-to-end flow simulation, the decision
|
||||
* trace the panel renders, and the current/next-state inspectors.
|
||||
*
|
||||
* Only the debug panel constructs this — nothing on the normal cockpit path
|
||||
* depends on it.
|
||||
*/
|
||||
export function useDebugSimulation(
|
||||
engine: ReturnType<typeof useCommunicationsEngine>,
|
||||
deps: DebugSimulationDeps,
|
||||
) {
|
||||
const {
|
||||
currentState, nextCandidates, variables: vars, flags, lastDecisionTrace,
|
||||
getStateDetails, moveTo: forceMove, normalizeATCText, renderATCMessage,
|
||||
processPilotTransmission, buildLLMContext, applyLLMDecision,
|
||||
} = engine
|
||||
const { setLastTransmission, clearLog, startDemoFlight } = deps
|
||||
|
||||
const debugMode = ref(true)
|
||||
|
||||
const simulationRunning = ref(false)
|
||||
const simulationTrace = ref<SimulationTraceEntry[]>([])
|
||||
const simulationError = ref('')
|
||||
|
||||
type SimulationDecisionTemplate = {
|
||||
next: string
|
||||
controllerSayState?: string
|
||||
controllerSayTpl?: string
|
||||
updates?: Record<string, any>
|
||||
note?: string
|
||||
}
|
||||
|
||||
type SimulationTraceEntry = {
|
||||
kind: 'info' | 'pilot' | 'atc' | 'llm-input' | 'llm-output'
|
||||
id: string
|
||||
label: string
|
||||
payload?: any
|
||||
}
|
||||
|
||||
const completedPilotSteps = computed(() => simulationTrace.value.filter(entry => entry.kind === 'pilot').length)
|
||||
|
||||
const simulationPilotSteps = [
|
||||
'CD_CHECK_ATIS',
|
||||
'CD_VERIFY_READBACK',
|
||||
'GRD_READY_FOR_PUSH',
|
||||
'GRD_TAXI_REQUEST',
|
||||
'GRD_TAXI_READBACK',
|
||||
'TWR_LINEUP_REQ',
|
||||
'TWR_TAKEOFF_READBACK',
|
||||
'DEP_IDENT',
|
||||
'DEP_CLIMB_READBACK',
|
||||
'DES_READBACK',
|
||||
'APP_ESTABLISHED',
|
||||
'TWR_LAND_READBACK',
|
||||
'GRD_TAXI_IN_REQ',
|
||||
'GRD_TAXI_IN_READBACK'
|
||||
] as const
|
||||
|
||||
type SimulationPilotState = typeof simulationPilotSteps[number]
|
||||
|
||||
const simulationDecisions: Record<SimulationPilotState, SimulationDecisionTemplate> = {
|
||||
CD_CHECK_ATIS: {
|
||||
next: 'CD_ISSUE_CLR',
|
||||
controllerSayState: 'CD_ISSUE_CLR',
|
||||
updates: {
|
||||
push_available: true,
|
||||
runway_occupied: false,
|
||||
pilot_able: true,
|
||||
runway_available: true,
|
||||
push_delay_min: 0,
|
||||
surface_wind: '220/05',
|
||||
speed_restriction: '210 knots',
|
||||
emergency_heading: '180'
|
||||
}
|
||||
},
|
||||
CD_VERIFY_READBACK: {
|
||||
next: 'CD_READBACK_CHECK'
|
||||
},
|
||||
GRD_READY_FOR_PUSH: {
|
||||
next: 'GRD_PUSH_APPROVE',
|
||||
controllerSayState: 'GRD_PUSH_APPROVE'
|
||||
},
|
||||
GRD_TAXI_REQUEST: {
|
||||
next: 'GRD_TAXI_INSTR',
|
||||
controllerSayState: 'GRD_TAXI_INSTR'
|
||||
},
|
||||
GRD_TAXI_READBACK: {
|
||||
next: 'TWR_CONTACT',
|
||||
controllerSayState: 'TWR_CONTACT'
|
||||
},
|
||||
TWR_LINEUP_REQ: {
|
||||
next: 'TWR_TAKEOFF_CLR',
|
||||
controllerSayState: 'TWR_TAKEOFF_CLR'
|
||||
},
|
||||
TWR_TAKEOFF_READBACK: {
|
||||
next: 'DEP_CONTACT',
|
||||
controllerSayState: 'DEP_CONTACT'
|
||||
},
|
||||
DEP_IDENT: {
|
||||
next: 'DEP_CLIMB_INSTR',
|
||||
controllerSayState: 'DEP_CLIMB_INSTR'
|
||||
},
|
||||
DEP_CLIMB_READBACK: {
|
||||
next: 'ENR_HANDOFF',
|
||||
controllerSayState: 'ENR_HANDOFF'
|
||||
},
|
||||
DES_READBACK: {
|
||||
next: 'APP_HANDOFF',
|
||||
controllerSayState: 'APP_HANDOFF',
|
||||
updates: {
|
||||
speed_restriction: '180 knots'
|
||||
}
|
||||
},
|
||||
APP_ESTABLISHED: {
|
||||
next: 'TWR_LAND_CONTACT',
|
||||
controllerSayState: 'TWR_LAND_CONTACT',
|
||||
updates: {
|
||||
runway_available: true,
|
||||
surface_wind: '210/06'
|
||||
}
|
||||
},
|
||||
TWR_LAND_READBACK: {
|
||||
next: 'TWR_VACATE',
|
||||
controllerSayState: 'TWR_VACATE'
|
||||
},
|
||||
GRD_TAXI_IN_REQ: {
|
||||
next: 'GRD_TAXI_INSTR_IN',
|
||||
controllerSayState: 'GRD_TAXI_INSTR_IN'
|
||||
},
|
||||
GRD_TAXI_IN_READBACK: {
|
||||
next: 'FLOW_COMPLETE'
|
||||
}
|
||||
}
|
||||
|
||||
const recordedAtcStates = new Set<string>()
|
||||
const simulationStepCount = simulationPilotSteps.length
|
||||
|
||||
const wait = (ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms))
|
||||
|
||||
const decisionTrace = computed(() => lastDecisionTrace.value)
|
||||
const timelineSteps = computed(() => decisionTrace.value?.candidateTimeline?.steps ?? [])
|
||||
const timelineUsedFallback = computed(() => Boolean(decisionTrace.value?.candidateTimeline?.fallbackUsed))
|
||||
const traceAutoSelection = computed(() => decisionTrace.value?.autoSelection ?? null)
|
||||
const traceFallback = computed(() => decisionTrace.value?.fallback ?? null)
|
||||
|
||||
const cloneForTrace = <T>(value: T): T => {
|
||||
if (value === undefined || value === null) {
|
||||
return value
|
||||
}
|
||||
try {
|
||||
return JSON.parse(JSON.stringify(value))
|
||||
} catch (err) {
|
||||
console.warn('Failed to clone trace payload, returning original value.', err)
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
function describeElimination(entry: any): string {
|
||||
if (!entry || typeof entry !== 'object') {
|
||||
return ''
|
||||
}
|
||||
if (entry.kind === 'regex' && entry.context?.patterns?.length) {
|
||||
const patterns = entry.context.patterns
|
||||
.map((pattern: any) => pattern?.pattern)
|
||||
.filter((value: string | undefined) => Boolean(value))
|
||||
.join(', ')
|
||||
return patterns ? `Patterns: ${patterns}` : entry.reason
|
||||
}
|
||||
if (entry.kind === 'condition' && entry.context?.condition) {
|
||||
const condition = entry.context.condition
|
||||
if (condition.type === 'regex' || condition.type === 'regex_not') {
|
||||
const flag = condition.pattern ? `/${condition.pattern}/${condition.patternFlags || 'i'}` : ''
|
||||
return flag ? `Condition: ${condition.type} ${flag}` : entry.reason
|
||||
}
|
||||
const variable = condition.variable || 'value'
|
||||
const operator = condition.operator || '=='
|
||||
const expected = entry.context?.expectedValue ?? condition.value ?? '—'
|
||||
const actual = entry.context?.actualValue ?? '—'
|
||||
return `${variable} ${operator} ${expected} (actual: ${actual})`
|
||||
}
|
||||
return entry.reason
|
||||
}
|
||||
|
||||
const debugState = computed(() => {
|
||||
if (!currentState.value) return null
|
||||
const sayTpl = currentState.value.say_tpl
|
||||
return {
|
||||
id: currentState.value.id,
|
||||
phase: currentState.value.phase,
|
||||
role: currentState.value.role,
|
||||
frequencyName: currentState.value.frequencyName,
|
||||
sayPlain: sayTpl ? renderATCMessage(sayTpl) : '',
|
||||
sayNormalized: sayTpl ? normalizeATCText(sayTpl, { ...vars.value, ...flags.value }) : ''
|
||||
}
|
||||
})
|
||||
|
||||
const debugNextStates = computed(() => {
|
||||
return nextCandidates.value.map(id => {
|
||||
const state = getStateDetails(id)
|
||||
if (!state) {
|
||||
return { id, role: '', phase: '', frequencyName: undefined, sayPlain: '', sayNormalized: '' }
|
||||
}
|
||||
const sayPlain = state.say_tpl ? renderATCMessage(state.say_tpl) : ''
|
||||
const sayNormalized = state.say_tpl ? normalizeATCText(state.say_tpl, { ...vars.value, ...flags.value }) : ''
|
||||
return {
|
||||
id,
|
||||
role: state.role,
|
||||
phase: state.phase,
|
||||
frequencyName: state.frequencyName,
|
||||
sayPlain,
|
||||
sayNormalized
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const formatTracePayload = (payload: any): string => {
|
||||
if (payload === null || payload === undefined) return ''
|
||||
if (typeof payload === 'string') return payload
|
||||
try {
|
||||
return JSON.stringify(payload, null, 2)
|
||||
} catch (err) {
|
||||
return String(payload)
|
||||
}
|
||||
}
|
||||
|
||||
const recordCurrentAtcMessage = () => {
|
||||
const state = currentState.value
|
||||
if (!state?.id || recordedAtcStates.has(state.id) || !state.say_tpl) {
|
||||
return
|
||||
}
|
||||
|
||||
const plain = renderATCMessage(state.say_tpl)
|
||||
const normalized = normalizeATCText(state.say_tpl, { ...vars.value, ...flags.value })
|
||||
|
||||
simulationTrace.value.push({
|
||||
kind: 'atc',
|
||||
id: state.id,
|
||||
label: `ATC • ${state.phase}`,
|
||||
payload: { text: plain, normalized }
|
||||
})
|
||||
|
||||
setLastTransmission(`ATC: ${plain}`)
|
||||
recordedAtcStates.add(state.id)
|
||||
}
|
||||
|
||||
const pickNextStateId = (state: ReturnType<typeof getStateDetails> | null): string | null => {
|
||||
if (!state) return null
|
||||
|
||||
const chains: Array<Array<{ to: string }>> = []
|
||||
if (state.ok_next?.length) chains.push(state.ok_next.map(({ to }) => ({ to })))
|
||||
if (state.next?.length) chains.push(state.next.map(({ to }) => ({ to })))
|
||||
if (state.bad_next?.length) chains.push(state.bad_next.map(({ to }) => ({ to })))
|
||||
if (state.timer_next?.length) chains.push(state.timer_next.map(({ to }) => ({ to })))
|
||||
|
||||
for (const list of chains) {
|
||||
for (const entry of list) {
|
||||
if (entry?.to) return entry.to
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const advanceAutomaticStates = async () => {
|
||||
let guard = 0
|
||||
|
||||
while (guard++ < 50) {
|
||||
const state = currentState.value
|
||||
if (!state?.id) break
|
||||
if (state.auto === 'end') break
|
||||
|
||||
const autoMode = Boolean(state.auto && state.auto !== 'end')
|
||||
if (!autoMode && state.role === 'pilot') break
|
||||
|
||||
const nextId = pickNextStateId(state)
|
||||
if (!nextId || nextId === state.id) break
|
||||
|
||||
forceMove(nextId)
|
||||
await nextTick()
|
||||
recordCurrentAtcMessage()
|
||||
}
|
||||
}
|
||||
|
||||
const runFullSimulation = async () => {
|
||||
if (simulationRunning.value) return
|
||||
|
||||
simulationRunning.value = true
|
||||
simulationTrace.value = []
|
||||
simulationError.value = ''
|
||||
recordedAtcStates.clear()
|
||||
debugMode.value = true
|
||||
|
||||
try {
|
||||
simulationTrace.value.push({
|
||||
kind: 'info',
|
||||
id: 'init',
|
||||
label: 'Simulation Start',
|
||||
payload: { timestamp: new Date().toISOString(), steps: simulationStepCount }
|
||||
})
|
||||
|
||||
startDemoFlight()
|
||||
await nextTick()
|
||||
clearLog()
|
||||
recordedAtcStates.clear()
|
||||
|
||||
if (currentState.value?.id !== 'CD_CHECK_ATIS') {
|
||||
forceMove('CD_CHECK_ATIS')
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
for (const stepId of simulationPilotSteps) {
|
||||
const state = getStateDetails(stepId)
|
||||
if (!state?.utterance_tpl) {
|
||||
throw new Error(`Missing pilot utterance for ${stepId}`)
|
||||
}
|
||||
|
||||
if (currentState.value?.id !== stepId) {
|
||||
forceMove(stepId)
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
await wait(120)
|
||||
|
||||
const pilotText = renderATCMessage(state.utterance_tpl)
|
||||
const pilotNormalized = normalizeATCText(state.utterance_tpl, { ...vars.value, ...flags.value })
|
||||
|
||||
simulationTrace.value.push({
|
||||
kind: 'pilot',
|
||||
id: stepId,
|
||||
label: `Pilot • ${state.phase}`,
|
||||
payload: { text: pilotText, normalized: pilotNormalized }
|
||||
})
|
||||
|
||||
setLastTransmission(`Pilot: ${pilotText}`)
|
||||
|
||||
const quickResponse = processPilotTransmission(pilotText)
|
||||
if (quickResponse) {
|
||||
simulationTrace.value.push({
|
||||
kind: 'info',
|
||||
id: `${stepId}-quick`,
|
||||
label: 'Quick Response',
|
||||
payload: { text: quickResponse }
|
||||
})
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
const ctx = buildLLMContext(pilotText)
|
||||
simulationTrace.value.push({
|
||||
kind: 'llm-input',
|
||||
id: stepId,
|
||||
label: 'LLM Request',
|
||||
payload: cloneForTrace(ctx)
|
||||
})
|
||||
|
||||
const template = simulationDecisions[stepId]
|
||||
if (!template) {
|
||||
throw new Error(`Missing simulation decision for ${stepId}`)
|
||||
}
|
||||
|
||||
const decision: any = { next_state: template.next }
|
||||
if (template.controllerSayTpl) {
|
||||
decision.controller_say_tpl = template.controllerSayTpl
|
||||
} else if (template.controllerSayState) {
|
||||
const sayState = getStateDetails(template.controllerSayState)
|
||||
if (sayState?.say_tpl) {
|
||||
decision.controller_say_tpl = sayState.say_tpl
|
||||
}
|
||||
}
|
||||
if (template.updates) {
|
||||
decision.updates = template.updates
|
||||
}
|
||||
|
||||
simulationTrace.value.push({
|
||||
kind: 'llm-output',
|
||||
id: stepId,
|
||||
label: `LLM Response → ${decision.next_state}`,
|
||||
payload: cloneForTrace(decision)
|
||||
})
|
||||
|
||||
applyLLMDecision(decision)
|
||||
await nextTick()
|
||||
recordCurrentAtcMessage()
|
||||
await advanceAutomaticStates()
|
||||
await wait(200)
|
||||
}
|
||||
|
||||
simulationTrace.value.push({
|
||||
kind: 'info',
|
||||
id: 'complete',
|
||||
label: 'Simulation Complete',
|
||||
payload: {
|
||||
timestamp: new Date().toISOString(),
|
||||
finalState: currentState.value?.id,
|
||||
pilotSteps: completedPilotSteps.value
|
||||
}
|
||||
})
|
||||
} catch (err: any) {
|
||||
const message = err?.message || String(err)
|
||||
simulationError.value = message
|
||||
simulationTrace.value.push({
|
||||
kind: 'info',
|
||||
id: 'error',
|
||||
label: 'Simulation aborted',
|
||||
payload: { error: message }
|
||||
})
|
||||
} finally {
|
||||
simulationRunning.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
debugMode,
|
||||
simulationRunning,
|
||||
simulationTrace,
|
||||
simulationError,
|
||||
simulationStepCount,
|
||||
completedPilotSteps,
|
||||
decisionTrace,
|
||||
timelineSteps,
|
||||
timelineUsedFallback,
|
||||
traceAutoSelection,
|
||||
traceFallback,
|
||||
debugState,
|
||||
debugNextStates,
|
||||
describeElimination,
|
||||
formatTracePayload,
|
||||
runFullSimulation,
|
||||
}
|
||||
}
|
||||
@@ -1529,6 +1529,7 @@ import {
|
||||
import { useAtisPlayback } from '~/composables/useAtisPlayback'
|
||||
import { usePttRecording } from '~/composables/usePttRecording'
|
||||
import { useBugReport } from '~/composables/useBugReport'
|
||||
import { useDebugSimulation } from '~/composables/useDebugSimulation'
|
||||
import { useSimBridgeSync } from '~/composables/useSimBridgeSync'
|
||||
import { useSpeechInterrupt } from '~/composables/useSpeechInterrupt'
|
||||
import { useRadioSpeech } from '~/composables/useRadioSpeech'
|
||||
@@ -1739,51 +1740,8 @@ const clearLog = () => {
|
||||
clearLastTransmission()
|
||||
}
|
||||
|
||||
const decisionTrace = computed(() => lastDecisionTrace.value)
|
||||
const timelineSteps = computed(() => decisionTrace.value?.candidateTimeline?.steps ?? [])
|
||||
const timelineUsedFallback = computed(() => Boolean(decisionTrace.value?.candidateTimeline?.fallbackUsed))
|
||||
const traceAutoSelection = computed(() => decisionTrace.value?.autoSelection ?? null)
|
||||
const traceFallback = computed(() => decisionTrace.value?.fallback ?? null)
|
||||
const sessionLabel = computed(() => engineSessionId.value || flags.value.session_id || '-')
|
||||
|
||||
const cloneForTrace = <T>(value: T): T => {
|
||||
if (value === undefined || value === null) {
|
||||
return value
|
||||
}
|
||||
try {
|
||||
return JSON.parse(JSON.stringify(value))
|
||||
} catch (err) {
|
||||
console.warn('Failed to clone trace payload, returning original value.', err)
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
function describeElimination(entry: any): string {
|
||||
if (!entry || typeof entry !== 'object') {
|
||||
return ''
|
||||
}
|
||||
if (entry.kind === 'regex' && entry.context?.patterns?.length) {
|
||||
const patterns = entry.context.patterns
|
||||
.map((pattern: any) => pattern?.pattern)
|
||||
.filter((value: string | undefined) => Boolean(value))
|
||||
.join(', ')
|
||||
return patterns ? `Patterns: ${patterns}` : entry.reason
|
||||
}
|
||||
if (entry.kind === 'condition' && entry.context?.condition) {
|
||||
const condition = entry.context.condition
|
||||
if (condition.type === 'regex' || condition.type === 'regex_not') {
|
||||
const flag = condition.pattern ? `/${condition.pattern}/${condition.patternFlags || 'i'}` : ''
|
||||
return flag ? `Condition: ${condition.type} ${flag}` : entry.reason
|
||||
}
|
||||
const variable = condition.variable || 'value'
|
||||
const operator = condition.operator || '=='
|
||||
const expected = entry.context?.expectedValue ?? condition.value ?? '—'
|
||||
const actual = entry.context?.actualValue ?? '—'
|
||||
return `${variable} ${operator} ${expected} (actual: ${actual})`
|
||||
}
|
||||
return entry.reason
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Scenario definitions
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -2037,7 +1995,6 @@ const speechSpeed = ref(0.95)
|
||||
const radioCheckLoading = ref(false)
|
||||
const radioEffectsEnabled = ref(true)
|
||||
const readbackEnabled = ref(false)
|
||||
const debugMode = ref(true)
|
||||
|
||||
// ── Bug Report ───────────────────────────────────────────────────────────────
|
||||
const {
|
||||
@@ -2164,10 +2121,6 @@ const TABS: PmTab[] = [
|
||||
]
|
||||
const visibleTabs = computed(() => TABS)
|
||||
|
||||
const simulationRunning = ref(false)
|
||||
const simulationTrace = ref<SimulationTraceEntry[]>([])
|
||||
const simulationError = ref('')
|
||||
|
||||
// Send unauthenticated visitors to login while preserving where they were
|
||||
// headed (e.g. /live-atc?token=… so the bridge link survives the round-trip),
|
||||
// instead of dropping them on the classroom fallback after sign-in.
|
||||
@@ -2270,120 +2223,6 @@ watch(vatsimId, (id) => {
|
||||
})
|
||||
|
||||
// Computed Properties
|
||||
const completedPilotSteps = computed(() => simulationTrace.value.filter(entry => entry.kind === 'pilot').length)
|
||||
|
||||
type SimulationDecisionTemplate = {
|
||||
next: string
|
||||
controllerSayState?: string
|
||||
controllerSayTpl?: string
|
||||
updates?: Record<string, any>
|
||||
note?: string
|
||||
}
|
||||
|
||||
type SimulationTraceEntry = {
|
||||
kind: 'info' | 'pilot' | 'atc' | 'llm-input' | 'llm-output'
|
||||
id: string
|
||||
label: string
|
||||
payload?: any
|
||||
}
|
||||
|
||||
const simulationPilotSteps = [
|
||||
'CD_CHECK_ATIS',
|
||||
'CD_VERIFY_READBACK',
|
||||
'GRD_READY_FOR_PUSH',
|
||||
'GRD_TAXI_REQUEST',
|
||||
'GRD_TAXI_READBACK',
|
||||
'TWR_LINEUP_REQ',
|
||||
'TWR_TAKEOFF_READBACK',
|
||||
'DEP_IDENT',
|
||||
'DEP_CLIMB_READBACK',
|
||||
'DES_READBACK',
|
||||
'APP_ESTABLISHED',
|
||||
'TWR_LAND_READBACK',
|
||||
'GRD_TAXI_IN_REQ',
|
||||
'GRD_TAXI_IN_READBACK'
|
||||
] as const
|
||||
|
||||
type SimulationPilotState = typeof simulationPilotSteps[number]
|
||||
|
||||
const simulationDecisions: Record<SimulationPilotState, SimulationDecisionTemplate> = {
|
||||
CD_CHECK_ATIS: {
|
||||
next: 'CD_ISSUE_CLR',
|
||||
controllerSayState: 'CD_ISSUE_CLR',
|
||||
updates: {
|
||||
push_available: true,
|
||||
runway_occupied: false,
|
||||
pilot_able: true,
|
||||
runway_available: true,
|
||||
push_delay_min: 0,
|
||||
surface_wind: '220/05',
|
||||
speed_restriction: '210 knots',
|
||||
emergency_heading: '180'
|
||||
}
|
||||
},
|
||||
CD_VERIFY_READBACK: {
|
||||
next: 'CD_READBACK_CHECK'
|
||||
},
|
||||
GRD_READY_FOR_PUSH: {
|
||||
next: 'GRD_PUSH_APPROVE',
|
||||
controllerSayState: 'GRD_PUSH_APPROVE'
|
||||
},
|
||||
GRD_TAXI_REQUEST: {
|
||||
next: 'GRD_TAXI_INSTR',
|
||||
controllerSayState: 'GRD_TAXI_INSTR'
|
||||
},
|
||||
GRD_TAXI_READBACK: {
|
||||
next: 'TWR_CONTACT',
|
||||
controllerSayState: 'TWR_CONTACT'
|
||||
},
|
||||
TWR_LINEUP_REQ: {
|
||||
next: 'TWR_TAKEOFF_CLR',
|
||||
controllerSayState: 'TWR_TAKEOFF_CLR'
|
||||
},
|
||||
TWR_TAKEOFF_READBACK: {
|
||||
next: 'DEP_CONTACT',
|
||||
controllerSayState: 'DEP_CONTACT'
|
||||
},
|
||||
DEP_IDENT: {
|
||||
next: 'DEP_CLIMB_INSTR',
|
||||
controllerSayState: 'DEP_CLIMB_INSTR'
|
||||
},
|
||||
DEP_CLIMB_READBACK: {
|
||||
next: 'ENR_HANDOFF',
|
||||
controllerSayState: 'ENR_HANDOFF'
|
||||
},
|
||||
DES_READBACK: {
|
||||
next: 'APP_HANDOFF',
|
||||
controllerSayState: 'APP_HANDOFF',
|
||||
updates: {
|
||||
speed_restriction: '180 knots'
|
||||
}
|
||||
},
|
||||
APP_ESTABLISHED: {
|
||||
next: 'TWR_LAND_CONTACT',
|
||||
controllerSayState: 'TWR_LAND_CONTACT',
|
||||
updates: {
|
||||
runway_available: true,
|
||||
surface_wind: '210/06'
|
||||
}
|
||||
},
|
||||
TWR_LAND_READBACK: {
|
||||
next: 'TWR_VACATE',
|
||||
controllerSayState: 'TWR_VACATE'
|
||||
},
|
||||
GRD_TAXI_IN_REQ: {
|
||||
next: 'GRD_TAXI_INSTR_IN',
|
||||
controllerSayState: 'GRD_TAXI_INSTR_IN'
|
||||
},
|
||||
GRD_TAXI_IN_READBACK: {
|
||||
next: 'FLOW_COMPLETE'
|
||||
}
|
||||
}
|
||||
|
||||
const recordedAtcStates = new Set<string>()
|
||||
const simulationStepCount = simulationPilotSteps.length
|
||||
|
||||
const wait = (ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms))
|
||||
|
||||
const speechInterrupt = useSpeechInterrupt()
|
||||
const { stopCurrentSpeech } = speechInterrupt
|
||||
@@ -2427,38 +2266,6 @@ const {
|
||||
prefetchAtisAudio,
|
||||
} = useAtisPlayback(engine, freq, currentScreen, signalStrength, setLastTransmission)
|
||||
|
||||
const debugState = computed(() => {
|
||||
if (!currentState.value) return null
|
||||
const sayTpl = currentState.value.say_tpl
|
||||
return {
|
||||
id: currentState.value.id,
|
||||
phase: currentState.value.phase,
|
||||
role: currentState.value.role,
|
||||
frequencyName: currentState.value.frequencyName,
|
||||
sayPlain: sayTpl ? renderATCMessage(sayTpl) : '',
|
||||
sayNormalized: sayTpl ? normalizeATCText(sayTpl, { ...vars.value, ...flags.value }) : ''
|
||||
}
|
||||
})
|
||||
|
||||
const debugNextStates = computed(() => {
|
||||
return nextCandidates.value.map(id => {
|
||||
const state = getStateDetails(id)
|
||||
if (!state) {
|
||||
return { id, role: '', phase: '', frequencyName: undefined, sayPlain: '', sayNormalized: '' }
|
||||
}
|
||||
const sayPlain = state.say_tpl ? renderATCMessage(state.say_tpl) : ''
|
||||
const sayNormalized = state.say_tpl ? normalizeATCText(state.say_tpl, { ...vars.value, ...flags.value }) : ''
|
||||
return {
|
||||
id,
|
||||
role: state.role,
|
||||
phase: state.phase,
|
||||
frequencyName: state.frequencyName,
|
||||
sayPlain,
|
||||
sayNormalized
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Methods
|
||||
const normalizeExpectedText = (text: string): string => {
|
||||
if (!flightContext.value) return text
|
||||
@@ -3072,200 +2879,24 @@ const formatTime = (date: Date): string => {
|
||||
})
|
||||
}
|
||||
|
||||
const formatTracePayload = (payload: any): string => {
|
||||
if (payload === null || payload === undefined) return ''
|
||||
if (typeof payload === 'string') return payload
|
||||
try {
|
||||
return JSON.stringify(payload, null, 2)
|
||||
} catch (err) {
|
||||
return String(payload)
|
||||
}
|
||||
}
|
||||
|
||||
const recordCurrentAtcMessage = () => {
|
||||
const state = currentState.value
|
||||
if (!state?.id || recordedAtcStates.has(state.id) || !state.say_tpl) {
|
||||
return
|
||||
}
|
||||
|
||||
const plain = renderATCMessage(state.say_tpl)
|
||||
const normalized = normalizeATCText(state.say_tpl, { ...vars.value, ...flags.value })
|
||||
|
||||
simulationTrace.value.push({
|
||||
kind: 'atc',
|
||||
id: state.id,
|
||||
label: `ATC • ${state.phase}`,
|
||||
payload: { text: plain, normalized }
|
||||
})
|
||||
|
||||
setLastTransmission(`ATC: ${plain}`)
|
||||
recordedAtcStates.add(state.id)
|
||||
}
|
||||
|
||||
const pickNextStateId = (state: ReturnType<typeof getStateDetails> | null): string | null => {
|
||||
if (!state) return null
|
||||
|
||||
const chains: Array<Array<{ to: string }>> = []
|
||||
if (state.ok_next?.length) chains.push(state.ok_next.map(({ to }) => ({ to })))
|
||||
if (state.next?.length) chains.push(state.next.map(({ to }) => ({ to })))
|
||||
if (state.bad_next?.length) chains.push(state.bad_next.map(({ to }) => ({ to })))
|
||||
if (state.timer_next?.length) chains.push(state.timer_next.map(({ to }) => ({ to })))
|
||||
|
||||
for (const list of chains) {
|
||||
for (const entry of list) {
|
||||
if (entry?.to) return entry.to
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const advanceAutomaticStates = async () => {
|
||||
let guard = 0
|
||||
|
||||
while (guard++ < 50) {
|
||||
const state = currentState.value
|
||||
if (!state?.id) break
|
||||
if (state.auto === 'end') break
|
||||
|
||||
const autoMode = Boolean(state.auto && state.auto !== 'end')
|
||||
if (!autoMode && state.role === 'pilot') break
|
||||
|
||||
const nextId = pickNextStateId(state)
|
||||
if (!nextId || nextId === state.id) break
|
||||
|
||||
forceMove(nextId)
|
||||
await nextTick()
|
||||
recordCurrentAtcMessage()
|
||||
}
|
||||
}
|
||||
|
||||
const runFullSimulation = async () => {
|
||||
if (simulationRunning.value) return
|
||||
|
||||
simulationRunning.value = true
|
||||
simulationTrace.value = []
|
||||
simulationError.value = ''
|
||||
recordedAtcStates.clear()
|
||||
debugMode.value = true
|
||||
|
||||
try {
|
||||
simulationTrace.value.push({
|
||||
kind: 'info',
|
||||
id: 'init',
|
||||
label: 'Simulation Start',
|
||||
payload: { timestamp: new Date().toISOString(), steps: simulationStepCount }
|
||||
})
|
||||
|
||||
startDemoFlight()
|
||||
await nextTick()
|
||||
clearLog()
|
||||
recordedAtcStates.clear()
|
||||
|
||||
if (currentState.value?.id !== 'CD_CHECK_ATIS') {
|
||||
forceMove('CD_CHECK_ATIS')
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
for (const stepId of simulationPilotSteps) {
|
||||
const state = getStateDetails(stepId)
|
||||
if (!state?.utterance_tpl) {
|
||||
throw new Error(`Missing pilot utterance for ${stepId}`)
|
||||
}
|
||||
|
||||
if (currentState.value?.id !== stepId) {
|
||||
forceMove(stepId)
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
await wait(120)
|
||||
|
||||
const pilotText = renderATCMessage(state.utterance_tpl)
|
||||
const pilotNormalized = normalizeATCText(state.utterance_tpl, { ...vars.value, ...flags.value })
|
||||
|
||||
simulationTrace.value.push({
|
||||
kind: 'pilot',
|
||||
id: stepId,
|
||||
label: `Pilot • ${state.phase}`,
|
||||
payload: { text: pilotText, normalized: pilotNormalized }
|
||||
})
|
||||
|
||||
setLastTransmission(`Pilot: ${pilotText}`)
|
||||
|
||||
const quickResponse = processPilotTransmission(pilotText)
|
||||
if (quickResponse) {
|
||||
simulationTrace.value.push({
|
||||
kind: 'info',
|
||||
id: `${stepId}-quick`,
|
||||
label: 'Quick Response',
|
||||
payload: { text: quickResponse }
|
||||
})
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
const ctx = buildLLMContext(pilotText)
|
||||
simulationTrace.value.push({
|
||||
kind: 'llm-input',
|
||||
id: stepId,
|
||||
label: 'LLM Request',
|
||||
payload: cloneForTrace(ctx)
|
||||
})
|
||||
|
||||
const template = simulationDecisions[stepId]
|
||||
if (!template) {
|
||||
throw new Error(`Missing simulation decision for ${stepId}`)
|
||||
}
|
||||
|
||||
const decision: any = { next_state: template.next }
|
||||
if (template.controllerSayTpl) {
|
||||
decision.controller_say_tpl = template.controllerSayTpl
|
||||
} else if (template.controllerSayState) {
|
||||
const sayState = getStateDetails(template.controllerSayState)
|
||||
if (sayState?.say_tpl) {
|
||||
decision.controller_say_tpl = sayState.say_tpl
|
||||
}
|
||||
}
|
||||
if (template.updates) {
|
||||
decision.updates = template.updates
|
||||
}
|
||||
|
||||
simulationTrace.value.push({
|
||||
kind: 'llm-output',
|
||||
id: stepId,
|
||||
label: `LLM Response → ${decision.next_state}`,
|
||||
payload: cloneForTrace(decision)
|
||||
})
|
||||
|
||||
applyLLMDecision(decision)
|
||||
await nextTick()
|
||||
recordCurrentAtcMessage()
|
||||
await advanceAutomaticStates()
|
||||
await wait(200)
|
||||
}
|
||||
|
||||
simulationTrace.value.push({
|
||||
kind: 'info',
|
||||
id: 'complete',
|
||||
label: 'Simulation Complete',
|
||||
payload: {
|
||||
timestamp: new Date().toISOString(),
|
||||
finalState: currentState.value?.id,
|
||||
pilotSteps: completedPilotSteps.value
|
||||
}
|
||||
})
|
||||
} catch (err: any) {
|
||||
const message = err?.message || String(err)
|
||||
simulationError.value = message
|
||||
simulationTrace.value.push({
|
||||
kind: 'info',
|
||||
id: 'error',
|
||||
label: 'Simulation aborted',
|
||||
payload: { error: message }
|
||||
})
|
||||
} finally {
|
||||
simulationRunning.value = false
|
||||
}
|
||||
}
|
||||
// Debug drawer: scripted simulation + decision-trace inspectors.
|
||||
const {
|
||||
debugMode,
|
||||
simulationRunning,
|
||||
simulationTrace,
|
||||
simulationError,
|
||||
simulationStepCount,
|
||||
completedPilotSteps,
|
||||
timelineSteps,
|
||||
timelineUsedFallback,
|
||||
traceAutoSelection,
|
||||
traceFallback,
|
||||
debugState,
|
||||
debugNextStates,
|
||||
describeElimination,
|
||||
formatTracePayload,
|
||||
runFullSimulation,
|
||||
} = useDebugSimulation(engine, { setLastTransmission, clearLog, startDemoFlight })
|
||||
|
||||
// Lifecycle
|
||||
onMounted(async () => {
|
||||
|
||||
Reference in New Issue
Block a user