feat(chat-record): polish viewer controls and highlights

This commit is contained in:
digua committed 2026-06-25 23:18:11 +08:00
1 parent 851166188d
commit e5954d8fb1
7 files changed
+63 -56

No files matched your search

@@ -70,10 +70,8 @@ const currentColor = computed(() => colorPalette[colorIndex.value])
const avatarColor = computed(() => currentColor.value.avatar)
const nameColor = computed(() => currentColor.value.name)
// 气泡颜色(Owner 使用微粉色,其他人使用微灰色)
const bubbleColor = computed(() =>
isOwner.value ? 'bg-pink-50/80 dark:bg-pink-950/20' : 'bg-gray-100/80 dark:bg-gray-800/80'
)
// 气泡颜色
const bubbleColor = 'bg-gray-100/80 dark:bg-gray-800/80'
// 显示名称(包含别名)
const displayName = computed(() => {
@@ -116,23 +114,23 @@ const avatarLetter = computed(() => {
return '?'
})
const HIGHLIGHT_MARK_CLASS =
'bg-transparent text-inherit underline decoration-primary-500/80 underline-offset-4 dark:decoration-primary-400/80'
// 高亮关键词
function highlightContent(content: string): string {
if (!props.highlightKeywords?.length || !content) return content
const pattern = props.highlightKeywords.map((k) => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')
const regex = new RegExp(`(${pattern})`, 'gi')
return content.replace(
regex,
'<mark class="bg-transparent border-b-2 border-yellow-400 dark:border-yellow-500">$1</mark>'
)
return content.replace(regex, `<mark class="${HIGHLIGHT_MARK_CLASS}">$1</mark>`)
}
</script>
<template>
<div
class="group px-4 py-2.5 transition-all duration-300"
:class="[isTarget ? 'bg-amber-500/5 dark:bg-amber-500/10' : 'hover:bg-gray-50/40 dark:hover:bg-gray-900/10']"
:class="[isTarget ? 'bg-primary-500/5 dark:bg-primary-500/10' : 'hover:bg-gray-50/40 dark:hover:bg-gray-900/10']"
>
<!-- Owner 消息显示在右侧 -->
<div class="flex gap-3" :class="isOwner ? 'flex-row-reverse' : ''">
@@ -164,7 +162,7 @@ function highlightContent(content: string): string {
<div class="flex items-start gap-1 max-w-[calc(100%-68px)]" :class="isOwner ? 'flex-row-reverse' : ''">
<div
class="relative inline-block rounded-2xl px-3.5 py-2.5 transition-all duration-300 border border-gray-100 dark:border-gray-800/40"
:class="[bubbleColor, isTarget ? 'ring-2 ring-amber-400 dark:ring-amber-500' : '']"
:class="bubbleColor"
>
<!-- 回复引用样式 -->
<div
@@ -148,7 +148,7 @@ function getSessionAvatarClass(sessionId: number): string {
// 估算项目高度
const ESTIMATED_DATE_HEIGHT = 28 // 日期头高度
const ESTIMATED_SESSION_HEIGHT = 52 // 会话项高度(含两行摘要)
const ESTIMATED_SESSION_HEIGHT = 52 // 会话项高度(摘要/生成按钮单独一行)
// 虚拟化器
const virtualizer = useVirtualizer(
@@ -227,10 +227,10 @@ function scrollToBottom() {
}
// 滚动到指定会话
function scrollToSession(sessionId: number) {
function scrollToSession(sessionId: number, behavior: 'auto' | 'smooth' = 'auto') {
const index = flatList.value.findIndex((item) => item.type === 'session' && item.session.id === sessionId)
if (index !== -1) {
virtualizer.value.scrollToIndex(index, { align: 'center' })
virtualizer.value.scrollToIndex(index, { align: 'center', behavior })
}
}
@@ -296,7 +296,7 @@ watch(
() => props.activeSessionId,
(newId) => {
if (newId) {
scrollToSession(newId)
scrollToSession(newId, 'smooth')
}
}
)
@@ -333,7 +333,9 @@ watch(
<span class="text-xs font-medium text-gray-600 dark:text-gray-300">{{ t('records.timeline.timeline') }}</span>
<div class="flex items-center gap-0.5">
<UTooltip :text="t('records.batchSummary.title')">
<UButton icon="i-heroicons-sparkles" variant="ghost" size="xs" @click="showBatchSummaryModal = true" />
<UButton icon="i-heroicons-sparkles" variant="ghost" size="xs" @click="showBatchSummaryModal = true">
{{ t('records.batchSummary.trigger') }}
</UButton>
</UTooltip>
<UButton icon="i-heroicons-chevron-left" variant="ghost" size="xs" @click="isCollapsed = true" />
</div>
@@ -375,7 +377,7 @@ watch(
<template v-else-if="flatList[virtualItem.index]?.type === 'session'">
<div class="px-1.5 py-0.5">
<button
class="group relative flex w-full gap-2.5 items-center rounded-xl p-1.5 pl-2.5 text-left transition-all duration-200"
class="group relative flex h-12 w-full items-center gap-2.5 rounded-xl p-1.5 pl-2.5 text-left transition-all duration-200"
:class="[
activeSessionId === (flatList[virtualItem.index] as { session: ChatSessionItem }).session.id
? 'bg-pink-500/5 dark:bg-pink-500/10'
@@ -395,38 +397,35 @@ watch(
<!-- 莫兰迪配色哈希头像 -->
<div
class="w-7 h-7 rounded-lg flex items-center justify-center shrink-0 transition-colors"
class="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg px-1 font-mono text-[10px] font-semibold transition-colors"
:class="
getSessionAvatarClass((flatList[virtualItem.index] as { session: ChatSessionItem }).session.id)
"
>
<UIcon name="i-heroicons-chat-bubble-oval-left" class="w-3.5 h-3.5" />
<span class="truncate">
{{ (flatList[virtualItem.index] as { session: ChatSessionItem }).session.messageCount }}
</span>
</div>
<!-- 会话内容信息 -->
<div class="flex-1 min-w-0">
<!-- 时间和消息数 -->
<div class="flex items-center justify-between mb-0.5">
<span class="text-xs font-semibold text-gray-700 dark:text-gray-200">
{{ formatTime((flatList[virtualItem.index] as { session: ChatSessionItem }).session.startTs) }}
</span>
<span
class="font-mono text-[10px] px-1.5 py-0.5 rounded bg-gray-200/50 dark:bg-gray-700/50 text-gray-500 dark:text-gray-400 font-medium"
>
{{ (flatList[virtualItem.index] as { session: ChatSessionItem }).session.messageCount }}
</span>
</div>
<!-- 摘要或生成按钮 -->
<div class="flex w-full items-center">
<!-- 有摘要:显示摘要(两行) -->
<div class="min-w-0 flex-1">
<div class="min-w-0">
<!-- 有摘要:时间和摘要作为连续文本排版,换行后从整行起点继续显示 -->
<UTooltip
v-if="(flatList[virtualItem.index] as { session: ChatSessionItem }).session.summary"
:popper="{ placement: 'right' }"
:content="{ side: 'right', align: 'start' }"
:ui="{ content: 'z-[10001] h-auto max-h-80 overflow-y-auto' }"
>
<span class="line-clamp-2 text-xs leading-normal text-gray-400 dark:text-gray-500 font-normal">
{{ (flatList[virtualItem.index] as { session: ChatSessionItem }).session.summary }}
<span
class="line-clamp-2 min-w-0 text-xs font-normal leading-snug text-gray-500 dark:text-gray-400"
>
<span class="font-medium">
{{
formatTime((flatList[virtualItem.index] as { session: ChatSessionItem }).session.startTs)
}}
</span>
<span class="ml-1">
{{ (flatList[virtualItem.index] as { session: ChatSessionItem }).session.summary }}
</span>
</span>
<template #content>
<div class="max-w-sm whitespace-pre-wrap text-sm leading-relaxed font-normal">
@@ -434,13 +433,19 @@ watch(
</div>
</template>
</UTooltip>
<span v-else class="text-xs font-medium text-gray-500 dark:text-gray-400">
{{ formatTime((flatList[virtualItem.index] as { session: ChatSessionItem }).session.startTs) }}
</span>
</div>
<div
v-if="!(flatList[virtualItem.index] as { session: ChatSessionItem }).session.summary"
class="mt-0.5 flex min-w-0 items-center"
>
<!-- 无摘要且消息数>=3:显示生成按钮 -->
<span
v-else-if="
(flatList[virtualItem.index] as { session: ChatSessionItem }).session.messageCount >= 3
"
class="flex items-center gap-1 text-xs text-gray-400 hover:text-pink-500 dark:text-gray-500 dark:hover:text-pink-400 cursor-pointer"
v-if="(flatList[virtualItem.index] as { session: ChatSessionItem }).session.messageCount >= 3"
class="flex min-w-0 cursor-pointer items-center gap-1 text-xs text-gray-400 hover:text-pink-500 dark:text-gray-500 dark:hover:text-pink-400"
@click="
generateSummary((flatList[virtualItem.index] as { session: ChatSessionItem }).session, $event)
"
@@ -448,14 +453,14 @@ watch(
<UIcon
v-if="isGenerating((flatList[virtualItem.index] as { session: ChatSessionItem }).session.id)"
name="i-heroicons-arrow-path"
class="h-3 w-3 animate-spin"
class="h-3 w-3 shrink-0 animate-spin"
/>
<UIcon v-else name="i-heroicons-sparkles" class="h-3 w-3" />
<span>{{ t('records.timeline.generateSummary') }}</span>
<UIcon v-else name="i-heroicons-sparkles" class="h-3 w-3 shrink-0" />
<span class="truncate">{{ t('records.timeline.generateSummary') }}</span>
</span>
<!-- 消息数<3:显示提示 -->
<span v-else class="text-xs italic text-gray-300 dark:text-gray-600">
<span v-else class="min-w-0 flex-1 truncate text-xs italic text-gray-300 dark:text-gray-600">
{{ t('records.timeline.tooFewMessages') }}
</span>
</div>
@@ -44,6 +44,16 @@ const timeSelectVisible = computed(() => !['ai-chat', 'memory', 'lab', 'debug'].
<PageHeader :title="title" :avatar="avatar" size="compact" :icon="icon" :icon-class="iconClass">
<template #actions>
<template v-if="layoutStore.toolsPanelPosition === 'header'">
<UTooltip :text="t('analysis.tooltip.viewChatRecord')">
<UButton
icon="i-heroicons-chat-bubble-bottom-center-text"
variant="ghost"
color="gray"
size="sm"
class="hover:bg-gray-100 dark:hover:bg-gray-800"
@click="emit('openChatRecord')"
/>
</UTooltip>
<UTooltip :text="t('analysis.tooltip.incrementalImport')">
<UButton
icon="i-heroicons-plus-circle"
@@ -64,16 +74,6 @@ const timeSelectVisible = computed(() => !['ai-chat', 'memory', 'lab', 'debug'].
@click="emit('openMemberManagement')"
/>
</UTooltip>
<UTooltip :text="t('analysis.tooltip.viewChatRecord')">
<UButton
icon="i-heroicons-chat-bubble-bottom-center-text"
variant="ghost"
color="gray"
size="sm"
class="hover:bg-gray-100 dark:hover:bg-gray-800"
@click="emit('openChatRecord')"
/>
</UTooltip>
<CaptureButton color="gray" />
<UTooltip :text="t('analysis.tooltip.more')">
<UButton
+1
View File
@@ -30,6 +30,7 @@
"summaryUnknownError": "Unknown error",
"batchSummary": {
"title": "Batch Generate Summaries",
"trigger": "Batch",
"byRange": "By Range",
"byTime": "By Time",
"selectRange": "Select Range",
+1
View File
@@ -30,6 +30,7 @@
"summaryUnknownError": "不明なエラー",
"batchSummary": {
"title": "要約をまとめて作成",
"trigger": "一括生成",
"byRange": "範囲で選択",
"byTime": "時間で選択",
"selectRange": "範囲を選択",
+1
View File
@@ -30,6 +30,7 @@
"summaryUnknownError": "未知错误",
"batchSummary": {
"title": "批量生成摘要",
"trigger": "批量生成",
"byRange": "按范围",
"byTime": "按时间",
"selectRange": "选择范围",
+1
View File
@@ -30,6 +30,7 @@
"summaryUnknownError": "未知錯誤",
"batchSummary": {
"title": "批次產生摘要",
"trigger": "批次產生",
"byRange": "依範圍",
"byTime": "依時間",
"selectRange": "選擇範圍",