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

feat(story-simulation): 实现推演报告和故事草稿展示 UI

Mochocyang 3 месяцев назад
Родитель
Сommit
934619df5f

+ 185 - 0
src/components/novel/story-simulation/simulation-report-view.tsx

@@ -0,0 +1,185 @@
+import { useTranslation } from "react-i18next"
+import { RefreshCw, Sparkles, TrendingUp } from "lucide-react"
+import { Button } from "@/components/ui/button"
+import { useStorySimulationStore } from "@/stores/story-simulation-store"
+import type { StoryBranch } from "@/lib/novel/story-simulation/types"
+
+const PROBABILITY_COLORS: Record<string, string> = {
+  high: "bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300",
+  medium: "bg-amber-100 text-amber-700 dark:bg-amber-900 dark:text-amber-300",
+  low: "bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300",
+}
+
+interface SimulationReportViewProps {
+  onResimulate: () => void
+  onGenerateDraft: (branch: StoryBranch) => void
+}
+
+export function SimulationReportView({
+  onResimulate,
+  onGenerateDraft,
+}: SimulationReportViewProps) {
+  const { t } = useTranslation()
+  const report = useStorySimulationStore((s) => s.currentReport)
+
+  if (!report) return null
+
+  return (
+    <div className="flex h-full flex-col">
+      <div className="flex items-center justify-between border-b px-4 py-3">
+        <div className="flex items-center gap-2">
+          <TrendingUp className="h-4 w-4 text-primary" />
+          <h2 className="text-sm font-semibold">{t("storySimulation.reportTitle")}</h2>
+        </div>
+        <Button variant="outline" size="sm" onClick={onResimulate}>
+          <RefreshCw className="h-3.5 w-3.5" />
+          {t("storySimulation.resimulate")}
+        </Button>
+      </div>
+
+      <div className="flex-1 overflow-y-auto p-4">
+        <div className="mx-auto max-w-3xl space-y-6">
+          {/* 角色行为分析 */}
+          {report.characterAnalyses.length > 0 && (
+            <section>
+              <h3 className="mb-3 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
+                {t("storySimulation.characterAnalysis")}
+              </h3>
+              <div className="space-y-3">
+                {report.characterAnalyses.map((char) => (
+                  <div key={char.characterId} className="rounded-lg border p-3">
+                    <div className="flex items-center justify-between">
+                      <span className="font-medium">{char.name}</span>
+                      <span className="rounded px-1.5 py-0.5 text-xs bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300">
+                        {t("storySimulation.consistencyScore")}: {char.consistencyScore}
+                      </span>
+                    </div>
+
+                    {char.behaviors.length > 0 && (
+                      <div className="mt-2">
+                        <p className="mb-1 text-xs font-medium text-muted-foreground">
+                          {t("storySimulation.behaviors")}
+                        </p>
+                        <ul className="space-y-1">
+                          {char.behaviors.map((b, i) => (
+                            <li key={i} className="text-sm">
+                              <span className="text-muted-foreground">[{b.node}]</span>{" "}
+                              {b.action}
+                              <span className="text-muted-foreground">
+                                {" "}
+                                — {t("storySimulation.motivation")}: {b.motivation}
+                              </span>
+                            </li>
+                          ))}
+                        </ul>
+                      </div>
+                    )}
+
+                    {char.stateChanges.length > 0 && (
+                      <div className="mt-2">
+                        <p className="mb-1 text-xs font-medium text-muted-foreground">
+                          {t("storySimulation.stateChanges")}
+                        </p>
+                        <ul className="list-disc space-y-0.5 pl-4 text-sm">
+                          {char.stateChanges.map((s, i) => (
+                            <li key={i}>{s}</li>
+                          ))}
+                        </ul>
+                      </div>
+                    )}
+                  </div>
+                ))}
+              </div>
+            </section>
+          )}
+
+          {/* 走向分支 */}
+          {report.branches.length > 0 && (
+            <section>
+              <h3 className="mb-3 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
+                {t("storySimulation.storyBranches")}
+              </h3>
+              <div className="space-y-3">
+                {report.branches.map((branch, idx) => (
+                  <div key={idx} className="rounded-lg border p-3">
+                    <div className="flex flex-wrap items-center gap-2">
+                      <span className="font-medium">{branch.title}</span>
+                      {branch.recommendation && (
+                        <span className="rounded px-1.5 py-0.5 text-xs bg-primary/10 text-primary">
+                          {t("storySimulation.recommended")}
+                        </span>
+                      )}
+                      <span
+                        className={`rounded px-1.5 py-0.5 text-xs ${PROBABILITY_COLORS[branch.probability]}`}
+                      >
+                        {t("storySimulation.probability")}:{" "}
+                        {t(`storySimulation.probability${branch.probability.charAt(0).toUpperCase()}${branch.probability.slice(1)}`)}
+                      </span>
+                    </div>
+
+                    <p className="mt-2 text-sm text-muted-foreground">{branch.summary}</p>
+
+                    {branch.keyEvents.length > 0 && (
+                      <div className="mt-2">
+                        <p className="mb-1 text-xs font-medium text-muted-foreground">
+                          {t("storySimulation.keyEvents")}
+                        </p>
+                        <ul className="list-disc space-y-0.5 pl-4 text-sm">
+                          {branch.keyEvents.map((e, i) => (
+                            <li key={i}>{e}</li>
+                          ))}
+                        </ul>
+                      </div>
+                    )}
+
+                    <div className="mt-2 grid gap-2 sm:grid-cols-2">
+                      {branch.pros && (
+                        <div className="rounded-md bg-green-50 p-2 text-sm dark:bg-green-950/30">
+                          <span className="font-medium text-green-700 dark:text-green-400">
+                            {t("storySimulation.pros")}:{" "}
+                          </span>
+                          {branch.pros}
+                        </div>
+                      )}
+                      {branch.cons && (
+                        <div className="rounded-md bg-red-50 p-2 text-sm dark:bg-red-950/30">
+                          <span className="font-medium text-red-700 dark:text-red-400">
+                            {t("storySimulation.cons")}:{" "}
+                          </span>
+                          {branch.cons}
+                        </div>
+                      )}
+                    </div>
+
+                    <Button
+                      variant="default"
+                      size="sm"
+                      className="mt-3"
+                      onClick={() => onGenerateDraft(branch)}
+                    >
+                      <Sparkles className="h-3.5 w-3.5" />
+                      {t("storySimulation.generateDraft")}
+                    </Button>
+                  </div>
+                ))}
+              </div>
+            </section>
+          )}
+
+          {/* 综合推荐 */}
+          {report.recommendation && (
+            <section>
+              <div className="rounded-lg border border-primary/20 bg-primary/5 p-4">
+                <h3 className="mb-2 flex items-center gap-2 text-xs font-semibold uppercase tracking-wide text-primary">
+                  <Sparkles className="h-3.5 w-3.5" />
+                  {t("storySimulation.recommendation")}
+                </h3>
+                <p className="text-sm leading-relaxed">{report.recommendation}</p>
+              </div>
+            </section>
+          )}
+        </div>
+      </div>
+    </div>
+  )
+}

