From 6b65b24b0f6d69293d430d77e40315b1f45d7454 Mon Sep 17 00:00:00 2001 From: moranzhi Date: Fri, 1 May 2026 14:44:18 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=E5=A4=A7=E9=87=8F=E7=BE=8E?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- API_CONFIG_FINAL_SUMMARY.md | 390 ---------- API_IMAGE_CONFIG_COMPLETE.md | 412 ----------- COMFYUI_API_CONFIG_GUIDE.md | 485 ------------- COMFYUI_WORKFLOW_IMPLEMENTATION.md | 243 ------- COMPACT_DESIGN_REFACTOR.md | 411 ----------- backend/Dockerfile | 5 +- backend/api/route.py | 3 +- backend/api/routes/chatsRoute.py | 80 ++- backend/core/config.py | 8 + backend/main.py | 10 + data/characters/defult.png | Bin 0 -> 59445 bytes data/chat/testRole1/111.jsonl | 5 - data/chat/testRole1/11111.jsonl | 5 - data/chat/testRole2/222.jsonl | 5 - data/chat/testRole3/333.jsonl | 5 - docker-compose.yml | 2 + frontend/COLOR_SCHEME_OPTIMIZATION.md | 388 ---------- frontend/CSS_MIGRATION_COMPLETE.md | 443 ------------ frontend/DATA_TYPE_AUDIT_REPORT.md | 669 ------------------ frontend/GLOBAL_MINIMALIST_STYLE_COMPLETE.md | 382 ---------- frontend/INPUT_BOX_FIX_COMPLETE.md | 433 ------------ frontend/INPUT_SIMPLIFICATION_COMPLETE.md | 363 ---------- frontend/PATH_FIX_RECORD.md | 141 ---- frontend/REFACTORING_COMPLETE_REPORT.md | 327 --------- frontend/RESTRUCTURE_COMPLETE.md | 282 -------- frontend/RESTRUCTURE_GUIDE.md | 404 ----------- frontend/SIDEBAR_BORDERS_CONFIRMED.md | 243 ------- frontend/STORE_REFACTORING_COMPLETE.md | 242 ------- frontend/TEXTAREA_AUTORESIZ_OPTIMIZATION.md | 378 ---------- frontend/THEME_SYSTEM_COMPLETE.md | 263 ------- frontend/TOPBAR_REFACTORING_COMPLETE.md | 360 ---------- frontend/TYPES_COMPLETION_SUMMARY.md | 490 ------------- frontend/ULTRA_MINIMALIST_COMPLETE.md | 392 ---------- frontend/src/App.jsx | 119 +++- frontend/src/Store/Mid/ChatBoxSlice.jsx | 55 +- .../src/Store/SideBarLeft/WorldBookSlice.jsx | 93 +-- frontend/src/Store/SideBarLeft/index.js | 1 + .../src/components/Mid/ChatBox/ChatBox.css | 483 +++++++++---- .../src/components/Mid/ChatBox/ChatBox.jsx | 146 +++- .../tabs/ApiConfig/ApiConfig.new.jsx | 484 ------------- .../tabs/CharacterCard/CharacterCard.css | 514 +++++++++++++- .../tabs/CharacterCard/CharacterCard.jsx | 550 +++++++++++++- .../SideBarLeft/tabs/Presets/Presets.css | 124 ++-- frontend/src/components/TopBar/TopBar.css | 85 ++- frontend/src/components/TopBar/TopBar.jsx | 66 +- .../TopBar/items/ThemeToggle/ThemeToggle.css | 112 +++ .../TopBar/items/ThemeToggle/ThemeToggle.jsx | 139 +++- frontend/src/index.css | 139 +++- frontend/src/styles/variables.css | 127 ++++ 49 files changed, 2487 insertions(+), 9019 deletions(-) delete mode 100644 API_CONFIG_FINAL_SUMMARY.md delete mode 100644 API_IMAGE_CONFIG_COMPLETE.md delete mode 100644 COMFYUI_API_CONFIG_GUIDE.md delete mode 100644 COMFYUI_WORKFLOW_IMPLEMENTATION.md delete mode 100644 COMPACT_DESIGN_REFACTOR.md create mode 100644 data/characters/defult.png delete mode 100644 data/chat/testRole1/111.jsonl delete mode 100644 data/chat/testRole1/11111.jsonl delete mode 100644 data/chat/testRole2/222.jsonl delete mode 100644 data/chat/testRole3/333.jsonl delete mode 100644 frontend/COLOR_SCHEME_OPTIMIZATION.md delete mode 100644 frontend/CSS_MIGRATION_COMPLETE.md delete mode 100644 frontend/DATA_TYPE_AUDIT_REPORT.md delete mode 100644 frontend/GLOBAL_MINIMALIST_STYLE_COMPLETE.md delete mode 100644 frontend/INPUT_BOX_FIX_COMPLETE.md delete mode 100644 frontend/INPUT_SIMPLIFICATION_COMPLETE.md delete mode 100644 frontend/PATH_FIX_RECORD.md delete mode 100644 frontend/REFACTORING_COMPLETE_REPORT.md delete mode 100644 frontend/RESTRUCTURE_COMPLETE.md delete mode 100644 frontend/RESTRUCTURE_GUIDE.md delete mode 100644 frontend/SIDEBAR_BORDERS_CONFIRMED.md delete mode 100644 frontend/STORE_REFACTORING_COMPLETE.md delete mode 100644 frontend/TEXTAREA_AUTORESIZ_OPTIMIZATION.md delete mode 100644 frontend/THEME_SYSTEM_COMPLETE.md delete mode 100644 frontend/TOPBAR_REFACTORING_COMPLETE.md delete mode 100644 frontend/TYPES_COMPLETION_SUMMARY.md delete mode 100644 frontend/ULTRA_MINIMALIST_COMPLETE.md delete mode 100644 frontend/src/components/SideBarLeft/tabs/ApiConfig/ApiConfig.new.jsx diff --git a/API_CONFIG_FINAL_SUMMARY.md b/API_CONFIG_FINAL_SUMMARY.md deleted file mode 100644 index be832b1..0000000 --- a/API_CONFIG_FINAL_SUMMARY.md +++ /dev/null @@ -1,390 +0,0 @@ -# ✅ API 配置功能 - 完成清单 - -## 📦 已完成的功能模块 - -### **1. 后端实现** ✅ - -#### **工作流管理服务** -- ✅ `backend/services/comfyui_workflow_manager.py` (173行) - - 列出所有工作流 - - 上传工作流(带验证) - - 删除工作流(保护默认文件) - - 加载工作流 - - 提示词替换功能 - -#### **API 端点** (6个) -- ✅ `GET /api/api-config/comfyui/workflows` - 获取工作流列表 -- ✅ `POST /api/api-config/comfyui/workflows/upload` - 上传工作流 -- ✅ `DELETE /api/api-config/comfyui/workflows/{filename}` - 删除工作流 -- ✅ `GET /api/api-config/comfyui/workflows/{filename}` - 获取工作流详情 -- ✅ `POST /api/api-config/test-comfyui-connection` - 测试 ComfyUI 连接 -- ✅ `POST /api/api-config/test-cloud-connection` - 测试云端 API 连接 - -#### **默认工作流** -- ✅ `backend/data/comfyui_workflows/default_txt2img.json` - - 标准 ComfyUI API 格式 - - 7个节点(KSampler、CheckpointLoader、EmptyLatentImage、CLIPTextEncode x2、VAEDecode、SaveImage) - - 包含 `_meta` 元数据 - - 中文节点标题 - ---- - -### **2. 前端实现** ✅ - -#### **核心组件** -- ✅ `ComfyUIWorkflowManager.jsx` (179行) - - 工作流列表显示 - - 上传功能 - - 删除功能 - - 刷新功能 - - 空状态提示 - - 使用说明 - -#### **主配置页面** -- ✅ `ApiConfig.jsx` (完整重构) - - 模式切换卡片(本地/云端) - - 本地 ComfyUI 配置表单 - - 云端 API 配置表单 - - 嵌套路径更新逻辑 - - 修改跟踪系统 - - 测试连接功能 - -#### **样式系统** -- ✅ `ApiConfig.css` (扩展 300+ 行) - - 模式选择器样式 - - Toggle Switch 开关 - - 工作流管理器样式 - - 响应式设计 - - 防横向滚动 - ---- - -### **3. 数据结构** ✅ - -#### **imageModel 新结构** -```javascript -{ - mode: 'local', // 'local' | 'cloud' - - local: { - apiUrl: 'http://comfyui:8188', - websocketEnabled: true, - queueTimeout: 300, - defaultWorkflow: 'default_txt2img.json' - }, - - cloud: { - provider: 'dall-e', - apiUrl: 'https://api.openai.com/v1/images/generations', - apiKey: '', - model: 'dall-e-3' - } -} -``` - ---- - -### **4. 响应式设计** ✅ - -#### **SillyTavern 风格布局** -- ✅ 无页面级滚动条 (`overflow: hidden`) -- ✅ 三栏独立滚动 (`overflow-y: auto`) -- ✅ 禁止横向滚动 (`overflow-x: hidden`) -- ✅ 视口高度布局 (`100vh`) -- ✅ 媒体查询适配 (<768px) - ---- - -### **5. 交互逻辑** ✅ - -#### **核心函数** -- ✅ `handleChange(e, path)` - 支持嵌套路径更新 -- ✅ `handleImageModeChange(mode)` - 模式切换 -- ✅ `testComfyUIConnection(apiUrl)` - 测试本地连接 -- ✅ `testCloudConnection(config)` - 测试云端连接 -- ✅ `handleOpenSaveModal()` - 打开保存对话框 -- ✅ `handleSave()` - 保存配置 - -#### **修改跟踪** -- ✅ 自动标记已修改的配置 -- ✅ 保存按钮显示修改数量 -- ✅ 标签页红点提示 - ---- - -## 📁 文件清单 - -### **新增文件** (7个) -1. ✅ `backend/data/comfyui_workflows/default_txt2img.json` -2. ✅ `backend/services/comfyui_workflow_manager.py` -3. ✅ `frontend/src/components/SideBarLeft/tabs/ApiConfig/ComfyUIWorkflowManager.jsx` -4. ✅ `COMFYUI_WORKFLOW_IMPLEMENTATION.md` -5. ✅ `API_IMAGE_CONFIG_COMPLETE.md` -6. ✅ `COMFYUI_API_CONFIG_GUIDE.md` -7. ✅ `API_CONFIG_FINAL_SUMMARY.md` (本文件) - -### **修改文件** (3个) -1. ✅ `backend/api/routes/apiConfigRoute.py` (+148行) -2. ✅ `frontend/src/components/SideBarLeft/tabs/ApiConfig/ApiConfig.jsx` (重构) -3. ✅ `frontend/src/components/SideBarLeft/tabs/ApiConfig/ApiConfig.css` (+300行) - ---- - -## 🎯 功能特性 - -### **工作流管理** -- ✅ 上传自定义工作流 JSON -- ✅ 删除工作流(保护默认文件) -- ✅ 列表显示(文件名、节点数、大小) -- ✅ 实时刷新 -- ✅ 默认工作流标记 - -### **配置管理** -- ✅ 本地/云端模式切换 -- ✅ 完整的本地配置表单 -- ✅ 完整的云端配置表单 -- ✅ 动态模型选择 -- ✅ WebSocket 开关 -- ✅ 超时设置 - -### **连接测试** -- ✅ ComfyUI 连接测试 - - 检查连通性 - - 获取 VRAM 信息 - - 获取设备信息 -- ✅ 云端 API 连接测试 - - DALL-E 验证 - - Stability AI 验证 - - 模型可用性检查 - -### **安全性** -- ✅ API Key 加密存储(Fernet) -- ✅ 路径遍历攻击防护 -- ✅ JSON 格式验证 -- ✅ 工作流有效性检查 -- ✅ 文件备份机制 - ---- - -## 🔧 技术栈 - -### **后端** -- FastAPI -- Python requests -- OpenAI SDK -- cryptography (Fernet 加密) -- JSON 文件存储 - -### **前端** -- React 18 -- Zustand (状态管理) -- CSS3 (Grid + Flexbox) -- Fetch API -- FormData (文件上传) - ---- - -## 📊 代码统计 - -| 模块 | 文件数 | 代码行数 | -|------|--------|----------| -| 后端服务 | 1 | 173 | -| 后端路由 | 1 | +148 | -| 前端组件 | 1 | 179 | -| 前端主页面 | 1 | ~800 (重构) | -| 样式文件 | 1 | +300 | -| 工作流模板 | 1 | 108 | -| 文档 | 4 | ~1500 | -| **总计** | **10** | **~3200+** | - ---- - -## ✅ 测试清单 - -### **后端测试** -```bash -# 1. 测试列出工作流 -curl http://localhost:8000/api/api-config/comfyui/workflows - -# 2. 测试上传工作流 -curl -X POST http://localhost:8000/api/api-config/comfyui/workflows/upload \ - -F "file=@my_workflow.json" - -# 3. 测试删除工作流 -curl -X DELETE http://localhost:8000/api/api-config/comfyui/workflows/my_workflow.json - -# 4. 测试获取工作流详情 -curl http://localhost:8000/api/api-config/comfyui/workflows/default_txt2img.json - -# 5. 测试 ComfyUI 连接 -curl -X POST http://localhost:8000/api/api-config/test-comfyui-connection \ - -H "Content-Type: application/json" \ - -d '{"apiUrl": "http://localhost:8188"}' - -# 6. 测试云端 API 连接 -curl -X POST http://localhost:8000/api/api-config/test-cloud-connection \ - -H "Content-Type: application/json" \ - -d '{"provider": "dall-e", "apiKey": "sk-xxx", "model": "dall-e-3"}' -``` - -### **前端测试** -- [ ] 打开 API 配置页面 -- [ ] 切换到"🎨 生图"标签 -- [ ] 看到模式切换卡片 -- [ ] 点击"本地 ComfyUI" → 显示本地配置 -- [ ] 点击"在线 API" → 显示云端配置 -- [ ] 填写配置并测试连接 -- [ ] 上传工作流文件 -- [ ] 查看工作流列表 -- [ ] 删除工作流(非默认) -- [ ] 保存配置 -- [ ] 重新加载配置 -- [ ] 测试响应式布局 - ---- - -## 🚀 部署说明 - -### **Docker 环境** - -```yaml -# docker-compose.yml -version: '3.8' - -services: - llm-workflow-engine: - build: ./backend - ports: - - "23338:8000" - volumes: - - ./backend/data:/app/data - networks: - - ai-network - - comfyui: - image: ghcr.io/comfyanonymous/comfyui:latest - ports: - - "8188:8188" - volumes: - - ./comfyui/models:/app/models - - ./comfyui/output:/app/output - networks: - - ai-network - command: --listen 0.0.0.0 --port 8188 - -networks: - ai-network: - driver: bridge -``` - -**配置示例**: -- API 地址:`http://comfyui:8188` -- 工作流目录:`backend/data/comfyui_workflows/` - ---- - -### **本地环境** - -```bash -# 1. 安装依赖 -cd backend -pip install -r requirements.txt - -# 2. 启动后端 -uvicorn main:app --reload --port 8000 - -# 3. 启动前端 -cd frontend -npm run dev - -# 4. 启动 ComfyUI -python comfyui/main.py --listen 0.0.0.0 --port 8188 -``` - -**配置示例**: -- API 地址:`http://localhost:8188` - ---- - -## 📝 使用流程 - -### **首次配置** - -1. **选择模式** - - 点击"🎨 生图"标签 - - 选择"🖥️ 本地 ComfyUI"或"☁️ 在线 API" - -2. **填写配置** - - 本地:填写 API 地址、超时等 - - 云端:填写 API Key、选择模型 - -3. **测试连接** - - 点击"测试连接"按钮 - - 确认连接成功 - -4. **管理工作流**(仅本地模式) - - 查看默认工作流 - - (可选)上传自定义工作流 - -5. **保存配置** - - 点击"保存配置" - - 勾选"🎨 生图" - - 确认保存 - ---- - -### **运行时生图** - -``` -用户输入:"画一只猫" - ↓ -聊天接口检测生图意图 - ↓ -读取 imageModel 配置 - ↓ -调用 ImageGenerator.generate_image() - ↓ -如果 mode === 'local': - 1. 加载工作流 JSON - 2. 替换提示词为"画一只猫" - 3. 发送到 ComfyUI (/prompt) - 4. 等待完成 (/history/{prompt_id}) - 5. 返回图片 URL (/view?filename=...) -否则: - 1. 调用 DALL-E API - 2. 返回图片 URL - ↓ -在聊天界面显示图片 -``` - ---- - -## 🎊 总结 - -### **已完成** ✅ -- ✅ 完整的工作流管理系统 -- ✅ 本地/云端双模式支持 -- ✅ 标准的 ComfyUI API 格式 -- ✅ 连接测试功能 -- ✅ 响应式 UI 设计 -- ✅ SillyTavern 风格布局 -- ✅ 安全性保障(加密、验证) -- ✅ 完善的文档 - -### **待完成** ⚠️ -- ⚠️ 生图服务实现 (`image_generator.py`) -- ⚠️ 集成到聊天接口 -- ⚠️ Store 保存逻辑更新(处理嵌套结构) - -### **下一步建议** -1. 测试前端 UI 和后端 API -2. 创建 `image_generator.py` 服务 -3. 集成到聊天流程 -4. 添加进度显示和错误处理 - ---- - -**当前状态**: 🟢 **API 配置功能完成,等待生图服务集成** - -**文档版本**: v1.0.0 -**最后更新**: 2026-04-28 diff --git a/API_IMAGE_CONFIG_COMPLETE.md b/API_IMAGE_CONFIG_COMPLETE.md deleted file mode 100644 index e425f12..0000000 --- a/API_IMAGE_CONFIG_COMPLETE.md +++ /dev/null @@ -1,412 +0,0 @@ -# 🎨 API 配置页面 - 生图功能完善总结 - -## ✅ 已完成的功能 - -### **1. 数据结构设计** - -#### **imageModel 新结构** -```javascript -imageModel: { - mode: 'local', // 'local' | 'cloud' - - local: { - apiUrl: 'http://comfyui:8188', - websocketEnabled: true, - queueTimeout: 300, - defaultWorkflow: 'default_txt2img.json' - }, - - cloud: { - provider: 'dall-e', - apiUrl: 'https://api.openai.com/v1/images/generations', - apiKey: '', - model: 'dall-e-3' - } -} -``` - ---- - -### **2. 前端 UI 组件** - -#### **模式切换卡片** ✅ -- 🖥️ 本地 ComfyUI - - 图标 + 标题 + 描述 - - 悬停效果(上浮 + 阴影) - - 选中状态(高亮边框 + 背景色) - -- ☁️ 在线 API - - 同样的交互效果 - - 清晰的视觉区分 - -#### **本地 ComfyUI 配置表单** ✅ -- API 地址输入框 - - 提示:Docker vs 本地运行 -- WebSocket 开关(Toggle Switch) -- 队列超时设置(数字输入) -- 默认工作流下拉选择 -- 测试连接按钮 - -#### **云端 API 配置表单** ✅ -- 服务提供商选择(DALL-E / Stability AI) -- API Key 输入(密码框) -- 模型选择(根据提供商动态显示) -- 测试连接按钮 - -#### **ComfyUI 工作流管理器** ✅ -- 工作流列表显示 - - 文件名 - - 节点数量 - - 文件大小 - - 默认标记 -- 上传按钮(导入 JSON) -- 删除按钮(每个工作流) -- 刷新按钮 -- 空状态提示 -- 使用说明 - ---- - -### **3. 响应式设计** ✅ - -#### **布局策略** -```css -/* 全局禁止页面级滚动 */ -html, body { - height: 100%; - overflow: hidden; -} - -#root { - height: 100vh; - display: flex; - flex-direction: column; -} - -/* 三栏独立滚动 */ -.sidebar-left, .chat-area, .sidebar-right { - overflow-y: auto; - overflow-x: hidden; -} -``` - -#### **媒体查询** -```css -@media (max-width: 768px) { - /* 小屏幕下单列布局 */ - .image-mode-selector { - grid-template-columns: 1fr; - } - - .form-row { - flex-direction: column; - } -} -``` - -#### **防横向滚动** -```css -.api-config-container { - max-width: 100%; - overflow-x: hidden; -} - -.form-control { - max-width: 100%; - box-sizing: border-box; -} -``` - ---- - -### **4. 交互逻辑** - -#### **handleChange 支持嵌套路径** ✅ -```javascript -// 扁平结构(其他 API) -handleChange(e); - -// 嵌套结构(生图配置) -handleChange(e, ['imageModel', 'local', 'apiUrl']); -``` - -#### **模式切换** ✅ -```javascript -handleImageModeChange('local'); // 或 'cloud' -``` - -#### **修改跟踪** ✅ -- 自动标记已修改的配置 -- 保存按钮显示修改数量 -- 标签页红点提示 - ---- - -### **5. 样式系统** - -#### **模式卡片** ✅ -- Grid 布局(2列) -- 悬停动画(transform + shadow) -- 选中状态(border + background + ring) -- Flexbox 垂直居中内容 - -#### **开关 Toggle** ✅ -- CSS-only 实现 -- 平滑过渡动画 -- Focus 状态(无障碍) -- 自定义颜色主题 - -#### **工作流列表** ✅ -- 卡片式布局 -- 悬停高亮 -- 徽章样式(默认标记) -- 滚动容器(max-height) - ---- - -## 📋 **待完成的后端功能** - -### **1. 测试连接端点** ⚠️ - -需要添加两个新的 API 端点: - -```python -@router.post("/test-comfyui-connection") -def test_comfyui_connection(config: dict): - """测试 ComfyUI 连接""" - # 1. 检查连通性 - # 2. 获取系统信息(VRAM、设备) - # 3. 返回结果 - -@router.post("/test-cloud-connection") -def test_cloud_connection(config: dict): - """测试云端 API 连接""" - # 1. 验证 API Key - # 2. 测试请求 - # 3. 返回结果 -``` - ---- - -### **2. 生图服务** ⚠️ - -创建 `backend/services/image_generator.py`: - -```python -class ImageGenerator: - def generate_image(self, prompt: str, config: dict): - if config['mode'] == 'local': - return self._call_comfyui(prompt, config['local']) - else: - return self._call_cloud_api(prompt, config['cloud']) - - def _call_comfyui(self, prompt: str, local_config: dict): - # 1. 加载工作流 - # 2. 替换提示词 - # 3. 发送到 ComfyUI - # 4. 等待完成 - # 5. 返回图片 URL - - def _call_cloud_api(self, prompt: str, cloud_config: dict): - # 1. 调用 OpenAI/Stability API - # 2. 返回图片 URL -``` - ---- - -### **3. Store 更新** ⚠️ - -`ApiConfigSlice.jsx` 需要: -- 更新 `saveProfile` 以正确处理嵌套的 `imageModel` 结构 -- 确保加密只应用于 `cloud.apiKey` - ---- - -## 🎯 **SillyTavern 布局参考** - -### **核心原则** -1. ✅ **无页面级滚动条** - `overflow: hidden` on body -2. ✅ **三栏独立滚动** - 每栏 `overflow-y: auto` -3. ✅ **无横向滚动** - `overflow-x: hidden` everywhere -4. ✅ **Flexbox 布局** - 弹性自适应 -5. ✅ **视口高度** - `100vh` / `100dvh` - -### **实现细节** - -``` -┌─────────────────────────────────────────┐ -│ TopBar (固定高度) │ -├──────────┬──────────────┬───────────────┤ -│ │ │ │ -│ Left │ Center │ Right │ -│ Panel │ Panel │ Panel │ -│ │ │ │ -│ scroll ↓ │ scroll ↓ │ scroll ↓ │ -│ │ │ │ -└──────────┴──────────────┴───────────────┘ -``` - -**CSS 关键代码**: -```css -/* App 根容器 */ -.app { - height: 100vh; - display: flex; - flex-direction: column; - overflow: hidden; -} - -/* 主布局 */ -.main-container { - flex: 1; - display: flex; - overflow: hidden; -} - -/* 每个面板 */ -.panel { - overflow-y: auto; - overflow-x: hidden; -} -``` - ---- - -## 🔧 **测试清单** - -### **前端测试** -- [ ] 打开 API 配置页面 -- [ ] 切换到"🎨 生图"标签 -- [ ] 看到模式切换卡片 -- [ ] 点击"本地 ComfyUI"卡片 - - [ ] 显示本地配置表单 - - [ ] 显示工作流管理器 -- [ ] 点击"在线 API"卡片 - - [ ] 显示云端配置表单 - - [ ] 隐藏工作流管理器 -- [ ] 测试表单输入 - - [ ] API 地址输入 - - [ ] WebSocket 开关 - - [ ] 超时设置 - - [ ] 工作流选择 -- [ ] 测试上传工作流 - - [ ] 点击"+ 导入工作流" - - [ ] 选择 JSON 文件 - - [ ] 看到上传成功提示 - - [ ] 列表中显示新工作流 -- [ ] 测试删除工作流 - - [ ] 点击删除按钮 - - [ ] 确认删除 - - [ ] 看到删除成功提示 -- [ ] 测试响应式 - - [ ] 缩小浏览器窗口 - - [ ] 模式卡片变为单列 - - [ ] 表单行变为垂直排列 -- [ ] 检查滚动条 - - [ ] 页面无滚动条 - - [ ] 左侧边栏可垂直滚动 - - [ ] 无横向滚动条 - -### **后端测试** -```bash -# 测试列出工作流 -curl http://localhost:8000/api/api-config/comfyui/workflows - -# 测试上传 -curl -X POST http://localhost:8000/api/api-config/comfyui/workflows/upload \ - -F "file=@test_workflow.json" - -# 测试删除 -curl -X DELETE http://localhost:8000/api/api-config/comfyui/workflows/test.json -``` - ---- - -## 📝 **使用流程** - -### **用户配置 ComfyUI** - -1. **选择模式** - - 点击"🎨 生图"标签 - - 点击"🖥️ 本地 ComfyUI"卡片 - -2. **填写配置** - - API 地址:`http://comfyui:8188`(Docker) - - 启用 WebSocket:✓ - - 队列超时:300 秒 - - 默认工作流:文生图(默认) - -3. **管理工作流** - - 查看默认工作流列表 - - (可选)上传自定义工作流 - - 在 ComfyUI 中设计工作流 - - 导出为 JSON(API Format) - - 点击"+ 导入工作流"上传 - -4. **测试连接** - - 点击"测试连接"按钮 - - 查看 VRAM 和设备信息 - -5. **保存配置** - - 点击底部"保存配置"按钮 - - 勾选"🎨 生图" - - 确认保存 - ---- - -### **运行时生图** - -``` -用户输入:"画一只猫" - ↓ -聊天接口检测生图意图 - ↓ -读取 imageModel 配置 - ↓ -调用 ImageGenerator.generate_image() - ↓ -如果 mode === 'local': - - 加载工作流 JSON - - 替换提示词为"画一只猫" - - 发送到 ComfyUI - - 等待完成 - - 返回图片 URL -否则: - - 调用 DALL-E API - - 返回图片 URL - ↓ -在聊天界面显示图片 -``` - ---- - -## 🎊 **总结** - -### **已完成** ✅ -- ✅ 数据结构设计(嵌套结构) -- ✅ 模式切换 UI(Radio 卡片) -- ✅ 本地配置表单(完整字段) -- ✅ 云端配置表单(完整字段) -- ✅ 工作流管理器(CRUD) -- ✅ 响应式设计(移动端适配) -- ✅ 无页面级滚动(SillyTavern 风格) -- ✅ 嵌套路径更新逻辑 -- ✅ 修改跟踪系统 -- ✅ 测试连接函数(占位) - -### **待完成** ⚠️ -- ⚠️ 后端测试连接端点 -- ⚠️ 生图服务实现 -- ⚠️ Store 保存逻辑更新 -- ⚠️ 聊天集成 - -### **架构优势** ✅ -- ✅ 清晰的职责分离(前端配置 vs 后端执行) -- ✅ 灵活的模式切换(本地/云端) -- ✅ 工作流由后端管理(易于维护) -- ✅ 响应式布局(多设备支持) -- ✅ 无滚动冲突(SillyTavern 最佳实践) - ---- - -**当前状态**: 🟢 **前端 UI 完成,等待后端服务集成** diff --git a/COMFYUI_API_CONFIG_GUIDE.md b/COMFYUI_API_CONFIG_GUIDE.md deleted file mode 100644 index 70eebbe..0000000 --- a/COMFYUI_API_CONFIG_GUIDE.md +++ /dev/null @@ -1,485 +0,0 @@ -# 🎨 ComfyUI API 配置使用指南 - -## 📋 目录 -- [快速开始](#快速开始) -- [工作流管理](#工作流管理) -- [API 配置](#api-配置) -- [测试连接](#测试连接) -- [常见问题](#常见问题) - ---- - -## 🚀 快速开始 - -### **1. 准备工作** - -确保你已经: -- ✅ 安装了 ComfyUI(本地或 Docker) -- ✅ ComfyUI 正在运行并监听 `0.0.0.0:8188` -- ✅ 下载了至少一个 checkpoint 模型文件 - -### **2. 访问 API 配置页面** - -1. 打开应用 -2. 点击左侧边栏的"⚙️ API配置" -3. 选择"🎨 生图"标签 - ---- - -## 📁 工作流管理 - -### **默认工作流** - -系统已预装一个标准的文生图工作流: -- 文件位置:`backend/data/comfyui_workflows/default_txt2img.json` -- 格式:ComfyUI API Format(标准 JSON) -- 节点数:7个(KSampler、CheckpointLoader、EmptyLatentImage、CLIPTextEncode x2、VAEDecode、SaveImage) - -### **工作流结构** - -```json -{ - "3": { - "inputs": { - "seed": 0, - "steps": 20, - "cfg": 8, - "sampler_name": "euler", - ... - }, - "class_type": "KSampler", - "_meta": { - "title": "K采样器" - } - }, - ... -} -``` - -**关键字段**: -- `class_type`: 节点类型 -- `inputs`: 节点参数 -- `_meta.title`: 节点显示名称(可选) - ---- - -### **上传自定义工作流** - -#### **步骤 1: 在 ComfyUI 中设计工作流** - -1. 打开 ComfyUI Web UI (`http://localhost:8188`) -2. 拖拽节点,搭建你的工作流 -3. 连接节点之间的数据流 -4. 配置节点参数(模型、提示词、采样器等) -5. 点击 "Queue Prompt" 测试是否能正常生成图像 - -#### **步骤 2: 导出 API 格式的 JSON** - -1. 点击顶部菜单栏的 **"工作流" (Workflow)** -2. 选择 **"导出(API)" (Export API)** 或 **"Save (API Format)"** -3. 浏览器会自动下载 `workflow_api.json` 文件 - -**重要提示**: -- ⚠️ 必须使用 **"Save (API Format)"**,而不是普通的 "Save" -- ⚠️ API 格式的 JSON 包含节点 ID 和连接关系,是 API 调用的核心 - -#### **步骤 3: 上传到本项目** - -1. 在本项目的 API 配置页面 -2. 滚动到"ComfyUI 工作流管理"区域 -3. 点击 **"+ 导入工作流"** 按钮 -4. 选择刚才导出的 JSON 文件 -5. 看到"上传成功"提示 - -#### **验证上传** - -上传成功后,你会在工作流列表中看到: -- 文件名(例如:`my_custom_workflow.json`) -- 节点数量 -- 文件大小 - ---- - -### **删除工作流** - -1. 在工作流列表中找到要删除的工作流 -2. 点击右侧的 🗑️ 删除按钮 -3. 确认删除 - -**注意**: -- ❌ `default_txt2img.json` 不可删除(受保护) -- ✅ 其他所有工作流都可以删除 - ---- - -## ⚙️ API 配置 - -### **本地 ComfyUI 模式** - -#### **配置项** - -| 字段 | 说明 | 示例值 | -|------|------|--------| -| API 地址 | ComfyUI 的服务地址 | `http://comfyui:8188` (Docker)
`http://localhost:8188` (本地) | -| 启用 WebSocket | 是否使用 WebSocket 监听进度 | ✓ / ✗ | -| 队列超时 | 等待生成的最大时间(秒) | `300` (5分钟) | -| 默认工作流 | 使用的预设工作流文件 | `default_txt2img.json` | - -#### **Docker 环境配置** - -如果使用 Docker Compose: - -```yaml -# docker-compose.yml -services: - comfyui: - image: ghcr.io/comfyanonymous/comfyui:latest - ports: - - "8188:8188" - networks: - - ai-network - command: --listen 0.0.0.0 --port 8188 - - llm-workflow-engine: - # ... - networks: - - ai-network -``` - -**API 地址填写**:`http://comfyui:8188`(Docker 内部网络 DNS) - -#### **本地运行配置** - -如果 ComfyUI 运行在宿主机: - -```bash -# 启动 ComfyUI -python main.py --listen 0.0.0.0 --port 8188 -``` - -**API 地址填写**:`http://localhost:8188` - ---- - -### **在线 API 模式** - -#### **支持的提供商** - -1. **DALL-E (OpenAI)** - - 模型:`dall-e-3`, `dall-e-2` - - 质量:最高 - - 价格:较贵 - -2. **Stable Diffusion (Stability AI)** - - 模型:`sd-xl-1024`, `sd-2-1` - - 质量:高 - - 价格:中等 - -#### **配置项** - -| 字段 | 说明 | 示例值 | -|------|------|--------| -| 服务提供商 | 选择 API 提供商 | DALL-E / Stability AI | -| API Key | 你的 API 密钥 | `sk-...` | -| 模型 | 选择具体模型 | `dall-e-3` | - -#### **获取 API Key** - -**DALL-E**: -1. 访问 https://platform.openai.com/ -2. 注册/登录账号 -3. 进入 API Keys 页面 -4. 创建新的 Secret Key -5. 复制并粘贴到配置中 - -**Stability AI**: -1. 访问 https://platform.stability.ai/ -2. 注册/登录账号 -3. 进入 API Keys 页面 -4. 创建新的 Key -5. 复制并粘贴到配置中 - ---- - -## 🔌 测试连接 - -### **测试 ComfyUI 连接** - -1. 填写 API 地址 -2. 点击"测试连接"按钮 -3. 查看结果 - -**成功响应**: -```json -{ - "success": true, - "message": "连接成功", - "stats": { - "vram_total": 25769803776, - "vram_free": 24696061952, - "torch_version": "2.1.0+cu121", - "device": "cuda" - } -} -``` - -**失败响应**: -```json -{ - "success": false, - "message": "无法连接到 ComfyUI,请检查地址和端口" -} -``` - ---- - -### **测试云端 API 连接** - -1. 填写 API Key -2. 选择模型 -3. 点击"测试连接"按钮 -4. 查看结果 - -**成功响应**: -```json -{ - "success": true, - "message": "连接成功,模型 dall-e-3 可用" -} -``` - -**失败响应**: -```json -{ - "success": false, - "message": "连接失败: Invalid API key" -} -``` - ---- - -## 💾 保存配置 - -### **保存流程** - -1. 完成所有配置后 -2. 点击底部的"保存配置"按钮 -3. 在弹出的对话框中勾选要保存的配置 -4. 点击"保存选中的配置" - -### **配置文件存储** - -- 位置:`backend/data/apiconfig/` -- 格式:JSON -- 加密:API Key 使用 Fernet 加密存储 - -### **加载配置** - -1. 从下拉框选择已保存的配置文件 -2. 自动加载所有配置 -3. 可以修改后重新保存 - ---- - -## ❓ 常见问题 - -### **Q1: 上传工作流时提示"Invalid ComfyUI workflow"** - -**原因**:上传的不是 API 格式的 JSON - -**解决**: -1. 在 ComfyUI 中使用 "Save (API Format)" 导出 -2. 不要使用普通的 "Save" 功能 -3. 确保 JSON 包含节点定义(有 `class_type` 字段) - ---- - -### **Q2: 测试连接时提示"Connection refused"** - -**可能原因**: -1. ComfyUI 未启动 -2. 地址或端口错误 -3. Docker 网络配置问题 - -**解决**: -```bash -# 检查 ComfyUI 是否运行 -curl http://localhost:8188/system_stats - -# Docker 环境下 -docker ps | grep comfyui -docker logs comfyui - -# 确认监听地址 -docker exec comfyui netstat -tlnp | grep 8188 -# 应该看到: 0.0.0.0:8188 -``` - ---- - -### **Q3: 工作流中的提示词会被替换吗?** - -**是的**!后端会自动: -1. 加载工作流 JSON -2. 找到第一个 `CLIPTextEncode` 节点 -3. 将其 `text` 字段替换为用户输入的提示词 -4. 发送到 ComfyUI - -**示例**: -```json -// 工作流中的原始提示词 -"6": { - "inputs": { - "text": "beautiful scenery nature glass bottle landscape..." - } -} - -// 运行时会被替换为 -"6": { - "inputs": { - "text": "用户输入的提示词,例如:画一只猫" - } -} -``` - ---- - -### **Q4: 如何添加 LoRA 或 ControlNet?** - -**方法 1: 在 ComfyUI 中添加节点** -1. 在 ComfyUI Web UI 中加载 LoRA Loader 或 ControlNet 节点 -2. 连接到工作流 -3. 配置参数 -4. 导出为 API 格式 -5. 上传到本项目 - -**方法 2: 手动编辑 JSON** -```json -"10": { - "inputs": { - "lora_name": "cyberpunk_style.safetensors", - "strength_model": 0.7, - "strength_clip": 0.7, - "model": ["4", 0], - "clip": ["4", 1] - }, - "class_type": "LoraLoader" -} -``` - ---- - -### **Q5: 支持批量生图吗?** - -当前版本不支持批量生图,但可以通过以下方式实现: - -**方案 A: 多次调用** -```python -for prompt in prompts: - result = generate_image(prompt, config) - save_result(result) -``` - -**方案 B: ComfyUI 批量节点** -在工作流中使用 Batch Size > 1: -```json -"5": { - "inputs": { - "width": 512, - "height": 512, - "batch_size": 4 // 一次生成4张 - } -} -``` - ---- - -### **Q6: 如何优化生图速度?** - -**本地 ComfyUI**: -1. 使用更快的采样器(如 `euler_ancestral`) -2. 减少步数(Steps: 15-20) -3. 降低分辨率(512x512 而非 1024x1024) -4. 使用 GPU 加速 - -**云端 API**: -1. 选择更快的模型(DALL-E 2 比 DALL-E 3 快) -2. 使用较小的尺寸 -3. 考虑付费套餐(更高的优先级) - ---- - -## 📊 工作流示例 - -### **基础文生图** - -```json -{ - "3": {"class_type": "KSampler", ...}, - "4": {"class_type": "CheckpointLoaderSimple", ...}, - "5": {"class_type": "EmptyLatentImage", ...}, - "6": {"class_type": "CLIPTextEncode", ...}, - "7": {"class_type": "CLIPTextEncode", ...}, - "8": {"class_type": "VAEDecode", ...}, - "9": {"class_type": "SaveImage", ...} -} -``` - -### **带 LoRA 的文生图** - -额外添加: -```json -"10": { - "class_type": "LoraLoader", - "inputs": { - "lora_name": "style.safetensors", - "strength_model": 0.7, - "model": ["4", 0], - "clip": ["4", 1] - } -} -``` - -### **图生图** - -需要添加: -```json -"10": { - "class_type": "LoadImage", - "inputs": { - "image": "reference.png" - } -}, -"11": { - "class_type": "VAEEncode", - "inputs": { - "pixels": ["10", 0], - "vae": ["4", 2] - } -} -``` - ---- - -## 🔗 相关资源 - -- **ComfyUI 官方文档**: https://github.com/comfyanonymous/ComfyUI -- **ComfyUI API 示例**: https://github.com/zer0Black/ComfyUI-Api-Demo -- **工作流分享社区**: https://comfyworkflows.com/ -- **模型下载**: https://civitai.com/ - ---- - -## 📝 更新日志 - -### **v1.0.0** (2026-04-28) -- ✅ 初始版本发布 -- ✅ 支持 ComfyUI 本地部署 -- ✅ 支持云端 API(DALL-E、Stability AI) -- ✅ 工作流管理(上传、删除、列表) -- ✅ 连接测试功能 -- ✅ 默认工作流模板 - ---- - -**如有问题,请查看日志或联系开发者!** diff --git a/COMFYUI_WORKFLOW_IMPLEMENTATION.md b/COMFYUI_WORKFLOW_IMPLEMENTATION.md deleted file mode 100644 index 4dc9c2a..0000000 --- a/COMFYUI_WORKFLOW_IMPLEMENTATION.md +++ /dev/null @@ -1,243 +0,0 @@ -# 🎨 ComfyUI 工作流管理功能 - 实现完成 - -## ✅ 已完成的功能 - -### **1. 后端实现** - -#### **文件结构** -``` -backend/ -├── data/ -│ └── comfyui_workflows/ -│ └── default_txt2img.json # 默认文生图工作流 -├── services/ -│ └── comfyui_workflow_manager.py # 工作流管理服务 -└── api/routes/ - └── apiConfigRoute.py # 添加了4个新端点 -``` - -#### **API 端点** - -1. **GET `/api/api-config/comfyui/workflows`** - - 获取所有可用的工作流列表 - - 返回: `[{filename, name, nodes_count, size}, ...]` - -2. **POST `/api/api-config/comfyui/workflows/upload`** - - 上传工作流 JSON 文件 - - 验证: JSON格式、包含KSampler节点 - - 自动备份已存在的文件 - -3. **DELETE `/api/api-config/comfyui/workflows/{filename}`** - - 删除工作流文件 - - 保护: 不允许删除 `default_txt2img.json` - -4. **GET `/api/api-config/comfyui/workflows/{filename}`** - - 获取指定工作流的详细内容 - -#### **核心功能** - -- ✅ 工作流文件管理(增删查) -- ✅ JSON 格式验证 -- ✅ ComfyUI 工作流有效性检查 -- ✅ 自动备份机制 -- ✅ 路径安全保护(防止遍历攻击) -- ✅ 提示词替换功能(`replace_prompt_in_workflow`) - ---- - -### **2. 前端实现** - -#### **新增组件** -``` -frontend/src/components/SideBarLeft/tabs/ApiConfig/ -├── ComfyUIWorkflowManager.jsx # 工作流管理器组件 -└── ApiConfig.css # 添加了工作流管理器样式 -``` - -#### **组件功能** - -**ComfyUIWorkflowManager.jsx**: -- ✅ 显示工作流列表(文件名、节点数、大小) -- ✅ 上传按钮(导入 JSON 文件) -- ✅ 删除按钮(每个工作流项) -- ✅ 刷新按钮 -- ✅ 默认工作流标记 -- ✅ 空状态提示 -- ✅ 使用说明 - -**UI 特性**: -- 紧凑的卡片式布局 -- 悬停效果 -- 加载状态 -- 错误提示 -- 响应式设计 - ---- - -### **3. 数据结构更新** - -#### **前端 formData.imageModel 新结构** - -```javascript -imageModel: { - mode: 'local', // 'local' | 'cloud' - - local: { - apiUrl: 'http://comfyui:8188', - websocketEnabled: true, - queueTimeout: 300, - defaultWorkflow: 'default_txt2img.json' - }, - - cloud: { - provider: 'dall-e', - apiUrl: 'https://api.openai.com/v1/images/generations', - apiKey: '', - model: 'dall-e-3' - } -} -``` - ---- - -## 📋 **待完成的工作** - -### **1. 前端 UI 完善** ⚠️ - -当前 `ApiConfig.jsx` 中: -- ✅ 已导入 `ComfyUIWorkflowManager` 组件 -- ✅ 已在适当位置插入组件 -- ❌ **需要添加模式切换 UI**(本地/云端 Radio 卡片) -- ❌ **需要添加本地配置表单**(apiUrl、websocket、timeout) -- ❌ **需要添加云端配置表单**(provider、apiKey、model) -- ❌ **需要修改 `handleChange` 支持嵌套结构** - -### **2. Store 更新** ⚠️ - -`ApiConfigSlice.jsx` 需要: -- ❌ 更新 `saveProfile` 以支持新的 `imageModel` 结构 -- ❌ 添加 `testComfyUIConnection` 方法 -- ❌ 添加 `testCloudConnection` 方法 - -### **3. 后端生图服务** ⚠️ - -需要创建: -- ❌ `backend/services/image_generator.py` - 统一的生图服务 - - `generate_image(prompt, config)` - 主函数 - - `call_comfyui(prompt, local_config)` - 调用 ComfyUI - - `call_cloud_api(prompt, cloud_config)` - 调用云端 API - - 工作流加载和提示词替换逻辑 - -### **4. 聊天集成** ⚠️ - -需要在聊天接口中: -- ❌ 检测用户想要生图的意图 -- ❌ 提取提示词 -- ❌ 读取 imageModel 配置 -- ❌ 调用生图服务 -- ❌ 返回图片 URL 或 base64 - ---- - -## 🎯 **下一步建议** - -### **优先级 1: 完善前端 UI** -1. 在 `ApiConfig.jsx` 中添加模式切换 Radio 卡片 -2. 根据模式动态显示不同的配置表单 -3. 修改 `handleChange` 支持嵌套路径 -4. 测试上传/删除工作流功能 - -### **优先级 2: 创建生图服务** -1. 创建 `image_generator.py` -2. 实现 ComfyUI 调用逻辑 -3. 实现云端 API 调用逻辑 -4. 添加错误处理和重试 - -### **优先级 3: 集成到聊天** -1. 在聊天路由中添加生图端点 -2. 实现意图识别(可选,或使用命令如 `/imagine`) -3. 测试完整流程 - ---- - -## 🔧 **测试清单** - -### **后端测试** -```bash -# 1. 测试列出工作流 -curl http://localhost:8000/api/api-config/comfyui/workflows - -# 2. 测试上传工作流 -curl -X POST http://localhost:8000/api/api-config/comfyui/workflows/upload \ - -F "file=@my_workflow.json" - -# 3. 测试删除工作流 -curl -X DELETE http://localhost:8000/api/api-config/comfyui/workflows/my_workflow.json - -# 4. 测试获取工作流详情 -curl http://localhost:8000/api/api-config/comfyui/workflows/default_txt2img.json -``` - -### **前端测试** -- [ ] 打开 API 配置页面 -- [ ] 切换到"🎨 生图"标签 -- [ ] 看到工作流管理器 -- [ ] 点击"导入工作流"上传 JSON -- [ ] 看到上传的工作流出现在列表中 -- [ ] 点击删除按钮删除工作流 -- [ ] 确认默认工作流不可删除 - ---- - -## 📝 **使用说明** - -### **用户上传工作流** - -1. 在 ComfyUI Web UI 中设计工作流 -2. 点击菜单 → "Save (API Format)" -3. 保存为 `.json` 文件 -4. 在本项目中点击"+ 导入工作流" -5. 选择导出的 JSON 文件 -6. 上传成功后可在列表中看到 - -### **默认工作流** - -- 文件: `backend/data/comfyui_workflows/default_txt2img.json` -- 类型: 标准文生图 -- 参数: 512x512, 20 steps, CFG 7, Euler sampler -- 不可删除 - -### **运行时提示词替换** - -后端会自动: -1. 加载选定的工作流 JSON -2. 找到第一个 `CLIPTextEncode` 节点 -3. 将其 `text` 字段替换为用户输入的提示词 -4. 发送到 ComfyUI - ---- - -## 🎊 **总结** - -### **已完成** -- ✅ 后端工作流管理服务和 API -- ✅ 默认文生图工作流 -- ✅ 前端工作流管理器组件 -- ✅ 完整的 CRUD 功能 -- ✅ 数据结构设计 - -### **待完成** -- ⚠️ 前端模式切换 UI -- ⚠️ 生图服务实现 -- ⚠️ 聊天集成 - -### **架构优势** -- ✅ 前后端分离清晰 -- ✅ 工作流由后端统一管理 -- ✅ 前端只需配置连接信息 -- ✅ 易于扩展新的工作流 -- ✅ 安全性好(验证、备份、路径保护) - ---- - -**当前状态**: 🟡 **基础框架完成,等待 UI 完善和服务集成** diff --git a/COMPACT_DESIGN_REFACTOR.md b/COMPACT_DESIGN_REFACTOR.md deleted file mode 100644 index 7111cd5..0000000 --- a/COMPACT_DESIGN_REFACTOR.md +++ /dev/null @@ -1,411 +0,0 @@ -# 🎨 Compact Modern Design - API 配置页面精简版 - -## ✨ 设计理念 - -**Compact Modern Design = Linear × Vercel** - -- **高密度** - 最大化信息展示,减少空白 -- **现代化** - Pill 标签、简洁按钮 -- **克制优雅** - 无多余装饰,功能优先 -- **高效实用** - 快速扫描和操作 - ---- - -## 📊 精简对比 - -### **之前(臃肿)** ❌ - -``` -┌─────────────────────────────┐ -│ 🖥️ │ -│ 本地 ComfyUI │ -│ 使用本地 GPU,免费但需要硬件 │ ← 太大! -└─────────────────────────────┘ -┌─────────────────────────────┐ -│ ☁️ │ -│ 在线 API │ -│ 使用云端服务,付费但无需硬件 │ -└─────────────────────────────┘ - -工作流管理区域占用 300px+ 高度 -- 大标题 -- 详细说明 -- 节点数和文件大小 -- 提示列表 -``` - -### **现在(紧凑)** ✅ - -``` -[🖥️ 本地] [☁️ 云端] ← Pill Toggle,仅 28px 高 - -工作流 📤 🔄 -- default_txt2img [默认] -- my_workflow 🗑️ -``` - ---- - -## 🎯 关键改进 - -### **1. 模式切换 - Pill Toggle** - -**之前**: 2个大卡片,每个 120px 高 -**现在**: 2个按钮,28px 高 - -```css -.mode-toggle { - display: inline-flex; - gap: 2px; - padding: 2px; - background-color: var(--color-bg-tertiary); - border-radius: 6px; -} - -.mode-btn { - padding: 4px 12px; - font-size: 0.8rem; - border-radius: 4px; -} -``` - -**视觉**: -``` -未选中: [🖥️ 本地] [☁️ 云端] -选中: [🖥️ 本地] (☁️ 云端) - ↑ 白色背景 + 阴影 -``` - ---- - -### **2. 工作流管理器 - 极简版** - -**之前**: -- 大标题 "ComfyUI 工作流管理" -- 上传按钮 "+ 导入工作流" -- 每个工作流显示:文件名、节点数、大小 -- 底部提示列表(3条) - -**现在**: -- 小标签 "工作流" -- 图标按钮 📤 🔄 -- 仅显示文件名 + 默认标记 -- 无说明文字 - -```jsx -
-
- 工作流 -
- - -
-
- -
-
- - 默认 - default_txt2img - -
-
-
-``` - -**高度对比**: -- 之前: ~350px -- 现在: ~150px(减少 57%) - ---- - -### **3. 表单间距 - 紧凑化** - -**之前**: -```css -.form-group { - margin-bottom: var(--spacing-md); /* 16px */ -} - -.form-control { - padding: 8px 12px; -} -``` - -**现在**: -```css -.form-group { - margin-bottom: var(--spacing-sm); /* 8px */ -} - -.form-control { - padding: 6px 10px; - font-size: 0.85rem; -} -``` - -**节省空间**: 每个字段减少 8px - ---- - -### **4. 删除冗余元素** - -#### **移除的装饰** -- ❌ 卡片阴影(mode-card box-shadow) -- ❌ 悬停动画(transform: translateY) -- ❌ 渐变背景 -- ❌ 大图标(2.5rem → 0.9rem) -- ❌ 详细说明文字 -- ❌ 节点数和文件大小 -- ❌ 提示列表 - -#### **保留的核心** -- ✅ 功能按钮 -- ✅ 必要标签 -- ✅ 状态指示(active badge) -- ✅ 基本悬停反馈 - ---- - -## 📐 尺寸规范 - -### **间距系统** - -| 元素 | 之前 | 现在 | 减少 | -|------|------|------|------| -| 容器 padding | 16px | 12px | -25% | -| 字段间距 | 16px | 8px | -50% | -| 按钮 padding | 8px 16px | 4px 12px | -40% | -| 卡片间隙 | 16px | 2px | -87% | - -### **字体大小** - -| 元素 | 之前 | 现在 | -|------|------|------| -| 标题 | 1.1rem | 0.75rem (uppercase) | -| 标签 | 0.85rem | 0.75rem | -| 输入框 | 0.9rem | 0.85rem | -| 按钮 | 0.85rem | 0.8rem | - -### **组件高度** - -| 组件 | 之前 | 现在 | 减少 | -|------|------|------|------| -| 模式切换 | 120px × 2 | 28px | -88% | -| 工作流管理器 | 350px | 150px | -57% | -| 表单区域 | ~600px | ~450px | -25% | -| **总计** | **~1100px** | **~650px** | **-41%** | - ---- - -## 🎨 视觉风格 - -### **颜色使用** - -```css -/* 背景色层次 */ ---color-bg-primary: /* 输入框背景 */ ---color-bg-secondary: /* 工作流项背景 */ ---color-bg-tertiary: /* Toggle/Manager 背景 */ ---color-bg-elevated: /* Active/Hover 状态 */ - -/* 文字颜色 */ ---color-text-primary: /* 主要文字 */ ---color-text-secondary: /* 标签/次要 */ ---color-text-muted: /* 提示/禁用 */ -``` - -### **圆角规范** - -```css -border-radius: 4px; /* 按钮、输入框 */ -border-radius: 6px; /* 容器、Toggle */ -border-radius: 3px; /* Badge */ -``` - -### **过渡动画** - -```css -transition: all 0.15s ease; /* 快速响应 */ -``` - ---- - -## 💡 设计原则应用 - -### **1. 高密度** - -✅ 减少 padding/margin -✅ 缩小字体 -✅ 去除装饰性空白 - -**结果**: 同屏显示更多信息 - ---- - -### **2. 现代化** - -✅ Pill Toggle(类似 macOS/iOS) -✅ 图标按钮(简洁直观) -✅ 扁平化设计(无渐变/阴影) - -**参考**: Linear、Vercel Dashboard - ---- - -### **3. 克制优雅** - -✅ 只保留必要元素 -✅ 统一的设计语言 -✅ 克制的色彩使用 - -**理念**: Less is More - ---- - -### **4. 高效实用** - -✅ 一眼看到关键信息 -✅ 快速操作(点击即切换) -✅ 减少认知负担 - -**目标**: 最小化操作步骤 - ---- - -## 📱 响应式考虑 - -虽然侧边栏宽度固定,但仍需保证: - -✅ 无横向滚动 -✅ 内容自适应宽度 -✅ 小屏幕下仍可操作 - -**实现**: -```css -.api-config-container { - max-width: 100%; - overflow-x: hidden; -} - -.form-control { - width: 100%; - box-sizing: border-box; -} -``` - ---- - -## 🎯 用户体验提升 - -### **操作效率** - -| 任务 | 之前 | 现在 | 提升 | -|------|------|------|------| -| 切换模式 | 点击大卡片 | 点击按钮 | 更快 | -| 上传工作流 | 找按钮+阅读说明 | 直接点图标 | 更直观 | -| 查看工作流 | 滚动长列表 | 紧凑列表 | 更快 | -| 填写表单 | 大间距需滚动 | 紧凑少滚动 | 更高效 | - -### **视觉清晰度** - -- ✅ 减少视觉噪音 -- ✅ 突出关键操作 -- ✅ 统一的设计语言 - -### **学习成本** - -- ✅ 符合常见模式(Pill Toggle) -- ✅ 图标直观易懂 -- ✅ 无需阅读说明 - ---- - -## 🔧 技术实现 - -### **CSS 架构** - -``` -ApiConfig.css -├── 基础样式(已有) -│ ├── .api-config-container -│ ├── .config-tabs -│ ├── .form-group -│ └── .btn -│ -└── Compact Modern(新增) - ├── .mode-toggle - ├── .mode-btn - ├── .workflow-manager-compact - ├── .btn-icon - └── .workflow-item-compact -``` - -### **组件结构** - -```jsx -ApiConfig.jsx -├── Config Tabs(Pill 标签) -├── Profile Manager(配置管理) -├── Mode Toggle(模式切换)← 新增 -├── Form Section(表单) -│ ├── Local Config(本地配置) -│ └── Cloud Config(云端配置) -└── Workflow Manager(工作流)← 精简 -``` - ---- - -## 📊 性能优化 - -### **渲染性能** - -- ✅ 减少 DOM 节点(从 ~80 个 → ~40 个) -- ✅ 简化 CSS(去除复杂选择器) -- ✅ 减少动画(仅保留必要的 transition) - -### **加载速度** - -- ✅ CSS 文件减小(-155 行) -- ✅ 组件代码简化(-40 行) - ---- - -## ✅ 验收标准 - -### **视觉检查** -- [ ] 模式切换为 Pill 样式 -- [ ] 工作流管理器紧凑(<200px) -- [ ] 无多余装饰元素 -- [ ] 字体大小统一(0.75-0.85rem) - -### **功能检查** -- [ ] 模式切换正常工作 -- [ ] 工作流上传/删除正常 -- [ ] 表单输入正常 -- [ ] 无横向滚动 - -### **响应式检查** -- [ ] 不同宽度下无溢出 -- [ ] 所有元素可见且可操作 - ---- - -## 🎊 总结 - -### **精简成果** - -- ✅ 垂直空间减少 **41%** -- ✅ DOM 节点减少 **50%** -- ✅ CSS 代码减少 **155 行** -- ✅ 视觉复杂度降低 **60%** - -### **设计哲学** - -> "在有限的空间内,提供最大的价值和最好的体验。" - -**关键词**: 紧凑 · 现代 · 高效 · 克制 · 精致 · 专业 - ---- - -**当前状态**: 🟢 **Compact Modern Design 已实现** diff --git a/backend/Dockerfile b/backend/Dockerfile index beeccf3..581a6e6 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -12,7 +12,10 @@ ENV PYTHONDONTWRITEBYTECODE=1 COPY requirements.txt . # 安装依赖 -RUN pip install --no-cache-dir -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt +RUN pip install --no-cache-dir -r requirements.txt + +# 安装 Pillow(使用阿里云镜像源) +RUN pip install --no-cache-dir Pillow -i https://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com || echo "Pillow installation failed, will install manually" # 复制所有代码 COPY . . diff --git a/backend/api/route.py b/backend/api/route.py index 6eb8759..53d2fa7 100644 --- a/backend/api/route.py +++ b/backend/api/route.py @@ -1,5 +1,5 @@ from fastapi import APIRouter -from .routes import presetsRoute, chatsRoute, worldbooksRoute, apiConfigRoute +from .routes import presetsRoute, chatsRoute, worldbooksRoute, apiConfigRoute, charactersRoute from utils.file_utils import get_all_roles_and_chats from core.config import settings from pathlib import Path @@ -11,6 +11,7 @@ router.include_router(presetsRoute.router) router.include_router(chatsRoute.router) router.include_router(worldbooksRoute.router) router.include_router(apiConfigRoute.router) +router.include_router(charactersRoute.router) # 保留原有的其他路由 diff --git a/backend/api/routes/chatsRoute.py b/backend/api/routes/chatsRoute.py index 855b3da..60295ce 100644 --- a/backend/api/routes/chatsRoute.py +++ b/backend/api/routes/chatsRoute.py @@ -1,56 +1,112 @@ from fastapi import APIRouter, HTTPException, status -# TODO: 实现 ChatService 来替代旧的 ChatHistory 逻辑 -# from services.chat_service import ChatService +from pathlib import Path +try: + from backend.services.chat_service import ChatService + from backend.core.config import settings +except ImportError: + # Docker环境:直接从当前目录导入 + from services.chat_service import ChatService + from core.config import settings router = APIRouter(prefix="/chat", tags=["chat"]) +# 初始化聊天服务 +data_path = Path(settings.DATA_PATH) if hasattr(settings, 'DATA_PATH') else Path("data") +chat_service = ChatService(data_path) + @router.get("", response_model=dict) async def list_all_chats(): """获取所有角色的所有聊天列表""" - # return await ChatService.list_all_chats() - return {"chats": []} + return chat_service.list_all_chats() + +@router.get("/{role_name}") +async def list_role_chats(role_name: str): + """获取指定角色的所有聊天列表""" + try: + all_chats = chat_service.list_all_chats() + # 从所有聊天中筛选出该角色的聊天 + role_chats = all_chats.get(role_name, []) + return role_chats + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) @router.get("/{role_name}/{chat_name}") async def get_chat(role_name: str, chat_name: str): """获取指定聊天的完整内容""" - raise HTTPException(status_code=501, detail="Not Implemented") + try: + return chat_service.get_chat(role_name, chat_name) + except FileNotFoundError as e: + raise HTTPException(status_code=404, detail=str(e)) @router.post("/{role_name}", status_code=status.HTTP_201_CREATED) -async def create_chat(role_name: str, chat_name: str, metadata: dict = None): +async def create_chat(role_name: str, chat_data: dict): """创建新聊天""" - raise HTTPException(status_code=501, detail="Not Implemented") + try: + chat_name = chat_data.get("chat_name", "新聊天") + metadata = chat_data.get("metadata", {}) + return chat_service.create_chat(role_name, chat_name, metadata) + except FileExistsError as e: + raise HTTPException(status_code=400, detail=str(e)) @router.put("/{role_name}/{chat_name}") async def update_chat(role_name: str, chat_name: str, update_data: dict): """更新聊天元数据""" + # TODO: 实现更新聊天元数据功能 raise HTTPException(status_code=501, detail="Not Implemented") @router.delete("/{role_name}/{chat_name}") async def delete_chat(role_name: str, chat_name: str): """删除指定聊天""" + # TODO: 实现删除聊天功能 raise HTTPException(status_code=501, detail="Not Implemented") @router.get("/{role_name}/{chat_name}/messages") async def list_messages(role_name: str, chat_name: str): """获取聊天的所有消息""" - raise HTTPException(status_code=501, detail="Not Implemented") + try: + chat_data = chat_service.get_chat(role_name, chat_name) + return {"messages": chat_data["messages"]} + except FileNotFoundError as e: + raise HTTPException(status_code=404, detail=str(e)) @router.get("/{role_name}/{chat_name}/messages/{floor}") async def get_message(role_name: str, chat_name: str, floor: int): """获取指定楼层的消息""" - raise HTTPException(status_code=501, detail="Not Implemented") + try: + chat_data = chat_service.get_chat(role_name, chat_name) + for msg in chat_data["messages"]: + if msg.get("floor") == floor: + return msg + raise HTTPException(status_code=404, detail=f"Message at floor {floor} not found") + except FileNotFoundError as e: + raise HTTPException(status_code=404, detail=str(e)) @router.post("/{role_name}/{chat_name}/messages", status_code=status.HTTP_201_CREATED) async def add_message(role_name: str, chat_name: str, message_data: dict): """向聊天添加新消息""" - raise HTTPException(status_code=501, detail="Not Implemented") + try: + return chat_service.add_message(role_name, chat_name, message_data) + except FileNotFoundError as e: + raise HTTPException(status_code=404, detail=str(e)) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) @router.put("/{role_name}/{chat_name}/messages/{floor}") async def update_message(role_name: str, chat_name: str, floor: int, update_data: dict): """更新指定楼层的消息""" - raise HTTPException(status_code=501, detail="Not Implemented") + try: + return chat_service.update_message(role_name, chat_name, floor, update_data) + except FileNotFoundError as e: + raise HTTPException(status_code=404, detail=str(e)) + except ValueError as e: + raise HTTPException(status_code=404, detail=str(e)) @router.delete("/{role_name}/{chat_name}/messages/{floor}") async def delete_message(role_name: str, chat_name: str, floor: int): """删除指定楼层的消息""" - raise HTTPException(status_code=501, detail="Not Implemented") + try: + return chat_service.delete_message(role_name, chat_name, floor) + except FileNotFoundError as e: + raise HTTPException(status_code=404, detail=str(e)) + except ValueError as e: + raise HTTPException(status_code=404, detail=str(e)) diff --git a/backend/core/config.py b/backend/core/config.py index 8f3f659..8c5071a 100644 --- a/backend/core/config.py +++ b/backend/core/config.py @@ -54,6 +54,12 @@ class Settings: # ComfyUI 工作流目录 COMFYUI_WORKFLOWS_PATH = DATA_PATH / "comfyui_workflows" + + # 角色卡目录 + CHARACTERS_PATH = DATA_PATH / "characters" + + # 图片资源目录 + IMAGES_PATH = DATA_PATH / "images" def ensure_directories(self): """确保所有配置的目录存在,如果不存在则创建""" @@ -64,6 +70,8 @@ class Settings: self.CHAT_PATH, self.TEMP_PATH, self.COMFYUI_WORKFLOWS_PATH, + self.CHARACTERS_PATH, + self.IMAGES_PATH, ] for directory in directories: directory.mkdir(parents=True, exist_ok=True) diff --git a/backend/main.py b/backend/main.py index 0bd088a..a685786 100644 --- a/backend/main.py +++ b/backend/main.py @@ -17,12 +17,22 @@ for logger_name in ['uvicorn', 'uvicorn.access', 'fastapi']: # backend/app/main.py from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware try: from backend.api.route import router except ImportError: from api.route import router app = FastAPI(title="LLM Workflow Engine") +# 配置CORS +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # 开发环境允许所有来源,生产环境应该指定具体域名 + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + # 注册路由 app.include_router(router, prefix="/api") diff --git a/data/characters/defult.png b/data/characters/defult.png new file mode 100644 index 0000000000000000000000000000000000000000..f27bf75c8c8ad8627d3447247add869f8febc607 GIT binary patch literal 59445 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

JW%JGm1K;wzs?9HW-<->6 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 G4ErCe46O?Q literal 0 HcmV?d00001 diff --git a/data/chat/testRole1/111.jsonl b/data/chat/testRole1/111.jsonl deleted file mode 100644 index 30358dd..0000000 --- a/data/chat/testRole1/111.jsonl +++ /dev/null @@ -1,5 +0,0 @@ -{"user_name": "User", "character_name": "AI Dungeon Master", "integrity": "uuid-001", "chat_id_hash": "hash-001", "note_prompt": "你是一个经验丰富的D&D地下城主。", "note_interval": 0, "note_position": 0, "note_depth": 0, "note_role": 0, "extensions": {}, "timedWorldInfo": {}, "variables": {}, "tainted": false, "lastInContextMessageId": -1} -{"name": "User", "is_user": true, "is_system": false, "floor": 0, "send_date": "1700000000000", "mes": "你好,我想开始一个新的冒险。", "extra": {}, "swipes": [], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []} -{"name": "AI Dungeon Master", "is_user": false, "is_system": false, "floor": 1, "send_date": "1700000001000", "mes": "欢迎,冒险者。请告诉我你想扮演什么角色?", "extra": {"api": "openai", "model": "gpt-4"}, "swipes": ["欢迎,冒险者。请告诉我你想扮演什么角色?", "你好,旅行者。在这个奇幻世界中,你是谁?"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "api": "openai", "model": "gpt-4", "reasoning": null, "reasoning_duration": null, "reasoning_signature": null, "time_to_first_token": null, "bias": null} -{"name": "User", "is_user": true, "is_system": false, "floor": 2, "send_date": "1700000002000", "mes": "我想成为一名人类战士。", "extra": {}, "swipes": [], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []} -{"name": "AI Dungeon Master", "is_user": false, "is_system": false, "floor": 3, "send_date": "1700000003000", "mes": "很好。你站在喧闹的酒馆门口,手里握着一把旧长剑。你打算做什么?", "extra": {"api": "openai", "model": "gpt-4"}, "swipes": ["很好。你站在喧闹的酒馆门口,手里握着一把旧长剑。你打算做什么?", "明白了。作为一名人类战士,你正身处繁华的市集广场。你的下一步行动是?"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "api": "openai", "model": "gpt-4", "reasoning": null, "reasoning_duration": null, "reasoning_signature": null, "time_to_first_token": null, "bias": null} diff --git a/data/chat/testRole1/11111.jsonl b/data/chat/testRole1/11111.jsonl deleted file mode 100644 index 21bf633..0000000 --- a/data/chat/testRole1/11111.jsonl +++ /dev/null @@ -1,5 +0,0 @@ -{"user_name": "Commander", "character_name": "XCOM AI", "integrity": "uuid-003", "chat_id_hash": "hash-003", "note_prompt": "你是一名XCOM基地的中央AI,负责协助指挥官管理外星威胁。", "note_interval": 0, "note_position": 0, "note_depth": 0, "note_role": 0, "extensions": {}, "timedWorldInfo": {}, "variables": {}, "tainted": false, "lastInContextMessageId": -1} -{"name": "Commander", "is_user": true, "is_system": false, "floor": 0, "send_date": "1700000000000", "mes": "报告当前的外星活动情况。", "extra": {}, "swipes": [], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []} -{"name": "XCOM AI", "is_user": false, "is_system": false, "floor": 1, "send_date": "1700000001000", "mes": "指挥官,卫星侦测到在南美洲丛林中有高能反应。可能是外星着陆舱。", "extra": {"api": "openai", "model": "gpt-4"}, "swipes": ["指挥官,卫星侦测到在南美洲丛林中有高能反应。可能是外星着陆舱。", "警报。我们在非洲检测到异常信号,疑似外星绑架行动正在进行。"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "api": "openai", "model": "gpt-4", "reasoning": null, "reasoning_duration": null, "reasoning_signature": null, "time_to_first_token": null, "bias": null} -{"name": "Commander", "is_user": true, "is_system": false, "floor": 2, "send_date": "1700000002000", "mes": "派遣布拉德福上尉带领一个小队去调查。", "extra": {}, "swipes": [], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []} -{"name": "XCOM AI", "is_user": false, "is_system": false, "floor": 3, "send_date": "1700000003000", "mes": "指令已确认。天火运输机正在起飞。预计到达时间:20分钟。", "extra": {"api": "openai", "model": "gpt-4"}, "swipes": ["指令已确认。天火运输机正在起飞。预计到达时间:20分钟。", "收到。正在部署天火运输机。布拉德福上尉已登机。"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "api": "openai", "model": "gpt-4", "reasoning": null, "reasoning_duration": null, "reasoning_signature": null, "time_to_first_token": null, "bias": null} diff --git a/data/chat/testRole2/222.jsonl b/data/chat/testRole2/222.jsonl deleted file mode 100644 index c721f79..0000000 --- a/data/chat/testRole2/222.jsonl +++ /dev/null @@ -1,5 +0,0 @@ -{"user_name": "Player", "character_name": "Game Master", "integrity": "uuid-002", "chat_id_hash": "hash-002", "note_prompt": "场景:赛博朋克风格的未来城市。", "note_interval": 0, "note_position": 0, "note_depth": 0, "note_role": 0, "extensions": {}, "timedWorldInfo": {}, "variables": {}, "tainted": false, "lastInContextMessageId": -1} -{"name": "Player", "is_user": true, "is_system": false, "floor": 0, "send_date": "1700000000000", "mes": "我检查我的义体状态。", "extra": {}, "swipes": [], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []} -{"name": "Game Master", "is_user": false, "is_system": false, "floor": 1, "send_date": "1700000001000", "mes": "你的视觉义眼显示系统正常,但左臂的伺服电机发出轻微的嗡嗡声,似乎需要维护。", "extra": {"api": "openai", "model": "gpt-4"}, "swipes": ["你的视觉义眼显示系统正常,但左臂的伺服电机发出轻微的嗡嗡声,似乎需要维护。", "系统自检完成。你的神经接口连接稳定,但义体排异反应指数略有上升。"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "api": "openai", "model": "gpt-4", "reasoning": null, "reasoning_duration": null, "reasoning_signature": null, "time_to_first_token": null, "bias": null} -{"name": "Player", "is_user": true, "is_system": false, "floor": 2, "send_date": "1700000002000", "mes": "我联系我的黑客朋友,问他知不知道哪里有靠谱的义体医生。", "extra": {}, "swipes": [], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []} -{"name": "Game Master", "is_user": false, "is_system": false, "floor": 3, "send_date": "1700000003000", "mes": "你的朋友回复说:'去下城区的老维克那里,虽然他的店看起来很破,但他手艺没得说。'", "extra": {"api": "openai", "model": "gpt-4"}, "swipes": ["你的朋友回复说:'去下城区的老维克那里,虽然他的店看起来很破,但他手艺没得说。'", "通讯接通。你的朋友告诉你:'别去连锁店,去太平间后巷找'扳手',他收费公道。'"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "api": "openai", "model": "gpt-4", "reasoning": null, "reasoning_duration": null, "reasoning_signature": null, "time_to_first_token": null, "bias": null} diff --git a/data/chat/testRole3/333.jsonl b/data/chat/testRole3/333.jsonl deleted file mode 100644 index 0e8b60c..0000000 --- a/data/chat/testRole3/333.jsonl +++ /dev/null @@ -1,5 +0,0 @@ -{"user_name": "Player", "character_name": "Narrator", "integrity": "uuid-004", "chat_id_hash": "hash-004", "note_prompt": "这是一个文字冒险游戏,你需要描述场景并等待玩家输入。", "note_interval": 0, "note_position": 0, "note_depth": 0, "note_role": 0, "extensions": {}, "timedWorldInfo": {}, "variables": {}, "tainted": false, "lastInContextMessageId": -1} -{"name": "Player", "is_user": true, "is_system": false, "floor": 0, "send_date": "1700000000000", "mes": "开始游戏。", "extra": {}, "swipes": [], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []} -{"name": "Narrator", "is_user": false, "is_system": false, "floor": 1, "send_date": "1700000001000", "mes": "你醒来时发现自己躺在一片陌生的森林里,四周弥漫着浓雾。你身边有一个背包和一把生锈的匕首。", "extra": {"api": "openai", "model": "gpt-4"}, "swipes": ["你醒来时发现自己躺在一片陌生的森林里,四周弥漫着浓雾。你身边有一个背包和一把生锈的匕首。", "当你睁开眼睛,发现自己身处一艘废弃的飞船中,应急灯闪烁着红光。你手里紧握着一个数据盘。"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "api": "openai", "model": "gpt-4", "reasoning": null, "reasoning_duration": null, "reasoning_signature": null, "time_to_first_token": null, "bias": null} -{"name": "Player", "is_user": true, "is_system": false, "floor": 2, "send_date": "1700000002000", "mes": "我打开背包看看里面有什么。", "extra": {}, "swipes": [], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": []} -{"name": "Narrator", "is_user": false, "is_system": false, "floor": 3, "send_date": "1700000003000", "mes": "背包里有一块干硬的面包,一个水壶(里面还有半壶水),以及一张画着奇怪符号的羊皮纸。", "extra": {"api": "openai", "model": "gpt-4"}, "swipes": ["背包里有一块干硬的面包,一个水壶(里面还有半壶水),以及一张画着奇怪符号的羊皮纸。", "背包里只有一把激光手枪,能量槽仅剩10%。还有一张写着'不要相信AI'的纸条。"], "swipe_id": 0, "force_avatar": null, "variables": [], "variables_initialized": [], "is_ejs_processed": [], "api": "openai", "model": "gpt-4", "reasoning": null, "reasoning_duration": null, "reasoning_signature": null, "time_to_first_token": null, "bias": null} diff --git a/docker-compose.yml b/docker-compose.yml index c94e46b..647ee49 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,6 +7,8 @@ services: dockerfile: Dockerfile container_name: llm-backend command: uvicorn main:app --host 0.0.0.0 --port 8000 --reload + ports: + - "23337:8000" volumes: - ./backend:/app - ./data:/app/data diff --git a/frontend/COLOR_SCHEME_OPTIMIZATION.md b/frontend/COLOR_SCHEME_OPTIMIZATION.md deleted file mode 100644 index 45edd4b..0000000 --- a/frontend/COLOR_SCHEME_OPTIMIZATION.md +++ /dev/null @@ -1,388 +0,0 @@ -# 🎨 成熟配色方案优化 - 自然舒适的视觉体验 - -## ✅ 完成的优化 - -参考 **Material Design**、**VS Code**、**GitHub Dark** 等成熟网站的配色方案,优化了整体色彩系统。 - ---- - -## 🎯 设计理念 - -### 核心原则 - -1. **避免纯黑纯白** - 使用深灰/暖白,减少视觉疲劳 -2. **层次分明** - 通过不同灰度建立清晰的视觉层级 -3. **柔和对比** - 文字与背景保持舒适对比度(4.5:1 以上) -4. **不抢眼** - 低饱和度、低调优雅的色彩 -5. **自然舒适** - 长时间使用不刺眼、不疲劳 - ---- - -## 📊 配色方案对比 - -### 暗色主题(Dark Theme) - -#### 之前 - 偏蓝的深色 -```css ---color-bg-primary: #0f1115; /* 深蓝黑 */ ---color-bg-secondary: #161920; /* 中蓝黑 */ ---color-bg-tertiary: #1c1f27; /* 浅蓝黑 */ ---color-text-primary: #e8eaed; /* 亮白 */ ---color-accent: #6d8cff; /* 亮蓝色 */ -``` - -**问题**: -- ❌ 偏蓝调,不够中性 -- ❌ 文字过亮,对比度过高 -- ❌ 强调色过于鲜艳 - ---- - -#### 之后 - Material Design 标准深灰 -```css ---color-bg-primary: #121212; /* 深灰黑 - Material Design 标准 */ ---color-bg-secondary: #1e1e1e; /* 中灰黑 */ ---color-bg-tertiary: #2d2d2d; /* 浅灰黑 */ ---color-text-primary: #e0e0e0; /* 柔白 - 降低亮度 */ ---color-accent: #8b9cf7; /* 柔和蓝紫 - 降低饱和度 */ -``` - -**优势**: -- ✅ 中性灰色调,更专业 -- ✅ 文字柔和,对比度适中(符合 WCAG 4.5:1) -- ✅ 强调色优雅,不抢眼 - ---- - -### 亮色主题(Light Theme) - -#### 之前 - 冷白色 -```css ---color-bg-primary: #fafbfc; /* 冷白 */ ---color-text-primary: #1a1d21; /* 近黑 */ ---color-accent: #5b7fff; /* 亮蓝色 */ -``` - -**问题**: -- ❌ 背景偏冷 -- ❌ 文字过深,对比度过高 -- ❌ 强调色与暗色主题不一致 - ---- - -#### 之后 - 暖白色 -```css ---color-bg-primary: #fafafa; /* 暖白 - 更柔和 */ ---color-text-primary: #2c2c2c; /* 深灰 - 非纯黑 */ ---color-accent: #7a8be6; /* 与暗色主题一致 */ -``` - -**优势**: -- ✅ 暖白色调,更舒适 -- ✅ 文字为深灰而非纯黑,减少刺眼感 -- ✅ 强调色与暗色主题保持一致 - ---- - -## 🎨 完整配色体系 - -### 暗色主题配色表 - -| 用途 | 颜色值 | 说明 | -|------|--------|------| -| **背景层** | | | -| 主背景 | `#121212` | Material Design 标准深灰黑 | -| 次级背景 | `#1e1e1e` | 卡片、面板背景 | -| 三级背景 | `#2d2d2d` | 输入框、按钮背景 | -| 浮层背景 | `#252525` | 下拉菜单、弹窗 | -| 微妙背景 | `#1a1a1a` | 渐变、装饰 | -| **文字层** | | | -| 主文字 | `#e0e0e0` | 正文、标题(柔白) | -| 次要文字 | `#9e9e9e` | 副标题、说明(中灰) | -| 弱化文字 | `#757575` | 占位符、禁用(深灰) | -| **边框层** | | | -| 边框 | `#333333` | 分隔线、边框 | -| 浅色边框 | `#2a2a2a` | 轻微分隔 | -| 聚焦边框 | `#404040` | 输入框聚焦 | -| **强调色** | | | -| 主强调色 | `#8b9cf7` | 链接、按钮、激活状态 | -| 悬停 | `#9dabff` | 鼠标悬停 | -| 激活 | `#7a8be6` | 点击激活 | -| 浅色强调 | `rgba(139, 156, 247, 0.1)` | 背景高亮 | -| 极浅强调 | `rgba(139, 156, 247, 0.05)` | 微妙高亮 | - ---- - -### 亮色主题配色表 - -| 用途 | 颜色值 | 说明 | -|------|--------|------| -| **背景层** | | | -| 主背景 | `#fafafa` | 暖白色,非纯白 | -| 次级背景 | `#ffffff` | 纯白(卡片、面板) | -| 三级背景 | `#f5f5f5` | 浅灰色 | -| 浮层背景 | `#ffffff` | 下拉菜单、弹窗 | -| 微妙背景 | `#f0f0f0` | 渐变、装饰 | -| **文字层** | | | -| 主文字 | `#2c2c2c` | 正文、标题(深灰) | -| 次要文字 | `#666666` | 副标题、说明(中灰) | -| 弱化文字 | `#999999` | 占位符、禁用(浅灰) | -| **边框层** | | | -| 边框 | `#e0e0e0` | 分隔线、边框 | -| 浅色边框 | `#ebebeb` | 轻微分隔 | -| 聚焦边框 | `#d0d0d0` | 输入框聚焦 | -| **强调色** | | | -| 主强调色 | `#7a8be6` | 与暗色主题一致 | -| 悬停 | `#6a7bd6` | 鼠标悬停 | -| 激活 | `#5a6bc6` | 点击激活 | -| 浅色强调 | `rgba(122, 139, 230, 0.08)` | 背景高亮 | -| 极浅强调 | `rgba(122, 139, 230, 0.04)` | 微妙高亮 | - ---- - -## 🔍 设计细节 - -### 1. 为什么不用纯黑(#000000)? - -**科学依据**: -- OLED 屏幕显示纯黑时像素完全关闭,导致"拖影"效应 -- 纯黑与亮色文字对比度过高,造成视觉疲劳 -- 深灰(#121212)能更好地表达阴影和层次感 - -**Material Design 官方建议**: -> "Use dark gray (#121212) instead of pure black for surfaces. This allows shadows to be visible and creates depth." - ---- - -### 2. 为什么不用纯白(#ffffff)作为文字? - -**可读性研究**: -- 纯白文字在深色背景上会产生"光晕"效应 -- 柔白(#e0e0e0)降低亮度,减少眼睛疲劳 -- 长文本阅读时,柔白比纯白更舒适 - -**WCAG 对比度要求**: -- 普通文本:至少 4.5:1 -- 大号文本:至少 3:1 - -当前配色: -- `#e0e0e0` on `#121212` = **13.2:1** ✅ 远超标准 -- `#9e9e9e` on `#121212` = **7.8:1** ✅ 符合标准 - ---- - -### 3. 强调色选择逻辑 - -**之前**: `#6d8cff` (亮蓝色) -- 饱和度高,过于鲜艳 -- 在深色背景上显得突兀 - -**之后**: `#8b9cf7` (柔和蓝紫) -- 降低饱和度,更优雅 -- 带紫色调,更有质感 -- 与深色背景融合更好 - -**灵感来源**: -- Material Design 3 的 Primary Color -- VS Code 的链接颜色 -- GitHub Dark 的强调色 - ---- - -### 4. 阴影优化 - -**之前**: 多层阴影叠加 -```css ---shadow-md: 0 4px 8px rgba(0, 0, 0, 0.25), - 0 2px 4px rgba(0, 0, 0, 0.2); -``` - -**之后**: 单层阴影 -```css ---shadow-md: 0 4px 8px rgba(0, 0, 0, 0.4); -``` - -**原因**: -- 简化渲染,提升性能 -- 单层阴影更清晰、更现代 -- 适当提高透明度,确保在深色背景上可见 - ---- - -## 📈 视觉效果对比 - -### 暗色主题 - -**之前**: -``` -┌─────────────────────────────┐ -│ 深蓝黑背景 (#0f1115) │ -│ │ -│ 亮白文字 (#e8eaed) │ ← 对比度过高 -│ 亮蓝强调 (#6d8cff) │ ← 过于鲜艳 -└─────────────────────────────┘ -``` - -**之后**: -``` -┌─────────────────────────────┐ -│ 深灰黑背景 (#121212) │ -│ │ -│ 柔白文字 (#e0e0e0) │ ← 柔和舒适 -│ 蓝紫强调 (#8b9cf7) │ ← 优雅低调 -└─────────────────────────────┘ -``` - ---- - -### 亮色主题 - -**之前**: -``` -┌─────────────────────────────┐ -│ 冷白背景 (#fafbfc) │ -│ │ -│ 近黑文字 (#1a1d21) │ ← 对比度过高 -│ 亮蓝强调 (#5b7fff) │ ← 刺眼 -└─────────────────────────────┘ -``` - -**之后**: -``` -┌─────────────────────────────┐ -│ 暖白背景 (#fafafa) │ -│ │ -│ 深灰文字 (#2c2c2c) │ ← 柔和自然 -│ 蓝紫强调 (#7a8be6) │ ← 优雅统一 -└─────────────────────────────┘ -``` - ---- - -## 🎯 参考资源 - -### Material Design 暗色主题 -- 官方文档: https://material.io/design/color/dark-theme.html -- 推荐背景: `#121212` -- 推荐文字: `#e0e0e0` (主要), `#9e9e9e` (次要) - -### VS Code Dark+ -- 背景: `#1e1e1e` -- 文字: `#d4d4d4` -- 强调: `#569cd6` (蓝色) - -### GitHub Dark -- 背景: `#0d1117` -- 卡片: `#161b22` -- 边框: `#30363d` -- 文字: `#c9d1d9` - -### Apple Human Interface Guidelines -- 推荐使用系统灰度色板 -- 避免纯黑纯白 -- 保持足够的对比度 - ---- - -## ✅ 验证清单 - -### 暗色主题 -- [x] 背景使用深灰而非纯黑 (#121212) -- [x] 文字使用柔白而非纯白 (#e0e0e0) -- [x] 强调色降低饱和度 (#8b9cf7) -- [x] 边框颜色适中 (#333333) -- [x] 阴影适度增强 (0.3-0.55) -- [x] 对比度符合 WCAG 标准 - -### 亮色主题 -- [x] 背景使用暖白而非冷白 (#fafafa) -- [x] 文字使用深灰而非纯黑 (#2c2c2c) -- [x] 强调色与暗色主题一致 (#7a8be6) -- [x] 边框颜色柔和 (#e0e0e0) -- [x] 阴影非常轻微 (0.04-0.09) -- [x] 整体不刺眼 - -### 整体效果 -- [x] 暗色主题不偏蓝,中性灰色 -- [x] 亮色主题温暖舒适 -- [x] 两个主题强调色保持一致 -- [x] 所有颜色都不抢眼 -- [x] 长时间使用不疲劳 - ---- - -## 🎊 最终效果 - -### 用户体验提升 - -1. **更舒适的视觉** - - 无纯黑纯白的刺眼感 - - 柔和的对比度 - - 自然的色彩过渡 - -2. **更专业的印象** - - Material Design 标准配色 - - 中性灰色调 - - 优雅的强调色 - -3. **更好的可读性** - - 符合 WCAG 无障碍标准 - - 清晰的视觉层级 - - 适当的对比度 - -4. **更统一的风格** - - 暗色/亮色主题协调一致 - - 强调色保持统一 - - 整体不抢眼、不突兀 - ---- - -## 💡 使用建议 - -### 何时使用各层级背景 - -``` -主背景 (#121212 / #fafafa) - └─ 页面整体背景 - -次级背景 (#1e1e1e / #ffffff) - ├─ 侧边栏 - ├─ 卡片 - └─ 面板 - -三级背景 (#2d2d2d / #f5f5f5) - ├─ 输入框 - ├─ 按钮 - └─ 下拉选项 - -浮层背景 (#252525 / #ffffff) - ├─ 弹窗 - ├─ 下拉菜单 - └─ 工具提示 -``` - -### 何时使用各层级文字 - -``` -主文字 (#e0e0e0 / #2c2c2c) - ├─ 正文 - ├─ 标题 - └─ 重要信息 - -次要文字 (#9e9e9e / #666666) - ├─ 副标题 - ├─ 说明文字 - └─ 标签 - -弱化文字 (#757575 / #999999) - ├─ 占位符 - ├─ 禁用状态 - └─ 辅助信息 -``` - ---- - -**完成时间**: 2026-04-28 -**状态**: ✅ 成熟配色方案优化完成 -**参考标准**: Material Design、VS Code、GitHub Dark -**核心理念**: 自然、舒适、不抢眼 diff --git a/frontend/CSS_MIGRATION_COMPLETE.md b/frontend/CSS_MIGRATION_COMPLETE.md deleted file mode 100644 index 4443246..0000000 --- a/frontend/CSS_MIGRATION_COMPLETE.md +++ /dev/null @@ -1,443 +0,0 @@ -# 🎨 CSS 完整迁移报告 - -## ✅ 已完成的工作 - -### 1. **全局样式系统** - -#### 新增文件 -- ✅ `src/styles/variables.css` - 完整的 CSS 变量定义(深色/浅色主题) -- ✅ `src/styles/reset.css` - CSS Reset 和全局动画、布局样式 - -#### 核心特性 -```css -/* 深色主题(默认)*/ ---color-bg-primary: #0f1115; /* 优雅深色背景 */ ---color-accent: #6d8cff; /* 柔和蓝色强调色 */ ---radius-md: 12px; /* 精致圆角 */ ---shadow-lg: 多层阴影创造深度 */ ---transition-normal: 250ms cubic-bezier(...); /* 流畅动画 */ - -/* 浅色主题 */ ---color-bg-primary: #fafbfc; /* 明亮干净背景 */ ---color-accent: #5b7fff; /* 稍深的蓝色 */ -``` - ---- - -### 2. **主布局样式 (index.css)** - -#### 更新内容 -- ✅ `.app` - 应用容器,使用 flexbox 布局 -- ✅ `.main-container` - 主内容区域,包含三个面板 -- ✅ `.sidebar-left` - 左侧边栏(20% 宽度) -- ✅ `.chat-area` - 中间聊天区域(60% 宽度),带渐变背景 -- ✅ `.sidebar-right` - 右侧边栏(20% 宽度) -- ✅ 自定义滚动条样式(webkit) -- ✅ 主题切换过渡动画 - -#### 关键改进 -```css -/* 之前 */ -.sidebar-left { - width: 22.5%; - background-color: #ffffff; -} - -/* 之后 */ -.sidebar-left { - flex: 0 0 20%; - background-color: var(--color-bg-secondary); - border-right: 1px solid var(--color-border); - box-shadow: var(--shadow-xs); -} - -/* 聊天区域添加渐变背景 */ -.chat-area::before { - content: ''; - position: absolute; - background: - radial-gradient(circle at 20% 30%, rgba(109, 140, 255, 0.04) 0%, transparent 50%), - radial-gradient(circle at 80% 70%, rgba(109, 140, 255, 0.03) 0%, transparent 50%), - linear-gradient(180deg, var(--color-bg-primary) 0%, var(--color-bg-subtle) 100%); - pointer-events: none; - z-index: 0; -} -``` - ---- - -### 3. **TopBar 样式 (TopBar.css)** - -#### 完全重构 -- ✅ `.toolbar` - 顶部工具栏,56px 高度,毛玻璃效果 -- ✅ `.toolbar-icon` - 工具栏图标按钮 -- ✅ `.status-badge` - 状态徽章(角色、模型、预设、世界书) -- ✅ `.action-btn` - 操作按钮(设置、拓展、主题切换) -- ✅ `.theme-toggle` - 主题切换按钮特殊样式 -- ✅ `.panel-overlay` - 弹出面板遮罩层 -- ✅ `.panel-content` - 弹出面板内容 - -#### 视觉对比 - -**之前**: -```css -.toolbar { - height: 50px; - background-color: #fff; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); -} -``` - -**之后**: -```css -.toolbar { - height: 56px; - background-color: var(--color-bg-secondary); - border-bottom: 1px solid var(--color-border); - box-shadow: var(--shadow-md); - backdrop-filter: blur(10px); -} -``` - ---- - -### 4. **SideBarLeft 样式 (SideBarLeft.css)** - -#### 完全重构 -- ✅ `.sidebar-tabs` - 标签页容器 -- ✅ `.tab-button` - 标签按钮,底部边框激活指示器 -- ✅ `.sidebar-content` - 侧边栏内容区域 -- ✅ `.tab-placeholder` - 空状态占位符 - -#### 关键改进 -```css -/* 之前 */ -.tab-button.active::after { - content: ''; - position: absolute; - bottom: 0; - height: 3px; - background-color: #4a90e2; -} - -/* 之后 */ -.tab-button.active { - color: var(--color-accent); - border-bottom-color: var(--color-accent); - background: var(--color-accent-ultra-light); -} -``` - ---- - -### 5. **SideBarRight 样式 (SideBarRight.css)** - -#### 完全重构 -- ✅ `.sidebar-tabs` - 标签页容器 -- ✅ `.tab-button` - 标签按钮 -- ✅ `.sidebar-content` - 侧边栏内容区域 -- ✅ `.panel-section` - 面板分区(支持双标签) -- ✅ `.panel-section.has-divider` - 分隔线样式 -- ✅ `.tab-placeholder` - 空状态占位符 - -#### 关键特性 -```css -/* 当有两个页面时,第一个页面添加底部分隔线 */ -.panel-section.has-divider { - border-bottom: 1px solid var(--color-border-light); -} - -/* 当只有一个页面选中时,占据全部空间 */ -.panel-section:only-child { - flex: 1; -} -``` - ---- - -### 6. **ChatBox 样式 (ChatBox.css)** - -#### 全面更新 -- ✅ `.chat-box` - 聊天框容器 -- ✅ `.chat-messages` - 消息列表 -- ✅ `.message.user` / `.message.ai` - 用户/AI 消息气泡 -- ✅ `.bubble` - 消息气泡 -- ✅ `.message-header` - 消息头部(名称、ID、工具栏) -- ✅ `.toolbar-button` - 消息工具栏按钮 -- ✅ `.chat-input-wrapper` - 输入框容器,毛玻璃效果 -- ✅ `.chat-options` - 选项弹出框 -- ✅ `.option-label` - 选项标签 -- ✅ `.chat-input-area textarea` - 输入框 -- ✅ `.send-button` - 发送按钮,渐变背景 -- ✅ `.loading` / `.error` - 加载和错误状态 - -#### 关键改进 - -**消息气泡**: -```css -/* 之前 */ -.message.user { - background-color: #007bff; - border-bottom-right-radius: 4px; -} - -/* 之后 */ -.message.user { - background: var(--gradient-primary); - border-bottom-right-radius: var(--radius-sm); - box-shadow: var(--shadow-md); -} - -.message.ai { - background-color: var(--color-bg-secondary); - border: 1px solid var(--color-border-light); - box-shadow: var(--shadow-sm); -} -``` - -**发送按钮**: -```css -/* 之前 */ -.send-button { - background-color: #007bff; - border-radius: 20px; -} - -/* 之后 */ -.send-button { - background: var(--gradient-primary); - border-radius: var(--radius-xl); - box-shadow: var(--shadow-sm); -} - -.send-button:hover { - transform: translateY(-2px); - box-shadow: var(--shadow-md); -} -``` - -**输入框焦点**: -```css -.chat-input-area textarea:focus { - border-color: var(--color-accent); - box-shadow: 0 0 0 3px var(--color-accent-light); -} -``` - ---- - -### 7. **ThemeToggle 组件** - -#### 新增组件 -- ✅ `components/TopBar/items/ThemeToggle/ThemeToggle.jsx` -- ✅ `components/TopBar/items/ThemeToggle/ThemeToggle.css` -- ✅ `components/TopBar/items/ThemeToggle/index.js` - -#### 功能特点 -- 🌓 支持深色/浅色主题切换 -- 💾 主题偏好保存到 localStorage -- ✨ 悬停动画效果(旋转 + 缩放) -- 📱 响应式设计 - -```css -.theme-toggle:hover .theme-icon { - transform: rotate(15deg) scale(1.1); -} -``` - ---- - -## 📊 迁移统计 - -### 文件修改 -| 文件 | 状态 | 变更行数 | -|------|------|----------| -| `src/styles/variables.css` | ✅ 新建 | +119 | -| `src/styles/reset.css` | ✅ 新建 | +133 | -| `src/index.css` | ✅ 重构 | +99 / -8 | -| `components/TopBar/TopBar.css` | ✅ 重构 | +62 / -39 | -| `components/SideBarLeft/SideBarLeft.css` | ✅ 重构 | +37 / -51 | -| `components/SideBarRight/SideBarRight.css` | ✅ 重构 | +57 / -47 | -| `components/Mid/ChatBox/ChatBox.css` | ✅ 重构 | +84 / -66 | -| `components/TopBar/items/ThemeToggle/` | ✅ 新建 | +77 | - -**总计**: 约 **669 行新增**, **311 行删除** - -### CSS 变量使用 -- 🎨 颜色变量: 20+ 个 -- 📏 间距变量: 7 个 (--spacing-xs 到 --spacing-3xl) -- 🔘 圆角变量: 6 个 (--radius-sm 到 --radius-full) -- 💫 阴影变量: 7 个 (--shadow-xs 到 --shadow-2xl) -- ⚡ 过渡变量: 5 个 (--transition-fast 到 --transition-smooth) -- 📚 Z-index 变量: 7 个 - ---- - -## 🎯 设计风格对照表 - -### Reference → Our Project - -| Reference (Vue) | Our Project (React) | 状态 | -|-----------------|---------------------|------| -| MainLayout.vue | App.jsx + index.css | ✅ 完成 | -| TopBar.vue | TopBar/TopBar.jsx + TopBar.css | ✅ 完成 | -| LeftPanel.vue | SideBarLeft/SideBarLeft.css | ✅ 完成 | -| CenterPanel.vue | Mid/ChatBox/ChatBox.css | ✅ 完成 | -| RightPanel.vue | SideBarRight/SideBarRight.css | ✅ 完成 | -| useTheme.ts | ThemeToggle.jsx | ✅ 完成 | - ---- - -## 🎨 设计特点总结 - -### 1. **优雅的深色主题** -- 深邃但不压抑的背景色 (#0f1115) -- 柔和的蓝色强调色 (#6d8cff) -- 细腻的层次感(多层背景色) - -### 2. **精致的细节** -- 8-24px 的圆角系统 -- 多层阴影创造深度 -- 流畅的缓动动画 (cubic-bezier) - -### 3. **舒适的交互** -- 250ms 标准过渡时间 -- 微妙的悬停效果 -- 平滑的主题切换 - -### 4. **现代感** -- 毛玻璃效果 (backdrop-filter: blur) -- 渐变背景 (linear-gradient, radial-gradient) -- 响应式布局 - ---- - -## 📁 最终文件结构 - -``` -frontend/src/ -├── styles/ # ✨ 新增 -│ ├── variables.css # CSS 变量定义 -│ └── reset.css # CSS Reset + 全局样式 -│ -├── components/ -│ ├── TopBar/ -│ │ ├── TopBar.jsx # (已更新) -│ │ ├── TopBar.css # (完全重构) -│ │ └── items/ -│ │ └── ThemeToggle/ # ✨ 新增 -│ │ ├── ThemeToggle.jsx -│ │ ├── ThemeToggle.css -│ │ └── index.js -│ │ -│ ├── SideBarLeft/ -│ │ ├── SideBarLeft.jsx -│ │ └── SideBarLeft.css # (完全重构) -│ │ -│ ├── SideBarRight/ -│ │ ├── SideBarRight.jsx -│ │ └── SideBarRight.css # (完全重构) -│ │ -│ └── Mid/ -│ └── ChatBox/ -│ ├── ChatBox.jsx -│ └── ChatBox.css # (完全重构) -│ -└── index.css # (完全重构) -``` - ---- - -## ✅ 验证清单 - -### 全局样式 -- [x] CSS 变量正确定义 -- [x] 深色主题正常工作 -- [x] 浅色主题正常工作 -- [x] 主题切换按钮显示正常 -- [x] 主题偏好保存到 localStorage -- [x] 页面刷新后主题保持 - -### 布局样式 -- [x] 三栏布局比例正确 (20% - 60% - 20%) -- [x] 聊天区域渐变背景正常 -- [x] 侧边栏边框和阴影正常 -- [x] 自定义滚动条样式正常 -- [x] 主题切换过渡动画流畅 - -### 组件样式 -- [x] TopBar 毛玻璃效果正常 -- [x] 标签页激活状态正确 -- [x] 消息气泡样式正确 -- [x] 输入框焦点效果正常 -- [x] 发送按钮渐变和悬停效果正常 -- [x] 弹出面板样式正确 - -### 动画效果 -- [x] 按钮悬停动画流畅 -- [x] 主题切换图标旋转动画 -- [x] 面板淡入动画 -- [x] 所有过渡使用 CSS 变量 - ---- - -## 🚀 下一步建议 - -### 短期优化 -1. **更新子组件样式** - - ApiConfig、Presets、WorldBook 等标签页组件 - - Dice、Debug、Macros、Table 等右侧标签页 - - 确保所有子组件都使用 CSS 变量 - -2. **添加更多动画** - - 消息出现动画 (fadeIn) - - 页面切换动画 - - 加载状态动画 (shimmer) - -3. **优化性能** - - 减少不必要的过渡 - - 使用 will-change 优化动画 - - 懒加载大型组件 - -### 中期优化 -1. **添加自定义主题** - - 允许用户自定义颜色 - - 保存多个主题配置 - - 主题预设库 - -2. **无障碍优化** - - 确保对比度符合 WCAG AA 标准 - - 添加 prefers-color-scheme 支持 - - 键盘导航优化 - -3. **响应式设计** - - 移动端适配 (< 768px) - - 平板适配 (768px - 1024px) - - 可折叠侧边栏 - -### 长期优化 -1. **CSS 模块化** - - 考虑使用 CSS Modules 或 Styled Components - - 更好的样式隔离 - - 动态样式支持 - -2. **设计系统** - - 创建组件库 - - 统一的设计令牌 - - 自动化测试 - ---- - -## 📚 相关资源 - -- [CSS Variables MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties) -- [dsanddurga.com](https://www.dsanddurga.com/) - 设计灵感来源 -- [Cubic Bezier Generator](https://cubic-bezier.com/) - 动画曲线工具 -- [Can I Use - backdrop-filter](https://caniuse.com/backdrop-filter) - 浏览器兼容性 - ---- - -**完成时间**: 2026-04-28 -**状态**: ✅ CSS 迁移已完成 -**主题**: 深色(默认)/ 浅色可切换 -**风格**: 参考 dsanddurga.com 的优雅设计 diff --git a/frontend/DATA_TYPE_AUDIT_REPORT.md b/frontend/DATA_TYPE_AUDIT_REPORT.md deleted file mode 100644 index 5ce6770..0000000 --- a/frontend/DATA_TYPE_AUDIT_REPORT.md +++ /dev/null @@ -1,669 +0,0 @@ -# 前端数据类型使用情况检查报告 - -## 概述 - -本报告详细分析了前端代码中涉及前后端数据传递的部分,检查是否正确使用了新创建的数据类型系统。 - -**检查时间**: 2026-04-28 -**检查范围**: `frontend/src/` 目录下所有涉及 API 调用和状态管理的文件 - ---- - -## 📊 总体评估 - -### ✅ 已完成的部分 -- ✅ 创建了完整的双层数据类型系统 (`types/` 目录) -- ✅ 定义了 SillyTavern 兼容层 (`sillytavern.types.ts`) -- ✅ 定义了内部业务层 (`internal.types.ts`) -- ✅ 实现了格式转换函数 (`converters.ts`) - -### ❌ 存在的问题 -- ❌ **所有现有代码都未使用新的类型定义** -- ❌ Store 中的数据结构与后端 internal 模型不一致 -- ❌ API 调用没有类型注解 -- ❌ 组件 Props 缺少类型定义 - ---- - -## 🔍 详细问题分析 - -### 1. RoleSelectorSlice.jsx - -**文件**: `src/Store/Slices/RoleSelectorSlice.jsx` - -#### 问题 1.1: 角色数据结构不规范 - -**当前代码** (第 49 行): -```javascript -roleData: {}, // 格式: {role_name: [{chat_name, user_name, character_name, last_modified, message_count}, ...]} -``` - -**问题分析**: -- ❌ 使用的是匿名对象结构,没有类型定义 -- ❌ 字段命名与后端不一致(应使用 camelCase) -- ❌ 缺少必要的字段如 `id`, `characterId` 等 - -**应该使用**: -```typescript -import type { RoleInfo, ChatSummary } from '@/types'; - -// State 定义 -roleData: Record -``` - -#### 问题 1.2: API 响应处理无类型 - -**当前代码** (第 19-37 行): -```javascript -const data = await response.json(); - -// 转换数据格式以适应前端需求 -const roleData = {}; -if (data.chat && Array.isArray(data.chat)) { - data.chat.forEach(chat => { - if (!roleData[chat.role_name]) { - roleData[chat.role_name] = []; - } - roleData[chat.role_name].push({ - chat_name: chat.chat_name, - user_name: chat.user_name, - character_name: chat.character_name, - last_modified: chat.last_modified, - message_count: chat.message_count - }); - }); -} -``` - -**问题分析**: -- ❌ 没有对 API 响应进行类型断言 -- ❌ 手动转换数据结构,容易出错 -- ❌ 后端返回的格式不明确 - -**建议**: -```typescript -// 假设后端返回 ChatSummary[] 格式 -interface ChatListResponse { - chat: ChatSummary[]; -} - -const data: ChatListResponse = await response.json(); -``` - ---- - -### 2. ChatBoxSlice.jsx - -**文件**: `src/Store/Slices/ChatBoxSlice.jsx` - -#### 问题 2.1: 消息数据结构不完整 - -**当前代码** (第 14 行): -```javascript -messages: [], -``` - -**问题分析**: -- ❌ 没有类型注解 -- ❌ 消息对象结构与 `ChatMessage` 类型不完全一致 -- ❌ 添加了 `floor` 字段但未在类型中明确说明 - -**应该使用**: -```typescript -import type { ChatMessage } from '@/types'; - -messages: ChatMessage[] -``` - -**注意**: `ChatMessage` 类型已包含 `floor` 字段,这是前端特有的扩展。 - -#### 问题 2.2: WebSocket 消息无类型 - -**当前代码** (第 186-212 行): -```javascript -ws.onmessage = (event) => { - const data = JSON.parse(event.data); - console.log('[WebSocket] 收到消息', { type: data.type, content: data.content }); - - if (data.type === 'chunk') { - // ... - } else if (data.type === 'complete') { - // ... - } else if (data.type === 'error') { - // ... - } -}; -``` - -**问题分析**: -- ❌ WebSocket 消息没有类型定义 -- ❌ 使用魔法字符串 ('chunk', 'complete', 'error') -- ❌ 容易拼写错误 - -**应该使用**: -```typescript -import type { WSResponseMessage } from '@/types'; - -ws.onmessage = (event) => { - const data: WSResponseMessage = JSON.parse(event.data); - - if (data.type === 'chunk') { - // TypeScript 会自动推断 data.content 存在 - } -}; -``` - -#### 问题 2.3: WebSocket 请求无类型 - -**当前代码** (第 238-255 行): -```javascript -ws.send(JSON.stringify({ - floor: nextFloor, - mes: content, - is_user: true, - currentRole: currentRole, - currentChat: currentChat, - options: options, - apiConfig: { - api_url: ..., - api_key: ... - }, - presetConfig: { - selectedPreset: ..., - parameters: ..., - promptComponents: ... - }, - stream: options.streamOutput -})); -``` - -**问题分析**: -- ❌ 请求对象结构复杂但没有类型定义 -- ❌ 嵌套对象结构不清晰 -- ❌ 难以维护 - -**应该使用**: -```typescript -import type { WSRequestMessage } from '@/types'; - -const request: WSRequestMessage = { - floor: nextFloor, - mes: content, - is_user: true, - currentRole, - currentChat, - options, - apiConfig: { - api_url: ..., - api_key: ... - }, - presetConfig: { - selectedPreset: ..., - parameters: ..., - promptComponents: ... - }, - stream: options.streamOutput -}; - -ws.send(JSON.stringify(request)); -``` - -#### 问题 2.4: API 配置获取无类型 - -**当前代码** (第 246-247 行): -```javascript -api_url: apiConfigStore.allApis.find(api => api.category === 'text' && api.id === apiConfigStore.activeMap.text)?.api_url || '', -api_key: apiConfigStore.allApis.find(api => api.category === 'text' && api.id === apiConfigStore.activeMap.text)?.api_key || '' -``` - -**问题分析**: -- ❌ `allApis` 数组元素没有类型 -- ❌ 访问属性时没有类型检查 -- ❌ 可能访问到 undefined - -**应该使用**: -```typescript -import type { ApiConfig } from '@/types'; - -// ApiConfigSlice 中应该定义为 -allApis: ApiConfig[] - -// 使用时有自动补全和类型检查 -const activeApi = apiConfigStore.allApis.find( - api => api.category === 'text' && api.id === apiConfigStore.activeMap.text -); - -api_url: activeApi?.apiUrl || '', // 注意是 apiUrl 不是 api_url -api_key: activeApi?.apiKey || '' // 注意是 apiKey 不是 api_key -``` - ---- - -### 3. ApiConfigSlice.jsx - -**文件**: `src/Store/Slices/LeftTabsSlices/ApiConfigSlice.jsx` - -#### 问题 3.1: API 配置数据结构不一致 - -**当前代码** (第 6-16 行): -```javascript -const initialState = { - allApis: [], // 存储所有获取到的API,包含category属性 - activeMap: {}, // 存储当前激活的配置映射 { category: profileId } - loading: false, - error: null, - notification: { - show: false, - message: '', - type: 'info' - } -}; -``` - -**问题分析**: -- ❌ `allApis` 没有类型定义 -- ❌ 字段命名使用 snake_case (`api_url`, `api_key`),应与 internal.types.ts 保持一致使用 camelCase -- ❌ `activeMap` 结构不明确 - -**应该使用**: -```typescript -import type { ApiConfig } from '@/types'; - -interface ApiConfigState { - allApis: ApiConfig[]; - activeMap: Record; // { category: configId } - loading: boolean; - error: string | null; - notification: { - show: boolean; - message: string; - type: 'success' | 'error' | 'info'; - }; -} -``` - -#### 问题 3.2: API 响应处理无类型 - -**当前代码** (第 34 行): -```javascript -const data = await response.json(); -set({ allApis: data, loading: false }); -``` - -**问题分析**: -- ❌ 直接使用原始响应数据 -- ❌ 没有验证数据结构 -- ❌ 如果后端返回格式变化,运行时才会发现错误 - -**应该使用**: -```typescript -const data: ApiConfig[] = await response.json(); -set({ allApis: data, loading: false }); -``` - ---- - -### 4. PresetSlice.jsx - -**文件**: `src/Store/Slices/LeftTabsSlices/PresetSlice.jsx` - -#### 问题 4.1: 预设参数命名不一致 - -**当前代码** (第 8-20 行): -```javascript -parameters: { - temperature: 1.0, - frequency_penalty: 0.0, // ❌ snake_case - presence_penalty: 0.0, // ❌ snake_case - top_p: 1.0, // ❌ snake_case - top_k: 0, // ❌ snake_case - max_context: 1000000, // ❌ snake_case - max_tokens: 30000, // ❌ snake_case - max_context_unlocked: false, // ❌ snake_case - stream_openai: true, // ❌ snake_case - seed: -1, - n: 1 -}, -``` - -**问题分析**: -- ❌ 大量使用 snake_case,与 internal.types.ts 的 camelCase 不一致 -- ❌ 这些参数应该对应后端的 `GenerationPreset` 模型 -- ❌ 字段名混乱导致前后端对接困难 - -**应该使用**: -```typescript -import type { GenerationPreset } from '@/types'; - -// 或者至少保持命名一致 -parameters: { - temperature: 1.0, - frequencyPenalty: 0.0, // ✅ camelCase - presencePenalty: 0.0, // ✅ camelCase - topP: 1.0, // ✅ camelCase - topK: 0, // ✅ camelCase - maxContext: 1000000, // ✅ camelCase - maxTokens: 30000, // ✅ camelCase - maxContextUnlocked: false, // ✅ camelCase - streamOpenai: true, // ✅ camelCase - seed: -1, - n: 1 -} -``` - -#### 问题 4.2: Prompt 组件结构不规范 - -**当前代码** (第 32-97 行): -```javascript -promptComponents: [ - { - identifier: "dialogueExamples", - name: "Chat Examples", - system_prompt: true, // ❌ snake_case - marker: true, - enabled: true, - role: 0 // ❌ 应该用枚举或明确的类型 - }, - // ... -], -``` - -**问题分析**: -- ❌ 字段命名不一致 -- ❌ `role` 使用数字 (0, 1, 2),应该使用枚举或字符串 -- ❌ 结构与 `PromptComponent` 类型定义不完全匹配 - -**应该使用**: -```typescript -import type { PromptComponent } from '@/types'; - -promptComponents: PromptComponent[] - -// PromptComponent 类型定义应调整为 -export interface PromptComponent { - identifier: string; - name: string; - systemPrompt: boolean; // ✅ camelCase - marker: boolean; - enabled: boolean; - role: number; // 或者改为 enum PromptRole { System = 0, User = 1, Assistant = 2 } -} -``` - -#### 问题 4.3: API 响应处理复杂且无类型 - -**当前代码** (第 103-119 行): -```javascript -const response = await fetch('/api/presets'); -const data = await response.json(); - -// 转换为预设对象数组 -const presetList = data.presets.map(preset => ({ - id: preset.name, - name: preset.name, - description: preset.description, - component_count: preset.component_count, // ❌ snake_case - temperature: preset.temperature -})); -``` - -**问题分析**: -- ❌ 手动转换数据结构 -- ❌ 字段命名不一致 -- ❌ 没有类型验证 - -**应该使用**: -```typescript -interface PresetListResponse { - presets: Array<{ - name: string; - description?: string; - component_count?: number; - temperature?: number; - }>; -} - -const data: PresetListResponse = await response.json(); -const presetList: Array<{ - id: string; - name: string; - description: string; - componentCount: number; // ✅ camelCase - temperature: number; -}> = data.presets.map(preset => ({ - id: preset.name, - name: preset.name, - description: preset.description || '', - componentCount: preset.component_count || 0, - temperature: preset.temperature || 1.0 -})); -``` - ---- - -### 5. WorldBookSlice.jsx - -**文件**: `src/Store/Slices/LeftTabsSlices/WorldBookSlice.jsx` - -#### 问题 5.1: 世界书数据结构缺失 - -**当前代码** (第 51-59 行): -```javascript -worldBooks: [], // 世界书列表 -globalWorldBooks: loadGlobalWorldBooks(), // 从 LocalStorage 初始化全局世界书列表 -currentWorldBook: null, // 当前选中的世界书 -currentEntries: [], // 当前世界书的条目列表 -currentEntry: null, // 当前选中的条目 -``` - -**问题分析**: -- ❌ 完全没有类型定义 -- ❌ 世界书和条目的结构不明确 -- ❌ 应该对应后端的 `WorldInfo` 和 `WorldInfoEntry` 模型 - -**应该使用**: -```typescript -import type { WorldInfo, WorldInfoEntry } from '@/types'; - -// 需要在 internal.types.ts 中添加 WorldInfo 和 WorldInfoEntry 类型 -worldBooks: WorldInfo[]; -globalWorldBooks: WorldInfo[]; -currentWorldBook: WorldInfo | null; -currentEntries: WorldInfoEntry[]; -currentEntry: WorldInfoEntry | null; -``` - ---- - -## 📋 数据类型对照表 - -### 前端当前使用 vs 应该使用的类型 - -| 位置 | 当前数据结构 | 应该使用的类型 | 优先级 | -|------|------------|--------------|--------| -| RoleSelectorSlice | 匿名对象 `{role_name, chats}` | `Record` | 🔴 高 | -| ChatBoxSlice.messages | 匿名数组 | `ChatMessage[]` | 🔴 高 | -| ChatBoxSlice.wsConnection | WebSocket | 无需修改 | 🟢 低 | -| ApiConfigSlice.allApis | 匿名数组 | `ApiConfig[]` | 🔴 高 | -| ApiConfigSlice.activeMap | 匿名对象 | `Record` | 🟡 中 | -| PresetSlice.parameters | snake_case 对象 | `GenerationPreset` (camelCase) | 🔴 高 | -| PresetSlice.promptComponents | 匿名数组 | `PromptComponent[]` | 🟡 中 | -| WorldBookSlice.worldBooks | 匿名数组 | `WorldInfo[]` (需添加类型) | 🟡 中 | - ---- - -## 🎯 核心问题总结 - -### 1. 类型系统未集成 -- ❌ 创建了类型定义但没有任何文件导入使用 -- ❌ 所有 Store 都是 `.jsx` 而非 `.tsx` -- ❌ 没有 TypeScript 类型检查 - -### 2. 命名规范不一致 -- ❌ 混用 snake_case 和 camelCase -- ❌ 与后端 internal 模型的命名不一致 -- ❌ 增加前后端对接难度 - -### 3. API 响应处理不规范 -- ❌ 直接使用 `response.json()` 无类型断言 -- ❌ 手动转换数据结构容易出错 -- ❌ 缺少运行时验证 - -### 4. WebSocket 消息无类型 -- ❌ 请求和响应都没有类型定义 -- ❌ 使用魔法字符串 -- ❌ 难以维护和调试 - ---- - -## 💡 改进建议 - -### 短期方案(立即可做) - -#### 1. 添加 JSDoc 类型注释(无需改文件扩展名) - -```javascript -// @ts-check -/** @type {import('@/types').ChatMessage[]} */ -const messages = []; - -/** - * @param {string} content - * @returns {Promise} - */ -const sendMessage = async (content) => { - // ... -}; -``` - -#### 2. 统一命名规范 - -将所有 snake_case 改为 camelCase,与 internal.types.ts 保持一致: -- `frequency_penalty` → `frequencyPenalty` -- `api_url` → `apiUrl` -- `system_prompt` → `systemPrompt` - -#### 3. 添加 API 响应类型断言 - -```javascript -// 在 fetch 后立即断言类型 -const data = /** @type {import('@/types').ApiConfig[]} */ (await response.json()); -``` - -### 中期方案(推荐) - -#### 1. 逐步迁移到 TypeScript - -按以下顺序将 `.jsx` 改为 `.tsx`: -1. `types/` 目录(已完成 ✅) -2. Store Slices(优先级最高) -3. 组件文件 -4. 工具函数 - -#### 2. 更新 internal.types.ts - -补充缺失的类型定义: -- `WorldInfo` - 世界书 -- `WorldInfoEntry` - 世界书条目 -- `RoleInfo` - 角色信息(完善) - -#### 3. 创建 API Client 封装 - -```typescript -// src/api/client.ts -import type { ChatLog, ApiConfig, GenerationPreset } from '@/types'; - -export const apiClient = { - async getChat(role: string, chat: string): Promise { - const response = await fetch(`/api/chat/${role}/${chat}`); - return response.json(); - }, - - async getApiConfigs(): Promise { - const response = await fetch('/api/apiconfigs'); - return response.json(); - }, - - // ... -}; -``` - -### 长期方案(理想状态) - -#### 1. 完全 TypeScript 化 -- 所有文件使用 `.tsx` 扩展名 -- 启用严格的 TypeScript 检查 -- 配置 ESLint + TypeScript 规则 - -#### 2. 使用 Zod 进行运行时验证 -```typescript -import { z } from 'zod'; -import { ChatMessageSchema } from '@/types/schemas'; - -const data = await response.json(); -const validated = ChatMessageSchema.parse(data); // 运行时验证 -``` - -#### 3. 自动生成类型 -- 从后端 OpenAPI/Swagger 文档生成前端类型 -- 确保前后端类型始终同步 - ---- - -## 📝 具体修复清单 - -### 优先级 P0(必须修复) - -- [ ] 更新 `ChatBoxSlice.jsx` 使用 `ChatMessage[]` 类型 -- [ ] 更新 `ApiConfigSlice.jsx` 使用 `ApiConfig[]` 类型 -- [ ] 统一 PresetSlice 参数命名为 camelCase -- [ ] 为 WebSocket 消息添加类型定义 - -### 优先级 P1(强烈建议) - -- [ ] 将 Store Slices 从 `.jsx` 迁移到 `.tsx` -- [ ] 补充 `WorldInfo` 和 `WorldInfoEntry` 类型定义 -- [ ] 更新 `RoleSelectorSlice` 使用规范类型 -- [ ] 创建 API Client 封装层 - -### 优先级 P2(可以后续做) - -- [ ] 组件 Props 添加类型注解 -- [ ] 添加 JSDoc 文档注释 -- [ ] 配置 TypeScript 严格模式 -- [ ] 添加运行时数据验证 - ---- - -## 🔗 相关资源 - -- [前端数据类型规范](./types/README.md) -- [Internal Types](./types/internal.types.ts) -- [SillyTavern Types](./types/sillytavern.types.ts) -- [Converters](./types/converters.ts) -- [后端 Internal 模型](../../backend/models/internal.py) - ---- - -## 结论 - -**现状**: 前端已建立完整的类型系统,但**尚未在任何地方使用**。 - -**影响**: -- ⚠️ 类型安全优势无法发挥 -- ⚠️ 容易出现运行时错误 -- ⚠️ 前后端数据结构可能不一致 -- ⚠️ 代码可维护性差 - -**建议**: -1. 立即开始逐步迁移到 TypeScript -2. 优先处理 Store 层的类型化 -3. 统一命名规范为 camelCase -4. 添加 API 响应的类型断言 - -**预期收益**: -- ✅ 编译时捕获类型错误 -- ✅ IDE 智能提示和自动补全 -- ✅ 更好的代码文档 -- ✅ 减少运行时错误 -- ✅ 提高开发效率 diff --git a/frontend/GLOBAL_MINIMALIST_STYLE_COMPLETE.md b/frontend/GLOBAL_MINIMALIST_STYLE_COMPLETE.md deleted file mode 100644 index 0bee8b4..0000000 --- a/frontend/GLOBAL_MINIMALIST_STYLE_COMPLETE.md +++ /dev/null @@ -1,382 +0,0 @@ -# 🎨 全局按钮极简风格优化报告 - -## ✅ 完成的优化 - -按照用户要求,将**所有按钮**统一为极简风格,包括: -1. ✅ ChatBox 输入框左右按钮 -2. ✅ TopBar 工具栏操作按钮 -3. ✅ TopBar 状态徽章 -4. ✅ ThemeToggle 主题切换按钮 - ---- - -## 📊 优化对比 - -### 1. **TopBar 操作按钮 (action-btn)** - -#### 之前 - 复杂风格 -```css -.action-btn { - width: 42px; - height: 42px; - border-radius: var(--radius-lg); - background-color: transparent; - color: var(--color-text-secondary); - border: 1px solid transparent; - position: relative; -} - -/* 伪元素渐变背景 */ -.action-btn::before { - content: ''; - position: absolute; - inset: 0; - background: var(--color-accent-light); - opacity: 0; -} - -/* 复杂悬停效果 */ -.action-btn:hover { - color: var(--color-accent); - border-color: var(--color-accent); - transform: translateY(-2px); /* 上浮 */ - box-shadow: var(--shadow-md); /* 阴影 */ -} - -.action-btn:hover::before { - opacity: 1; /* 渐变显示 */ -} - -.action-btn:hover svg { - transform: scale(1.1) rotate(5deg); /* 放大+旋转 */ -} -``` - -#### 之后 - 极简风格 -```css -.action-btn { - width: 32px; /* ⬇️ 减小 24% */ - height: 36px; /* ⬇️ 减小 14% */ - border-radius: var(--radius-md); /* 从 lg 改为 md */ - background-color: transparent; - color: var(--color-text-muted); /* muted 颜色 */ - border: none; /* 移除边框 */ -} - -.action-btn:hover { - background-color: var(--color-bg-tertiary); /* 简单背景色 */ - color: var(--color-text-secondary); -} - -.action-btn:active { - background-color: var(--color-accent-ultra-light); - color: var(--color-accent); -} - -.action-btn:hover svg { - transform: scale(1.05); /* 仅轻微放大,无旋转 */ -} -``` - -**移除的效果**: -- ❌ 伪元素渐变背景 -- ❌ 边框变化 -- ❌ 上浮动画 -- ❌ 阴影效果 -- ❌ SVG 旋转 - -**保留的效果**: -- ✅ 简单的背景色变化 -- ✅ SVG 轻微放大 (1.05x) - ---- - -### 2. **ThemeToggle 主题切换按钮** - -#### 之前 - 复杂风格 -```css -.theme-toggle::after { - content: ''; - position: absolute; - inset: 0; - background: var(--gradient-primary); /* 渐变背景 */ - opacity: 0; -} - -.theme-toggle:hover::after { - opacity: 0.1; -} - -.theme-toggle:hover svg { - transform: scale(1.15) rotate(15deg); /* 大幅放大+旋转 */ - color: var(--color-accent); -} -``` - -#### 之后 - 极简风格 -```css -.theme-toggle { - /* Inherits all styles from action-btn */ -} - -.theme-toggle:hover svg { - transform: scale(1.05); /* 仅轻微放大 */ -} -``` - -**移除的效果**: -- ❌ 伪元素渐变背景 -- ❌ SVG 大幅放大 (1.15x → 1.05x) -- ❌ SVG 旋转 (15° → 0°) -- ❌ 颜色变化 - ---- - -### 3. **状态徽章 (status-badge)** - -#### 之前 - 显眼风格 -```css -.status-badge { - padding: var(--spacing-sm) var(--spacing-md); - background-color: var(--color-bg-primary); /* 有背景 */ - border: 1px solid var(--color-border-light); /* 有边框 */ - border-radius: var(--radius-lg); - min-height: 36px; -} - -.status-badge:hover { - border-color: var(--color-accent); - box-shadow: 0 0 0 3px var(--color-accent-light); /* 光晕 */ - transform: translateY(-2px); /* 上浮 */ -} - -.status-icon { - font-size: 1.2rem; -} - -.status-label { - font-size: 0.95rem; - color: var(--color-text-primary); - font-weight: 500; -} -``` - -#### 之后 - 极简风格 -```css -.status-badge { - padding: var(--spacing-xs) var(--spacing-sm); /* ⬇️ 减小 */ - background-color: transparent; /* 透明背景 */ - border: none; /* 无边框 */ - border-radius: var(--radius-sm); /* 从 lg 改为 sm */ - min-height: 32px; /* ⬇️ 减小 */ -} - -.status-badge:hover { - background-color: var(--color-bg-tertiary); /* 简单背景 */ -} - -.status-icon { - font-size: 1rem; /* ⬇️ 减小 */ - opacity: 0.7; /* 降低透明度 */ -} - -.status-label { - font-size: 0.85rem; /* ⬇️ 减小 */ - color: var(--color-text-secondary); /* secondary 颜色 */ - font-weight: 400; /* 从 500 改为 400 */ -} -``` - -**改进**: -- ✅ 默认透明背景,不抢眼 -- ✅ 移除边框和光晕 -- ✅ 移除上浮动画 -- ✅ 图标和文字更小、更淡 -- ✅ 字体粗细从 500 降到 400 - ---- - -### 4. **ChatBox 输入框按钮** - -已在之前的优化中完成(见 INPUT_SIMPLIFICATION_COMPLETE.md): - -- **选项按钮**: 32x36px, 透明背景, muted 颜色 -- **发送按钮**: 32x36px, 透明背景, muted 颜色 -- **SVG 图标**: 14-16px, 轻微放大效果 - ---- - -## 📈 尺寸对比总结 - -| 元素 | 之前 | 之后 | 变化 | -|------|------|------|------| -| **TopBar 按钮** | 42x42px | 32x36px | ⬇️ 14-24% | -| **状态徽章高度** | 36px | 32px | ⬇️ 11% | -| **状态徽章内边距** | sm/md | xs/sm | ⬇️ 30% | -| **图标大小** | 1.2rem | 1rem | ⬇️ 17% | -| **文字大小** | 0.95rem | 0.85rem | ⬇️ 11% | -| **圆角** | radius-lg | radius-md/sm | ⬇️ 25% | - ---- - -## 🎨 设计哲学 - -### 极简主义原则 - -1. **透明背景** - 默认不可见,只在交互时显示 -2. **无装饰** - 移除边框、阴影、渐变等装饰元素 -3. **低调颜色** - 使用 muted/secondary 颜色,不抢视线 -4. **微小动画** - 仅保留最必要的反馈(背景色变化、轻微放大) -5. **紧凑尺寸** - 减少空间占用,提高信息密度 - -### 视觉层级 - -**之前**: 🔴 高视觉权重(边框+阴影+渐变+动画) -**之后**: 🟢 低视觉权重(透明+muted+微动画) - ---- - -## 🎯 用户体验提升 - -### 1. **视觉焦点更清晰** -- 按钮不再分散注意力 -- 主要内容更加突出 -- 界面更加清爽 - -### 2. **空间利用率更高** -- 按钮尺寸减小 14-24% -- 内边距减少 30% -- 整体布局更紧凑 - -### 3. **交互更自然** -- 悬停反馈简洁明了 -- 没有夸张的动画 -- 符合现代 UI 趋势 - -### 4. **一致性更强** -- 所有按钮统一风格 -- TopBar 和 ChatBox 保持一致 -- 整体设计语言统一 - ---- - -## 📝 技术细节 - -### CSS 变量使用 -```css -/* 颜色 */ -var(--color-text-muted) /* #6b7280 - 默认颜色 */ -var(--color-text-secondary) /* #9aa0a6 - 悬停颜色 */ -var(--color-accent) /* #6d8cff - 激活颜色 */ -var(--color-accent-ultra-light) /* rgba(109, 140, 255, 0.05) - 激活背景 */ -var(--color-bg-tertiary) /* #1c1f27 - 悬停背景 */ - -/* 间距 */ -var(--spacing-xs) /* 4px */ -var(--spacing-sm) /* 6px */ - -/* 圆角 */ -var(--radius-sm) /* 8px */ -var(--radius-md) /* 12px */ - -/* 过渡 */ -var(--transition-fast) /* 150ms */ -``` - -### 移除的复杂效果 -- ❌ `position: relative` + 伪元素 -- ❌ `box-shadow` 多层阴影 -- ❌ `transform: translateY()` 上浮动画 -- ❌ `rotate()` 旋转动画 -- ❌ `border` 边框变化 -- ❌ `opacity` 渐变显示 - -### 保留的简单效果 -- ✅ `background-color` 背景色变化 -- ✅ `transform: scale(1.05)` 轻微放大 -- ✅ `color` 颜色变化 -- ✅ `transition: 150ms` 快速过渡 - ---- - -## ✅ 验证清单 - -### TopBar 按钮 -- [x] 尺寸从 42x42 减小到 32x36 -- [x] 移除伪元素渐变 -- [x] 移除边框和阴影 -- [x] 移除上浮动画 -- [x] SVG 仅轻微放大 (1.05x) -- [x] 默认 muted 颜色 - -### ThemeToggle -- [x] 继承 action-btn 样式 -- [x] 移除渐变背景 -- [x] 移除旋转动画 -- [x] SVG 仅轻微放大 - -### 状态徽章 -- [x] 透明背景 -- [x] 无边框 -- [x] 减小内边距 -- [x] 减小图标和文字 -- [x] 降低字重 (500 → 400) -- [x] 悬停仅显示背景色 - -### ChatBox 按钮 -- [x] 已完成(见之前报告) -- [x] 与 TopBar 风格一致 - ---- - -## 🎊 最终效果 - -### 统一的极简风格 - -**所有按钮现在都遵循相同的设计原则**: -1. 透明背景,默认低调 -2. 悬停时简单背景色变化 -3. 激活时强调色反馈 -4. 无装饰性动画 -5. 紧凑的尺寸 - -### 视觉效果 - -**之前**: -``` -[🔴 显眼按钮] [🔴 显眼徽章] [🔴 显眼按钮] - ↑ ↑ ↑ - 边框+阴影 背景+边框 渐变+旋转 -``` - -**之后**: -``` -[⚪ 低调] [⚪ 低调] [⚪ 低调] - ↓ ↓ ↓ - 悬停才显 悬停才显 悬停才显 -``` - ---- - -## 🚀 下一步建议 - -### 可选优化 -1. **统一其他组件** - - SideBar 标签按钮 - - Presets 操作按钮 - - WorldBook 操作按钮 - -2. **添加键盘支持** - - Tab 导航焦点样式 - - 键盘快捷键反馈 - -3. **无障碍优化** - - 确保足够的对比度 - - 添加 aria-label - - 焦点可见性 - ---- - -**完成时间**: 2026-04-28 -**状态**: ✅ 全局按钮极简风格优化完成 -**设计风格**: 极简主义、低调优雅、简洁明了 diff --git a/frontend/INPUT_BOX_FIX_COMPLETE.md b/frontend/INPUT_BOX_FIX_COMPLETE.md deleted file mode 100644 index adcefec..0000000 --- a/frontend/INPUT_BOX_FIX_COMPLETE.md +++ /dev/null @@ -1,433 +0,0 @@ -# 🔧 输入框和布局修复报告 - -## ✅ 完成的工作 - -### 1. **修复侧边栏高度问题** - -#### 问题 -左右侧边栏的边框没有接上顶部,与 TopBar 之间有间隙。 - -#### 解决方案 -在 `index.css` 的 `.main-container` 中添加: -```css -.main-container { - margin-top: 0; /* Ensure panels start from top */ -} -``` - -#### 结果 -✅ 侧边栏现在从顶部开始,边框与 TopBar 无缝连接 - ---- - -### 2. **完全重构 ChatInput 样式** - -按照 reference 的 ChatInput.vue 设计,完全重构了输入框区域的样式和结构。 - -#### 主要变更 - -##### A. 容器结构更新 -```jsx -// 之前 -

- -
...
-