diff --git a/agents/my_basic_agent_1_2_9.py b/agents/my_basic_agent_1_2_9.py index f81ea89..0c6097a 100644 --- a/agents/my_basic_agent_1_2_9.py +++ b/agents/my_basic_agent_1_2_9.py @@ -1016,6 +1016,9 @@ async def entrypoint(ctx: JobContext, avatar_dispatcher_url: str = None, vision_ # disable input audio at the start session.input.set_audio_enabled(False) + + # Track current audio state for mode switching + _audio_enabled_state = False @ctx.room.local_participant.register_rpc_method("start_turn") async def start_turn(data: rtc.RpcInvocationData): @@ -1053,6 +1056,16 @@ async def entrypoint(ctx: JobContext, avatar_dispatcher_url: str = None, vision_ session.clear_user_turn() logger.info("cancel turn") + @ctx.room.local_participant.register_rpc_method("switch_ptt_and_rt") + async def switch_ptt_and_rt(data: rtc.RpcInvocationData): + nonlocal _audio_enabled_state + # Toggle audio input state + _audio_enabled_state = not _audio_enabled_state + session.input.set_audio_enabled(_audio_enabled_state) + mode = "push-to-talk" if not _audio_enabled_state else "realtime" + logger.info(f"Switched to {mode} mode (audio enabled: {_audio_enabled_state})") + return json.dumps({"success": True, "mode": mode, "audio_enabled": _audio_enabled_state}) + if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--avatar-url", type=str, default=None, help="Avatar dispatcher URL") diff --git a/src/components/playground/PhoneSimulator.tsx b/src/components/playground/PhoneSimulator.tsx index 68e51e8..8eb9f03 100644 --- a/src/components/playground/PhoneSimulator.tsx +++ b/src/components/playground/PhoneSimulator.tsx @@ -65,6 +65,7 @@ export function PhoneSimulator({ const lastPhoneMode = useRef(phoneMode); const [isPushToTalkActive, setIsPushToTalkActive] = useState(false); const [interruptRejected, setInterruptRejected] = useState(false); + const [isPushToTalkMode, setIsPushToTalkMode] = useState(true); // false = realtime mode, true = PTT mode (default) const pushToTalkButtonRef = useRef(null); useEffect(() => { @@ -428,6 +429,23 @@ export function PhoneSimulator({ setShowVoiceMenu(!showVoiceMenu); }; + const handleModeSwitch = async () => { + if (!room || !voiceAssistant.agent) return; + + try { + await room.localParticipant.performRpc({ + destinationIdentity: voiceAssistant.agent.identity, + method: "switch_ptt_and_rt", + payload: "", + }); + // Toggle mode on success + setIsPushToTalkMode(prev => !prev); + } catch (error: any) { + console.error("Failed to switch mode:", error); + // Don't show error toast for mode switch failures, just log + } + }; + // Check if agent supports push-to-talk (optional check, button will show regardless) const supportsPushToTalk = useMemo(() => { if (!voiceAssistant.agent || !agentAttributes.attributes) return false; @@ -513,6 +531,9 @@ export function PhoneSimulator({ }; const handlePushToTalkEnd = useCallback(async () => { + // Always clear interrupt rejection state when button is released + setInterruptRejected(false); + if (!room || !voiceAssistant.agent || !isPushToTalkActive) return; try { @@ -522,16 +543,17 @@ export function PhoneSimulator({ payload: "", }); setIsPushToTalkActive(false); - setInterruptRejected(false); } catch (error: any) { console.error("Failed to end turn:", error); // Don't show error toast on end_turn failure as it might be called during cleanup setIsPushToTalkActive(false); - setInterruptRejected(false); } }, [room, voiceAssistant.agent, isPushToTalkActive]); const handlePushToTalkCancel = useCallback(async () => { + // Always clear interrupt rejection state when button is cancelled + setInterruptRejected(false); + if (!room || !voiceAssistant.agent || !isPushToTalkActive) return; try { @@ -541,11 +563,9 @@ export function PhoneSimulator({ payload: "", }); setIsPushToTalkActive(false); - setInterruptRejected(false); } catch (error) { console.error("Failed to cancel turn:", error); setIsPushToTalkActive(false); - setInterruptRejected(false); } }, [room, voiceAssistant.agent, isPushToTalkActive]); @@ -587,10 +607,14 @@ export function PhoneSimulator({ // Handle global mouseup/touchend to end push-to-talk even if released outside button const handleGlobalMouseUp = () => { + // Clear interrupt rejection state immediately when button is released + setInterruptRejected(false); handlePushToTalkEnd(); }; const handleGlobalTouchEnd = () => { + // Clear interrupt rejection state immediately when button is released + setInterruptRejected(false); handlePushToTalkEnd(); }; @@ -1011,56 +1035,127 @@ export function PhoneSimulator({ ) : (
- {/* Push-to-Talk Button - Centered and Bigger */} + {/* Mode Toggle Switch */} {phoneMode !== "important_message" && phoneMode !== "hand_off" && voiceAssistant.agent && ( - + + + 实时对话模式 + +
)} - {/* Other Controls */} -
- {phoneMode !== "important_message" && phoneMode !== "hand_off" && ( - + {showCameraMenu && ( +
+ {cameras.length === 0 ? ( +
+ No cameras found +
+ ) : ( + cameras.map((device) => ( + + )) + )} +
)} - - )} +
- -
+ {/* Large Push-to-Talk Button - Center */} + + + {/* End Call Button - Right */} + + + )} + + {/* Realtime Mode Layout */} + {!isPushToTalkMode && ( +
+ {phoneMode !== "important_message" && phoneMode !== "hand_off" && ( + + )} + + {/* End Call Button */} + +
+ )} )