From 2ca304d64b824e8e102fad8ce3a9571f5bcb0345 Mon Sep 17 00:00:00 2001 From: Xin Wang Date: Thu, 4 Dec 2025 14:45:19 +0800 Subject: [PATCH] add phone simulator --- src/components/playground/PhoneSimulator.tsx | 173 +++++++++++++++++++ src/components/playground/Playground.tsx | 46 ++--- src/components/playground/icons.tsx | 114 ++++++++++++ 3 files changed, 298 insertions(+), 35 deletions(-) create mode 100644 src/components/playground/PhoneSimulator.tsx diff --git a/src/components/playground/PhoneSimulator.tsx b/src/components/playground/PhoneSimulator.tsx new file mode 100644 index 0000000..393d41a --- /dev/null +++ b/src/components/playground/PhoneSimulator.tsx @@ -0,0 +1,173 @@ +"use client"; + +import { useConfig } from "@/hooks/useConfig"; +import { + BarVisualizer, + useConnectionState, + useLocalParticipant, + useRoomContext, + useTracks, + useVoiceAssistant, + VideoTrack, +} from "@livekit/components-react"; +import { ConnectionState, Track, LocalParticipant } from "livekit-client"; +import { useEffect, useMemo, useState } from "react"; +import { BatteryIcon, MicIcon, MicOffIcon, PhoneIcon, PhoneOffIcon, WifiIcon } from "./icons"; + +export interface PhoneSimulatorProps { + onConnect: () => void; +} + +export function PhoneSimulator({ onConnect }: PhoneSimulatorProps) { + const { config } = useConfig(); + const room = useRoomContext(); + const roomState = useConnectionState(); + const { localParticipant } = useLocalParticipant(); + const tracks = useTracks(); + const voiceAssistant = useVoiceAssistant(); + + const [currentTime, setCurrentTime] = useState(""); + + useEffect(() => { + const updateTime = () => { + const now = new Date(); + setCurrentTime( + now.toLocaleTimeString("en-US", { + hour: "numeric", + minute: "2-digit", + hour12: true, + }) + ); + }; + updateTime(); + const interval = setInterval(updateTime, 60000); + return () => clearInterval(interval); + }, []); + + const localTracks = tracks.filter( + ({ participant }) => participant instanceof LocalParticipant + ); + const localCameraTrack = localTracks.find( + ({ source }) => source === Track.Source.Camera + ); + + const isMicEnabled = localParticipant.isMicrophoneEnabled; + + const handleMicToggle = async () => { + if (isMicEnabled) { + await localParticipant.setMicrophoneEnabled(false); + } else { + await localParticipant.setMicrophoneEnabled(true); + } + }; + + const handleDisconnect = () => { + room.disconnect(); + }; + + const videoContent = useMemo(() => { + if (roomState === ConnectionState.Disconnected) { + return ( +
+ +
+ ); + } + + if (!localCameraTrack) { + return ( +
+
+ Camera off +
+
+ ); + } + + return ( + + ); + }, [roomState, localCameraTrack]); + + return ( +
+ + {/* Status Bar */} +
+ {currentTime} +
+ + +
+
+ + {/* Dynamic Island / Notch Placeholder */} +
+ + {/* Main Content */} +
+ {videoContent} + + {/* Agent Audio Visualizer (Top Left) */} + {roomState === ConnectionState.Connected && voiceAssistant.audioTrack && ( +
+
+ +
+
+ )} +
+ + {/* Call Controls Overlay */} + {roomState === ConnectionState.Connected && ( +
+ + + +
+ )} +
+ ); +} + diff --git a/src/components/playground/Playground.tsx b/src/components/playground/Playground.tsx index 683aaba..beefafd 100644 --- a/src/components/playground/Playground.tsx +++ b/src/components/playground/Playground.tsx @@ -6,6 +6,7 @@ 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 { PhoneSimulator } from "@/components/playground/PhoneSimulator"; import { PlaygroundHeader } from "@/components/playground/PlaygroundHeader"; import { PlaygroundTab, @@ -561,29 +562,15 @@ export default function Playground({ ]); let mobileTabs: PlaygroundTab[] = []; - if (config.settings.outputs.video) { + if (config.settings.outputs.video || config.settings.outputs.audio) { mobileTabs.push({ - title: "Video", + title: "Phone", content: ( - {videoTileContent} - - ), - }); - } - - if (config.settings.outputs.audio) { - mobileTabs.push({ - title: "Audio", - content: ( - - {audioTileContent} + onConnect(true)} /> ), }); @@ -641,24 +628,13 @@ export default function Playground({ : "flex" }`} > - {config.settings.outputs.video && ( - - {videoTileContent} - - )} - {config.settings.outputs.audio && ( - - {audioTileContent} - - )} + + onConnect(true)} /> + {config.settings.chat && ( diff --git a/src/components/playground/icons.tsx b/src/components/playground/icons.tsx index 7f39daa..5e9120b 100644 --- a/src/components/playground/icons.tsx +++ b/src/components/playground/icons.tsx @@ -37,3 +37,117 @@ export const ChevronIcon = () => ( /> ); + +export const BatteryIcon = ({ className }: { className?: string }) => ( + + + + +); + +export const WifiIcon = ({ className }: { className?: string }) => ( + + + + + + +); + +export const MicIcon = ({ className }: { className?: string }) => ( + + + + + + +); + +export const MicOffIcon = ({ className }: { className?: string }) => ( + + + + + + + +); + +export const PhoneOffIcon = ({ className }: { className?: string }) => ( + + + + +); + +export const PhoneIcon = ({ className }: { className?: string }) => ( + + + +);