Spaces:
Running
Running
| 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; |