Fix API key placeholder - add valid MiniMax key in .env.local
This commit is contained in:
@@ -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 }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
@@ -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<Lang>("es");
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
const [showChat, setShowChat] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const recognitionRef = useRef<SpeechRecognition | null>(null);
|
||||
const synthRef = useRef<SpeechSynthesis | null>(null);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(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 (
|
||||
<div className="fixed bottom-6 right-6 z-50">
|
||||
{/* Chat Toggle */}
|
||||
<button
|
||||
onClick={() => setShowChat(!showChat)}
|
||||
className={`${buttonColor} w-14 h-14 rounded-full shadow-lg flex items-center justify-center transition-all hover:scale-110 ${isListening ? "animate-pulse" : ""}`}
|
||||
title={isListening ? "Listening..." : "AI Assistant"}
|
||||
>
|
||||
{isSpeaking ? "🔊" : isListening ? "👂" : "🎙️"}
|
||||
</button>
|
||||
|
||||
{/* Chat Panel */}
|
||||
{showChat && (
|
||||
<div className={`absolute bottom-20 right-0 w-80 ${bgColor} rounded-2xl shadow-2xl border border-white/10 overflow-hidden`}>
|
||||
{/* Header */}
|
||||
<div className={`${theme === "dark" ? "bg-brand-purple" : "bg-blue-600"} p-3 flex items-center justify-between`}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xl">🤖</span>
|
||||
<span className={`font-semibold ${theme === "dark" ? "text-white" : "text-white"}`}>
|
||||
{businessName} AI
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setLanguage(lang => lang === "es" ? "en" : "es")}
|
||||
className="text-xs bg-white/20 px-2 py-1 rounded text-white"
|
||||
>
|
||||
{language === "es" ? "🇪🇸 ES" : "🇬🇧 EN"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
<div className={`h-64 overflow-y-auto p-3 space-y-2 ${textColor}`}>
|
||||
{messages.map((msg, i) => (
|
||||
<div key={i} className={`flex ${msg.role === "user" ? "justify-end" : "justify-start"}`}>
|
||||
<div className={`max-w-[80%] px-3 py-2 rounded-lg text-sm ${
|
||||
msg.role === "user"
|
||||
? buttonColor + " text-white"
|
||||
: theme === "dark" ? "bg-white/10 text-white" : "bg-gray-100 text-gray-900"
|
||||
}`}>
|
||||
{msg.content}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{isListening && (
|
||||
<div className="flex justify-start">
|
||||
<div className="bg-white/10 px-3 py-2 rounded-lg text-sm text-white animate-pulse">
|
||||
🎤 Listening...
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{/* Controls */}
|
||||
<div className={`p-3 border-t ${theme === "dark" ? "border-white/10" : "border-gray-200"} flex gap-2`}>
|
||||
<button
|
||||
onClick={toggleListening}
|
||||
disabled={isSpeaking}
|
||||
className={`flex-1 py-2 rounded-lg font-semibold text-white transition ${
|
||||
isListening ? "bg-red-500 animate-pulse" : buttonColor + " hover:opacity-90"
|
||||
} disabled:opacity-50`}
|
||||
>
|
||||
{isListening ? "🛑 Stop" : "🎤 Hablar"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<div className="px-3 pb-2 text-xs text-red-400">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Type declarations for Web Speech API
|
||||
declare global {
|
||||
interface Window {
|
||||
SpeechRecognition: typeof SpeechRecognition;
|
||||
webkitSpeechRecognition: typeof SpeechRecognition;
|
||||
}
|
||||
}
|
||||
@@ -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 */}
|
||||
|
||||
<MiniMaxVoiceWidget
|
||||
businessName="Restaurante Ejemplo"
|
||||
businessType="restaurant"
|
||||
theme="dark"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 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! 🚀**
|
||||
Reference in New Issue
Block a user