import { ArrowLeft, ImagePlus, Loader2, Sparkles, X } from 'lucide-react'; import { type ChangeEvent, useEffect, useMemo, useRef, useState } from 'react'; import type { PuzzleAgentActionRequest } from '../../../packages/shared/src/contracts/puzzleAgentActions'; import type { CreatePuzzleAgentSessionRequest, PuzzleAgentSessionSnapshot, SendPuzzleAgentMessageRequest, } from '../../../packages/shared/src/contracts/puzzleAgentSession'; import { readPuzzleReferenceImageAsDataUrl } from '../../services/puzzleReferenceImage'; import { normalizePuzzleImageModel, PUZZLE_IMAGE_MODEL_GPT_IMAGE_2, type PuzzleImageModelId, } from './puzzleImageModelOptions'; import { PuzzleImageModelPicker } from './PuzzleImageModelPicker'; type PuzzleAgentWorkspaceProps = { session: PuzzleAgentSessionSnapshot | null; isBusy?: boolean; error?: string | null; onBack: () => void; onSubmitMessage: (payload: SendPuzzleAgentMessageRequest) => void; onExecuteAction: (payload: PuzzleAgentActionRequest) => void; onCreateFromForm?: (payload: CreatePuzzleAgentSessionRequest) => void; onAutoSaveForm?: (payload: CreatePuzzleAgentSessionRequest) => void; initialFormPayload?: CreatePuzzleAgentSessionRequest | null; }; type PuzzleFormState = { workTitle: string; workDescription: string; pictureDescription: string; referenceImageSrc: string; referenceImageLabel: string; imageModel: PuzzleImageModelId; }; const EMPTY_FORM_STATE: PuzzleFormState = { workTitle: '', workDescription: '', pictureDescription: '', referenceImageSrc: '', referenceImageLabel: '', imageModel: PUZZLE_IMAGE_MODEL_GPT_IMAGE_2, }; function resolveInitialFormState( session: PuzzleAgentSessionSnapshot | null, initialFormPayload: CreatePuzzleAgentSessionRequest | null = null, ): PuzzleFormState { const formDraft = session?.draft?.formDraft; if (formDraft) { return { workTitle: formDraft.workTitle ?? '', workDescription: formDraft.workDescription ?? '', pictureDescription: formDraft.pictureDescription ?? '', referenceImageSrc: initialFormPayload?.referenceImageSrc ?? '', referenceImageLabel: initialFormPayload?.referenceImageSrc ? '已选择参考图' : '', imageModel: normalizePuzzleImageModel(initialFormPayload?.imageModel), }; } if (initialFormPayload) { return { workTitle: initialFormPayload.workTitle ?? initialFormPayload.seedText ?? '', workDescription: initialFormPayload.workDescription ?? '', pictureDescription: initialFormPayload.pictureDescription ?? '', referenceImageSrc: initialFormPayload.referenceImageSrc ?? '', referenceImageLabel: initialFormPayload.referenceImageSrc ? '已选择参考图' : '', imageModel: normalizePuzzleImageModel(initialFormPayload.imageModel), }; } if (!session) { return EMPTY_FORM_STATE; } 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 || '', referenceImageSrc: '', referenceImageLabel: '', imageModel: PUZZLE_IMAGE_MODEL_GPT_IMAGE_2, }; } /** * 拼图创作入口已从 Agent 对话改为填表式。 * 组件名保留为 PuzzleAgentWorkspace 以兼容现有路由与草稿恢复入口。 */ export function PuzzleAgentWorkspace({ session, isBusy = false, error = null, onBack, onExecuteAction, onCreateFromForm, onAutoSaveForm, initialFormPayload = null, }: PuzzleAgentWorkspaceProps) { const [formState, setFormState] = useState(() => resolveInitialFormState(session, initialFormPayload), ); const [referenceImageError, setReferenceImageError] = useState( null, ); const previousSessionIdRef = useRef( session?.sessionId ?? null, ); const appliedInitialFormKeyRef = useRef(null); useEffect(() => { const currentSessionId = session?.sessionId ?? null; if ( currentSessionId && previousSessionIdRef.current === null && appliedInitialFormKeyRef.current === JSON.stringify(initialFormPayload ?? null) ) { previousSessionIdRef.current = currentSessionId; return; } previousSessionIdRef.current = currentSessionId; const nextInitialFormKey = currentSessionId ?? JSON.stringify(initialFormPayload ?? null); if (appliedInitialFormKeyRef.current === nextInitialFormKey) { return; } appliedInitialFormKeyRef.current = nextInitialFormKey; setFormState(resolveInitialFormState(session, initialFormPayload)); setReferenceImageError(null); }, [initialFormPayload, session?.sessionId]); const workTitle = formState.workTitle.trim(); const workDescription = formState.workDescription.trim(); const pictureDescription = formState.pictureDescription.trim(); const canSubmit = Boolean(workTitle && workDescription && pictureDescription) && !isBusy; const autosavePayload = useMemo( () => ({ seedText: workTitle, workTitle, workDescription, pictureDescription, referenceImageSrc: formState.referenceImageSrc || null, imageModel: formState.imageModel, }), [ formState.referenceImageSrc, formState.imageModel, pictureDescription, workDescription, workTitle, ], ); const autosaveSignature = JSON.stringify([ autosavePayload.workTitle, autosavePayload.workDescription, autosavePayload.pictureDescription, autosavePayload.imageModel, ]); const lastAutosaveSignatureRef = useRef(autosaveSignature); const autosaveSessionIdRef = useRef(session?.sessionId ?? null); useEffect(() => { const currentSessionId = session?.sessionId ?? null; if (autosaveSessionIdRef.current === currentSessionId) { return; } autosaveSessionIdRef.current = currentSessionId; lastAutosaveSignatureRef.current = autosaveSignature; }, [autosaveSignature, session?.sessionId]); useEffect(() => { if ( !session || session.stage !== 'collecting_anchors' || !session.draft?.formDraft || !onAutoSaveForm || lastAutosaveSignatureRef.current === autosaveSignature ) { return; } const timer = window.setTimeout(() => { lastAutosaveSignatureRef.current = autosaveSignature; onAutoSaveForm(autosavePayload); }, 700); return () => window.clearTimeout(timer); }, [ autosavePayload, autosaveSignature, onAutoSaveForm, session?.draft?.formDraft, session?.stage, session?.sessionId, ]); const handleReferenceImageChange = async ( event: ChangeEvent, ) => { const file = event.target.files?.[0]; event.currentTarget.value = ''; if (!file) { return; } try { const dataUrl = await readPuzzleReferenceImageAsDataUrl(file); setFormState((current) => ({ ...current, referenceImageSrc: dataUrl, referenceImageLabel: file.name.trim() || '本地参考图', })); setReferenceImageError(null); } catch (uploadError) { setReferenceImageError( uploadError instanceof Error ? uploadError.message : '参考图读取失败,请重试。', ); } }; const submitForm = () => { if (!canSubmit) { return; } const payload = { seedText: workTitle, workTitle, workDescription, pictureDescription, referenceImageSrc: formState.referenceImageSrc || null, imageModel: formState.imageModel, }; if (!session && onCreateFromForm) { onCreateFromForm(payload); return; } onExecuteAction({ action: 'compile_puzzle_draft', promptText: pictureDescription, workTitle, workDescription, pictureDescription, referenceImageSrc: formState.referenceImageSrc || null, imageModel: formState.imageModel, candidateCount: 1, }); }; return (