configurable room name and perform RPC calls (#127)

This commit is contained in:
Long Chen 2025-03-12 10:27:51 +08:00 committed by GitHub
parent 2082deb0e3
commit 201a13cc78
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 146 additions and 16 deletions

View File

@ -20,3 +20,43 @@ export const NameValueRow: React.FC<NameValueRowProps> = ({
</div> </div>
); );
}; };
type EditableNameValueRowProps = {
name: string;
value: string;
valueColor?: string;
onValueChange?: (value: string) => void;
placeholder?: string;
editable: boolean;
};
export const EditableNameValueRow: React.FC<EditableNameValueRowProps> = ({
name,
value,
valueColor = "gray-300",
onValueChange,
placeholder,
editable,
}) => {
if (editable && onValueChange) {
return (
<div className="flex flex-row w-full items-baseline text-sm">
<div className="grow shrink-0 text-gray-500">{name}</div>
<input
type="text"
value={value}
onChange={(e) => onValueChange(e.target.value)}
className={`text-xs shrink text-${valueColor} text-right bg-transparent border-b border-gray-800 focus:outline-none focus:border-gray-600 px-2 py-0`}
placeholder={placeholder}
/>
</div>
);
}
return (
<NameValueRow
name={name}
value={value}
valueColor={valueColor}
/>
);
};

View File

@ -23,11 +23,13 @@ import {
useRoomInfo, useRoomInfo,
useTracks, useTracks,
useVoiceAssistant, useVoiceAssistant,
useRoomContext,
} from "@livekit/components-react"; } from "@livekit/components-react";
import { ConnectionState, LocalParticipant, Track } from "livekit-client"; import { ConnectionState, LocalParticipant, Track } from "livekit-client";
import { QRCodeSVG } from "qrcode.react"; import { QRCodeSVG } from "qrcode.react";
import { ReactNode, useCallback, useEffect, useMemo, useState } from "react"; import { ReactNode, useCallback, useEffect, useMemo, useState } from "react";
import tailwindTheme from "../../lib/tailwindTheme.preval"; import tailwindTheme from "../../lib/tailwindTheme.preval";
import { EditableNameValueRow } from "@/components/config/NameValueRow";
export interface PlaygroundMeta { export interface PlaygroundMeta {
name: string; name: string;
@ -56,6 +58,10 @@ export default function Playground({
const roomState = useConnectionState(); const roomState = useConnectionState();
const tracks = useTracks(); const tracks = useTracks();
const room = useRoomContext();
const [rpcMethod, setRpcMethod] = useState("");
const [rpcPayload, setRpcPayload] = useState("");
useEffect(() => { useEffect(() => {
if (roomState === ConnectionState.Connected) { if (roomState === ConnectionState.Connected) {
@ -212,6 +218,21 @@ export default function Playground({
return <></>; return <></>;
}, [config.settings.theme_color, voiceAssistant.audioTrack]); }, [config.settings.theme_color, voiceAssistant.audioTrack]);
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(() => { const settingsTileContent = useMemo(() => {
return ( return (
<div className="flex flex-col gap-4 h-full w-full items-start overflow-y-auto"> <div className="flex flex-col gap-4 h-full w-full items-start overflow-y-auto">
@ -222,19 +243,65 @@ export default function Playground({
)} )}
<ConfigurationPanelItem title="Settings"> <ConfigurationPanelItem title="Settings">
{localParticipant && ( <div className="flex flex-col gap-4">
<div className="flex flex-col gap-2"> <EditableNameValueRow
<NameValueRow name="Room"
name="Room" value={roomState === ConnectionState.Connected ? name : config.settings.room_name}
value={name} valueColor={`${config.settings.theme_color}-500`}
valueColor={`${config.settings.theme_color}-500`} onValueChange={(value) => {
/> const newSettings = { ...config.settings };
<NameValueRow newSettings.room_name = value;
name="Participant" setUserSettings(newSettings);
value={localParticipant.identity} }}
/> placeholder="Enter room name"
</div> editable={roomState !== ConnectionState.Connected}
)} />
<EditableNameValueRow
name="Participant"
value={roomState === ConnectionState.Connected ?
(localParticipant?.identity || '') :
(config.settings.participant_name || '')}
valueColor={`${config.settings.theme_color}-500`}
onValueChange={(value) => {
const newSettings = { ...config.settings };
newSettings.participant_name = value;
setUserSettings(newSettings);
}}
placeholder="Enter participant id"
editable={roomState !== ConnectionState.Connected}
/>
</div>
<div className="flex flex-col gap-2 mt-4">
<div className="text-xs text-gray-500 mt-2">RPC Method</div>
<input
type="text"
value={rpcMethod}
onChange={(e) => 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"
/>
<div className="text-xs text-gray-500 mt-2">RPC Payload</div>
<textarea
value={rpcPayload}
onChange={(e) => setRpcPayload(e.target.value)}
className="w-full text-white text-sm bg-transparent border border-gray-800 rounded-sm px-3 py-2"
placeholder="RPC payload"
rows={2}
/>
<button
onClick={handleRpcCall}
disabled={!voiceAssistant.agent || !rpcMethod}
className={`mt-2 px-2 py-1 rounded-sm text-xs
${voiceAssistant.agent && rpcMethod
? `bg-${config.settings.theme_color}-500 hover:bg-${config.settings.theme_color}-600`
: 'bg-gray-700 cursor-not-allowed'
} text-white`}
>
Perform RPC Call
</button>
</div>
</ConfigurationPanelItem> </ConfigurationPanelItem>
<ConfigurationPanelItem title="Status"> <ConfigurationPanelItem title="Status">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
@ -327,6 +394,9 @@ export default function Playground({
themeColors, themeColors,
setUserSettings, setUserSettings,
voiceAssistant.agent, voiceAssistant.agent,
rpcMethod,
rpcPayload,
handleRpcCall,
]); ]);
let mobileTabs: PlaygroundTab[] = []; let mobileTabs: PlaygroundTab[] = [];

View File

@ -34,6 +34,8 @@ export type UserSettings = {
}; };
ws_url: string; ws_url: string;
token: string; token: string;
room_name: string;
participant_name: string;
}; };
// Fallback if NEXT_PUBLIC_APP_CONFIG is not set // Fallback if NEXT_PUBLIC_APP_CONFIG is not set
@ -55,6 +57,8 @@ const defaultConfig: AppConfig = {
}, },
ws_url: "", ws_url: "",
token: "", token: "",
room_name: "",
participant_name: "",
}, },
show_qr: false, show_qr: false,
}; };
@ -122,6 +126,8 @@ export const ConfigProvider = ({ children }: { children: React.ReactNode }) => {
}, },
ws_url: "", ws_url: "",
token: "", token: "",
room_name: "",
participant_name: "",
} as UserSettings; } as UserSettings;
}, [appConfig]); }, [appConfig]);

