feat: 优化 Chat 页面和 Agents 页面
- 优化 Chat 页面交互和消息显示 - 增强 Agents 页面功能 - 改进 ChatAgentSelector 组件 - 优化 ChatMessage 和 ChatSidebar 组件 - 更新聊天逻辑 useAgents 和 chat 模块 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick, onMounted, onUnmounted } from 'vue'
|
||||
import { ref, nextTick, watch, onMounted, onUnmounted } from 'vue'
|
||||
import { useChat } from './chat/chat'
|
||||
import ChatHeader from '@/components/chat/ChatHeader.vue'
|
||||
import ChatMessage from '@/components/chat/ChatMessage.vue'
|
||||
@@ -45,19 +45,126 @@ const {
|
||||
|
||||
const messagesContainer = ref<HTMLElement | null>(null)
|
||||
|
||||
// Mock 流式响应(用于测试前端流式效果)
|
||||
const mockStreamResponse = async (content: string, messageIndex: number) => {
|
||||
const chars = content.split('')
|
||||
for (let i = 0; i < chars.length; i++) {
|
||||
messages.value[messageIndex].content += chars[i]
|
||||
await nextTick()
|
||||
scrollToBottom()
|
||||
await new Promise(resolve => setTimeout(resolve, 15))
|
||||
// 构建 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
|
||||
}
|
||||
messages.value[messageIndex].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) {
|
||||
@@ -65,6 +172,11 @@ const scrollToBottom = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 监听消息变化,自动滚动到底部
|
||||
watch(messages, () => {
|
||||
nextTick(() => scrollToBottom())
|
||||
}, { deep: true })
|
||||
|
||||
// 切换模型下拉框
|
||||
const toggleModelDropdown = () => {
|
||||
showModelDropdown.value = !showModelDropdown.value
|
||||
@@ -76,133 +188,91 @@ const handleSelectModel = (model: any) => {
|
||||
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
|
||||
|
||||
const userContent = inputMessage.value.trim()
|
||||
inputMessage.value = ''
|
||||
resetInputHeight()
|
||||
|
||||
// 重置输入框高度
|
||||
nextTick(() => {
|
||||
const textarea = document.querySelector('.chat-input-textarea') as HTMLTextAreaElement
|
||||
if (textarea) {
|
||||
textarea.style.height = 'auto'
|
||||
}
|
||||
})
|
||||
|
||||
// 如果没有会话,创建一个新会话
|
||||
if (!currentSessionId.value) {
|
||||
await createSession()
|
||||
const session = await createSession()
|
||||
if (!session) return
|
||||
}
|
||||
|
||||
const userMessage = {
|
||||
id: Date.now(),
|
||||
role: 'user' as const,
|
||||
content: userContent,
|
||||
timestamp: new Date()
|
||||
}
|
||||
const userMessage = createUserMessage(userContent)
|
||||
messages.value.push(userMessage)
|
||||
|
||||
// 保存用户消息到后端
|
||||
await saveMessage('user', userContent)
|
||||
|
||||
const aiMessage = {
|
||||
id: Date.now() + 1,
|
||||
role: 'assistant' as const,
|
||||
content: '',
|
||||
timestamp: new Date(),
|
||||
isStreaming: true
|
||||
}
|
||||
const aiMessage = createAssistantMessage()
|
||||
messages.value.push(aiMessage)
|
||||
|
||||
nextTick(() => scrollToBottom())
|
||||
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
const requestBody: any = {
|
||||
agent_id: String(selectedAgent.value?.id || 1),
|
||||
message: userContent,
|
||||
}
|
||||
|
||||
if (selectedModel.value) {
|
||||
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',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(buildRequestBody(userContent)),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Request failed: ${response.status}`)
|
||||
}
|
||||
|
||||
// 真正的流式处理:边读取边显示
|
||||
const reader = response.body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let buffer = ''
|
||||
const aiMessageIndex = messages.value.length - 1
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
|
||||
const lines = buffer.split('\n')
|
||||
buffer = lines.pop() || ''
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
const data = line.slice(6).trim()
|
||||
if (data && data !== '[DONE]') {
|
||||
// 直接累加内容并显示(真正的流式)
|
||||
messages.value[aiMessageIndex].content += data
|
||||
await nextTick()
|
||||
scrollToBottom()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理剩余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
|
||||
isLoading.value = false
|
||||
scrollToBottom()
|
||||
|
||||
// 保存 AI 消息到后端
|
||||
await saveMessage('assistant', messages.value[aiMessageIndex].content)
|
||||
await handleStreamResponse(response)
|
||||
} catch (error: any) {
|
||||
console.error('[Stream] 错误:', error)
|
||||
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
|
||||
handleMessageError(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
console.log('[Chat] Component mounted, calling init()')
|
||||
init()
|
||||
})
|
||||
|
||||
@@ -269,6 +339,7 @@ onUnmounted(() => {
|
||||
@select-agent="selectAgent"
|
||||
@select-session="selectSession"
|
||||
@select-group="selectGroup"
|
||||
@delete-session="handleDeleteSession"
|
||||
/>
|
||||
|
||||
<!-- 智能体选择弹窗 -->
|
||||
|
||||
Reference in New Issue
Block a user