+
+
+
+
{{ selectedAgent?.avatar || '🧠' }}
+
和 {{ selectedAgent?.name || 'AI' }} 开始对话
+
发送消息开始聊天
+
+
+
+
{
@open-agent-selector="openAgentSelector"
@select-agent="selectAgent"
@select-session="selectSession"
+ @select-group="selectGroup"
/>
@@ -254,55 +285,3 @@ const sendMessage = async () => {
/>
-
-
diff --git a/web/src/views/chat/chat.css b/web/src/views/chat/chat.css
new file mode 100644
index 0000000..768d38e
--- /dev/null
+++ b/web/src/views/chat/chat.css
@@ -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;
+}
diff --git a/web/src/views/chat/chat.ts b/web/src/views/chat/chat.ts
new file mode 100644
index 0000000..ad26553
--- /dev/null
+++ b/web/src/views/chat/chat.ts
@@ -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
= {
+ 'OpenAI': '🤖',
+ 'Claude': '🧠',
+ 'Google': '✨',
+ 'Gemini': '✨',
+ 'Ollama': '🦙',
+ 'DeepSeek': '🔮',
+ 'Moonshot': '🌙',
+ 'Kimi': '🌙',
+ 'Baidu': '🐉',
+ '文心一言': '🐉',
+ 'Aliyun': '☁️',
+ 'Ali': '☁️',
+ '通义千问': '☁️',
+ 'Azure': '⬛',
+ 'Anthropic': '🧠',
+ }
+ return icons[provider] || '💬'
+}
+
+// 创建 composable
+export function useChat() {
+ // 模型相关状态
+ const chatModels = ref([])
+ const selectedModel = ref(null)
+ const modelsLoading = ref(false)
+ const showModelDropdown = ref(false)
+
+ // 助手相关状态
+ const chatAgents = ref([])
+ const selectedAgent = ref(null)
+
+ // 消息相关状态
+ const messages = ref([])
+
+ // 历史对话
+ const chatSessions = ref([])
+ const currentSessionId = ref(null)
+
+ // 群聊
+ const groupChats = ref([])
+
+ // 智能体选择弹窗
+ const showAgentSelector = ref(false)
+ const selectMode = ref<'single' | 'group'>('single')
+ const selectedAgents = ref([])
+ 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,
+ }
+}