Files
X-Agents/web/src/views/Chat.vue
DESKTOP-72TV0V4\caoxiaozhu d9484f16c7 refactor: 简化 Chat 页面移除推荐智能体模块
- 移除 selectAgentAndCreateSession 方法
- 移除推荐智能体卡片区域
- 精简页面代码

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 21:44:20 +08:00

375 lines
9.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ref, nextTick, watch, onMounted, onUnmounted } from 'vue'
import { ElMessage } from 'element-plus'
import { useChat } from './chat/chat'
import ChatHeader from '@/components/chat/ChatHeader.vue'
import ChatMessage from '@/components/chat/ChatMessage.vue'
import ChatInput from '@/components/chat/ChatInput.vue'
import ChatSidebar from '@/components/chat/ChatSidebar.vue'
import ChatAgentSelector from '@/components/chat/ChatAgentSelector.vue'
import './chat/chat.css'
const {
chatModels,
selectedModel,
showModelDropdown,
chatAgents,
selectedAgent,
messages,
chatSessions,
currentSessionId,
groupChats,
showAgentSelector,
selectMode,
selectedAgents,
groupChatName,
inputMessage,
isLoading,
sidebarCollapsed,
fetchModels,
openAgentSelector,
toggleAgentSelection,
confirmAgentSelection,
cancelAgentSelection,
selectAgent,
selectGroup,
selectSession,
newChat,
clearMessages,
toggleSidebar,
createSession,
saveMessage,
deleteSession,
init,
cleanup,
} = useChat()
const messagesContainer = ref<HTMLElement | null>(null)
// 构建 API 请求体
const buildRequestBody = (userContent: string) => {
const requestBody: any = {
agent_id: String(selectedAgent.value?.id || 1),
message: userContent,
}
if (selectedModel.value) {
requestBody.model_id = selectedModel.value.id
}
if (currentSessionId.value) {
requestBody.session_id = currentSessionId.value
}
return requestBody
}
// 解析流式响应数据
const parseStreamData = (rawData: string): string => {
if (!rawData || rawData === '[DONE]') return ''
try {
const parsed = JSON.parse(rawData)
if (typeof parsed === 'string') {
return parsed
}
return parsed.content || parsed.delta?.content || ''
} catch {
return ''
}
}
// 处理流式响应
const handleStreamResponse = async (response: Response) => {
const reader = response.body.getReader()
const decoder = new TextDecoder('utf-8')
let buffer = ''
const aiMessageIndex = messages.value.length - 1
while (true) {
const { done, value } = await reader.read()
if (done) break
const decoded = decoder.decode(value, { stream: true })
buffer += decoded
const lines = buffer.split('\n')
buffer = lines.pop() || ''
for (const line of lines) {
if (line.startsWith('data: ')) {
const content = parseStreamData(line.slice(6).trim())
if (content) {
messages.value[aiMessageIndex].content += content
await nextTick()
scrollToBottom()
}
}
}
}
// 处理剩余buffer
if (buffer.startsWith('data: ')) {
const content = parseStreamData(buffer.slice(6).trim())
if (content) {
messages.value[aiMessageIndex].content += content
}
}
messages.value[aiMessageIndex].isStreaming = false
isLoading.value = false
scrollToBottom()
// 保存 AI 消息
await saveMessage('assistant', messages.value[aiMessageIndex].content)
// 第二轮对话结束后生成标题
if (messages.value.length === 5 && currentSessionId.value) {
generateSessionTitle()
}
}
// 处理消息发送错误
const handleMessageError = (error: any) => {
const errorIndex = messages.value.findIndex(m => m.isStreaming)
if (errorIndex > -1) {
messages.value[errorIndex].content = `Error: ${error.message || 'Failed to send message'}`
messages.value[errorIndex].isStreaming = false
}
isLoading.value = false
}
// 重置输入框
const resetInputHeight = () => {
nextTick(() => {
const textarea = document.querySelector('.chat-input-textarea') as HTMLTextAreaElement
if (textarea) {
textarea.style.height = 'auto'
}
})
}
// 创建用户消息对象
const createUserMessage = (content: string) => ({
id: Date.now(),
role: 'user' as const,
content,
timestamp: new Date()
})
// 创建 AI 消息对象
const createAssistantMessage = () => ({
id: Date.now() + 1,
role: 'assistant' as const,
content: '',
timestamp: new Date(),
isStreaming: true
})
// 滚动到底部
const scrollToBottom = () => {
if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
}
}
// 监听消息变化,自动滚动到底部
watch(messages, () => {
nextTick(() => scrollToBottom())
}, { deep: true })
// 切换模型下拉框
const toggleModelDropdown = () => {
showModelDropdown.value = !showModelDropdown.value
}
// 选择模型
const handleSelectModel = (model: any) => {
selectedModel.value = model
showModelDropdown.value = false
}
// 删除会话
const handleDeleteSession = async (session: any) => {
try {
const response = await fetch(`/api/chat/sessions/${session.id}`, {
method: 'DELETE',
})
if (response.ok) {
const index = chatSessions.value.findIndex((s: any) => s.id === session.id)
if (index > -1) {
chatSessions.value.splice(index, 1)
}
if (currentSessionId.value === session.id) {
currentSessionId.value = null
messages.value = []
}
}
} catch {
// ignore
}
}
// 生成会话标题
const generateSessionTitle = async () => {
if (!currentSessionId.value) return
try {
const response = await fetch('/api/chat/sessions/generate-title', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: currentSessionId.value })
})
if (response.ok) {
const data = await response.json()
const sessionIndex = chatSessions.value.findIndex((s: any) => s.id === currentSessionId.value)
if (sessionIndex > -1) {
chatSessions.value[sessionIndex].title = data.title
}
}
} catch {
// ignore
}
}
// 发送消息
const sendMessage = async () => {
if (!inputMessage.value.trim() || isLoading.value) return
// 如果没有会话,提示用户先选择智能体
if (!currentSessionId.value) {
ElMessage.warning('请先选择或创建一个会话')
return
}
const userContent = inputMessage.value.trim()
inputMessage.value = ''
resetInputHeight()
const userMessage = createUserMessage(userContent)
messages.value.push(userMessage)
await saveMessage('user', userContent)
const aiMessage = createAssistantMessage()
messages.value.push(aiMessage)
nextTick(() => scrollToBottom())
isLoading.value = true
try {
const response = await fetch('/api/agent/chat/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(buildRequestBody(userContent)),
})
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`)
}
await handleStreamResponse(response)
} catch (error: any) {
handleMessageError(error)
}
}
// 初始化
onMounted(() => {
init()
})
onUnmounted(() => {
cleanup()
})
</script>
<template>
<div class="h-screen flex bg-[#09090b]">
<!-- 主聊天区域 -->
<div class="flex-1 flex flex-col bg-[#09090b]">
<!-- 顶部栏 -->
<ChatHeader
v-if="currentSessionId"
:selected-agent="selectedAgent"
:chat-models="chatModels"
:selected-model="selectedModel"
:show-model-dropdown="showModelDropdown"
:sidebar-collapsed="sidebarCollapsed"
@toggle-dropdown="toggleModelDropdown"
@select-model="handleSelectModel"
@toggle-sidebar="toggleSidebar"
@clear-chat="clearMessages"
@new-chat="newChat"
/>
<!-- 消息区域 -->
<div ref="messagesContainer" class="flex-1 overflow-y-auto py-4">
<!-- 无会话时显示引导界面 -->
<div v-if="!currentSessionId" class="h-full flex items-center justify-center empty-chat">
<div class="text-center" style="position: relative; z-index: 1;">
<div class="empty-logo">🧠</div>
<h2 class="empty-title">欢迎使用 X-Agents</h2>
<p class="empty-desc">与智能 AI 助手对话获取专业解答与创意灵感</p>
<button @click="newChat" class="empty-btn">
<i class="fa-solid fa-plus mr-2"></i>
开始新对话
</button>
</div>
</div>
<!-- 有会话但无消息时显示欢迎提示 -->
<div v-else-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
v-for="message in messages"
:key="message.id"
:message="message"
:selected-agent="selectedAgent"
/>
</div>
</div>
<!-- 输入区域 - 仅在有会话时显示 -->
<ChatInput
v-if="currentSessionId"
v-model="inputMessage"
:loading="isLoading"
@send="sendMessage"
/>
</div>
<!-- 右侧边栏 -->
<ChatSidebar
:collapsed="sidebarCollapsed"
:chat-agents="chatAgents"
:selected-agent="selectedAgent"
:chat-sessions="chatSessions"
:group-chats="groupChats"
@open-agent-selector="openAgentSelector"
@select-agent="selectAgent"
@select-session="selectSession"
@select-group="selectGroup"
@delete-session="handleDeleteSession"
/>
<!-- 智能体选择弹窗 -->
<ChatAgentSelector
:show="showAgentSelector"
:select-mode="selectMode"
:chat-agents="chatAgents"
:selected-agents="selectedAgents"
:group-chat-name="groupChatName"
@close="cancelAgentSelection"
@toggle-select="toggleAgentSelection"
@confirm="confirmAgentSelection"
@update:group-chat-name="groupChatName = $event"
/>
</div>
</template>