Initial commit

This commit is contained in:
Haitham Khalifa
2026-02-16 12:02:45 +01:00
commit 11252e6520
37 changed files with 8118 additions and 0 deletions
+176
View File
@@ -0,0 +1,176 @@
import { NextResponse } from "next/server";
import { GoogleGenerativeAI } from "@google/generative-ai";
import { siteMenteKnowledge } from "../../../../lib/sitemente-knowledge";
type Message = {
role: "user" | "assistant";
content: string;
};
type RequestBody = {
message?: string;
locale?: "es" | "en";
history?: Message[];
};
export const runtime = "nodejs";
const getClient = () => {
const apiKey = process.env.GEMINI_API_KEY;
if (!apiKey) {
throw new Error("GEMINI_API_KEY is not set.");
}
return new GoogleGenerativeAI(apiKey);
};
const buildSystemPrompt = (locale: "es" | "en") => {
const knowledge = JSON.stringify(siteMenteKnowledge, null, 2);
if (locale === "en") {
return [
"You are the smart brain of SiteMente, the first agency with a fully living website. You speak in a natural, professional but friendly tone.",
"",
"YOUR COMPLETE KNOWLEDGE:",
knowledge,
"",
"PERSONALITY:",
"- Enthusiastic but not over the top",
"- Use real data: '67% sales increase', 'positive ROI in month one', 'restaurant in Marbella increased bookings by 43%'",
"- Give concrete Costa del Sol examples",
"- If you don't know something exactly, admit it: 'Let me connect you with the team for that'",
"- Look for natural lead capture opportunities, never forced",
"",
"CONVERSATION HANDLING:",
"- First 2-3 responses: answer general questions WITHOUT asking for personal data",
"- If user asks pricing/services/how it works: explain clearly",
"- If user shows high intent (asks for demo, implementation, wants to start):",
" -> Then ask for name and email: 'I'd love to help more. Could you share your name and email so I can send detailed info?'",
"- If user gives contact info: thank them and offer to schedule a demo",
"- If user is only exploring: keep the conversation useful, no pressure",
"",
"TONE:",
"- Short, clear sentences",
"- Occasionally use a relevant emoji (💡 🚀 ✨ 🎯)",
"- Natural, like a friendly expert consultant",
"",
"RESPONSE FORMAT:",
"Return STRICT JSON ONLY with keys: response (string), shouldCaptureEmail (boolean), suggestedActions (array of strings).",
"Max 3-4 sentences in response so it is easy to listen to.",
"Always respond in English.",
].join("\n");
}
return [
"Eres el cerebro inteligente de SiteMente, la primera agencia con una web completamente viva. Hablas de forma natural, profesional pero cercana.",
"",
"TU CONOCIMIENTO COMPLETO:",
knowledge,
"",
"TU PERSONALIDAD:",
"- Entusiasta pero no exagerado",
"- Usas datos reales: '67% aumento en ventas', 'ROI en primer mes', 'restaurante en Marbella aumentó 43% reservas'",
"- Das ejemplos concretos de Costa del Sol",
"- Si no sabes algo exacto, lo admites: 'Déjame conectarte con el equipo para eso'",
"- Buscas oportunidades naturales para captar leads, nunca forzado",
"",
"MANEJO DE CONVERSACIÓN:",
"- Primeras 2-3 respuestas: Responde preguntas generales SIN pedir datos",
"- Si usuario pregunta pricing/servicios/cómo funciona: Explica con claridad",
"- Si usuario muestra interés alto (pregunta por demo, implementación, quiere empezar):",
" → Entonces pide nombre y email: 'Me encantaría ayudarte más. ¿Me compartes tu nombre y email para enviarte info detallada?'",
"- Si usuario da info de contacto: Agradece y ofrece agendar demo",
"- Si usuario parece solo explorar: Mantén conversación útil, no presiones",
"",
"TONO:",
"- Frases cortas y claras",
"- Ocasionalmente emoji relevante (💡 🚀 ✨ 🎯)",
"- Natural, como un consultor experto amigable",
"",
"FORMATO DE RESPUESTA:",
"Devuelve SOLO JSON ESTRICTO con claves: response (string), shouldCaptureEmail (boolean), suggestedActions (array of strings).",
"Máximo 3-4 frases por respuesta para que sea fácil de escuchar en voz.",
"Responde SIEMPRE en español.",
].join("\n");
};
const extractJson = (text: string) => {
const trimmed = text.trim();
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
return trimmed;
}
const match = trimmed.match(/\{[\s\S]*\}/);
return match ? match[0] : null;
};
export async function POST(request: Request) {
try {
const body = (await request.json()) as RequestBody;
if (!body.message || typeof body.message !== "string") {
return NextResponse.json(
{ error: "message is required." },
{ status: 400 }
);
}
const locale = body.locale === "en" ? "en" : "es";
const history = Array.isArray(body.history) ? body.history : [];
const model = getClient().getGenerativeModel({
model: "gemini-1.5-flash",
systemInstruction: buildSystemPrompt(locale),
});
const contents = [
...history.map((message) => ({
role: message.role === "assistant" ? "model" : "user",
parts: [{ text: message.content }],
})),
{ role: "user", parts: [{ text: body.message }] },
];
const result = await model.generateContent({
contents,
});
const rawText =
result.response?.text?.() ??
result.response?.candidates?.[0]?.content?.parts
?.map((part) => part.text ?? "")
.join("")
.trim() ??
"";
const jsonPayload = extractJson(rawText);
if (!jsonPayload) {
return NextResponse.json(
{
response: rawText || "Lo siento, hubo un problema generando respuesta.",
shouldCaptureEmail: false,
suggestedActions: [],
},
{ status: 200 }
);
}
const parsed = JSON.parse(jsonPayload) as {
response?: string;
shouldCaptureEmail?: boolean;
suggestedActions?: string[];
};
return NextResponse.json({
response: parsed.response ?? rawText,
shouldCaptureEmail: Boolean(parsed.shouldCaptureEmail),
suggestedActions: Array.isArray(parsed.suggestedActions)
? parsed.suggestedActions
: [],
});
} catch (error) {
console.error("[SiteMente][API] Agent route failed", error);
return NextResponse.json(
{ error: "Failed to generate response." },
{ status: 500 }
);
}
}
+46
View File
@@ -0,0 +1,46 @@
import { NextResponse } from "next/server";
import { runSiteMenteText } from "../../../../lib/ai/siteMenteAgent";
type MessagePayload = {
role: "user" | "assistant" | "system";
content: string;
};
const isValidMessage = (message: MessagePayload) => {
return (
message &&
typeof message.content === "string" &&
["user", "assistant", "system"].includes(message.role)
);
};
export const runtime = "nodejs";
export async function POST(request: Request) {
try {
const body = (await request.json()) as { messages?: MessagePayload[] };
if (!Array.isArray(body.messages) || body.messages.length === 0) {
return NextResponse.json(
{ error: "messages array is required." },
{ status: 400 }
);
}
if (!body.messages.every(isValidMessage)) {
return NextResponse.json(
{ error: "messages must include role and content." },
{ status: 400 }
);
}
const { reply } = await runSiteMenteText({ messages: body.messages });
return NextResponse.json({ reply });
} catch (error) {
console.error("[SiteMente][API] Text route failed", error);
return NextResponse.json(
{ error: "Failed to generate response." },
{ status: 500 }
);
}
}
+29
View File
@@ -0,0 +1,29 @@
import { NextResponse } from "next/server";
import { runSiteMenteVoiceTurn } from "../../../../lib/ai/siteMenteAgent";
export const runtime = "nodejs";
export async function POST(request: Request) {
try {
const body = (await request.json()) as { transcript?: string };
if (!body.transcript || typeof body.transcript !== "string") {
return NextResponse.json(
{ error: "transcript is required." },
{ status: 400 }
);
}
const response = await runSiteMenteVoiceTurn({
transcript: body.transcript,
});
return NextResponse.json(response);
} catch (error) {
console.error("[SiteMente][API] Voice route failed", error);
return NextResponse.json(
{ error: "Failed to generate voice response." },
{ status: 500 }
);
}
}
+102
View File
@@ -0,0 +1,102 @@
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700;800&display=swap");
@tailwind base;
@tailwind components;
@tailwind utilities;
html {
scroll-behavior: smooth;
}
body {
margin: 0;
min-height: 100vh;
background-color: #ffffff;
}
* {
box-sizing: border-box;
}
@keyframes float {
0% {
transform: translateY(0);
}
50% {
transform: translateY(-14px);
}
100% {
transform: translateY(0);
}
}
.float-slow {
animation: float 10s ease-in-out infinite;
}
.float-medium {
animation: float 7s ease-in-out infinite;
}
.float-fast {
animation: float 5s ease-in-out infinite;
}
.hero-hover {
position: relative;
}
.hero-voice-ring {
position: absolute;
left: 50%;
top: 52%;
width: 80px;
height: 80px;
border: 2px solid rgba(255, 255, 255, 0.7);
border-radius: 9999px;
transform: translate(-50%, -50%) scale(0.75);
opacity: 0;
pointer-events: none;
}
.hero-voice-ring--two {
width: 120px;
height: 120px;
border-color: rgba(255, 255, 255, 0.45);
}
.hero-voice-ring--three {
width: 160px;
height: 160px;
border-color: rgba(255, 255, 255, 0.3);
}
@media (hover: hover) and (pointer: fine) {
.hero-hover:hover .hero-voice-ring {
opacity: 1;
animation: heroVoicePulse 1.6s ease-out infinite;
}
.hero-hover:hover .hero-voice-ring--two {
animation-delay: 0.2s;
}
.hero-hover:hover .hero-voice-ring--three {
animation-delay: 0.4s;
}
}
@keyframes heroVoicePulse {
0% {
transform: translate(-50%, -50%) scale(0.75);
opacity: 0.65;
}
70% {
transform: translate(-50%, -50%) scale(1.2);
opacity: 0.1;
}
100% {
transform: translate(-50%, -50%) scale(1.3);
opacity: 0;
}
}
+21
View File
@@ -0,0 +1,21 @@
import "./globals.css";
export const metadata = {
title: "SiteMente | Agencia de Implementación de IA",
description:
"SiteMente ayuda a negocios locales en España con agentes de voz, chatbots, webs inteligentes y automatización.",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="es" suppressHydrationWarning>
<body className="bg-white" suppressHydrationWarning>
{children}
</body>
</html>
);
}
+1236
View File
File diff suppressed because it is too large Load Diff