import { type CSSProperties, useEffect, useRef, useState } from 'react'; import { Room as LiveKitRoom, RoomEvent, Track } from 'livekit-client'; import { ArrowLeftIcon, BarChart3Icon, BrainIcon, CheckIcon, CircleDotIcon, EyeIcon, GitBranchIcon, ExternalLinkIcon, MessageSquareIcon, MicIcon, MicOffIcon, MonitorUpIcon, PhoneCallIcon, PhoneOffIcon, PanelLeftIcon, PanelRightIcon, RadioTowerIcon, ShieldIcon, SparklesIcon, TriangleAlertIcon, Volume2Icon, VolumeXIcon, WorkflowIcon, XIcon, } from 'lucide-react'; import type { Room, RemoteTrack, RemoteTrackPublication } from 'livekit-client'; import type { HermesJob, HermesJobEvent, MemberWorkHistory, MemberWorkHistoryEvent, MemberWorkHistoryFile, Pod, PodActivityEvent, PodActivityKind, PodActivitySource, } from '@podman/shared'; import { useBeat } from '../livekit/useBeat.js'; import { abortLiveConversationHermesJob, getLiveConversationHermesJob, getMemberWorkHistory, startLiveConversation, stopLiveConversation, testPodVoice, type LiveConversationSession, } from '../lib/api.js'; import { useInterventions, primeSpeech } from '../livekit/useInterventions.js'; import { usePodActivity } from '../hooks/use-pod-activity.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 { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Separator } from '@/components/ui/separator'; import { Sidebar, SidebarContent, SidebarHeader, SidebarInset, SidebarProvider, SidebarRail, } from '@/components/ui/sidebar'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { cn } from '@/lib/utils'; const STREAM_SIDEBAR_WIDTH = 'clamp(20rem, 22vw, 23rem)'; const STREAM_RAIL_WIDTH = '4rem'; 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]; } function readStoredBool(key: string, fallback: boolean): boolean { try { const value = localStorage.getItem(key); return value === null ? fallback : value === 'true'; } catch { return fallback; } } 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 [testingVoice, setTestingVoice] = useState(false); const [note, setNote] = useState(null); const [audioBlocked, setAudioBlocked] = useState(false); const [remoteAudioTracks, setRemoteAudioTracks] = useState(0); const [micOn, setMicOn] = useState(false); const [historyMember, setHistoryMember] = useState(null); const [history, setHistory] = useState(null); const [historyLoading, setHistoryLoading] = useState(false); const [historyError, setHistoryError] = useState(null); const [conversationRoom, setConversationRoom] = useState(null); const [conversationSession, setConversationSession] = useState( null, ); const [conversationState, setConversationState] = useState< 'idle' | 'connecting' | 'listening' | 'speaking' | 'interrupted' | 'error' >('idle'); const [conversationNote, setConversationNote] = useState(null); const [hermesJob, setHermesJob] = useState(null); const [hermesJobEvents, setHermesJobEvents] = useState([]); const [leftStreamOpen, setLeftStreamOpen] = useState(() => readStoredBool('podman.myStreamOpen', true), ); const [rightStreamOpen, setRightStreamOpen] = useState(() => readStoredBool('podman.teamStreamOpen', true), ); const { active, hermes, voiceCue, actionUrl, respond } = useInterventions(room); const { beat, toggleBeat: runBeat } = useBeat(room); const activity = usePodActivity(team.id, me); const audioRef = useRef(null); const audioElementsRef = useRef(new Map()); const conversationAudioElementsRef = useRef(new Map()); const screenTrackRef = useRef(null); const onLeaveRef = useRef(onLeave); onLeaveRef.current = onLeave; useEffect(() => { if (!room) return; const refresh = () => setParticipants(snapshot(room, me)); refresh(); const attachAudio = (track: RemoteTrack, pub: RemoteTrackPublication) => { if (track.kind !== Track.Kind.Audio || !audioRef.current) return; const key = pub.trackSid || track.sid || track.mediaStreamTrack.id; if (audioElementsRef.current.has(key)) return; const element = track.attach(); element.autoplay = true; audioElementsRef.current.set(key, element); audioRef.current.appendChild(element); setRemoteAudioTracks(audioElementsRef.current.size); }; const removeAudio = (track: RemoteTrack, pub?: RemoteTrackPublication) => { const key = pub?.trackSid || track.sid || track.mediaStreamTrack.id; const attached = audioElementsRef.current.get(key); if (attached) { attached.remove(); audioElementsRef.current.delete(key); setRemoteAudioTracks(audioElementsRef.current.size); } track.detach().forEach((el) => el.remove()); }; const attachExistingAudio = () => { room.remoteParticipants.forEach((participant) => { participant.audioTrackPublications.forEach((publication) => { const track = publication.track; if (track) attachAudio(track, publication); }); }); }; const onAudio = (track: RemoteTrack, pub: RemoteTrackPublication) => attachAudio(track, pub); const onAudioGone = (track: RemoteTrack, pub: RemoteTrackPublication) => removeAudio(track, pub); const onDisconnected = () => onLeaveRef.current(); // Browsers block autoplay of incoming audio until a user gesture unlocks it. // Surface a button whenever the room can't play sound so PodMan's voice cues // are actually heard. const onPlaybackChanged = () => setAudioBlocked(!room.canPlaybackAudio); const unlockAudioFromGesture = () => { void room.startAudio().finally(() => setAudioBlocked(!room.canPlaybackAudio)); }; onPlaybackChanged(); setMicOn(room.localParticipant.isMicrophoneEnabled); window.addEventListener('pointerdown', unlockAudioFromGesture, { once: true, capture: true, }); window.addEventListener('keydown', unlockAudioFromGesture, { once: true, capture: true }); room .on(RoomEvent.ParticipantConnected, refresh) .on(RoomEvent.ParticipantDisconnected, refresh) .on(RoomEvent.ActiveSpeakersChanged, refresh) .on(RoomEvent.TrackSubscribed, onAudio) .on(RoomEvent.TrackUnsubscribed, onAudioGone) .on(RoomEvent.AudioPlaybackStatusChanged, onPlaybackChanged) .on(RoomEvent.Disconnected, onDisconnected); attachExistingAudio(); return () => { room .off(RoomEvent.ParticipantConnected, refresh) .off(RoomEvent.ParticipantDisconnected, refresh) .off(RoomEvent.ActiveSpeakersChanged, refresh) .off(RoomEvent.TrackSubscribed, onAudio) .off(RoomEvent.TrackUnsubscribed, onAudioGone) .off(RoomEvent.AudioPlaybackStatusChanged, onPlaybackChanged) .off(RoomEvent.Disconnected, onDisconnected); window.removeEventListener('pointerdown', unlockAudioFromGesture, { capture: true }); window.removeEventListener('keydown', unlockAudioFromGesture, { capture: true }); audioElementsRef.current.forEach((el) => el.remove()); audioElementsRef.current.clear(); setRemoteAudioTracks(0); }; }, [room, me]); useEffect(() => { if (!conversationRoom) return; const attachAudio = (track: RemoteTrack, pub: RemoteTrackPublication) => { if (track.kind !== Track.Kind.Audio || !audioRef.current) return; const key = `conversation:${pub.trackSid || track.sid || track.mediaStreamTrack.id}`; if (conversationAudioElementsRef.current.has(key)) return; const element = track.attach(); element.autoplay = true; conversationAudioElementsRef.current.set(key, element); audioRef.current.appendChild(element); setRemoteAudioTracks( audioElementsRef.current.size + conversationAudioElementsRef.current.size, ); }; const removeAudio = (track: RemoteTrack, pub?: RemoteTrackPublication) => { const key = `conversation:${pub?.trackSid || track.sid || track.mediaStreamTrack.id}`; const attached = conversationAudioElementsRef.current.get(key); if (attached) { attached.remove(); conversationAudioElementsRef.current.delete(key); setRemoteAudioTracks( audioElementsRef.current.size + conversationAudioElementsRef.current.size, ); } track.detach().forEach((el) => el.remove()); }; const refreshState = () => { const agentSpeaking = Array.from(conversationRoom.remoteParticipants.values()).some( (participant) => participant.isSpeaking, ); setConversationState((current) => current === 'connecting' || current === 'error' ? current : agentSpeaking ? 'speaking' : 'listening', ); }; const onData = (payload: Uint8Array) => { try { const msg = JSON.parse(new TextDecoder().decode(payload)) as { type?: string; event?: { interrupt?: boolean; summary?: string }; }; if (msg.type === 'LIVE_CONVERSATION_EVENT') { setConversationState(msg.event?.interrupt ? 'interrupted' : 'listening'); if (msg.event?.summary) setConversationNote(msg.event.summary); } if (msg.type === 'HERMES_JOB_EVENT') { const event = msg.event as HermesJobEvent; setHermesJobEvents((events) => [...events.filter((item) => item.id !== event.id), event].slice(-12), ); setConversationNote(event.message); } } catch { // Ignore non-PodMan private-room data. } }; conversationRoom .on(RoomEvent.TrackSubscribed, attachAudio) .on(RoomEvent.TrackUnsubscribed, removeAudio) .on(RoomEvent.ActiveSpeakersChanged, refreshState) .on(RoomEvent.DataReceived, onData) .on(RoomEvent.Disconnected, () => setConversationState('idle')); refreshState(); return () => { conversationRoom .off(RoomEvent.TrackSubscribed, attachAudio) .off(RoomEvent.TrackUnsubscribed, removeAudio) .off(RoomEvent.ActiveSpeakersChanged, refreshState) .off(RoomEvent.DataReceived, onData); conversationAudioElementsRef.current.forEach((el) => el.remove()); conversationAudioElementsRef.current.clear(); setRemoteAudioTracks(audioElementsRef.current.size); }; }, [conversationRoom]); useEffect(() => { if (!conversationSession) { setHermesJob(null); setHermesJobEvents([]); return; } let alive = true; const refresh = async () => { try { const next = await getLiveConversationHermesJob(team.id, conversationSession.sessionId); if (!alive) return; setHermesJob(next.job); setHermesJobEvents(next.events); } catch { // Keep voice conversation usable even if the status panel cannot refresh. } }; void refresh(); const interval = setInterval(() => void refresh(), 2500); return () => { alive = false; clearInterval(interval); }; }, [team.id, conversationSession]); useEffect(() => { if (!historyMember) { setHistory(null); setHistoryError(null); setHistoryLoading(false); return; } let alive = true; setHistory(null); setHistoryError(null); setHistoryLoading(true); getMemberWorkHistory(team.id, historyMember) .then((next) => { if (alive) setHistory(next); }) .catch((e) => { if (alive) setHistoryError((e as Error).message); }) .finally(() => { if (alive) setHistoryLoading(false); }); return () => { alive = false; }; }, [team.id, historyMember]); async function enableSound() { primeSpeech(); // unlock browser voice from this gesture if (!room) return; try { await room.startAudio(); setAudioBlocked(!room.canPlaybackAudio); } catch (e) { setNote(`Could not enable sound: ${(e as Error).message}`); } } // Publish/unpublish the local mic. Enabling triggers the browser permission // prompt, so the button doubles as a "is my mic set up right?" check. async function toggleMic() { if (!room) return; setNote(null); try { const next = !micOn; await room.localParticipant.setMicrophoneEnabled(next); setMicOn(next); } catch (e) { setNote(`Could not toggle mic: ${(e as Error).message}`); } } useEffect(() => { return () => { screenTrackRef.current?.stop(); screenTrackRef.current = null; }; }, []); useEffect(() => { return () => { void conversationRoom?.disconnect(); }; }, [conversationRoom]); useEffect(() => { localStorage.setItem('podman.myStreamOpen', String(leftStreamOpen)); }, [leftStreamOpen]); useEffect(() => { localStorage.setItem('podman.teamStreamOpen', String(rightStreamOpen)); }, [rightStreamOpen]); async function onToggleBeat() { setNote(null); try { await runBeat(); } catch (e) { setNote(`Audio test failed: ${(e as Error).message}`); } } async function playPodManVoiceTest() { if (!room) return; setNote(null); setTestingVoice(true); try { await room.startAudio().catch(() => {}); setAudioBlocked(!room.canPlaybackAudio); await testPodVoice(team.id); setNote('PodMan voice test sent.'); } catch (e) { setNote(`PodMan voice test failed: ${(e as Error).message}`); } finally { setTestingVoice(false); } } async function toggleLiveConversation() { setNote(null); setConversationNote(null); if (conversationRoom && conversationSession) { const endingRoom = conversationRoom; const endingSession = conversationSession; setConversationRoom(null); setConversationSession(null); setConversationState('idle'); setHermesJob(null); setHermesJobEvents([]); try { await endingRoom.localParticipant.setMicrophoneEnabled(false).catch(() => {}); await endingRoom.disconnect(); await stopLiveConversation(team.id, endingSession.sessionId).catch(() => {}); } catch (e) { setNote(`Could not stop live conversation: ${(e as Error).message}`); } return; } setConversationState('connecting'); try { primeSpeech(); await room?.startAudio().catch(() => {}); const session = await startLiveConversation(team.id, { identity: me, displayName: me }); const privateRoom = new LiveKitRoom({ adaptiveStream: true, dynacast: true }); privateRoom.on(RoomEvent.Disconnected, () => { setConversationRoom(null); setConversationSession(null); setConversationState('idle'); }); await privateRoom.connect(session.url, session.token); await privateRoom.startAudio().catch(() => {}); await privateRoom.localParticipant.setMicrophoneEnabled(true); setConversationSession(session); setConversationRoom(privateRoom); setConversationState('listening'); } catch (e) { setConversationState('error'); setConversationRoom(null); setConversationSession(null); setHermesJob(null); setHermesJobEvents([]); setNote(`Live conversation failed: ${(e as Error).message}`); } } async function stopHermesJob() { if (!conversationSession || !hermesJob) return; setNote(null); try { const next = await abortLiveConversationHermesJob(team.id, conversationSession.sessionId); setHermesJob(next.job); setConversationNote('Hermes is aborting the current job.'); } catch (e) { setNote(`Could not stop Hermes job: ${(e as Error).message}`); } } async function toggleScreen() { primeSpeech(); // unlock browser voice from this gesture too if (!room) return; setNote(null); try { await room.startAudio().catch(() => {}); setAudioBlocked(!room.canPlaybackAudio); 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}`); } } async function answerIntervention(status: 'accepted' | 'dismissed', accepted: boolean) { setNote(null); try { await respond(status, accepted); } catch (e) { setNote(`Action failed: ${(e as Error).message}`); } } const podmanPresent = participants.some((p) => p.name.toLowerCase() === 'podman'); return ( setLeftStreamOpen((open) => !open)} emptyTitle="No personal signal yet" emptyDescription="Start the local git watcher or share your IDE screen to populate this lane." />
Leave pod
{initials(team.name) || 'PM'}

