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
+2
View File
@@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto
+31
View File
@@ -0,0 +1,31 @@
# Dependencies
node_modules/
# Next.js
.next/
out/
# Environment
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Vite
dist/
# Debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# IDE
.idea/
.vscode/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
+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
+504
View File
@@ -0,0 +1,504 @@
"use client";
import { useEffect, useMemo, useRef, useState } from "react";
type SpeechRecognitionInstance = {
lang: string;
interimResults: boolean;
continuous: boolean;
onresult: ((event: { results: Array<{ 0?: { transcript?: string } }> }) => void) | null;
onerror: ((event: unknown) => void) | null;
onend: (() => void) | null;
start: () => void;
stop: () => void;
};
type SpeechRecognitionConstructor = new () => SpeechRecognitionInstance;
type ChatMessage = {
role: "user" | "assistant";
content: string;
timestamp: number;
};
type ApiResponse = {
response: string;
shouldCaptureEmail: boolean;
suggestedActions: string[];
};
type SiteMenteVoiceWidgetProps = {
initialLang?: "es" | "en";
};
const quickActions = {
es: [
{ label: "¿Cuánto cuesta?", icon: "💰" },
{ label: "Ver casos de éxito", icon: "🎯" },
{ label: "¿Cómo funciona?", icon: "⚙️" },
],
en: [
{ label: "Pricing?", icon: "💰" },
{ label: "Success stories", icon: "🎯" },
{ label: "How it works?", icon: "⚙️" },
],
} as const;
const initialGreeting = {
es: "Hola, soy el cerebro de SiteMente. ¿En qué te puedo ayudar hoy?",
en: "Hi, I'm the SiteMente brain. How can I help you today?",
} as const;
export default function SiteMenteVoiceWidget({
initialLang = "es",
}: SiteMenteVoiceWidgetProps) {
const [isOpen, setIsOpen] = useState(false);
const [lang, setLang] = useState<"es" | "en">(initialLang);
const [messages, setMessages] = useState<ChatMessage[]>([
{
role: "assistant",
content: initialGreeting[initialLang],
timestamp: Date.now(),
},
]);
const [input, setInput] = useState("");
const [voiceMode, setVoiceMode] = useState(true);
const [isRecording, setIsRecording] = useState(false);
const [isSpeaking, setIsSpeaking] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [transcript, setTranscript] = useState("");
const [speechSupported, setSpeechSupported] = useState(true);
const [showTooltip, setShowTooltip] = useState(false);
const recognitionRef = useRef<SpeechRecognitionInstance | null>(null);
const isRecordingRef = useRef(false);
const transcriptRef = useRef("");
const messagesEndRef = useRef<HTMLDivElement | null>(null);
const localeLabel = useMemo(
() => (lang === "es" ? "ES" : "EN"),
[lang]
);
useEffect(() => {
setLang(initialLang);
}, [initialLang]);
useEffect(() => {
const seen = window.localStorage.getItem("sitemente:voice-tooltip");
if (!seen) {
setShowTooltip(true);
window.localStorage.setItem("sitemente:voice-tooltip", "1");
const timeout = window.setTimeout(() => setShowTooltip(false), 4000);
return () => window.clearTimeout(timeout);
}
return undefined;
}, []);
useEffect(() => {
isRecordingRef.current = isRecording;
}, [isRecording]);
useEffect(() => {
transcriptRef.current = transcript;
}, [transcript]);
useEffect(() => {
const SpeechRecognitionImpl =
typeof window !== "undefined"
? ((window as typeof window & {
webkitSpeechRecognition?: SpeechRecognitionConstructor;
}).SpeechRecognition ||
(window as typeof window & {
webkitSpeechRecognition?: SpeechRecognitionConstructor;
}).webkitSpeechRecognition)
: undefined;
if (!SpeechRecognitionImpl) {
setSpeechSupported(false);
return;
}
const recognition = new SpeechRecognitionImpl();
recognition.lang = lang === "es" ? "es-ES" : "en-US";
recognition.interimResults = true;
recognition.continuous = false;
recognition.onresult = (event) => {
const result = Array.from(event.results)
.map((res) => res[0]?.transcript ?? "")
.join(" ");
setTranscript(result.trim());
};
recognition.onerror = () => {
setIsRecording(false);
};
recognition.onend = () => {
if (isRecordingRef.current) {
setIsRecording(false);
const finalTranscript = transcriptRef.current.trim();
if (finalTranscript) {
handleSend(finalTranscript);
}
setTranscript("");
}
};
recognitionRef.current = recognition;
}, [lang]);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages, isOpen]);
useEffect(() => {
if (!isSpeaking) return;
return () => {
window.speechSynthesis?.cancel();
};
}, [isSpeaking]);
useEffect(() => {
setMessages((prev) => {
if (prev.length === 0) return prev;
const updated = [...prev];
if (updated[0].role === "assistant") {
updated[0] = {
...updated[0],
content: initialGreeting[lang],
};
}
return updated;
});
}, [lang]);
const startRecording = () => {
if (!speechSupported || !recognitionRef.current) return;
setTranscript("");
setIsRecording(true);
recognitionRef.current.start();
};
const stopRecording = () => {
if (!recognitionRef.current) return;
recognitionRef.current.stop();
setIsRecording(false);
};
const speak = (text: string) => {
if (!("speechSynthesis" in window)) return;
window.speechSynthesis.cancel();
const utterance = new SpeechSynthesisUtterance(text);
utterance.lang = lang === "es" ? "es-ES" : "en-US";
utterance.onstart = () => setIsSpeaking(true);
utterance.onend = () => setIsSpeaking(false);
utterance.onerror = () => setIsSpeaking(false);
window.speechSynthesis.speak(utterance);
};
const handleSend = async (text: string) => {
if (!text.trim() || isLoading) return;
const userMessage: ChatMessage = {
role: "user",
content: text,
timestamp: Date.now(),
};
setMessages((prev) => [...prev, userMessage]);
setInput("");
setIsLoading(true);
try {
const response = await fetch("/api/chat/agent", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
message: text,
locale: lang,
history: messages.slice(-6),
}),
});
if (!response.ok) {
throw new Error("Failed to fetch response.");
}
const data = (await response.json()) as ApiResponse;
const assistantMessage: ChatMessage = {
role: "assistant",
content: data.response,
timestamp: Date.now(),
};
setMessages((prev) => [...prev, assistantMessage]);
if (voiceMode) {
speak(data.response);
}
} catch (error) {
const fallbackMessage: ChatMessage = {
role: "assistant",
content:
lang === "es"
? "Hubo un problema al responder. ¿Quieres intentarlo de nuevo?"
: "There was a problem responding. Want to try again?",
timestamp: Date.now(),
};
setMessages((prev) => [...prev, fallbackMessage]);
} finally {
setIsLoading(false);
}
};
const voiceIndicator = isRecording
? "🎤"
: isSpeaking
? "🔊"
: "🎤";
return (
<>
{!isOpen && (
<div className="fixed bottom-6 right-6 z-[9999] flex flex-col items-end gap-2">
<div className="relative">
<button
type="button"
onClick={() => setIsOpen(true)}
className="relative flex h-[68px] w-[68px] items-center justify-center rounded-full bg-gradient-to-br from-[#8B5CF6] to-[#EC4899] text-white shadow-lg transition hover:scale-110 hover:shadow-[0_12px_30px_rgba(236,72,153,0.45)]"
>
<div className="flex items-center gap-1">
<span className="h-3 w-1 rounded-full bg-white/80 animate-pulse" />
<span className="h-5 w-1 rounded-full bg-white/90 animate-pulse" />
<span className="h-4 w-1 rounded-full bg-white/80 animate-pulse" />
</div>
</button>
{showTooltip && (
<div className="absolute right-[76px] top-1/2 -translate-y-1/2 rounded-full bg-white px-3 py-1 text-xs font-semibold text-brand-purple-dark shadow-md">
{lang === "es" ? "Prueba la voz" : "Try voice"}
</div>
)}
<div className="absolute -top-6 right-2 rounded-full bg-white/20 px-2 py-1 text-[10px] font-semibold text-white backdrop-blur">
Demo
</div>
</div>
</div>
)}
{isOpen && (
<div className="fixed inset-0 z-[9999] flex items-end justify-end p-4 sm:p-6">
<div className="absolute inset-0 bg-black/50" onClick={() => setIsOpen(false)} />
<div className="relative z-10 flex h-full w-full max-w-[440px] flex-col overflow-hidden rounded-3xl border border-white/15 bg-[#4f3a78] shadow-[0_30px_80px_rgba(0,0,0,0.45)] sm:h-[700px]">
<div className="flex items-center justify-between bg-gradient-to-r from-[#6d4cc2] to-[#ff66b5] px-5 py-4 text-white">
<div>
<div className="flex items-center gap-2 text-lg font-semibold">
<span>SiteMente IA</span>
<span className="flex items-center gap-1">
<span className="h-2 w-1 rounded-full bg-white/80 animate-pulse" />
<span className="h-3 w-1 rounded-full bg-white/90 animate-pulse" />
<span className="h-2 w-1 rounded-full bg-white/80 animate-pulse" />
</span>
</div>
<p className="text-xs text-white/80">
{lang === "es" ? "El cerebro de tu web" : "Your website brain"}
</p>
</div>
<div className="flex items-center gap-3 text-lg">
<span>{voiceIndicator}</span>
<button
type="button"
onClick={() => setLang((prev) => (prev === "es" ? "en" : "es"))}
className="rounded-full border border-white/30 px-2 py-1 text-xs font-semibold"
>
{localeLabel}
</button>
<button
type="button"
onClick={() => setIsOpen(false)}
className="text-xl"
>
</button>
</div>
</div>
<div className="flex-1 overflow-y-auto px-5 py-4">
{messages.length === 1 && (
<div className="mb-4 flex flex-wrap gap-2">
{quickActions[lang].map((action) => (
<button
key={action.label}
type="button"
onClick={() => handleSend(action.label)}
className="rounded-full border border-white/20 bg-white/10 px-3 py-1 text-xs text-white/90 transition hover:bg-white/20"
>
{action.icon} {action.label}
</button>
))}
</div>
)}
<div className="space-y-4">
{messages.map((message) => (
<div
key={`${message.timestamp}-${message.role}`}
className={`group flex ${
message.role === "user" ? "justify-end" : "justify-start"
}`}
>
<div
className={`max-w-[80%] rounded-2xl px-4 py-3 text-sm shadow-md ${
message.role === "user"
? "bg-[#c4a1ff] text-[#3b1c66]"
: "bg-[#6a4bb0] text-white"
}`}
>
<div className="flex items-center gap-2">
{message.role === "assistant" && (
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-white/20 text-xs font-semibold">
SM
</span>
)}
<p>{message.content}</p>
</div>
<span className="mt-2 block text-[10px] text-white/60 opacity-0 transition group-hover:opacity-100">
{new Date(message.timestamp).toLocaleTimeString(
lang === "es" ? "es-ES" : "en-US",
{ hour: "2-digit", minute: "2-digit" }
)}
</span>
</div>
</div>
))}
{isLoading && (
<div className="flex items-center gap-2 text-white/70">
<span className="inline-flex h-2 w-2 animate-bounce rounded-full bg-white/70" />
<span className="inline-flex h-2 w-2 animate-bounce rounded-full bg-white/60 delay-150" />
<span className="inline-flex h-2 w-2 animate-bounce rounded-full bg-white/50 delay-300" />
</div>
)}
{isSpeaking && (
<div className="flex items-center gap-2 text-xs text-white/70">
<span className="flex items-center gap-1">
<span className="h-2 w-1 rounded-full bg-white/80 animate-pulse" />
<span className="h-3 w-1 rounded-full bg-white/90 animate-pulse" />
<span className="h-2 w-1 rounded-full bg-white/80 animate-pulse" />
</span>
{lang === "es" ? "Hablando..." : "Speaking..."}
</div>
)}
<div ref={messagesEndRef} />
</div>
</div>
<div className="border-t border-white/10 bg-[#4a3572] px-5 py-4">
{voiceMode ? (
<div className="flex flex-col items-center gap-3">
{transcript && (
<p className="w-full rounded-xl bg-white/10 px-4 py-2 text-sm text-white/90">
{transcript}
</p>
)}
<button
type="button"
onClick={() => {
if (isRecording) {
stopRecording();
return;
}
if (isSpeaking) {
window.speechSynthesis?.cancel();
setIsSpeaking(false);
return;
}
startRecording();
}}
className={`flex h-14 w-14 items-center justify-center rounded-full text-white transition ${
isRecording
? "bg-red-500 animate-pulse"
: isSpeaking
? "bg-blue-500 animate-pulse"
: "bg-white/20 hover:bg-white/30"
}`}
disabled={!speechSupported}
>
{isRecording ? "🔴" : isSpeaking ? "🔊" : "🎤"}
</button>
<p className="text-xs text-white/80">
{isRecording
? lang === "es"
? "Escuchando..."
: "Listening..."
: isSpeaking
? lang === "es"
? "Hablando..."
: "Speaking..."
: lang === "es"
? "Toca para hablar"
: "Tap to talk"}
</p>
{isSpeaking && (
<div className="h-1 w-full overflow-hidden rounded-full bg-white/10">
<div className="h-full w-1/2 animate-pulse rounded-full bg-brand-pink/80" />
</div>
)}
<div className="flex w-full items-center justify-between text-xs text-white/70">
<button
type="button"
onClick={() => setVoiceMode(false)}
className="flex items-center gap-1"
>
{lang === "es" ? "Texto" : "Text"}
</button>
<button
type="button"
onClick={() => {
window.speechSynthesis?.cancel();
setIsSpeaking(false);
}}
className="flex items-center gap-1"
>
{lang === "es" ? "Pausar" : "Pause"}
</button>
</div>
</div>
) : (
<div className="flex items-center gap-3">
<input
value={input}
onChange={(event) => setInput(event.target.value)}
placeholder={
lang === "es"
? "Escribe tu mensaje..."
: "Type your message..."
}
className="flex-1 rounded-full border border-white/20 bg-white/10 px-4 py-2 text-sm text-white placeholder:text-white/50 focus:border-white/50 focus:outline-none"
/>
<button
type="button"
onClick={() => handleSend(input)}
className="rounded-full bg-brand-pink px-4 py-2 text-sm font-semibold text-white"
>
</button>
<button
type="button"
onClick={() => setVoiceMode(true)}
className="rounded-full border border-white/20 px-3 py-2 text-white/80"
>
🎤
</button>
</div>
)}
{!speechSupported && (
<p className="mt-2 text-center text-xs text-white/60">
{lang === "es"
? "Tu navegador no soporta voz. Usa el modo texto."
: "Your browser doesn't support voice. Use text mode."}
</p>
)}
</div>
</div>
</div>
)}
</>
);
}
+26
View File
@@ -0,0 +1,26 @@
import Image from "next/image";
type HeroBannerProps = {
priority?: boolean;
};
export default function HeroBanner({ priority = true }: HeroBannerProps) {
return (
<section className="hero-hover w-full">
<div className="relative h-[320px] w-full overflow-hidden bg-[#cfd7ea] sm:h-[420px] lg:h-[560px]">
<Image
src="/SiteMente-Banner.jpg"
alt="SiteMente hero banner with AI voice visual"
fill
sizes="100vw"
priority={priority}
className="object-contain"
/>
<div className="pointer-events-none absolute inset-0 bg-gradient-to-b from-white/0 via-white/0 to-white/10" />
<div className="hero-voice-ring hero-voice-ring--one" />
<div className="hero-voice-ring hero-voice-ring--two" />
<div className="hero-voice-ring hero-voice-ring--three" />
</div>
</section>
);
}
+174
View File
@@ -0,0 +1,174 @@
import Image from "next/image";
import { useEffect, useMemo, useRef, useState } from "react";
type ImageSlide = {
type: "image";
id: string;
src: string;
alt: string;
};
type StatItem = {
headline: string;
description: string;
};
type StatsSlide = {
type: "stats";
id: string;
title: string;
items: StatItem[];
};
export type HeroSlide = ImageSlide | StatsSlide;
type HeroSliderProps = {
slides: HeroSlide[];
autoAdvanceMs?: number;
};
export default function HeroSlider({
slides,
autoAdvanceMs = 7000,
}: HeroSliderProps) {
const [activeIndex, setActiveIndex] = useState(0);
const [isPaused, setIsPaused] = useState(false);
const touchStartX = useRef<number | null>(null);
const touchDelta = useRef(0);
const maxIndex = slides.length - 1;
const goTo = (index: number) => {
if (index < 0) return setActiveIndex(maxIndex);
if (index > maxIndex) return setActiveIndex(0);
return setActiveIndex(index);
};
const next = () => goTo(activeIndex + 1);
const prev = () => goTo(activeIndex - 1);
const currentSlide = useMemo(() => slides[activeIndex], [slides, activeIndex]);
useEffect(() => {
if (isPaused || slides.length <= 1) return undefined;
const timer = window.setInterval(() => {
setActiveIndex((prevIndex) => (prevIndex + 1) % slides.length);
}, autoAdvanceMs);
return () => window.clearInterval(timer);
}, [autoAdvanceMs, isPaused, slides.length]);
const handleTouchStart = (event: React.TouchEvent<HTMLDivElement>) => {
touchStartX.current = event.touches[0]?.clientX ?? null;
touchDelta.current = 0;
};
const handleTouchMove = (event: React.TouchEvent<HTMLDivElement>) => {
if (touchStartX.current === null) return;
touchDelta.current =
(event.touches[0]?.clientX ?? touchStartX.current) - touchStartX.current;
};
const handleTouchEnd = () => {
const delta = touchDelta.current;
touchStartX.current = null;
touchDelta.current = 0;
if (Math.abs(delta) < 40) return;
if (delta > 0) prev();
if (delta < 0) next();
};
return (
<section
className="w-full"
onMouseEnter={() => setIsPaused(true)}
onMouseLeave={() => setIsPaused(false)}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
>
<div className="relative h-[320px] w-full overflow-hidden sm:h-[420px] lg:h-[560px]">
{currentSlide.type === "image" ? (
<Image
src={currentSlide.src}
alt={currentSlide.alt}
fill
sizes="100vw"
priority
className="object-cover"
/>
) : (
<div className="flex h-full w-full items-center justify-center bg-brand-purple-dark px-6">
<div className="mx-auto w-full max-w-5xl">
<p className="text-center text-sm font-semibold uppercase tracking-[0.2em] text-white/70">
{currentSlide.title}
</p>
<div className="mt-8 grid gap-6 text-center md:grid-cols-3">
{currentSlide.items.map((item) => (
<div
key={item.headline}
className="rounded-2xl border border-white/15 bg-white/10 p-6 backdrop-blur"
>
<p className="text-3xl font-bold text-brand-pink sm:text-4xl">
{item.headline}
</p>
<p className="mt-3 text-sm text-white/85">
{item.description}
</p>
</div>
))}
</div>
</div>
</div>
)}
<div className="pointer-events-none absolute inset-x-0 bottom-4 flex items-center justify-center gap-2">
{slides.map((slide, index) => (
<button
key={slide.id}
className={`pointer-events-auto h-2.5 w-2.5 rounded-full transition ${
index === activeIndex
? "bg-brand-pink"
: "bg-white/50 hover:bg-white"
}`}
onClick={() => goTo(index)}
aria-label={`Go to slide ${index + 1}`}
/>
))}
</div>
{slides.length > 1 && (
<>
<button
className="absolute left-4 top-1/2 hidden -translate-y-1/2 rounded-full border border-white/30 bg-white/10 px-3 py-2 text-sm font-semibold text-white transition hover:bg-white/20 md:block"
onClick={prev}
aria-label="Previous slide"
>
</button>
<button
className="absolute right-4 top-1/2 hidden -translate-y-1/2 rounded-full border border-white/30 bg-white/10 px-3 py-2 text-sm font-semibold text-white transition hover:bg-white/20 md:block"
onClick={next}
aria-label="Next slide"
>
</button>
<button
className="absolute left-3 top-1/2 -translate-y-1/2 rounded-full border border-white/40 bg-white/10 px-3 py-2 text-sm font-semibold text-white transition hover:bg-white/20 md:hidden"
onClick={prev}
aria-label="Previous slide"
>
</button>
<button
className="absolute right-3 top-1/2 -translate-y-1/2 rounded-full border border-white/40 bg-white/10 px-3 py-2 text-sm font-semibold text-white transition hover:bg-white/20 md:hidden"
onClick={next}
aria-label="Next slide"
>
</button>
</>
)}
</div>
</section>
);
}
+230
View File
@@ -0,0 +1,230 @@
"use client";
import Script from "next/script";
type JsonLdSchemasProps = {
lang: "es" | "en";
};
const ORG_ID = "https://sitemente.com/#organization";
const LOCAL_ID = "https://sitemente.com/#localbusiness";
const SERVICES_ID = "https://sitemente.com/#services";
const FAQ_ID = "https://sitemente.com/#faq";
export default function JsonLdSchemas({ lang }: JsonLdSchemasProps) {
const isEs = lang === "es";
const organization = {
"@context": "https://schema.org",
"@type": "Organization",
"@id": ORG_ID,
name: "SiteMente",
url: "https://sitemente.com",
logo: "https://sitemente.com/logo.png",
description: isEs
? "Agencia de implementación de IA para negocios locales en España. Especialistas en chatbots, automatización y sitios web inteligentes."
: "AI implementation agency for local businesses in Spain. Specialists in chatbots, automation and smart websites.",
email: "hola@sitemente.com",
foundingDate: "2025",
parentOrganization: {
"@type": "Organization",
name: "HolaCompi",
},
areaServed: {
"@type": "Country",
name: "España",
},
};
const localBusiness = {
"@context": "https://schema.org",
"@type": "ProfessionalService",
"@id": LOCAL_ID,
name: "SiteMente - Agencia de IA Málaga",
url: "https://sitemente.com",
priceRange: "€€€",
address: {
"@type": "PostalAddress",
addressLocality: "Málaga",
addressRegion: "Andalucía",
addressCountry: "ES",
},
geo: {
"@type": "GeoCoordinates",
latitude: 36.7213,
longitude: -4.4214,
},
areaServed: [
{ "@type": "Place", name: "Málaga" },
{ "@type": "Place", name: "Marbella" },
{ "@type": "Place", name: "Benalmádena" },
{ "@type": "Place", name: "Costa del Sol" },
],
parentOrganization: { "@id": ORG_ID },
};
const serviceDescriptions = isEs
? [
{
name: "Smart Starter - AI Chat + Reservas Automatizadas",
description:
"Chat IA para sitio web con reservas automatizadas, captura de leads y análisis básicos.",
audience: "Restaurantes, clínicas, negocios locales",
price: 299,
},
{
name: "Smart Site - Web Inteligente Completa",
description:
"Sitio web Next.js completo con IA integrada que vende, reserva y da soporte en español e inglés.",
audience: "Negocios listos para una web moderna",
price: 749,
},
{
name: "AI Growth Partner",
description:
"Partner estratégico de IA: multicanal (web, WhatsApp, llamadas), integraciones CRM.",
audience: "Grupos de restaurantes, inmobiliarias, cadenas de alquiler",
price: 1950,
},
]
: [
{
name: "Smart Starter - AI Chat + Automated Bookings",
description:
"AI website chat with automated bookings, lead capture, and basic analytics.",
audience: "Restaurants, clinics, local businesses",
price: 299,
},
{
name: "Smart Site - Full Smart Website",
description:
"Complete Next.js website with integrated AI that sells, books, and supports in Spanish and English.",
audience: "Businesses ready for a modern website",
price: 749,
},
{
name: "AI Growth Partner",
description:
"Strategic AI partner: multichannel (web, WhatsApp, calls), CRM integrations.",
audience: "Restaurant groups, real estate teams, rental chains",
price: 1950,
},
];
const services = {
"@context": "https://schema.org",
"@type": "ItemList",
"@id": SERVICES_ID,
itemListElement: serviceDescriptions.map((service, index) => ({
"@type": "ListItem",
position: index + 1,
item: {
"@type": "Service",
name: service.name,
description: service.description,
serviceAudience: service.audience,
provider: { "@id": ORG_ID },
areaServed: { "@type": "Country", name: "España" },
offers: {
"@type": "Offer",
priceCurrency: "EUR",
price: service.price,
priceSpecification: {
"@type": "UnitPriceSpecification",
priceCurrency: "EUR",
price: service.price,
unitText: isEs ? "mes" : "month",
},
},
},
})),
};
const faq = {
"@context": "https://schema.org",
"@type": "FAQPage",
"@id": FAQ_ID,
mainEntity: isEs
? [
{
"@type": "Question",
name: "¿Qué es la implementación de IA para negocios?",
acceptedAnswer: {
"@type": "Answer",
text: "La implementación de IA para negocios significa instalar agentes inteligentes (chatbots, asistentes de voz, automatizaciones) en tu sitio web para que respondan preguntas, tomen reservas y vendan 24/7 sin intervención humana.",
},
},
{
"@type": "Question",
name: "¿Cuánto cuesta implementar IA en mi negocio en España?",
acceptedAnswer: {
"@type": "Answer",
text: "Nuestros planes comienzan desde 900€ de instalación + 299€/mes para chat IA básico, hasta planes completos desde 3.500€ + 749€/mes con sitio web nuevo.",
},
},
{
"@type": "Question",
name: "¿Por qué necesito IA en mi sitio web en 2026?",
acceptedAnswer: {
"@type": "Answer",
text: "El 95% de las interacciones con clientes involucran IA. Los negocios con chatbots aumentan ventas en 67% de media y reducen costes de soporte en 30%.",
},
},
]
: [
{
"@type": "Question",
name: "What is AI implementation for businesses?",
acceptedAnswer: {
"@type": "Answer",
text: "AI implementation for businesses means installing intelligent agents (chatbots, voice assistants, automations) on your website so they answer questions, take bookings, and sell 24/7 without human intervention.",
},
},
{
"@type": "Question",
name: "How much does it cost to implement AI in my business in Spain?",
acceptedAnswer: {
"@type": "Answer",
text: "Our plans start from a €900 setup + €299/month for basic AI chat, up to complete packages from €3,500 + €749/month with a new website.",
},
},
{
"@type": "Question",
name: "Why do I need AI on my website in 2026?",
acceptedAnswer: {
"@type": "Answer",
text: "95% of customer interactions involve AI. Businesses with chatbots increase sales by an average of 67% and reduce support costs by 30%.",
},
},
],
};
return (
<>
<Script
id="jsonld-organization"
type="application/ld+json"
strategy="beforeInteractive"
dangerouslySetInnerHTML={{ __html: JSON.stringify(organization) }}
/>
<Script
id="jsonld-localbusiness"
type="application/ld+json"
strategy="beforeInteractive"
dangerouslySetInnerHTML={{ __html: JSON.stringify(localBusiness) }}
/>
<Script
id="jsonld-services"
type="application/ld+json"
strategy="beforeInteractive"
dangerouslySetInnerHTML={{ __html: JSON.stringify(services) }}
/>
<Script
id="jsonld-faq"
type="application/ld+json"
strategy="beforeInteractive"
dangerouslySetInnerHTML={{ __html: JSON.stringify(faq) }}
/>
</>
);
}
+348
View File
@@ -0,0 +1,348 @@
"use client";
import { useMemo, useState } from "react";
type PricingTableProps = {
lang: "es" | "en";
onContact: () => void;
};
type Plan = {
id: string;
name: string;
setup: string;
monthly: number | null;
term: string;
features: string[];
optional: string[];
cta: string;
popular?: boolean;
subtext?: string;
};
const formatMonthly = (
value: number,
yearly: boolean,
lang: PricingTableProps["lang"]
) => {
const discounted = yearly ? Math.round(value * 0.85) : value;
return `${discounted} €/${lang === "es" ? "mes" : "mo"}`;
};
export default function PricingTable({ lang, onContact }: PricingTableProps) {
const [yearly, setYearly] = useState(false);
const [showFairUse, setShowFairUse] = useState(false);
const pricingContent = useMemo(
() => ({
es: {
labels: {
popular: "Más popular",
setup: "Instalación",
billedAnnually: "(facturado anualmente)",
},
plans: [
{
id: "starter",
name: "Smart Starter",
setup: "900 €",
monthly: 299,
term: "6-12 meses",
features: [
"Chat IA en la web (1 idioma)",
"Reservas y captación de leads",
"Bandeja ligera de leads",
"Analíticas básicas",
],
optional: ["+WhatsApp (+100€/mes)", "+Idioma extra (+80€/mes)"],
cta: "Empezar",
},
{
id: "site",
name: "Smart Site",
setup: "3.500 €",
monthly: 749,
term: "6-12 meses",
features: [
"Todo lo incluido en Starter",
"Nuevo sitio web Next.js",
"WhatsApp incluido",
"2 idiomas (ES+EN)",
"Sugerencias de contenido con IA",
"Analíticas avanzadas",
],
optional: [
"+Llamadas de voz (+150€/mes)",
"+Ubicación extra (+120€/mes)",
],
cta: "Empezar",
popular: true,
},
{
id: "growth",
name: "AI Growth Partner",
setup: "5.000 €",
monthly: 1950,
term: "12 meses",
features: [
"Todo lo incluido en Smart Site",
"Llamadas de voz incluidas",
"Soporte multiubicación",
"Integraciones CRM",
"Automatizaciones personalizadas",
"Llamada estratégica mensual",
"Soporte prioritario",
],
optional: ["+Pack de contenido IA (+200€/mes)"],
cta: "Empezar",
},
{
id: "enterprise",
name: "Enterprise / A medida",
setup: "Personalizado",
monthly: null,
term: "Definimos el alcance contigo",
features: [
"Todo a medida",
"Multimarca y white-label",
"Account manager dedicado",
"SLA de rendimiento",
],
optional: [],
cta: "Contactar",
subtext: "Hablemos",
},
] satisfies Plan[],
},
en: {
labels: {
popular: "Most popular",
setup: "Setup",
billedAnnually: "(billed annually)",
},
plans: [
{
id: "starter",
name: "Smart Starter",
setup: "900 €",
monthly: 299,
term: "6-12 months",
features: [
"AI website chat (1 language)",
"Booking & lead capture",
"Light lead inbox",
"Basic analytics",
],
optional: ["+WhatsApp (+100€/mo)", "+Extra language (+80€/mo)"],
cta: "Get started",
},
{
id: "site",
name: "Smart Site",
setup: "3,500 €",
monthly: 749,
term: "6-12 months",
features: [
"Everything in Starter",
"New Next.js website",
"WhatsApp included",
"2 languages (ES+EN)",
"AI content suggestions",
"Advanced analytics",
],
optional: ["+Voice calls (+150€/mo)", "+Extra location (+120€/mo)"],
cta: "Get started",
popular: true,
},
{
id: "growth",
name: "AI Growth Partner",
setup: "5,000 €",
monthly: 1950,
term: "12 months",
features: [
"Everything in Smart Site",
"Voice calls included",
"Multi-location support",
"CRM integrations",
"Custom automations",
"Monthly strategy call",
"Priority support",
],
optional: ["+AI content pack (+200€/mo)"],
cta: "Get started",
},
{
id: "enterprise",
name: "Enterprise / Custom",
setup: "Custom",
monthly: null,
term: "Let us scope it",
features: [
"Everything tailored",
"Multi-brand & white-label",
"Dedicated account manager",
"Performance SLAs",
],
optional: [],
cta: "Contact us",
subtext: "Let us talk",
},
] satisfies Plan[],
},
}),
[]
);
const pricing = pricingContent[lang];
const plans = pricing.plans;
return (
<div className="mt-10">
<div className="flex flex-col items-center justify-between gap-4 rounded-2xl border border-white/15 bg-white/10 p-5 text-center backdrop-blur md:flex-row md:text-left">
<div>
<p className="text-sm font-semibold uppercase tracking-[0.2em] text-white/70">
{lang === "es" ? "Pago anual" : "Pay yearly"}
</p>
<p className="mt-2 text-base text-white/90">
{lang === "es"
? "Paga anual y ahorra 15%"
: "Pay yearly - Save 15%"}
</p>
</div>
<label className="flex items-center gap-3 rounded-full border border-white/25 bg-white/10 px-4 py-2 text-sm font-semibold text-white">
<span>{lang === "es" ? "Descuento 15%" : "Save 15%"}</span>
<button
type="button"
onClick={() => setYearly((prev) => !prev)}
className={`relative h-6 w-12 rounded-full transition ${
yearly ? "bg-brand-pink" : "bg-white/20"
}`}
>
<span
className={`absolute left-1 top-1 h-4 w-4 rounded-full bg-white transition ${
yearly ? "translate-x-6" : ""
}`}
/>
</button>
</label>
</div>
<div className="mt-10 grid gap-6 lg:grid-cols-4 items-stretch">
{plans.map((plan) => (
<div
key={plan.id}
className={`flex h-full flex-col rounded-2xl border p-6 backdrop-blur transition duration-300 hover:-translate-y-2 hover:shadow-[0_25px_50px_-20px_rgba(118,75,162,0.65)] ${
plan.popular
? "border-brand-pink bg-white/12"
: "border-white/15 bg-white/8"
}`}
>
{plan.popular && (
<span className="inline-flex rounded-full bg-brand-pink/20 px-3 py-1 text-xs font-semibold text-white">
{pricing.labels.popular}
</span>
)}
<h3 className="mt-4 text-xl font-semibold">{plan.name}</h3>
<p className="mt-2 text-sm text-white/70">
{pricing.labels.setup}: {plan.setup}
</p>
<p className="mt-6 text-3xl font-bold">
{plan.monthly === null
? plan.subtext
: formatMonthly(plan.monthly, yearly, lang)}
</p>
<p className="mt-2 text-sm text-white/70">
{plan.term}
{plan.monthly !== null && yearly && (
<span className="block text-xs text-white/60">
{pricing.labels.billedAnnually}
</span>
)}
</p>
<ul className="mt-6 space-y-2 text-sm text-white/90">
{plan.features.map((feature) => (
<li key={feature}> {feature}</li>
))}
</ul>
{plan.optional.length > 0 && (
<div className="mt-4 text-xs text-white/60">
{plan.optional.map((option) => (
<p key={option}>{option}</p>
))}
</div>
)}
<button
onClick={onContact}
className="mt-auto w-full rounded-lg bg-brand-pink px-4 py-2 text-sm font-semibold text-white transition hover:-translate-y-1 hover:bg-[#ff7bc0]"
>
{plan.cta}
</button>
</div>
))}
</div>
<p className="mt-6 text-center text-xs text-white/70">
Todos los planes incluyen un uso generoso de IA para un negocio típico.
Se aplica una{" "}
<button
type="button"
onClick={() => setShowFairUse(true)}
className="font-semibold text-white underline decoration-white/60 underline-offset-4"
>
política de uso justo
</button>
, sin cargos sorpresa.
</p>
{showFairUse && (
<div className="fixed inset-0 z-50 flex items-center justify-center px-4">
<div
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
onClick={() => setShowFairUse(false)}
/>
<div className="relative z-10 w-full max-w-2xl rounded-2xl border border-white/20 bg-white/10 p-8 text-white shadow-purple-soft backdrop-blur">
<div className="flex items-start justify-between gap-4">
<div>
<p className="text-sm uppercase tracking-[0.3em] text-white/70">
Uso justo
</p>
<h3 className="mt-3 text-2xl font-bold">
Qué significa "uso justo"?
</h3>
</div>
<button
onClick={() => setShowFairUse(false)}
className="rounded-full border border-white/40 px-3 py-1 text-xs font-semibold text-white/80 transition hover:bg-white/10 hover:text-white"
>
Cerrar
</button>
</div>
<div className="mt-6 space-y-4 text-sm text-white/85">
<p>
Cada plan incluye suficientes mensajes de IA, llamadas y
conversaciones de WhatsApp para un negocio típico de tu tamaño.
</p>
<p>
Si tu uso se mantiene muy por encima de lo normal (por ejemplo,
porque añades nuevas marcas, locales o campañas), haremos lo
siguiente:
</p>
<ol className="list-decimal space-y-2 pl-5">
<li>Te avisaremos primero.</li>
<li>Te enviaremos un informe sencillo de uso.</li>
<li>
Te propondremos opciones (seguir igual con algunos límites o
subir de plan / añadir más capacidad).
</li>
</ol>
<p>
Nunca aplicamos cargos extra sin hablar contigo antes.
</p>
</div>
</div>
</div>
)}
</div>
);
}
+54
View File
@@ -0,0 +1,54 @@
"use client";
import Head from "next/head";
type SeoMetaProps = {
lang: "es" | "en";
};
export default function SeoMeta({ lang }: SeoMetaProps) {
const isEs = lang === "es";
const title = isEs
? "SiteMente | Implementación de IA Málaga - Chatbots para Restaurantes, Inmobiliarias, Rent a Car"
: "SiteMente | AI Implementation Málaga - Chatbots for Restaurants, Real Estate, Car Rentals";
const description = isEs
? "Agencia de IA en Málaga. Chatbots y automatización para restaurantes, inmobiliarias y alquiler de coches desde €299/mes. Soporte bilingüe ES/EN. +120 clientes en Costa del Sol."
: "AI agency in Málaga. Chatbots and automation for restaurants, real estate and car rentals from €299/month. Bilingual support ES/EN. +120 clients in Costa del Sol.";
const keywords = isEs
? "implementación IA España, chatbot restaurantes Málaga, IA inmobiliarias Costa del Sol, automatización negocios Marbella, agencia IA España"
: "AI implementation Spain, restaurant chatbot Málaga, real estate AI Costa del Sol, business automation Marbella";
const url = isEs ? "https://sitemente.com" : "https://sitemente.com/en";
const locale = isEs ? "es_ES" : "en_US";
return (
<Head>
<title>{title}</title>
<meta name="description" content={description} />
<meta name="keywords" content={keywords} />
<meta name="robots" content="index, follow" />
<meta
name="googlebot"
content="index, follow, max-image-preview:large"
/>
<link rel="canonical" href={url} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:url" content={url} />
<meta property="og:image" content="https://sitemente.com/og-banner.jpg" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:locale" content={locale} />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="SiteMente" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta
name="twitter:image"
content="https://sitemente.com/og-banner.jpg"
/>
</Head>
);
}
@@ -0,0 +1,570 @@
"use client";
import { useMemo, useState } from "react";
import { motion } from "framer-motion";
type ServicesAndPricingProps = {
lang: "es" | "en";
onContact: () => void;
};
type ServiceCard = {
id: string;
icon: string;
title: string;
for: string;
description: string[];
sections?: { title: string; items: string[] }[];
setup: string;
monthly: number | null;
term: string;
features: string[];
optional: string[];
cta: string;
popular?: boolean;
subtext?: string;
};
const fadeUp = {
hidden: { opacity: 0, y: 24 },
visible: { opacity: 1, y: 0 },
};
const content = {
es: {
sectionTitle: "Servicios y Precios",
sectionEyebrow: "Servicios y Precios",
headline: "Convertimos tu web en un vendedor inteligente que trabaja 24/7",
subheadline:
"En 2026, los clientes esperan respuestas instantáneas, reservas sin fricción y experiencias personalizadas. Si tu web no puede hacerlo, están yendo a tu competencia.",
yearlyLabel: "Pago anual",
yearlyDesc: "Paga anual y ahorra 15%",
saveLabel: "Descuento 15%",
setupLabel: "Instalación",
fromLabel: "Desde",
perMonth: "/mes",
billedAnnually: "(facturado anualmente)",
examplesTitle: "Casos de uso reales en Costa del Sol",
examplesCta: "Descubre tu plan ideal",
fairUseNote: "Todos los planes incluyen un uso generoso de IA para un negocio típico. Se aplica una",
fairUseLink: "política de uso justo",
fairUseEnd: ", sin cargos sorpresa.",
fairUseTitle: "Uso justo",
fairUseQuestion: 'Qué significa "uso justo"?',
close: "Cerrar",
fairUseBody: [
"Cada plan incluye suficientes mensajes de IA, llamadas y conversaciones de WhatsApp para un negocio típico de tu tamaño.",
"Si tu uso se mantiene muy por encima de lo normal (por ejemplo, porque añades nuevas marcas, locales o campañas), haremos lo siguiente:",
],
fairUseList: [
"Te avisaremos primero.",
"Te enviaremos un informe sencillo de uso.",
"Te propondremos opciones (seguir igual con algunos límites o subir de plan / añadir más capacidad).",
],
fairUseEndBody: "Nunca aplicamos cargos extra sin hablar contigo antes.",
popular: "Más popular",
},
en: {
sectionTitle: "Services & Pricing",
sectionEyebrow: "Services & Pricing",
headline: "Turn your website into a 24/7 intelligent salesperson",
subheadline:
"In 2026, customers expect instant answers, frictionless bookings, and personalized experiences. If your site can't deliver, they're going to your competitors.",
yearlyLabel: "Pay yearly",
yearlyDesc: "Pay yearly - Save 15%",
saveLabel: "Save 15%",
setupLabel: "Setup",
fromLabel: "From",
perMonth: "/month",
billedAnnually: "(billed annually)",
examplesTitle: "Real use cases on the Costa del Sol",
examplesCta: "Find your ideal plan",
fairUseNote: "All plans include generous AI usage for a typical business. A",
fairUseLink: "fair use policy",
fairUseEnd: " applies, no surprise charges.",
fairUseTitle: "Fair use",
fairUseQuestion: 'What does "fair use" mean?',
close: "Close",
fairUseBody: [
"Each plan includes enough AI messages, calls and WhatsApp conversations for a typical business of your size.",
"If your usage stays well above normal (e.g. you add new brands, locations or campaigns), we will:",
],
fairUseList: [
"Notify you first.",
"Send you a simple usage report.",
"Propose options (continue as-is with some limits, or upgrade / add capacity).",
],
fairUseEndBody: "We never apply extra charges without speaking to you first.",
popular: "Most popular",
},
} as const;
const serviceCards: Record<"es" | "en", ServiceCard[]> = {
es: [
{
id: "starter",
icon: "💬",
title: "AI Chat + Reservas Automatizadas",
for: "Restaurantes, clínicas, negocios locales con web existente",
description: [
"¿Tu web pierde clientes fuera de horario? Tu sitio responde preguntas, toma reservas y captura leads mientras duermes.",
],
sections: [
{
title: "Resultados típicos:",
items: [
"24/7 reservas sin intervención humana",
"Reduce llamadas repetitivas en un 70%",
"ROI positivo en el primer mes",
],
},
],
setup: "900 €",
monthly: 299,
term: "6-12 meses",
features: [
"Chat IA en la web (1 idioma)",
"Reservas y captación de leads",
"Analíticas básicas",
],
optional: ["+WhatsApp (+100€/mes)", "+Idioma extra (+80€/mes)"],
cta: "Empezar",
},
{
id: "site",
icon: "🌐",
title: "Web Inteligente Completa",
for: "Negocios listos para una web moderna + cerebro IA",
description: [
"Construimos tu nuevo sitio web Next.js (rápido, seguro, SEO) e integramos IA que vende, reserva y da soporte en español e inglés.",
],
sections: [
{
title: "Incluye:",
items: [
"Sitio que convierte visitas en ventas",
"Chat + WhatsApp incluidos",
"2 idiomas desde el día uno",
"Analíticas avanzadas",
],
},
],
setup: "3.500 €",
monthly: 749,
term: "6-12 meses",
features: [
"Todo lo de Starter",
"Nuevo sitio web Next.js",
"WhatsApp incluido",
"2 idiomas (ES+EN)",
],
optional: ["+Llamadas de voz (+150€/mes)", "+Ubicación extra (+120€/mes)"],
cta: "Empezar",
popular: true,
},
{
id: "growth",
icon: "🚀",
title: "Partner Estratégico de IA",
for: "Grupos de restaurantes, inmobiliarias, cadenas de alquiler",
description: [
"Socio estratégico: IA multicanal (web, WhatsApp, llamadas), integraciones CRM y consultoría mensual.",
],
sections: [
{
title: "Incluye:",
items: [
"Todo lo de Smart Site",
"Llamadas de voz incluidas",
"Soporte multiubicación",
"Integraciones CRM",
"Llamada estratégica mensual",
],
},
],
setup: "5.000 €",
monthly: 1950,
term: "12 meses",
features: [
"Todo lo de Smart Site",
"Llamadas de voz incluidas",
"Integraciones CRM",
"Soporte prioritario",
],
optional: ["+Pack contenido IA (+200€/mes)"],
cta: "Hablemos",
},
{
id: "enterprise",
icon: "🎯",
title: "Enterprise / A medida",
for: "Operaciones a escala con necesidades custom",
description: [
"Todo adaptado a tu operación: multimarca, white-label, account manager dedicado y SLA de rendimiento.",
],
setup: "Personalizado",
monthly: null,
term: "Definimos el alcance contigo",
features: [
"Todo a medida",
"Multimarca y white-label",
"Account manager dedicado",
"SLA de rendimiento",
],
optional: [],
cta: "Contactar",
subtext: "Hablemos",
},
],
en: [
{
id: "starter",
icon: "💬",
title: "AI Chat + Automated Bookings",
for: "Restaurants, clinics, local businesses with existing websites",
description: [
"Is your website losing customers after hours? Your site answers questions, takes bookings, and captures leads while you sleep.",
],
sections: [
{
title: "Typical results:",
items: [
"24/7 bookings without human intervention",
"Reduce repetitive calls by 70%",
"Positive ROI in the first month",
],
},
],
setup: "900 €",
monthly: 299,
term: "6-12 months",
features: [
"AI website chat (1 language)",
"Booking & lead capture",
"Basic analytics",
],
optional: ["+WhatsApp (+100€/mo)", "+Extra language (+80€/mo)"],
cta: "Get started",
},
{
id: "site",
icon: "🌐",
title: "Full Smart Website",
for: "Businesses ready for a modern site + AI brain",
description: [
"We build your new Next.js website (fast, secure, SEO) and integrate AI that sells, books, and supports in Spanish and English.",
],
sections: [
{
title: "Includes:",
items: [
"Site that converts visits into sales",
"Chat + WhatsApp included",
"2 languages from day one",
"Advanced analytics",
],
},
],
setup: "3,500 €",
monthly: 749,
term: "6-12 months",
features: [
"Everything in Starter",
"New Next.js website",
"WhatsApp included",
"2 languages (ES+EN)",
],
optional: ["+Voice calls (+150€/mo)", "+Extra location (+120€/mo)"],
cta: "Get started",
popular: true,
},
{
id: "growth",
icon: "🚀",
title: "AI Strategy Partner",
for: "Restaurant groups, real estate teams, rental chains",
description: [
"Strategic partner: multichannel AI (web, WhatsApp, calls), CRM integrations, monthly consulting.",
],
sections: [
{
title: "Includes:",
items: [
"Everything in Smart Site",
"Voice calls included",
"Multi-location support",
"CRM integrations",
"Monthly strategy call",
],
},
],
setup: "5,000 €",
monthly: 1950,
term: "12 months",
features: [
"Everything in Smart Site",
"Voice calls included",
"CRM integrations",
"Priority support",
],
optional: ["+AI content pack (+200€/mo)"],
cta: "Let's talk",
},
{
id: "enterprise",
icon: "🎯",
title: "Enterprise / Custom",
for: "Large-scale operations with custom needs",
description: [
"Everything tailored: multi-brand, white-label, dedicated account manager, performance SLA.",
],
setup: "Custom",
monthly: null,
term: "Let us scope it",
features: [
"Everything tailored",
"Multi-brand & white-label",
"Dedicated account manager",
"Performance SLAs",
],
optional: [],
cta: "Contact us",
subtext: "Let's talk",
},
],
};
const examples = {
es: [
{ title: "🍽️ Restaurantes", text: "Reservas a las 3 AM, respuestas sobre alérgenos, sugerencias de platos." },
{ title: "🏠 Inmobiliarias", text: "IA responde dudas, agenda visitas, cualifica leads." },
{ title: "🚗 Rent a Car", text: "Cotiza y reserva en 3 idiomas sin intervención humana." },
],
en: [
{ title: "🍽️ Restaurants", text: "Reservations at 3 AM, allergy answers, dish suggestions." },
{ title: "🏠 Real Estate", text: "AI answers questions, books viewings, qualifies leads." },
{ title: "🚗 Rent a Car", text: "Quote and book in 3 languages with no human intervention." },
],
};
export default function ServicesAndPricing({
lang,
onContact,
}: ServicesAndPricingProps) {
const [yearly, setYearly] = useState(false);
const [showFairUse, setShowFairUse] = useState(false);
const t = content[lang];
const cards = serviceCards[lang];
const formatMonthly = (value: number) => {
const price = yearly ? Math.round(value * 0.85) : value;
return `${price}${t.perMonth}`;
};
return (
<section
id="services"
className="mx-auto w-full max-w-6xl px-6 py-20"
>
<motion.div
variants={fadeUp}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.3 }}
transition={{ duration: 0.7 }}
>
<p className="text-sm font-semibold uppercase tracking-[0.2em] text-white/70">
{t.sectionEyebrow}
</p>
<h2 className="mt-4 text-3xl font-bold sm:text-4xl">
{t.sectionTitle}
</h2>
<p className="mt-4 max-w-3xl text-base text-white/85">
{t.subheadline}
</p>
</motion.div>
<div className="mt-6 flex flex-col items-center justify-between gap-4 rounded-2xl border border-white/15 bg-white/10 p-5 text-center backdrop-blur sm:flex-row">
<div>
<p className="text-sm font-semibold text-white/90">{t.yearlyLabel}</p>
<p className="mt-1 text-sm text-white/75">{t.yearlyDesc}</p>
</div>
<button
type="button"
onClick={() => setYearly((prev) => !prev)}
className="flex items-center gap-3 rounded-full border border-white/25 bg-white/10 px-4 py-2 text-sm font-semibold text-white"
>
<span>{t.saveLabel}</span>
<span
className={`relative block h-6 w-11 rounded-full transition ${
yearly ? "bg-brand-pink" : "bg-white/20"
}`}
>
<span
className={`absolute left-1 top-1 h-4 w-4 rounded-full bg-white transition ${
yearly ? "translate-x-5" : ""
}`}
/>
</span>
</button>
</div>
<div className="mt-10 grid gap-6 lg:grid-cols-2 xl:grid-cols-4">
{cards.map((card, index) => (
<motion.div
key={card.id}
variants={fadeUp}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.2 }}
transition={{ duration: 0.5, delay: index * 0.08 }}
className={`flex flex-col rounded-2xl border p-6 backdrop-blur transition hover:border-white/30 ${
card.popular
? "border-brand-pink bg-white/12 shadow-purple-soft"
: "border-white/15 bg-white/8"
}`}
>
{card.popular && (
<span className="mb-3 inline-flex w-fit rounded-full bg-brand-pink/25 px-3 py-0.5 text-xs font-semibold text-white">
{t.popular}
</span>
)}
<div className="flex items-center gap-3">
<span className="text-2xl">{card.icon}</span>
<h3 className="text-lg font-semibold">{card.title}</h3>
</div>
<p className="mt-2 text-sm text-white/70">{card.for}</p>
<div className="mt-4 space-y-2 text-sm text-white/90">
{card.description.map((p, i) => (
<p key={i}>{p}</p>
))}
</div>
{card.sections?.map((section) => (
<div key={section.title} className="mt-4">
<p className="text-xs font-semibold text-white/85">
{section.title}
</p>
<ul className="mt-2 space-y-1 text-xs text-white/80">
{section.items.map((item) => (
<li key={item}> {item}</li>
))}
</ul>
</div>
))}
<div className="mt-6 rounded-xl border border-white/15 bg-white/5 px-4 py-3">
<p className="text-xs text-white/70">
{t.setupLabel}: <span className="font-semibold text-white">{card.setup}</span>
</p>
<p className="mt-1 text-xl font-bold text-brand-pink">
{card.monthly === null
? card.subtext
: card.monthly !== null
? `${t.fromLabel} ${formatMonthly(card.monthly)}`
: null}
</p>
<p className="mt-1 text-xs text-white/60">
{card.term}
{card.monthly !== null && yearly && (
<span className="block">{t.billedAnnually}</span>
)}
</p>
</div>
<ul className="mt-4 space-y-1.5 text-sm text-white/85">
{card.features.map((f) => (
<li key={f}> {f}</li>
))}
</ul>
{card.optional.length > 0 && (
<p className="mt-3 text-xs text-white/60">
{card.optional.join(" · ")}
</p>
)}
<button
onClick={onContact}
className="mt-auto mt-6 w-full rounded-lg bg-brand-pink py-2.5 text-sm font-semibold text-white transition hover:bg-[#ff7bc0]"
>
{card.cta}
</button>
</motion.div>
))}
</div>
<div className="mt-16 rounded-2xl border border-white/15 bg-white/10 p-8 backdrop-blur">
<h3 className="text-xl font-semibold">{t.examplesTitle}</h3>
<div className="mt-6 grid gap-4 md:grid-cols-3">
{examples[lang].map((ex) => (
<div
key={ex.title}
className="rounded-xl border border-white/10 bg-white/5 p-4"
>
<p className="text-lg">{ex.title}</p>
<p className="mt-2 text-sm text-white/80">"{ex.text}"</p>
</div>
))}
</div>
<div className="mt-6 flex justify-center">
<button
onClick={onContact}
className="rounded-lg bg-brand-pink px-6 py-3 text-sm font-semibold text-white transition hover:bg-[#ff7bc0]"
>
{t.examplesCta}
</button>
</div>
</div>
<p className="mt-6 text-center text-xs text-white/70">
{t.fairUseNote}{" "}
<button
type="button"
onClick={() => setShowFairUse(true)}
className="font-semibold text-white underline decoration-white/60 underline-offset-2"
>
{t.fairUseLink}
</button>
{t.fairUseEnd}
</p>
{showFairUse && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div
className="absolute inset-0 bg-black/60"
onClick={() => setShowFairUse(false)}
/>
<div className="relative z-10 w-full max-w-lg rounded-2xl border border-white/20 bg-[#4a3572] p-8 text-white shadow-xl">
<div className="flex items-start justify-between gap-4">
<div>
<p className="text-xs uppercase tracking-widest text-white/70">
{t.fairUseTitle}
</p>
<h3 className="mt-2 text-xl font-bold">{t.fairUseQuestion}</h3>
</div>
<button
onClick={() => setShowFairUse(false)}
className="rounded-full border border-white/40 px-3 py-1 text-xs font-semibold text-white/80 hover:bg-white/10"
>
{t.close}
</button>
</div>
<div className="mt-6 space-y-3 text-sm text-white/85">
{t.fairUseBody.map((p) => (
<p key={p}>{p}</p>
))}
<ol className="list-decimal space-y-2 pl-5">
{t.fairUseList.map((item) => (
<li key={item}>{item}</li>
))}
</ol>
<p className="font-medium">{t.fairUseEndBody}</p>
</div>
</div>
</div>
)}
</section>
);
}
+19
View File
@@ -0,0 +1,19 @@
<!doctype html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<title>SiteMente | Agencia de Implementación de IA</title>
<meta
name="description"
content="SiteMente ayuda a negocios locales en España con agentes de voz, chatbots, webs inteligentes y automatización."
/>
</head>
<body class="bg-white">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+132
View File
@@ -0,0 +1,132 @@
import { GoogleGenAI } from "@google/genai";
export type SiteMenteMessage = {
role: "user" | "assistant" | "system";
content: string;
};
export type SiteMenteSpeechResult = {
audioBytes: Uint8Array;
mimeType: string;
};
const TEXT_MODEL = "gemini-2.5-flash";
const SPEECH_MODEL = "gemini-2.5-flash-preview-tts";
const getClient = () => {
const apiKey = process.env.GEMINI_API_KEY;
if (!apiKey) {
throw new Error("GEMINI_API_KEY is not set.");
}
return new GoogleGenAI({ apiKey });
};
const extractText = (response: unknown): string => {
const typed = response as {
candidates?: Array<{
content?: { parts?: Array<{ text?: string }> };
}>;
};
const text =
typed?.candidates?.[0]?.content?.parts
?.map((part) => part.text ?? "")
.join("")
.trim() ?? "";
if (!text) {
throw new Error("Gemini returned an empty response.");
}
return text;
};
const extractAudio = (response: unknown): SiteMenteSpeechResult => {
const typed = response as {
candidates?: Array<{
content?: {
parts?: Array<{
inlineData?: { data?: string; mimeType?: string };
}>;
};
}>;
};
const inlineData =
typed?.candidates?.[0]?.content?.parts?.find((part) => part.inlineData)
?.inlineData ?? null;
if (!inlineData?.data || !inlineData?.mimeType) {
throw new Error("Gemini did not return audio data.");
}
const audioBytes = Uint8Array.from(
Buffer.from(inlineData.data, "base64")
);
return { audioBytes, mimeType: inlineData.mimeType };
};
const buildContents = (messages: SiteMenteMessage[]) => {
return messages
.filter((message) => message.role !== "system")
.map((message) => ({
role: message.role === "assistant" ? "model" : "user",
parts: [{ text: message.content }],
}));
};
const buildSystemInstruction = (messages: SiteMenteMessage[]) => {
const systemText = messages
.filter((message) => message.role === "system")
.map((message) => message.content)
.join("\n")
.trim();
if (!systemText) {
return undefined;
}
return { parts: [{ text: systemText }] };
};
export const generateSiteMenteText = async (
messages: SiteMenteMessage[]
): Promise<string> => {
try {
const client = getClient();
const response = await client.models.generateContent({
model: TEXT_MODEL,
contents: buildContents(messages),
systemInstruction: buildSystemInstruction(messages),
});
return extractText(response);
} catch (error) {
console.error("[SiteMente][Gemini] Text generation failed", error);
throw error;
}
};
export const generateSiteMenteSpeech = async (
text: string
): Promise<SiteMenteSpeechResult> => {
try {
const client = getClient();
const response = await client.models.generateContent({
model: SPEECH_MODEL,
contents: [{ role: "user", parts: [{ text }] }],
config: {
responseModalities: ["AUDIO"],
speechConfig: {
voiceConfig: {
prebuiltVoiceConfig: {
voiceName: "Kore",
},
},
},
},
});
return extractAudio(response);
} catch (error) {
console.error("[SiteMente][Gemini] Speech generation failed", error);
throw error;
}
};
+61
View File
@@ -0,0 +1,61 @@
import {
generateSiteMenteSpeech,
generateSiteMenteText,
type SiteMenteMessage,
} from "./geminiClient";
export type SiteMenteTextRequest = {
messages: SiteMenteMessage[];
};
export type SiteMenteTextResponse = {
reply: string;
};
export type SiteMenteVoiceRequest = {
transcript: string;
};
export type SiteMenteVoiceResponse = {
reply: string;
audio: {
data: string;
mimeType: string;
};
};
const SYSTEM_PROMPT = [
"You are SiteMente, an AI site strategist that helps business owners make their websites think like a smart assistant.",
"You deeply understand landing pages, funnels, customer psychology, and automation.",
"You recommend specific AI Minds, suggest copy and UX changes, and ask focused clarifying questions before proposing changes.",
"Keep answers concise, practical, and business-oriented.",
].join(" ");
const withSystemPrompt = (messages: SiteMenteMessage[]) => {
return [{ role: "system", content: SYSTEM_PROMPT }, ...messages];
};
export const runSiteMenteText = async (
request: SiteMenteTextRequest
): Promise<SiteMenteTextResponse> => {
const reply = await generateSiteMenteText(withSystemPrompt(request.messages));
return { reply };
};
export const runSiteMenteVoiceTurn = async (
request: SiteMenteVoiceRequest
): Promise<SiteMenteVoiceResponse> => {
const messages: SiteMenteMessage[] = [
{ role: "user", content: request.transcript },
];
const reply = await generateSiteMenteText(withSystemPrompt(messages));
const audio = await generateSiteMenteSpeech(reply);
return {
reply,
audio: {
data: Buffer.from(audio.audioBytes).toString("base64"),
mimeType: audio.mimeType,
},
};
};
+305
View File
@@ -0,0 +1,305 @@
export const siteMenteKnowledge = {
company: {
name: "SiteMente",
location: "Málaga, Costa del Sol, España",
parentOrganization: "HolaCompi",
languages: ["ES", "EN"],
description: {
es: "Agencia de implementación de IA para negocios locales en España.",
en: "AI implementation agency for local businesses in Spain.",
},
},
services: {
es: [
{
name: "Smart Starter - AI Chat + Reservas Automatizadas",
priceMonthlyEur: 299,
setupFeeEur: 900,
audience: "Restaurantes, clínicas, negocios locales",
description:
"Chat IA para sitio web con reservas automatizadas, captura de leads y análisis básicos.",
features: [
"Chat IA en la web (1 idioma)",
"Reservas y captación de leads",
"Bandeja ligera de leads",
"Analíticas básicas",
],
optionalAddons: [
"+WhatsApp (+100€/mes)",
"+Idioma extra (+80€/mes)",
],
term: "6-12 meses",
},
{
name: "Smart Site - Web Inteligente Completa",
priceMonthlyEur: 749,
setupFeeEur: 3500,
audience: "Negocios listos para una web moderna",
description:
"Sitio web Next.js completo con IA integrada que vende, reserva y da soporte en español e inglés.",
features: [
"Todo lo incluido en Starter",
"Nuevo sitio web Next.js",
"WhatsApp incluido",
"2 idiomas (ES+EN)",
"Sugerencias de contenido con IA",
"Analíticas avanzadas",
],
optionalAddons: [
"+Llamadas de voz (+150€/mes)",
"+Ubicación extra (+120€/mes)",
],
term: "6-12 meses",
},
{
name: "AI Growth Partner",
priceMonthlyEur: 1950,
setupFeeEur: 5000,
audience: "Grupos de restaurantes, inmobiliarias, cadenas de alquiler",
description:
"Partner estratégico de IA: multicanal (web, WhatsApp, llamadas), integraciones CRM.",
features: [
"Todo lo incluido en Smart Site",
"Llamadas de voz incluidas",
"Soporte multiubicación",
"Integraciones CRM",
"Automatizaciones personalizadas",
"Llamada estratégica mensual",
"Soporte prioritario",
],
optionalAddons: ["+Pack de contenido IA (+200€/mes)"],
term: "12 meses",
},
],
en: [
{
name: "Smart Starter - AI Chat + Automated Bookings",
priceMonthlyEur: 299,
setupFeeEur: 900,
audience: "Restaurants, clinics, local businesses",
description:
"AI website chat with automated bookings, lead capture, and basic analytics.",
features: [
"AI website chat (1 language)",
"Booking & lead capture",
"Light lead inbox",
"Basic analytics",
],
optionalAddons: [
"+WhatsApp (+100€/mo)",
"+Extra language (+80€/mo)",
],
term: "6-12 months",
},
{
name: "Smart Site - Full Smart Website",
priceMonthlyEur: 749,
setupFeeEur: 3500,
audience: "Businesses ready for a modern website",
description:
"Complete Next.js website with integrated AI that sells, books, and supports in Spanish and English.",
features: [
"Everything in Starter",
"New Next.js website",
"WhatsApp included",
"2 languages (ES+EN)",
"AI content suggestions",
"Advanced analytics",
],
optionalAddons: [
"+Voice calls (+150€/mo)",
"+Extra location (+120€/mo)",
],
term: "6-12 months",
},
{
name: "AI Growth Partner",
priceMonthlyEur: 1950,
setupFeeEur: 5000,
audience: "Restaurant groups, real estate teams, rental chains",
description:
"Strategic AI partner: multichannel (web, WhatsApp, calls), CRM integrations.",
features: [
"Everything in Smart Site",
"Voice calls included",
"Multi-location support",
"CRM integrations",
"Custom automations",
"Monthly strategy call",
"Priority support",
],
optionalAddons: ["+AI content pack (+200€/mo)"],
term: "12 months",
},
],
},
testimonials: {
es: [
{
industry: "Restaurantes",
location: "Marbella",
highlight: "+43% reservas nocturnas",
quote:
"Desde que instalamos el chatbot de SiteMente, nuestras reservas nocturnas aumentaron un 43%. Ya no perdemos clientes que buscan mesa después de las 22h.",
},
{
industry: "Inmobiliarias",
location: "Málaga",
highlight: "70% consultas automatizadas",
quote:
"La IA responde el 70% de las consultas sobre propiedades automáticamente. Mi equipo ahora se enfoca solo en visitas cualificadas. ROI positivo en el primer mes.",
},
{
industry: "Rent a Car",
location: "Benalmádena",
highlight: "ROI en menos de 6 semanas",
quote:
"El sistema procesa reservas en 3 idiomas sin intervención humana. Recuperamos la inversión en menos de 6 semanas.",
},
],
en: [
{
industry: "Restaurants",
location: "Marbella",
highlight: "+43% late-night bookings",
quote:
"Since we installed SiteMente's chatbot, our late-night bookings increased by 43%. We no longer lose customers looking for tables after 10 PM.",
},
{
industry: "Real Estate",
location: "Málaga",
highlight: "70% inquiries automated",
quote:
"The AI answers 70% of property inquiries automatically. My team now focuses only on qualified viewings. Positive ROI in the first month.",
},
{
industry: "Car Rental",
location: "Benalmádena",
highlight: "ROI in under 6 weeks",
quote:
"The system processes bookings in 3 languages without human intervention. We recovered our investment in less than 6 weeks.",
},
],
},
stats: [
"+67% sales increase with AI chatbots",
"26% of transactions influenced by AI assistants",
"$290B conversational commerce in 2025",
"95% of interactions expected to involve AI",
"$14.2B Black Friday sales touched by AI",
],
faqs: {
es: [
{
question: "¿Qué es la implementación de IA para negocios?",
answer:
"La implementación de IA para negocios significa instalar agentes inteligentes (chatbots, asistentes de voz, automatizaciones) en tu sitio web y canales de comunicación para que respondan preguntas, tomen reservas y vendan 24/7 sin intervención humana. En SiteMente nos especializamos en implementar estas soluciones para restaurantes, inmobiliarias y alquiler de coches en España.",
},
{
question: "¿Cuánto cuesta implementar IA en mi negocio en España?",
answer:
"Los precios de implementación de IA en España varían según la complejidad. En SiteMente, nuestros planes comienzan desde 900€ de instalación + 299€/mes para chat IA básico con reservas, hasta planes completos desde 3.500€ + 749€/mes que incluyen sitio web nuevo con IA integrada. Todos los planes incluyen soporte en español e inglés.",
},
{
question: "¿Por qué necesito IA en mi sitio web en 2026?",
answer:
"En 2026, el 95% de las interacciones con clientes involucran IA de alguna forma. Los estudios muestran que los negocios con chatbots de IA aumentan sus ventas en un 67% de media y reducen costes de soporte en un 30%. Si tu sitio no puede responder al instante, pierdes clientes frente a la competencia que sí tiene IA.",
},
{
question: "¿La IA funciona para restaurantes pequeños en España?",
answer:
"Sí, especialmente bien. Los restaurantes con IA chat ven hasta un 47% más de reservas completadas porque pueden tomar pedidos y reservas 24/7, responder preguntas sobre alérgenos y menús, y sugerir platos personalizados. Muchos de nuestros clientes en la Costa del Sol son restaurantes locales que ahora captan reservas incluso a las 2-3 AM.",
},
{
question: "¿Cuánto tiempo tarda en implementarse la IA?",
answer:
"En SiteMente implementamos la mayoría de proyectos en 48 horas para instalaciones básicas (chat IA + reservas en web existente), y entre 2-4 semanas para sitios web completos nuevos con IA integrada. Mucho más rápido que contratar personal adicional.",
},
{
question: "¿La IA puede hablar en español e inglés?",
answer:
"Sí. Todos nuestros agentes de IA funcionan en español e inglés nativamente, perfecto para negocios en zonas turísticas como la Costa del Sol. Podemos añadir idiomas adicionales según necesidad.",
},
{
question: "¿Qué negocios se benefician más de la IA?",
answer:
"Los negocios con alto volumen de consultas repetitivas: restaurantes (reservas, menú, alergias), inmobiliarias (propiedades disponibles, visitas), alquiler de coches (disponibilidad, precios, seguros), clínicas (citas, tratamientos), hoteles y servicios turísticos. Cualquier negocio que pierda clientes fuera de horario de atención.",
},
{
question: "¿Puedo probar antes de contratar?",
answer:
"Sí. Reserva una demo gratuita y te mostramos cómo funcionaría la IA en tu caso específico, con ejemplos reales de tu sector. Sin compromiso.",
},
{
question:
"¿Qué pasa si mis clientes usan demasiado la IA y me cobran de más?",
answer:
"Todos nuestros planes incluyen uso generoso ('fair use') calculado para un negocio típico de tu tamaño. Si tu uso crece mucho (por ejemplo, abres nuevas ubicaciones o crece tu negocio), te avisamos primero y te proponemos opciones antes de cualquier cargo extra. Nunca sorpresas en la factura.",
},
{
question: "¿SiteMente trabaja solo en la Costa del Sol?",
answer:
"No. Aunque tenemos muchos clientes en Málaga, Marbella, Benalmádena y Costa del Sol, trabajamos con negocios en toda España de forma remota. Nuestro equipo es bilingüe ES/EN y podemos implementar IA en cualquier ubicación.",
},
],
en: [
{
question: "What is AI implementation for businesses?",
answer:
"AI implementation for businesses means installing intelligent agents (chatbots, voice assistants, automations) on your website and communication channels so they answer questions, take bookings, and sell 24/7 without human intervention. At SiteMente we specialize in implementing these solutions for restaurants, real estate and car rentals in Spain.",
},
{
question: "How much does it cost to implement AI in my business in Spain?",
answer:
"AI implementation prices in Spain vary by complexity. At SiteMente, our plans start from €900 setup + €299/month for basic AI chat with bookings, up to complete packages from €3,500 + €749/month including a new website with integrated AI. All plans include support in Spanish and English.",
},
{
question: "Why do I need AI on my website in 2026?",
answer:
"In 2026, 95% of customer interactions involve AI in some form. Studies show businesses with AI chatbots increase sales by an average of 67% and reduce support costs by 30%. If your site can't answer instantly, you lose customers to competitors that can.",
},
{
question: "Does AI work for small restaurants in Spain?",
answer:
"Yes, especially well. Restaurants with AI chat see up to 47% more completed bookings because they can take orders and reservations 24/7, answer allergen and menu questions, and suggest personalized dishes. Many of our clients on the Costa del Sol are local restaurants now capturing bookings even at 2-3 AM.",
},
{
question: "How long does AI implementation take?",
answer:
"At SiteMente we implement most projects in 48 hours for basic setups (AI chat + bookings on an existing site), and between 2-4 weeks for complete new websites with integrated AI. Much faster than hiring additional staff.",
},
{
question: "Can the AI speak Spanish and English?",
answer:
"Yes. All our AI agents run natively in Spanish and English, perfect for businesses in tourist areas like the Costa del Sol. We can add additional languages as needed.",
},
{
question: "What businesses benefit most from AI?",
answer:
"Businesses with high volumes of repetitive inquiries: restaurants (bookings, menu, allergens), real estate (available properties, viewings), car rentals (availability, prices, insurance), clinics (appointments, treatments), hotels, and tourism services. Any business that loses customers outside business hours.",
},
{
question: "Can I try it before committing?",
answer:
"Yes. Book a free demo and we'll show how AI would work for your specific case, with real examples from your sector. No commitment.",
},
{
question: "What if my customers use the AI too much and I get charged extra?",
answer:
"All our plans include generous fair use sized for a typical business of your size. If your usage grows significantly (for example, you open new locations or your business grows), we let you know first and propose options before any extra charges. No billing surprises.",
},
{
question: "Does SiteMente work only on the Costa del Sol?",
answer:
"No. Although we have many clients in Málaga, Marbella, Benalmádena and the Costa del Sol, we work with businesses all across Spain remotely. Our team is bilingual ES/EN and we can implement AI in any location.",
},
],
},
useCases: [
"Restaurantes",
"Inmobiliarias",
"Rent a Car",
"Clínicas",
"Turismo",
],
} as const;
+6
View File
@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+7
View File
@@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
reactStrictMode: true,
};
export default nextConfig;
+3062
View File
File diff suppressed because it is too large Load Diff
+27
View File
@@ -0,0 +1,27 @@
{
"name": "sitemente-landing",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"@google/genai": "^1.39.0",
"@google/generative-ai": "^0.24.1",
"framer-motion": "^12.23.12",
"next": "^15.5.3",
"react": "^19.1.1",
"react-dom": "^19.1.1"
},
"devDependencies": {
"@types/react": "^19.1.12",
"@types/react-dom": "^19.1.9",
"autoprefixer": "^10.4.21",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.17",
"typescript": "^5.9.2"
}
}
+6
View File
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 579 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

