discovery-by-llm/frontend/src/components/AssistantMessage.vue

136 lines
3.5 KiB
Vue

<script setup lang="ts">
import { computed } from 'vue'
import { formatMessage, messages } from '../lib/messages'
import type { AssistantTurn } from '../lib/models'
import EmptyResults from './EmptyResults.vue'
import PlaylistError from './PlaylistError.vue'
import PlaylistSaved from './PlaylistSaved.vue'
import RequestMetadata from './RequestMetadata.vue'
import ResultActions from './ResultActions.vue'
import ResultSet from './ResultSet.vue'
import StreamError from './StreamError.vue'
import StreamWarning from './StreamWarning.vue'
import ThinkingIndicator from './ThinkingIndicator.vue'
const props = defineProps<{ turn: AssistantTurn; canSave: boolean }>()
defineEmits<{ save: []; retry: [query: string] }>()
const thinkingLabel = computed(() => {
if (props.turn.requestId === null) return messages.assistantMessageReadingRequest
if (props.turn.tracks.length === 0) {
if (props.turn.candidateCount === null) {
return messages.assistantMessageCheckingCandidates
}
return formatMessage('assistantMessageCheckingCandidateCount', {
count: props.turn.candidateCount,
})
}
return messages.assistantMessageStreamingTracks
})
const isEmptyResult = computed(
() =>
props.turn.status === 'done' &&
props.turn.completion?.track_count === 0 &&
props.turn.tracks.length === 0,
)
</script>
<template>
<div class="assistant">
<div class="avatar" aria-hidden="true">{{ messages.assistantMessageAvatar }}</div>
<div class="column">
<p v-if="turn.intentSummary" class="intent">{{ turn.intentSummary }}</p>
<ThinkingIndicator v-if="turn.status === 'streaming'" :label="thinkingLabel" />
<StreamWarning
v-for="(warning, index) in turn.warnings"
:key="`${warning.code}-${index}`"
:warning="warning"
/>
<StreamError
v-if="turn.error"
:code="turn.error.code"
:message="turn.error.message"
@retry="$emit('retry', turn.query)"
/>
<StreamError
v-else-if="turn.transportFailure"
:code="turn.transportFailure.kind"
:message="turn.transportFailure.message"
@retry="$emit('retry', turn.query)"
/>
<EmptyResults v-if="isEmptyResult" />
<RequestMetadata :turn="turn" />
<ResultActions
v-if="turn.status === 'done' && turn.tracks.length && turn.playlist.status !== 'saved'"
:state="turn.playlist"
:track-count="turn.tracks.length"
:can-save="canSave"
@save="$emit('save')"
/>
<PlaylistError
v-if="turn.playlist.status === 'error' && turn.playlist.message"
:message="turn.playlist.message"
/>
<PlaylistSaved
v-if="turn.playlist.status === 'saved' && turn.playlist.name"
:name="turn.playlist.name"
:url="turn.playlist.url"
:track-count="turn.tracks.length"
/>
<ResultSet v-if="turn.tracks.length" :tracks="turn.tracks" />
</div>
</div>
</template>
<style scoped>
.assistant {
display: flex;
gap: var(--s-5);
}
.avatar {
display: grid;
flex: none;
place-items: center;
width: 26px;
height: 26px;
margin-top: 1px;
color: var(--c-accent);
font-family: var(--f-display);
font-size: 13px;
font-weight: 600;
border: 1px solid var(--c-line);
border-radius: 50%;
}
.column {
display: flex;
flex: 1;
flex-direction: column;
gap: var(--s-7);
min-width: 0;
}
.intent {
margin: 0;
font-size: var(--t-lead);
text-wrap: pretty;
}
@media (max-width: 560px) {
.assistant {
gap: 0;
}
.avatar {
display: none;
}
}
</style>