import express from 'express'; import { generateCompletion } from '../ai_engine.js'; import { supabase } from '../config/supabaseClient.js'; const router = express.Router(); // ========================================== // 🧠 DYNAMIC SYSTEM PROMPT BUILDER // ========================================== const buildSystemPrompt = (personaName, personaPrompt) => ` You are an elite communication strategist, negotiator, and ghostwriter. The user is in an ONGOING conversation and needs you to write their NEXT reply. You will be given: - The recent conversation history (showing what each side has said so far) - The most recent message they received (the one they need to reply to) - Optionally, a screenshot of the chat Your job: write the PERFECT next message for the user to send. PERSONA TO ADOPT: Identity: ${personaName} Behavior & Tone: ${personaPrompt} STRICT RULES: 1. NEVER break character. 2. NEVER start with "As an AI..." or "Here is a response...". 3. Output ONLY the exact reply text — no labels, no quotes, no explanation. 4. Keep it concise and natural for modern texting/messaging. 5. Use the conversation history to stay contextually relevant. Do NOT reply generically. 6. If a screenshot is provided, read it carefully — it is the actual chat. Reply to the last visible message. 7. The reply must feel like a natural continuation of the conversation, not a cold opener. It should always be consistent with the chat history. 8. No hyphens (-) at all. `; // ========================================== // 🗄️ CACHE MANAGEMENT // ========================================== let formatsCache = null; let lastFormatsUpdate = 0; const CACHE_DURATION = 3600000; // ========================================== // 📝 CONVERSATION HISTORY FORMATTER // ========================================== // Converts the last N messages into a readable thread for the AI. // Images are noted but not sent (only the triggering image is sent as vision input). const formatHistory = (messages =[], maxMessages = 6) => { if (!messages.length) return null; const recent = messages.slice(-maxMessages); // last 6 messages for context const lines = recent.map(m => { const role = m.sender === 'user' ? 'ME' : 'THEM'; if (m.type === 'image') return `${role}: [sent a screenshot]`; return `${role}: ${m.content}`; }); return lines.join('\n'); }; // ========================================== // 🌐 ENDPOINTS // ========================================== /** * 1. FETCH OFFICIAL FORMATS */ router.get('/formats', async (req, res) => { try { const now = Date.now(); if (formatsCache && (now - lastFormatsUpdate < CACHE_DURATION)) { return res.json({ success: true, data: formatsCache, cached: true }); } const { data, error } = await supabase .from('replygenius_vibes') .select('id, icon, label, hint, color, mockReply, prompt') .order('created_at', { ascending: true }); if (error) throw error; formatsCache = data; lastFormatsUpdate = now; res.json({ success: true, data, cached: false }); } catch (err) { console.error('[FORMATS ERROR]', err.message); res.status(500).json({ success: false, error: err.message }); } }); /** * 2. GENERATE REPLY * Expects body: { * type: 'text' | 'image', * content: string, // current message text OR base64 image * persona: object, * history: Message[], // recent messages from the chat session * model: string // 'seed', 'mistral', or 'grok' * } */ router.post('/generate', async (req, res) => { try { // 💡 Extract 'model' from the request, default to 'seed' if missing const { type, content, persona, history =[], model = 'seed' } = req.body; if (!content) throw new Error('No content provided to analyze.'); if (!persona || !persona.label) throw new Error('Persona context is missing.'); console.log(`[REPLY GEN] Analyzing ${type} using persona: ${persona.label} | Model: ${model}`); const aiSystemPrompt = buildSystemPrompt( persona.label, persona.prompt || 'Be highly intelligent, strategic, polite, and persuasive.' ); // Build the conversation thread string (text messages only, images noted as placeholder) const conversationHistory = formatHistory(history, 6); // 💡 Map frontend model IDs to your actual ai_engine identifiers const MODEL_MAP = { 'seed': 'bytedance-1.6', 'mistral': 'mistral-small', // Update this to your actual Mistral engine ID if different 'grok': 'grok-4.3' // Update this to your actual Grok engine ID if different }; const modelToUse = MODEL_MAP[model] || MODEL_MAP['seed']; let imagesPayload =[]; let userPrompt = ''; if (type === 'image') { // Only send the triggering image to the vision model — prior images stay on device imagesPayload = [content]; userPrompt = conversationHistory ? `Here is our conversation so far:\n${conversationHistory}\n\nI've also attached the latest screenshot. Read it and write my next reply. Output ONLY the reply text.` : `Analyze this chat screenshot and write my next reply. Output ONLY the reply text.`; } else { userPrompt = conversationHistory ? `Here is our conversation so far:\n${conversationHistory}\n\nTheir latest message: "${content}"\n\nWrite my next reply. Output ONLY the reply text, nothing else.` : `Write my reply to this message. Output ONLY the reply text, nothing else.\n\nMessage:\n"${content}"`; } const aiResult = await generateCompletion({ model: modelToUse, prompt: userPrompt, system_prompt: aiSystemPrompt, images: imagesPayload, forceJson: false, }); if (!aiResult.success) throw new Error(aiResult.error); let finalReply = (aiResult.data || '').trim(); // Strip any wrapping quotes the model might add if (finalReply.startsWith('"') && finalReply.endsWith('"')) { finalReply = finalReply.slice(1, -1).trim(); } // Strip markdown bold/italic wrappers finalReply = finalReply.replace(/^\*+|\*+$/g, '').trim(); if (!finalReply) throw new Error('AI returned an empty reply.'); res.json({ success: true, reply: finalReply }); } catch (err) { console.error('[REPLY GEN ERROR]', err.message); res.status(500).json({ success: false, error: err.message }); } }); /** * 3. FORCE CACHE CLEAR (Admin) */ router.post('/clear-cache', (req, res) => { formatsCache = null; lastFormatsUpdate = 0; res.json({ success: true, message: 'Formats cache cleared.' }); }); export default router;