Translate pilot monitoring and learn pages

This commit is contained in:
Remi
2025-09-19 08:54:57 +02:00
parent db4e30292c
commit c18568f12b
5 changed files with 225 additions and 228 deletions

View File

@@ -10,28 +10,27 @@
>
<section class="cookie-consent-panel">
<header class="cookie-consent-header">
<h2 id="cookie-consent-title">Cookies &amp; Analysen</h2>
<h2 id="cookie-consent-title">Cookies &amp; Analytics</h2>
<p id="cookie-consent-description">
Wir verwenden notwendige Cookies, damit die Website zuverlässig funktioniert. Darüber hinaus würden wir gern optionale
Analyse-Cookies von Hotjar einsetzen, um das Nutzungserlebnis zu verbessern. Sie entscheiden, ob Sie diese zulassen
alle Details finden Sie in unserer
<a href="/datenschutz" target="_blank" rel="noopener">Datenschutzerklärung</a>.
We use essential cookies to keep the site reliable. We would also like to enable optional Hotjar analytics cookies to
improve the experience. You decide whether to allow themfull details are available in our
<a href="/datenschutz" target="_blank" rel="noopener">privacy policy</a>.
</p>
</header>
<div class="cookie-consent-options" role="list">
<article class="cookie-option is-required" role="listitem">
<div class="cookie-option-text">
<h3>Notwendige Cookies</h3>
<p>Speichern Ihre Auswahl und sorgen für sichere Anmeldung sowie eine stabile Grundfunktionalität.</p>
<h3>Necessary cookies</h3>
<p>Store your preferences and keep sign-in secure while ensuring the core experience remains stable.</p>
</div>
<span class="cookie-option-badge" aria-hidden="true">Immer aktiv</span>
<span class="cookie-option-badge" aria-hidden="true">Always active</span>
</article>
<article class="cookie-option" role="listitem">
<div class="cookie-option-text">
<h3>Analyse (Hotjar)</h3>
<p>Hilft uns, Nutzungsverhalten anonym auszuwerten und unsere Inhalte gezielt zu verbessern.</p>
<h3>Analytics (Hotjar)</h3>
<p>Helps us analyse anonymised usage patterns and improve our content in a focused way.</p>
</div>
<button
type="button"
@@ -44,21 +43,20 @@
<span class="cookie-toggle-thumb" />
</span>
<span class="sr-only">
Analyse-Cookies {{ analyticsSelection ? 'deaktivieren' : 'aktivieren' }}
{{ analyticsSelection ? 'Disable analytics cookies' : 'Enable analytics cookies' }}
</span>
</button>
</article>
</div>
<p class="cookie-consent-note">
Optionales Tracking wird erst nach Ihrer Zustimmung geladen. Sie können Ihre Entscheidung jederzeit über Cookie-Einstellungen
unten links widerrufen.
Optional tracking loads only after you agree. You can change your decision any time via "Cookie settings" in the lower left.
</p>
<div class="cookie-consent-actions">
<button type="button" class="cookie-button ghost" @click="handleRejectAll">Nur notwendige</button>
<button type="button" class="cookie-button secondary" @click="handleSave">Auswahl speichern</button>
<button type="button" class="cookie-button primary" @click="handleAcceptAll">Alle akzeptieren</button>
<button type="button" class="cookie-button ghost" @click="handleRejectAll">Essential only</button>
<button type="button" class="cookie-button secondary" @click="handleSave">Save selection</button>
<button type="button" class="cookie-button primary" @click="handleAcceptAll">Accept all</button>
</div>
</section>
</div>
@@ -66,7 +64,7 @@
<transition name="cookie-manage">
<button v-if="showManageButton" type="button" class="cookie-manage-button" @click="openManager">
Cookie-Einstellungen
Cookie settings
</button>
</transition>
</template>

View File

