148 lines
4.4 KiB
TypeScript
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',
|
|
},
|
|
});
|
|
}
|