- New AvBeatLogo inline-SVG component (rounded-square emblem with forward-leaning AV monogram + horizontal beam detail) at src/components/AvBeatLogo.tsx, three variants (full/wordmark/icon). - Header.tsx now uses the responsive AV BEAT logo lockup in the top-left, links to /, full lockup on desktop (emblem+wordmark+tagline), emblem+wordmark on tablet, emblem-only on mobile. - Removed DualSpeedTicker (the 'RECENT FORUM POSTS' ticker strip); the news ticker / glow chrome does not match the new aesthetic and the forum row was the explicit removal target. - Tailwind theme + globals.css now expose the AV BEAT 2026 palette as semantic tokens: --brand-blue (#1D4ED8), --brand-blue-bright (#38BDF8), --brand-navy (#0F172A), --brand-bg (#F8FAFC), --brand-surface, --brand-border, --brand-text, --brand-text-muted. Legacy --color-* aliases re-point to the new tokens so any inline-styled component re-themes for free. - Site-wide hex sweep migrates 2,769 hardcoded color literals across 144 files from the old dark-broadcast palette to the new tokens (orange -> blue, dark-brown -> white surface / navy text, cream -> navy). - Layout body class flipped from 'bb-neon' to 'bg-brand-bg text-brand-text' so the dark glow chrome no longer leaks through the new light theme.
110 lines
4.4 KiB
TypeScript
110 lines
4.4 KiB
TypeScript
import Link from "next/link";
|
|
import { notFound } from "next/navigation";
|
|
import { createAdminClient } from "@/lib/supabase/admin";
|
|
import EventTagPicker from './EventTagPicker';
|
|
import ReviewActions from "./ReviewActions";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
async function loadDetail(id: string) {
|
|
const sb = createAdminClient();
|
|
const { data: art } = await sb
|
|
.from("ai_rewritten_articles")
|
|
.select("*")
|
|
.eq("id", id)
|
|
.maybeSingle();
|
|
if (!art) return null;
|
|
const { data: persona } = await sb
|
|
.from("ai_personas")
|
|
.select("name, slug, avatar_url, beat")
|
|
.eq("id", (art as any).persona_id)
|
|
.maybeSingle();
|
|
const { data: original } = await sb
|
|
.from("ai_original_submissions")
|
|
.select("raw_title, raw_content, contributor_name, contributor_email, submitted_at, source")
|
|
.eq("id", (art as any).original_submission_id)
|
|
.maybeSingle();
|
|
const { data: job } = await sb
|
|
.from("ai_rewrite_jobs")
|
|
.select("attempt_number, ai_detector_score, banned_words_found, model, input_tokens, output_tokens, cache_read_tokens")
|
|
.eq("id", (art as any).rewrite_job_id)
|
|
.maybeSingle();
|
|
return { art, persona, original, job };
|
|
}
|
|
|
|
export default async function ReviewDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
|
const { id } = await params;
|
|
const detail = await loadDetail(id);
|
|
if (!detail) notFound();
|
|
|
|
const { art, persona, original, job } = detail as any;
|
|
|
|
return (
|
|
<main className="mx-auto max-w-6xl px-6 py-10 text-[#e5e7eb]">
|
|
<Link href="/admin/review-queue" className="text-sm text-[#1D4ED8] hover:underline">
|
|
← Back to queue
|
|
</Link>
|
|
|
|
<header className="mt-4 mb-6 border-b border-[#222] pb-4">
|
|
<div className="flex items-center gap-3 text-xs uppercase tracking-wider text-[#1D4ED8]">
|
|
<span>{persona?.name || "—"}</span>
|
|
<span className="text-[#4b5563]">·</span>
|
|
<span>{art.category}</span>
|
|
<span className="text-[#4b5563]">·</span>
|
|
<span>{art.status}</span>
|
|
</div>
|
|
<h1 className="mt-2 text-3xl font-bold tracking-tight">{art.title}</h1>
|
|
<p className="mt-2 text-[#9ca3af]">{art.excerpt}</p>
|
|
</header>
|
|
|
|
<EventTagPicker articleId={art.id} />
|
|
<div className="my-4" />
|
|
<ReviewActions id={art.id} currentStatus={art.status} />
|
|
|
|
<section className="mt-8 grid grid-cols-1 md:grid-cols-2 gap-8">
|
|
<div>
|
|
<h2 className="text-sm font-semibold uppercase tracking-wider text-[#9ca3af] mb-3">
|
|
Rewritten ({persona?.name})
|
|
</h2>
|
|
<article
|
|
className="prose prose-invert max-w-none text-sm"
|
|
dangerouslySetInnerHTML={{ __html: art.body }}
|
|
/>
|
|
<div className="mt-4 flex flex-wrap gap-2 text-xs">
|
|
{(art.tags || []).map((t: string) => (
|
|
<span key={t} className="rounded bg-[#1f2937] px-2 py-0.5">{t}</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<h2 className="text-sm font-semibold uppercase tracking-wider text-[#9ca3af] mb-3">
|
|
Original submission
|
|
</h2>
|
|
<div className="text-xs text-[#6b7280] mb-3">
|
|
{original?.contributor_name || "(no name)"} · {original?.contributor_email || "(no email)"} ·{" "}
|
|
{original?.source} · {original?.submitted_at && new Date(original.submitted_at).toLocaleString()}
|
|
</div>
|
|
<h3 className="text-base font-semibold mb-2">{original?.raw_title}</h3>
|
|
<pre className="whitespace-pre-wrap text-sm text-[#9ca3af] bg-[#0b0f17] border border-[#1f2937] rounded p-4 max-h-[600px] overflow-auto">
|
|
{original?.raw_content}
|
|
</pre>
|
|
</div>
|
|
</section>
|
|
|
|
<section className="mt-8 rounded border border-[#1f2937] bg-[#0b0f17] p-4 text-xs text-[#9ca3af]">
|
|
<h2 className="font-semibold uppercase tracking-wider mb-2">Rewrite job</h2>
|
|
<ul className="space-y-1">
|
|
<li>Attempt: {job?.attempt_number}</li>
|
|
<li>Model: {job?.model}</li>
|
|
<li>AI-detector score: {job?.ai_detector_score?.toFixed?.(3) ?? "—"}</li>
|
|
<li>Banned words found: {(job?.banned_words_found || []).join(", ") || "none"}</li>
|
|
<li>
|
|
Tokens: input {job?.input_tokens ?? 0} / output {job?.output_tokens ?? 0} / cache-read{" "}
|
|
{job?.cache_read_tokens ?? 0}
|
|
</li>
|
|
</ul>
|
|
</section>
|
|
</main>
|
|
);
|
|
}
|