This commit is contained in:
2026-05-05 14:40:41 +08:00
parent e847fcea6f
commit 07e777fef8
76 changed files with 4246 additions and 444 deletions

View File

@@ -8,6 +8,7 @@ import type {
SendPuzzleAgentMessageRequest,
} from '../../../packages/shared/src/contracts/puzzleAgentSession';
import { readPuzzleReferenceImageAsDataUrl } from '../../services/puzzleReferenceImage';
import { PUZZLE_CREATION_TEMPLATES } from './puzzleCreationTemplates';
import {
normalizePuzzleImageModel,
PUZZLE_IMAGE_MODEL_GPT_IMAGE_2,
@@ -28,8 +29,6 @@ type PuzzleAgentWorkspaceProps = {
};
type PuzzleFormState = {
workTitle: string;
workDescription: string;
pictureDescription: string;
referenceImageSrc: string;
referenceImageLabel: string;
@@ -37,8 +36,6 @@ type PuzzleFormState = {
};
const EMPTY_FORM_STATE: PuzzleFormState = {
workTitle: '',
workDescription: '',
pictureDescription: '',
referenceImageSrc: '',
referenceImageLabel: '',
@@ -52,8 +49,6 @@ function resolveInitialFormState(
const formDraft = session?.draft?.formDraft;
if (formDraft) {
return {
workTitle: formDraft.workTitle ?? '',
workDescription: formDraft.workDescription ?? '',
pictureDescription: formDraft.pictureDescription ?? '',
referenceImageSrc: initialFormPayload?.referenceImageSrc ?? '',
referenceImageLabel: initialFormPayload?.referenceImageSrc
@@ -65,10 +60,10 @@ function resolveInitialFormState(
if (initialFormPayload) {
return {
workTitle:
initialFormPayload.workTitle ?? initialFormPayload.seedText ?? '',
workDescription: initialFormPayload.workDescription ?? '',
pictureDescription: initialFormPayload.pictureDescription ?? '',
pictureDescription:
initialFormPayload.pictureDescription ??
initialFormPayload.seedText ??
'',
referenceImageSrc: initialFormPayload.referenceImageSrc ?? '',
referenceImageLabel: initialFormPayload.referenceImageSrc
? '已选择参考图'
@@ -82,19 +77,12 @@ function resolveInitialFormState(
}
return {
workTitle:
session.draft?.workTitle ||
session.draft?.levelName ||
session.seedText ||
session.anchorPack.themePromise.value ||
session.messages.find((message) => message.role === 'user')?.text ||
'',
workDescription:
session.draft?.workDescription ||
session.anchorPack.themePromise.value ||
'',
pictureDescription:
session.draft?.summary || session.anchorPack.visualSubject.value || '',
session.draft?.formDraft?.pictureDescription ||
session.draft?.levels?.[0]?.pictureDescription ||
session.anchorPack.visualSubject.value ||
session.seedText ||
'',
referenceImageSrc: '',
referenceImageLabel: '',
imageModel: PUZZLE_IMAGE_MODEL_GPT_IMAGE_2,
@@ -121,6 +109,9 @@ export function PuzzleAgentWorkspace({
const [referenceImageError, setReferenceImageError] = useState<string | null>(
null,
);
const [selectedTemplateId, setSelectedTemplateId] = useState(
PUZZLE_CREATION_TEMPLATES[0]?.id ?? '',
);
const previousSessionIdRef = useRef<string | null>(
session?.sessionId ?? null,
);
@@ -148,18 +139,13 @@ export function PuzzleAgentWorkspace({
appliedInitialFormKeyRef.current = nextInitialFormKey;
setFormState(resolveInitialFormState(session, initialFormPayload));
setReferenceImageError(null);
}, [initialFormPayload, session?.sessionId]);
}, [initialFormPayload, session]);
const workTitle = formState.workTitle.trim();
const workDescription = formState.workDescription.trim();
const pictureDescription = formState.pictureDescription.trim();
const canSubmit =
Boolean(workTitle && workDescription && pictureDescription) && !isBusy;
const canSubmit = Boolean(pictureDescription) && !isBusy;
const autosavePayload = useMemo(
() => ({
seedText: workTitle,
workTitle,
workDescription,
seedText: pictureDescription,
pictureDescription,
referenceImageSrc: formState.referenceImageSrc || null,
imageModel: formState.imageModel,
@@ -168,13 +154,9 @@ export function PuzzleAgentWorkspace({
formState.referenceImageSrc,
formState.imageModel,
pictureDescription,
workDescription,
workTitle,
],
);
const autosaveSignature = JSON.stringify([
autosavePayload.workTitle,
autosavePayload.workDescription,
autosavePayload.pictureDescription,
autosavePayload.imageModel,
]);
@@ -189,7 +171,7 @@ export function PuzzleAgentWorkspace({
autosaveSessionIdRef.current = currentSessionId;
lastAutosaveSignatureRef.current = autosaveSignature;
}, [autosaveSignature, session?.sessionId]);
}, [autosaveSignature, session]);
useEffect(() => {
if (
@@ -214,7 +196,7 @@ export function PuzzleAgentWorkspace({
onAutoSaveForm,
session?.draft?.formDraft,
session?.stage,
session?.sessionId,
session,
]);
const handleReferenceImageChange = async (
@@ -243,15 +225,28 @@ export function PuzzleAgentWorkspace({
}
};
const applyTemplatePrompt = (templateId: string) => {
const template = PUZZLE_CREATION_TEMPLATES.find(
(item) => item.id === templateId,
);
if (!template) {
return;
}
setSelectedTemplateId(template.id);
setFormState((current) => ({
...current,
pictureDescription: template.prompt,
}));
};
const submitForm = () => {
if (!canSubmit) {
return;
}
const payload = {
seedText: workTitle,
workTitle,
workDescription,
seedText: pictureDescription,
pictureDescription,
referenceImageSrc: formState.referenceImageSrc || null,
imageModel: formState.imageModel,
@@ -265,8 +260,6 @@ export function PuzzleAgentWorkspace({
onExecuteAction({
action: 'compile_puzzle_draft',
promptText: pictureDescription,
workTitle,
workDescription,
pictureDescription,
referenceImageSrc: formState.referenceImageSrc || null,
imageModel: formState.imageModel,
@@ -275,7 +268,7 @@ export function PuzzleAgentWorkspace({
};
return (
<div className="platform-remap-surface mx-auto flex h-full min-h-0 w-full max-w-4xl flex-col">
<div className="platform-remap-surface mx-auto flex h-full min-h-0 w-full max-w-5xl flex-col">
<div className="mb-4 flex items-center justify-between gap-3">
<button
type="button"
@@ -291,61 +284,107 @@ export function PuzzleAgentWorkspace({
</div>
<div className="min-h-0 flex-1 overflow-y-auto pr-1">
<section className="platform-subpanel rounded-[1.5rem] p-4 sm:p-5">
<div className="space-y-5">
<label className="block">
<span className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
<div className="mb-5">
<div className="flex flex-wrap items-center gap-2">
<h1 className="m-0 text-5xl font-black leading-none tracking-normal text-[var(--platform-text-strong)] sm:text-7xl">
</h1>
<span className="rounded-full border border-emerald-200 bg-emerald-50 px-3 py-1 text-[11px] font-black text-emerald-700">
BETA
</span>
</div>
</div>
<section className="platform-subpanel overflow-hidden rounded-[1.5rem] p-4 sm:p-5">
<div className="rounded-[1.25rem] border border-[var(--platform-subpanel-border)] bg-white/70 p-3 sm:p-4">
<div className="mb-3 flex min-h-6 items-center justify-between gap-3">
<span className="text-xs font-black text-[var(--platform-text-soft)]">
Template
</span>
<span className="max-w-[11rem] truncate text-xs font-black text-[var(--platform-text-strong)]">
{PUZZLE_CREATION_TEMPLATES.find(
(item) => item.id === selectedTemplateId,
)?.title ?? PUZZLE_CREATION_TEMPLATES[0]?.title}
</span>
</div>
<div
className="flex gap-3 overflow-x-auto pb-2"
aria-label="拼图创作模板"
>
{PUZZLE_CREATION_TEMPLATES.map((template) => {
const selected = template.id === selectedTemplateId;
return (
<button
key={template.id}
type="button"
disabled={isBusy}
onClick={() => applyTemplatePrompt(template.id)}
className={`min-h-[10.2rem] w-[7.45rem] shrink-0 rounded-[1rem] border p-2 text-left transition ${
selected
? 'border-emerald-300 bg-emerald-50/86 shadow-[0_0_0_1px_rgba(16,185,129,0.18)]'
: 'border-[var(--platform-subpanel-border)] bg-white/82 hover:bg-white'
} ${isBusy ? 'cursor-not-allowed opacity-55' : ''}`}
aria-pressed={selected}
aria-label={`${template.title}模板`}
>
<span className="block aspect-square overflow-hidden rounded-[0.8rem] bg-[var(--platform-subpanel-fill)]">
<img
src={template.imageSrc}
alt=""
className="h-full w-full object-cover"
loading="lazy"
/>
</span>
<span className="mt-2 block min-h-8 overflow-hidden text-ellipsis text-xs font-black leading-4 text-[var(--platform-text-strong)]">
{template.title}
</span>
{selected ? (
<span className="mt-2 inline-flex max-w-full rounded-full bg-emerald-100 px-2 py-1 text-[10px] font-black text-emerald-700">
</span>
) : null}
</button>
);
})}
</div>
</div>
<div className="mt-4 space-y-4">
<label
className={`inline-flex min-h-10 cursor-pointer items-center gap-2 rounded-full border border-[var(--platform-subpanel-border)] bg-white/92 px-4 text-sm font-black text-[var(--platform-text-strong)] shadow-sm transition hover:bg-white ${isBusy ? 'cursor-not-allowed opacity-55' : ''}`}
title={formState.referenceImageSrc ? '更换参考图' : '添加参考图'}
>
<ImagePlus className="h-4 w-4" />
<span>
{formState.referenceImageSrc ? '更换参考图' : '上传参考图'}
</span>
<input
value={formState.workTitle}
type="file"
accept="image/png,image/jpeg,image/webp"
disabled={isBusy}
onChange={(event) =>
setFormState((current) => ({
...current,
workTitle: event.target.value,
}))
}
className="mt-2 w-full rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 py-3 text-base font-semibold text-[var(--platform-text-strong)] outline-none"
aria-label="作品名称"
onChange={(event) => {
void handleReferenceImageChange(event);
}}
className="hidden"
/>
</label>
<label className="block">
<span className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
</span>
<textarea
value={formState.workDescription}
disabled={isBusy}
rows={4}
onChange={(event) =>
setFormState((current) => ({
...current,
workDescription: event.target.value,
}))
}
className="mt-2 w-full resize-none rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 py-3 text-sm leading-6 text-[var(--platform-text-strong)] outline-none"
aria-label="作品描述"
/>
</label>
<label className="block">
<span className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
</span>
<div className="relative mt-2">
<span className="sr-only"></span>
<div className="relative">
<textarea
value={formState.pictureDescription}
disabled={isBusy}
rows={10}
placeholder="一只猫在雨夜灯牌下回头,霓虹反光清晰,街角有花店和小伞,适合切成拼图。"
onChange={(event) =>
setFormState((current) => ({
...current,
pictureDescription: event.target.value,
}))
}
className="w-full resize-none rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 py-3 pb-16 text-sm leading-6 text-[var(--platform-text-strong)] outline-none"
className="min-h-[18rem] w-full resize-none rounded-[1.35rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 py-4 pb-16 text-base leading-7 text-[var(--platform-text-strong)] outline-none placeholder:text-zinc-400 sm:min-h-[20rem]"
aria-label="画面描述"
/>
<PuzzleImageModelPicker
@@ -358,26 +397,6 @@ export function PuzzleAgentWorkspace({
}))
}
/>
<label
className={`absolute bottom-3 right-3 inline-flex h-10 w-10 cursor-pointer items-center justify-center rounded-full border border-amber-300/70 bg-white/96 text-amber-700 shadow-sm transition hover:bg-amber-50 ${isBusy ? 'cursor-not-allowed opacity-55' : ''}`}
title={
formState.referenceImageSrc ? '更换参考图' : '添加参考图'
}
>
<ImagePlus className="h-4 w-4" />
<span className="sr-only">
{formState.referenceImageSrc ? '更换参考图' : '添加参考图'}
</span>
<input
type="file"
accept="image/png,image/jpeg,image/webp"
disabled={isBusy}
onChange={(event) => {
void handleReferenceImageChange(event);
}}
className="hidden"
/>
</label>
</div>
</label>