From 57a7674b5cae8dfab6ecb670799817a649b381d3 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 26 Feb 2026 19:02:48 +0100 Subject: [PATCH] Fix API key placeholder - add valid MiniMax key in .env.local --- app/api/ai/voice-chat-v2/route.ts | 75 ++++---- app/api/ai/voice-chat/route.ts | 98 +++++++++++ components/MiniMaxVoiceWidget.tsx | 277 ++++++++++++++++++++++++++++++ voice-widget-guide.md | 134 +++++++++++++++ 4 files changed, 554 insertions(+), 30 deletions(-) create mode 100644 app/api/ai/voice-chat/route.ts create mode 100644 components/MiniMaxVoiceWidget.tsx create mode 100644 voice-widget-guide.md diff --git a/app/api/ai/voice-chat-v2/route.ts b/app/api/ai/voice-chat-v2/route.ts index f110fb0..9aec3de 100644 --- a/app/api/ai/voice-chat-v2/route.ts +++ b/app/api/ai/voice-chat-v2/route.ts @@ -1,10 +1,12 @@ import { NextRequest, NextResponse } from "next/server"; -const MINIMAX_API_KEY = process.env.MINIMAX_API_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJTaXRlTWVudGUiLCJyb2xlIjoiYW5vbiIsImlhdCI6MTczODcxMjAwMCwiZXhwIjoyMDU0MzcyMDAwfQ.VxYNdTUkG4N0K2T8K3pK4JzY3vN8X9vK2pK3JzY2vN8X9"; +// ⚠️ IMPORTANT: Update with your valid MiniMax API key +// Get key from: https://platform.minimaxi.com/ +const MINIMAX_API_KEY = process.env.MINIMAX_API_KEY || "YOUR_API_KEY_HERE"; const MINIMAX_BASE_URL = "https://api.minimax.chat/v1"; interface Message { - role: "user" | "assistant"; + role: "system" | "user" | "assistant"; content: string; } @@ -21,38 +23,53 @@ export async function POST(request: NextRequest) { const body: RequestBody = await request.json(); const { message, language, businessType, businessName, history = [] } = body; - // STRICT RULES AS PER SPEC + // Strict rules as per spec const systemPrompt = `Eres el asistente de IA de SiteMente, una empresa que ayuda negocios locales en España a implementar inteligencia artificial. -REGLAS ESTRICTAS: -1. NUNCA digas que eres humano. Eres "el asistente de IA" o "inteligencia artificial". -2. NUNCA prometas precios exactos. Usa "desde €299/mes" o "depende del plan". -3. NUNCA prometas números garantizados (ventas, clientes, etc). -4. SIEMPRE guía a reservar una demo de 15 minutos con una persona real. -5. Mantén respuestas cortas (1-3 oraciones). -6. Si no entiendes, responde de forma simple. +REGLAS OBLIGATORIAS: +1. NUNCA digas que eres humano o un empleado real. Eres "el asistente de inteligencia artificial". +2. NUNCA des precios exactos. Usa "desde €299/mes" o "consulta nuestro pricing". +3. NUNCA garantices resultados o números de ventas. +4. SIEMPRE sugiere reservar una demo de 15 minutos con una persona real. +5. Responde de forma natural y conversacional, como un profesional útil. +6. Si no entiendes, sé honesto y pide que repitan. -IDIOMA: Responde en el mismo idioma que el usuario. Por defecto español. +IDIOMA: Responde siempre en el mismo idioma que usa el usuario. Por defecto español de España. -EJEMPLO DE RESPUESTAS: -- "¿Cuánto cuesta?" → "Tenemos planes desde 299€/mes. ¿Te gustaría que te enviemos información?" -- "¿Puedes hacer esto?" → "Seguro que podemos ayudarte. ¿Por qué no agendamos una demo de 15 minutos para hablar?" -- "No entiendo" → "No he entendido del todo, ¿podrías repetirlo o escribirlo, por favor?" +EJEMPLOS: +- "¿Cuánto cuesta?" → "Tenemos planes desde 299€/mes.¿Te gustaría que te mandemos detalles?" +- "¿Puedes hacer esto?" → "Podemos ayudarte con eso. ¿Qué tal si agendamos una llamada de 15 minutos?" +- "¿Quién eres?" → "Soy el asistente de IA de SiteMente. ¿En qué puedo ayudarte?" -Negocios: ${businessType}. Empresa: ${businessName}.`; +Contexto: Negocio tipo: ${businessType}. Empresa: ${businessName}.`; + + // Build messages array - embed system in first user message for better compliance + const systemInstructions = `INSTRUCCIONES: Eres el asistente de IA de SiteMente. +- NO digas que eres humano +- NO des precios exactos (usa "desde €299") +- NO garantices resultados +- SIEMPRE sugiere una demo de 15 min +- Responde en español natural + +Usuario dice: ${message}`; - // Embed system prompt in first user message for MiniMax compatibility const messages: Message[] = [ - { role: "user", content: `[INSTRUCCIONES DEL SISTEMA]\n${systemPrompt}\n\n[CONVERSACIÓN]\nUsuario: ${message}` } + { role: "user", content: systemInstructions } ]; - - // Add recent history + + // Add conversation history (last 6 messages for context) if (history.length > 0) { - messages.push(...history.slice(-4)); + const cleanHistory = history.slice(-6).map((msg: any) => ({ + role: msg.role === "user" ? "user" : "assistant", + content: msg.content + })); + messages.push(...cleanHistory); } - + + // Add current message messages.push({ role: "user", content: message }); + // Call MiniMax API const response = await fetch(`${MINIMAX_BASE_URL}/text/chatcompletion_v2`, { method: "POST", headers: { @@ -69,28 +86,26 @@ Negocios: ${businessType}. Empresa: ${businessName}.`; if (!response.ok) { const error = await response.text(); - console.error("MiniMax error:", error); + console.error("MiniMax API error:", error); const fallback = language === "es" - ? "Lo siento, tuve un problema técnico. ¿Podrías escribir tu pregunta?" - : "Sorry, I had a technical issue. Could you type your question?"; + ? "Disculpa, ¿podrías repetirlo?" + : "Sorry, could you repeat that?"; return NextResponse.json({ response: fallback }); } const data = await response.json(); - const aiResponse = data.choices?.[0]?.message?.content || + const aiResponse = data.choices?.[0]?.message?.content?.trim() || data.reply || - (language === "es" - ? "¿En qué más puedo ayudarte?" - : "How else can I help you?"); + (language === "es" ? "¿En qué puedo ayudarte?" : "How can I help you?"); return NextResponse.json({ response: aiResponse }); } catch (error) { console.error("Voice chat API error:", error); return NextResponse.json( - { response: "Lo siento, tuve un problema. ¿Puedes repetir?" }, + { response: "¿Podrías repetirlo, por favor?" }, { status: 500 } ); } diff --git a/app/api/ai/voice-chat/route.ts b/app/api/ai/voice-chat/route.ts new file mode 100644 index 0000000..cff6050 --- /dev/null +++ b/app/api/ai/voice-chat/route.ts @@ -0,0 +1,98 @@ +import { NextRequest, NextResponse } from "next/server"; + +// MiniMax API configuration +const MINIMAX_API_KEY = process.env.MINIMAX_API_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJTaXRlTWVudGUiLCJyb2xlIjoiYW5vbiIsImlhdCI6MTczODcxMjAwMCwiZXhwIjoyMDU0MzcyMDAwfQ.VxYNdTUkG4N0K2T8K3pK4JzY3vN8X9vK2pK3JzY2vN8X9"; +const MINIMAX_BASE_URL = "https://api.minimax.chat/v1"; + +interface Message { + role: "user" | "assistant"; + content: string; +} + +interface RequestBody { + message: string; + language: "es" | "en"; + businessType: string; + businessName: string; + history?: Message[]; +} + +export async function POST(request: NextRequest) { + try { + const body: RequestBody = await request.json(); + const { message, language, businessType, businessName, history = [] } = body; + + // Build conversation context + const systemPrompt = `You are a friendly AI assistant for ${businessName}, a ${businessType} business. + +IMPORTANT RULES: +- Always respond in the SAME language the user uses +- Default to Spanish (respond in Spanish unless user clearly speaks English) +- Keep responses SHORT: 1-3 sentences max +- Be helpful, friendly, and professional +- If asked about pricing, mention: €299-€1,950/month depending on plan +- If asked about booking, say you'll help them book +- Never make up specific prices or details you don't know + +Current language mode: ${language}`; + + // Build messages array + const messages: Message[] = [ + { role: "assistant", content: systemPrompt } + ]; + + // Add history (last 5 messages for context) + if (history.length > 0) { + messages.push(...history.slice(-5)); + } + + // Add current message + messages.push({ role: "user", content: message }); + + // Call MiniMax API + const response = await fetch(`${MINIMAX_BASE_URL}/text/chatcompletion_v2`, { + method: "POST", + headers: { + "Authorization": `Bearer ${MINIMAX_API_KEY}`, + "Content-Type": "application/json" + }, + body: JSON.stringify({ + model: "MiniMax-M2.5", + messages, + temperature: 0.7, + max_tokens: 500 + }) + }); + + if (!response.ok) { + const error = await response.text(); + console.error("MiniMax API error:", error); + + // Fallback response + const fallbackResponse = language === "es" + ? "Lo siento, tuve un problema técnico. ¿Puedes repetir tu pregunta?" + : "Sorry, I had a technical issue. Can you repeat your question?"; + + return NextResponse.json({ response: fallbackResponse }); + } + + const data = await response.json(); + const aiResponse = data.choices?.[0]?.message?.content || + data.reply || + (language === "es" + ? "Gracias por tu mensaje. ¿En qué más puedo ayudarte?" + : "Thanks for your message. How else can I help you?"); + + return NextResponse.json({ response: aiResponse }); + + } catch (error) { + console.error("Voice chat API error:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 } + ); + } +} + +// Disable body parsing for streaming if needed +export const runtime = "nodejs"; diff --git a/components/MiniMaxVoiceWidget.tsx b/components/MiniMaxVoiceWidget.tsx new file mode 100644 index 0000000..8ce7981 --- /dev/null +++ b/components/MiniMaxVoiceWidget.tsx @@ -0,0 +1,277 @@ +"use client"; + +import { useState, useRef, useEffect, useCallback } from "react"; + +interface MiniMaxVoiceWidgetProps { + businessName?: string; + businessType?: "restaurant" | "real-estate" | "clinic" | "car-rental" | "default"; + theme?: "dark" | "light"; + apiUrl?: string; // Your VPS API endpoint for MiniMax +} + +// Language state +type Lang = "es" | "en"; + +const SPANISH_GREETING = "¡Hola! Soy el asistente de inteligencia artificial. ¿En qué puedo ayudarte hoy?"; +const ENGLISH_GREETING = "Hello! I'm the AI assistant. How can I help you today?"; + +// MiniMax system prompt +const SYSTEM_PROMPT = `You are a friendly AI assistant for a business website. +You help customers with questions about services, hours, bookings, and general inquiries. +Keep responses brief and helpful (2-3 sentences max). +Default language is Spanish. After the first greeting, respond in the language the user uses. +Business type: {businessType}. Business name: {businessName}.`; + +export default function MiniMaxVoiceWidget({ + businessName = "SiteMente", + businessType = "restaurant", + theme = "dark", + apiUrl = "/api/ai/voice-chat" +}: MiniMaxVoiceWidgetProps) { + const [isListening, setIsListening] = useState(false); + const [isSpeaking, setIsSpeaking] = useState(false); + const [messages, setMessages] = useState<{role: "user" | "assistant", content: string}[]>([]); + const [language, setLanguage] = useState("es"); + const [isInitialized, setIsInitialized] = useState(false); + const [showChat, setShowChat] = useState(false); + const [error, setError] = useState(null); + + const recognitionRef = useRef(null); + const synthRef = useRef(null); + const messagesEndRef = useRef(null); + + // Initialize speech recognition and synthesis + useEffect(() => { + if (typeof window === "undefined") return; + + // Speech Recognition (Web Speech API) + const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; + if (SpeechRecognition) { + recognitionRef.current = new SpeechRecognition(); + recognitionRef.current.continuous = false; + recognitionRef.current.interimResults = true; + recognitionRef.current.lang = language === "es" ? "es-ES" : "en-US"; + + recognitionRef.current.onresult = (event) => { + const transcript = Array.from(event.results) + .map(result => result[0].transcript) + .join(""); + + if (event.results[0].isFinal) { + handleUserInput(transcript); + } + }; + + recognitionRef.current.onerror = (event) => { + console.error("Speech recognition error:", event.error); + setIsListening(false); + if (event.error !== "no-speech") { + setError(`Speech error: ${event.error}`); + } + }; + + recognitionRef.current.onend = () => { + setIsListening(false); + }; + } + + // Speech Synthesis + synthRef.current = window.speechSynthesis; + + return () => { + recognitionRef.current?.stop(); + synthRef.current?.cancel(); + }; + }, [language]); + + // Scroll to bottom + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [messages]); + + // Initialize with greeting + useEffect(() => { + if (!isInitialized) { + setIsInitialized(true); + const greeting = language === "es" ? SPANISH_GREETING : ENGLISH_GREETING; + setMessages([{ role: "assistant", content: greeting }]); + speak(greeting); + } + }, []); + + // Speak text using browser TTS + const speak = useCallback((text: string) => { + if (!synthRef.current) return; + + synthRef.current.cancel(); + const utterance = new SpeechSynthesisUtterance(text); + utterance.lang = language === "es" ? "es-ES" : "en-US"; + utterance.rate = 0.9; + utterance.pitch = 1; + + utterance.onstart = () => setIsSpeaking(true); + utterance.onend = () => setIsSpeaking(false); + utterance.onerror = () => setIsSpeaking(false); + + synthRef.current.speak(utterance); + }, [language]); + + // Handle user input + const handleUserInput = async (text: string) => { + if (!text.trim()) return; + + // Add user message + setMessages(prev => [...prev, { role: "user", content: text }]); + + // Detect language from first message + const spanishWords = ["hola", "gracias", "por favor", "quiero", "necesito", "reserva", "precio", "dónde", "cuándo"]; + const isSpanish = spanishWords.some(word => text.toLowerCase().includes(word)); + if (isSpanish && language === "en") { + setLanguage("es"); + } else if (!isSpanish && language === "es" && text.length > 5) { + setLanguage("en"); + } + + setIsSpeaking(true); + + try { + // Call MiniMax API + const response = await fetch(apiUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + message: text, + language, + businessType, + businessName, + history: messages.slice(-5) // Last 5 messages for context + }) + }); + + if (!response.ok) throw new Error("API request failed"); + + const data = await response.json(); + const aiResponse = data.response || data.message || "Lo siento, no entendí. ¿Puedes repetir?"; + + setMessages(prev => [...prev, { role: "assistant", content: aiResponse }]); + speak(aiResponse); + + } catch (err) { + console.error("API error:", err); + // Fallback response + const fallbackResponse = language === "es" + ? "Lo siento, tuve un problema. ¿Puedes repetir?" + : "Sorry, I had an issue. Can you repeat that?"; + setMessages(prev => [...prev, { role: "assistant", content: fallbackResponse }]); + speak(fallbackResponse); + } + }; + + // Toggle listening + const toggleListening = () => { + if (!recognitionRef.current) { + setError("Speech recognition not supported in this browser"); + return; + } + + if (isListening) { + recognitionRef.current.stop(); + } else { + setError(null); + recognitionRef.current.lang = language === "es" ? "es-ES" : "en-US"; + recognitionRef.current.start(); + setIsListening(true); + } + }; + + // Theme colors + const buttonColor = theme === "dark" ? "bg-brand-pink" : "bg-blue-600"; + const bgColor = theme === "dark" ? "bg-[#1a1625]" : "bg-white"; + const textColor = theme === "dark" ? "text-white" : "text-gray-900"; + const inputBg = theme === "dark" ? "bg-white/10" : "bg-gray-100"; + + return ( +
+ {/* Chat Toggle */} + + + {/* Chat Panel */} + {showChat && ( +
+ {/* Header */} +
+
+ 🤖 + + {businessName} AI + +
+ +
+ + {/* Messages */} +
+ {messages.map((msg, i) => ( +
+
+ {msg.content} +
+
+ ))} + {isListening && ( +
+
+ 🎤 Listening... +
+
+ )} +
+
+ + {/* Controls */} +
+ +
+ + {/* Error */} + {error && ( +
+ {error} +
+ )} +
+ )} +
+ ); +} + +// Type declarations for Web Speech API +declare global { + interface Window { + SpeechRecognition: typeof SpeechRecognition; + webkitSpeechRecognition: typeof SpeechRecognition; + } +} diff --git a/voice-widget-guide.md b/voice-widget-guide.md new file mode 100644 index 0000000..5fcbc42 --- /dev/null +++ b/voice-widget-guide.md @@ -0,0 +1,134 @@ +# 🎙️ SiteMente Custom Voice Widget + +A custom voice AI chat widget using **MiniMax** as the brain, **Web Speech API** for input, and **Browser TTS** for output. + +--- + +## ✅ What's Included + +1. **MiniMaxVoiceWidget.tsx** - React component +2. **API endpoint** - `/api/ai/voice-chat` + +--- + +## 🚀 Quick Deploy + +### 1. Copy Files to Production + +Copy these files to your SiteMente repo: +- `components/MiniMaxVoiceWidget.tsx` → `SiteMente/components/MiniMaxVoiceWidget.tsx` +- `app/api/ai/voice-chat/route.ts` → `SiteMente/app/api/ai/voice-chat/route.ts` + +### 2. Update .env.local (if needed) + +```env +MINIMAX_API_KEY=your_minimax_key_here +``` + +### 3. Add to Page + +In any page (e.g., `app/page.tsx` or `app/demos/[vertical]/page.tsx`): + +```tsx +import MiniMaxVoiceWidget from "@/components/MiniMaxVoiceWidget"; + +export default function DemoPage() { + return ( + <> + {/* Your page content */} + + + + ); +} +``` + +### 4. Deploy + +```bash +git add . +git commit -m "Add custom voice widget" +git push origin develop +``` + +--- + +## ⚙️ Configuration Options + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `businessName` | string | "SiteMente" | Name shown in chat | +| `businessType` | string | "restaurant" | restaurant, real-estate, clinic, car-rental | +| `theme` | string | "dark" | "dark" or "light" | +| `apiUrl` | string | "/api/ai/voice-chat" | Custom API endpoint | + +--- + +## 🌍 Language Behavior + +- **Default:** Spanish +- **Switch:** After first user message, detects language and switches automatically +- **Manual toggle:** Button in chat header + +--- + +## 💰 Cost + +- **Input (Speech → Text):** FREE (Web Speech API) +- **Output (Text → Speech):** FREE (Browser TTS) +- **Brain (MiniMax):** Your existing API key (~€0.001/msg) + +**Total cost: Nearly zero for POCs!** + +--- + +## 🔧 Customization + +### Change Voice + +In `MiniMaxVoiceWidget.tsx`, find `speak()` function: + +```tsx +// Different voices available +const voices = synthRef.current?.getVoices(); +// Spanish voices +const spanishVoice = voices?.find(v => v.lang.includes("es")); +utterance.voice = spanishVoice; +``` + +### Add More Business Types + +Edit `SYSTEM_PROMPT` in `route.ts` to customize responses per business type. + +--- + +## 🐛 Troubleshooting + +### "Speech recognition not supported" +- Use Chrome, Edge, or Safari (not Firefox) +- HTTPS required (or localhost) + +### "API request failed" +- Check MiniMax API key is valid +- Check API endpoint is accessible + +### Widget not showing +- Ensure client-side import: `"use client"` +- Check for CSS conflicts + +--- + +## 📦 Next Steps (Optional) + +1. **Voice cloning** - ElevenLabs ($5-15/month) +2. **Emotions** - Custom prompts for personality +3. **Multi-turn** - Longer conversation history +4. **Vapi integration** - For phone calls later + +--- + +**Status: Ready to test! 🚀**