mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-05 00:47:48 +08:00
feat: 实现对话轮次折叠视图
- 新增轮次分组逻辑,将消息按 user/assistant 配对分组 - 新增 TurnCard 组件,支持折叠/展开单个轮次 - 新增 TurnBadges 组件,显示思考/工具/图片/错误图标 - 历史轮次默认折叠,最新轮次默认展开 - 折叠时显示用户和助手消息摘要 - 统一 UI 设计规范:边距、字体、图标、圆角
This commit is contained in:
@@ -7,25 +7,22 @@
|
||||
<!-- 文本块 -->
|
||||
<pre
|
||||
v-if="block.type === 'text'"
|
||||
class="m-0 text-[13px] leading-relaxed rounded-md"
|
||||
:class="[
|
||||
block.className,
|
||||
block.preWrap !== false ? 'whitespace-pre-wrap break-words' : ''
|
||||
]"
|
||||
class="m-0 text-sm leading-relaxed whitespace-pre-wrap break-words"
|
||||
:class="block.className"
|
||||
>{{ block.content }}</pre>
|
||||
|
||||
<!-- 可折叠块 -->
|
||||
<details
|
||||
v-else-if="block.type === 'collapsible'"
|
||||
class="group cursor-pointer bg-muted/30 rounded-md px-3 py-2"
|
||||
class="group cursor-pointer bg-muted/30 rounded-lg px-3 py-2"
|
||||
:class="block.className"
|
||||
:open="block.defaultOpen"
|
||||
>
|
||||
<summary class="flex items-center gap-1 list-none select-none [&::-webkit-details-marker]:hidden">
|
||||
<ChevronRight class="w-4 h-4 transition-transform duration-200 group-open:rotate-90" />
|
||||
<span class="text-muted-foreground">{{ block.title }}</span>
|
||||
<summary class="flex items-center gap-2 list-none select-none [&::-webkit-details-marker]:hidden">
|
||||
<ChevronRight class="w-4 h-4 text-muted-foreground transition-transform duration-200 group-open:rotate-90" />
|
||||
<span class="text-xs text-muted-foreground">{{ block.title }}</span>
|
||||
</summary>
|
||||
<div class="mt-2 p-3 bg-muted/50 rounded-md">
|
||||
<div class="mt-2 p-3 bg-muted/50 rounded-lg">
|
||||
<BlockRenderer :blocks="block.content" />
|
||||
</div>
|
||||
</details>
|
||||
@@ -33,11 +30,11 @@
|
||||
<!-- 代码块 -->
|
||||
<div
|
||||
v-else-if="block.type === 'code'"
|
||||
class="bg-muted/50 rounded-md overflow-hidden"
|
||||
class="bg-muted/50 rounded-lg overflow-hidden"
|
||||
>
|
||||
<div
|
||||
v-if="block.language"
|
||||
class="px-3 py-1 text-[11px] font-medium bg-muted/50 text-muted-foreground border-b border-border"
|
||||
class="px-3 py-2 text-xs font-medium bg-muted/50 text-muted-foreground border-b border-border"
|
||||
>
|
||||
{{ block.language }}
|
||||
</div>
|
||||
@@ -51,7 +48,7 @@
|
||||
<Badge
|
||||
v-else-if="block.type === 'badge'"
|
||||
:variant="block.variant || 'secondary'"
|
||||
class="inline-flex w-fit"
|
||||
class="w-fit"
|
||||
>
|
||||
{{ block.label }}
|
||||
</Badge>
|
||||
@@ -59,19 +56,19 @@
|
||||
<!-- 图片块 -->
|
||||
<div
|
||||
v-else-if="block.type === 'image'"
|
||||
class="bg-muted/30 p-3 rounded-md"
|
||||
class="bg-muted/30 p-3 rounded-lg"
|
||||
>
|
||||
<img
|
||||
v-if="block.src"
|
||||
:src="block.src"
|
||||
:alt="block.alt || '图片'"
|
||||
class="max-w-full max-h-[400px] rounded"
|
||||
class="max-w-full max-h-[400px] rounded-lg"
|
||||
>
|
||||
<div
|
||||
v-else
|
||||
class="flex items-center gap-2 text-muted-foreground text-xs"
|
||||
>
|
||||
<ImageIcon class="w-6 h-6" />
|
||||
<ImageIcon class="w-4 h-4" />
|
||||
<span>{{ block.mimeType || block.alt || '图片' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -79,13 +76,13 @@
|
||||
<!-- 错误块 -->
|
||||
<div
|
||||
v-else-if="block.type === 'error'"
|
||||
class="flex items-center gap-2 p-3 bg-destructive/10 border border-destructive/20 rounded-md text-destructive text-[13px]"
|
||||
class="flex items-center gap-2 p-3 bg-destructive/10 border border-destructive/20 rounded-lg text-destructive text-sm"
|
||||
>
|
||||
<AlertCircle class="w-4 h-4 shrink-0" />
|
||||
<span>{{ block.message }}</span>
|
||||
<span
|
||||
v-if="block.code"
|
||||
class="font-mono text-[11px] opacity-80"
|
||||
class="font-mono text-xs opacity-80"
|
||||
>{{ block.code }}</span>
|
||||
</div>
|
||||
|
||||
@@ -101,9 +98,7 @@
|
||||
>
|
||||
<BlockRenderer :blocks="block.header" />
|
||||
</div>
|
||||
<div>
|
||||
<BlockRenderer :blocks="block.children" />
|
||||
</div>
|
||||
<BlockRenderer :blocks="block.children" />
|
||||
</div>
|
||||
|
||||
<!-- 消息块 -->
|
||||
@@ -113,12 +108,12 @@
|
||||
:class="messageBlockClasses[block.role] || messageBlockClasses.default"
|
||||
>
|
||||
<div
|
||||
class="flex items-center gap-1.5 px-3 py-2 text-xs font-medium"
|
||||
class="flex items-center gap-2 px-3 py-2 text-xs font-medium"
|
||||
:class="messageHeaderClasses[block.role] || messageHeaderClasses.default"
|
||||
>
|
||||
<component
|
||||
:is="getRoleIcon(block.role)"
|
||||
class="w-3.5 h-3.5"
|
||||
class="w-4 h-4"
|
||||
/>
|
||||
<span>{{ block.roleLabel || getRoleLabel(block.role) }}</span>
|
||||
<template v-if="block.badges">
|
||||
@@ -126,13 +121,12 @@
|
||||
v-for="(badge, badgeIndex) in block.badges"
|
||||
:key="badgeIndex"
|
||||
:variant="badge.variant || 'secondary'"
|
||||
class="ml-2 text-xs"
|
||||
>
|
||||
{{ badge.label }}
|
||||
</Badge>
|
||||
</template>
|
||||
</div>
|
||||
<div class="px-3 pb-3 text-[13px] leading-relaxed">
|
||||
<div class="px-3 pb-3 text-sm leading-relaxed">
|
||||
<BlockRenderer :blocks="block.content" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -140,37 +134,36 @@
|
||||
<!-- 工具调用块 -->
|
||||
<div
|
||||
v-else-if="block.type === 'tool_use'"
|
||||
class="bg-muted/30 px-3 py-2 rounded-md"
|
||||
class="bg-muted/30 rounded-lg overflow-hidden"
|
||||
>
|
||||
<div class="flex items-center gap-1.5 text-xs font-medium mb-2 text-muted-foreground">
|
||||
<Wrench class="w-3 h-3" />
|
||||
<div class="flex items-center gap-2 px-3 py-2 text-xs font-medium text-muted-foreground">
|
||||
<Wrench class="w-4 h-4" />
|
||||
<span>{{ block.toolName }}</span>
|
||||
<span
|
||||
v-if="block.toolId"
|
||||
class="font-mono text-[10px] opacity-60"
|
||||
class="font-mono text-xs opacity-60"
|
||||
>{{ block.toolId }}</span>
|
||||
</div>
|
||||
<pre class="m-0 p-3 bg-muted/50 rounded-md font-mono text-xs max-h-[200px] overflow-y-auto whitespace-pre-wrap break-words">{{ block.input }}</pre>
|
||||
<pre class="m-0 p-3 bg-muted/50 font-mono text-xs max-h-[200px] overflow-y-auto whitespace-pre-wrap break-words">{{ block.input }}</pre>
|
||||
</div>
|
||||
|
||||
<!-- 工具结果块 -->
|
||||
<div
|
||||
v-else-if="block.type === 'tool_result'"
|
||||
class="px-3 py-2 rounded-md"
|
||||
class="rounded-lg overflow-hidden"
|
||||
:class="block.isError ? 'bg-destructive/10 border border-destructive/20' : 'bg-muted/30'"
|
||||
>
|
||||
<div class="flex items-center gap-1.5 text-xs font-medium mb-2 text-muted-foreground">
|
||||
<FileText class="w-3 h-3" />
|
||||
<div class="flex items-center gap-2 px-3 py-2 text-xs font-medium text-muted-foreground">
|
||||
<FileText class="w-4 h-4" />
|
||||
<span>工具结果</span>
|
||||
<Badge
|
||||
v-if="block.isError"
|
||||
variant="destructive"
|
||||
class="ml-2 text-xs"
|
||||
>
|
||||
错误
|
||||
</Badge>
|
||||
</div>
|
||||
<pre class="m-0 p-3 bg-muted/50 rounded-md font-mono text-xs max-h-[200px] overflow-y-auto whitespace-pre-wrap break-words">{{ block.content }}</pre>
|
||||
<pre class="m-0 p-3 bg-muted/50 font-mono text-xs max-h-[200px] overflow-y-auto whitespace-pre-wrap break-words">{{ block.content }}</pre>
|
||||
</div>
|
||||
|
||||
<!-- 分隔符块 -->
|
||||
@@ -182,7 +175,7 @@
|
||||
<!-- 标签块 -->
|
||||
<div
|
||||
v-else-if="block.type === 'label'"
|
||||
class="flex gap-2 text-[13px]"
|
||||
class="flex gap-2 text-sm"
|
||||
>
|
||||
<span class="text-muted-foreground">{{ block.label }}:</span>
|
||||
<span :class="block.mono ? 'font-mono' : ''">{{ block.value }}</span>
|
||||
@@ -200,7 +193,6 @@ defineProps<{
|
||||
blocks: RenderBlock[]
|
||||
}>()
|
||||
|
||||
// 消息块样式映射
|
||||
const messageBlockClasses: Record<string, string> = {
|
||||
user: 'bg-primary/[0.08] border border-primary/20',
|
||||
assistant: 'bg-muted/50 border border-border',
|
||||
@@ -209,7 +201,6 @@ const messageBlockClasses: Record<string, string> = {
|
||||
default: 'bg-muted/30 border border-border',
|
||||
}
|
||||
|
||||
// 消息头部样式映射
|
||||
const messageHeaderClasses: Record<string, string> = {
|
||||
user: 'text-primary',
|
||||
assistant: 'text-foreground',
|
||||
@@ -220,31 +211,21 @@ const messageHeaderClasses: Record<string, string> = {
|
||||
|
||||
const getRoleIcon = (role: string) => {
|
||||
switch (role) {
|
||||
case 'user':
|
||||
return User
|
||||
case 'assistant':
|
||||
return Bot
|
||||
case 'system':
|
||||
return Settings
|
||||
case 'tool':
|
||||
return Wrench
|
||||
default:
|
||||
return User
|
||||
case 'user': return User
|
||||
case 'assistant': return Bot
|
||||
case 'system': return Settings
|
||||
case 'tool': return Wrench
|
||||
default: return User
|
||||
}
|
||||
}
|
||||
|
||||
const getRoleLabel = (role: string) => {
|
||||
switch (role) {
|
||||
case 'user':
|
||||
return 'User'
|
||||
case 'assistant':
|
||||
return 'Assistant'
|
||||
case 'system':
|
||||
return 'System'
|
||||
case 'tool':
|
||||
return 'Tool'
|
||||
default:
|
||||
return role
|
||||
case 'user': return 'User'
|
||||
case 'assistant': return 'Assistant'
|
||||
case 'system': return 'System'
|
||||
case 'tool': return 'Tool'
|
||||
default: return role
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div class="max-h-[500px] overflow-y-auto scrollbar-gutter-stable">
|
||||
<div class="flex flex-col gap-3 p-1">
|
||||
<div class="max-h-[500px] overflow-y-auto">
|
||||
<div class="flex flex-col gap-2 p-2">
|
||||
<!-- 渲染错误提示 -->
|
||||
<div
|
||||
v-if="renderResult.error"
|
||||
class="flex items-center gap-2 p-3 bg-destructive/10 border border-destructive/30 rounded-lg text-destructive text-[13px]"
|
||||
class="flex items-center gap-2 p-3 bg-destructive/10 border border-destructive/20 rounded-lg text-destructive text-sm"
|
||||
>
|
||||
<AlertCircle class="w-4 h-4 shrink-0" />
|
||||
<span>{{ renderResult.error }}</span>
|
||||
@@ -18,16 +18,87 @@
|
||||
{{ emptyMessage }}
|
||||
</div>
|
||||
|
||||
<!-- 渲染内容块 -->
|
||||
<!-- 轮次视图 -->
|
||||
<template v-else>
|
||||
<BlockRenderer :blocks="renderResult.blocks" />
|
||||
<!-- System Prompt -->
|
||||
<div
|
||||
v-if="groupedConversation.system"
|
||||
class="rounded-lg border border-dashed border-border bg-card overflow-hidden"
|
||||
>
|
||||
<div
|
||||
class="flex items-center gap-2 px-3 py-2 cursor-pointer hover:bg-muted/30 transition-colors"
|
||||
@click="systemExpanded = !systemExpanded"
|
||||
>
|
||||
<Settings class="w-4 h-4 text-muted-foreground" />
|
||||
<span class="text-xs font-medium text-muted-foreground">System</span>
|
||||
<span class="text-xs text-muted-foreground">
|
||||
({{ groupedConversation.system.length }} 字符)
|
||||
</span>
|
||||
<component
|
||||
:is="systemExpanded ? ChevronDown : ChevronRight"
|
||||
class="w-4 h-4 text-muted-foreground ml-auto"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="systemExpanded"
|
||||
class="p-3 text-sm leading-relaxed"
|
||||
>
|
||||
<pre class="m-0 whitespace-pre-wrap break-words">{{ groupedConversation.system }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 历史轮次(折叠区域) -->
|
||||
<div
|
||||
v-if="historyTurns.length > 0"
|
||||
class="flex flex-col gap-2"
|
||||
>
|
||||
<!-- 历史轮次标题栏 -->
|
||||
<div
|
||||
class="flex items-center gap-2 px-3 py-2 cursor-pointer hover:bg-muted/30 rounded-lg border border-dashed border-border transition-colors"
|
||||
@click="historyExpanded = !historyExpanded"
|
||||
>
|
||||
<span class="text-xs font-medium text-muted-foreground">
|
||||
历史轮次 ({{ historyTurns.length }})
|
||||
</span>
|
||||
<component
|
||||
:is="historyExpanded ? ChevronDown : ChevronRight"
|
||||
class="w-4 h-4 text-muted-foreground ml-auto"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 历史轮次列表 -->
|
||||
<div
|
||||
v-if="historyExpanded"
|
||||
class="flex flex-col gap-2"
|
||||
>
|
||||
<TurnCard
|
||||
v-for="turn in historyTurns"
|
||||
:key="turn.index"
|
||||
:turn="turn"
|
||||
:expanded="expandedTurns.has(turn.index)"
|
||||
@toggle="toggleTurn(turn.index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 最新轮次(默认展开,但可折叠) -->
|
||||
<template v-if="latestTurns.length > 0">
|
||||
<TurnCard
|
||||
v-for="turn in latestTurns"
|
||||
:key="turn.index"
|
||||
:turn="turn"
|
||||
:expanded="expandedTurns.has(turn.index)"
|
||||
:is-latest="turn.index === groupedConversation.totalTurns"
|
||||
@toggle="toggleTurn(turn.index)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- 流式响应标记 -->
|
||||
<div
|
||||
v-if="renderResult.isStream"
|
||||
class="flex items-center gap-1 px-2 py-1 text-[11px] text-muted-foreground bg-muted/30 rounded w-fit"
|
||||
class="flex items-center gap-1.5 px-3 py-2 text-xs text-muted-foreground bg-muted/30 rounded-lg w-fit"
|
||||
>
|
||||
<Zap class="w-3 h-3" />
|
||||
<Zap class="w-4 h-4" />
|
||||
<span>流式响应</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -36,12 +107,74 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { AlertCircle, Zap } from 'lucide-vue-next'
|
||||
import BlockRenderer from './BlockRenderer.vue'
|
||||
import { ref, computed, watch, nextTick } from 'vue'
|
||||
import { AlertCircle, Zap, Settings, ChevronRight, ChevronDown } from 'lucide-vue-next'
|
||||
import TurnCard from './TurnCard.vue'
|
||||
import type { RenderResult } from '../../conversation'
|
||||
import { groupRenderBlocksIntoTurns } from '../../conversation/grouper'
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
renderResult: RenderResult
|
||||
emptyMessage: string
|
||||
}>()
|
||||
|
||||
// 状态
|
||||
const systemExpanded = ref(false)
|
||||
const historyExpanded = ref(false)
|
||||
const expandedTurns = ref<Set<number>>(new Set())
|
||||
|
||||
// 将渲染结果转换为分组对话
|
||||
const groupedConversation = computed(() => {
|
||||
return groupRenderBlocksIntoTurns(props.renderResult.blocks, props.renderResult.isStream)
|
||||
})
|
||||
|
||||
// 历史轮次(除最后 1-2 轮外的所有轮次)
|
||||
const historyTurns = computed(() => {
|
||||
const turns = groupedConversation.value.turns
|
||||
if (turns.length <= 2) return []
|
||||
return turns.slice(0, -2)
|
||||
})
|
||||
|
||||
// 最新轮次(最后 1-2 轮)
|
||||
const latestTurns = computed(() => {
|
||||
const turns = groupedConversation.value.turns
|
||||
if (turns.length <= 2) return turns
|
||||
return turns.slice(-2)
|
||||
})
|
||||
|
||||
// 切换单个轮次的展开状态
|
||||
function toggleTurn(index: number) {
|
||||
const newSet = new Set(expandedTurns.value)
|
||||
if (newSet.has(index)) {
|
||||
newSet.delete(index)
|
||||
} else {
|
||||
newSet.add(index)
|
||||
}
|
||||
expandedTurns.value = newSet
|
||||
}
|
||||
|
||||
// 初始化最新轮次为展开状态
|
||||
function initExpandedTurns() {
|
||||
const turns = groupedConversation.value.turns
|
||||
if (turns.length <= 2) {
|
||||
expandedTurns.value = new Set(turns.map(t => t.index))
|
||||
} else {
|
||||
expandedTurns.value = new Set(turns.slice(-2).map(t => t.index))
|
||||
}
|
||||
}
|
||||
|
||||
// 当 renderResult 变化时重置状态
|
||||
watch(() => props.renderResult, () => {
|
||||
systemExpanded.value = false
|
||||
nextTick(() => {
|
||||
initExpandedTurns()
|
||||
})
|
||||
}, { deep: true })
|
||||
|
||||
// 初始加载时也要初始化
|
||||
watch(groupedConversation, () => {
|
||||
if (expandedTurns.value.size === 0 && groupedConversation.value.turns.length > 0) {
|
||||
initExpandedTurns()
|
||||
}
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<div class="flex items-center gap-1">
|
||||
<Brain
|
||||
v-if="showBadge('thinking') && stats.hasThinking"
|
||||
class="w-3.5 h-3.5 text-muted-foreground"
|
||||
title="包含思考过程"
|
||||
/>
|
||||
<Wrench
|
||||
v-if="showBadge('tool') && stats.hasToolUse"
|
||||
class="w-3.5 h-3.5 text-muted-foreground"
|
||||
title="包含工具调用"
|
||||
/>
|
||||
<span
|
||||
v-if="showBadge('tool') && stats.toolCount > 1"
|
||||
class="text-xs text-muted-foreground"
|
||||
>×{{ stats.toolCount }}</span>
|
||||
<ImageIcon
|
||||
v-if="showBadge('image') && stats.hasImage"
|
||||
class="w-3.5 h-3.5 text-muted-foreground"
|
||||
title="包含图片"
|
||||
/>
|
||||
<AlertCircle
|
||||
v-if="showBadge('error') && stats.hasError"
|
||||
class="w-3.5 h-3.5 text-muted-foreground"
|
||||
title="包含错误"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Brain, Wrench, Image as ImageIcon, AlertCircle } from 'lucide-vue-next'
|
||||
import type { TurnStats } from '../../conversation/types'
|
||||
|
||||
type BadgeType = 'thinking' | 'tool' | 'image' | 'error'
|
||||
|
||||
const props = defineProps<{
|
||||
stats: TurnStats
|
||||
showOnly?: BadgeType[]
|
||||
}>()
|
||||
|
||||
function showBadge(type: BadgeType): boolean {
|
||||
if (!props.showOnly || props.showOnly.length === 0) {
|
||||
return true
|
||||
}
|
||||
return props.showOnly.includes(type)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div class="rounded-lg border border-dashed border-border bg-card overflow-hidden">
|
||||
<!-- 标题栏 -->
|
||||
<div
|
||||
class="flex items-center gap-2 px-3 py-2 cursor-pointer hover:bg-muted/30 transition-colors"
|
||||
@click="$emit('toggle')"
|
||||
>
|
||||
<span
|
||||
class="text-xs font-medium shrink-0"
|
||||
:class="isLatest ? 'text-primary' : 'text-muted-foreground'"
|
||||
>
|
||||
#{{ turn.index }}
|
||||
</span>
|
||||
<TurnBadges :stats="turn.stats" />
|
||||
<!-- 折叠时显示摘要 -->
|
||||
<template v-if="!expanded">
|
||||
<span
|
||||
v-if="turn.summary.user"
|
||||
class="text-xs text-muted-foreground truncate"
|
||||
>
|
||||
{{ turn.summary.user }}
|
||||
</span>
|
||||
<span
|
||||
v-if="turn.summary.user && turn.summary.assistant"
|
||||
class="text-xs text-muted-foreground shrink-0"
|
||||
>→</span>
|
||||
<span
|
||||
v-if="turn.summary.assistant"
|
||||
class="text-xs text-muted-foreground truncate"
|
||||
>
|
||||
{{ turn.summary.assistant }}
|
||||
</span>
|
||||
</template>
|
||||
<component
|
||||
:is="expanded ? ChevronDown : ChevronRight"
|
||||
class="w-4 h-4 text-muted-foreground shrink-0 ml-auto"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 展开内容 -->
|
||||
<div
|
||||
v-if="expanded"
|
||||
class="p-3 flex flex-col gap-2"
|
||||
>
|
||||
<!-- 用户消息 -->
|
||||
<div
|
||||
v-if="turn.user"
|
||||
class="rounded-lg bg-primary/[0.08] border border-primary/20 overflow-hidden"
|
||||
>
|
||||
<div class="flex items-center gap-2 px-3 py-2 text-xs font-medium text-primary">
|
||||
<User class="w-4 h-4" />
|
||||
<span>User</span>
|
||||
</div>
|
||||
<div class="px-3 pb-3 text-sm leading-relaxed">
|
||||
<BlockRenderer :blocks="userRenderBlocks" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 助手消息 -->
|
||||
<div
|
||||
v-if="turn.assistant"
|
||||
class="rounded-lg bg-muted/50 border border-border overflow-hidden"
|
||||
>
|
||||
<div class="flex items-center gap-2 px-3 py-2 text-xs font-medium text-foreground">
|
||||
<Bot class="w-4 h-4" />
|
||||
<span>Assistant</span>
|
||||
<TurnBadges
|
||||
:stats="turn.stats"
|
||||
:show-only="['thinking', 'tool']"
|
||||
/>
|
||||
</div>
|
||||
<div class="px-3 pb-3 text-sm leading-relaxed">
|
||||
<BlockRenderer :blocks="assistantRenderBlocks" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { User, Bot, ChevronRight, ChevronDown } from 'lucide-vue-next'
|
||||
import BlockRenderer from './BlockRenderer.vue'
|
||||
import TurnBadges from './TurnBadges.vue'
|
||||
import type { ConversationTurn } from '../../conversation/types'
|
||||
import { contentBlocksToRenderBlocks } from '../../conversation/converter'
|
||||
|
||||
const props = defineProps<{
|
||||
turn: ConversationTurn
|
||||
expanded: boolean
|
||||
isLatest?: boolean
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
toggle: []
|
||||
}>()
|
||||
|
||||
const userRenderBlocks = computed(() => {
|
||||
if (!props.turn.user) return []
|
||||
return contentBlocksToRenderBlocks(props.turn.user.content)
|
||||
})
|
||||
|
||||
const assistantRenderBlocks = computed(() => {
|
||||
if (!props.turn.assistant) return []
|
||||
return contentBlocksToRenderBlocks(props.turn.assistant.content)
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user