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,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import type { Agent } from '@/views/chat/chat'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -14,12 +15,32 @@ const emit = defineEmits<{
|
||||
(e: 'toggleSelect', agent: Agent): void
|
||||
(e: 'confirm'): void
|
||||
(e: 'update:groupChatName', value: string): void
|
||||
(e: 'delete', agent: Agent): void
|
||||
}>()
|
||||
|
||||
// 点击智能体 - 只是选择,不直接确认
|
||||
const handleAgentClick = (agent: Agent) => {
|
||||
emit('toggleSelect', agent)
|
||||
}
|
||||
|
||||
// 删除智能体
|
||||
const handleDeleteAgent = async (agent: Agent, event: Event) => {
|
||||
event.stopPropagation()
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除智能体 "${agent.name}" 吗?删除后无法恢复。`,
|
||||
'删除确认',
|
||||
{
|
||||
confirmButtonText: '确定删除',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}
|
||||
)
|
||||
emit('delete', agent)
|
||||
} catch {
|
||||
// 用户取消删除
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -29,10 +50,10 @@ const handleAgentClick = (agent: Agent) => {
|
||||
<div class="bg-dark-800 rounded-xl w-full max-w-md border border-dark-600 shadow-2xl" @click.stop>
|
||||
<div class="p-4 border-b border-dark-600">
|
||||
<h3 class="text-lg font-semibold text-white">
|
||||
{{ selectMode === 'single' ? '选择智能体' : '选择群聊成员' }}
|
||||
{{ selectMode === 'single' ? '选择会话' : '选择群聊成员' }}
|
||||
</h3>
|
||||
<p class="text-sm text-gray-400 mt-1">
|
||||
{{ selectMode === 'single' ? '选择一个智能体开始对话' : '选择多个智能体创建群聊' }}
|
||||
{{ selectMode === 'single' ? '选择一个智能体开始新对话' : '选择多个智能体创建群聊' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -53,7 +74,7 @@ const handleAgentClick = (agent: Agent) => {
|
||||
v-for="agent in chatAgents"
|
||||
:key="agent.id"
|
||||
@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 group"
|
||||
:class="selectedAgents.some(a => a.id === agent.id)
|
||||
? 'bg-orange-500/20 border border-orange-500/50'
|
||||
: 'bg-dark-700 hover:bg-dark-600 border border-transparent'"
|
||||
|
||||
@@ -63,7 +63,12 @@ const copyMessage = async () => {
|
||||
: 'bg-[#1e1e28] text-gray-100 rounded-bl-sm'"
|
||||
>
|
||||
<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-flex items-center ml-1 align-middle">
|
||||
<span class="w-1.5 h-1.5 mx-0.5 bg-orange-400 rounded-full animate-bounce" style="animation-delay: 0ms;"></span>
|
||||
<span class="w-1.5 h-1.5 mx-0.5 bg-orange-400 rounded-full animate-bounce" style="animation-delay: 150ms;"></span>
|
||||
<span class="w-1.5 h-1.5 mx-0.5 bg-orange-400 rounded-full animate-bounce" style="animation-delay: 300ms;"></span>
|
||||
</span>
|
||||
|
||||
<!-- 复制按钮 -->
|
||||
<Transition name="fade">
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import type { Agent, ChatSession, GroupChat } from '@/views/chat/chat'
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
collapsed: boolean
|
||||
chatAgents: Agent[]
|
||||
selectedAgent: Agent | null
|
||||
chatSessions: ChatSession[]
|
||||
groupChats: GroupChat[]
|
||||
groupChats?: GroupChat[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -14,8 +16,16 @@ const emit = defineEmits<{
|
||||
(e: 'selectAgent', agent: Agent): void
|
||||
(e: 'selectSession', session: ChatSession): void
|
||||
(e: 'selectGroup', group: GroupChat): void
|
||||
(e: 'deleteSession', session: ChatSession): void
|
||||
}>()
|
||||
|
||||
// 根据 agent_id 获取智能体名称
|
||||
const getAgentName = (agentId: number | string | undefined) => {
|
||||
if (!agentId) return '未知智能体'
|
||||
const agent = props.chatAgents.find(a => a.id === agentId)
|
||||
return agent?.name || '未知智能体'
|
||||
}
|
||||
|
||||
const formatRelativeTime = (date: Date) => {
|
||||
const now = new Date()
|
||||
const diff = now.getTime() - date.getTime()
|
||||
@@ -27,6 +37,25 @@ const formatRelativeTime = (date: Date) => {
|
||||
if (days < 7) return `${days}天前`
|
||||
return date.toLocaleDateString('zh-CN')
|
||||
}
|
||||
|
||||
// 删除会话
|
||||
const handleDeleteSession = async (session: ChatSession, event: Event) => {
|
||||
event.stopPropagation()
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除与 "${session.title}" 的对话吗?`,
|
||||
'删除确认',
|
||||
{
|
||||
confirmButtonText: '确定删除',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}
|
||||
)
|
||||
emit('deleteSession', session)
|
||||
} catch {
|
||||
// 用户取消删除
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -69,47 +98,40 @@ const formatRelativeTime = (date: Date) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AI 助手选择 -->
|
||||
<div class="px-3 pb-3">
|
||||
<div class="text-xs text-white/40 uppercase tracking-wider mb-2 px-1">选择 AI 助手</div>
|
||||
<div class="space-y-1">
|
||||
<button
|
||||
v-for="agent in chatAgents"
|
||||
:key="agent.id"
|
||||
@click="emit('selectAgent', agent)"
|
||||
class="w-full flex items-center gap-2 px-3 py-2 rounded-lg transition-all duration-200"
|
||||
:class="selectedAgent?.id === agent.id
|
||||
? 'bg-orange-500/15 text-orange-400'
|
||||
: 'text-white/60 hover:bg-white/5 hover:text-white'"
|
||||
>
|
||||
<span class="text-base">{{ agent.avatar }}</span>
|
||||
<span class="text-sm truncate">{{ agent.name }}</span>
|
||||
<span
|
||||
v-if="agent.status === 'online'"
|
||||
class="w-1.5 h-1.5 rounded-full bg-emerald-400 ml-auto"
|
||||
></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 群聊列表 -->
|
||||
<!-- 会话列表 -->
|
||||
<div class="flex-1 overflow-y-auto px-3 pb-3">
|
||||
<div class="text-xs text-white/40 uppercase tracking-wider mb-2 px-1">群聊</div>
|
||||
<div class="text-xs text-white/40 uppercase tracking-wider mb-2 px-1">会话</div>
|
||||
<div class="space-y-1">
|
||||
<button
|
||||
v-for="group in groupChats"
|
||||
:key="group.id"
|
||||
@click="emit('selectGroup', group)"
|
||||
v-for="session in chatSessions"
|
||||
:key="session.id"
|
||||
@click="emit('selectSession', session)"
|
||||
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">
|
||||
<svg class="w-4 h-4 text-white/30 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"></path>
|
||||
</svg>
|
||||
<span class="text-sm text-white/70 group-hover:text-white truncate">{{ group.name }}</span>
|
||||
<span class="text-sm text-white/70 group-hover:text-white truncate flex-1">{{ session.title || '新会话' }}</span>
|
||||
<!-- 删除按钮 -->
|
||||
<span
|
||||
@click="handleDeleteSession(session, $event)"
|
||||
class="hidden group-hover:flex w-6 h-6 items-center justify-center rounded-md text-white/30 hover:text-red-400 hover:bg-red-500/20 transition-colors"
|
||||
title="删除会话"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 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>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 mt-1 pl-6">
|
||||
<span class="text-xs text-orange-500/70">{{ getAgentName(session.agent_id) }}</span>
|
||||
<span class="text-xs text-white/30">{{ formatRelativeTime(session.timestamp) }}</span>
|
||||
</div>
|
||||
<div class="text-xs text-white/30 mt-1 pl-6">{{ group.members.length }} members</div>
|
||||
</button>
|
||||
<div v-if="!chatSessions || chatSessions.length === 0" class="text-xs text-white/30 text-center py-4">
|
||||
暂无会话记录
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { onUnmounted } from 'vue'
|
||||
import { Play, Pause, Edit, Trash2 } from 'lucide-vue-next'
|
||||
import { useAgents } from './agents/useAgents'
|
||||
import './agents/agents.css'
|
||||
@@ -57,8 +58,13 @@ const {
|
||||
getSkillsDisplayText,
|
||||
toggleSkillSelection,
|
||||
selectAllSkills,
|
||||
statusClass
|
||||
statusClass,
|
||||
cleanup
|
||||
} = useAgents()
|
||||
|
||||
onUnmounted(() => {
|
||||
cleanup()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -162,18 +168,18 @@ const {
|
||||
class="btn-icon"
|
||||
:title="agent.status === 'active' ? 'Deactivate' : 'Activate'"
|
||||
>
|
||||
<Pause v-if="agent.status === 'active'" class="w-4 h-4 text-gray-500 hover:text-yellow-400 transition-colors" />
|
||||
<Play v-else class="w-4 h-4 text-gray-500 hover:text-green-400 transition-colors" />
|
||||
<Pause v-if="agent.status === 'active'" class="w-4 h-4 text-gray-400 hover:text-yellow-400 transition-colors" />
|
||||
<Play v-else class="w-4 h-4 text-gray-400 hover:text-green-400 transition-colors" />
|
||||
</button>
|
||||
<button @click="openEdit(agent)" class="btn-icon" title="Edit">
|
||||
<Edit class="w-4 h-4 text-gray-500 hover:text-white transition-colors" />
|
||||
<Edit class="w-4 h-4 text-gray-400 hover:text-white transition-colors" />
|
||||
</button>
|
||||
<button
|
||||
@click.stop="deleteAgent(agent.id)"
|
||||
class="btn-icon"
|
||||
title="Delete"
|
||||
>
|
||||
<Trash2 class="w-4 h-4 text-gray-500 hover:text-red-400 transition-colors" />
|
||||
<Trash2 class="w-4 h-4 text-gray-400 hover:text-red-500 transition-colors" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
<!-- 智能体选择弹窗 -->
|
||||
|
||||
@@ -278,7 +278,7 @@ async function createAgent() {
|
||||
const skillsLabels = newAgent.value.selectedSkills.map(id => getSkillLabel(id)).join(', ')
|
||||
|
||||
agents.value.unshift({
|
||||
id: result.agent_id,
|
||||
id: result.agent_id_str || result.agent_id,
|
||||
name: newAgent.value.name,
|
||||
avatar: newAgent.value.avatar,
|
||||
description: newAgent.value.description,
|
||||
@@ -341,6 +341,7 @@ async function saveEdit() {
|
||||
body: JSON.stringify({
|
||||
name: editingAgent.value.name,
|
||||
description: editingAgent.value.description,
|
||||
avatar: editingAgent.value.avatar,
|
||||
skills: skills,
|
||||
role_description: editingAgent.value.prompt,
|
||||
model_provider: selectedModel?.provider || '',
|
||||
@@ -353,6 +354,7 @@ async function saveEdit() {
|
||||
if (agent) {
|
||||
agent.name = editingAgent.value.name
|
||||
agent.description = editingAgent.value.description
|
||||
agent.avatar = editingAgent.value.avatar
|
||||
agent.skills = editingAgent.value.skillsMode === 'all' ? '*' : editingAgent.value.selectedSkills.join(', ')
|
||||
agent.model = selectedModel?.name || ''
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ export interface ChatMessage {
|
||||
}
|
||||
|
||||
export interface Agent {
|
||||
id: number
|
||||
id: string | number
|
||||
name: string
|
||||
avatar: string
|
||||
description: string
|
||||
@@ -40,7 +40,7 @@ export interface ChatSession {
|
||||
}
|
||||
|
||||
export interface GroupChat {
|
||||
id: number
|
||||
id: string | number
|
||||
name: string
|
||||
members: string[]
|
||||
lastMessage: string
|
||||
@@ -82,7 +82,6 @@ export const renderMarkdown = (content: string): string => {
|
||||
const processed = preprocessContent(content)
|
||||
return marked.parse(processed) as string
|
||||
} catch (e) {
|
||||
console.error('Markdown parse error:', e)
|
||||
return content
|
||||
}
|
||||
}
|
||||
@@ -150,24 +149,21 @@ export function useChat() {
|
||||
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)
|
||||
} catch {
|
||||
// 静默处理
|
||||
} finally {
|
||||
modelsLoading.value = false
|
||||
}
|
||||
@@ -178,7 +174,6 @@ export function useChat() {
|
||||
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) => ({
|
||||
@@ -186,9 +181,9 @@ export function useChat() {
|
||||
name: agent.name,
|
||||
avatar: agent.avatar || '🧠',
|
||||
description: agent.description || '',
|
||||
accentColor: agent.accent_color || '#f97316',
|
||||
accentColor: '#f97316',
|
||||
gradient: 'from-orange-500/20 to-amber-500/20',
|
||||
status: agent.status === 'active' ? 'online' : 'offline'
|
||||
status: agent.is_active ? 'online' : 'offline'
|
||||
}))
|
||||
|
||||
// 默认选中第一个智能体
|
||||
@@ -196,8 +191,8 @@ export function useChat() {
|
||||
selectedAgent.value = chatAgents.value[0]
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch agents:', error)
|
||||
} catch {
|
||||
// 静默处理
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,7 +202,6 @@ export function useChat() {
|
||||
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) => ({
|
||||
@@ -219,9 +213,13 @@ export function useChat() {
|
||||
timestamp: new Date(s.created_at || Date.now()),
|
||||
status: s.status
|
||||
}))
|
||||
|
||||
// 自动选择最近的会话并加载消息
|
||||
// 页面加载时不自动选择会话,显示空页面
|
||||
// 用户点击"新建聊天"或选择智能体时才创建会话
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch sessions:', error)
|
||||
} catch {
|
||||
// 静默处理
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,7 +229,6 @@ export function useChat() {
|
||||
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) => ({
|
||||
@@ -242,13 +239,13 @@ export function useChat() {
|
||||
timestamp: new Date(g.created_at || Date.now())
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch groups:', error)
|
||||
} catch {
|
||||
// 静默处理
|
||||
}
|
||||
}
|
||||
|
||||
// 创建群聊
|
||||
const createGroup = async (name: string, agentIds: number[]) => {
|
||||
const createGroup = async (name: string, agentIds: (string | number)[]) => {
|
||||
try {
|
||||
const userId = localStorage.getItem('user_id') || 'default-user'
|
||||
const response = await fetch('/api/chat/groups', {
|
||||
@@ -260,8 +257,12 @@ export function useChat() {
|
||||
agent_ids: JSON.stringify(agentIds)
|
||||
})
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('Create group failed:', response.status, await response.text())
|
||||
return null
|
||||
}
|
||||
const group = await response.json()
|
||||
console.log('[Chat] Created group:', group)
|
||||
|
||||
// 添加到群聊列表
|
||||
groupChats.value.unshift({
|
||||
@@ -273,8 +274,7 @@ export function useChat() {
|
||||
})
|
||||
|
||||
return group
|
||||
} catch (error) {
|
||||
console.error('Failed to create group:', error)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -293,8 +293,12 @@ export function useChat() {
|
||||
model_id: selectedModel.value?.id
|
||||
})
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
return null
|
||||
}
|
||||
|
||||
const session = await response.json()
|
||||
console.log('[Chat] Created session:', session)
|
||||
|
||||
// 添加到会话列表
|
||||
chatSessions.value.unshift({
|
||||
@@ -307,8 +311,7 @@ export function useChat() {
|
||||
|
||||
currentSessionId.value = session.id
|
||||
return session
|
||||
} catch (error) {
|
||||
console.error('Failed to create session:', error)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -318,7 +321,6 @@ export function useChat() {
|
||||
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) => ({
|
||||
@@ -328,27 +330,37 @@ export function useChat() {
|
||||
timestamp: new Date(m.created_at)
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch messages:', error)
|
||||
} catch {
|
||||
// 静默处理
|
||||
}
|
||||
}
|
||||
|
||||
// 保存消息到后端
|
||||
const saveMessage = async (role: 'user' | 'assistant', content: string) => {
|
||||
if (!currentSessionId.value) return
|
||||
const sessionId = currentSessionId.value
|
||||
if (!sessionId || typeof sessionId !== 'string' || sessionId.trim() === '') {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查内容是否有效
|
||||
if (!content || typeof content !== 'string' || content.trim() === '') {
|
||||
return
|
||||
}
|
||||
|
||||
const payload = {
|
||||
session_id: sessionId,
|
||||
role: role,
|
||||
content: content
|
||||
}
|
||||
|
||||
try {
|
||||
await fetch('/api/chat/messages', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
session_id: currentSessionId.value,
|
||||
role: role,
|
||||
content: content
|
||||
})
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to save message:', error)
|
||||
} catch {
|
||||
// 静默处理
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,8 +372,8 @@ export function useChat() {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ title })
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to update session:', error)
|
||||
} catch {
|
||||
// 静默处理
|
||||
}
|
||||
}
|
||||
|
||||
@@ -376,8 +388,8 @@ export function useChat() {
|
||||
currentSessionId.value = null
|
||||
messages.value = []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to delete session:', error)
|
||||
} catch {
|
||||
// 静默处理
|
||||
}
|
||||
}
|
||||
|
||||
@@ -452,19 +464,47 @@ export function useChat() {
|
||||
showAgentSelector.value = false
|
||||
}
|
||||
|
||||
// 选择助手
|
||||
const selectAgent = (agent: Agent) => {
|
||||
// 选择助手 - 如果是同一智能体则不创建新会话
|
||||
const selectAgent = async (agent: Agent) => {
|
||||
// 如果选择的是同一智能体,不创建新会话,直接返回
|
||||
if (selectedAgent.value?.id === agent.id) {
|
||||
return
|
||||
}
|
||||
|
||||
selectedAgent.value = agent
|
||||
|
||||
// 创建新会话
|
||||
const session = await createSession(`与 ${agent.name} 的对话`)
|
||||
if (session) {
|
||||
currentSessionId.value = session.id
|
||||
}
|
||||
|
||||
messages.value = [
|
||||
{ id: 1, role: 'assistant', content: `你好!我是 ${agent.name}。有什么我可以帮助你的吗?`, timestamp: new Date() }
|
||||
{ id: Date.now(), role: 'assistant', content: `你好!我是 ${agent.name},你的 AI 助手。有什么我可以帮助你的吗?`, timestamp: new Date() }
|
||||
]
|
||||
|
||||
// 保存助手欢迎消息
|
||||
if (currentSessionId.value) {
|
||||
await saveMessage('assistant', messages.value[0].content)
|
||||
}
|
||||
}
|
||||
|
||||
// 选择群聊
|
||||
const selectGroup = (group: GroupChat) => {
|
||||
const selectGroup = async (group: GroupChat) => {
|
||||
// 创建新会话
|
||||
const session = await createSession(group.name)
|
||||
if (session) {
|
||||
currentSessionId.value = session.id
|
||||
}
|
||||
|
||||
messages.value = [
|
||||
{ id: 1, role: 'assistant', content: `你好!欢迎进入群聊 "${group.name}",${group.members.length} 位智能体已加入。`, timestamp: new Date() }
|
||||
{ id: Date.now(), role: 'assistant', content: `你好!欢迎进入群聊 "${group.name}",${group.members.length} 位智能体已加入。`, timestamp: new Date() }
|
||||
]
|
||||
|
||||
// 保存助手欢迎消息
|
||||
if (currentSessionId.value) {
|
||||
await saveMessage('assistant', messages.value[0].content)
|
||||
}
|
||||
}
|
||||
|
||||
// 选择历史对话
|
||||
|
||||
Reference in New Issue
Block a user