Files
Genarrative/src/components/creative-agent/CreativeAgentWorkspace.tsx
T
kdletters ce13bdbb02 继续收口浅色返回按钮
将统一创作页与多个浅色工作台返回入口切换为 PlatformBackActionButton
按页面保留 compact 与 regular 返回尺寸及禁用态
清理冗余 ArrowLeft 引用并完成对应页面定向验证
2026-06-11 04:54:08 +08:00

348 lines
12 KiB
TypeScript

import { CheckCircle2, Puzzle } from 'lucide-react';
import { useEffect, useMemo, useState } from 'react';
import type {
CreativeAgentInputPart,
CreativeAgentSessionSnapshot,
CreativeAgentSseEvent,
} from '../../../packages/shared/src/contracts/creativeAgent';
import type {
PuzzleCreativeTemplateProtocol,
PuzzleCreativeTemplateSelection,
} from '../../../packages/shared/src/contracts/puzzleCreativeTemplate';
import { PlatformActionButton } from '../common/PlatformActionButton';
import { PlatformBackActionButton } from '../common/PlatformBackActionButton';
import { PlatformEmptyState } from '../common/PlatformEmptyState';
import { PlatformIconBadge } from '../common/PlatformIconBadge';
import { PlatformMediaFrame } from '../common/PlatformMediaFrame';
import { PlatformPillBadge } from '../common/PlatformPillBadge';
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
import { PlatformSubpanel } from '../common/PlatformSubpanel';
import { CreativeAgentInputComposer } from './CreativeAgentInputComposer';
import { CreativeAgentProcessPanel } from './CreativeAgentProcessPanel';
import { CreativeAgentStageTimeline } from './CreativeAgentStageTimeline';
import { CreativeAgentTemplateConfirmPanel } from './CreativeAgentTemplateConfirmPanel';
import {
buildCreativeAgentProcessItems,
buildPuzzleTemplateSelectionFromProtocol,
createCreativeAgentClientMessageId,
CREATIVE_AGENT_STAGE_LABEL,
} from './creativeAgentViewModel';
type CreativeAgentWorkspaceProps = {
session: CreativeAgentSessionSnapshot | null;
isBusy: boolean;
isStreaming: boolean;
error: string | null;
eventLog: CreativeAgentSseEvent[];
onBack: () => void;
showBackButton?: boolean;
onSubmitMessage: (payload: {
clientMessageId: string;
content: CreativeAgentInputPart[];
}) => void;
onConfirmTemplate: (selection: PuzzleCreativeTemplateSelection) => void;
onCancelTemplate?: () => void;
onOpenTarget: () => void;
};
type CreativeAgentTemplateCatalogPanelProps = {
templates: PuzzleCreativeTemplateProtocol[];
isBusy: boolean;
onSelect: (template: PuzzleCreativeTemplateProtocol) => void;
};
function CreativeAgentTemplateCatalogPanel({
templates,
isBusy,
onSelect,
}: CreativeAgentTemplateCatalogPanelProps) {
if (templates.length === 0) {
return null;
}
return (
<PlatformSubpanel radius="lg">
<div className="grid gap-3 sm:grid-cols-3">
{templates.map((template) => (
<PlatformSubpanel
as="button"
key={template.templateId}
disabled={isBusy}
onClick={() => onSelect(template)}
interactive
surface="flat"
radius="md"
padding="sm"
className="group min-h-[10.5rem] hover:-translate-y-0.5"
data-testid="creative-agent-template-card"
>
<PlatformMediaFrame
src={template.previewImageSrc}
alt={template.title}
fallbackLabel={template.title}
fallbackContent={
<PlatformIconBadge
icon={<Puzzle className="h-4 w-4" />}
size="base"
tone="softBright"
/>
}
aspect="landscape"
surface="none"
className="rounded-[0.95rem] border border-white/70 bg-[radial-gradient(circle_at_28%_20%,rgba(255,255,255,0.92),transparent_32%),linear-gradient(135deg,rgba(255,194,123,0.86),rgba(255,93,132,0.82)_52%,rgba(92,186,255,0.78))]"
fallbackClassName="tracking-normal"
/>
<div className="mt-3 text-sm font-black text-[var(--platform-text-strong)]">
{template.title}
</div>
<div className="mt-1 line-clamp-2 text-xs leading-5 text-[var(--platform-text-base)]">
{template.summary}
</div>
<div className="mt-3 text-xs font-bold text-[var(--platform-text-soft)]">
{`${template.defaultLevelCount} 关 · ${template.costRange.minPoints}-${template.costRange.maxPoints} 泥点`}
</div>
</PlatformSubpanel>
))}
</div>
</PlatformSubpanel>
);
}
export function CreativeAgentWorkspace({
session,
isBusy,
isStreaming,
error,
eventLog,
onBack,
showBackButton = true,
onSubmitMessage,
onConfirmTemplate,
onCancelTemplate,
onOpenTarget,
}: CreativeAgentWorkspaceProps) {
const stage = session?.stage ?? 'idle';
const messages = session?.messages ?? [];
const selection = session?.puzzleTemplateSelection ?? null;
const templateCatalog = session?.puzzleTemplateCatalog ?? [];
const targetBinding = session?.targetBinding ?? null;
const [pendingSelection, setPendingSelection] =
useState<PuzzleCreativeTemplateSelection | null>(null);
useEffect(() => {
// 中文注释:会话切换时清掉本地待确认模板,避免上一轮选择残留到新会话。
setPendingSelection(null);
}, [session?.sessionId]);
const processItems = useMemo(
() => buildCreativeAgentProcessItems(eventLog, session),
[eventLog, session],
);
const visibleSelection = targetBinding
? null
: (selection ?? pendingSelection);
const shouldShowTemplateCatalog =
!targetBinding &&
!selection &&
templateCatalog.length > 0 &&
stage === 'waiting_template_confirmation';
return (
<div className="platform-remap-surface mx-auto flex h-full min-h-0 w-full flex-col xl:max-w-[min(100%,88rem)] xl:px-1">
<div className="mb-3 flex items-center justify-between gap-3">
{showBackButton ? (
<PlatformBackActionButton
onClick={onBack}
disabled={isBusy}
className="px-3"
/>
) : (
<span aria-hidden="true" />
)}
<PlatformPillBadge tone="cool" size="xs" className="px-3">
{CREATIVE_AGENT_STAGE_LABEL[stage]}
</PlatformPillBadge>
</div>
<div className="min-h-0 flex-1 overflow-y-auto pr-1">
<div className="space-y-4 pb-4">
<section className="platform-surface platform-surface--hero relative overflow-hidden rounded-[1.6rem] px-4 py-5 sm:px-5">
<div className="absolute inset-0 bg-[var(--platform-hero-overlay-strong)]" />
<div className="relative z-10 flex items-end justify-between gap-4">
<div className="min-w-0">
<div className="text-2xl font-black leading-tight text-white sm:text-3xl">
智能创作
</div>
<div className="mt-2 max-w-xl text-sm font-semibold leading-6 text-zinc-100/86">
当前生成拼图草稿
</div>
</div>
<PlatformIconBadge
icon={<Puzzle className="h-5 w-5" />}
size="lg"
tone="hero"
className="hidden sm:grid"
/>
</div>
</section>
<CreativeAgentStageTimeline stage={stage} />
{targetBinding ? (
<PlatformSubpanel
radius="lg"
className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"
>
<div className="flex items-center gap-3">
<PlatformIconBadge
icon={<CheckCircle2 className="h-5 w-5" />}
size="base"
tone="success"
/>
<div>
<div className="text-base font-black text-[var(--platform-text-strong)]">
拼图草稿已就绪
</div>
<div className="mt-1 text-sm text-[var(--platform-text-base)]">
{targetBinding.targetStage === 'puzzle-result'
? '可以进入结果页继续编辑'
: '可以进入拼图工作区继续处理'}
</div>
</div>
</div>
<PlatformActionButton disabled={isBusy} onClick={onOpenTarget}>
打开草稿
</PlatformActionButton>
</PlatformSubpanel>
) : null}
{messages.length > 0 ? (
<div className="space-y-2">
{messages.map((message) => (
<div
key={message.id}
className={`max-w-[86%] rounded-[1.15rem] px-4 py-3 text-sm leading-6 ${
message.role === 'user'
? 'ml-auto bg-[var(--platform-button-primary-solid)] text-[var(--platform-button-primary-text)]'
: 'platform-subpanel text-[var(--platform-text-base)]'
}`}
>
{message.text}
</div>
))}
</div>
) : (
<PlatformEmptyState
surface="subpanel"
size="compact"
className="rounded-[1.35rem] font-semibold"
>
发一句想法,或加一张参考图。
</PlatformEmptyState>
)}
<CreativeAgentProcessPanel
items={processItems}
isStreaming={isStreaming}
/>
{shouldShowTemplateCatalog ? (
<CreativeAgentTemplateCatalogPanel
templates={templateCatalog}
isBusy={isBusy || isStreaming}
onSelect={(template) => {
setPendingSelection(
buildPuzzleTemplateSelectionFromProtocol(template),
);
}}
/>
) : null}
{session?.puzzleImageGenerationPlan ? (
<PlatformSubpanel
as="div"
title="关卡计划"
titleClassName="tracking-[0.18em]"
radius="lg"
>
<div className="mt-3 grid gap-2 sm:grid-cols-2">
{session.puzzleImageGenerationPlan.levels.map((level) => (
<PlatformSubpanel
key={level.levelId}
as="div"
surface="flat"
radius="sm"
padding="none"
className="bg-white/58 px-3 py-3"
>
<div className="text-sm font-black text-[var(--platform-text-strong)]">
{level.levelName}
</div>
<div className="mt-1 line-clamp-2 text-xs leading-5 text-[var(--platform-text-base)]">
{level.pictureDescription}
</div>
</PlatformSubpanel>
))}
</div>
</PlatformSubpanel>
) : null}
{error ? (
<PlatformStatusMessage
tone="error"
surface="platform"
size="md"
className="rounded-[1.25rem]"
>
{error}
</PlatformStatusMessage>
) : null}
</div>
</div>
<div className="pb-[max(0.25rem,env(safe-area-inset-bottom))]">
<CreativeAgentInputComposer
isBusy={isBusy || isStreaming}
onSubmit={({ text, image }) => {
const content: CreativeAgentInputPart[] = [];
if (text) {
content.push({
type: 'input_text',
text,
});
}
if (image) {
content.push({
type: 'input_image',
imageUrl: image.imageUrl,
thumbnailUrl: image.thumbnailUrl,
assetId: null,
});
}
onSubmitMessage({
clientMessageId: createCreativeAgentClientMessageId(),
content,
});
}}
/>
</div>
{visibleSelection && visibleSelection.requiresUserConfirmation ? (
<CreativeAgentTemplateConfirmPanel
selection={visibleSelection}
isBusy={isBusy || isStreaming}
onConfirm={(nextSelection) => {
setPendingSelection(null);
onConfirmTemplate(nextSelection);
}}
onCancel={() => {
setPendingSelection(null);
onCancelTemplate?.();
}}
/>
) : null}
</div>
);
}
export default CreativeAgentWorkspace;