feat(frontend): Day 6 - frontend basic architecture completed

This commit is contained in:
AI Clinical Dev Team
2025-10-10 17:22:37 +08:00
parent 0db54b2d31
commit f7a500bc79
20 changed files with 6718 additions and 0 deletions

19
frontend/src/App.tsx Normal file
View File

@@ -0,0 +1,19 @@
import { Routes, Route, Navigate } from 'react-router-dom'
import MainLayout from './layouts/MainLayout'
import HomePage from './pages/HomePage'
import AgentPage from './pages/AgentPage'
function App() {
return (
<Routes>
<Route path="/" element={<MainLayout />}>
<Route index element={<HomePage />} />
<Route path="agent/:agentId" element={<AgentPage />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Route>
</Routes>
)
}
export default App

23
frontend/src/api/index.ts Normal file
View File

@@ -0,0 +1,23 @@
import request from './request'
// 健康检查
export const healthCheck = () => {
return request.get('/health')
}
// API信息
export const getApiInfo = () => {
return request.get('/')
}
// TODO: Day 9+ 添加更多API
// - 项目管理API
// - 对话API
// - 知识库API
// - 用户API
export default {
healthCheck,
getApiInfo,
}

View File

@@ -0,0 +1,59 @@
import axios, { AxiosInstance, AxiosResponse } from 'axios'
// 创建axios实例
const request: AxiosInstance = axios.create({
baseURL: '/api/v1',
timeout: 30000,
headers: {
'Content-Type': 'application/json',
},
})
// 请求拦截器
request.interceptors.request.use(
(config) => {
// 添加token
const token = localStorage.getItem('token')
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)
// 响应拦截器
request.interceptors.response.use(
(response: AxiosResponse) => {
return response.data
},
(error) => {
if (error.response) {
const { status } = error.response
switch (status) {
case 401:
// 未授权清除token并跳转登录
localStorage.removeItem('token')
window.location.href = '/login'
break
case 403:
console.error('没有权限访问该资源')
break
case 404:
console.error('请求的资源不存在')
break
case 500:
console.error('服务器错误')
break
default:
console.error(`请求错误: ${status}`)
}
}
return Promise.reject(error)
}
)
export default request

23
frontend/src/index.css Normal file
View File

@@ -0,0 +1,23 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
#root {
width: 100vw;
height: 100vh;
overflow: hidden;
}

View File

@@ -0,0 +1,168 @@
import { useState } from 'react'
import { Outlet, useNavigate, useLocation } from 'react-router-dom'
import { Layout, Menu, Avatar, Dropdown, Button } from 'antd'
import {
MenuFoldOutlined,
MenuUnfoldOutlined,
HomeOutlined,
ExperimentOutlined,
UserOutlined,
SettingOutlined,
LogoutOutlined,
} from '@ant-design/icons'
import type { MenuProps } from 'antd'
const { Header, Sider, Content } = Layout
// 12个智能体配置
const AGENTS = [
{ id: 'topic-evaluation', name: '选题评价', icon: '📊' },
{ id: 'picos-construction', name: 'PICOS构建', icon: '🔍' },
{ id: 'literature-search', name: '文献检索', icon: '📚' },
{ id: 'literature-screening', name: '文献筛选', icon: '🎯' },
{ id: 'data-extraction', name: '数据提取', icon: '📋' },
{ id: 'bias-assessment', name: '偏倚评价', icon: '⚖️' },
{ id: 'meta-analysis', name: 'Meta分析', icon: '📈' },
{ id: 'forest-plot', name: '森林图绘制', icon: '🌲' },
{ id: 'results-interpretation', name: '结果解读', icon: '💡' },
{ id: 'protocol-writing', name: '方案撰写', icon: '📝' },
{ id: 'article-writing', name: '文章撰写', icon: '✍️' },
{ id: 'submission-assistance', name: '投稿辅助', icon: '📬' },
]
const MainLayout = () => {
const [collapsed, setCollapsed] = useState(false)
const navigate = useNavigate()
const location = useLocation()
// 菜单项配置
const menuItems: MenuProps['items'] = [
{
key: '/',
icon: <HomeOutlined />,
label: '首页',
},
{
key: 'agents',
icon: <ExperimentOutlined />,
label: '智能体',
children: AGENTS.map((agent) => ({
key: `/agent/${agent.id}`,
label: `${agent.icon} ${agent.name}`,
})),
},
]
// 用户下拉菜单
const userMenuItems: MenuProps['items'] = [
{
key: 'profile',
icon: <UserOutlined />,
label: '个人中心',
},
{
key: 'settings',
icon: <SettingOutlined />,
label: '设置',
},
{
type: 'divider',
},
{
key: 'logout',
icon: <LogoutOutlined />,
label: '退出登录',
danger: true,
},
]
const handleMenuClick = ({ key }: { key: string }) => {
navigate(key)
}
const handleUserMenuClick = ({ key }: { key: string }) => {
if (key === 'logout') {
console.log('退出登录')
// TODO: 实现登出逻辑
}
}
return (
<Layout style={{ height: '100vh' }}>
{/* 侧边栏 */}
<Sider trigger={null} collapsible collapsed={collapsed} theme="light">
<div
style={{
height: 64,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: collapsed ? 18 : 20,
fontWeight: 'bold',
color: '#1890ff',
borderBottom: '1px solid #f0f0f0',
}}
>
{collapsed ? '🏥' : '🏥 AI临床研究'}
</div>
<Menu
mode="inline"
selectedKeys={[location.pathname]}
defaultOpenKeys={['agents']}
items={menuItems}
onClick={handleMenuClick}
style={{ borderRight: 0 }}
/>
</Sider>
{/* 主内容区 */}
<Layout>
{/* 头部 */}
<Header
style={{
padding: '0 24px',
background: '#fff',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
borderBottom: '1px solid #f0f0f0',
}}
>
<Button
type="text"
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
onClick={() => setCollapsed(!collapsed)}
style={{
fontSize: '16px',
width: 64,
height: 64,
}}
/>
<Dropdown menu={{ items: userMenuItems, onClick: handleUserMenuClick }} placement="bottomRight">
<div style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 8 }}>
<Avatar icon={<UserOutlined />} />
<span></span>
</div>
</Dropdown>
</Header>
{/* 内容区 */}
<Content
style={{
margin: '24px',
padding: 24,
background: '#fff',
borderRadius: 8,
overflow: 'auto',
}}
>
<Outlet />
</Content>
</Layout>
</Layout>
)
}
export default MainLayout

