"use client"; import { LoadingSVG } from "@/components/button/LoadingSVG"; import { ChatMessageType } from "@/components/chat/ChatTile"; import { ColorPicker } from "@/components/colorPicker/ColorPicker"; import { AudioInputTile } from "@/components/config/AudioInputTile"; import { ConfigurationPanelItem } from "@/components/config/ConfigurationPanelItem"; import { NameValueRow } from "@/components/config/NameValueRow"; import { PlaygroundHeader } from "@/components/playground/PlaygroundHeader"; import { PlaygroundTab, PlaygroundTabbedTile, PlaygroundTile, } from "@/components/playground/PlaygroundTile"; import { useConfig } from "@/hooks/useConfig"; import { TranscriptionTile } from "@/transcriptions/TranscriptionTile"; import { BarVisualizer, VideoTrack, useConnectionState, useDataChannel, useLocalParticipant, useRoomInfo, useTracks, useVoiceAssistant, useRoomContext, } from "@livekit/components-react"; import { ConnectionState, LocalParticipant, Track } from "livekit-client"; import { QRCodeSVG } from "qrcode.react"; import { ReactNode, useCallback, useEffect, useMemo, useState } from "react"; import tailwindTheme from "../../lib/tailwindTheme.preval"; import { EditableNameValueRow } from "@/components/config/NameValueRow"; export interface PlaygroundMeta { name: string; value: string; } export interface PlaygroundProps { logo?: ReactNode; themeColors: string[]; onConnect: (connect: boolean, opts?: { token: string; url: string }) => void; } const headerHeight = 56; export default function Playground({ logo, themeColors, onConnect, }: PlaygroundProps) { const { config, setUserSettings } = useConfig(); const { name } = useRoomInfo(); const [transcripts, setTranscripts] = useState([]); const { localParticipant } = useLocalParticipant(); const voiceAssistant = useVoiceAssistant(); const roomState = useConnectionState(); const tracks = useTracks(); const room = useRoomContext(); const [rpcMethod, setRpcMethod] = useState(""); const [rpcPayload, setRpcPayload] = useState(""); useEffect(() => { if (roomState === ConnectionState.Connected) { localParticipant.setCameraEnabled(config.settings.inputs.camera); localParticipant.setMicrophoneEnabled(config.settings.inputs.mic); } }, [config, localParticipant, roomState]); const agentVideoTrack = tracks.find( (trackRef) => trackRef.publication.kind === Track.Kind.Video && trackRef.participant.isAgent ); const localTracks = tracks.filter( ({ participant }) => participant instanceof LocalParticipant ); const localCameraTrack = localTracks.find( ({ source }) => source === Track.Source.Camera ); const localScreenTrack = localTracks.find( ({ source }) => source === Track.Source.ScreenShare ); const localMicTrack = localTracks.find( ({ source }) => source === Track.Source.Microphone ); const onDataReceived = useCallback( (msg: any) => { if (msg.topic === "transcription") { const decoded = JSON.parse( new TextDecoder("utf-8").decode(msg.payload) ); let timestamp = new Date().getTime(); if ("timestamp" in decoded && decoded.timestamp > 0) { timestamp = decoded.timestamp; } setTranscripts([ ...transcripts, { name: "You", message: decoded.text, timestamp: timestamp, isSelf: true, }, ]); } }, [transcripts] ); useDataChannel(onDataReceived); const videoTileContent = useMemo(() => { const videoFitClassName = `object-${config.video_fit || "cover"}`; const disconnectedContent = (
No video track. Connect to get started.
); const loadingContent = (
Waiting for video track
); const videoContent = ( ); let content = null; if (roomState === ConnectionState.Disconnected) { content = disconnectedContent; } else if (agentVideoTrack) { content = videoContent; } else { content = loadingContent; } return (
{content}
); }, [agentVideoTrack, config, roomState]); useEffect(() => { document.body.style.setProperty( "--lk-theme-color", // @ts-ignore tailwindTheme.colors[config.settings.theme_color]["500"] ); document.body.style.setProperty( "--lk-drop-shadow", `var(--lk-theme-color) 0px 0px 18px` ); }, [config.settings.theme_color]); const audioTileContent = useMemo(() => { const disconnectedContent = (
No audio track. Connect to get started.
); const waitingContent = (
Waiting for audio track
); const visualizerContent = (
); if (roomState === ConnectionState.Disconnected) { return disconnectedContent; } if (!voiceAssistant.audioTrack) { return waitingContent; } return visualizerContent; }, [ voiceAssistant.audioTrack, config.settings.theme_color, roomState, voiceAssistant.state, ]); const chatTileContent = useMemo(() => { if (voiceAssistant.agent) { return ( ); } return <>; }, [config.settings.theme_color, voiceAssistant.audioTrack, voiceAssistant.agent]); const handleRpcCall = useCallback(async () => { if (!voiceAssistant.agent || !room) return; try { const response = await room.localParticipant.performRpc({ destinationIdentity: voiceAssistant.agent.identity, method: rpcMethod, payload: rpcPayload, }); console.log('RPC response:', response); } catch (e) { console.error('RPC call failed:', e); } }, [room, rpcMethod, rpcPayload, voiceAssistant.agent]); const settingsTileContent = useMemo(() => { return (
{config.description && ( {config.description} )}
{ const newSettings = { ...config.settings }; newSettings.room_name = value; setUserSettings(newSettings); }} placeholder="Enter room name" editable={roomState !== ConnectionState.Connected} /> { const newSettings = { ...config.settings }; newSettings.participant_name = value; setUserSettings(newSettings); }} placeholder="Enter participant id" editable={roomState !== ConnectionState.Connected} />
RPC Method
setRpcMethod(e.target.value)} className="w-full text-white text-sm bg-transparent border border-gray-800 rounded-sm px-3 py-2" placeholder="RPC method name" />
RPC Payload