From 10f9c0061aa86bde811e29f75c028abd4e11538e Mon Sep 17 00:00:00 2001 From: Nostars Developer Date: Thu, 16 Apr 2026 18:11:35 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B0=8F=E4=BC=98=E5=8C=96=EF=BC=8C=E4=BA=A7?= =?UTF-8?q?=E5=93=81=E6=84=BF=E6=99=AF=EF=BC=8C=E8=A7=86=E8=A7=89=E8=A7=84?= =?UTF-8?q?=E8=8C=83=EF=BC=8C=E4=BA=A4=E4=BA=92=E5=92=8C=E5=AF=BC=E8=88=AA?= =?UTF-8?q?=E5=A4=A7=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cursor/changelog/changelog-full.md | 160 +++ .cursor/changelog/changelog-headlines.md | 6 + .cursor/changelog/changelog-recent.md | 30 + .cursor/deferred/registry.md | 38 + .cursor/distillery/golden-rules.md | 5 + .cursor/distillery/insights.md | 8 + .cursor/distillery/problems.md | 5 + .cursor/hooks.json | 4 +- .cursor/hooks/README.md | 38 + .cursor/hooks/check-changelog.sh | 58 + .cursor/hooks/run-hook.ps1 | 35 + .cursor/hooks/session-init.sh | 15 + .cursor/local-env.example.json | 5 + .cursor/rules/common/distillery-recall.mdc | 44 + .cursor/rules/project/local-env-check.mdc | 47 + .cursor/skills/epee-orchestrator/registry.md | 10 +- .cursor/skills/problem-distillery/SKILL.md | 232 ++++ .cursor/skills/project-launcher/SKILL.md | 88 +- .gitignore | 3 + art-agent/ENVIRONMENT.md | 206 ++++ art-agent/backend/app/agent/loop.py | 31 +- art-agent/backend/app/agent/tools.py | 32 +- art-agent/backend/app/config.py | 44 + .../backend/app/services/view_transform.py | 157 +++ art-agent/backend/requirements-lock.txt | Bin 0 -> 2200 bytes art-agent/backend/requirements.txt | 28 +- art-agent/frontend/package-lock.json | 127 +- art-agent/frontend/package.json | 7 +- art-agent/frontend/src/app/gallery/page.tsx | 56 +- art-agent/frontend/src/app/globals.css | 431 ++++++- art-agent/frontend/src/app/login/page.tsx | 78 +- art-agent/frontend/src/app/page.tsx | 176 +-- .../src/components/chat/chat-input.tsx | 392 +++--- .../src/components/chat/chat-messages.tsx | 105 +- .../src/components/chat/image-grid.tsx | 105 +- .../src/components/chat/model-selector.tsx | 97 +- .../components/detail/image-detail-panel.tsx | 47 +- .../src/components/layout/top-nav.tsx | 80 +- .../frontend/src/components/profile-modal.tsx | 53 +- .../src/components/sidebar/session-list.tsx | 185 +-- .../src/components/sidebar/sidebar.tsx | 6 +- .../src/components/ui/ambient-particles.tsx | 78 ++ .../src/components/ui/data-stream.tsx | 20 + .../src/components/ui/firefly-burst.tsx | 62 + .../src/components/ui/motion-presets.ts | 113 ++ .../src/components/ui/ripple-effect.tsx | 53 + art-agent/frontend/src/lib/app-context.tsx | 6 +- docs/art-agent/INFOLAYER.md | 614 ++++++++++ docs/art-agent/THE-GREATE-EPEEKIT.md | 1077 +++++++++++++++++ docs/art-agent/UX-STYLE-GUIDE.md | 417 +++++++ docs/art-agent/VISUAL-STYLE-GUIDE.md | 143 +-- 51 files changed, 5010 insertions(+), 847 deletions(-) create mode 100644 .cursor/distillery/golden-rules.md create mode 100644 .cursor/distillery/insights.md create mode 100644 .cursor/distillery/problems.md create mode 100644 .cursor/hooks/README.md create mode 100644 .cursor/hooks/check-changelog.sh create mode 100644 .cursor/hooks/run-hook.ps1 create mode 100644 .cursor/hooks/session-init.sh create mode 100644 .cursor/local-env.example.json create mode 100644 .cursor/rules/common/distillery-recall.mdc create mode 100644 .cursor/rules/project/local-env-check.mdc create mode 100644 .cursor/skills/problem-distillery/SKILL.md create mode 100644 art-agent/ENVIRONMENT.md create mode 100644 art-agent/backend/app/services/view_transform.py create mode 100644 art-agent/backend/requirements-lock.txt create mode 100644 art-agent/frontend/src/components/ui/ambient-particles.tsx create mode 100644 art-agent/frontend/src/components/ui/data-stream.tsx create mode 100644 art-agent/frontend/src/components/ui/firefly-burst.tsx create mode 100644 art-agent/frontend/src/components/ui/motion-presets.ts create mode 100644 art-agent/frontend/src/components/ui/ripple-effect.tsx create mode 100644 docs/art-agent/INFOLAYER.md create mode 100644 docs/art-agent/THE-GREATE-EPEEKIT.md create mode 100644 docs/art-agent/UX-STYLE-GUIDE.md diff --git a/.cursor/changelog/changelog-full.md b/.cursor/changelog/changelog-full.md index 0b4cd32..6e64801 100644 --- a/.cursor/changelog/changelog-full.md +++ b/.cursor/changelog/changelog-full.md @@ -4,6 +4,166 @@ ## 记录 +### [CL-20260416-1600] 2026-04-16 16:00 — 视觉风格从暗绿底全面转为白底青绿(绢本山水) +- **tags**: 前端, 视觉风格, 色彩体系, CSS变量, 白底, 千里江山图 +- **affected_files**: + - art-agent/frontend/src/app/globals.css + - art-agent/frontend/src/app/page.tsx + - art-agent/frontend/src/app/login/page.tsx + - art-agent/frontend/src/app/gallery/page.tsx + - art-agent/frontend/src/components/chat/chat-messages.tsx + - art-agent/frontend/src/components/chat/chat-input.tsx + - art-agent/frontend/src/components/layout/top-nav.tsx + - art-agent/frontend/src/components/sidebar/sidebar.tsx + - art-agent/frontend/src/components/sidebar/session-list.tsx + - art-agent/frontend/src/components/profile-modal.tsx + - art-agent/frontend/src/components/detail/image-detail-panel.tsx + - art-agent/frontend/src/components/ui/ambient-particles.tsx + - docs/art-agent/VISUAL-STYLE-GUIDE.md +- **what**: 整个应用从暗色(墨绿黑 #0C1210)底色改为白色(绢本白 #FAFAF7)底色,文字改为墨黑(#1A1A1A),框体和光效保留千里江山图的蓝绿配色但色值加深适配白底 +- **why**: 用户反馈当前风格"太偏绿了"。评估后认同——原版将背景、面板、文字全都带绿调,导致全屏绿色信息过载。千里江山图本身是青绿设色于白色绢本之上,白底才更贴近原画精髓 +- **decisions**: + - 背景 #0C1210 → #FAFAF7(绢本微暖白),面板从绿底毛玻璃改为白色毛玻璃 + - accent #4DB8A4 → #2E8B7A(加深以确保白底上对比度) + - 文字 #E0E8E2 → #1A1A1A(墨黑),次级文字 #7A9485 → #6B7B8A(灰蓝去绿调) + - 所有 rgba(77,184,164,...) 硬编码统一替换为 rgba(46,139,122,...) + - 云雾背景透明度大幅降低(白底上需要更淡才不会突兀) + - Logo 内部颜色从 #0C1210 改为 white(石青底白字) + - 保留所有动效系统(云烟缭绕、萤火粒子、水波纹等),仅调整颜色参数 +- **notes**: + - surface-1/2/3 从深色半透明改为白色半透明,阴影也从重阴影改为轻阴影 + - 网格底纹 opacity 从 0.03 降为 0.018 + - VISUAL-STYLE-GUIDE.md 文档同步重写 +- **source_chat**: [白底青绿视觉风格改造] + +### [CL-20260416-1500] 2026-04-16 15:00 — UX风格指南实现度补全:接入涟漪/sendBounce/会话切换动画/Error飘散/全屏云雾加载/流式opacity阶梯等10项 +- **tags**: 前端, UX补全, 动效, framer-motion, 微交互, 排版使用, 实现状态文档 +- **affected_files**: + - art-agent/frontend/src/components/chat/chat-input.tsx + - art-agent/frontend/src/components/chat/chat-messages.tsx + - art-agent/frontend/src/components/chat/image-grid.tsx + - art-agent/frontend/src/components/ui/motion-presets.ts + - art-agent/frontend/src/app/page.tsx + - art-agent/frontend/src/app/login/page.tsx + - art-agent/frontend/src/app/gallery/page.tsx + - art-agent/frontend/src/app/globals.css + - art-agent/frontend/src/lib/app-context.tsx + - art-agent/frontend/src/components/sidebar/sidebar.tsx + - art-agent/frontend/src/components/profile-modal.tsx + - docs/art-agent/UX-STYLE-GUIDE.md +- **what**: 补全 UX-STYLE-GUIDE.md 中描述但未实现的 10 项特性,并在文档中标注每项实现状态 +- **why**: 分析发现风格指南完成度约 65-70%,多项微交互只写了文档描述或代码定义了但未接入业务 +- **decisions**: 对每项未实现特性逐一补全代码,同时更新文档加入 ✅/🔶/⬜ 状态标记 +- **notes**: 补全项包括:(1) ripple-effect.tsx 接入发送按钮 (2) sendBounce 替代 buttonTap (3) 会话切换 sessionSlide 滑入滑出 (4) Error errorFloat 飘散消失+4s自动清除 (5) 回到底部按钮水滴形 (6) 全屏加载改云雾凝聚 mist-loader (7) AI回复 StreamingLines 逐行 opacity 阶梯 (8) typo-h1/h2/strong 实际使用到组件 (9) 用户消息 .user-bubble 左上切角 (10) 图片占位 .stone-placeholder 石青渐变呼吸脉冲。文档每个 section 加入实现文件路径和状态标记。 + +### [CL-20260416-1330] 2026-04-16 13:30 — 交互体验全面升级:信息层级 + 科技质感 + 微交互趣味性 +- **tags**: 前端, UX升级, 动效, framer-motion, 排版体系, 微交互, 视觉层级, 科技感, 粒子, 流光 +- **affected_files**: + - art-agent/frontend/src/app/globals.css + - art-agent/frontend/src/app/page.tsx + - art-agent/frontend/src/app/login/page.tsx + - art-agent/frontend/src/app/gallery/page.tsx + - art-agent/frontend/src/components/chat/chat-messages.tsx + - art-agent/frontend/src/components/chat/chat-input.tsx + - art-agent/frontend/src/components/chat/image-grid.tsx + - art-agent/frontend/src/components/chat/model-selector.tsx + - art-agent/frontend/src/components/sidebar/sidebar.tsx + - art-agent/frontend/src/components/sidebar/session-list.tsx + - art-agent/frontend/src/components/layout/top-nav.tsx + - art-agent/frontend/src/components/detail/image-detail-panel.tsx + - art-agent/frontend/src/components/profile-modal.tsx + - art-agent/frontend/src/components/ui/motion-presets.ts (新建) + - art-agent/frontend/src/components/ui/ambient-particles.tsx (新建) + - art-agent/frontend/src/components/ui/data-stream.tsx (新建) + - art-agent/frontend/src/components/ui/ripple-effect.tsx (新建) + - art-agent/frontend/src/components/ui/firefly-burst.tsx (新建) + - docs/art-agent/UX-STYLE-GUIDE.md (新建) +- **what**: 四阶段全面升级交互体验:信息层级体系、科技质感注入、微交互趣味性、统一状态反馈 +- **why**: 用户要求信息层级差异更突出、画面更高级典雅且具科技感、交互有巧思让使用心情愉悦 +- **decisions**: + - 新增 framer-motion 作为动画引擎(~30KB gzipped),不引入其他 UI 库 + - 排版从 5 级扩展到 7 级(Display 渐变文字 / H1 底部石青线 / H2 左侧竖线 / Body Strong 石青色 / Caption / Micro 大写加宽字间距),CSS 工具类 `.typo-*` + - Surface 层级从 1 级扩展到 4 级(Surface-0 背景 → Surface-3 弹窗),每级递增 backdrop-blur、border 亮度、box-shadow + - 背景网格底纹(radial-gradient 点阵 opacity 0.03)从 body::before 实现,原云雾改为独立 DOM 元素 `.fog-layer-far/near` + - 环境粒子纯 CSS 实现(15-28 个随机漂浮+闪烁的石青色小光点),空态时增多营造"未探索深山"感 + - 数据流光(石青色 linear-gradient 横向流动 2px),AI 活动时出现在输入栏上方,生图时加速 + - 用户消息改为右对齐渐变背景+右上切角,助手消息改为 surface-2 + 左侧 2px 石青竖线 + - 消息入场动画:用户消息 translateY+scale spring,助手消息 blur 消散(云雾凝聚) + - 发送按钮 whileTap scale 0.85,可发送时呼吸脉冲发光 + - 输入框水面感应:焦点时底部 scaleX 扩展线 + box-shadow 随输入长度增强 + - 收藏星标改为赭金色,点击时 5-7 个金色粒子向四周飘散(萤火点亮) + - 图片水墨晕染:skeleton shimmer 占位 → clip-path circle 从中心扩展揭示 + - Loading 从 bounce 圆点改为水波纹 scaleY 动画,skeleton 用 shimmer 横向扫描 + - 错误状态赭石红 + translateX 震动 2px × 4 次 + - 空态使用 Display 级渐变文字 + stagger 入场动画 + - 滚到底部按钮改圆形 + spring 弹入/退出 + - 侧栏会话项 hover 时左侧 2px 竖线 slide-in + - 所有按钮 hover 添加 translateY(-1px) + shadow 加深浮起效果 + - 所有弹出层改用 surface-3(强模糊+明显阴影+更亮边框) + - 统一动画参数:motion-presets.ts 定义 spring/springGentle、fadeIn/slideUp/mistReveal、messageEnter/assistantEnter、buttonTap/sendBounce、ripple/sparkle/inkReveal、staggerContainer/staggerItem +- **notes**: + - framer-motion 通过 npm install 添加(3 个包),前端 bundle 增加约 30KB gzipped + - 环境粒子使用 CSS animation 而非 Canvas,不影响性能 + - 所有动画使用 transform/opacity/filter,避免触发 layout + - body::before 原来用于云雾现改为点阵网格,云雾改为 AmbientParticles 组件内的 DOM 元素 + - 构建验证通过(next build 无错误),First Load JS 增量约 50KB(framer-motion) +- **source_chat**: [交互体验全面升级](6b0e5860-9c5a-4b44-aa96-affba21e9472) + +### [CL-20260416-1000] 2026-04-16 10:00 — 集成 Zero123++ 视角变换工具(单图→6视角) +- **tags**: 视角变换, Zero123++, Novel View Synthesis, 新工具, Replicate, 图片处理 +- **affected_files**: + - art-agent/backend/app/config.py + - art-agent/backend/app/agent/tools.py + - art-agent/backend/app/agent/loop.py + - art-agent/backend/app/services/view_transform.py +- **what**: 新增 `transform_view` 工具,集成 Zero123++ 模型实现单图生成 6 个固定视角图片 +- **why**: 用户需要将手绘建筑从侧面视角转为其他角度,视角变换是与生图完全不同的能力,需要独立工具 +- **decisions**: + - 视角变换作为独立工具 `transform_view` 而非 `generate_image` 的扩展——输入不需要 prompt,只需一张图 + - 新增独立服务模块 `view_transform.py`,与 `image_gen.py` 分离——逻辑完全不同(无 prompt、grid 拆分) + - 在 `config.py` 中新增 `VIEW_TRANSFORM_MODELS` 独立注册表——与 `IMAGE_MODELS` 分离,未来可扩展更多视角变换模型 + - Zero123++ 输出为 3x2 grid 拼接图,用 Pillow 拆分为 6 张独立图片返回 + - 进阶方案(3D 重建 + ControlNet 风格还原 pipeline)记录到延期方案 `[advanced-view-transform]` +- **notes**: + - Zero123++ 视角固定(Azimuth: 30/90/150/210/270/330°, Elevation: 30/-20 交替),不支持自定义角度 + - 手绘风格保持度可能不理想(模型偏向写实/3D 风格),需实际测试验证 + - 输入图片需为正方形,建议 >= 320x320 + - Replicate 上的模型 ID: `jd7h/zero123plusplus:c69c6559...`,~$0.074/次,~76s + - loop.py 中对 `transform_view` 做了特殊结果处理(返回带视角信息的图片列表) + +### [CL-20260416-0930] 2026-04-16 09:30 — 设备绑定配置与 Skill/Hooks 解耦(local-env.json) +- **tags**: 基础设施, local-env, 跨设备, hooks, project-launcher, Skill +- **affected_files**: + - .cursor/local-env.json + - .cursor/local-env.example.json + - .cursor/hooks.json + - .cursor/hooks/run-hook.ps1 + - .cursor/hooks/session-init.sh + - .cursor/hooks/check-changelog.sh + - .cursor/hooks/README.md + - .cursor/rules/project/local-env-check.mdc + - .cursor/skills/project-launcher/SKILL.md + - .gitignore + - art-agent/ENVIRONMENT.md +- **what**: 将设备绑定的配置(Node.js 路径、shell 类型)从 Skill 和 hooks 中解耦到 `.cursor/local-env.json`(gitignored)。project-launcher Skill 改为从 local-env.json 读取路径;hooks 增加 run-hook.ps1 dispatcher + bash 版脚本支持跨平台;新增 local-env-check Rule 自动探测生成;ENVIRONMENT.md 去除平台硬编码改为通用说明 +- **why**: Node.js 路径和 shell 类型在不同设备上不同,硬编码在 Skill/hooks 中导致换机或跨平台时需要修改 git tracked 文件 +- **decisions**: 采用 `.cursor/local-env.json`(gitignored)+ `.cursor/local-env.example.json`(tracked 模板)的模式,类似 `.env` / `.env.example` 的惯例;hooks.json 的跨平台靠为每个 hook 提供 .ps1 和 .sh 双版本 +- **notes**: hooks.json 的 command 字段本身仍需按平台手动调整(Windows 用 powershell,macOS/Linux 用 bash),这是 Cursor hooks 机制的限制 +- **source_chat**: [local-env 设备配置解耦] + +### [CL-20260416-0900] 2026-04-16 09:00 — 环境依赖全面修复 + 统一环境文档 +- **tags**: 环境, 依赖, Node.js, Python, venv, npm, requirements, ENVIRONMENT +- **affected_files**: + - art-agent/backend/requirements.txt + - art-agent/backend/requirements-lock.txt + - art-agent/ENVIRONMENT.md + - .cursor/skills/project-launcher/SKILL.md +- **what**: 排查并修复缺失的前后端依赖(venv 和 node_modules 均不存在),安装 Node.js v22.15.0 到 %LOCALAPPDATA%\nodejs\,创建 Python venv 并安装全部后端依赖,npm install 前端依赖;requirements.txt 从范围版本更新为精确锁定版本;新增 requirements-lock.txt(完整依赖树 61 个包)和 ENVIRONMENT.md 统一环境文档 +- **why**: venv 和 node_modules 被 gitignore,换机/重装后需要重新搭建;原 Node.js 安装已丢失;requirements.txt 仅用 >= 范围无版本锁定 +- **decisions**: Node.js 采用 zip 解压到 %LOCALAPPDATA% 而非 MSI 安装(无管理员权限);requirements.txt 直接锁定到当前精确版本而非继续用范围约束 +- **notes**: project-launcher Skill 中的 Node.js 路径已同步更新为 %LOCALAPPDATA%\nodejs\ +- **source_chat**: [环境依赖修复与文档生成] + ### [CL-20260416-0830] 2026-04-16 08:30 — 生图按模型预处理 prompt(官网/社区策略对齐) - **tags**: 后端, 生图, prompt, SDXL, Flux, GPT Image, Gemini, Replicate, IP-Adapter, image_gen, Agent - **affected_files**: diff --git a/.cursor/changelog/changelog-headlines.md b/.cursor/changelog/changelog-headlines.md index f820f76..a94d596 100644 --- a/.cursor/changelog/changelog-headlines.md +++ b/.cursor/changelog/changelog-headlines.md @@ -2,6 +2,12 @@ 最近 ~50 次改动的一句话概要,按时间倒序排列。每次会话自动注入上下文。 +- [CL-20260416-1600] 视觉风格从暗绿底全面转为白底青绿(绢本山水):背景#FAFAF7+文字#1A1A1A+accent#2E8B7A,13个文件颜色替换,面板改白色毛玻璃,云雾透明度降低,VISUAL-STYLE-GUIDE.md重写 +- [CL-20260416-1500] UX风格指南实现度补全:涟漪/sendBounce/会话切换动画/Error飘散/云雾加载/流式opacity/排版类使用/左上切角/石青占位等10项全部落地,文档每项标注✅状态+实现文件 +- [CL-20260416-1330] 交互体验全面升级:新增framer-motion动画引擎,7级排版体系+4级Surface层级+网格底纹科技感,萤火粒子/数据流光/水墨晕染/投石入水/云雾凝聚/萤火点亮等微交互,用户/助手消息视觉拉开差距,统一Loading/空态/Error状态,全组件hover浮起+侧栏竖线+呼吸脉冲,新增UX-STYLE-GUIDE.md文档 +- [CL-20260416-1000] 集成Zero123++视角变换:新增transform_view工具+view_transform.py服务,单图输入生成6个固定视角图片(grid拆分),config.py新增VIEW_TRANSFORM_MODELS注册表,loop.py特殊处理视角变换SSE事件,进阶方案记入延期 +- [CL-20260416-0930] 设备绑定配置解耦:新增.cursor/local-env.json(gitignored)存放nodejs_path和shell,project-launcher Skill和hooks改为读取该文件;hooks增加bash版+dispatcher跨平台;新增local-env-check Rule自动探测;ENVIRONMENT.md去除硬编码 +- [CL-20260416-0900] 环境依赖全面修复+统一环境文档:安装Node.js v22.15.0+创建Python venv+安装全部前后端依赖;requirements.txt锁定精确版本;新增requirements-lock.txt和ENVIRONMENT.md;更新project-launcher Skill路径 - [CL-20260416-0830] 生图按模型预处理 prompt:新增 image_prompt_strategy(SDXL 默认负向词+---NEGATIVE---拆分,Flux 剥粘贴的 Negative 段,GPT/Gemini/IP-Adapter 规范化);GenerateResult 带 effective_prompt/negative_prompt;Replicate SDXL 传 negative_prompt;工具说明 SDXL 分隔符 - [CL-20260416-0810] 对话输入栏支持剪贴板粘贴图片为参考图:ChatInput根容器onPasteCapture提取image/*文件并走startUpload,placeholder/title提示 - [CL-20260416-0745] Gemini原生生图:延长读写超时+对RemoteProtocolError等可重试错误自动重试(默认3次指数退避),环境变量可微调VECTORENGINE_GEMINI_*_TIMEOUT diff --git a/.cursor/changelog/changelog-recent.md b/.cursor/changelog/changelog-recent.md index d15d384..f7dc98c 100644 --- a/.cursor/changelog/changelog-recent.md +++ b/.cursor/changelog/changelog-recent.md @@ -3,6 +3,36 @@ 最近 ~10 次改动的摘要记录,按时间倒序排列。 当 Agent 检测到当前任务与近期改动相关时自动读取。 +### [CL-20260416-1600] 2026-04-16 — 视觉风格从暗绿底全面转为白底青绿(绢本山水) +- **tags**: 前端, 视觉风格, 色彩体系, CSS变量, 白底, 千里江山图 +- **affected_files**: globals.css, page.tsx, login/page.tsx, gallery/page.tsx, chat-messages.tsx, chat-input.tsx, top-nav.tsx, sidebar.tsx, session-list.tsx, profile-modal.tsx, image-detail-panel.tsx, ambient-particles.tsx, VISUAL-STYLE-GUIDE.md +- **summary**: 背景从墨绿黑(#0C1210)改为绢本白(#FAFAF7),文字改为墨黑(#1A1A1A),accent色加深(#4DB8A4→#2E8B7A)适配白底,所有面板从深色毛玻璃改为白色毛玻璃,13个文件中硬编码的旧rgba全部替换,云雾/粒子透明度降低,VISUAL-STYLE-GUIDE.md同步重写。设计理念:千里江山图是青绿设色于绢本白底,白底留白才能让青绿有呼吸感。 + +### [CL-20260416-1500] 2026-04-16 — UX 风格指南实现度补全(10 项未实现特性 + 文档状态标注) +- **tags**: 前端, UX补全, 动效, framer-motion, 微交互, 文档 +- **affected_files**: chat-input.tsx, chat-messages.tsx, image-grid.tsx, motion-presets.ts, page.tsx, login/page.tsx, gallery/page.tsx, globals.css, app-context.tsx, sidebar.tsx, profile-modal.tsx, UX-STYLE-GUIDE.md +- **summary**: 逐项分析 UX-STYLE-GUIDE.md 中所有特性的实现度(原 ~65%),补全 10 项未实现特性:涟漪接入发送按钮、sendBounce 弹缩、会话切换滑入滑出动画、Error 飘散消失+自动清除、回到底部水滴形、全屏云雾凝聚加载、AI 回复逐行 opacity 阶梯、typo-h1/h2/strong 实际使用、用户消息左上切角、图片占位石青渐变呼吸脉冲。文档更新为每项标注 ✅ 状态 + 实现文件路径。 + +### [CL-20260416-1330] 2026-04-16 — 交互体验全面升级:信息层级 + 科技质感 + 微交互趣味性 +- **tags**: 前端, UX升级, 动效, framer-motion, 排版体系, 微交互, 视觉层级, 科技感 +- **affected_files**: globals.css, page.tsx, chat-messages.tsx, chat-input.tsx, image-grid.tsx, session-list.tsx, sidebar.tsx, top-nav.tsx, image-detail-panel.tsx, profile-modal.tsx, model-selector.tsx, login/page.tsx, gallery/page.tsx, UX-STYLE-GUIDE.md, motion-presets.ts, ambient-particles.tsx, data-stream.tsx, ripple-effect.tsx, firefly-burst.tsx +- **summary**: 四阶段全面升级。Phase 1:globals.css 新增 7 级排版体系(Display 渐变→Micro 大写)+ 4 级 Surface 层级 + 点阵网格底纹 + 水波纹/shimmer/涟漪等CSS动画。Phase 2:新建环境萤火粒子组件(纯CSS)+ 数据流光组件(AI 活动时输入栏上方),用户/助手消息视觉差异(渐变背景+切角 vs glass-panel+左侧竖线)。Phase 3:引入 framer-motion,统一动画参数 motion-presets.ts;发送按钮投石入水(spring 弹缩+呼吸脉冲)、输入框水面感应(焦点扩展线+亮度随输入增强)、AI 回复云雾凝聚(blur 消散入场)、收藏萤火点亮(金色粒子爆发)、图片水墨晕染(clip-path 圆形揭示)。Phase 4:统一 Loading 水波纹 + shimmer 骨架屏 + 错误赭石红震动 + 空态 Display 渐变欢迎语 + stagger 入场,滚到底部按钮改圆形 spring 弹入,侧栏会话左侧竖线 hover,全组件 hover 浮起。新增 UX-STYLE-GUIDE.md 交互体验文档。 + +### [CL-20260416-1000] 2026-04-16 — 集成 Zero123++ 视角变换工具(单图→6视角) +- **tags**: 视角变换, Zero123++, Novel View Synthesis, 新工具, Replicate +- **affected_files**: config.py, tools.py, loop.py, view_transform.py +- **summary**: 新增 `transform_view` 工具集成 Zero123++(Replicate),输入一张图片生成 6 个固定视角图片。独立服务模块 `view_transform.py` 处理 Replicate 调用 + grid 拼接图 Pillow 拆分。config.py 新增 `VIEW_TRANSFORM_MODELS` 注册表。loop.py 对视角变换结果做特殊 SSE 事件处理。进阶方案(3D重建+ControlNet)记入延期方案。 + +### [CL-20260416-0930] 2026-04-16 — 设备绑定配置与 Skill/Hooks 解耦(local-env.json) +- **tags**: 基础设施, local-env, 跨设备, hooks, project-launcher, Skill +- **affected_files**: local-env.json, local-env.example.json, hooks.json, run-hook.ps1, session-init.sh, check-changelog.sh, local-env-check.mdc, project-launcher/SKILL.md, .gitignore, ENVIRONMENT.md +- **summary**: 新增 `.cursor/local-env.json`(gitignored)存放设备绑定配置(nodejs_path、shell),project-launcher Skill 和 hooks 均改为从中读取。hooks 增加 bash 版脚本 + PowerShell dispatcher 支持跨平台。新增 `local-env-check` Rule 在会话开始时自动探测生成。ENVIRONMENT.md 去除平台硬编码改为通用说明。 + +### [CL-20260416-0900] 2026-04-16 — 环境依赖全面修复 + 统一环境文档 +- **tags**: 环境, 依赖, Node.js, Python, venv, npm, requirements, ENVIRONMENT +- **affected_files**: requirements.txt, requirements-lock.txt, ENVIRONMENT.md, project-launcher/SKILL.md +- **summary**: 排查缺失依赖(venv/node_modules 均不存在),安装 Node.js v22.15.0 到 %LOCALAPPDATA%\nodejs\,创建后端 venv 并安装全部依赖(14 个直接 + 47 个间接),npm install 前端依赖(46 个包)。requirements.txt 锁定精确版本,新增 requirements-lock.txt 和 ENVIRONMENT.md 统一环境文档。project-launcher Skill Node.js 路径已同步更新。 + ### [CL-20260416-0830] 2026-04-16 — 生图按模型预处理 prompt - **tags**: 后端, 生图, prompt, SDXL, Flux, Replicate, image_gen, Agent工具 - **affected_files**: image_prompt_strategy.py, image_gen.py, tools.py diff --git a/.cursor/deferred/registry.md b/.cursor/deferred/registry.md index e0b31cb..2a51222 100644 --- a/.cursor/deferred/registry.md +++ b/.cursor/deferred/registry.md @@ -147,6 +147,44 @@ - **chosen_alternative**: 当前资源库使用手动标签 + Prompt 文本搜索 - **deferred_reason**: 当前阶段资源量少,手动管理足够;等资源积累到一定量级后再引入自动化 +### [advanced-view-transform] 进阶视角变换 Pipeline(3D 重建 + ControlNet 风格还原) +- **status**: deferred +- **tags**: 视角变换, 3D重建, ControlNet, Zero123++, TripoSR, InstantMesh, Novel View Synthesis, 手绘风格, IP-Adapter +- **recorded**: 2026-04-16 +- **source_chat**: [视角变换方案讨论与 Zero123++ 集成](view-transform-zero123plus) +- **prerequisite**: Zero123++ 基础验证完成、确认视角变换是高频需求后 +- **related_files**: + - art-agent/backend/app/services/view_transform.py + - art-agent/backend/app/config.py + - art-agent/backend/app/agent/tools.py +- **context**: | + 当前已集成 Zero123++(固定 6 视角输出),但存在两个局限: + 1. 视角固定(6 个预设角度),无法自定义任意角度 + 2. 手绘风格保持度不够——Zero123++ 更擅长写实/3D 渲染风格 + + 进阶方案分三个层级: + + A. 更强的多视角生成模型: + - Era3D:高质量多视角一致性,建筑类友好 + - SyncDreamer:同步多视角生成,一致性好 + - Wonder3D:多视角 + 法线图,精确视角控制 + + B. ControlNet 引导的视角变换: + - 用 Depth Anything V2 从原图提取深度图 + - 用 ControlNet(depth/normal 模式)+ 目标视角描述重新生成 + - 通过 IP-Adapter 注入原图风格保持手绘风格 + + C. 最佳质量方案(多步组合 Pipeline): + 原图 → TripoSR/InstantMesh 3D 重建 → 旋转到任意目标角度渲染灰度/线稿 + → ControlNet(canny/depth) + IP-Adapter(原图风格) → 手绘风格新视角图 + 这是质量最高的方案:视角精准 + 风格保真 + + 实施依赖: + - 方案 B/C 需要 ComfyUI 环境(与 hf-inference-endpoints 延期方案关联) + - 方案 C 的 TripoSR/InstantMesh 可先在 Replicate 上验证 +- **chosen_alternative**: 当前使用 Zero123++ 固定 6 视角输出(基础验证阶段) +- **deferred_reason**: 先用 Zero123++ 验证视角变换的实际需求频率和用户反馈,再投入进阶方案 + --- ## Completed / Cancelled Items diff --git a/.cursor/distillery/golden-rules.md b/.cursor/distillery/golden-rules.md new file mode 100644 index 0000000..9b0d868 --- /dev/null +++ b/.cursor/distillery/golden-rules.md @@ -0,0 +1,5 @@ +# Golden Rules + +经过实践验证(权重 >= 5)的精炼认知,每次新会话自动注入。 + + diff --git a/.cursor/distillery/insights.md b/.cursor/distillery/insights.md new file mode 100644 index 0000000..aaeb59c --- /dev/null +++ b/.cursor/distillery/insights.md @@ -0,0 +1,8 @@ +# Problem Distillery — Insights + +从已解决的顽固问题中蒸馏出的精炼方法论。 + + + +--- +last_distill_date: 2026-04-16 diff --git a/.cursor/distillery/problems.md b/.cursor/distillery/problems.md new file mode 100644 index 0000000..c27e8f4 --- /dev/null +++ b/.cursor/distillery/problems.md @@ -0,0 +1,5 @@ +# Problem Distillery — Problems + +反复出现的顽固问题追踪记录,按时间倒序排列。 + + diff --git a/.cursor/hooks.json b/.cursor/hooks.json index f3c7ae3..b289cdb 100644 --- a/.cursor/hooks.json +++ b/.cursor/hooks.json @@ -3,13 +3,13 @@ "hooks": { "sessionStart": [ { - "command": "powershell -ExecutionPolicy Bypass -File .cursor/hooks/session-init.ps1", + "command": "powershell -ExecutionPolicy Bypass -File .cursor/hooks/run-hook.ps1 session-init", "timeout": 5 } ], "stop": [ { - "command": "powershell -ExecutionPolicy Bypass -File .cursor/hooks/check-changelog.ps1", + "command": "powershell -ExecutionPolicy Bypass -File .cursor/hooks/run-hook.ps1 check-changelog", "timeout": 10, "loop_limit": 1 } diff --git a/.cursor/hooks/README.md b/.cursor/hooks/README.md new file mode 100644 index 0000000..51f3c71 --- /dev/null +++ b/.cursor/hooks/README.md @@ -0,0 +1,38 @@ +# Cursor Hooks 说明 + +## 结构 + +``` +hooks/ +├── run-hook.ps1 # Windows dispatcher(读取 local-env.json 后分发) +├── session-init.ps1 # sessionStart hook(PowerShell 版) +├── session-init.sh # sessionStart hook(bash 版) +├── check-changelog.ps1 # stop hook(PowerShell 版) +├── check-changelog.sh # stop hook(bash 版) +└── README.md # 本文件 +``` + +## 跨平台适配 + +`hooks.json` 中的 `command` 字段是**平台绑定的**——Windows 用 `powershell`,macOS/Linux 用 `bash`。 + +### Windows(当前) + +hooks.json 使用 `powershell ... run-hook.ps1` 作为入口,dispatcher 根据 +`.cursor/local-env.json` 的 `shell` 字段决定执行 `.ps1` 还是 `.sh` 脚本。 + +### 迁移到 macOS / Linux + +将 hooks.json 的 command 改为直接调用 `.sh` 脚本: + +```json +{ + "version": 1, + "hooks": { + "sessionStart": [{ "command": "bash .cursor/hooks/session-init.sh", "timeout": 5 }], + "stop": [{ "command": "bash .cursor/hooks/check-changelog.sh", "timeout": 10, "loop_limit": 1 }] + } +} +``` + +同时更新 `.cursor/local-env.json` 的 `shell` 字段为 `bash` 或 `zsh`。 diff --git a/.cursor/hooks/check-changelog.sh b/.cursor/hooks/check-changelog.sh new file mode 100644 index 0000000..5265952 --- /dev/null +++ b/.cursor/hooks/check-changelog.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# Changelog sync guard — stop hook (bash 版) + +input=$(cat) + +# === 豁免检查 1:环境变量跳过标志 === +if [ -n "$CURSOR_SKIP_CHANGELOG" ]; then + echo '{}'; exit 0 +fi + +# === 豁免检查 2:从 stdin 解析 composer_mode === +mode=$(echo "$input" | python3 -c "import sys,json; print(json.load(sys.stdin).get('composer_mode',''))" 2>/dev/null || echo "") +if [ -n "$mode" ] && [ "$mode" != "agent" ]; then + echo '{}'; exit 0 +fi + +# === 豁免检查 3:从 stdin 文本匹配 debug 上下文关键词 === +if echo "$input" | grep -qiE '"mode"\s*:\s*"debug"|debug[\s_-]?mode|Debug Mode'; then + echo '{}'; exit 0 +fi + +changelog=".cursor/changelog/changelog-headlines.md" +srcDir="art-agent" + +# === 豁免检查 4:changelog 文件不存在 === +if [ ! -f "$changelog" ]; then + echo '{}'; exit 0 +fi + +clMtime=$(stat -c %Y "$changelog" 2>/dev/null || stat -f %m "$changelog" 2>/dev/null) + +# === 豁免检查 5:ack 标记文件足够新 === +ackFile=".cursor/changelog/.changelog-ack" +if [ -f "$ackFile" ]; then + ackMtime=$(stat -c %Y "$ackFile" 2>/dev/null || stat -f %m "$ackFile" 2>/dev/null) + if [ "$ackMtime" -ge "$clMtime" ] 2>/dev/null; then + echo '{}'; exit 0 + fi +fi + +# === 豁免检查 6:stdin 中无文件编辑证据 === +if ! echo "$input" | grep -qE 'StrReplace|Write\s*tool|edit_file|file_write|write_to_file|Created file|Modified file|Wrote contents|"tool"\s*:\s*"(str_replace|write|edit)"'; then + echo '{}'; exit 0 +fi + +# === 核心检查:是否有源文件比 changelog 更新 === +newerFile=$(find "$srcDir" -type f \( -name "*.py" -o -name "*.tsx" -o -name "*.ts" -o -name "*.css" \) \ + ! -path "*/node_modules/*" ! -path "*/.next/*" ! -path "*/__pycache__/*" ! -path "*/venv/*" \ + -newer "$changelog" -print -quit 2>/dev/null) + +if [ -n "$newerFile" ]; then + fname=$(basename "$newerFile") + msg="[Hook] Source file updated (e.g. $fname) but changelog not synced. Run dev-changelog Skill operation A NOW to write all three changelog layers." + echo "{\"followup_message\":\"$msg\"}" +else + echo '{}' +fi +exit 0 diff --git a/.cursor/hooks/run-hook.ps1 b/.cursor/hooks/run-hook.ps1 new file mode 100644 index 0000000..a6b726b --- /dev/null +++ b/.cursor/hooks/run-hook.ps1 @@ -0,0 +1,35 @@ +# run-hook.ps1 — 通用 hook dispatcher +# 从 .cursor/local-env.json 读取 shell 类型,决定执行 .ps1 还是 .sh 脚本 +# 用法:powershell -ExecutionPolicy Bypass -File .cursor/hooks/run-hook.ps1 +# 示例:run-hook.ps1 session-init → 执行 session-init.ps1 或 session-init.sh + +param([string]$HookName) + +$localEnvPath = ".cursor\local-env.json" +$shell = "powershell" + +if (Test-Path $localEnvPath) { + try { + $config = Get-Content $localEnvPath -Raw | ConvertFrom-Json + if ($config.shell) { $shell = $config.shell } + } catch {} +} + +$hookDir = ".cursor\hooks" +$input = [Console]::In.ReadToEnd() + +if ($shell -eq "powershell") { + $scriptPath = Join-Path $hookDir "$HookName.ps1" + if (Test-Path $scriptPath) { + $input | powershell -ExecutionPolicy Bypass -File $scriptPath + } else { + Write-Output '{}' + } +} else { + $scriptPath = Join-Path $hookDir "$HookName.sh" + if (Test-Path $scriptPath) { + $input | bash $scriptPath + } else { + Write-Output '{}' + } +} diff --git a/.cursor/hooks/session-init.sh b/.cursor/hooks/session-init.sh new file mode 100644 index 0000000..34bc498 --- /dev/null +++ b/.cursor/hooks/session-init.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# session-init.sh — 会话启动时检测 composer_mode,非 agent 模式设置跳过标志 + +input=$(cat) + +mode=$(echo "$input" | python3 -c "import sys,json; print(json.load(sys.stdin).get('composer_mode',''))" 2>/dev/null || echo "") + +rm -f ".cursor/changelog/.changelog-ack" + +if [ -n "$mode" ] && [ "$mode" != "agent" ]; then + echo "{\"env\":{\"CURSOR_SKIP_CHANGELOG\":\"1\",\"CURSOR_COMPOSER_MODE\":\"$mode\"}}" +else + echo "{}" +fi +exit 0 diff --git a/.cursor/local-env.example.json b/.cursor/local-env.example.json new file mode 100644 index 0000000..963a16f --- /dev/null +++ b/.cursor/local-env.example.json @@ -0,0 +1,5 @@ +{ + "_comment": "设备绑定的本地环境配置。复制为 local-env.json 并修改为本机实际值。", + "nodejs_path": "C:\\Users\\\\AppData\\Local\\nodejs", + "shell": "powershell" +} diff --git a/.cursor/rules/common/distillery-recall.mdc b/.cursor/rules/common/distillery-recall.mdc new file mode 100644 index 0000000..f47815f --- /dev/null +++ b/.cursor/rules/common/distillery-recall.mdc @@ -0,0 +1,44 @@ +## Problem Distillery 上下文注入 + +### Golden Rules 注入 + +每次会话处理用户第一个任务前,执行以下操作: + +1. 读取 `.cursor/distillery/golden-rules.md`(不存在则跳过) +2. 如文件存在且有实质条目(不仅是标题),将全部条目作为背景知识注入上下文 +3. 这些是经过实践反复验证的精炼认知,Agent 在分析和解决问题时应自然参考 +4. 不需要在回复中显式提及"根据 Golden Rule" + +### 蒸馏提醒检查 + +每次会话处理用户第一个任务前,额外检查: + +1. 读取 `.cursor/distillery/insights.md`(不存在则跳过) +2. 检查文件尾部的 `last_distill_date` 字段 +3. 如果距今超过 7 天,读取 `.cursor/distillery/problems.md` +4. 统计 `status: resolved` 且无 `distilled:` 标记的条目数量 +5. 如有未蒸馏的已解决条目,提醒用户: + > 你有 N 个已解决的顽固问题尚未总结,要花几分钟蒸馏一下吗? +6. 每次会话最多提醒一次 + +### 顽固问题检测 + +Agent 在整个对话过程中应保持对以下信号的被动感知: + +1. 用户表达问题未解决:"还有问题"、"没解决"、"还是一样"、"又出现了"、"不行"、"没用" +2. Agent 自身意识到同一问题已尝试 2 次以上仍未解决 +3. pitfall-journal 中已有记录的问题再次出现 + +检测到上述信号时,读取 `problem-distillery` Skill 并执行其操作 A。 + +### 验证触发 + +当操作 A 触发时(追踪新的或再次出现的顽固问题),如果 `insights.md` 非空, +还应执行操作 D 的被动验证流程——匹配是否有相关的已蒸馏经验可供参考。 + +### 注意 + +- Golden Rules 注入是极轻量操作(预期 < 30 行),每次会话都执行 +- 蒸馏提醒按需触发,只在条件满足时提醒 +- problems.md 的详细记录**不主动读取**,仅在操作 A/B/C 时按需读取 +- 本 Rule 只负责触发和注入,具体操作流程由 `problem-distillery` Skill 定义 diff --git a/.cursor/rules/project/local-env-check.mdc b/.cursor/rules/project/local-env-check.mdc new file mode 100644 index 0000000..b6af07d --- /dev/null +++ b/.cursor/rules/project/local-env-check.mdc @@ -0,0 +1,47 @@ +--- +description: 检查本地环境配置文件 .cursor/local-env.json 是否存在,不存在时自动探测并生成 +globs: +alwaysApply: true +--- + +## 本地环境配置自动初始化 + +每次会话处理用户第一个任务前,检查 `.cursor/local-env.json` 是否存在: + +### 文件存在时 + +读取并验证 JSON 格式正确、包含 `nodejs_path` 和 `shell` 两个字段。 +字段缺失时补充探测(见下方流程),不覆盖已有字段。 + +### 文件不存在时 + +执行自动探测流程,生成 `.cursor/local-env.json`: + +``` +1. 探测 shell 类型: + - Windows($env:OS 包含 "Windows")→ "powershell" + - macOS/Linux → 读取 $SHELL 环境变量末段(bash/zsh/fish 等) + +2. 探测 Node.js 路径: + - Windows:运行 `where.exe node`,排除 Cursor 内置的 node + (路径含 "cursor" 的不算),取第一个有效结果的父目录 + - macOS/Linux:运行 `which node`,取父目录 + - 找不到 → 设为 null + +3. 写入 .cursor/local-env.json: + { + "nodejs_path": "<探测到的路径或 null>", + "shell": "<探测到的 shell>" + } + +4. 简要告知用户: + > 已自动生成本地环境配置 `.cursor/local-env.json`, + > 检测到 Node.js 路径为 xxx,shell 为 xxx。如需调整请手动编辑该文件。 +``` + +### 注意事项 + +- `local-env.json` 已在 `.gitignore` 中,不会进入版本控制 +- 该文件存储**设备绑定**的配置,与 `.env`(存储密钥/API key)互补 +- 只在文件不存在或字段缺失时触发探测,不会每次会话都重新探测 +- 探测结果不一定准确(如多个 Node.js 版本并存),用户可随时手动修改 diff --git a/.cursor/skills/epee-orchestrator/registry.md b/.cursor/skills/epee-orchestrator/registry.md index 4934ab2..bba1e5e 100644 --- a/.cursor/skills/epee-orchestrator/registry.md +++ b/.cursor/skills/epee-orchestrator/registry.md @@ -49,10 +49,18 @@ - **路径**: .cursor/skills/pitfall-journal/SKILL.md - **备注**: 与 dev-changelog 互补——changelog 记事实,pitfall 记经验;由 pitfall-recall Rule 触发自动检索 +### problem-distillery +- **类型**: 基础设施 +- **能力**: 追踪反复出现的顽固问题,记录解决过程和弯路,定期蒸馏精炼方法论,经实践验证后自动升级为新会话常规注入 +- **触发场景**: "还有问题"、"没解决"、"还是一样"、"又出现了"、"试了好多次"、"反复出现"、Agent 意识到同一问题尝试 2 次以上未解决、"蒸馏"、"总结经验" +- **输出**: .cursor/distillery/ 下的 problems.md、insights.md、golden-rules.md +- **路径**: .cursor/skills/problem-distillery/SKILL.md +- **备注**: 与 pitfall-journal 互补——pitfall 是事后快照,distillery 是过程追踪 + 知识提炼;golden-rules 由 distillery-recall Rule 每次会话注入 + ### project-launcher - **类型**: 项目级 - **能力**: 一键启动 Art Agent 全部服务(Ollama + 后端 FastAPI + 前端 Next.js),自动检测已运行的服务并跳过 - **触发场景**: "启动项目"、"运行项目"、"跑起来"、"打开前端和后端"、"启动服务"、"start"、"launch"、"run dev"、"启动前端"、"启动后端" - **输出**: Ollama 后台服务 + 两个独立的终端窗口(前端 Next.js + 后端 FastAPI) - **路径**: .cursor/skills/project-launcher/SKILL.md -- **备注**: 项目专属 Skill;Ollama 必须先于后端启动(Mem0 embedding 依赖);前后端窗口必须可见 +- **备注**: 项目专属 Skill;启动前必须先读取 `.cursor/local-env.json` 获取 nodejs_path 和 shell;Ollama 必须先于后端启动(Mem0 embedding 依赖);前后端窗口必须可见 diff --git a/.cursor/skills/problem-distillery/SKILL.md b/.cursor/skills/problem-distillery/SKILL.md new file mode 100644 index 0000000..5dfa12e --- /dev/null +++ b/.cursor/skills/problem-distillery/SKILL.md @@ -0,0 +1,232 @@ +--- +name: problem-distillery +description: >- + 从顽固问题中蒸馏方法论的渐进式知识系统。追踪反复出现且未被彻底解决的问题, + 记录解决过程和弯路,定期提炼精炼认知,经实践验证后自动升级为常规注入。 + 当用户表达"还有问题"、"没解决"、"还是一样"、"又出现了"等语义, + 或 Agent 自身意识到同一问题已尝试 2 次以上仍未解决时触发。 +--- + +# Problem Distillery + +从反复出现的顽固问题中,经过"追踪 → 解决 → 蒸馏 → 验证 → 注入"的完整生命周期, +蒸馏出经过实践验证的精炼认知和方法论。 + +与其他系统的区别: +- **pitfall-journal**:互补。pitfall 是事后快照(debug 完记一笔),distillery 是过程追踪 + 知识提炼 +- **dev-changelog**:不重叠。changelog 记事实改动,distillery 记问题解决过程和认知沉淀 + +## 数据文件 + +``` +.cursor/distillery/ + problems.md -- 问题记录(open + resolved),操作 A/B 写入 + insights.md -- 蒸馏后的精炼方法论,操作 C 写入 + golden-rules.md -- 权重达标后升级的条目,操作 D 管理,新会话自动注入 +``` + +## 条目格式 + +### problems.md 条目 + +```markdown +### [PD-YYYYMMDD-HHMM] 一句话标题 +- **status**: open | resolved +- **fingerprint**: 关键特征词列表(用于匹配同一问题) +- **category**: tech | workflow | decision | other +- **first_seen**: YYYY-MM-DD HH:MM +- **attempts**: + 1. [YYYY-MM-DD HH:MM] 尝试了什么 → 结果如何 + 2. [YYYY-MM-DD HH:MM] 又尝试了什么 → 结果如何 +- **resolution**: (解决后填写)最终解决方案 +- **dead_ends**: (解决后填写)走过的弯路及其失败原因 +- **spark**: (解决后填写)一句话启发——这个问题教会了什么 +- **distilled**: (蒸馏后填写)IN-xxx +``` + +### insights.md 条目 + +```markdown +### [IN-YYYYMMDD-NN] 一句话方法论 +- **source_problems**: [PD-xxx, PD-yyy] +- **weight**: 0 +- **promoted**: false | true | demoted +- **content**: 2-3 句精炼认知 +``` + +文件尾部保留元数据: + +```markdown +--- +last_distill_date: YYYY-MM-DD +``` + +### golden-rules.md + +```markdown +# Golden Rules + +经过实践验证(权重 >= 5)的精炼认知,每次新会话自动注入。 + +1. [GR-001] 一句话认知(来源: IN-xxx, 累计验证 N 次) +2. [GR-002] ... +``` + +硬上限 15 条。超出时按权重排序保留 top-15,被淘汰的条目降回 insights.md(promoted 改为 demoted)。 + +## 操作 A:追踪记录(问题进行中) + +### 触发条件 + +以下**任一**场景触发: + +1. **用户明确表达**问题未解决: + - "还有问题"、"问题没有解决"、"还是一样"、"又出现了" + - "试了好多次了"、"这个问题反复出现"、"不行"、"没用" + - 以及其他表达"问题反复发生、Agent 没有完全解决"的语义 +2. **Agent 自身意识到**同一问题已经尝试了 2 次以上仍未解决 + +### 适用范围 + +不限于技术问题——工作流设计、产品决策等各类反复纠结的问题均适用。 + +### 流程 + +``` +1. 提取当前问题的指纹: + - 技术问题:错误信息关键词、涉及文件/模块、症状描述 + - 非技术问题:核心矛盾点、涉及领域、反复出现的决策困境 + +2. 读取 problems.md,用指纹匹配 status=open 的条目 + +3. 匹配到已有条目 → 在 attempts 中追加本次尝试记录 + +4. 无匹配 → 创建新条目: + - 生成 ID:PD-YYYYMMDD-HHMM + - status: open + - 记录首次尝试 + +5. 告知用户:"已开始追踪这个问题 [PD-xxx]"(首次) + 或 "已更新追踪记录 [PD-xxx],这是第 N 次尝试"(后续) +``` + +### 与 pitfall-journal 的衔接 + +当 pitfall-journal 中某个条目的同一问题反复出现(用户再次报告相同症状), +Agent 应意识到这已超出 pitfall 的"一次性记录"范畴,主动触发操作 A 建立追踪。 + +## 操作 B:解决记录 + +### 触发条件 + +存在 status=open 的追踪条目,且满足以下**任一**: +- 用户确认问题已解决:"好了"、"解决了"、"终于可以了" +- Agent 判断问题已解决(测试通过、错误消失等) + +### 流程 + +``` +1. 填写 resolution:最终的解决方案 +2. 填写 dead_ends:走过的弯路及其失败原因(从 attempts 中归纳) +3. 填写 spark:一句话启发——这个问题教会了什么 +4. status 改为 resolved +5. 回复末尾附 [顽固问题已解决并记录] +``` + +### 注意 + +- 如果问题在当前会话中从发现到解决只花了 1-2 次尝试,不需要走 distillery 流程 + (那是 pitfall-journal 的范畴) +- 只有经历了"反复尝试"的问题才值得 distillery 追踪 + +## 操作 C:定期蒸馏 + +### 触发条件 + +由 `distillery-recall` Rule 在每次会话开头检查: +- 读取 insights.md 尾部的 `last_distill_date` +- 如果距今超过 7 天,且 problems.md 中有未标记 `distilled` 的 resolved 条目 +- 则提醒用户:"你有 N 个已解决的顽固问题尚未总结,要花几分钟蒸馏一下吗?" +- 每次会话最多提醒一次 + +### 流程(用户同意后) + +``` +1. 读取所有 status=resolved 且无 distilled 标记的条目 +2. Agent 分析这些问题的共性,提出归纳建议: + - 哪些问题有共同的根因模式? + - 能提炼出什么通用的认知或方法论? + - 建议的表述(2-3 句精炼认知) +3. 用户确认/修改后: + - 写入 insights.md(ID 格式:IN-YYYYMMDD-NN,NN 为当日序号) + - weight 初始为 0 + - promoted: false +4. 更新 insights.md 尾部的 last_distill_date +5. 在对应 problems.md 条目上标记 distilled: IN-xxx +``` + +## 操作 D:验证与升级 + +### 被动验证 + +当操作 A 触发时(遇到新的或再次出现的顽固问题),额外执行: + +``` +1. 读取 insights.md,对当前问题做语义匹配 +2. 如有相关条目,向用户展示: + "之前总结过一条相关经验 [IN-xxx]: [内容摘要],可能对当前问题有帮助。" +3. 在问题解决流程中,跟踪该经验是否发挥了作用: + - 用户确认"这个提示有用" + - 或 Agent 判断解决方案与该 insight 的方向一致 +4. 确认有用 → weight += 1,在 insights.md 中更新 +5. 确认无用 → 不扣分(weight 只增不减,避免偶然失误惩罚好经验) +``` + +### 升级为 Golden Rule + +``` +1. 当某条 insight 的 weight >= 5: + a. 检查 golden-rules.md 当前条目数 + b. 如 < 15 → 直接升级: + - 在 golden-rules.md 中追加条目(ID: GR-NNN,NNN 为递增序号) + - insights.md 中 promoted 改为 true + c. 如 = 15 → 比较权重: + - 新条目权重 > golden-rules.md 中最低权重条目 → 替换 + - 被替换的条目降回 insights.md(promoted 改为 demoted) + - 否则不升级 +2. 升级后告知用户: + "经验 [IN-xxx] 已累计验证 N 次,升级为 Golden Rule [GR-NNN],后续新会话将自动注入。" +``` + +### 新会话注入 + +由 `distillery-recall` Rule 负责: +- 每次会话开头读取 `golden-rules.md` +- 如果非空,将全部条目作为背景知识注入上下文 +- 注入方式与 `changelog-headlines` 同级别——轻量、不显式提及来源 + +## 容量与性能控制 + +| 文件 | 策略 | 阈值 | +|------|------|------| +| problems.md | resolved 且已 distilled 的条目超过 50 条时,归档到 problems-archive.md | 50 条 | +| insights.md | 无上限(条目本身是精炼的,每条 3-5 行) | — | +| golden-rules.md | 硬上限,按权重淘汰 | 15 条 | +| 新会话注入成本 | 只读 golden-rules.md(预期 < 30 行) | 极轻量 | + +## 与其他系统的协作 + +| 系统 | 关系 | 说明 | +|------|------|------| +| `pitfall-journal` | 上游来源 | pitfall 条目反复出现时,升级为 distillery 追踪 | +| `dev-changelog` | 不重叠 | changelog 记事实改动,distillery 记过程和认知 | +| `distillery-recall` Rule | 下游消费者 | 负责 golden-rules 注入和蒸馏提醒 | +| `epee-orchestrator` | 注册 | 在 registry.md 中注册本 Skill | + +## 自迭代日志 + +本节记录使用本 Skill 过程中发现的必要检查项。 + +### 已知必要检查 + +(暂无) diff --git a/.cursor/skills/project-launcher/SKILL.md b/.cursor/skills/project-launcher/SKILL.md index 5867fc2..8bcd03c 100644 --- a/.cursor/skills/project-launcher/SKILL.md +++ b/.cursor/skills/project-launcher/SKILL.md @@ -32,14 +32,33 @@ description: >- - 前端需要 Node.js >= 18 - **Ollama 必须在后端之前启动**:Mem0 记忆系统依赖 Ollama 提供本地 embedding 服务(`nomic-embed-text` 模型),端口 `11434` -## 环境 PATH 须知 +## 本地环境配置(local-env.json) -本机 Node.js 安装在 `C:\Program Files\nodejs\` 但**未加入系统 PATH**。 -新开的终端窗口默认找不到 `node` / `npm` 命令。 +设备绑定的路径和配置统一存放在 `.cursor/local-env.json`(已 gitignored),不硬编码到 Skill 中。 -**解决方式**:在启动前端的命令中,先将 Node.js 路径注入到当前会话的 `$env:PATH` 中。 +**启动前必须先读取该文件**,从中获取 `nodejs_path` 和 `shell` 字段。 -> 如果后续 Node.js 路径发生变化(如用户重新安装或使用 nvm),需要更新此处。 +| 字段 | 用途 | 示例值 | +|------|------|--------| +| `nodejs_path` | Node.js 安装目录(含 node/npm) | `C:\Users\xxx\AppData\Local\nodejs` | +| `shell` | 当前设备使用的 shell 类型 | `powershell` / `bash` / `zsh` | + +**文件不存在时的自动探测流程**: + +``` +1. 创建空的 JSON 对象 {} +2. 探测 Node.js 路径: + - 运行 `where.exe node`(Windows)或 `which node`(macOS/Linux) + - 取其父目录作为 nodejs_path + - 如果找不到,将 nodejs_path 设为 null,后续启动前端时提醒用户手动配置 +3. 探测 shell 类型: + - Windows 默认 "powershell" + - macOS/Linux 读取 $SHELL 环境变量,提取末尾(bash/zsh/fish 等) +4. 写入 .cursor/local-env.json +5. 告知用户已自动生成本地配置,可手动调整 +``` + +**Node.js 不在系统 PATH 时**:启动前端的命令中需要将 `nodejs_path` 注入到当前会话的 PATH。 ## 核心操作:启动服务 @@ -48,23 +67,25 @@ description: >- ``` 1. 确定项目根目录(workspace 根目录下的 art-agent/) -2. 确定操作系统和 Shell 类型(Windows / macOS / Linux) +2. 读取 .cursor/local-env.json: + - 存在 → 解析 nodejs_path 和 shell + - 不存在 → 执行自动探测流程(见上方),生成后再读取 3. 检查并启动 Ollama: - 检测 Ollama 是否已在运行(请求 http://localhost:11434/api/tags) - 未运行 → 启动 Ollama 服务,等待就绪 - 已运行 → 跳过 -4. 启动后端(在独立可见终端窗口中): - - Windows(PowerShell 或 CMD 均适用): - 使用 `Start-Process` 或 `start cmd` 打开新的终端窗口 - - macOS/Linux: - 使用对应的终端打开方式 +4. 根据 shell 字段选择对应的启动命令模板: + - powershell → 使用 PowerShell 命令 + - bash/zsh → 使用 macOS/Linux 命令 -5. 启动前端(在另一个独立可见终端窗口中): - - 同样在新的终端窗口中启动 +5. 启动后端(在独立可见终端窗口中) -6. 确认三个服务正在运行,告知用户访问地址 +6. 启动前端(在另一个独立可见终端窗口中): + - 如果 nodejs_path 不为 null,先注入到 PATH + +7. 确认三个服务正在运行,告知用户访问地址 ``` ### Ollama 启动(跨平台通用) @@ -87,44 +108,36 @@ try { > Ollama 启动后会常驻后台,不需要独立终端窗口。如果用户系统已将 Ollama 设为开机自启, > 则检测会直接通过,不会重复启动。 -### Windows 启动命令 +### 启动命令模板 **关键要求**:必须在**新的、可见的终端窗口**中启动,不能在 Cursor 内置终端后台运行。 -#### PowerShell 环境 +以下模板中的变量说明: +- `BACKEND_PATH` / `FRONTEND_PATH`:替换为实际绝对路径 +- `NODEJS_PATH`:从 `local-env.json` 的 `nodejs_path` 字段读取 + +#### shell = "powershell" 启动后端: ```powershell Start-Process powershell -ArgumentList '-NoExit', '-Command', "cd 'BACKEND_PATH'; .\venv\Scripts\Activate.ps1; uvicorn app.main:app --reload --host 0.0.0.0 --port 8000" -WindowStyle Normal ``` -启动前端(注意注入 Node.js PATH): +启动前端(注入从 local-env.json 读取的 NODEJS_PATH): ```powershell -Start-Process powershell -ArgumentList '-NoExit', '-Command', "& { `$env:PATH = 'C:\Program Files\nodejs;' + `$env:PATH; `$Host.UI.RawUI.WindowTitle = 'Art Agent Frontend'; cd 'FRONTEND_PATH'; npm run dev }" -WindowStyle Normal +Start-Process powershell -ArgumentList '-NoExit', '-Command', "& { `$env:PATH = 'NODEJS_PATH;' + `$env:PATH; `$Host.UI.RawUI.WindowTitle = 'Art Agent Frontend'; cd 'FRONTEND_PATH'; npm run dev }" -WindowStyle Normal ``` -#### CMD 环境 +#### shell = "bash" / "zsh"(macOS / Linux) 启动后端: -```cmd -start "Art Agent Backend" cmd /k "cd /d BACKEND_PATH && venv\Scripts\activate && uvicorn app.main:app --reload --host 0.0.0.0 --port 8000" -``` - -启动前端(注意注入 Node.js PATH): -```cmd -start "Art Agent Frontend" cmd /k "set PATH=C:\Program Files\nodejs;%PATH% && cd /d FRONTEND_PATH && npm run dev" -``` - -### macOS / Linux 启动命令 - -根据用户终端环境选择: - ```bash -# 后端 osascript -e 'tell application "Terminal" to do script "cd BACKEND_PATH && source venv/bin/activate && uvicorn app.main:app --reload --host 0.0.0.0 --port 8000"' +``` -# 前端 -osascript -e 'tell application "Terminal" to do script "cd FRONTEND_PATH && npm run dev"' +启动前端(NODEJS_PATH 为 null 时跳过 PATH 注入): +```bash +osascript -e 'tell application "Terminal" to do script "export PATH=NODEJS_PATH:$PATH && cd FRONTEND_PATH && npm run dev"' ``` ### 执行注意事项 @@ -156,5 +169,6 @@ osascript -e 'tell application "Terminal" to do script "cd FRONTEND_PATH && npm ### 已知必要检查 -1. **Node.js PATH 注入** — 本机 Node.js (`C:\Program Files\nodejs\`) 未加入系统 PATH,新开的终端窗口默认找不到 `npm`。启动前端时必须先将此路径注入到会话 PATH 中。 -2. **Ollama 必须先于后端启动** — Mem0 记忆系统依赖 Ollama 的 `nomic-embed-text` 模型做本地 embedding(端口 11434)。Ollama 未运行时后端能启动但对话会报 502 错误。启动流程必须在后端之前检测并启动 Ollama。 +1. **先读 local-env.json** — 启动前必须读取 `.cursor/local-env.json` 获取 `nodejs_path` 和 `shell`。文件不存在时执行自动探测并生成。绝不在 Skill 中硬编码设备相关的路径。 +2. **Node.js PATH 注入** — 如果 `nodejs_path` 不为 null,说明 Node.js 未在系统 PATH 中,启动前端时必须将该路径注入到会话 PATH。 +3. **Ollama 必须先于后端启动** — Mem0 记忆系统依赖 Ollama 的 `nomic-embed-text` 模型做本地 embedding(端口 11434)。Ollama 未运行时后端能启动但对话会报 502 错误。启动流程必须在后端之前检测并启动 Ollama。 diff --git a/.gitignore b/.gitignore index 7d85189..62027a9 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,9 @@ art-agent/backend/data/ # ─── 工具二进制 ────────────────────────────────────────── *.exe +# ─── 本地环境配置(设备绑定,不入版本控制)────────────── +.cursor/local-env.json + # ─── IDE / 编辑器 ──────────────────────────────────────── .vscode/ .idea/ diff --git a/art-agent/ENVIRONMENT.md b/art-agent/ENVIRONMENT.md new file mode 100644 index 0000000..9a0d79c --- /dev/null +++ b/art-agent/ENVIRONMENT.md @@ -0,0 +1,206 @@ +# EPEEKit 开发环境说明 + +> 最后更新:2026-04-16 + +## 系统要求 + +| 项目 | 最低版本 | 备注 | +|------|---------|------| +| OS | Windows 10+ / macOS / Linux | 跨平台支持 | +| Python | 3.12+ | 用于后端 | +| Node.js | 22+ LTS | 用于前端 | +| npm | 10+ | 随 Node.js 安装 | +| Ollama | latest | Mem0 embedding 服务依赖,需启动 `nomic-embed-text` 模型 | + +## 本地环境配置(local-env.json) + +设备绑定的路径(如 Node.js 安装位置)**不硬编码在任何 Skill 或配置中**, +统一通过 `.cursor/local-env.json` 管理。 + +| 字段 | 说明 | 示例 | +|------|------|------| +| `nodejs_path` | Node.js 安装目录(含 node/npm),为 `null` 表示已在系统 PATH | `C:\Users\xxx\AppData\Local\nodejs` | +| `shell` | 当前设备的 shell 类型 | `powershell` / `bash` / `zsh` | + +- **首次使用**:Agent 会自动探测并生成,也可手动复制 `.cursor/local-env.example.json` 并修改 +- **已 gitignored**:不进入版本控制,每台设备独立维护 +- **模板参考**:`.cursor/local-env.example.json` + +### Node.js PATH 注意事项 + +如果 `nodejs_path` 不为 null,说明 Node.js 未在系统 PATH 中,启动前端前需注入: + +```powershell +# PowerShell(路径来自 local-env.json 的 nodejs_path) +$env:PATH = "你的nodejs_path;" + $env:PATH +``` + +```bash +# bash/zsh +export PATH="你的nodejs_path:$PATH" +``` + +## 项目结构 + +``` +EPEEAIKit/ +├── .cursor/ +│ ├── local-env.json # 本地环境配置(gitignored,设备绑定) +│ ├── local-env.example.json # 本地环境配置模板(git tracked) +│ ├── hooks.json # Cursor hooks 配置 +│ └── hooks/ # Hook 脚本(.ps1 + .sh 双版本) +├── art-agent/ +│ ├── backend/ # FastAPI 后端(Python) +│ │ ├── app/ # 应用代码 +│ │ ├── venv/ # Python 虚拟环境(gitignored) +│ │ ├── requirements.txt # 直接依赖(锁定版本) +│ │ └── requirements-lock.txt # 完整依赖树(pip freeze) +│ ├── frontend/ # Next.js 前端(TypeScript) +│ │ ├── src/ # 应用代码 +│ │ ├── node_modules/ # npm 依赖(gitignored) +│ │ ├── package.json # 直接依赖 +│ │ └── package-lock.json # 完整依赖树锁定 +│ └── ENVIRONMENT.md # 本文件 +``` + +## 后端依赖(Python) + +### 直接依赖 + +| 包名 | 版本 | 用途 | +|------|------|------| +| fastapi | 0.135.3 | Web 框架 | +| uvicorn | 0.44.0 | ASGI 服务器 | +| openai | 2.32.0 | OpenAI / 向量引擎 LLM & 生图 API | +| replicate | 1.0.7 | Replicate 生图 API(Flux/SDXL/Kolors 等) | +| sse-starlette | 3.3.4 | Server-Sent Events 流式响应 | +| python-multipart | 0.0.26 | 文件上传处理 | +| httpx | 0.28.1 | 异步 HTTP 客户端 | +| python-dotenv | 1.2.2 | 环境变量 `.env` 加载 | +| Pillow | 12.2.0 | 图像处理 | +| mem0ai | 1.0.11 | Mem0 长期记忆系统 | +| ollama | 0.6.1 | Ollama 本地 LLM/embedding 客户端 | +| sqlmodel | 0.0.38 | SQLModel ORM(SQLite 用户表) | +| pwdlib[argon2,bcrypt] | 0.3.0 | 密码哈希(Argon2id + bcrypt 兼容) | +| python-jose[cryptography] | 3.5.0 | JWT 认证 | + +### 间接依赖(关键) + +| 包名 | 版本 | 来源 | +|------|------|------| +| pydantic | 2.13.1 | fastapi 依赖 | +| starlette | 1.0.0 | fastapi 依赖 | +| SQLAlchemy | 2.0.49 | sqlmodel / mem0ai 依赖 | +| qdrant-client | 1.17.1 | mem0ai 依赖(向量数据库) | +| numpy | 2.4.4 | qdrant-client 依赖 | +| grpcio | 1.80.0 | qdrant-client 依赖 | +| cryptography | 46.0.7 | python-jose[cryptography] 依赖 | +| argon2-cffi | 25.1.0 | pwdlib[argon2] 依赖 | +| bcrypt | 5.0.0 | pwdlib[bcrypt] 依赖 | + +完整依赖树见 `requirements-lock.txt`(共 61 个包)。 + +## 前端依赖(Node.js) + +### 直接依赖(dependencies) + +| 包名 | 版本 | 用途 | +|------|------|------| +| next | 15.5.15 | React 全栈框架 | +| react | 19.2.5 | UI 库 | +| react-dom | 19.2.5 | React DOM 渲染 | + +### 开发依赖(devDependencies) + +| 包名 | 版本 | 用途 | +|------|------|------| +| typescript | 5.9.3 | TypeScript 编译器 | +| tailwindcss | 4.2.2 | CSS 工具框架 | +| @tailwindcss/postcss | 4.2.2 | Tailwind PostCSS 插件 | +| postcss | 8.5.9 | CSS 后处理器 | +| @types/node | 22.19.17 | Node.js 类型定义 | +| @types/react | 19.2.14 | React 类型定义 | +| @types/react-dom | 19.2.3 | ReactDOM 类型定义 | + +完整依赖树见 `package-lock.json`(共 46 个包)。 + +## 外部服务依赖 + +| 服务 | 端口 | 用途 | 必需 | +|------|------|------|------| +| Ollama | 11434 | 本地 embedding(nomic-embed-text)| 是(Mem0 依赖) | +| Qdrant | 6333 | 本地向量数据库(Mem0 存储) | 是(Mem0 依赖) | + +## 环境变量 + +后端通过 `.env` 文件加载配置,主要包含: +- LLM API Key(向量引擎 / DeepSeek) +- Replicate API Token +- 代理配置(HTTPS_PROXY) +- Gemini 超时参数(VECTORENGINE_GEMINI_*_TIMEOUT) +- JWT Secret + +详见 `art-agent/backend/.env`(gitignored,不纳入版本控制)。 + +## 环境搭建步骤 + +### 0. 本地环境配置(首次) + +复制 `.cursor/local-env.example.json` 为 `.cursor/local-env.json`,修改为本机实际值。 +或者启动 Cursor Agent 会话,Agent 会自动探测并生成。 + +### 1. 后端 + +```bash +cd art-agent/backend + +# 创建虚拟环境 +python -m venv venv + +# 激活虚拟环境 +# Windows PowerShell: +.\venv\Scripts\Activate.ps1 +# macOS/Linux: +source venv/bin/activate + +# 安装依赖 +pip install -r requirements.txt + +# 启动(需要先启动 Ollama) +uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 +``` + +### 2. 前端 + +```bash +cd art-agent/frontend + +# 如果 Node.js 未在系统 PATH 中(参考 local-env.json 的 nodejs_path): +# PowerShell: $env:PATH = "你的nodejs_path;" + $env:PATH +# bash/zsh: export PATH="你的nodejs_path:$PATH" + +# 安装依赖 +npm install + +# 启动开发服务器 +npm run dev +``` + +### 3. Ollama + +```bash +# 启动 Ollama 服务 +ollama serve + +# 拉取 embedding 模型(首次) +ollama pull nomic-embed-text +``` + +## 访问地址 + +| 服务 | 地址 | +|------|------| +| 前端页面 | http://localhost:3000 | +| 后端 API | http://localhost:8000 | +| API 文档 | http://localhost:8000/docs | +| Ollama | http://localhost:11434 | diff --git a/art-agent/backend/app/agent/loop.py b/art-agent/backend/app/agent/loop.py index e89ebea..06d07c6 100644 --- a/art-agent/backend/app/agent/loop.py +++ b/art-agent/backend/app/agent/loop.py @@ -46,6 +46,7 @@ SYSTEM_PROMPT = """\ - 如果用户提供了参考图(支持多张),将参考图的风格元素融入生成 prompt - 理解多张参考图各自的角色(如"图1的主体 + 图2的风格/视角"),并在 prompt 中准确传达 - 理解用户在图片上的标注(框选区域 + 文字批注),精准定位需要修改的部分 +- 将一张图片转换为多个不同视角(使用 transform_view 工具),适合建筑、物体等需要从不同角度查看的场景 ## 工作流程 1. 理解用户需求,必要时追问细节(尺寸、风格、用途等) @@ -268,9 +269,13 @@ async def run_agent_loop( tc_data = tool_calls_data[idx] tool_name = tc_data["name"] + tool_message = ( + "正在进行视角变换..." if tool_name == "transform_view" + else "正在生成图片..." + ) yield { "type": "tool_start", - "data": {"tool": tool_name, "message": "正在生成图片..."}, + "data": {"tool": tool_name, "message": tool_message}, } try: @@ -292,7 +297,29 @@ async def run_agent_loop( }, } - if result.get("images"): + if result.get("error") and not result.get("success"): + yield { + "type": "tool_error", + "data": { + "tool": tool_name, + "errors": [result["error"]], + "model_name": used_model, + }, + } + + if tool_name == "transform_view" and result.get("images"): + # transform_view 返回带视角信息的图片列表 + image_urls = [v["url"] for v in result["images"]] + yield { + "type": "image_result", + "data": { + "images": image_urls, + "prompt_used": "", + "model_name": used_model, + "view_info": result["images"], + }, + } + elif result.get("images"): yield { "type": "image_result", "data": { diff --git a/art-agent/backend/app/agent/tools.py b/art-agent/backend/app/agent/tools.py index 829a2e5..c95540e 100644 --- a/art-agent/backend/app/agent/tools.py +++ b/art-agent/backend/app/agent/tools.py @@ -1,6 +1,7 @@ """Agent 可调用的工具定义和实现。""" from app.services.image_gen import generate_images +from app.services.view_transform import transform_view # OpenAI Function Calling 格式的工具定义 TOOL_DEFINITIONS = [ @@ -32,7 +33,27 @@ TOOL_DEFINITIONS = [ "required": ["prompt"], }, }, - } + }, + { + "type": "function", + "function": { + "name": "transform_view", + "description": ( + "将一张图片转换为多个不同视角。" + "使用 Zero123++ 模型从单张图片生成 6 个固定视角的图片。" + "适用于建筑、物体等需要从不同角度查看的场景。" + "输入图片必须是正方形(或会被自动裁切为正方形),建议分辨率 >= 320x320。" + "输出 6 个视角:方位角 30°/90°/150°/210°/270°/330°," + "仰角交替为 30°/-20°(正俯视/微仰视)。" + "此工具需要参考图作为输入——必须先有用户上传的图片才能使用。" + ), + "parameters": { + "type": "object", + "properties": {}, + "required": [], + }, + }, + }, ] @@ -65,4 +86,13 @@ async def execute_tool( "model_id": result.model_id, } + if tool_name == "transform_view": + if not ref_image_urls: + return { + "success": False, + "error": "视角变换需要一张输入图片,请先上传参考图", + } + result = await transform_view(ref_image_urls[0]) + return result + return {"success": False, "error": f"未知工具: {tool_name}"} diff --git a/art-agent/backend/app/config.py b/art-agent/backend/app/config.py index 8bbd431..07d731f 100644 --- a/art-agent/backend/app/config.py +++ b/art-agent/backend/app/config.py @@ -228,6 +228,50 @@ IMAGE_MODELS: dict[str, dict[str, Any]] = { } +# ─── 视角变换模型注册表 ───────────────────────────────── +# +# 视角变换模型不用于生图,而是将已有图片转换为不同视角。 +# 输入通常只需一张图,输出为多个固定视角的图片。 + +VIEW_TRANSFORM_MODELS: dict[str, dict[str, Any]] = { + "zero123plus": { + "id": "zero123plus", + "name": "Zero123++", + "provider": "replicate", + "model_id": "jd7h/zero123plusplus:c69c6559a29011b576f1ff0371b3bc1add2856480c60520c7e9ce0b40a6e9052", + "description": "单图生成 6 个固定视角,适合建筑/物体的多角度预览", + "output_views": [ + {"azimuth": 30, "elevation": 30}, + {"azimuth": 90, "elevation": -20}, + {"azimuth": 150, "elevation": 30}, + {"azimuth": 210, "elevation": -20}, + {"azimuth": 270, "elevation": 30}, + {"azimuth": 330, "elevation": -20}, + ], + "grid_layout": {"cols": 3, "rows": 2}, + }, +} + + +def get_view_transform_model_config(model_id: str | None = None) -> dict[str, Any]: + """获取视角变换模型配置,默认返回 zero123plus。""" + if model_id and model_id in VIEW_TRANSFORM_MODELS: + return VIEW_TRANSFORM_MODELS[model_id] + return VIEW_TRANSFORM_MODELS["zero123plus"] + + +def get_view_transform_models_list() -> list[dict]: + """返回前端下拉列表所需的视角变换模型摘要信息。""" + return [ + { + "id": cfg["id"], + "name": cfg["name"], + "description": cfg["description"], + } + for cfg in VIEW_TRANSFORM_MODELS.values() + ] + + def get_default_image_model_id() -> str: """返回 .env 中配置的默认模型短 ID,若不在注册表中则回退到 flux-schnell。""" env_model = os.getenv("IMAGE_MODEL", "flux-schnell") diff --git a/art-agent/backend/app/services/view_transform.py b/art-agent/backend/app/services/view_transform.py new file mode 100644 index 0000000..6065d08 --- /dev/null +++ b/art-agent/backend/app/services/view_transform.py @@ -0,0 +1,157 @@ +"""视角变换服务 — 调用 Zero123++ 等模型,将单张图片转为多视角图片。""" + +import base64 +import logging +import mimetypes +import os +import uuid +from pathlib import Path +from typing import Any + +import httpx +from PIL import Image + +from app.config import get_view_transform_model_config +from app.services.image_gen import _make_replicate_client, GENERATED_DIR, to_data_uri + +logger = logging.getLogger(__name__) + +_replicate_client = _make_replicate_client() + + +def _split_grid_image( + image_path: Path, + cols: int, + rows: int, + view_labels: list[dict[str, int]], +) -> list[dict[str, Any]]: + """将 Zero123++ 输出的 grid 拼接图切分为独立的视角图片。 + + 返回格式:[{"url": "/generated/xxx.png", "azimuth": 30, "elevation": 30}, ...] + Grid 布局从左到右、从上到下依次对应 view_labels 中的视角。 + """ + img = Image.open(image_path) + w, h = img.size + cell_w = w // cols + cell_h = h // rows + + results: list[dict[str, Any]] = [] + for idx, view in enumerate(view_labels): + row_idx = idx // cols + col_idx = idx % cols + left = col_idx * cell_w + upper = row_idx * cell_h + right = left + cell_w + lower = upper + cell_h + + cropped = img.crop((left, upper, right, lower)) + filename = f"view_{uuid.uuid4().hex[:8]}_az{view['azimuth']}_el{view['elevation']}.png" + filepath = GENERATED_DIR / filename + cropped.save(filepath, "PNG") + + results.append({ + "url": f"/generated/{filename}", + "azimuth": view["azimuth"], + "elevation": view["elevation"], + }) + + return results + + +async def _download_to_local(url: str) -> Path: + """下载远程图片到本地 generated/ 目录,返回本地路径。""" + filename = f"grid_{uuid.uuid4().hex}.png" + filepath = GENERATED_DIR / filename + proxy = os.environ.get("HTTPS_PROXY") or os.environ.get("HTTP_PROXY") + async with httpx.AsyncClient(proxy=proxy, timeout=httpx.Timeout(120.0)) as client: + resp = await client.get(url, follow_redirects=True) + resp.raise_for_status() + filepath.write_bytes(resp.content) + return filepath + + +async def transform_view( + image_path: str, + model_id: str | None = None, +) -> dict[str, Any]: + """调用视角变换模型,返回多视角图片列表。 + + Args: + image_path: 输入图片路径(本地路径、data URI 或 URL) + model_id: 视角变换模型 ID,默认 zero123plus + + Returns: + { + "success": bool, + "images": [{"url": str, "azimuth": int, "elevation": int}, ...], + "grid_image": str, # 原始拼接图的本地 URL + "model_name": str, + "error": str | None, + } + """ + config = get_view_transform_model_config(model_id) + model_name = config["name"] + replicate_model_id = config["model_id"] + grid_layout = config["grid_layout"] + output_views = config["output_views"] + + GENERATED_DIR.mkdir(parents=True, exist_ok=True) + + try: + image_uri = to_data_uri(image_path) + input_params = {"image": image_uri} + + logger.info("调用 %s 进行视角变换...", model_name) + output = await _replicate_client.async_run( + replicate_model_id, input=input_params, wait=False + ) + + # Zero123++ 返回单张 grid 拼接图 + items = output if isinstance(output, list) else [output] + if not items: + return { + "success": False, + "images": [], + "grid_image": None, + "model_name": model_name, + "error": "模型未返回任何输出", + } + + grid_url = str(items[0]) + if not (grid_url.startswith("http://") or grid_url.startswith("https://")): + return { + "success": False, + "images": [], + "grid_image": None, + "model_name": model_name, + "error": f"模型返回非图片内容: {grid_url[:200]}", + } + + grid_local_path = await _download_to_local(grid_url) + grid_local_url = f"/generated/{grid_local_path.name}" + + view_images = _split_grid_image( + grid_local_path, + cols=grid_layout["cols"], + rows=grid_layout["rows"], + view_labels=output_views, + ) + + return { + "success": True, + "images": view_images, + "grid_image": grid_local_url, + "model_name": model_name, + "error": None, + } + + except Exception as e: + detail = str(e) or f"{type(e).__name__}: {repr(e)}" + logger.error("视角变换失败: %s", detail, exc_info=True) + return { + "success": False, + "images": [], + "grid_image": None, + "model_name": model_name, + "error": detail, + } diff --git a/art-agent/backend/requirements-lock.txt b/art-agent/backend/requirements-lock.txt new file mode 100644 index 0000000000000000000000000000000000000000..64ed7d38b914a82016057ceef484a83aa2448bcd GIT binary patch literal 2200 zcmZvd%W~RK5Jc}fRrwTL79xHuvdBK0R6f9k1vWS!$wDN?AD^Vpv@Vhp7bVKp%=Gm1 zJoxi_nQDES)TgDMby}sFo+ef4vp$#UBA!M1m_Eqkf8RcxQmeSb+5b9Xvi~VOnJVg+K3~WMRj;Gje{o=Jb~gP5Kr1MMO^#(345P2kmo} zp4%}~6BDzM*JB`YEvPV0cPkvOQKRiX4jW4)WlW9dyA_L-GHvCPt0hma<|G~mbzqzt zBcKKPRr=05>0Z2zUddXE0~=^1;OI_fjH%Dvn|X3)M)k?qYyY@(+*aS&W8m-3_};k- zF|$HrEz?+(VV8bJoK+i@o91~698V|Aeb+Mk5pyqV9%f*Sc^%m13rPOV zH_ypQy@so^Wy$%`9FkFA~Wns@wtpR9K@mE{VW3&ZN?ouYJS-* LZpJh#IIiO_h6*}$ literal 0 HcmV?d00001 diff --git a/art-agent/backend/requirements.txt b/art-agent/backend/requirements.txt index 34bb58b..6ecfa0a 100644 --- a/art-agent/backend/requirements.txt +++ b/art-agent/backend/requirements.txt @@ -1,14 +1,14 @@ -fastapi>=0.115.0 -uvicorn>=0.32.0 -openai>=1.55.0 -replicate>=1.0.0 -sse-starlette>=2.1.0 -python-multipart>=0.0.12 -httpx>=0.27.0 -python-dotenv>=1.0.0 -Pillow>=10.4.0 -mem0ai -ollama -sqlmodel>=0.0.22 -pwdlib[argon2,bcrypt]>=0.3.0 -python-jose[cryptography]>=3.3.0 +fastapi==0.135.3 +uvicorn==0.44.0 +openai==2.32.0 +replicate==1.0.7 +sse-starlette==3.3.4 +python-multipart==0.0.26 +httpx==0.28.1 +python-dotenv==1.2.2 +Pillow==12.2.0 +mem0ai==1.0.11 +ollama==0.6.1 +sqlmodel==0.0.38 +pwdlib[argon2,bcrypt]==0.3.0 +python-jose[cryptography]==3.5.0 diff --git a/art-agent/frontend/package-lock.json b/art-agent/frontend/package-lock.json index e7a0310..14ee88b 100644 --- a/art-agent/frontend/package-lock.json +++ b/art-agent/frontend/package-lock.json @@ -8,6 +8,7 @@ "name": "epeekit-frontend", "version": "0.1.0", "dependencies": { + "framer-motion": "^12.38.0", "next": "^15.1.0", "react": "^19.0.0", "react-dom": "^19.0.0" @@ -138,9 +139,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -157,9 +155,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -176,9 +171,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -195,9 +187,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -214,9 +203,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -233,9 +219,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -252,9 +235,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -271,9 +251,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -290,9 +267,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -315,9 +289,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -340,9 +311,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -365,9 +333,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -390,9 +355,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -415,9 +377,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -440,9 +399,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -465,9 +421,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -654,9 +607,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -673,9 +623,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -692,9 +639,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -711,9 +655,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -897,9 +838,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -917,9 +855,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -937,9 +872,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -957,9 +889,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1134,6 +1063,33 @@ "node": ">=10.13.0" } }, + "node_modules/framer-motion": { + "version": "12.38.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz", + "integrity": "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.38.0", + "motion-utils": "^12.36.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -1294,9 +1250,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1318,9 +1271,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1342,9 +1292,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1366,9 +1313,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1434,6 +1378,21 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/motion-dom": { + "version": "12.38.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz", + "integrity": "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.36.0" + } + }, + "node_modules/motion-utils": { + "version": "12.36.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz", + "integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==", + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", diff --git a/art-agent/frontend/package.json b/art-agent/frontend/package.json index 758a5c6..62a9042 100644 --- a/art-agent/frontend/package.json +++ b/art-agent/frontend/package.json @@ -9,17 +9,18 @@ "lint": "next lint" }, "dependencies": { + "framer-motion": "^12.38.0", "next": "^15.1.0", "react": "^19.0.0", "react-dom": "^19.0.0" }, "devDependencies": { + "@tailwindcss/postcss": "^4.0.0", "@types/node": "^22.0.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", - "typescript": "^5.7.0", + "postcss": "^8.5.0", "tailwindcss": "^4.0.0", - "@tailwindcss/postcss": "^4.0.0", - "postcss": "^8.5.0" + "typescript": "^5.7.0" } } diff --git a/art-agent/frontend/src/app/gallery/page.tsx b/art-agent/frontend/src/app/gallery/page.tsx index 5ed94fc..ebcebee 100644 --- a/art-agent/frontend/src/app/gallery/page.tsx +++ b/art-agent/frontend/src/app/gallery/page.tsx @@ -1,9 +1,13 @@ "use client"; import { useState, useMemo } from "react"; +import { motion, AnimatePresence } from "framer-motion"; import { TopNav } from "@/components/layout/top-nav"; +import { AmbientParticles } from "@/components/ui/ambient-particles"; +import { useFireflyBurst, FireflyBurst } from "@/components/ui/firefly-burst"; import { useApp } from "@/lib/app-context"; import { getImageUrl } from "@/lib/api"; +import { staggerContainer, staggerItem, slideUp } from "@/components/ui/motion-presets"; import type { ImageAsset } from "@/lib/types"; type ViewMode = "grid" | "list"; @@ -110,12 +114,13 @@ export default function GalleryPage() { return (
+
{/* 工具栏 */} -
{/* 区块标题 */} @@ -125,7 +130,7 @@ export default function GalleryPage() { - 资源库 + 资源库
{/* 搜索 */} @@ -272,19 +277,28 @@ export default function GalleryPage() {
{filtered.length === 0 ? (
-
-
+ + -
-

+ + {assets.length === 0 ? "还没有生成过图片" : "没有匹配的资源"} -

-
+ + + {assets.length === 0 ? "在对话中生成的图片会自动出现在这里" : "试试调整筛选条件"} + +
) : viewMode === "grid" ? (
@@ -294,7 +308,7 @@ export default function GalleryPage() { className={`group relative rounded-xl overflow-hidden neon-border cursor-pointer bg-[var(--bg-card)] backdrop-blur-sm transition-all ${ selectedIds.has(asset.id) - ? "!border-[var(--accent)] ring-1 ring-[var(--accent)]/30 shadow-[0_0_15px_rgba(77,184,164,0.08)]" + ? "!border-[var(--accent)] ring-1 ring-[var(--accent)]/30 shadow-[0_0_15px_rgba(46,139,122,0.08)]" : "" }`} onClick={() => setDetailAsset(asset)} @@ -310,7 +324,7 @@ export default function GalleryPage() {
@@ -527,15 +541,17 @@ function GalleryDetailModal({ className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm" onClick={onClose} > -
e.stopPropagation()} > {/* 头部 */}
- 资源详情 + 资源详情
-
+
); } diff --git a/art-agent/frontend/src/app/globals.css b/art-agent/frontend/src/app/globals.css index ffbe1c1..0531e1d 100644 --- a/art-agent/frontend/src/app/globals.css +++ b/art-agent/frontend/src/app/globals.css @@ -1,20 +1,20 @@ @import "tailwindcss"; -/* ===== 青绿山水色彩体系 ===== */ +/* ===== 青绿山水色彩体系(白底·绢本) ===== */ :root { - --bg-primary: #0C1210; - --bg-secondary: #131E1A; - --bg-tertiary: #1C2A24; - --bg-card: rgba(16, 30, 24, 0.65); - --text-primary: #E0E8E2; - --text-secondary: #7A9485; - --accent: #4DB8A4; - --accent-hover: #6CD4BE; - --accent-secondary: #3A8FB7; + --bg-primary: #FAFAF7; + --bg-secondary: #F2F1EC; + --bg-tertiary: #E8E6DF; + --bg-card: rgba(255, 255, 255, 0.8); + --text-primary: #1A1A1A; + --text-secondary: #6B7B8A; + --accent: #2E8B7A; + --accent-hover: #3AA38E; + --accent-secondary: #3A7FB8; --hot: #C4654A; - --border: rgba(77, 184, 164, 0.12); - --border-glow: rgba(77, 184, 164, 0.25); - --mist: rgba(77, 184, 164, 0.06); + --border: rgba(46, 139, 122, 0.15); + --border-glow: rgba(46, 139, 122, 0.30); + --mist: rgba(46, 139, 122, 0.04); --gold: #B8935A; } @@ -22,8 +22,126 @@ body { background: var(--bg-primary); color: var(--text-primary); font-family: 'Inter', system-ui, -apple-system, sans-serif; + font-size: 14px; + letter-spacing: 0.01em; overscroll-behavior: none; -webkit-tap-highlight-color: transparent; + /* 绢本底纹:极淡暖白,像宣纸的自然质感 */ + background-image: + radial-gradient(ellipse at 20% 80%, rgba(46, 139, 122, 0.03) 0%, transparent 50%), + radial-gradient(ellipse at 80% 20%, rgba(58, 127, 184, 0.02) 0%, transparent 50%); +} + +/* ===== 排版层级体系(7 级) ===== */ + +.typo-display { + font-size: 32px; + font-weight: 300; + letter-spacing: -0.02em; + line-height: 1.2; + background: linear-gradient(135deg, var(--accent) 0%, var(--accent-secondary) 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + color: transparent; +} + +.typo-h1 { + font-size: 24px; + font-weight: 700; + letter-spacing: -0.01em; + line-height: 1.3; + color: var(--text-primary); + position: relative; + padding-bottom: 10px; +} +.typo-h1::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 40px; + height: 2px; + background: linear-gradient(90deg, var(--accent) 0%, transparent 100%); + border-radius: 1px; +} + +.typo-h2 { + font-size: 18px; + font-weight: 600; + letter-spacing: 0; + line-height: 1.4; + color: var(--text-primary); + padding-left: 12px; + position: relative; +} +.typo-h2::before { + content: ''; + position: absolute; + left: 0; + top: 2px; + bottom: 2px; + width: 3px; + background: var(--accent); + border-radius: 2px; +} + +.typo-strong { + font-weight: 600; + color: var(--accent); +} + +.typo-caption { + font-size: 12px; + font-weight: 400; + letter-spacing: 0.02em; + color: var(--text-secondary); +} + +.typo-micro { + font-size: 11px; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--text-secondary); +} + +/* ===== 表面层级系统(Surface Elevation) ===== */ + +.surface-1 { + background: var(--bg-secondary); + border: 1px solid var(--border); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04); +} + +.surface-2 { + background: rgba(255, 255, 255, 0.75); + backdrop-filter: blur(20px) saturate(1.1); + -webkit-backdrop-filter: blur(20px) saturate(1.1); + border: 1px solid rgba(46, 139, 122, 0.12); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06), + 0 1px 2px rgba(0, 0, 0, 0.04); +} + +.surface-3 { + background: rgba(255, 255, 255, 0.9); + backdrop-filter: blur(24px) saturate(1.15); + -webkit-backdrop-filter: blur(24px) saturate(1.15); + border: 1px solid rgba(46, 139, 122, 0.18); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08), + 0 2px 8px rgba(0, 0, 0, 0.04); +} + +/* ===== 网格底纹(绢本纹理潜意识层) ===== */ +body::before { + content: ''; + position: fixed; + inset: 0; + background-image: radial-gradient(circle, var(--accent) 0.5px, transparent 0.5px); + background-size: 24px 24px; + opacity: 0.018; + pointer-events: none; + z-index: 0; } /* ===== 山雾背景 — 远景层 ===== */ @@ -41,60 +159,50 @@ body { 100% { transform: translate(0, 0) scale(1); } } -body::before { - content: ''; +.fog-layer-far { position: fixed; inset: -10%; background: - radial-gradient(ellipse 80% 50% at 15% 75%, rgba(77, 184, 164, 0.07) 0%, transparent 60%), - radial-gradient(ellipse 60% 40% at 75% 25%, rgba(58, 143, 183, 0.05) 0%, transparent 55%), - radial-gradient(ellipse 90% 60% at 50% 50%, rgba(77, 184, 164, 0.03) 0%, transparent 70%); + radial-gradient(ellipse 80% 50% at 15% 75%, rgba(46, 139, 122, 0.04) 0%, transparent 60%), + radial-gradient(ellipse 60% 40% at 75% 25%, rgba(58, 127, 184, 0.03) 0%, transparent 55%), + radial-gradient(ellipse 90% 60% at 50% 50%, rgba(46, 139, 122, 0.02) 0%, transparent 70%); animation: fogDriftFar 60s ease-in-out infinite; pointer-events: none; z-index: 0; } -body::after { - content: ''; +.fog-layer-near { position: fixed; inset: -15%; background: - radial-gradient(ellipse 70% 45% at 80% 70%, rgba(77, 184, 164, 0.06) 0%, transparent 55%), - radial-gradient(ellipse 50% 60% at 25% 30%, rgba(58, 143, 183, 0.04) 0%, transparent 50%); + radial-gradient(ellipse 70% 45% at 80% 70%, rgba(46, 139, 122, 0.03) 0%, transparent 55%), + radial-gradient(ellipse 50% 60% at 25% 30%, rgba(58, 127, 184, 0.025) 0%, transparent 50%); animation: fogDriftNear 45s ease-in-out infinite; pointer-events: none; z-index: 0; } -/* ===== 山雾玻璃面板 ===== */ +/* ===== 山雾玻璃面板(兼容旧用法) ===== */ .glass-panel { - background: - linear-gradient(165deg, rgba(16, 30, 24, 0.15) 0%, transparent 50%), - rgba(16, 30, 24, 0.55); - backdrop-filter: blur(24px) saturate(1.1); - -webkit-backdrop-filter: blur(24px) saturate(1.1); + background: rgba(255, 255, 255, 0.65); + backdrop-filter: blur(20px) saturate(1.1); + -webkit-backdrop-filter: blur(20px) saturate(1.1); border: 1px solid var(--border); } -/* ===== 静态霓虹边框(卡片等普通元素) ===== */ +/* ===== 静态发光边框(卡片等普通元素) ===== */ .neon-border { border: 1px solid var(--border); - transition: border-color 200ms ease, box-shadow 200ms ease; + transition: border-color 200ms ease, box-shadow 200ms ease, transform 200ms ease; } .neon-border:hover { border-color: var(--border-glow); - box-shadow: 0 0 18px rgba(77, 184, 164, 0.08), - inset 0 0 12px rgba(77, 184, 164, 0.03); + box-shadow: 0 0 18px rgba(46, 139, 122, 0.08), + inset 0 0 12px rgba(46, 139, 122, 0.03); + transform: translateY(-1px); } /* ===== 云烟缭绕边框 ===== */ -/* - * .glow-border — 选中态的云烟边框 - * 常态:石青色微弱均匀发光 - * hover:云烟缓慢缭绕,opacity 平滑过渡 - * 移开:云烟淡散回静态 - */ - @property --glow-angle { syntax: ""; initial-value: 0deg; @@ -107,19 +215,16 @@ body::after { .glow-border { position: relative; background: var(--bg-secondary); - border: 1px solid rgba(77, 184, 164, 0.18) !important; - box-shadow: 0 0 10px rgba(77, 184, 164, 0.05), - inset 0 0 8px rgba(77, 184, 164, 0.02); + border: 1px solid rgba(46, 139, 122, 0.20) !important; + box-shadow: 0 0 10px rgba(46, 139, 122, 0.06); transition: border-color 400ms ease, box-shadow 400ms ease; z-index: 0; } .glow-border:hover { - border-color: rgba(77, 184, 164, 0.28) !important; - box-shadow: 0 0 16px rgba(77, 184, 164, 0.08), - inset 0 0 12px rgba(77, 184, 164, 0.03); + border-color: rgba(46, 139, 122, 0.32) !important; + box-shadow: 0 0 16px rgba(46, 139, 122, 0.10); } -/* 云烟边框层 — 宽弧段柔化渐变,6s 缓慢旋转 */ .glow-border::before { content: ''; position: absolute; @@ -130,11 +235,11 @@ body::after { from var(--glow-angle), transparent 0%, transparent 15%, - rgba(77, 184, 164, 0.2) 25%, - rgba(58, 143, 183, 0.35) 38%, - rgba(77, 184, 164, 0.4) 50%, - rgba(58, 143, 183, 0.35) 62%, - rgba(77, 184, 164, 0.2) 75%, + rgba(46, 139, 122, 0.2) 25%, + rgba(58, 127, 184, 0.3) 38%, + rgba(46, 139, 122, 0.35) 50%, + rgba(58, 127, 184, 0.3) 62%, + rgba(46, 139, 122, 0.2) 75%, transparent 85%, transparent 100% ); @@ -149,7 +254,6 @@ body::after { z-index: -1; } -/* 雾气扩散层 — 更大范围、更模糊 */ .glow-border::after { content: ''; position: absolute; @@ -159,9 +263,9 @@ body::after { from var(--glow-angle), transparent 0%, transparent 20%, - rgba(77, 184, 164, 0.06) 35%, - rgba(58, 143, 183, 0.08) 50%, - rgba(77, 184, 164, 0.06) 65%, + rgba(46, 139, 122, 0.05) 35%, + rgba(58, 127, 184, 0.06) 50%, + rgba(46, 139, 122, 0.05) 65%, transparent 80%, transparent 100% ); @@ -194,6 +298,144 @@ body::after { ); } +/* ===== 数据流光(AI 活动时输入栏上方) ===== */ +@keyframes dataStream { + 0% { transform: translateX(-100%); } + 100% { transform: translateX(200%); } +} +.data-stream { + height: 2px; + background: linear-gradient( + 90deg, + transparent 0%, + var(--accent) 30%, + var(--accent-secondary) 50%, + var(--accent) 70%, + transparent 100% + ); + animation: dataStream 2s ease-in-out infinite; + opacity: 0.7; +} +.data-stream-slow { + animation-duration: 3s; +} +.data-stream-fast { + animation-duration: 1.2s; +} + +/* ===== 水波纹 loading 动画 ===== */ +@keyframes waveRise { + 0%, 100% { transform: scaleY(0.4); } + 50% { transform: scaleY(1); } +} +.wave-dot { + width: 4px; + height: 16px; + border-radius: 2px; + background: var(--accent); + animation: waveRise 1.2s ease-in-out infinite; + transform-origin: center bottom; +} + +/* ===== 图片加载占位 — 石青渐变呼吸脉冲 ===== */ +@keyframes stoneBreath { + 0%, 100% { opacity: 0.6; } + 50% { opacity: 1; } +} +.stone-placeholder { + background: linear-gradient(135deg, rgba(46, 139, 122, 0.10) 0%, rgba(58, 127, 184, 0.06) 100%); + animation: stoneBreath 2s ease-in-out infinite; + border-radius: 8px; +} + +/* ===== shimmer 骨架屏 ===== */ +@keyframes shimmer { + 0% { background-position: -200% 0; } + 100% { background-position: 200% 0; } +} +.skeleton-shimmer { + background: linear-gradient( + 90deg, + var(--bg-tertiary) 0%, + var(--bg-secondary) 40%, + var(--bg-tertiary) 60%, + var(--bg-tertiary) 100% + ); + background-size: 200% 100%; + animation: shimmer 1.8s ease-in-out infinite; + border-radius: 8px; +} + +/* ===== 涟漪效果 ===== */ +@keyframes rippleExpand { + 0% { transform: scale(0); opacity: 0.4; } + 100% { transform: scale(2.5); opacity: 0; } +} +.ripple-ring { + position: absolute; + width: 20px; + height: 20px; + border-radius: 50%; + border: 2px solid var(--accent); + animation: rippleExpand 600ms ease-out forwards; + pointer-events: none; +} + +/* ===== 水墨晕染揭示 ===== */ +@keyframes inkReveal { + 0% { clip-path: circle(0% at 50% 50%); } + 100% { clip-path: circle(75% at 50% 50%); } +} +.ink-reveal { + animation: inkReveal 600ms ease-out forwards; +} + +/* ===== 错误状态震动 ===== */ +@keyframes errorShake { + 0%, 100% { transform: translateX(0); } + 20% { transform: translateX(-2px); } + 40% { transform: translateX(2px); } + 60% { transform: translateX(-2px); } + 80% { transform: translateX(2px); } +} +.error-shake { + animation: errorShake 300ms ease-out; +} + +/* ===== 呼吸脉冲(发送按钮就绪态) ===== */ +@keyframes breathPulse { + 0%, 100% { box-shadow: 0 0 8px rgba(46, 139, 122, 0.18); } + 50% { box-shadow: 0 0 20px rgba(46, 139, 122, 0.35); } +} +.breath-pulse { + animation: breathPulse 2s ease-in-out infinite; +} + +/* ===== 输入框水面感应 ===== */ +@keyframes waterExpand { + 0% { transform: scaleX(0); } + 100% { transform: scaleX(1); } +} +.water-focus-line { + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 2px; + background: linear-gradient(90deg, transparent 0%, var(--accent) 50%, transparent 100%); + transform: scaleX(0); + transition: transform 300ms ease-out; + pointer-events: none; +} +.water-focus-active .water-focus-line { + transform: scaleX(1); +} + +/* ===== 用户消息气泡 — 左上切角 ===== */ +.user-bubble { + border-radius: 2px 16px 16px 16px; +} + /* ===== 自定义滚动条 ===== */ ::-webkit-scrollbar { width: 6px; @@ -203,11 +445,11 @@ body::after { background: transparent; } ::-webkit-scrollbar-thumb { - background: rgba(77, 184, 164, 0.15); + background: rgba(46, 139, 122, 0.15); border-radius: 3px; } ::-webkit-scrollbar-thumb:hover { - background: rgba(77, 184, 164, 0.28); + background: rgba(46, 139, 122, 0.30); } select option { @@ -215,6 +457,7 @@ select option { color: var(--text-primary); } +/* ===== 通用动画 ===== */ .sidebar-overlay { animation: fadeIn 200ms ease-out; } @@ -226,7 +469,81 @@ select option { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } } +@keyframes floatUp { + from { opacity: 1; transform: translateY(0); } + to { opacity: 0; transform: translateY(-12px); } +} +/* ===== 通用按钮 hover 浮起 ===== */ +.btn-hover-lift { + transition: transform 200ms ease, box-shadow 200ms ease; +} +.btn-hover-lift:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); +} + +/* ===== 侧栏会话左侧竖线 hover ===== */ +.session-hover-line { + position: relative; +} +.session-hover-line::before { + content: ''; + position: absolute; + left: 0; + top: 4px; + bottom: 4px; + width: 2px; + background: var(--accent); + border-radius: 1px; + transform: scaleY(0); + transition: transform 200ms ease-out; + transform-origin: center; +} +.session-hover-line:hover::before { + transform: scaleY(1); +} + +/* ===== 全屏加载 — 云雾凝聚 ===== */ +@keyframes mistCondense { + 0% { transform: scale(1.5); opacity: 0; filter: blur(20px); } + 50% { transform: scale(1.1); opacity: 0.6; filter: blur(8px); } + 100% { transform: scale(1); opacity: 1; filter: blur(0); } +} +@keyframes mistRing { + 0% { transform: scale(0.3); opacity: 0; } + 50% { opacity: 0.5; } + 100% { transform: scale(2); opacity: 0; } +} +.mist-loader { + position: relative; + width: 80px; + height: 80px; + display: flex; + align-items: center; + justify-content: center; +} +.mist-loader-ring { + position: absolute; + inset: 0; + border-radius: 50%; + border: 2px solid var(--accent); + animation: mistRing 2s ease-out infinite; + opacity: 0; +} +.mist-loader-ring-2 { + animation-delay: 1s; +} +.mist-loader-core { + width: 12px; + height: 12px; + border-radius: 50%; + background: var(--accent); + box-shadow: 0 0 20px rgba(46, 139, 122, 0.4), 0 0 60px rgba(46, 139, 122, 0.15); + animation: mistCondense 1.5s ease-out forwards, breathPulse 2s ease-in-out 1.5s infinite; +} + +/* ===== 100dvh 兼容 ===== */ @supports (height: 100dvh) { .h-screen { height: 100dvh; diff --git a/art-agent/frontend/src/app/login/page.tsx b/art-agent/frontend/src/app/login/page.tsx index 52f0c4f..ed2b77f 100644 --- a/art-agent/frontend/src/app/login/page.tsx +++ b/art-agent/frontend/src/app/login/page.tsx @@ -1,8 +1,11 @@ "use client"; import { useState, useEffect, type FormEvent } from "react"; +import { motion, AnimatePresence } from "framer-motion"; import { useRouter } from "next/navigation"; import { useAuth } from "@/lib/auth-context"; +import { AmbientParticles } from "@/components/ui/ambient-particles"; +import { staggerContainer, staggerItem, buttonTap, errorFloat } from "@/components/ui/motion-presets"; export default function LoginPage() { const { login, isAuthenticated } = useAuth(); @@ -32,6 +35,7 @@ export default function LoginPage() { router.replace("/"); } catch (err) { setError(err instanceof Error ? err.message : "登录失败"); + setTimeout(() => setError(""), 4000); } finally { setLoading(false); } @@ -39,24 +43,19 @@ export default function LoginPage() { return (
- {/* 背景环境光 */} -
-
-
+ -
-
+ + {/* Logo 发光 */}
+ shadow-[0_0_40px_rgba(46,139,122,0.15)]"> @@ -64,12 +63,15 @@ export default function LoginPage() {
-

EPEEKit

-

AI 美术资源生成工具

-
+

EPEEKit

+

AI 美术资源生成工具

+ -
+
@@ -97,32 +99,42 @@ export default function LoginPage() { bg-[var(--bg-primary)] border border-[var(--border)] text-[var(--text-primary)] placeholder:text-[var(--text-secondary)] focus:outline-none focus:border-[var(--accent)]/50 - focus:shadow-[0_0_10px_rgba(77,184,164,0.06)] + focus:shadow-[0_0_10px_rgba(46,139,122,0.06)] transition-all" />
- {error && ( -
- {error} -
- )} + + {error && ( + + {error} + + )} + - - -
+ + +
); } diff --git a/art-agent/frontend/src/app/page.tsx b/art-agent/frontend/src/app/page.tsx index 0600c65..7f3b3c7 100644 --- a/art-agent/frontend/src/app/page.tsx +++ b/art-agent/frontend/src/app/page.tsx @@ -1,14 +1,17 @@ "use client"; import { useCallback, useEffect, useRef, useState, type DragEvent } from "react"; +import { motion, AnimatePresence } from "framer-motion"; import { ChatMessages } from "@/components/chat/chat-messages"; import { ChatInput, type ChatInputHandle } from "@/components/chat/chat-input"; import { Sidebar } from "@/components/sidebar/sidebar"; import { TopNav } from "@/components/layout/top-nav"; import { ImageDetailPanel } from "@/components/detail/image-detail-panel"; +import { AmbientParticles } from "@/components/ui/ambient-particles"; import { useApp } from "@/lib/app-context"; import { sendChat, getImageUrl, uploadRefImage } from "@/lib/api"; import { generateId } from "@/lib/store"; +import { scrollBtnEnter, scrollBtnClick, staggerContainer, staggerItem } from "@/components/ui/motion-presets"; import type { ChatMessage, ImageAsset, ApiMessage, AnnotationData } from "@/lib/types"; export default function Home() { @@ -29,6 +32,7 @@ export default function Home() { const [streamingText, setStreamingText] = useState(""); const [streamingImages, setStreamingImages] = useState([]); const [statusText, setStatusText] = useState(""); + const [isGeneratingImage, setIsGeneratingImage] = useState(false); const [pendingAnnotation, setPendingAnnotation] = useState(null); const [lastRefImageUrls, setLastRefImageUrls] = useState([]); const lastRefPerSession = useRef>(new Map()); @@ -57,7 +61,6 @@ export default function Home() { } }, []); - // 实时记录当前会话的滚动位置 + 判断是否显示"回到底部"按钮 useEffect(() => { const el = scrollRef.current; if (!el || !activeSessionId) return; @@ -72,7 +75,6 @@ export default function Home() { return () => el.removeEventListener("scroll", handler); }, [activeSessionId]); - // 会话切换:标记 switching,等 DOM 更新后恢复位置 + 恢复参考图状态 useEffect(() => { if (!activeSessionId) return; if (prevSessionId.current && prevSessionId.current !== activeSessionId) { @@ -152,6 +154,7 @@ export default function Home() { }; appendMessage(activeSessionId, userMessage); setIsLoading(true); + setIsGeneratingImage(false); setStreamingText(""); setStreamingImages([]); setStatusText(""); @@ -179,6 +182,7 @@ export default function Home() { case "tool_start": setStatusText(event.data.message as string); + setIsGeneratingImage(true); scrollToBottom(); break; @@ -207,6 +211,7 @@ export default function Home() { collectedImages = [...collectedImages, ...newAssets]; setStreamingImages([...collectedImages]); + setIsGeneratingImage(false); if (modelName) { usedModelName = modelName; setStatusText(`由 ${modelName} 生成`); @@ -225,6 +230,7 @@ export default function Home() { assistantText += `\n\n⚠️ 图片生成失败${modelHint}:\n${errorDetail}`; setStreamingText(assistantText); setStatusText(""); + setIsGeneratingImage(false); scrollToBottom(); break; } @@ -256,6 +262,7 @@ export default function Home() { }; appendMessage(activeSessionId, assistantMessage); setIsLoading(false); + setIsGeneratingImage(false); setStreamingText(""); setStreamingImages([]); setStatusText(""); @@ -268,8 +275,13 @@ export default function Home() { setPendingAnnotation(data); }, []); + const isEmptyState = messages.length === 0 && !isLoading; + return (
+ {/* 环境粒子(萤火 + 云雾) */} + +
@@ -323,7 +335,7 @@ export default function Home() { border border-[var(--border)] text-[var(--text-secondary)] hover:text-[var(--accent)] hover:border-[var(--accent)]/40 - transition-all cursor-pointer + transition-all cursor-pointer btn-hover-lift hidden md:flex" title="展开侧边栏" > @@ -341,7 +353,7 @@ export default function Home() { src={pendingAnnotation.snapshot} alt="标注预览" className="w-10 h-10 rounded-lg object-cover border border-[var(--accent)]/40 - shadow-[0_0_8px_rgba(77,184,164,0.12)]" + shadow-[0_0_8px_rgba(46,139,122,0.12)]" /> 标注已就绪({pendingAnnotation.annotations.length} 处)— 输入修改意见后发送 @@ -356,83 +368,103 @@ export default function Home() {
)} -
- {messages.length === 0 && !isLoading ? ( -
-
- {/* Logo 发光效果 */} -
- - - - - - +
+
+ {isEmptyState ? ( +
+ + {/* Logo 发光效果 */} + + + + + + + + + +

+ 欢迎使用 EPEEKit +

+

+ 描述你想要的美术资源,我来帮你生成。 +
+ 你可以上传参考图来引导风格方向。 +

+
+ + {[ + "画一个赛博朋克风格的退出按钮", + "设计一个卡通风格的金币图标", + "画一个奇幻风格的游戏角色立绘", + ].map((hint) => ( + + ))} + +
-

- 欢迎使用 EPEEKit -

-

- 描述你想要的美术资源,我来帮你生成。 -
- 你可以上传参考图来引导风格方向。 -

-
- {[ - "画一个赛博朋克风格的退出按钮", - "设计一个卡通风格的金币图标", - "画一个奇幻风格的游戏角色立绘", - ].map((hint) => ( - - ))} -
-
+ ) : ( + + )}
- ) : ( - - )}
- {showScrollBtn && ( - - )} + {/* 回到底部 — 落叶归根(水滴形) */} + + {showScrollBtn && ( + { + scrollToBottom(); + }} + className="absolute bottom-16 right-4 z-10 + w-9 h-11 flex items-center justify-center + surface-2 text-[var(--text-secondary)] + hover:text-[var(--accent)] hover:border-[var(--accent)]/40 + shadow-lg shadow-black/30 + cursor-pointer" + style={{ borderRadius: "50% 50% 50% 50% / 35% 35% 65% 65%" }} + title="回到底部" + > + + + + + )} + getImageUrl(u))} onClearLastRefImage={() => { diff --git a/art-agent/frontend/src/components/chat/chat-input.tsx b/art-agent/frontend/src/components/chat/chat-input.tsx index 8c9336c..1953464 100644 --- a/art-agent/frontend/src/components/chat/chat-input.tsx +++ b/art-agent/frontend/src/components/chat/chat-input.tsx @@ -1,8 +1,12 @@ "use client"; import { useRef, useState, useCallback, useImperativeHandle, forwardRef, type KeyboardEvent, type DragEvent, type ClipboardEvent } from "react"; +import { motion } from "framer-motion"; import { ModelSelector } from "./model-selector"; +import { DataStream } from "@/components/ui/data-stream"; +import { useRipple, RippleLayer } from "@/components/ui/ripple-effect"; import { uploadRefImage, type UploadProgress } from "@/lib/api"; +import { sendBounce } from "@/components/ui/motion-presets"; type UploadStatus = "idle" | "uploading" | "done" | "error"; @@ -26,6 +30,8 @@ interface ChatInputProps { onClearLastRefImage?: () => void; /** 文件在 ChatInput 区域内被 drop 时触发,通知父组件清除拖拽覆盖层 */ onFileDrop?: () => void; + /** 是否正在生成图片(用于数据流光加速) */ + isGeneratingImage?: boolean; } export interface ChatInputHandle { @@ -34,7 +40,6 @@ export interface ChatInputHandle { let _uploadCounter = 0; -/** 从剪贴板提取可上传的图片文件(Ctrl+V / 右键粘贴共用) */ function imageFilesFromClipboard(data: DataTransfer | null): File[] { if (!data?.items?.length) return []; const out: File[] = []; @@ -49,25 +54,38 @@ function imageFilesFromClipboard(data: DataTransfer | null): File[] { } export const ChatInput = forwardRef(function ChatInput( - { onSend, disabled, lastRefServerUrls, lastRefPreviewUrls, onClearLastRefImage, onFileDrop }, + { onSend, disabled, lastRefServerUrls, lastRefPreviewUrls, onClearLastRefImage, onFileDrop, isGeneratingImage }, ref ) { const [text, setText] = useState(""); const [uploads, setUploads] = useState([]); const [isDragging, setIsDragging] = useState(false); const [selectedModel, setSelectedModel] = useState(""); + const [isFocused, setIsFocused] = useState(false); + const { ripples, trigger: triggerRipple } = useRipple(); const fileInputRef = useRef(null); + const sendBtnRef = useRef(null); const abortRefs = useRef>(new Map()); const hasActiveUpload = uploads.some((u) => u.status === "uploading"); const allDone = uploads.length > 0 && uploads.every((u) => u.status === "done" || u.status === "error"); const doneUrls = uploads.filter((u) => u.status === "done" && u.serverUrl).map((u) => u.serverUrl!); + const canSend = text.trim() && !disabled && !hasActiveUpload; + const handleSubmit = () => { const trimmed = text.trim(); if (!trimmed || disabled) return; if (hasActiveUpload) return; + // 投石入水涟漪 + if (sendBtnRef.current) { + const rect = sendBtnRef.current.getBoundingClientRect(); + const parent = sendBtnRef.current.offsetParent as HTMLElement | null; + const parentRect = parent?.getBoundingClientRect() ?? rect; + triggerRipple(rect.left - parentRect.left + rect.width / 2, rect.top - parentRect.top + rect.height / 2); + } + const refUrls = doneUrls.length > 0 ? doneUrls : (lastRefServerUrls ?? []); onSend(trimmed, refUrls, selectedModel || null); setText(""); @@ -183,7 +201,7 @@ export const ChatInput = forwardRef(function Ch return (
(function Ch onDrop={handleDrop} onPasteCapture={handlePasteCapture} > - {isDragging && ( -
- 松开以添加参考图(可多张) -
- )} + {/* 数据流光 */} + - {/* 沿用上次参考图提示 */} - {uploads.length === 0 && lastRefPreviewUrls && lastRefPreviewUrls.length > 0 && ( -
-
- {lastRefPreviewUrls.map((url, i) => ( - {`沿用参考图 - ))} +
+ {isDragging && ( +
+ 松开以添加参考图(可多张)
- - 沿用上次参考图({lastRefPreviewUrls.length} 张) - - -
- )} + )} - {/* 参考图上传状态区(多图) */} - {uploads.length > 0 && ( -
- {uploads.map((item) => ( -
- {item.previewUrl && ( + {/* 沿用上次参考图提示 */} + {uploads.length === 0 && lastRefPreviewUrls && lastRefPreviewUrls.length > 0 && ( +
+
+ {lastRefPreviewUrls.map((url, i) => ( 参考图 + ))} +
+ + 沿用上次参考图({lastRefPreviewUrls.length} 张) + + +
+ )} + + {/* 参考图上传状态区(多图) */} + {uploads.length > 0 && ( +
+ {uploads.map((item) => ( +
+ {item.previewUrl && ( + 参考图 + )} + {item.status === "uploading" && ( +
+ + + + +
+ )} + {item.status === "done" && ( +
+ ✓ +
+ )} + {item.status === "error" && ( +
+ ! +
+ )} + {item.status !== "uploading" && ( + + )} +
+ ))} +
+ {hasActiveUpload && ( + + + 上传中... + )} - {item.status === "uploading" && ( -
- - - - -
+ {allDone && ( + + {doneUrls.length} 张参考图已就绪 + )} - {item.status === "done" && ( -
- ✓ -
- )} - {item.status === "error" && ( -
- ! -
- )} - {item.status !== "uploading" && ( - + {uploads.some((u) => u.status === "error") && ( + + {uploads.filter((u) => u.status === "error").length} 张上传失败 + )}
- ))} - {/* 汇总状态 */} -
- {hasActiveUpload && ( - - - 上传中... - - )} - {allDone && ( - - {doneUrls.length} 张参考图已就绪 - - )} - {uploads.some((u) => u.status === "error") && ( - - {uploads.filter((u) => u.status === "error").length} 张上传失败 - - )}
+ )} + +
+ + + {/* 上传参考图按钮 */} + + + + {/* 文字输入框 — 水面感应 */} +
+