feat(design): integrate conversation-first creation flow
This commit is contained in:
@@ -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'),
|
||||
|
||||
Reference in New Issue
Block a user