2026-03-10 16:09:09 +08:00
|
|
|
|
<script setup lang="ts">
|
2026-03-11 17:22:47 +08:00
|
|
|
|
import { ref, nextTick, onMounted, onUnmounted } from 'vue'
|
2026-03-10 16:09:09 +08:00
|
|
|
|
|
2026-03-11 16:26:10 +08:00
|
|
|
|
const API_BASE = 'http://localhost:8082'
|
|
|
|
|
|
|
2026-03-11 17:22:47 +08:00
|
|
|
|
// 模型列表
|
|
|
|
|
|
interface ChatModel {
|
|
|
|
|
|
id: string
|
|
|
|
|
|
name: string
|
|
|
|
|
|
model_type: string
|
|
|
|
|
|
provider: string
|
|
|
|
|
|
model: string
|
|
|
|
|
|
status: string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const chatModels = ref<ChatModel[]>([])
|
|
|
|
|
|
const selectedModel = ref<ChatModel | null>(null)
|
|
|
|
|
|
const modelsLoading = ref(false)
|
|
|
|
|
|
const showModelDropdown = ref(false)
|
|
|
|
|
|
|
|
|
|
|
|
// 根据 provider 获取图标
|
|
|
|
|
|
const getModelIcon = (provider: string) => {
|
|
|
|
|
|
const icons: Record<string, string> = {
|
|
|
|
|
|
'OpenAI': '🤖',
|
|
|
|
|
|
'Claude': '🧠',
|
|
|
|
|
|
'Google': '✨',
|
|
|
|
|
|
'Gemini': '✨',
|
|
|
|
|
|
'Ollama': '🦙',
|
|
|
|
|
|
'DeepSeek': '🔮',
|
|
|
|
|
|
'Moonshot': '🌙',
|
|
|
|
|
|
'Kimi': '🌙',
|
|
|
|
|
|
'Baidu': '🐉',
|
|
|
|
|
|
'文心一言': '🐉',
|
|
|
|
|
|
'Aliyun': '☁️',
|
|
|
|
|
|
'Ali': '☁️',
|
|
|
|
|
|
'通义千问': '☁️',
|
|
|
|
|
|
'Azure': '⬛',
|
|
|
|
|
|
'Anthropic': '🧠',
|
|
|
|
|
|
}
|
|
|
|
|
|
return icons[provider] || '💬'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取模型列表
|
|
|
|
|
|
const fetchModels = async () => {
|
|
|
|
|
|
modelsLoading.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(`${API_BASE}/model/list`)
|
|
|
|
|
|
const data = await response.json()
|
|
|
|
|
|
if (data.list) {
|
|
|
|
|
|
// 过滤出 chat 类型且 active 状态的模型
|
|
|
|
|
|
chatModels.value = data.list.filter((m: ChatModel) => m.model_type === 'chat' && m.status === 'active')
|
|
|
|
|
|
console.log('Chat models:', chatModels.value)
|
|
|
|
|
|
// 默认选择第一个
|
|
|
|
|
|
if (chatModels.value.length > 0 && !selectedModel.value) {
|
|
|
|
|
|
selectedModel.value = chatModels.value[0]
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Failed to fetch models:', error)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
modelsLoading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
fetchModels()
|
|
|
|
|
|
// 点击外部关闭下拉框
|
|
|
|
|
|
document.addEventListener('click', handleClickOutside)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
|
document.removeEventListener('click', handleClickOutside)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 点击外部关闭下拉框
|
|
|
|
|
|
const handleClickOutside = (e: MouseEvent) => {
|
|
|
|
|
|
const target = e.target as HTMLElement
|
|
|
|
|
|
if (!target.closest('.model-dropdown')) {
|
|
|
|
|
|
showModelDropdown.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 16:09:09 +08:00
|
|
|
|
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'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 17:38:57 +08:00
|
|
|
|
interface ChatSession {
|
|
|
|
|
|
id: number
|
|
|
|
|
|
title: string
|
|
|
|
|
|
agentId: number
|
|
|
|
|
|
lastMessage: string
|
|
|
|
|
|
timestamp: Date
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 16:09:09 +08:00
|
|
|
|
// 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 messages = ref<ChatMessage[]>([
|
|
|
|
|
|
{ id: 1, role: 'assistant', content: '你好!我是 Claude,你的 AI 助手。有什么我可以帮助你的吗?', timestamp: new Date() },
|
|
|
|
|
|
])
|
|
|
|
|
|
|
2026-03-10 17:38:57 +08:00
|
|
|
|
// 模拟历史对话列表
|
|
|
|
|
|
const chatSessions = ref<ChatSession[]>([
|
|
|
|
|
|
{ id: 1, title: '关于 Python 学习的讨论', agentId: 1, lastMessage: '谢谢你!', timestamp: new Date(Date.now() - 3600000) },
|
|
|
|
|
|
{ id: 2, title: '代码调试帮助', agentId: 1, lastMessage: '让我看看这个问题...', timestamp: new Date(Date.now() - 7200000) },
|
|
|
|
|
|
{ id: 3, title: '数据分析咨询', agentId: 4, lastMessage: 'DeepSeek: 好的', timestamp: new Date(Date.now() - 86400000) },
|
|
|
|
|
|
])
|
|
|
|
|
|
|
2026-03-11 14:26:25 +08:00
|
|
|
|
// 群聊数据
|
|
|
|
|
|
interface GroupChat {
|
|
|
|
|
|
id: number
|
|
|
|
|
|
name: string
|
|
|
|
|
|
members: string[]
|
|
|
|
|
|
lastMessage: string
|
|
|
|
|
|
timestamp: Date
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const groupChats = ref<GroupChat[]>([
|
|
|
|
|
|
{ id: 1, name: 'AI 讨论组', members: ['Claude', 'GPT-4', 'Gemini'], lastMessage: '我们来讨论一下...', timestamp: new Date(Date.now() - 1800000) },
|
|
|
|
|
|
{ id: 2, name: '编程助手', members: ['Claude', 'DeepSeek'], lastMessage: '这段代码有问题吗?', timestamp: new Date(Date.now() - 3600000) },
|
|
|
|
|
|
{ id: 3, name: '创意头脑风暴', members: ['GPT-4', 'Claude', 'Kimi'], lastMessage: '有个新想法...', timestamp: new Date(Date.now() - 7200000) },
|
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
|
|
// 智能体选择弹窗状态
|
|
|
|
|
|
const showAgentSelector = ref(false)
|
|
|
|
|
|
const selectMode = ref<'single' | 'group'>('single')
|
|
|
|
|
|
const selectedAgents = ref<Agent[]>([])
|
|
|
|
|
|
const groupChatName = ref('')
|
|
|
|
|
|
|
|
|
|
|
|
// 打开智能体选择器
|
|
|
|
|
|
const openAgentSelector = (mode: 'single' | 'group') => {
|
|
|
|
|
|
selectMode.value = mode
|
|
|
|
|
|
selectedAgents.value = []
|
|
|
|
|
|
groupChatName.value = ''
|
|
|
|
|
|
showAgentSelector.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 切换智能体选择(群聊模式)
|
|
|
|
|
|
const toggleAgentSelection = (agent: Agent) => {
|
|
|
|
|
|
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 = () => {
|
|
|
|
|
|
if (selectMode.value === 'single') {
|
|
|
|
|
|
// 单聊模式:选择一个智能体开始对话
|
|
|
|
|
|
if (selectedAgents.value.length > 0) {
|
|
|
|
|
|
selectedAgent.value = selectedAgents.value[0]
|
|
|
|
|
|
messages.value = [
|
|
|
|
|
|
{ id: 1, role: 'assistant', content: `你好!我是 ${selectedAgent.value.name},你的 AI 助手。有什么我可以帮助你的吗?`, timestamp: new Date() }
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 群聊模式:选择多个智能体
|
|
|
|
|
|
const name = groupChatName.value.trim() || `群聊 (${selectedAgents.value.length}人)`
|
|
|
|
|
|
console.log('创建群聊:', { name, members: selectedAgents.value })
|
|
|
|
|
|
// 添加到群聊列表
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 17:38:57 +08:00
|
|
|
|
// 侧边栏展开/收起状态
|
|
|
|
|
|
const sidebarCollapsed = ref(false)
|
|
|
|
|
|
|
2026-03-10 16:09:09 +08:00
|
|
|
|
// 输入内容
|
|
|
|
|
|
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
|
2026-03-11 16:26:10 +08:00
|
|
|
|
|
|
|
|
|
|
try {
|
2026-03-11 17:22:47 +08:00
|
|
|
|
// 构建请求体,包含模型信息
|
|
|
|
|
|
const requestBody: any = {
|
|
|
|
|
|
agent_id: selectedAgent.value?.id || 1,
|
|
|
|
|
|
message: userContent,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果选择了模型,只传递 model_id(后端会根据 id 查询 api_key 和 base_url)
|
|
|
|
|
|
if (selectedModel.value) {
|
|
|
|
|
|
requestBody.model_id = selectedModel.value.id
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 16:26:10 +08:00
|
|
|
|
// 调用后端 API
|
|
|
|
|
|
const response = await fetch(`${API_BASE}/api/agent/chat`, {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
|
},
|
2026-03-11 17:22:47 +08:00
|
|
|
|
body: JSON.stringify(requestBody),
|
2026-03-11 16:26:10 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json()
|
|
|
|
|
|
const fullResponse = data.reply || data.response || 'No response'
|
|
|
|
|
|
|
|
|
|
|
|
// 流式显示回复
|
|
|
|
|
|
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)
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
aiMessage.content = `Error: ${error.message || 'Failed to send message'}`
|
|
|
|
|
|
aiMessage.isStreaming = false
|
|
|
|
|
|
isLoading.value = false
|
|
|
|
|
|
}
|
2026-03-10 16:09:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 滚动到底部
|
|
|
|
|
|
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() }
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 17:38:57 +08:00
|
|
|
|
// 选择历史对话
|
|
|
|
|
|
const selectSession = (session: ChatSession) => {
|
|
|
|
|
|
const agent = chatAgents.value.find(a => a.id === session.agentId)
|
|
|
|
|
|
if (agent) {
|
|
|
|
|
|
selectedAgent.value = agent
|
|
|
|
|
|
}
|
|
|
|
|
|
messages.value = [
|
|
|
|
|
|
{ id: 1, role: 'assistant', content: `已加载会话:${session.title}`, timestamp: new Date() }
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 16:09:09 +08:00
|
|
|
|
// 新建聊天
|
|
|
|
|
|
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' })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 17:38:57 +08:00
|
|
|
|
// 格式化相对时间
|
|
|
|
|
|
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')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 16:09:09 +08:00
|
|
|
|
// 回车发送
|
|
|
|
|
|
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'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 17:38:57 +08:00
|
|
|
|
// 折叠侧边栏
|
2026-03-10 16:09:09 +08:00
|
|
|
|
const toggleSidebar = () => {
|
|
|
|
|
|
sidebarCollapsed.value = !sidebarCollapsed.value
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
::-webkit-scrollbar {
|
2026-03-10 17:38:57 +08:00
|
|
|
|
width: 6px;
|
2026-03-10 16:09:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
::-webkit-scrollbar-track {
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
::-webkit-scrollbar-thumb {
|
2026-03-10 17:38:57 +08:00
|
|
|
|
background: rgba(255, 255, 255, 0.08);
|
|
|
|
|
|
border-radius: 3px;
|
2026-03-10 16:09:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
::-webkit-scrollbar-thumb:hover {
|
2026-03-10 17:38:57 +08:00
|
|
|
|
background: rgba(255, 255, 255, 0.15);
|
2026-03-10 16:09:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 17:22:47 +08:00
|
|
|
|
/* 下拉框动画 */
|
|
|
|
|
|
.dropdown-enter-active,
|
|
|
|
|
|
.dropdown-leave-active {
|
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.dropdown-enter-from,
|
|
|
|
|
|
.dropdown-leave-to {
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
transform: translateY(-8px);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 16:09:09 +08:00
|
|
|
|
@keyframes messageSlideIn {
|
|
|
|
|
|
from {
|
|
|
|
|
|
opacity: 0;
|
2026-03-10 17:38:57 +08:00
|
|
|
|
transform: translateY(16px) scale(0.96);
|
2026-03-10 16:09:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
to {
|
|
|
|
|
|
opacity: 1;
|
2026-03-10 17:38:57 +08:00
|
|
|
|
transform: translateY(0) scale(1);
|
2026-03-10 16:09:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.message-enter {
|
2026-03-10 17:38:57 +08:00
|
|
|
|
animation: messageSlideIn 0.4s cubic-bezier(0.22, 1, 0.36, 1) forwards;
|
2026-03-10 16:09:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes blink {
|
|
|
|
|
|
0%, 100% { opacity: 1; }
|
|
|
|
|
|
50% { opacity: 0; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.cursor-blink {
|
|
|
|
|
|
animation: blink 1s step-end infinite;
|
|
|
|
|
|
}
|
2026-03-10 17:38:57 +08:00
|
|
|
|
|
|
|
|
|
|
@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;
|
|
|
|
|
|
}
|
2026-03-10 16:09:09 +08:00
|
|
|
|
</style>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
2026-03-10 17:38:57 +08:00
|
|
|
|
<div class="h-screen flex bg-[#09090b]">
|
2026-03-10 16:09:09 +08:00
|
|
|
|
<!-- 主聊天区域 -->
|
2026-03-10 17:38:57 +08:00
|
|
|
|
<div class="flex-1 flex flex-col bg-[#09090b]">
|
2026-03-10 16:09:09 +08:00
|
|
|
|
<!-- 顶部栏 -->
|
2026-03-10 17:38:57 +08:00
|
|
|
|
<div class="h-16 px-4 flex items-center justify-between border-b border-white/[0.06] bg-[#0c0c0f]/80 backdrop-blur-xl">
|
2026-03-10 16:09:09 +08:00
|
|
|
|
<!-- 左侧:当前AI信息 -->
|
|
|
|
|
|
<div class="flex items-center gap-3">
|
|
|
|
|
|
<div v-if="selectedAgent" class="flex items-center gap-3">
|
|
|
|
|
|
<div
|
2026-03-10 17:38:57 +08:00
|
|
|
|
class="w-9 h-9 rounded-xl flex items-center justify-center text-lg shadow-lg"
|
|
|
|
|
|
:style="{ backgroundColor: selectedAgent.accentColor + '15', color: selectedAgent.accentColor }"
|
2026-03-10 16:09:09 +08:00
|
|
|
|
>
|
|
|
|
|
|
{{ selectedAgent.avatar }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
2026-03-10 17:38:57 +08:00
|
|
|
|
<div class="text-sm font-semibold text-white tracking-wide">{{ selectedAgent?.name || 'Chat' }}</div>
|
2026-03-10 16:09:09 +08:00
|
|
|
|
<div class="text-[11px] flex items-center gap-1.5">
|
2026-03-10 17:38:57 +08:00
|
|
|
|
<span class="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse"></span>
|
2026-03-10 16:09:09 +08:00
|
|
|
|
<span class="text-white/40">Online</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-10 17:38:57 +08:00
|
|
|
|
<!-- 中间:空白 -->
|
|
|
|
|
|
<div class="flex-1"></div>
|
|
|
|
|
|
|
2026-03-11 17:22:47 +08:00
|
|
|
|
<!-- 右侧:模型选择和折叠按钮 -->
|
|
|
|
|
|
<div class="flex items-center gap-3">
|
|
|
|
|
|
<!-- 模型选择下拉框 -->
|
|
|
|
|
|
<div class="relative model-dropdown" v-if="chatModels.length > 0">
|
|
|
|
|
|
<button
|
|
|
|
|
|
@click="showModelDropdown = !showModelDropdown"
|
|
|
|
|
|
class="flex items-center gap-2 px-3 py-1.5 rounded-lg border border-white/[0.08] bg-[#1a1a24] hover:border-orange-500/30 transition-all duration-200"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
|
<!-- 动态获取模型图标 -->
|
|
|
|
|
|
<span class="text-base">{{ getModelIcon(selectedModel?.provider || '') }}</span>
|
|
|
|
|
|
<span class="text-sm text-white">{{ selectedModel?.name || 'Select Model' }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span class="w-1.5 h-1.5 rounded-full bg-emerald-400"></span>
|
|
|
|
|
|
<svg class="w-3.5 h-3.5 text-white/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 下拉菜单 -->
|
|
|
|
|
|
<Transition name="dropdown">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="showModelDropdown"
|
|
|
|
|
|
class="absolute right-0 top-full mt-2 w-64 bg-[#1a1a24] border border-white/[0.08] rounded-xl shadow-2xl shadow-black/50 overflow-hidden z-50"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="p-2">
|
|
|
|
|
|
<div class="text-xs text-white/40 px-2 py-1 mb-1">Select Chat Model</div>
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-for="model in chatModels"
|
|
|
|
|
|
:key="model.id"
|
|
|
|
|
|
@click="selectedModel = model; showModelDropdown = false"
|
|
|
|
|
|
class="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg transition-all duration-150"
|
|
|
|
|
|
:class="selectedModel?.id === model.id
|
|
|
|
|
|
? 'bg-orange-500/15 border border-orange-500/30'
|
|
|
|
|
|
: 'hover:bg-white/5'"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span class="text-lg">{{ getModelIcon(model.provider) }}</span>
|
|
|
|
|
|
<div class="flex-1 text-left">
|
|
|
|
|
|
<div class="text-sm text-white">{{ model.name }}</div>
|
|
|
|
|
|
<div class="text-xs text-white/40">{{ model.provider }} · {{ model.model }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span class="w-2 h-2 rounded-full bg-emerald-400"></span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Transition>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<!-- 没有模型时的提示 -->
|
|
|
|
|
|
<div v-else class="text-xs text-white/30 px-2 flex items-center gap-2">
|
|
|
|
|
|
<span class="w-2 h-2 rounded-full bg-white/20"></span>
|
|
|
|
|
|
No models
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-10 16:09:09 +08:00
|
|
|
|
<button
|
|
|
|
|
|
@click="toggleSidebar"
|
2026-03-10 17:38:57 +08:00
|
|
|
|
class="p-2.5 rounded-xl hover:bg-white/[0.06] text-white/35 hover:text-white/80 transition-all duration-200"
|
|
|
|
|
|
:title="sidebarCollapsed ? '展开侧边栏' : '收起侧边栏'"
|
2026-03-10 16:09:09 +08:00
|
|
|
|
>
|
2026-03-10 17:38:57 +08:00
|
|
|
|
<svg class="w-[18px] h-[18px] transition-transform duration-300" :class="sidebarCollapsed ? '' : 'rotate-180'" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
2026-03-10 16:09:09 +08:00
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 6h16M4 12h16M4 18h16"></path>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 消息区域 -->
|
2026-03-10 17:38:57 +08:00
|
|
|
|
<div ref="messagesContainer" class="flex-1 overflow-y-auto py-4">
|
|
|
|
|
|
<div class="px-6">
|
2026-03-10 16:09:09 +08:00
|
|
|
|
<div
|
|
|
|
|
|
v-for="message in messages"
|
|
|
|
|
|
:key="message.id"
|
2026-03-10 17:38:57 +08:00
|
|
|
|
class="message-enter flex items-start mb-4"
|
2026-03-10 16:09:09 +08:00
|
|
|
|
:class="message.role === 'user' ? 'flex-row-reverse' : ''"
|
|
|
|
|
|
>
|
|
|
|
|
|
<!-- 头像 -->
|
|
|
|
|
|
<div
|
2026-03-10 17:38:57 +08:00
|
|
|
|
class="w-9 h-9 rounded-full flex-shrink-0 flex items-center justify-center mx-3 mt-1"
|
|
|
|
|
|
:class="message.role === 'user' ? 'bg-gradient-to-br from-orange-500 to-amber-600' : ''"
|
2026-03-10 16:09:09 +08:00
|
|
|
|
:style="message.role === 'assistant' && selectedAgent ? {
|
2026-03-10 17:38:57 +08:00
|
|
|
|
backgroundColor: selectedAgent.accentColor + '25',
|
2026-03-10 16:09:09 +08:00
|
|
|
|
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>
|
|
|
|
|
|
|
2026-03-10 17:38:57 +08:00
|
|
|
|
<!-- 气泡和时间戳容器 -->
|
|
|
|
|
|
<div :class="message.role === 'user' ? 'mr-3 ml-auto' : 'ml-3'">
|
|
|
|
|
|
<!-- 消息气泡 -->
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="px-4 py-2.5 rounded-xl text-[14px] leading-6"
|
|
|
|
|
|
:class="message.role === 'user'
|
|
|
|
|
|
? 'bg-gradient-to-br from-orange-500 to-orange-600 text-white rounded-tr-sm'
|
|
|
|
|
|
: 'bg-[#2a2a35] text-white/90 rounded-tl-sm max-w-[80%]'"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ message.content }}
|
|
|
|
|
|
<span v-if="message.isStreaming" class="inline-block w-0.5 h-4 ml-0.5 bg-orange-300 cursor-blink align-middle"></span>
|
2026-03-10 16:09:09 +08:00
|
|
|
|
</div>
|
2026-03-10 17:38:57 +08:00
|
|
|
|
<!-- 时间戳 -->
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="text-[11px] text-white/30 mt-1"
|
|
|
|
|
|
:class="message.role === 'user' ? 'text-right' : ''"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ formatTime(message.timestamp) }}
|
2026-03-10 16:09:09 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 输入区域 -->
|
2026-03-10 17:38:57 +08:00
|
|
|
|
<div class="p-5 border-t border-white/[0.06] bg-[#0c0c0f]/60 backdrop-blur-xl">
|
2026-03-10 16:09:09 +08:00
|
|
|
|
<div class="max-w-3xl mx-auto">
|
2026-03-10 17:38:57 +08:00
|
|
|
|
<div class="relative bg-[#12121a] rounded-2xl border border-white/[0.08] focus-within:border-orange-500/40 focus-within:shadow-[0_0_30px_rgba(249,115,22,0.08)] transition-all duration-300">
|
2026-03-10 16:09:09 +08:00
|
|
|
|
<!-- 附件按钮 -->
|
2026-03-10 17:38:57 +08:00
|
|
|
|
<button class="absolute left-4 top-1/2 -translate-y-1/2 text-white/25 hover:text-orange-400 transition-colors p-1">
|
2026-03-10 16:09:09 +08:00
|
|
|
|
<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"
|
2026-03-10 17:38:57 +08:00
|
|
|
|
placeholder="发送消息..."
|
2026-03-10 16:09:09 +08:00
|
|
|
|
rows="1"
|
2026-03-10 17:38:57 +08:00
|
|
|
|
class="w-full bg-transparent text-white placeholder-white/25 py-4 pl-12 pr-28 resize-none focus:outline-none text-[15px]"
|
2026-03-10 16:09:09 +08:00
|
|
|
|
></textarea>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 发送按钮 -->
|
|
|
|
|
|
<button
|
|
|
|
|
|
@click="sendMessage"
|
|
|
|
|
|
:disabled="!inputMessage.trim() || isLoading"
|
2026-03-10 17:38:57 +08:00
|
|
|
|
class="absolute right-2 top-1/2 -translate-y-1/2 w-9 h-9 rounded-lg flex items-center justify-center transition-all duration-200 disabled:opacity-30 disabled:cursor-not-allowed"
|
|
|
|
|
|
:class="inputMessage.trim() && !isLoading
|
|
|
|
|
|
? 'bg-orange-500 hover:bg-orange-400 shadow-lg shadow-orange-500/30 active:scale-90'
|
|
|
|
|
|
: 'bg-white/10'"
|
2026-03-10 16:09:09 +08:00
|
|
|
|
>
|
2026-03-10 17:38:57 +08:00
|
|
|
|
<svg v-if="!isLoading" class="w-4 h-4 text-white" viewBox="0 0 24 24" fill="currentColor">
|
|
|
|
|
|
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
<svg v-else class="w-4 h-4 text-white animate-spin" fill="none" viewBox="0 0 24 24">
|
|
|
|
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
|
|
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
2026-03-10 16:09:09 +08:00
|
|
|
|
</svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 提示 -->
|
2026-03-10 17:38:57 +08:00
|
|
|
|
<div class="text-center mt-3">
|
|
|
|
|
|
<span class="text-[10px] text-white/20 tracking-wide">AI 可能会产生错误信息,请核实重要内容</span>
|
2026-03-10 16:09:09 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-10 17:38:57 +08:00
|
|
|
|
<!-- 右侧边栏:AI Hub -->
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="flex-shrink-0 border-l border-white/[0.06] bg-[#0c0c0f] transition-all duration-300 ease-in-out overflow-hidden"
|
|
|
|
|
|
:class="sidebarCollapsed ? 'w-0 opacity-0' : 'w-72 opacity-100'"
|
2026-03-10 16:09:09 +08:00
|
|
|
|
>
|
2026-03-10 17:38:57 +08:00
|
|
|
|
<div class="w-72 h-full flex flex-col">
|
|
|
|
|
|
<!-- 侧边栏头部 -->
|
|
|
|
|
|
<div class="p-4 border-b border-white/[0.06]">
|
|
|
|
|
|
<div class="flex items-center gap-2 text-white font-semibold">
|
|
|
|
|
|
<svg class="w-5 h-5 text-orange-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"></path>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
<span>AI Hub</span>
|
2026-03-10 16:09:09 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-10 17:38:57 +08:00
|
|
|
|
<!-- 新建对话按钮 -->
|
|
|
|
|
|
<div class="p-3">
|
2026-03-11 14:26:25 +08:00
|
|
|
|
<div class="flex gap-2">
|
|
|
|
|
|
<button
|
|
|
|
|
|
@click="openAgentSelector('single')"
|
|
|
|
|
|
class="flex-1 flex items-center justify-center gap-2 px-3 py-2.5 bg-orange-500 hover:bg-orange-400 rounded-lg text-white text-sm font-medium 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="2" d="M12 4v16m8-8H4"></path>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
<span>新建对话</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
@click="openAgentSelector('group')"
|
|
|
|
|
|
class="flex-1 flex items-center justify-center gap-2 px-3 py-2.5 bg-dark-700 hover:bg-dark-600 border border-dark-500 rounded-lg text-white/80 text-sm font-medium 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="2" 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>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
<span>新建群聊</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
2026-03-10 16:09:09 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-10 17:38:57 +08:00
|
|
|
|
<!-- AI 助手选择 -->
|
|
|
|
|
|
<div class="px-3 pb-3">
|
|
|
|
|
|
<div class="text-xs text-white/40 uppercase tracking-wider mb-2 px-1">选择 AI 助手</div>
|
2026-03-10 16:09:09 +08:00
|
|
|
|
<div class="space-y-1">
|
2026-03-10 17:38:57 +08:00
|
|
|
|
<button
|
2026-03-10 16:09:09 +08:00
|
|
|
|
v-for="agent in chatAgents"
|
|
|
|
|
|
:key="agent.id"
|
|
|
|
|
|
@click="selectAgent(agent)"
|
2026-03-10 17:38:57 +08:00
|
|
|
|
class="w-full flex items-center gap-2 px-3 py-2 rounded-lg transition-all duration-200"
|
2026-03-10 16:09:09 +08:00
|
|
|
|
:class="selectedAgent?.id === agent.id
|
2026-03-10 17:38:57 +08:00
|
|
|
|
? 'bg-orange-500/15 text-orange-400'
|
|
|
|
|
|
: 'text-white/60 hover:bg-white/5 hover:text-white'"
|
2026-03-10 16:09:09 +08:00
|
|
|
|
>
|
2026-03-10 17:38:57 +08:00
|
|
|
|
<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>
|
2026-03-10 16:09:09 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-11 14:26:25 +08:00
|
|
|
|
<!-- 群聊列表 -->
|
2026-03-10 17:38:57 +08:00
|
|
|
|
<div class="flex-1 overflow-y-auto px-3 pb-3">
|
2026-03-11 14:26:25 +08:00
|
|
|
|
<div class="text-xs text-white/40 uppercase tracking-wider mb-2 px-1">群聊</div>
|
2026-03-10 17:38:57 +08:00
|
|
|
|
<div class="space-y-1">
|
|
|
|
|
|
<button
|
2026-03-11 14:26:25 +08:00
|
|
|
|
v-for="group in groupChats"
|
|
|
|
|
|
:key="group.id"
|
2026-03-10 17:38:57 +08:00
|
|
|
|
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">
|
2026-03-11 14:26:25 +08:00
|
|
|
|
<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>
|
2026-03-10 17:38:57 +08:00
|
|
|
|
</svg>
|
2026-03-11 14:26:25 +08:00
|
|
|
|
<span class="text-sm text-white/70 group-hover:text-white truncate">{{ group.name }}</span>
|
2026-03-10 17:38:57 +08:00
|
|
|
|
</div>
|
2026-03-11 14:26:25 +08:00
|
|
|
|
<div class="text-xs text-white/30 mt-1 pl-6">{{ group.members.length }} members</div>
|
2026-03-10 17:38:57 +08:00
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
2026-03-10 16:09:09 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-03-10 17:38:57 +08:00
|
|
|
|
</div>
|
2026-03-10 16:09:09 +08:00
|
|
|
|
</div>
|
2026-03-11 14:26:25 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 智能体选择弹窗 -->
|
|
|
|
|
|
<Teleport to="body">
|
|
|
|
|
|
<Transition name="fade">
|
|
|
|
|
|
<div v-if="showAgentSelector" class="fixed inset-0 bg-black/60 flex items-center justify-center z-50" @click="cancelAgentSelection">
|
|
|
|
|
|
<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' ? '选择智能体' : '选择群聊成员' }}
|
|
|
|
|
|
</h3>
|
|
|
|
|
|
<p class="text-sm text-gray-400 mt-1">
|
|
|
|
|
|
{{ selectMode === 'single' ? '选择一个智能体开始对话' : '选择多个智能体创建群聊' }}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="p-4 max-h-80 overflow-y-auto">
|
|
|
|
|
|
<!-- 群聊名称输入框 -->
|
|
|
|
|
|
<div v-if="selectMode === 'group'" class="mb-4">
|
|
|
|
|
|
<input
|
|
|
|
|
|
v-model="groupChatName"
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
placeholder="Enter group name..."
|
|
|
|
|
|
class="w-full bg-dark-700 border border-dark-500 rounded-lg px-4 py-2.5 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange"
|
|
|
|
|
|
>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="space-y-2">
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-for="agent in chatAgents"
|
|
|
|
|
|
:key="agent.id"
|
|
|
|
|
|
@click="selectMode === 'group' ? toggleAgentSelection(agent) : (selectedAgents = [agent], confirmAgentSelection())"
|
|
|
|
|
|
class="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg transition-all duration-200"
|
|
|
|
|
|
: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'"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span class="text-xl">{{ agent.avatar }}</span>
|
|
|
|
|
|
<div class="flex-1 text-left">
|
|
|
|
|
|
<div class="text-white font-medium">{{ agent.name }}</div>
|
|
|
|
|
|
<div class="text-xs text-gray-400">{{ agent.description }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span
|
|
|
|
|
|
v-if="agent.status === 'online'"
|
|
|
|
|
|
class="w-2 h-2 rounded-full bg-emerald-400"
|
|
|
|
|
|
></span>
|
|
|
|
|
|
<span
|
|
|
|
|
|
v-if="selectMode === 'group' && selectedAgents.some(a => a.id === agent.id)"
|
|
|
|
|
|
class="w-5 h-5 rounded-full bg-orange-500 flex items-center justify-center"
|
|
|
|
|
|
>
|
|
|
|
|
|
<svg class="w-3 h-3 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"></path>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="p-4 border-t border-dark-600 flex gap-3">
|
|
|
|
|
|
<button
|
|
|
|
|
|
@click="cancelAgentSelection"
|
|
|
|
|
|
class="flex-1 py-2.5 bg-dark-700 hover:bg-dark-600 text-gray-300 rounded-lg transition-colors"
|
|
|
|
|
|
>
|
|
|
|
|
|
Cancel
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-if="selectMode === 'group'"
|
|
|
|
|
|
@click="confirmAgentSelection"
|
|
|
|
|
|
:disabled="selectedAgents.length < 2"
|
|
|
|
|
|
class="flex-1 py-2.5 bg-orange-500 hover:bg-orange-400 text-white rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
|
|
|
|
>
|
|
|
|
|
|
Create Group ({{ selectedAgents.length }})
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Transition>
|
|
|
|
|
|
</Teleport>
|
2026-03-10 16:09:09 +08:00
|
|
|
|
</template>
|
2026-03-11 14:26:25 +08:00
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.fade-enter-active,
|
|
|
|
|
|
.fade-leave-active {
|
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.fade-enter-from,
|
|
|
|
|
|
.fade-leave-to {
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|