修改前端技术栈后,改为react技术,准备放弃重构
This commit is contained in:
@@ -1,26 +1,46 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import type { DatasetResponse, GenerateRequest, GenerateResponse } from '@/types';
|
// 导入类型
|
||||||
|
import type { GenerateRequest, GenerateResponse, Message } from '@/types';
|
||||||
|
|
||||||
// 1. 创建 Axios 实例
|
// 定义角色列表接口
|
||||||
// 注意:这里使用相对路径 '/api' 是为了配合 vite.config.ts 中的代理配置
|
// 结构: { "RoleName": ["file1.jsonl", "file2.jsonl"] }
|
||||||
// 这样在 Docker 内部请求会被转发到 http://backend:8000
|
export interface ChatRoleListResponse {
|
||||||
// 如果您更倾向于直接请求宿主机端口,也可以改为 'http://localhost:3001'
|
[roleName: string]: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建 Axios 实例
|
||||||
const apiClient = axios.create({
|
const apiClient = axios.create({
|
||||||
baseURL: '/api',
|
baseURL: '/api', // 配合 Vite 代理,转发到 http://backend:8000
|
||||||
timeout: 60000, // 本地生成可能较慢,设置 60s 超时
|
timeout: 60000,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. API 接口函数封装
|
// ==========================================
|
||||||
|
// API 接口函数封装
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取所有数据集和聊天记录列表
|
* 获取所有角色和聊天记录列表
|
||||||
* 对应后端: GET /get_all_role_and_chat
|
* 对应后端: GET /get_all_role_and_chat
|
||||||
*/
|
*/
|
||||||
export const getDatasets = async (): Promise<DatasetResponse> => {
|
export const getChatRoleList = async (): Promise<ChatRoleListResponse> => {
|
||||||
const response = await apiClient.get<DatasetResponse>('/get_all_role_and_chat');
|
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;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -56,7 +76,6 @@ export const uploadImage = async (file: File): Promise<{ url: string }> => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 3. 预留:流式响应处理 (SSE)
|
// 3. 预留:流式响应处理 (SSE)
|
||||||
// 由于 Axios 对 SSE 支持一般,后续这里可能会改用原生 fetch 或 EventSource
|
|
||||||
export const generateReplyStream = async (payload: GenerateRequest): Promise<void> => {
|
export const generateReplyStream = async (payload: GenerateRequest): Promise<void> => {
|
||||||
// TODO: 待实现 SSE 逻辑
|
// TODO: 待实现 SSE 逻辑
|
||||||
console.log('SSE stream logic placeholder', payload);
|
console.log('SSE stream logic placeholder', payload);
|
||||||
|
|||||||
@@ -44,17 +44,82 @@ const Layout: React.FC<LayoutProps> = ({ left, middle, right }) => {
|
|||||||
|
|
||||||
{/* --- 顶部工具栏 --- */}
|
{/* --- 顶部工具栏 --- */}
|
||||||
<header className="h-14 bg-white border-b border-[#D1D9E6] flex items-center justify-between px-4 shrink-0 z-20 shadow-sm">
|
<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">
|
{/* --- 左侧:双层下拉框 (替换原来的 打开/保存按钮) --- */}
|
||||||
📂 打开
|
<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>
|
</button>
|
||||||
<button className="text-[#0056B3] font-semibold hover:bg-blue-50 px-3 py-1 rounded transition">
|
|
||||||
💾 保存
|
{/* 悬浮菜单 */}
|
||||||
|
{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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<h1 className="text-lg font-bold text-[#0056B3]">AI WorkFlow Engine</h1>
|
{/* --- 中间:标题 (替换原来的 AI WorkFlow Engine) --- */}
|
||||||
|
{/* 这里留空,或者您可以放一个小的 Logo/图标 */}
|
||||||
|
<div className="text-lg font-bold text-[#0056B3]">
|
||||||
|
{/* 可以在这里放图标,或者直接留空让布局更紧凑 */}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* --- 右侧:设置 (保留原来的代码) --- */}
|
||||||
<div className="flex items-center space-x-4">
|
<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 className="text-[#0056B3] font-semibold hover:bg-blue-50 px-3 py-1 rounded transition">
|
||||||
⚙️ 设置
|
⚙️ 设置
|
||||||
@@ -62,6 +127,7 @@ const Layout: React.FC<LayoutProps> = ({ left, middle, right }) => {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
||||||
{/* --- 主体区域:三栏布局 --- */}
|
{/* --- 主体区域:三栏布局 --- */}
|
||||||
<main className="flex-1 flex overflow-hidden">
|
<main className="flex-1 flex overflow-hidden">
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { Message, ChatState, SpliceBlock, GenerationParams } from '@/types';
|
import type { Message, SpliceBlock, GenerationParams } from '@/types';
|
||||||
|
|
||||||
// --- 辅助函数:生成唯一 ID ---
|
// --- 辅助函数:生成唯一 ID ---
|
||||||
const generateId = (): string => {
|
const generateId = (): string => {
|
||||||
@@ -24,36 +24,71 @@ const initialGenParams: GenerationParams = {
|
|||||||
stream: true,
|
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 定义 ---
|
// --- Store 定义 ---
|
||||||
export const useChatStore = create<ChatState>((set, get) => ({
|
export const useChatStore = create<ChatState>((set, get) => ({
|
||||||
|
|
||||||
// --- 状态初始化 ---
|
// --- 状态初始化 ---
|
||||||
messages: {},
|
messages: {},
|
||||||
currentMessageId: null,
|
currentMessageId: null,
|
||||||
|
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
inputMessage: "",
|
inputMessage: "",
|
||||||
|
renderHtml: true,
|
||||||
renderHtml: true, // 默认开启 HTML 渲染,对应原 app.py
|
|
||||||
selectedFile: null,
|
selectedFile: null,
|
||||||
|
|
||||||
spliceBlocks: initialSpliceBlocks,
|
spliceBlocks: initialSpliceBlocks,
|
||||||
genParams: initialGenParams,
|
genParams: initialGenParams,
|
||||||
|
|
||||||
|
// --- 角色和文件列表 ---
|
||||||
|
datasets: {}, // 初始为空字典
|
||||||
|
currentRole: null, // 初始未选中
|
||||||
|
currentChatFile: null, // 初始未选中
|
||||||
|
|
||||||
// --- Actions (操作方法) ---
|
// --- 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 }),
|
setInputMessage: (text: string) => set({ inputMessage: text }),
|
||||||
|
|
||||||
// 2. 切换 HTML 渲染开关
|
// 5. 切换 HTML 渲染开关
|
||||||
toggleRenderHtml: () => set((state) => ({ renderHtml: !state.renderHtml })),
|
toggleRenderHtml: () => set((state) => ({ renderHtml: !state.renderHtml })),
|
||||||
|
|
||||||
// 3. 更新生成参数 (支持部分更新)
|
// 6. 更新生成参数
|
||||||
updateGenParams: (newParams: Partial<GenerationParams>) =>
|
updateGenParams: (newParams: Partial<GenerationParams>) =>
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
genParams: { ...state.genParams, ...newParams },
|
genParams: { ...state.genParams, ...newParams },
|
||||||
})),
|
})),
|
||||||
|
|
||||||
// 4. 切换拼接块状态 (即时生效)
|
// 7. 切换拼接块状态
|
||||||
toggleSpliceBlock: (id: string | number) =>
|
toggleSpliceBlock: (id: string | number) =>
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
spliceBlocks: state.spliceBlocks.map((block) =>
|
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) => {
|
addMessage: (role: 'user' | 'assistant', content: string, parentId: string | null = null) => {
|
||||||
const newMessage: Message = {
|
const newMessage: Message = {
|
||||||
id: generateId(),
|
id: generateId(),
|
||||||
@@ -70,13 +105,12 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|||||||
role,
|
role,
|
||||||
content,
|
content,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
isStreaming: false, // 初始为非流式状态
|
isStreaming: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const newMessages = { ...state.messages, [newMessage.id]: newMessage };
|
const newMessages = { ...state.messages, [newMessage.id]: newMessage };
|
||||||
|
|
||||||
// 如果有父节点,更新父节点的 childrenIds
|
|
||||||
if (parentId) {
|
if (parentId) {
|
||||||
const parent = state.messages[parentId];
|
const parent = state.messages[parentId];
|
||||||
if (parent) {
|
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) => {
|
updateStreamingMessage: (messageId: string, contentChunk: string, isComplete: boolean = false) => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const currentMsg = state.messages[messageId];
|
const currentMsg = state.messages[messageId];
|
||||||
@@ -108,21 +142,18 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|||||||
[messageId]: {
|
[messageId]: {
|
||||||
...currentMsg,
|
...currentMsg,
|
||||||
content: currentMsg.content + contentChunk,
|
content: currentMsg.content + contentChunk,
|
||||||
isStreaming: !isComplete, // 如果完成,则结束流式状态
|
isStreaming: !isComplete,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// 7. 切换当前消息分支 (点击历史消息时)
|
// 10. 切换当前消息分支
|
||||||
switchBranch: (messageId: string) => {
|
switchBranch: (messageId: string) => {
|
||||||
set({ currentMessageId: messageId });
|
set({ currentMessageId: messageId });
|
||||||
},
|
},
|
||||||
|
|
||||||
// 8. 设置加载状态
|
// 11. 设置加载状态
|
||||||
setLoading: (loading: boolean) => set({ isLoading: loading }),
|
setLoading: (loading: boolean) => set({ isLoading: loading }),
|
||||||
|
|
||||||
// 9. 设置当前选中的文件
|
|
||||||
setSelectedFile: (filePath: string | null) => set({ selectedFile: filePath }),
|
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user