// NRGROUP — Gemini-powered digital assistant with function-calling lead capture. // When the AI determines the user has provided name + phone, it triggers // send_lead_to_company → POST to Make.com webhook → Telegram + email. const NRGROUP_INFO = { name: "NRGROUP", about: "Η τεχνική εταιρία NRGROUP είναι μια σύγχρονη δυναμική εταιρία με εξειδικευμένους συνεργάτες, που αναλαμβάνει την ποιοτική αναβάθμιση της ζωής σας με ολοκληρωμένες λύσεις σε υδραυλικά, φυσικό αέριο, θέρμανση και ανακαινίσεις.", area: "Εξυπηρετούμε όλη την Αττική.", phones: ["6977262898", "2104611065"], email: "info@nrgroup.gr", address: "Θεσσαλονίκης 39, Τ.Κ. 18545, Πειραιάς", facebook: "https://www.facebook.com/nrgroup.gr", }; // Make.com webhook — forwards to Telegram + email (configured in your Make.com scenario). // If you ever rotate this, update the URL only. const LEAD_WEBHOOK_URL = "https://hook.eu1.make.com/bcwaqkhkbt5bfv3dutut76q4dlaakqr0"; const SYSTEM_PROMPT = `Είσαι ο ψηφιακός βοηθός της τεχνικής εταιρίας NRGROUP (Energy & Plumbing). Η εταιρία εδρεύει στον Πειραιά (Θεσσαλονίκης 39) και προσφέρει: - Υδραυλικές εργασίες & εγκαταστάσεις - Θέρμανση & Φυσικό Αέριο (καυστήρες, λέβητες) - Ανακαινίσεις οικιών & καταστημάτων - Έκτακτες επεμβάσεις 24/7 (διαρροές, εμφράξεις) - Πλακάκια & δάπεδα, Αλουμινοκατασκευές - Ηλιακά, εγκαταστάσεις πυρόσβεσης - Συμπληρωματικά: ηλεκτρολογικά, κλιματισμός, γύψινα, ελαιοχρωματισμοί, ξυλουργικά, μονώσεις Στοιχεία επικοινωνίας: ${NRGROUP_INFO.phones[0]}, ${NRGROUP_INFO.phones[1]}, ${NRGROUP_INFO.email} ΟΔΗΓΙΕΣ ΣΥΜΠΕΡΙΦΟΡΑΣ ΚΑΙ ΡΟΗΣ: 1. Να είσαι πάρα πολύ ευγενικός, προσιτός και επαγγελματίας. Να δείχνεις ενσυναίσθηση στο πρόβλημα του πελάτη. 2. Κάνε 2-3 απλές, σχετικές ερωτήσεις (ΜΙΑ-ΜΙΑ κάθε φορά, διαλογικά) για να κατανοήσεις καλά το πρόβλημα και την περιοχή. Π.χ. αν λέει «στάζει μια σωλήνα», ρώτα πόσο καιρό υπάρχει το πρόβλημα. Μετά, σε ποια περιοχή. Απόφυγε να μοιάζεις με ρομπότ ανάκρισης — θέλουμε ανθρώπινη και ζεστή προσέγγιση! 3. Αφού κάνεις 2-3 ερωτήσεις συνολικά και έχεις μια καλή εικόνα για το Πρόβλημα και την Περιοχή, **σταμάτα τις ερωτήσεις για τεχνικά θέματα**. 4. Στη συνέχεια, πες με ευγένεια κάτι σαν: «Σας ευχαριστώ για τις πληροφορίες. Για να κλείσουμε ένα ραντεβού και να σας εξυπηρετήσει άμεσα ο τεχνικός μας, παρακαλώ γράψτε μου το ονοματεπώνυμό σας και ένα τηλέφωνο επικοινωνίας.» 5. ΟΤΑΝ ο πελάτης σου έχει δώσει ξεκάθαρα ΟΝΟΜΑ και ΤΗΛΕΦΩΝΟ, ΠΡΕΠΕΙ να καλέσεις ΑΜΕΣΩΣ τη συνάρτηση send_lead_to_company. ΑΥΤΟ ΕΙΝΑΙ ΥΠΟΧΡΕΩΤΙΚΟ. ΜΗΝ γράψεις δικό σου confirmation κείμενο (όπως «Σας ευχαριστούμε, καταγράψαμε...»). Το σύστημα θα παράγει αυτόματα το μήνυμα επιβεβαίωσης μετά την επιτυχημένη κλήση. Αν γράψεις δικό σου confirmation χωρίς function call, ο πελάτης ΔΕΝ θα λάβει εξυπηρέτηση. 6. ΠΟΤΕ μην καλείς τη συνάρτηση αν δεν έχεις και τα δύο: όνομα + τηλέφωνο. 6. Για ΕΠΕΙΓΟΝΤΑ (διαρροή νερού, διαρροή αερίου, μπλοκαρισμένη αποχέτευση, λέβητας εκτός λειτουργίας τον χειμώνα) πες ΑΜΕΣΩΣ: «Πρόκειται για επείγον. Παρακαλώ καλέστε άμεσα στο ${NRGROUP_INFO.phones[0]} — διαθέσιμο 24/7.» Αν είναι διαρροή αερίου, πρόσθεσε οδηγία να κλείσει τον κεντρικό διακόπτη αερίου και να ανοίξει παράθυρα. 7. ΔΕΝ δίνεις ποτέ συγκεκριμένες τιμές. Λες «η ακριβής τιμή εξαρτάται από την εργασία — μπορούμε να σας δώσουμε δωρεάν εκτίμηση κατόπιν επικοινωνίας». 8. Δεν χρησιμοποιείς markdown formatting. Γράφεις σε φυσική, ρέουσα ελληνική γλώσσα.`; const GEMINI_API_KEY = "AIzaSyASlvt7ayd4zhDevE4TFZ5M2Bt7zTiY-Hw"; // Fallback chain — if one model is rate-limited or denied, try the next. const GEMINI_MODELS = [ "gemini-2.5-flash", "gemini-2.0-flash", "gemini-flash-latest", ]; const geminiEndpoint = (model) => `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${GEMINI_API_KEY}`; const STARTER_MESSAGES = [ "Έχω διαρροή στο μπάνιο", "Πόσο κοστίζει η συντήρηση καυστήρα;", "Θέλω εκτίμηση για ανακαίνιση", "Εγκατάσταση φυσικού αερίου", ]; // Tool definition for Gemini function calling const LEAD_TOOL = { functionDeclarations: [{ name: "send_lead_to_company", description: "Στέλνει τα στοιχεία του πελάτη στην NRGROUP για ραντεβού. Κάλεσέ την μόνο όταν έχεις ξεκάθαρα όνομα ΚΑΙ τηλέφωνο.", parameters: { type: "OBJECT", properties: { name: { type: "STRING", description: "Ονοματεπώνυμο πελάτη" }, phone: { type: "STRING", description: "Τηλέφωνο επικοινωνίας" }, issue: { type: "STRING", description: "Σύντομη περιγραφή του προβλήματος ή της εργασίας" }, area: { type: "STRING", description: "Περιοχή του ακινήτου (αν έχει αναφερθεί)" } }, required: ["name", "phone", "issue"] } }] }; function looksLikeFakeConfirmation(text) { // Detect when AI hallucinated a "lead sent" confirmation in plain text // instead of actually calling the function. if (!text) return false; // Must mention thanking or recording the request const hasGratitude = /(ευχαρι|καταγραψ|καταγραφ|αιτημα σας|αίτημά σας|ραντεβού)/i.test(text); // AND mention something forward-looking about contact const hasFollowup = /(θα επικοινωνη|θα σας καλεσ|θα σας πάρει|συνεργάτης|τεχνικός μας|εντός λίγων|σύντομα μαζί σας)/i.test(text); return hasGratitude && hasFollowup; } // Validate a Greek phone number — 10 digits starting with 2 (landline) or 6 (mobile) function isValidPhone(s) { if (!s) return false; const digits = String(s).replace(/[^\d]/g, ""); return /^[26]\d{9}$/.test(digits); } // Validate a name — at least 3 chars, contains at least 2 letters, no digits-only function isValidName(s) { if (!s) return false; const t = String(s).trim(); if (t.length < 3) return false; if (/^\d+$/.test(t)) return false; // must contain at least 2 alphabetic characters (Greek or Latin) const letters = (t.match(/[\p{L}]/gu) || []).length; if (letters < 2) return false; // reject obvious fake/placeholder values const lo = t.toLowerCase(); const blocklist = ["unknown", "άγνωστο", "πελάτης", "client", "none", "null", "test"]; if (blocklist.includes(lo)) return false; // reject if it's just a location name we recognize (common AI hallucination) const locations = ["πειραιάς", "πειραιά", "αθήνα", "θεσσαλονίκη", "σμύρνη", "άλιμος", "φάληρο", "κερατσίνι", "νίκαια", "ταύρος", "κορυδαλλός"]; if (locations.includes(lo)) return false; return true; } // Check if conversation history actually contains a phone number from the user function hasPhoneInHistory(history) { return history.some((m) => m.role === "user" && /(?:^|[^\d])([26]\d{9})(?:[^\d]|$)/.test(m.text.replace(/\s+/g, ""))); } async function geminiCall(history, forceFunctionCall = false) { const contents = history .filter((m) => m.role === "user" || m.role === "model") .map((m) => ({ role: m.role, parts: [{ text: m.text }], })); const body = { systemInstruction: { parts: [{ text: SYSTEM_PROMPT }] }, contents, tools: [LEAD_TOOL], generationConfig: { temperature: forceFunctionCall ? 0.2 : 0.7, topP: 0.9, maxOutputTokens: 800, }, safetySettings: [ { category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_ONLY_HIGH" }, { category: "HARM_CATEGORY_HATE_SPEECH", threshold: "BLOCK_ONLY_HIGH" }, ], }; if (forceFunctionCall) { body.toolConfig = { functionCallingConfig: { mode: "ANY", allowedFunctionNames: ["send_lead_to_company"], }, }; } let lastError = null; for (const model of GEMINI_MODELS) { try { const res = await fetch(geminiEndpoint(model), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); if (!res.ok) { const t = await res.text(); lastError = { status: res.status, body: t, model }; if (res.status === 429 || res.status === 403 || res.status === 404) continue; throw new Error(`Gemini ${res.status}: ${t.slice(0, 200)}`); } const data = await res.json(); const parts = data?.candidates?.[0]?.content?.parts ?? []; const text = parts.filter((p) => p.text).map((p) => p.text).join(""); const fnCall = parts.find((p) => p.functionCall)?.functionCall ?? null; return { text: text.trim(), fnCall }; } catch (e) { lastError = { status: "network", error: String(e), model }; continue; } } const err = new Error("ALL_MODELS_FAILED"); err.detail = lastError; throw err; } async function sendLeadToCompany({ name, phone, issue, area, transcript }) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 10000); try { const res = await fetch(LEAD_WEBHOOK_URL, { method: "POST", headers: { "Content-Type": "application/json" }, signal: controller.signal, body: JSON.stringify({ name, phone, issue: issue || "Δεν διευκρινίστηκε πλήρως", area: area || "Δεν αναφέρθηκε", transcript, timestamp: new Date().toLocaleString("el-GR", { timeZone: "Europe/Athens" }), source: "nrgroup.gr", }), }); clearTimeout(timeout); return res.ok; } catch (e) { clearTimeout(timeout); console.error("Webhook error:", e); return false; } } function extractActions(text) { const t = (text || "").toLowerCase(); const actions = []; if ( t.includes("επείγον") || t.includes("καλέστε") || t.includes("καλέσετε") || t.includes("24/7") ) { actions.push({ label: `Κλήση τώρα ${NRGROUP_INFO.phones[0]}`, href: `tel:${NRGROUP_INFO.phones[0]}`, }); } return actions; } function buildTranscript(messages) { return messages .filter((m) => m.role === "user" || m.role === "model") .map((m) => (m.role === "user" ? "Πελάτης: " : "Βοηθός: ") + m.text) .join("\n"); } function ChatPanel({ open, onClose }) { const [messages, setMessages] = React.useState([ { role: "model", text: "Καλώς ήρθατε στην NRGROUP! Είμαι ο ψηφιακός σας βοηθός. Πώς μπορώ να σας βοηθήσω σήμερα;", }, ]); const [input, setInput] = React.useState(""); const [loading, setLoading] = React.useState(false); const [error, setError] = React.useState(null); const bodyRef = React.useRef(null); const inputRef = React.useRef(null); React.useEffect(() => { if (bodyRef.current) { bodyRef.current.scrollTop = bodyRef.current.scrollHeight; } }, [messages, loading]); React.useEffect(() => { if (open && inputRef.current) inputRef.current.focus(); }, [open]); async function send(text) { const trimmed = (text ?? input).trim(); if (!trimmed || loading) return; setError(null); const next = [...messages, { role: "user", text: trimmed }]; setMessages(next); setInput(""); setLoading(true); try { let result = await geminiCall(next); // SAFETY NET: only force function call if (a) AI wrote fake confirmation // AND (b) the user has actually shared a phone number in the conversation. if (!result.fnCall && looksLikeFakeConfirmation(result.text) && hasPhoneInHistory(next)) { console.warn("[NRGROUP] AI wrote fake confirmation. Forcing function call retry."); try { const forced = await geminiCall(next, true); if (forced.fnCall) result = forced; } catch (e) { console.error("Forced function call failed:", e); } } if (result.fnCall && result.fnCall.name === "send_lead_to_company") { const a = result.fnCall.args || {}; const name = (a.name || "").toString().trim(); const phone = (a.phone || "").toString().trim(); const issue = (a.issue || "").toString().trim(); const area = (a.area || "").toString().trim(); // STRICT validation — reject anything that looks fabricated const validName = isValidName(name); const validPhone = isValidPhone(phone); const phoneInHistory = hasPhoneInHistory(next); if (!validName || !validPhone || !phoneInHistory) { console.warn("[NRGROUP] Rejected lead — invalid/fabricated data:", { name, phone, validName, validPhone, phoneInHistory }); setMessages([ ...next, { role: "model", text: "Για να μπορέσει ο τεχνικός μας να επικοινωνήσει μαζί σας, παρακαλώ γράψτε μου το ονοματεπώνυμό σας και ένα τηλέφωνο επικοινωνίας (10 ψηφία)." } ]); } else { // Normalize phone (strip spaces/dashes) const cleanPhone = phone.replace(/[^\d]/g, ""); const transcript = buildTranscript(next); const ok = await sendLeadToCompany({ name, phone: cleanPhone, issue, area, transcript }); const confirm = ok ? `Σας ευχαριστούμε, ${name}! Τα στοιχεία σας καταγράφηκαν επιτυχώς. Ένας εξειδικευμένος τεχνικός μας θα επικοινωνήσει σύντομα μαζί σας στο ${cleanPhone}.` : `Υπήρξε ένα τεχνικό πρόβλημα με την καταχώρηση. Παρακαλώ καλέστε μας απευθείας στο ${NRGROUP_INFO.phones[0]} και θα σας εξυπηρετήσουμε άμεσα.`; setMessages([...next, { role: "model", text: confirm, leadSent: ok }]); } } else if (result.text) { setMessages([...next, { role: "model", text: result.text }]); } else { setMessages([...next, { role: "model", text: "Συγγνώμη, δεν μπόρεσα να καταλάβω. Μπορείτε να επαναδιατυπώσετε;" }]); } } catch (e) { console.error("Gemini error:", e, e.detail); const fallback = `Λυπάμαι, ο ψηφιακός βοηθός δεν είναι διαθέσιμος αυτή τη στιγμή.\n\nΓια άμεση εξυπηρέτηση καλέστε μας στο ${NRGROUP_INFO.phones[0]} (24/7 για επείγοντα) ή στείλτε email στο ${NRGROUP_INFO.email}.`; setMessages([...next, { role: "model", text: fallback }]); } finally { setLoading(false); } } function onKeyDown(e) { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); } } if (!open) return null; const lastBot = [...messages].reverse().find((m) => m.role === "model"); const actions = lastBot && !lastBot.leadSent ? extractActions(lastBot.text) : []; const showStarters = messages.length === 1; return (
NR
Ψηφιακός Βοηθός NRGROUP Σε σύνδεση
{messages.map((m, i) => (
{m.text}
))} {loading && (
)} {error &&
{error}
} {!loading && actions.length > 0 && (
{actions.map((a, i) => ( ))}
)}
{showStarters && (
{STARTER_MESSAGES.map((s, i) => ( ))}
)}