Phase B: AI press-release rewrite pipeline
Pluggable rewrite client (Anthropic default, ollama/openrouter stubs)
using claude-opus-4-7 with prompt caching on system prompt + style guide.
Routes the raw submission through:
1. bb.ai_original_submissions — raw never edited
2. bb.ai_rewrite_jobs — every attempt logged with token counts
3. quality gates — AI-tell banned-word check + heuristic
AI-detector score (threshold 0.65)
4. regenerate up to 3x — on banned words or detector trip
5. bb.ai_rewritten_articles — status ai_rewritten_pending_review
6. /admin/review-queue — list + detail page with approve / reject /
needs-human actions; on approve, the
original submission is FK-linked to the
published rewrite
New files only — no existing routes or contributor flow modified.
Persona auto-selection routes by beat keywords; 6 personas seeded
(marcus-halverson, elena-vasquez, darren-okafor, sarah-quinn, mike-trayton,
priya-rao) with SVG monogram avatars in public/assets/images/personas/.
Smoke test (Matrox / ST 2110 sample press release):
- persona auto-routed to darren-okafor (protocols-specs)
- anthropic 22.3s, in=2616 / out=1372 tokens
- gates passed first try, score=0.000, no banned words
- review_queue row created
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
71
src/app/api/admin/review-queue/[id]/route.ts
Normal file
71
src/app/api/admin/review-queue/[id]/route.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { createClient } from "@/lib/supabase/server";
|
||||
import { createAdminClient } from "@/lib/supabase/admin";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
async function requireAdmin() {
|
||||
const sb = await createClient();
|
||||
const { data: { user } } = await sb.auth.getUser();
|
||||
if (!user) return { ok: false as const, status: 401, error: "Unauthorized" };
|
||||
const { data: profile } = await sb
|
||||
.from("user_profiles")
|
||||
.select("role, is_active")
|
||||
.eq("id", user.id)
|
||||
.single();
|
||||
if (!profile?.is_active) return { ok: false as const, status: 403, error: "Inactive" };
|
||||
if (!["administrator", "admin", "editor"].includes(profile.role)) {
|
||||
return { ok: false as const, status: 403, error: "Insufficient permissions" };
|
||||
}
|
||||
return { ok: true as const, userId: user.id };
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const auth = await requireAdmin();
|
||||
if (!auth.ok) return NextResponse.json({ error: auth.error }, { status: auth.status });
|
||||
|
||||
const { id } = await params;
|
||||
const { action, note } = (await req.json()) as { action: string; note?: string };
|
||||
|
||||
const admin = createAdminClient();
|
||||
const nowIso = new Date().toISOString();
|
||||
|
||||
let nextStatus = "";
|
||||
let extras: Record<string, any> = {};
|
||||
|
||||
if (action === "approve") {
|
||||
nextStatus = "published";
|
||||
extras.published_at = nowIso;
|
||||
} else if (action === "reject") {
|
||||
nextStatus = "rejected";
|
||||
} else if (action === "needs_human") {
|
||||
nextStatus = "needs_human_rewrite";
|
||||
} else {
|
||||
return NextResponse.json({ error: "Unknown action" }, { status: 400 });
|
||||
}
|
||||
|
||||
const { data, error } = await admin
|
||||
.from("ai_rewritten_articles")
|
||||
.update({
|
||||
status: nextStatus,
|
||||
reviewer_id: auth.userId,
|
||||
reviewer_note: note || null,
|
||||
reviewed_at: nowIso,
|
||||
updated_at: nowIso,
|
||||
...extras,
|
||||
})
|
||||
.eq("id", id)
|
||||
.select("id, original_submission_id")
|
||||
.single();
|
||||
|
||||
if (error || !data) return NextResponse.json({ error: error?.message || "Not found" }, { status: 500 });
|
||||
|
||||
if (action === "approve") {
|
||||
await admin
|
||||
.from("ai_original_submissions")
|
||||
.update({ published_rewrite_id: (data as any).id })
|
||||
.eq("id", (data as any).original_submission_id);
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true, id: (data as any).id, status: nextStatus });
|
||||
}
|
||||
Reference in New Issue
Block a user