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:
,
, , 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 , , 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;