136 lines
3.5 KiB
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>
|