File size: 6,658 Bytes
23bbcba
 
 
 
 
 
 
 
 
 
fdb5d73
 
 
 
 
 
 
 
 
23bbcba
fdb5d73
 
23bbcba
 
 
fdb5d73
23bbcba
fdb5d73
 
 
 
05995d7
 
23bbcba
 
 
c696f2f
23bbcba
 
 
c696f2f
23bbcba
fdb5d73
 
 
 
 
620707d
fdb5d73
 
 
 
 
 
 
 
 
 
 
 
23bbcba
 
 
 
 
 
 
 
c696f2f
 
 
 
23bbcba
c696f2f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23bbcba
 
 
c696f2f
fdb5d73
 
 
 
620707d
 
fdb5d73
23bbcba
 
c696f2f
620707d
 
c696f2f
 
 
 
620707d
c696f2f
 
 
 
 
 
fdb5d73
 
 
620707d
 
 
bc8b2d0
 
620707d
 
 
 
 
c696f2f
 
 
fdb5d73
c696f2f
fdb5d73
 
 
 
 
c696f2f
fdb5d73
 
 
c696f2f
 
 
 
 
 
 
fdb5d73
c696f2f
 
 
 
 
 
 
 
 
23bbcba
fdb5d73
c696f2f
 
 
 
 
 
 
 
 
 
23bbcba
 
 
c696f2f
23bbcba
 
c696f2f
 
 
23bbcba
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
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;