Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Thank you for your interest in contributing to the Useless Facts App! This docum
1. **Fork the repository** on GitHub
2. **Clone your fork** locally:
```bash
git clone https://github.com/YOUR_USERNAME/useless-app.git
git clone https://github.com/werther41/useless-app.git
cd useless-app
```
3. **Install dependencies**:
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ This project demonstrates the power of AI vibe coding throughout the entire deve
1. **Clone the repository**

```bash
git clone https://github.com/yourusername/useless-facts-app.git
git clone https://github.com/werther41/useless-app.git
cd useless-facts-app
```

Expand Down Expand Up @@ -403,7 +403,7 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
If you have questions or need help:

1. Check the [documentation](documents/)
2. Open an [issue](https://github.com/yourusername/useless-facts-app/issues)
2. Open an [issue](https://github.com/werther41/useless-app/issues)
3. Review the [API documentation](documents/api-docs.md)

---
Expand Down
58 changes: 48 additions & 10 deletions app/api/facts/real-time/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@ import { createGoogleGenerativeAI } from "@ai-sdk/google"
import { streamText } from "ai"
import { nanoid } from "nanoid"

import { db } from "@/lib/db"
import { generateEmbedding } from "@/lib/embeddings"
import { createFact } from "@/lib/facts"
import { initializeDatabase } from "@/lib/init-db"
import { SYSTEM_PROMPT, createUserPrompt } from "@/lib/prompts"
import {
buildSystemPrompt,
createUserPrompt,
TonePreset,
} from "@/lib/prompts"
import { getRandomQueryText } from "@/lib/query-texts"
import {
findArticlesByTopics,
Expand All @@ -28,22 +33,42 @@ export async function POST(request: NextRequest) {
// Initialize database if needed
await initializeDatabase()

// Parse request body for selected topics
// Parse request body for selected topics and tone
let selectedTopics: string[] = []
let matchedTopics: string[] = []
let tone: TonePreset | null = null
let articleId: string | null = null

try {
const body = await request.json()
selectedTopics = body.selectedTopics || []
tone = body.tone || null
articleId = body.articleId || null
} catch (error) {
// If no body or invalid JSON, continue with empty topics (fallback to random)
console.log("No selected topics provided, using random query")
console.log("No request body provided, using random query")
}

let article: any = null

// Try topic-based search first if topics are provided
if (selectedTopics.length > 0) {
// If articleId is provided (for regenerate), fetch that specific article
if (articleId) {
try {
const result = await db.execute(
`SELECT * FROM news_articles WHERE id = ?`,
[articleId]
)
if (result.rows.length > 0) {
article = result.rows[0]
console.log(`📰 Using provided article ID: ${articleId}`)
}
} catch (error) {
console.log(`❌ Error fetching article by ID: ${articleId}`, error)
}
}

// Try topic-based search first if topics are provided and no articleId
if (!article && selectedTopics.length > 0) {
console.log(
`🎯 Searching for articles with topics: ${selectedTopics.join(", ")}`
)
Expand Down Expand Up @@ -124,11 +149,19 @@ export async function POST(request: NextRequest) {
})

// Create the prompt for generating a fun fact
const systemPrompt = SYSTEM_PROMPT
const userPrompt = createUserPrompt(article.title, article.content)
const systemPrompt = buildSystemPrompt(tone)
const userPrompt = createUserPrompt(
article.title,
article.content,
matchedTopics.length > 0 ? matchedTopics : undefined,
!!articleId // isRegenerate if articleId was provided
)

console.log(`🤖 System Prompt: "${systemPrompt}"`)
console.log(`💬 User Prompt: "${userPrompt}"`)
console.log(`🤖 System Prompt: "${systemPrompt.substring(0, 100)}..."`)
console.log(`💬 User Prompt: "${userPrompt.substring(0, 100)}..."`)
if (tone) {
console.log(`🎭 Tone: ${tone}`)
}

// Generate the streaming response using Gemini
console.log(`🚀 Starting AI generation with Gemini...`)
Expand All @@ -139,7 +172,9 @@ export async function POST(request: NextRequest) {
})

console.log(`✅ AI generation completed, creating streaming response...`)
console.log(`📝 Expected JSON format: {"funFact": "..."}`)
console.log(
`📝 Expected JSON format: {"funFact": "...", "whyInteresting": "...", "sourceSnippet": "..."}`
)

// Create response with metadata
const response = result.toTextStreamResponse()
Expand All @@ -164,6 +199,9 @@ export async function POST(request: NextRequest) {
response.headers.set("X-Matched-Topics", matchedTopics.join(", "))
}

// Add article ID header for regenerate functionality
response.headers.set("X-Article-ID", article.id)

// Add cache control headers to prevent caching issues
response.headers.set(
"Cache-Control",
Expand Down
150 changes: 150 additions & 0 deletions app/api/facts/regenerate/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { NextRequest, NextResponse } from "next/server"
import { createGoogleGenerativeAI } from "@ai-sdk/google"
import { streamText } from "ai"

import { db } from "@/lib/db"
import { initializeDatabase } from "@/lib/init-db"
import {
buildSystemPrompt,
createUserPrompt,
TonePreset,
} from "@/lib/prompts"

// Force dynamic rendering
export const dynamic = "force-dynamic"
export const revalidate = 0

// Initialize the Google Generative AI provider
const google = createGoogleGenerativeAI({
apiKey: process.env.GOOGLE_API_KEY!,
})

export async function POST(request: NextRequest) {
try {
// Initialize database if needed
await initializeDatabase()

// Parse request body
let articleId: string | null = null
let tone: TonePreset | null = null

try {
const body = await request.json()
articleId = body.articleId || null
tone = body.tone || null

if (!articleId) {
return NextResponse.json(
{
success: false,
error: "articleId is required",
},
{ status: 400 }
)
}
} catch (error) {
return NextResponse.json(
{
success: false,
error: "Invalid request body",
},
{ status: 400 }
)
}

// Fetch the article by ID
const result = await db.execute(
`SELECT * FROM news_articles WHERE id = ?`,
[articleId]
)

if (result.rows.length === 0) {
return NextResponse.json(
{
success: false,
error: "Article not found",
},
{ status: 404 }
)
}

const article = result.rows[0]

console.log(`📰 Regenerating fact for article: ${articleId}`)
console.log(`🎭 Tone: ${tone || "default"}`)

// Create the prompt for generating a new fact with different angle
const systemPrompt = buildSystemPrompt(tone)
const userPrompt = createUserPrompt(
article.title as string,
article.content as string,
undefined, // No matched topics for regenerate
true // isRegenerate = true
)

console.log(`🤖 System Prompt: "${systemPrompt.substring(0, 100)}..."`)
console.log(`💬 User Prompt: "${userPrompt.substring(0, 100)}..."`)

// Generate the streaming response using Gemini
console.log(`🚀 Starting AI generation with Gemini...`)
const aiResult = await streamText({
model: google("models/gemini-2.0-flash-lite"),
system: systemPrompt,
prompt: userPrompt,
})

console.log(`✅ AI generation completed, creating streaming response...`)
console.log(
`📝 Expected JSON format: {"funFact": "...", "whyInteresting": "...", "sourceSnippet": "..."}`
)

// Create response with metadata
const response = aiResult.toTextStreamResponse()

// Add custom headers with article metadata (sanitize for HTTP headers)
response.headers.set(
"X-Article-Source",
(article.source as string).replace(/[^\x00-\x7F]/g, "")
)
response.headers.set("X-Article-URL", article.url as string)
response.headers.set(
"X-Article-Title",
(article.title as string).replace(/[^\x00-\x7F]/g, "")
)
response.headers.set(
"X-Article-Date",
(article.published_at as string) || (article.created_at as string)
)
response.headers.set("X-Article-ID", article.id as string)

// Add cache control headers to prevent caching issues
response.headers.set(
"Cache-Control",
"no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0, s-maxage=0"
)
response.headers.set("Pragma", "no-cache")
response.headers.set("Expires", "0")
response.headers.set("Surrogate-Control", "no-store")
response.headers.set("CDN-Cache-Control", "no-store")
response.headers.set("Vercel-CDN-Cache-Control", "no-store")
response.headers.set("Cloudflare-CDN-Cache-Control", "no-store")

// Add timestamp to prevent caching
response.headers.set("X-Timestamp", Date.now().toString())

return response
} catch (error) {
console.error("Error regenerating fact:", error)
return NextResponse.json(
{
success: false,
error:
error instanceof Error
? error.message
: "Failed to regenerate fact",
},
{ status: 500 }
)
}
}

18 changes: 17 additions & 1 deletion app/api/facts/save-realtime/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,15 @@ export async function POST(request: NextRequest) {
await initializeDatabase()

const body = await request.json()
const { text, source, source_url } = body
const {
text,
source,
source_url,
why_interesting,
source_snippet,
tone,
article_id,
} = body

// Validate the input
if (!text || typeof text !== "string") {
Expand All @@ -36,6 +44,10 @@ export async function POST(request: NextRequest) {
source: source || null,
source_url: source_url || null,
fact_type: "realtime",
why_interesting: why_interesting || null,
source_snippet: source_snippet || null,
tone: tone || null,
article_id: article_id || null,
})

console.log(`💾 Saved real-time fact to database: ${factId}`)
Expand All @@ -48,6 +60,10 @@ export async function POST(request: NextRequest) {
source: savedFact.source,
source_url: savedFact.source_url,
fact_type: savedFact.fact_type,
why_interesting: savedFact.why_interesting,
source_snippet: savedFact.source_snippet,
tone: savedFact.tone,
article_id: savedFact.article_id,
created_at: savedFact.created_at,
updated_at: savedFact.updated_at,
},
Expand Down
23 changes: 23 additions & 0 deletions components/fact-page-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,29 @@ export function FactPageClient({ fact }: FactPageClientProps) {
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* Why It's Interesting */}
{currentFact.why_interesting && (
<div className="rounded-lg border border-primary/20 bg-primary/5 p-4">
<p className="mb-1 text-sm font-semibold text-primary">
Why it&apos;s interesting:
</p>
<p className="text-sm text-foreground">
{currentFact.why_interesting}
</p>
</div>
)}

{/* Source Snippet */}
{currentFact.source_snippet && (
<div className="rounded-lg border border-muted bg-muted/30 p-4">
<p className="mb-1 text-sm font-semibold text-muted-foreground">
Source snippet:
</p>
<p className="text-sm italic text-muted-foreground">
&quot;{currentFact.source_snippet}&quot;
</p>
</div>
)}
{/* Rating Display */}
<div className="flex items-center justify-center gap-4">
<div className="flex items-center gap-2">
Expand Down
Loading