Major changes
This commit is contained in:
@@ -0,0 +1,252 @@
|
||||
import { NextRequest } from 'next/server';
|
||||
import { prisma } from '@/lib/db';
|
||||
|
||||
interface WikipediaSummary {
|
||||
title: string;
|
||||
extract: string;
|
||||
thumbnail?: { source: string };
|
||||
originalimage?: { source: string };
|
||||
}
|
||||
|
||||
interface WikiParseResult {
|
||||
parse: {
|
||||
title: string;
|
||||
text: { '*': string };
|
||||
images: string[];
|
||||
};
|
||||
}
|
||||
|
||||
async function getWikipediaSummary(title: string): Promise<WikipediaSummary | null> {
|
||||
try {
|
||||
const apiTitle = title.replace(/%20/g, '_');
|
||||
const response = await fetch(
|
||||
`https://en.wikipedia.org/api/rest_v1/page/summary/${apiTitle}`,
|
||||
{
|
||||
headers: { 'User-Agent': 'WikiPlus/1.0' },
|
||||
}
|
||||
);
|
||||
if (!response.ok) return null;
|
||||
return await response.json();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function getWikipediaContent(title: string): Promise<{ text: string; images: string[] } | null> {
|
||||
try {
|
||||
const apiTitle = title.replace(/%20/g, '_');
|
||||
const response = await fetch(
|
||||
`https://en.wikipedia.org/w/api.php?action=parse&page=${encodeURIComponent(apiTitle)}&format=json&prop=text|images&disableeditsection=true&redirects=true`,
|
||||
{
|
||||
headers: { 'User-Agent': 'WikiPlus/1.0' },
|
||||
}
|
||||
);
|
||||
if (!response.ok) return null;
|
||||
const data: WikiParseResult = await response.json();
|
||||
if (!data.parse) return null;
|
||||
|
||||
// Extract plain text from HTML
|
||||
const html = data.parse.text?.['*'] || '';
|
||||
const plainText = html
|
||||
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
||||
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
||||
.replace(/<[^>]+>/g, ' ')
|
||||
.replace(/ /g, ' ')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
|
||||
// Get image URLs
|
||||
const images = (data.parse.images || [])
|
||||
.filter((img: string) => !img.includes('icon') && !img.includes('logo') && !img.includes('Commons-logo'))
|
||||
.slice(0, 5)
|
||||
.map((img: string) => `https://en.wikipedia.org/wiki/Special:FilePath/${encodeURIComponent(img)}`);
|
||||
|
||||
return { text: plainText.slice(0, 15000), images };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const title = searchParams.get('title');
|
||||
|
||||
if (!title) {
|
||||
return new Response('Missing title parameter', { status: 400 });
|
||||
}
|
||||
|
||||
// Normalize the slug for database lookup
|
||||
const slug = title.replace(/%20/g, '_').replace(/ /g, '_');
|
||||
|
||||
// Check if article exists in database
|
||||
const existingArticle = await prisma.article.findUnique({
|
||||
where: { slug },
|
||||
});
|
||||
|
||||
if (existingArticle) {
|
||||
// Return cached article wrapped in <article> tags
|
||||
const cachedContent = `<article>${existingArticle.content}</article>`;
|
||||
return new Response(cachedContent, {
|
||||
headers: {
|
||||
'Content-Type': 'text/plain; charset=utf-8',
|
||||
'X-Cache': 'HIT',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const apiKey = process.env.OPENROUTER_API_KEY;
|
||||
if (!apiKey) {
|
||||
return new Response('OpenRouter API key not configured', { status: 500 });
|
||||
}
|
||||
|
||||
// Fetch Wikipedia data
|
||||
const [summary, content] = await Promise.all([
|
||||
getWikipediaSummary(title),
|
||||
getWikipediaContent(title),
|
||||
]);
|
||||
|
||||
if (!summary || !content) {
|
||||
return new Response('Article not found', { status: 404 });
|
||||
}
|
||||
|
||||
const imageUrl = summary.thumbnail?.source || summary.originalimage?.source;
|
||||
const imageContext = imageUrl
|
||||
? `\n\nMain image available: ${imageUrl}\nAdditional images: ${content.images.join(', ')}`
|
||||
: content.images.length > 0
|
||||
? `\n\nImages available: ${content.images.join(', ')}`
|
||||
: '';
|
||||
|
||||
const systemPrompt = `You are an expert writer creating comprehensive, in-depth articles for an AI-powered Wikipedia alternative. Your goal is to transform encyclopedia content into rich, detailed, and engaging prose that thoroughly covers the topic.
|
||||
|
||||
Guidelines:
|
||||
- Write a COMPREHENSIVE and DETAILED article - aim for depth and thoroughness
|
||||
- Cover ALL major aspects of the topic: history, significance, key details, related concepts, and impact
|
||||
- Use markdown formatting extensively:
|
||||
- Use ## for main sections and ### for subsections
|
||||
- Use **bold** for key terms and *italic* for emphasis
|
||||
- Use bullet lists and numbered lists where appropriate
|
||||
- Use > blockquotes for notable quotes or key facts
|
||||
- Include the main image at the top using markdown: 
|
||||
- Structure the article with multiple well-developed sections (5-8 sections minimum)
|
||||
- Each section should have multiple paragraphs with detailed explanations
|
||||
- Add context that helps readers understand why this topic matters
|
||||
- Include interesting facts, historical context, and connections to broader themes
|
||||
- Maintain factual accuracy - expand on the source material but don't invent facts
|
||||
- Write at least 1000-1500 words for a thorough treatment of the topic
|
||||
- Output ONLY the article content wrapped in <article></article> tags
|
||||
- Do not include any text outside the <article> tags`;
|
||||
|
||||
const userPrompt = `Write a comprehensive, detailed article about "${summary.title}" based on this Wikipedia content:
|
||||
|
||||
Summary: ${summary.extract}
|
||||
|
||||
Full content: ${content.text}${imageContext}
|
||||
|
||||
Requirements:
|
||||
- Write a THOROUGH article with 5-8 well-developed sections minimum
|
||||
- Each section should have multiple detailed paragraphs
|
||||
- Cover history, significance, key facts, and broader context
|
||||
- Use rich markdown formatting throughout
|
||||
- Aim for 1000-1500+ words total
|
||||
- Output in markdown wrapped in <article></article> tags`;
|
||||
|
||||
// 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: 'minimax/minimax-m2.1',
|
||||
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 generate article', { status: 500 });
|
||||
}
|
||||
|
||||
// Transform the OpenRouter SSE stream to extract content and save to DB
|
||||
const encoder = new TextEncoder();
|
||||
const decoder = new TextDecoder();
|
||||
let fullContent = '';
|
||||
|
||||
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) {
|
||||
fullContent += contentChunk;
|
||||
controller.enqueue(encoder.encode(contentChunk));
|
||||
}
|
||||
} catch {
|
||||
// Skip invalid JSON
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
async flush() {
|
||||
// Extract content between <article> tags and save to database
|
||||
const articleMatch = fullContent.match(/<article>([\s\S]*?)<\/article>/);
|
||||
const articleContent = articleMatch ? articleMatch[1].trim() : fullContent;
|
||||
|
||||
if (articleContent) {
|
||||
try {
|
||||
const newArticle = await prisma.article.create({
|
||||
data: {
|
||||
slug,
|
||||
title: summary.title,
|
||||
content: articleContent,
|
||||
imageUrl: imageUrl || null,
|
||||
},
|
||||
});
|
||||
|
||||
// Create initial history entry
|
||||
await prisma.articleHistory.create({
|
||||
data: {
|
||||
articleId: newArticle.id,
|
||||
oldContent: '',
|
||||
newContent: articleContent,
|
||||
reason: 'Initial article',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to save article to database:', error);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
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',
|
||||
'X-Cache': 'MISS',
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import type { WikipediaTopic } from '@/lib/types';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const response = await fetch(
|
||||
'https://en.wikipedia.org/api/rest_v1/page/random/summary',
|
||||
{
|
||||
headers: {
|
||||
'User-Agent': 'TeachMeSomethingNew/1.0',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch from Wikipedia API');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const topic: WikipediaTopic = {
|
||||
title: data.title,
|
||||
extract: data.extract,
|
||||
imageUrl: data.thumbnail?.source || data.originalimage?.source,
|
||||
pageUrl: data.content_urls?.desktop?.page,
|
||||
};
|
||||
|
||||
return NextResponse.json(topic);
|
||||
} catch (error) {
|
||||
console.error('Error fetching random topic:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch random topic' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { prisma } from '@/lib/db';
|
||||
|
||||
export interface SearchResult {
|
||||
title: string;
|
||||
description: string;
|
||||
thumbnail?: string;
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const query = searchParams.get('query');
|
||||
|
||||
if (!query) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Query parameter is required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// Use the Wikipedia Action API for search
|
||||
const response = await fetch(
|
||||
`https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch=${encodeURIComponent(query)}&srlimit=20&format=json&origin=*`,
|
||||
{
|
||||
headers: {
|
||||
'User-Agent': 'WikiPlus/1.0',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch from Wikipedia API');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Get page IDs for thumbnail fetching
|
||||
const searchResults = data.query?.search || [];
|
||||
const pageIds = searchResults.map((r: { pageid: number }) => r.pageid).join('|');
|
||||
|
||||
// Fetch thumbnails for results
|
||||
let thumbnails: Record<string, string> = {};
|
||||
if (pageIds) {
|
||||
const thumbResponse = await fetch(
|
||||
`https://en.wikipedia.org/w/api.php?action=query&pageids=${pageIds}&prop=pageimages&pithumbsize=200&format=json&origin=*`,
|
||||
{
|
||||
headers: {
|
||||
'User-Agent': 'WikiPlus/1.0',
|
||||
},
|
||||
}
|
||||
);
|
||||
if (thumbResponse.ok) {
|
||||
const thumbData = await thumbResponse.json();
|
||||
const pages = thumbData.query?.pages || {};
|
||||
for (const pageId in pages) {
|
||||
if (pages[pageId].thumbnail) {
|
||||
thumbnails[pageId] = pages[pageId].thumbnail.source;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const results: SearchResult[] = searchResults.map((item: {
|
||||
pageid: number;
|
||||
title: string;
|
||||
snippet: string;
|
||||
}) => ({
|
||||
title: item.title,
|
||||
description: item.snippet.replace(/<[^>]*>/g, ''), // Strip HTML tags from snippet
|
||||
thumbnail: thumbnails[item.pageid.toString()],
|
||||
}));
|
||||
|
||||
// Log the search
|
||||
try {
|
||||
await prisma.search.create({
|
||||
data: {
|
||||
query,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error logging search:', error);
|
||||
// Don't fail the search if logging fails
|
||||
}
|
||||
|
||||
return NextResponse.json({ results });
|
||||
} catch (error) {
|
||||
console.error('Error searching Wikipedia:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to search Wikipedia' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { prisma } from '@/lib/db';
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
const [totalArticles, searchesToday, allArticles] = await Promise.all([
|
||||
prisma.article.count(),
|
||||
prisma.search.count({
|
||||
where: {
|
||||
createdAt: {
|
||||
gte: today,
|
||||
},
|
||||
},
|
||||
}),
|
||||
prisma.article.findMany({
|
||||
select: {
|
||||
content: true,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const totalWords = allArticles.reduce((sum, article) => {
|
||||
const words = article.content.split(/\s+/).filter(word => word.length > 0).length;
|
||||
return sum + words;
|
||||
}, 0);
|
||||
|
||||
return NextResponse.json({
|
||||
totalArticles,
|
||||
searchesToday,
|
||||
totalWords,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching stats:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch statistics' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user