226 lines
7.1 KiB
TypeScript
226 lines
7.1 KiB
TypeScript
import express from 'express';
|
|
import OpenAI from 'openai';
|
|
import { db } from './db';
|
|
import { settings } from './db/schema';
|
|
import { eq } from 'drizzle-orm';
|
|
|
|
const router = express.Router();
|
|
|
|
// System prompt that defines how the AI should generate articles
|
|
const SYSTEM_PROMPT = `You are an expert content writer creating high-quality blog articles for Ghost CMS.
|
|
|
|
CRITICAL REQUIREMENTS:
|
|
1. Generate production-ready HTML content that can be published directly to Ghost
|
|
2. Use semantic HTML5 tags: <h2>, <h3>, <p>, <ul>, <ol>, <blockquote>, <strong>, <em>
|
|
3. For images, use this EXACT placeholder format: {{IMAGE:description_of_image}}
|
|
- Example: {{IMAGE:screenshot_of_dashboard}}
|
|
- Example: {{IMAGE:team_photo_at_conference}}
|
|
- Use descriptive, snake_case names that indicate what the image should show
|
|
4. Structure articles with clear sections using headings
|
|
5. Write engaging, SEO-friendly content with natural keyword integration
|
|
6. Include a compelling introduction and conclusion
|
|
7. Use lists and formatting to improve readability
|
|
8. Do NOT include <html>, <head>, <body> tags - only the article content
|
|
9. Do NOT use markdown - use HTML tags only
|
|
10. Ensure all HTML is valid and properly closed
|
|
|
|
OUTPUT FORMAT:
|
|
Return only the HTML content, ready to be inserted into Ghost's content editor.`;
|
|
|
|
router.post('/generate', async (req, res) => {
|
|
try {
|
|
const {
|
|
prompt,
|
|
audioTranscriptions,
|
|
selectedImageUrls,
|
|
} = req.body as {
|
|
prompt: string;
|
|
audioTranscriptions?: string[];
|
|
selectedImageUrls?: string[];
|
|
};
|
|
|
|
if (!prompt) {
|
|
return res.status(400).json({ error: 'prompt is required' });
|
|
}
|
|
|
|
const apiKey = process.env.OPENAI_API_KEY;
|
|
if (!apiKey) {
|
|
return res.status(500).json({ error: 'OpenAI API key not configured' });
|
|
}
|
|
|
|
const openai = new OpenAI({ apiKey });
|
|
|
|
// Get system prompt from settings or use default
|
|
let systemPrompt = SYSTEM_PROMPT;
|
|
try {
|
|
const settingRows = await db
|
|
.select()
|
|
.from(settings)
|
|
.where(eq(settings.key, 'system_prompt'))
|
|
.limit(1);
|
|
if (settingRows.length > 0) {
|
|
systemPrompt = settingRows[0].value;
|
|
console.log('[AI Generate] Using custom system prompt from settings');
|
|
} else {
|
|
console.log('[AI Generate] Using default system prompt');
|
|
}
|
|
} catch (err) {
|
|
console.warn('[AI Generate] Failed to load system prompt from settings, using default:', err);
|
|
}
|
|
|
|
// Build context from audio transcriptions
|
|
let contextSection = '';
|
|
if (audioTranscriptions && audioTranscriptions.length > 0) {
|
|
contextSection += '\n\nAUDIO TRANSCRIPTIONS:\n';
|
|
audioTranscriptions.forEach((transcript, idx) => {
|
|
contextSection += `\n[Transcript ${idx + 1}]:\n${transcript}\n`;
|
|
});
|
|
}
|
|
|
|
// Add information about available images
|
|
if (selectedImageUrls && selectedImageUrls.length > 0) {
|
|
contextSection += '\n\nAVAILABLE IMAGES:\n';
|
|
contextSection += `You have ${selectedImageUrls.length} images available. Use {{IMAGE:description}} placeholders where images should be inserted.\n`;
|
|
}
|
|
|
|
const userPrompt = `${prompt}${contextSection}`;
|
|
|
|
console.log('[AI Generate] Starting generation with OpenAI...');
|
|
console.log('[AI Generate] User prompt length:', userPrompt.length);
|
|
|
|
const completion = await openai.chat.completions.create({
|
|
model: 'gpt-4o', // Using GPT-4 with internet access capability
|
|
messages: [
|
|
{
|
|
role: 'system',
|
|
content: systemPrompt,
|
|
},
|
|
{
|
|
role: 'user',
|
|
content: userPrompt,
|
|
},
|
|
],
|
|
temperature: 0.7,
|
|
max_tokens: 4000,
|
|
});
|
|
|
|
const generatedContent = completion.choices[0]?.message?.content || '';
|
|
|
|
if (!generatedContent) {
|
|
return res.status(500).json({ error: 'No content generated' });
|
|
}
|
|
|
|
console.log('[AI Generate] Generation successful, length:', generatedContent.length);
|
|
|
|
// Extract image placeholders for tracking
|
|
const imagePlaceholderRegex = /\{\{IMAGE:([^}]+)\}\}/g;
|
|
const imagePlaceholders: string[] = [];
|
|
let match;
|
|
while ((match = imagePlaceholderRegex.exec(generatedContent)) !== null) {
|
|
imagePlaceholders.push(match[1]);
|
|
}
|
|
|
|
return res.json({
|
|
content: generatedContent,
|
|
imagePlaceholders,
|
|
tokensUsed: completion.usage?.total_tokens || 0,
|
|
model: completion.model,
|
|
});
|
|
} catch (err: any) {
|
|
console.error('[AI Generate] Error:', err);
|
|
return res.status(500).json({
|
|
error: 'AI generation failed',
|
|
details: err?.message || 'Unknown error'
|
|
});
|
|
}
|
|
});
|
|
|
|
// Generate metadata (title, tags, canonical URL) from content
|
|
router.post('/generate-metadata', async (req, res) => {
|
|
try {
|
|
const { contentHtml } = req.body as { contentHtml: string };
|
|
|
|
if (!contentHtml) {
|
|
return res.status(400).json({ error: 'contentHtml is required' });
|
|
}
|
|
|
|
const apiKey = process.env.OPENAI_API_KEY;
|
|
if (!apiKey) {
|
|
return res.status(500).json({ error: 'OpenAI API key not configured' });
|
|
}
|
|
|
|
const openai = new OpenAI({ apiKey });
|
|
|
|
// Strip HTML tags for better analysis
|
|
const textContent = contentHtml.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
|
|
const preview = textContent.slice(0, 2000); // First 2000 chars
|
|
|
|
console.log('[AI Metadata] Generating metadata...');
|
|
|
|
const completion = await openai.chat.completions.create({
|
|
model: 'gpt-4o',
|
|
messages: [
|
|
{
|
|
role: 'system',
|
|
content: `You are an SEO expert. Generate metadata for blog posts.
|
|
|
|
REQUIREMENTS:
|
|
1. Title: Compelling, SEO-friendly, 50-60 characters
|
|
2. Tags: 3-5 relevant tags, comma-separated
|
|
3. Canonical URL: SEO-friendly slug based on title (lowercase, hyphens, no special chars)
|
|
|
|
OUTPUT FORMAT (JSON):
|
|
{
|
|
"title": "Your Compelling Title Here",
|
|
"tags": "tag1, tag2, tag3",
|
|
"canonicalUrl": "your-seo-friendly-slug"
|
|
}
|
|
|
|
Return ONLY valid JSON, no markdown, no explanation.`,
|
|
},
|
|
{
|
|
role: 'user',
|
|
content: `Generate metadata for this article:\n\n${preview}`,
|
|
},
|
|
],
|
|
temperature: 0.7,
|
|
max_tokens: 300,
|
|
});
|
|
|
|
const response = completion.choices[0]?.message?.content || '';
|
|
|
|
if (!response) {
|
|
return res.status(500).json({ error: 'No metadata generated' });
|
|
}
|
|
|
|
console.log('[AI Metadata] Raw response:', response);
|
|
|
|
// Parse JSON response
|
|
let metadata;
|
|
try {
|
|
// Remove markdown code blocks if present
|
|
const cleaned = response.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();
|
|
metadata = JSON.parse(cleaned);
|
|
} catch (parseErr) {
|
|
console.error('[AI Metadata] JSON parse error:', parseErr);
|
|
return res.status(500).json({ error: 'Failed to parse metadata response' });
|
|
}
|
|
|
|
console.log('[AI Metadata] Generated:', metadata);
|
|
|
|
return res.json({
|
|
title: metadata.title || '',
|
|
tags: metadata.tags || '',
|
|
canonicalUrl: metadata.canonicalUrl || '',
|
|
});
|
|
} catch (err: any) {
|
|
console.error('[AI Metadata] Error:', err);
|
|
return res.status(500).json({
|
|
error: 'Metadata generation failed',
|
|
details: err?.message || 'Unknown error'
|
|
});
|
|
}
|
|
});
|
|
|
|
export default router;
|