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:
Claude Code
2026-02-03 19:20:02 +08:00
commit edbddf855d
76 changed files with 14681 additions and 0 deletions

31
frontend/Dockerfile Normal file
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

28
frontend/package.json Normal file
View 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
View 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

View 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

View 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

View 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

View 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

View 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
View 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
View 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>
)

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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)
}
}))

View 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
}
})
}
}
}))

View 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
View 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
View 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" }]
}

View 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
View 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
}
}
}
})