Files
wikiplus/app/api/article/[slug]/review/route.ts
T
2026-01-24 16:52:19 -06:00

148 lines
4.4 KiB
TypeScript

import { NextRequest } from 'next/server';
import { prisma } from '@/lib/db';
import * as Diff from 'diff';
function generateDiffText(oldContent: string, newContent: string): string {
const changes = Diff.diffLines(oldContent, newContent);
const diffLines: string[] = [];
for (const change of changes) {
const lines = change.value.split('\n').filter(line => line !== '');
for (const line of lines) {
if (change.added) {
diffLines.push(`+ ${line}`);
} else if (change.removed) {
diffLines.push(`- ${line}`);
}
}
}
return diffLines.join('\n');
}
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ slug: string }> }
) {
const { slug } = await params;
const body = await request.json();
const { newContent } = body;
if (!newContent || typeof newContent !== 'string') {
return new Response('Missing newContent', { status: 400 });
}
// Fetch current article
const article = await prisma.article.findUnique({
where: { slug },
});
if (!article) {
return new Response('Article not found', { status: 404 });
}
const apiKey = process.env.OPENROUTER_API_KEY;
if (!apiKey) {
return new Response('OpenRouter API key not configured', { status: 500 });
}
// Generate diff between old and new content
const diffText = generateDiffText(article.content, newContent);
const systemPrompt = `You are an article edit reviewer. Your job is to evaluate proposed changes (diffs) to articles.
You will be given:
1. The current article content (assume this is factually correct and the source of truth)
2. A diff showing the proposed changes (lines starting with "-" are removed, lines starting with "+" are added)
Evaluate ONLY the changes based on:
1. **Consistency** - Do the edits contradict or conflict with the existing article content?
2. **Quality** - Is the new/modified text well-written and grammatically correct?
3. **Relevance** - Are the changes relevant to the article topic?
4. **Appropriateness** - Is the content appropriate (no spam, vandalism, offensive content)?
Do NOT fact-check against external knowledge. Assume the original article is correct and evaluate whether the edits are consistent with it.
After your evaluation, you MUST output your decision in this exact format:
<action approve="true" reason="Your brief reason for approval" />
OR
<action approve="false" reason="Your brief reason for rejection" />
Be constructive in your feedback. If rejecting, explain what could be improved.`;
const userPrompt = `Please review the following article edit:
**Current Article:**
${article.content.slice(0, 6000)}
**Proposed Changes (diff):**
\`\`\`diff
${diffText}
\`\`\`
Evaluate only the changes shown in the diff and provide your decision.`;
// Call OpenRouter API with streaming
const openRouterResponse = await fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'openai/gpt-oss-120b',
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userPrompt },
],
stream: true,
}),
});
if (!openRouterResponse.ok) {
const error = await openRouterResponse.text();
console.error('OpenRouter error:', error);
return new Response('Failed to review article', { status: 500 });
}
// Transform the OpenRouter SSE stream
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const transformStream = new TransformStream({
async transform(chunk, controller) {
const text = decoder.decode(chunk);
const lines = text.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') {
continue;
}
try {
const parsed = JSON.parse(data);
const contentChunk = parsed.choices?.[0]?.delta?.content;
if (contentChunk) {
controller.enqueue(encoder.encode(contentChunk));
}
} catch {
// Skip invalid JSON
}
}
}
},
});
const stream = openRouterResponse.body?.pipeThrough(transformStream);
return new Response(stream, {
headers: {
'Content-Type': 'text/plain; charset=utf-8',
'Transfer-Encoding': 'chunked',
'Cache-Control': 'no-cache',
},
});
}