18
frontend/src/main.tsx Normal file
View File

@@ -0,0 +1,18 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
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}>
<BrowserRouter>
<App />
</BrowserRouter>
</ConfigProvider>
</React.StrictMode>,
)

View File

@@ -0,0 +1,92 @@
import { useParams } from 'react-router-dom'
import { Card, Typography, Tag, Button, Space } from 'antd'
import { ArrowLeftOutlined } from '@ant-design/icons'
import { useNavigate } from 'react-router-dom'
const { Title, Paragraph } = Typography
const AGENTS_MAP: Record<string, { name: string; icon: string; desc: string }> = {
'topic-evaluation': { name: '选题评价', icon: '📊', desc: '评估研究选题的价值和可行性' },
'picos-construction': { name: 'PICOS构建', icon: '🔍', desc: '构建研究问题的PICOS框架' },
'literature-search': { name: '文献检索', icon: '📚', desc: '系统化检索相关医学文献' },
'literature-screening': { name: '文献筛选', icon: '🎯', desc: '根据纳入排除标准筛选文献' },
'data-extraction': { name: '数据提取', icon: '📋', desc: '从文献中提取关键数据' },
'bias-assessment': { name: '偏倚评价', icon: '⚖️', desc: '评估研究偏倚风险' },
'meta-analysis': { name: 'Meta分析', icon: '📈', desc: '进行统计学meta分析' },
'forest-plot': { name: '森林图绘制', icon: '🌲', desc: '绘制meta分析森林图' },
'results-interpretation': { name: '结果解读', icon: '💡', desc: '解读分析结果的临床意义' },
'protocol-writing': { name: '方案撰写', icon: '📝', desc: '撰写研究方案' },
'article-writing': { name: '文章撰写', icon: '✍️', desc: '撰写学术论文' },
'submission-assistance': { name: '投稿辅助', icon: '📬', desc: '辅助期刊投稿' },
}
const AgentPage = () => {
const { agentId } = useParams<{ agentId: string }>()
const navigate = useNavigate()
const agent = agentId ? AGENTS_MAP[agentId] : null
if (!agent) {
return (
<Card>
<Paragraph></Paragraph>
<Button onClick={() => navigate('/')}></Button>
</Card>
)
}
return (
<div>
{/* 头部 */}
<Space direction="vertical" size="large" style={{ width: '100%' }}>
<Button icon={<ArrowLeftOutlined />} onClick={() => navigate('/')}>
</Button>
<Card>
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<div>
<span style={{ fontSize: 48, marginRight: 16 }}>{agent.icon}</span>
<Title level={2} style={{ display: 'inline', margin: 0 }}>
{agent.name}
</Title>
<Tag color="blue" style={{ marginLeft: 16 }}>
AI智能体
</Tag>
</div>
<Paragraph type="secondary" style={{ fontSize: 16 }}>
{agent.desc}
</Paragraph>
</Space>
</Card>
{/* 功能区域 - 占位符 */}
<Card title="智能对话区域" extra={<Tag color="orange"></Tag>}>
<div
style={{
height: 400,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: '#f5f5f5',
borderRadius: 8,
}}
>
<Space direction="vertical" align="center">
<div style={{ fontSize: 64 }}>💬</div>
<Title level={4} type="secondary">
...
</Title>
<Paragraph type="secondary">
Day 9-10
</Paragraph>
</Space>
</div>
</Card>
</Space>
</div>
)
}
export default AgentPage

