fix(frontend): harden the streaming ui and extract copy and constants
This commit is contained in:
parent
036ef3cc6f
commit
a3af8f45cc
36 changed files with 1662 additions and 226 deletions
|
|
@ -1,5 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
import { MESSAGE_LIST_PIN_THRESHOLD_PX } from '../lib/constants'
|
||||
import { formatMessage, messages } from '../lib/messages'
|
||||
import type { AssistantTurn, ChatTurn } from '../lib/models'
|
||||
import AssistantMessage from './AssistantMessage.vue'
|
||||
import EmptyState from './EmptyState.vue'
|
||||
|
|
@ -18,37 +20,49 @@ defineEmits<{
|
|||
|
||||
const list = ref<HTMLElement | null>(null)
|
||||
const pinned = ref(true)
|
||||
const fingerprint = computed(() =>
|
||||
props.turns
|
||||
.map((turn) =>
|
||||
turn.role === 'user'
|
||||
? turn.id
|
||||
: `${turn.id}:${turn.tracks.length}:${turn.warnings.length}:${turn.status}`,
|
||||
)
|
||||
.join('|'),
|
||||
)
|
||||
function turnFingerprint(turn: ChatTurn): string {
|
||||
if (turn.role === 'user') return turn.id
|
||||
return `${turn.id}:${turn.tracks.length}:${turn.warnings.length}:${turn.status}`
|
||||
}
|
||||
|
||||
function findLatestAssistant(): AssistantTurn | undefined {
|
||||
for (let index = props.turns.length - 1; index >= 0; index -= 1) {
|
||||
const turn = props.turns[index]
|
||||
if (turn?.role === 'assistant') return turn
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
const fingerprint = computed(() => props.turns.map(turnFingerprint).join('|'))
|
||||
const streaming = computed(() =>
|
||||
props.turns.some((turn) => turn.role === 'assistant' && turn.status === 'streaming'),
|
||||
)
|
||||
const latestAssistant = computed(() =>
|
||||
[...props.turns].reverse().find((turn): turn is AssistantTurn => turn.role === 'assistant'),
|
||||
)
|
||||
const latestAssistant = computed(findLatestAssistant)
|
||||
const announcement = computed(() => {
|
||||
const turn = latestAssistant.value
|
||||
if (!turn) return ''
|
||||
if (turn.status === 'error') return 'The recommendation request failed.'
|
||||
if (turn.status === 'error') return messages.messageListRequestFailed
|
||||
if (turn.status === 'done') {
|
||||
return `${turn.completion?.track_count ?? turn.tracks.length} tracks ready.`
|
||||
const trackCount = turn.completion?.track_count ?? turn.tracks.length
|
||||
return formatMessage('messageListTracksReady', { count: trackCount })
|
||||
}
|
||||
if (turn.requestId === null) return 'Reading the request.'
|
||||
if (turn.tracks.length === 0) return 'Checking recommendation candidates.'
|
||||
return `${turn.tracks.length} verified tracks received.`
|
||||
const warning = turn.warnings.at(-1)
|
||||
if (warning) {
|
||||
return formatMessage('messageListWarning', {
|
||||
count: turn.warnings.length,
|
||||
message: warning.message,
|
||||
})
|
||||
}
|
||||
if (turn.requestId === null) return messages.messageListReadingRequest
|
||||
if (turn.tracks.length === 0) return messages.messageListCheckingCandidates
|
||||
return formatMessage('messageListVerifiedTracksReceived', { count: turn.tracks.length })
|
||||
})
|
||||
|
||||
function onScroll(): void {
|
||||
const element = list.value
|
||||
if (!element) return
|
||||
pinned.value = element.scrollHeight - element.scrollTop - element.clientHeight < 48
|
||||
const distanceFromBottom = element.scrollHeight - element.scrollTop - element.clientHeight
|
||||
pinned.value = distanceFromBottom < MESSAGE_LIST_PIN_THRESHOLD_PX
|
||||
}
|
||||
|
||||
async function scrollToLatest(): Promise<void> {
|
||||
|
|
@ -66,7 +80,12 @@ watch(streaming, (value) => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<main ref="list" class="list" aria-label="Conversation" @scroll="onScroll">
|
||||
<main
|
||||
ref="list"
|
||||
class="list"
|
||||
:aria-label="messages.messageListConversationLabel"
|
||||
@scroll="onScroll"
|
||||
>
|
||||
<p class="visually-hidden" aria-live="polite" aria-atomic="true">
|
||||
{{ announcement }}
|
||||
</p>
|
||||
|
|
@ -107,7 +126,7 @@ watch(streaming, (value) => {
|
|||
}
|
||||
|
||||
.turn {
|
||||
animation: message-in 0.3s ease both;
|
||||
animation: message-in var(--dur-message) ease both;
|
||||
}
|
||||
|
||||
@keyframes message-in {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue