Просмотр исходного кода

fix: review center negative chapter numbers, memory center snapshot display, and range search

- fact-snapshot.ts: chapterLabel() for negative chapter numbers in evidence text

- memory-center.ts: separate chapter/outline snapshots in sorting, chapters first

- memory-center-view.tsx: negative chapter numbers show title; add range search for snapshots
Mochocyang 2 месяцев назад
Родитель
Сommit
7171d7d8e8

+ 76 - 12
src/components/novel/memory-center-view.tsx

@@ -1,4 +1,4 @@
-import { Suspense, lazy, useCallback, useEffect, useRef, useState } from "react"
+import { Suspense, lazy, useCallback, useEffect, useMemo, useRef, useState } from "react"
 import { useTranslation } from "react-i18next"
 import {
   AlertTriangle,
@@ -6,6 +6,7 @@ import {
   Pencil,
   RefreshCw,
   Save,
+  Search,
   Trash2,
   X,
 } from "lucide-react"
@@ -85,7 +86,9 @@ function SnapshotCard({
   onDelete: (chapterNumber: number, title: string) => void
   t: (key: string, opts?: Record<string, unknown>) => string
 }) {
-  const title = card.chapterTitle || t("novel.memoryCenter.snapshots.chapter", { chapter: card.chapterNumber })
+  const title = card.chapterNumber < 0
+    ? (card.chapterTitle || `大纲快照(${card.chapterNumber})`)
+    : (card.chapterTitle || t("novel.memoryCenter.snapshots.chapter", { chapter: card.chapterNumber }))
   return (
     <div className="rounded-md border p-3">
       <div className="flex items-center justify-between gap-3">
@@ -518,20 +521,81 @@ function MemoryCenterDetailPanel({
   onDeleteMarkdown: () => void
   t: (key: string, opts?: Record<string, unknown>) => string
 }) {
+  const [rangeStart, setRangeStart] = useState("")
+  const [rangeEnd, setRangeEnd] = useState("")
+
   if (detailView.kind === "snapshotList") {
+    const chapterCards = detailView.cards.filter((c) => c.chapterNumber > 0)
+    const maxChapter = chapterCards.length > 0 ? chapterCards[0].chapterNumber : 0
+
+    const filteredCards = useMemo(() => {
+      const start = rangeStart.trim() ? Number(rangeStart.trim()) : 0
+      const end = rangeEnd.trim() ? Number(rangeEnd.trim()) : 0
+      if (start > 0 && end > 0 && start <= end) {
+        return detailView.cards.filter((c) => c.chapterNumber >= start && c.chapterNumber <= end)
+      }
+      if (start > 0) {
+        return detailView.cards.filter((c) => c.chapterNumber >= start)
+      }
+      if (end > 0) {
+        return detailView.cards.filter((c) => c.chapterNumber <= end)
+      }
+      return detailView.cards
+    }, [detailView.cards, rangeStart, rangeEnd])
+
     return (
       <div className="space-y-3">
-        {detailView.cards.map((card) => (
-          <SnapshotCard
-            key={card.chapterNumber}
-            card={card}
-            buttonId={`memory-center-detail-snapshot-${card.chapterNumber}`}
-            onOpen={onOpenSnapshot}
-            onEdit={onEditSnapshot}
-            onDelete={onDeleteSnapshot}
-            t={t}
+        <div className="flex items-center gap-2 rounded-md border bg-muted/30 px-3 py-2">
+          <Search className="h-4 w-4 shrink-0 text-muted-foreground" />
+          <input
+            type="number"
+            min={1}
+            max={maxChapter}
+            placeholder="起始章"
+            value={rangeStart}
+            onChange={(e) => setRangeStart(e.target.value)}
+            className="h-7 w-20 rounded border bg-background px-2 text-xs text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring"
           />
-        ))}
+          <span className="text-xs text-muted-foreground">—</span>
+          <input
+            type="number"
+            min={1}
+            max={maxChapter}
+            placeholder="结束章"
+            value={rangeEnd}
+            onChange={(e) => setRangeEnd(e.target.value)}
+            className="h-7 w-20 rounded border bg-background px-2 text-xs text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring"
+          />
+          {(rangeStart || rangeEnd) ? (
+            <button
+              type="button"
+              onClick={() => { setRangeStart(""); setRangeEnd("") }}
+              className="shrink-0 text-xs text-muted-foreground hover:text-foreground"
+            >
+              <X className="h-3.5 w-3.5" />
+            </button>
+          ) : null}
+          <span className="ml-auto text-xs text-muted-foreground">
+            {filteredCards.length !== detailView.cards.length
+              ? `${filteredCards.length}/${detailView.cards.length}`
+              : `共${detailView.cards.length}条`}
+          </span>
+        </div>
+        {filteredCards.length === 0 ? (
+          <p className="py-8 text-center text-sm text-muted-foreground">该区间内暂无章节快照</p>
+        ) : (
+          filteredCards.map((card) => (
+            <SnapshotCard
+              key={card.chapterNumber}
+              card={card}
+              buttonId={`memory-center-detail-snapshot-${card.chapterNumber}`}
+              onOpen={onOpenSnapshot}
+              onEdit={onEditSnapshot}
+              onDelete={onDeleteSnapshot}
+              t={t}
+            />
+          ))
+        )}
       </div>
     )
   }

+ 24 - 16
src/lib/novel/fact-snapshot.ts

@@ -1,5 +1,13 @@
 import type { ChapterSnapshot } from "./chapter-ingest"
 
+/** 格式化章节标签:负数章节号(大纲快照)使用章节标题,正数使用"第N章" */
+function chapterLabel(snapshot: ChapterSnapshot): string {
+  if (snapshot.chapterNumber < 0) {
+    return snapshot.chapterTitle || `大纲快照(${snapshot.chapterNumber})`
+  }
+  return `第${snapshot.chapterNumber}章`
+}
+
 export interface FactCheckResult {
   severity: "blocking" | "high" | "medium" | "low"
   type: "character_jump" | "location_conflict" | "item_holder_change"
@@ -92,11 +100,11 @@ function checkCharacterJump(
           severity: "blocking",
           type: "character_jump",
           message: `角色"${name}"状态从"${prevState}"跳变到"${currState}",但中间缺少受伤或治疗事件`,
-          evidenceA: `第${prev.chapterNumber}章:${name}=${prevState}`,
-          evidenceB: `第${curr.chapterNumber}章:${name}=${currState}`,
+          evidenceA: `${chapterLabel(prev)}:${name}=${prevState}`,
+          evidenceB: `${chapterLabel(curr)}:${name}=${currState}`,
           chapters: [prev.chapterNumber, curr.chapterNumber],
           confidence: 1,
-          suggestion: `请在第${prev.chapterNumber}章到第${curr.chapterNumber}章之间补充状态变化事件,或修正角色状态。`,
+          suggestion: `请在${chapterLabel(prev)}到${chapterLabel(curr)}之间补充状态变化事件,或修正角色状态。`,
         })
       }
       continue
@@ -107,8 +115,8 @@ function checkCharacterJump(
         severity: "medium",
         type: "character_jump",
         message: `角色"${name}"状态从"${prevState}"变为"${currState}",需要确认是否有对应事件支撑`,
-        evidenceA: `第${prev.chapterNumber}章:${name}=${prevState}`,
-        evidenceB: `第${curr.chapterNumber}章:${name}=${currState}`,
+        evidenceA: `${chapterLabel(prev)}:${name}=${prevState}`,
+        evidenceB: `${chapterLabel(curr)}:${name}=${currState}`,
         chapters: [prev.chapterNumber, curr.chapterNumber],
         confidence: 0.7,
         suggestion: "请确认该状态变化是否合理,若合理可忽略。",
@@ -156,8 +164,8 @@ function checkItemHolderChange(
         severity: "medium",
         type: "item_holder_change",
         message: `物品"${itemName}"的持有者从"${prevHolder}"变为"${currHolder}",但缺少转移事件`,
-        evidenceA: `第${prev.chapterNumber}章:${itemName}由${prevHolder}持有`,
-        evidenceB: `第${curr.chapterNumber}章:${itemName}由${currHolder}持有`,
+        evidenceA: `${chapterLabel(prev)}:${itemName}由${prevHolder}持有`,
+        evidenceB: `${chapterLabel(curr)}:${itemName}由${currHolder}持有`,
         chapters: [prev.chapterNumber, curr.chapterNumber],
         confidence: 0.8,
         suggestion: `请补充"${itemName}"如何从${prevHolder}转移到${currHolder},或修正持有者信息。`,
@@ -202,8 +210,8 @@ function checkOrgFlip(
         severity: "medium",
         type: "org_flip",
         message: `组织"${orgName}"的领导者从"${prevLeader}"变为"${currLeader}",但缺少权力变更事件`,
-        evidenceA: `第${prev.chapterNumber}章:${orgName}由${prevLeader}领导`,
-        evidenceB: `第${curr.chapterNumber}章:${orgName}由${currLeader}领导`,
+        evidenceA: `${chapterLabel(prev)}:${orgName}由${prevLeader}领导`,
+        evidenceB: `${chapterLabel(curr)}:${orgName}由${currLeader}领导`,
         chapters: [prev.chapterNumber, curr.chapterNumber],
         confidence: 0.8,
         suggestion: `请补充"${orgName}"权力变更的原因,或修正领导者信息。`,
@@ -242,8 +250,8 @@ function checkTimelineConflict(
           severity: "high",
           type: "timeline_conflict",
           message: `时间线冲突:同一时间点"${prevTime}"出现互相矛盾的事件`,
-          evidenceA: `第${prev.chapterNumber}章:${prevEvent}`,
-          evidenceB: `第${curr.chapterNumber}章:${currEvent}`,
+          evidenceA: `${chapterLabel(prev)}:${prevEvent}`,
+          evidenceB: `${chapterLabel(curr)}:${currEvent}`,
           chapters: [prev.chapterNumber, curr.chapterNumber],
           confidence: 0.9,
           suggestion: `请统一"${prevTime}"这个时间点发生的事件。`,
@@ -294,8 +302,8 @@ function checkSettingConflict(
           severity: "high",
           type: "setting_conflict",
           message: `设定矛盾:关于"${prevSubject}"的描述前后不一致`,
-          evidenceA: `第${prev.chapterNumber}章:${prevFact}`,
-          evidenceB: `第${curr.chapterNumber}章:${currFact}`,
+          evidenceA: `${chapterLabel(prev)}:${prevFact}`,
+          evidenceB: `${chapterLabel(curr)}:${currFact}`,
           chapters: [prev.chapterNumber, curr.chapterNumber],
           confidence: 0.85,
           suggestion: `请统一"${prevSubject}"的设定描述。`,
@@ -362,8 +370,8 @@ function checkRelationshipReversal(
           severity: "medium",
           type: "relationship_reversal",
           message: `关系反转:${pairKey.replace("->", "与")}从"${prevStatus}"变为"${currStatus}",但缺少过渡事件`,
-          evidenceA: `第${prev.chapterNumber}章:${pairKey.replace("->", "与")}关系=${prevStatus}`,
-          evidenceB: `第${curr.chapterNumber}章:${pairKey.replace("->", "与")}关系=${currStatus}`,
+          evidenceA: `${chapterLabel(prev)}:${pairKey.replace("->", "与")}关系=${prevStatus}`,
+          evidenceB: `${chapterLabel(curr)}:${pairKey.replace("->", "与")}关系=${currStatus}`,
           chapters: [prev.chapterNumber, curr.chapterNumber],
           confidence: 0.75,
           suggestion: "请补充关系变化的原因,或修正关系状态。",
@@ -413,7 +421,7 @@ function checkCausalityBreak(
       severity: "low",
       type: "causality_break",
       message: `事件"${eventName}"引用了可能不存在的前置事件:${detail.cause}`,
-      evidenceA: `第${curr.chapterNumber}章:该事件依赖第${causeChapter}章内容`,
+      evidenceA: `${chapterLabel(curr)}:该事件依赖第${causeChapter}章内容`,
       evidenceB: "当前快照中未找到匹配的前置事件",
       chapters: [causeChapter, curr.chapterNumber],
       confidence: 0.5,

+ 7 - 2
src/lib/novel/memory-center.ts

@@ -209,8 +209,13 @@ export function buildMemoryCenterSnapshotCards(
   limit = 6,
   maxItemsPerList = 3,
 ): MemoryCenterSnapshotCard[] {
-  return [...snapshots]
-    .sort((left, right) => right.chapterNumber - left.chapterNumber)
+  // 分离章节快照(正数)和大纲快照(负数),章节快照降序排列在前
+  const chapterSnapshots = snapshots.filter((s) => s.chapterNumber > 0)
+  const outlineSnapshots = snapshots.filter((s) => s.chapterNumber < 0)
+  chapterSnapshots.sort((a, b) => b.chapterNumber - a.chapterNumber)
+  outlineSnapshots.sort((a, b) => b.chapterNumber - a.chapterNumber)
+  const sorted = [...chapterSnapshots, ...outlineSnapshots]
+  return sorted
     .slice(0, limit)
     .map((snapshot) => {
       const characterStateChanges = trimList(snapshot.characterStateChanges, maxItemsPerList)