"use client"; import { useState } from "react"; import { suggestForeshadowCode } from "@/lib/foreshadow/board"; import { useForeshadow } from "@/lib/foreshadow/useForeshadow"; import type { ForeshadowSuggestion } from "@/lib/review/sse"; interface ForeshadowSuggestionsProps { suggestions: ForeshadowSuggestion[]; // 审项是否未完成(incomplete)。 incomplete: boolean; projectId: string; chapterNo: number; } interface RegisterFormState { code: string; title: string; plantedAt: number; content: string; } // 伏笔建议区(UX §6.4 ②):planted「新埋待确认」/ resolved「疑似回收」。 // 四审只读(不变量#3),但作者可在此**显式确认**:新埋→登记(POST),疑似回收→标记回收(PATCH→CLOSED)。 // 这是作者确认动作,非 AI 静默写库;复用伏笔看板端点(lib/foreshadow)。 export function ForeshadowSuggestions({ suggestions, incomplete, projectId, chapterNo, }: ForeshadowSuggestionsProps) { const fs = useForeshadow([]); const [openIdx, setOpenIdx] = useState(null); const [form, setForm] = useState(null); const [registered, setRegistered] = useState>(new Set()); const [resolved, setResolved] = useState>(new Set()); const [pending, setPending] = useState>(new Set()); const openRegister = (i: number, s: ForeshadowSuggestion): void => { setOpenIdx(i); setForm({ code: suggestForeshadowCode(s.code, i), title: s.title, plantedAt: chapterNo, content: s.note ?? s.where ?? "", }); }; const closeForm = (): void => { setOpenIdx(null); setForm(null); }; const markPending = (i: number, on: boolean): void => setPending((prev) => { const next = new Set(prev); if (on) next.add(i); else next.delete(i); return next; }); const submitRegister = async (i: number): Promise => { if (!form || !form.code.trim() || !form.title.trim()) return; markPending(i, true); const ok = await fs.register({ projectId, code: form.code, title: form.title, plantedAt: form.plantedAt, content: form.content, }); markPending(i, false); if (ok) { setRegistered((prev) => new Set(prev).add(i)); closeForm(); } }; const markResolved = async (i: number, code: string): Promise => { markPending(i, true); const ok = await fs.transition(code, { projectId, toStatus: "CLOSED" }); markPending(i, false); if (ok) setResolved((prev) => new Set(prev).add(i)); }; return (

伏笔 (foreshadow-analyst)

{incomplete ? (

◌ 未完成

) : suggestions.length === 0 ? (

无伏笔建议。

) : (
    {suggestions.map((s, i) => { const code = s.code; const isResolved = s.kind === "resolved"; const done = isResolved ? resolved.has(i) : registered.has(i); const busy = pending.has(i); return (
  • {isResolved ? "⚑ 疑似回收" : "+ 新埋待确认"} {code ? ( {code} ) : null}

    {s.title}

    {s.where ? ( {s.where} ) : null} {s.note ?

    {s.note}

    : null} {/* 作者确认动作区 */} {done ? (

    ✓ {isResolved ? "已标记回收" : "已登记"}

    ) : isResolved ? ( code ? ( ) : (

    未关联编码,去伏笔看板处理。

    ) ) : openIdx === i && form ? ( void submitRegister(i)} onCancel={closeForm} /> ) : ( )}
  • ); })}
)}
); } interface RegisterFormInlineProps { form: RegisterFormState; busy: boolean; onChange: (form: RegisterFormState) => void; onSubmit: () => void; onCancel: () => void; } // 新埋伏笔登记表单:预填标题/埋设章/线索,code 给建议值可改(code/title 必填)。 function RegisterFormInline({ form, busy, onChange, onSubmit, onCancel, }: RegisterFormInlineProps) { const canSubmit = !busy && form.code.trim().length > 0 && form.title.trim().length > 0; const field = "w-full rounded border border-line bg-bg px-2 py-1 text-xs text-ink focus:border-cinnabar focus:outline-none"; return (
onChange({ ...form, code: e.target.value })} className={field} />
onChange({ ...form, title: e.target.value })} className={field} />
onChange({ ...form, plantedAt: Number(e.target.value) || 1 }) } className={field} />