mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-08 02:25:36 +08:00
merge
This commit is contained in:
@@ -10,80 +10,53 @@
|
||||
>
|
||||
<section class="cookie-consent-panel">
|
||||
<header class="cookie-consent-header">
|
||||
<h2 id="cookie-consent-title">{{ isGerman ? 'Cookies & Analysen' : 'Cookies & analytics' }}</h2>
|
||||
<h2 id="cookie-consent-title">Cookies & Analytics</h2>
|
||||
<p id="cookie-consent-description">
|
||||
<template v-if="isGerman">
|
||||
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
|
||||
</template>
|
||||
<template v-else>
|
||||
We use essential cookies to keep the site reliable. We would also like to use optional Hotjar analytics cookies to improve your
|
||||
experience. You decide whether to allow them—see our
|
||||
</template>
|
||||
<a href="/datenschutz" target="_blank" rel="noopener">{{ isGerman ? 'Datenschutzerklärung' : 'privacy policy' }}</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 them—full 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>{{ isGerman ? 'Notwendige Cookies' : 'Essential cookies' }}</h3>
|
||||
<p>
|
||||
{{
|
||||
isGerman
|
||||
? 'Speichern Ihre Auswahl und sorgen für sichere Anmeldung sowie eine stabile Grundfunktionalität.'
|
||||
: 'Store your selection and enable secure sign-in plus a stable core experience.'
|
||||
}}
|
||||
</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">{{ isGerman ? 'Immer aktiv' : 'Always on' }}</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>{{ isGerman ? 'Analyse (Hotjar)' : 'Analytics (Hotjar)' }}</h3>
|
||||
<p>
|
||||
{{
|
||||
isGerman
|
||||
? 'Hilft uns, Nutzungsverhalten anonym auszuwerten und unsere Inhalte gezielt zu verbessern.'
|
||||
: 'Helps us analyse anonymised usage to improve content and product decisions.'
|
||||
}}
|
||||
</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"
|
||||
class="cookie-toggle"
|
||||
role="switch"
|
||||
:aria-checked="analyticsSelection"
|
||||
@click="toggleAnalytics"
|
||||
type="button"
|
||||
class="cookie-toggle"
|
||||
role="switch"
|
||||
:aria-checked="analyticsSelection"
|
||||
@click="toggleAnalytics"
|
||||
>
|
||||
<span class="cookie-toggle-track" :class="{ 'is-active': analyticsSelection }">
|
||||
<span class="cookie-toggle-thumb" />
|
||||
</span>
|
||||
<span class="sr-only">
|
||||
{{
|
||||
isGerman
|
||||
? `Analyse-Cookies ${analyticsSelection ? 'deaktivieren' : 'aktivieren'}`
|
||||
: `${analyticsSelection ? 'Disable' : 'Enable'} analytics cookies`
|
||||
}}
|
||||
{{ analyticsSelection ? 'Disable analytics cookies' : 'Enable analytics cookies' }}
|
||||
</span>
|
||||
</button>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<p class="cookie-consent-note">
|
||||
{{
|
||||
isGerman
|
||||
? 'Optionales Tracking wird erst nach Ihrer Zustimmung geladen. Sie können Ihre Entscheidung jederzeit über „Cookie-Einstellungen“ unten links widerrufen.'
|
||||
: 'Optional tracking only loads after you consent. You can revisit your choice at any time via “Cookie settings” in the lower left.'
|
||||
}}
|
||||
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">{{ isGerman ? 'Nur notwendige' : 'Only necessary' }}</button>
|
||||
<button type="button" class="cookie-button secondary" @click="handleSave">{{ isGerman ? 'Auswahl speichern' : 'Save selection' }}</button>
|
||||
<button type="button" class="cookie-button primary" @click="handleAcceptAll">{{ isGerman ? 'Alle akzeptieren' : 'Accept all' }}</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>
|
||||
@@ -91,22 +64,19 @@
|
||||
|
||||
<transition name="cookie-manage">
|
||||
<button v-if="showManageButton" type="button" class="cookie-manage-button" @click="openManager">
|
||||
{{ isGerman ? 'Cookie-Einstellungen' : 'Cookie settings' }}
|
||||
Cookie settings
|
||||
</button>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useState } from '#imports';
|
||||
|
||||
const { hasConsent, analyticsEnabled, acceptAll, rejectAll, savePreferences } = useCookieConsent();
|
||||
|
||||
const isDialogVisible = ref(!hasConsent.value);
|
||||
const isManuallyOpened = ref(false);
|
||||
const analyticsSelection = ref(analyticsEnabled.value);
|
||||
const locale = useState<'de' | 'en'>('landing-locale', () => 'de');
|
||||
const isGerman = computed(() => locale.value === 'de');
|
||||
|
||||
watch(
|
||||
() => hasConsent.value,
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</button>
|
||||
<span class="brand">OpenSquawk</span>
|
||||
<span class="sep">|</span>
|
||||
<span class="mode">Training</span>
|
||||
<span class="mode">Training Hub</span>
|
||||
</div>
|
||||
|
||||
<div class="hud-right">
|
||||
@@ -20,20 +20,8 @@
|
||||
<!-- Quick ATC: Voice & Level -->
|
||||
<div class="chip inline">
|
||||
<v-icon size="16" class="text-accent">mdi-radio-handheld</v-icon>
|
||||
<span class="ml-1">Level {{ cfg.radioLevel }}</span>
|
||||
<input type="range" min="1" max="5" step="1" v-model.number="cfg.radioLevel" aria-label="Radio level"/>
|
||||
</div>
|
||||
<div class="chip inline">
|
||||
<v-icon size="16" class="text-accent">mdi-speedometer</v-icon>
|
||||
<span class="ml-1">Speed {{ cfg.audioSpeed.toFixed(1) }}x</span>
|
||||
<input
|
||||
type="range"
|
||||
min="0.7"
|
||||
max="1.3"
|
||||
step="0.1"
|
||||
v-model.number="cfg.audioSpeed"
|
||||
aria-label="ATC audio speed"
|
||||
/>
|
||||
<span class="ml-1">L {{ cfg.radioLevel }}</span>
|
||||
<input type="range" min="1" max="5" step="1" v-model.number="cfg.radioLevel"/>
|
||||
</div>
|
||||
|
||||
<button class="btn ghost" @click="panel='progress'">
|
||||
@@ -61,12 +49,12 @@
|
||||
<div class="panel hero-panel" :style="worldTiltStyle" @mousemove="tilt">
|
||||
<div class="hero-left">
|
||||
<div class="eyebrow">ALPHA BUILD · TRAINING</div>
|
||||
<h1 class="h1">ATC Training World</h1>
|
||||
<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>
|
||||
Start Training
|
||||
Start training
|
||||
</button>
|
||||
<button class="btn ghost" @click="panel='progress'">
|
||||
<v-icon>mdi-progress-check</v-icon>
|
||||
@@ -110,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">Start with the ICAO alphabet & numbers, then basics, ground, and beyond.</div>
|
||||
<div class="muted">Start with the ICAO alphabet & numbers, then basics, ground, and more.</div>
|
||||
</div>
|
||||
|
||||
<div class="tiles">
|
||||
@@ -179,7 +167,7 @@
|
||||
<v-icon size="18">mdi-headset</v-icon>
|
||||
{{ l.title }}
|
||||
</div>
|
||||
<div class="chip" :class="{ ok: bestScore(current.id,l.id)>=80 }">
|
||||
<div class="chip" :class="{ ok: bestScore(current.id,l.id)>=80 }">
|
||||
{{ bestScore(current.id, l.id) ? bestScore(current.id, l.id) + '%' : 'new' }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -236,7 +224,7 @@
|
||||
{{ targetPhrase }}
|
||||
</div>
|
||||
<div v-if="audioContentHidden" class="audio-note muted small">
|
||||
Audio Challenge active – listen first.
|
||||
Audio challenge active – listen first.
|
||||
</div>
|
||||
</div>
|
||||
<div class="row wrap">
|
||||
@@ -257,7 +245,7 @@
|
||||
@click="revealAudioContent"
|
||||
>
|
||||
<v-icon size="16">mdi-eye</v-icon>
|
||||
Show
|
||||
Reveal text
|
||||
</button>
|
||||
<button class="btn ghost mini" type="button" @click="rollScenario(true)">
|
||||
<v-icon size="16">mdi-dice-5</v-icon>
|
||||
@@ -284,7 +272,7 @@
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<div class="label">Your 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}`">
|
||||
@@ -306,7 +294,7 @@
|
||||
<v-icon v-if="fieldPass(segment.key)" size="16" class="blank-status ok">mdi-check</v-icon>
|
||||
<v-icon v-else-if="fieldHasAnswer(segment.key)" size="16" class="blank-status warn">mdi-alert</v-icon>
|
||||
<small v-if="result" class="blank-feedback">
|
||||
Expected: {{ fieldExpectedValue(segment.key) }}
|
||||
Soll: {{ fieldExpectedValue(segment.key) }}
|
||||
</small>
|
||||
</label>
|
||||
</template>
|
||||
@@ -323,16 +311,7 @@
|
||||
</button>
|
||||
<button class="btn ghost" type="button" @click="fillSolution">
|
||||
<v-icon size="18">mdi-auto-fix</v-icon>
|
||||
Autofill
|
||||
</button>
|
||||
<button
|
||||
v-if="result"
|
||||
class="btn soft"
|
||||
type="button"
|
||||
@click="goToNextLesson"
|
||||
>
|
||||
<v-icon size="18">mdi-arrow-right-bold</v-icon>
|
||||
{{ nextLesson ? 'Next Lesson' : 'Back to Hub' }}
|
||||
Auto-fill
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="result" class="score">
|
||||
@@ -354,7 +333,7 @@
|
||||
|
||||
<!-- PROGRESS -->
|
||||
<section v-if="panel==='progress'" class="container" aria-label="Progress">
|
||||
<h2 class="h2">Overall Progress</h2>
|
||||
<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">
|
||||
@@ -374,21 +353,21 @@
|
||||
<!-- SETTINGS DIALOG -->
|
||||
<v-dialog v-model="showSettings" max-width="720">
|
||||
<div class="panel dialog">
|
||||
<h3 class="h3">ATC Settings</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">Works offline, starts faster, and saves our API calls.</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">Hides target text & details until you reveal them manually.</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>
|
||||
@@ -398,21 +377,6 @@
|
||||
<v-slider v-model="cfg.radioLevel" :min="1" :max="5" :step="1" color="cyan" thumb-label/>
|
||||
</div>
|
||||
|
||||
<div class="set-row">
|
||||
<div class="set-info">
|
||||
<span>ATC audio speed</span>
|
||||
<small class="muted">Fine-tune the playback speed for ATC voice lines.</small>
|
||||
</div>
|
||||
<v-slider
|
||||
v-model="cfg.audioSpeed"
|
||||
:min="0.7"
|
||||
:max="1.3"
|
||||
:step="0.1"
|
||||
color="cyan"
|
||||
thumb-label
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="set-row">
|
||||
<span>Voice</span>
|
||||
<v-text-field v-model="cfg.voice" placeholder="alloy" hide-details density="compact" variant="outlined"/>
|
||||
@@ -704,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'],
|
||||
@@ -1054,8 +1018,8 @@ const modules = ref<ModuleDef[]>([
|
||||
desc: 'Spell letters and digits clearly',
|
||||
keywords: ['Alphabet', 'Numbers', 'Normalize'],
|
||||
hints: [
|
||||
'Spell each letter with the ICAO name (e.g. Delta, Lima, Hotel).',
|
||||
'Digits on the radio: tree, fower, fife, niner.'
|
||||
'Spell each letter with its ICAO name (e.g. Delta, Lima, Hotel).',
|
||||
'Use ATC numbers: tree, fower, fife, niner.'
|
||||
],
|
||||
fields: [
|
||||
{
|
||||
@@ -1068,7 +1032,7 @@ const modules = ref<ModuleDef[]>([
|
||||
},
|
||||
{
|
||||
key: 'digits',
|
||||
label: 'Digits',
|
||||
label: 'Numbers',
|
||||
expected: scenario => scenario.flightNumberWords,
|
||||
placeholder: 'one two three',
|
||||
width: 'lg',
|
||||
@@ -1076,7 +1040,7 @@ const modules = ref<ModuleDef[]>([
|
||||
}
|
||||
],
|
||||
readback: [
|
||||
{ type: 'text', text: 'Callsign: ' },
|
||||
{ type: 'text', text: 'Call sign: ' },
|
||||
{ type: 'field', key: 'letters', width: 'lg' },
|
||||
{ type: 'text', text: ' · ' },
|
||||
{ type: 'field', key: 'digits', width: 'md' }
|
||||
@@ -1084,20 +1048,20 @@ const modules = ref<ModuleDef[]>([
|
||||
defaultFrequency: 'DEL',
|
||||
phrase: scenario => `${scenario.callsignNato} ${scenario.flightNumberWords}`,
|
||||
info: scenario => [
|
||||
`Registration: ${scenario.callsign}`,
|
||||
`Airline call: ${scenario.radioCall}`,
|
||||
`ICAO: ${scenario.callsignNato}`,
|
||||
`Callsign: ${scenario.callsign}`,
|
||||
`Radio call: ${scenario.radioCall}`,
|
||||
`ICAO spelling: ${scenario.callsignNato}`,
|
||||
`Flight number spoken: ${scenario.flightNumberWords}`
|
||||
],
|
||||
generate: createBaseScenario
|
||||
},
|
||||
{
|
||||
id: 'atis',
|
||||
title: 'Understand ATIS',
|
||||
desc: 'Extract the identifier and core data from the ATIS',
|
||||
title: 'Understand the ATIS',
|
||||
desc: 'Extract the identifier and key data from the ATIS',
|
||||
keywords: ['ATIS', 'Weather'],
|
||||
hints: [
|
||||
'Remember the ATIS designator as a single letter.',
|
||||
'Remember the ATIS identifier as a single letter.',
|
||||
'Order: runway – wind – visibility – temperature – dew point – QNH.'
|
||||
],
|
||||
fields: [
|
||||
@@ -1180,19 +1144,19 @@ const modules = ref<ModuleDef[]>([
|
||||
defaultFrequency: 'ATIS',
|
||||
phrase: scenario => scenario.atisText,
|
||||
info: () => [
|
||||
'Take note of the designator, runway, wind, visibility, temperature, dew point, and QNH.',
|
||||
'Visibility: four digits or 9999 for ≥10 km · QNH as a four-digit number.'
|
||||
'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: 'Decode METAR',
|
||||
desc: 'Extract raw METAR values.',
|
||||
title: 'Decode the METAR',
|
||||
desc: 'Extract the raw METAR values',
|
||||
keywords: ['METAR', 'Weather'],
|
||||
hints: [
|
||||
'Read METARs in blocks: wind – visibility – clouds – temperature – QNH.',
|
||||
'The temperature block looks like 18/10, negative values start with M.'
|
||||
'Read the METAR in blocks: wind – visibility – clouds – temperature – QNH.',
|
||||
'The temperature block looks like 18/10; negative values start with M.'
|
||||
],
|
||||
fields: [
|
||||
{
|
||||
@@ -1256,8 +1220,8 @@ const modules = ref<ModuleDef[]>([
|
||||
desc: 'Report readability',
|
||||
keywords: ['Ground', 'Comms'],
|
||||
hints: [
|
||||
'Reply with "Readability" plus the number.',
|
||||
'Always end the check with your callsign.'
|
||||
'Reply with "Readability" followed by the number.',
|
||||
'Always finish the check with your call sign.'
|
||||
],
|
||||
fields: [
|
||||
{
|
||||
@@ -1291,7 +1255,7 @@ const modules = ref<ModuleDef[]>([
|
||||
phrase: scenario => `${scenario.airport.name} Ground, ${scenario.radioCall}, radio check on ${scenario.groundFreq}.`,
|
||||
info: scenario => [
|
||||
`Frequency: ${scenario.groundFreq} (${scenario.frequencyWords.GND})`,
|
||||
`Expected answer: readability ${scenario.readability} (${scenario.readabilityWord})`
|
||||
`Expected response: Readability ${scenario.readability} (${scenario.readabilityWord})`
|
||||
],
|
||||
generate: createBaseScenario
|
||||
}
|
||||
@@ -1300,16 +1264,16 @@ const modules = ref<ModuleDef[]>([
|
||||
{
|
||||
id: 'arc',
|
||||
title: 'ARC Decision Tree',
|
||||
subtitle: 'From the clearance call to departure',
|
||||
subtitle: 'From clearance call to departure',
|
||||
art: gradientArt(['#f97316', '#fb923c', '#0f172a']),
|
||||
lessons: [
|
||||
{
|
||||
id: 'clearance-contact',
|
||||
title: 'Delivery: Initial contact',
|
||||
desc: 'Call clearance delivery',
|
||||
desc: 'Contact clearance delivery',
|
||||
keywords: ['Delivery', 'Clearance'],
|
||||
hints: [
|
||||
'Order: unit, callsign, ATIS, destination, stand, request.',
|
||||
'Order: unit, call sign, ATIS, destination, stand, request.',
|
||||
'Say the ATIS as a single letter.'
|
||||
],
|
||||
fields: [
|
||||
@@ -1334,7 +1298,7 @@ const modules = ref<ModuleDef[]>([
|
||||
},
|
||||
{
|
||||
key: 'cd-stand',
|
||||
label: 'Stand',
|
||||
label: 'Stand/Gate',
|
||||
expected: scenario => scenario.stand,
|
||||
width: 'sm'
|
||||
}
|
||||
@@ -1354,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}`,
|
||||
`ATIS: ${scenario.atisCode}`,
|
||||
`Destination: ${scenario.destination.city} (${scenario.destination.icao})`,
|
||||
`Stand: ${scenario.stand}`
|
||||
`Stand/Gate: ${scenario.stand}`
|
||||
],
|
||||
generate: createBaseScenario
|
||||
},
|
||||
{
|
||||
id: 'clearance-readback',
|
||||
title: 'Clearance Readback',
|
||||
desc: 'Read back the clearance completely',
|
||||
desc: 'Read back the clearance in full',
|
||||
keywords: ['Delivery', 'Readback'],
|
||||
hints: [
|
||||
'Remember the sequence: destination – SID – runway – altitude – squawk.',
|
||||
'Spell out the altitude and squawk digits clearly.'
|
||||
'Remember the order: destination – SID – runway – altitude – squawk.',
|
||||
'Speak altitude and squawk digits clearly.'
|
||||
],
|
||||
fields: [
|
||||
{
|
||||
@@ -1429,11 +1393,11 @@ const modules = ref<ModuleDef[]>([
|
||||
{
|
||||
id: 'pushback',
|
||||
title: 'Push & Start Readback',
|
||||
desc: 'Confirm the pushback clearance',
|
||||
desc: 'Acknowledge the pushback clearance',
|
||||
keywords: ['Ground', 'Pushback'],
|
||||
hints: [
|
||||
'Repeat the runway direction and QNH, callsign at the end.',
|
||||
'You may say QNH as a number or as "QNH xxxx".'
|
||||
'Repeat the runway direction and QNH, call sign at the end.',
|
||||
'QNH may be just the number or "QNH xxxx".'
|
||||
],
|
||||
fields: [
|
||||
{
|
||||
@@ -1473,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
|
||||
@@ -1481,11 +1445,11 @@ const modules = ref<ModuleDef[]>([
|
||||
{
|
||||
id: 'taxi',
|
||||
title: 'Taxi Readback',
|
||||
desc: 'Read back the taxi clearance with the hold short instruction',
|
||||
desc: 'Read back the taxi clearance with hold short',
|
||||
keywords: ['Ground', 'Taxi'],
|
||||
hints: [
|
||||
'Repeat the route as given, including the hold short.',
|
||||
'Finish with the callsign again.'
|
||||
'Repeat the route exactly as received, including the hold short.',
|
||||
'Finish with the call sign.'
|
||||
],
|
||||
fields: [
|
||||
{
|
||||
@@ -1542,11 +1506,11 @@ const modules = ref<ModuleDef[]>([
|
||||
{
|
||||
id: 'lineup',
|
||||
title: 'Line-up Clearance',
|
||||
desc: 'Confirm line up and wait',
|
||||
desc: 'Acknowledge line up and wait',
|
||||
keywords: ['Tower', 'Line Up'],
|
||||
hints: [
|
||||
'Repeat the runway, then say "line up and wait".',
|
||||
'Place the callsign at the end.'
|
||||
'Place the call sign at the end.'
|
||||
],
|
||||
fields: [
|
||||
{
|
||||
@@ -1578,7 +1542,7 @@ const modules = ref<ModuleDef[]>([
|
||||
phrase: scenario => `${scenario.radioCall}, line up and wait runway ${scenario.runway}.`,
|
||||
info: scenario => [
|
||||
`Tower: ${scenario.towerFreq} (${scenario.frequencyWords.TWR})`,
|
||||
`Line up: runway ${scenario.runway}`
|
||||
`Line-up: runway ${scenario.runway}`
|
||||
],
|
||||
generate: createBaseScenario
|
||||
},
|
||||
@@ -1588,8 +1552,8 @@ const modules = ref<ModuleDef[]>([
|
||||
desc: 'Acknowledge the takeoff clearance',
|
||||
keywords: ['Tower', 'Departure'],
|
||||
hints: [
|
||||
'Order: runway – cleared for takeoff – callsign.',
|
||||
'Wind call can be omitted if not stated.'
|
||||
'Order: runway – cleared for takeoff – call sign.',
|
||||
'Wind information can be omitted if it was not given.'
|
||||
],
|
||||
fields: [
|
||||
{
|
||||
@@ -1631,13 +1595,13 @@ const modules = ref<ModuleDef[]>([
|
||||
desc: 'Switch to departure',
|
||||
keywords: ['Departure', 'Handoff'],
|
||||
hints: [
|
||||
'Repeat the frequency exactly, either as digits or spoken.',
|
||||
'Add the callsign at the end.'
|
||||
'Repeat the frequency exactly, either as digits or spoken form.',
|
||||
'Append the call sign at the end.'
|
||||
],
|
||||
fields: [
|
||||
{
|
||||
key: 'dep-freq',
|
||||
label: 'Frequency',
|
||||
label: 'Frequenz',
|
||||
expected: scenario => scenario.departureFreq,
|
||||
alternatives: scenario => [
|
||||
scenario.departureFreq,
|
||||
@@ -1675,11 +1639,11 @@ const modules = ref<ModuleDef[]>([
|
||||
{
|
||||
id: 'departure-checkin',
|
||||
title: 'Departure Check-in',
|
||||
desc: 'First call with departure',
|
||||
desc: 'Initial call to departure',
|
||||
keywords: ['Departure', 'Check-in'],
|
||||
hints: [
|
||||
'Address the unit, then state the callsign.',
|
||||
'Repeat the altitude and SID exactly as given.'
|
||||
'Address the unit first, then state the call sign.',
|
||||
'Repeat the altitude and SID exactly as received.'
|
||||
],
|
||||
fields: [
|
||||
{
|
||||
@@ -1730,11 +1694,11 @@ const modules = ref<ModuleDef[]>([
|
||||
{
|
||||
id: 'climb-instruction',
|
||||
title: 'Climb & Direct',
|
||||
desc: 'Read back the climb instruction completely',
|
||||
desc: 'Read back the climb instruction in full',
|
||||
keywords: ['Departure', 'Climb'],
|
||||
hints: [
|
||||
'Start with "Climb", then the altitude and any direct.',
|
||||
'Repeat the callsign at the end.'
|
||||
'Start with "Climb", then the altitude and any direct clearance.',
|
||||
'Repeat the call sign at the end.'
|
||||
],
|
||||
fields: [
|
||||
{
|
||||
@@ -1788,13 +1752,13 @@ const modules = ref<ModuleDef[]>([
|
||||
desc: 'Acknowledge the handoff to center',
|
||||
keywords: ['Center', 'Handoff'],
|
||||
hints: [
|
||||
'Repeat the frequency exactly (with or without the decimal).',
|
||||
'Place the callsign at the end.'
|
||||
'Repeat the frequency exactly (with or without the decimal point).',
|
||||
'Place the call sign at the end.'
|
||||
],
|
||||
fields: [
|
||||
{
|
||||
key: 'ctr-freq',
|
||||
label: 'Frequency',
|
||||
label: 'Frequenz',
|
||||
expected: scenario => scenario.centerFreq,
|
||||
alternatives: scenario => [
|
||||
scenario.centerFreq,
|
||||
@@ -1970,7 +1934,6 @@ if (isClient) {
|
||||
resetAudioReveal()
|
||||
})
|
||||
watch(() => cfg.value.radioLevel, markConfigDirty)
|
||||
watch(() => cfg.value.audioSpeed, markConfigDirty)
|
||||
watch(() => cfg.value.voice, markConfigDirty)
|
||||
}
|
||||
|
||||
@@ -2040,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 {
|
||||
@@ -2066,12 +2029,6 @@ function fieldExpectedValue(key: string): string {
|
||||
|
||||
const targetPhrase = computed(() => (activeLesson.value && scenario.value ? activeLesson.value.phrase(scenario.value) : ''))
|
||||
const lessonInfo = computed(() => (activeLesson.value && scenario.value ? activeLesson.value.info(scenario.value) : []))
|
||||
const nextLesson = computed(() => {
|
||||
if (!current.value || !activeLesson.value) return null
|
||||
const lessons = current.value.lessons
|
||||
const index = lessons.findIndex(lesson => lesson.id === activeLesson.value?.id)
|
||||
return index >= 0 && index < lessons.length - 1 ? lessons[index + 1] : null
|
||||
})
|
||||
|
||||
watch(activeLesson, lesson => {
|
||||
if (lesson) {
|
||||
@@ -2201,18 +2158,6 @@ function selectLesson(lesson: Lesson) {
|
||||
activeLesson.value = lesson
|
||||
}
|
||||
|
||||
function goToNextLesson() {
|
||||
stopAudio()
|
||||
if (nextLesson.value) {
|
||||
activeLesson.value = nextLesson.value
|
||||
return
|
||||
}
|
||||
panel.value = 'hub'
|
||||
activeLesson.value = null
|
||||
current.value = null
|
||||
result.value = null
|
||||
}
|
||||
|
||||
function bestScore(modId: string, lesId: string) {
|
||||
return progress.value[modId]?.[lesId]?.best || 0
|
||||
}
|
||||
@@ -2241,8 +2186,8 @@ function avgScore(modId: string) {
|
||||
|
||||
const dailies = ref([
|
||||
{ id: 'd1', title: '3 readbacks ≥80%', sub: 'Reward: +50 XP', reward: 50 },
|
||||
{ id: 'd2', title: 'Start one module', sub: 'Reward: +20 XP', reward: 20 },
|
||||
{ id: 'd3', title: 'Play a target phrase', sub: 'Reward: +10 XP', reward: 10 }
|
||||
{ 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 }) {
|
||||
@@ -2335,8 +2280,7 @@ async function say(text: string) {
|
||||
if (!trimmed) return
|
||||
|
||||
const rateBase = 0.9 + (cfg.value.radioLevel - 3) * 0.12
|
||||
const rateAdjusted = rateBase * (cfg.value.audioSpeed || 1)
|
||||
const normalizedRate = Math.min(1.6, Math.max(0.6, rateAdjusted))
|
||||
const normalizedRate = Math.min(1.5, Math.max(0.6, rateBase))
|
||||
|
||||
const hasBrowserTts = cfg.value.tts && typeof window !== 'undefined' && 'speechSynthesis' in window
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<v-card class="bg-white/5 backdrop-blur border border-white/10">
|
||||
<v-card-text class="space-y-4">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold">VATSIM integration</h2>
|
||||
<h2 class="text-lg font-semibold">VATSIM Integration</h2>
|
||||
<p class="text-sm text-white/70">Enter your CID to load your filtered flight plans.</p>
|
||||
</div>
|
||||
<v-text-field
|
||||
@@ -59,8 +59,8 @@
|
||||
<v-card class="bg-white/5 backdrop-blur border border-white/10">
|
||||
<v-card-text class="space-y-4">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold">Demo mode</h2>
|
||||
<p class="text-sm text-white/70">Start with a sample flight plan to test the workflow.</p>
|
||||
<h2 class="text-lg font-semibold">Demo Mode</h2>
|
||||
<p class="text-sm text-white/70">Start with a sample flight plan for testing.</p>
|
||||
</div>
|
||||
<v-btn
|
||||
block
|
||||
@@ -203,7 +203,7 @@
|
||||
@click="backToSetup"
|
||||
prepend-icon="mdi-airplane-off"
|
||||
>
|
||||
Choose a different flight
|
||||
Select new flight
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
@@ -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 (you)</span>
|
||||
<span class="text-xs uppercase font-semibold">Pilot (You)</span>
|
||||
</div>
|
||||
<p class="font-mono text-white">{{ normalizeExpectedText(currentStep.pilot) }}</p>
|
||||
</div>
|
||||
@@ -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">Frequency directory</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">
|
||||
Frequencies for {{ flightContext.dep || 'Departure' }}
|
||||
Frequencies for {{ flightContext.dep || 'departure' }}
|
||||
</v-expansion-panel-title>
|
||||
<v-expansion-panel-text>
|
||||
<div
|
||||
@@ -527,7 +527,7 @@
|
||||
@click:append-inner="sendPilotText"
|
||||
/>
|
||||
<p class="text-xs text-white/50">
|
||||
For emergencies when PTT fails or for testing.
|
||||
For emergencies when PTT fails or for testing
|
||||
</p>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
@@ -564,7 +564,7 @@
|
||||
v-if="simulationRunning && simulationTrace.length === 0"
|
||||
class="text-sm text-white/60"
|
||||
>
|
||||
Simulation initializing…
|
||||
Simulation initializing...
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -609,7 +609,7 @@
|
||||
<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">
|
||||
Radio: {{ debugState.sayNormalized }}
|
||||
@@ -617,7 +617,7 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-xs uppercase tracking-[0.3em] text-white/40 mb-2">Next decisions</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"
|
||||
@@ -702,7 +702,7 @@
|
||||
</div>
|
||||
|
||||
<p v-if="log.length === 0" class="text-xs text-white/50 text-center py-4">
|
||||
No transmissions logged yet.
|
||||
No communications recorded yet.
|
||||
</p>
|
||||
</div>
|
||||
</v-card-text>
|
||||
@@ -752,7 +752,7 @@
|
||||
<template #prepend>
|
||||
<v-icon size="16">mdi-alert-circle-outline</v-icon>
|
||||
</template>
|
||||
{{ lastTransmissionFaulty ? 'Review issue' : 'Mark issue' }}
|
||||
{{ lastTransmissionFaulty ? 'Edit issue' : 'Mark as faulty' }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="lastTransmissionFaulty"
|
||||
@@ -801,12 +801,11 @@
|
||||
<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" />
|
||||
Flag transmission as faulty
|
||||
Mark transmission as faulty
|
||||
</v-card-title>
|
||||
<v-card-text class="space-y-4 text-sm text-white/70">
|
||||
<p>
|
||||
Leave an optional short description so the dev team can trace the
|
||||
transmission.
|
||||
Optionally leave a short note so the dev team can follow up on the transmission.
|
||||
</p>
|
||||
<v-textarea
|
||||
v-model="transmissionIssueNote"
|
||||
|
||||
Reference in New Issue
Block a user