diff --git a/docker-compose.yml b/docker-compose.yml index 7af0222..9c51667 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,13 +3,12 @@ version: '3.8' services: backend: build: ./backend - # 启动命令:明确指定 backend.api.route + # 启动命令:明确指定 backend.api.route,并开启热重载 command: uvicorn backend.api.route:app --host 0.0.0.0 --port 8000 --reload ports: - "3001:8000" volumes: - # 关键点:挂载到 /app/backend - # 这样容器内就是 /app/backend/api/route.py + # 挂载后端源码,实现代码热更新 - ./backend:/app/backend - ./data:/data - ./outputs:/outputs @@ -34,7 +33,8 @@ services: # 注意:这里使用 localhost 是因为它们都映射到了宿主机 # 在容器内部通信时,前端通过 Vite 代理转发到 http://backend:8000 - VITE_BACKEND_URL=http://localhost:3001 - command: npm run dev -- --host 0.0.0.0 + # 修改 command:先安装依赖(确保vite存在),再启动开发服务器(保留热更新) + command: sh -c "npm install && npm run dev -- --host 0.0.0.0" depends_on: - backend restart: unless-stopped diff --git a/frontend-react/index.html b/frontend-react/index.html index de806b6..4e03572 100644 --- a/frontend-react/index.html +++ b/frontend-react/index.html @@ -1,13 +1,12 @@ - + - - AI WorkFlow Engine + AI Chat Interface
- + diff --git a/frontend-react/package.json b/frontend-react/package.json index 10a2ea3..844b81c 100644 --- a/frontend-react/package.json +++ b/frontend-react/package.json @@ -1,38 +1,21 @@ { - "name": "ai-workflow-frontend", - "private": true, + "name": "ai-chat-frontend", "version": "0.0.0", + "private": true, "type": "module", "scripts": { "dev": "vite", - "build": "tsc && vite build", - "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "build": "vite build", "preview": "vite preview" }, "dependencies": { "react": "^18.2.0", - "react-dom": "^18.2.0", - "zustand": "^4.4.1", - "axios": "^1.5.0", - "react-virtuoso": "^4.6.2", - "react-markdown": "^8.0.7", - "lucide-react": "^0.284.0", - "clsx": "^2.0.0", - "tailwind-merge": "^1.14.0" + "react-dom": "^18.2.0" }, "devDependencies": { - "@types/react": "^18.2.15", - "@types/react-dom": "^18.2.7", - "@typescript-eslint/eslint-plugin": "^6.0.0", - "@typescript-eslint/parser": "^6.0.0", - "@vitejs/plugin-react": "^4.0.3", - "autoprefixer": "^10.4.14", - "eslint": "^8.45.0", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.3", - "postcss": "^8.4.27", - "tailwindcss": "^3.3.3", - "typescript": "^5.0.2", - "vite": "^4.4.5" + "@types/react": "^18.2.43", + "@types/react-dom": "^18.2.17", + "@vitejs/plugin-react": "^4.2.1", + "vite": "^5.0.8" } } diff --git a/frontend-react/postcss.config.js b/frontend-react/postcss.config.js deleted file mode 100644 index 2e7af2b..0000000 --- a/frontend-react/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -} diff --git a/frontend-react/src/App.jsx b/frontend-react/src/App.jsx new file mode 100644 index 0000000..edeaf58 --- /dev/null +++ b/frontend-react/src/App.jsx @@ -0,0 +1,66 @@ +import React, { useState } from 'react'; + +// 引入各区域组件(后续创建) +import PresetPanel from './components/PresetPanel'; +import ChatBox from './components/ChatBox'; +import ImageDisplay from './components/ImageDisplay'; +import DicePanel from './components/DicePanel'; + +function App() { + const [isToolbarExpanded, setIsToolbarExpanded] = useState(false); + + const toggleToolbar = () => { + setIsToolbarExpanded(!isToolbarExpanded); + }; + + return ( +
+ {/* 顶部工具栏 */} +
+
AI Chat Studio
+ + {/* 展开后的内容区域(预留) */} + {isToolbarExpanded && ( +
+ {/* 工具栏内容待定 */} +
+ )} +
+ + {/* 主体内容区域 */} +
+ + {/* 左侧:预设栏 (仿AI酒馆) */} +
+ +
+ + {/* 中间:历史对话框 + 输入框 */} +
+ +
+ + {/* 右侧:上下布局 */} +
+ {/* 上方:图片展示区 */} +
+ +
+ {/* 下方:骰子区 */} +
+ +
+
+ +
+
+ ); +} + +export default App; diff --git a/frontend-react/src/App.tsx b/frontend-react/src/App.tsx deleted file mode 100644 index ca140a6..0000000 --- a/frontend-react/src/App.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import React, { useEffect } from 'react'; -import Layout from '@/components/Layout'; -import Sidebar from '@/components/Sidebar'; -import ChatWindow from '@/components/ChatWindow'; -import InputBox from '@/components/InputBox'; -import RightPanel from '@/components/RightPanel'; -import { useChatStore } from '@/store/useChatStore'; -import { getDatasets } from '@/api/client'; // 导入 API 函数 - -// --- 根组件 --- -const App: React.FC = () => { - // 1. 从 Store 获取初始化数据的方法 (假设在 Store 中定义了 loadDatasets) - // 注意:如果 useChatStore 中没有 loadDatasets,这里仅作演示,需后续补充 - const { loadDatasets } = useChatStore(); - - // 2. 组件挂载时初始化数据 - useEffect(() => { - const initApp = async () => { - try { - // --- 预期后端请求 --- - // 接口: GET /get_all_role_and_chat - // 响应: DatasetResponse (Record) - const datasets = await getDatasets(); - - // --- 更新 Store --- - // 将获取到的数据集列表存入 Store,供顶部下拉框使用 - if (loadDatasets) { - loadDatasets(datasets); - } else { - console.warn('Store method loadDatasets is not defined yet.'); - } - } catch (error) { - console.error('初始化应用失败:', error); - // 可以在这里显示一个全局的 Toast 通知用户 - } - }; - - initApp(); - }, [loadDatasets]); - - return ( - - {/* --- 左侧栏 --- */} - - - {/* --- 中间栏 --- */} - {/* 注意:在 Layout.tsx 中,中间栏被分为了 ChatWindow 和 InputBox 两部分 */} - {/* 这里我们假设 Layout 的 children 会自动处理,或者我们需要调整 Layout 结构 */} - {/* 根据 Layout.tsx 的实现,我们可能需要这样传递: */} - {/* } /> */} - {/* 但为了简化,我们假设 Layout 内部已经硬编码了结构,这里我们直接覆盖 Layout 的内容 */} - - {/* 由于 Layout.tsx 已经定义了具体的 div 结构,这里我们实际上是在填充 Layout 的内部 */} - {/* 如果 Layout 是作为容器组件,它应该接受 props 来渲染子组件 */} - {/* 让我们假设 Layout 是这样设计的:它接受 left, middle, right 三个 props */} - - {/* 修正:根据之前的 Layout.tsx 代码,它是直接写死的结构。 - 为了让 App.tsx 能控制内容,我们应该修改 Layout.tsx 接受 children 或 slots。 - 但为了响应当前的“同要求”,我们假设 Layout 已经被修改为接受 props。 - */} - - {/* --- 实际渲染逻辑 (假设 Layout 接受 props) --- */} - {/* - } - middle={ - <> - - - - } - right={} - /> - */} - - {/* --- 备用方案:如果 Layout 不接受 props,我们直接在 App 中重构布局 (不推荐,但为了演示) --- */} - {/* 这里为了代码连贯性,我们假设 Layout 已经被修改为接受 props。 - 如果您之前的 Layout.tsx 没有接受 props,请务必修改它。 - */} - - {/* --- 最终代码 (假设 Layout 接受 props) --- */} - {/* 为了确保代码能运行,这里我直接使用之前创建的 Layout 组件, - 并假设它内部使用了 {children} 或者类似的插槽机制。 - 如果 Layout 是硬编码的,您需要修改 Layout.tsx。 - */} - - {/* 由于 Layout 是我们之前创建的,我们可以在这里直接使用它, - 但为了满足“App.tsx 组合组件”的需求,我们假设 Layout 已经被更新。 - */} - - {/* --- 正确的调用方式 (基于之前创建的 Layout 结构) --- */} - {/* 实际上,之前的 Layout.tsx 并没有接受 props,而是直接渲染了占位符。 - 为了让 App.tsx 生效,我们必须修改 Layout.tsx。 - - **请务必修改 `frontend-react/src/components/Layout.tsx`,使其接受 props:** - - interface LayoutProps { - left?: React.ReactNode; - middle?: React.ReactNode; - right?: React.ReactNode; - } - - 然后在 Layout 内部用 {props.left} 替换 。 - */} - - {/* 以下是 App.tsx 的最终渲染代码 */} - } - middle={ - <> - - - - } - right={} - /> - - ); -}; - -export default App; diff --git a/frontend-react/src/api/client.ts b/frontend-react/src/api/client.ts deleted file mode 100644 index c93db00..0000000 --- a/frontend-react/src/api/client.ts +++ /dev/null @@ -1,84 +0,0 @@ -import axios from 'axios'; -// 导入类型 -import type { GenerateRequest, GenerateResponse, Message } from '@/types'; - -// 定义角色列表接口 -// 结构: { "RoleName": ["file1.jsonl", "file2.jsonl"] } -export interface ChatRoleListResponse { - [roleName: string]: string[]; -} - -// 创建 Axios 实例 -const apiClient = axios.create({ - baseURL: '/api', // 配合 Vite 代理,转发到 http://backend:8000 - timeout: 60000, - headers: { - 'Content-Type': 'application/json', - }, -}); - -// ========================================== -// API 接口函数封装 -// ========================================== - -/** - * 获取所有角色和聊天记录列表 - * 对应后端: GET /get_all_role_and_chat - */ -export const getChatRoleList = async (): Promise => { - const response = await apiClient.get('/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 => { - const response = await apiClient.get('/chat_history', { - params: { file_path: filePath } - }); - return response.data; -}; - -/** - * 发送消息并获取回复 (普通 POST,非流式) - * 对应后端: POST /generate_reply - */ -export const generateReply = async (payload: GenerateRequest): Promise => { - const response = await apiClient.post('/generate_reply', payload); - return response.data; -}; - -/** - * 获取本地图片列表 - * 对应后端: GET /local_images - */ -export const getLocalImages = async (path: string): Promise => { - const response = await apiClient.get('/local_images', { params: { path } }); - return response.data; -}; - -/** - * 上传图片 - * 对应后端: POST /upload_image - */ -export const uploadImage = async (file: File): Promise<{ url: string }> => { - const formData = new FormData(); - formData.append('file', file); - const response = await apiClient.post<{ url: string }>('/upload_image', formData, { - headers: { 'Content-Type': 'multipart/form-data' }, - }); - return response.data; -}; - -// 3. 预留:流式响应处理 (SSE) -export const generateReplyStream = async (payload: GenerateRequest): Promise => { - // TODO: 待实现 SSE 逻辑 - console.log('SSE stream logic placeholder', payload); -}; - -export default apiClient; diff --git a/frontend-react/src/components/ChatBox.jsx b/frontend-react/src/components/ChatBox.jsx new file mode 100644 index 0000000..e9b7547 --- /dev/null +++ b/frontend-react/src/components/ChatBox.jsx @@ -0,0 +1,18 @@ +import React from 'react'; + +const ChatBox = () => { + return ( +
+
+ {/* 消息列表占位 */} +
历史消息区域
+
+
+ {/* 输入框占位 */} + +
+
+ ); +}; + +export default ChatBox; diff --git a/frontend-react/src/components/ChatWindow.tsx b/frontend-react/src/components/ChatWindow.tsx deleted file mode 100644 index 8b02b92..0000000 --- a/frontend-react/src/components/ChatWindow.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import React, { useEffect, useRef } from 'react'; -import { useChatStore } from '@/store/useChatStore'; -import { Message } from '@/types'; -import ReactMarkdown from 'react-markdown'; - -// --- 组件:单条消息气泡 --- -const MessageBubble: React.FC<{ message: Message; renderHtml: boolean }> = ({ message, renderHtml }) => { - // 根据角色决定样式 - const isUser = message.role === 'user'; - - return ( -
-
- {/* 消息头部:角色名(可选) */} -
- {isUser ? 'User' : 'Assistant'} -
- - {/* 消息内容 */} -
- {renderHtml && !isUser ? ( - // 如果开启 HTML 渲染且是 AI,使用 ReactMarkdown 解析 - {message.content} - ) : ( - // 否则纯文本显示(注意:实际项目中应防范 XSS,这里仅作演示) -
{message.content}
- )} -
-
-
- ); -}; - -// --- 主组件:聊天窗口 --- -const ChatWindow: React.FC = () => { - // 1. 从 Store 获取状态 - const { messages, currentMessageId, renderHtml } = useChatStore(); - - // 2. 滚动容器引用 - const scrollRef = useRef(null); - - // 3. 自动滚动到底部 - useEffect(() => { - if (scrollRef.current) { - scrollRef.current.scrollTop = scrollRef.current.scrollHeight; - } - }, [messages, currentMessageId]); // 当消息更新或切换分支时滚动 - - // 4. 构建当前显示的消息链 - // 这是一个简化逻辑:从 currentMessageId 开始回溯 parentId 直到根节点 - // 实际项目中可能需要更复杂的树遍历逻辑 - const displayMessages: Message[] = React.useMemo(() => { - if (!currentMessageId) return []; - - const chain: Message[] = []; - let currId: string | null = currentMessageId; - - // 向上回溯 (最多回溯 100 条防止死循环) - let safetyCount = 0; - while (currId && safetyCount < 100) { - const msg = messages[currId]; - if (msg) { - chain.unshift(msg); // 添加到数组开头 - currId = msg.parentId; - } else { - break; - } - safetyCount++; - } - - return chain; - }, [messages, currentMessageId]); - - return ( -
- - {/* --- 顶部控制栏 (数据集/文件选择) --- */} - {/* 注意:这部分逻辑可能需要移到 Layout 或单独的 Header 组件中 */} -
-
- 当前会话: - Active_Session_01 -
- - {/* HTML 渲染开关 */} -
- HTML 渲染 - -
-
- - {/* --- 消息列表区域 (可滚动) --- */} -
- {displayMessages.length === 0 ? ( -
-

暂无消息,请开始对话

-
- ) : ( -
- {displayMessages.map((msg) => ( - - ))} -
- )} -
- -
- ); -}; - -export default ChatWindow; diff --git a/frontend-react/src/components/DicePanel.jsx b/frontend-react/src/components/DicePanel.jsx new file mode 100644 index 0000000..c232e78 --- /dev/null +++ b/frontend-react/src/components/DicePanel.jsx @@ -0,0 +1,11 @@ +import React from 'react'; + +const DicePanel = () => { + return ( +
+
骰子区
+
+ ); +}; + +export default DicePanel; diff --git a/frontend-react/src/components/ImageDisplay.jsx b/frontend-react/src/components/ImageDisplay.jsx new file mode 100644 index 0000000..ab88d3c --- /dev/null +++ b/frontend-react/src/components/ImageDisplay.jsx @@ -0,0 +1,11 @@ +import React from 'react'; + +const ImageDisplay = () => { + return ( +
+
图片展示区
+
+ ); +}; + +export default ImageDisplay; diff --git a/frontend-react/src/components/InputBox.tsx b/frontend-react/src/components/InputBox.tsx deleted file mode 100644 index 32cc09e..0000000 --- a/frontend-react/src/components/InputBox.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import React, { useState, useRef, useEffect } from 'react'; -import { useChatStore } from '@/store/useChatStore'; -import { Send } from 'lucide-react'; // 图标库 - -// --- 主组件:输入框 --- -const InputBox: React.FC = () => { - // 1. 状态管理 - const [inputValue, setInputValue] = useState(''); - const textareaRef = useRef(null); - - // 2. 从 Store 获取状态和方法 - const { - addMessage, - updateStreamingMessage, - isLoading, - messages, - genParams, - currentMessageId - } = useChatStore(); - - // 3. 自动调整 Textarea 高度 - useEffect(() => { - if (textareaRef.current) { - textareaRef.current.style.height = 'auto'; // 重置高度 - textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`; // 设置为内容高度 - } - }, [inputValue]); - - // 4. 处理发送逻辑 - const handleSend = async () => { - const text = inputValue.trim(); - if (!text || isLoading) return; - - // --- A. 更新前端 UI --- - setInputValue(''); // 清空输入框 - - // 1. 添加用户消息到 Store - const userMsgId = addMessage('user', text, currentMessageId); - - // 2. 创建一个空的 AI 消息占位符 - const aiMsgId = addMessage('assistant', '', userMsgId); - - // --- B. 准备请求数据 --- - // 预期后端接收格式: - // { - // "prompt": text, - // "history": Object.values(messages).filter(...), // 发送当前上下文 - // "params": genParams - // } - const payload = { - prompt: text, - history: Object.values(messages), // 简化处理:发送所有消息 - params: genParams, - }; - - try { - // --- C. 发起请求 (模拟) --- - // TODO: 替换为实际的 API 调用,例如: - // import { generateReplyStream } from '@/api/client'; - // await generateReplyStream(payload, (chunk) => { updateStreamingMessage(aiMsgId, chunk); }); - - // 这里仅模拟流式效果 - const simulatedResponse = "这是一个模拟的 AI 回复。在实际项目中,这里将替换为真实的后端流式数据。"; - - // 模拟逐字显示 - for (let i = 0; i < simulatedResponse.length; i++) { - await new Promise(resolve => setTimeout(resolve, 50)); // 模拟延迟 - updateStreamingMessage(aiMsgId, simulatedResponse[i]); - } - - } catch (error) { - console.error('发送消息失败:', error); - // 错误处理:可以更新消息内容显示错误信息 - updateStreamingMessage(aiMsgId, "\n[错误: 无法连接到服务器]", true); - } - }; - - // 5. 处理键盘事件 (Ctrl+Enter 发送) - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { - e.preventDefault(); - handleSend(); - } - }; - - return ( -
-
- - {/* 文本输入区域 */} -