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 (
+
+ );
+ }
+
+ 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 }) => (
+
+);