修改前端技术栈后,改为react技术,准备放弃重构

This commit is contained in:
2026-03-18 17:59:26 +08:00
parent 85f2bbe78c
commit 3c4a11eca8
3 changed files with 165 additions and 49 deletions

View File

@@ -1,26 +1,46 @@
import axios from 'axios';
import type { DatasetResponse, GenerateRequest, GenerateResponse } from '@/types';
// 导入类型
import type { GenerateRequest, GenerateResponse, Message } from '@/types';
// 1. 创建 Axios 实例
// 注意:这里使用相对路径 '/api' 是为了配合 vite.config.ts 中的代理配置
// 这样在 Docker 内部请求会被转发到 http://backend:8000
// 如果您更倾向于直接请求宿主机端口,也可以改为 'http://localhost:3001'
// 定义角色列表接口
// 结构: { "RoleName": ["file1.jsonl", "file2.jsonl"] }
export interface ChatRoleListResponse {
[roleName: string]: string[];
}
// 创建 Axios 实例
const apiClient = axios.create({
baseURL: '/api',
timeout: 60000, // 本地生成可能较慢,设置 60s 超时
baseURL: '/api', // 配合 Vite 代理,转发到 http://backend:8000
timeout: 60000,
headers: {
'Content-Type': 'application/json',
},
});
// 2. API 接口函数封装
// ==========================================
// API 接口函数封装
// ==========================================
/**
* 获取所有数据集和聊天记录列表
* 获取所有角色和聊天记录列表
* 对应后端: GET /get_all_role_and_chat
*/
export const getDatasets = async (): Promise<DatasetResponse> => {
const response = await apiClient.get<DatasetResponse>('/get_all_role_and_chat');
export const getChatRoleList = async (): Promise<ChatRoleListResponse> => {
const response = await apiClient.get<ChatRoleListResponse>('/get_all_role_and_chat');
return response.data;
};
/**
* 获取特定聊天记录
* 对应后端: GET /chat_history?file_path=xxx.jsonl
*
* @param filePath - 文件路径,如 "RoleA/chat1.jsonl"
* @returns Message[] - 消息数组
*/
export const getChatHistory = async (filePath: string): Promise<Message[]> => {
const response = await apiClient.get<Message[]>('/chat_history', {
params: { file_path: filePath }
});
return response.data;
};
@@ -56,10 +76,9 @@ export const uploadImage = async (file: File): Promise<{ url: string }> => {
};
// 3. 预留:流式响应处理 (SSE)
// 由于 Axios 对 SSE 支持一般,后续这里可能会改用原生 fetch 或 EventSource
export const generateReplyStream = async (payload: GenerateRequest): Promise<void> => {
// TODO: 待实现 SSE 逻辑
console.log('SSE stream logic placeholder', payload);
};
export default apiClient;
export default apiClient;

View File

@@ -43,24 +43,90 @@ const Layout: React.FC<LayoutProps> = ({ left, middle, right }) => {
<div className="flex flex-col h-screen bg-[#F0F4F8] text-[#1A1A1A] font-sans overflow-hidden">
{/* --- 顶部工具栏 --- */}
<header className="h-14 bg-white border-b border-[#D1D9E6] flex items-center justify-between px-4 shrink-0 z-20 shadow-sm">
<div className="flex items-center space-x-4">
<button className="text-[#0056B3] font-semibold hover:bg-blue-50 px-3 py-1 rounded transition">
📂
</button>
<button className="text-[#0056B3] font-semibold hover:bg-blue-50 px-3 py-1 rounded transition">
💾
</button>
</div>
<header className="h-14 bg-white border-b border-[#D1D9E6] flex items-center justify-between px-4 shrink-0 z-20 shadow-sm">
<h1 className="text-lg font-bold text-[#0056B3]">AI WorkFlow Engine</h1>
{/* --- 左侧:双层下拉框 (替换原来的 打开/保存按钮) --- */}
<div className="flex items-center space-x-2">
{/* 1. 第一层:角色选择 */}
<select
value={currentRole || ''}
onChange={(e) => selectRole(e.target.value)}
className="text-sm border border-gray-300 rounded px-2 py-1 focus:outline-none focus:border-blue-500 bg-white"
>
<option value="" disabled></option>
{Object.keys(datasets).map((role) => (
<option key={role} value={role}>{role}</option>
))}
</select>
{/* 2. 第二层:文件选择 (仅在选择角色后显示) */}
{currentRole && datasets[currentRole] && (
<div className="flex items-center space-x-1 relative"> {/* relative 用于定位菜单 */}
<select
value={currentChatFile || ''}
onChange={(e) => selectChatFile(e.target.value)}
className="text-sm border border-gray-300 rounded px-2 py-1 focus:outline-none focus:border-blue-500 bg-white"
>
<option value="" disabled></option>
{datasets[currentRole].map((file) => {
// 去掉 .jsonl 后缀用于显示
const displayName = file.replace('.jsonl', '');
return <option key={file} value={file}>{displayName}</option>;
})}
</select>
{/* 3. 操作按钮 (重命名/删除) */}
<button
onClick={() => setShowMenu(!showMenu)}
className="text-gray-500 hover:text-blue-600 px-1"
title="操作"
>
</button>
{/* 悬浮菜单 */}
{showMenu && (
<div className="absolute top-full left-0 mt-1 bg-white border border-gray-200 rounded shadow-lg py-1 z-50 min-w-[100px]">
<button
className="block w-full text-left px-4 py-2 text-sm hover:bg-gray-100"
onClick={() => {
console.log('重命名功能待实现');
setShowMenu(false);
}}
>
</button>
<button
className="block w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-red-50"
onClick={() => {
console.log('删除功能待实现');
setShowMenu(false);
}}
>
</button>
</div>
)}
</div>
)}
</div>
{/* --- 中间:标题 (替换原来的 AI WorkFlow Engine) --- */}
{/* 这里留空,或者您可以放一个小的 Logo/图标 */}
<div className="text-lg font-bold text-[#0056B3]">
{/* 可以在这里放图标,或者直接留空让布局更紧凑 */}
</div>
{/* --- 右侧:设置 (保留原来的代码) --- */}
<div className="flex items-center space-x-4">
<button className="text-[#0056B3] font-semibold hover:bg-blue-50 px-3 py-1 rounded transition">
</button>
</div>
</header>
<div className="flex items-center space-x-4">
<button className="text-[#0056B3] font-semibold hover:bg-blue-50 px-3 py-1 rounded transition">
</button>
</div>
</header>
{/* --- 主体区域:三栏布局 --- */}
<main className="flex-1 flex overflow-hidden">

View File

@@ -1,5 +1,5 @@
import { create } from 'zustand';
import { Message, ChatState, SpliceBlock, GenerationParams } from '@/types';
import type { Message, SpliceBlock, GenerationParams } from '@/types';
// --- 辅助函数:生成唯一 ID ---
const generateId = (): string => {
@@ -24,36 +24,71 @@ const initialGenParams: GenerationParams = {
stream: true,
};
// --- 状态接口定义 ---
export interface ChatState {
// 1. 消息树状态
messages: Record<string, Message>;
currentMessageId: string | null;
isLoading: boolean;
inputMessage: string;
// 2. 设置状态
renderHtml: boolean;
selectedFile: string | null;
spliceBlocks: SpliceBlock[];
genParams: GenerationParams;
// 3. 角色与文件状态
datasets: Record<string, string[]>; // 存储角色列表字典
currentRole: string | null;
currentChatFile: string | null;
}
// --- Store 定义 ---
export const useChatStore = create<ChatState>((set, get) => ({
// --- 状态初始化 ---
messages: {},
currentMessageId: null,
isLoading: false,
inputMessage: "",
renderHtml: true, // 默认开启 HTML 渲染,对应原 app.py
renderHtml: true,
selectedFile: null,
spliceBlocks: initialSpliceBlocks,
genParams: initialGenParams,
// --- 角色和文件列表 ---
datasets: {}, // 初始为空字典
currentRole: null, // 初始未选中
currentChatFile: null, // 初始未选中
// --- Actions (操作方法) ---
// 1. 设置输入框内容
// 1. 加载数据集列表
loadDatasets: (data: Record<string, string[]>) => set({ datasets: data }),
// 2. 选择角色
selectRole: (roleName: string) => set((state) => ({
currentRole: roleName,
currentChatFile: null // 切换角色时,必须清空文件选择
})),
// 3. 选择聊天文件
selectChatFile: (fileName: string) => set({ currentChatFile: fileName }),
// 4. 设置输入框内容
setInputMessage: (text: string) => set({ inputMessage: text }),
// 2. 切换 HTML 渲染开关
// 5. 切换 HTML 渲染开关
toggleRenderHtml: () => set((state) => ({ renderHtml: !state.renderHtml })),
// 3. 更新生成参数 (支持部分更新)
// 6. 更新生成参数
updateGenParams: (newParams: Partial<GenerationParams>) =>
set((state) => ({
genParams: { ...state.genParams, ...newParams },
})),
// 4. 切换拼接块状态 (即时生效)
// 7. 切换拼接块状态
toggleSpliceBlock: (id: string | number) =>
set((state) => ({
spliceBlocks: state.spliceBlocks.map((block) =>
@@ -61,7 +96,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
),
})),
// 5. 添加新消息 (用户发送或 AI 完整回复后调用)
// 8. 添加新消息
addMessage: (role: 'user' | 'assistant', content: string, parentId: string | null = null) => {
const newMessage: Message = {
id: generateId(),
@@ -70,13 +105,12 @@ export const useChatStore = create<ChatState>((set, get) => ({
role,
content,
timestamp: Date.now(),
isStreaming: false, // 初始为非流式状态
isStreaming: false,
};
set((state) => {
const newMessages = { ...state.messages, [newMessage.id]: newMessage };
// 如果有父节点,更新父节点的 childrenIds
if (parentId) {
const parent = state.messages[parentId];
if (parent) {
@@ -93,10 +127,10 @@ export const useChatStore = create<ChatState>((set, get) => ({
};
});
return newMessage.id; // 返回新消息 ID方便后续操作
return newMessage.id;
},
// 6. 更新流式消息内容 (打字机效果)
// 9. 更新流式消息内容
updateStreamingMessage: (messageId: string, contentChunk: string, isComplete: boolean = false) => {
set((state) => {
const currentMsg = state.messages[messageId];
@@ -108,21 +142,18 @@ export const useChatStore = create<ChatState>((set, get) => ({
[messageId]: {
...currentMsg,
content: currentMsg.content + contentChunk,
isStreaming: !isComplete, // 如果完成,则结束流式状态
isStreaming: !isComplete,
},
},
};
});
},
// 7. 切换当前消息分支 (点击历史消息时)
// 10. 切换当前消息分支
switchBranch: (messageId: string) => {
set({ currentMessageId: messageId });
},
// 8. 设置加载状态
// 11. 设置加载状态
setLoading: (loading: boolean) => set({ isLoading: loading }),
// 9. 设置当前选中的文件
setSelectedFile: (filePath: string | null) => set({ selectedFile: filePath }),
}));