feat(design): integrate conversation-first creation flow
This commit is contained in:
@@ -25,7 +25,7 @@ Makelore 是 Electron 桌面客户端。Renderer 负责项目操作与状态展
|
||||
| Pi Extension, Subagents & Lifecycle | 必需的生成式 Makelore extension、Main 显式选定的已安装 extensions、UI interaction、ephemeral child、write lease 与 background run lease | Makelore bridge 固定为首个 extension,其余选定 extension 全部经 Pi 的 explicit additional paths 加载且 ambient discovery 关闭;child 并发 4、单次最多 8、禁止递归;active/uncertain run 不因页面隐藏或 confirmation timeout 被停止,replacement/stop 必须可解释并清理所有 ownership |
|
||||
| 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 是服务端 Current Specification 的投影;Renderer 只持有草稿和已接受投影 |
|
||||
| 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 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 不下发 |
|
||||
@@ -84,6 +84,7 @@ Makelore 是 Electron 桌面客户端。Renderer 负责项目操作与状态展
|
||||
- 落盘文件名 `works-cloud-deploy.json` 仅为已安装客户端的数据兼容;领域模型和代码接口是 submission binding,不表示仍存在 cloud deployment coordinator。
|
||||
- AI Design 的一个 Workspace 公开一个当前 Direction、一个持久 Agent Session 和一个 Current Specification;conversation timeline 是交互历史,不是独立创建或选择的语义权威。
|
||||
- Living Form 与 chat、direct edit、decision、proposal、lock、Asset binding、restore 共用同一服务端 reducer。Renderer 草稿在 accepted 前不进入 canonical state。
|
||||
- AI Design Renderer 必须把自由描述与 Agent 的自然追问放在主交互面。Specification 投影只复述当前理解并提供默认收起的细节和可选手动调整;普通 Decision Prompt 不得自动渲染成要求青少年逐项完成的表单。
|
||||
- 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。
|
||||
@@ -100,4 +101,4 @@ Makelore 是 Electron 桌面客户端。Renderer 负责项目操作与状态展
|
||||
|
||||
## Last Updated
|
||||
|
||||
2026-09-03
|
||||
2026-09-04
|
||||
|
||||
@@ -4,6 +4,24 @@ This file is the integrated default-branch snapshot. Feature tasks record progre
|
||||
|
||||
## Integrated Through
|
||||
|
||||
- Conversation-first AI Design client source
|
||||
`fe50e4ba198649a07c5c9443f15edf7dfa2a47e6` from task
|
||||
`20260903-design-guided-conversation-client-8b4e1c72` is integrated onto local
|
||||
`main` by task `20260904-integrate-guided-design-6a3f9c82`. ImageCanvas now makes
|
||||
free-form conversation the dominant creation surface: first-use guidance invites
|
||||
creators aged 8–16 to describe a subject, scene, or feeling in their own words,
|
||||
the Agent-facing examples only prefill the composer, and ordinary persisted
|
||||
Decision Prompts no longer appear as required option-card forms. The secondary
|
||||
surface is now a compact “AI 听懂的想法” projection whose constraints,
|
||||
references, and video details are collapsed by default; manual field editing is
|
||||
explicitly optional. ADR-007 remains intact: Current Specification is still the
|
||||
server semantic authority, Quote confirmation still submits only immutable Quote
|
||||
identity, and Main transport/operation certainty is unchanged. Focused/adjacent
|
||||
45-test, typecheck, scoped lint, production build, pressure 1/1, and Electron E2E
|
||||
2/2 passed. The ordinary full suite passed 1,866 tests with 2 conditional skips
|
||||
and one unrelated Pi real-process 2-second timing assertion at 2,232 ms; that
|
||||
exact file passed 5/5 in isolation. No server contract, paid generation, package,
|
||||
deployment, publication, or remote push is claimed.
|
||||
- AI Design command-outcome source
|
||||
`83f3f134a6779251aaa18f3290308cd9221b038f` from task
|
||||
`20260903-design-message-send-client-8d3f2a71` is integrated on `main` as
|
||||
@@ -670,6 +688,7 @@ Robot 绑定设备默认先显示“引导配网 / 已有激活码”路径选
|
||||
- 本地构建必须使用安装版 Electron Node、固定 npm 11.6.2 和项目 `package-lock.json` 锁定的 Vite,不得回退到全局 PATH、已有 `node_modules` 或未验证的 npm 闭包;依赖安装需要网络。
|
||||
- 项目 Vite config/plugins 以桌面用户权限执行,不能称为 sandbox;此风险边界必须在发布说明中保留。
|
||||
- AI Design 的 Current Specification 是唯一语义权威。Direction events 必须按 Workspace/Direction/revision/operation identity 收敛;assistant delta、event cursor、Task progress 和 Asset updates 不得改写 Living Form。
|
||||
- AI Design 的用户交互以自然对话为主:先理解并复述用户已经说清楚的内容,再一次只推进一个关键问题。Current Specification 的客户端投影只作为“AI 已理解”的辅助摘要和可选手动调整面板;新增字段不得自动变成必填问题或选项卡表单。
|
||||
- Design unknown-result replay 必须复用原 stable command 与 semantic operation ID;业务错误不得重复提交。Immutable Quote confirmation 只提交 Quote ID,Task recovery 不授权新的生成 intent。
|
||||
- AI 绘画 Main-owned Workspace JSON 请求和共享 Works token refresh 必须在 30 秒内结束并释放共同等待者;只允许 `GET`/`HEAD`/`OPTIONS` 在 Electron transport 失败后透明改走 Node fetch,PATCH/POST 等 mutation 必须由具有显式幂等身份的上层协议决定是否重试。临时 `disable-http2` 只用于安装包诊断,不能替代该有限生命周期与非重放边界。
|
||||
- 服务端 current Direction Session 与 semantic history 不由客户端在注销或退出时删除;Main 只关闭本地流、清除 drafts/pending state 和本机凭据。
|
||||
@@ -683,4 +702,4 @@ Robot 绑定设备默认先显示“引导配网 / 已有激活码”路径选
|
||||
|
||||
## Last Updated
|
||||
|
||||
2026-09-03
|
||||
2026-09-04
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
# Task: Make AI Design conversation primary
|
||||
|
||||
## Identity
|
||||
|
||||
- Task ID: 20260903-design-guided-conversation-client-8b4e1c72
|
||||
- Mode: Feature
|
||||
- Branch: codex/20260903-design-guided-conversation-client-8b4e1c72-design-guided-conversation-client
|
||||
- Worktree: D:\Datas\OthersProjects\.codex-worktrees\makelore\20260903-design-guided-conversation-client-8b4e1c72
|
||||
- Base commit: 14efa99f0ca7a02d6ae4e08311f125c56b9399a6
|
||||
- Owner: codex-root
|
||||
- Status: Ready for Integration
|
||||
|
||||
## Scope
|
||||
|
||||
- Make the AI Design conversation the primary creation surface and remove the
|
||||
impression that a young creator must complete a design form before the Agent can
|
||||
help.
|
||||
- Reframe the canonical Specification projection as a compact, secondary “current
|
||||
understanding” summary with optional detail editing; keep Quote, Task, Asset, and
|
||||
typed reducer behavior unchanged.
|
||||
- Add clear first-use guidance that invites a free description and gives a few
|
||||
example starting points without turning them into required choices.
|
||||
- Own only the ImageCanvas Renderer presentation and focused unit/Electron tests.
|
||||
Do not change Electron Main transport, server DTOs, Plugin navigation, packaging,
|
||||
deployment, publication, or paid generation.
|
||||
|
||||
## Intent And Constraints
|
||||
|
||||
- Preserve ADR-007: the server Current Specification remains the single semantic
|
||||
authority, Renderer drafts stay local until accepted, and generation still uses
|
||||
an immutable Quote ID.
|
||||
- Conversation must remain visually and behaviorally usable when the Specification
|
||||
is sparse. The interface should invite the user to describe subject, scene, and
|
||||
feeling in their own words, not enumerate professional fields or force option
|
||||
cards.
|
||||
- The compact summary may show what the Agent currently understands and expose an
|
||||
optional detail drawer, but it must not compete with the composer for attention or
|
||||
claim readiness before server validation.
|
||||
- Keep Chinese-only copy, the existing light visual language, keyboard behavior,
|
||||
reduced-motion support, 40–44 px interactive targets, and current responsive
|
||||
single-mount rules.
|
||||
- Preserve the prior definitive-failure versus transport-unknown behavior and all
|
||||
stable operation identities.
|
||||
|
||||
## Concurrent Task Gate
|
||||
|
||||
- Task ID: `20260903-design-guided-conversation-client-8b4e1c72`.
|
||||
- Mode: Feature.
|
||||
- Branch/worktree:
|
||||
`codex/20260903-design-guided-conversation-client-8b4e1c72-design-guided-conversation-client` /
|
||||
`D:\Datas\OthersProjects\.codex-worktrees\makelore\20260903-design-guided-conversation-client-8b4e1c72`.
|
||||
- Base commit: `14efa99f0ca7a02d6ae4e08311f125c56b9399a6`.
|
||||
- Ownership result: isolated and exclusively claimed by `codex-root`; the root
|
||||
`main` worktree and its three foreign untracked task records remain untouched.
|
||||
- Peer assessment: the only planning Design peers are an old test-only Enter change
|
||||
on an August baseline and an undefined historical assessment task. Neither owns
|
||||
the current ImageCanvas production surface. Plugin navigation and other active
|
||||
tasks use separate worktrees and have no path overlap.
|
||||
- Gate result: Passed.
|
||||
|
||||
## Project Context Loaded
|
||||
|
||||
- Read the startup memory set, active task, integrated state, decision index,
|
||||
ADR-007, system overview, module/data flow, Design business rules and success
|
||||
criteria, evidence/reflection/commitment/stale indexes, the integrated youth Design
|
||||
source, the command-outcome source, and relevant peer scopes.
|
||||
- Confirmed that the existing “我的创作” card is a projection, not semantic
|
||||
authority. Making it secondary is compatible with ADR-007 and does not require a
|
||||
DTO or reducer change.
|
||||
- `project-positioning.md` and the top-level project success placeholder remain
|
||||
incomplete; concrete product constraints come from `AGENTS.md`, current state,
|
||||
ADR-007, architecture, domain rules, and the previous integrated Design tasks.
|
||||
- Planning Gate result: Passed.
|
||||
|
||||
## Outcome
|
||||
|
||||
- The conversation is now the dominant creation surface on desktop and mobile. Its
|
||||
first-use copy teaches creators to describe a subject, scene, or feeling in their
|
||||
own words, and example sentences only prefill the composer.
|
||||
- Ordinary server Decision Prompts are no longer rendered as option-card forms or
|
||||
allowed to hold the youth-facing projection in an artificial “choice” phase.
|
||||
- The secondary panel is now “AI 听懂的想法”: it states that it updates from the
|
||||
conversation, keeps the concise summary and production readiness visible, and
|
||||
places constraints, references, and video details behind an optional disclosure.
|
||||
- Manual field editing remains available as an explicitly optional action. Quote,
|
||||
Task, Asset, typed reducer, stable operation identity, and Main transport behavior
|
||||
are unchanged.
|
||||
- The desktop layout gives materially more width to conversation while preserving a
|
||||
single mounted summary/production surface at each responsive breakpoint.
|
||||
|
||||
## Verification
|
||||
|
||||
- Red baseline: the two initial component files produced `8 failed / 7 passed`,
|
||||
confirming the old headings, mobile entry, and Decision Prompt cards still drove
|
||||
the experience.
|
||||
- Focused conversation/card/projection tests: `22 passed`.
|
||||
- Adjacent ImageCanvas, Fine Tune, issue-copy, and store regression: `45 passed`.
|
||||
- TypeScript `tsc --noEmit`: passed.
|
||||
- Changed-file ESLint: passed.
|
||||
- Production Vite/Electron build: passed; only existing browserslist, dynamic-import,
|
||||
and chunk-size warnings were emitted.
|
||||
- Electron/Playwright Design workspace flow: `2 passed`, covering desktop/mobile
|
||||
responsive mounts, optional manual adjustment, conversation, Quote, and generation
|
||||
confirmation.
|
||||
- Full unit run reached `1866 passed / 2 skipped / 1 failed`; the sole failure was an
|
||||
unrelated Pi real-process timing assertion (2.232 seconds against a 2-second
|
||||
threshold) under parallel load. Its isolated rerun passed `5/5`.
|
||||
- Coding chat pressure test, which the package script runs separately: `1 passed`.
|
||||
- `git diff --check`: passed.
|
||||
|
||||
## Follow-ups
|
||||
|
||||
- None recorded.
|
||||
|
||||
## Promotion Candidates
|
||||
|
||||
- Target: `.project-docs/30-worklog/current-state.md` and the canonical AI Design UX
|
||||
description.
|
||||
- Proposal: record the MakeLore AI Design surface as conversation-primary. The
|
||||
persistent Specification projection is a compact “AI understood” summary and
|
||||
optional manual-edit surface, never a prerequisite form for beginning or
|
||||
continuing creation.
|
||||
- Evidence: explicit user direction, red tests demonstrating the former form-led
|
||||
behavior, 45 focused/adjacent passing tests, production build, and 2/2 Electron
|
||||
end-to-end flow.
|
||||
- Future impact: new Specification fields must not automatically become visible
|
||||
questions or controls; Agent guidance should remain natural language and request
|
||||
only one consequential clarification at a time.
|
||||
- Semantic conflicts: this supersedes the earlier “persistent Creation Card as the
|
||||
primary co-authored form” presentation while preserving ADR-007 authority,
|
||||
reducer, Quote, and production boundaries.
|
||||
- Human confirmation: already supplied by the user in this task; no additional
|
||||
product-direction confirmation is required during integration.
|
||||
@@ -0,0 +1,93 @@
|
||||
# Task: Integrate conversation-first AI Design client
|
||||
|
||||
## Identity
|
||||
|
||||
- Task ID: 20260904-integrate-guided-design-6a3f9c82
|
||||
- Mode: Integration
|
||||
- Branch: main
|
||||
- Worktree: D:\Datas\OthersProjects\makelore
|
||||
- Base commit: 14efa99f0ca7a02d6ae4e08311f125c56b9399a6
|
||||
- Owner: codex-root
|
||||
- Status: Ready for Integration
|
||||
|
||||
## Scope
|
||||
|
||||
- Integrate completed client source `fe50e4b` from task
|
||||
`20260903-design-guided-conversation-client-8b4e1c72` onto exact `main`
|
||||
baseline `14efa99f0ca7a02d6ae4e08311f125c56b9399a6`.
|
||||
- Promote the accepted conversation-first AI Design presentation into canonical
|
||||
current state, system overview, and business rules.
|
||||
- Preserve the three explicitly adopted foreign untracked task records byte-for-byte;
|
||||
do not add, move, edit, stash, reset, or clean them.
|
||||
- Do not change server, Electron Main, DTO/API, generation, billing, plugins,
|
||||
package, deployment, publication, or remote history.
|
||||
|
||||
## Intent And Constraints
|
||||
|
||||
- The user explicitly replaced the former form-led presentation with a natural
|
||||
conversation that helps young creators describe a clear need. This is the
|
||||
human-confirmed product direction for the integration decision.
|
||||
- ADR-007 remains accepted: Current Specification is still server authority,
|
||||
Renderer drafts remain local until accepted, and generation confirmation still
|
||||
submits only an immutable Quote identity.
|
||||
- Integrate the source task record unchanged. Canonical documents may summarize its
|
||||
accepted promotion candidate but must not overstate local automation as production
|
||||
deployment or real paid generation evidence.
|
||||
- The completed source task passed focused, adjacent, typecheck, lint, production
|
||||
build, and Electron end-to-end verification. One unrelated Pi real-process timing
|
||||
assertion failed only under full parallel load and passed 5/5 in isolation.
|
||||
|
||||
## Project Context Loaded
|
||||
|
||||
- Concurrent Task Gate: passed in Integration mode on root `main`; the worktree is
|
||||
exclusively owned by this task and its three known foreign task records were
|
||||
adopted by exact fingerprint under prior user authorization.
|
||||
- Planning Gate: passed after reading the active and source tasks, integrated current
|
||||
state, ADR-007, system overview, business rules, success criteria, and the evidence,
|
||||
reflection, commitment, and stale indexes.
|
||||
- Other active tasks are isolated in separate worktrees. The completed Design source
|
||||
owns the exact ImageCanvas changes being integrated; plugin navigation is already
|
||||
present in the base; no active peer owns or semantically conflicts with this scope.
|
||||
- `project-positioning.md` remains a project-wide placeholder. Concrete constraints
|
||||
come from accepted ADR-007 and the populated architecture/domain/current-state
|
||||
records.
|
||||
|
||||
## Outcome
|
||||
|
||||
- Applied source `fe50e4ba198649a07c5c9443f15edf7dfa2a47e6` without
|
||||
conflicts onto its exact parent baseline. The source task record remained
|
||||
byte-identical to the committed source blob.
|
||||
- `main` now contains the conversation-first ImageCanvas: natural description and
|
||||
one-at-a-time Agent questions are primary, persisted option cards are not rendered
|
||||
as a form, the “AI 听懂的想法” projection is secondary, detailed constraints are
|
||||
collapsed by default, and manual field adjustment is explicitly optional.
|
||||
- Promoted the accepted behavior to `current-state.md`, `system-overview.md`, and
|
||||
`business-rules.md` without changing ADR-007's authority or Quote boundary.
|
||||
- The three adopted foreign untracked task records remain unmodified and excluded
|
||||
from the integration commit.
|
||||
|
||||
## Verification
|
||||
|
||||
- Source product paths match source commit `fe50e4b` exactly.
|
||||
- Root-main focused/adjacent unit regression: `45 passed`.
|
||||
- Root-main TypeScript `tsc --noEmit`: passed.
|
||||
- Root-main changed-file ESLint: passed.
|
||||
- Root-main production Vite/Electron build: passed with only existing browserslist,
|
||||
dynamic-import, and chunk-size warnings.
|
||||
- Root-main Electron/Playwright AI Design flow: `2 passed` across desktop/mobile,
|
||||
optional manual adjustment, conversation, Quote, and generation confirmation.
|
||||
- Source full-unit evidence: `1866 passed / 2 skipped / 1 unrelated timing failure`;
|
||||
the Pi real-process timing file passed `5/5` in isolation, and pressure passed
|
||||
`1/1`.
|
||||
- Source task record blob equality, `git diff --check`, project-doc completeness,
|
||||
and task-aware integration drift: passed.
|
||||
|
||||
## Follow-ups
|
||||
|
||||
- None recorded.
|
||||
|
||||
## Promotion Candidates
|
||||
|
||||
- Applied the source task's conversation-first presentation candidate to canonical
|
||||
current state, system architecture, and business rules. No unresolved promotion
|
||||
candidate remains.
|
||||
@@ -96,6 +96,7 @@
|
||||
- 客户端更新检查由 Electron Main 持有。缺少当前平台正式稳定 manifest 必须保持错误并提示稍后重试或从官网下载,不得宣称“已是最新版”;设置页只显示一条中文用户提示,原始堆栈、URL、路径和错误码只保留在 Main 日志。
|
||||
- 一个 AI Design Workspace 公开一个 current Direction、一个 persistent Agent Session 和一个 Current Specification;conversation timeline 不是独立创建或选择的权威对象。
|
||||
- Living Form 只能投影服务端 Current Specification。Chat、direct edit、decision、proposal、lock、Asset binding 与 restore 必须通过同一 V2 reducer;Renderer drafts 在 accepted 前保持本地。
|
||||
- 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。
|
||||
- Main 只向 Renderer 投影已知错误码的固定中文提示,未知上游错误文本必须脱敏为通用提示;Works Token、stream ticket、provider internals 留在 Main/服务端。
|
||||
@@ -142,4 +143,4 @@
|
||||
|
||||
## Last Reviewed
|
||||
|
||||
2026-09-03
|
||||
2026-09-04
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
import { Check, Sparkles, X } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { YouthActiveChoice } from './youth-form-projection';
|
||||
|
||||
export type CreativeChoiceGridProps = {
|
||||
choice: YouthActiveChoice;
|
||||
disabled?: boolean;
|
||||
onSelect: (optionId: string) => void;
|
||||
onReject?: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* A small, keyboard-friendly surface for the one decision the creator needs
|
||||
* to make next. The server-provided option label stays visible verbatim (so
|
||||
* options such as “你帮我选” remain useful), while the first option gets a
|
||||
* gentle recommendation marker.
|
||||
*/
|
||||
export function CreativeChoiceGrid({
|
||||
choice,
|
||||
disabled = false,
|
||||
onSelect,
|
||||
onReject,
|
||||
}: CreativeChoiceGridProps) {
|
||||
if (choice.options.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section
|
||||
data-testid="youth-creation-choice"
|
||||
className="rounded-2xl border border-brand/20 bg-brand/[0.035] p-4"
|
||||
>
|
||||
<div className="flex items-start gap-2.5">
|
||||
<div className="mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-xl bg-brand/[0.1] text-brand">
|
||||
<Sparkles className="h-4 w-4" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="text-sm font-semibold text-foreground">{choice.title}</h3>
|
||||
<p className="mt-1 text-xs leading-5 text-muted-foreground">{choice.body}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<fieldset className="mt-3 space-y-2">
|
||||
<legend className="sr-only">选择一个创作方向</legend>
|
||||
{choice.options.map((option, index) => {
|
||||
const descriptionId = `${choice.id}-${option.id}-description`;
|
||||
return (
|
||||
<button
|
||||
key={option.id}
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
aria-describedby={option.description ? descriptionId : undefined}
|
||||
className={cn(
|
||||
'flex min-h-11 w-full items-center gap-3 rounded-xl border border-border/70 bg-background px-3 text-left transition-colors duration-150',
|
||||
'hover:border-brand/35 hover:bg-brand/[0.04]',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/40 focus-visible:ring-offset-2',
|
||||
'disabled:pointer-events-none disabled:opacity-55',
|
||||
)}
|
||||
onClick={() => onSelect(option.id)}
|
||||
>
|
||||
<span className="flex min-w-0 flex-1 flex-col justify-center gap-0.5 py-2">
|
||||
<span className="flex flex-wrap items-center gap-2 text-sm font-medium text-foreground">
|
||||
<span>{option.label}</span>
|
||||
{index === 0 && (
|
||||
<span className="rounded-full bg-brand/[0.1] px-2 py-0.5 text-[11px] font-medium text-brand">
|
||||
推荐
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
{option.description && (
|
||||
<span
|
||||
id={descriptionId}
|
||||
className="text-xs leading-5 text-muted-foreground"
|
||||
>
|
||||
{option.description}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<Check className="h-4 w-4 shrink-0 text-brand/75" aria-hidden="true" />
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</fieldset>
|
||||
|
||||
{onReject && (
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
className="mt-2 inline-flex min-h-11 items-center gap-1.5 rounded-xl px-2.5 text-xs text-muted-foreground transition-colors hover:bg-background hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/35 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-55"
|
||||
onClick={onReject}
|
||||
>
|
||||
<X className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
先跳过这题
|
||||
</button>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -7,12 +7,11 @@ import { ImageWorkspaceApiError } from '@/lib/image-workspace';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useImageWorkspaceStore } from '@/stores/image-workspace';
|
||||
import type { DesignWorkspace } from '../../../shared/image-workspace';
|
||||
import { CreativeChoiceGrid } from './CreativeChoiceGrid';
|
||||
|
||||
const STARTER_MESSAGES = [
|
||||
'画一只会做饭的机器人',
|
||||
'把我的涂鸦变成一段动画',
|
||||
'做一张社团活动海报',
|
||||
'我想做一张热闹的社团招新海报',
|
||||
'我脑中有一只在月球踢球的熊猫',
|
||||
'把我的涂鸦变成一段会动的小故事',
|
||||
];
|
||||
|
||||
function chatFailureMessage(error: unknown): string {
|
||||
@@ -23,7 +22,7 @@ function chatFailureMessage(error: unknown): string {
|
||||
: '暂时没能确认这条消息的处理结果,内容还在输入框里';
|
||||
}
|
||||
const messages: Record<string, string> = {
|
||||
design_reasoner_invalid: 'AI 没有整理好这次想法,请再试一次',
|
||||
design_reasoner_invalid: 'AI 这次没听懂。你的内容还在输入框里,可以补充一个画面细节后再发送',
|
||||
design_reasoner_unavailable: 'AI 现在有点忙,暂时没能整理这次想法,请稍后再试',
|
||||
design_agent_run_failed: 'AI 这次没有整理好,内容还在输入框里,请再试一次',
|
||||
design_agent_run_cancelled: '这次整理已停止,内容还在输入框里',
|
||||
@@ -45,7 +44,6 @@ export function DesignConversationPane({ workspace }: { workspace: DesignWorkspa
|
||||
const chatDraft = useImageWorkspaceStore((state) => state.chatDraft);
|
||||
const setChatDraft = useImageWorkspaceStore((state) => state.setChatDraft);
|
||||
const sendChat = useImageWorkspaceStore((state) => state.sendChat);
|
||||
const resolveDecisionPrompt = useImageWorkspaceStore((state) => state.resolveDecisionPrompt);
|
||||
const assistantStreams = useImageWorkspaceStore((state) => state.assistantStreams);
|
||||
const pendingOperations = useImageWorkspaceStore((state) => state.pendingOperations);
|
||||
const scrollAnchorRef = useRef<HTMLDivElement>(null);
|
||||
@@ -56,17 +54,12 @@ export function DesignConversationPane({ workspace }: { workspace: DesignWorkspa
|
||||
const submittingChat = Object.values(pendingOperations).some(
|
||||
(operation) => operation.status === 'submitting' && operation.command.kind === 'apply_input',
|
||||
);
|
||||
const activeDecisionPrompt = workspace.form.decisionPrompts
|
||||
.find((prompt) => prompt.options.length > 0)
|
||||
?? workspace.form.decisionPrompts[0]
|
||||
?? null;
|
||||
|
||||
useEffect(() => {
|
||||
const scrollAnchor = scrollAnchorRef.current;
|
||||
if (typeof scrollAnchor?.scrollIntoView === 'function') {
|
||||
scrollAnchor.scrollIntoView({ block: 'end' });
|
||||
}
|
||||
}, [streams, workspace.form.decisionPrompts.length, workspace.turns.length]);
|
||||
}, [streams, workspace.turns.length]);
|
||||
|
||||
const submit = () => {
|
||||
if (!chatDraft.trim() || submittingChat) return;
|
||||
@@ -83,10 +76,10 @@ export function DesignConversationPane({ workspace }: { workspace: DesignWorkspa
|
||||
<header className="border-b border-border/70 bg-background px-5 py-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles className="h-4 w-4 text-brand" />
|
||||
<h2 className="text-sm font-semibold text-foreground">一起做作品</h2>
|
||||
<h2 className="text-sm font-semibold text-foreground">和 AI 聊聊你的想法</h2>
|
||||
</div>
|
||||
<p className="mt-1 text-xs leading-5 text-muted-foreground">
|
||||
先说一个想法就好,我会一次只问一个重要问题。
|
||||
像和同学讲故事一样说就好。AI 会先听懂,再一次问一个最重要的问题。
|
||||
</p>
|
||||
</header>
|
||||
|
||||
@@ -96,10 +89,11 @@ export function DesignConversationPane({ workspace }: { workspace: DesignWorkspa
|
||||
<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" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-foreground">你想创作什么?</h3>
|
||||
<h3 className="text-lg font-semibold text-foreground">先把脑中的画面说出来</h3>
|
||||
<p className="mt-2 text-sm leading-6 text-muted-foreground">
|
||||
可以说一个想法,也可以先放一张图片。不用一次想完整。
|
||||
不用懂设计。说说想画什么、发生在哪里,或者希望别人看到后有什么感觉;没有想全也没关系。
|
||||
</p>
|
||||
<p className="mt-5 text-xs font-medium text-foreground">不知道怎么开头?试试这样说</p>
|
||||
<div className="mt-5 space-y-2">
|
||||
{STARTER_MESSAGES.map((message) => (
|
||||
<button
|
||||
@@ -141,22 +135,6 @@ export function DesignConversationPane({ workspace }: { workspace: DesignWorkspa
|
||||
</div>
|
||||
))}
|
||||
|
||||
{activeDecisionPrompt && (
|
||||
<CreativeChoiceGrid
|
||||
choice={activeDecisionPrompt}
|
||||
disabled={submittingChat}
|
||||
onSelect={(optionId) => {
|
||||
void resolveDecisionPrompt(activeDecisionPrompt.id, 'select', optionId).catch(() => {
|
||||
toast.error('这个选择没有保存,请再试一次');
|
||||
});
|
||||
}}
|
||||
onReject={() => {
|
||||
void resolveDecisionPrompt(activeDecisionPrompt.id, 'reject').catch(() => {
|
||||
toast.error('这个选择没有保存,请再试一次');
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div ref={scrollAnchorRef} />
|
||||
</div>
|
||||
@@ -172,7 +150,7 @@ export function DesignConversationPane({ workspace }: { workspace: DesignWorkspa
|
||||
value={chatDraft}
|
||||
rows={3}
|
||||
disabled={submittingChat}
|
||||
placeholder="比如:画一只在月球踢球的熊猫……"
|
||||
placeholder="比如:我想做一张社团招新海报,要热闹、有活力……"
|
||||
className="min-h-[72px] resize-none border-0 bg-transparent px-2 py-1.5 shadow-none focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
onChange={(event) => setChatDraft(event.currentTarget.value)}
|
||||
onKeyDown={(event) => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
Ban,
|
||||
Check,
|
||||
ChevronDown,
|
||||
Film,
|
||||
ImageIcon,
|
||||
Lightbulb,
|
||||
@@ -25,7 +26,6 @@ import {
|
||||
projectYouthCreationCard,
|
||||
type YouthCreationCardModel,
|
||||
} from './youth-form-projection';
|
||||
import { CreativeChoiceGrid } from './CreativeChoiceGrid';
|
||||
|
||||
export type YouthCreationCardProps = {
|
||||
workspace: DesignWorkspace;
|
||||
@@ -245,20 +245,20 @@ function ActionFooter({
|
||||
case 'empty':
|
||||
return (
|
||||
<div className="flex flex-col gap-3 border-t border-border/60 pt-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<p className="text-xs leading-5 text-muted-foreground">不用想得很完整,先说一句就好。</p>
|
||||
<p className="text-xs leading-5 text-muted-foreground">先在对话里说说脑中的画面,不用一次想完整。</p>
|
||||
<Button type="button" className="min-h-11 rounded-xl" onClick={onFocusComposer}>
|
||||
<MessageCircle className="mr-2 h-4 w-4" aria-hidden="true" />
|
||||
先说一个想法
|
||||
开始描述
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
case 'exploring':
|
||||
return (
|
||||
<div className="flex flex-col gap-3 border-t border-border/60 pt-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<p className="text-xs leading-5 text-muted-foreground">你可以继续补充,也可以让 AI 帮你整理。</p>
|
||||
<p className="text-xs leading-5 text-muted-foreground">继续描述你想看到的画面,AI 会接着问一个关键问题。</p>
|
||||
<Button type="button" className="min-h-11 rounded-xl" onClick={onFocusComposer}>
|
||||
<MessageCircle className="mr-2 h-4 w-4" aria-hidden="true" />
|
||||
继续聊聊
|
||||
回到对话
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
@@ -336,37 +336,24 @@ export function YouthCreationCard({
|
||||
onOpenFineTune,
|
||||
onFocusComposer,
|
||||
}: YouthCreationCardProps) {
|
||||
const resolveDecisionPrompt = useImageWorkspaceStore((state) => state.resolveDecisionPrompt);
|
||||
const requestQuote = useImageWorkspaceStore((state) => state.requestQuote);
|
||||
const pendingOperations = useImageWorkspaceStore((state) => state.pendingOperations);
|
||||
const model = projectYouthCreationCard(workspace, { quoteBlockers });
|
||||
const submittingInput = Object.values(pendingOperations).some(
|
||||
(operation) => operation.status === 'submitting' && operation.command.kind === 'apply_input',
|
||||
);
|
||||
const requesting = Object.values(pendingOperations).some(
|
||||
(operation) => operation.status === 'submitting' && operation.command.kind === 'request_quote',
|
||||
);
|
||||
|
||||
const choose = (optionId: string) => {
|
||||
if (!model.activeChoice || submittingInput) return;
|
||||
void resolveDecisionPrompt(model.activeChoice.id, 'select', optionId).catch(() => {
|
||||
toast.error('这个选择没有保存,请再试一次');
|
||||
});
|
||||
};
|
||||
|
||||
const rejectChoice = () => {
|
||||
if (!model.activeChoice || submittingInput) return;
|
||||
void resolveDecisionPrompt(model.activeChoice.id, 'reject').catch(() => {
|
||||
toast.error('这个选择没有保存,请再试一次');
|
||||
});
|
||||
};
|
||||
|
||||
const request = () => {
|
||||
if (!generationAvailable || requesting || !model.canRequestQuote) return;
|
||||
void requestQuote().catch(() => {
|
||||
toast.error('制作方案还没准备好,请稍后再试');
|
||||
});
|
||||
};
|
||||
const hasMoreDetails = model.mustInclude.length > 0
|
||||
|| model.mustPreserve.length > 0
|
||||
|| model.mustAvoid.length > 0
|
||||
|| model.references.length > 0
|
||||
|| model.video !== null;
|
||||
|
||||
return (
|
||||
<section
|
||||
@@ -379,18 +366,22 @@ export function YouthCreationCard({
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles className="h-4 w-4 shrink-0 text-brand" aria-hidden="true" />
|
||||
<h2 id="youth-creation-card-title" className="text-base font-semibold text-foreground">
|
||||
我的创作
|
||||
AI 听懂的想法
|
||||
</h2>
|
||||
</div>
|
||||
<p className="mt-1 text-xs leading-5 text-muted-foreground">
|
||||
会跟着聊天自动更新,不需要逐项填写。
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
aria-label="手动调整(可选)"
|
||||
className="min-h-11 shrink-0 rounded-xl px-2.5 text-xs"
|
||||
onClick={onOpenFineTune}
|
||||
>
|
||||
<SlidersHorizontal className="mr-1.5 h-3.5 w-3.5" aria-hidden="true" />
|
||||
精细调整
|
||||
手动调整
|
||||
</Button>
|
||||
</header>
|
||||
|
||||
@@ -401,7 +392,7 @@ export function YouthCreationCard({
|
||||
<section>
|
||||
<div className="flex items-center gap-2 text-xs font-semibold text-foreground">
|
||||
<Lightbulb className="h-3.5 w-3.5 text-brand" aria-hidden="true" />
|
||||
<h3>想做什么</h3>
|
||||
<h3>目前听懂的是</h3>
|
||||
</div>
|
||||
<p className="mt-2 rounded-xl bg-surface-subtle/75 px-3 py-2.5 text-sm leading-6 text-foreground">
|
||||
{model.summary}
|
||||
@@ -433,35 +424,36 @@ export function YouthCreationCard({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
<TagSection
|
||||
title="一定要有"
|
||||
items={model.mustInclude}
|
||||
icon={<ListChecks className="h-3.5 w-3.5" aria-hidden="true" />}
|
||||
/>
|
||||
<TagSection
|
||||
title="想保留"
|
||||
items={model.mustPreserve}
|
||||
icon={<Check className="h-3.5 w-3.5" aria-hidden="true" />}
|
||||
/>
|
||||
<TagSection
|
||||
title="不要出现"
|
||||
items={model.mustAvoid}
|
||||
icon={<Ban className="h-3.5 w-3.5" aria-hidden="true" />}
|
||||
tone="avoid"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ReferenceSection model={model} />
|
||||
<VideoSection model={model} />
|
||||
|
||||
{model.activeChoice && (
|
||||
<CreativeChoiceGrid
|
||||
choice={model.activeChoice}
|
||||
disabled={submittingInput}
|
||||
onSelect={choose}
|
||||
onReject={rejectChoice}
|
||||
/>
|
||||
{hasMoreDetails && (
|
||||
<details
|
||||
data-testid="youth-creation-details"
|
||||
className="group rounded-xl border border-border/65 bg-surface-subtle/35"
|
||||
>
|
||||
<summary className="flex min-h-11 cursor-pointer list-none items-center justify-between gap-3 rounded-xl px-3 text-xs font-medium text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/35 [&::-webkit-details-marker]:hidden">
|
||||
<span>查看画面细节 <span className="font-normal text-muted-foreground">(可选)</span></span>
|
||||
<ChevronDown className="h-4 w-4 text-muted-foreground transition-transform group-open:rotate-180" aria-hidden="true" />
|
||||
</summary>
|
||||
<div className="space-y-4 border-t border-border/60 px-3 py-4">
|
||||
<TagSection
|
||||
title="一定要有"
|
||||
items={model.mustInclude}
|
||||
icon={<ListChecks className="h-3.5 w-3.5" aria-hidden="true" />}
|
||||
/>
|
||||
<TagSection
|
||||
title="想保留"
|
||||
items={model.mustPreserve}
|
||||
icon={<Check className="h-3.5 w-3.5" aria-hidden="true" />}
|
||||
/>
|
||||
<TagSection
|
||||
title="不要出现"
|
||||
items={model.mustAvoid}
|
||||
icon={<Ban className="h-3.5 w-3.5" aria-hidden="true" />}
|
||||
tone="avoid"
|
||||
/>
|
||||
<ReferenceSection model={model} />
|
||||
<VideoSection model={model} />
|
||||
</div>
|
||||
</details>
|
||||
)}
|
||||
|
||||
<ActionFooter
|
||||
|
||||
@@ -53,7 +53,7 @@ function LoadingWorkspace() {
|
||||
return (
|
||||
<div className="flex h-full min-h-0 flex-col bg-background" aria-label="正在加载 AI 设计">
|
||||
<div className="h-16 animate-pulse border-b border-border/60 bg-surface-subtle/60" />
|
||||
<div className="grid min-h-0 flex-1 lg:grid-cols-[minmax(420px,1.35fr)_minmax(360px,0.9fr)]">
|
||||
<div className="grid min-h-0 flex-1 lg:grid-cols-[minmax(0,1.8fr)_minmax(320px,0.72fr)]">
|
||||
<div className="space-y-4 border-r border-border/60 bg-surface-subtle/40 p-5">
|
||||
<div className="h-16 animate-pulse rounded-2xl bg-surface-subtle" />
|
||||
<div className="ml-auto h-20 w-4/5 animate-pulse rounded-2xl bg-brand/[0.07]" />
|
||||
@@ -261,14 +261,14 @@ export function ImageCanvas() {
|
||||
>
|
||||
<span className="flex min-w-0 items-center gap-2">
|
||||
<Sparkles className="h-4 w-4 shrink-0 text-brand" />
|
||||
<span className="truncate text-sm font-semibold text-foreground">我的创作</span>
|
||||
<span className="truncate text-sm font-semibold text-foreground">当前想法与作品</span>
|
||||
</span>
|
||||
<span className="truncate text-xs text-muted-foreground">{cardModel.readinessMessage}</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<main className="grid min-h-0 flex-1 lg:grid-cols-[minmax(420px,1.35fr)_minmax(360px,0.9fr)]">
|
||||
<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} />
|
||||
</div>
|
||||
@@ -298,8 +298,8 @@ export function ImageCanvas() {
|
||||
<Sheet open={creationCardOpen} onOpenChange={setCreationCardOpen}>
|
||||
<SheetContent side="bottom" className="flex max-h-[88vh] flex-col overflow-hidden rounded-t-3xl p-0">
|
||||
<SheetHeader className="sr-only">
|
||||
<SheetTitle>我的创作</SheetTitle>
|
||||
<SheetDescription>查看当前想法、制作方案和作品。</SheetDescription>
|
||||
<SheetTitle>当前想法与作品</SheetTitle>
|
||||
<SheetDescription>查看 AI 已经听懂的内容、制作方案和作品。</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto bg-surface-subtle/35 px-4 py-4">
|
||||
<div className="space-y-5 pb-[max(1rem,env(safe-area-inset-bottom))]">
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type {
|
||||
DesignAsset,
|
||||
DesignCompilationIssue,
|
||||
DesignDecisionPrompt,
|
||||
DesignMedium,
|
||||
DesignSpecificationValues,
|
||||
DesignWorkspace,
|
||||
@@ -141,11 +140,6 @@ export type YouthVideoSummary = {
|
||||
storyBeats: string[];
|
||||
};
|
||||
|
||||
export type YouthActiveChoice = Pick<
|
||||
DesignDecisionPrompt,
|
||||
'id' | 'kind' | 'title' | 'body' | 'options' | 'targetPaths'
|
||||
>;
|
||||
|
||||
export type YouthCreationCardModel = {
|
||||
phase: YouthCreationPhase;
|
||||
summary: string | null;
|
||||
@@ -157,7 +151,6 @@ export type YouthCreationCardModel = {
|
||||
mustAvoid: string[];
|
||||
references: YouthReferenceSummary[];
|
||||
video: YouthVideoSummary | null;
|
||||
activeChoice: YouthActiveChoice | null;
|
||||
blockers: DesignCompilationIssue[];
|
||||
blockerCount: number;
|
||||
canRequestQuote: boolean;
|
||||
@@ -344,8 +337,6 @@ export function projectYouthCreationCard(
|
||||
): YouthCreationCardModel {
|
||||
const values = workspace.form.specification.values;
|
||||
const summary = projectSummary(values);
|
||||
const activeChoice = workspace.form.decisionPrompts
|
||||
.find((prompt) => prompt.options.length > 0) ?? null;
|
||||
const blockers = (options.quoteBlockers ?? [])
|
||||
.filter((issue) => issue.severity === 'blocker')
|
||||
.map(projectYouthCompilationIssue);
|
||||
@@ -361,11 +352,9 @@ export function projectYouthCreationCard(
|
||||
} else if (offeredQuote) {
|
||||
phase = 'confirming';
|
||||
readinessMessage = '制作方案准备好了';
|
||||
} else if (blockers.length > 0 || activeChoice) {
|
||||
} else if (blockers.length > 0) {
|
||||
phase = 'exploring';
|
||||
readinessMessage = blockers.length > 0
|
||||
? `还差 ${blockers.length} 个关键选择`
|
||||
: '还有一个关键选择';
|
||||
readinessMessage = `还差 ${blockers.length} 个关键选择`;
|
||||
} else if (values.intent.media && hasCoreIdea(values)) {
|
||||
phase = 'ready';
|
||||
readinessMessage = '想法已经清楚,可以检查制作方案';
|
||||
@@ -388,7 +377,6 @@ export function projectYouthCreationCard(
|
||||
mustAvoid: values.constraints.must_avoid.filter(Boolean),
|
||||
references: projectReferences(workspace),
|
||||
video: projectVideo(values),
|
||||
activeChoice,
|
||||
blockers,
|
||||
blockerCount: blockers.length,
|
||||
canRequestQuote: !offeredQuote && (phase === 'ready' || phase === 'result'),
|
||||
|
||||
@@ -407,7 +407,8 @@ test.describe('AI Design V2 workspace', () => {
|
||||
))).toBe(true);
|
||||
await expect(page.getByTestId('image-workspace-conversation')).toBeVisible();
|
||||
await expect(page.getByTestId('image-workspace-creation-pane')).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: '我的创作' })).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: '和 AI 聊聊你的想法' })).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'AI 听懂的想法' })).toBeVisible();
|
||||
await expect(page.getByText('把便携咖啡机呈现为城市通勤中的精密工具')).toBeVisible();
|
||||
await expect(page.getByText('Living Form')).toHaveCount(0);
|
||||
await expect(page.getByText('目标与用途')).toHaveCount(0);
|
||||
@@ -438,7 +439,7 @@ test.describe('AI Design V2 workspace', () => {
|
||||
await page.setViewportSize({ width: 375, height: 800 });
|
||||
await expect(page.getByTestId('image-workspace-conversation')).toBeVisible();
|
||||
await expect(page.getByTestId('image-workspace-creation-pane')).toBeHidden();
|
||||
await page.getByRole('button', { name: /^我的创作/ }).click();
|
||||
await page.getByRole('button', { name: /^当前想法与作品/ }).click();
|
||||
await expect(page.getByTestId('youth-creation-card')).toBeVisible();
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
@@ -449,7 +450,7 @@ test.describe('AI Design V2 workspace', () => {
|
||||
await page.setViewportSize(desktopViewport ?? { width: 1280, height: 800 });
|
||||
await expect(page.getByTestId('image-workspace-creation-pane')).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: '精细调整' }).click();
|
||||
await page.getByRole('button', { name: '手动调整(可选)' }).click();
|
||||
await page.getByRole('button', { name: '电脑横屏' }).click();
|
||||
await page.getByRole('button', { name: '完成调整' }).click();
|
||||
await expect(page.getByTestId('youth-creation-card').getByText('电脑横屏')).toBeVisible();
|
||||
|
||||
@@ -61,7 +61,7 @@ function prepareWorkspace(overrides = {}) {
|
||||
}
|
||||
|
||||
function openMobileCreationCard() {
|
||||
fireEvent.click(screen.getByRole('button', { name: /^我的创作/ }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /^当前想法与作品/ }));
|
||||
}
|
||||
|
||||
describe('youth AI Design Canvas page', () => {
|
||||
@@ -76,28 +76,62 @@ describe('youth AI Design Canvas page', () => {
|
||||
useImageWorkspaceStore.getState().reset();
|
||||
});
|
||||
|
||||
it('starts with conversation and a concise Creation Card instead of a professional field matrix', () => {
|
||||
it('starts with a guided conversation and keeps the structured summary secondary', () => {
|
||||
prepareWorkspace();
|
||||
render(<ImageCanvas />);
|
||||
|
||||
expect(screen.getByTestId('image-workspace-conversation')).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: '一起做作品' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: '和 AI 聊聊你的想法' })).toBeInTheDocument();
|
||||
expect(screen.getByText('像和同学讲故事一样说就好。AI 会先听懂,再一次问一个最重要的问题。')).toBeInTheDocument();
|
||||
openMobileCreationCard();
|
||||
const card = screen.getByTestId('youth-creation-card');
|
||||
expect(card).toBeInTheDocument();
|
||||
expect(within(card).getByRole('heading', { name: '我的创作' })).toBeInTheDocument();
|
||||
expect(within(card).getByRole('heading', { name: 'AI 听懂的想法' })).toBeInTheDocument();
|
||||
expect(screen.getByText('做一张便携咖啡机的小红书主视觉')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Living Form')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('目标与用途')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/规格版本|编译器|生成策略|字段决策/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('invites a free description and does not turn a legacy decision prompt into a form', () => {
|
||||
const workspace = designWorkspaceFixture({
|
||||
form: designFormFixture({
|
||||
decisionPrompts: [{
|
||||
id: 'choice-1',
|
||||
kind: 'question',
|
||||
basedOnSpecificationRevisionId: 'specification-revision-3',
|
||||
targetPaths: ['visual.art_direction'],
|
||||
title: '画面想要什么感觉?',
|
||||
body: '请从下面选一个。',
|
||||
options: [
|
||||
{ id: 'cartoon', label: '有趣的卡通', description: null },
|
||||
{ id: 'cinema', label: '像电影一样', description: null },
|
||||
],
|
||||
}],
|
||||
}),
|
||||
turns: [],
|
||||
});
|
||||
prepareWorkspace(workspace);
|
||||
render(<ImageCanvas />);
|
||||
|
||||
const conversation = screen.getByTestId('image-workspace-conversation');
|
||||
expect(within(conversation).getByRole('heading', { name: '先把脑中的画面说出来' })).toBeInTheDocument();
|
||||
expect(within(conversation).getByText(/想画什么、发生在哪里/)).toBeInTheDocument();
|
||||
expect(within(conversation).queryByText('画面想要什么感觉?')).not.toBeInTheDocument();
|
||||
expect(within(conversation).queryByRole('button', { name: '有趣的卡通' })).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(within(conversation).getByRole('button', { name: '我想做一张热闹的社团招新海报' }));
|
||||
expect(within(conversation).getByRole('textbox', { name: '告诉 AI 你想创作什么' })).toHaveValue(
|
||||
'我想做一张热闹的社团招新海报',
|
||||
);
|
||||
});
|
||||
|
||||
it('sends high-impact fine adjustments through the existing typed field operations', () => {
|
||||
const { actions } = prepareWorkspace();
|
||||
render(<ImageCanvas />);
|
||||
|
||||
openMobileCreationCard();
|
||||
fireEvent.click(screen.getByRole('button', { name: '精细调整' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '手动调整(可选)' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '电脑横屏' }));
|
||||
|
||||
expect(actions.applyFieldOperations).toHaveBeenCalledWith(
|
||||
@@ -160,7 +194,9 @@ describe('youth AI Design Canvas page', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: '发送消息' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toastErrorMock).toHaveBeenCalledWith('AI 没有整理好这次想法,请再试一次');
|
||||
expect(toastErrorMock).toHaveBeenCalledWith(
|
||||
'AI 这次没听懂。你的内容还在输入框里,可以补充一个画面细节后再发送',
|
||||
);
|
||||
});
|
||||
expect(toastErrorMock).not.toHaveBeenCalledWith('消息没有发出去,请再试一次');
|
||||
});
|
||||
|
||||
@@ -100,7 +100,8 @@ describe('YouthCreationCard', () => {
|
||||
it('shows a plain creation summary and offers a youth-facing ready action', () => {
|
||||
const { requestQuote } = renderCard();
|
||||
|
||||
expect(screen.getByRole('heading', { name: '我的创作' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: 'AI 听懂的想法' })).toBeInTheDocument();
|
||||
expect(screen.getByText('会跟着聊天自动更新,不需要逐项填写。')).toBeInTheDocument();
|
||||
expect(screen.getByText('把便携咖啡机呈现为城市通勤中的精密工具')).toBeInTheDocument();
|
||||
expect(screen.getByText('图片')).toBeInTheDocument();
|
||||
expect(screen.getByText('竖版海报')).toBeInTheDocument();
|
||||
@@ -113,7 +114,7 @@ describe('YouthCreationCard', () => {
|
||||
expect(requestQuote).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('shows only the first choice, keeps the server label, and resolves it through the store', () => {
|
||||
it('does not surface legacy decision prompts as a form', () => {
|
||||
const workspace = designWorkspaceFixture({
|
||||
form: designFormFixture({
|
||||
decisionPrompts: [{
|
||||
@@ -141,18 +142,12 @@ describe('YouthCreationCard', () => {
|
||||
});
|
||||
const { resolveDecisionPrompt } = renderCard(workspace);
|
||||
|
||||
expect(screen.getByTestId('youth-creation-choice')).toBeInTheDocument();
|
||||
expect(screen.getByText('画面想要什么感觉?')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /有趣的卡通/ })).toHaveClass('min-h-11');
|
||||
expect(screen.getByText('推荐')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /你帮我选/ })).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('youth-creation-choice')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('画面想要什么感觉?')).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: /有趣的卡通/ })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: /你帮我选/ })).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('这个问题不该显示')).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /你帮我选/ }));
|
||||
expect(resolveDecisionPrompt).toHaveBeenCalledWith('choice-1', 'select', 'delegate');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '先跳过这题' }));
|
||||
expect(resolveDecisionPrompt).toHaveBeenCalledWith('choice-1', 'reject');
|
||||
expect(resolveDecisionPrompt).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows known must-have, must-avoid, reference, video, and plain blocker details only', () => {
|
||||
@@ -213,6 +208,7 @@ describe('YouthCreationCard', () => {
|
||||
|
||||
renderCard(workspace, { quoteBlockers: [blocker] });
|
||||
|
||||
fireEvent.click(screen.getByText('查看画面细节'));
|
||||
expect(screen.getByText('一定要有')).toBeInTheDocument();
|
||||
expect(screen.getByText('远处的地球')).toBeInTheDocument();
|
||||
expect(screen.getByText('想保留')).toBeInTheDocument();
|
||||
@@ -258,10 +254,10 @@ describe('YouthCreationCard', () => {
|
||||
);
|
||||
|
||||
expect(within(screen.getByTestId('youth-creation-readiness')).getByText('等你的第一个想法')).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: '先说一个想法' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '开始描述' }));
|
||||
expect(onFocusComposer).toHaveBeenCalledOnce();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '精细调整' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '手动调整(可选)' }));
|
||||
expect(onOpenFineTune).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
|
||||
@@ -139,7 +139,7 @@ describe('youth Design projection', () => {
|
||||
expect(model.readinessMessage).not.toContain('可以开始制作');
|
||||
});
|
||||
|
||||
it('shows only the first consequential choice and concrete quote blockers', () => {
|
||||
it('projects concrete quote blockers without exposing server decision prompts', () => {
|
||||
const workspace = designWorkspaceFixture({
|
||||
form: designFormFixture({
|
||||
decisionPrompts: [{
|
||||
@@ -174,8 +174,6 @@ describe('youth Design projection', () => {
|
||||
const model = projectYouthCreationCard(workspace, { quoteBlockers: blockers });
|
||||
|
||||
expect(model.phase).toBe('exploring');
|
||||
expect(model.activeChoice?.id).toBe('question-1');
|
||||
expect(model.activeChoice?.options).toHaveLength(3);
|
||||
expect(model.blockers).toEqual([{
|
||||
...blockers[0],
|
||||
message: '先选择作品形状。',
|
||||
|
||||
Reference in New Issue
Block a user