"use client"; import { ArrowLeft, ArrowRight, Check, CheckCircle2, Sparkles } from "lucide-react"; import { useRouter } from "next/navigation"; import { useEffect, useRef, useState } from "react"; import { ThinkingIndicator } from "@/components/ThinkingIndicator"; import { useToast } from "@/components/Toast"; import { Badge } from "@/components/ui/Badge"; import { Button } from "@/components/ui/Button"; import { Field } from "@/components/ui/Field"; import { SegmentedControl } from "@/components/ui/SegmentedControl"; import { Select } from "@/components/ui/Select"; import { TextArea } from "@/components/ui/TextArea"; import { TextInput } from "@/components/ui/TextInput"; import { api } from "@/lib/api/client"; import { buttonClass } from "@/lib/ui/variants"; import type { ProjectPlanView } from "@/lib/api/types"; import { useProjectPlan } from "@/lib/wizard/useProjectPlan"; import { ENDING_TYPES, GENRES, NARRATIVE_POVS, SELLING_POINT_PRESETS, STEP_TITLES, STRUCTURES, TONES, WIZARD_STEPS, applyPlanPatch, canAdvance, canGeneratePlan, canSubmit, clampStep, emptyWizardForm, mapPlanResultToWizardForm, toCreateRequest, toPlanSeedRequest, type WizardForm, } from "@/lib/wizard/wizard"; // 立项向导(UX §6.2)。Client Component:收集字段 → POST /projects → 进工作台。 export function ProjectWizard() { const router = useRouter(); const toast = useToast(); const [step, setStep] = useState(1); const [form, setForm] = useState(emptyWizardForm); const [submitting, setSubmitting] = useState(false); // 换步后把焦点移到新步标题,方便键盘/读屏用户感知步骤变化;首帧不抢第 1 步输入框的 autoFocus。 const stepHeadingRef = useRef(null); const isFirstRender = useRef(true); useEffect(() => { if (isFirstRender.current) { isFirstRender.current = false; return; } stepHeadingRef.current?.focus(); }, [step]); const update = (patch: Partial): void => setForm((prev) => ({ ...prev, ...patch })); const toggleSellingPoint = (point: string): void => setForm((prev) => ({ ...prev, sellingPoints: prev.sellingPoints.includes(point) ? prev.sellingPoints.filter((p) => p !== point) : [...prev.sellingPoints, point], })); // AI 立项方案按字段回填:只填当前为空的字段(保护作者已编辑内容,不 bulk 覆盖)。 const applyPlan = (plan: ProjectPlanView): void => { const patch = mapPlanResultToWizardForm(plan); const filled = (Object.keys(patch) as Array).filter( (k) => typeof form[k] === "string" && (form[k] as string).trim() === "", ); setForm((prev) => applyPlanPatch(prev, patch)); toast( filled.length > 0 ? `已回填 ${filled.length} 个空字段(已编辑内容不覆盖)` : "向导相关字段均已填写,未覆盖任何内容", filled.length > 0 ? "success" : "info", ); }; const goBack = (): void => setStep((s) => clampStep(s - 1)); const goNext = (): void => setStep((s) => clampStep(s + 1)); const submit = async (): Promise => { if (!canSubmit(form)) { toast("请先填写书名", "error"); setStep(1); return; } setSubmitting(true); const { data, error } = await api.POST("/projects", { body: toCreateRequest(form), }); if (error || !data) { setSubmitting(false); toast("创建作品失败,请重试", "error"); return; } router.push(`/projects/${data.id}/write`); }; const isLast = step === WIZARD_STEPS; return (

新建作品

步骤 {step}/{WIZARD_STEPS}

{STEP_TITLES[step - 1]}

{step === 1 && } {step === 2 && ( )} {step === 3 && } {step === 4 && } {step === 5 && }
{isLast ? ( ) : ( )}
); } function StepDots({ current }: { current: number }) { return ( ); } interface StepProps { form: WizardForm; update: (patch: Partial) => void; } function StepBasics({ form, update }: StepProps) { const titleMissing = form.title.trim().length === 0; return (
update({ title: e.target.value })} placeholder="例:逐光而行" autoFocus />
); } // AI 立项方案助手(灵感⑤):种子(题材+logline)就绪后一键生成结构化方案 → 预览 → 按字段回填。 function PlanAssistant({ form, onApplyPlan, }: { form: WizardForm; onApplyPlan: (plan: ProjectPlanView) => void; }) { const { status, plan, generate } = useProjectPlan(); const ready = canGeneratePlan(form); const generating = status === "generating"; return (

{ready ? "根据题材 + 一句话故事推演书级蓝图,按字段回填(不覆盖已填内容)。" : "填好「题材」(上一步)与「一句话故事」后即可生成。"}

{status === "done" && plan && (
{plan.title_candidates && plan.title_candidates.length > 0 && ( )}
)}
); } function PlanRow({ label, value }: { label: string; value: string }) { if (!value.trim()) return null; return (
{label}
{value}
); } function StepStory({ form, update, toggleSellingPoint, onApplyPlan, }: StepProps & { toggleSellingPoint: (p: string) => void; onApplyPlan: (plan: ProjectPlanView) => void; }) { return (