diff --git a/components/SiteMenteVoiceWidget.tsx b/components/SiteMenteVoiceWidget.tsx
index 94ed446..3197529 100644
--- a/components/SiteMenteVoiceWidget.tsx
+++ b/components/SiteMenteVoiceWidget.tsx
@@ -1,6 +1,7 @@
"use client";
import { useState, useRef, useEffect } from "react";
+import SynthflowWidget from "./SynthflowWidget";
interface SiteMenteVoiceWidgetProps {
businessName?: string;
@@ -158,15 +159,13 @@ export default function SiteMenteVoiceWidget({
- {/* Synthflow Widget - Embedded iframe */}
+ {/* Synthflow Widget - Custom WebSocket */}
{mode === "synthflow" && (
-
-
)}
diff --git a/components/SynthflowWidget.tsx b/components/SynthflowWidget.tsx
new file mode 100644
index 0000000..fa110ad
--- /dev/null
+++ b/components/SynthflowWidget.tsx
@@ -0,0 +1,235 @@
+"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}
+
+ )}
+
+
+ );
+}