refactor: 重构AI对话目录

This commit is contained in:
digua
2026-01-12 23:53:33 +08:00
parent 08e221c876
commit 2b04c377d6
3 changed files with 265 additions and 163 deletions
@@ -0,0 +1,118 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
// Props
defineProps<{
// 当前工具执行状态
currentToolStatus: {
name: string
displayName: string
status: 'running' | 'done' | 'error'
} | null
// 当前轮次已使用的工具列表
toolsUsed: string[]
}>()
</script>
<template>
<div class="flex items-start gap-3">
<!-- AI 头像 -->
<div
class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-linear-to-br from-pink-500 to-pink-600"
>
<UIcon name="i-heroicons-sparkles" class="h-4 w-4 text-white" />
</div>
<!-- 状态气泡 -->
<div class="rounded-2xl rounded-tl-sm bg-gray-100 px-4 py-3 dark:bg-gray-800">
<!-- 工具执行状态 -->
<div v-if="currentToolStatus" class="space-y-2">
<div class="flex items-center gap-2">
<span
class="inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium"
:class="[
currentToolStatus.status === 'running'
? 'bg-pink-100 text-pink-700 dark:bg-pink-900/30 dark:text-pink-300'
: currentToolStatus.status === 'done'
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300'
: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300',
]"
>
<UIcon
:name="
currentToolStatus.status === 'running'
? 'i-heroicons-cog-6-tooth'
: currentToolStatus.status === 'done'
? 'i-heroicons-check-circle'
: 'i-heroicons-x-circle'
"
class="h-3 w-3"
:class="{ 'animate-spin': currentToolStatus.status === 'running' }"
/>
{{ currentToolStatus.displayName }}
</span>
<!-- 运行中的动画 -->
<span v-if="currentToolStatus.status === 'running'" class="flex gap-1">
<span class="h-1.5 w-1.5 animate-bounce rounded-full bg-pink-500 [animation-delay:0ms]" />
<span class="h-1.5 w-1.5 animate-bounce rounded-full bg-pink-500 [animation-delay:150ms]" />
<span class="h-1.5 w-1.5 animate-bounce rounded-full bg-pink-500 [animation-delay:300ms]" />
</span>
<!-- 完成后处理中状态 -->
<span
v-else-if="currentToolStatus.status === 'done'"
class="flex items-center gap-1.5 text-xs text-gray-500 dark:text-gray-400"
>
<span>{{ t('processingResult') }}</span>
<span class="flex gap-1">
<span class="h-1 w-1 animate-bounce rounded-full bg-gray-400 [animation-delay:0ms]" />
<span class="h-1 w-1 animate-bounce rounded-full bg-gray-400 [animation-delay:150ms]" />
<span class="h-1 w-1 animate-bounce rounded-full bg-gray-400 [animation-delay:300ms]" />
</span>
</span>
</div>
<!-- 已使用的工具列表 -->
<div v-if="toolsUsed.length > 1" class="flex flex-wrap gap-1">
<span class="text-xs text-gray-400">{{ t('called') }}</span>
<span
v-for="tool in toolsUsed.slice(0, -1)"
:key="tool"
class="inline-flex items-center gap-1 rounded-full bg-gray-200 px-2 py-0.5 text-xs text-gray-600 dark:bg-gray-700 dark:text-gray-400"
>
<UIcon name="i-heroicons-check" class="h-3 w-3 text-green-500" />
{{ tool }}
</span>
</div>
</div>
<!-- 默认状态无工具调用 -->
<div v-else class="flex items-center gap-2">
<span class="text-sm text-gray-600 dark:text-gray-400">{{ t('analyzing') }}</span>
<span class="flex gap-1">
<span class="h-2 w-2 animate-bounce rounded-full bg-pink-500 [animation-delay:0ms]" />
<span class="h-2 w-2 animate-bounce rounded-full bg-pink-500 [animation-delay:150ms]" />
<span class="h-2 w-2 animate-bounce rounded-full bg-pink-500 [animation-delay:300ms]" />
</span>
</div>
</div>
</div>
</template>
<i18n>
{
"zh-CN": {
"processingResult": "处理结果中",
"called": "已调用:",
"analyzing": "正在分析问题..."
},
"en-US": {
"processingResult": "Processing result",
"called": "Called:",
"analyzing": "Analyzing question..."
}
}
</i18n>
+12 -163
View File
@@ -1,11 +1,12 @@
<script setup lang="ts">
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
import { storeToRefs } from 'pinia'
import { useI18n } from 'vue-i18n'
import ConversationList from './ConversationList.vue'
import DataSourcePanel from './DataSourcePanel.vue'
import ChatMessage from './ChatMessage.vue'
import ChatInput from './ChatInput.vue'
import AIThinkingIndicator from './AIThinkingIndicator.vue'
import ChatStatusBar from './ChatStatusBar.vue'
import { useAIChat } from '@/composables/useAIChat'
import CaptureButton from '@/components/common/CaptureButton.vue'
import { usePromptStore } from '@/stores/prompt'
@@ -43,33 +44,10 @@ const {
// Store
const promptStore = usePromptStore()
const { aiPromptSettings, activePreset } = storeToRefs(promptStore)
// 当前聊天类型
const currentChatType = computed(() => props.chatType ?? 'group')
// 当前类型对应的预设列表(根据 applicableTo 过滤)
const currentPresets = computed(() => promptStore.getPresetsForChatType(currentChatType.value))
// 当前激活的预设 ID
const currentActivePresetId = computed(() => aiPromptSettings.value.activePresetId)
// 当前激活的预设(如果当前激活的预设不适用于当前类型,使用第一个可用预设)
const currentActivePreset = computed(() => {
const activeInList = currentPresets.value.find((p) => p.id === currentActivePresetId.value)
return activeInList || activePreset.value
})
// 预设下拉菜单状态
const isPresetPopoverOpen = ref(false)
// 设置激活预设
function setActivePreset(presetId: string) {
promptStore.setActivePreset(presetId)
// 关闭下拉菜单
isPresetPopoverOpen.value = false
}
// UI 状态
const isSourcePanelCollapsed = ref(false)
const hasLLMConfig = ref(false)
@@ -355,87 +333,15 @@ watch(
</template>
<!-- AI 思考中指示器(仅在没有任何内容块时显示) -->
<div
<AIThinkingIndicator
v-if="
isAIThinking &&
!messages[messages.length - 1]?.content &&
!(messages[messages.length - 1]?.contentBlocks?.length ?? 0)
"
class="flex items-start gap-3"
>
<div
class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-linear-to-br from-pink-500 to-pink-600"
>
<UIcon name="i-heroicons-sparkles" class="h-4 w-4 text-white" />
</div>
<div class="rounded-2xl rounded-tl-sm bg-gray-100 px-4 py-3 dark:bg-gray-800">
<!-- 工具执行状态 -->
<div v-if="currentToolStatus" class="space-y-2">
<div class="flex items-center gap-2">
<span
class="inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium"
:class="[
currentToolStatus.status === 'running'
? 'bg-pink-100 text-pink-700 dark:bg-pink-900/30 dark:text-pink-300'
: currentToolStatus.status === 'done'
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300'
: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300',
]"
>
<UIcon
:name="
currentToolStatus.status === 'running'
? 'i-heroicons-cog-6-tooth'
: currentToolStatus.status === 'done'
? 'i-heroicons-check-circle'
: 'i-heroicons-x-circle'
"
class="h-3 w-3"
:class="{ 'animate-spin': currentToolStatus.status === 'running' }"
/>
{{ currentToolStatus.displayName }}
</span>
<span v-if="currentToolStatus.status === 'running'" class="flex gap-1">
<span class="h-1.5 w-1.5 animate-bounce rounded-full bg-pink-500 [animation-delay:0ms]" />
<span class="h-1.5 w-1.5 animate-bounce rounded-full bg-pink-500 [animation-delay:150ms]" />
<span class="h-1.5 w-1.5 animate-bounce rounded-full bg-pink-500 [animation-delay:300ms]" />
</span>
<span
v-else-if="currentToolStatus.status === 'done'"
class="flex items-center gap-1.5 text-xs text-gray-500 dark:text-gray-400"
>
<span>{{ t('ai.status.processingResult') }}</span>
<span class="flex gap-1">
<span class="h-1 w-1 animate-bounce rounded-full bg-gray-400 [animation-delay:0ms]" />
<span class="h-1 w-1 animate-bounce rounded-full bg-gray-400 [animation-delay:150ms]" />
<span class="h-1 w-1 animate-bounce rounded-full bg-gray-400 [animation-delay:300ms]" />
</span>
</span>
</div>
<!-- 已使用的工具列表 -->
<div v-if="toolsUsedInCurrentRound.length > 1" class="flex flex-wrap gap-1">
<span class="text-xs text-gray-400">{{ t('ai.status.called') }}</span>
<span
v-for="tool in toolsUsedInCurrentRound.slice(0, -1)"
:key="tool"
class="inline-flex items-center gap-1 rounded-full bg-gray-200 px-2 py-0.5 text-xs text-gray-600 dark:bg-gray-700 dark:text-gray-400"
>
<UIcon name="i-heroicons-check" class="h-3 w-3 text-green-500" />
{{ tool }}
</span>
</div>
</div>
<!-- 默认状态 -->
<div v-else class="flex items-center gap-2">
<span class="text-sm text-gray-600 dark:text-gray-400">{{ t('ai.status.analyzing') }}</span>
<span class="flex gap-1">
<span class="h-2 w-2 animate-bounce rounded-full bg-pink-500 [animation-delay:0ms]" />
<span class="h-2 w-2 animate-bounce rounded-full bg-pink-500 [animation-delay:150ms]" />
<span class="h-2 w-2 animate-bounce rounded-full bg-pink-500 [animation-delay:300ms]" />
</span>
</div>
</div>
</div>
:current-tool-status="currentToolStatus"
:tools-used="toolsUsedInCurrentRound"
/>
</div>
</div>
@@ -462,69 +368,12 @@ watch(
/>
<!-- 底部状态栏 -->
<div class="flex items-center justify-between px-1">
<!-- 左侧:预设选择器 -->
<UPopover v-model:open="isPresetPopoverOpen" :ui="{ content: 'p-0' }">
<button
class="flex items-center gap-1.5 rounded-md px-2 py-1 text-xs text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-300"
>
<UIcon name="i-heroicons-chat-bubble-bottom-center-text" class="h-3.5 w-3.5" />
<span class="max-w-[120px] truncate">{{ currentActivePreset?.name || t('ai.preset.default') }}</span>
<UIcon name="i-heroicons-chevron-down" class="h-3 w-3" />
</button>
<template #content>
<div class="w-48 py-1">
<div class="px-3 py-1.5 text-xs font-medium text-gray-400 dark:text-gray-500">
{{ currentChatType === 'group' ? t('ai.preset.groupTitle') : t('ai.preset.privateTitle') }}
</div>
<button
v-for="preset in currentPresets"
:key="preset.id"
class="flex w-full items-center gap-2 px-3 py-2 text-left text-sm transition-colors hover:bg-gray-100 dark:hover:bg-gray-800"
:class="[
preset.id === currentActivePresetId
? 'text-pink-600 dark:text-pink-400'
: 'text-gray-700 dark:text-gray-300',
]"
@click="setActivePreset(preset.id)"
>
<UIcon
:name="
preset.id === currentActivePresetId
? 'i-heroicons-check-circle-solid'
: 'i-heroicons-document-text'
"
class="h-4 w-4 shrink-0"
:class="[preset.id === currentActivePresetId ? 'text-pink-500' : 'text-gray-400']"
/>
<span class="truncate">{{ preset.name }}</span>
</button>
</div>
</template>
</UPopover>
<!-- 右侧:Token 使用量 + 配置状态指示 -->
<div class="flex items-center gap-3">
<!-- Token 使用量 -->
<div
v-if="sessionTokenUsage.totalTokens > 0"
class="flex items-center gap-1.5 text-xs text-gray-400"
title="本次会话累计 Token 使用量"
>
<UIcon name="i-heroicons-chart-bar-square" class="h-3.5 w-3.5" />
<span>{{ sessionTokenUsage.totalTokens.toLocaleString() }} tokens</span>
</div>
<div
v-if="!isCheckingConfig"
class="flex items-center gap-1.5 text-xs transition-colors"
:class="[hasLLMConfig ? 'text-gray-400' : 'text-amber-500 font-medium']"
>
<span class="h-1.5 w-1.5 rounded-full" :class="[hasLLMConfig ? 'bg-green-500' : 'bg-amber-500']" />
{{ hasLLMConfig ? t('ai.status.connected') : t('ai.status.notConfigured') }}
</div>
</div>
</div>
<ChatStatusBar
:chat-type="currentChatType"
:session-token-usage="sessionTokenUsage"
:has-l-l-m-config="hasLLMConfig"
:is-checking-config="isCheckingConfig"
/>
</div>
</div>
</div>
@@ -0,0 +1,135 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { storeToRefs } from 'pinia'
import { useI18n } from 'vue-i18n'
import { usePromptStore } from '@/stores/prompt'
const { t } = useI18n()
// Props
const props = defineProps<{
chatType: 'group' | 'private'
sessionTokenUsage: { totalTokens: number }
hasLLMConfig: boolean
isCheckingConfig: boolean
}>()
// Store
const promptStore = usePromptStore()
const { aiPromptSettings, activePreset } = storeToRefs(promptStore)
// 当前类型对应的预设列表(根据 applicableTo 过滤)
const currentPresets = computed(() => promptStore.getPresetsForChatType(props.chatType))
// 当前激活的预设 ID
const currentActivePresetId = computed(() => aiPromptSettings.value.activePresetId)
// 当前激活的预设(如果当前激活的预设不适用于当前类型,使用第一个可用预设)
const currentActivePreset = computed(() => {
const activeInList = currentPresets.value.find((p) => p.id === currentActivePresetId.value)
return activeInList || activePreset.value
})
// 预设下拉菜单状态
const isPresetPopoverOpen = ref(false)
// 设置激活预设
function setActivePreset(presetId: string) {
promptStore.setActivePreset(presetId)
isPresetPopoverOpen.value = false
}
</script>
<template>
<div class="flex items-center justify-between px-1">
<!-- 左侧预设选择器 -->
<UPopover v-model:open="isPresetPopoverOpen" :ui="{ content: 'p-0' }">
<button
class="flex items-center gap-1.5 rounded-md px-2 py-1 text-xs text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-300"
>
<UIcon name="i-heroicons-chat-bubble-bottom-center-text" class="h-3.5 w-3.5" />
<span class="max-w-[120px] truncate">{{ currentActivePreset?.name || t('preset.default') }}</span>
<UIcon name="i-heroicons-chevron-down" class="h-3 w-3" />
</button>
<template #content>
<div class="w-48 py-1">
<div class="px-3 py-1.5 text-xs font-medium text-gray-400 dark:text-gray-500">
{{ chatType === 'group' ? t('preset.groupTitle') : t('preset.privateTitle') }}
</div>
<button
v-for="preset in currentPresets"
:key="preset.id"
class="flex w-full items-center gap-2 px-3 py-2 text-left text-sm transition-colors hover:bg-gray-100 dark:hover:bg-gray-800"
:class="[
preset.id === currentActivePresetId
? 'text-pink-600 dark:text-pink-400'
: 'text-gray-700 dark:text-gray-300',
]"
@click="setActivePreset(preset.id)"
>
<UIcon
:name="
preset.id === currentActivePresetId ? 'i-heroicons-check-circle-solid' : 'i-heroicons-document-text'
"
class="h-4 w-4 shrink-0"
:class="[preset.id === currentActivePresetId ? 'text-pink-500' : 'text-gray-400']"
/>
<span class="truncate">{{ preset.name }}</span>
</button>
</div>
</template>
</UPopover>
<!-- 右侧Token 使用量 + 配置状态指示 -->
<div class="flex items-center gap-3">
<!-- Token 使用量 -->
<div
v-if="sessionTokenUsage.totalTokens > 0"
class="flex items-center gap-1.5 text-xs text-gray-400"
:title="t('tokenUsageTitle')"
>
<UIcon name="i-heroicons-chart-bar-square" class="h-3.5 w-3.5" />
<span>{{ sessionTokenUsage.totalTokens.toLocaleString() }} tokens</span>
</div>
<!-- 配置状态 -->
<div
v-if="!isCheckingConfig"
class="flex items-center gap-1.5 text-xs transition-colors"
:class="[hasLLMConfig ? 'text-gray-400' : 'text-amber-500 font-medium']"
>
<span class="h-1.5 w-1.5 rounded-full" :class="[hasLLMConfig ? 'bg-green-500' : 'bg-amber-500']" />
{{ hasLLMConfig ? t('status.connected') : t('status.notConfigured') }}
</div>
</div>
</div>
</template>
<i18n>
{
"zh-CN": {
"preset": {
"default": "默认预设",
"groupTitle": "群聊提示词预设",
"privateTitle": "私聊提示词预设"
},
"tokenUsageTitle": "本次会话累计 Token 使用量",
"status": {
"connected": "AI 已连接",
"notConfigured": "请在全局设置中配置 AI 服务"
}
},
"en-US": {
"preset": {
"default": "Default Preset",
"groupTitle": "Group Chat Presets",
"privateTitle": "Private Chat Presets"
},
"tokenUsageTitle": "Total token usage in this session",
"status": {
"connected": "AI Connected",
"notConfigured": "Please configure AI service in Settings"
}
}
}
</i18n>