This commit is contained in:
itsrubberduck
2025-09-19 08:55:49 +02:00
4 changed files with 75 additions and 106 deletions

View File

@@ -10,80 +10,53 @@
>
<section class="cookie-consent-panel">
<header class="cookie-consent-header">
<h2 id="cookie-consent-title">{{ isGerman ? 'Cookies &amp; Analysen' : 'Cookies &amp; analytics' }}</h2>
<h2 id="cookie-consent-title">Cookies &amp; 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 themsee 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 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>{{ 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,

View File

@@ -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"

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,