Major changes

This commit is contained in:
owenqwenstarsky
2026-01-24 16:52:19 -06:00
parent 011e40119d
commit 7181791734
46 changed files with 8371 additions and 127 deletions
+34
View File
@@ -0,0 +1,34 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/db';
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ slug: string }> }
) {
const { slug } = await params;
// Fetch article to get its ID
const article = await prisma.article.findUnique({
where: { slug },
select: { id: true },
});
if (!article) {
return NextResponse.json({ error: 'Article not found' }, { status: 404 });
}
// Fetch history entries
const history = await prisma.articleHistory.findMany({
where: { articleId: article.id },
orderBy: { createdAt: 'desc' },
select: {
id: true,
oldContent: true,
newContent: true,
reason: true,
createdAt: true,
},
});
return NextResponse.json(history);
}
+147
View File
@@ -0,0 +1,147 @@
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',
},
});
}
+58
View File
@@ -0,0 +1,58 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/db';
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ slug: string }> }
) {
const { slug } = await params;
const body = await request.json();
const { newContent, reason } = body;
if (!newContent || typeof newContent !== 'string') {
return NextResponse.json({ error: 'Missing newContent' }, { status: 400 });
}
if (!reason || typeof reason !== 'string') {
return NextResponse.json({ error: 'Missing reason' }, { status: 400 });
}
// Fetch current article
const article = await prisma.article.findUnique({
where: { slug },
});
if (!article) {
return NextResponse.json({ error: 'Article not found' }, { status: 404 });
}
// Transaction: create history entry and update article
const result = await prisma.$transaction(async (tx) => {
// Create history entry
const historyEntry = await tx.articleHistory.create({
data: {
articleId: article.id,
oldContent: article.content,
newContent: newContent,
reason: reason,
},
});
// Update article content
await tx.article.update({
where: { id: article.id },
data: {
content: newContent,
updatedAt: new Date(),
},
});
return historyEntry;
});
return NextResponse.json({
success: true,
historyId: result.id,
});
}