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

新增渐进式阶段渲染组件

Mochocyang 3 месяцев назад
Родитель
Сommit
9e72d50c62
2 измененных файлов с 86 добавлено и 0 удалено
  1. 81 0
      src/components/sources/outline-workflow-stages.tsx
  2. 5 0
      src/index.css

+ 81 - 0
src/components/sources/outline-workflow-stages.tsx

@@ -0,0 +1,81 @@
+import React, { useState } from "react"
+import { CheckCircle2, ChevronRight, Loader2 } from "lucide-react"
+import { cn } from "@/lib/utils"
+import { buildOutlineStages, type OutlineStageInput, type OutlineStage } from "@/lib/novel/outline-stage-trace"
+
+interface OutlineWorkflowStagesProps extends OutlineStageInput {}
+
+function StageRow({ stage, open, onToggle }: {
+  stage: OutlineStage
+  open: boolean
+  onToggle: () => void
+}) {
+  return (
+    <div
+      className={cn(
+        "rounded-md border transition-all duration-250 ease-out",
+        stage.status === "active" && "border-sky-500/40 bg-sky-50/30 dark:bg-sky-950/10",
+        stage.status === "done" && "border-emerald-500/20 bg-emerald-50/10 dark:bg-emerald-950/5",
+        "opacity-0 translate-y-1 animate-[fadeIn_250ms_ease-out_forwards]"
+      )}
+    >
+      <button
+        type="button"
+        onClick={onToggle}
+        className="flex w-full items-center gap-2 px-3 py-1.5 text-left text-xs"
+      >
+        {stage.status === "done" ? (
+          <CheckCircle2 className="h-3.5 w-3.5 shrink-0 text-emerald-600 dark:text-emerald-400" />
+        ) : stage.status === "active" ? (
+          <Loader2 className="h-3.5 w-3.5 shrink-0 animate-spin text-sky-600 dark:text-sky-400" />
+        ) : null}
+        <span className="font-medium">{stage.title}</span>
+        {stage.summary && (
+          <span className="text-muted-foreground">· {stage.summary}</span>
+        )}
+        <ChevronRight
+          className={cn(
+            "ml-auto h-3 w-3 transition-transform",
+            open && "rotate-90"
+          )}
+        />
+      </button>
+      {open && stage.details.length > 0 && (
+        <div className="border-t border-border/50 px-3 py-2 text-xs text-muted-foreground space-y-1">
+          {stage.details.map((detail, i) => (
+            <div key={i}>{detail}</div>
+          ))}
+        </div>
+      )}
+    </div>
+  )
+}
+
+export function OutlineWorkflowStages(props: OutlineWorkflowStagesProps) {
+  const stages = buildOutlineStages(props)
+  const visibleStages = stages.filter((s) => s.status !== "hidden")
+
+  const defaultOpenId = stages.find((s) => s.status === "active")?.kind ?? null
+  const [openMap, setOpenMap] = useState<Record<string, boolean>>({})
+
+  if (visibleStages.length === 0) return null
+
+  return (
+    <div className="mb-2 space-y-1">
+      <div className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground mb-1">
+        <span>思考过程</span>
+      </div>
+      {visibleStages.map((stage) => {
+        const isOpen = openMap[stage.kind] ?? (stage.status === "active")
+        return (
+          <StageRow
+            key={stage.kind}
+            stage={stage}
+            open={isOpen}
+            onToggle={() => setOpenMap((prev) => ({ ...prev, [stage.kind]: !isOpen }))}
+          />
+        )
+      })}
+    </div>
+  )
+}

+ 5 - 0
src/index.css

@@ -853,3 +853,8 @@
 .dark *::-webkit-scrollbar-thumb:hover {
     background-color: hsl(var(--muted-foreground) / 0.5);
 }
+
+@keyframes fadeIn {
+  from { opacity: 0; transform: translateY(4px); }
+  to { opacity: 1; transform: translateY(0); }
+}