"use client"; import { useState, useRef, useEffect, useCallback } from "react"; interface SynthflowWidgetProps { apiKey: string; assistantId: string; theme?: "dark" | "light"; } export default function SynthflowWidget({ apiKey, assistantId, theme = "dark" }: SynthflowWidgetProps) { const [isConnected, setIsConnected] = useState(false); const [isTalking, setIsTalking] = useState(false); const [status, setStatus] = useState<"idle" | "connecting" | "ready" | "talking" | "error">("idle"); const [transcript, setTranscript] = useState(""); const [error, setError] = useState(""); const wsRef = useRef(null); const audioContextRef = useRef(null); const mediaStreamRef = useRef(null); const audioChunksRef = useRef([]); const connect = useCallback(async () => { try { setStatus("connecting"); setError(""); // Get WebSocket token const tokenRes = await fetch(`https://widget.synthflow.ai/websocket/token/${assistantId}`, { headers: { "Authorization": `Bearer ${apiKey}` } }); if (!tokenRes.ok) { throw new Error("Failed to get token"); } const { sessionURL } = await tokenRes.json(); // Connect to WebSocket const ws = new WebSocket(sessionURL); wsRef.current = ws; ws.onopen = () => { console.log("WebSocket connected"); setIsConnected(true); setStatus("ready"); // Send ready signal ws.send(JSON.stringify({ type: "status_client_ready" })); }; ws.onmessage = async (event) => { if (typeof event.data === "string") { const data = JSON.parse(event.data); console.log("WS message:", data); if (data.type === "transcript") { setTranscript(data.text || ""); } else if (data.type === "status_agent_ready") { setStatus("ready"); } } else if (event.data instanceof Blob) { // Audio from agent - play it const arrayBuffer = await event.data.arrayBuffer(); await playAudio(new Int16Array(arrayBuffer)); } }; ws.onerror = (err) => { console.error("WS error:", err); setError("Connection error"); setStatus("error"); }; ws.onclose = () => { setIsConnected(false); setStatus("idle"); }; } catch (err: any) { console.error("Connection failed:", err); setError(err.message); setStatus("error"); } }, [apiKey, assistantId]); const startRecording = async () => { try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); mediaStreamRef.current = stream; // Create audio context const audioContext = new AudioContext({ sampleRate: 48000 }); audioContextRef.current = audioContext; const source = audioContext.createMediaStreamSource(stream); const processor = audioContext.createScriptProcessor(4096, 1, 1); processor.onaudioprocess = (e) => { if (wsRef.current?.readyState === WebSocket.OPEN) { const inputData = e.inputBuffer.getChannelData(0); const int16Data = float32ToInt16(inputData); wsRef.current.send(int16Data); } }; source.connect(processor); processor.connect(audioContext.destination); setIsTalking(true); setStatus("talking"); } catch (err: any) { console.error("Recording error:", err); setError("Microphone access denied"); } }; const stopRecording = () => { if (mediaStreamRef.current) { mediaStreamRef.current.getTracks().forEach(track => track.stop()); } if (audioContextRef.current) { audioContextRef.current.close(); } setIsTalking(false); setStatus("ready"); }; const playAudio = async (int16Data: Int16Array) => { try { const ctx = new AudioContext({ sampleRate: 16000 }); const buffer = ctx.createBuffer(1, int16Data.length, 16000); buffer.getChannelData(0).set(int16ToFloat32(int16Data)); const source = ctx.createBufferSource(); source.buffer = buffer; source.connect(ctx.destination); source.start(); } catch (err) { console.error("Play audio error:", err); } }; const float32ToInt16 = (float32: Float32Array): Int16Array => { const int16 = new Int16Array(float32.length); for (let i = 0; i < float32.length; i++) { int16[i] = Math.max(-1, Math.min(1, float32[i])) * 0x7FFF; } return int16; }; const int16ToFloat32 = (int16: Int16Array): Float32Array => { const float32 = new Float32Array(int16.length); for (let i = 0; i < int16.length; i++) { float32[i] = int16[i] / 0x7FFF; } return float32; }; const toggleCall = () => { if (!isConnected) { connect(); } else if (isTalking) { stopRecording(); } else { startRecording(); } }; const colors = theme === "dark" ? { bg: "#1a1625", text: "#fff", accent: "#ff69b4" } : { bg: "#fff", text: "#000", accent: "#0066ff" }; return (
{/* Status indicator */}
{status === "idle" && "Click to start"} {status === "connecting" && "Connecting..."} {status === "ready" && "Ready"} {status === "talking" && "Listening..."} {status === "error" && error || "Error"}
{/* Call button */} {/* Transcript */} {transcript && (
{transcript}
)}
); }