{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'}

{activity.error && (

{activity.error}

)}
{ if (!open) setHistoryMember(null); }} />
setRightStreamOpen((open) => !open)} emptyTitle="No teammate signal yet" emptyDescription="Waiting for other members' screen, git, or collision events." /> ); } function Metric({ label, value }: { label: string; value: number | string }) { return (

{label}

{value}

); } function Participant({ participant, onOpenHistory, }: { participant: PInfo; onOpenHistory: (member: string) => void; }) { return (
{initials(participant.name)} {participant.speaking && }

{participant.name}

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

Work history {participant.speaking ? 'speaking' : 'connected'}
); } function WorkHistoryDialog({ member, history, loading, error, onOpenChange, }: { member: string | null; history: MemberWorkHistory | null; loading: boolean; error: string | null; onOpenChange: (open: boolean) => void; }) { return ( {member ? `${member}'s recent work` : 'Recent work'} Last {history?.windowHours ?? 24} hours from MongoDB observations and git state. {loading && (
Loading history
)} {error && !loading && ( History unavailable {error} )} {history && !loading && !error && (
{history.files.length ? ( <>

Recent files

{history.files.length}
{history.files.map((file) => ( ))}

Timeline

{history.timeline.length}
) : ( No recent work history MongoDB has no recent screen observations or git changes for this member. )}
)}
); } function HistoryStat({ label, value }: { label: string; value: number }) { return (

{label}

{value}

); } function maxFileScore(files: MemberWorkHistoryFile[]): number { return Math.max(1, ...files.map((file) => file.observations + file.gitChanges)); } function FileHistoryRow({ file, max }: { file: MemberWorkHistoryFile; max: number }) { const score = file.observations + file.gitChanges; const width = `${Math.max(8, Math.round((score / max) * 100))}%`; return (

{file.file}

{file.activities[0] ?? `${timeLabel(file.lastSeenAt)} ago`}

{file.current && ( current )} {score}
{file.observations} screen {file.gitChanges} git {file.confidenceAvg !== null && {Math.round(file.confidenceAvg * 100)}% conf}
); } function HistoryTimeline({ events }: { events: MemberWorkHistoryEvent[] }) { if (!events.length) { return

No timeline entries.

; } return (
{events.slice(0, 18).map((event) => { const Icon = event.source === 'git' ? GitBranchIcon : MonitorUpIcon; return (

{event.title}

{event.file}

{event.detail && (

{event.detail}

)}
); })}
); } function StatusLine({ label, value }: { label: string; value: string }) { return ( <>
{label} {value}
); } function ActivitySidebar({ side, title, collapsedLabel, testId, description, events, connected, open, onToggle, emptyTitle, emptyDescription, }: { side: 'left' | 'right'; title: string; collapsedLabel: string; testId: string; description: string; events: PodActivityEvent[]; connected: boolean; open: boolean; onToggle: () => void; emptyTitle: string; emptyDescription: string; }) { const ToggleIcon = side === 'left' ? PanelLeftIcon : PanelRightIcon; const critical = events.filter((event) => event.severity === 'critical').length; const toggleTestId = testId.replace('-sidebar', '-toggle'); return (

{title}

{connected ? 'streaming' : 'syncing'}

{description}

{open ? `Collapse ${title}` : `Expand ${title}`}
{events.length ? (
{ACTIVITY_CATEGORIES.map((category) => { const items = events.filter((event) => CATEGORY_OF[event.kind] === category.id); if (!items.length) return null; const CategoryIcon = category.icon; return (

{category.label}

{items.length}

{category.hint}

{items.map((event) => ( ))}
); })}
) : ( {emptyTitle} {emptyDescription} )}
{events.slice(0, 8).map((event) => { const Icon = activityIcon(event.kind); return ( {event.title} ); })}
); } function StreamStat({ label, value }: { label: string; value: number }) { return (

{label}

{value}

); } function ActivityItem({ event }: { event: PodActivityEvent }) { const Icon = activityIcon(event.kind); const metadata = activityMetadata(event); const title = activityTitle(event); return (

{title}

{event.detail && (

{event.detail}

)} {event.imageUrl && (
{activityImageAlt(event)}
)}
{KIND_LABEL[event.kind]} {metadata.map((item, index) => ( {item.label} ))}
); } function activityMetadata(event: PodActivityEvent): { label: string; title?: string; variant: 'secondary' | 'outline'; }[] { const actors = event.actors?.length ? event.actors : event.actor ? [event.actor] : []; const visibleActors = actors.slice(0, 2).map((actor) => ({ label: actor, variant: 'secondary' as const, })); const hiddenActors = actors.length - visibleActors.length; return [ ...visibleActors, ...(hiddenActors > 0 ? [ { label: `+${hiddenActors}`, title: actors.slice(2).join(', '), variant: 'secondary' as const, }, ] : []), ...(event.file ? [{ label: event.file, variant: 'outline' as const }] : []), ]; } function ActivityBadge({ variant, children, title, }: { variant: 'secondary' | 'outline'; children: string; title?: string; }) { return ( {children} ); } type ActivityCategoryId = 'signal' | 'decision'; const CATEGORY_OF: Record = { observation: 'signal', git: 'signal', collision: 'decision', intervention: 'decision', outcome: 'decision', }; const ACTIVITY_CATEGORIES: { id: ActivityCategoryId; label: string; hint: string; icon: typeof RadioTowerIcon; }[] = [ { id: 'signal', label: 'Signals', hint: 'Screen logs and local git activity routed to this stream.', icon: RadioTowerIcon, }, { id: 'decision', label: 'Reasoning & decisions', hint: 'What Hermes concluded and acted on — conflicts, interventions, outcomes.', icon: WorkflowIcon, }, ]; const KIND_LABEL: Record = { observation: 'Screen log', git: 'Git', collision: 'Conflict', intervention: 'Intervention', outcome: 'Outcome', }; const SOURCE_META: Record< PodActivitySource, { label: string; icon: typeof RadioTowerIcon; className: string } > = { vision: { label: 'Screen', icon: EyeIcon, className: 'border-chart-1/40 bg-chart-1/10 text-chart-1', }, git: { label: 'Git', icon: GitBranchIcon, className: 'border-chart-2/40 bg-chart-2/10 text-chart-2', }, memory: { label: 'Memory', icon: BrainIcon, className: 'border-chart-4/40 bg-chart-4/10 text-chart-4', }, hermes: { label: 'Hermes', icon: SparklesIcon, className: 'border-primary/40 bg-primary/10 text-primary', }, policy: { label: 'Policy', icon: ShieldIcon, className: 'border-chart-3/40 bg-chart-3/10 text-chart-3', }, }; function SourceChip({ source }: { source: PodActivitySource }) { const meta = SOURCE_META[source]; const Icon = meta.icon; return ( {meta.label} ); } function activityTitle(event: PodActivityEvent): string { if (event.kind !== 'observation') return event.title; return event.title.startsWith('Screen') ? event.title : `Screen log: ${event.title}`; } function activityImageAlt(event: PodActivityEvent): string { const actor = event.actor ?? event.actors?.[0] ?? 'teammate'; return `${actor} screen thumbnail`; } function activityIcon(kind: PodActivityKind) { switch (kind) { case 'git': return GitBranchIcon; case 'collision': return TriangleAlertIcon; case 'intervention': return MessageSquareIcon; case 'outcome': return CheckIcon; case 'observation': default: return MonitorUpIcon; } } function timeLabel(value: string): string { const then = Date.parse(value); if (!Number.isFinite(then)) return '--'; const seconds = Math.max(0, Math.floor((Date.now() - then) / 1000)); if (seconds < 10) return 'now'; if (seconds < 60) return `${seconds}s`; const minutes = Math.floor(seconds / 60); if (minutes < 60) return `${minutes}m`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours}h`; return `${Math.floor(hours / 24)}d`; } function initials(name: string): string { return name .split(/\s+/) .map((w) => w[0] ?? '') .join('') .slice(0, 2) .toUpperCase(); }