From 059dc656b33f6d58acc7e50d732c60816882a162 Mon Sep 17 00:00:00 2001 From: itsrubberduck Date: Tue, 16 Sep 2025 12:38:15 +0200 Subject: [PATCH] The system now intelligently handles edge cases while maintaining the core decision tree structure. The LLM will follow the schema when possible but can respond naturally when the pilot says something unexpected, making it much more realistic and flexible for training scenarios --- app/pages/pm.vue | 1062 ++++++++++++++++++++------ server/api/llm/decide.post.ts | 13 +- server/utils/openai.ts | 109 ++- shared/utils/communicationsEngine.ts | 351 ++++++--- 4 files changed, 1153 insertions(+), 382 deletions(-) diff --git a/app/pages/pm.vue b/app/pages/pm.vue index 430d9e8..487ab3e 100644 --- a/app/pages/pm.vue +++ b/app/pages/pm.vue @@ -1,270 +1,866 @@ diff --git a/server/api/llm/decide.post.ts b/server/api/llm/decide.post.ts index 0641fa0..42b880a 100644 --- a/server/api/llm/decide.post.ts +++ b/server/api/llm/decide.post.ts @@ -9,10 +9,21 @@ export default defineEventHandler(async (event) => { if (!body.state_id || !Array.isArray(body.candidates)) { throw createError({ statusCode: 400, statusMessage: 'Invalid shape' }) } + try { const decision = await routeDecision(body) - return decision // direkt das JSON des Routers zurückgeben + + // Log für Debugging bei off-schema oder radio check + if (decision.off_schema) { + console.log(`[ATC] Off-schema response for: "${body.pilot_utterance}"`) + } + if (decision.radio_check) { + console.log(`[ATC] Radio check processed: "${body.pilot_utterance}"`) + } + + return decision } catch (err: any) { + console.error('Router failed:', err) throw createError({ statusCode: 500, statusMessage: err?.message || 'Router failed' }) } }) diff --git a/server/utils/openai.ts b/server/utils/openai.ts index 8ff83e3..bd3127d 100644 --- a/server/utils/openai.ts +++ b/server/utils/openai.ts @@ -1,14 +1,9 @@ // server/utils/openai.ts import OpenAI from 'openai' -// Nutzt OPENAI_API_KEY & LLM_MODEL aus Env -const MODEL = process.env.LLM_MODEL || 'gpt-5-nano' +const MODEL = process.env.LLM_MODEL || 'gpt-4o-mini' export const openai = new OpenAI({apiKey: process.env.OPENAI_API_KEY!}) -/** - * Einfache Chat-Hülle, die reinen Text zurückgibt (z.B. für Debug/sonstige Server-Prompts). - * WARNING: Client darf diese Funktion NICHT direkt importieren. Nur serverseitig nutzen. - */ export async function decide(system: string, user: string): Promise { const body = { model: MODEL, @@ -19,7 +14,7 @@ export async function decide(system: string, user: string): Promise { ], } console.log('OpenAI request:', body) - const r = await openai.chat.completions.create() + const r = await openai.chat.completions.create(body) console.log('OpenAI response:', r) return r.choices?.[0]?.message?.content?.trim() || '' } @@ -39,31 +34,74 @@ export interface LLMDecision { updates?: Record flags?: Record controller_say_tpl?: string + off_schema?: boolean // Flag wenn LLM frei geantwortet hat + radio_check?: boolean // Flag für Radio Check +} + +// Token-optimierte Eingabe für OpenAI +function optimizeInputForLLM(input: LLMDecisionInput) { + // Nur relevante Candidate-Daten senden + const candidates = input.candidates.map(c => ({ + id: c.id, + role: c.state.role, + phase: c.state.phase, + say_tpl: c.state.say_tpl, + utterance_tpl: c.state.utterance_tpl + })) + + // Nur wichtige Variablen senden + const relevantVars = { + callsign: input.variables.callsign, + dep: input.variables.dep, + dest: input.variables.dest, + runway: input.variables.runway, + squawk: input.variables.squawk, + current_frequency: input.variables[`${input.flags.current_unit?.toLowerCase()}_freq`] + } + + return { + state_id: input.state_id, + current_role: input.state.role, + current_phase: input.state.phase, + candidates: candidates, + variables: relevantVars, + flags: input.flags, + pilot_utterance: input.pilot_utterance + } } -/** - * Router-Entscheidung: Erzwingt JSON-Ausgabe. - * Wähle NUR aus candidates[].id (Ausnahmen: RESUME_PRIOR_FLOW, GEN_NO_REPLY, - * INT_* nur wenn Guards erfüllt – v.a. MAYDAY/PANPAN nur in-air). - */ export async function routeDecision(input: LLMDecisionInput): Promise { + const optimizedInput = optimizeInputForLLM(input) + const system = [ - 'You are an ATC state router.', - 'Return STRICT JSON with keys: next_state (string), optional updates (object), flags (object), controller_say_tpl (string).', - 'Pick next_state ONLY from input.candidates[].id.', - 'Allowed exceptions:', - '- "RESUME_PRIOR_FLOW" to return from interrupts.', - '- "GEN_NO_REPLY" if pilot did not respond / mismatch.', - '- Interrupts "INT_MAYDAY" / "INT_PANPAN" only if input.flags.in_air === true.', - 'If nothing matches, prefer "GEN_NO_REPLY".', - 'Do NOT invent IDs. Keep updates minimal (only variables used in templates).', + 'You are an ATC state router with flexible response capability.', + 'Return STRICT JSON with keys: next_state (string), optional updates (object), flags (object), controller_say_tpl (string), off_schema (boolean), radio_check (boolean).', + '', + 'PRIMARY ROUTING:', + '- Pick next_state from candidates[].id when pilot utterance matches expected flow', + '- Use "RESUME_PRIOR_FLOW" to return from interrupts', + '- Use "GEN_NO_REPLY" if pilot did not respond clearly', + '', + 'SPECIAL CASES:', + '- RADIO CHECK: If pilot says "radio check" or requests signal test, set radio_check=true and respond with appropriate signal strength', + '- OFF-SCHEMA: If pilot says something that doesn\'t match any candidate but requires ATC response, set off_schema=true and provide appropriate controller_say_tpl', + '', + 'INTERRUPTS (only if flags.in_air === true):', + '- "INT_MAYDAY" for MAYDAY emergencies', + '- "INT_PANPAN" for PAN-PAN situations', + '', + 'RESPONSE GENERATION:', + '- Always use proper ATC phraseology in controller_say_tpl', + '- Include callsign and relevant flight data', + '- Keep responses professional and concise', + '', + 'If uncertain, prefer appropriate ATC response over silence.' ].join(' ') - const user = JSON.stringify(input) + const user = JSON.stringify(optimizedInput) const body = { model: MODEL, - // JSON-Ausgabe erzwingen response_format: {type: 'json_object'}, messages: [ {role: 'system', content: system}, @@ -72,19 +110,40 @@ export async function routeDecision(input: LLMDecisionInput): Promise } -// --- ATC Decition Tree laden --- +// Flight Phases und Communication Steps für bessere Integration +export const FLIGHT_PHASES = [ + { id: 'clearance', name: 'Clearance Delivery', frequency: '121.900', action: 'Request IFR clearance' }, + { id: 'ground', name: 'Ground Control', frequency: '121.700', action: 'Request pushback and taxi' }, + { id: 'tower', name: 'Tower', frequency: '118.700', action: 'Request takeoff clearance' }, + { id: 'departure', name: 'Departure', frequency: '125.350', action: 'Initial contact after takeoff' }, + { id: 'enroute', name: 'Center', frequency: '121.800', action: 'Cruise flight monitoring' }, + { id: 'approach', name: 'Approach', frequency: '120.800', action: 'Approach clearance' }, + { id: 'landing', name: 'Tower (Landing)', frequency: '118.700', action: 'Landing clearance' }, + { id: 'taxiin', name: 'Ground (Taxi In)', frequency: '121.700', action: 'Taxi to gate' } +] -// ----------------- Public API / Kontext ----------------- +export const COMMUNICATION_STEPS = [ + { + id: 'cd_request', + phase: 'clearance', + trigger: 'pilot', + frequency: '121.900', + frequencyName: 'Clearance Delivery', + pilot: '{callsign} information {atis_code}, IFR to {dest}, stand {stand}, request clearance.', + atc: '{callsign}, cleared to {dest} via {sid} departure, runway {runway}, climb {initial_altitude_ft} feet, squawk {squawk}.', + pilotResponse: '{callsign} cleared {dest} via {sid}, runway {runway}, climb {initial_altitude_ft}, squawk {squawk}.' + } + // Weitere Steps können hier definiert werden +] + +// --- ATC Decision Tree laden --- export interface FlightContext { callsign: string aircraft: string @@ -79,7 +105,7 @@ export interface FlightContext { atis_code: string sid: string transition: string - flight_level: string // z.B. "FL360" + flight_level: string ground_freq: string tower_freq: string departure_freq: string @@ -89,7 +115,7 @@ export interface FlightContext { taxi_route: string remarks?: string time_now?: string - // intern + phase: string lastTransmission?: string awaitingResponse?: boolean } @@ -101,9 +127,11 @@ export interface EngineLog { message: string normalized: string state: string + radioCheck?: boolean + offSchema?: boolean } -// ----------------- NATO/ICAO Normalizer ----------------- +// NATO/ICAO Normalizer (gekürzt für bessere Performance) const NATO_PHONETIC: Record = { A: 'Alpha', B: 'Bravo', C: 'Charlie', D: 'Delta', E: 'Echo', F: 'Foxtrot', G: 'Golf', H: 'Hotel', I: 'India', J: 'Juliett', K: 'Kilo', L: 'Lima', @@ -111,91 +139,46 @@ const NATO_PHONETIC: Record = { S: 'Sierra', T: 'Tango', U: 'Uniform', V: 'Victor', W: 'Whiskey', X: 'X-ray', Y: 'Yankee', Z: 'Zulu' } + const ICAO_NUMBERS: Record = { '0': 'zero', '1': 'wun', '2': 'too', '3': 'tree', '4': 'fower', '5': 'fife', '6': 'six', '7': 'seven', '8': 'eight', '9': 'niner' } -function normalizeAltitude(altitude: number): string { - if (altitude >= 1000) { - const thousands = Math.floor(altitude / 1000).toString().split('').map(d => ICAO_NUMBERS[d] || d).join(' ') - const remainder = altitude % 1000 - return remainder > 0 ? `${thousands} thousand ${remainder}` : `${thousands} thousand` - } - return altitude.toString() -} - export function normalizeATCText(text: string, context: Record): string { let normalized = renderTpl(text, context) - // runway 25L → two five left + // Runway normalization normalized = normalized.replace(/runway\s+(\d{1,2})([LRC]?)/gi, (_, num: string, suffix: string) => { const n = num.split('').map(d => ICAO_NUMBERS[d] || d).join(' ') const s = suffix === 'L' ? ' left' : suffix === 'R' ? ' right' : suffix === 'C' ? ' center' : '' return `runway ${n}${s}` }) - // FL350 → flight level three five zero + // Flight Level normalized = normalized.replace(/FL(\d{3})/gi, (_, lvl: string) => { const n = lvl.split('').map(d => ICAO_NUMBERS[d] || d).join(' ') return `flight level ${n}` }) - // 5000 ft → five thousand - normalized = normalized.replace(/\b(\d{1,5})\s*(?:feet?|ft)\b/gi, (_, alt: string) => normalizeAltitude(parseInt(alt))) - - // 121.9 → wun too wun decimal niner + // Frequency normalization normalized = normalized.replace(/(\d{3})\.(\d{1,3})/g, (_, a: string, b: string) => { const A = a.split('').map(d => ICAO_NUMBERS[d] || d).join(' ') const B = b.split('').map(d => ICAO_NUMBERS[d] || d).join(' ') return `${A} decimal ${B}` }) - // squawk 4567 → fower fife six seven + // Squawk codes normalized = normalized.replace(/squawk\s+(\d{4})/gi, (_, code: string) => { const n = code.split('').map(d => ICAO_NUMBERS[d] || d).join(' ') return `squawk ${n}` }) - // heading 270 → heading two seven zero - normalized = normalized.replace(/heading\s+(\d{3})/gi, (_, hdg: string) => { - const n = hdg.split('').map(d => ICAO_NUMBERS[d] || d).join(' ') - return `heading ${n}` - }) - - // via A, B5 → via Alpha, Bravo five - normalized = normalized.replace(/via\s+([A-Z](?:\d+)?(?:\s*,\s*[A-Z](?:\d+)?)*)/gi, (_, route: string) => { - const parts = route.split(/\s*,\s*/).map(seg => - seg.replace(/([A-Z])(\d+)?/g, (_m, L: string, N?: string) => { - const letter = NATO_PHONETIC[L] || L - const number = N ? N.split('').map(d => ICAO_NUMBERS[d] || d).join(' ') : '' - return number ? `${letter} ${number}` : letter - }) - ) - return `via ${parts.join(', ')}` - }) - - // A5 → Alpha five - normalized = normalized.replace(/\b([A-Z])(\d+)\b/g, (_m, L: string, N: string) => { - const letter = NATO_PHONETIC[L] || L - const number = N.split('').map(d => ICAO_NUMBERS[d] || d).join(' ') - return `${letter} ${number}` - }) - - // 280/15 → two eight zero at wun fife - normalized = normalized.replace(/(\d{3})\/(\d{1,2})/g, (_m, d: string, s: string) => { - const D = d.split('').map(x => ICAO_NUMBERS[x] || x).join(' ') - const S = s.split('').map(x => ICAO_NUMBERS[x] || x).join(' ') - return `${D} at ${S}` - }) - return normalized } -// ----------------- Template-Renderer ----------------- function renderTpl(tpl: string, ctx: Record): string { return tpl.replace(/\{([\w.]+)\}/g, (_m, key) => { - // Variablen können z.B. variables.callsign oder flags.in_air sein const parts = key.split('.') let cur: any = ctx for (const p of parts) cur = cur?.[p] @@ -203,21 +186,48 @@ function renderTpl(tpl: string, ctx: Record): string { }) } -// ----------------- Engine ----------------- export default function useCommunicationsEngine() { - // Decision Tree in-Memory - const tree = ref(atcDecisionTree as DecisionTree) + const tree = ref({ + ...atcDecisionTree as DecisionTree, + flags: { + ...atcDecisionTree.flags, + off_schema_count: 0, + radio_checks_done: 0 + } + }) + const states = computed>(() => tree.value.states) - // Laufzeitkontext const variables = ref>({ ...tree.value.variables }) const flags = ref({ ...tree.value.flags }) const currentStateId = ref(tree.value.start_state) - const communicationLog = ref([]) + // Flight Context für bessere Integration mit pm_alt.vue Stil + const flightContext = ref({ + callsign: '', + aircraft: 'A320', + dep: 'EDDF', + dest: 'EDDM', + stand: 'A12', + runway: '25R', + squawk: '1234', + atis_code: 'K', + sid: 'ANEKI7S', + transition: 'ANEKI', + flight_level: 'FL360', + ground_freq: '121.700', + tower_freq: '118.700', + departure_freq: '125.350', + approach_freq: '120.800', + handoff_freq: '121.800', + qnh_hpa: 1015, + taxi_route: 'A, V', + phase: 'clearance' + }) + const currentState = computed(() => { - const s = states.value[currentStateId.value] + const s = states.value[currentStateId.value] || states.value[tree.value.start_state] return { ...s, id: currentStateId.value } }) @@ -229,28 +239,41 @@ export default function useCommunicationsEngine() { ...(s.bad_next ?? []), ...(s.timer_next?.map(t => ({ to: t.to })) ?? []) ].map(x => x.to) - // uniq return Array.from(new Set(nxt)) }) - // Frequenz aus Variablen (Controller-Einheit) ableiten const activeFrequency = computed(() => { switch (flags.value.current_unit) { - case 'DEL': return renderTpl('{variables.delivery_freq}', { variables: variables.value }) - case 'GROUND': return renderTpl('{variables.ground_freq}', { variables: variables.value }) - case 'TOWER': return renderTpl('{variables.tower_freq}', { variables: variables.value }) - case 'DEP': return renderTpl('{variables.departure_freq}', { variables: variables.value }) - case 'APP': return renderTpl('{variables.approach_freq}', { variables: variables.value }) - case 'CTR': return renderTpl('{variables.handoff_freq}', { variables: variables.value }) + case 'DEL': return variables.value.delivery_freq + case 'GROUND': return variables.value.ground_freq + case 'TOWER': return variables.value.tower_freq + case 'DEP': return variables.value.departure_freq + case 'APP': return variables.value.approach_freq + case 'CTR': return variables.value.handoff_freq default: return undefined } }) - // ---------- öffentliche API ---------- + // Current Step für pm_alt.vue Integration + const currentStep = computed(() => { + const phase = flightContext.value.phase + const step = COMMUNICATION_STEPS.find(s => s.phase === phase) + if (step) { + return { + ...step, + pilot: renderTpl(step.pilot, { ...variables.value, ...flags.value }), + atc: step.atc ? renderTpl(step.atc, { ...variables.value, ...flags.value }) : undefined, + pilotResponse: step.pilotResponse ? renderTpl(step.pilotResponse, { ...variables.value, ...flags.value }) : undefined + } + } + return null + }) + function initializeFlight(fpl: any) { + // Variablen setzen variables.value = { ...variables.value, - callsign: fpl.callsign, + callsign: fpl.callsign || fpl.callsign, acf_type: fpl.aircraft?.split('/')[0] || 'A320', dep: fpl.dep || fpl.departure || 'EDDF', dest: fpl.arr || fpl.arrival || 'EDDM', @@ -263,8 +286,6 @@ export default function useCommunicationsEngine() { cruise_flight_level: fpl.altitude ? `FL${String(Math.floor(parseInt(fpl.altitude) / 100)).padStart(3, '0')}` : 'FL360', initial_altitude_ft: 5000, climb_altitude_ft: 7000, - star: 'RNAV X', - approach_type: 'ILS Z', taxi_route: 'A, V', delivery_freq: '121.900', ground_freq: '121.700', @@ -276,12 +297,27 @@ export default function useCommunicationsEngine() { remarks: 'standard', time_now: new Date().toISOString() } - flags.value = { ...flags.value, in_air: false, emergency_active: false, current_unit: 'DEL', stack: [] } + + // Flight Context aktualisieren + Object.assign(flightContext.value, { + ...variables.value, + phase: 'clearance' + }) + + flags.value = { + ...flags.value, + in_air: false, + emergency_active: false, + current_unit: 'DEL', + stack: [], + off_schema_count: 0, + radio_checks_done: 0 + } + currentStateId.value = tree.value.start_state communicationLog.value = [] } - // Pilot-Input verarbeiten → LLM bekommt State + Candidates function buildLLMContext(pilotTranscript: string) { const s = currentState.value return { @@ -294,92 +330,154 @@ export default function useCommunicationsEngine() { } } - // LLM-Entscheidung anwenden - function applyLLMDecision(decision: { next_state: string; updates?: Record; flags?: Record; controller_say_tpl?: string }) { - // Variablen/Flags updaten + function applyLLMDecision(decision: any) { + // Updates anwenden if (decision.updates) Object.assign(variables.value, decision.updates) if (decision.flags) Object.assign(flags.value, decision.flags) - // ggf. ATC sagt etwas sofort (z.B. Reminders) - if (decision.controller_say_tpl) { - speak('atc', decision.controller_say_tpl, currentStateId.value) + + // Off-Schema und Radio Check Tracking + if (decision.off_schema) { + flags.value.off_schema_count++ + console.log(`[Engine] Off-schema response #${flags.value.off_schema_count}`) + } + if (decision.radio_check) { + flags.value.radio_checks_done++ + console.log(`[Engine] Radio check #${flags.value.radio_checks_done}`) + } + + // Controller Response + if (decision.controller_say_tpl) { + speak('atc', decision.controller_say_tpl, currentStateId.value, { + radioCheck: decision.radio_check, + offSchema: decision.off_schema + }) + } + + // State Transition (nur wenn nicht Radio Check) + if (!decision.radio_check) { + moveTo(decision.next_state) } - moveTo(decision.next_state) } - // Direkter Pilotenspruch → einfache Router-Heuristik (Interrupts/Readbacks). Für echte Logik nutzt ihr LLM: - function processPilotTransmission(transcript: string) { - // Interrupt-Shortcuts (nur in der Luft) + function processPilotTransmission(transcript: string): string | null { + // Log pilot input + speak('pilot', transcript, currentStateId.value) + + // Radio Check Detection (fallback falls LLM es nicht erkennt) const t = transcript.toLowerCase() + if (t.includes('radio check') || (t.includes('read') && t.includes('check'))) { + const callsign = variables.value.callsign || '' + const response = `${callsign}, read you five by five.` + flags.value.radio_checks_done++ + + setTimeout(() => { + speak('atc', response, currentStateId.value, { radioCheck: true }) + }, 500) + + return response + } + + // Emergency Interrupts if (flags.value.in_air && /^(mayday|pan\s*pan)/.test(t)) { const intId = t.startsWith('mayday') ? 'INT_MAYDAY' : 'INT_PANPAN' moveTo(intId) - speak('pilot', transcript, intId) + return null + } + + return null // LLM soll entscheiden + } + + function processUserTransmission(transcript: string): string | null { + return processPilotTransmission(transcript) + } + + function moveTo(stateId: string) { + if (!states.value[stateId]) { + console.warn(`[Engine] Unknown state: ${stateId}`) return } - // Standard: Loggen & LLM-Context anbieten - speak('pilot', transcript, currentStateId.value) - } - - // Zum State springen und Auto-Aktionen ausführen - function moveTo(stateId: string) { - if (!states.value[stateId]) return - // Stack-Mechanik (vereinfacht): Interrupts pushen aktuellen State - if (stateId.startsWith('INT_')) flags.value.stack.push(currentStateId.value) + if (stateId.startsWith('INT_')) { + flags.value.stack.push(currentStateId.value) + } currentStateId.value = stateId const s = currentState.value - // Actions ausführen (nur SET/IF minimal) + // Actions ausführen for (const act of s.actions ?? []) { if (typeof act === 'string') continue if (act.if && !safeEvalBoolean(act.if)) continue if (act.set) setByPath({ variables: variables.value, flags: flags.value }, act.set, act.to) } - // Handoff → Frequenz/Unit setzen + // Handoff if (s.handoff?.to) { flags.value.current_unit = unitFromHandoff(s.handoff.to) if (s.handoff.freq) { - // Frequenz ins variables-Objekt aufnehmen, falls dynamisch - variables.value.handoff_freq = renderTpl(s.handoff.freq, { ...exposeCtx() }) + variables.value.handoff_freq = renderTpl(s.handoff.freq, exposeCtx()) } } - // Auto-Say (ATC/System) - if (s.say_tpl) speak(s.role, s.say_tpl, s.id!) + // Auto-Say + if (s.say_tpl) { + speak(s.role, s.say_tpl, s.id!) + } - // Auto-Ende / Monitor / Check-Readback werden vom LLM/Client gesteuert. + // Phase Update für Flight Context + updateFlightPhase(s.phase) + } + + function updateFlightPhase(phase: Phase) { + const phaseMap: Record = { + 'Clearance': 'clearance', + 'PushStart': 'ground', + 'TaxiOut': 'ground', + 'Departure': 'tower', + 'Climb': 'departure', + 'Enroute': 'enroute', + 'Descent': 'enroute', + 'Approach': 'approach', + 'Landing': 'landing', + 'TaxiIn': 'taxiin', + 'Preflight': 'clearance', + 'Postflight': 'taxiin', + 'Interrupt': flightContext.value.phase, + 'LostComms': flightContext.value.phase, + 'Missed': 'approach' + } + + if (phaseMap[phase]) { + flightContext.value.phase = phaseMap[phase] + } } - // Letzten Flow wieder aufnehmen (z.B. nach Interrupt) function resumePriorFlow() { const prev = flags.value.stack.pop() if (prev) moveTo(prev) } - // Hilfsfunktionen - function speak(speaker: Role, tpl: string, stateId: string) { + function speak(speaker: Role, tpl: string, stateId: string, options: { radioCheck?: boolean, offSchema?: boolean } = {}) { const msg = renderTpl(tpl, exposeCtx()) - communicationLog.value.push({ + const entry: EngineLog = { timestamp: new Date(), frequency: activeFrequency.value, speaker, message: msg, normalized: normalizeATCText(msg, exposeCtxFlat()), - state: stateId - }) + state: stateId, + radioCheck: options.radioCheck, + offSchema: options.offSchema + } + communicationLog.value.push(entry) } function exposeCtx() { return { variables: variables.value, flags: flags.value } } + function exposeCtxFlat() { - // Für den Normalizer wollen wir einfache Platzhalter-Namen - return { - ...variables.value, - ...flags.value - } + return { ...variables.value, ...flags.value } } function unitFromHandoff(to: string) { @@ -396,26 +494,28 @@ export default function useCommunicationsEngine() { const arr = ['A12','B15','C23','D8','E41','F18','G7','H33'] return arr[Math.floor(Math.random() * arr.length)] } + function genRunway() { const arr = ['25L','25R','07L','07R','18','36','09','27'] return arr[Math.floor(Math.random() * arr.length)] } + function genSquawk() { return String(Math.floor(Math.random() * 8000 + 1000)).padStart(4, '0') } + function genATIS() { const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' return letters[Math.floor(Math.random() * letters.length)] } + function genSID(_route: string) { const sids = ['SULUS5S', 'TOBAK2E', 'MARUN7F', 'CINDY1A', 'HELEN4B'] return sids[Math.floor(Math.random() * sids.length)] } - // einfache bool-Guards (nur Variablen/Flags, keine eval unsicherer Ausdrücke) function safeEvalBoolean(expr?: string): boolean { if (!expr) return true - // sehr einfache Auswertung: flags.in_air === true const m = expr.match(/^(variables|flags)\.([A-Za-z0-9_]+)\s*===\s*(true|false|'[^']*'|"[^"]*"|\d+)$/) if (!m) return false const bag = m[1] === 'variables' ? variables.value : flags.value as any @@ -441,7 +541,7 @@ export default function useCommunicationsEngine() { } return { - // state/vars/flags + // State currentState: computed(() => currentState.value), currentStateId: readonly(currentStateId), variables: readonly(variables), @@ -450,19 +550,24 @@ export default function useCommunicationsEngine() { activeFrequency, communicationLog: readonly(communicationLog), - // lifecycle + // pm_alt.vue Integration + flightContext: readonly(flightContext), + currentStep, + + // Lifecycle initializeFlight, - // Pilot in → baut Kontext für LLM + // Communication processPilotTransmission, + processUserTransmission, buildLLMContext, applyLLMDecision, - // Flow-Steuerung + // Flow Control moveTo, resumePriorFlow, - // utils + // Utilities normalizeATCText } }