Files
Genarrative/src/components/unified-creation/workspaces/JumpHopCreationWorkspace.tsx

153 lines
4.6 KiB
TypeScript

import { ArrowLeft, Loader2, Send } from 'lucide-react';
import { useMemo, useState } from 'react';
import type {
JumpHopSessionResponse,
JumpHopWorkspaceCreateRequest,
} from '../../../../packages/shared/src/contracts/jumpHop';
import { jumpHopClient } from '../../../services/jump-hop/jumpHopClient';
type JumpHopCreationWorkspaceProps = {
isBusy?: boolean;
error?: string | null;
onBack: () => void;
onSubmitted: (
result: JumpHopSessionResponse,
payload: JumpHopWorkspaceCreateRequest,
) => void;
showBackButton?: boolean;
unifiedChrome?: boolean;
};
type JumpHopWorkspaceFormState = {
themeText: string;
};
const DEFAULT_FORM_STATE: JumpHopWorkspaceFormState = {
themeText: '',
};
function buildJumpHopWorkspacePayload(
formState: JumpHopWorkspaceFormState,
): JumpHopWorkspaceCreateRequest {
const themeText = formState.themeText.trim();
return {
templateId: 'jump-hop',
themeText,
workTitle: `${themeText}跳一跳`,
workDescription: `${themeText}主题的俯视角跳跃作品`,
themeTags: [themeText, '跳一跳', '休闲'],
difficulty: 'standard',
stylePreset: 'minimal-blocks',
characterPrompt: '内置默认 3D 角色',
tilePrompt: `${themeText}主题的3D立方体主题身份方块包装图集`,
endMoodPrompt: null,
};
}
export function JumpHopCreationWorkspace({
isBusy = false,
error = null,
onBack,
onSubmitted,
showBackButton = true,
unifiedChrome = false,
}: JumpHopCreationWorkspaceProps) {
const [formState, setFormState] = useState(DEFAULT_FORM_STATE);
const [localError, setLocalError] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const canSubmit = useMemo(
() => Boolean(formState.themeText.trim()),
[formState],
);
const handleSubmit = async () => {
if (!canSubmit || isSubmitting || isBusy) {
setLocalError('请先补全输入。');
return;
}
setIsSubmitting(true);
setLocalError(null);
try {
const payload = buildJumpHopWorkspacePayload(formState);
const response = await jumpHopClient.createSession(payload);
onSubmitted(response, payload);
} catch (caughtError) {
setLocalError(
caughtError instanceof Error ? caughtError.message : '创建草稿失败。',
);
} finally {
setIsSubmitting(false);
}
};
return (
<div
className={
unifiedChrome
? 'jump-hop-workspace mx-auto flex min-h-0 w-full max-w-none flex-col overflow-visible'
: 'jump-hop-workspace platform-remap-surface mx-auto flex h-full min-h-0 w-full max-w-3xl flex-col px-3 pb-3 pt-3 sm:px-4 sm:pt-4'
}
data-unified-chrome={unifiedChrome ? 'true' : 'false'}
>
{showBackButton ? (
<div className="mb-3 flex items-center justify-between gap-3">
<button
type="button"
onClick={onBack}
className="platform-button platform-button--ghost min-h-0 px-3 py-2 text-sm"
>
<ArrowLeft className="h-4 w-4" />
</button>
</div>
) : null}
<div className="grid gap-3">
<label className="block sm:col-span-2">
<span className="text-xs font-bold tracking-[0.18em] text-[var(--platform-text-soft)]">
</span>
<input
value={formState.themeText}
onChange={(event) =>
setFormState((current) => ({
...current,
themeText: event.target.value,
}))
}
className="mt-2 w-full rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-3 py-3 text-sm font-semibold text-[var(--platform-text-strong)] outline-none"
/>
</label>
</div>
{localError || error ? (
<div className="platform-banner platform-banner--danger mt-3 rounded-2xl text-sm leading-6">
{localError ?? error}
</div>
) : null}
<div className="mt-auto flex justify-end gap-2 pb-[max(0.25rem,env(safe-area-inset-bottom))] pt-3">
<button
type="button"
onClick={handleSubmit}
disabled={!canSubmit || isSubmitting || isBusy}
className={`platform-button platform-button--primary min-h-11 justify-center gap-2 px-5 py-3 ${!canSubmit || isSubmitting || isBusy ? 'cursor-not-allowed opacity-55' : ''}`}
>
{isSubmitting ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Send className="h-4 w-4" />
)}
</button>
</div>
</div>
);
}
export default JumpHopCreationWorkspace;