feat: improve app ui and project metadata

This commit is contained in:
Yaojia Wang
2026-06-28 07:31:20 +02:00
parent 3bd464d400
commit 90a66437d7
86 changed files with 4892 additions and 1108 deletions

View File

@@ -2,8 +2,16 @@
import { useEffect, useMemo, useRef, useState } from "react";
import Link from "next/link";
import {
AlertTriangle,
CheckCircle2,
RotateCcw,
Square,
} from "lucide-react";
import { AppShell } from "@/components/AppShell";
import { Badge } from "@/components/ui/Badge";
import { Button } from "@/components/ui/Button";
import type { ProjectResponse, ReviewHistoryItem } from "@/lib/api/types";
import { friendlyError } from "@/lib/errors/messages";
import {
@@ -44,6 +52,8 @@ import { AnnotatedText } from "./AnnotatedText";
import { ConflictCard } from "./ConflictCard";
import { ForeshadowSuggestions } from "./ForeshadowSuggestions";
import { PacePanel } from "./PacePanel";
import { ReviewSectionPanel } from "./ReviewSectionPanel";
import { ReviewSummaryRail } from "./ReviewSummaryRail";
import { StylePanel } from "@/components/style/StylePanel";
import { RefineView } from "@/components/style/RefineView";
@@ -370,6 +380,13 @@ export function ReviewReport({
const resolved = allResolved(drafts);
const unresolved = unresolvedIndices(drafts).length;
const reviewing = review.isReviewing;
const hasReport =
review.state.phase === "done" ||
review.state.sections.length > 0 ||
seededConflicts.length > 0 ||
foreshadow.length > 0 ||
pace !== null ||
style !== null;
return (
<AppShell
@@ -378,33 +395,27 @@ export function ReviewReport({
projectId={project.id}
activeNav="review"
>
<div className="grid h-[calc(100vh-var(--chrome,4rem))] grid-cols-1 lg:grid-cols-[1fr_24rem]">
<div className="flex min-h-[calc(100vh-var(--chrome,4rem))] flex-col lg:grid lg:h-[calc(100vh-var(--chrome,4rem))] lg:min-h-0 lg:grid-cols-[minmax(0,1fr)_24rem]">
{/* 左:终稿正文 + 就地标注 */}
<section className="flex min-w-0 flex-col bg-bg">
<div className="flex items-center gap-3 border-b border-line bg-panel px-6 py-3">
<h1 className="font-serif text-lg text-ink">
<section className="flex min-h-[70vh] min-w-0 flex-col bg-bg lg:min-h-0">
<div className="flex items-center gap-3 border-b border-line bg-panel px-4 py-3 sm:px-6">
<h1 className="min-w-0 flex-1 truncate font-serif text-lg text-ink">
{chapterNo} 稿
</h1>
<span className="font-mono text-xs text-ink-soft">
<span className="hidden shrink-0 font-mono text-xs text-ink-soft sm:inline">
{conflictCount}
</span>
<div className="ml-auto">
<div className="shrink-0">
{reviewing ? (
<button
type="button"
onClick={review.stop}
className="rounded border border-conflict px-3 py-1.5 text-sm text-conflict"
>
<Button onClick={review.stop} variant="danger" size="sm">
<Square className="h-4 w-4" aria-hidden="true" />
</button>
</Button>
) : (
<button
type="button"
onClick={onReReview}
className="rounded bg-cinnabar px-3 py-1.5 text-sm text-panel hover:opacity-90"
>
稿
</button>
<Button onClick={onReReview} variant="primary" size="sm">
<RotateCcw className="h-4 w-4" aria-hidden="true" />
稿
</Button>
)}
</div>
</div>
@@ -413,7 +424,7 @@ export function ReviewReport({
<ReviewErrorNote error={review.state.error} />
) : null}
<div className="flex-1 overflow-auto px-6 py-6">
<div className="flex-1 overflow-auto px-4 py-4 sm:px-6 sm:py-6">
<div className="mb-2 flex items-start justify-between gap-3">
<p className="text-xs text-ink-soft">
稿 稿
@@ -438,101 +449,187 @@ export function ReviewReport({
</section>
{/* 右:审稿分区 + 冲突裁决 + 验收 gate */}
<aside className="flex flex-col overflow-auto border-l border-line bg-panel px-4 py-4">
<section className="mb-4">
<h2 className="text-xs font-semibold uppercase tracking-wide text-ink-soft">
(continuity)
</h2>
<SectionStatusLine
reviewing={reviewing}
done={review.state.phase === "done"}
conflictCount={conflictCount}
sections={review.state.sections}
/>
</section>
<aside
className="flex flex-col border-t border-line bg-panel px-4 py-4 lg:overflow-auto lg:border-l lg:border-t-0"
aria-label="审稿报告与验收"
>
<ReviewSummaryRail
conflictCount={conflictCount}
unresolvedCount={unresolved}
reviewing={reviewing}
hasReport={hasReport}
onNextUnresolved={jumpToNextUnresolved}
/>
<section>
{conflicts.length === 0 ? (
<p className="rounded border border-dashed border-line p-4 text-xs text-ink-soft">
{review.state.phase === "done" || seededConflicts.length === 0
? "未发现一致性冲突。"
: "进页未带审稿留痕,点「重新审稿」开始。"}
</p>
) : (
<div className="space-y-4">
{/* R2未裁决进度 + 跳到下一条未裁决 */}
<div className="flex items-center justify-between text-xs text-ink-soft">
<span>
{unresolved > 0 ? (
<span className="text-conflict">
{unresolved}/{conflictCount}
</span>
) : (
<span className="text-pass"> </span>
)}
</span>
<button
type="button"
onClick={jumpToNextUnresolved}
disabled={unresolved === 0}
className="rounded border border-line px-2 py-0.5 hover:border-cinnabar hover:text-cinnabar disabled:cursor-not-allowed disabled:opacity-40"
>
</button>
</div>
{/* B讲清裁决保存模型——「采纳/裁决」仅本地暂存,验收时一并落库 */}
<p className="rounded border border-dashed border-line/70 bg-bg/40 px-3 py-1.5 text-[11px] text-ink-soft">
稿
<div className="space-y-3">
<ReviewSectionPanel
title="一致性"
subtitle="continuity"
statusLabel={
reviewing
? "审稿中"
: conflictCount > 0
? `${conflictCount} 冲突`
: hasReport
? "通过"
: "待审"
}
statusVariant={
reviewing
? "info"
: conflictCount > 0
? "danger"
: hasReport
? "success"
: "neutral"
}
defaultOpen={conflictCount > 0 || !hasReport}
>
<SectionStatusLine
reviewing={reviewing}
done={review.state.phase === "done"}
conflictCount={conflictCount}
sections={review.state.sections}
/>
{conflicts.length === 0 ? (
<p className="mt-2 rounded border border-dashed border-line p-4 text-xs text-ink-soft">
{review.state.phase === "done" || seededConflicts.length === 0
? "未发现一致性冲突。"
: "进页未带审稿留痕,点「重新审稿」开始。"}
</p>
) : (
<div className="mt-3 space-y-4">
<p className="rounded border border-dashed border-line/70 bg-bg/40 px-3 py-1.5 text-[11px] text-ink-soft">
稿
</p>
{groups.map((group) => (
<div key={group.type}>
<h3 className="mb-2 flex items-center gap-2 text-xs font-semibold text-ink">
{group.type}
<span className="rounded-full bg-[var(--color-conflict)]/10 px-2 py-0.5 font-mono text-conflict">
{group.items.length}
</span>
</h3>
<ul className="space-y-3">
{group.items.map(({ conflict: c, index: i }) => (
<ConflictCard
key={i}
index={i}
conflict={c}
draft={drafts[i] ?? { verdict: null, note: "" }}
missing={missing.has(i)}
snippet={conflictSnippet(finalText, c.where)}
locatable={locatable.has(i)}
rewriting={rewriting.has(i)}
onVerdict={onVerdict}
onNote={onNote}
onJump={focusRegion}
/>
))}
</ul>
</div>
))}
</div>
)}
</section>
{groups.map((group) => {
const unresolvedInGroup = group.items.filter(
({ index }) => !drafts[index]?.verdict,
).length;
return (
<details
key={group.type}
open={unresolvedInGroup > 0}
className="rounded border border-line/70 bg-bg/35 p-2"
>
<summary className="flex cursor-pointer list-none items-center justify-between gap-2 text-xs font-semibold text-ink">
<span className="flex items-center gap-2">
{group.type}
<Badge
variant={
unresolvedInGroup > 0 ? "danger" : "success"
}
>
{group.items.length}
</Badge>
</span>
<span className="font-mono text-[11px] text-ink-soft">
{unresolvedInGroup > 0
? `${unresolvedInGroup} 未裁决`
: "已处理"}
</span>
</summary>
<ul className="mt-3 space-y-3">
{group.items.map(({ conflict: c, index: i }) => (
<ConflictCard
key={i}
index={i}
total={conflictCount}
conflict={c}
draft={drafts[i] ?? { verdict: null, note: "" }}
missing={missing.has(i)}
snippet={conflictSnippet(finalText, c.where)}
locatable={locatable.has(i)}
rewriting={rewriting.has(i)}
onVerdict={onVerdict}
onNote={onNote}
onJump={focusRegion}
/>
))}
</ul>
</details>
);
})}
</div>
)}
</ReviewSectionPanel>
<div className="mt-4 border-t border-line pt-4">
<ReviewSectionPanel
title="伏笔"
subtitle="foreshadow-analyst"
statusLabel={
sectionStatus("foreshadow") === "incomplete"
? "未完成"
: foreshadow.length > 0
? `${foreshadow.length} 建议`
: "无建议"
}
statusVariant={
sectionStatus("foreshadow") === "incomplete"
? "warning"
: foreshadow.length > 0
? "accent"
: "success"
}
defaultOpen={foreshadow.length > 0}
>
<ForeshadowSuggestions
suggestions={foreshadow}
incomplete={sectionStatus("foreshadow") === "incomplete"}
projectId={project.id}
chapterNo={chapterNo}
/>
</ReviewSectionPanel>
<ReviewSectionPanel
title="节奏"
subtitle="pace-checker"
statusLabel={
sectionStatus("pace") === "incomplete"
? "未完成"
: pace
? "已完成"
: "无报告"
}
statusVariant={
sectionStatus("pace") === "incomplete"
? "warning"
: pace
? "info"
: "neutral"
}
defaultOpen={pace !== null}
>
<PacePanel
pace={pace}
incomplete={sectionStatus("pace") === "incomplete"}
/>
</ReviewSectionPanel>
<ReviewSectionPanel
title="文风"
subtitle="style"
statusLabel={
sectionStatus("style") === "incomplete"
? "未完成"
: style
? "已完成"
: "无报告"
}
statusVariant={
sectionStatus("style") === "incomplete"
? "warning"
: style
? "info"
: "neutral"
}
defaultOpen={style !== null}
>
<StylePanel
style={style}
incomplete={sectionStatus("style") === "incomplete"}
onRefine={onRefineSegment}
/>
</ReviewSectionPanel>
{refineText !== null ? (
<RefineView
projectId={project.id}
@@ -613,9 +710,15 @@ function SectionStatusLine({
return (
<p className="mt-1 text-xs">
{conflictCount > 0 ? (
<span className="text-conflict"> {conflictCount} </span>
<Badge variant="danger">
<AlertTriangle className="h-3 w-3" aria-hidden="true" />
{conflictCount}
</Badge>
) : (
<span className="text-pass"> </span>
<Badge variant="success">
<CheckCircle2 className="h-3 w-3" aria-hidden="true" />
</Badge>
)}
</p>
);