Files
X-Agents/web/src/views/Agents.vue
DESKTOP-72TV0V4\caoxiaozhu cac05b4297 feat: 优化前端页面和组件
- 重构 Agents 页面
- 优化 Knowledge 页面
- 更新侧边栏导航
- 添加前端依赖

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 15:42:21 +08:00

430 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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
}
// AI 助手配置
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' },
])
// 当前选中的助手
const selectedAgent = ref<Agent | null>(agents.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 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}` : ''"
>
<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>