1
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user