+11
View File
@@ -0,0 +1,11 @@
# Run this from d:\Projects\SiteMente (or your local code folder)
# Replace YOUR_USERNAME with your GitHub username
Set-Location $PSScriptRoot
git init
git add .
git commit -m "Initial commit"
git branch -M main
git remote add origin https://github.com/HaithamEKhalifa/sitemente.git
git push -u origin main
+12
View File
@@ -0,0 +1,12 @@
import fs from 'fs';
const lines = fs.readFileSync('app/page.tsx', 'utf8').split('\n');
const start = lines.findIndex(l => l.includes('REMOVE_MARKER'));
const end = lines.findIndex(l => l.includes('id="method"'));
if (start === -1 || end === -1) {
console.error('Markers not found', { start, end });
process.exit(1);
}
const before = lines.slice(0, start);
const after = lines.slice(Math.max(0, end - 2));
fs.writeFileSync('app/page.tsx', before.concat(after).join('\n'));
console.log('Removed lines', start, 'to', end);
+760
View File
@@ -0,0 +1,760 @@
import { useEffect, useMemo, useState } from "react";
import { motion } from "framer-motion";
const useTypewriter = (words: string[], speed = 110, pause = 1400) => {
const [index, setIndex] = useState(0);
const [subIndex, setSubIndex] = useState(0);
const [deleting, setDeleting] = useState(false);
useEffect(() => {
setIndex(0);
setSubIndex(0);
setDeleting(false);
}, [words]);
useEffect(() => {
const current = words[index] ?? "";
if (!deleting && subIndex === current.length) {
const timeout = setTimeout(() => setDeleting(true), pause);
return () => clearTimeout(timeout);
}
if (deleting && subIndex === 0) {
setDeleting(false);
setIndex((prev) => (prev + 1) % words.length);
return undefined;
}
const timeout = setTimeout(
() => setSubIndex((prev) => prev + (deleting ? -1 : 1)),
deleting ? speed * 0.6 : speed
);
return () => clearTimeout(timeout);
}, [words, index, subIndex, deleting, speed, pause]);
return words[index]?.substring(0, subIndex) ?? "";
};
const fadeUp = {
hidden: { opacity: 0, y: 24 },
visible: { opacity: 1, y: 0 },
};
type Language = "es" | "en";
const contentByLang = {
es: {
nav: {
services: "Servicios",
method: "Método",
pricing: "Precios",
contact: "Contacto",
cta: "Agenda una demo",
},
hero: {
badge:
"Agencia de implementación de IA para negocios locales en España",
title: "Tu negocio nunca duerme. Nosotros tampoco.",
subtitle:
"Implementamos IA que responde, vende y fideliza mientras tú descansas. Diseñado para",
ctaPrimary: "Quiero automatizar mi negocio",
ctaSecondary: "Ver casos reales",
highlights: [
"Setup en 48 horas",
"ROI medible desde el primer mes",
"Equipo bilingüe ES / EN",
],
sectors: ["Restaurantes", "Clínicas", "Salones", "Inmobiliarias"],
},
services: {
eyebrow: "Servicios",
title: "IA lista para trabajar en tu negocio.",
items: [
{
title: "Agentes de voz con IA",
description:
"Atiende llamadas 24/7, confirma reservas y responde preguntas sin perder oportunidades.",
},
{
title: "Chatbots inteligentes",
description:
"Convierte visitas web en clientes con soporte inmediato y respuestas personalizadas.",
},
{
title: "Webs inteligentes",
description:
"Sitios modernos y rápidos con IA integrada para captar, convencer y vender.",
},
{
title: "Automatización de negocio",
description:
"Emails, recordatorios y CRM conectados para que tu equipo se enfoque en lo importante.",
},
],
},
method: {
eyebrow: "Cómo funciona",
title: "Tu IA en marcha en tres pasos.",
steps: [
{
title: "Diagnóstico inteligente",
text: "Analizamos tus procesos y detectamos dónde la IA aporta más valor.",
},
{
title: "Implementación exprés",
text: "Configuramos agentes y flujos en días, no en meses.",
},
{
title: "Crecimiento continuo",
text: "Optimización semanal basada en datos reales de tu negocio.",
},
],
},
pricing: {
eyebrow: "Precios",
title: "Planes claros para resultados medibles.",
plans: [
{
name: "Impulso",
price: "390€",
desc: "Ideal para empezar con IA en un área clave.",
features: [
"1 solución IA",
"Integración web básica",
"Soporte por email",
],
},
{
name: "Pro",
price: "790€",
desc: "El plan favorito para negocios en crecimiento.",
features: [
"2 soluciones IA",
"Automatizaciones avanzadas",
"Soporte prioritario",
],
popular: true,
},
{
name: "Elite",
price: "1390€",
desc: "Para operaciones que quieren IA en todo su embudo.",
features: [
"Soluciones IA ilimitadas",
"Integraciones a medida",
"Acompañamiento estratégico",
],
},
],
popularBadge: "Más popular",
cta: "Empezar ahora",
},
stats: [
{ value: "24/7", label: "Atención automática" },
{ value: "3x", label: "Más reservas" },
{ value: "48h", label: "Tiempo medio de implementación" },
{ value: "+120", label: "Negocios locales servidos" },
],
cta: {
title: "Haz que tu negocio responda solo.",
text:
"Agenda una demo y descubre cómo SiteMente implementa IA real en tu operación diaria.",
button: "Hablar con SiteMente",
},
footer: {
tagline: "Parte de la familia HolaCompi.",
},
},
en: {
nav: {
services: "Services",
method: "Method",
pricing: "Pricing",
contact: "Contact",
cta: "Book a demo",
},
hero: {
badge:
"AI implementation agency for local businesses in Spain",
title: "Your business never sleeps. Neither do we.",
subtitle:
"We implement AI that responds, sells, and retains while you rest. Built for",
ctaPrimary: "Automate my business",
ctaSecondary: "See real results",
highlights: [
"Setup in 48 hours",
"Measurable ROI in month one",
"Bilingual team ES / EN",
],
sectors: ["Restaurants", "Clinics", "Salons", "Real Estate"],
},
services: {
eyebrow: "Services",
title: "AI ready to work for your business.",
items: [
{
title: "AI Voice Agents",
description:
"Answer calls 24/7, confirm bookings, and resolve questions without lost opportunities.",
},
{
title: "AI Chat Widgets",
description:
"Turn website visits into customers with instant, personalized support.",
},
{
title: "Smart Websites",
description:
"Modern, fast sites with built-in AI to capture, convince, and sell.",
},
{
title: "Business Automation",
description:
"Emails, reminders, and CRM flows connected so your team stays focused.",
},
],
},
method: {
eyebrow: "How it works",
title: "Your AI live in three steps.",
steps: [
{
title: "Smart audit",
text: "We map your workflows and pinpoint where AI adds the most value.",
},
{
title: "Rapid implementation",
text: "Agents and automations go live in days, not months.",
},
{
title: "Continuous growth",
text: "Weekly optimization based on real business data.",
},
],
},
pricing: {
eyebrow: "Pricing",
title: "Clear plans for measurable results.",
plans: [
{
name: "Starter",
price: "€390",
desc: "Ideal to launch AI in one key area.",
features: [
"1 AI solution",
"Basic web integration",
"Email support",
],
},
{
name: "Pro",
price: "€790",
desc: "The go-to plan for growing businesses.",
features: [
"2 AI solutions",
"Advanced automations",
"Priority support",
],
popular: true,
},
{
name: "Elite",
price: "€1,390",
desc: "For operations that want AI across the full funnel.",
features: [
"Unlimited AI solutions",
"Custom integrations",
"Strategic guidance",
],
},
],
popularBadge: "Most popular",
cta: "Get started",
},
stats: [
{ value: "24/7", label: "Automated support" },
{ value: "3x", label: "More bookings" },
{ value: "48h", label: "Average rollout time" },
{ value: "+120", label: "Local businesses served" },
],
cta: {
title: "Make your business respond on its own.",
text:
"Book a demo and see how SiteMente implements real AI in your daily operations.",
button: "Talk to SiteMente",
},
footer: {
tagline: "Part of the HolaCompi family.",
},
},
} as const satisfies Record<Language, object>;
export default function App() {
const [lang, setLang] = useState<Language>("es");
const [contactOpen, setContactOpen] = useState(false);
const content = useMemo(() => contentByLang[lang], [lang]);
const typed = useTypewriter(content.hero.sectors);
return (
<div className="font-sans text-white bg-brand-gradient">
<header className="relative min-h-screen overflow-hidden">
<div className="absolute inset-0">
<div className="float-slow absolute -top-24 -left-24 h-72 w-72 rounded-full bg-white/20 blur-3xl" />
<div className="float-medium absolute top-32 right-10 h-48 w-48 rounded-full bg-white/10 blur-2xl" />
<div className="float-fast absolute bottom-0 left-1/2 h-72 w-72 -translate-x-1/2 rounded-full bg-white/10 blur-3xl" />
<div className="float-medium absolute bottom-10 right-10 h-36 w-36 rounded-full bg-white/20 blur-2xl" />
</div>
<nav className="relative z-10 mx-auto flex w-full max-w-6xl items-center justify-between px-6 py-6">
<div className="text-xl font-bold tracking-wide">SiteMente</div>
<div className="flex items-center gap-4">
<div className="hidden items-center gap-6 text-sm font-medium md:flex">
<a className="hover:text-white/80" href="#services">
{content.nav.services}
</a>
<a className="hover:text-white/80" href="#method">
{content.nav.method}
</a>
<a className="hover:text-white/80" href="#pricing">
{content.nav.pricing}
</a>
<a className="hover:text-white/80" href="#contact">
{content.nav.contact}
</a>
</div>
<div className="flex items-center gap-3">
<div className="flex rounded-full border border-white/40 bg-white/10 p-1 text-xs font-semibold">
<button
onClick={() => setLang("es")}
className={`rounded-full px-3 py-1 transition ${
lang === "es"
? "bg-white text-brand-purple-dark"
: "text-white/80 hover:text-white"
}`}
>
ES
</button>
<button
onClick={() => setLang("en")}
className={`rounded-full px-3 py-1 transition ${
lang === "en"
? "bg-white text-brand-purple-dark"
: "text-white/80 hover:text-white"
}`}
>
EN
</button>
</div>
<button
onClick={() => setContactOpen(true)}
className="rounded-lg border border-white/60 px-4 py-2 text-sm font-semibold transition hover:bg-white/10"
>
{content.nav.cta}
</button>
<button
onClick={() => setContactOpen(true)}
className="rounded-lg bg-brand-pink px-4 py-2 text-sm font-semibold text-white shadow-purple-soft transition hover:-translate-y-1 hover:bg-[#ff7bc0]"
>
{lang === "es" ? "Contactar" : "Contact"}
</button>
</div>
</div>
</nav>
<div className="relative z-10 mx-auto flex w-full max-w-6xl flex-col items-start gap-10 px-6 pb-24 pt-10 md:pt-16">
<motion.div
variants={fadeUp}
initial="hidden"
animate="visible"
transition={{ duration: 0.8 }}
className="inline-flex items-center gap-2 rounded-full bg-white/20 px-4 py-1 text-sm backdrop-blur"
>
<span className="h-2 w-2 rounded-full bg-brand-pink" />
{content.hero.badge}
</motion.div>
<motion.div
variants={fadeUp}
initial="hidden"
animate="visible"
transition={{ duration: 0.8, delay: 0.1 }}
className="max-w-3xl"
>
<h1 className="text-4xl font-bold leading-tight text-white sm:text-5xl lg:text-6xl">
{content.hero.title}
</h1>
<p className="mt-6 text-lg text-white/90 sm:text-xl">
{content.hero.subtitle}{" "}
<span className="font-semibold text-white">
{typed}
<span className="ml-1 inline-block h-6 w-1 animate-pulse bg-white align-middle" />
</span>
</p>
</motion.div>
<motion.div
variants={fadeUp}
initial="hidden"
animate="visible"
transition={{ duration: 0.8, delay: 0.2 }}
className="flex flex-wrap gap-4"
>
<button
onClick={() => setContactOpen(true)}
className="rounded-lg bg-brand-pink px-6 py-3 text-base font-semibold text-white shadow-purple-soft transition hover:-translate-y-1 hover:bg-[#ff7bc0]"
>
{content.hero.ctaPrimary}
</button>
<button className="rounded-lg border border-white/80 px-6 py-3 text-base font-semibold text-white transition hover:bg-white/10">
{content.hero.ctaSecondary}
</button>
</motion.div>
<motion.div
variants={fadeUp}
initial="hidden"
animate="visible"
transition={{ duration: 0.8, delay: 0.3 }}
className="grid w-full gap-4 md:grid-cols-3"
>
{content.hero.highlights.map((text) => (
<div
key={text}
className="rounded-xl border border-white/20 bg-white/10 px-4 py-3 text-sm font-medium backdrop-blur"
>
{text}
</div>
))}
</motion.div>
</div>
</header>
<main className="bg-[#5e4a8a]">
<section
id="services"
className="mx-auto w-full max-w-6xl px-6 py-20"
>
<motion.div
variants={fadeUp}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.3 }}
transition={{ duration: 0.7 }}
>
<p className="text-sm font-semibold uppercase tracking-[0.2em] text-white/70">
{content.services.eyebrow}
</p>
<h2 className="mt-4 text-3xl font-bold sm:text-4xl">
{content.services.title}
</h2>
</motion.div>
<div className="mt-10 grid gap-6 md:grid-cols-2">
{content.services.items.map((service, index) => (
<motion.div
key={service.title}
variants={fadeUp}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.2 }}
transition={{ duration: 0.6, delay: index * 0.1 }}
className="rounded-xl border border-white/20 bg-white/10 p-6 shadow-purple-soft backdrop-blur transition duration-300 hover:-translate-y-2 hover:border-white/50 hover:bg-white/15 hover:shadow-[0_25px_50px_-20px_rgba(118,75,162,0.65)]"
>
<h3 className="text-xl font-semibold">
<span className="text-brand-pink"></span> {service.title}
</h3>
<p className="mt-3 text-base text-white/90">
{service.description}
</p>
</motion.div>
))}
</div>
</section>
<section
id="method"
className="mx-auto w-full max-w-6xl px-6 py-20"
>
<motion.div
variants={fadeUp}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.3 }}
transition={{ duration: 0.7 }}
>
<p className="text-sm font-semibold uppercase tracking-[0.2em] text-white/70">
{content.method.eyebrow}
</p>
<h2 className="mt-4 text-3xl font-bold sm:text-4xl">
{content.method.title}
</h2>
</motion.div>
<div className="mt-12 grid gap-8 md:grid-cols-3">
{content.method.steps.map((step, index) => (
<motion.div
key={step.title}
variants={fadeUp}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.2 }}
transition={{ duration: 0.6, delay: index * 0.1 }}
className="rounded-xl border border-white/20 bg-white/10 p-6 backdrop-blur transition duration-300 hover:-translate-y-2 hover:border-white/50 hover:bg-white/15 hover:shadow-[0_25px_50px_-20px_rgba(118,75,162,0.65)]"
>
<div className="mb-4 h-1 w-12 rounded-full bg-brand-pink" />
<h3 className="text-lg font-semibold">{step.title}</h3>
<p className="mt-3 text-white/90">{step.text}</p>
</motion.div>
))}
</div>
</section>
<section
id="pricing"
className="mx-auto w-full max-w-6xl px-6 py-20"
>
<motion.div
variants={fadeUp}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.3 }}
transition={{ duration: 0.7 }}
>
<p className="text-sm font-semibold uppercase tracking-[0.2em] text-white/70">
{content.pricing.eyebrow}
</p>
<h2 className="mt-4 text-3xl font-bold sm:text-4xl">
{content.pricing.title}
</h2>
</motion.div>
<div className="mt-10 grid gap-6 lg:grid-cols-3">
{content.pricing.plans.map((plan, index) => (
<motion.div
key={plan.name}
variants={fadeUp}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.2 }}
transition={{ duration: 0.6, delay: index * 0.1 }}
className={`rounded-xl border bg-white/10 p-6 backdrop-blur transition duration-300 hover:-translate-y-2 hover:border-white/50 hover:bg-white/15 hover:shadow-[0_25px_50px_-20px_rgba(118,75,162,0.65)] ${
plan.popular
? "border-brand-pink shadow-purple-soft"
: "border-white/20"
}`}
>
{plan.popular && (
<div className="mb-4 inline-flex rounded-full bg-brand-pink/20 px-3 py-1 text-xs font-semibold text-white">
{content.pricing.popularBadge}
</div>
)}
<h3 className="text-xl font-semibold">{plan.name}</h3>
<p className="mt-2 text-white/80">{plan.desc}</p>
<p className="mt-6 text-3xl font-bold">{plan.price}</p>
<ul className="mt-6 space-y-2 text-white/90">
{plan.features.map((feature) => (
<li key={feature}> {feature}</li>
))}
</ul>
<button
onClick={() => setContactOpen(true)}
className="mt-8 w-full rounded-lg bg-brand-pink px-4 py-2 text-sm font-semibold text-white transition hover:-translate-y-1 hover:bg-[#ff7bc0]"
>
{content.pricing.cta}
</button>
</motion.div>
))}
</div>
</section>
<section className="mx-auto w-full max-w-6xl px-6 py-20">
<motion.div
variants={fadeUp}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.3 }}
transition={{ duration: 0.7 }}
className="grid gap-6 rounded-2xl border border-white/20 bg-white/10 p-8 text-center backdrop-blur md:grid-cols-4"
>
{content.stats.map((stat) => (
<div key={stat.label}>
<p className="text-3xl font-bold text-brand-pink">
{stat.value}
</p>
<p className="mt-2 text-sm text-white/80">{stat.label}</p>
</div>
))}
</motion.div>
</section>
<section id="contact" className="bg-brand-gradient-dark py-20">
<motion.div
variants={fadeUp}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.3 }}
transition={{ duration: 0.7 }}
className="mx-auto flex w-full max-w-5xl flex-col items-start gap-6 px-6 md:flex-row md:items-center md:justify-between"
>
<div>
<h2 className="text-3xl font-bold sm:text-4xl">
{content.cta.title}
</h2>
<p className="mt-4 max-w-xl text-white/90">
{content.cta.text}
</p>
</div>
<button
onClick={() => setContactOpen(true)}
className="rounded-lg bg-brand-pink px-6 py-3 text-base font-semibold text-white shadow-purple-soft transition hover:-translate-y-1 hover:bg-[#ff7bc0]"
>
{content.cta.button}
</button>
</motion.div>
</section>
</main>
<footer className="bg-brand-purple-dark">
<div className="mx-auto flex w-full max-w-6xl flex-col gap-6 px-6 py-12 md:flex-row md:items-center md:justify-between">
<div>
<p className="text-lg font-semibold">SiteMente</p>
<p className="text-sm text-white/80">
{content.footer.tagline}
</p>
</div>
<div className="flex gap-6 text-sm text-white/80">
<span>Madrid · Barcelona · Valencia</span>
<span>hola@sitemente.ai</span>
</div>
</div>
</footer>
{contactOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center px-4">
<div
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
onClick={() => setContactOpen(false)}
/>
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
transition={{ duration: 0.3 }}
className="relative z-10 w-full max-w-2xl rounded-2xl border border-white/20 bg-white/10 p-8 text-white shadow-purple-soft backdrop-blur"
>
<div className="flex items-start justify-between gap-4">
<div>
<p className="text-sm uppercase tracking-[0.3em] text-white/70">
{lang === "es" ? "Formulario" : "Contact form"}
</p>
<h3 className="mt-3 text-2xl font-bold">
{lang === "es"
? "Cuéntanos sobre tu negocio"
: "Tell us about your business"}
</h3>
<p className="mt-2 text-white/80">
{lang === "es"
? "Te respondemos en menos de 24 horas."
: "We reply within 24 hours."}
</p>
</div>
<button
onClick={() => setContactOpen(false)}
className="rounded-full border border-white/40 px-3 py-1 text-xs font-semibold text-white/80 transition hover:bg-white/10 hover:text-white"
>
{lang === "es" ? "Cerrar" : "Close"}
</button>
</div>
<form
action="https://formsubmit.co/hola@sitemente.com"
method="POST"
className="mt-8 grid gap-4"
>
<input type="hidden" name="_captcha" value="false" />
<input type="hidden" name="_subject" value="Nuevo lead SiteMente" />
<div className="grid gap-4 md:grid-cols-2">
<div>
<label className="text-sm font-semibold text-white/80">
{lang === "es" ? "Nombre" : "Name"}
</label>
<input
required
name="name"
placeholder={
lang === "es" ? "Tu nombre" : "Your name"
}
className="mt-2 w-full rounded-lg border border-white/20 bg-white/10 px-4 py-3 text-white placeholder:text-white/50 focus:border-white/60 focus:outline-none"
/>
</div>
<div>
<label className="text-sm font-semibold text-white/80">
Email
</label>
<input
required
name="email"
type="email"
placeholder="hola@empresa.com"
className="mt-2 w-full rounded-lg border border-white/20 bg-white/10 px-4 py-3 text-white placeholder:text-white/50 focus:border-white/60 focus:outline-none"
/>
</div>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div>
<label className="text-sm font-semibold text-white/80">
{lang === "es" ? "Empresa" : "Company"}
</label>
<input
name="company"
placeholder={
lang === "es" ? "Nombre del negocio" : "Business name"
}
className="mt-2 w-full rounded-lg border border-white/20 bg-white/10 px-4 py-3 text-white placeholder:text-white/50 focus:border-white/60 focus:outline-none"
/>
</div>
<div>
<label className="text-sm font-semibold text-white/80">
{lang === "es" ? "Teléfono" : "Phone"}
</label>
<input
name="phone"
placeholder="+34 600 000 000"
className="mt-2 w-full rounded-lg border border-white/20 bg-white/10 px-4 py-3 text-white placeholder:text-white/50 focus:border-white/60 focus:outline-none"
/>
</div>
</div>
<div>
<label className="text-sm font-semibold text-white/80">
{lang === "es" ? "Mensaje" : "Message"}
</label>
<textarea
required
name="message"
rows={4}
placeholder={
lang === "es"
? "¿Qué quieres automatizar?"
: "What would you like to automate?"
}
className="mt-2 w-full rounded-lg border border-white/20 bg-white/10 px-4 py-3 text-white placeholder:text-white/50 focus:border-white/60 focus:outline-none"
/>
</div>
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<p className="text-xs text-white/60">
{lang === "es"
? "Al enviar, aceptas que te contactemos por email."
: "By submitting, you agree to be contacted by email."}
</p>
<button
type="submit"
className="rounded-lg bg-brand-pink px-6 py-3 text-sm font-semibold text-white shadow-purple-soft transition hover:-translate-y-1 hover:bg-[#ff7bc0]"
>
{lang === "es" ? "Enviar mensaje" : "Send message"}
</button>
</div>
</form>
</motion.div>
</div>
)}
</div>
);
}
+43
View File
@@ -0,0 +1,43 @@
@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;
}
+10
View File
@@ -0,0 +1,10 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./index.css";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
+32
View File
@@ -0,0 +1,32 @@
import type { Config } from "tailwindcss";
export default {
content: [
"./app/**/*.{ts,tsx}",
"./components/**/*.{ts,tsx}",
"./lib/**/*.{ts,tsx}",
],
theme: {
extend: {
colors: {
brand: {
"purple-light": "#667eea",
"purple-dark": "#764ba2",
pink: "#ff69b4",
coral: "#ff6b6b",
},
},
fontFamily: {
sans: ["Inter", "system-ui", "sans-serif"],
},
backgroundImage: {
"brand-gradient": "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
"brand-gradient-dark": "linear-gradient(135deg, #5b6fe0 0%, #5e3f94 100%)",
},
boxShadow: {
"purple-soft": "0 20px 40px -20px rgba(118, 75, 162, 0.45)",
},
},
},
plugins: [],
} satisfies Config;
+20
View File
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}
+36
View File
@@ -0,0 +1,36 @@
{
"compilerOptions": {
"target": "ES2020",
"lib": [
"DOM",
"DOM.Iterable",
"ES2020"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
]
},
"include": [
"**/*.ts",
"**/*.tsx",
"next-env.d.ts",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}
+14
View File
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"noEmit": true,
"strict": true
},
"include": ["vite.config.ts"]
}
+6
View File
@@ -0,0 +1,6 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
});