Initial commit
This commit is contained in:
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user