Skip to main content
Glama

Voice Mode

by mbailey
page.tsx•7.42 kB
"use client"; import { CloseIcon } from "@/components/CloseIcon"; import { NoAgentNotification } from "@/components/NoAgentNotification"; import TranscriptionView from "@/components/TranscriptionView"; import { BarVisualizer, DisconnectButton, RoomAudioRenderer, RoomContext, VideoTrack, VoiceAssistantControlBar, useVoiceAssistant, } from "@livekit/components-react"; import { AnimatePresence, motion } from "framer-motion"; import { Room, RoomEvent } from "livekit-client"; import { useCallback, useEffect, useState } from "react"; import type { ConnectionDetails } from "./api/connection-details/route"; export default function Page() { const [room] = useState(new Room()); const [password, setPassword] = useState(""); const [error, setError] = useState(""); const onConnectButtonClicked = useCallback(async () => { // Generate room connection details, including: // - A random Room name // - A random Participant name // - An Access Token to permit the participant to join the room // - The URL of the LiveKit server to connect to // // In real-world application, you would likely allow the user to specify their // own participant name, and possibly to choose from existing rooms to join. setError(""); const url = new URL( process.env.NEXT_PUBLIC_CONN_DETAILS_ENDPOINT ?? "/api/connection-details", window.location.origin ); url.searchParams.set('password', password); const response = await fetch(url.toString()); if (!response.ok) { if (response.status === 401) { setError("Invalid password"); } else { setError("Connection failed"); } return; } const connectionDetailsData: ConnectionDetails = await response.json(); await room.connect(connectionDetailsData.serverUrl, connectionDetailsData.participantToken); await room.localParticipant.setMicrophoneEnabled(true); }, [room, password]); useEffect(() => { room.on(RoomEvent.MediaDevicesError, onDeviceFailure); return () => { room.off(RoomEvent.MediaDevicesError, onDeviceFailure); }; }, [room]); return ( <main data-lk-theme="default" className="h-full grid content-center bg-[var(--lk-bg)]"> <RoomContext.Provider value={room}> <div className="lk-room-container max-w-[1024px] w-[90vw] mx-auto max-h-[90vh]"> <SimpleVoiceAssistant onConnectButtonClicked={onConnectButtonClicked} password={password} setPassword={setPassword} error={error} /> </div> </RoomContext.Provider> </main> ); } function SimpleVoiceAssistant(props: { onConnectButtonClicked: () => void; password: string; setPassword: (password: string) => void; error: string; }) { const { state: agentState } = useVoiceAssistant(); return ( <> <AnimatePresence mode="wait"> {agentState === "disconnected" ? ( <motion.div key="disconnected" initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.95 }} transition={{ duration: 0.3, ease: [0.09, 1.04, 0.245, 1.055] }} className="grid items-center justify-center h-full" > <div className="flex flex-col items-center gap-4"> <input type="password" placeholder="Enter password" value={props.password} onChange={(e) => props.setPassword(e.target.value)} onKeyPress={(e) => { if (e.key === 'Enter') { props.onConnectButtonClicked(); } }} className="px-4 py-2 bg-gray-800 text-white rounded-md focus:outline-none focus:ring-2 focus:ring-white" /> {props.error && ( <p className="text-red-500 text-sm">{props.error}</p> )} <motion.button initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ duration: 0.3, delay: 0.1 }} className="uppercase px-4 py-2 bg-white text-black rounded-md" onClick={() => props.onConnectButtonClicked()} > Start a conversation </motion.button> </div> </motion.div> ) : ( <motion.div key="connected" initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -20 }} transition={{ duration: 0.3, ease: [0.09, 1.04, 0.245, 1.055] }} className="flex flex-col items-center gap-4 h-full" > <AgentVisualizer /> <div className="flex-1 w-full"> <TranscriptionView /> </div> <div className="w-full"> <ControlBar onConnectButtonClicked={props.onConnectButtonClicked} /> </div> <RoomAudioRenderer /> <NoAgentNotification state={agentState} /> </motion.div> )} </AnimatePresence> </> ); } function AgentVisualizer() { const { state: agentState, videoTrack, audioTrack } = useVoiceAssistant(); if (videoTrack) { return ( <div className="h-[512px] w-[512px] rounded-lg overflow-hidden"> <VideoTrack trackRef={videoTrack} /> </div> ); } return ( <div className="h-[300px] w-full"> <BarVisualizer state={agentState} barCount={5} trackRef={audioTrack} className="agent-visualizer" options={{ minHeight: 24 }} /> </div> ); } function ControlBar(props: { onConnectButtonClicked: () => void }) { const { state: agentState } = useVoiceAssistant(); return ( <div className="relative h-[60px]"> <AnimatePresence> {agentState === "disconnected" && ( <motion.button initial={{ opacity: 0, top: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0, top: "-10px" }} transition={{ duration: 1, ease: [0.09, 1.04, 0.245, 1.055] }} className="uppercase absolute left-1/2 -translate-x-1/2 px-4 py-2 bg-white text-black rounded-md" onClick={() => props.onConnectButtonClicked()} > Start a conversation </motion.button> )} </AnimatePresence> <AnimatePresence> {agentState !== "disconnected" && agentState !== "connecting" && ( <motion.div initial={{ opacity: 0, top: "10px" }} animate={{ opacity: 1, top: 0 }} exit={{ opacity: 0, top: "-10px" }} transition={{ duration: 0.4, ease: [0.09, 1.04, 0.245, 1.055] }} className="flex h-8 absolute left-1/2 -translate-x-1/2 justify-center" > <VoiceAssistantControlBar controls={{ leave: false }} /> <DisconnectButton> <CloseIcon /> </DisconnectButton> </motion.div> )} </AnimatePresence> </div> ); } function onDeviceFailure(error: Error) { console.error(error); alert( "Error acquiring camera or microphone permissions. Please make sure you grant the necessary permissions in your browser and reload the tab" ); }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mbailey/voicemode'

If you have feedback or need assistance with the MCP directory API, please join our Discord server