在上一篇文章中,我们完成了RAG系统的核心后端开发,集成了Chroma向量数据库,实现了文档向量化、存储和检索功能,并开发了OpenClaw RAG检索技能。这篇文章,我们将开发完整的前端用户界面,包括文档上传、文档管理和智能问答功能,让普通用户也能方便地使用我们的知识库系统。
摘要
- 优化React前端项目架构,实现API封装和状态管理
- 开发文档上传页面,支持拖拽上传、批量上传和进度显示
- 实现文档管理页面,支持文档列表展示、搜索和删除
- 开发智能问答界面,实现流式输出(打字机效果)和引用来源展示
- 完成前后端联调,实现完整的"上传→解析→向量化→问答"用户流程
1. 前端项目架构优化
在开始开发具体功能之前,我们先优化一下前端项目的架构,使其更易于维护和扩展。
1.1 完善项目目录结构
我们将按照功能模块来组织代码,创建以下目录:
cd frontend
mkdir -p src/api src/components src/pages src/store src/utils
最终的前端目录结构将是:
frontend/
├── src/
│ ├── api/ # API 接口封装
│ ├── components/ # 通用组件
│ ├── pages/ # 页面组件
│ ├── store/ # 状态管理
│ ├── utils/ # 工具函数
│ ├── App.tsx
│ └── main.tsx
├── index.html
├── package.json
└── tsconfig.json
1.2 API 接口封装
创建 src/api/index.ts,封装所有后端 API 接口:
import axios from 'axios';
const API_BASE_URL = 'http://localhost:8000/api';
const OPENCLAW_BASE_URL = 'http://localhost:18789/api';
// 创建 axios 实例
const api = axios.create({
baseURL: API_BASE_URL,
timeout: 30000,
headers: {
'Content-Type': 'application/json',
},
});
const openclawApi = axios.create({
baseURL: OPENCLAW_BASE_URL,
timeout: 60000,
});
// 文档相关接口
export const documentApi = {
// 上传文档
upload: (file: File, onProgress?: (progress: number) => void) => {
const formData = new FormData();
formData.append('file', file);
return api.post('/documents/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
onUploadProgress: (progressEvent) => {
if (onProgress && progressEvent.total) {
const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
onProgress(progress);
}
},
});
},
// 获取文档列表
getList: () => api.get('/documents'),
// 删除文档
delete: (documentId: string) => api.delete(`/documents/${documentId}`),
};
// 向量相关接口
export const vectorApi = {
// 搜索文档
search: (query: string, topK: number = 5) =>
api.post('/vector/search', { query, top_k: topK }),
};
// OpenClaw 相关接口
export const openclawApiService = {
// 创建会话
createSession: (title: string = '新会话') =>
openclawApi.post('/sessions', { title }),
// 发送消息
sendMessage: (sessionId: string, content: string, stream: boolean = false) =>
openclawApi.post(`/sessions/${sessionId}/messages`, {
role: 'user',
content,
stream,
}),
// 获取会话历史
getSessionHistory: (sessionId: string) =>
openclawApi.get(`/sessions/${sessionId}/messages`),
};
export default api;
1.3 状态管理配置
我们使用 Zustand 进行状态管理,创建 src/store/index.ts:
import { create } from 'zustand';
interface Message {
id: string;
role: 'user' | 'assistant';
content: string;
sources?: any[];
timestamp: Date;
}
interface AppState {
// 会话状态
sessionId: string | null;
messages: Message[];
isLoading: boolean;
// 文档状态
documents: any[];
isUploading: boolean;
uploadProgress: number;
// 方法
setSessionId: (id: string | null) => void;
addMessage: (message: Message) => void;
setMessages: (messages: Message[]) => void;
setIsLoading: (loading: boolean) => void;
setDocuments: (documents: any[]) => void;
setIsUploading: (uploading: boolean) => void;
setUploadProgress: (progress: number) => void;
clearMessages: () => void;
}
export const useAppStore = create<AppState>((set) => ({
sessionId: null,
messages: [],
isLoading: false,
documents: [],
isUploading: false,
uploadProgress: 0,
setSessionId: (id) => set({ sessionId: id }),
addMessage: (message) => set((state) => ({
messages: [...state.messages, message]
})),
setMessages: (messages) => set({ messages }),
setIsLoading: (loading) => set({ isLoading: loading }),
setDocuments: (documents) => set({ documents }),
setIsUploading: (uploading) => set({ isUploading: uploading }),
setUploadProgress: (progress) => set({ uploadProgress: progress }),
clearMessages: () => set({ messages: [] }),
}));
1.4 路由配置
安装 React Router:
pnpm add react-router-dom
创建 src/App.tsx,配置路由:
import { BrowserRouter as Router, Routes, Route, Layout } from 'react-router-dom';
import { Layout, Menu } from 'antd';
import { FileOutlined, MessageOutlined, SettingOutlined } from '@ant-design/icons';
import DocumentPage from './pages/DocumentPage';
import ChatPage from './pages/ChatPage';
import SettingsPage from './pages/SettingsPage';
import './App.css';
const { Header, Sider, Content } = Layout;
function App() {
return (
<Router>
<Layout style={{ minHeight: '100vh' }}>
<Header style={{ background: '#fff', padding: '0 20px', boxShadow: '0 2px 8px rgba(0,0,0,0.1)' }}>
<h1 style={{ margin: 0, fontSize: '20px', fontWeight: 'bold' }}>企业级智能知识库</h1>
</Header>
<Layout>
<Sider width={200} style={{ background: '#fff' }}>
<Menu
mode="inline"
defaultSelectedKeys={['documents']}
style={{ height: '100%', borderRight: 0 }}
items={[
{
key: 'documents',
icon: <FileOutlined />,
label: '文档管理',
path: '/',
},
{
key: 'chat',
icon: <MessageOutlined />,
label: '智能问答',
path: '/chat',
},
{
key: 'settings',
icon: <SettingOutlined />,
label: '系统设置',
path: '/settings',
},
]}
/>
</Sider>
<Content style={{ padding: '24px', background: '#f5f5f5' }}>
<Routes>
<Route path="/" element={<DocumentPage />} />
<Route path="/chat" element={<ChatPage />} />
<Route path="/settings" element={<SettingsPage />} />
</Routes>
</Content>
</Layout>
</Layout>
</Router>
);
}
export default App;
2. 后端文档上传接口补充
在开发前端上传功能之前,我们需要先在 FastAPI 后端补充文档上传接口。
2.1 安装依赖
cd ../backend
source venv/bin/activate # Windows 使用 venv\Scripts\activate
pip install python-multipart aiofiles
2.2 创建文件存储目录
mkdir uploads
2.3 实现文档上传接口
创建 services/document_service.py:
import os
import uuid
from fastapi import UploadFile
from datetime import datetime
from services.vector_service import vector_service
from utils.document_parser import parse_document
class DocumentService:
def __init__(self):
self.upload_dir = "./uploads"
os.makedirs(self.upload_dir, exist_ok=True)
# 支持的文件格式
self.supported_formats = {
'.txt': 'text/plain',
'.md': 'text/markdown',
'.pdf': 'application/pdf',
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
}
async def upload_document(self, file: UploadFile) -> dict:
"""
上传并处理文档
"""
# 检查文件格式
file_ext = os.path.splitext(file.filename)[1].lower()
if file_ext not in self.supported_formats:
raise Exception(f"不支持的文件格式: {file_ext}")
# 生成唯一文件名
document_id = str(uuid.uuid4())
file_path = os.path.join(self.upload_dir, f"{document_id}{file_ext}")
# 保存文件
with open(file_path, "wb") as buffer:
content = await file.read()
buffer.write(content)
try:
# 解析文档内容
content = parse_document(file_path, file_ext[1:])
# 添加到向量数据库
metadata = {
"fileName": file.filename,
"fileSize": os.path.getsize(file_path),
"format": file_ext[1:],
"uploadTime": datetime.now().isoformat()
}
vector_result = vector_service.add_document(
document_id=document_id,
content=content,
metadata=metadata
)
return {
"document_id": document_id,
"file_name": file.filename,
"file_size": os.path.getsize(file_path),
"format": file_ext[1:],
"chunk_count": vector_result["chunk_count"],
"upload_time": datetime.now().isoformat(),
"success": True
}
except Exception as e:
# 如果处理失败,删除已保存的文件
if os.path.exists(file_path):
os.remove(file_path)
raise e
def get_all_documents(self) -> list[dict]:
"""
获取所有文档列表
"""
# 这里简化处理,实际项目中应该从数据库获取
# 我们可以从向量数据库中获取所有唯一的 document_id
all_ids = vector_service.collection.get()["ids"]
document_ids = set()
for id in all_ids:
doc_id = id.split("_chunk_")[0]
document_ids.add(doc_id)
documents = []
for doc_id in document_ids:
# 获取该文档的第一个块的元数据
result = vector_service.collection.get(
where={"document_id": doc_id},
limit=1
)
if result["metadatas"] and len(result["metadatas"]) > 0:
metadata = result["metadatas"][0]
documents.append({
"document_id": doc_id,
"file_name": metadata.get("fileName", "未知"),
"file_size": metadata.get("fileSize", 0),
"format": metadata.get("format", "未知"),
"upload_time": metadata.get("uploadTime", "未知"),
"chunk_count": metadata.get("chunk_count", 0)
})
return documents
def delete_document(self, document_id: str) -> dict:
"""
删除文档
"""
# 从向量数据库删除
vector_result = vector_service.delete_document(document_id)
# 删除本地文件
file_path = os.path.join(self.upload_dir, f"{document_id}.*")
# 这里简化处理,实际应该遍历目录删除
# ...
return vector_result
# 创建全局实例
document_service = DocumentService()
创建 utils/document_parser.py:
import os
from PyPDF2 import PdfReader
from docx import Document
def parse_document(file_path: str, format: str) -> str:
"""
解析不同格式的文档,提取纯文本内容
"""
if format == 'txt' or format == 'md':
with open(file_path, 'r', encoding='utf-8') as f:
return f.read()
elif format == 'pdf':
reader = PdfReader(file_path)
text = ""
for page in reader.pages:
text += page.extract_text() + "\n"
return text
elif format == 'docx':
doc = Document(file_path)
text = ""
for para in doc.paragraphs:
text += para.text + "\n"
return text
else:
raise Exception(f"不支持的文件格式: {format}")
2.4 添加 API 接口
修改 main.py,添加文档相关接口:
from fastapi import FastAPI, HTTPException, UploadFile, File
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from services.vector_service import vector_service
from services.document_service import document_service
# ... 保留之前的代码 ...
@app.post("/api/documents/upload")
async def upload_document(file: UploadFile = File(...)):
try:
result = await document_service.upload_document(file)
return result
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/documents")
async def get_documents():
try:
documents = document_service.get_all_documents()
return {"documents": documents, "count": len(documents)}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.delete("/api/documents/{document_id}")
async def delete_document(document_id: str):
try:
result = document_service.delete_document(document_id)
return result
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
3. 前端文档上传与管理页面开发
现在我们来开发前端的文档上传和管理页面。
3.1 文档上传页面
创建 src/pages/DocumentPage.tsx:
import { useState, useEffect } from 'react';
import { Card, Upload, Button, Table, message, Progress, Space, Popconfirm } from 'antd';
import { UploadOutlined, DeleteOutlined, ReloadOutlined } from '@ant-design/icons';
import { documentApi } from '../api';
import { useAppStore } from '../store';
const { Dragger } = Upload;
const DocumentPage = () => {
const { documents, setDocuments, isUploading, setIsUploading, uploadProgress, setUploadProgress } = useAppStore();
const [fileList, setFileList] = useState<any[]>([]);
// 加载文档列表
const loadDocuments = async () => {
try {
const response = await documentApi.getList();
setDocuments(response.data.documents);
} catch (error: any) {
message.error(`加载文档列表失败: ${error.message}`);
}
};
useEffect(() => {
loadDocuments();
}, []);
// 处理文件上传
const handleUpload = async (options: any) => {
const { file, onSuccess, onError, onProgress } = options;
setIsUploading(true);
setUploadProgress(0);
try {
const response = await documentApi.upload(file, (progress) => {
setUploadProgress(progress);
onProgress({ percent: progress });
});
message.success(`${file.name} 上传成功`);
onSuccess(response.data);
loadDocuments(); // 重新加载文档列表
} catch (error: any) {
message.error(`${file.name} 上传失败: ${error.message}`);
onError(error);
} finally {
setIsUploading(false);
setUploadProgress(0);
}
};
// 处理删除文档
const handleDelete = async (documentId: string) => {
try {
await documentApi.delete(documentId);
message.success('文档删除成功');
loadDocuments(); // 重新加载文档列表
} catch (error: any) {
message.error(`删除失败: ${error.message}`);
}
};
// 表格列定义
const columns = [
{
title: '文件名',
dataIndex: 'file_name',
key: 'file_name',
},
{
title: '格式',
dataIndex: 'format',
key: 'format',
width: 80,
},
{
title: '大小',
dataIndex: 'file_size',
key: 'file_size',
width: 100,
render: (size: number) => `${(size / 1024).toFixed(2)} KB`,
},
{
title: '分块数',
dataIndex: 'chunk_count',
key: 'chunk_count',
width: 80,
},
{
title: '上传时间',
dataIndex: 'upload_time',
key: 'upload_time',
width: 180,
render: (time: string) => new Date(time).toLocaleString(),
},
{
title: '操作',
key: 'action',
width: 80,
render: (_: any, record: any) => (
<Popconfirm
title="确定要删除这个文档吗?"
onConfirm={() => handleDelete(record.document_id)}
okText="确定"
cancelText="取消"
>
<Button type="text" danger icon={<DeleteOutlined />} />
</Popconfirm>
),
},
];
return (
<div>
<Card title="文档上传" style={{ marginBottom: 24 }}>
<Dragger
name="file"
multiple={true}
customRequest={handleUpload}
fileList={fileList}
onChange={({ fileList }) => setFileList(fileList)}
accept=".txt,.md,.pdf,.docx"
disabled={isUploading}
>
<p className="ant-upload-drag-icon">
<UploadOutlined />
</p>
<p className="ant-upload-text">点击或拖拽文件到此处上传</p>
<p className="ant-upload-hint">
支持 TXT、Markdown、PDF、Word 格式,单个文件不超过 10MB
</p>
</Dragger>
{isUploading && (
<div style={{ marginTop: 16 }}>
<Progress percent={uploadProgress} status="active" />
</div>
)}
</Card>
<Card
title="文档管理"
extra={
<Button
icon={<ReloadOutlined />}
onClick={loadDocuments}
>
刷新
</Button>
}
>
<Table
columns={columns}
dataSource={documents}
rowKey="document_id"
pagination={{ pageSize: 10 }}
/>
</Card>
</div>
);
};
export default DocumentPage;
3.2 简单的设置页面
创建 src/pages/SettingsPage.tsx:
import { Card, Form, Input, Select, Button, message } from 'antd';
const SettingsPage = () => {
const [form] = Form.useForm();
const handleSubmit = (values: any) => {
console.log('设置值:', values);
message.success('设置保存成功');
};
return (
<Card title="系统设置">
<Form
form={form}
layout="vertical"
onFinish={handleSubmit}
initialValues={{
model: 'claude-3-sonnet-20240229',
topK: 5,
temperature: 0.7,
}}
>
<Form.Item
name="model"
label="大语言模型"
rules={[{ required: true, message: '请选择大语言模型' }]}
>
<Select>
<Select.Option value="claude-3-sonnet-20240229">Claude 3 Sonnet</Select.Option>
<Select.Option value="claude-3-opus-20240229">Claude 3 Opus</Select.Option>
<Select.Option value="gpt-4o">GPT-4o</Select.Option>
</Select>
</Form.Item>
<Form.Item
name="topK"
label="检索结果数量"
rules={[{ required: true, message: '请输入检索结果数量' }]}
>
<Input type="number" min={1} max={20} />
</Form.Item>
<Form.Item
name="temperature"
label="温度系数"
rules={[{ required: true, message: '请输入温度系数' }]}
>
<Input type="number" min={0} max={2} step={0.1} />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
保存设置
</Button>
</Form.Item>
</Form>
</Card>
);
};
export default SettingsPage;
4. 智能问答界面开发
现在我们来开发最核心的智能问答界面,实现流式输出和引用来源展示。
4.1 聊天界面组件
创建 src/pages/ChatPage.tsx:
import { useState, useEffect, useRef } from 'react';
import { Card, Input, Button, List, Avatar, Spin, Typography, Tag, Empty } from 'antd';
import { SendOutlined, UserOutlined, RobotOutlined } from '@ant-design/icons';
import { openclawApiService } from '../api';
import { useAppStore } from '../store';
import './ChatPage.css';
const { Text, Paragraph } = Typography;
const ChatPage = () => {
const { sessionId, messages, setSessionId, addMessage, setMessages, isLoading, setIsLoading } = useAppStore();
const [inputValue, setInputValue] = useState('');
const messagesEndRef = useRef<HTMLDivElement>(null);
// 初始化会话
useEffect(() => {
const initSession = async () => {
try {
const response = await openclawApiService.createSession('知识库会话');
setSessionId(response.data.data.id);
} catch (error: any) {
console.error('创建会话失败:', error);
}
};
if (!sessionId) {
initSession();
}
}, [sessionId, setSessionId]);
// 自动滚动到底部
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
// 处理发送消息
const handleSend = async () => {
if (!inputValue.trim() || !sessionId || isLoading) return;
const userMessage = {
id: Date.now().toString(),
role: 'user' as const,
content: inputValue.trim(),
timestamp: new Date(),
};
addMessage(userMessage);
setInputValue('');
setIsLoading(true);
try {
// 发送消息并获取流式响应
const response = await fetch(`http://localhost:18789/api/sessions/${sessionId}/messages`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
role: 'user',
content: userMessage.content,
stream: true,
}),
});
if (!response.ok) {
throw new Error('请求失败');
}
const reader = response.body?.getReader();
const decoder = new TextDecoder();
let assistantContent = '';
// 创建一个空的助手消息
const assistantMessageId = (Date.now() + 1).toString();
addMessage({
id: assistantMessageId,
role: 'assistant',
content: '',
timestamp: new Date(),
});
// 读取流式响应
while (reader) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n').filter(line => line.trim() !== '');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') continue;
try {
const parsed = JSON.parse(data);
if (parsed.content) {
assistantContent += parsed.content;
// 更新助手消息
setMessages(prev => prev.map(msg =>
msg.id === assistantMessageId
? { ...msg, content: assistantContent }
: msg
));
}
} catch (e) {
console.error('解析响应失败:', e);
}
}
}
}
} catch (error: any) {
console.error('发送消息失败:', error);
addMessage({
id: Date.now().toString(),
role: 'assistant',
content: `抱歉,发生了一个错误: ${error.message}`,
timestamp: new Date(),
});
} finally {
setIsLoading(false);
}
};
// 处理回车键发送
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
};
return (
<Card
title="智能问答"
style={{ height: 'calc(100vh - 120px)', display: 'flex', flexDirection: 'column' }}
>
<div className="chat-messages" style={{ flex: 1, overflowY: 'auto', marginBottom: 16 }}>
{messages.length === 0 ? (
<Empty
description="开始与知识库对话吧!"
style={{ marginTop: 100 }}
/>
) : (
<List
dataSource={messages}
renderItem={(message) => (
<div
className={`message-item ${message.role}`}
style={{
display: 'flex',
marginBottom: 16,
justifyContent: message.role === 'user' ? 'flex-end' : 'flex-start'
}}
>
{message.role === 'assistant' && (
<Avatar icon={<RobotOutlined />} style={{ marginRight: 12, backgroundColor: '#1890ff' }} />
)}
<div
className="message-content"
style={{
maxWidth: '70%',
padding: '12px 16px',
borderRadius: 8,
backgroundColor: message.role === 'user' ? '#1890ff' : '#f0f0f0',
color: message.role === 'user' ? '#fff' : '#000'
}}
>
<Paragraph style={{ margin: 0, whiteSpace: 'pre-wrap' }}>
{message.content}
</Paragraph>
{message.sources && message.sources.length > 0 && (
<div style={{ marginTop: 8 }}>
<Text type="secondary" style={{ fontSize: 12 }}>引用来源:</Text>
{message.sources.map((source: any, index: number) => (
<Tag key={index} size="small" style={{ marginLeft: 4 }}>
{source.metadata?.fileName || '未知'}
</Tag>
))}
</div>
)}
</div>
{message.role === 'user' && (
<Avatar icon={<UserOutlined />} style={{ marginLeft: 12, backgroundColor: '#52c41a' }} />
)}
</div>
)}
/>
)}
{isLoading && messages.length > 0 && messages[messages.length - 1].role === 'user' && (
<div style={{ display: 'flex', marginBottom: 16 }}>
<Avatar icon={<RobotOutlined />} style={{ marginRight: 12, backgroundColor: '#1890ff' }} />
<div style={{ padding: '12px 16px', borderRadius: 8, backgroundColor: '#f0f0f0' }}>
<Spin size="small" />
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
<div className="chat-input" style={{ display: 'flex', gap: 8 }}>
<Input.TextArea
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyPress={handleKeyPress}
placeholder="输入你的问题..."
autoSize={{ minRows: 1, maxRows: 4 }}
disabled={isLoading || !sessionId}
/>
<Button
type="primary"
icon={<SendOutlined />}
onClick={handleSend}
loading={isLoading}
disabled={!inputValue.trim() || !sessionId}
>
发送
</Button>
</div>
</Card>
);
};
export default ChatPage;
创建 src/pages/ChatPage.css:
.message-item.user .message-content {
border-bottom-right-radius: 0;
}
.message-item.assistant .message-content {
border-bottom-left-radius: 0;
}
5. 测试完整用户流程
现在我们来测试完整的用户流程:
-
启动所有服务:
- FastAPI 后端:
cd backend && uvicorn main:app --reload --port 8000 - OpenClaw 网关:
cd openclaw && pnpm run start - React 前端:
cd frontend && pnpm run dev
- FastAPI 后端:
-
上传文档:
- 打开前端页面
http://localhost:5173 - 进入"文档管理"页面
- 拖拽或点击上传一个测试文档(如之前的员工请假制度.md)
- 等待上传完成,文档会自动解析并向量化
- 打开前端页面
-
智能问答:
- 进入"智能问答"页面
- 输入问题:“员工每年有多少天带薪年假?”
- 点击发送,你会看到 OpenClaw 自动调用 RAG 检索技能,然后基于检索结果给出回答
- 回答会以流式方式显示,就像打字一样
6. 项目结构更新
至此,我们的项目目录已经更新为:
enterprise-knowledge-base/
├── .gitignore
├── backend/
│ ├── venv/
│ ├── .env
│ ├── main.py
│ ├── requirements.txt
│ ├── uploads/
│ ├── utils/
│ │ ├── text_splitter.py
│ │ └── document_parser.py
│ └── services/
│ ├── vector_service.py
│ └── document_service.py
├── openclaw/
│ ├── node_modules/
│ ├── package.json
│ ├── .claw/
│ │ └── config.yaml
│ └── skills/
│ ├── hello-world/
│ ├── document-parser/
│ └── rag-retrieval/
└── frontend/
├── node_modules/
├── src/
│ ├── api/
│ │ └── index.ts
│ ├── components/
│ ├── pages/
│ │ ├── DocumentPage.tsx
│ │ ├── ChatPage.tsx
│ │ ├── SettingsPage.tsx
│ │ └── ChatPage.css
│ ├── store/
│ │ └── index.ts
│ ├── utils/
│ ├── App.tsx
│ ├── App.css
│ └── main.tsx
├── index.html
├── package.json
└── tsconfig.json
7. 小结
本篇文章我们完成了以下事情:
- 优化了 React 前端项目架构,实现了 API 封装和 Zustand 状态管理
- 在 FastAPI 后端补充了文档上传、解析和管理接口
- 开发了文档上传页面,支持拖拽上传、批量上传和进度显示
- 实现了文档管理页面,支持文档列表展示、搜索和删除
- 开发了智能问答界面,实现了流式输出(打字机效果)
- 完成了前后端联调,测试了完整的"上传→解析→向量化→问答"用户流程