feat: 更新Chat前端页面
- 优化Chat组件交互体验 - 修复智能体选择逻辑 - 新增chat页面样式和脚本 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Agent } from '@/composables/useChat'
|
import type { Agent } from '@/views/chat/chat'
|
||||||
|
|
||||||
defineProps<{
|
const props = defineProps<{
|
||||||
show: boolean
|
show: boolean
|
||||||
selectMode: 'single' | 'group'
|
selectMode: 'single' | 'group'
|
||||||
chatAgents: Agent[]
|
chatAgents: Agent[]
|
||||||
@@ -15,6 +15,11 @@ const emit = defineEmits<{
|
|||||||
(e: 'confirm'): void
|
(e: 'confirm'): void
|
||||||
(e: 'update:groupChatName', value: string): void
|
(e: 'update:groupChatName', value: string): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
// 点击智能体 - 只是选择,不直接确认
|
||||||
|
const handleAgentClick = (agent: Agent) => {
|
||||||
|
emit('toggleSelect', agent)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -47,7 +52,7 @@ const emit = defineEmits<{
|
|||||||
<button
|
<button
|
||||||
v-for="agent in chatAgents"
|
v-for="agent in chatAgents"
|
||||||
:key="agent.id"
|
:key="agent.id"
|
||||||
@click="emit('toggleSelect', agent)"
|
@click="handleAgentClick(agent)"
|
||||||
class="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg transition-all duration-200"
|
class="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg transition-all duration-200"
|
||||||
:class="selectedAgents.some(a => a.id === agent.id)
|
:class="selectedAgents.some(a => a.id === agent.id)
|
||||||
? 'bg-orange-500/20 border border-orange-500/50'
|
? 'bg-orange-500/20 border border-orange-500/50'
|
||||||
@@ -82,7 +87,15 @@ const emit = defineEmits<{
|
|||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="selectMode === 'group'"
|
v-if="selectMode === 'single'"
|
||||||
|
@click="emit('confirm')"
|
||||||
|
:disabled="selectedAgents.length === 0"
|
||||||
|
class="flex-1 py-2.5 bg-orange-500 hover:bg-orange-400 text-white rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
@click="emit('confirm')"
|
@click="emit('confirm')"
|
||||||
:disabled="selectedAgents.length < 2"
|
:disabled="selectedAgents.length < 2"
|
||||||
class="flex-1 py-2.5 bg-orange-500 hover:bg-orange-400 text-white rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
class="flex-1 py-2.5 bg-orange-500 hover:bg-orange-400 text-white rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { getModelIcon, type Agent, type ChatModel } from '@/composables/useChat'
|
import { getModelIcon, type Agent, type ChatModel } from '@/views/chat/chat'
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
selectedAgent: Agent | null
|
selectedAgent: Agent | null
|
||||||
@@ -13,6 +13,8 @@ const emit = defineEmits<{
|
|||||||
(e: 'toggleDropdown'): void
|
(e: 'toggleDropdown'): void
|
||||||
(e: 'selectModel', model: ChatModel): void
|
(e: 'selectModel', model: ChatModel): void
|
||||||
(e: 'toggleSidebar'): void
|
(e: 'toggleSidebar'): void
|
||||||
|
(e: 'clearChat'): void
|
||||||
|
(e: 'newChat'): void
|
||||||
}>()
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -40,8 +42,30 @@ const emit = defineEmits<{
|
|||||||
<!-- 中间:空白 -->
|
<!-- 中间:空白 -->
|
||||||
<div class="flex-1"></div>
|
<div class="flex-1"></div>
|
||||||
|
|
||||||
<!-- 右侧:模型选择和折叠按钮 -->
|
<!-- 右侧:模型选择和操作按钮 -->
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-2">
|
||||||
|
<!-- 新建对话按钮 -->
|
||||||
|
<button
|
||||||
|
@click="emit('newChat')"
|
||||||
|
class="p-2 rounded-lg hover:bg-white/[0.06] text-white/35 hover:text-white/80 transition-all duration-200"
|
||||||
|
title="新建对话"
|
||||||
|
>
|
||||||
|
<svg class="w-[18px] h-[18px]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 4v16m8-8H4"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 清空对话按钮 -->
|
||||||
|
<button
|
||||||
|
@click="emit('clearChat')"
|
||||||
|
class="p-2 rounded-lg hover:bg-white/[0.06] text-white/35 hover:text-white/80 transition-all duration-200"
|
||||||
|
title="清空对话"
|
||||||
|
>
|
||||||
|
<svg class="w-[18px] h-[18px]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
<!-- 模型选择下拉框 -->
|
<!-- 模型选择下拉框 -->
|
||||||
<div class="relative model-dropdown" v-if="chatModels.length > 0">
|
<div class="relative model-dropdown" v-if="chatModels.length > 0">
|
||||||
<button
|
<button
|
||||||
@@ -52,7 +76,10 @@ const emit = defineEmits<{
|
|||||||
<span class="text-base">{{ getModelIcon(selectedModel?.provider || '') }}</span>
|
<span class="text-base">{{ getModelIcon(selectedModel?.provider || '') }}</span>
|
||||||
<span class="text-sm text-white">{{ selectedModel?.name || 'Select Model' }}</span>
|
<span class="text-sm text-white">{{ selectedModel?.name || 'Select Model' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="w-1.5 h-1.5 rounded-full bg-emerald-400"></span>
|
<span
|
||||||
|
class="w-1.5 h-1.5 rounded-full"
|
||||||
|
:class="selectedModel?.status === 1 ? 'bg-emerald-400' : 'bg-gray-400'"
|
||||||
|
></span>
|
||||||
<svg class="w-3.5 h-3.5 text-white/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-3.5 h-3.5 text-white/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -80,7 +107,10 @@ const emit = defineEmits<{
|
|||||||
<div class="text-sm text-white">{{ model.name }}</div>
|
<div class="text-sm text-white">{{ model.name }}</div>
|
||||||
<div class="text-xs text-white/40">{{ model.provider }} · {{ model.model }}</div>
|
<div class="text-xs text-white/40">{{ model.provider }} · {{ model.model }}</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="w-2 h-2 rounded-full bg-emerald-400"></span>
|
<span
|
||||||
|
class="w-2 h-2 rounded-full"
|
||||||
|
:class="model.status === 1 ? 'bg-emerald-400' : 'bg-gray-400'"
|
||||||
|
></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ const autoResize = (e: Event) => {
|
|||||||
@keydown="handleKeydown"
|
@keydown="handleKeydown"
|
||||||
placeholder="发送消息..."
|
placeholder="发送消息..."
|
||||||
rows="1"
|
rows="1"
|
||||||
class="w-full bg-transparent text-white placeholder-white/25 py-4 pl-12 pr-28 resize-none focus:outline-none text-[15px]"
|
class="chat-input-textarea w-full bg-transparent text-white placeholder-white/25 py-4 pl-12 pr-28 resize-none focus:outline-none text-[15px]"
|
||||||
></textarea>
|
></textarea>
|
||||||
|
|
||||||
<!-- 发送按钮 -->
|
<!-- 发送按钮 -->
|
||||||
@@ -63,9 +63,10 @@ const autoResize = (e: Event) => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 提示 -->
|
<!-- 字符计数 -->
|
||||||
<div class="text-center mt-3">
|
<div class="flex justify-between items-center mt-2 px-1">
|
||||||
<span class="text-[10px] text-white/20 tracking-wide">AI 可能会产生错误信息,请核实重要内容</span>
|
<span class="text-[10px] text-white/20 tracking-wide">AI 可能会产生错误信息,请核实重要内容</span>
|
||||||
|
<span class="text-[10px] text-white/20">{{ modelValue.length }}/4000</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,16 +1,33 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { renderMarkdown, type ChatMessage, type Agent } from '@/composables/useChat'
|
import { ref } from 'vue'
|
||||||
|
import { renderMarkdown, type ChatMessage, type Agent } from '@/views/chat/chat'
|
||||||
|
|
||||||
defineProps<{
|
const props = defineProps<{
|
||||||
message: ChatMessage
|
message: ChatMessage
|
||||||
selectedAgent: Agent | null
|
selectedAgent: Agent | null
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const showActions = ref(false)
|
||||||
|
const copySuccess = ref(false)
|
||||||
|
|
||||||
// 创建消息内容的函数
|
// 创建消息内容的函数
|
||||||
const getMessageContent = (content: string, isUser: boolean) => {
|
const getMessageContent = (content: string, isUser: boolean) => {
|
||||||
if (isUser) return content
|
if (isUser) return content
|
||||||
return renderMarkdown(content)
|
return renderMarkdown(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 复制消息
|
||||||
|
const copyMessage = async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(props.message.content)
|
||||||
|
copySuccess.value = true
|
||||||
|
setTimeout(() => {
|
||||||
|
copySuccess.value = false
|
||||||
|
}, 2000)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to copy:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -32,16 +49,38 @@ const getMessageContent = (content: string, isUser: boolean) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 消息容器 -->
|
<!-- 消息容器 -->
|
||||||
<div class="flex flex-col max-w-[70%]" :class="message.role === 'user' ? 'items-end' : 'items-start'">
|
<div
|
||||||
|
class="flex flex-col max-w-[70%] group"
|
||||||
|
:class="message.role === 'user' ? 'items-end' : 'items-start'"
|
||||||
|
@mouseenter="showActions = true"
|
||||||
|
@mouseleave="showActions = false"
|
||||||
|
>
|
||||||
<!-- 消息气泡 -->
|
<!-- 消息气泡 -->
|
||||||
<div
|
<div
|
||||||
class="px-4 py-3 rounded-2xl text-[14px] leading-relaxed markdown-body"
|
class="px-4 py-3 rounded-2xl text-[14px] leading-relaxed markdown-body relative"
|
||||||
:class="message.role === 'user'
|
:class="message.role === 'user'
|
||||||
? 'bg-gradient-to-br from-orange-500 to-orange-600 text-white rounded-br-sm'
|
? 'bg-gradient-to-br from-orange-500 to-orange-600 text-white rounded-br-sm'
|
||||||
: 'bg-[#1e1e28] text-gray-100 rounded-bl-sm'"
|
: 'bg-[#1e1e28] text-gray-100 rounded-bl-sm'"
|
||||||
>
|
>
|
||||||
<span v-html="getMessageContent(message.content, message.role === 'user')"></span>
|
<span v-html="getMessageContent(message.content, message.role === 'user')"></span>
|
||||||
<span v-if="message.isStreaming" class="inline-block w-0.5 h-4 ml-0.5 bg-orange-300 cursor-blink align-middle"></span>
|
<span v-if="message.isStreaming" class="inline-block w-0.5 h-4 ml-0.5 bg-orange-300 cursor-blink align-middle"></span>
|
||||||
|
|
||||||
|
<!-- 复制按钮 -->
|
||||||
|
<Transition name="fade">
|
||||||
|
<button
|
||||||
|
v-if="showActions && message.content"
|
||||||
|
@click="copyMessage"
|
||||||
|
class="absolute -top-2 right-2 p-1.5 rounded-lg bg-[#2a2a35] hover:bg-[#3a3a45] text-white/60 hover:text-white transition-all duration-150 shadow-lg"
|
||||||
|
:title="copySuccess ? '已复制' : '复制'"
|
||||||
|
>
|
||||||
|
<svg v-if="!copySuccess" class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
|
||||||
|
</svg>
|
||||||
|
<svg v-else class="w-3.5 h-3.5 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
<!-- 时间戳 -->
|
<!-- 时间戳 -->
|
||||||
<span class="text-[11px] text-gray-500 mt-1 px-1">
|
<span class="text-[11px] text-gray-500 mt-1 px-1">
|
||||||
@@ -132,4 +171,14 @@ const getMessageContent = (content: string, isUser: boolean) => {
|
|||||||
.markdown-body :deep(th) {
|
.markdown-body :deep(th) {
|
||||||
background: rgba(0, 0, 0, 0.2);
|
background: rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Agent, ChatSession, GroupChat } from '@/composables/useChat'
|
import type { Agent, ChatSession, GroupChat } from '@/views/chat/chat'
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
collapsed: boolean
|
collapsed: boolean
|
||||||
@@ -13,6 +13,7 @@ const emit = defineEmits<{
|
|||||||
(e: 'openAgentSelector', mode: 'single' | 'group'): void
|
(e: 'openAgentSelector', mode: 'single' | 'group'): void
|
||||||
(e: 'selectAgent', agent: Agent): void
|
(e: 'selectAgent', agent: Agent): void
|
||||||
(e: 'selectSession', session: ChatSession): void
|
(e: 'selectSession', session: ChatSession): void
|
||||||
|
(e: 'selectGroup', group: GroupChat): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const formatRelativeTime = (date: Date) => {
|
const formatRelativeTime = (date: Date) => {
|
||||||
@@ -98,6 +99,7 @@ const formatRelativeTime = (date: Date) => {
|
|||||||
<button
|
<button
|
||||||
v-for="group in groupChats"
|
v-for="group in groupChats"
|
||||||
:key="group.id"
|
:key="group.id"
|
||||||
|
@click="emit('selectGroup', group)"
|
||||||
class="w-full text-left px-3 py-2.5 rounded-lg hover:bg-white/5 transition-all duration-200 group"
|
class="w-full text-left px-3 py-2.5 rounded-lg hover:bg-white/5 transition-all duration-200 group"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, nextTick, onMounted, onUnmounted } from 'vue'
|
import { ref, nextTick, onMounted, onUnmounted } from 'vue'
|
||||||
import { useChat } from '@/composables/useChat'
|
import { useChat } from './chat/chat'
|
||||||
import ChatHeader from '@/components/chat/ChatHeader.vue'
|
import ChatHeader from '@/components/chat/ChatHeader.vue'
|
||||||
import ChatMessage from '@/components/chat/ChatMessage.vue'
|
import ChatMessage from '@/components/chat/ChatMessage.vue'
|
||||||
import ChatInput from '@/components/chat/ChatInput.vue'
|
import ChatInput from '@/components/chat/ChatInput.vue'
|
||||||
import ChatSidebar from '@/components/chat/ChatSidebar.vue'
|
import ChatSidebar from '@/components/chat/ChatSidebar.vue'
|
||||||
import ChatAgentSelector from '@/components/chat/ChatAgentSelector.vue'
|
import ChatAgentSelector from '@/components/chat/ChatAgentSelector.vue'
|
||||||
|
import './chat/chat.css'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
chatModels,
|
chatModels,
|
||||||
@@ -15,6 +16,7 @@ const {
|
|||||||
selectedAgent,
|
selectedAgent,
|
||||||
messages,
|
messages,
|
||||||
chatSessions,
|
chatSessions,
|
||||||
|
currentSessionId,
|
||||||
groupChats,
|
groupChats,
|
||||||
showAgentSelector,
|
showAgentSelector,
|
||||||
selectMode,
|
selectMode,
|
||||||
@@ -29,9 +31,16 @@ const {
|
|||||||
confirmAgentSelection,
|
confirmAgentSelection,
|
||||||
cancelAgentSelection,
|
cancelAgentSelection,
|
||||||
selectAgent,
|
selectAgent,
|
||||||
|
selectGroup,
|
||||||
selectSession,
|
selectSession,
|
||||||
newChat,
|
newChat,
|
||||||
|
clearMessages,
|
||||||
toggleSidebar,
|
toggleSidebar,
|
||||||
|
createSession,
|
||||||
|
saveMessage,
|
||||||
|
deleteSession,
|
||||||
|
init,
|
||||||
|
cleanup,
|
||||||
} = useChat()
|
} = useChat()
|
||||||
|
|
||||||
const messagesContainer = ref<HTMLElement | null>(null)
|
const messagesContainer = ref<HTMLElement | null>(null)
|
||||||
@@ -74,6 +83,19 @@ const sendMessage = async () => {
|
|||||||
const userContent = inputMessage.value.trim()
|
const userContent = inputMessage.value.trim()
|
||||||
inputMessage.value = ''
|
inputMessage.value = ''
|
||||||
|
|
||||||
|
// 重置输入框高度
|
||||||
|
nextTick(() => {
|
||||||
|
const textarea = document.querySelector('.chat-input-textarea') as HTMLTextAreaElement
|
||||||
|
if (textarea) {
|
||||||
|
textarea.style.height = 'auto'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 如果没有会话,创建一个新会话
|
||||||
|
if (!currentSessionId.value) {
|
||||||
|
await createSession()
|
||||||
|
}
|
||||||
|
|
||||||
const userMessage = {
|
const userMessage = {
|
||||||
id: Date.now(),
|
id: Date.now(),
|
||||||
role: 'user' as const,
|
role: 'user' as const,
|
||||||
@@ -82,6 +104,9 @@ const sendMessage = async () => {
|
|||||||
}
|
}
|
||||||
messages.value.push(userMessage)
|
messages.value.push(userMessage)
|
||||||
|
|
||||||
|
// 保存用户消息到后端
|
||||||
|
await saveMessage('user', userContent)
|
||||||
|
|
||||||
const aiMessage = {
|
const aiMessage = {
|
||||||
id: Date.now() + 1,
|
id: Date.now() + 1,
|
||||||
role: 'assistant' as const,
|
role: 'assistant' as const,
|
||||||
@@ -95,23 +120,6 @@ const sendMessage = async () => {
|
|||||||
|
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
|
|
||||||
// ====== MOCK 模式:使用 mock 测试流式效果 ======
|
|
||||||
const USE_MOCK = false
|
|
||||||
|
|
||||||
if (USE_MOCK) {
|
|
||||||
const mockResponses = [
|
|
||||||
"你好!我是你的 AI 助手。",
|
|
||||||
"我收到了你的消息:\"" + userContent + "\"",
|
|
||||||
"这是一个模拟的流式响应效果。",
|
|
||||||
"现在你可以看到文字逐字出现的效果了!"
|
|
||||||
]
|
|
||||||
const fullResponse = mockResponses.join(' ')
|
|
||||||
const aiMessageIndex = messages.value.length - 1
|
|
||||||
await mockStreamResponse(fullResponse, aiMessageIndex)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// ====== MOCK 模式结束 ======
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const requestBody: any = {
|
const requestBody: any = {
|
||||||
agent_id: selectedAgent.value?.id || 1,
|
agent_id: selectedAgent.value?.id || 1,
|
||||||
@@ -122,7 +130,11 @@ const sendMessage = async () => {
|
|||||||
requestBody.model_id = selectedModel.value.id
|
requestBody.model_id = selectedModel.value.id
|
||||||
}
|
}
|
||||||
|
|
||||||
// 先获取完整响应,再逐字符显示(模拟流式效果)
|
// 传入 session_id
|
||||||
|
if (currentSessionId.value) {
|
||||||
|
requestBody.session_id = currentSessionId.value
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch(`/api/agent/chat/stream`, {
|
const response = await fetch(`/api/agent/chat/stream`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -135,11 +147,11 @@ const sendMessage = async () => {
|
|||||||
throw new Error(`Request failed: ${response.status}`)
|
throw new Error(`Request failed: ${response.status}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 读取完整响应
|
// 真正的流式处理:边读取边显示
|
||||||
const reader = response.body.getReader()
|
const reader = response.body.getReader()
|
||||||
const decoder = new TextDecoder()
|
const decoder = new TextDecoder()
|
||||||
let buffer = ''
|
let buffer = ''
|
||||||
let fullText = ''
|
const aiMessageIndex = messages.value.length - 1
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const { done, value } = await reader.read()
|
const { done, value } = await reader.read()
|
||||||
@@ -152,34 +164,31 @@ const sendMessage = async () => {
|
|||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
if (line.startsWith('data: ')) {
|
if (line.startsWith('data: ')) {
|
||||||
const data = line.slice(6)
|
const data = line.slice(6).trim()
|
||||||
if (data && data !== '[DONE]') {
|
if (data && data !== '[DONE]') {
|
||||||
fullText += data
|
// 直接累加内容并显示(真正的流式)
|
||||||
}
|
messages.value[aiMessageIndex].content += data
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 逐字符显示(模拟流式),每3个字符更新一次UI,减少重绘
|
|
||||||
const aiMessageIndex = messages.value.length - 1
|
|
||||||
messages.value[aiMessageIndex].content = ''
|
|
||||||
let tempText = ''
|
|
||||||
|
|
||||||
for (let i = 0; i < fullText.length; i++) {
|
|
||||||
tempText += fullText[i]
|
|
||||||
// 每3个字符更新一次UI,减少重绘次数
|
|
||||||
if ((i + 1) % 3 === 0 || i === fullText.length - 1) {
|
|
||||||
messages.value[aiMessageIndex].content = tempText
|
|
||||||
await nextTick()
|
await nextTick()
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
// 添加小延迟,让动画更自然
|
}
|
||||||
await new Promise(r => setTimeout(r, 8))
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理剩余buffer中的数据
|
||||||
|
if (buffer.startsWith('data: ')) {
|
||||||
|
const data = buffer.slice(6).trim()
|
||||||
|
if (data && data !== '[DONE]') {
|
||||||
|
messages.value[aiMessageIndex].content += data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
messages.value[aiMessageIndex].isStreaming = false
|
messages.value[aiMessageIndex].isStreaming = false
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
|
|
||||||
|
// 保存 AI 消息到后端
|
||||||
|
await saveMessage('assistant', messages.value[aiMessageIndex].content)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[Stream] 错误:', error)
|
console.error('[Stream] 错误:', error)
|
||||||
const errorIndex = messages.value.findIndex(m => m.isStreaming)
|
const errorIndex = messages.value.findIndex(m => m.isStreaming)
|
||||||
@@ -190,6 +199,16 @@ const sendMessage = async () => {
|
|||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
onMounted(() => {
|
||||||
|
console.log('[Chat] Component mounted, calling init()')
|
||||||
|
init()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
cleanup()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -206,11 +225,22 @@ const sendMessage = async () => {
|
|||||||
@toggle-dropdown="toggleModelDropdown"
|
@toggle-dropdown="toggleModelDropdown"
|
||||||
@select-model="handleSelectModel"
|
@select-model="handleSelectModel"
|
||||||
@toggle-sidebar="toggleSidebar"
|
@toggle-sidebar="toggleSidebar"
|
||||||
|
@clear-chat="clearMessages"
|
||||||
|
@new-chat="newChat"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 消息区域 -->
|
<!-- 消息区域 -->
|
||||||
<div ref="messagesContainer" class="flex-1 overflow-y-auto py-4">
|
<div ref="messagesContainer" class="flex-1 overflow-y-auto py-4">
|
||||||
<div class="px-6">
|
<!-- 空状态欢迎提示 -->
|
||||||
|
<div v-if="messages.length === 0" class="h-full flex items-center justify-center">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-5xl mb-4">{{ selectedAgent?.avatar || '🧠' }}</div>
|
||||||
|
<h2 class="text-xl font-semibold text-white mb-2">和 {{ selectedAgent?.name || 'AI' }} 开始对话</h2>
|
||||||
|
<p class="text-white/40 text-sm">发送消息开始聊天</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 消息列表 -->
|
||||||
|
<div v-else class="px-6">
|
||||||
<ChatMessage
|
<ChatMessage
|
||||||
v-for="message in messages"
|
v-for="message in messages"
|
||||||
:key="message.id"
|
:key="message.id"
|
||||||
@@ -238,6 +268,7 @@ const sendMessage = async () => {
|
|||||||
@open-agent-selector="openAgentSelector"
|
@open-agent-selector="openAgentSelector"
|
||||||
@select-agent="selectAgent"
|
@select-agent="selectAgent"
|
||||||
@select-session="selectSession"
|
@select-session="selectSession"
|
||||||
|
@select-group="selectGroup"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 智能体选择弹窗 -->
|
<!-- 智能体选择弹窗 -->
|
||||||
@@ -254,55 +285,3 @@ const sendMessage = async () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background: rgba(255, 255, 255, 0.08);
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes messageSlideIn {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(16px) scale(0.96);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0) scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-enter {
|
|
||||||
animation: messageSlideIn 0.4s cubic-bezier(0.22, 1, 0.36, 1) forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes blink {
|
|
||||||
0%, 100% { opacity: 1; }
|
|
||||||
50% { opacity: 0; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.cursor-blink {
|
|
||||||
animation: blink 1s step-end infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse-glow {
|
|
||||||
0%, 100% { box-shadow: 0 0 0 0 rgba(249, 115, 22, 0.4); }
|
|
||||||
50% { box-shadow: 0 0 20px 4px rgba(249, 115, 22, 0.2); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.agent-glow {
|
|
||||||
animation: pulse-glow 2s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
51
web/src/views/chat/chat.css
Normal file
51
web/src/views/chat/chat.css
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
/* Chat 页面样式 */
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes messageSlideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(16px) scale(0.96);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-enter {
|
||||||
|
animation: messageSlideIn 0.4s cubic-bezier(0.22, 1, 0.36, 1) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blink {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.cursor-blink {
|
||||||
|
animation: blink 1s step-end infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-glow {
|
||||||
|
0%, 100% { box-shadow: 0 0 0 0 rgba(249, 115, 22, 0.4); }
|
||||||
|
50% { box-shadow: 0 0 20px 4px rgba(249, 115, 22, 0.2); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-glow {
|
||||||
|
animation: pulse-glow 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
591
web/src/views/chat/chat.ts
Normal file
591
web/src/views/chat/chat.ts
Normal file
@@ -0,0 +1,591 @@
|
|||||||
|
import { ref, nextTick, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { marked } from 'marked'
|
||||||
|
|
||||||
|
// 类型定义
|
||||||
|
export interface ChatModel {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
model_type: string
|
||||||
|
provider: string
|
||||||
|
model: string
|
||||||
|
status: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatMessage {
|
||||||
|
id: number
|
||||||
|
role: 'user' | 'assistant'
|
||||||
|
content: string
|
||||||
|
timestamp: Date
|
||||||
|
isStreaming?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Agent {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
avatar: string
|
||||||
|
description: string
|
||||||
|
accentColor: string
|
||||||
|
gradient: string
|
||||||
|
status: 'online' | 'offline'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatSession {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
agent_id: number
|
||||||
|
model_id?: string
|
||||||
|
last_message?: string
|
||||||
|
timestamp: Date
|
||||||
|
status?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GroupChat {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
members: string[]
|
||||||
|
lastMessage: string
|
||||||
|
timestamp: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
// 配置 marked
|
||||||
|
marked.setOptions({
|
||||||
|
breaks: true,
|
||||||
|
gfm: true
|
||||||
|
})
|
||||||
|
|
||||||
|
// 预处理内容:修复一些常见的 Markdown 问题
|
||||||
|
const preprocessContent = (content: string): string => {
|
||||||
|
if (!content) return ''
|
||||||
|
|
||||||
|
// 1. 标题:# 标题 -> # 标题(确保 # 后有空格)
|
||||||
|
content = content.replace(/(^|\n)(#{1,6})([^\s#\n])/g, '$1$2 $3')
|
||||||
|
|
||||||
|
// 2. 无序列表:-项目 -> - 项目
|
||||||
|
content = content.replace(/(\n)(\s*)([-*+])(\S)/g, '$1$2$3 $4')
|
||||||
|
|
||||||
|
// 3. 有序列表:1.项目 -> 1. 项目
|
||||||
|
content = content.replace(/(\n)(\s*)(\d+\.)(\S)/g, '$1$2$3 $4')
|
||||||
|
|
||||||
|
// 4. 引用:>引用 -> > 引用
|
||||||
|
content = content.replace(/(\n)(>+)([^\s>\n])/g, '$1$2 $4')
|
||||||
|
|
||||||
|
// 5. 修复 ##1. 这种情况(连续处理)
|
||||||
|
content = content.replace(/#{1,6}\d+\./g, match => match.replace(/\d+\./, ' '))
|
||||||
|
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染 Markdown
|
||||||
|
export const renderMarkdown = (content: string): string => {
|
||||||
|
if (!content) return ''
|
||||||
|
try {
|
||||||
|
const processed = preprocessContent(content)
|
||||||
|
return marked.parse(processed) as string
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Markdown parse error:', e)
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据 provider 获取图标
|
||||||
|
export const getModelIcon = (provider: string) => {
|
||||||
|
const icons: Record<string, string> = {
|
||||||
|
'OpenAI': '🤖',
|
||||||
|
'Claude': '🧠',
|
||||||
|
'Google': '✨',
|
||||||
|
'Gemini': '✨',
|
||||||
|
'Ollama': '🦙',
|
||||||
|
'DeepSeek': '🔮',
|
||||||
|
'Moonshot': '🌙',
|
||||||
|
'Kimi': '🌙',
|
||||||
|
'Baidu': '🐉',
|
||||||
|
'文心一言': '🐉',
|
||||||
|
'Aliyun': '☁️',
|
||||||
|
'Ali': '☁️',
|
||||||
|
'通义千问': '☁️',
|
||||||
|
'Azure': '⬛',
|
||||||
|
'Anthropic': '🧠',
|
||||||
|
}
|
||||||
|
return icons[provider] || '💬'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建 composable
|
||||||
|
export function useChat() {
|
||||||
|
// 模型相关状态
|
||||||
|
const chatModels = ref<ChatModel[]>([])
|
||||||
|
const selectedModel = ref<ChatModel | null>(null)
|
||||||
|
const modelsLoading = ref(false)
|
||||||
|
const showModelDropdown = ref(false)
|
||||||
|
|
||||||
|
// 助手相关状态
|
||||||
|
const chatAgents = ref<Agent[]>([])
|
||||||
|
const selectedAgent = ref<Agent | null>(null)
|
||||||
|
|
||||||
|
// 消息相关状态
|
||||||
|
const messages = ref<ChatMessage[]>([])
|
||||||
|
|
||||||
|
// 历史对话
|
||||||
|
const chatSessions = ref<ChatSession[]>([])
|
||||||
|
const currentSessionId = ref<string | null>(null)
|
||||||
|
|
||||||
|
// 群聊
|
||||||
|
const groupChats = ref<GroupChat[]>([])
|
||||||
|
|
||||||
|
// 智能体选择弹窗
|
||||||
|
const showAgentSelector = ref(false)
|
||||||
|
const selectMode = ref<'single' | 'group'>('single')
|
||||||
|
const selectedAgents = ref<Agent[]>([])
|
||||||
|
const groupChatName = ref('')
|
||||||
|
|
||||||
|
// 输入相关
|
||||||
|
const inputMessage = ref('')
|
||||||
|
const isLoading = ref(false)
|
||||||
|
|
||||||
|
// 侧边栏
|
||||||
|
const sidebarCollapsed = ref(false)
|
||||||
|
|
||||||
|
// 获取模型列表(只获取 active 的 chat 模型)
|
||||||
|
const fetchModels = async () => {
|
||||||
|
modelsLoading.value = true
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/model/list`)
|
||||||
|
const data = await response.json()
|
||||||
|
console.log('[Chat] Raw models:', data.list)
|
||||||
|
|
||||||
|
if (data.list) {
|
||||||
|
// 只过滤出 active 的 chat 模型 (status: 1=active, 0=inactive)
|
||||||
|
const activeChatModels = data.list.filter((m: ChatModel) =>
|
||||||
|
m.model_type === 'chat' && m.status === 1
|
||||||
|
)
|
||||||
|
console.log('[Chat] Filtered chat models:', activeChatModels)
|
||||||
|
chatModels.value = activeChatModels
|
||||||
|
|
||||||
|
// 默认选中第一个 active 的 chat 模型
|
||||||
|
if (chatModels.value.length > 0) {
|
||||||
|
selectedModel.value = chatModels.value[0]
|
||||||
|
console.log('[Chat] Selected model:', selectedModel.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch models:', error)
|
||||||
|
} finally {
|
||||||
|
modelsLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取智能体列表
|
||||||
|
const fetchAgents = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/agent/list')
|
||||||
|
const data = await response.json()
|
||||||
|
console.log('[Chat] Agents:', data)
|
||||||
|
|
||||||
|
if (data.agents) {
|
||||||
|
chatAgents.value = data.agents.map((agent: any) => ({
|
||||||
|
id: agent.id,
|
||||||
|
name: agent.name,
|
||||||
|
avatar: agent.avatar || '🧠',
|
||||||
|
description: agent.description || '',
|
||||||
|
accentColor: agent.accent_color || '#f97316',
|
||||||
|
gradient: 'from-orange-500/20 to-amber-500/20',
|
||||||
|
status: agent.status === 'active' ? 'online' : 'offline'
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 默认选中第一个智能体
|
||||||
|
if (chatAgents.value.length > 0 && !selectedAgent.value) {
|
||||||
|
selectedAgent.value = chatAgents.value[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch agents:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取会话列表
|
||||||
|
const fetchSessions = async () => {
|
||||||
|
try {
|
||||||
|
const userId = localStorage.getItem('user_id') || 'default-user'
|
||||||
|
const response = await fetch(`/api/chat/sessions?user_id=${userId}&limit=50`)
|
||||||
|
const data = await response.json()
|
||||||
|
console.log('[Chat] Sessions:', data)
|
||||||
|
|
||||||
|
if (data.list) {
|
||||||
|
chatSessions.value = data.list.map((s: any) => ({
|
||||||
|
id: s.id,
|
||||||
|
title: s.title || '新会话',
|
||||||
|
agent_id: s.agent_id,
|
||||||
|
model_id: s.model_id,
|
||||||
|
last_message: s.last_message,
|
||||||
|
timestamp: new Date(s.created_at || Date.now()),
|
||||||
|
status: s.status
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch sessions:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取群聊列表
|
||||||
|
const fetchGroups = async () => {
|
||||||
|
try {
|
||||||
|
const userId = localStorage.getItem('user_id') || 'default-user'
|
||||||
|
const response = await fetch(`/api/chat/groups?user_id=${userId}`)
|
||||||
|
const data = await response.json()
|
||||||
|
console.log('[Chat] Groups:', data)
|
||||||
|
|
||||||
|
if (data.list) {
|
||||||
|
groupChats.value = data.list.map((g: any) => ({
|
||||||
|
id: g.id,
|
||||||
|
name: g.name,
|
||||||
|
members: g.agent_ids ? JSON.parse(g.agent_ids) : [],
|
||||||
|
lastMessage: g.description || '',
|
||||||
|
timestamp: new Date(g.created_at || Date.now())
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch groups:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建群聊
|
||||||
|
const createGroup = async (name: string, agentIds: number[]) => {
|
||||||
|
try {
|
||||||
|
const userId = localStorage.getItem('user_id') || 'default-user'
|
||||||
|
const response = await fetch('/api/chat/groups', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
user_id: userId,
|
||||||
|
name: name,
|
||||||
|
agent_ids: JSON.stringify(agentIds)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
const group = await response.json()
|
||||||
|
console.log('[Chat] Created group:', group)
|
||||||
|
|
||||||
|
// 添加到群聊列表
|
||||||
|
groupChats.value.unshift({
|
||||||
|
id: group.id,
|
||||||
|
name: group.name,
|
||||||
|
members: agentIds.map(id => String(id)),
|
||||||
|
lastMessage: '',
|
||||||
|
timestamp: new Date()
|
||||||
|
})
|
||||||
|
|
||||||
|
return group
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create group:', error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新会话
|
||||||
|
const createSession = async (title: string = '新会话') => {
|
||||||
|
try {
|
||||||
|
const userId = localStorage.getItem('user_id') || 'default-user'
|
||||||
|
const response = await fetch('/api/chat/sessions', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
user_id: userId,
|
||||||
|
agent_id: selectedAgent.value?.id || 1,
|
||||||
|
title: title,
|
||||||
|
model_id: selectedModel.value?.id
|
||||||
|
})
|
||||||
|
})
|
||||||
|
const session = await response.json()
|
||||||
|
console.log('[Chat] Created session:', session)
|
||||||
|
|
||||||
|
// 添加到会话列表
|
||||||
|
chatSessions.value.unshift({
|
||||||
|
id: session.id,
|
||||||
|
title: session.title || title,
|
||||||
|
agent_id: session.agent_id,
|
||||||
|
model_id: session.model_id,
|
||||||
|
timestamp: new Date()
|
||||||
|
})
|
||||||
|
|
||||||
|
currentSessionId.value = session.id
|
||||||
|
return session
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create session:', error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取会话历史消息
|
||||||
|
const fetchSessionMessages = async (sessionId: string) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/chat/sessions/${sessionId}/messages?limit=100`)
|
||||||
|
const data = await response.json()
|
||||||
|
console.log('[Chat] Messages:', data)
|
||||||
|
|
||||||
|
if (data.list) {
|
||||||
|
messages.value = data.list.map((m: any) => ({
|
||||||
|
id: m.id,
|
||||||
|
role: m.role as 'user' | 'assistant',
|
||||||
|
content: m.content,
|
||||||
|
timestamp: new Date(m.created_at)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch messages:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存消息到后端
|
||||||
|
const saveMessage = async (role: 'user' | 'assistant', content: string) => {
|
||||||
|
if (!currentSessionId.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fetch('/api/chat/messages', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
session_id: currentSessionId.value,
|
||||||
|
role: role,
|
||||||
|
content: content
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save message:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新会话标题
|
||||||
|
const updateSessionTitle = async (sessionId: string, title: string) => {
|
||||||
|
try {
|
||||||
|
await fetch(`/api/chat/sessions/${sessionId}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ title })
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update session:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除会话
|
||||||
|
const deleteSession = async (sessionId: string) => {
|
||||||
|
try {
|
||||||
|
await fetch(`/api/chat/sessions/${sessionId}`, { method: 'DELETE' })
|
||||||
|
chatSessions.value = chatSessions.value.filter(s => s.id !== sessionId)
|
||||||
|
|
||||||
|
// 如果删除的是当前会话,清空消息
|
||||||
|
if (currentSessionId.value === sessionId) {
|
||||||
|
currentSessionId.value = null
|
||||||
|
messages.value = []
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to delete session:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开智能体选择器
|
||||||
|
const openAgentSelector = (mode: 'single' | 'group') => {
|
||||||
|
selectMode.value = mode
|
||||||
|
selectedAgents.value = []
|
||||||
|
groupChatName.value = ''
|
||||||
|
showAgentSelector.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换智能体选择
|
||||||
|
const toggleAgentSelection = (agent: Agent) => {
|
||||||
|
if (selectMode.value === 'single') {
|
||||||
|
// 单选模式:直接设置为只有一个
|
||||||
|
selectedAgents.value = [agent]
|
||||||
|
} else {
|
||||||
|
// 群聊模式:切换选择状态
|
||||||
|
const index = selectedAgents.value.findIndex(a => a.id === agent.id)
|
||||||
|
if (index > -1) {
|
||||||
|
selectedAgents.value.splice(index, 1)
|
||||||
|
} else {
|
||||||
|
selectedAgents.value.push(agent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认选择
|
||||||
|
const confirmAgentSelection = async () => {
|
||||||
|
if (selectMode.value === 'single') {
|
||||||
|
if (selectedAgents.value.length > 0) {
|
||||||
|
selectedAgent.value = selectedAgents.value[0]
|
||||||
|
|
||||||
|
// 创建新会话
|
||||||
|
const session = await createSession()
|
||||||
|
if (session) {
|
||||||
|
currentSessionId.value = session.id
|
||||||
|
}
|
||||||
|
|
||||||
|
messages.value = [
|
||||||
|
{ id: Date.now(), role: 'assistant', content: `你好!我是 ${selectedAgent.value.name},你的 AI 助手。有什么我可以帮助你的吗?`, timestamp: new Date() }
|
||||||
|
]
|
||||||
|
|
||||||
|
// 保存助手欢迎消息
|
||||||
|
if (currentSessionId.value) {
|
||||||
|
await saveMessage('assistant', messages.value[0].content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const name = groupChatName.value.trim() || `群聊 (${selectedAgents.value.length}人)`
|
||||||
|
const agentIds = selectedAgents.value.map(a => a.id)
|
||||||
|
|
||||||
|
// 调用后端 API 创建群聊
|
||||||
|
const group = await createGroup(name, agentIds)
|
||||||
|
|
||||||
|
if (!group) {
|
||||||
|
// 如果 API 调用失败,使用本地数据
|
||||||
|
groupChats.value.unshift({
|
||||||
|
id: Date.now(),
|
||||||
|
name: name,
|
||||||
|
members: selectedAgents.value.map(a => a.name),
|
||||||
|
lastMessage: 'New group created',
|
||||||
|
timestamp: new Date()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
showAgentSelector.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消选择
|
||||||
|
const cancelAgentSelection = () => {
|
||||||
|
showAgentSelector.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择助手
|
||||||
|
const selectAgent = (agent: Agent) => {
|
||||||
|
selectedAgent.value = agent
|
||||||
|
messages.value = [
|
||||||
|
{ id: 1, role: 'assistant', content: `你好!我是 ${agent.name}。有什么我可以帮助你的吗?`, timestamp: new Date() }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择群聊
|
||||||
|
const selectGroup = (group: GroupChat) => {
|
||||||
|
messages.value = [
|
||||||
|
{ id: 1, role: 'assistant', content: `你好!欢迎进入群聊 "${group.name}",${group.members.length} 位智能体已加入。`, timestamp: new Date() }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择历史对话
|
||||||
|
const selectSession = async (session: ChatSession) => {
|
||||||
|
const agent = chatAgents.value.find(a => a.id === session.agent_id)
|
||||||
|
if (agent) {
|
||||||
|
selectedAgent.value = agent
|
||||||
|
}
|
||||||
|
|
||||||
|
currentSessionId.value = session.id
|
||||||
|
await fetchSessionMessages(session.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新建聊天 - 先打开智能体选择器
|
||||||
|
const newChat = () => {
|
||||||
|
// 打开智能体选择器,让用户选择智能体
|
||||||
|
openAgentSelector('single')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空对话
|
||||||
|
const clearMessages = () => {
|
||||||
|
messages.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化时间
|
||||||
|
const formatTime = (date: Date) => {
|
||||||
|
return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化相对时间
|
||||||
|
const formatRelativeTime = (date: Date) => {
|
||||||
|
const now = new Date()
|
||||||
|
const diff = now.getTime() - date.getTime()
|
||||||
|
const hours = Math.floor(diff / 3600000)
|
||||||
|
const days = Math.floor(diff / 86400000)
|
||||||
|
|
||||||
|
if (hours < 1) return '刚刚'
|
||||||
|
if (hours < 24) return `${hours}小时前`
|
||||||
|
if (days < 7) return `${days}天前`
|
||||||
|
return date.toLocaleDateString('zh-CN')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换侧边栏
|
||||||
|
const toggleSidebar = () => {
|
||||||
|
sidebarCollapsed.value = !sidebarCollapsed.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击外部关闭下拉框
|
||||||
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
|
const target = e.target as HTMLElement
|
||||||
|
if (!target.closest('.model-dropdown')) {
|
||||||
|
showModelDropdown.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
const init = () => {
|
||||||
|
fetchModels()
|
||||||
|
fetchAgents()
|
||||||
|
fetchSessions()
|
||||||
|
fetchGroups()
|
||||||
|
document.addEventListener('click', handleClickOutside)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理
|
||||||
|
const cleanup = () => {
|
||||||
|
document.removeEventListener('click', handleClickOutside)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 模型
|
||||||
|
chatModels,
|
||||||
|
selectedModel,
|
||||||
|
modelsLoading,
|
||||||
|
showModelDropdown,
|
||||||
|
fetchModels,
|
||||||
|
fetchAgents,
|
||||||
|
// 助手
|
||||||
|
chatAgents,
|
||||||
|
selectedAgent,
|
||||||
|
selectAgent,
|
||||||
|
selectGroup,
|
||||||
|
// 消息
|
||||||
|
messages,
|
||||||
|
newChat,
|
||||||
|
clearMessages,
|
||||||
|
// 历史对话
|
||||||
|
chatSessions,
|
||||||
|
currentSessionId,
|
||||||
|
selectSession,
|
||||||
|
fetchSessions,
|
||||||
|
createSession,
|
||||||
|
fetchSessionMessages,
|
||||||
|
saveMessage,
|
||||||
|
updateSessionTitle,
|
||||||
|
deleteSession,
|
||||||
|
// 群聊
|
||||||
|
groupChats,
|
||||||
|
fetchGroups,
|
||||||
|
createGroup,
|
||||||
|
// 智能体选择
|
||||||
|
showAgentSelector,
|
||||||
|
selectMode,
|
||||||
|
selectedAgents,
|
||||||
|
groupChatName,
|
||||||
|
openAgentSelector,
|
||||||
|
toggleAgentSelection,
|
||||||
|
confirmAgentSelection,
|
||||||
|
cancelAgentSelection,
|
||||||
|
// 输入
|
||||||
|
inputMessage,
|
||||||
|
isLoading,
|
||||||
|
// 侧边栏
|
||||||
|
sidebarCollapsed,
|
||||||
|
toggleSidebar,
|
||||||
|
// 工具
|
||||||
|
formatTime,
|
||||||
|
formatRelativeTime,
|
||||||
|
getModelIcon,
|
||||||
|
// 生命周期
|
||||||
|
init,
|
||||||
|
cleanup,
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user