feat: AI聊天室多Agent协作讨论平台
- 实现Agent管理,支持AI辅助生成系统提示词 - 支持多个AI提供商(OpenRouter、智谱、MiniMax等) - 实现聊天室和讨论引擎 - WebSocket实时消息推送 - 前端使用React + Ant Design - 后端使用FastAPI + MongoDB Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
31
frontend/Dockerfile
Normal file
31
frontend/Dockerfile
Normal file
@@ -0,0 +1,31 @@
|
||||
# AI聊天室前端 Dockerfile
|
||||
# 构建阶段
|
||||
FROM node:20-alpine as builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 复制依赖文件
|
||||
COPY package.json .
|
||||
|
||||
# 安装依赖
|
||||
RUN npm install
|
||||
|
||||
# 复制源代码
|
||||
COPY . .
|
||||
|
||||
# 构建
|
||||
RUN npm run build
|
||||
|
||||
# 生产阶段
|
||||
FROM nginx:alpine
|
||||
|
||||
# 复制构建产物
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
|
||||
# 复制nginx配置
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>AI聊天室 - 多Agent协作讨论平台</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
46
frontend/nginx.conf
Normal file
46
frontend/nginx.conf
Normal file
@@ -0,0 +1,46 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# 前端路由支持
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# API代理
|
||||
location /api {
|
||||
proxy_pass http://backend:8000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 86400;
|
||||
}
|
||||
|
||||
# WebSocket代理
|
||||
location /ws {
|
||||
proxy_pass http://backend:8000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_read_timeout 86400;
|
||||
}
|
||||
|
||||
# 静态资源缓存
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
# Gzip压缩
|
||||
gzip on;
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
||||
gzip_min_length 1000;
|
||||
}
|
||||
3192
frontend/package-lock.json
generated
Normal file
3192
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
frontend/package.json
Normal file
28
frontend/package.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "ai-chatroom-frontend",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.2.6",
|
||||
"antd": "^5.13.0",
|
||||
"axios": "^1.6.5",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.21.1",
|
||||
"zustand": "^4.4.7",
|
||||
"socket.io-client": "^4.7.4",
|
||||
"dayjs": "^1.11.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.47",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.11"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
}
|
||||
}
|
||||
39
frontend/src/App.tsx
Normal file
39
frontend/src/App.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 应用主组件
|
||||
* 定义路由和布局
|
||||
*/
|
||||
import React from 'react'
|
||||
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
|
||||
import { Layout } from 'antd'
|
||||
import Sidebar from './components/Sidebar'
|
||||
import Dashboard from './pages/Dashboard'
|
||||
import ProviderConfig from './pages/ProviderConfig'
|
||||
import AgentManagement from './pages/AgentManagement'
|
||||
import ChatRoom from './pages/ChatRoom'
|
||||
import DiscussionHistory from './pages/DiscussionHistory'
|
||||
|
||||
const { Content } = Layout
|
||||
|
||||
const App: React.FC = () => {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Layout style={{ minHeight: '100vh' }}>
|
||||
<Sidebar />
|
||||
<Layout>
|
||||
<Content style={{ margin: '24px', overflow: 'auto' }}>
|
||||
<Routes>
|
||||
<Route path="/" element={<Navigate to="/dashboard\" replace />} />
|
||||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
<Route path="/providers" element={<ProviderConfig />} />
|
||||
<Route path="/agents" element={<AgentManagement />} />
|
||||
<Route path="/chatroom/:roomId?" element={<ChatRoom />} />
|
||||
<Route path="/history" element={<DiscussionHistory />} />
|
||||
</Routes>
|
||||
</Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</BrowserRouter>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
122
frontend/src/components/AgentCard.tsx
Normal file
122
frontend/src/components/AgentCard.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* Agent卡片组件
|
||||
*/
|
||||
import React from 'react'
|
||||
import { Card, Avatar, Tag, Switch, Typography, Space, Button, Tooltip } from 'antd'
|
||||
import {
|
||||
RobotOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
CopyOutlined,
|
||||
PlayCircleOutlined
|
||||
} from '@ant-design/icons'
|
||||
import type { Agent } from '../types'
|
||||
|
||||
const { Text, Paragraph } = Typography
|
||||
|
||||
interface AgentCardProps {
|
||||
agent: Agent
|
||||
onEdit?: (agent: Agent) => void
|
||||
onDelete?: (agent: Agent) => void
|
||||
onDuplicate?: (agent: Agent) => void
|
||||
onTest?: (agent: Agent) => void
|
||||
onToggleEnabled?: (agent: Agent, enabled: boolean) => void
|
||||
}
|
||||
|
||||
const AgentCard: React.FC<AgentCardProps> = ({
|
||||
agent,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onDuplicate,
|
||||
onTest,
|
||||
onToggleEnabled
|
||||
}) => {
|
||||
return (
|
||||
<Card
|
||||
hoverable
|
||||
style={{
|
||||
borderTop: `3px solid ${agent.color}`,
|
||||
opacity: agent.enabled ? 1 : 0.6
|
||||
}}
|
||||
actions={[
|
||||
<Tooltip title="测试对话" key="test">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<PlayCircleOutlined />}
|
||||
onClick={() => onTest?.(agent)}
|
||||
/>
|
||||
</Tooltip>,
|
||||
<Tooltip title="复制" key="copy">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => onDuplicate?.(agent)}
|
||||
/>
|
||||
</Tooltip>,
|
||||
<Tooltip title="编辑" key="edit">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => onEdit?.(agent)}
|
||||
/>
|
||||
</Tooltip>,
|
||||
<Tooltip title="删除" key="delete">
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => onDelete?.(agent)}
|
||||
/>
|
||||
</Tooltip>
|
||||
]}
|
||||
>
|
||||
<Card.Meta
|
||||
avatar={
|
||||
<Avatar
|
||||
size={48}
|
||||
icon={<RobotOutlined />}
|
||||
style={{ backgroundColor: agent.color }}
|
||||
src={agent.avatar}
|
||||
/>
|
||||
}
|
||||
title={
|
||||
<Space>
|
||||
<span>{agent.name}</span>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={agent.enabled}
|
||||
onChange={(checked) => onToggleEnabled?.(agent, checked)}
|
||||
/>
|
||||
</Space>
|
||||
}
|
||||
description={
|
||||
<div>
|
||||
<Text type="secondary">{agent.role}</Text>
|
||||
|
||||
<div style={{ marginTop: 8 }}>
|
||||
{agent.capabilities.memory_enabled && (
|
||||
<Tag color="blue">记忆</Tag>
|
||||
)}
|
||||
{agent.capabilities.multimodal && (
|
||||
<Tag color="purple">多模态</Tag>
|
||||
)}
|
||||
{agent.capabilities.mcp_tools.length > 0 && (
|
||||
<Tag color="green">MCP工具</Tag>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Paragraph
|
||||
type="secondary"
|
||||
ellipsis={{ rows: 2 }}
|
||||
style={{ marginTop: 8, marginBottom: 0, fontSize: 12 }}
|
||||
>
|
||||
{agent.system_prompt}
|
||||
</Paragraph>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default AgentCard
|
||||
97
frontend/src/components/MessageBubble.tsx
Normal file
97
frontend/src/components/MessageBubble.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* 消息气泡组件
|
||||
*/
|
||||
import React from 'react'
|
||||
import { Avatar, Typography } from 'antd'
|
||||
import { RobotOutlined, InfoCircleOutlined } from '@ant-design/icons'
|
||||
import type { Message, Agent } from '../types'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const { Text, Paragraph } = Typography
|
||||
|
||||
interface MessageBubbleProps {
|
||||
message: Message
|
||||
agent?: Agent
|
||||
}
|
||||
|
||||
const MessageBubble: React.FC<MessageBubbleProps> = ({ message, agent }) => {
|
||||
const isSystem = message.message_type === 'system' || !message.agent_id
|
||||
|
||||
if (isSystem) {
|
||||
return (
|
||||
<div className="message-fade-in" style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
margin: '16px 0'
|
||||
}}>
|
||||
<div style={{
|
||||
background: '#f0f0f0',
|
||||
padding: '8px 16px',
|
||||
borderRadius: 8,
|
||||
maxWidth: '80%'
|
||||
}}>
|
||||
<InfoCircleOutlined style={{ marginRight: 8, color: '#666' }} />
|
||||
<Text type="secondary" style={{ whiteSpace: 'pre-wrap' }}>
|
||||
{message.content}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const agentColor = agent?.color || '#1890ff'
|
||||
const agentName = agent?.name || message.agent_id
|
||||
|
||||
return (
|
||||
<div className="message-fade-in" style={{
|
||||
display: 'flex',
|
||||
gap: 12,
|
||||
margin: '12px 0',
|
||||
padding: '8px 12px'
|
||||
}}>
|
||||
<Avatar
|
||||
size={40}
|
||||
icon={<RobotOutlined />}
|
||||
style={{
|
||||
backgroundColor: agentColor,
|
||||
flexShrink: 0
|
||||
}}
|
||||
src={agent?.avatar}
|
||||
/>
|
||||
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
marginBottom: 4
|
||||
}}>
|
||||
<Text strong style={{ color: agentColor }}>
|
||||
{agentName}
|
||||
</Text>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
第{message.round}轮 · {dayjs(message.created_at).format('HH:mm:ss')}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
background: '#fff',
|
||||
padding: '12px 16px',
|
||||
borderRadius: '0 12px 12px 12px',
|
||||
boxShadow: '0 1px 2px rgba(0,0,0,0.1)',
|
||||
borderLeft: `3px solid ${agentColor}`
|
||||
}}>
|
||||
<Paragraph style={{
|
||||
margin: 0,
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word'
|
||||
}}>
|
||||
{message.content}
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MessageBubble
|
||||
139
frontend/src/components/ProviderCard.tsx
Normal file
139
frontend/src/components/ProviderCard.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* AI接口卡片组件
|
||||
*/
|
||||
import React from 'react'
|
||||
import { Card, Tag, Switch, Typography, Space, Button, Tooltip } from 'antd'
|
||||
import {
|
||||
ApiOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
CheckCircleOutlined,
|
||||
CloudOutlined,
|
||||
DesktopOutlined
|
||||
} from '@ant-design/icons'
|
||||
import type { AIProvider } from '../types'
|
||||
|
||||
const { Text } = Typography
|
||||
|
||||
// 提供商类型图标和颜色
|
||||
const PROVIDER_CONFIG: Record<string, { color: string; label: string; local?: boolean }> = {
|
||||
minimax: { color: '#1890ff', label: 'MiniMax' },
|
||||
zhipu: { color: '#52c41a', label: '智谱AI' },
|
||||
openrouter: { color: '#722ed1', label: 'OpenRouter' },
|
||||
kimi: { color: '#eb2f96', label: 'Kimi' },
|
||||
deepseek: { color: '#13c2c2', label: 'DeepSeek' },
|
||||
gemini: { color: '#4285f4', label: 'Gemini' },
|
||||
ollama: { color: '#fa8c16', label: 'Ollama', local: true },
|
||||
llmstudio: { color: '#a0d911', label: 'LLM Studio', local: true }
|
||||
}
|
||||
|
||||
interface ProviderCardProps {
|
||||
provider: AIProvider
|
||||
onEdit?: (provider: AIProvider) => void
|
||||
onDelete?: (provider: AIProvider) => void
|
||||
onTest?: (provider: AIProvider) => void
|
||||
onToggleEnabled?: (provider: AIProvider, enabled: boolean) => void
|
||||
testing?: boolean
|
||||
}
|
||||
|
||||
const ProviderCard: React.FC<ProviderCardProps> = ({
|
||||
provider,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onTest,
|
||||
onToggleEnabled,
|
||||
testing = false
|
||||
}) => {
|
||||
const config = PROVIDER_CONFIG[provider.provider_type] || {
|
||||
color: '#666',
|
||||
label: provider.provider_type
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
hoverable
|
||||
style={{
|
||||
borderTop: `3px solid ${config.color}`,
|
||||
opacity: provider.enabled ? 1 : 0.6
|
||||
}}
|
||||
actions={[
|
||||
<Tooltip title="测试连接" key="test">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<CheckCircleOutlined />}
|
||||
onClick={() => onTest?.(provider)}
|
||||
loading={testing}
|
||||
/>
|
||||
</Tooltip>,
|
||||
<Tooltip title="编辑" key="edit">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => onEdit?.(provider)}
|
||||
/>
|
||||
</Tooltip>,
|
||||
<Tooltip title="删除" key="delete">
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => onDelete?.(provider)}
|
||||
/>
|
||||
</Tooltip>
|
||||
]}
|
||||
>
|
||||
<Card.Meta
|
||||
avatar={
|
||||
<div style={{
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 8,
|
||||
background: config.color,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#fff',
|
||||
fontSize: 24
|
||||
}}>
|
||||
{config.local ? <DesktopOutlined /> : <CloudOutlined />}
|
||||
</div>
|
||||
}
|
||||
title={
|
||||
<Space>
|
||||
<span>{provider.name}</span>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={provider.enabled}
|
||||
onChange={(checked) => onToggleEnabled?.(provider, checked)}
|
||||
/>
|
||||
</Space>
|
||||
}
|
||||
description={
|
||||
<div>
|
||||
<Space size={4} wrap>
|
||||
<Tag color={config.color}>{config.label}</Tag>
|
||||
{provider.use_proxy && <Tag color="orange">代理</Tag>}
|
||||
{config.local && <Tag color="green">本地</Tag>}
|
||||
</Space>
|
||||
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
模型: {provider.model}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{provider.api_key_masked && (
|
||||
<div style={{ marginTop: 4 }}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
密钥: {provider.api_key_masked}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProviderCard
|
||||
92
frontend/src/components/Sidebar.tsx
Normal file
92
frontend/src/components/Sidebar.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* 侧边栏导航组件
|
||||
*/
|
||||
import React from 'react'
|
||||
import { useNavigate, useLocation } from 'react-router-dom'
|
||||
import { Layout, Menu } from 'antd'
|
||||
import {
|
||||
DashboardOutlined,
|
||||
ApiOutlined,
|
||||
RobotOutlined,
|
||||
MessageOutlined,
|
||||
HistoryOutlined
|
||||
} from '@ant-design/icons'
|
||||
|
||||
const { Sider } = Layout
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
key: '/dashboard',
|
||||
icon: <DashboardOutlined />,
|
||||
label: '控制台'
|
||||
},
|
||||
{
|
||||
key: '/providers',
|
||||
icon: <ApiOutlined />,
|
||||
label: 'AI接口配置'
|
||||
},
|
||||
{
|
||||
key: '/agents',
|
||||
icon: <RobotOutlined />,
|
||||
label: 'Agent管理'
|
||||
},
|
||||
{
|
||||
key: '/chatroom',
|
||||
icon: <MessageOutlined />,
|
||||
label: '聊天室'
|
||||
},
|
||||
{
|
||||
key: '/history',
|
||||
icon: <HistoryOutlined />,
|
||||
label: '讨论历史'
|
||||
}
|
||||
]
|
||||
|
||||
const Sidebar: React.FC = () => {
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
|
||||
// 获取当前选中的菜单项
|
||||
const selectedKey = menuItems.find(
|
||||
item => location.pathname.startsWith(item.key)
|
||||
)?.key || '/dashboard'
|
||||
|
||||
return (
|
||||
<Sider
|
||||
theme="dark"
|
||||
width={200}
|
||||
style={{
|
||||
overflow: 'auto',
|
||||
height: '100vh',
|
||||
position: 'fixed',
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
height: 64,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#fff',
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
borderBottom: '1px solid rgba(255,255,255,0.1)'
|
||||
}}>
|
||||
AI聊天室
|
||||
</div>
|
||||
|
||||
<Menu
|
||||
theme="dark"
|
||||
mode="inline"
|
||||
selectedKeys={[selectedKey]}
|
||||
items={menuItems}
|
||||
onClick={({ key }) => navigate(key)}
|
||||
style={{ borderRight: 0 }}
|
||||
/>
|
||||
</Sider>
|
||||
)
|
||||
}
|
||||
|
||||
export default Sidebar
|
||||
74
frontend/src/components/TypingIndicator.tsx
Normal file
74
frontend/src/components/TypingIndicator.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* 输入状态指示器
|
||||
*/
|
||||
import React from 'react'
|
||||
import { Avatar, Typography } from 'antd'
|
||||
import { RobotOutlined } from '@ant-design/icons'
|
||||
import type { Agent } from '../types'
|
||||
|
||||
const { Text } = Typography
|
||||
|
||||
interface TypingIndicatorProps {
|
||||
agents: Agent[]
|
||||
}
|
||||
|
||||
const TypingIndicator: React.FC<TypingIndicatorProps> = ({ agents }) => {
|
||||
if (agents.length === 0) return null
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
padding: '8px 12px',
|
||||
marginTop: 8
|
||||
}}>
|
||||
<div style={{ display: 'flex' }}>
|
||||
{agents.slice(0, 3).map((agent, index) => (
|
||||
<Avatar
|
||||
key={agent.agent_id}
|
||||
size={32}
|
||||
icon={<RobotOutlined />}
|
||||
style={{
|
||||
backgroundColor: agent.color,
|
||||
marginLeft: index > 0 ? -8 : 0,
|
||||
border: '2px solid #fff'
|
||||
}}
|
||||
src={agent.avatar}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<Text type="secondary">
|
||||
{agents.length === 1
|
||||
? `${agents[0].name} 正在思考`
|
||||
: `${agents.length} 位Agent正在思考`
|
||||
}
|
||||
</Text>
|
||||
<span style={{ display: 'flex', gap: 2 }}>
|
||||
<span className="typing-dot" style={{
|
||||
width: 4,
|
||||
height: 4,
|
||||
borderRadius: '50%',
|
||||
background: '#999'
|
||||
}} />
|
||||
<span className="typing-dot" style={{
|
||||
width: 4,
|
||||
height: 4,
|
||||
borderRadius: '50%',
|
||||
background: '#999'
|
||||
}} />
|
||||
<span className="typing-dot" style={{
|
||||
width: 4,
|
||||
height: 4,
|
||||
borderRadius: '50%',
|
||||
background: '#999'
|
||||
}} />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TypingIndicator
|
||||
73
frontend/src/index.css
Normal file
73
frontend/src/index.css
Normal file
@@ -0,0 +1,73 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body, #root {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
background-color: #f0f2f5;
|
||||
}
|
||||
|
||||
/* 自定义滚动条 */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
|
||||
/* 消息动画 */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.message-fade-in {
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
/* 打字动画 */
|
||||
@keyframes typing {
|
||||
0%, 60%, 100% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
30% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.typing-dot {
|
||||
animation: typing 1.4s infinite;
|
||||
}
|
||||
|
||||
.typing-dot:nth-child(2) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.typing-dot:nth-child(3) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
17
frontend/src/main.tsx
Normal file
17
frontend/src/main.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* 应用入口
|
||||
*/
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import { ConfigProvider } from 'antd'
|
||||
import zhCN from 'antd/locale/zh_CN'
|
||||
import App from './App'
|
||||
import './index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<ConfigProvider locale={zhCN}>
|
||||
<App />
|
||||
</ConfigProvider>
|
||||
</React.StrictMode>
|
||||
)
|
||||
471
frontend/src/pages/AgentManagement.tsx
Normal file
471
frontend/src/pages/AgentManagement.tsx
Normal file
@@ -0,0 +1,471 @@
|
||||
/**
|
||||
* Agent管理页面
|
||||
*/
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import {
|
||||
Row, Col, Button, Modal, Form, Input, Select, Switch, Slider,
|
||||
Typography, message, Spin, Empty, Tabs, Card, Space, Tooltip
|
||||
} from 'antd'
|
||||
import { PlusOutlined, RobotOutlined, BulbOutlined, LoadingOutlined } from '@ant-design/icons'
|
||||
import { useAgentStore } from '../stores/agentStore'
|
||||
import { useProviderStore } from '../stores/providerStore'
|
||||
import AgentCard from '../components/AgentCard'
|
||||
import type { Agent } from '../types'
|
||||
|
||||
const { Title, Paragraph, Text } = Typography
|
||||
const { TextArea } = Input
|
||||
|
||||
const AgentManagement: React.FC = () => {
|
||||
const {
|
||||
agents, templates, loading,
|
||||
fetchAgents, fetchTemplates,
|
||||
createAgent, updateAgent, deleteAgent,
|
||||
testAgent, duplicateAgent, createFromTemplate, generatePrompt
|
||||
} = useAgentStore()
|
||||
const { providers, fetchProviders } = useProviderStore()
|
||||
|
||||
const [modalVisible, setModalVisible] = useState(false)
|
||||
const [editingAgent, setEditingAgent] = useState<Agent | null>(null)
|
||||
const [testModalVisible, setTestModalVisible] = useState(false)
|
||||
const [testingAgent, setTestingAgent] = useState<Agent | null>(null)
|
||||
const [testResult, setTestResult] = useState<string>('')
|
||||
const [testLoading, setTestLoading] = useState(false)
|
||||
const [generatingPrompt, setGeneratingPrompt] = useState(false)
|
||||
const [form] = Form.useForm()
|
||||
|
||||
useEffect(() => {
|
||||
fetchAgents()
|
||||
fetchTemplates()
|
||||
fetchProviders()
|
||||
}, [])
|
||||
|
||||
const enabledProviders = providers.filter(p => p.enabled)
|
||||
|
||||
// 打开创建/编辑弹窗
|
||||
const openModal = (agent?: Agent) => {
|
||||
setEditingAgent(agent || null)
|
||||
if (agent) {
|
||||
form.setFieldsValue({
|
||||
name: agent.name,
|
||||
role: agent.role,
|
||||
system_prompt: agent.system_prompt,
|
||||
provider_id: agent.provider_id,
|
||||
temperature: agent.temperature,
|
||||
max_tokens: agent.max_tokens,
|
||||
memory_enabled: agent.capabilities.memory_enabled,
|
||||
multimodal: agent.capabilities.multimodal,
|
||||
speak_threshold: agent.behavior.speak_threshold,
|
||||
max_speak_per_round: agent.behavior.max_speak_per_round,
|
||||
color: agent.color
|
||||
})
|
||||
} else {
|
||||
form.resetFields()
|
||||
form.setFieldsValue({
|
||||
temperature: 0.7,
|
||||
max_tokens: 2000,
|
||||
speak_threshold: 0.5,
|
||||
max_speak_per_round: 2,
|
||||
color: '#1890ff'
|
||||
})
|
||||
}
|
||||
setModalVisible(true)
|
||||
}
|
||||
|
||||
// 保存
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
const values = await form.validateFields()
|
||||
|
||||
const data = {
|
||||
name: values.name,
|
||||
role: values.role,
|
||||
system_prompt: values.system_prompt,
|
||||
provider_id: values.provider_id,
|
||||
temperature: values.temperature,
|
||||
max_tokens: values.max_tokens,
|
||||
capabilities: {
|
||||
memory_enabled: values.memory_enabled || false,
|
||||
multimodal: values.multimodal || false,
|
||||
mcp_tools: [],
|
||||
skills: []
|
||||
},
|
||||
behavior: {
|
||||
speak_threshold: values.speak_threshold,
|
||||
max_speak_per_round: values.max_speak_per_round,
|
||||
speak_style: 'balanced'
|
||||
},
|
||||
color: values.color
|
||||
}
|
||||
|
||||
if (editingAgent) {
|
||||
await updateAgent(editingAgent.agent_id, data)
|
||||
message.success('更新成功')
|
||||
} else {
|
||||
await createAgent(data)
|
||||
message.success('创建成功')
|
||||
}
|
||||
|
||||
setModalVisible(false)
|
||||
} catch (e) {
|
||||
message.error((e as Error).message)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = (agent: Agent) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除Agent "${agent.name}" 吗?`,
|
||||
okText: '删除',
|
||||
okType: 'danger',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await deleteAgent(agent.agent_id)
|
||||
message.success('删除成功')
|
||||
} catch (e) {
|
||||
message.error((e as Error).message)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 测试
|
||||
const handleTest = (agent: Agent) => {
|
||||
setTestingAgent(agent)
|
||||
setTestResult('')
|
||||
setTestModalVisible(true)
|
||||
}
|
||||
|
||||
const runTest = async (testMessage: string) => {
|
||||
if (!testingAgent) return
|
||||
setTestLoading(true)
|
||||
try {
|
||||
const result = await testAgent(testingAgent.agent_id, testMessage)
|
||||
if (result.success) {
|
||||
setTestResult(result.response || '无响应')
|
||||
} else {
|
||||
setTestResult(`错误: ${result.message}`)
|
||||
}
|
||||
} catch (e) {
|
||||
setTestResult(`错误: ${(e as Error).message}`)
|
||||
} finally {
|
||||
setTestLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 复制
|
||||
const handleDuplicate = async (agent: Agent) => {
|
||||
try {
|
||||
await duplicateAgent(agent.agent_id)
|
||||
message.success('复制成功')
|
||||
} catch (e) {
|
||||
message.error((e as Error).message)
|
||||
}
|
||||
}
|
||||
|
||||
// 切换启用
|
||||
const handleToggle = async (agent: Agent, enabled: boolean) => {
|
||||
try {
|
||||
await updateAgent(agent.agent_id, { enabled })
|
||||
message.success(enabled ? '已启用' : '已禁用')
|
||||
} catch (e) {
|
||||
message.error((e as Error).message)
|
||||
}
|
||||
}
|
||||
|
||||
// 从模板创建
|
||||
const handleCreateFromTemplate = async (templateId: string) => {
|
||||
if (enabledProviders.length === 0) {
|
||||
message.error('请先配置AI接口')
|
||||
return
|
||||
}
|
||||
|
||||
Modal.confirm({
|
||||
title: '选择AI接口',
|
||||
content: (
|
||||
<Select
|
||||
id="template-provider-select"
|
||||
style={{ width: '100%', marginTop: 16 }}
|
||||
placeholder="选择AI接口"
|
||||
options={enabledProviders.map(p => ({
|
||||
value: p.provider_id,
|
||||
label: `${p.name} (${p.model})`
|
||||
}))}
|
||||
/>
|
||||
),
|
||||
onOk: async () => {
|
||||
const select = document.getElementById('template-provider-select') as HTMLSelectElement
|
||||
const providerId = select?.value
|
||||
if (providerId) {
|
||||
try {
|
||||
await createFromTemplate(templateId, providerId)
|
||||
message.success('创建成功')
|
||||
} catch (e) {
|
||||
message.error((e as Error).message)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// AI生成系统提示词
|
||||
const handleGeneratePrompt = async () => {
|
||||
const name = form.getFieldValue('name')
|
||||
const role = form.getFieldValue('role')
|
||||
const providerId = form.getFieldValue('provider_id')
|
||||
|
||||
if (!name || !role) {
|
||||
message.warning('请先填写Agent名称和角色')
|
||||
return
|
||||
}
|
||||
|
||||
if (!providerId) {
|
||||
message.warning('请先选择AI接口')
|
||||
return
|
||||
}
|
||||
|
||||
setGeneratingPrompt(true)
|
||||
try {
|
||||
const result = await generatePrompt({
|
||||
provider_id: providerId,
|
||||
name,
|
||||
role,
|
||||
description: form.getFieldValue('description')
|
||||
})
|
||||
|
||||
if (result.success && result.prompt) {
|
||||
form.setFieldValue('system_prompt', result.prompt)
|
||||
message.success('提示词生成成功')
|
||||
} else {
|
||||
message.error(result.message || '生成失败')
|
||||
}
|
||||
} catch (e) {
|
||||
message.error(`生成失败: ${(e as Error).message}`)
|
||||
} finally {
|
||||
setGeneratingPrompt(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ marginLeft: 200 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 24 }}>
|
||||
<Title level={2}>Agent管理</Title>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => openModal()}>
|
||||
创建Agent
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Tabs defaultActiveKey="agents" items={[
|
||||
{
|
||||
key: 'agents',
|
||||
label: 'Agent列表',
|
||||
children: (
|
||||
<Spin spinning={loading}>
|
||||
{agents.length > 0 ? (
|
||||
<Row gutter={[16, 16]}>
|
||||
{agents.map((agent) => (
|
||||
<Col key={agent.agent_id} xs={24} sm={12} lg={8} xl={6}>
|
||||
<AgentCard
|
||||
agent={agent}
|
||||
onEdit={openModal}
|
||||
onDelete={handleDelete}
|
||||
onDuplicate={handleDuplicate}
|
||||
onTest={handleTest}
|
||||
onToggleEnabled={handleToggle}
|
||||
/>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
) : (
|
||||
<Empty description="暂无Agent" />
|
||||
)}
|
||||
</Spin>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'templates',
|
||||
label: '预设模板',
|
||||
children: (
|
||||
<Row gutter={[16, 16]}>
|
||||
{templates.map((template) => (
|
||||
<Col key={template.template_id} xs={24} sm={12} lg={8} xl={6}>
|
||||
<Card
|
||||
hoverable
|
||||
style={{ borderTop: `3px solid ${template.color}` }}
|
||||
onClick={() => handleCreateFromTemplate(template.template_id)}
|
||||
>
|
||||
<Card.Meta
|
||||
avatar={
|
||||
<div style={{
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: '50%',
|
||||
background: template.color,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#fff'
|
||||
}}>
|
||||
<RobotOutlined style={{ fontSize: 24 }} />
|
||||
</div>
|
||||
}
|
||||
title={template.name}
|
||||
description={
|
||||
<Paragraph ellipsis={{ rows: 2 }} style={{ marginBottom: 0 }}>
|
||||
{template.role}
|
||||
</Paragraph>
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
]} />
|
||||
|
||||
{/* 创建/编辑弹窗 */}
|
||||
<Modal
|
||||
title={editingAgent ? '编辑Agent' : '创建Agent'}
|
||||
open={modalVisible}
|
||||
onOk={handleSave}
|
||||
onCancel={() => setModalVisible(false)}
|
||||
width={700}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="名称"
|
||||
rules={[{ required: true, message: '请输入名称' }]}
|
||||
>
|
||||
<Input placeholder="Agent名称" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
name="role"
|
||||
label="角色"
|
||||
rules={[{ required: true, message: '请输入角色' }]}
|
||||
>
|
||||
<Input placeholder="如: 产品经理、开发工程师" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Form.Item
|
||||
name="system_prompt"
|
||||
label={
|
||||
<Space>
|
||||
系统提示词
|
||||
<Tooltip title="根据名称和角色自动生成">
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={generatingPrompt ? <LoadingOutlined /> : <BulbOutlined />}
|
||||
onClick={handleGeneratePrompt}
|
||||
disabled={generatingPrompt}
|
||||
>
|
||||
{generatingPrompt ? '生成中...' : 'AI生成'}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
}
|
||||
rules={[{ required: true, message: '请输入系统提示词' }]}
|
||||
>
|
||||
<TextArea rows={4} placeholder="定义Agent的行为和专业领域,或点击右上角「AI生成」自动编写" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="provider_id"
|
||||
label="AI接口"
|
||||
rules={[{ required: true, message: '请选择AI接口' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="选择AI接口"
|
||||
options={enabledProviders.map(p => ({
|
||||
value: p.provider_id,
|
||||
label: `${p.name} (${p.model})`
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item name="temperature" label="温度 (创造性)">
|
||||
<Slider min={0} max={2} step={0.1} marks={{ 0: '精确', 1: '平衡', 2: '创造' }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="max_tokens" label="最大Token数">
|
||||
<Slider min={500} max={8000} step={100} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={8}>
|
||||
<Form.Item name="memory_enabled" label="启用记忆" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item name="multimodal" label="多模态支持" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item name="color" label="代表颜色">
|
||||
<Input type="color" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item name="speak_threshold" label="发言倾向">
|
||||
<Slider min={0} max={1} step={0.1} marks={{ 0: '谨慎', 0.5: '适中', 1: '积极' }} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="max_speak_per_round" label="每轮最大发言次数">
|
||||
<Slider min={1} max={5} step={1} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* 测试弹窗 */}
|
||||
<Modal
|
||||
title={`测试Agent: ${testingAgent?.name}`}
|
||||
open={testModalVisible}
|
||||
onCancel={() => setTestModalVisible(false)}
|
||||
footer={null}
|
||||
width={600}
|
||||
>
|
||||
<Form onFinish={(v) => runTest(v.message)}>
|
||||
<Form.Item name="message" initialValue="你好,请简单介绍一下你自己。">
|
||||
<TextArea rows={2} placeholder="输入测试消息" />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" loading={testLoading}>
|
||||
发送测试
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
{testResult && (
|
||||
<Card title="响应" style={{ marginTop: 16 }}>
|
||||
<Paragraph style={{ whiteSpace: 'pre-wrap' }}>
|
||||
{testResult}
|
||||
</Paragraph>
|
||||
</Card>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AgentManagement
|
||||
419
frontend/src/pages/ChatRoom.tsx
Normal file
419
frontend/src/pages/ChatRoom.tsx
Normal file
@@ -0,0 +1,419 @@
|
||||
/**
|
||||
* 聊天室页面
|
||||
*/
|
||||
import React, { useEffect, useState, useRef } from 'react'
|
||||
import { useParams, useNavigate } from 'react-router-dom'
|
||||
import {
|
||||
Layout, Card, List, Button, Input, Modal, Form, Select,
|
||||
Typography, Tag, Space, Empty, Spin, message, Avatar, Progress
|
||||
} from 'antd'
|
||||
import {
|
||||
PlusOutlined, PlayCircleOutlined, PauseCircleOutlined,
|
||||
StopOutlined, RobotOutlined, DeleteOutlined
|
||||
} from '@ant-design/icons'
|
||||
import { useChatroomStore } from '../stores/chatroomStore'
|
||||
import { useAgentStore } from '../stores/agentStore'
|
||||
import MessageBubble from '../components/MessageBubble'
|
||||
import TypingIndicator from '../components/TypingIndicator'
|
||||
|
||||
const { Sider, Content } = Layout
|
||||
const { Title, Text, Paragraph } = Typography
|
||||
const { TextArea } = Input
|
||||
|
||||
const ChatRoom: React.FC = () => {
|
||||
const { roomId } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const {
|
||||
chatrooms, currentRoom, messages, typingAgents, loading,
|
||||
fetchChatrooms, fetchChatroom, createChatroom, deleteChatroom,
|
||||
startDiscussion, pauseDiscussion, resumeDiscussion, stopDiscussion,
|
||||
connectWebSocket, disconnectWebSocket, fetchMessages
|
||||
} = useChatroomStore()
|
||||
|
||||
const { agents, fetchAgents, getAgentById } = useAgentStore()
|
||||
|
||||
const [createModalVisible, setCreateModalVisible] = useState(false)
|
||||
const [objectiveModalVisible, setObjectiveModalVisible] = useState(false)
|
||||
const [objective, setObjective] = useState('')
|
||||
const [form] = Form.useForm()
|
||||
|
||||
useEffect(() => {
|
||||
fetchChatrooms()
|
||||
fetchAgents()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (roomId) {
|
||||
fetchChatroom(roomId)
|
||||
fetchMessages(roomId)
|
||||
connectWebSocket(roomId)
|
||||
}
|
||||
|
||||
return () => {
|
||||
disconnectWebSocket()
|
||||
}
|
||||
}, [roomId])
|
||||
|
||||
// 自动滚动到底部
|
||||
useEffect(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
||||
}, [messages])
|
||||
|
||||
// 获取正在输入的Agent信息
|
||||
const typingAgentsList = Array.from(typingAgents)
|
||||
.map(id => getAgentById(id))
|
||||
.filter(Boolean) as typeof agents
|
||||
|
||||
// 创建聊天室
|
||||
const handleCreateRoom = async () => {
|
||||
try {
|
||||
const values = await form.validateFields()
|
||||
const room = await createChatroom({
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
agents: values.agents,
|
||||
moderator_agent_id: values.moderator_agent_id
|
||||
})
|
||||
message.success('创建成功')
|
||||
setCreateModalVisible(false)
|
||||
navigate(`/chatroom/${room.room_id}`)
|
||||
} catch (e) {
|
||||
message.error((e as Error).message)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除聊天室
|
||||
const handleDeleteRoom = (roomId: string, roomName: string) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除聊天室 "${roomName}" 吗?`,
|
||||
okText: '删除',
|
||||
okType: 'danger',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await deleteChatroom(roomId)
|
||||
message.success('删除成功')
|
||||
if (currentRoom?.room_id === roomId) {
|
||||
navigate('/chatroom')
|
||||
}
|
||||
} catch (e) {
|
||||
message.error((e as Error).message)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 启动讨论
|
||||
const handleStartDiscussion = async () => {
|
||||
if (!currentRoom || !objective.trim()) return
|
||||
try {
|
||||
await startDiscussion(currentRoom.room_id, objective)
|
||||
message.success('讨论已启动')
|
||||
setObjectiveModalVisible(false)
|
||||
} catch (e) {
|
||||
message.error((e as Error).message)
|
||||
}
|
||||
}
|
||||
|
||||
// 暂停/恢复/停止
|
||||
const handlePause = async () => {
|
||||
if (!currentRoom) return
|
||||
try {
|
||||
await pauseDiscussion(currentRoom.room_id)
|
||||
message.info('讨论已暂停')
|
||||
} catch (e) {
|
||||
message.error((e as Error).message)
|
||||
}
|
||||
}
|
||||
|
||||
const handleResume = async () => {
|
||||
if (!currentRoom) return
|
||||
try {
|
||||
await resumeDiscussion(currentRoom.room_id)
|
||||
message.success('讨论已恢复')
|
||||
} catch (e) {
|
||||
message.error((e as Error).message)
|
||||
}
|
||||
}
|
||||
|
||||
const handleStop = async () => {
|
||||
if (!currentRoom) return
|
||||
Modal.confirm({
|
||||
title: '确认停止',
|
||||
content: '确定要停止当前讨论吗?',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await stopDiscussion(currentRoom.room_id)
|
||||
message.info('正在停止讨论...')
|
||||
} catch (e) {
|
||||
message.error((e as Error).message)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const enabledAgents = agents.filter(a => a.enabled)
|
||||
|
||||
return (
|
||||
<Layout style={{ marginLeft: 200, height: 'calc(100vh - 48px)' }}>
|
||||
{/* 聊天室列表 */}
|
||||
<Sider width={280} theme="light" style={{
|
||||
borderRight: '1px solid #f0f0f0',
|
||||
overflow: 'auto'
|
||||
}}>
|
||||
<div style={{ padding: 16 }}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
block
|
||||
onClick={() => setCreateModalVisible(true)}
|
||||
>
|
||||
创建聊天室
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<List
|
||||
dataSource={chatrooms}
|
||||
renderItem={(room) => (
|
||||
<List.Item
|
||||
style={{
|
||||
padding: '12px 16px',
|
||||
cursor: 'pointer',
|
||||
background: room.room_id === roomId ? '#e6f7ff' : 'transparent'
|
||||
}}
|
||||
onClick={() => navigate(`/chatroom/${room.room_id}`)}
|
||||
actions={[
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
size="small"
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleDeleteRoom(room.room_id, room.name)
|
||||
}}
|
||||
/>
|
||||
]}
|
||||
>
|
||||
<List.Item.Meta
|
||||
title={
|
||||
<Space>
|
||||
<span>{room.name}</span>
|
||||
<Tag color={
|
||||
room.status === 'active' ? 'green' :
|
||||
room.status === 'paused' ? 'orange' :
|
||||
room.status === 'completed' ? 'blue' : 'default'
|
||||
} style={{ marginLeft: 4 }}>
|
||||
{room.status === 'active' ? '进行中' :
|
||||
room.status === 'paused' ? '已暂停' :
|
||||
room.status === 'completed' ? '已完成' : '空闲'}
|
||||
</Tag>
|
||||
</Space>
|
||||
}
|
||||
description={
|
||||
<Text type="secondary" ellipsis style={{ fontSize: 12 }}>
|
||||
{room.agents.length} 个Agent
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Sider>
|
||||
|
||||
{/* 聊天内容区 */}
|
||||
<Content style={{ display: 'flex', flexDirection: 'column', background: '#fff' }}>
|
||||
{currentRoom ? (
|
||||
<>
|
||||
{/* 头部 */}
|
||||
<div style={{
|
||||
padding: '12px 24px',
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<div>
|
||||
<Title level={4} style={{ margin: 0 }}>{currentRoom.name}</Title>
|
||||
{currentRoom.objective && (
|
||||
<Text type="secondary">{currentRoom.objective}</Text>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Space>
|
||||
{currentRoom.status === 'active' && (
|
||||
<Tag color="green">第 {currentRoom.current_round} 轮</Tag>
|
||||
)}
|
||||
|
||||
{currentRoom.status === 'idle' && (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlayCircleOutlined />}
|
||||
onClick={() => setObjectiveModalVisible(true)}
|
||||
>
|
||||
开始讨论
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{currentRoom.status === 'active' && (
|
||||
<>
|
||||
<Button icon={<PauseCircleOutlined />} onClick={handlePause}>
|
||||
暂停
|
||||
</Button>
|
||||
<Button danger icon={<StopOutlined />} onClick={handleStop}>
|
||||
停止
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{currentRoom.status === 'paused' && (
|
||||
<>
|
||||
<Button type="primary" icon={<PlayCircleOutlined />} onClick={handleResume}>
|
||||
继续
|
||||
</Button>
|
||||
<Button danger icon={<StopOutlined />} onClick={handleStop}>
|
||||
停止
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{currentRoom.status === 'completed' && (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlayCircleOutlined />}
|
||||
onClick={() => setObjectiveModalVisible(true)}
|
||||
>
|
||||
新讨论
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
{/* Agent列表 */}
|
||||
<div style={{
|
||||
padding: '8px 24px',
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
flexWrap: 'wrap'
|
||||
}}>
|
||||
{currentRoom.agents.map(agentId => {
|
||||
const agent = getAgentById(agentId)
|
||||
return agent ? (
|
||||
<Tag
|
||||
key={agentId}
|
||||
color={agent.color}
|
||||
icon={<Avatar size={16} style={{ background: agent.color }}><RobotOutlined /></Avatar>}
|
||||
>
|
||||
{agent.name}
|
||||
</Tag>
|
||||
) : null
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* 消息区 */}
|
||||
<div style={{
|
||||
flex: 1,
|
||||
overflow: 'auto',
|
||||
padding: '16px 24px',
|
||||
background: '#fafafa'
|
||||
}}>
|
||||
{messages.length > 0 ? (
|
||||
<>
|
||||
{messages.map(msg => (
|
||||
<MessageBubble
|
||||
key={msg.message_id}
|
||||
message={msg}
|
||||
agent={msg.agent_id ? getAgentById(msg.agent_id) : undefined}
|
||||
/>
|
||||
))}
|
||||
<TypingIndicator agents={typingAgentsList} />
|
||||
<div ref={messagesEndRef} />
|
||||
</>
|
||||
) : (
|
||||
<Empty description="暂无消息" />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div style={{
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<Empty description="选择或创建一个聊天室开始讨论" />
|
||||
</div>
|
||||
)}
|
||||
</Content>
|
||||
|
||||
{/* 创建聊天室弹窗 */}
|
||||
<Modal
|
||||
title="创建聊天室"
|
||||
open={createModalVisible}
|
||||
onOk={handleCreateRoom}
|
||||
onCancel={() => setCreateModalVisible(false)}
|
||||
width={600}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="名称"
|
||||
rules={[{ required: true, message: '请输入名称' }]}
|
||||
>
|
||||
<Input placeholder="聊天室名称" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="description" label="描述">
|
||||
<TextArea rows={2} placeholder="聊天室描述" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="agents"
|
||||
label="参与Agent"
|
||||
rules={[{ required: true, message: '请选择至少一个Agent' }]}
|
||||
>
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder="选择参与讨论的Agent"
|
||||
options={enabledAgents.map(a => ({
|
||||
value: a.agent_id,
|
||||
label: a.name
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="moderator_agent_id" label="主持人Agent">
|
||||
<Select
|
||||
placeholder="选择主持人(用于共识判断)"
|
||||
allowClear
|
||||
options={enabledAgents.map(a => ({
|
||||
value: a.agent_id,
|
||||
label: a.name
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* 设置讨论目标弹窗 */}
|
||||
<Modal
|
||||
title="设置讨论目标"
|
||||
open={objectiveModalVisible}
|
||||
onOk={handleStartDiscussion}
|
||||
onCancel={() => setObjectiveModalVisible(false)}
|
||||
>
|
||||
<TextArea
|
||||
rows={4}
|
||||
placeholder="输入本次讨论的目标,例如:设计一个用户友好的登录系统"
|
||||
value={objective}
|
||||
onChange={(e) => setObjective(e.target.value)}
|
||||
/>
|
||||
</Modal>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChatRoom
|
||||
199
frontend/src/pages/Dashboard.tsx
Normal file
199
frontend/src/pages/Dashboard.tsx
Normal file
@@ -0,0 +1,199 @@
|
||||
/**
|
||||
* 控制台页面
|
||||
* 显示系统概览
|
||||
*/
|
||||
import React, { useEffect } from 'react'
|
||||
import { Row, Col, Card, Statistic, List, Typography, Tag, Button, Empty } from 'antd'
|
||||
import {
|
||||
ApiOutlined,
|
||||
RobotOutlined,
|
||||
MessageOutlined,
|
||||
CheckCircleOutlined,
|
||||
ArrowRightOutlined
|
||||
} from '@ant-design/icons'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useProviderStore } from '../stores/providerStore'
|
||||
import { useAgentStore } from '../stores/agentStore'
|
||||
import { useChatroomStore } from '../stores/chatroomStore'
|
||||
|
||||
const { Title, Text } = Typography
|
||||
|
||||
const Dashboard: React.FC = () => {
|
||||
const navigate = useNavigate()
|
||||
const { providers, fetchProviders } = useProviderStore()
|
||||
const { agents, fetchAgents } = useAgentStore()
|
||||
const { chatrooms, fetchChatrooms } = useChatroomStore()
|
||||
|
||||
useEffect(() => {
|
||||
fetchProviders()
|
||||
fetchAgents()
|
||||
fetchChatrooms()
|
||||
}, [])
|
||||
|
||||
const enabledProviders = providers.filter(p => p.enabled)
|
||||
const enabledAgents = agents.filter(a => a.enabled)
|
||||
const activeRooms = chatrooms.filter(r => r.status === 'active')
|
||||
|
||||
return (
|
||||
<div style={{ marginLeft: 200 }}>
|
||||
<Title level={2}>控制台</Title>
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<Row gutter={16} style={{ marginBottom: 24 }}>
|
||||
<Col span={6}>
|
||||
<Card hoverable onClick={() => navigate('/providers')}>
|
||||
<Statistic
|
||||
title="AI接口"
|
||||
value={enabledProviders.length}
|
||||
suffix={`/ ${providers.length}`}
|
||||
prefix={<ApiOutlined />}
|
||||
valueStyle={{ color: '#1890ff' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card hoverable onClick={() => navigate('/agents')}>
|
||||
<Statistic
|
||||
title="Agent"
|
||||
value={enabledAgents.length}
|
||||
suffix={`/ ${agents.length}`}
|
||||
prefix={<RobotOutlined />}
|
||||
valueStyle={{ color: '#52c41a' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card hoverable onClick={() => navigate('/chatroom')}>
|
||||
<Statistic
|
||||
title="聊天室"
|
||||
value={chatrooms.length}
|
||||
prefix={<MessageOutlined />}
|
||||
valueStyle={{ color: '#722ed1' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="活跃讨论"
|
||||
value={activeRooms.length}
|
||||
prefix={<CheckCircleOutlined />}
|
||||
valueStyle={{ color: activeRooms.length > 0 ? '#fa8c16' : '#999' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={16}>
|
||||
{/* 最近聊天室 */}
|
||||
<Col span={12}>
|
||||
<Card
|
||||
title="聊天室"
|
||||
extra={
|
||||
<Button type="link" onClick={() => navigate('/chatroom')}>
|
||||
查看全部 <ArrowRightOutlined />
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{chatrooms.length > 0 ? (
|
||||
<List
|
||||
dataSource={chatrooms.slice(0, 5)}
|
||||
renderItem={(room) => (
|
||||
<List.Item
|
||||
actions={[
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => navigate(`/chatroom/${room.room_id}`)}
|
||||
>
|
||||
进入
|
||||
</Button>
|
||||
]}
|
||||
>
|
||||
<List.Item.Meta
|
||||
title={
|
||||
<span>
|
||||
{room.name}
|
||||
<Tag
|
||||
color={
|
||||
room.status === 'active' ? 'green' :
|
||||
room.status === 'paused' ? 'orange' :
|
||||
room.status === 'completed' ? 'blue' : 'default'
|
||||
}
|
||||
style={{ marginLeft: 8 }}
|
||||
>
|
||||
{room.status === 'active' ? '进行中' :
|
||||
room.status === 'paused' ? '已暂停' :
|
||||
room.status === 'completed' ? '已完成' : '空闲'}
|
||||
</Tag>
|
||||
</span>
|
||||
}
|
||||
description={
|
||||
<Text type="secondary" ellipsis>
|
||||
{room.objective || room.description || '暂无描述'}
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Empty description="暂无聊天室" />
|
||||
)}
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* Agent列表 */}
|
||||
<Col span={12}>
|
||||
<Card
|
||||
title="Agent"
|
||||
extra={
|
||||
<Button type="link" onClick={() => navigate('/agents')}>
|
||||
管理 <ArrowRightOutlined />
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{agents.length > 0 ? (
|
||||
<List
|
||||
dataSource={agents.slice(0, 5)}
|
||||
renderItem={(agent) => (
|
||||
<List.Item>
|
||||
<List.Item.Meta
|
||||
avatar={
|
||||
<div style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: '50%',
|
||||
background: agent.color,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#fff'
|
||||
}}>
|
||||
<RobotOutlined />
|
||||
</div>
|
||||
}
|
||||
title={
|
||||
<span>
|
||||
{agent.name}
|
||||
{!agent.enabled && (
|
||||
<Tag color="default" style={{ marginLeft: 8 }}>已禁用</Tag>
|
||||
)}
|
||||
</span>
|
||||
}
|
||||
description={agent.role}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Empty description="暂无Agent" />
|
||||
)}
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Dashboard
|
||||
245
frontend/src/pages/DiscussionHistory.tsx
Normal file
245
frontend/src/pages/DiscussionHistory.tsx
Normal file
@@ -0,0 +1,245 @@
|
||||
/**
|
||||
* 讨论历史页面
|
||||
*/
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import {
|
||||
Table, Card, Tag, Typography, Space, Button,
|
||||
Modal, List, Descriptions, Progress, Empty
|
||||
} from 'antd'
|
||||
import {
|
||||
CheckCircleOutlined, CloseCircleOutlined,
|
||||
EyeOutlined, RobotOutlined
|
||||
} from '@ant-design/icons'
|
||||
import { discussionApi } from '../services/api'
|
||||
import type { DiscussionResult } from '../types'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const { Title, Text, Paragraph } = Typography
|
||||
|
||||
const DiscussionHistory: React.FC = () => {
|
||||
const [discussions, setDiscussions] = useState<DiscussionResult[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [detailVisible, setDetailVisible] = useState(false)
|
||||
const [selectedDiscussion, setSelectedDiscussion] = useState<DiscussionResult | null>(null)
|
||||
|
||||
const fetchDiscussions = async (page = 1, pageSize = 20) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const result = await discussionApi.list(undefined, pageSize)
|
||||
setDiscussions(result.discussions)
|
||||
setTotal(result.total)
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch discussions:', e)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchDiscussions()
|
||||
}, [])
|
||||
|
||||
const showDetail = (record: DiscussionResult) => {
|
||||
setSelectedDiscussion(record)
|
||||
setDetailVisible(true)
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '讨论目标',
|
||||
dataIndex: 'objective',
|
||||
key: 'objective',
|
||||
width: 300,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'consensus_reached',
|
||||
key: 'consensus_reached',
|
||||
width: 120,
|
||||
render: (reached: boolean, record: DiscussionResult) => (
|
||||
<Space>
|
||||
{reached ? (
|
||||
<Tag color="success" icon={<CheckCircleOutlined />}>达成共识</Tag>
|
||||
) : (
|
||||
<Tag color="default" icon={<CloseCircleOutlined />}>未达成</Tag>
|
||||
)}
|
||||
</Space>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '置信度',
|
||||
dataIndex: 'confidence',
|
||||
key: 'confidence',
|
||||
width: 120,
|
||||
render: (confidence: number) => (
|
||||
<Progress
|
||||
percent={Math.round(confidence * 100)}
|
||||
size="small"
|
||||
strokeColor={confidence >= 0.8 ? '#52c41a' : confidence >= 0.5 ? '#faad14' : '#ff4d4f'}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '轮数',
|
||||
dataIndex: 'total_rounds',
|
||||
key: 'total_rounds',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '消息数',
|
||||
dataIndex: 'total_messages',
|
||||
key: 'total_messages',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '参与Agent',
|
||||
dataIndex: 'participating_agents',
|
||||
key: 'participating_agents',
|
||||
width: 150,
|
||||
render: (agents: string[]) => (
|
||||
<Text type="secondary">{agents.length} 个</Text>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
width: 180,
|
||||
render: (time: string) => dayjs(time).format('YYYY-MM-DD HH:mm')
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 80,
|
||||
render: (_: unknown, record: DiscussionResult) => (
|
||||
<Button
|
||||
type="link"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => showDetail(record)}
|
||||
>
|
||||
详情
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<div style={{ marginLeft: 200 }}>
|
||||
<Title level={2}>讨论历史</Title>
|
||||
|
||||
<Card>
|
||||
<Table
|
||||
dataSource={discussions}
|
||||
columns={columns}
|
||||
rowKey="discussion_id"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
total,
|
||||
pageSize: 20,
|
||||
showTotal: (t) => `共 ${t} 条记录`
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* 详情弹窗 */}
|
||||
<Modal
|
||||
title="讨论详情"
|
||||
open={detailVisible}
|
||||
onCancel={() => setDetailVisible(false)}
|
||||
footer={null}
|
||||
width={800}
|
||||
>
|
||||
{selectedDiscussion && (
|
||||
<div>
|
||||
<Descriptions bordered column={2}>
|
||||
<Descriptions.Item label="讨论目标" span={2}>
|
||||
{selectedDiscussion.objective}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="状态">
|
||||
{selectedDiscussion.consensus_reached ? (
|
||||
<Tag color="success">达成共识</Tag>
|
||||
) : (
|
||||
<Tag color="default">未达成共识</Tag>
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="置信度">
|
||||
<Progress
|
||||
percent={Math.round(selectedDiscussion.confidence * 100)}
|
||||
size="small"
|
||||
style={{ width: 150 }}
|
||||
/>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="总轮数">
|
||||
{selectedDiscussion.total_rounds}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="总消息数">
|
||||
{selectedDiscussion.total_messages}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="结束原因" span={2}>
|
||||
{selectedDiscussion.end_reason || '无'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="开始时间">
|
||||
{dayjs(selectedDiscussion.created_at).format('YYYY-MM-DD HH:mm:ss')}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="结束时间">
|
||||
{selectedDiscussion.completed_at
|
||||
? dayjs(selectedDiscussion.completed_at).format('YYYY-MM-DD HH:mm:ss')
|
||||
: '进行中'}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
<Card title="结果摘要" style={{ marginTop: 16 }}>
|
||||
<Paragraph>{selectedDiscussion.summary || '暂无摘要'}</Paragraph>
|
||||
</Card>
|
||||
|
||||
<Card title="行动项" style={{ marginTop: 16 }}>
|
||||
{selectedDiscussion.action_items.length > 0 ? (
|
||||
<List
|
||||
dataSource={selectedDiscussion.action_items}
|
||||
renderItem={(item) => (
|
||||
<List.Item>
|
||||
<CheckCircleOutlined style={{ color: '#52c41a', marginRight: 8 }} />
|
||||
{item}
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Empty description="暂无行动项" />
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card title="未解决问题" style={{ marginTop: 16 }}>
|
||||
{selectedDiscussion.unresolved_issues.length > 0 ? (
|
||||
<List
|
||||
dataSource={selectedDiscussion.unresolved_issues}
|
||||
renderItem={(item) => (
|
||||
<List.Item>
|
||||
<CloseCircleOutlined style={{ color: '#faad14', marginRight: 8 }} />
|
||||
{item}
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Empty description="无未解决问题" />
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card title="Agent贡献" style={{ marginTop: 16 }}>
|
||||
<Space wrap>
|
||||
{Object.entries(selectedDiscussion.agent_contributions).map(([agentId, count]) => (
|
||||
<Tag key={agentId} icon={<RobotOutlined />}>
|
||||
{agentId}: {count}条消息
|
||||
</Tag>
|
||||
))}
|
||||
</Space>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DiscussionHistory
|
||||
260
frontend/src/pages/ProviderConfig.tsx
Normal file
260
frontend/src/pages/ProviderConfig.tsx
Normal file
@@ -0,0 +1,260 @@
|
||||
/**
|
||||
* AI接口配置页面
|
||||
*/
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import {
|
||||
Row, Col, Button, Modal, Form, Input, Select, Switch,
|
||||
InputNumber, Typography, message, Spin, Empty
|
||||
} from 'antd'
|
||||
import { PlusOutlined } from '@ant-design/icons'
|
||||
import { useProviderStore } from '../stores/providerStore'
|
||||
import ProviderCard from '../components/ProviderCard'
|
||||
import type { AIProvider } from '../types'
|
||||
|
||||
const { Title } = Typography
|
||||
|
||||
// 提供商类型选项
|
||||
const PROVIDER_TYPES = [
|
||||
{ value: 'minimax', label: 'MiniMax' },
|
||||
{ value: 'zhipu', label: '智谱AI (ChatGLM)' },
|
||||
{ value: 'openrouter', label: 'OpenRouter' },
|
||||
{ value: 'kimi', label: 'Kimi (月之暗面)' },
|
||||
{ value: 'deepseek', label: 'DeepSeek' },
|
||||
{ value: 'gemini', label: 'Google Gemini' },
|
||||
{ value: 'ollama', label: 'Ollama (本地)' },
|
||||
{ value: 'llmstudio', label: 'LLM Studio (本地)' }
|
||||
]
|
||||
|
||||
const ProviderConfig: React.FC = () => {
|
||||
const {
|
||||
providers,
|
||||
loading,
|
||||
fetchProviders,
|
||||
createProvider,
|
||||
updateProvider,
|
||||
deleteProvider,
|
||||
testProvider
|
||||
} = useProviderStore()
|
||||
|
||||
const [modalVisible, setModalVisible] = useState(false)
|
||||
const [editingProvider, setEditingProvider] = useState<AIProvider | null>(null)
|
||||
const [testingId, setTestingId] = useState<string | null>(null)
|
||||
const [form] = Form.useForm()
|
||||
|
||||
useEffect(() => {
|
||||
fetchProviders()
|
||||
}, [])
|
||||
|
||||
// 打开创建/编辑弹窗
|
||||
const openModal = (provider?: AIProvider) => {
|
||||
setEditingProvider(provider || null)
|
||||
if (provider) {
|
||||
form.setFieldsValue({
|
||||
provider_type: provider.provider_type,
|
||||
name: provider.name,
|
||||
model: provider.model,
|
||||
api_key: '', // 不显示已有密钥
|
||||
base_url: provider.base_url,
|
||||
use_proxy: provider.use_proxy,
|
||||
http_proxy: provider.proxy_config?.http_proxy,
|
||||
https_proxy: provider.proxy_config?.https_proxy,
|
||||
timeout: provider.timeout
|
||||
})
|
||||
} else {
|
||||
form.resetFields()
|
||||
}
|
||||
setModalVisible(true)
|
||||
}
|
||||
|
||||
// 保存
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
const values = await form.validateFields()
|
||||
|
||||
const data = {
|
||||
provider_type: values.provider_type,
|
||||
name: values.name,
|
||||
model: values.model,
|
||||
api_key: values.api_key || undefined,
|
||||
base_url: values.base_url || '',
|
||||
use_proxy: values.use_proxy || false,
|
||||
proxy_config: values.use_proxy ? {
|
||||
http_proxy: values.http_proxy,
|
||||
https_proxy: values.https_proxy
|
||||
} : undefined,
|
||||
timeout: values.timeout || 60
|
||||
}
|
||||
|
||||
if (editingProvider) {
|
||||
await updateProvider(editingProvider.provider_id, data)
|
||||
message.success('更新成功')
|
||||
} else {
|
||||
await createProvider(data)
|
||||
message.success('创建成功')
|
||||
}
|
||||
|
||||
setModalVisible(false)
|
||||
} catch (e) {
|
||||
message.error((e as Error).message)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = (provider: AIProvider) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除AI接口 "${provider.name}" 吗?`,
|
||||
okText: '删除',
|
||||
okType: 'danger',
|
||||
onOk: async () => {
|
||||
try {
|
||||
await deleteProvider(provider.provider_id)
|
||||
message.success('删除成功')
|
||||
} catch (e) {
|
||||
message.error((e as Error).message)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 测试
|
||||
const handleTest = async (provider: AIProvider) => {
|
||||
setTestingId(provider.provider_id)
|
||||
try {
|
||||
const result = await testProvider(provider.provider_id)
|
||||
if (result.success) {
|
||||
message.success(`连接成功,延迟: ${result.latency_ms?.toFixed(0)}ms`)
|
||||
} else {
|
||||
message.error(`连接失败: ${result.message}`)
|
||||
}
|
||||
} catch (e) {
|
||||
message.error((e as Error).message)
|
||||
} finally {
|
||||
setTestingId(null)
|
||||
}
|
||||
}
|
||||
|
||||
// 切换启用状态
|
||||
const handleToggle = async (provider: AIProvider, enabled: boolean) => {
|
||||
try {
|
||||
await updateProvider(provider.provider_id, { enabled })
|
||||
message.success(enabled ? '已启用' : '已禁用')
|
||||
} catch (e) {
|
||||
message.error((e as Error).message)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ marginLeft: 200 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 24 }}>
|
||||
<Title level={2}>AI接口配置</Title>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => openModal()}>
|
||||
添加接口
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Spin spinning={loading}>
|
||||
{providers.length > 0 ? (
|
||||
<Row gutter={[16, 16]}>
|
||||
{providers.map((provider) => (
|
||||
<Col key={provider.provider_id} xs={24} sm={12} lg={8} xl={6}>
|
||||
<ProviderCard
|
||||
provider={provider}
|
||||
onEdit={openModal}
|
||||
onDelete={handleDelete}
|
||||
onTest={handleTest}
|
||||
onToggleEnabled={handleToggle}
|
||||
testing={testingId === provider.provider_id}
|
||||
/>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
) : (
|
||||
<Empty description="暂无AI接口配置" />
|
||||
)}
|
||||
</Spin>
|
||||
|
||||
{/* 创建/编辑弹窗 */}
|
||||
<Modal
|
||||
title={editingProvider ? '编辑AI接口' : '添加AI接口'}
|
||||
open={modalVisible}
|
||||
onOk={handleSave}
|
||||
onCancel={() => setModalVisible(false)}
|
||||
width={600}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
name="provider_type"
|
||||
label="提供商类型"
|
||||
rules={[{ required: true, message: '请选择提供商类型' }]}
|
||||
>
|
||||
<Select
|
||||
options={PROVIDER_TYPES}
|
||||
placeholder="选择AI提供商"
|
||||
disabled={!!editingProvider}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="名称"
|
||||
rules={[{ required: true, message: '请输入名称' }]}
|
||||
>
|
||||
<Input placeholder="自定义名称" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="model"
|
||||
label="模型"
|
||||
rules={[{ required: true, message: '请输入模型名称' }]}
|
||||
>
|
||||
<Input placeholder="如: gpt-4-turbo, glm-4" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="api_key"
|
||||
label="API密钥"
|
||||
rules={[{ required: !editingProvider, message: '请输入API密钥' }]}
|
||||
>
|
||||
<Input.Password
|
||||
placeholder={editingProvider ? '留空则不修改' : '输入API密钥'}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="base_url" label="API地址">
|
||||
<Input placeholder="留空使用默认地址" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="use_proxy" label="使用代理" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item noStyle shouldUpdate={(prev, curr) => prev.use_proxy !== curr.use_proxy}>
|
||||
{({ getFieldValue }) =>
|
||||
getFieldValue('use_proxy') && (
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item name="http_proxy" label="HTTP代理">
|
||||
<Input placeholder="http://127.0.0.1:7890" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="https_proxy" label="HTTPS代理">
|
||||
<Input placeholder="http://127.0.0.1:7890" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="timeout" label="超时时间(秒)">
|
||||
<InputNumber min={10} max={300} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProviderConfig
|
||||
195
frontend/src/services/api.ts
Normal file
195
frontend/src/services/api.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* API服务封装
|
||||
*/
|
||||
import axios from 'axios'
|
||||
import type {
|
||||
AIProvider,
|
||||
Agent,
|
||||
AgentTemplate,
|
||||
ChatRoom,
|
||||
Message,
|
||||
DiscussionResult,
|
||||
TestResponse
|
||||
} from '../types'
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: '/api',
|
||||
timeout: 30000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
// 响应拦截器
|
||||
api.interceptors.response.use(
|
||||
response => response.data,
|
||||
error => {
|
||||
const message = error.response?.data?.detail || error.message || '请求失败'
|
||||
return Promise.reject(new Error(message))
|
||||
}
|
||||
)
|
||||
|
||||
// ============ AI接口管理 ============
|
||||
|
||||
export const providerApi = {
|
||||
// 获取所有接口
|
||||
list: (enabledOnly = false): Promise<AIProvider[]> =>
|
||||
api.get('/providers', { params: { enabled_only: enabledOnly } }),
|
||||
|
||||
// 获取单个接口
|
||||
get: (id: string): Promise<AIProvider> =>
|
||||
api.get(`/providers/${id}`),
|
||||
|
||||
// 创建接口
|
||||
create: (data: Partial<AIProvider>): Promise<AIProvider> =>
|
||||
api.post('/providers', data),
|
||||
|
||||
// 更新接口
|
||||
update: (id: string, data: Partial<AIProvider>): Promise<AIProvider> =>
|
||||
api.put(`/providers/${id}`, data),
|
||||
|
||||
// 删除接口
|
||||
delete: (id: string): Promise<void> =>
|
||||
api.delete(`/providers/${id}`),
|
||||
|
||||
// 测试接口
|
||||
test: (id: string): Promise<TestResponse> =>
|
||||
api.post(`/providers/${id}/test`),
|
||||
|
||||
// 测试配置
|
||||
testConfig: (data: Record<string, unknown>): Promise<TestResponse> =>
|
||||
api.post('/providers/test', data)
|
||||
}
|
||||
|
||||
// ============ Agent管理 ============
|
||||
|
||||
export const agentApi = {
|
||||
// 获取所有Agent
|
||||
list: (enabledOnly = false): Promise<Agent[]> =>
|
||||
api.get('/agents', { params: { enabled_only: enabledOnly } }),
|
||||
|
||||
// 获取单个Agent
|
||||
get: (id: string): Promise<Agent> =>
|
||||
api.get(`/agents/${id}`),
|
||||
|
||||
// 创建Agent
|
||||
create: (data: Partial<Agent>): Promise<Agent> =>
|
||||
api.post('/agents', data),
|
||||
|
||||
// 更新Agent
|
||||
update: (id: string, data: Partial<Agent>): Promise<Agent> =>
|
||||
api.put(`/agents/${id}`, data),
|
||||
|
||||
// 删除Agent
|
||||
delete: (id: string): Promise<void> =>
|
||||
api.delete(`/agents/${id}`),
|
||||
|
||||
// 测试Agent
|
||||
test: (id: string, message?: string): Promise<TestResponse & { response?: string }> =>
|
||||
api.post(`/agents/${id}/test`, { message }),
|
||||
|
||||
// 复制Agent
|
||||
duplicate: (id: string, newName?: string): Promise<Agent> =>
|
||||
api.post(`/agents/${id}/duplicate`, null, { params: { new_name: newName } }),
|
||||
|
||||
// 获取模板
|
||||
getTemplates: (): Promise<AgentTemplate[]> =>
|
||||
api.get('/agents/templates'),
|
||||
|
||||
// 从模板创建
|
||||
createFromTemplate: (templateId: string, providerId: string): Promise<Agent> =>
|
||||
api.post(`/agents/from-template/${templateId}`, null, { params: { provider_id: providerId } }),
|
||||
|
||||
// AI生成系统提示词
|
||||
generatePrompt: (data: {
|
||||
provider_id: string
|
||||
name: string
|
||||
role: string
|
||||
description?: string
|
||||
}): Promise<{ success: boolean; prompt?: string; message?: string }> =>
|
||||
api.post('/agents/generate-prompt', data)
|
||||
}
|
||||
|
||||
// ============ 聊天室管理 ============
|
||||
|
||||
export const chatroomApi = {
|
||||
// 获取所有聊天室
|
||||
list: (): Promise<ChatRoom[]> =>
|
||||
api.get('/chatrooms'),
|
||||
|
||||
// 获取单个聊天室
|
||||
get: (id: string): Promise<ChatRoom> =>
|
||||
api.get(`/chatrooms/${id}`),
|
||||
|
||||
// 创建聊天室
|
||||
create: (data: Partial<ChatRoom>): Promise<ChatRoom> =>
|
||||
api.post('/chatrooms', data),
|
||||
|
||||
// 更新聊天室
|
||||
update: (id: string, data: Partial<ChatRoom>): Promise<ChatRoom> =>
|
||||
api.put(`/chatrooms/${id}`, data),
|
||||
|
||||
// 删除聊天室
|
||||
delete: (id: string): Promise<void> =>
|
||||
api.delete(`/chatrooms/${id}`),
|
||||
|
||||
// 添加Agent
|
||||
addAgent: (roomId: string, agentId: string): Promise<ChatRoom> =>
|
||||
api.post(`/chatrooms/${roomId}/agents/${agentId}`),
|
||||
|
||||
// 移除Agent
|
||||
removeAgent: (roomId: string, agentId: string): Promise<ChatRoom> =>
|
||||
api.delete(`/chatrooms/${roomId}/agents/${agentId}`),
|
||||
|
||||
// 获取消息
|
||||
getMessages: (roomId: string, limit = 50, discussionId?: string): Promise<Message[]> =>
|
||||
api.get(`/chatrooms/${roomId}/messages`, {
|
||||
params: { limit, discussion_id: discussionId }
|
||||
}),
|
||||
|
||||
// 启动讨论
|
||||
startDiscussion: (roomId: string, objective: string): Promise<{ is_active: boolean }> =>
|
||||
api.post(`/chatrooms/${roomId}/start`, { objective }),
|
||||
|
||||
// 暂停讨论
|
||||
pauseDiscussion: (roomId: string): Promise<{ is_active: boolean }> =>
|
||||
api.post(`/chatrooms/${roomId}/pause`),
|
||||
|
||||
// 恢复讨论
|
||||
resumeDiscussion: (roomId: string): Promise<{ is_active: boolean }> =>
|
||||
api.post(`/chatrooms/${roomId}/resume`),
|
||||
|
||||
// 停止讨论
|
||||
stopDiscussion: (roomId: string): Promise<{ is_active: boolean }> =>
|
||||
api.post(`/chatrooms/${roomId}/stop`),
|
||||
|
||||
// 获取状态
|
||||
getStatus: (roomId: string): Promise<{
|
||||
is_active: boolean
|
||||
status: string
|
||||
current_round: number
|
||||
}> =>
|
||||
api.get(`/chatrooms/${roomId}/status`)
|
||||
}
|
||||
|
||||
// ============ 讨论结果 ============
|
||||
|
||||
export const discussionApi = {
|
||||
// 获取列表
|
||||
list: (roomId?: string, limit = 20): Promise<{ discussions: DiscussionResult[], total: number }> =>
|
||||
api.get('/discussions', { params: { room_id: roomId, limit } }),
|
||||
|
||||
// 获取单个
|
||||
get: (id: string): Promise<DiscussionResult> =>
|
||||
api.get(`/discussions/${id}`),
|
||||
|
||||
// 获取聊天室历史
|
||||
getRoomHistory: (roomId: string, limit = 10): Promise<DiscussionResult[]> =>
|
||||
api.get(`/discussions/room/${roomId}`, { params: { limit } }),
|
||||
|
||||
// 获取最新
|
||||
getLatest: (roomId: string): Promise<DiscussionResult> =>
|
||||
api.get(`/discussions/room/${roomId}/latest`)
|
||||
}
|
||||
|
||||
export default api
|
||||
173
frontend/src/services/websocket.ts
Normal file
173
frontend/src/services/websocket.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* WebSocket服务
|
||||
* 管理聊天室的实时连接
|
||||
*/
|
||||
import type { WSMessage, Message } from '../types'
|
||||
|
||||
type MessageHandler = (message: WSMessage) => void
|
||||
type ErrorHandler = (error: Event) => void
|
||||
|
||||
class WebSocketService {
|
||||
private ws: WebSocket | null = null
|
||||
private roomId: string | null = null
|
||||
private messageHandlers: MessageHandler[] = []
|
||||
private errorHandlers: ErrorHandler[] = []
|
||||
private reconnectAttempts = 0
|
||||
private maxReconnectAttempts = 5
|
||||
private reconnectDelay = 1000
|
||||
private heartbeatInterval: number | null = null
|
||||
|
||||
/**
|
||||
* 连接到聊天室
|
||||
*/
|
||||
connect(roomId: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.ws && this.roomId === roomId) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
// 关闭现有连接
|
||||
this.disconnect()
|
||||
|
||||
this.roomId = roomId
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
const wsUrl = `${protocol}//${window.location.host}/api/chatrooms/ws/${roomId}`
|
||||
|
||||
try {
|
||||
this.ws = new WebSocket(wsUrl)
|
||||
|
||||
this.ws.onopen = () => {
|
||||
console.log('WebSocket connected:', roomId)
|
||||
this.reconnectAttempts = 0
|
||||
this.startHeartbeat()
|
||||
resolve()
|
||||
}
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data) as WSMessage
|
||||
this.messageHandlers.forEach(handler => handler(data))
|
||||
} catch (e) {
|
||||
console.error('Failed to parse WebSocket message:', e)
|
||||
}
|
||||
}
|
||||
|
||||
this.ws.onerror = (error) => {
|
||||
console.error('WebSocket error:', error)
|
||||
this.errorHandlers.forEach(handler => handler(error))
|
||||
reject(error)
|
||||
}
|
||||
|
||||
this.ws.onclose = () => {
|
||||
console.log('WebSocket disconnected')
|
||||
this.stopHeartbeat()
|
||||
this.attemptReconnect()
|
||||
}
|
||||
} catch (e) {
|
||||
reject(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开连接
|
||||
*/
|
||||
disconnect(): void {
|
||||
this.stopHeartbeat()
|
||||
if (this.ws) {
|
||||
this.ws.close()
|
||||
this.ws = null
|
||||
}
|
||||
this.roomId = null
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
*/
|
||||
send(data: unknown): void {
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
this.ws.send(typeof data === 'string' ? data : JSON.stringify(data))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加消息处理器
|
||||
*/
|
||||
onMessage(handler: MessageHandler): () => void {
|
||||
this.messageHandlers.push(handler)
|
||||
return () => {
|
||||
this.messageHandlers = this.messageHandlers.filter(h => h !== handler)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加错误处理器
|
||||
*/
|
||||
onError(handler: ErrorHandler): () => void {
|
||||
this.errorHandlers.push(handler)
|
||||
return () => {
|
||||
this.errorHandlers = this.errorHandlers.filter(h => h !== handler)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否已连接
|
||||
*/
|
||||
isConnected(): boolean {
|
||||
return this.ws !== null && this.ws.readyState === WebSocket.OPEN
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前房间ID
|
||||
*/
|
||||
getCurrentRoomId(): string | null {
|
||||
return this.roomId
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始心跳
|
||||
*/
|
||||
private startHeartbeat(): void {
|
||||
this.heartbeatInterval = window.setInterval(() => {
|
||||
this.send('ping')
|
||||
}, 30000)
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止心跳
|
||||
*/
|
||||
private stopHeartbeat(): void {
|
||||
if (this.heartbeatInterval) {
|
||||
clearInterval(this.heartbeatInterval)
|
||||
this.heartbeatInterval = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试重连
|
||||
*/
|
||||
private attemptReconnect(): void {
|
||||
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
||||
console.log('Max reconnect attempts reached')
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.roomId) return
|
||||
|
||||
this.reconnectAttempts++
|
||||
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1)
|
||||
|
||||
console.log(`Attempting reconnect in ${delay}ms...`)
|
||||
|
||||
setTimeout(() => {
|
||||
if (this.roomId) {
|
||||
this.connect(this.roomId).catch(console.error)
|
||||
}
|
||||
}, delay)
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例
|
||||
export const wsService = new WebSocketService()
|
||||
export default wsService
|
||||
103
frontend/src/stores/agentStore.ts
Normal file
103
frontend/src/stores/agentStore.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Agent状态管理
|
||||
*/
|
||||
import { create } from 'zustand'
|
||||
import type { Agent, AgentTemplate } from '../types'
|
||||
import { agentApi } from '../services/api'
|
||||
|
||||
interface AgentState {
|
||||
agents: Agent[]
|
||||
templates: AgentTemplate[]
|
||||
loading: boolean
|
||||
error: string | null
|
||||
|
||||
// Actions
|
||||
fetchAgents: () => Promise<void>
|
||||
fetchTemplates: () => Promise<void>
|
||||
createAgent: (data: Partial<Agent>) => Promise<Agent>
|
||||
updateAgent: (id: string, data: Partial<Agent>) => Promise<Agent>
|
||||
deleteAgent: (id: string) => Promise<void>
|
||||
testAgent: (id: string, message?: string) => Promise<{ success: boolean; response?: string }>
|
||||
duplicateAgent: (id: string, newName?: string) => Promise<Agent>
|
||||
createFromTemplate: (templateId: string, providerId: string) => Promise<Agent>
|
||||
generatePrompt: (data: {
|
||||
provider_id: string
|
||||
name: string
|
||||
role: string
|
||||
description?: string
|
||||
}) => Promise<{ success: boolean; prompt?: string; message?: string }>
|
||||
getAgentById: (id: string) => Agent | undefined
|
||||
}
|
||||
|
||||
export const useAgentStore = create<AgentState>((set, get) => ({
|
||||
agents: [],
|
||||
templates: [],
|
||||
loading: false,
|
||||
error: null,
|
||||
|
||||
fetchAgents: async () => {
|
||||
set({ loading: true, error: null })
|
||||
try {
|
||||
const agents = await agentApi.list()
|
||||
set({ agents, loading: false })
|
||||
} catch (e) {
|
||||
set({ error: (e as Error).message, loading: false })
|
||||
}
|
||||
},
|
||||
|
||||
fetchTemplates: async () => {
|
||||
try {
|
||||
const templates = await agentApi.getTemplates()
|
||||
set({ templates })
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch templates:', e)
|
||||
}
|
||||
},
|
||||
|
||||
createAgent: async (data) => {
|
||||
const agent = await agentApi.create(data)
|
||||
set({ agents: [...get().agents, agent] })
|
||||
return agent
|
||||
},
|
||||
|
||||
updateAgent: async (id, data) => {
|
||||
const agent = await agentApi.update(id, data)
|
||||
set({
|
||||
agents: get().agents.map(a =>
|
||||
a.agent_id === id ? agent : a
|
||||
)
|
||||
})
|
||||
return agent
|
||||
},
|
||||
|
||||
deleteAgent: async (id) => {
|
||||
await agentApi.delete(id)
|
||||
set({
|
||||
agents: get().agents.filter(a => a.agent_id !== id)
|
||||
})
|
||||
},
|
||||
|
||||
testAgent: async (id, message) => {
|
||||
return await agentApi.test(id, message)
|
||||
},
|
||||
|
||||
duplicateAgent: async (id, newName) => {
|
||||
const agent = await agentApi.duplicate(id, newName)
|
||||
set({ agents: [...get().agents, agent] })
|
||||
return agent
|
||||
},
|
||||
|
||||
createFromTemplate: async (templateId, providerId) => {
|
||||
const agent = await agentApi.createFromTemplate(templateId, providerId)
|
||||
set({ agents: [...get().agents, agent] })
|
||||
return agent
|
||||
},
|
||||
|
||||
generatePrompt: async (data) => {
|
||||
return await agentApi.generatePrompt(data)
|
||||
},
|
||||
|
||||
getAgentById: (id) => {
|
||||
return get().agents.find(a => a.agent_id === id)
|
||||
}
|
||||
}))
|
||||
183
frontend/src/stores/chatroomStore.ts
Normal file
183
frontend/src/stores/chatroomStore.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* 聊天室状态管理
|
||||
*/
|
||||
import { create } from 'zustand'
|
||||
import type { ChatRoom, Message, WSMessage } from '../types'
|
||||
import { chatroomApi } from '../services/api'
|
||||
import { wsService } from '../services/websocket'
|
||||
|
||||
interface ChatroomState {
|
||||
chatrooms: ChatRoom[]
|
||||
currentRoom: ChatRoom | null
|
||||
messages: Message[]
|
||||
typingAgents: Set<string>
|
||||
loading: boolean
|
||||
error: string | null
|
||||
|
||||
// Actions
|
||||
fetchChatrooms: () => Promise<void>
|
||||
fetchChatroom: (id: string) => Promise<void>
|
||||
createChatroom: (data: Partial<ChatRoom>) => Promise<ChatRoom>
|
||||
updateChatroom: (id: string, data: Partial<ChatRoom>) => Promise<ChatRoom>
|
||||
deleteChatroom: (id: string) => Promise<void>
|
||||
|
||||
// 讨论控制
|
||||
startDiscussion: (roomId: string, objective: string) => Promise<void>
|
||||
pauseDiscussion: (roomId: string) => Promise<void>
|
||||
resumeDiscussion: (roomId: string) => Promise<void>
|
||||
stopDiscussion: (roomId: string) => Promise<void>
|
||||
|
||||
// WebSocket
|
||||
connectWebSocket: (roomId: string) => Promise<void>
|
||||
disconnectWebSocket: () => void
|
||||
|
||||
// 消息
|
||||
fetchMessages: (roomId: string) => Promise<void>
|
||||
addMessage: (message: Message) => void
|
||||
setTyping: (agentId: string, isTyping: boolean) => void
|
||||
|
||||
// 状态更新
|
||||
updateRoomStatus: (status: string, round?: number) => void
|
||||
}
|
||||
|
||||
export const useChatroomStore = create<ChatroomState>((set, get) => ({
|
||||
chatrooms: [],
|
||||
currentRoom: null,
|
||||
messages: [],
|
||||
typingAgents: new Set(),
|
||||
loading: false,
|
||||
error: null,
|
||||
|
||||
fetchChatrooms: async () => {
|
||||
set({ loading: true, error: null })
|
||||
try {
|
||||
const chatrooms = await chatroomApi.list()
|
||||
set({ chatrooms, loading: false })
|
||||
} catch (e) {
|
||||
set({ error: (e as Error).message, loading: false })
|
||||
}
|
||||
},
|
||||
|
||||
fetchChatroom: async (id) => {
|
||||
set({ loading: true, error: null })
|
||||
try {
|
||||
const room = await chatroomApi.get(id)
|
||||
set({ currentRoom: room, loading: false })
|
||||
} catch (e) {
|
||||
set({ error: (e as Error).message, loading: false })
|
||||
}
|
||||
},
|
||||
|
||||
createChatroom: async (data) => {
|
||||
const room = await chatroomApi.create(data)
|
||||
set({ chatrooms: [...get().chatrooms, room] })
|
||||
return room
|
||||
},
|
||||
|
||||
updateChatroom: async (id, data) => {
|
||||
const room = await chatroomApi.update(id, data)
|
||||
set({
|
||||
chatrooms: get().chatrooms.map(r =>
|
||||
r.room_id === id ? room : r
|
||||
),
|
||||
currentRoom: get().currentRoom?.room_id === id ? room : get().currentRoom
|
||||
})
|
||||
return room
|
||||
},
|
||||
|
||||
deleteChatroom: async (id) => {
|
||||
await chatroomApi.delete(id)
|
||||
set({
|
||||
chatrooms: get().chatrooms.filter(r => r.room_id !== id),
|
||||
currentRoom: get().currentRoom?.room_id === id ? null : get().currentRoom
|
||||
})
|
||||
},
|
||||
|
||||
startDiscussion: async (roomId, objective) => {
|
||||
await chatroomApi.startDiscussion(roomId, objective)
|
||||
get().updateRoomStatus('active')
|
||||
},
|
||||
|
||||
pauseDiscussion: async (roomId) => {
|
||||
await chatroomApi.pauseDiscussion(roomId)
|
||||
get().updateRoomStatus('paused')
|
||||
},
|
||||
|
||||
resumeDiscussion: async (roomId) => {
|
||||
await chatroomApi.resumeDiscussion(roomId)
|
||||
get().updateRoomStatus('active')
|
||||
},
|
||||
|
||||
stopDiscussion: async (roomId) => {
|
||||
await chatroomApi.stopDiscussion(roomId)
|
||||
},
|
||||
|
||||
connectWebSocket: async (roomId) => {
|
||||
await wsService.connect(roomId)
|
||||
|
||||
// 监听消息
|
||||
wsService.onMessage((wsMsg: WSMessage) => {
|
||||
switch (wsMsg.type) {
|
||||
case 'message':
|
||||
if (wsMsg.data) {
|
||||
get().addMessage(wsMsg.data as Message)
|
||||
}
|
||||
break
|
||||
case 'typing':
|
||||
if (wsMsg.agent_id) {
|
||||
get().setTyping(wsMsg.agent_id, wsMsg.is_typing ?? false)
|
||||
}
|
||||
break
|
||||
case 'status':
|
||||
get().updateRoomStatus(wsMsg.status ?? '')
|
||||
break
|
||||
case 'round':
|
||||
get().updateRoomStatus('active', wsMsg.round)
|
||||
break
|
||||
case 'error':
|
||||
set({ error: wsMsg.error ?? '未知错误' })
|
||||
break
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
disconnectWebSocket: () => {
|
||||
wsService.disconnect()
|
||||
},
|
||||
|
||||
fetchMessages: async (roomId) => {
|
||||
try {
|
||||
const messages = await chatroomApi.getMessages(roomId)
|
||||
set({ messages: messages.reverse() })
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch messages:', e)
|
||||
}
|
||||
},
|
||||
|
||||
addMessage: (message) => {
|
||||
set({ messages: [...get().messages, message] })
|
||||
},
|
||||
|
||||
setTyping: (agentId, isTyping) => {
|
||||
const typingAgents = new Set(get().typingAgents)
|
||||
if (isTyping) {
|
||||
typingAgents.add(agentId)
|
||||
} else {
|
||||
typingAgents.delete(agentId)
|
||||
}
|
||||
set({ typingAgents })
|
||||
},
|
||||
|
||||
updateRoomStatus: (status, round) => {
|
||||
const currentRoom = get().currentRoom
|
||||
if (currentRoom) {
|
||||
set({
|
||||
currentRoom: {
|
||||
...currentRoom,
|
||||
status: status as ChatRoom['status'],
|
||||
current_round: round ?? currentRoom.current_round
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}))
|
||||
62
frontend/src/stores/providerStore.ts
Normal file
62
frontend/src/stores/providerStore.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* AI接口状态管理
|
||||
*/
|
||||
import { create } from 'zustand'
|
||||
import type { AIProvider } from '../types'
|
||||
import { providerApi } from '../services/api'
|
||||
|
||||
interface ProviderState {
|
||||
providers: AIProvider[]
|
||||
loading: boolean
|
||||
error: string | null
|
||||
|
||||
// Actions
|
||||
fetchProviders: () => Promise<void>
|
||||
createProvider: (data: Partial<AIProvider>) => Promise<AIProvider>
|
||||
updateProvider: (id: string, data: Partial<AIProvider>) => Promise<AIProvider>
|
||||
deleteProvider: (id: string) => Promise<void>
|
||||
testProvider: (id: string) => Promise<{ success: boolean; message: string }>
|
||||
}
|
||||
|
||||
export const useProviderStore = create<ProviderState>((set, get) => ({
|
||||
providers: [],
|
||||
loading: false,
|
||||
error: null,
|
||||
|
||||
fetchProviders: async () => {
|
||||
set({ loading: true, error: null })
|
||||
try {
|
||||
const providers = await providerApi.list()
|
||||
set({ providers, loading: false })
|
||||
} catch (e) {
|
||||
set({ error: (e as Error).message, loading: false })
|
||||
}
|
||||
},
|
||||
|
||||
createProvider: async (data) => {
|
||||
const provider = await providerApi.create(data)
|
||||
set({ providers: [...get().providers, provider] })
|
||||
return provider
|
||||
},
|
||||
|
||||
updateProvider: async (id, data) => {
|
||||
const provider = await providerApi.update(id, data)
|
||||
set({
|
||||
providers: get().providers.map(p =>
|
||||
p.provider_id === id ? provider : p
|
||||
)
|
||||
})
|
||||
return provider
|
||||
},
|
||||
|
||||
deleteProvider: async (id) => {
|
||||
await providerApi.delete(id)
|
||||
set({
|
||||
providers: get().providers.filter(p => p.provider_id !== id)
|
||||
})
|
||||
},
|
||||
|
||||
testProvider: async (id) => {
|
||||
return await providerApi.test(id)
|
||||
}
|
||||
}))
|
||||
150
frontend/src/types/index.ts
Normal file
150
frontend/src/types/index.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* TypeScript类型定义
|
||||
*/
|
||||
|
||||
// AI接口提供商
|
||||
export interface ProxyConfig {
|
||||
http_proxy?: string
|
||||
https_proxy?: string
|
||||
no_proxy?: string[]
|
||||
}
|
||||
|
||||
export interface RateLimit {
|
||||
requests_per_minute: number
|
||||
tokens_per_minute: number
|
||||
}
|
||||
|
||||
export interface AIProvider {
|
||||
provider_id: string
|
||||
provider_type: string
|
||||
name: string
|
||||
api_key_masked: string
|
||||
base_url: string
|
||||
model: string
|
||||
use_proxy: boolean
|
||||
proxy_config: ProxyConfig
|
||||
rate_limit: RateLimit
|
||||
timeout: number
|
||||
extra_params: Record<string, unknown>
|
||||
enabled: boolean
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
// Agent
|
||||
export interface AgentCapabilities {
|
||||
memory_enabled: boolean
|
||||
mcp_tools: string[]
|
||||
skills: string[]
|
||||
multimodal: boolean
|
||||
}
|
||||
|
||||
export interface AgentBehavior {
|
||||
speak_threshold: number
|
||||
max_speak_per_round: number
|
||||
speak_style: string
|
||||
}
|
||||
|
||||
export interface Agent {
|
||||
agent_id: string
|
||||
name: string
|
||||
role: string
|
||||
system_prompt: string
|
||||
provider_id: string
|
||||
temperature: number
|
||||
max_tokens: number
|
||||
capabilities: AgentCapabilities
|
||||
behavior: AgentBehavior
|
||||
avatar?: string
|
||||
color: string
|
||||
enabled: boolean
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface AgentTemplate {
|
||||
template_id: string
|
||||
name: string
|
||||
role: string
|
||||
system_prompt: string
|
||||
color: string
|
||||
}
|
||||
|
||||
// 聊天室
|
||||
export interface ChatRoomConfig {
|
||||
max_rounds: number
|
||||
message_history_size: number
|
||||
consensus_threshold: number
|
||||
round_interval: number
|
||||
allow_user_interrupt: boolean
|
||||
}
|
||||
|
||||
export interface ChatRoom {
|
||||
room_id: string
|
||||
name: string
|
||||
description: string
|
||||
objective: string
|
||||
agents: string[]
|
||||
moderator_agent_id?: string
|
||||
config: ChatRoomConfig
|
||||
status: 'idle' | 'active' | 'paused' | 'completed' | 'error'
|
||||
current_round: number
|
||||
current_discussion_id?: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
completed_at?: string
|
||||
}
|
||||
|
||||
// 消息
|
||||
export interface Message {
|
||||
message_id: string
|
||||
room_id: string
|
||||
discussion_id: string
|
||||
agent_id?: string
|
||||
content: string
|
||||
message_type: 'text' | 'image' | 'file' | 'system' | 'action'
|
||||
round: number
|
||||
created_at: string
|
||||
}
|
||||
|
||||
// 讨论结果
|
||||
export interface DiscussionResult {
|
||||
discussion_id: string
|
||||
room_id: string
|
||||
objective: string
|
||||
consensus_reached: boolean
|
||||
confidence: number
|
||||
summary: string
|
||||
action_items: string[]
|
||||
unresolved_issues: string[]
|
||||
key_decisions: string[]
|
||||
total_rounds: number
|
||||
total_messages: number
|
||||
participating_agents: string[]
|
||||
agent_contributions: Record<string, number>
|
||||
status: string
|
||||
end_reason: string
|
||||
created_at: string
|
||||
completed_at?: string
|
||||
}
|
||||
|
||||
// WebSocket消息类型
|
||||
export interface WSMessage {
|
||||
type: 'message' | 'status' | 'typing' | 'round' | 'error'
|
||||
data?: unknown
|
||||
status?: string
|
||||
agent_id?: string
|
||||
is_typing?: boolean
|
||||
round?: number
|
||||
total_rounds?: number
|
||||
error?: string
|
||||
timestamp?: string
|
||||
}
|
||||
|
||||
// API响应
|
||||
export interface TestResponse {
|
||||
success: boolean
|
||||
message: string
|
||||
model?: string
|
||||
latency_ms?: number
|
||||
}
|
||||
25
frontend/tsconfig.json
Normal file
25
frontend/tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
10
frontend/tsconfig.node.json
Normal file
10
frontend/tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
19
frontend/vite.config.ts
Normal file
19
frontend/vite.config.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
port: 3000,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8000',
|
||||
changeOrigin: true
|
||||
},
|
||||
'/ws': {
|
||||
target: 'ws://localhost:8000',
|
||||
ws: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user