Spaces:
Running
Running
| import { useState, useRef } from "react"; | |
| import API from "../api"; | |
| import "./PurityAI.css"; | |
| const ELEVEN_KEY = "sk_2d090a3d6c13a30f2a89c5913a30c3f83ac82ff99a46c9b8"; | |
| const VOICE_ID = "EXAVITQu4vr4xnSDxMaL"; | |
| export default function PurityAI({ balance, onTransfer, onExternalTransfer }) { | |
| const [active, setActive] = useState(false); | |
| const [status, setStatus] = useState("idle"); | |
| const [transcript, setTranscript] = useState(""); | |
| const [response, setResponse] = useState(""); | |
| const speak = async (text) => { | |
| setStatus("speaking"); | |
| setResponse(text); | |
| try { | |
| const res = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${VOICE_ID}`, { | |
| method: "POST", | |
| headers: { | |
| "xi-api-key": ELEVEN_KEY, | |
| "Content-Type": "application/json", | |
| }, | |
| body: JSON.stringify({ | |
| text, | |
| model_id: "eleven_monolingual_v1", | |
| voice_settings: { stability: 0.5, similarity_boost: 0.75 }, | |
| }), | |
| }); | |
| if (!res.ok) throw new Error("ElevenLabs failed"); | |
| const blob = await res.blob(); | |
| const url = URL.createObjectURL(blob); | |
| const audio = new Audio(url); | |
| audio.onended = () => setStatus("idle"); | |
| await audio.play(); | |
| } catch { | |
| const u = new SpeechSynthesisUtterance(text); | |
| u.rate = 0.9; u.pitch = 1.1; | |
| const voices = speechSynthesis.getVoices(); | |
| const female = voices.find(v => v.name.includes("Female") || v.name.includes("Samantha") || v.name.includes("Google UK English Female")); | |
| if (female) u.voice = female; | |
| u.onend = () => setStatus("idle"); | |
| speechSynthesis.speak(u); | |
| } | |
| }; | |
| const handleCommand = async (text) => { | |
| const lower = text.toLowerCase(); | |
| if (lower.includes("balance") || lower.includes("how much")) { | |
| const bal = balance?.balance?.toLocaleString("en-NG") ?? "unavailable"; | |
| speak(`Your available balance is ${bal} Naira.`); | |
| } else if (lower.includes("send") || lower.includes("transfer") || lower.includes("pay")) { | |
| speak("Opening the transfer screen for you right now."); | |
| setTimeout(() => onTransfer && onTransfer(), 1500); | |
| } else if (lower.includes("bank") || lower.includes("external") || lower.includes("moniepoint")) { | |
| speak("Opening external bank transfer."); | |
| setTimeout(() => onExternalTransfer && onExternalTransfer(), 1500); | |
| } else if (lower.includes("hello") || lower.includes("hi") || lower.includes("hey")) { | |
| speak(`Hello! I'm Purity, your dolor3v assistant. I can check your balance, help you send money, or transfer to other banks. What would you like to do?`); | |
| } else if (lower.includes("account") || lower.includes("number")) { | |
| speak(`Your account number is ${balance?.account_number ?? "unavailable"}.`); | |
| } else { | |
| speak("I can help you check your balance, send money, or make a bank transfer. Just tell me what you need!"); | |
| } | |
| }; | |
| const startListening = () => { | |
| const SR = window.SpeechRecognition || window.webkitSpeechRecognition; | |
| if (!SR) { | |
| speak("Voice recognition isn't available on this browser. Try Chrome."); | |
| return; | |
| } | |
| const r = new SR(); | |
| r.lang = "en-US"; | |
| r.onstart = () => setStatus("listening"); | |
| r.onresult = (e) => { | |
| const text = e.results[0][0].transcript; | |
| setTranscript(text); | |
| handleCommand(text); | |
| }; | |
| r.onerror = () => { setStatus("idle"); speak("I didn't catch that. Please try again."); }; | |
| r.onend = () => { if (status === "listening") setStatus("idle"); }; | |
| r.start(); | |
| }; | |
| const handleTap = () => { | |
| if (!active) { | |
| setActive(true); | |
| speak("Hi! I'm Purity, your dolor3v banking assistant. How can I help you today?"); | |
| } else { | |
| startListening(); | |
| } | |
| }; | |
| return ( | |
| <div className="purity-wrap"> | |
| <div className={`purity-orb ${status}`} onClick={handleTap}> | |
| <div className="orb-ring" /> | |
| <div className="orb-ring r2" /> | |
| <div className="orb-core"> | |
| {status === "listening" ? "🎙" : status === "speaking" ? "🔊" : "✦"} | |
| </div> | |
| </div> | |
| <h2 className="purity-name">Purity AI</h2> | |
| <p className="purity-sub"> | |
| {status === "listening" ? "Listening..." : status === "speaking" ? "Speaking..." : active ? "Tap to speak" : "Tap to wake up"} | |
| </p> | |
| {transcript && ( | |
| <div className="purity-transcript">You: "{transcript}"</div> | |
| )} | |
| {response && ( | |
| <div className="purity-response">Purity: "{response}"</div> | |
| )} | |
| <div className="purity-hints"> | |
| <span onClick={() => handleCommand("check my balance")}>💰 Balance</span> | |
| <span onClick={() => handleCommand("send money")}>↑ Send</span> | |
| <span onClick={() => handleCommand("transfer to bank")}>🏦 Bank</span> | |
| <span onClick={() => handleCommand("hello")}>👋 Hi</span> | |
| </div> | |
| </div> | |
| ); | |
| } | |