mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-08 18:45:33 +08:00
feat(live-atc): add TextInputPanel, LastTransmissionCard, CommLogRail
Moves the last three inline cockpit blocks out of the page into components, re-skinned onto the navy/instrument-panel language the rest of the cockpit already uses (the v-card bg-white/5 glass treatment is gone). LastTransmissionCard collapses the old four-button row (Mark as faulty / Edit issue / Reset / Delete) to two icon buttons, per the design doc. Nothing is lost: "Reset" was a duplicate of the issue dialog's own "Remove flag" action, so the amber flag icon opens that dialog and a plain × dismisses the card. resetLastTransmissionFault therefore has no caller left on the page. CommLogRail wraps the readback-check block plus CommLog for the desktop rail; the .pm-readback-* styles move with it. The mobile Log tab keeps using CommLog directly, as the design doc specifies. Verified by rendering all three on a throwaway route with fake data: the suggestion chip fills the text field through v-model, the flag button flips the card into its faulty skin, and the readback rows render matched/missing. Typecheck clean, 167/167 tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
89
app/components/live-atc/cockpit/CommLogRail.vue
Normal file
89
app/components/live-atc/cockpit/CommLogRail.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<script setup lang="ts">
|
||||
import type { ReadbackFieldDetail } from '~/composables/useRadioBackend'
|
||||
|
||||
withDefaults(defineProps<{
|
||||
log: readonly any[]
|
||||
/** Per-field STT readback breakdown for the last pilot call; empty hides the block. */
|
||||
readbackReport: readonly ReadbackFieldDetail[]
|
||||
readbackTranscript: string
|
||||
limit?: number
|
||||
dense?: boolean
|
||||
}>(), {
|
||||
limit: 30,
|
||||
dense: true,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{ (e: 'clear'): void }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rail">
|
||||
<!-- STT readback check: what was recognised vs missing in the last call. -->
|
||||
<div v-if="readbackReport.length" class="pm-readback-check">
|
||||
<div class="pm-readback-head">
|
||||
<v-icon size="15">mdi-magnify-scan</v-icon>
|
||||
<span>Readback check</span>
|
||||
</div>
|
||||
<p v-if="readbackTranscript" class="pm-readback-heard">heard: “{{ readbackTranscript }}”</p>
|
||||
<div
|
||||
v-for="r in readbackReport"
|
||||
:key="r.field"
|
||||
class="pm-readback-row"
|
||||
:class="r.matched ? 'is-ok' : 'is-missing'"
|
||||
>
|
||||
<v-icon size="13">{{ r.matched ? 'mdi-check-circle' : 'mdi-close-circle' }}</v-icon>
|
||||
<span class="pm-readback-text">
|
||||
<span class="pm-readback-field">{{ r.field }}</span>
|
||||
= <span class="pm-readback-expected">{{ r.expected || '—' }}</span>
|
||||
<template v-if="r.matched"> → “{{ r.matched_via }}”</template>
|
||||
<template v-else>
|
||||
→ not recognised<span v-if="r.accepted_forms.length" class="pm-readback-forms">
|
||||
(say: {{ r.accepted_forms.join(' / ') }})</span>
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CommLog :entries="log" :limit="limit" :dense="dense" @clear="emit('clear')" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.pm-readback-check {
|
||||
margin-bottom: 10px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
background: color-mix(in srgb, var(--text) 4%, transparent);
|
||||
font-size: 11.5px;
|
||||
}
|
||||
.pm-readback-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .12em;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: color-mix(in srgb, var(--text) 55%, transparent);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.pm-readback-heard {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
color: color-mix(in srgb, var(--text) 50%, transparent);
|
||||
margin-bottom: 6px;
|
||||
font-size: 11px;
|
||||
}
|
||||
.pm-readback-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.pm-readback-row.is-ok { color: #6ee7a8; }
|
||||
.pm-readback-row.is-missing { color: #fca5a5; }
|
||||
.pm-readback-field { color: var(--t2); }
|
||||
.pm-readback-expected { color: var(--text); font-weight: 600; }
|
||||
.pm-readback-forms { color: var(--t4); }
|
||||
</style>
|
||||
172
app/components/live-atc/cockpit/LastTransmissionCard.vue
Normal file
172
app/components/live-atc/cockpit/LastTransmissionCard.vue
Normal file
@@ -0,0 +1,172 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
/** The transcribed (or typed) text of the pilot's last transmission. */
|
||||
text: string
|
||||
faulty: boolean
|
||||
faultNote: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'flag-issue'): void
|
||||
(e: 'clear'): void
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="ltx" :class="{ 'is-faulty': faulty }" aria-label="Last transmission">
|
||||
<header class="ltx__head">
|
||||
<v-icon size="14" class="ltx__icon">mdi-microphone-outline</v-icon>
|
||||
<span class="ltx__title">Last transmission</span>
|
||||
<span v-if="faulty" class="ltx__badge">Faulty</span>
|
||||
|
||||
<div class="ltx__actions">
|
||||
<!-- One entry point instead of the old four-button row: the issue dialog
|
||||
itself carries "Remove flag", so a dedicated reset button was dead weight. -->
|
||||
<v-tooltip :text="faulty ? 'Edit issue' : 'Mark as faulty'" location="top">
|
||||
<template #activator="{ props: tip }">
|
||||
<button
|
||||
v-bind="tip"
|
||||
type="button"
|
||||
class="ltx__btn ltx__btn--flag"
|
||||
:aria-label="faulty ? 'Edit issue' : 'Mark as faulty'"
|
||||
@click="emit('flag-issue')"
|
||||
>
|
||||
<v-icon size="16">mdi-alert-circle-outline</v-icon>
|
||||
</button>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
<v-tooltip text="Dismiss" location="top">
|
||||
<template #activator="{ props: tip }">
|
||||
<button
|
||||
v-bind="tip"
|
||||
type="button"
|
||||
class="ltx__btn"
|
||||
aria-label="Dismiss last transmission"
|
||||
@click="emit('clear')"
|
||||
>
|
||||
<v-icon size="16">mdi-close</v-icon>
|
||||
</button>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<p class="ltx__text">{{ text }}</p>
|
||||
|
||||
<p v-if="faulty" class="ltx__note">
|
||||
{{ faultNote || 'Marked as faulty.' }}
|
||||
</p>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ltx {
|
||||
border-radius: 18px;
|
||||
border: 1px solid rgba(34, 211, 238, 0.24);
|
||||
background: rgba(34, 211, 238, 0.07);
|
||||
padding: 12px 14px 14px;
|
||||
}
|
||||
|
||||
.ltx.is-faulty {
|
||||
border-color: rgba(248, 113, 113, 0.32);
|
||||
background: rgba(248, 113, 113, 0.08);
|
||||
}
|
||||
|
||||
.ltx__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.ltx__icon,
|
||||
.ltx__title {
|
||||
color: #67e8f9;
|
||||
}
|
||||
|
||||
.ltx.is-faulty .ltx__icon,
|
||||
.ltx.is-faulty .ltx__title {
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
.ltx__title {
|
||||
font-size: 9.5px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.24em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.ltx__badge {
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
color: #2b0606;
|
||||
background: #f87171;
|
||||
border-radius: 999px;
|
||||
padding: 2px 7px;
|
||||
}
|
||||
|
||||
.ltx__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.ltx__btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 9px;
|
||||
border: 1px solid transparent;
|
||||
color: var(--t3);
|
||||
background: transparent;
|
||||
transition: color 120ms ease, background 120ms ease, border-color 120ms ease;
|
||||
}
|
||||
|
||||
.ltx__btn:hover {
|
||||
color: var(--text);
|
||||
background: color-mix(in srgb, var(--text) 8%, transparent);
|
||||
}
|
||||
|
||||
.ltx__btn--flag {
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.ltx__btn--flag:hover {
|
||||
color: #fcd34d;
|
||||
border-color: rgba(251, 191, 36, 0.4);
|
||||
background: rgba(251, 191, 36, 0.12);
|
||||
}
|
||||
|
||||
.ltx.is-faulty .ltx__btn--flag {
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
.ltx.is-faulty .ltx__btn--flag:hover {
|
||||
color: #fecaca;
|
||||
border-color: rgba(248, 113, 113, 0.45);
|
||||
background: rgba(248, 113, 113, 0.14);
|
||||
}
|
||||
|
||||
.ltx__text {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
white-space: pre-line;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.ltx__note {
|
||||
margin-top: 10px;
|
||||
padding: 7px 10px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(248, 113, 113, 0.28);
|
||||
background: rgba(248, 113, 113, 0.1);
|
||||
font-size: 12px;
|
||||
color: #fecaca;
|
||||
}
|
||||
</style>
|
||||
109
app/components/live-atc/cockpit/TextInputPanel.vue
Normal file
109
app/components/live-atc/cockpit/TextInputPanel.vue
Normal file
@@ -0,0 +1,109 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
activeFrequency: string
|
||||
/** The phrase the backend expects next; offered as a one-tap fill. */
|
||||
expectedPhrase: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{ (e: 'send'): void }>()
|
||||
|
||||
const pilotInput = defineModel<string>({ required: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="txt" aria-label="Text transmission">
|
||||
<header class="txt__head">
|
||||
<span class="txt__title">Text input</span>
|
||||
<span class="txt__freq">{{ activeFrequency || '---.---' }}</span>
|
||||
</header>
|
||||
|
||||
<v-text-field
|
||||
v-model="pilotInput"
|
||||
label="Pilot transmission"
|
||||
variant="outlined"
|
||||
color="cyan"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
append-inner-icon="mdi-send"
|
||||
@keyup.enter="emit('send')"
|
||||
@click:append-inner="emit('send')"
|
||||
/>
|
||||
|
||||
<div v-if="expectedPhrase" class="txt__suggest">
|
||||
<span class="txt__suggest-label">Suggested phrase</span>
|
||||
<button type="button" class="txt__suggest-chip" @click="pilotInput = expectedPhrase">
|
||||
{{ expectedPhrase }}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.txt {
|
||||
border-radius: 18px;
|
||||
border: 1px solid var(--border);
|
||||
background:
|
||||
linear-gradient(165deg,
|
||||
color-mix(in srgb, var(--bg2) 90%, transparent),
|
||||
color-mix(in srgb, var(--bg) 96%, transparent));
|
||||
box-shadow:
|
||||
inset 0 1px 0 color-mix(in srgb, var(--text) 6%, transparent),
|
||||
0 20px 44px -28px rgba(0, 0, 0, 0.9);
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.txt__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.txt__title {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.24em;
|
||||
text-transform: uppercase;
|
||||
color: var(--t4);
|
||||
}
|
||||
|
||||
.txt__freq {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-size: 12px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: #67e8f9;
|
||||
}
|
||||
|
||||
.txt__suggest {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.txt__suggest-label {
|
||||
font-size: 9.5px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.24em;
|
||||
text-transform: uppercase;
|
||||
color: var(--t4);
|
||||
}
|
||||
|
||||
.txt__suggest-chip {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-size: 12px;
|
||||
color: #67e8f9;
|
||||
border: 1px solid rgba(34, 211, 238, 0.35);
|
||||
background: rgba(34, 211, 238, 0.08);
|
||||
border-radius: 999px;
|
||||
padding: 4px 12px;
|
||||
text-align: left;
|
||||
transition: background 130ms ease, border-color 130ms ease;
|
||||
}
|
||||
|
||||
.txt__suggest-chip:hover {
|
||||
background: rgba(34, 211, 238, 0.16);
|
||||
border-color: rgba(34, 211, 238, 0.5);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user