修改前端技术栈后,改为react技术,准备放弃重构
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 }),
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user