View File

@ -54,7 +54,14 @@ export const ConnectionProvider = ({
throw new Error("NEXT_PUBLIC_LIVEKIT_URL is not set"); throw new Error("NEXT_PUBLIC_LIVEKIT_URL is not set");
} }
url = process.env.NEXT_PUBLIC_LIVEKIT_URL; url = process.env.NEXT_PUBLIC_LIVEKIT_URL;
const { accessToken } = await fetch("/api/token").then((res) => const params = new URLSearchParams();
if (config.settings.room_name) {
params.append('roomName', config.settings.room_name);
}
if (config.settings.participant_name) {
params.append('participantName', config.settings.participant_name);
}
const { accessToken } = await fetch(`/api/token?${params}`).then((res) =>
res.json() res.json()
); );
token = accessToken; token = accessToken;
@ -68,6 +75,8 @@ export const ConnectionProvider = ({
cloudWSUrl, cloudWSUrl,
config.settings.token, config.settings.token,
config.settings.ws_url, config.settings.ws_url,
config.settings.room_name,
config.settings.participant_name,
generateToken, generateToken,
setToastMessage, setToastMessage,
] ]

View File

@ -25,8 +25,13 @@ export default async function handleToken(
return; return;
} }
const roomName = `room-${generateRandomAlphanumeric(4)}-${generateRandomAlphanumeric(4)}`; // Get room name from query params or generate random one
const identity = `identity-${generateRandomAlphanumeric(4)}` const roomName = req.query.roomName as string ||
`room-${generateRandomAlphanumeric(4)}-${generateRandomAlphanumeric(4)}`;
// Get participant name from query params or generate random one
const identity = req.query.participantName as string ||
`identity-${generateRandomAlphanumeric(4)}`;
const grant: VideoGrant = { const grant: VideoGrant = {
room: roomName, room: roomName,