"use client"; import { useMemo, useState } from "react"; import dynamic from "next/dynamic"; import { Clock3, Globe2, Share2, Sparkles, UserRound } from "lucide-react"; import { AppShell } from "@/components/AppShell"; import { CharacterGenerator } from "@/components/generation/CharacterGenerator"; import { WorldGenerator } from "@/components/generation/WorldGenerator"; import { Badge } from "@/components/ui/Badge"; import { Button } from "@/components/ui/Button"; import { Card } from "@/components/ui/Card"; import { EmptyState } from "@/components/ui/EmptyState"; import { PageHeader } from "@/components/ui/PageHeader"; import { SegmentedControl } from "@/components/ui/SegmentedControl"; import type { CharacterCardView, ProjectResponse, WorldEntityCardView, } from "@/lib/api/types"; import { mergeCharacterCards, worldEntityRules, } from "@/lib/generation/cards"; type CodexTab = "characters" | "world" | "timeline"; interface CodexPageProps { project: ProjectResponse; // 后端读端点(GET .../characters / .../world_entities)拉来的跨会话全量真源。 initialCharacters: CharacterCardView[]; initialWorldEntities: WorldEntityCardView[]; // 进页可经 ?gen=character|world 直接打开对应生成器(命令面板动作跳转)。 initialTab?: CodexTab; } const TABS: { key: CodexTab; label: string }[] = [ { key: "characters", label: "人物" }, { key: "world", label: "世界观" }, { key: "timeline", label: "时间线" }, ]; // 关系图谱(React Flow)只在客户端渲染:ssr:false 绕开 canvas/window 依赖, // 并把 React Flow 及其样式拆到独立 chunk,进人物 tab 才加载。 const RelationshipGraph = dynamic( () => import("@/components/characters/RelationshipGraph").then( (mod) => mod.RelationshipGraph, ), { ssr: false, loading: () => (
关系图谱加载中…
), }, ); // 设定库 Codex(UX §6.5):管理人物 / 世界观 / 时间线。 // 初始列表来自后端读端点(跨会话全量真源);本会话新入库的角色卡再合并补显, // 故刷新后不再为空、且不与真源重复(按 name 去重,见 mergeCharacterCards)。 // 世界观本期无入库端点(仅生成预览)→ 展示真源 + 生成器预览。时间线为 P2/派生,暂占位。 export function CodexPage({ project, initialCharacters, initialWorldEntities, initialTab = "characters", }: CodexPageProps) { const [tab, setTab] = useState(initialTab); // 本会话内新入库的角色(即时回显,无需刷新即见)。 const [sessionCards, setSessionCards] = useState([]); // 真源(初始全量)+ 本会话新卡,按 name 去重合并。 const characters = useMemo( () => mergeCharacterCards(initialCharacters, sessionCards), [initialCharacters, sessionCards], ); return (
({ value: t.key, label: t.label }))} />
{tab === "characters" ? (

已入库人物({characters.length})

{characters.length > 0 ? (
    {characters.map((c, i) => (
  • {c.name}

    {c.role}

    {c.relations && c.relations.length > 0 ? (
      {c.relations.map((r, j) => (
    • {r.kind} · {r.name}
    • ))}
    ) : null}
  • ))}
) : ( document .getElementById("character-generator") ?.scrollIntoView({ block: "center" }) } variant="primary" size="sm" >
{characters.length > 0 ? (

) : null}
setSessionCards((prev) => [...prev, ...cards]) } />
) : null} {tab === "world" ? (

已入库世界观({initialWorldEntities.length})

{initialWorldEntities.length > 0 ? (
{initialWorldEntities.map((entity, i) => (
{worldEntityRules(entity).length > 0 ? (
    {worldEntityRules(entity).map((rule, j) => (
  • {rule}
  • ))}
) : (

(无显式硬规则)

)}
))}
) : ( document .getElementById("world-generator") ?.scrollIntoView({ block: "center" }) } variant="primary" size="sm" >
) : null} {tab === "timeline" ? ( ) : null}
); }