Files
Sizzy e22a8dc248 chnages
chnages
2026-07-01 01:10:13 +05:45

121 lines
4.9 KiB
JavaScript

// ============================================================
// chatController.js — AI assistant proxy (Google Gemini)
// ============================================================
// The browser NEVER calls Gemini directly and never sees the API
// key. The widget posts a message here; this controller forwards
// it to the Gemini free-tier API using the server-side
// GEMINI_API_KEY and returns the assistant's reply.
//
// Escalation: the model is instructed to append the marker
// [[ESCALATE]] when a human representative should take over
// (pricing negotiation, complaints, account/legal issues, or
// anything it cannot confidently answer). We strip the marker and
// surface an `escalate` flag so the UI can offer the Contact page.
// ============================================================
const axios = require('axios');
const GEMINI_MODEL = process.env.GEMINI_MODEL || 'gemini-2.5-flash';
const GEMINI_URL =
`https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent`;
// Persona / knowledge for the assistant.
const SYSTEM_PROMPT = `You are "Ava", the AI virtual assistant for AI-Solutions, a Sunderland-based
start-up that uses artificial intelligence to improve the digital employee experience. AI-Solutions
offers AI-powered virtual assistants, rapid affordable prototyping, and bespoke software solutions
that speed up design, engineering and innovation for industry clients.
Your job: answer visitor questions about the company, its services, past projects, events and how to
get in touch. Be concise (2-4 sentences), warm, and professional. Never invent specific prices,
contracts, or personal data.
Escalate to a human by ending your reply with the exact marker [[ESCALATE]] when: the visitor asks
for pricing/quotes, wants to book or schedule a demo, raises a complaint, asks something you cannot
answer confidently, or explicitly asks to speak to a person. When you escalate, briefly tell them a
representative will help and suggest the Contact Us form.`;
// Keyword fallback so escalation still works if the model forgets the marker.
const ESCALATION_HINTS = [
'human', 'representative', 'agent', 'real person', 'speak to someone',
'price', 'pricing', 'quote', 'cost', 'complaint', 'refund', 'book a demo',
'schedule a demo', 'sales',
];
function looksLikeEscalation(text) {
const t = (text || '').toLowerCase();
return ESCALATION_HINTS.some(k => t.includes(k));
}
// POST /api/chat body: { message, history?: [{role, text}] }
async function chat(req, res, next) {
try {
const message = (req.body.message || '').toString().trim();
if (!message) {
return res.status(400).json({ message: 'Please enter a message.' });
}
if (message.length > 1000) {
return res.status(400).json({ message: 'Message is too long (max 1000 characters).' });
}
// Graceful degradation when the key isn't configured.
if (!process.env.GEMINI_API_KEY) {
return res.json({
reply:
"I'm not fully online right now, but our team would love to help. " +
'Please use the Contact Us page and someone will get back to you shortly.',
escalate: true,
});
}
// Build conversation history (last 8 turns) in Gemini's format.
const history = Array.isArray(req.body.history) ? req.body.history.slice(-8) : [];
const contents = history
.filter(h => h && h.text)
.map(h => ({
role: h.role === 'assistant' || h.role === 'model' ? 'model' : 'user',
parts: [{ text: String(h.text).slice(0, 1000) }],
}));
contents.push({ role: 'user', parts: [{ text: message }] });
const { data } = await axios.post(
`${GEMINI_URL}?key=${process.env.GEMINI_API_KEY}`,
{
system_instruction: { parts: [{ text: SYSTEM_PROMPT }] },
contents,
generationConfig: { temperature: 0.6, maxOutputTokens: 400, topP: 0.9 },
},
{ headers: { 'Content-Type': 'application/json' }, timeout: 20000 }
);
let reply =
data?.candidates?.[0]?.content?.parts?.map(p => p.text).join(' ').trim() || '';
let escalate = false;
if (reply.includes('[[ESCALATE]]')) {
escalate = true;
reply = reply.replace(/\[\[ESCALATE\]\]/g, '').trim();
}
// Fallback signal from the user's own words.
if (!escalate && looksLikeEscalation(message)) escalate = true;
if (!reply) {
reply =
"I'm sorry, I didn't quite catch that. Could you rephrase, or reach our team via the Contact Us page?";
escalate = true;
}
return res.json({ reply, escalate });
} catch (err) {
// Don't leak provider errors to the client — degrade gracefully.
console.error('[Chat] Gemini error:', err.response?.data?.error?.message || err.message);
return res.json({
reply:
"I'm having trouble reaching my AI service right now. Our team can help directly — " +
'please use the Contact Us page and we will be in touch.',
escalate: true,
});
}
}
module.exports = { chat };