mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-05-17 11:59:01 +08:00
refactor: 重构AI对话目录
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user