From 41664b2ae3b1139f7f00401e2aeb5b483319d29e Mon Sep 17 00:00:00 2001 From: brother7 <7brother7@gmail.com> Date: Tue, 15 Sep 2026 14:58:21 +0800 Subject: [PATCH] feat(canvas): preview generated task images in app --- .../20260915-design-image-preview-b24997a6.md | 51 +++++++ README.md | 2 +- src/pages/ImageCanvas/DesignImagePreview.tsx | 103 ++++++++++++++ src/pages/ImageCanvas/DesignPlanHistory.tsx | 4 +- tests/e2e/image-workspace-v2.spec.ts | 74 +++++++++- tests/unit/design-image-preview.test.tsx | 126 ++++++++++++++++++ 6 files changed, 357 insertions(+), 3 deletions(-) create mode 100644 .project-docs/30-worklog/tasks/20260915-design-image-preview-b24997a6.md create mode 100644 src/pages/ImageCanvas/DesignImagePreview.tsx create mode 100644 tests/unit/design-image-preview.test.tsx diff --git a/.project-docs/30-worklog/tasks/20260915-design-image-preview-b24997a6.md b/.project-docs/30-worklog/tasks/20260915-design-image-preview-b24997a6.md new file mode 100644 index 0000000..38c61e7 --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260915-design-image-preview-b24997a6.md @@ -0,0 +1,51 @@ +# Task: Add enlarged image preview to AI Design task results + +## Identity + +- Task ID: 20260915-design-image-preview-b24997a6 +- Mode: Feature +- Branch: codex/20260915-design-image-preview-b24997a6-design-image-preview +- Worktree: D:\Datas\OthersProjects\.codex-worktrees\makelore\20260915-design-image-preview-b24997a6 +- Base commit: 80e52496d2696c3b579c911ee7c07f9588b368a6 +- Owner: codex +- Status: Ready for Integration + +## Scope + +- Add an in-app enlarged image viewer to generated images in AI Design task history, on desktop and compact layouts. Preserve the existing download action. +- Own DesignPlanHistory, its new image-preview component, focused unit/Electron coverage, README, and this record. + +## Intent And Constraints + +- Concurrent Gate Passed: bundled check/start succeeded and status confirmed the exact task, feature mode, branch, worktree and base above. Main contains three pre-existing untracked task records, so work is isolated from main. +- Planning Gate Passed: read agent entry, own task, memory index, positioning, integrated state, decision index, architecture, domain/success criteria, ADR-007, evidence/reflection/commitment/stale indexes and peer task scopes. Most registered peers are completed packaging/integration or historical reviews; active known work is unrelated, except an old Enter-key E2E test in a retired file. Placeholder peer scopes remain unknown, with no concrete semantic dependency on this feature. +- Positioning contains placeholders; use README, executable code and accepted V2 rules. Asset URL resolution remains through the existing Main-owned Host API helper. No contract, generation, billing, provider, persistence or global theme change. +- User prohibits subagents without consent; implementation and verification are performed directly. +- Implement a persistent enlarge icon beside download, a light modal that fits the complete image, optional native-size scrolling, load/error state, and accessible close/Escape behavior. Reset preview across Workspace switches. +- Verify selected-asset correctness, absence of download side effects, failures and dismissal with focused tests; verify rendered dimensions and desktop/compact interaction in Electron, then typecheck/lint/build for renderer integration. + +## Outcome + +- Generated image thumbnails now expose a persistent enlarge button beside the existing download button in task history. Desktop rail and compact conversation history share the same implementation. +- The light in-app dialog uses the existing canonical Asset content URL, initially fits the complete image, and supports native-size scrolling, dimensions, loading/error feedback, close button, Escape, and focus restoration. Viewing never calls the download endpoint. Video actions remain unchanged. +- Workspace changes remount history content to dismiss the old preview; reopening resets the viewer and permits retry after a load error. README reflects the new behavior. +- Feature complete in the isolated task branch; no main merge, push, package or installed-client update performed. + +## Verification + +- Exact pinned pnpm 10.33.4; frozen-lockfile install succeeded without lockfile changes. +- `pnpm run typecheck`: passed. +- `pnpm exec vitest run tests/unit/design-image-preview.test.tsx tests/unit/image-canvas-page.test.tsx tests/unit/image-workspace-api.test.ts --maxWorkers=4`: 31 passed, including five new preview cases. Covers selected result, image-only action, fit/native mode, reopening, close/Escape/focus, independent download, URL/image failures, and Workspace changes. +- Scoped ESLint over both changed product files and both test files: passed with no warnings or errors. +- `pnpm run build:vite`: all Renderer/Main/Preload/utility targets passed. Existing Browserslist age, chunk-size and mixed-import warnings remain; none prevented compilation. +- `pnpm exec playwright test tests/e2e/image-workspace-v2.spec.ts -g 'keeps conversation'`: 1 passed on Windows Electron. Existing V2 edit/confirmation flow remains covered; completed task preview verifies a 1600×2400 fixture, fit behavior, exact native dimensions and scrolling, Escape/focus restoration, 375px compact bounds and close action, with zero download requests. +- Electron uses deterministic local Host API/media fixtures; no paid generation or production asset access is claimed. The full unrelated unit suite was not run. +- Task Documentation Gate: reviewed changes against recorded base; `git diff --check` and the bundled task-aware documentation drift check passed. + +## Follow-ups + +- Integrate this feature branch and rebuild the desktop client to deliver the button to installed users. + +## Promotion Candidates + +- None. This is a local presentation improvement within the accepted V2 Asset and Main-network boundaries; README documents the user-visible behavior without changing canonical architecture or domain rules. diff --git a/README.md b/README.md index 0857386..03aa8fa 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Makelore 是一个面向软件、视觉创作、智能机器人与个人云智 Agents 侧栏的「渠道」统一管理个人微信账号。每个账号由唯一扫码微信身份连接,可以在发布智能体前先扫码,之后选择或更换目标智能体;多个微信账号可以使用同一个已发布智能体,切换目标不需要重新扫码。账号可单独启用、暂停、重新连接或断开,不提供联系人邀请或授权调用者管理。Agent 的「使用与分享」只展示已关联渠道并提供定位入口。选中账号的二级详情保留运行/投递活动、失败文件补发和微信对话;对话可审批、停止、新开一段并保存实际产物,不同账号的会话相互隔离。自动任务可明确选择把结果发送到已经开始对话的微信账号,暂停保留目标;渠道触发的聊天等应计费模型调用仍由智能体创建者支付,费用统计包含微信渠道。渠道需要配套版本的 AgentBus Core/微信 Adapter、Yuxi 和 Works Square,桌面用户无需填写服务凭据。Main 保管操作恢复记录,重开界面不会自动重发绑定或执行请求;验证码不写入恢复记录,已移除的联系人权限操作记录只能丢弃。用户与应用分享不属于微信渠道账号管理范围。 -- `Makelore Canvas|AI 绘画`:每个设计项目(Workspace)维护一份从创建起就存在的 Living Form。左侧项目栏负责新建、切换和管理 Workspace,并在桌面设计模式下以 256px 宽度常驻展开;中央沿用 AI 编程的安静对话画布、自然消息流和底部悬浮输入器,AI 整理出的制作方案作为对话内的轻量可编辑稿持续更新;桌面端右侧同为 256px 的全高历史作品栏集中展示当前项目的制作记录与生成结果。紧凑窗口通过左侧抽屉访问项目列表,历史记录保留在时间线中。参考图从本地上传后以 `@图片N` 绑定,具体用法只写在创作提示词中。 +- `Makelore Canvas|AI 绘画`:每个设计项目(Workspace)维护一份从创建起就存在的 Living Form。左侧项目栏负责新建、切换和管理 Workspace,并在桌面设计模式下以 256px 宽度常驻展开;中央沿用 AI 编程的安静对话画布、自然消息流和底部悬浮输入器,AI 整理出的制作方案作为对话内的轻量可编辑稿持续更新;桌面端右侧同为 256px 的全高历史作品栏集中展示当前项目的制作记录与生成结果。任务中的已生成图片可通过“放大查看图片”按钮直接打开大图,支持适应窗口、原始尺寸及 Esc 关闭,无需先下载。紧凑窗口通过左侧抽屉访问项目列表,历史记录保留在时间线中。参考图从本地上传后以 `@图片N` 绑定,具体用法只写在创作提示词中。 - `Makelore Robot|AI 机器`:管理机器人智能体、设备激活绑定、智能体配置与设备分配;机器人工作台的智能体位于 Robot 全局侧栏,选中后在内容区先查看绑定设备、再查看基础设置,当前智能体通过 URL 参数保持可分享选择;绑定设备时默认先选择“引导配网”或“已有激活码”。在 Windows 与 macOS 的引导路径中,Makelore 可在弹窗内扫描并连接附近开放的 `Xiaozhi-*` 配网热点,失败时仍可通过系统 Wi-Fi 手动连接;后续继续复用机器人现有热点配网页面,不修改固件,也不由 Makelore 接收 Wi-Fi 密码。 应用启动默认进入 AI 模块入口选择页。入口页可在未登录状态浏览;未登录用户点击已开通模块时进入客户端原生登录页,可使用账号密码或手机号短信验证码登录。密码登录可选“记住密码”:正式安装包仅由 Electron Main 使用系统受保护凭据存储加密保存和回填账号密码,不写入 Renderer 持久状态,未打包开发版或系统安全存储不可用时禁用该选项。登录请求由 Renderer 经 Host API 交给 Electron Main,再由 Main 调用 Works Square;成功后回到入口选择页。已登录时,Electron Main 会从 Works Square `/api/auth/me` 读取当前账号,只向 Renderer 投影用户名、账号/租户/部门标识、权限名列表与四个模块布尔开关,不透传上游资料或凭据。工作区门禁同时要求有效 Token 和完整用户身份;旧状态缺失身份时会先尝试从 Main 恢复,仍无法确认则清除残留会话并返回登录页。被管理员关闭的模块会在入口页置灰且无法点击,直接访问其工作区路径也会返回入口页。旧服务端未返回策略或缺少单项字段时默认开放;这个客户端门禁不替代服务端 API 授权。 diff --git a/src/pages/ImageCanvas/DesignImagePreview.tsx b/src/pages/ImageCanvas/DesignImagePreview.tsx new file mode 100644 index 0000000..8fdd71c --- /dev/null +++ b/src/pages/ImageCanvas/DesignImagePreview.tsx @@ -0,0 +1,103 @@ +import { useEffect, useState } from 'react'; +import { ImageIcon, Loader2, Maximize2, X } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog'; +import { resolveImageWorkspaceAssetUrl } from '@/lib/image-workspace'; +import type { DesignAsset } from '../../../shared/image-workspace'; + +function ImagePreviewContent({ asset }: { asset: DesignAsset }) { + const [source, setSource] = useState(null); + const [loaded, setLoaded] = useState(false); + const [failed, setFailed] = useState(false); + const [originalSize, setOriginalSize] = useState(false); + + useEffect(() => { + let active = true; + void resolveImageWorkspaceAssetUrl(asset.contentPath) + .then((url) => { if (active) setSource(url); }) + .catch(() => { if (active) setFailed(true); }); + return () => { active = false; }; + }, [asset.contentPath]); + + return ( + <> +
+
+ 图片预览 + + {asset.width} × {asset.height} + +
+
+ + + + +
+
+
+ {failed ? ( +
+
+ ) : ( + <> + {!loaded && ( +
+
+ )} + {source && ( + AI 作品大图 setLoaded(true)} + onError={() => setFailed(true)} + /> + )} + + )} +
+ + ); +} + +export function DesignImagePreviewButton({ asset }: { asset: DesignAsset }) { + return ( + + + + + + + + + ); +} diff --git a/src/pages/ImageCanvas/DesignPlanHistory.tsx b/src/pages/ImageCanvas/DesignPlanHistory.tsx index b4343e0..a47632e 100644 --- a/src/pages/ImageCanvas/DesignPlanHistory.tsx +++ b/src/pages/ImageCanvas/DesignPlanHistory.tsx @@ -4,6 +4,7 @@ import { saveImageWorkspaceAsset } from '@/lib/image-workspace'; import { cn } from '@/lib/utils'; import type { DesignGenerationTask, DesignWorkspace } from '../../../shared/image-workspace'; import { DesignAssetThumbnail } from './DesignAssetThumbnail'; +import { DesignImagePreviewButton } from './DesignImagePreview'; import { youthTaskIssueMessage } from './youth-issue-copy'; const STATUS_LABELS: Record = { @@ -41,7 +42,7 @@ export function DesignPlanHistory({ workspace }: { workspace: DesignWorkspace }) if (tasks.length === 0) return null; return ( -
+
{tasks.map((task) => { const resultAssets = task.resultAssetIds .map((assetId) => workspace.assets.find((asset) => asset.assetId === assetId) ?? null) @@ -111,6 +112,7 @@ export function DesignPlanHistory({ workspace }: { workspace: DesignWorkspace }) {resultAssets.map((asset) => (
+ {asset.mediaType === 'image' && }