+ 67 - 0
src/components/novel/story-simulation/story-draft-view.tsx

@@ -0,0 +1,67 @@
+import { useState } from "react"
+import { useTranslation } from "react-i18next"
+import { ArrowLeft, Check, Copy, FileText } from "lucide-react"
+import { Button } from "@/components/ui/button"
+import { useStorySimulationStore } from "@/stores/story-simulation-store"
+
+interface StoryDraftViewProps {
+  onBack: () => void
+}
+
+export function StoryDraftView({ onBack }: StoryDraftViewProps) {
+  const { t } = useTranslation()
+  const draft = useStorySimulationStore((s) => s.currentDraft)
+  const [copied, setCopied] = useState(false)
+
+  if (!draft) return null
+
+  const handleCopyAll = async () => {
+    const text = draft.chapters
+      .map((ch) => `${ch.title}\n\n${ch.content}`)
+      .join("\n\n---\n\n")
+    await navigator.clipboard.writeText(text)
+    setCopied(true)
+    setTimeout(() => setCopied(false), 2000)
+  }
+
+  return (
+    <div className="flex h-full flex-col">
+      <div className="flex items-center justify-between border-b px-4 py-3">
+        <div className="flex items-center gap-2">
+          <Button variant="ghost" size="icon-sm" onClick={onBack}>
+            <ArrowLeft className="h-4 w-4" />
+          </Button>
+          <h2 className="text-sm font-semibold">{t("storySimulation.draftTitle")}</h2>
+        </div>
+        <Button variant="outline" size="sm" onClick={handleCopyAll}>
+          {copied ? (
+            <Check className="h-3.5 w-3.5" />
+          ) : (
+            <Copy className="h-3.5 w-3.5" />
+          )}
+          {copied ? t("storySimulation.copied") : t("storySimulation.copyAll")}
+        </Button>
+      </div>
+
+      <div className="flex-1 overflow-y-auto p-4">
+        <div className="mx-auto max-w-3xl space-y-4">
+          <div className="text-xs text-muted-foreground">
+            {t("storySimulation.totalWords")}: {draft.totalWords}
+          </div>
+
+          {draft.chapters.map((chapter, idx) => (
+            <div key={idx} className="rounded-lg border p-4">
+              <h3 className="mb-2 flex items-center gap-2 font-medium">
+                <FileText className="h-4 w-4 text-muted-foreground" />
+                {chapter.title}
+              </h3>
+              <p className="whitespace-pre-wrap text-sm leading-relaxed">
+                {chapter.content}
+              </p>
+            </div>
+          ))}
+        </div>
+      </div>
+    </div>
+  )
+}

+ 11 - 1
src/i18n/en.json

@@ -1776,6 +1776,8 @@
     "goal": "Goal",
     "cause": "Cause",
     "expectedOutcome": "Expected Outcome",
+    "coreConflict": "Core Conflict",
+    "involvedCharacters": "Involved Characters",
     "consistencyScore": "Consistency Score",
     "probability": "Probability",
     "probabilityHigh": "High",
@@ -1786,6 +1788,14 @@
     "actualWords": "Actual Words",
     "totalWords": "Total Words",
     "error": "Error",
-    "retry": "Retry"
+    "retry": "Retry",
+    "back": "Back",
+    "copied": "Copied",
+    "keyEvents": "Key Events",
+    "behaviors": "Behaviors",
+    "motivation": "Motivation",
+    "stateChanges": "State Changes",
+    "summary": "Summary",
+    "recommended": "Recommended"
   }
 }

+ 11 - 1
src/i18n/zh.json

@@ -1653,6 +1653,8 @@
     "goal": "目标",
     "cause": "起因",
     "expectedOutcome": "预期走向",
+    "coreConflict": "核心冲突",
+    "involvedCharacters": "涉及角色",
     "consistencyScore": "人设一致性",
     "probability": "概率",
     "probabilityHigh": "高",
@@ -1663,6 +1665,14 @@
     "actualWords": "实际字数",
     "totalWords": "总字数",
     "error": "错误",
-    "retry": "重试"
+    "retry": "重试",
+    "back": "返回",
+    "copied": "已复制",
+    "keyEvents": "关键事件",
+    "behaviors": "行为",
+    "motivation": "动机",
+    "stateChanges": "状态变化",
+    "summary": "摘要",
+    "recommended": "推荐"
   }
 }