Skip to content

Commit c3e3c8e

Browse files
study8677claude
andcommitted
feat: rebuild review flow as split workspace with server-side scoring
- two-pane workspace: code viewer left, review panel right - line-anchored findings with inline cards and jump-to-line - simplified form: merge decision, conclusion, findings, tests - auto-save drafts to localStorage - move scoring to POST /api/review/[slug]/submit; reveal and scoringHints no longer shipped to the client before submit Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent b00fc1c commit c3e3c8e

7 files changed

Lines changed: 1410 additions & 605 deletions

File tree

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { NextResponse } from "next/server";
2+
3+
import { getReviewChallenge } from "@/lib/challenges/review";
4+
import { scoreReviewDraft, type ReviewDraft, type ReviewLineFinding } from "@/lib/challenges/review-submission";
5+
6+
type ReviewSubmitRouteProps = {
7+
params: Promise<{
8+
slug: string;
9+
}>;
10+
};
11+
12+
function asString(value: unknown) {
13+
return typeof value === "string" ? value : "";
14+
}
15+
16+
function asLineFinding(value: unknown): ReviewLineFinding | null {
17+
if (!value || typeof value !== "object") {
18+
return null;
19+
}
20+
21+
const finding = value as Record<string, unknown>;
22+
const lineNumber = typeof finding.lineNumber === "number" ? finding.lineNumber : Number(finding.lineNumber);
23+
24+
if (!Number.isFinite(lineNumber) || lineNumber < 1) {
25+
return null;
26+
}
27+
28+
return {
29+
id: asString(finding.id),
30+
fileName: asString(finding.fileName),
31+
lineNumber,
32+
severity: asString(finding.severity),
33+
problem: asString(finding.problem),
34+
fix: asString(finding.fix)
35+
};
36+
}
37+
38+
function normalizeDraft(value: unknown): ReviewDraft {
39+
const draft = value && typeof value === "object" ? value as Record<string, unknown> : {};
40+
const mergeDecision = asString(draft.mergeDecision);
41+
const findings = Array.isArray(draft.findings)
42+
? draft.findings.map(asLineFinding).filter((finding): finding is ReviewLineFinding => Boolean(finding))
43+
: [];
44+
const legacyFinding = [asString(draft.problem), asString(draft.impact), asString(draft.fix)].filter(Boolean).join("\n");
45+
46+
return {
47+
mergeDecision: mergeDecision === "yes" || mergeDecision === "no" || mergeDecision === "unknown" ? mergeDecision : "",
48+
conclusion: asString(draft.conclusion),
49+
findings: findings.length > 0 || !legacyFinding
50+
? findings
51+
: [{
52+
id: "legacy-finding",
53+
fileName: "",
54+
lineNumber: 1,
55+
severity: asString(draft.severity) || "high",
56+
problem: legacyFinding,
57+
fix: asString(draft.fix)
58+
}],
59+
tests: asString(draft.tests)
60+
};
61+
}
62+
63+
export async function POST(request: Request, { params }: ReviewSubmitRouteProps) {
64+
const { slug } = await params;
65+
const challenge = getReviewChallenge(slug);
66+
67+
if (!challenge) {
68+
return NextResponse.json({ error: "Review challenge not found." }, { status: 404 });
69+
}
70+
71+
let body: unknown;
72+
73+
try {
74+
body = await request.json();
75+
} catch {
76+
return NextResponse.json({ error: "Invalid JSON body." }, { status: 400 });
77+
}
78+
79+
const draft = normalizeDraft(body && typeof body === "object" ? (body as Record<string, unknown>).draft : null);
80+
const feedback = scoreReviewDraft(draft, challenge.metadata.scoringHints, challenge.reveal);
81+
82+
return NextResponse.json({
83+
feedback,
84+
reveal: challenge.reveal
85+
});
86+
}

src/app/challenges/review/[slug]/page.tsx

Lines changed: 18 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,8 @@ import type { Metadata } from "next";
22
import Link from "next/link";
33
import { notFound } from "next/navigation";
44

