This commit is contained in:
2026-05-08 11:44:42 +08:00
parent b08127031c
commit abf1f1ebea
249 changed files with 39411 additions and 887 deletions

View File

@@ -0,0 +1,313 @@
import { ArrowLeft, 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 { 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;
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 (
<section className="platform-subpanel rounded-[1.35rem] p-4">
<div className="grid gap-3 sm:grid-cols-3">
{templates.map((template) => (
<button
key={template.templateId}
type="button"
disabled={isBusy}
onClick={() => onSelect(template)}
className="group min-h-[10.5rem] rounded-[1.15rem] border border-[var(--platform-subpanel-border)] bg-white/68 p-3 text-left transition hover:-translate-y-0.5 hover:bg-white/88 disabled:opacity-55"
>
<div className="overflow-hidden 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))]">
<div className="flex aspect-[16/9] items-center justify-center">
{template.previewImageSrc ? (
<img
src={template.previewImageSrc}
alt={template.title}
className="h-full w-full object-cover"
/>
) : (
<span className="inline-flex h-10 w-10 items-center justify-center rounded-full bg-white/84 text-[var(--platform-text-strong)] shadow-sm">
<Puzzle className="h-4 w-4" />
</span>
)}
</div>
</div>
<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>
</button>
))}
</div>
</section>
);
}
export function CreativeAgentWorkspace({
session,
isBusy,
isStreaming,
error,
eventLog,
onBack,
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">
<button
type="button"
onClick={onBack}
disabled={isBusy}
className={`platform-button platform-button--ghost min-h-0 px-3 py-1.5 text-[11px] ${isBusy ? 'opacity-45' : ''}`}
>
<span className="inline-flex items-center gap-1.5">
<ArrowLeft className="h-3.5 w-3.5" />
</span>
</button>
<div className="platform-pill platform-pill--cool px-3 text-[11px]">
{CREATIVE_AGENT_STAGE_LABEL[stage]}
</div>
</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>
<span className="hidden h-12 w-12 shrink-0 items-center justify-center rounded-full bg-white/18 text-white sm:inline-flex">
<Puzzle className="h-5 w-5" />
</span>
</div>
</section>
<CreativeAgentStageTimeline stage={stage} />
{targetBinding ? (
<section className="platform-subpanel flex flex-col gap-3 rounded-[1.35rem] p-4 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3">
<span className="inline-flex h-10 w-10 items-center justify-center rounded-full bg-emerald-100 text-emerald-700">
<CheckCircle2 className="h-5 w-5" />
</span>
<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>
<button
type="button"
disabled={isBusy}
onClick={onOpenTarget}
className="platform-button platform-button--primary"
>
稿
</button>
</section>
) : 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-fill)] text-[var(--platform-button-primary-text)]'
: 'platform-subpanel text-[var(--platform-text-base)]'
}`}
>
{message.text}
</div>
))}
</div>
) : (
<div className="platform-subpanel rounded-[1.35rem] p-4 text-sm font-semibold text-[var(--platform-text-base)]">
</div>
)}
<CreativeAgentProcessPanel
items={processItems}
isStreaming={isStreaming}
/>
{shouldShowTemplateCatalog ? (
<CreativeAgentTemplateCatalogPanel
templates={templateCatalog}
isBusy={isBusy || isStreaming}
onSelect={(template) => {
setPendingSelection(
buildPuzzleTemplateSelectionFromProtocol(template),
);
}}
/>
) : null}
{session?.puzzleImageGenerationPlan ? (
<div className="platform-subpanel rounded-[1.35rem] p-4">
<div className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
</div>
<div className="mt-3 grid gap-2 sm:grid-cols-2">
{session.puzzleImageGenerationPlan.levels.map((level) => (
<div
key={level.levelId}
className="rounded-[1rem] border border-[var(--platform-subpanel-border)] 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>
</div>
))}
</div>
</div>
) : null}
{error ? (
<div className="platform-banner platform-banner--danger rounded-[1.25rem] text-sm leading-6">
{error}
</div>
) : 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;