import { useEffect, useRef, useState } from 'react'; import { RoomEvent, Track } from 'livekit-client'; import { ArrowLeftIcon, CheckIcon, CircleDotIcon, MonitorUpIcon, RadioTowerIcon, SparklesIcon, Volume2Icon, XIcon, } from 'lucide-react'; import type { Room, RemoteTrack, RemoteTrackPublication, RemoteParticipant } from 'livekit-client'; import type { Pod } from '@podman/shared'; import { startBeat, type BeatHandle } from '../lib/beat.js'; import { useInterventions } from '../livekit/useInterventions.js'; import LiveWaveform from '@/components/ruixen/live-waveform'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Avatar, AvatarBadge, AvatarFallback } from '@/components/ui/avatar'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from '@/components/ui/card'; import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle, } from '@/components/ui/empty'; import { Separator } from '@/components/ui/separator'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { cn } from '@/lib/utils'; interface PInfo { id: string; name: string; isLocal: boolean; speaking: boolean; } function snapshot(room: Room, fallbackName: string): PInfo[] { const lp = room.localParticipant; const local: PInfo = { id: lp.identity, name: lp.name || fallbackName, isLocal: true, speaking: lp.isSpeaking, }; const remotes = Array.from(room.remoteParticipants.values()).map((p) => ({ id: p.identity, name: p.name || p.identity, isLocal: false, speaking: p.isSpeaking, })); return [local, ...remotes]; } export function PodView({ team, me, room, devMode, onLeave, }: { team: Pod; me: string; room: Room | null; devMode: boolean; onLeave: () => void; }) { const [participants, setParticipants] = useState([]); const [sharing, setSharing] = useState(false); const [playingBeat, setPlayingBeat] = useState(false); const [note, setNote] = useState(null); const { active, respond } = useInterventions(room); const audioRef = useRef(null); const beatRef = useRef(null); const screenTrackRef = useRef(null); const onLeaveRef = useRef(onLeave); onLeaveRef.current = onLeave; useEffect(() => { if (!room) return; const refresh = () => setParticipants(snapshot(room, me)); refresh(); const onAudio = (track: RemoteTrack, _pub: RemoteTrackPublication, _p: RemoteParticipant) => { if (track.kind === Track.Kind.Audio && audioRef.current) { audioRef.current.appendChild(track.attach()); } }; const onAudioGone = (track: RemoteTrack) => track.detach().forEach((el) => el.remove()); const onDisconnected = () => onLeaveRef.current(); room .on(RoomEvent.ParticipantConnected, refresh) .on(RoomEvent.ParticipantDisconnected, refresh) .on(RoomEvent.ActiveSpeakersChanged, refresh) .on(RoomEvent.TrackSubscribed, onAudio) .on(RoomEvent.TrackUnsubscribed, onAudioGone) .on(RoomEvent.Disconnected, onDisconnected); return () => { room .off(RoomEvent.ParticipantConnected, refresh) .off(RoomEvent.ParticipantDisconnected, refresh) .off(RoomEvent.ActiveSpeakersChanged, refresh) .off(RoomEvent.TrackSubscribed, onAudio) .off(RoomEvent.TrackUnsubscribed, onAudioGone) .off(RoomEvent.Disconnected, onDisconnected); }; }, [room, me]); useEffect(() => { return () => { beatRef.current?.stop(); beatRef.current = null; screenTrackRef.current?.stop(); screenTrackRef.current = null; }; }, []); async function toggleBeat() { if (!room) return; setNote(null); try { if (playingBeat) { if (beatRef.current) await room.localParticipant.unpublishTrack(beatRef.current.track); beatRef.current?.stop(); beatRef.current = null; setPlayingBeat(false); } else { await room.startAudio().catch(() => {}); const handle = startBeat(); beatRef.current = handle; await room.localParticipant.publishTrack(handle.track, { name: 'podman-beat' }); setPlayingBeat(true); } } catch (e) { setNote(`Audio test failed: ${(e as Error).message}`); } } async function toggleScreen() { if (!room) return; setNote(null); try { if (sharing) { if (screenTrackRef.current) await room.localParticipant.unpublishTrack(screenTrackRef.current); screenTrackRef.current?.stop(); screenTrackRef.current = null; setSharing(false); return; } if (!window.isSecureContext || !navigator.mediaDevices?.getDisplayMedia) { setNote('Screen capture needs HTTPS.'); return; } const stream = await navigator.mediaDevices.getDisplayMedia({ video: true }); const track = stream.getVideoTracks()[0]; if (!track) return; track.onended = () => { screenTrackRef.current = null; setSharing(false); }; await room.localParticipant.publishTrack(track, { source: Track.Source.ScreenShare }); screenTrackRef.current = track; setSharing(true); } catch (e) { setNote(`Screen share stopped: ${(e as Error).message}`); } } const podmanPresent = participants.some((p) => p.name.toLowerCase() === 'podman'); return (
Leave pod

{team.name}

{room ? 'live' : 'local'}

{team.repo}

{devMode && ( Local mode LiveKit is not configured for this session. )} {note && ( Room notice {note} )}
Room state People and media currently visible to PodMan. {participants.length === 0 ? ( Connecting Waiting for LiveKit room state. ) : (
{participants.map((p) => ( ))}
)}

Roster: {team.members.join(', ') || 'No saved members'}

); } function Metric({ label, value }: { label: string; value: number | string }) { return (

{label}

{value}

); } function Participant({ participant }: { participant: PInfo }) { return (
{initials(participant.name)} {participant.speaking && }

{participant.name}

{participant.isLocal ? 'you' : 'remote'}

{participant.speaking ? 'speaking' : 'connected'}
); } function StatusLine({ label, value }: { label: string; value: string }) { return ( <>
{label} {value}
); } function initials(name: string): string { return name .split(/\s+/) .map((w) => w[0] ?? '') .join('') .slice(0, 2) .toUpperCase(); }