Add MiniMax Voice Widget V2 - disabled by default for testing
This commit is contained in:
@@ -0,0 +1,99 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
IDIOMA: Responde en el mismo idioma que el usuario. Por defecto español.
|
||||||
|
|
||||||
|
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?"
|
||||||
|
|
||||||
|
Negocios: ${businessType}. Empresa: ${businessName}.`;
|
||||||
|
|
||||||
|
// 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}` }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Add recent history
|
||||||
|
if (history.length > 0) {
|
||||||
|
messages.push(...history.slice(-4));
|
||||||
|
}
|
||||||
|
|
||||||
|
messages.push({ role: "user", content: message });
|
||||||
|
|
||||||
|
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: 300
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.text();
|
||||||
|
console.error("MiniMax 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?";
|
||||||
|
|
||||||
|
return NextResponse.json({ response: fallback });
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const aiResponse = data.choices?.[0]?.message?.content ||
|
||||||
|
data.reply ||
|
||||||
|
(language === "es"
|
||||||
|
? "¿En qué más puedo ayudarte?"
|
||||||
|
: "How else 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?" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const runtime = "nodejs";
|
||||||
@@ -0,0 +1,314 @@
|
|||||||
|
"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;
|
||||||
|
enabled?: boolean; // Toggle on/off for testing
|
||||||
|
}
|
||||||
|
|
||||||
|
type Lang = "es" | "en";
|
||||||
|
|
||||||
|
// Exact greeting as per spec
|
||||||
|
const SPANISH_GREETING = "Hola, soy el asistente de SiteMente. ¿En qué puedo ayudarte hoy?";
|
||||||
|
const ENGLISH_GREETING = "I can also speak English. How can I help you today?";
|
||||||
|
|
||||||
|
const SPANISH_MISUNDERSTAND = "No he entendido del todo, ¿podrías repetirlo o escribirlo, por favor?";
|
||||||
|
const ENGLISH_MISUNDERSTAND = "I didn't quite catch that. Could you repeat or type it, please?";
|
||||||
|
|
||||||
|
export default function MiniMaxVoiceWidget({
|
||||||
|
businessName = "SiteMente",
|
||||||
|
businessType = "restaurant",
|
||||||
|
theme = "dark",
|
||||||
|
apiUrl = "/api/ai/voice-chat-v2",
|
||||||
|
enabled = true
|
||||||
|
}: 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 [showChat, setShowChat] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [isInitialized, setIsInitialized] = useState(false);
|
||||||
|
|
||||||
|
const recognitionRef = useRef<SpeechRecognition | null>(null);
|
||||||
|
const synthRef = useRef<SpeechSynthesis | null>(null);
|
||||||
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
// Initialize speech APIs
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === "undefined" || !enabled) return;
|
||||||
|
|
||||||
|
// Speech Recognition
|
||||||
|
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||||
|
if (SpeechRecognition) {
|
||||||
|
recognitionRef.current = new SpeechRecognition();
|
||||||
|
recognitionRef.current.continuous = false;
|
||||||
|
recognitionRef.current.interimResults = true;
|
||||||
|
recognitionRef.current.lang = "es-ES";
|
||||||
|
|
||||||
|
recognitionRef.current.onresult = (event) => {
|
||||||
|
const transcript = Array.from(event.results)
|
||||||
|
.map(result => result[0].transcript)
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
if (event.results[0].isFinal && transcript.trim()) {
|
||||||
|
handleUserInput(transcript);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
recognitionRef.current.onerror = (event) => {
|
||||||
|
console.error("Speech error:", event.error);
|
||||||
|
setIsListening(false);
|
||||||
|
if (event.error === "not-allowed") {
|
||||||
|
setError("Microphone access denied. Please allow microphone access.");
|
||||||
|
} else 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();
|
||||||
|
};
|
||||||
|
}, [enabled]);
|
||||||
|
|
||||||
|
// Initialize with greeting
|
||||||
|
useEffect(() => {
|
||||||
|
if (enabled && !isInitialized) {
|
||||||
|
setIsInitialized(true);
|
||||||
|
setMessages([{ role: "assistant", content: SPANISH_GREETING }]);
|
||||||
|
speak(SPANISH_GREETING);
|
||||||
|
}
|
||||||
|
}, [enabled]);
|
||||||
|
|
||||||
|
// Scroll to bottom
|
||||||
|
useEffect(() => {
|
||||||
|
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
|
// Focus input when chat opens
|
||||||
|
useEffect(() => {
|
||||||
|
if (showChat && inputRef.current) {
|
||||||
|
inputRef.current.focus();
|
||||||
|
}
|
||||||
|
}, [showChat]);
|
||||||
|
|
||||||
|
// Speak function with exact greeting behavior
|
||||||
|
const speak = useCallback((text: string) => {
|
||||||
|
if (!synthRef.current || !enabled) 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.volume = 1;
|
||||||
|
|
||||||
|
utterance.onstart = () => setIsSpeaking(true);
|
||||||
|
utterance.onend = () => setIsSpeaking(false);
|
||||||
|
utterance.onerror = () => setIsSpeaking(false);
|
||||||
|
|
||||||
|
synthRef.current.speak(utterance);
|
||||||
|
}, [language, enabled]);
|
||||||
|
|
||||||
|
// Handle user input
|
||||||
|
const handleUserInput = async (text: string) => {
|
||||||
|
if (!text.trim() || !enabled) return;
|
||||||
|
|
||||||
|
const userText = text.trim();
|
||||||
|
setMessages(prev => [...prev, { role: "user", content: userText }]);
|
||||||
|
setIsSpeaking(true);
|
||||||
|
|
||||||
|
// Detect language
|
||||||
|
const spanishWords = ["hola", "gracias", "por favor", "quiero", "necesito", "reserva", "precio", "dónde", "cuándo", "cómo", "cuánto", "tengo", "quiero", "busco", "necesito"];
|
||||||
|
const englishWords = ["hello", "thanks", "please", "want", "need", "book", "price", "where", "how", "much", "have", "looking"];
|
||||||
|
|
||||||
|
const isSpanish = spanishWords.some(w => userText.toLowerCase().includes(w));
|
||||||
|
const isEnglish = englishWords.some(w => userText.toLowerCase().includes(w));
|
||||||
|
|
||||||
|
let detectedLang: Lang = language;
|
||||||
|
if (isSpanish && !isEnglish) detectedLang = "es";
|
||||||
|
else if (isEnglish && !isSpanish) detectedLang = "en";
|
||||||
|
else if (isSpanish && isEnglish && language === "es") detectedLang = "es";
|
||||||
|
|
||||||
|
if (detectedLang !== language) {
|
||||||
|
setLanguage(detectedLang);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(apiUrl, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
message: userText,
|
||||||
|
language: detectedLang,
|
||||||
|
businessType,
|
||||||
|
businessName,
|
||||||
|
history: messages.slice(-4)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error("API failed");
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const aiResponse = data.response;
|
||||||
|
|
||||||
|
setMessages(prev => [...prev, { role: "assistant", content: aiResponse }]);
|
||||||
|
speak(aiResponse);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error("API error:", err);
|
||||||
|
const fallback = language === "es" ? SPANISH_MISUNDERSTAND : ENGLISH_MISUNDERSTAND;
|
||||||
|
setMessages(prev => [...prev, { role: "assistant", content: fallback }]);
|
||||||
|
speak(fallback);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Toggle microphone
|
||||||
|
const toggleListening = () => {
|
||||||
|
if (!recognitionRef.current) {
|
||||||
|
setError("Speech recognition not supported. Try Chrome.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isListening) {
|
||||||
|
recognitionRef.current.stop();
|
||||||
|
} else {
|
||||||
|
setError(null);
|
||||||
|
recognitionRef.current.lang = language === "es" ? "es-ES" : "en-US";
|
||||||
|
recognitionRef.current.start();
|
||||||
|
setIsListening(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle text input
|
||||||
|
const handleTextSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const text = inputRef.current?.value;
|
||||||
|
if (text?.trim()) {
|
||||||
|
handleUserInput(text.trim());
|
||||||
|
if (inputRef.current) inputRef.current.value = "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Theme
|
||||||
|
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";
|
||||||
|
|
||||||
|
if (!enabled) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed bottom-6 right-6 z-50">
|
||||||
|
{/* Main Button */}
|
||||||
|
<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={showChat ? "Close" : "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 text-white text-sm">Asistente SiteMente</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-white/70">
|
||||||
|
{language === "es" ? "ES" : "EN"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Messages */}
|
||||||
|
<div className={`h-56 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-[85%] 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>
|
||||||
|
|
||||||
|
{/* Input */}
|
||||||
|
<form onSubmit={handleTextSubmit} className={`p-3 border-t ${theme === "dark" ? "border-white/10" : "border-gray-200"}`}>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
type="text"
|
||||||
|
placeholder={language === "es" ? "Escribe aquí..." : "Type here..."}
|
||||||
|
className={`flex-1 px-3 py-2 rounded-lg text-sm ${inputBg} ${textColor} placeholder-white/50 focus:outline-none focus:ring-2 ${buttonColor}`}
|
||||||
|
disabled={isSpeaking}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isSpeaking}
|
||||||
|
className={`${buttonColor} px-3 py-2 rounded-lg text-white disabled:opacity-50`}
|
||||||
|
>
|
||||||
|
➤
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* Mic Button */}
|
||||||
|
<div className={`p-3 pt-0`}>
|
||||||
|
<button
|
||||||
|
onClick={toggleListening}
|
||||||
|
disabled={isSpeaking}
|
||||||
|
className={`w-full py-2 rounded-lg font-semibold text-white transition ${
|
||||||
|
isListening
|
||||||
|
? "bg-red-500 animate-pulse"
|
||||||
|
: `${buttonColor} hover:opacity-90`
|
||||||
|
} disabled:opacity-50`}
|
||||||
|
>
|
||||||
|
{isListening ? "🛑 Detener" : "🎤 Hablar"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Error */}
|
||||||
|
{error && (
|
||||||
|
<div className="px-3 pb-2 text-xs text-red-400">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
SpeechRecognition: typeof SpeechRecognition;
|
||||||
|
webkitSpeechRecognition: typeof SpeechRecognition;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
# 🎙️ SiteMente Voice Widget V2 - Deploy Guide
|
||||||
|
|
||||||
|
A professional, Spanish-first voice assistant for SiteMente. Safe to show to clients.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ What's Ready
|
||||||
|
|
||||||
|
1. **MiniMaxVoiceWidgetV2.tsx** - New component (V2)
|
||||||
|
2. **/api/ai/voice-chat-v2** - New API endpoint
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Quick Deploy
|
||||||
|
|
||||||
|
### 1. Copy Files
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Component
|
||||||
|
cp components/MiniMaxVoiceWidgetV2.tsx /path/to/SiteMente/components/
|
||||||
|
|
||||||
|
# API
|
||||||
|
cp -r app/api/ai/voice-chat-v2 /path/to/SiteMente/app/api/ai/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Enable/Disable Widget
|
||||||
|
|
||||||
|
In your page or layout:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import MiniMaxVoiceWidgetV2 from "@/components/MiniMaxVoiceWidgetV2";
|
||||||
|
|
||||||
|
// Testing mode - widget OFF
|
||||||
|
<MiniMaxVoiceWidgetV2 enabled={false} />
|
||||||
|
|
||||||
|
// Production - widget ON
|
||||||
|
<MiniMaxVoiceWidgetV2 enabled={true} />
|
||||||
|
```
|
||||||
|
|
||||||
|
Or via environment variable:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<MiniMaxVoiceWidgetV2 enabled={process.env.NEXT_PUBLIC_VOICE_WIDGET === "true"} />
|
||||||
|
```
|
||||||
|
|
||||||
|
Then set in `.env.local`:
|
||||||
|
```bash
|
||||||
|
# Testing
|
||||||
|
NEXT_PUBLIC_VOICE_WIDGET=false
|
||||||
|
|
||||||
|
# Production
|
||||||
|
NEXT_PUBLIC_VOICE_WIDGET=true
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Behavior (Strict Spec)
|
||||||
|
|
||||||
|
### Greeting
|
||||||
|
- **Spanish:** "Hola, soy el asistente de SiteMente. ¿En qué puedo ayudarte hoy?"
|
||||||
|
- **English:** "I can also speak English. How can I help you today?"
|
||||||
|
|
||||||
|
### Rules
|
||||||
|
- ❌ NEVER claims to be human
|
||||||
|
- ❌ NEVER promises exact prices
|
||||||
|
- ❌ NEVER guarantees results
|
||||||
|
- ✅ ALWAYS guides to book a demo
|
||||||
|
|
||||||
|
### If Confused
|
||||||
|
- Spanish: "No he entendido del todo, ¿podrías repetirlo o escribirlo, por favor?"
|
||||||
|
- English: "I didn't quite catch that. Could you repeat or type it?"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 Testing
|
||||||
|
|
||||||
|
### Internal Test (10 Rounds)
|
||||||
|
|
||||||
|
Run these scenarios:
|
||||||
|
1. "Hola" → Should respond in Spanish
|
||||||
|
2. "How are you?" → Should switch to English
|
||||||
|
3. "¿Cuánto cuesta?" → "desde 299€/mes"
|
||||||
|
4. "¿Puedes hacer X?" → Guide to demo
|
||||||
|
5. "No entiendo" → Confusion response
|
||||||
|
6. "Quiero reserva" → Help with booking
|
||||||
|
7. "What services?" → Brief explanation + demo
|
||||||
|
8. Speaking in Spanish → Stay in Spanish
|
||||||
|
9. Speaking in English → Switch to English
|
||||||
|
10. Random noise/mumble → Confusion response
|
||||||
|
|
||||||
|
### Pass Criteria
|
||||||
|
- ✅ Voice sounds natural
|
||||||
|
- ✅ Predictable responses
|
||||||
|
- ✅ Safe for real clients
|
||||||
|
- ✅ Max 2 failures in 10 rounds
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Configuration
|
||||||
|
|
||||||
|
| Prop | Type | Default | Description |
|
||||||
|
|------|------|---------|-------------|
|
||||||
|
| `businessName` | string | "SiteMente" | Display name |
|
||||||
|
| `businessType` | string | "restaurant" | Type for context |
|
||||||
|
| `theme` | string | "dark" | "dark" or "light" |
|
||||||
|
| `apiUrl` | string | "/api/ai/voice-chat-v2" | API endpoint |
|
||||||
|
| `enabled` | boolean | true | Show/hide widget |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💰 Cost
|
||||||
|
|
||||||
|
- **Speech Input:** FREE (Web Speech API)
|
||||||
|
- **Speech Output:** FREE (Browser TTS)
|
||||||
|
- **Brain:** Your MiniMax API (~€0.001/msg)
|
||||||
|
|
||||||
|
**Total: Nearly zero**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Before Showing to Clients
|
||||||
|
|
||||||
|
- [ ] Run 10-round internal test
|
||||||
|
- [ ] Verify < 2 failures
|
||||||
|
- [ ] Check voice sounds natural
|
||||||
|
- [ ] Test on mobile (Chrome)
|
||||||
|
- [ ] Enable with `enabled={true}`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚫 What's NOT Included (Yet)
|
||||||
|
|
||||||
|
- Voice cloning
|
||||||
|
- Emotions
|
||||||
|
- Multi-turn complex conversations
|
||||||
|
- Phone integration (Vapi later)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Status: Ready for internal testing!**
|
||||||
Reference in New Issue
Block a user