Compare commits

...
5 Commits
Author SHA1 Message Date
brother7 336e0bb0ca docs: integrate design stream and quote handoff
Electron E2E / Electron E2E (macos-latest) (push) Has been cancelled
Electron E2E / Electron E2E (ubuntu-latest) (push) Has been cancelled
Electron E2E / Electron E2E (windows-latest) (push) Has been cancelled
2026-09-04 11:46:11 +08:00
brother7 23f96a523e fix(design): restore streamed replies and quote handoff 2026-09-04 11:37:03 +08:00
brother7 229b1b1ce3 docs: diagnose AI Design streaming and generation 2026-09-04 11:10:51 +08:00
brother7 aa3f52a8f8 docs: finalize AI Design feedback integration 2026-09-04 10:20:44 +08:00
brother7 3bda17aa3c fix(design): remove redundant assistant progress banner 2026-09-04 10:09:05 +08:00
16 changed files with 828 additions and 46 deletions

No files matched your search

@@ -26,7 +26,7 @@ Makelore 是 Electron 桌面客户端。Renderer 负责项目操作与状态展
| Official Hosted Plugins | Acquired code-owned bundled Game Resource package → effective parent snapshot → code-owned Main adapter → fixed Works Square hosted route | 无设备下载、更新、Beta 或签名步骤;Renderer/Package/Pi 不持有 Provider key、model 或 URL。每次计费操作要求显式确认,child 不继承 hosted tool。历史 Hosted Web Search 不再进入当前客户端。 |
| Device Packages | Conversation install tools → Main-owned inspect/preview/confirm/commit → immutable local generation → parent Skill/Pi-extension resources | 支持 npm、Git、绝对本地 Plugin 目录与 loose `SKILL.md`;没有可见安装入口、Account Library、Release、Admission 或 Marketplace Package Store。可执行 extension 拥有桌面用户权限且生命周期脚本禁用。每个 generation 包含所有显式安装且当前启用的 Skill/extension;新/idle parent 自动刷新,active parent 在 turn settled 后刷新,child 始终为空。 |
| AI Design Workspace & Living Form | 一个 Workspace 的当前 Direction、Current Specification、持久 Agent Session、conversation timeline、Tasks 与 Assets | 自然对话是主创作面;Living Form 仅以“AI 已理解”的紧凑辅助摘要与可选手动调整投影服务端 Current Specification,Renderer 只持有草稿和已接受投影 |
| AI Design Input & Reconciliation | Chat、字段/集合编辑、decision、proposal、lock、Asset binding 与 restore | 全部进入同一 `design.input.apply` reducer;稳定 command/operation ID 支持 unknown-result 重放,revision conflict 刷新权威状态 |
| AI Design Input & Reconciliation | Chat、字段/集合编辑、decision、proposal、lock、Asset binding 与 restore | 全部进入同一 `design.input.apply` reducer;稳定 command/operation ID 支持 unknown-result 重放,revision conflict 刷新权威状态;待提交 chat 从同一 pending operation 临时投影,assistant delta 只能在匹配该 operation 的一个未完成助手气泡中临时绘制且不生成独立整理进度栏 |
| AI Design Gateway Routing | Main-owned Works Square V2 adapter 与 Direction event stream | Main 持有 Works Token、stream ticket、WebSocket、重试分类和错误脱敏;事件顺序与 Task progress 不构成 Specification 真值 |
| AI Design Quote & Task Controls | 精确 Specification revision 编译出的不可变 Quote 与 Workspace Tasks | 客户端只展示 public output summary、warnings、expiry 与 Token Points,并以 Quote ID 确认;Provider Prompt/model/route/storage/billing atoms 不下发 |
| AI Design Assets | Workspace 已完成作品或本地上传的真实 Asset | Asset 通过 typed binding 写入 Specification;生成结果经 Main-owned asset download 保存 |
@@ -88,6 +88,8 @@ Makelore 是 Electron 桌面客户端。Renderer 负责项目操作与状态展
- Main 通过 `design.input.apply`、`design.quote.request` 和 `design.generation.confirm` 访问 V2。unknown transport result 必须复用原 command/operation identity;结构化业务错误不得重放,未知上游文本不得穿透安全投影。
- Main-owned Canvas Workspace JSON 请求和 shared Works token refresh 的完整生命周期最多 30 秒,超时 abort transport 并以固定 `504 DESIGN_WORKSPACE_REQUEST_TIMEOUT` 结束 Renderer 等待。Electron `net.fetch` 失败后的 Node fetch 透明回退只允许 `GET`、`HEAD`、`OPTIONS`;PATCH/POST 等 mutation 只允许由持有显式幂等身份的上层协议决定重试,不能由底层 transport 隐式重放。该边界不改变上面的 WebSocket→REST 幂等 fallback。
- Renderer 的异步与流式结果必须核对 Workspace、Direction、revision 和 operation identity;Direction snapshot 是 Specification 真值,Task/Asset 事件只更新 Workspace resource projection。
- 待提交 chat 可以从现有 pending operation 立即投影为明确标注“发送中/正在确认”的用户气泡,但只有服务端 canonical turn 能进入对话历史;确定失败必须恢复原草稿,不能另建一套消息状态或新业务意图。
- `design.assistant.delta` 是未完成的传输状态,不是 canonical assistant turn。Renderer 只能把它临时绘制为与同一 pending chat identity 绑定的单个未完成助手气泡,并在该 operation 确定收敛后由 canonical turn 替换;不得持久化为第二条消息、伪装为已完成回复或创建独立整理进度栏。unknown outcome 保留原 identity 与已有片段,其他 operation 的更新不得全局清除它;右侧 Current Specification 继续呈现 AI 当前整理出的设计理解。重叠连接与重放 chunk 必须按连接 generation 和 `chunkIndex` 收敛。
- 确认生成只提交不可变 Quote ID。Task 事务已提交但事件迟到时可刷新 Workspace projection;Task progress 不得改写 Living Form 或授权新的生成操作。
- Updater feed 选择、原始错误日志、下载和安装生命周期只属于 Electron Main。Renderer 不得把缺失稳定 manifest 投影为“已是最新版”,也不得显示原始堆栈、URL、路径或错误码;并发检查共享同一错误事件时只发送一次错误状态,后续独立重试仍可重新报告。
- 图片与视频复用同一个单图来源选择器。图片 Brief 可从当前 Workspace 的已完成作品或本地上传中选择一张参考图继续生成;视频 Brief 使用同一入口绑定首帧。两条路径都必须通过现有 Workspace Asset 上传/选择契约提交一个真实 `attachmentAssetIds`,不得用本地路径或自然语言描述代替资产身份。
+42
View File
@@ -4,6 +4,46 @@ This file is the integrated default-branch snapshot. Feature tasks record progre
## Integrated Through
- AI Design streamed-reply diagnosis
`229b1b1ce39b7f1541a93ea3c980ab82b6d6266c` and client fix
`23f96a523eb37d8397bb3766ce95a59d56e59225` from tasks
`20260904-diagnose-design-stream-6a4e9c21` and
`20260904-fix-design-stream-4f7b91c2` are integrated onto local `main` and
canonically reconciled by task `20260904-integrate-design-stream-5e7c2a91`.
The existing post-validation `design.assistant.delta` transport is visible again as
exactly one provisional assistant-shaped bubble tied to its pending chat identity;
canonical `workspace.turns` still owns history and replaces the unfinished bubble,
while unknown outcomes keep the same identity and partial reply. The removed generic
“AI 正在整理你的想法” banner is not restored. A ready conversation now offers an
inline Quote step; successful Quote requests reveal the confirmation on desktop or
open it on mobile, but only explicit immutable Quote-ID confirmation creates a paid
Task. Unknown Quote requests disable both entry points and retry the original
operation. Focused tests passed 35/35, the Electron Quote-confirm-Task flow passed
1/1, typecheck, scoped lint, Vite build, document gates, and independent review
passed. The ordinary full suite had one unrelated Pi real-process 2-second timing
miss among 1,873 passing tests and 2 skips; that exact test passed immediately in
isolation, and the pressure test passed 1/1. Provider-native first-token streaming
remains a separate server follow-up; no server, database, billing, Plugin,
packaging, deployment, publication, or remote push changed.
- AI Design conversation-feedback sources
`403236115bf81e7617856cd8219b1be23f6abbb8` from task
`20260904-design-summary-separation-9c4e7a21` and
`3bda17aa3c1d034ddb3feaa08e516398e91cba0a` from task
`20260904-remove-design-progress-4e8a1c73` are integrated onto local `main` and
canonically reconciled by task `20260904-finalize-design-feedback-7c4e2a91`. A
submitted chat appears immediately as a provisional user bubble from its existing
pending operation, shows `发送中` or `正在确认`, restores the retained draft after
definitive failure, and is replaced by the canonical Workspace turn without a
second message store. Raw `design.assistant.delta` text remains internal transport
state: it is neither a finished reply nor a visible progress banner. The right-side
“AI 听懂的想法” remains the single organized-understanding projection, while
connection generation fencing and `chunkIndex` deduplication still converge repeated
streams. Current Specification, immutable Quote confirmation, server/Main contracts,
plugins, packaging, deployment, publication, and paid Provider behavior are
unchanged. Source verification passed focused/adjacent tests, typecheck, scoped
lint, production build, and both existing AI Design Electron checks individually;
the banner-removal follow-up passed its focused 10-test file, typecheck, and scoped
lint.
- Conversation-first AI Design client source
`fe50e4ba198649a07c5c9443f15edf7dfa2a47e6` from task
`20260903-design-guided-conversation-client-8b4e1c72` is integrated onto local
@@ -551,6 +591,8 @@ This file is the integrated default-branch snapshot. Feature tasks record progre
AI Design Canvas 现在默认以对话和一张持续可见的“我的创作”卡服务 8-16 岁创作者;专业字段矩阵收进按需打开的“精细调整”,移动端保持对话优先并只挂载一个卡片/底部面板。Creation Card、精细调整、Quote、Task 与结果提示都只投影权威状态,已知问题按 code 转成通俗中文,未知服务端或 Provider 文本不会直接显示。一个 Workspace 仍只公开一个 current Direction、一个 persistent Agent Session 和一个 Current Specification;conversation timeline 只记录交互历史。Chat、direct edits、decision responses、proposal acceptance、locks、Asset binding 与 restore 都通过 `design.input.apply` 进入同一服务端 reducer,Renderer drafts 在 accepted 前保持本地。Main 持有 Works Token、stream ticket、WebSocket、request deadline、stable command/operation IDs 与错误脱敏;unknown result 只能复用原 identity,结构化业务错误不得重放。Generation 由服务端对 exact Specification revision 编译 immutable Quote,客户端只展示 public output plan、warnings、expiry 与 Token Points,并以 Quote ID 调用 `design.generation.confirm`;Provider Prompt、model、route、storage 和 billing atoms 不进入 Renderer。Task/Asset events 独立收敛 Workspace resources,不改写 Living Form。Development 与 packaged builds 均使用 Works Square V2,V1 DTO、local semantic adapter、mutable Quote PATCH 与 editable provider Prompt 已移除。
新提交的 Design chat 会立即从现有 pending operation 投影为带“发送中”或“正在确认”的临时用户气泡,服务端确认的 canonical turn 到达后再替换它;确定失败时原草稿重新出现在输入框。`design.assistant.delta` 只作为内部传输与收敛状态,不显示原始片段,也不在对话区生成独立整理进度栏;右侧 Current Specification 摘要继续承担“AI 当前听懂的内容”。重复连接和事件按 connection generation 与 `chunkIndex` 收敛。
Canvas 侧栏提供“获取灵感”进入 Prompt Museum。列表、筛选、分页、详情、作者/来源/许可证和图片地址全部由服务端经 Main-owned Host API 提供,客户端不打包静态数据集;服务端相对媒体只允许固定 `/api/image-prompt-museum/{entry}/media/{thumbnail|number}` 形状,并由 Main 注入 Works Bearer、执行一次 401 刷新、可信 raster MIME 与 10 MiB 上限后转为 Renderer data URL;credential-free HTTPS CDN 图片保持直连。图片失败只显示卡片内占位,不阻断卡片或详情;缺少来源 URL 时显示纯文本。“使用此 Prompt”只把原文带回当前 Canvas 输入框,不自动发送。该模块不是投稿、点赞、评论或排行榜社区。客户端契约已就绪,但不据此宣称 Works Square 内容后台和生产数据已经部署。`pnpm run dev` 现在默认使用云端 Canvas 适配器,本地适配器只能通过显式开发命令启用;产品 UI 只保留中文。
密码登录提供可选“记住密码”。该记录与七天登录会话分离,只在正式安装包且系统安全存储可用时由 Electron Main 加密落盘;Renderer 仅在登录页内存中接收回填,不写 Zustand/localStorage,Works Square 也不持久化桌面密码。退出登录和短信登录不删除记录,成功的未勾选密码登录会清除旧记录。未打包开发版禁用该选项,避免未签名 Electron 调试进程触发 macOS 钥匙串。
@@ -0,0 +1,132 @@
# Task: Diagnose AI Design streaming and generation trigger
## Identity
- Task ID: 20260904-diagnose-design-stream-6a4e9c21
- Mode: Feature
- Branch: codex/20260904-diagnose-design-stream-6a4e9c21-diagnose-design-stream
- Worktree: D:\Datas\OthersProjects\.codex-worktrees\makelore\20260904-diagnose-design-stream-6a4e9c21
- Base commit: aa3f52a8f8414c9ad939d396bd2828c3e07c70e7
- Owner: codex-root
- Status: Ready for Integration
## Scope
- Diagnose, without changing product behavior, why AI Design assistant replies no
longer appear incrementally and why the screenshot path does not visibly create a
generation Task.
- Trace the current Renderer/store/Main contracts at the public test seams for
`design.assistant.delta`, Quote request/opening, immutable Quote confirmation, and
Workspace Task reconciliation.
- Build focused red-capable regressions that distinguish a presentation regression
from a transport failure and distinguish “no Quote requested” from “Quote confirmed
but no Task recovered”. Remove temporary probes before completion.
- Own only this task record and temporary diagnostic tests. Do not change product
code, the Works Square server, database, billing, packaging, publication, Plugin
navigation, or the three foreign untracked root task records.
## Intent And Constraints
- The user reports two observable regressions after the conversation-feedback UI
changes: completed assistant text arrives all at once, and the ready-state action
appears not to trigger generation.
- Preserve ADR-007. Streaming deltas are not canonical conversation turns, but the
diagnosis must determine whether they can be presented as an explicitly unfinished
reply without duplicating the right-side Current Specification summary.
- Generation remains a deliberate two-step boundary: request/check an immutable Quote,
then explicitly confirm its ID. A chat reply saying creation is possible is not
authorization to create a paid Task.
- Do not infer failure from the screenshot alone. Reproduce the actual button callback,
panel-mount, command dispatch, confirmation, and Task projection seams one at a time.
- Preserve stable operation identity and no-replay behavior while diagnosing.
## Project Context Loaded
- Concurrent Task Gate and Planning Gate passed in the managed isolated worktree at
exact client `main` base `aa3f52a8f8414c9ad939d396bd2828c3e07c70e7`.
- Loaded the startup memory set, ADR-007, architecture/data flow, business rules,
success criteria, evidence/reflection/commitment indexes, and the three integrated
AI Design conversation-feedback task records.
- Assessed 176 peer task records with no unreadable record. Relevant Canvas peers are
historical Ready-for-Integration sources already reflected in this base; none is a
live semantic conflict. One old undefined Design assessment remains unknown but is
on a stale base and this task is read-only diagnosis.
- Likely seams are `DesignConversationPane`, `YouthCreationCard`, the ImageCanvas
composition, `image-workspace` store, Quote/Task panels, and focused unit/Electron
fixtures.
- Gate result: Passed.
## Outcome
- Confirmed a Renderer presentation regression rather than a transport outage. The
server still persists and emits `design.assistant.delta`; Main maps it and the
Renderer store deduplicates and appends it to `assistantStreams`. The current
`DesignConversationPane` no longer subscribes to or renders that state, so only the
later canonical `workspace.turns` assistant message becomes visible.
- Identified the exact history. Commit `c6b0490` replaced the visible unfinished text
with a generic progress banner to keep the reasoner's summary-like draft out of the
conversation. Commit `3bda17a` then removed both the banner and the
`assistantStreams` subscription. The latter change explains why replies now appear
only after completion.
- Confirmed the server event is not provider-native time-to-first-token streaming.
`OpenAIJsonDesignReasoningRunner` waits for one complete JSON-object response;
`DesignAgentRuntimeV2` then takes the validated canonical `assistant_message`, splits
it into six-character chunks, and the Gateway spaces frames by 60 ms. Restoring the
old bubble would restore incremental painting only after reasoning has completed and
risks recreating the summary-as-chat confusion reported by the user.
- Built a temporary red-capable Renderer probe that seeded a pending operation plus
`assistantStreams` text. It failed because no streamed text was rendered, while the
provisional user message remained visible. The probe was removed after diagnosis.
- Found no break in the current source generation command chain. A ready Creation Card
calls `requestQuote`; an offered immutable Quote exposes an explicit Design Point
confirmation; confirmation calls `confirmGeneration(quoteId)`; the Electron flow
projects the resulting Task. Focused unit tests and the real Electron scenario pass.
- The supplied screenshot is factually before Quote confirmation: the card is still in
`ready` and offers “看看制作方案”. A chat sentence saying the design can be generated
is not authorization for a paid Task. Whether that particular installed client
emitted a Quote command after a click remains unverified because no matching Run or
client version was supplied.
- No product, server, database, billing, packaging, publication, or Plugin navigation
code was changed.
## Verification
- Temporary red probe:
`pnpm exec vitest run tests/unit/image-canvas-page.test.tsx -t "shows unfinished assistant reply text incrementally"`
-> expected failure: streamed text was absent from the conversation. The temporary
assertion was restored and is not part of this task.
- `pnpm exec vitest run tests/unit/youth-creation-card.test.tsx tests/unit/image-canvas-page.test.tsx -t "plain creation summary|production confirmation"`
-> 2 passed, 15 skipped.
- `pnpm run build:vite` -> passed for Renderer, Main, Preload, and utility processes;
only existing Browserslist, chunk-size, and dynamic-import warnings were reported.
- `node ./node_modules/@playwright/test/cli.js test tests/e2e/image-workspace-v2.spec.ts -g "keeps conversation and youth-friendly direct edits"`
-> 1 passed. The scenario exercised “看看制作方案” -> offered Quote -> explicit
Design Point confirmation -> queued Task projection.
## Follow-ups
- Product decision required before implementation: choose between (a) showing the
already-finalized assistant message with a typewriter effect after reasoning, or
(b) introducing a distinct conversational streaming contract that can emit safe
assistant prose before the structured Specification proposal is complete. Option
(b) is the only route to genuine lower time-to-first-visible-text; do not render raw
structured reasoner JSON or revive the former summary-like draft bubble.
- Make the two-step production boundary explicit in the conversation: when the assistant
says the idea is ready, point to or provide an inline “看看制作方案” action; after Quote
creation, keep the existing explicit Design Point confirmation. Do not auto-create a
paid Task from ordinary chat.
- If a user reports that clicking “看看制作方案” itself does nothing, capture that exact
operation's client version, `client_operation_id`, and `design.quote.request` Run. The
current source/Electron fixture does not reproduce such a failure.
## Promotion Candidates
- Target canonical documents: `.project-docs/20-architecture/system-overview.md` and
`.project-docs/40-domain/business-rules.md`. Proposal: distinguish provider-native
conversational streaming from post-validation chunk projection, and record that
readiness chat must lead users to Quote review but cannot itself authorize a paid
generation Task. Evidence: this task's red presentation probe, commit history, and
passing end-to-end Quote-confirm-Task flow. Future impact: prevents another UI change
from conflating an internal structured-reasoning draft, a canonical assistant turn,
and a progress indicator. No known semantic conflict; human confirmation is required
because the streaming architecture and conversation CTA behavior are product choices.
@@ -0,0 +1,89 @@
# Task: Finalize AI Design conversation feedback integration
## Identity
- Task ID: 20260904-finalize-design-feedback-7c4e2a91
- Mode: Integration
- Branch: main
- Worktree: D:\Datas\OthersProjects\makelore
- Base commit: 3bda17aa3c1d034ddb3feaa08e516398e91cba0a
- Owner: codex-root
- Status: Ready for Integration
## Scope
- Reconcile the already integrated AI Design conversation-feedback sources
`403236115bf81e7617856cd8219b1be23f6abbb8` and
`3bda17aa3c1d034ddb3feaa08e516398e91cba0a` into canonical current state,
architecture, and business rules.
- Record the final behavior: submitted user messages appear immediately with honest
pending status, raw assistant deltas remain internal, and the main conversation has
no assistant-progress banner because the right-side Current Specification projection
owns the organized understanding.
- Preserve the three adopted foreign untracked task records byte-for-byte. Do not add,
edit, move, stash, reset, clean, or commit them.
- Do not change product code, Works Square server, Electron Main/API contracts,
generation, billing, plugins, navigation, packaging, publication, remote history, or
paid Provider behavior.
## Intent And Constraints
- The user explicitly authorized recovery of the prior integration lock and made the
final presentation decision that the central progress banner is redundant and must
be removed rather than restyled.
- Client `main` already contains both source records and product changes at exact base
`3bda17aa3c1d034ddb3feaa08e516398e91cba0a`; this task performs canonical integration
only, so the document drift boundary does not need to cross another task's record.
- ADR-007 remains accepted. Current Specification is still server authority, its
right-side projection remains auxiliary UI, provisional user messages remain pending
projections, and only canonical turns enter conversation history.
- The newer source narrows the older optional progress presentation: raw assistant
deltas stay internal and invisible. The user's explicit instruction resolves that
semantic conflict.
- Concurrent Task Gate and Integration Planning Gate passed. Relevant source tasks are
Ready for Integration; other active work remains isolated and does not alter this
exact final behavior.
## Outcome
- Canonically recorded both integrated conversation-feedback sources on client `main`
at product frontier `3bda17aa3c1d034ddb3feaa08e516398e91cba0a`.
- The main conversation no longer renders raw assistant delta text or an independent
“AI 正在整理你的想法” progress banner. The right-side Current Specification summary
is the single organized-understanding surface.
- Immediate provisional user bubbles, `发送中` / `正在确认`, canonical turn replacement,
draft recovery, connection-generation fencing, and `chunkIndex` deduplication remain
intact.
- Updated canonical current state, system architecture, and business rules without
changing ADR-007, product code, server/Main contracts, immutable Quotes, navigation,
plugins, packaging, billing, or remote history.
- The earlier root lock and one transient wrong-base integration lock were recovered
under the user's explicit authorization. Each uncommitted document edit was restored
before release; the three adopted foreign files remained unchanged throughout.
## Verification
- Product source commit `3bda17a` has exact parent
`403236115bf81e7617856cd8219b1be23f6abbb8`; its changed paths are limited to the
source task record, `DesignConversationPane`, and its focused test.
- Root focused Renderer test passed: `pnpm exec vitest run
tests/unit/image-canvas-page.test.tsx` → `1 file / 10 tests`.
- Root TypeScript `pnpm exec tsc --noEmit` passed.
- Root scoped ESLint for the changed component and test passed.
- Root product paths matched source `3bda17a` exactly and `git diff --check` passed
before canonical documentation.
- The earlier conversation-feedback source retains its adjacent 51-test,
production-build, and individual Electron evidence; this final integration did not
repeat those broader checks for a presentation-only removal.
## Follow-ups
- Build and publish a new Makelore client version before expecting installed users to
receive this UI change. Packaging, deployment, publication, and remote push are not
part of this integration.
## Promotion Candidates
- Applied the two source promotion candidates to `current-state.md`,
`system-overview.md`, and `business-rules.md` using the user's final presentation
decision. No unresolved promotion candidate remains.
@@ -0,0 +1,149 @@
# Task: Restore AI Design reply streaming and generation CTA
## Identity
- Task ID: 20260904-fix-design-stream-4f7b91c2
- Mode: Feature
- Branch: codex/20260904-diagnose-design-stream-6a4e9c21-diagnose-design-stream
- Worktree: D:\Datas\OthersProjects\.codex-worktrees\makelore\20260904-diagnose-design-stream-6a4e9c21
- Base commit: 229b1b1ce39b7f1541a93ea3c980ab82b6d6266c
- Owner: codex-root
- Status: Ready for Integration
## Scope
- Restore a visible, explicitly unfinished assistant reply in the AI Design
conversation from the existing `design.assistant.delta` / `assistantStreams`
transport, and remove it when the canonical assistant turn settles.
- Add a youth-readable inline “看看制作方案” action at the end of a ready
conversation. Reuse the existing Quote request and reveal the offered Quote after
success; preserve the separate explicit Design Point confirmation before Task
creation.
- Own only `DesignConversationPane`, the ImageCanvas composition, the smallest
necessary Quote/Card/store settlement seams, focused Renderer/Electron tests, and
this task record.
- Do not modify the Works Square server, reasoner contract, database/API DTOs,
billing semantics, Plugin navigation, packaging, publication, or the three foreign
untracked task records in the client root.
## Intent And Constraints
- The user confirmed the preferred interaction: the conversation should paint the
assistant's reply incrementally again, but must not restore the removed generic
“AI 正在整理你的想法” progress banner or present the right-side Specification
summary as a second completed chat reply.
- A streamed fragment is provisional presentation only. `workspace.turns` remains
canonical history; canonical settlement replaces the provisional bubble and must
not leave duplicate or permanently pulsing text.
- The existing transport is post-validation chunk projection, not provider-native
time-to-first-token streaming. This task restores honest incremental painting from
that supported contract and does not broaden the server work.
- Readiness may invite the user to inspect a Quote, but ordinary chat never authorizes
a paid generation Task. Quote request, offered Quote, explicit Quote-ID confirmation,
and Task projection remain separate boundaries.
- Preserve provisional user-message immediacy, stable operation identity, unknown
outcome recovery, and youth-readable failure copy.
## Project Context Loaded
- Concurrent Task Gate passed for task `20260904-fix-design-stream-4f7b91c2`, mode
Feature, on branch
`codex/20260904-diagnose-design-stream-6a4e9c21-diagnose-design-stream` in isolated
worktree
`D:\Datas\OthersProjects\.codex-worktrees\makelore\20260904-diagnose-design-stream-6a4e9c21`
at exact base `229b1b1ce39b7f1541a93ea3c980ab82b6d6266c`; ownership is claimed by
`codex-root`.
- Loaded the startup memory set, current state, ADR-007, system overview, business
rules, success criteria, evidence/reflection/commitment indexes, and the completed
diagnosis task that reproduced the missing streamed presentation while proving the
Quote-confirm-Task source path still works.
- Relevant peer tasks are historical/stale or already integrated. This scope does not
overlap the Plugin navigation planner, the older Enter-submission test task, or the
three foreign untracked root records.
- The current canonical docs intentionally keep raw deltas out of completed history
and prohibit a separate progress banner. The user's explicit product decision adds
a narrower distinction: a delta may be shown as one visibly unfinished assistant
bubble that is replaced by the canonical turn. Record this as a promotion candidate
rather than editing canonical docs in Feature mode.
- Planning Gate result: Passed.
## Plan
1. Add failing Renderer regressions for provisional assistant visibility,
canonical replacement, and the ready-conversation Quote action.
2. Implement the minimal presentation and settlement changes, then make both the
conversation CTA and right-side CTA reveal the offered Quote.
3. Run focused unit/Electron checks plus typecheck, scoped lint, build, diff, and
project-document gates; document the exact outcome and remaining architectural
limitation.
## Outcome
- Restored the existing `assistantStreams` presentation as one assistant-shaped,
visibly unfinished reply beneath its matching pending chat operation. New chunks
update that same bubble and its pulse cursor; the removed generic “AI 正在整理你的
想法” status banner remains absent.
- Kept provisional output out of canonical history. Per-command success or definitive
failure clears that operation's stream, and the conversation only renders a stream
while its original pending chat identity still exists, so the later canonical
`workspace.turns` reply replaces rather than duplicates it. Direction events do not
globally clear unrelated streams because they expose no client operation identity;
unknown outcomes retain their original pending identity and partial reply.
- Added an inline ready-state handoff at the end of the conversation. It explains that
the next step only calculates Design Point cost and requests a Quote; it never calls
generation confirmation.
- After a Quote request succeeds from either the conversation action or the right-side
Creation Card, desktop scrolls the offered Quote into view and mobile opens the
Current Idea/Works sheet before locating it. The existing explicit
“花 N 设计点开始制作” Quote-ID confirmation remains the only Task-creation action.
- No Works Square server, reasoner, database/API DTO, billing, Plugin navigation,
packaging, or publication code changed.
## Verification
- Red phase: the focused Renderer suite failed exactly because no streamed assistant
bubble and no conversation Quote action existed (2 failed / 26 passed).
- Focused final suite:
`pnpm exec vitest run tests/unit/image-canvas-page.test.tsx tests/unit/image-workspace-store.test.ts tests/unit/youth-creation-card.test.tsx`
-> 3 files / 35 tests passed.
- The focused regressions include two simultaneous stream identities, an unknown Quote
that disables both Quote entry points and retries the original operation, and the
mobile inline action opening the Sheet with its offered Quote.
- Electron production flow:
`node ./node_modules/@playwright/test/cli.js test tests/e2e/image-workspace-v2.spec.ts -g "keeps conversation and youth-friendly direct edits"`
-> 1 passed; it used the new conversation action, verified the Quote was in the
viewport, explicitly confirmed 12 Design Points, and observed the queued Task.
- `pnpm exec tsc --noEmit`, scoped ESLint for every changed TypeScript/test file,
`pnpm run build:vite`, and `git diff --check` passed. Vite reported only existing
Browserslist, chunk-size, and dynamic/static-import warnings.
- Full ordinary suite reached 224 passing files / 1,873 passing tests / 2 declared
skips. Its only failure was an unrelated real Pi subprocess latency assertion
(2,310 ms versus a 2,000 ms threshold); the exact process test passed 1/1 when
immediately rerun alone. The separately gated pressure test passed 1/1.
- Independent read-only review found and then verified fixes for the two concrete P2
gaps above (global stream cleanup and duplicate unknown Quote submission); final
verdict: PASS with no remaining P1/P2 blocker.
## Follow-ups
- The current server does not provide provider-native time-to-first-token streaming:
it validates the complete structured reasoner response, then projects the final
assistant message in short chunks. Genuine earlier first-visible-text would require
a separately designed server conversational-stream contract; this client task does
not pretend otherwise.
## Promotion Candidates
- Target canonical documents: `.project-docs/20-architecture/system-overview.md` and
`.project-docs/40-domain/business-rules.md`. Proposal: distinguish completed
canonical assistant turns, a single provisional assistant bubble backed by
`design.assistant.delta`, and the prohibited generic progress/summary banner; also
record that ready conversation UI may request/reveal a Quote but only explicit
Quote-ID confirmation can create a paid Task. Evidence: the red-to-green Renderer
regressions and passing Electron Quote-confirm-Task flow in this task. Future impact:
prevents another presentation cleanup from accidentally removing supported
incremental feedback or collapsing Quote review into generation authorization.
Semantic conflict: current wording says raw delta must not be displayed as a
completed reply and forbids a separate progress bar; the new behavior preserves
both constraints by displaying exactly one explicitly unfinished reply. Human
confirmation was required and was supplied by the user before implementation.
@@ -0,0 +1,114 @@
# Task: Integrate AI Design streamed replies and quote handoff
## Identity
- Task ID: 20260904-integrate-design-stream-5e7c2a91
- Mode: Integration
- Branch: codex/20260904-diagnose-design-stream-6a4e9c21-diagnose-design-stream
- Worktree: D:\Datas\OthersProjects\.codex-worktrees\makelore\20260904-diagnose-design-stream-6a4e9c21
- Base commit: 23f96a523eb37d8397bb3766ce95a59d56e59225
- Owner: codex-root
- Status: Ready for Integration
## Scope
- Integrate diagnosis commit `229b1b1ce39b7f1541a93ea3c980ab82b6d6266c`
and implementation commit `23f96a523eb37d8397bb3766ce95a59d56e59225`
from tasks `20260904-diagnose-design-stream-6a4e9c21` and
`20260904-fix-design-stream-4f7b91c2` onto local client `main`.
- Reconcile the accepted promotion candidate into `current-state.md`, the AI Design
architecture boundary, and business rules: a delta may appear only as one visibly
unfinished assistant reply; Quote review remains separate from paid generation
confirmation.
- Preserve the three known foreign untracked task records in the root checkout exactly
as found. Do not modify the Works Square server, billing, Plugin navigation,
packaging, publication, remote refs, or any unrelated client feature.
## Intent And Constraints
- The user explicitly requested this completed client fix be merged to the main branch
and had already confirmed the product decision behind the canonical wording change.
- Root `main` was verified at exact head
`aa3f52a8f8414c9ad939d396bd2828c3e07c70e7`, with only the three previously known
untracked records. The source is a linear descendant of that frontier, so use a
fast-forward after canonical reconciliation; do not stash, reset, clean, add, or
adopt the foreign records.
- Keep `workspace.turns` as canonical conversation history. The provisional stream is
presentation tied to an existing pending chat identity, never a second completed
turn or generic progress banner.
- Keep production authorization two-step: Quote request/reveal may be prompted by the
conversation, while only explicit confirmation of the immutable Quote ID creates a
paid Task.
## Project Context Loaded
- Concurrent Task Gate passed for integration task
`20260904-integrate-design-stream-5e7c2a91`, owner `codex-root`, mode Integration,
branch `codex/20260904-diagnose-design-stream-6a4e9c21-diagnose-design-stream`,
worktree
`D:\Datas\OthersProjects\.codex-worktrees\makelore\20260904-diagnose-design-stream-6a4e9c21`,
base `23f96a523eb37d8397bb3766ce95a59d56e59225`; the repository Integration lock is held.
- Loaded the startup memory set, current integrated state, system overview, business
rules, ADR-007 context, and both completed source task records including outcome,
verification, follow-up, and promotion candidates.
- Relevant active peers are unrelated historical/stale task records. The exclusive
Integration lock prevents concurrent canonical reconciliation; no peer owns a
conflicting current AI Design product decision.
- The old canonical rule hid raw delta text entirely. The accepted source narrows that
rule without weakening authority: exactly one provisional bubble may paint the
already validated assistant text, while canonical turns, Current Specification,
stable operation identity, Quote identity, and explicit payment confirmation remain
unchanged. The user's confirmation resolves the semantic conflict.
- Planning Gate result: Passed.
## Plan
1. Reconcile the accepted AI Design stream/Quote behavior into canonical architecture,
business rules, and current-state integration evidence.
2. Re-run task-aware document and source verification at the final integration head.
3. Commit the integration record, fast-forward exact root `main`, verify the three
foreign untracked files are unchanged, then complete and release the Integration
lock.
## Outcome
- Accepted and reconciled both source tasks on top of exact local `main` frontier
`aa3f52a8f8414c9ad939d396bd2828c3e07c70e7`; their commit chain is linear and
contains no unrelated product changes.
- Updated the canonical architecture and business rules to distinguish one
operation-bound unfinished assistant bubble from canonical conversation history
and from the prohibited generic progress banner. Unknown operations retain their
identity/partial text and cannot be erased by unrelated updates.
- Recorded the ready-conversation Quote handoff: desktop reveals the offered Quote,
mobile opens it, and neither path creates a Task until the user explicitly confirms
the immutable Quote ID. Unknown Quote requests keep and retry the original operation
instead of creating another intent.
- Updated `current-state.md` with source commits, behavior, verification, the known
post-validation streaming limitation, and the explicit no-server/no-billing scope.
- Prepared the integrated branch for a fast-forward of root `main`; the root's three
pre-existing untracked task records remain outside this commit and are not adopted,
staged, modified, moved, or deleted.
## Verification
- Source task evidence: focused 35/35, Electron Quote-confirm-Task 1/1, typecheck,
scoped ESLint, Vite build, diff check, pressure 1/1, and independent final review
passed. The full suite's only miss was an unrelated Pi process 2-second timing check,
which passed immediately in isolation.
- Integration-head rerun: focused Renderer/store/Card suite 3 files / 35 tests passed;
`pnpm exec tsc --noEmit` passed.
- Integration-head Electron flow passed 1/1 and again exercised the conversation CTA,
Quote visibility, explicit 12-Design-Point confirmation, and queued Task projection.
- `git diff --check`, `check_project_docs.py`, and task-aware
`check_doc_drift.py --task-id 20260904-integrate-design-stream-5e7c2a91` passed.
## Follow-ups
- Provider-native first-token streaming still requires a separately scoped server
contract; this integration only exposes the existing validated-message chunk
projection honestly.
## Promotion Candidates
- None. The source candidates were accepted by the user and applied to canonical
architecture, domain rules, and current state in this integration task.
@@ -0,0 +1,86 @@
# Task: Remove redundant AI Design progress banner
## Identity
- Task ID: 20260904-remove-design-progress-4e8a1c73
- Mode: Feature
- Branch: codex/20260904-remove-design-progress-4e8a1c73-remove-design-progress
- Worktree: D:\Datas\OthersProjects\.codex-worktrees\makelore\20260904-remove-design-progress-4e8a1c73
- Base commit: 403236115bf81e7617856cd8219b1be23f6abbb8
- Owner: codex-root
- Status: Ready for Integration
## Scope
- Remove the visible AI Design assistant-progress banner from the main conversation
pane. The right-side “AI 听懂的想法” projection remains the sole surface for the
currently organized design understanding.
- Preserve immediate provisional user messages, their `发送中` / `正在确认` feedback,
canonical assistant replies, and the existing internal stream reconciliation.
- Update focused Renderer regression coverage only. Do not change the Works Square
server, Electron Main/API contracts, Current Specification authority, generation,
navigation, packaging, publication, or unrelated project records.
## Intent And Constraints
- The user explicitly identified the progress banner as duplicated information and
requested that it be removed. It must not be replaced by another central loading
surface.
- Raw assistant deltas remain non-canonical and must stay out of the conversation.
Removing their presentation must not remove their Store state or change operation
identity and failure recovery.
- Keep the pending user bubble as the honest acknowledgement that a send was accepted
locally; only the redundant assistant-side banner is removed.
- Concurrent Task Gate and Planning Gate passed in the managed isolated worktree from
exact client `main` base `403236115bf81e7617856cd8219b1be23f6abbb8`.
- The root integration task overlaps the same AI Design area but owns only root/canonical
reconciliation; this feature task owns the component and focused test in isolation.
- ADR-007 remains unchanged: the server Current Specification is semantic authority,
while the right-side youth summary is its secondary client projection.
## Outcome
- `DesignConversationPane` no longer subscribes to `assistantStreams` for visible
presentation and no longer renders the “AI 正在整理你的想法” / result-confirmation
banner in the conversation timeline.
- Raw assistant deltas remain Store-owned transport state and still never appear as
committed chat content. The final canonical assistant turn remains unchanged.
- Provisional user messages and their `发送中` / `正在确认` labels remain visible, so
removing the duplicated assistant banner does not remove send acknowledgement.
- The right-side “AI 听懂的想法” card and all Current Specification behavior are
unchanged.
## Verification
- Focused Renderer test: `pnpm exec vitest run tests/unit/image-canvas-page.test.tsx`
passed `1 file / 10 tests`.
- TypeScript: `pnpm exec tsc --noEmit` passed.
- Scoped ESLint for the changed component and test passed.
- The isolated worktree dependencies were restored from the existing lockfile with
offline, frozen resolution and lifecycle scripts disabled; no dependency version or
lockfile changed.
## Follow-ups
- Integrate this source commit after the existing root AI Design integration task has
safely completed or released its ownership. Do not overwrite that task's dirty
canonical-document reconciliation or its three preserved foreign records.
## Promotion Candidates
- Target: `.project-docs/30-worklog/current-state.md`, AI Design presentation wording
in `.project-docs/20-architecture/system-overview.md`, and the corresponding rule in
`.project-docs/40-domain/business-rules.md`.
- Proposal: record that raw assistant deltas stay internal and invisible in the main
conversation; the right-side Current Specification projection is the single surface
for the AI's organized understanding. The main timeline shows provisional/canonical
user messages and canonical assistant turns, without a separate assistant-progress
banner.
- Evidence: the user explicitly identified the banner as duplicated information and
requested its removal; focused Renderer coverage confirms the banner is absent while
send acknowledgement remains.
- Future impact: later conversation changes should not reintroduce a second summary or
progress card into the timeline merely because assistant delta transport state exists.
- Semantic conflict: this narrows the prior task's optional visible progress projection;
it does not change ADR-007 or server authority. Human confirmation is already present
in this task request.
+4 -2
View File
@@ -99,10 +99,12 @@
- AI Design 必须以自然对话帮助用户描述清楚需求:先提取和复述已明确内容,每次最多推进一个真正影响作品的问题。客户端 Current Specification 投影是辅助摘要和可选手动调整,不得把内部字段、缺项或普通 Decision Prompt 变成用户必须逐项填写的表单。
- 每个 mutation 使用稳定 command 与 semantic operation identity。transport-unknown 只能重放原命令;business rejection、timeout 或用户再次点击不能自动生成新的业务意图。
- Direction projection 是 Specification 真值。event cursor、assistant delta、Task progress 与 Asset event 只用于传输/资源收敛,不得推进或覆盖 canonical specification revision。
- 用户提交 chat 后,客户端可以立即把同一个 pending operation 投影为临时用户气泡,并明确显示“发送中”或“正在确认”;只有服务端返回的 canonical turn 才进入 conversation timeline。确定失败时必须让原草稿重新可编辑,不得把临时投影持久化为第二条消息。
- `design.assistant.delta` 只用于未完成回复的传输和结果收敛,不得进入 canonical conversation timeline。客户端可以把它临时显示为与同一 pending chat identity 绑定的单个未完成助手气泡;确定收敛后必须由 canonical turn 替换,unknown outcome 则保留原 identity 与已有片段。不得把片段伪装为完成回复、持久化为第二条消息、因其他 operation 更新而全局清除,或另加独立的整理进度栏。AI 当前整理出的设计理解仍由右侧 Current Specification 投影承载;连接重叠或事件重放按 connection generation 与 `chunkIndex` 去重收敛。
- Main 只向 Renderer 投影已知错误码的固定中文提示,未知上游错误文本必须脱敏为通用提示;Works Token、stream ticket、provider internals 留在 Main/服务端。
- AI 绘画 Workspace JSON 请求与 shared Works token refresh 必须覆盖取凭据、发请求和读取响应 body 的完整 30 秒 deadline;即使底层 transport 忽略 abort,调用方也必须确定性结束为 `504 DESIGN_WORKSPACE_REQUEST_TIMEOUT` 并释放共同等待者。Electron-to-Node 透明 fallback 仅允许 `GET`/`HEAD`/`OPTIONS`;mutation 不得因 transport failure 被隐式重放,任何重试必须由上层显式幂等合同授权。
- `design.quote.request` 必须绑定 exact current Specification revision 并返回 immutable Quote;任何 production-meaning edit 都需要新 revision 与新 Quote。
- `design.generation.confirm` 只提交 Quote identity。客户端不编辑 provider Prompt/model/route/storage,不计算 Token Points,也不把 Task recovery 当作再次确认授权。
- `design.quote.request` 必须绑定 exact current Specification revision 并返回 immutable Quote;任何 production-meaning edit 都需要新 revision 与新 Quote。对话或 Current Specification 就绪态可以引导用户请求并查看 Quote,桌面端可定位到报价区、移动端可打开报价面板,但该动作不得创建 Task。
- `design.generation.confirm` 只提交用户明确确认的 Quote identity。客户端不编辑 provider Prompt/model/route/storage,不计算 Token Points,也不把聊天就绪文案、Quote 展示、重复点击或 Task recovery 当作生成授权。
- 图片/视频 source 必须是当前 Workspace 的 canonical Asset,并通过 typed binding 进入 Specification;medium/role 决定用途,不得从 quick-reply 文案、V1 Brief 或本地路径推断。
- Task/Asset reconciliation 只能更新 Workspace resources;已经落库但事件迟到的 Task 可通过 refresh 恢复,不能覆盖 Living Form、local drafts 或 pending Design input。
- 删除 Canvas 项目必须要求用户完整输入项目名并通过 Main-owned Workspace DELETE。成功后被删 Workspace 的会话、任务和资产不得继续留在 Renderer 可访问状态;服务端决定软删除、任务取消、预留积分释放和运行中任务结算。
@@ -1,5 +1,12 @@
import { useEffect, useRef } from 'react';
import { Clock3, Loader2, MessageSquareText, Send, Sparkles } from 'lucide-react';
import {
Clock3,
Loader2,
MessageSquareText,
Send,
Sparkles,
WandSparkles,
} from 'lucide-react';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
@@ -40,10 +47,19 @@ function chatFailureMessage(error: unknown): string {
return '暂时没能确认这条消息的处理结果,内容还在输入框里';
}
export function DesignConversationPane({ workspace }: { workspace: DesignWorkspace }) {
export function DesignConversationPane({
workspace,
canRequestQuote,
onQuoteOffered,
}: {
workspace: DesignWorkspace;
canRequestQuote: boolean;
onQuoteOffered: () => void;
}) {
const chatDraft = useImageWorkspaceStore((state) => state.chatDraft);
const setChatDraft = useImageWorkspaceStore((state) => state.setChatDraft);
const sendChat = useImageWorkspaceStore((state) => state.sendChat);
const requestQuote = useImageWorkspaceStore((state) => state.requestQuote);
const assistantStreams = useImageWorkspaceStore((state) => state.assistantStreams);
const pendingOperations = useImageWorkspaceStore((state) => state.pendingOperations);
const scrollAnchorRef = useRef<HTMLDivElement>(null);
@@ -60,28 +76,32 @@ export function DesignConversationPane({ workspace }: { workspace: DesignWorkspa
status: operation.status,
}];
});
const streamingOperationIds = Object.entries(assistantStreams)
.filter(([, text]) => text.trim())
.map(([operationId]) => operationId);
const hasAssistantProgress = streamingOperationIds.length > 0;
const hasUnknownAssistantProgress = streamingOperationIds.some(
(operationId) => pendingOperations[operationId]?.status === 'unknown',
);
const submittingDesignInput = Object.values(pendingOperations).some(
(operation) => operation.status === 'submitting' && operation.command.kind === 'apply_input',
);
const quoteRequestPending = Object.values(pendingOperations).some(
(operation) => operation.command.kind === 'request_quote'
&& operation.command.workspaceId === workspace.workspace.workspaceId,
);
const streamingReplies = pendingChatMessages.flatMap((message) => {
const text = assistantStreams[message.operationId]?.trim();
return text ? [{ ...message, text }] : [];
});
const projectedDraft = chatDraft.trim() !== ''
&& pendingChatMessages.some((message) => message.message === chatDraft.trim());
const composerDraft = projectedDraft ? '' : chatDraft;
const pendingChatKey = pendingChatMessages
.map((message) => `${message.operationId}:${message.status}`)
.join('|');
const streamingReplyKey = streamingReplies
.map((reply) => `${reply.operationId}:${reply.text}`)
.join('|');
useEffect(() => {
const scrollAnchor = scrollAnchorRef.current;
if (typeof scrollAnchor?.scrollIntoView === 'function') {
scrollAnchor.scrollIntoView({ block: 'end' });
}
}, [hasAssistantProgress, pendingChatKey, workspace.turns.length]);
}, [canRequestQuote, pendingChatKey, streamingReplyKey, workspace.turns.length]);
const submit = () => {
if (!composerDraft.trim() || submittingDesignInput) return;
@@ -90,6 +110,15 @@ export function DesignConversationPane({ workspace }: { workspace: DesignWorkspa
});
};
const prepareQuote = () => {
if (!canRequestQuote || quoteRequestPending || pendingChatMessages.length > 0) return;
void requestQuote()
.then(onQuoteOffered)
.catch(() => {
toast.error('制作方案还没准备好,请稍后再试');
});
};
return (
<section
data-testid="image-workspace-conversation"
@@ -107,8 +136,7 @@ export function DesignConversationPane({ workspace }: { workspace: DesignWorkspa
<div className="min-h-0 flex-1 overflow-y-auto px-4 py-5">
{workspace.turns.length === 0
&& pendingChatMessages.length === 0
&& !hasAssistantProgress && (
&& pendingChatMessages.length === 0 && (
<div className="mx-auto flex h-full max-w-sm flex-col justify-center py-8">
<div className="mb-5 flex h-11 w-11 items-center justify-center rounded-2xl bg-brand/[0.09] text-brand">
<MessageSquareText className="h-5 w-5" />
@@ -169,26 +197,45 @@ export function DesignConversationPane({ workspace }: { workspace: DesignWorkspa
</div>
))}
{hasAssistantProgress && (
{streamingReplies.map((reply) => (
<div
key={reply.operationId}
data-testid={`streaming-design-assistant-${reply.operationId}`}
role="status"
aria-live="polite"
data-testid="design-assistant-progress"
className="mr-auto flex max-w-[92%] items-start gap-3 rounded-xl bg-brand/[0.06] px-3.5 py-3 text-muted-foreground shadow-sm"
aria-atomic="true"
className="mr-auto max-w-[92%] rounded-2xl rounded-bl-md border border-brand/20 bg-background px-3.5 py-3 text-sm leading-6 text-foreground shadow-sm"
>
<span className="mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-background text-brand shadow-sm">
<Loader2 className="h-3.5 w-3.5 animate-spin" aria-hidden="true" />
</span>
<div className="min-w-0">
<p className="text-xs font-semibold text-foreground">
{hasUnknownAssistantProgress ? '正在确认刚才的整理结果' : 'AI 正在整理你的想法'}
</p>
<p className="mt-0.5 text-xs leading-5 text-muted-foreground">
{hasUnknownAssistantProgress
? '结果回来后会显示在对话里,不需要重复发送。'
: '这不是新的回复,完成后会显示下一步问题或建议。'}
{reply.text}
<span
aria-hidden="true"
className="ml-1 inline-block h-3.5 w-1 animate-pulse rounded-full bg-brand align-middle"
/>
</div>
))}
{canRequestQuote && pendingChatMessages.length === 0 && (
<div
data-testid="design-conversation-quote-action"
className="flex flex-col gap-3 rounded-2xl border border-brand/20 bg-brand/[0.045] p-4 sm:flex-row sm:items-center sm:justify-between"
>
<div>
<p className="text-sm font-semibold text-foreground">想法已经清楚了</p>
<p className="mt-1 text-xs leading-5 text-muted-foreground">
先看看需要多少设计点,确认后才会开始制作。
</p>
</div>
<Button
type="button"
className="min-h-11 shrink-0 rounded-xl"
disabled={quoteRequestPending}
onClick={prepareQuote}
>
{quoteRequestPending
? <Loader2 className="mr-2 h-4 w-4 animate-spin" aria-hidden="true" />
: <WandSparkles className="mr-2 h-4 w-4" aria-hidden="true" />}
{quoteRequestPending ? '正在准备方案' : '下一步:看看制作方案'}
</Button>
</div>
)}
@@ -177,7 +177,11 @@ export function DesignQuotePanel({ workspace }: { workspace: DesignWorkspace })
}
return (
<section className="rounded-2xl border border-brand/25 bg-background p-4 shadow-sm">
<section
id="design-offered-quote"
data-testid="design-offered-quote"
className="rounded-2xl border border-brand/25 bg-background p-4 shadow-sm"
>
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div>
<div className="flex flex-wrap items-center gap-2">
+9 -4
View File
@@ -33,6 +33,7 @@ export type YouthCreationCardProps = {
generationAvailable: boolean;
onOpenFineTune: () => void;
onFocusComposer: () => void;
onQuoteOffered?: () => void;
};
function formatDuration(seconds: number): string {
@@ -335,19 +336,23 @@ export function YouthCreationCard({
generationAvailable,
onOpenFineTune,
onFocusComposer,
onQuoteOffered,
}: YouthCreationCardProps) {
const requestQuote = useImageWorkspaceStore((state) => state.requestQuote);
const pendingOperations = useImageWorkspaceStore((state) => state.pendingOperations);
const model = projectYouthCreationCard(workspace, { quoteBlockers });
const requesting = Object.values(pendingOperations).some(
(operation) => operation.status === 'submitting' && operation.command.kind === 'request_quote',
(operation) => operation.command.kind === 'request_quote'
&& operation.command.workspaceId === workspace.workspace.workspaceId,
);
const request = () => {
if (!generationAvailable || requesting || !model.canRequestQuote) return;
void requestQuote().catch(() => {
toast.error('制作方案还没准备好,请稍后再试');
});
void requestQuote()
.then(() => onQuoteOffered?.())
.catch(() => {
toast.error('制作方案还没准备好,请稍后再试');
});
};
const hasMoreDetails = model.mustInclude.length > 0
|| model.mustPreserve.length > 0
+18 -1
View File
@@ -185,6 +185,15 @@ export function ImageCanvas() {
setCreationCardOpen(false);
setFineTuneOpen(true);
};
const revealOfferedQuote = () => {
if (!desktopLayout) setCreationCardOpen(true);
globalThis.setTimeout(() => {
const quote = document.getElementById('design-offered-quote');
if (typeof quote?.scrollIntoView === 'function') {
quote.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}, 0);
};
const productionContent = (
<div className="space-y-5">
<DesignPendingOperationsPanel />
@@ -270,7 +279,13 @@ export function ImageCanvas() {
<main className="grid min-h-0 flex-1 lg:grid-cols-[minmax(0,1.8fr)_minmax(320px,0.72fr)]">
<div className="flex min-h-0 lg:border-r lg:border-border/70">
<DesignConversationPane workspace={workspace} />
<DesignConversationPane
workspace={workspace}
canRequestQuote={generationAvailable
&& cardModel.phase === 'ready'
&& cardModel.canRequestQuote}
onQuoteOffered={revealOfferedQuote}
/>
</div>
{desktopLayout && (
@@ -286,6 +301,7 @@ export function ImageCanvas() {
generationAvailable={generationAvailable}
onOpenFineTune={openFineTune}
onFocusComposer={focusComposer}
onQuoteOffered={revealOfferedQuote}
/>
{productionContent}
</div>
@@ -309,6 +325,7 @@ export function ImageCanvas() {
generationAvailable={generationAvailable}
onOpenFineTune={openFineTune}
onFocusComposer={focusComposer}
onQuoteOffered={revealOfferedQuote}
/>
{productionContent}
</div>
+2 -1
View File
@@ -462,8 +462,9 @@ test.describe('AI Design V2 workspace', () => {
await expect(page.getByTestId('image-workspace-conversation'))
.toContainText('记住了:主体更简洁,也会为标题留出空间。');
await page.getByRole('button', { name: '看看制作方案' }).click();
await page.getByRole('button', { name: '下一步:看看制作方案', exact: true }).click();
await expect(page.getByRole('heading', { name: '制作方案准备好了' })).toBeVisible();
await expect(page.getByTestId('design-offered-quote')).toBeInViewport();
await expect(page.getByText('开始后会按现在的想法制作;如果再改想法,需要重新准备方案。')).toBeVisible();
await expect(page.getByLabel(/Prompt|提示词/i)).toHaveCount(0);
await expect(page.getByText(/规格版本|编译器|生成方式|不可变/)).toHaveCount(0);
+71 -6
View File
@@ -93,11 +93,12 @@ describe('youth AI Design Canvas page', () => {
expect(screen.queryByText(/规格版本|编译器|生成策略|字段决策/)).not.toBeInTheDocument();
});
it('presents unfinished AI output as progress instead of a chat reply', () => {
prepareWorkspace();
const unfinishedDraft = '收到,我们要收到,我们要制作一张社团活动海报。';
it('shows one unfinished assistant reply incrementally and replaces it with the canonical turn', async () => {
const { workspace } = prepareWorkspace();
const firstChunk = '收到,我们要制作';
const unfinishedDraft = `${firstChunk}一张社团活动海报。`;
useImageWorkspaceStore.setState({
assistantStreams: { 'operation-chat-1': unfinishedDraft },
assistantStreams: { 'operation-chat-1': firstChunk },
pendingOperations: {
'operation-chat-1': {
id: 'operation-chat-1',
@@ -121,9 +122,69 @@ describe('youth AI Design Canvas page', () => {
render(<ImageCanvas />);
const conversation = screen.getByTestId('image-workspace-conversation');
expect(within(conversation).getByRole('status')).toHaveTextContent('AI 正在整理你的想法');
const pendingMessage = within(conversation).getByTestId('pending-design-chat-operation-chat-1');
expect(within(pendingMessage).getByText('做一张社团活动海报')).toBeInTheDocument();
expect(within(pendingMessage).getByText('发送中')).toBeInTheDocument();
expect(within(conversation).queryByRole('button', { name: '下一步:看看制作方案' }))
.not.toBeInTheDocument();
expect(within(conversation).queryByTestId('design-assistant-progress')).not.toBeInTheDocument();
expect(within(conversation).queryByText('AI 正在整理你的想法')).not.toBeInTheDocument();
expect(within(conversation).getByText('我已经整理了用途、受众和初步概念,请确认右侧建议。')).toBeInTheDocument();
expect(within(conversation).queryByText(unfinishedDraft)).not.toBeInTheDocument();
const streamingReply = within(conversation)
.getByTestId('streaming-design-assistant-operation-chat-1');
expect(streamingReply).toHaveTextContent(firstChunk);
act(() => {
useImageWorkspaceStore.setState({
assistantStreams: { 'operation-chat-1': unfinishedDraft },
});
});
expect(streamingReply).toHaveTextContent(unfinishedDraft);
act(() => {
useImageWorkspaceStore.setState({
workspace: {
...workspace,
turns: [...workspace.turns, {
turnId: 'turn-2',
rawTurnSequence: 2,
userMessage: '做一张社团活动海报',
assistantMessage: unfinishedDraft,
}],
},
pendingOperations: {},
});
});
await waitFor(() => {
expect(within(conversation).queryByTestId('streaming-design-assistant-operation-chat-1'))
.not.toBeInTheDocument();
});
expect(within(conversation).getAllByText(unfinishedDraft)).toHaveLength(1);
expect(within(conversation).getByRole('button', { name: '下一步:看看制作方案' }))
.toBeInTheDocument();
});
it('opens the mobile quote confirmation from the ready conversation without starting a paid task', async () => {
const { actions } = prepareWorkspace();
const quotedWorkspace = designWorkspaceFixture({
form: designFormFixture({ activeQuotes: [designQuoteFixture()] }),
});
actions.requestQuote.mockImplementationOnce(async () => {
useImageWorkspaceStore.setState({ workspace: quotedWorkspace });
return quotedWorkspace;
});
render(<ImageCanvas />);
const conversation = screen.getByTestId('image-workspace-conversation');
fireEvent.click(within(conversation).getByRole('button', { name: '下一步:看看制作方案' }));
expect(actions.requestQuote).toHaveBeenCalledOnce();
expect(actions.confirmGeneration).not.toHaveBeenCalled();
await waitFor(() => {
expect(screen.getByRole('heading', { name: '制作方案准备好了' })).toBeInTheDocument();
});
expect(screen.getByTestId('design-offered-quote')).toBeInTheDocument();
});
it('shows a submitted message immediately and replaces it with the canonical turn', async () => {
@@ -334,9 +395,13 @@ describe('youth AI Design Canvas page', () => {
});
render(<ImageCanvas />);
expect(within(screen.getByTestId('image-workspace-conversation'))
.getByRole('button', { name: '正在准备方案' })).toBeDisabled();
openMobileCreationCard();
expect(screen.getByText('还在确认刚才的操作')).toBeInTheDocument();
expect(screen.getByText(/不会重复制作或重复扣费/)).toBeInTheDocument();
expect(screen.getByRole('button', { name: '看看制作方案', exact: true })).toBeDisabled();
expect(actions.requestQuote).not.toHaveBeenCalled();
fireEvent.click(screen.getByRole('button', { name: '查看原来的结果' }));
expect(actions.retryOperation).toHaveBeenCalledWith('operation-1');
});
+23 -1
View File
@@ -523,6 +523,25 @@ describe('V2 Living Form store', () => {
const source = await loadedStore();
useImageWorkspaceStore.getState().setFieldDraft('intent.purpose', '本地草稿');
useImageWorkspaceStore.setState({
assistantStreams: { 'operation-chat-unknown': '这段回复还没确认' },
pendingOperations: {
'operation-chat-unknown': {
id: 'operation-chat-unknown',
label: '发送创作想法',
command: {
kind: 'apply_input',
workspaceId: 'workspace-1',
sessionId: 'session-1',
expectedDirectionRevision: 4,
clientOperationId: 'operation-chat-unknown',
input: { kind: 'chat', message: '保留这条未知结果' },
},
status: 'unknown',
error: '结果尚未确认',
clearDraftPaths: [],
clearChatDraft: true,
},
},
quoteBlockers: [{
code: 'missing_choice',
message: '还需要选择作品形状',
@@ -573,9 +592,12 @@ describe('V2 Living Form store', () => {
}),
});
expect(useImageWorkspaceStore.getState().assistantStreams).toEqual({
'operation-chat-unknown': '这段回复还没确认',
'operation-chat-1': '正在整理',
});
expect(useImageWorkspaceStore.getState()).toMatchObject({
lastEventId: 'session-1:8',
assistantStreams: { 'operation-chat-1': '正在整理' },
fieldDrafts: { 'intent.purpose': '本地草稿' },
quoteBlockers: [],
workspace: { form: { directionRevision: 5, specificationRevision: 4 } },
+8 -3
View File
@@ -1,4 +1,4 @@
import { cleanup, fireEvent, render, screen, within } from '@testing-library/react';
import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { YouthCreationCard } from '@/pages/ImageCanvas/YouthCreationCard';
import { DesignTaskPanel } from '@/pages/ImageCanvas/DesignProductionPanel';
@@ -21,12 +21,14 @@ function renderCard(
quoteBlockers?: DesignCompilationIssue[];
onOpenFineTune?: () => void;
onFocusComposer?: () => void;
onQuoteOffered?: () => void;
} = {},
) {
const requestQuote = vi.fn().mockResolvedValue(workspace);
const resolveDecisionPrompt = vi.fn().mockResolvedValue(workspace);
const onOpenFineTune = options.onOpenFineTune ?? vi.fn();
const onFocusComposer = options.onFocusComposer ?? vi.fn();
const onQuoteOffered = options.onQuoteOffered ?? vi.fn();
useImageWorkspaceStore.setState({
workspace,
quoteBlockers: [],
@@ -41,6 +43,7 @@ function renderCard(
generationAvailable={options.generationAvailable ?? true}
onOpenFineTune={onOpenFineTune}
onFocusComposer={onFocusComposer}
onQuoteOffered={onQuoteOffered}
/>,
);
return {
@@ -49,6 +52,7 @@ function renderCard(
resolveDecisionPrompt,
onOpenFineTune,
onFocusComposer,
onQuoteOffered,
};
}
@@ -97,8 +101,8 @@ describe('YouthCreationCard', () => {
vi.clearAllMocks();
});
it('shows a plain creation summary and offers a youth-facing ready action', () => {
const { requestQuote } = renderCard();
it('shows a plain creation summary and reveals the offered quote after requesting it', async () => {
const { onQuoteOffered, requestQuote } = renderCard();
expect(screen.getByRole('heading', { name: 'AI 听懂的想法' })).toBeInTheDocument();
expect(screen.getByText('会跟着聊天自动更新,不需要逐项填写。')).toBeInTheDocument();
@@ -112,6 +116,7 @@ describe('YouthCreationCard', () => {
fireEvent.click(screen.getByRole('button', { name: '看看制作方案' }));
expect(requestQuote).toHaveBeenCalledOnce();
await waitFor(() => expect(onQuoteOffered).toHaveBeenCalledOnce());
});
it('does not surface legacy decision prompts as a form', () => {