5-
import { ReviewFileBrowser } from "@/components/challenges/ReviewFileBrowser";
6-
import { ReviewSubmissionForm } from "@/components/challenges/ReviewSubmissionForm";
5+
import { ReviewWorkspace } from "@/components/challenges/ReviewWorkspace";
76
import { AppShell } from "@/components/layout/AppShell";
8-
import { Badge } from "@/components/ui/Badge";
97
import {
108
getReviewBackground,
119
getReviewChallenge,
@@ -45,8 +43,7 @@ export default async function ReviewChallengePage({ params }: ReviewChallengePag
4543
notFound();
4644
}
4745

48-
const { metadata, files, reveal } = challenge;
49-
const displayId = metadata.order.toString().padStart(3, "0");
46+
const { metadata, files } = challenge;
5047
const pr = getReviewPrBrief(metadata);
5148
const background = getReviewBackground(metadata);
5249

@@ -60,12 +57,11 @@ export default async function ReviewChallengePage({ params }: ReviewChallengePag
6057
</Link>
6158
<nav className="nav" aria-label="Challenge navigation">
6259
<Link href="/">题库</Link>
63-
<a className="active" href="#review">
64-
Review {displayId}
60+
<a className="active" href="#files">
61+
Review {metadata.order.toString().padStart(3, "0")}
6562
</a>
6663
<a href="#files">题目文件</a>
6764
<a href="#submit">提交 Review</a>
68-
<a href="#rubric">评分</a>
6965
</nav>
7066
<div className="actions">
7167
<Link className="button button-outline" href="/">
@@ -75,110 +71,20 @@ export default async function ReviewChallengePage({ params }: ReviewChallengePag
7571
</div>
7672
</header>
7773

78-
<main className="page challenge-page">
79-
<div className="notice">
80-
<span>
81-
<strong>真实来源改编</strong> {metadata.source.project} 案例,改编为 AI PR 审核题。上游来源在提交 review 后展示。
82-
</span>
83-
<span className="mono">Review {displayId}</span>
84-
</div>
85-
86-
<section className="challenge-header card">
87-
<div className="challenge-kicker">
88-
<span className="id">{displayId}</span>
89-
<Badge tone="review">Review</Badge>
90-
<Badge tone={metadata.difficulty}>{metadata.difficulty}</Badge>
91-
<span className="pill">{metadata.language}</span>
92-
</div>
93-
<h1>{metadata.title.zh}</h1>
94-
<p>{metadata.summary.zh}</p>
95-
</section>
96-
97-
<div className="challenge-layout">
98-
<article className="challenge-main">
99-
<section className="challenge-section card" id="review">
100-
<div className="card-head">
101-
<h2>待审核的 PR</h2>
102-
<span className="mono">
103-
{pr.author} · CI passed
104-
</span>
105-
</div>
106-
<div className="section-body">
107-
<p>
108-
<strong>{pr.title}</strong>
109-
</p>
110-
{pr.body.map((paragraph) => (
111-
<p key={paragraph}>{paragraph}</p>
112-
))}
113-
</div>
114-
</section>
115-
116-
<ReviewFileBrowser files={files} defaultFileName={metadata.reviewTarget.file} />
117-
118-
{background.length > 0 ? (
119-
<section className="challenge-section card">
120-
<div className="card-head">
121-
<h2>背景</h2>
122-
<span className="mono">{metadata.source.project}</span>
123-
</div>
124-
<div className="section-body">
125-
{background.map((item) => (
126-
<p key={item}>{item}</p>
127-
))}
128-
</div>
129-
</section>
130-
) : null}
131-
132-
<ReviewSubmissionForm
133-
challengeId={metadata.id}
134-
scoringHints={metadata.scoringHints}
135-
reveal={reveal}
136-
/>
137-
</article>
138-
139-
<aside className="challenge-side">
140-
<section className="card">
141-
<div className="card-head">
142-
<h2>快速判断</h2>
143-
</div>
144-
<div className="section-body compact">
145-
<p>语言 / 领域:{metadata.language}</p>
146-
<p>类型:Review Mode,不要求你改代码。</p>
147-
<p>你是 reviewer:这个 PR 的结论由你负责,可以合并、不能合并、需要更多信息都可能是正确答案。</p>
148-
<p>可以使用任何 AI 工具辅助,但评分看的是你给出的证据和判断。</p>
149-
</div>
150-
</section>
151-
152-
<section className="card" id="rubric">
153-
<div className="card-head">
154-
<h2>评分重点</h2>
155-
</div>
156-
<div className="rubric-list">
157-
<div className="rubric-item">
158-
<span>合并判断</span>
159-
<strong>30%</strong>
160-
</div>
161-
<div className="rubric-item">
162-
<span>核心风险</span>
163-
<strong>30%</strong>
164-
</div>
165-
<div className="rubric-item">
166-
<span>边界语义</span>
167-
<strong>15%</strong>
168-
</div>
169-
<div className="rubric-item">
170-
<span>测试质量</span>
171-
<strong>10%</strong>
172-
</div>
173-
<div className="rubric-item">
174-
<span>修复建议</span>
175-
<strong>15%</strong>
176-
</div>
177-
</div>
178-
</section>
179-
</aside>
180-
</div>
181-
</main>
74+
<ReviewWorkspace
75+
background={background}
76+
challengeId={metadata.id}
77+
challengeSlug={metadata.slug}
78+
defaultFileName={metadata.reviewTarget.file}
79+
difficulty={metadata.difficulty}
80+
files={files}
81+
language={metadata.language}
82+
order={metadata.order}
83+
pr={pr}
84+
sourceProject={metadata.source.project}
85+
summary={metadata.summary}
86+
title={metadata.title}
87+
/>
18288
</AppShell>
18389
);
18490
}

0 commit comments

Comments
 (0)