@@ -10,7 +10,7 @@
</button>
<span class="brand">OpenSquawk</span>
<span class="sep">|</span>
<span class="mode">Lernwelt</span>
<span class="mode">Training Hub</span>
</div>
<div class="hud-right">
@@ -26,16 +26,16 @@
<button class="btn ghost" @click="panel='progress'">
<v-icon size="18">mdi-chart-line</v-icon>
Fortschritt
Progress
</button>
<!-- ATC Einstellungen -->
<button class="btn ghost" @click="showSettings=true" title="ATC-Einstellungen">
<button class="btn ghost" @click="showSettings=true" title="ATC settings">
<v-icon size="18">mdi-tune</v-icon>
ATC
</button>
<button class="btn ghost" @click="resetAll" title="Zurücksetzen">
<button class="btn ghost" @click="resetAll" title="Reset">
<v-icon size="18">mdi-refresh</v-icon>
Reset
</button>
@@ -49,16 +49,16 @@
<div class="panel hero-panel" :style="worldTiltStyle" @mousemove="tilt">
<div class="hero-left">
<div class="eyebrow">ALPHA BUILD · TRAINING</div>
<h1 class="h1">ATC Lernwelt</h1>
<p class="muted">Geführte Pfade · Missionen · XP · Abzeichen. Lokal gespeichert.</p>
<h1 class="h1">ATC Learning Hub</h1>
<p class="muted">Guided paths · missions · XP · badges. Stored locally.</p>
<div class="actions">
<button class="btn primary" @click="panel='hub'">
<v-icon>mdi-play</v-icon>
Losfliegen
Start training
</button>
<button class="btn ghost" @click="panel='progress'">
<v-icon>mdi-progress-check</v-icon>
Fortschritt
Progress
</button>
<button class="btn ghost" @click="testBeep">
<v-icon>mdi-volume-high</v-icon>
@@ -86,7 +86,7 @@
</div>
<div class="card-title">{{ d.title }}</div>
<div class="card-sub muted">{{ d.sub }}</div>
<button class="btn mini soft" @click="startDaily(d)">Annehmen</button>
<button class="btn mini soft" @click="startDaily(d)">Accept</button>
</div>
</div>
</div>
@@ -98,7 +98,7 @@
<main v-if="panel==='hub'" class="container" role="main">
<div class="hub-head">
<h2 class="h2">Mission Hub</h2>
<div class="muted">Starte mit ICAO Alphabet & Zahlen, dann Basics, Ground </div>
<div class="muted">Start with the ICAO alphabet & numbers, then basics, ground, and more.</div>
</div>
<div class="tiles">
@@ -128,7 +128,7 @@
</button>
<button class="btn soft" @click="quickContinue(m.id)">
<v-icon size="18">mdi-skip-next</v-icon>
Weiter
Continue
</button>
</div>
</div>
@@ -137,7 +137,7 @@
</main>
<!-- MODULE -->
<section v-if="panel==='module' && current" class="container play" aria-label="Modul">
<section v-if="panel==='module' && current" class="container play" aria-label="Module">
<div class="play-head">
<div class="crumbs">
<button class="link" @click="panel='hub'">
@@ -167,8 +167,8 @@
<v-icon size="18">mdi-headset</v-icon>
{{ l.title }}
</div>
<div class="chip" :class="{ ok: bestScore(current.id,l.id)>=80 }">
{{ bestScore(current.id, l.id) ? bestScore(current.id, l.id) + '%' : 'neu' }}
<div class="chip" :class="{ ok: bestScore(current.id,l.id)>=80 }">
{{ bestScore(current.id, l.id) ? bestScore(current.id, l.id) + '%' : 'new' }}
</div>
</div>
<div class="muted small">{{ l.desc }}</div>
@@ -186,12 +186,12 @@
<div class="scenario-sub">{{ scenario.radioCall }}</div>
</div>
<div class="scenario-item">
<span class="scenario-label">Flugplatz</span>
<span class="scenario-label">Airport</span>
<div class="scenario-value">{{ scenario.airport.icao }}</div>
<div class="scenario-sub">{{ scenario.airport.name }}</div>
</div>
<div class="scenario-item wide">
<span class="scenario-label">Frequenzen</span>
<span class="scenario-label">Frequencies</span>
<div class="freq-chips">
<button
v-for="freq in scenario.frequencies"
@@ -224,7 +224,7 @@
{{ targetPhrase }}
</div>
<div v-if="audioContentHidden" class="audio-note muted small">
Audio Challenge aktiv zuerst anhören.
Audio challenge active listen first.
</div>
</div>
<div class="row wrap">
@@ -245,11 +245,11 @@
@click="revealAudioContent"
>
<v-icon size="16">mdi-eye</v-icon>
Anzeigen
Reveal text
</button>
<button class="btn ghost mini" type="button" @click="rollScenario(true)">
<v-icon size="16">mdi-dice-5</v-icon>
Würfeln
Roll
</button>
</div>
</div>
@@ -272,7 +272,7 @@
</div>
<div class="col">
<div class="label">Deine Readback</div>
<div class="label">Your readback</div>
<div class="panel readback-panel">
<div class="cloze">
<template v-for="(segment, idx) in activeLesson.readback" :key="segment.type === 'field' ? `f-${segment.key}` : `t-${idx}`">
@@ -303,7 +303,7 @@
<div class="row wrap controls">
<button class="btn primary" type="button" :disabled="evaluating" @click="evaluate">
<v-icon size="18">mdi-check</v-icon>
Prüfen
Check
</button>
<button class="btn soft" type="button" @click="clearAnswers">
<v-icon size="18">mdi-eraser</v-icon>
@@ -311,20 +311,20 @@
</button>
<button class="btn ghost" type="button" @click="fillSolution">
<v-icon size="18">mdi-auto-fix</v-icon>
Autotext
Auto-fill
</button>
</div>
<div v-if="result" class="score">
<div class="score-num">{{ result.score }}%</div>
<div class="muted small">
Felder korrekt: {{ result.hits }}/{{ activeLesson.fields.length }} · Ähnlichkeit: {{ Math.round(result.sim * 100) }}%
Fields correct: {{ result.hits }}/{{ activeLesson.fields.length }} · Similarity: {{ Math.round(result.sim * 100) }}%
</div>
</div>
<div v-if="result" class="field-checks">
<div v-for="field in result.fields" :key="field.key" class="field-check" :class="{ ok: field.pass }">
<div class="field-name">{{ field.label }}</div>
<div class="field-answer">{{ field.answer || '—' }}</div>
<div class="field-expected">Soll: {{ field.expected }}</div>
<div class="field-expected">Expected: {{ field.expected }}</div>
</div>
</div>
</div>
@@ -332,8 +332,8 @@
</section>
<!-- PROGRESS -->
<section v-if="panel==='progress'" class="container" aria-label="Fortschritt">
<h2 class="h2">Gesamtfortschritt</h2>
<section v-if="panel==='progress'" class="container" aria-label="Progress">
<h2 class="h2">Overall progress</h2>
<div class="progress-grid">
<div v-for="m in modules" :key="m.id" class="panel">
<div class="row between">
@@ -353,27 +353,27 @@
<!-- SETTINGS DIALOG -->
<v-dialog v-model="showSettings" max-width="720">
<div class="panel dialog">
<h3 class="h3">ATC Einstellungen</h3>
<h3 class="h3">ATC settings</h3>
<div class="settings">
<div class="set-row">
<div class="set-info">
<span>Browser-TTS (Web Speech)</span>
<small class="muted">Läuft offline, startet schneller und spart unsere API-Anfragen.</small>
<span>Browser TTS (Web Speech)</span>
<small class="muted">Runs offline, starts faster, and saves our API calls.</small>
</div>
<v-switch v-model="cfg.tts" color="cyan" hide-details inset/>
</div>
<div class="set-row">
<div class="set-info">
<span>Audio Challenge</span>
<small class="muted">Blendet Zieltext & Infos, bis du sie manuell einblendest.</small>
<span>Audio challenge</span>
<small class="muted">Hides target text & info until you reveal it manually.</small>
</div>
<v-switch v-model="cfg.audioChallenge" color="cyan" hide-details inset/>
</div>
<div class="set-row">
<span>Funk-Level (1..5)</span>
<span>Radio level (1..5)</span>
<v-slider v-model="cfg.radioLevel" :min="1" :max="5" :step="1" color="cyan" thumb-label/>
</div>
@@ -383,22 +383,22 @@
</div>
<div class="set-row">
<span>Probe-TTS</span>
<span>Test TTS</span>
<div class="row">
<button class="btn soft" @click="say('Frankfurt Ground, Lufthansa one two three, request taxi.')">
<v-icon>mdi-volume-high</v-icon>
Probe
Test
</button>
<button class="btn ghost" @click="stopAudio">
<v-icon>mdi-stop</v-icon>
Stopp
Stop
</button>
</div>
</div>
</div>
<div class="row end">
<button class="btn soft" @click="showSettings=false">Schließen</button>
<button class="btn soft" @click="showSettings=false">Close</button>
</div>
</div>
</v-dialog>
@@ -668,7 +668,7 @@ const airportsData: AirportData[] = [
},
{
icao: 'EDDM',
name: 'München',
name: 'Munich',
city: 'Munich',
runways: ['26R', '26L', '08R', '08L'],
stands: ['211', '214', '302', 'N16', 'H45'],
@@ -1014,17 +1014,17 @@ const modules = ref<ModuleDef[]>([
lessons: [
{
id: 'icao-alphabet',
title: 'ICAO Alphabet & Zahlen',
desc: 'Buchstaben und Ziffern sauber buchstabieren',
title: 'ICAO Alphabet & Numbers',
desc: 'Spell letters and digits clearly',
keywords: ['Alphabet', 'Numbers', 'Normalize'],
hints: [
'Buchstabiere jeden Buchstaben mit dem ICAO-Namen (z. B. Delta, Lima, Hotel).',
'Ziffern im Funk: tree, fower, fife, niner.'
'Spell each letter with its ICAO name (e.g. Delta, Lima, Hotel).',
'Use ATC numbers: tree, fower, fife, niner.'
],
fields: [
{
key: 'letters',
label: 'Buchstaben',
label: 'Letters',
expected: scenario => scenario.callsignNato,
placeholder: 'Delta Lima Hotel',
width: 'xl',
@@ -1032,7 +1032,7 @@ const modules = ref<ModuleDef[]>([
},
{
key: 'digits',
label: 'Zahlen',
label: 'Numbers',
expected: scenario => scenario.flightNumberWords,
placeholder: 'one two three',
width: 'lg',
@@ -1040,7 +1040,7 @@ const modules = ref<ModuleDef[]>([
}
],
readback: [
{ type: 'text', text: 'Rufezeichen: ' },
{ type: 'text', text: 'Call sign: ' },
{ type: 'field', key: 'letters', width: 'lg' },
{ type: 'text', text: ' · ' },
{ type: 'field', key: 'digits', width: 'md' }
@@ -1048,21 +1048,21 @@ const modules = ref<ModuleDef[]>([
defaultFrequency: 'DEL',
phrase: scenario => `${scenario.callsignNato} ${scenario.flightNumberWords}`,
info: scenario => [
`Kennzeichen: ${scenario.callsign}`,
`Airline Call: ${scenario.radioCall}`,
`ICAO: ${scenario.callsignNato}`,
`Flightnummer gesprochen: ${scenario.flightNumberWords}`
`Callsign: ${scenario.callsign}`,
`Radio call: ${scenario.radioCall}`,
`ICAO spelling: ${scenario.callsignNato}`,
`Flight number spoken: ${scenario.flightNumberWords}`
],
generate: createBaseScenario
},
{
id: 'atis',
title: 'ATIS verstehen',
desc: 'Kennung und Kerndaten aus der ATIS ziehen',
keywords: ['ATIS', 'Wetter'],
title: 'Understand the ATIS',
desc: 'Extract the identifier and key data from the ATIS',
keywords: ['ATIS', 'Weather'],
hints: [
'ATIS-Kennung als einzelnen Buchstaben merken.',
'Reihenfolge: Runway Wind Sicht Temperatur Taupunkt QNH.'
'Remember the ATIS identifier as a single letter.',
'Order: runway wind visibility temperature dew point QNH.'
],
fields: [
{
@@ -1074,7 +1074,7 @@ const modules = ref<ModuleDef[]>([
scenario.atisCode.toLowerCase(),
`Information ${scenario.atisCode}`
],
placeholder: 'Buchstabe',
placeholder: 'Letter',
width: 'xs',
threshold: 0.9
},
@@ -1098,21 +1098,21 @@ const modules = ref<ModuleDef[]>([
},
{
key: 'atis-visibility',
label: 'Sicht',
label: 'Visibility',
expected: scenario => scenario.atisSummary.visibility,
alternatives: scenario => [scenario.visibility, scenario.visibilityWords],
width: 'sm'
},
{
key: 'atis-temp',
label: 'Temperatur',
label: 'Temperature',
expected: scenario => scenario.atisSummary.temperature,
alternatives: scenario => [scenario.temperatureWords],
width: 'sm'
},
{
key: 'atis-dew',
label: 'Taupunkt',
label: 'Dew point',
expected: scenario => scenario.atisSummary.dewpoint,
alternatives: scenario => [scenario.dewpointWords],
width: 'sm'
@@ -1144,19 +1144,19 @@ const modules = ref<ModuleDef[]>([
defaultFrequency: 'ATIS',
phrase: scenario => scenario.atisText,
info: () => [
'Notiere Kennung, Runway, Wind, Sichtweite, Temperatur, Taupunkt und QNH.',
'Sicht: vier Ziffern oder 9999 für ≥10 km · QNH als vierstellige Zahl.'
'Note the identifier, runway, wind, visibility, temperature, dew point, and QNH.',
'Visibility: four digits or 9999 for ≥10 km; QNH as a four-digit value.'
],
generate: createBaseScenario
},
{
id: 'metar',
title: 'METAR entschlüsseln',
desc: 'Rohe METAR-Werte extrahieren',
title: 'Decode the METAR',
desc: 'Extract the raw METAR values',
keywords: ['METAR', 'Weather'],
hints: [
'METAR blockweise lesen: Wind Sicht Wolken Temperatur QNH.',
'Der Temperaturblock hat die Form 18/10, Minuswerte beginnen mit M.'
'Read the METAR in blocks: wind visibility clouds temperature QNH.',
'The temperature block looks like 18/10; negative values start with M.'
],
fields: [
{
@@ -1172,7 +1172,7 @@ const modules = ref<ModuleDef[]>([
},
{
key: 'metar-vis',
label: 'Sicht',
label: 'Visibility',
expected: scenario => scenario.metarSegments.visibility,
alternatives: scenario => [scenario.visibility],
placeholder: '9999',
@@ -1199,9 +1199,9 @@ const modules = ref<ModuleDef[]>([
readback: [
{ type: 'text', text: 'Wind ' },
{ type: 'field', key: 'metar-wind', width: 'md' },
{ type: 'text', text: ', Sicht ' },
{ type: 'text', text: ', visibility ' },
{ type: 'field', key: 'metar-vis', width: 'sm' },
{ type: 'text', text: ', Temperatur ' },
{ type: 'text', text: ', temperature ' },
{ type: 'field', key: 'metar-temp', width: 'md' },
{ type: 'text', text: ', QNH ' },
{ type: 'field', key: 'metar-qnh', width: 'sm' }
@@ -1210,18 +1210,18 @@ const modules = ref<ModuleDef[]>([
phrase: scenario => scenario.metar,
info: scenario => [
`METAR: ${scenario.metar}`,
`Interpretation: Wind ${scenario.metarSegments.wind}, Sicht ${scenario.visibilityWords}, Temperatur ${scenario.temperature}°C, QNH ${scenario.qnh}`
`Interpretation: Wind ${scenario.metarSegments.wind}, visibility ${scenario.visibilityWords}, temperature ${scenario.temperature}°C, QNH ${scenario.qnh}`
],
generate: createBaseScenario
},
{
id: 'radio-check',
title: 'Radio Check',
desc: 'Lesbarkeit melden',
desc: 'Report readability',
keywords: ['Ground', 'Comms'],
hints: [
'Antwort besteht aus "Readability" plus Zahl.',
'Beende den Check immer mit deinem Callsign.'
'Reply with "Readability" followed by the number.',
'Always finish the check with your call sign.'
],
fields: [
{
@@ -1238,7 +1238,7 @@ const modules = ref<ModuleDef[]>([
},
{
key: 'rc-readability',
label: 'Lesbarkeit',
label: 'Readability',
expected: scenario => scenario.readabilityWord,
alternatives: scenario => [scenario.readability.toString(), scenario.readabilityWord],
placeholder: 'five',
@@ -1254,8 +1254,8 @@ const modules = ref<ModuleDef[]>([
defaultFrequency: 'GND',
phrase: scenario => `${scenario.airport.name} Ground, ${scenario.radioCall}, radio check on ${scenario.groundFreq}.`,
info: scenario => [
`Frequenz: ${scenario.groundFreq} (${scenario.frequencyWords.GND})`,
`Erwartete Antwort: Readability ${scenario.readability} (${scenario.readabilityWord})`
`Frequency: ${scenario.groundFreq} (${scenario.frequencyWords.GND})`,
`Expected response: Readability ${scenario.readability} (${scenario.readabilityWord})`
],
generate: createBaseScenario
}
@@ -1264,17 +1264,17 @@ const modules = ref<ModuleDef[]>([
{
id: 'arc',
title: 'ARC Decision Tree',
subtitle: 'Vom Clearance Call bis Departure',
subtitle: 'From clearance call to departure',
art: gradientArt(['#f97316', '#fb923c', '#0f172a']),
lessons: [
{
id: 'clearance-contact',
title: 'Delivery: Erstkontakt',
desc: 'Clearance Delivery kontaktieren',
title: 'Delivery: Initial contact',
desc: 'Contact clearance delivery',
keywords: ['Delivery', 'Clearance'],
hints: [
'Reihenfolge: Einheit, Callsign, ATIS, Ziel, Stand, Request.',
'ATIS als einzelner Buchstabe sprechen.'
'Order: unit, call sign, ATIS, destination, stand, request.',
'Say the ATIS as a single letter.'
],
fields: [
{
@@ -1298,7 +1298,7 @@ const modules = ref<ModuleDef[]>([
},
{
key: 'cd-stand',
label: 'Stand',
label: 'Stand/Gate',
expected: scenario => scenario.stand,
width: 'sm'
}
@@ -1318,20 +1318,20 @@ const modules = ref<ModuleDef[]>([
defaultFrequency: 'DEL',
phrase: scenario => `${scenario.airport.city} Delivery, ${scenario.radioCall}, information ${scenario.atisCode}, IFR to ${scenario.destination.city}, stand ${scenario.stand}, request clearance.`,
info: scenario => [
`ATIS ${scenario.atisCode}`,
`Ziel: ${scenario.destination.city} (${scenario.destination.icao})`,
`Stand: ${scenario.stand}`
`ATIS: ${scenario.atisCode}`,
`Destination: ${scenario.destination.city} (${scenario.destination.icao})`,
`Stand/Gate: ${scenario.stand}`
],
generate: createBaseScenario
},
{
id: 'clearance-readback',
title: 'Clearance Readback',
desc: 'Freigabe komplett zurücklesen',
desc: 'Read back the clearance in full',
keywords: ['Delivery', 'Readback'],
hints: [
'Reihenfolge merken: Ziel SID Runway Altitude Squawk.',
'Zahlen bei Altitude und Squawk sauber ausschreiben.'
'Remember the order: destination SID runway altitude squawk.',
'Speak altitude and squawk digits clearly.'
],
fields: [
{
@@ -1393,11 +1393,11 @@ const modules = ref<ModuleDef[]>([
{
id: 'pushback',
title: 'Push & Start Readback',
desc: 'Pushback-Freigabe bestätigen',
desc: 'Acknowledge the pushback clearance',
keywords: ['Ground', 'Pushback'],
hints: [
'Bahnrichtung und QNH übernehmen, Callsign ans Ende.',
'QNH darf als Zahl oder "QNH xxxx" stehen.'
'Repeat the runway direction and QNH, call sign at the end.',
'QNH may be just the number or "QNH xxxx".'
],
fields: [
{
@@ -1437,7 +1437,7 @@ const modules = ref<ModuleDef[]>([
defaultFrequency: 'GND',
phrase: scenario => `${scenario.radioCall}, push and start approved, facing runway ${scenario.runway}. QNH ${scenario.qnh}.`,
info: scenario => [
`Stand: ${scenario.stand}`,
`Stand/Gate: ${scenario.stand}`,
`Ground: ${scenario.groundFreq} (${scenario.frequencyWords.GND})`
],
generate: createBaseScenario
@@ -1445,11 +1445,11 @@ const modules = ref<ModuleDef[]>([
{
id: 'taxi',
title: 'Taxi Readback',
desc: 'Taxi-Freigabe mit Hold Short',
desc: 'Read back the taxi clearance with hold short',
keywords: ['Ground', 'Taxi'],
hints: [
'Route wie gehört wiederholen, inklusive Hold Short.',
'Am Ende wieder Callsign.'
'Repeat the route exactly as received, including the hold short.',
'Finish with the call sign.'
],
fields: [
{
@@ -1498,19 +1498,19 @@ const modules = ref<ModuleDef[]>([
defaultFrequency: 'GND',
phrase: scenario => `${scenario.radioCall}, taxi to runway ${scenario.runway} via ${scenario.taxiRoute}, hold short runway ${scenario.runway}.`,
info: scenario => [
`Taxi-Route: ${scenario.taxiRoute}`,
`Hold Short: ${scenario.runway}`
`Taxi route: ${scenario.taxiRoute}`,
`Hold short: ${scenario.runway}`
],
generate: createBaseScenario
},
{
id: 'lineup',
title: 'Line-up Clearance',
desc: 'Aufrollen und warten bestätigen',
desc: 'Acknowledge line up and wait',
keywords: ['Tower', 'Line Up'],
hints: [
'Runway wiederholen, dann "line up and wait".',
'Callsign am Ende setzen.'
'Repeat the runway, then say "line up and wait".',
'Place the call sign at the end.'
],
fields: [
{
@@ -1549,11 +1549,11 @@ const modules = ref<ModuleDef[]>([
{
id: 'takeoff',
title: 'Takeoff Clearance',
desc: 'Takeoff-Freigabe bestätigen',
desc: 'Acknowledge the takeoff clearance',
keywords: ['Tower', 'Departure'],
hints: [
'Reihenfolge: Runway cleared for takeoff Callsign.',
'Windangabe kann entfallen, wenn nicht gefordert.'
'Order: runway cleared for takeoff call sign.',
'Wind information can be omitted if it was not given.'
],
fields: [
{
@@ -1592,11 +1592,11 @@ const modules = ref<ModuleDef[]>([
{
id: 'departure-handoff',
title: 'Departure Handoff',
desc: 'Auf Departure wechseln',
desc: 'Switch to departure',
keywords: ['Departure', 'Handoff'],
hints: [
'Frequenz exakt wiederholen, optional als Zahl oder gesprochen.',
'Callsign am Ende anhängen.'
'Repeat the frequency exactly, either as digits or spoken form.',
'Append the call sign at the end.'
],
fields: [
{
@@ -1632,18 +1632,18 @@ const modules = ref<ModuleDef[]>([
phrase: scenario => `${scenario.radioCall}, contact departure ${scenario.departureFreq}.`,
info: scenario => [
`Departure: ${scenario.departureFreq} (${scenario.frequencyWords.DEP})`,
`Tower Handoff nach Start.`
`Tower handoff after departure.`
],
generate: createBaseScenario
},
{
id: 'departure-checkin',
title: 'Departure Check-in',
desc: 'Erstmeldung bei Departure',
desc: 'Initial call to departure',
keywords: ['Departure', 'Check-in'],
hints: [
'Einheit ansprechen, dann Callsign nennen.',
'Altitude und SID exakt wie gehört wiederholen.'
'Address the unit first, then state the call sign.',
'Repeat the altitude and SID exactly as received.'
],
fields: [
{
@@ -1694,11 +1694,11 @@ const modules = ref<ModuleDef[]>([
{
id: 'climb-instruction',
title: 'Climb & Direct',
desc: 'Steigauftrag komplett zurücklesen',
desc: 'Read back the climb instruction in full',
keywords: ['Departure', 'Climb'],
hints: [
'Mit "Climb" beginnen, dann Höhe und gegebenenfalls Direct.',
'Callsign am Ende wiederholen.'
'Start with "Climb", then the altitude and any direct clearance.',
'Repeat the call sign at the end.'
],
fields: [
{
@@ -1749,11 +1749,11 @@ const modules = ref<ModuleDef[]>([
{
id: 'center-handoff',
title: 'Center Handoff',
desc: 'Übergabe an Center bestätigen',
desc: 'Acknowledge the handoff to center',
keywords: ['Center', 'Handoff'],
hints: [
'Frequenz exakt wiedergeben (mit oder ohne Dezimalpunkt).',
'Callsign am Ende platzieren.'
'Repeat the frequency exactly (with or without the decimal point).',
'Place the call sign at the end.'
],
fields: [
{
@@ -1789,7 +1789,7 @@ const modules = ref<ModuleDef[]>([
phrase: scenario => `${scenario.radioCall}, contact center ${scenario.centerFreq}.`,
info: scenario => [
`Center: ${scenario.centerFreq} (${scenario.frequencyWords.CTR})`,
`Nächste Einheit: Center`
`Next unit: Center`
],
generate: createBaseScenario
}
@@ -2003,7 +2003,7 @@ function blankStateClass(key: string): string {
}
function fieldLabel(key: string): string {
return fieldMap.value[key]?.label ?? 'Feld'
return fieldMap.value[key]?.label ?? 'Field'
}
function fieldPlaceholder(key: string): string {
@@ -2185,9 +2185,9 @@ function avgScore(modId: string) {
}
const dailies = ref([
{ id: 'd1', title: '3 Readbacks ≥80%', sub: 'Belohnung: +50 XP', reward: 50 },
{ id: 'd2', title: '1 Modul starten', sub: 'Belohnung: +20 XP', reward: 20 },
{ id: 'd3', title: 'Zielphrase abspielen', sub: 'Belohnung: +10 XP', reward: 10 }
{ id: 'd1', title: '3 readbacks ≥80%', sub: 'Reward: +50 XP', reward: 50 },
{ id: 'd2', title: 'Start 1 module', sub: 'Reward: +20 XP', reward: 20 },
{ id: 'd3', title: 'Play the target phrase', sub: 'Reward: +10 XP', reward: 10 }
])
function startDaily(daily: { id: string; reward: number; title: string }) {

View File

@@ -22,7 +22,7 @@
<v-card-text class="space-y-4">
<div>
<h2 class="text-lg font-semibold">VATSIM Integration</h2>
<p class="text-sm text-white/70">Gib deine CID ein um deine gefilterten Flugpläne zu laden</p>
<p class="text-sm text-white/70">Enter your CID to load your filtered flight plans.</p>
</div>
<v-text-field
v-model="vatsimId"
@@ -41,7 +41,7 @@
:loading="loading"
@click="loadFlightPlans"
>
Flugpläne abrufen
Load flight plans
</v-btn>
<v-alert
v-if="error"
@@ -60,7 +60,7 @@
<v-card-text class="space-y-4">
<div>
<h2 class="text-lg font-semibold">Demo Mode</h2>
<p class="text-sm text-white/70">Starte mit einem Beispiel-Flugplan zum Testen</p>
<p class="text-sm text-white/70">Start with a sample flight plan for testing.</p>
</div>
<v-btn
block
@@ -68,7 +68,7 @@
variant="outlined"
@click="startDemoFlight"
>
Demo starten (DLH39A EDDFEDDM)
Start demo (DLH39A EDDFEDDM)
</v-btn>
</v-card-text>
</v-card>
@@ -80,20 +80,20 @@
<v-btn icon @click="currentScreen = 'login'" class="text-cyan-300">
<v-icon>mdi-arrow-left</v-icon>
</v-btn>
<h2 class="text-lg font-semibold">Verfügbare Flugpläne</h2>
<h2 class="text-lg font-semibold">Available flight plans</h2>
<v-chip color="cyan" variant="outlined" size="small">{{ vatsimId }}</v-chip>
</div>
<div v-if="loading" class="text-center py-8">
<v-progress-circular indeterminate color="cyan" class="mb-4" />
<p class="text-sm text-white/70">Lade Flugpläne von VATSIM...</p>
<p class="text-sm text-white/70">Loading flight plans from VATSIM</p>
</div>
<div v-else-if="flightPlans.length === 0" class="text-center py-8">
<v-icon size="48" class="text-white/30 mb-4">mdi-airplane-off</v-icon>
<p class="text-white/70">Keine Flugpläne gefunden</p>
<p class="text-white/70">No flight plans found</p>
<v-btn color="cyan" variant="outlined" class="mt-4" @click="currentScreen = 'login'">
Zurück
Back
</v-btn>
</div>
@@ -135,7 +135,7 @@
<v-card-text class="space-y-4">
<div class="flex items-start justify-between gap-3">
<div>
<p class="text-xs uppercase tracking-[0.3em] text-white/50">Aktiver Flug</p>
<p class="text-xs uppercase tracking-[0.3em] text-white/50">Active flight</p>
<h2 class="text-2xl font-semibold">{{ flightContext.callsign || 'N/A' }}</h2>
<p class="text-sm text-white/70">{{ flightContext.dep }} {{ flightContext.dest }}</p>
</div>
@@ -203,7 +203,7 @@
@click="backToSetup"
prepend-icon="mdi-airplane-off"
>
Neuen Flug wählen
Select new flight
</v-btn>
</v-card-text>
</v-card>
@@ -247,7 +247,7 @@
@click="swapFrequencies"
:class="{ 'swap-animation': swapAnimation }"
>
Frequenzen tauschen
Swap frequencies
</v-btn>
<!-- Signal Quality -->
@@ -270,7 +270,7 @@
<v-card v-if="currentStep" class="bg-white/5 border border-white/10">
<v-card-text class="space-y-3">
<div class="flex items-center justify-between">
<p class="text-xs uppercase tracking-[0.3em] text-white/40">Erwartete Kommunikation</p>
<p class="text-xs uppercase tracking-[0.3em] text-white/40">Expected communication</p>
<v-chip color="orange" variant="tonal" size="small">{{ currentStep.frequencyName }}</v-chip>
</div>
@@ -285,7 +285,7 @@
<div v-if="currentStep.pilot" class="space-y-2 rounded-2xl bg-blue-500/10 border border-blue-500/20 p-3 text-sm">
<div class="flex items-center gap-2 text-blue-300">
<v-icon size="16">mdi-account-pilot</v-icon>
<span class="text-xs uppercase font-semibold">Pilot (Du)</span>
<span class="text-xs uppercase font-semibold">Pilot (You)</span>
</div>
<p class="font-mono text-white">{{ normalizeExpectedText(currentStep.pilot) }}</p>
</div>
@@ -314,9 +314,9 @@
class="bg-cyan-500/10 text-cyan-100 mb-4"
density="compact"
>
Mikrofonberechtigung erforderlich r Push-To-Talk.
Microphone permission required for push-to-talk.
<template #append>
<v-btn color="cyan" size="small" variant="flat" @click="requestMicAccess">Erlauben</v-btn>
<v-btn color="cyan" size="small" variant="flat" @click="requestMicAccess">Allow</v-btn>
</template>
</v-alert>
@@ -344,7 +344,7 @@
{{ radioMode === 'atc' ? 'ATC' : 'INTERCOM' }}
</p>
<p class="mt-2 text-sm text-white/60">
{{ radioMode === 'atc' ? `Sendet auf ${frequencies.active}` : 'Crew Intercom • Checklist' }}
{{ radioMode === 'atc' ? `Transmitting on ${frequencies.active}` : 'Crew Intercom • Checklist' }}
</p>
</div>
</div>
@@ -376,7 +376,7 @@
prepend-icon="mdi-information-variant"
density="comfortable"
>
ATIS abhören
Listen to ATIS
</v-btn>
<v-btn
color="cyan"
@@ -386,7 +386,7 @@
prepend-icon="mdi-progress-check"
density="comfortable"
>
Voller Flug (Simulation)
Full flight (simulation)
</v-btn>
</div>
</v-card-text>
@@ -396,7 +396,7 @@
<v-card class="bg-white/5 border border-white/10">
<v-card-text class="space-y-3">
<div class="flex flex-wrap items-center justify-between gap-3">
<h3 class="text-lg font-semibold">Frequenzübersicht</h3>
<h3 class="text-lg font-semibold">Frequency overview</h3>
<div class="flex items-center gap-2">
<v-progress-circular
v-if="airportFrequencyLoading"
@@ -421,7 +421,7 @@
<v-expansion-panels variant="accordion" class="bg-transparent">
<v-expansion-panel>
<v-expansion-panel-title class="text-sm text-white/70">
Frequenzen r {{ flightContext.dep || 'Abflug' }}
Frequencies for {{ flightContext.dep || 'departure' }}
</v-expansion-panel-title>
<v-expansion-panel-text>
<div
@@ -429,13 +429,13 @@
class="flex items-center gap-2 text-sm text-white/60"
>
<v-progress-circular indeterminate size="20" color="cyan" />
<span>Lade Frequenzen vom Netzwerk</span>
<span>Loading frequencies from the network</span>
</div>
<div
v-else-if="airportFrequencies.length === 0"
class="text-xs text-white/50"
>
Keine Frequenzen verfügbar. Bitte später erneut versuchen.
No frequencies available. Please try again later.
</div>
<div v-else class="space-y-3">
<div
@@ -527,7 +527,7 @@
@click:append-inner="sendPilotText"
/>
<p class="text-xs text-white/50">
Für Notfälle wenn PTT nicht funktioniert oder r Tests
For emergencies when PTT fails or for testing
</p>
</v-card-text>
</v-card>
@@ -546,7 +546,7 @@
</v-chip>
</div>
<v-chip size="small" :color="simulationRunning ? 'orange' : 'grey'" variant="tonal">
{{ simulationRunning ? 'Läuft' : 'Bereit' }}
{{ simulationRunning ? 'Running' : 'Ready' }}
</v-chip>
</div>
@@ -564,7 +564,7 @@
v-if="simulationRunning && simulationTrace.length === 0"
class="text-sm text-white/60"
>
Simulation initialisiert...
Simulation initializing...
</div>
<div
@@ -602,22 +602,22 @@
</div>
<div class="space-y-2 rounded-2xl border border-white/10 bg-black/30 p-3">
<p class="text-xs uppercase tracking-[0.3em] text-white/40">Aktueller Knoten</p>
<p class="text-xs uppercase tracking-[0.3em] text-white/40">Current node</p>
<p class="font-mono text-sm text-white">{{ debugState?.id || '—' }}</p>
<p class="text-[11px] text-white/50">
{{ debugState ? `${debugState.role}${debugState.phase}` : 'N/A' }}
<span v-if="debugState?.frequencyName" class="ml-1 text-white/40">({{ debugState.frequencyName }})</span>
</p>
<p v-if="debugState?.sayPlain" class="text-xs text-white/70">
Auto: <span class="font-mono text-white">{{ debugState.sayPlain }}</span>
Auto (LLM): <span class="font-mono text-white">{{ debugState.sayPlain }}</span>
</p>
<p v-if="debugState?.sayNormalized" class="text-[11px] text-white/40">
Funk: {{ debugState.sayNormalized }}
Radio: {{ debugState.sayNormalized }}
</p>
</div>
<div>
<p class="text-xs uppercase tracking-[0.3em] text-white/40 mb-2">Nächste Entscheidungen</p>
<p class="text-xs uppercase tracking-[0.3em] text-white/40 mb-2">Upcoming decisions</p>
<div v-if="debugNextStates.length" class="space-y-2">
<div
v-for="state in debugNextStates"
@@ -646,11 +646,11 @@
ATC: <span class="font-mono text-white">{{ state.sayPlain }}</span>
</p>
<p v-if="state.sayNormalized" class="text-[11px] text-white/40">
Funk: {{ state.sayNormalized }}
Radio: {{ state.sayNormalized }}
</p>
</div>
</div>
<p v-else class="text-xs text-white/50">Keine weiteren Entscheidungen verfügbar.</p>
<p v-else class="text-xs text-white/50">No further decisions available.</p>
</div>
</v-card-text>
</v-card>
@@ -671,7 +671,7 @@
:disabled="log.length === 0"
@click="clearLog"
>
Leeren
Clear
</v-btn>
</div>
@@ -702,7 +702,7 @@
</div>
<p v-if="log.length === 0" class="text-xs text-white/50 text-center py-4">
Noch keine Kommunikation aufgezeichnet.
No communications recorded yet.
</p>
</div>
</v-card-text>
@@ -730,7 +730,7 @@
class="text-xs uppercase tracking-[0.3em]"
:class="lastTransmissionFaulty ? 'text-red-300' : 'text-cyan-300'"
>
Letzte Übertragung
Last transmission
</span>
</div>
<div class="flex flex-wrap items-center gap-1">
@@ -740,7 +740,7 @@
color="red"
variant="flat"
>
Fehlerhaft
Faulty
</v-chip>
<v-btn
variant="text"
@@ -752,7 +752,7 @@
<template #prepend>
<v-icon size="16">mdi-alert-circle-outline</v-icon>
</template>
{{ lastTransmissionFaulty ? 'Fehler bearbeiten' : 'Fehler markieren' }}
{{ lastTransmissionFaulty ? 'Edit issue' : 'Mark as faulty' }}
</v-btn>
<v-btn
v-if="lastTransmissionFaulty"
@@ -765,7 +765,7 @@
<template #prepend>
<v-icon size="16">mdi-restore</v-icon>
</template>
Zurücksetzen
Reset
</v-btn>
<v-btn
variant="text"
@@ -777,7 +777,7 @@
<template #prepend>
<v-icon size="16">mdi-close</v-icon>
</template>
Löschen
Delete
</v-btn>
</div>
</div>
@@ -786,12 +786,12 @@
v-if="lastTransmissionFaulty"
class="space-y-2 rounded-xl border border-red-400/30 bg-red-500/10 px-3 py-2"
>
<p class="text-xs uppercase tracking-[0.25em] text-red-200/80">Fehlerbeschreibung</p>
<p class="text-xs uppercase tracking-[0.25em] text-red-200/80">Issue description</p>
<p v-if="lastTransmissionFaultNote" class="text-sm text-red-100">
{{ lastTransmissionFaultNote }}
</p>
<p v-else class="text-xs text-red-200/70">
Als fehlerhaft markiert.
Marked as faulty.
</p>
</div>
</v-card-text>
@@ -801,16 +801,15 @@
<v-card class="bg-[#0b101d] border border-white/10 text-white">
<v-card-title class="flex items-center gap-2 text-base font-semibold">
<v-icon icon="mdi-alert-circle-outline" color="#f87171" size="20" />
Übertragung als fehlerhaft markieren
Mark transmission as faulty
</v-card-title>
<v-card-text class="space-y-4 text-sm text-white/70">
<p>
Hinterlasse optional eine kurze Beschreibung, damit das Dev Team die
Übertragung nachvollziehen kann.
Optionally leave a short note so the dev team can follow up on the transmission.
</p>
<v-textarea
v-model="transmissionIssueNote"
label="Fehlerbeschreibung (optional)"
label="Issue description (optional)"
variant="outlined"
color="red"
rows="3"
@@ -819,7 +818,7 @@
</v-card-text>
<v-card-actions class="justify-end gap-2">
<v-btn variant="text" color="grey" @click="cancelTransmissionIssue">
Abbrechen
Cancel
</v-btn>
<v-btn
v-if="lastTransmissionFaulty"
@@ -827,10 +826,10 @@
color="cyan"
@click="removeTransmissionIssue"
>
Markierung entfernen
Remove flag
</v-btn>
<v-btn color="red" variant="flat" @click="confirmTransmissionIssue">
Speichern
Save
</v-btn>
</v-card-actions>
</v-card>
@@ -844,7 +843,7 @@
<div class="space-y-4">
<div>
<label class="text-xs uppercase tracking-[0.3em] text-white/40 block mb-2">
Signal Stärke
Signal strength
</label>
<v-slider
v-model="signalStrength"
@@ -863,14 +862,14 @@
v-model="radioEffectsEnabled"
color="cyan"
inset
label="Radio-Effekte"
label="Radio effects"
hide-details
/>
<v-switch
v-model="readbackEnabled"
color="cyan"
inset
label="Readback Stimme"
label="Readback voice"
hide-details
/>
<v-switch
@@ -1579,11 +1578,11 @@ const loadFlightPlans = async () => {
flightPlans.value = response.slice(0, 10)
currentScreen.value = 'flightselect'
} else {
error.value = 'Keine Flugpläne für diese VATSIM ID gefunden'
error.value = 'No flight plans found for this VATSIM ID'
}
} catch (err) {
console.error('Error loading flight plans:', err)
error.value = 'Fehler beim Laden der Flugpläne. Bitte VATSIM ID prüfen.'
error.value = 'Error loading flight plans. Please check the VATSIM ID.'
} finally {
loading.value = false
}
@@ -1947,7 +1946,7 @@ const playAtisBroadcast = async () => {
}
if (!content) {
setLastTransmission('ATIS: Keine aktuellen Informationen verfügbar')
setLastTransmission('ATIS: No current information available')
return
}

View File

@@ -5,7 +5,7 @@ export type Lesson = { id: string; title: string; desc: string; target: string;
export type ModuleDef = { id: string; title: string; subtitle: string; art: string; lessons: Lesson[] }
const modules = ref<ModuleDef[]>([
// NEW: ICAO Kapitel
// New: ICAO chapter
{
id: 'icao',
title: 'ICAO Alphabet',
@@ -15,33 +15,33 @@ const modules = ref<ModuleDef[]>([
{
id: 'alpha',
title: 'Alphabet AM',
desc: 'Alpha bis Mike sprechen.',
desc: 'Say Alpha through Mike.',
target: 'Alpha Bravo Charlie Delta Echo Foxtrot Golf Hotel India Juliett Kilo Lima Mike.',
hints: ['konstant sprechen', 'deutlich trennen'],
hints: ['Keep a steady pace', 'Separate each word clearly'],
keywords: ['Alpha', 'Bravo', 'Charlie', 'Delta', 'Echo', 'Foxtrot', 'Golf', 'Hotel', 'India', 'Juliett', 'Kilo', 'Lima', 'Mike']
},
{
id: 'alpha2',
title: 'Alphabet NZ',
desc: 'November bis Zulu.',
desc: 'November through Zulu.',
target: 'November Oscar Papa Quebec Romeo Sierra Tango Uniform Victor Whiskey X-ray Yankee Zulu.',
hints: ['X-ray mit hyphen', 'Juliett mit zwei t'],
hints: ['Spell X-ray with a hyphen', "Juliett has two t's"],
keywords: ['November', 'Oscar', 'Papa', 'Quebec', 'Romeo', 'Sierra', 'Tango', 'Uniform', 'Victor', 'Whiskey', 'X-ray', 'Yankee', 'Zulu']
},
{
id: 'numbers',
title: 'Zahlen',
desc: 'ICAO-Nummernlesen.',
title: 'Numbers',
desc: 'Read ICAO numbers.',
target: 'Tree Fower Fife Six Seven Eight Niner Zero.',
hints: ['Nine → Niner', 'Three → Tree', 'Four → Fower', 'Five → Fife'],
keywords: ['Tree', 'Fower', 'Fife', 'Niner']
},
{
id: 'callsign-icao',
title: 'Callsign Buchstabieren',
desc: 'Beispiel-Callsign.',
title: 'Spell the call sign',
desc: 'Sample call sign.',
target: 'DLH one two three, Lufthansa one two three.',
hints: ['DLH → Lufthansa', 'Nummern ICAO'],
hints: ['DLH → Lufthansa', 'Use ICAO numbers'],
keywords: ['Lufthansa', 'DLH', 'one', 'two', 'three']
}
]
@@ -49,19 +49,19 @@ const modules = ref<ModuleDef[]>([
{
id: 'basics',
title: 'Basics',
subtitle: 'Callsign · Struktur · Zahlen',
subtitle: 'Call sign · Structure · Numbers',
art: 'https://images.unsplash.com/photo-1541392822270-85b2ff6c4577?q=80&w=1600&auto=format&fit=crop',
lessons: [
{
id: 'checkin',
title: 'Check-in',
desc: 'Erster Call korrekt.',
desc: 'Make the first call correctly.',
target: 'Frankfurt Ground, Lufthansa one two three at stand A12, request taxi.',
hints: ['Station • Callsign • Position • Intent'],
keywords: ['Frankfurt Ground', 'Lufthansa', 'stand', 'request taxi']
},
{
id: 'readback', title: 'Short Readback', desc: 'Kurz bestätigen.',
id: 'readback', title: 'Short Readback', desc: 'Give a short acknowledgement.',
target: 'Lufthansa one two three, roger.',
hints: ['Callsign + roger/affirm'], keywords: ['roger', 'affirm']
}
@@ -79,7 +79,7 @@ const modules = ref<ModuleDef[]>([
hints: ['Taxi to runway • via • hold short'], keywords: ['taxi to runway', 'via', 'hold short']
},
{
id: 'handoff', title: 'Handoff', desc: 'Frequenzwechsel.',
id: 'handoff', title: 'Handoff', desc: 'Frequency change.',
target: 'Contact Tower on one one niner decimal five, Lufthansa one two three.',
hints: ['Contact Tower on … • decimal'], keywords: ['Contact Tower', 'decimal']
}
@@ -92,7 +92,7 @@ const modules = ref<ModuleDef[]>([
art: 'https://images.unsplash.com/photo-1494412685616-a5d310fbb07d?q=80&w=1600&auto=format&fit=crop',
lessons: [
{
id: 'lineup', title: 'Line up', desc: 'Aufrollen und warten.',
id: 'lineup', title: 'Line up', desc: 'Line up and wait.',
target: 'Lufthansa one two three, line up and wait runway two five.',
hints: ['line up and wait'], keywords: ['line up and wait']
}
@@ -105,7 +105,7 @@ const modules = ref<ModuleDef[]>([
art: 'https://images.unsplash.com/photo-1542089363-07b2d92aacc3?q=80&w=1600&auto=format&fit=crop',
lessons: [
{
id: 'vacate', title: 'Vacate', desc: 'Verlasse Bahn, melde frei.',
id: 'vacate', title: 'Vacate', desc: 'Exit the runway and report clear.',
target: 'Lufthansa one two three, vacated runway two five via A six.',
hints: ['vacated runway • via taxiway'], keywords: ['vacated', 'runway']
}
@@ -118,7 +118,7 @@ const modules = ref<ModuleDef[]>([
art: 'https://images.unsplash.com/photo-1508264769638-658b34d79f6e?q=80&w=1600&auto=format&fit=crop',
lessons: [
{
id: 'checkin', title: 'IFR Check-in', desc: 'Erster Online-Call.',
id: 'checkin', title: 'IFR Check-in', desc: 'First online call.',
target: 'Frankfurt Ground, Lufthansa one two three, A320 at stand A12, IFR to Munich, information Bravo, request clearance.',
hints: ['IFR/VFR • ATIS Info • Request'], keywords: ['IFR', 'information', 'request clearance']
}

View File

@@ -1,8 +1,8 @@
// composables/communicationsEngine.ts
// communicationsEngine composable
import { ref, computed, readonly } from 'vue'
import atcDecisionTree from "../data/atcDecisionTree";
// --- DecisionTree-Types (aus ~/data/atcDecisionTree.json abgeleitet) ---
// --- DecisionTree types (derived from ~/data/atcDecisionTree.json) ---
type Role = 'pilot' | 'atc' | 'system'
type Phase =
| 'Preflight' | 'Clearance' | 'PushStart' | 'TaxiOut' | 'Departure'
@@ -31,8 +31,8 @@ interface DTState {
auto?: 'check_readback' | 'monitor' | 'end' | 'pop_stack_or_route_by_intent'
readback_required?: string[]
actions?: (DTActionSet | string)[]
frequency?: string // Für einfachere Freq-Zugriffe
frequencyName?: string // Name der Frequenz (DEL, GND, TWR, etc.)
frequency?: string // Shortcut for accessing frequencies
frequencyName?: string // Frequency label (DEL, GND, TWR, etc.)
}
interface DecisionTree {
@@ -47,8 +47,8 @@ interface DecisionTree {
emergency_active: boolean
current_unit: string
stack: string[]
off_schema_count: number // Zähler für Off-Schema Antworten
radio_checks_done: number // Zähler für Radio Checks
off_schema_count: number // Counter for off-schema responses
radio_checks_done: number // Counter for radio checks
}
policies: {
timeouts: {
@@ -67,7 +67,7 @@ interface DecisionTree {
states: Record<string, DTState>
}
// Flight Phases und Communication Steps für bessere Integration
// Flight phases and communication steps for better 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' },
@@ -90,7 +90,7 @@ export const COMMUNICATION_STEPS = [
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
// Additional steps can be defined here
]
type FrequencyVariableKey = 'atis_freq'
@@ -101,7 +101,7 @@ type FrequencyVariableKey = 'atis_freq'
| 'approach_freq'
| 'handoff_freq'
// --- ATC Decision Tree laden ---
// --- Load ATC decision tree ---
export interface FlightContext {
callsign: string
aircraft: string
@@ -140,7 +140,7 @@ export interface EngineLog {
offSchema?: boolean
}
// NATO/ICAO Normalizer (gekürzt für bessere Performance)
// NATO/ICAO normalizer trimmed for better performance
const NATO_PHONETIC: Record<string, string> = {
A: 'Alpha', B: 'Bravo', C: 'Charlie', D: 'Delta', E: 'Echo', F: 'Foxtrot',
G: 'Golf', H: 'Hotel', I: 'India', J: 'Juliett', K: 'Kilo', L: 'Lima',
@@ -212,7 +212,7 @@ export default function useCommunicationsEngine() {
const currentStateId = ref<string>(tree.value.start_state)
const communicationLog = ref<EngineLog[]>([])
// Flight Context für bessere Integration mit pm_alt.vue Stil
// Flight context used for pm_alt.vue integration
const flightContext = ref<FlightContext>({
callsign: '',
aircraft: 'A320',
@@ -264,7 +264,7 @@ export default function useCommunicationsEngine() {
}
})
// Current Step für pm_alt.vue Integration
// Current step for pm_alt.vue integration
const currentStep = computed(() => {
const phase = flightContext.value.phase
const step = COMMUNICATION_STEPS.find(s => s.phase === phase)
@@ -280,7 +280,7 @@ export default function useCommunicationsEngine() {
})
function initializeFlight(fpl: any) {
// Variablen setzen
// Set variables
variables.value = {
...variables.value,
callsign: fpl.callsign || fpl.callsign,
@@ -313,7 +313,7 @@ export default function useCommunicationsEngine() {
time_now: new Date().toISOString()
}
// Flight Context aktualisieren
// Update flight context
Object.assign(flightContext.value, {
...variables.value,
phase: 'clearance'
@@ -364,11 +364,11 @@ export default function useCommunicationsEngine() {
}
function applyLLMDecision(decision: any) {
// Updates anwenden
// Apply updates
if (decision.updates) Object.assign(variables.value, decision.updates)
if (decision.flags) Object.assign(flags.value, decision.flags)
// Off-Schema und Radio Check Tracking
// Track off-schema responses and radio checks
if (decision.off_schema) {
flags.value.off_schema_count++
console.log(`[Engine] Off-schema response #${flags.value.off_schema_count}`)
@@ -378,7 +378,7 @@ export default function useCommunicationsEngine() {
console.log(`[Engine] Radio check #${flags.value.radio_checks_done}`)
}
// Controller Response
// Controller response
if (decision.controller_say_tpl) {
speak('atc', decision.controller_say_tpl, currentStateId.value, {
radioCheck: decision.radio_check,
@@ -386,7 +386,7 @@ export default function useCommunicationsEngine() {
})
}
// State Transition (nur wenn nicht Radio Check)
// State transition (only when not a radio check)
if (!decision.radio_check) {
moveTo(decision.next_state)
}
@@ -396,7 +396,7 @@ export default function useCommunicationsEngine() {
// Log pilot input
speak('pilot', transcript, currentStateId.value)
// Radio Check Detection (fallback falls LLM es nicht erkennt)
// Radio check detection (fallback if the LLM misses it)
const t = transcript.toLowerCase()
if (t.includes('radio check') || (t.includes('read') && t.includes('check'))) {
const callsign = variables.value.callsign || ''
@@ -417,7 +417,7 @@ export default function useCommunicationsEngine() {
return null
}
return null // LLM soll entscheiden
return null // Let the LLM decide
}
function processUserTransmission(transcript: string): string | null {
@@ -437,7 +437,7 @@ export default function useCommunicationsEngine() {
currentStateId.value = stateId
const s = currentState.value
// Actions ausführen
// Execute actions
for (const act of s.actions ?? []) {
if (typeof act === 'string') continue
if (act.if && !safeEvalBoolean(act.if)) continue
@@ -457,7 +457,7 @@ export default function useCommunicationsEngine() {
speak(s.role, s.say_tpl, s.id!)
}
// Phase Update für Flight Context
// Update flight context phase
updateFlightPhase(s.phase)
}
@@ -599,7 +599,7 @@ export default function useCommunicationsEngine() {
communicationLog: readonly(communicationLog),
clearCommunicationLog: () => { communicationLog.value = [] },
// pm_alt.vue Integration
// pm_alt.vue integration
flightContext: readonly(flightContext),
currentStep,