feat: 新增 Chat 聊天页面并优化 Agents
- 新增 Chat.vue 聊天页面组件 - 重构 Agents.vue 页面代码 - 更新侧边栏和路由配置 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -41,7 +41,7 @@ interface MenuItem {
|
||||
|
||||
// 第1组: Chat, Agents
|
||||
const group1 = computed(() => [
|
||||
{ name: 'Chat', icon: 'fa-robot', path: '/agents' },
|
||||
{ name: 'Chat', icon: 'fa-robot', path: '/chat' },
|
||||
{ name: 'Agents', icon: 'fa-users', badge: 3, path: '/agents' },
|
||||
])
|
||||
|
||||
@@ -68,11 +68,6 @@ const group4 = computed(() => [
|
||||
const activeMenu = computed(() => {
|
||||
const currentPath = route.path
|
||||
|
||||
// Special case for /agents - prioritize Chat over Agents
|
||||
if (currentPath === '/agents') {
|
||||
return 'Chat'
|
||||
}
|
||||
|
||||
// Check all groups
|
||||
const allGroups = [...group1.value, ...group2.value, ...group3.value, ...group4.value]
|
||||
const item = allGroups.find(item => item.path === currentPath)
|
||||
@@ -135,7 +130,7 @@ const handleUserCommand = (command: string) => {
|
||||
<!-- 第1组: Chat, Agents -->
|
||||
<li v-for="item in group1" :key="item.name">
|
||||
<a
|
||||
href="#"
|
||||
href="javascript:void(0)"
|
||||
class="flex items-center justify-between px-3 py-2.5 rounded-lg transition-colors text-sm"
|
||||
:class="activeMenu === item.name ? 'bg-dark-600 text-white' : 'text-gray-400 hover:bg-dark-600 hover:text-white'"
|
||||
@click="navigateTo(item)"
|
||||
@@ -154,7 +149,7 @@ const handleUserCommand = (command: string) => {
|
||||
<!-- 第2组: Database, Knowledge -->
|
||||
<li v-for="item in group2" :key="item.name">
|
||||
<a
|
||||
href="#"
|
||||
href="javascript:void(0)"
|
||||
class="flex items-center justify-between px-3 py-2.5 rounded-lg transition-colors text-sm"
|
||||
:class="activeMenu === item.name ? 'bg-dark-600 text-white' : 'text-gray-400 hover:bg-dark-600 hover:text-white'"
|
||||
@click="navigateTo(item)"
|
||||
@@ -173,7 +168,7 @@ const handleUserCommand = (command: string) => {
|
||||
<!-- 第3组: Skills, Tools, Script -->
|
||||
<li v-for="item in group3" :key="item.name">
|
||||
<a
|
||||
href="#"
|
||||
href="javascript:void(0)"
|
||||
class="flex items-center justify-between px-3 py-2.5 rounded-lg transition-colors text-sm"
|
||||
:class="activeMenu === item.name ? 'bg-dark-600 text-white' : 'text-gray-400 hover:bg-dark-600 hover:text-white'"
|
||||
@click="navigateTo(item)"
|
||||
@@ -192,7 +187,7 @@ const handleUserCommand = (command: string) => {
|
||||
<!-- 第4组: Dashboard, Account, Settings -->
|
||||
<li v-for="item in group4" :key="item.name">
|
||||
<a
|
||||
href="#"
|
||||
href="javascript:void(0)"
|
||||
class="flex items-center justify-between px-3 py-2.5 rounded-lg transition-colors text-sm"
|
||||
:class="activeMenu === item.name ? 'bg-dark-600 text-white' : 'text-gray-400 hover:bg-dark-600 hover:text-white'"
|
||||
@click="navigateTo(item)"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import Dashboard from '@/views/Dashboard.vue'
|
||||
import Login from '@/views/Login.vue'
|
||||
import Chat from '@/views/Chat.vue'
|
||||
import Agents from '@/views/Agents.vue'
|
||||
import Team from '@/views/Team.vue'
|
||||
import Skill from '@/views/Skill.vue'
|
||||
@@ -24,6 +25,11 @@ const router = createRouter({
|
||||
name: 'dashboard',
|
||||
component: Dashboard
|
||||
},
|
||||
{
|
||||
path: '/chat',
|
||||
name: 'chat',
|
||||
component: Chat
|
||||
},
|
||||
{
|
||||
path: '/agents',
|
||||
name: 'agents',
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick } from 'vue'
|
||||
|
||||
interface ChatMessage {
|
||||
id: number
|
||||
role: 'user' | 'assistant'
|
||||
content: string
|
||||
timestamp: Date
|
||||
isStreaming?: boolean
|
||||
}
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
interface Agent {
|
||||
id: number
|
||||
@@ -16,414 +8,220 @@ interface Agent {
|
||||
description: string
|
||||
accentColor: string
|
||||
gradient: string
|
||||
status: 'running' | 'stopped'
|
||||
framework: string
|
||||
model: string
|
||||
mcpServers: number
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
// AI 助手配置
|
||||
// 管理页面的 agents
|
||||
const agents = ref<Agent[]>([
|
||||
{ id: 1, name: 'Claude', avatar: '🧠', description: 'Anthropic AI', accentColor: '#f97316', gradient: 'from-orange-500/20 to-amber-500/20' },
|
||||
{ id: 2, name: 'Gemini', avatar: '✨', description: 'Google DeepMind', accentColor: '#8b5cf6', gradient: 'from-violet-500/20 to-purple-500/20' },
|
||||
{ id: 3, name: 'ChatGPT', avatar: '💬', description: 'OpenAI', accentColor: '#10b981', gradient: 'from-emerald-500/20 to-green-500/20' },
|
||||
{ id: 4, name: 'DeepSeek', avatar: '🔮', description: 'DeepSeek AI', accentColor: '#3b82f6', gradient: 'from-blue-500/20 to-cyan-500/20' },
|
||||
{ id: 5, name: 'Kimi', avatar: '🌙', description: 'Moonshot AI', accentColor: '#ec4899', gradient: 'from-pink-500/20 to-rose-500/20' },
|
||||
{ id: 6, name: '文心一言', avatar: '🐉', description: 'Baidu', accentColor: '#ef4444', gradient: 'from-red-500/20 to-orange-500/20' },
|
||||
{ id: 7, name: '通义千问', avatar: '☁️', description: 'Alibaba', accentColor: '#06b6d4', gradient: 'from-cyan-500/20 to-sky-500/20' },
|
||||
{ id: 1, name: 'Claude Agent', avatar: '🧠', description: 'General purpose AI assistant', accentColor: '#f97316', gradient: 'from-orange-500/20 to-amber-500/20', status: 'running', framework: 'Google ADK', model: 'gemini-2.0-flash', mcpServers: 2, createdAt: '2025-04-10' },
|
||||
{ id: 2, name: 'Code Assistant', avatar: '💻', description: 'Specialized in code generation', accentColor: '#3b82f6', gradient: 'from-blue-500/20 to-cyan-500/20', status: 'running', framework: 'OpenAI', model: 'gpt-4o', mcpServers: 1, createdAt: '2025-04-08' },
|
||||
{ id: 3, name: 'Data Analyst', avatar: '📊', description: 'Data analysis and visualization', accentColor: '#10b981', gradient: 'from-emerald-500/20 to-green-500/20', status: 'stopped', framework: 'PydanticAI', model: 'gpt-4o-mini', mcpServers: 3, createdAt: '2025-04-05' },
|
||||
{ id: 4, name: 'Research Bot', avatar: '🔬', description: 'Academic research assistant', accentColor: '#8b5cf6', gradient: 'from-violet-500/20 to-purple-500/20', status: 'running', framework: 'LangChain', model: 'claude-3-5-sonnet', mcpServers: 2, createdAt: '2025-04-12' },
|
||||
{ id: 5, name: '客服助手', avatar: '🎧', description: 'Customer support agent', accentColor: '#ec4899', gradient: 'from-pink-500/20 to-rose-500/20', status: 'running', framework: 'Google ADK', model: 'gemini-1.5-pro', mcpServers: 4, createdAt: '2025-04-11' },
|
||||
])
|
||||
|
||||
// 当前选中的助手
|
||||
const selectedAgent = ref<Agent | null>(agents.value[0])
|
||||
const sidebarCollapsed = ref(false)
|
||||
const searchQuery = ref('')
|
||||
const filterStatus = ref('all')
|
||||
|
||||
// 聊天消息
|
||||
const messages = ref<ChatMessage[]>([
|
||||
{ id: 1, role: 'assistant', content: '你好!我是 Claude,你的 AI 助手。有什么我可以帮助你的吗?', timestamp: new Date() },
|
||||
])
|
||||
// 过滤后的 agents
|
||||
const filteredAgents = computed(() => {
|
||||
return agents.value.filter(agent => {
|
||||
const matchSearch = agent.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
|
||||
agent.framework.toLowerCase().includes(searchQuery.value.toLowerCase())
|
||||
const matchStatus = filterStatus.value === 'all' || agent.status === filterStatus.value
|
||||
return matchSearch && matchStatus
|
||||
})
|
||||
})
|
||||
|
||||
// 输入内容
|
||||
const inputMessage = ref('')
|
||||
const isLoading = ref(false)
|
||||
const messagesContainer = ref<HTMLElement | null>(null)
|
||||
// 统计数据
|
||||
const stats = computed(() => ({
|
||||
total: agents.value.length,
|
||||
running: agents.value.filter(a => a.status === 'running').length,
|
||||
stopped: agents.value.filter(a => a.status === 'stopped').length,
|
||||
}))
|
||||
|
||||
// 发送消息
|
||||
const sendMessage = async () => {
|
||||
if (!inputMessage.value.trim() || isLoading.value) return
|
||||
|
||||
const userContent = inputMessage.value.trim()
|
||||
inputMessage.value = ''
|
||||
|
||||
const userMessage: ChatMessage = {
|
||||
id: Date.now(),
|
||||
role: 'user',
|
||||
content: userContent,
|
||||
timestamp: new Date()
|
||||
}
|
||||
messages.value.push(userMessage)
|
||||
|
||||
const aiMessage: ChatMessage = {
|
||||
id: Date.now() + 1,
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
timestamp: new Date(),
|
||||
isStreaming: true
|
||||
}
|
||||
messages.value.push(aiMessage)
|
||||
|
||||
nextTick(() => scrollToBottom())
|
||||
|
||||
isLoading.value = true
|
||||
const fullResponse = `我理解你发送了消息: "${userContent}"
|
||||
|
||||
作为 AI 助手,我可以帮助你:
|
||||
• 回答各种问题
|
||||
• 编写代码和调试
|
||||
• 分析和处理数据
|
||||
• 翻译和写作
|
||||
• 头脑风暴和创意建议
|
||||
|
||||
请告诉我你需要什么帮助?`
|
||||
|
||||
let currentIndex = 0
|
||||
const words = fullResponse.split('')
|
||||
|
||||
const streamInterval = setInterval(() => {
|
||||
if (currentIndex < words.length) {
|
||||
aiMessage.content += words[currentIndex]
|
||||
currentIndex++
|
||||
nextTick(() => scrollToBottom())
|
||||
} else {
|
||||
clearInterval(streamInterval)
|
||||
aiMessage.isStreaming = false
|
||||
isLoading.value = false
|
||||
}
|
||||
}, 30)
|
||||
}
|
||||
|
||||
// 滚动到底部
|
||||
const scrollToBottom = () => {
|
||||
if (messagesContainer.value) {
|
||||
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
|
||||
// 状态颜色
|
||||
const statusClass = (status: string) => {
|
||||
switch (status) {
|
||||
case 'running': return 'bg-primary-success'
|
||||
case 'stopped': return 'bg-gray-500'
|
||||
default: return 'bg-gray-500'
|
||||
}
|
||||
}
|
||||
|
||||
// 复制消息
|
||||
const copyMessage = (content: string) => {
|
||||
navigator.clipboard.writeText(content)
|
||||
// 切换状态
|
||||
const toggleStatus = (agent: Agent) => {
|
||||
agent.status = agent.status === 'running' ? 'stopped' : 'running'
|
||||
}
|
||||
|
||||
// 选择助手
|
||||
const selectAgent = (agent: Agent) => {
|
||||
selectedAgent.value = agent
|
||||
messages.value = [
|
||||
{ id: 1, role: 'assistant', content: `你好!我是 ${agent.name}。有什么我可以帮助你的吗?`, timestamp: new Date() }
|
||||
]
|
||||
}
|
||||
|
||||
// 新建聊天
|
||||
const newChat = () => {
|
||||
messages.value = [
|
||||
{ id: 1, role: 'assistant', content: `你好!我是 ${selectedAgent.value?.name || 'Claude'}。有什么我可以帮助你的吗?`, timestamp: new Date() }
|
||||
]
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (date: Date) => {
|
||||
return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
|
||||
}
|
||||
|
||||
// 回车发送
|
||||
const handleKeydown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
sendMessage()
|
||||
}
|
||||
}
|
||||
|
||||
// 调整输入框高度
|
||||
const autoResize = (e: Event) => {
|
||||
const target = e.target as HTMLTextAreaElement
|
||||
target.style.height = 'auto'
|
||||
target.style.height = Math.min(target.scrollHeight, 160) + 'px'
|
||||
}
|
||||
|
||||
// 切换侧边栏
|
||||
const toggleSidebar = () => {
|
||||
sidebarCollapsed.value = !sidebarCollapsed.value
|
||||
setTimeout(() => {
|
||||
scrollToBottom()
|
||||
}, 350)
|
||||
// 删除 Agent
|
||||
const deleteAgent = (id: number) => {
|
||||
agents.value = agents.value.filter(a => a.id !== id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
@keyframes messageSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(12px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.message-enter {
|
||||
animation: messageSlideIn 0.35s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0; }
|
||||
}
|
||||
|
||||
.cursor-blink {
|
||||
animation: blink 1s step-end infinite;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div class="h-screen flex bg-[#0a0a0f]">
|
||||
<!-- 主聊天区域 -->
|
||||
<div class="flex-1 flex flex-col bg-[#0a0a0f]">
|
||||
<!-- 顶部栏 -->
|
||||
<div class="h-14 px-6 flex items-center justify-between border-b border-white/5 bg-[#0d0d12]/50 backdrop-blur-sm">
|
||||
<!-- 左侧:当前AI信息 -->
|
||||
<!-- 主内容区域 -->
|
||||
<div class="p-6 min-h-screen">
|
||||
<!-- 顶部导航 -->
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fa-solid fa-robot text-gray-400"></i>
|
||||
<span class="font-medium">Agents</span>
|
||||
</div>
|
||||
<button class="btn-primary">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
New Agent
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Stats Cards -->
|
||||
<div class="grid grid-cols-4 gap-4 mb-6">
|
||||
<div class="bg-dark-700 rounded-xl p-4 border border-dark-500">
|
||||
<div class="flex items-center gap-3">
|
||||
<div v-if="selectedAgent" class="flex items-center gap-3">
|
||||
<div
|
||||
class="w-8 h-8 rounded-lg flex items-center justify-center text-lg shadow-lg"
|
||||
:style="{ backgroundColor: selectedAgent.accentColor + '20', color: selectedAgent.accentColor }"
|
||||
>
|
||||
{{ selectedAgent.avatar }}
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-medium text-white">{{ selectedAgent?.name || 'Chat' }}</div>
|
||||
<div class="text-[11px] flex items-center gap-1.5">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse"></span>
|
||||
<span class="text-white/40">Online</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-10 h-10 rounded-lg bg-dark-600 flex items-center justify-center">
|
||||
<i class="fa-solid fa-robot text-gray-400"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右上角操作 -->
|
||||
<div class="flex items-center gap-2">
|
||||
<button class="p-2 rounded-lg hover:bg-white/5 text-white/40 hover:text-white transition-colors">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="p-2 rounded-lg hover:bg-white/5 text-white/40 hover:text-white transition-colors">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<!-- 展开侧边栏按钮(仅在侧边栏隐藏时显示) -->
|
||||
<button
|
||||
v-if="sidebarCollapsed"
|
||||
@click="toggleSidebar"
|
||||
class="p-2 rounded-lg hover:bg-white/5 text-white/40 hover:text-white transition-colors"
|
||||
title="Show AI assistants"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 6h16M4 12h16M4 18h16"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 消息区域 -->
|
||||
<div ref="messagesContainer" class="flex-1 overflow-y-auto px-6 py-6">
|
||||
<div class="max-w-3xl mx-auto space-y-6">
|
||||
<div
|
||||
v-for="message in messages"
|
||||
:key="message.id"
|
||||
class="message-enter flex gap-4"
|
||||
:class="message.role === 'user' ? 'flex-row-reverse' : ''"
|
||||
>
|
||||
<!-- 头像 -->
|
||||
<div
|
||||
class="w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 shadow-lg"
|
||||
:class="message.role === 'user' ? 'bg-gradient-to-br from-emerald-500 to-teal-600' : ''"
|
||||
:style="message.role === 'assistant' && selectedAgent ? {
|
||||
backgroundColor: selectedAgent.accentColor + '20',
|
||||
color: selectedAgent.accentColor
|
||||
} : {}"
|
||||
>
|
||||
<span v-if="message.role === 'user'" class="text-white text-sm">👤</span>
|
||||
<span v-else class="text-lg">{{ selectedAgent?.avatar || '🧠' }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 消息内容 -->
|
||||
<div
|
||||
class="max-w-[75%] rounded-2xl px-4 py-3"
|
||||
:class="message.role === 'user' ? 'bg-[#1e1e28] text-white' : 'bg-transparent'"
|
||||
>
|
||||
<div class="text-sm leading-relaxed whitespace-pre-wrap text-white/90">{{ message.content }}
|
||||
<span v-if="message.isStreaming" class="inline-block w-0.5 h-4 ml-0.5 bg-violet-400 cursor-blink align-middle"></span>
|
||||
</div>
|
||||
|
||||
<!-- 消息底部 -->
|
||||
<div class="flex items-center justify-end mt-2 gap-3">
|
||||
<span class="text-[10px] text-white/25">{{ formatTime(message.timestamp) }}</span>
|
||||
<button
|
||||
v-if="message.role === 'assistant' && !message.isStreaming"
|
||||
@click="copyMessage(message.content)"
|
||||
class="text-white/25 hover:text-violet-400 transition-colors"
|
||||
title="Copy"
|
||||
>
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-white">{{ stats.total }}</div>
|
||||
<div class="text-xs text-gray-400">Total Agents</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 输入区域 -->
|
||||
<div class="p-4 border-t border-white/5 bg-[#0d0d12]/50">
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<div class="relative bg-[#12121a] rounded-2xl border border-white/8 focus-within:border-violet-500/40 focus-within:shadow-lg focus-within:shadow-violet-500/10 transition-all duration-300">
|
||||
<!-- 附件按钮 -->
|
||||
<button class="absolute left-4 top-1/2 -translate-y-1/2 text-white/30 hover:text-white/60 transition-colors p-1">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13"></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- 输入框 -->
|
||||
<textarea
|
||||
v-model="inputMessage"
|
||||
@keydown="handleKeydown"
|
||||
@input="autoResize"
|
||||
placeholder="Send a message..."
|
||||
rows="1"
|
||||
class="w-full bg-transparent text-white placeholder-white/30 py-3.5 pl-12 pr-24 resize-none focus:outline-none text-sm"
|
||||
></textarea>
|
||||
|
||||
<!-- 发送按钮 -->
|
||||
<button
|
||||
@click="sendMessage"
|
||||
:disabled="!inputMessage.trim() || isLoading"
|
||||
class="absolute right-2 top-1/2 -translate-y-1/2 p-2 rounded-xl bg-violet-500 hover:bg-violet-400 disabled:bg-white/8 disabled:text-white/20 text-white transition-all duration-200 hover:shadow-lg hover:shadow-violet-500/25"
|
||||
>
|
||||
<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="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="bg-dark-700 rounded-xl p-4 border border-dark-500">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-primary-success/20 flex items-center justify-center">
|
||||
<i class="fa-solid fa-circle-check text-primary-success"></i>
|
||||
</div>
|
||||
|
||||
<!-- 提示 -->
|
||||
<div class="text-center mt-2.5">
|
||||
<span class="text-[10px] text-white/20">AI can make mistakes. Please verify important information.</span>
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-primary-success">{{ stats.running }}</div>
|
||||
<div class="text-xs text-gray-400">Running</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-dark-700 rounded-xl p-4 border border-dark-500">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-gray-500/20 flex items-center justify-center">
|
||||
<i class="fa-solid fa-circle-stop text-gray-400"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-gray-400">{{ stats.stopped }}</div>
|
||||
<div class="text-xs text-gray-400">Stopped</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-dark-700 rounded-xl p-4 border border-dark-500">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-primary-cyan/20 flex items-center justify-center">
|
||||
<i class="fa-solid fa-plug text-primary-cyan"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-primary-cyan">{{ agents.reduce((sum, a) => sum + a.mcpServers, 0) }}</div>
|
||||
<div class="text-xs text-gray-400">MCP Servers</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧边栏 - 可折叠 -->
|
||||
<transition
|
||||
enter-active-class="transition-all duration-300 ease-out"
|
||||
enter-from-class="opacity-0 translate-x-4"
|
||||
enter-to-class="opacity-100 translate-x-0"
|
||||
leave-active-class="transition-all duration-250 ease-in"
|
||||
leave-from-class="opacity-100 translate-x-0"
|
||||
leave-to-class="opacity-0 translate-x-4"
|
||||
>
|
||||
<div v-show="!sidebarCollapsed" class="w-72 bg-[#0d0d12] border-l border-white/5 flex flex-col">
|
||||
<!-- Logo -->
|
||||
<div class="p-4 border-b border-white/5">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-9 h-9 rounded-xl bg-gradient-to-br from-violet-500 to-indigo-600 flex items-center justify-center shadow-lg shadow-violet-500/25">
|
||||
<span class="text-white text-lg">🤖</span>
|
||||
</div>
|
||||
<span class="text-lg font-semibold text-white tracking-tight">AI Hub</span>
|
||||
</div>
|
||||
<button
|
||||
@click="toggleSidebar"
|
||||
class="p-1.5 rounded-lg hover:bg-white/5 text-white/30 hover:text-white/60 transition-colors"
|
||||
title="Hide sidebar"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 搜索和筛选 -->
|
||||
<div class="flex gap-4 mb-6">
|
||||
<div class="flex-1 relative">
|
||||
<i class="fa-solid fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"></i>
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="Search agents by name or framework..."
|
||||
class="search-input w-full"
|
||||
>
|
||||
</div>
|
||||
<el-select v-model="filterStatus" placeholder="Select" class="w-40" size="large">
|
||||
<el-option label="All Status" value="all" />
|
||||
<el-option label="Running" value="running" />
|
||||
<el-option label="Stopped" value="stopped" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<!-- 新建聊天按钮 -->
|
||||
<div class="p-4">
|
||||
<button
|
||||
@click="newChat"
|
||||
class="w-full py-2.5 px-4 bg-[#1a1a24] hover:bg-[#22222e] border border-white/8 hover:border-violet-500/30 rounded-xl text-white/90 text-sm flex items-center justify-center gap-2 transition-all duration-200 hover:shadow-lg hover:shadow-violet-500/10 group"
|
||||
>
|
||||
<svg class="w-4 h-4 text-violet-400 group-hover:rotate-90 transition-transform duration-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
|
||||
</svg>
|
||||
<span class="font-medium">New Chat</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- AI 助手列表 -->
|
||||
<div class="flex-1 overflow-y-auto px-3 py-2">
|
||||
<div class="text-[11px] font-medium text-white/30 uppercase tracking-wider px-3 mb-3">AI Assistants</div>
|
||||
<div class="space-y-1">
|
||||
<div
|
||||
v-for="agent in agents"
|
||||
:key="agent.id"
|
||||
@click="selectAgent(agent)"
|
||||
class="group px-3 py-2.5 rounded-xl cursor-pointer transition-all duration-200"
|
||||
:class="selectedAgent?.id === agent.id
|
||||
? 'bg-gradient-to-r ' + agent.gradient + ' border-l-2'
|
||||
: 'hover:bg-white/[0.03] border-l-2 border-transparent'"
|
||||
:style="selectedAgent?.id === agent.id ? `border-left-color: ${agent.accentColor}` : ''"
|
||||
>
|
||||
<!-- Agents 列表 -->
|
||||
<div class="bg-dark-700 rounded-xl overflow-hidden">
|
||||
<table class="w-full">
|
||||
<thead class="bg-dark-600">
|
||||
<tr>
|
||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Agent Name</th>
|
||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Framework</th>
|
||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Model</th>
|
||||
<th class="text-center px-5 py-3 text-sm font-medium text-gray-400">MCP</th>
|
||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Status</th>
|
||||
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Created</th>
|
||||
<th class="text-right px-5 py-3 text-sm font-medium text-gray-400">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="agent in filteredAgents" :key="agent.id" class="table-row">
|
||||
<td class="px-5 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="w-8 h-8 rounded-lg flex items-center justify-center text-lg transition-transform duration-200 group-hover:scale-110"
|
||||
:class="selectedAgent?.id === agent.id ? 'shadow-lg' : ''"
|
||||
class="w-10 h-10 rounded-lg flex items-center justify-center text-lg"
|
||||
:style="{ backgroundColor: agent.accentColor + '20', color: agent.accentColor }"
|
||||
>
|
||||
{{ agent.avatar }}
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-sm font-medium text-white/90 truncate">{{ agent.name }}</div>
|
||||
<div class="text-[11px] text-white/40 truncate">{{ agent.description }}</div>
|
||||
<div>
|
||||
<div class="font-medium text-white">{{ agent.name }}</div>
|
||||
<div class="text-xs text-gray-500">{{ agent.description }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-5 py-4">
|
||||
<span class="bg-dark-500 px-2 py-1 rounded text-sm text-gray-300">{{ agent.framework }}</span>
|
||||
</td>
|
||||
<td class="px-5 py-4 text-gray-300">{{ agent.model }}</td>
|
||||
<td class="px-5 py-4 text-center">
|
||||
<span class="text-primary-cyan">{{ agent.mcpServers }}</span>
|
||||
</td>
|
||||
<td class="px-5 py-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-2 h-2 rounded-full" :class="statusClass(agent.status)"></span>
|
||||
<span class="capitalize text-sm text-gray-300">{{ agent.status }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-5 py-4 text-gray-400 text-sm">{{ agent.createdAt }}</td>
|
||||
<td class="px-5 py-4">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<button
|
||||
@click="toggleStatus(agent)"
|
||||
class="btn-icon"
|
||||
:title="agent.status === 'running' ? 'Stop' : 'Start'"
|
||||
>
|
||||
<i :class="['fa-solid', agent.status === 'running' ? 'fa-stop' : 'fa-play', 'text-gray-400']"></i>
|
||||
</button>
|
||||
<button class="btn-icon" title="Edit">
|
||||
<i class="fa-solid fa-pen text-gray-400"></i>
|
||||
</button>
|
||||
<button class="btn-icon" title="Settings">
|
||||
<i class="fa-solid fa-gear text-gray-400"></i>
|
||||
</button>
|
||||
<button
|
||||
@click="deleteAgent(agent.id)"
|
||||
class="btn-icon"
|
||||
title="Delete"
|
||||
>
|
||||
<i class="fa-solid fa-trash text-gray-400 hover:text-primary-danger"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- 底部设置 -->
|
||||
<div class="p-4 border-t border-white/5">
|
||||
<button class="w-full py-2.5 rounded-xl bg-white/[0.02] hover:bg-white/[0.05] text-white/50 hover:text-white/80 text-sm flex items-center justify-center gap-2 transition-all duration-200">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
</svg>
|
||||
<span>Settings</span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- 空状态 -->
|
||||
<div v-if="filteredAgents.length === 0" class="py-12 text-center text-gray-500">
|
||||
<i class="fa-solid fa-robot text-4xl mb-3"></i>
|
||||
<p>No agents found</p>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
430
web/src/views/Chat.vue
Normal file
430
web/src/views/Chat.vue
Normal file
@@ -0,0 +1,430 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick } from 'vue'
|
||||
|
||||
interface ChatMessage {
|
||||
id: number
|
||||
role: 'user' | 'assistant'
|
||||
content: string
|
||||
timestamp: Date
|
||||
isStreaming?: boolean
|
||||
}
|
||||
|
||||
interface Agent {
|
||||
id: number
|
||||
name: string
|
||||
avatar: string
|
||||
description: string
|
||||
accentColor: string
|
||||
gradient: string
|
||||
status: 'online' | 'offline'
|
||||
}
|
||||
|
||||
// AI 助手配置
|
||||
const chatAgents = ref<Agent[]>([
|
||||
{ id: 1, name: 'Claude', avatar: '🧠', description: 'Anthropic AI', accentColor: '#f97316', gradient: 'from-orange-500/20 to-amber-500/20', status: 'online' },
|
||||
{ id: 2, name: 'Gemini', avatar: '✨', description: 'Google DeepMind', accentColor: '#8b5cf6', gradient: 'from-violet-500/20 to-purple-500/20', status: 'online' },
|
||||
{ id: 3, name: 'ChatGPT', avatar: '💬', description: 'OpenAI', accentColor: '#10b981', gradient: 'from-emerald-500/20 to-green-500/20', status: 'offline' },
|
||||
{ id: 4, name: 'DeepSeek', avatar: '🔮', description: 'DeepSeek AI', accentColor: '#3b82f6', gradient: 'from-blue-500/20 to-cyan-500/20', status: 'online' },
|
||||
{ id: 5, name: 'Kimi', avatar: '🌙', description: 'Moonshot AI', accentColor: '#ec4899', gradient: 'from-pink-500/20 to-rose-500/20', status: 'online' },
|
||||
{ id: 6, name: '文心一言', avatar: '🐉', description: 'Baidu', accentColor: '#ef4444', gradient: 'from-red-500/20 to-orange-500/20', status: 'offline' },
|
||||
{ id: 7, name: '通义千问', avatar: '☁️', description: 'Alibaba', accentColor: '#06b6d4', gradient: 'from-cyan-500/20 to-sky-500/20', status: 'online' },
|
||||
])
|
||||
|
||||
// 当前选中的助手
|
||||
const selectedAgent = ref<Agent | null>(chatAgents.value[0])
|
||||
const sidebarCollapsed = ref(false)
|
||||
|
||||
// 聊天消息
|
||||
const messages = ref<ChatMessage[]>([
|
||||
{ id: 1, role: 'assistant', content: '你好!我是 Claude,你的 AI 助手。有什么我可以帮助你的吗?', timestamp: new Date() },
|
||||
])
|
||||
|
||||
// 输入内容
|
||||
const inputMessage = ref('')
|
||||
const isLoading = ref(false)
|
||||
const messagesContainer = ref<HTMLElement | null>(null)
|
||||
|
||||
// 发送消息
|
||||
const sendMessage = async () => {
|
||||
if (!inputMessage.value.trim() || isLoading.value) return
|
||||
|
||||
const userContent = inputMessage.value.trim()
|
||||
inputMessage.value = ''
|
||||
|
||||
const userMessage: ChatMessage = {
|
||||
id: Date.now(),
|
||||
role: 'user',
|
||||
content: userContent,
|
||||
timestamp: new Date()
|
||||
}
|
||||
messages.value.push(userMessage)
|
||||
|
||||
const aiMessage: ChatMessage = {
|
||||
id: Date.now() + 1,
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
timestamp: new Date(),
|
||||
isStreaming: true
|
||||
}
|
||||
messages.value.push(aiMessage)
|
||||
|
||||
nextTick(() => scrollToBottom())
|
||||
|
||||
isLoading.value = true
|
||||
const fullResponse = `我理解你发送了消息: "${userContent}"
|
||||
|
||||
作为 AI 助手,我可以帮助你:
|
||||
• 回答各种问题
|
||||
• 编写代码和调试
|
||||
• 分析和处理数据
|
||||
• 翻译和写作
|
||||
• 头脑风暴和创意建议
|
||||
|
||||
请告诉我你需要什么帮助?`
|
||||
|
||||
let currentIndex = 0
|
||||
const words = fullResponse.split('')
|
||||
|
||||
const streamInterval = setInterval(() => {
|
||||
if (currentIndex < words.length) {
|
||||
aiMessage.content += words[currentIndex]
|
||||
currentIndex++
|
||||
nextTick(() => scrollToBottom())
|
||||
} else {
|
||||
clearInterval(streamInterval)
|
||||
aiMessage.isStreaming = false
|
||||
isLoading.value = false
|
||||
}
|
||||
}, 30)
|
||||
}
|
||||
|
||||
// 滚动到底部
|
||||
const scrollToBottom = () => {
|
||||
if (messagesContainer.value) {
|
||||
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
|
||||
}
|
||||
}
|
||||
|
||||
// 复制消息
|
||||
const copyMessage = (content: string) => {
|
||||
navigator.clipboard.writeText(content)
|
||||
}
|
||||
|
||||
// 选择助手
|
||||
const selectAgent = (agent: Agent) => {
|
||||
selectedAgent.value = agent
|
||||
messages.value = [
|
||||
{ id: 1, role: 'assistant', content: `你好!我是 ${agent.name}。有什么我可以帮助你的吗?`, timestamp: new Date() }
|
||||
]
|
||||
}
|
||||
|
||||
// 新建聊天
|
||||
const newChat = () => {
|
||||
messages.value = [
|
||||
{ id: 1, role: 'assistant', content: `你好!我是 ${selectedAgent.value?.name || 'Claude'}。有什么我可以帮助你的吗?`, timestamp: new Date() }
|
||||
]
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (date: Date) => {
|
||||
return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
|
||||
}
|
||||
|
||||
// 回车发送
|
||||
const handleKeydown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
sendMessage()
|
||||
}
|
||||
}
|
||||
|
||||
// 调整输入框高度
|
||||
const autoResize = (e: Event) => {
|
||||
const target = e.target as HTMLTextAreaElement
|
||||
target.style.height = 'auto'
|
||||
target.style.height = Math.min(target.scrollHeight, 160) + 'px'
|
||||
}
|
||||
|
||||
// 切换侧边栏
|
||||
const toggleSidebar = () => {
|
||||
sidebarCollapsed.value = !sidebarCollapsed.value
|
||||
setTimeout(() => {
|
||||
scrollToBottom()
|
||||
}, 350)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
@keyframes messageSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(12px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.message-enter {
|
||||
animation: messageSlideIn 0.35s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0; }
|
||||
}
|
||||
|
||||
.cursor-blink {
|
||||
animation: blink 1s step-end infinite;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div class="h-screen flex bg-[#0a0a0f]">
|
||||
<!-- 主聊天区域 -->
|
||||
<div class="flex-1 flex flex-col bg-[#0a0a0f]">
|
||||
<!-- 顶部栏 -->
|
||||
<div class="h-14 px-6 flex items-center justify-between border-b border-white/5 bg-[#0d0d12]/50 backdrop-blur-sm">
|
||||
<!-- 左侧:当前AI信息 -->
|
||||
<div class="flex items-center gap-3">
|
||||
<div v-if="selectedAgent" class="flex items-center gap-3">
|
||||
<div
|
||||
class="w-8 h-8 rounded-lg flex items-center justify-center text-lg shadow-lg"
|
||||
:style="{ backgroundColor: selectedAgent.accentColor + '20', color: selectedAgent.accentColor }"
|
||||
>
|
||||
{{ selectedAgent.avatar }}
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-medium text-white">{{ selectedAgent?.name || 'Chat' }}</div>
|
||||
<div class="text-[11px] flex items-center gap-1.5">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse"></span>
|
||||
<span class="text-white/40">Online</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右上角操作 -->
|
||||
<div class="flex items-center gap-2">
|
||||
<button class="p-2 rounded-lg hover:bg-white/5 text-white/40 hover:text-white transition-colors">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="p-2 rounded-lg hover:bg-white/5 text-white/40 hover:text-white transition-colors">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 5v.01M12 12v.01M12 19v.01M12 6a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2zm0 7a1 1 0 110-2 1 1 0 010 2z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<!-- 展开侧边栏按钮 -->
|
||||
<button
|
||||
v-if="sidebarCollapsed"
|
||||
@click="toggleSidebar"
|
||||
class="p-2 rounded-lg hover:bg-white/5 text-white/40 hover:text-white transition-colors"
|
||||
title="Show AI assistants"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 6h16M4 12h16M4 18h16"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 消息区域 -->
|
||||
<div ref="messagesContainer" class="flex-1 overflow-y-auto px-6 py-6">
|
||||
<div class="max-w-3xl mx-auto space-y-6">
|
||||
<div
|
||||
v-for="message in messages"
|
||||
:key="message.id"
|
||||
class="message-enter flex gap-4"
|
||||
:class="message.role === 'user' ? 'flex-row-reverse' : ''"
|
||||
>
|
||||
<!-- 头像 -->
|
||||
<div
|
||||
class="w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 shadow-lg"
|
||||
:class="message.role === 'user' ? 'bg-gradient-to-br from-emerald-500 to-teal-600' : ''"
|
||||
:style="message.role === 'assistant' && selectedAgent ? {
|
||||
backgroundColor: selectedAgent.accentColor + '20',
|
||||
color: selectedAgent.accentColor
|
||||
} : {}"
|
||||
>
|
||||
<span v-if="message.role === 'user'" class="text-white text-sm">👤</span>
|
||||
<span v-else class="text-lg">{{ selectedAgent?.avatar || '🧠' }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 消息内容 -->
|
||||
<div
|
||||
class="max-w-[75%] rounded-2xl px-4 py-3"
|
||||
:class="message.role === 'user' ? 'bg-[#1e1e28] text-white' : 'bg-transparent'"
|
||||
>
|
||||
<div class="text-sm leading-relaxed whitespace-pre-wrap text-white/90">{{ message.content }}
|
||||
<span v-if="message.isStreaming" class="inline-block w-0.5 h-4 ml-0.5 bg-violet-400 cursor-blink align-middle"></span>
|
||||
</div>
|
||||
|
||||
<!-- 消息底部 -->
|
||||
<div class="flex items-center justify-end mt-2 gap-3">
|
||||
<span class="text-[10px] text-white/25">{{ formatTime(message.timestamp) }}</span>
|
||||
<button
|
||||
v-if="message.role === 'assistant' && !message.isStreaming"
|
||||
@click="copyMessage(message.content)"
|
||||
class="text-white/25 hover:text-violet-400 transition-colors"
|
||||
title="Copy"
|
||||
>
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 输入区域 -->
|
||||
<div class="p-4 border-t border-white/5 bg-[#0d0d12]/50">
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<div class="relative bg-[#12121a] rounded-2xl border border-white/8 focus-within:border-violet-500/40 focus-within:shadow-lg focus-within:shadow-violet-500/10 transition-all duration-300">
|
||||
<!-- 附件按钮 -->
|
||||
<button class="absolute left-4 top-1/2 -translate-y-1/2 text-white/30 hover:text-white/60 transition-colors p-1">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13"></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- 输入框 -->
|
||||
<textarea
|
||||
v-model="inputMessage"
|
||||
@keydown="handleKeydown"
|
||||
@input="autoResize"
|
||||
placeholder="Send a message..."
|
||||
rows="1"
|
||||
class="w-full bg-transparent text-white placeholder-white/30 py-3.5 pl-12 pr-24 resize-none focus:outline-none text-sm"
|
||||
></textarea>
|
||||
|
||||
<!-- 发送按钮 -->
|
||||
<button
|
||||
@click="sendMessage"
|
||||
:disabled="!inputMessage.trim() || isLoading"
|
||||
class="absolute right-2 top-1/2 -translate-y-1/2 p-2 rounded-xl bg-violet-500 hover:bg-violet-400 disabled:bg-white/8 disabled:text-white/20 text-white transition-all duration-200 hover:shadow-lg hover:shadow-violet-500/25"
|
||||
>
|
||||
<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="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 提示 -->
|
||||
<div class="text-center mt-2.5">
|
||||
<span class="text-[10px] text-white/20">AI can make mistakes. Please verify important information.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧边栏 - 可折叠 -->
|
||||
<transition
|
||||
enter-active-class="transition-all duration-300 ease-out"
|
||||
enter-from-class="opacity-0 translate-x-4"
|
||||
enter-to-class="opacity-100 translate-x-0"
|
||||
leave-active-class="transition-all duration-250 ease-in"
|
||||
leave-from-class="opacity-100 translate-x-0"
|
||||
leave-to-class="opacity-0 translate-x-4"
|
||||
>
|
||||
<div v-show="!sidebarCollapsed" class="w-72 bg-[#0d0d12] border-l border-white/5 flex flex-col">
|
||||
<!-- Logo -->
|
||||
<div class="p-4 border-b border-white/5">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-9 h-9 rounded-xl bg-gradient-to-br from-violet-500 to-indigo-600 flex items-center justify-center shadow-lg shadow-violet-500/25">
|
||||
<span class="text-white text-lg">🤖</span>
|
||||
</div>
|
||||
<span class="text-lg font-semibold text-white tracking-tight">AI Hub</span>
|
||||
</div>
|
||||
<button
|
||||
@click="toggleSidebar"
|
||||
class="p-1.5 rounded-lg hover:bg-white/5 text-white/30 hover:text-white/60 transition-colors"
|
||||
title="Hide sidebar"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新建聊天按钮 -->
|
||||
<div class="p-4">
|
||||
<button
|
||||
@click="newChat"
|
||||
class="w-full py-2.5 px-4 bg-[#1a1a24] hover:bg-[#22222e] border border-white/8 hover:border-violet-500/30 rounded-xl text-white/90 text-sm flex items-center justify-center gap-2 transition-all duration-200 hover:shadow-lg hover:shadow-violet-500/10 group"
|
||||
>
|
||||
<svg class="w-4 h-4 text-violet-400 group-hover:rotate-90 transition-transform duration-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
|
||||
</svg>
|
||||
<span class="font-medium">New Chat</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- AI 助手列表 -->
|
||||
<div class="flex-1 overflow-y-auto px-3 py-2">
|
||||
<div class="text-[11px] font-medium text-white/30 uppercase tracking-wider px-3 mb-3">AI Assistants</div>
|
||||
<div class="space-y-1">
|
||||
<div
|
||||
v-for="agent in chatAgents"
|
||||
:key="agent.id"
|
||||
@click="selectAgent(agent)"
|
||||
class="group px-3 py-2.5 rounded-xl cursor-pointer transition-all duration-200"
|
||||
:class="selectedAgent?.id === agent.id
|
||||
? 'bg-gradient-to-r ' + agent.gradient + ' border-l-2'
|
||||
: 'hover:bg-white/[0.03] border-l-2 border-transparent'"
|
||||
:style="selectedAgent?.id === agent.id ? `border-left-color: ${agent.accentColor}` : ''"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="w-8 h-8 rounded-lg flex items-center justify-center text-lg transition-transform duration-200 group-hover:scale-110"
|
||||
:class="selectedAgent?.id === agent.id ? 'shadow-lg' : ''"
|
||||
:style="{ backgroundColor: agent.accentColor + '20', color: agent.accentColor }"
|
||||
>
|
||||
{{ agent.avatar }}
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-sm font-medium text-white/90 truncate">{{ agent.name }}</div>
|
||||
<div class="text-[11px] text-white/40 truncate">{{ agent.description }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部设置 -->
|
||||
<div class="p-4 border-t border-white/5">
|
||||
<button class="w-full py-2.5 rounded-xl bg-white/[0.02] hover:bg-white/[0.05] text-white/50 hover:text-white/80 text-sm flex items-center justify-center gap-2 transition-all duration-200">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
</svg>
|
||||
<span>Settings</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user