feat: 更新Chat前端页面
- 优化Chat组件交互体验 - 修复智能体选择逻辑 - 新增chat页面样式和脚本 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
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 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,
|
||||
@@ -15,6 +16,7 @@ const {
|
||||
selectedAgent,
|
||||
messages,
|
||||
chatSessions,
|
||||
currentSessionId,
|
||||
groupChats,
|
||||
showAgentSelector,
|
||||
selectMode,
|
||||
@@ -29,9 +31,16 @@ const {
|
||||
confirmAgentSelection,
|
||||
cancelAgentSelection,
|
||||
selectAgent,
|
||||
selectGroup,
|
||||
selectSession,
|
||||
newChat,
|
||||
clearMessages,
|
||||
toggleSidebar,
|
||||
createSession,
|
||||
saveMessage,
|
||||
deleteSession,
|
||||
init,
|
||||
cleanup,
|
||||
} = useChat()
|
||||
|
||||
const messagesContainer = ref<HTMLElement | null>(null)
|
||||
@@ -74,6 +83,19 @@ const sendMessage = async () => {
|
||||
const userContent = inputMessage.value.trim()
|
||||
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 = {
|
||||
id: Date.now(),
|
||||
role: 'user' as const,
|
||||
@@ -82,6 +104,9 @@ const sendMessage = async () => {
|
||||
}
|
||||
messages.value.push(userMessage)
|
||||
|
||||
// 保存用户消息到后端
|
||||
await saveMessage('user', userContent)
|
||||
|
||||
const aiMessage = {
|
||||
id: Date.now() + 1,
|
||||
role: 'assistant' as const,
|
||||
@@ -95,23 +120,6 @@ const sendMessage = async () => {
|
||||
|
||||
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 {
|
||||
const requestBody: any = {
|
||||
agent_id: selectedAgent.value?.id || 1,
|
||||
@@ -122,7 +130,11 @@ const sendMessage = async () => {
|
||||
requestBody.model_id = selectedModel.value.id
|
||||
}
|
||||
|
||||
// 先获取完整响应,再逐字符显示(模拟流式效果)
|
||||
// 传入 session_id
|
||||
if (currentSessionId.value) {
|
||||
requestBody.session_id = currentSessionId.value
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/agent/chat/stream`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -135,11 +147,11 @@ const sendMessage = async () => {
|
||||
throw new Error(`Request failed: ${response.status}`)
|
||||
}
|
||||
|
||||
// 读取完整响应
|
||||
// 真正的流式处理:边读取边显示
|
||||
const reader = response.body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let buffer = ''
|
||||
let fullText = ''
|
||||
const aiMessageIndex = messages.value.length - 1
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
@@ -152,34 +164,31 @@ const sendMessage = async () => {
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
const data = line.slice(6)
|
||||
const data = line.slice(6).trim()
|
||||
if (data && data !== '[DONE]') {
|
||||
fullText += data
|
||||
// 直接累加内容并显示(真正的流式)
|
||||
messages.value[aiMessageIndex].content += data
|
||||
await nextTick()
|
||||
scrollToBottom()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 逐字符显示(模拟流式),每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()
|
||||
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
|
||||
isLoading.value = false
|
||||
scrollToBottom()
|
||||
|
||||
// 保存 AI 消息到后端
|
||||
await saveMessage('assistant', messages.value[aiMessageIndex].content)
|
||||
} catch (error: any) {
|
||||
console.error('[Stream] 错误:', error)
|
||||
const errorIndex = messages.value.findIndex(m => m.isStreaming)
|
||||
@@ -190,6 +199,16 @@ const sendMessage = async () => {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
console.log('[Chat] Component mounted, calling init()')
|
||||
init()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
cleanup()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -206,11 +225,22 @@ const sendMessage = async () => {
|
||||
@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 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
|
||||
v-for="message in messages"
|
||||
:key="message.id"
|
||||
@@ -238,6 +268,7 @@ const sendMessage = async () => {
|
||||
@open-agent-selector="openAgentSelector"
|
||||
@select-agent="selectAgent"
|
||||
@select-session="selectSession"
|
||||
@select-group="selectGroup"
|
||||
/>
|
||||
|
||||
<!-- 智能体选择弹窗 -->
|
||||
@@ -254,55 +285,3 @@ const sendMessage = async () => {
|
||||
/>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user