Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 24 additions & 22 deletions web/src/app/debate/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Navbar } from "@/components/Navbar";
import { DebateReplay } from "@/components/debate/DebateReplay";
import { notFound } from "next/navigation";
import { getDebateById, getDebates } from "@/lib/benchmark-data";
import fs from "fs";
import path from "path";

Expand All @@ -9,17 +10,16 @@ interface Props {
}

async function getDebateData(id: string) {
// Try to load from benchmark results
const debatePath = path.join(
process.cwd(),
"..",
"benchmark",
"results",
"debates",
`${id}.json`
);

// Try to load full transcript from filesystem (available in monorepo dev/build)
try {
const debatePath = path.join(
process.cwd(),
"..",
"benchmark",
"results",
"debates",
`${id}.json`
);
if (fs.existsSync(debatePath)) {
const raw = fs.readFileSync(debatePath, "utf-8");
return JSON.parse(raw);
Expand All @@ -28,7 +28,8 @@ async function getDebateData(id: string) {
// fall through
}

return null;
// Fall back to bundled benchmark-results.json summary data
return getDebateById(id);
}

export default async function DebateReplayPage({ params }: Props) {
Expand All @@ -49,26 +50,27 @@ export default async function DebateReplayPage({ params }: Props) {
);
}

// Generate static paths for completed debates
export async function generateStaticParams() {
const debatesDir = path.join(
process.cwd(),
"..",
"benchmark",
"results",
"debates"
);

// Try filesystem first (monorepo dev/build)
try {
const debatesDir = path.join(
process.cwd(),
"..",
"benchmark",
"results",
"debates"
);
if (fs.existsSync(debatesDir)) {
return fs
const fromFs = fs
.readdirSync(debatesDir)
.filter((f) => f.endsWith(".json") && !f.startsWith("test"))
.map((f) => ({ id: f.replace(".json", "") }));
if (fromFs.length > 0) return fromFs;
}
} catch {
// fall through
}

return [];
// Fall back to bundled data
return getDebates().map((d) => ({ id: d.debate_id }));
}
111 changes: 60 additions & 51 deletions web/src/components/debate/DebateReplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -232,62 +232,71 @@ export function DebateReplay({ debate }: Props) {
<MessageSquare className="h-5 w-5 text-blue-400" />
Full Debate Transcript
</h2>
<div className="space-y-4">
{debate.transcript.map((entry, idx) => {
const showPhaseHeader = entry.phase !== currentPhase;
currentPhase = entry.phase;
const phaseInfo = phaseLabels[entry.phase] || {
label: entry.phase,
icon: null,
};
{debate.transcript.length === 0 ? (
<Card className="p-6 text-center text-muted-foreground">
<Brain className="h-8 w-8 mx-auto mb-2 opacity-50" />
<p className="text-sm">
Transcript not available for this debate. Vote results and scoring details are shown above and below.
</p>
</Card>
) : (
<div className="space-y-4">
{debate.transcript.map((entry, idx) => {
const showPhaseHeader = entry.phase !== currentPhase;
currentPhase = entry.phase;
const phaseInfo = phaseLabels[entry.phase] || {
label: entry.phase,
icon: null,
};

return (
<div key={idx}>
{showPhaseHeader && (
<div className="flex items-center gap-2 mb-3 mt-6">
{phaseInfo.icon}
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider">
{phaseInfo.label}
</h3>
<Separator className="flex-1" />
</div>
)}
return (
<div key={idx}>
{showPhaseHeader && (
<div className="flex items-center gap-2 mb-3 mt-6">
{phaseInfo.icon}
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider">
{phaseInfo.label}
</h3>
<Separator className="flex-1" />
</div>
)}

<div
className={`p-4 rounded-lg border ${
entry.side === "FOR"
? "border-blue-500/20 bg-blue-500/5 ml-0 mr-8"
: "border-red-500/20 bg-red-500/5 ml-8 mr-0"
}`}
>
<div className="flex items-center gap-2 mb-2">
<Badge
variant="outline"
className={`text-[10px] ${
entry.side === "FOR"
? "text-blue-400 border-blue-500/50"
: "text-red-400 border-red-500/50"
}`}
>
{entry.side}
</Badge>
<span className="text-xs font-semibold">
{entry.speaker}
</span>
{entry.type && (
<Badge variant="secondary" className="text-[10px]">
{entry.type}
<div
className={`p-4 rounded-lg border ${
entry.side === "FOR"
? "border-blue-500/20 bg-blue-500/5 ml-0 mr-8"
: "border-red-500/20 bg-red-500/5 ml-8 mr-0"
}`}
>
<div className="flex items-center gap-2 mb-2">
<Badge
variant="outline"
className={`text-[10px] ${
entry.side === "FOR"
? "text-blue-400 border-blue-500/50"
: "text-red-400 border-red-500/50"
}`}
>
{entry.side}
</Badge>
)}
<span className="text-xs font-semibold">
{entry.speaker}
</span>
{entry.type && (
<Badge variant="secondary" className="text-[10px]">
{entry.type}
</Badge>
)}
</div>
<p className="text-sm leading-relaxed whitespace-pre-wrap">
{entry.content}
</p>
</div>
<p className="text-sm leading-relaxed whitespace-pre-wrap">
{entry.content}
</p>
</div>
</div>
);
})}
</div>
);
})}
</div>
)}
</div>

{/* Final Votes */}
Expand Down
41 changes: 39 additions & 2 deletions web/src/lib/benchmark-data.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// AI² Benchmark — Data Loading Layer
// Loads pre-computed benchmark results from JSON files

import { BenchmarkData, LeaderboardEntry, JudgeTendencies, DebateSummary } from "./types";
import { MODELS } from "./models";
import { BenchmarkData, LeaderboardEntry, JudgeTendencies, DebateSummary, DebateResult, Vote } from "./types";
import { MODELS, PERSONA_MAP } from "./models";

// This will be populated from the benchmark results
// For now, we use placeholder data that gets overwritten when results are available
Expand Down Expand Up @@ -64,6 +64,43 @@ export function getDebates(): DebateSummary[] {
return getBenchmarkData().debates;
}

export function getDebateById(debateId: string): DebateResult | null {
const data = getBenchmarkData();
const summary = data.debates.find((d) => d.debate_id === debateId);
if (!summary) return null;

const initialVotes: Vote[] = (summary.score.vote_details || []).map((vd) => ({
stance: vd.initial_stance as Vote["stance"],
confidence: vd.initial_confidence,
reasoning: `Based on ${(PERSONA_MAP[vd.judge_model_id] || vd.persona || "neutral judge").toLowerCase()} perspective.`,
judge_model_id: vd.judge_model_id,
persona: vd.persona,
}));

const finalVotes: Vote[] = (summary.score.vote_details || []).map((vd) => ({
stance: vd.final_stance as Vote["stance"],
confidence: vd.final_confidence,
reasoning: vd.stance_changed
? `Changed position after hearing the debate arguments.`
: `Maintained ${vd.final_stance.toLowerCase()} position after the debate.`,
judge_model_id: vd.judge_model_id,
persona: vd.persona,
}));

return {
debate_id: summary.debate_id,
motion: summary.motion,
model_for: summary.model_for,
model_against: summary.model_against,
transcript: [],
initial_votes: initialVotes,
final_votes: finalVotes,
audience_questions: [],
score: summary.score,
metadata: summary.metadata,
};
}

export function getModelStats(modelId: string) {
const data = getBenchmarkData();
const debates = data.debates.filter(
Expand Down