feat: improve app ui and project metadata
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user