View File

@@ -0,0 +1,72 @@
import { Card, Row, Col, Typography, Button } from 'antd'
import { useNavigate } from 'react-router-dom'
import { ExperimentOutlined, RocketOutlined } from '@ant-design/icons'
const { Title, Paragraph } = Typography
const AGENTS = [
{ id: 'topic-evaluation', name: '选题评价', icon: '📊', desc: '评估研究选题的价值和可行性' },
{ id: 'picos-construction', name: 'PICOS构建', icon: '🔍', desc: '构建研究问题的PICOS框架' },
{ id: 'literature-search', name: '文献检索', icon: '📚', desc: '系统化检索相关医学文献' },
{ id: 'literature-screening', name: '文献筛选', icon: '🎯', desc: '根据纳入排除标准筛选文献' },
{ id: 'data-extraction', name: '数据提取', icon: '📋', desc: '从文献中提取关键数据' },
{ id: 'bias-assessment', name: '偏倚评价', icon: '⚖️', desc: '评估研究偏倚风险' },
{ id: 'meta-analysis', name: 'Meta分析', icon: '📈', desc: '进行统计学meta分析' },
{ id: 'forest-plot', name: '森林图绘制', icon: '🌲', desc: '绘制meta分析森林图' },
{ id: 'results-interpretation', name: '结果解读', icon: '💡', desc: '解读分析结果的临床意义' },
{ id: 'protocol-writing', name: '方案撰写', icon: '📝', desc: '撰写研究方案' },
{ id: 'article-writing', name: '文章撰写', icon: '✍️', desc: '撰写学术论文' },
{ id: 'submission-assistance', name: '投稿辅助', icon: '📬', desc: '辅助期刊投稿' },
]
const HomePage = () => {
const navigate = useNavigate()
return (
<div>
{/* 欢迎区域 */}
<Card style={{ marginBottom: 24, background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' }}>
<div style={{ color: 'white', padding: '20px 0' }}>
<Title level={2} style={{ color: 'white', marginBottom: 16 }}>
<RocketOutlined /> 使 AI临床研究平台
</Title>
<Paragraph style={{ color: 'white', fontSize: 16, marginBottom: 0 }}>
12
</Paragraph>
</div>
</Card>
{/* 智能体卡片 */}
<Title level={3} style={{ marginBottom: 16 }}>
<ExperimentOutlined />
</Title>
<Row gutter={[16, 16]}>
{AGENTS.map((agent) => (
<Col xs={24} sm={12} md={8} lg={6} key={agent.id}>
<Card
hoverable
style={{ height: '100%' }}
onClick={() => navigate(`/agent/${agent.id}`)}
>
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: 48, marginBottom: 12 }}>{agent.icon}</div>
<Title level={4} style={{ marginBottom: 8 }}>
{agent.name}
</Title>
<Paragraph type="secondary" style={{ marginBottom: 16, minHeight: 44 }}>
{agent.desc}
</Paragraph>
<Button type="primary" block>
使
</Button>
</div>
</Card>
</Col>
))}
</Row>
</div>
)
}
export default HomePage

View File

@@ -0,0 +1,89 @@
// 用户类型
export interface User {
id: string
email: string
name?: string
avatarUrl?: string
role: 'user' | 'admin'
}
// 项目类型
export interface Project {
id: string
userId: string
name: string
description: string
conversationCount: number
createdAt: string
updatedAt: string
}
// 会话类型
export interface Conversation {
id: string
userId: string
projectId?: string
agentId: string
title: string
modelName: string
messageCount: number
totalTokens: number
createdAt: string
updatedAt: string
}
// 消息类型
export interface Message {
id: string
conversationId: string
role: 'user' | 'assistant'
content: string
metadata?: {
kbReferences?: string[]
citations?: any[]
modelParams?: any
}
tokens?: number
isPinned: boolean
createdAt: string
}
// 知识库类型
export interface KnowledgeBase {
id: string
userId: string
name: string
description?: string
difyDatasetId: string
fileCount: number
totalSizeBytes: number
createdAt: string
updatedAt: string
}
// 文档类型
export interface Document {
id: string
kbId: string
userId: string
filename: string
fileType: string
fileSizeBytes: number
fileUrl: string
difyDocumentId: string
status: 'uploading' | 'processing' | 'completed' | 'failed'
progress: number
errorMessage?: string
segmentsCount?: number
tokensCount?: number
uploadedAt: string
processedAt?: string
}
// API响应类型
export interface ApiResponse<T = any> {
success: boolean
data: T
message?: string
}