fix(frontend): harden the streaming ui and extract copy and constants
Some checks are pending
ci / backend (push) Waiting to run
ci / frontend (push) Waiting to run

This commit is contained in:
Justin Visser 2026-08-10 15:42:18 +02:00
parent 036ef3cc6f
commit a3af8f45cc
36 changed files with 1662 additions and 226 deletions

View file

@ -1,40 +1,49 @@
<script setup lang="ts">
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
import { messages } from '../lib/messages'
import type { AssistantTurn, EventLogEntry } from '../lib/models'
const props = defineProps<{
latest: AssistantTurn | undefined
log: EventLogEntry[]
opener: HTMLElement | null
}>()
const emit = defineEmits<{ close: []; reset: [] }>()
const panel = ref<HTMLElement | null>(null)
const closeButton = ref<HTMLButtonElement | null>(null)
let returnFocus: HTMLElement | null = null
function latestValue(value: string | null | undefined): string {
return value ?? messages.devPanelWaiting
}
const rows = computed(() => [
{ label: 'request id', value: props.latest?.requestId ?? 'waiting' },
{ label: messages.devPanelRequestId, value: latestValue(props.latest?.requestId) },
{
label: 'candidate count',
value: props.latest?.candidateCount?.toString() ?? 'waiting',
label: messages.devPanelCandidateCount,
value: latestValue(props.latest?.candidateCount?.toString()),
},
{
label: 'track count',
value:
props.latest?.completion?.track_count.toString() ??
props.latest?.tracks.length.toString() ??
'waiting',
label: messages.devPanelTrackCount,
value: latestValue(
props.latest?.completion?.track_count.toString() ?? props.latest?.tracks.length.toString(),
),
},
{
label: 'total ms',
value: props.latest?.completion?.total_ms.toString() ?? 'waiting',
label: messages.devPanelTotalMilliseconds,
value: latestValue(props.latest?.completion?.total_ms.toString()),
},
])
const entries = computed(() =>
props.log.length
? [...props.log].reverse()
: [{ timestamp: '--:--:--', type: 'idle', detail: 'waiting for a request' }],
)
const entries = computed(() => {
if (props.log.length) return [...props.log].reverse()
return [
{
timestamp: '--:--:--',
type: messages.devPanelIdle,
detail: messages.devPanelWaitingForRequest,
},
]
})
function focusableElements(): HTMLElement[] {
if (!panel.value) return []
@ -67,12 +76,15 @@ function onKeydown(event: KeyboardEvent): void {
}
onMounted(async () => {
returnFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null
await nextTick()
closeButton.value?.focus()
})
onUnmounted(() => returnFocus?.focus())
onUnmounted(() => {
const fallback = document.querySelector<HTMLElement>('[data-dev-panel-opener]')
const target = props.opener?.isConnected ? props.opener : fallback
target?.focus()
})
</script>
<template>
@ -86,21 +98,21 @@ onUnmounted(() => returnFocus?.focus())
@keydown="onKeydown"
>
<div class="bar">
<h2 id="dev-panel-title" class="caps">Dev panel</h2>
<h2 id="dev-panel-title" class="caps">{{ messages.devPanelTitle }}</h2>
<button
ref="closeButton"
class="close"
type="button"
aria-label="Close dev panel"
:aria-label="messages.devPanelCloseLabel"
@click="$emit('close')"
>
<span aria-hidden="true">X</span>
<span aria-hidden="true">{{ messages.devPanelCloseSymbol }}</span>
</button>
</div>
<div class="body">
<section>
<h3 class="caps">Latest request</h3>
<p class="caption">Only counters supplied by the recommendation contract are shown.</p>
<h3 class="caps">{{ messages.devPanelLatestRequest }}</h3>
<p class="caption">{{ messages.devPanelContractCaption }}</p>
<div class="table">
<div v-for="row in rows" :key="row.label" class="row">
<span class="key">{{ row.label }}</span
@ -109,11 +121,13 @@ onUnmounted(() => returnFocus?.focus())
</div>
</section>
<section>
<h3 class="caps section-heading">Conversation</h3>
<button class="reset" type="button" @click="$emit('reset')">Clear conversation</button>
<h3 class="caps section-heading">{{ messages.devPanelConversation }}</h3>
<button class="reset" type="button" @click="$emit('reset')">
{{ messages.devPanelClearConversation }}
</button>
</section>
<section>
<h3 class="caps section-heading">Event stream</h3>
<h3 class="caps section-heading">{{ messages.devPanelEventStream }}</h3>
<div class="log">
<div v-for="(entry, index) in entries" :key="index" class="entry">
<span class="time">{{ entry.timestamp }}</span>