Simplify frontend room surfaces
This commit is contained in:
+5
-101
@@ -2,14 +2,8 @@ import { useEffect, useState } from 'react';
|
|||||||
import type { Room } from 'livekit-client';
|
import type { Room } from 'livekit-client';
|
||||||
import {
|
import {
|
||||||
AlertCircleIcon,
|
AlertCircleIcon,
|
||||||
BrainCircuitIcon,
|
|
||||||
CircleDotIcon,
|
|
||||||
RadioTowerIcon,
|
|
||||||
RefreshCwIcon,
|
RefreshCwIcon,
|
||||||
SparklesIcon,
|
SparklesIcon,
|
||||||
UsersIcon,
|
|
||||||
WifiIcon,
|
|
||||||
ShieldCheckIcon,
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import type { Pod, PodInput } from '@podman/shared';
|
import type { Pod, PodInput } from '@podman/shared';
|
||||||
import { joinPod } from './lib/pod.js';
|
import { joinPod } from './lib/pod.js';
|
||||||
@@ -19,7 +13,6 @@ import { CreatePodForm } from './components/CreatePodForm.js';
|
|||||||
import { PodView } from './components/PodView.js';
|
import { PodView } from './components/PodView.js';
|
||||||
import { GraphView } from './components/GraphView.js';
|
import { GraphView } from './components/GraphView.js';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -40,7 +33,6 @@ import {
|
|||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
|
|
||||||
const SESSION_KEY = 'podman.session';
|
const SESSION_KEY = 'podman.session';
|
||||||
const fmt = new Intl.NumberFormat('en', { notation: 'compact' });
|
|
||||||
|
|
||||||
function pathPodId(): string | null {
|
function pathPodId(): string | null {
|
||||||
const [segment] = window.location.pathname.split('/').filter(Boolean);
|
const [segment] = window.location.pathname.split('/').filter(Boolean);
|
||||||
@@ -68,7 +60,6 @@ export default function App() {
|
|||||||
const [pending, setPending] = useState<Set<string>>(new Set());
|
const [pending, setPending] = useState<Set<string>>(new Set());
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [presence, setPresence] = useState<Record<string, string[]>>({});
|
const [presence, setPresence] = useState<Record<string, string[]>>({});
|
||||||
const [memory, setMemory] = useState<api.MemoryStats | null>(null);
|
|
||||||
const [joinedPodId, setJoinedPodId] = useState<string | null>(null);
|
const [joinedPodId, setJoinedPodId] = useState<string | null>(null);
|
||||||
const [member, setMember] = useState('');
|
const [member, setMember] = useState('');
|
||||||
const [devMode, setDevMode] = useState(false);
|
const [devMode, setDevMode] = useState(false);
|
||||||
@@ -81,14 +72,12 @@ export default function App() {
|
|||||||
async function refresh() {
|
async function refresh() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const [nextPods, nextPresence, nextMemory] = await Promise.all([
|
const [nextPods, nextPresence] = await Promise.all([
|
||||||
api.listPods(),
|
api.listPods(),
|
||||||
api.getPresence().catch(() => presence),
|
api.getPresence().catch(() => presence),
|
||||||
api.getMemoryStats().catch(() => memory),
|
|
||||||
]);
|
]);
|
||||||
setPods(nextPods);
|
setPods(nextPods);
|
||||||
setPresence(nextPresence);
|
setPresence(nextPresence);
|
||||||
setMemory(nextMemory);
|
|
||||||
setError(null);
|
setError(null);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError((e as Error).message);
|
setError((e as Error).message);
|
||||||
@@ -130,13 +119,9 @@ export default function App() {
|
|||||||
let alive = true;
|
let alive = true;
|
||||||
const tick = async () => {
|
const tick = async () => {
|
||||||
try {
|
try {
|
||||||
const [p, m] = await Promise.all([
|
const p = await api.getPresence();
|
||||||
api.getPresence(),
|
|
||||||
api.getMemoryStats().catch(() => memory),
|
|
||||||
]);
|
|
||||||
if (alive) {
|
if (alive) {
|
||||||
setPresence(p);
|
setPresence(p);
|
||||||
setMemory(m);
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
/* presence is best-effort */
|
/* presence is best-effort */
|
||||||
@@ -279,14 +264,6 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const showReconnecting = restoring || (joinedPodId !== null && joinedPod === null);
|
const showReconnecting = restoring || (joinedPodId !== null && joinedPod === null);
|
||||||
const liveNames = Array.from(new Set(Object.values(presence).flat()));
|
|
||||||
const liveTotal = liveNames.length;
|
|
||||||
const totalMembers = pods.reduce((sum, p) => sum + p.members.length, 0);
|
|
||||||
const activeRooms = Object.values(presence).filter((names) => names.length > 0).length;
|
|
||||||
const podManOnline = liveNames.some((name) => name.toLowerCase() === 'podman');
|
|
||||||
const latestActivity = memory
|
|
||||||
? memory.observations + memory.collisions + memory.interventions + memory.outcomes
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
if (joinedPod) {
|
if (joinedPod) {
|
||||||
return (
|
return (
|
||||||
@@ -301,46 +278,22 @@ export default function App() {
|
|||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background text-foreground">
|
<div className="min-h-screen bg-background text-foreground">
|
||||||
<div className="mx-auto flex min-h-screen w-full max-w-[1440px] flex-col gap-6 px-4 py-4 sm:px-6 lg:px-8">
|
<div className="mx-auto flex min-h-screen w-full max-w-[1440px] flex-col gap-6 px-4 py-4 sm:px-6 lg:px-8">
|
||||||
<header className="sticky top-0 z-10 -mx-4 flex flex-col gap-5 border-b bg-background/86 px-4 pb-5 pt-2 backdrop-blur-xl sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8">
|
<header className="sticky top-0 z-10 -mx-4 border-b bg-background/86 px-4 pb-4 pt-2 backdrop-blur-xl sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8">
|
||||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div className="flex min-w-0 items-center gap-3">
|
<div className="flex min-w-0 items-center gap-3">
|
||||||
<div className="grid size-10 place-items-center rounded-lg bg-primary text-sm font-semibold text-primary-foreground shadow-sm">
|
<div className="grid size-10 place-items-center rounded-lg bg-primary text-sm font-semibold text-primary-foreground shadow-sm">
|
||||||
PM
|
PM
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<h1 className="text-[1.95rem] font-semibold leading-none tracking-tight">PodMan</h1>
|
||||||
<h1 className="text-[1.95rem] font-semibold leading-none tracking-tight">
|
|
||||||
PodMan
|
|
||||||
</h1>
|
|
||||||
<Badge variant={podManOnline ? 'default' : 'secondary'}>
|
|
||||||
<CircleDotIcon data-icon="inline-start" />
|
|
||||||
{podManOnline ? 'online' : 'standby'}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Live engineering rooms, team memory, and intervention routing.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Badge variant="outline" className="h-8 rounded-lg px-3">
|
|
||||||
<ShieldCheckIcon data-icon="inline-start" />
|
|
||||||
Privacy-limited
|
|
||||||
</Badge>
|
|
||||||
<Button variant="outline" onClick={() => void refresh()} disabled={loading}>
|
<Button variant="outline" onClick={() => void refresh()} disabled={loading}>
|
||||||
<RefreshCwIcon data-icon="inline-start" />
|
<RefreshCwIcon data-icon="inline-start" />
|
||||||
Refresh
|
Refresh
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-4">
|
|
||||||
<StatPill icon={WifiIcon} label="Live" value={fmt.format(liveTotal)} />
|
|
||||||
<StatPill icon={RadioTowerIcon} label="Rooms" value={fmt.format(activeRooms)} />
|
|
||||||
<StatPill icon={UsersIcon} label="Roster" value={fmt.format(totalMembers)} />
|
|
||||||
<StatPill icon={BrainCircuitIcon} label="Memory" value={fmt.format(latestActivity)} />
|
|
||||||
</div>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
@@ -374,9 +327,6 @@ export default function App() {
|
|||||||
<p className="text-xs font-medium uppercase text-muted-foreground">Workspaces</p>
|
<p className="text-xs font-medium uppercase text-muted-foreground">Workspaces</p>
|
||||||
<h2 className="text-xl font-semibold tracking-tight">Active pods</h2>
|
<h2 className="text-xl font-semibold tracking-tight">Active pods</h2>
|
||||||
</div>
|
</div>
|
||||||
<p className="max-w-xl text-sm leading-6 text-muted-foreground">
|
|
||||||
Join the room that matches your current workstream.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
@@ -420,21 +370,6 @@ export default function App() {
|
|||||||
|
|
||||||
<aside className="flex flex-col gap-4">
|
<aside className="flex flex-col gap-4">
|
||||||
<CreatePodForm busy={pending.has('new')} onCreate={handleCreate} />
|
<CreatePodForm busy={pending.has('new')} onCreate={handleCreate} />
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Operating brief</CardTitle>
|
|
||||||
<CardDescription>Current coordination signals.</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-3">
|
|
||||||
<BriefLine label="Notification default" value="Card" />
|
|
||||||
<BriefLine label="Escalation" value="Hermes voice only when urgent" />
|
|
||||||
<BriefLine label="Memory events" value={fmt.format(latestActivity)} />
|
|
||||||
<BriefLine
|
|
||||||
label="Live people"
|
|
||||||
value={liveNames.length ? liveNames.join(', ') : 'None'}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</aside>
|
</aside>
|
||||||
</main>
|
</main>
|
||||||
)}
|
)}
|
||||||
@@ -443,37 +378,6 @@ export default function App() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function StatPill({
|
|
||||||
icon: Icon,
|
|
||||||
label,
|
|
||||||
value,
|
|
||||||
}: {
|
|
||||||
icon: React.ComponentType<{ className?: string }>;
|
|
||||||
label: string;
|
|
||||||
value: string;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="flex min-h-16 items-center gap-3 rounded-lg border bg-card/90 px-3 py-2 shadow-sm">
|
|
||||||
<div className="grid size-8 place-items-center rounded-md bg-muted">
|
|
||||||
<Icon className="size-4 text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
<div className="min-w-0">
|
|
||||||
<p className="text-xs font-medium uppercase text-muted-foreground">{label}</p>
|
|
||||||
<p className="text-base font-medium">{value}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function BriefLine({ label, value }: { label: string; value: string }) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-start justify-between gap-4 rounded-md bg-muted/45 px-3 py-2">
|
|
||||||
<span className="text-sm text-muted-foreground">{label}</span>
|
|
||||||
<span className="max-w-44 text-right text-sm font-medium">{value}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function PodSkeleton() {
|
function PodSkeleton() {
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ import {
|
|||||||
} from '../lib/api.js';
|
} from '../lib/api.js';
|
||||||
import { useInterventions, primeSpeech } from '../livekit/useInterventions.js';
|
import { useInterventions, primeSpeech } from '../livekit/useInterventions.js';
|
||||||
import { usePodActivity } from '../hooks/use-pod-activity.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 { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||||
import { Avatar, AvatarBadge, AvatarFallback } from '@/components/ui/avatar';
|
import { Avatar, AvatarBadge, AvatarFallback } from '@/components/ui/avatar';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
@@ -145,7 +144,7 @@ export function PodView({
|
|||||||
const [testingVoice, setTestingVoice] = useState(false);
|
const [testingVoice, setTestingVoice] = useState(false);
|
||||||
const [note, setNote] = useState<string | null>(null);
|
const [note, setNote] = useState<string | null>(null);
|
||||||
const [audioBlocked, setAudioBlocked] = useState(false);
|
const [audioBlocked, setAudioBlocked] = useState(false);
|
||||||
const [remoteAudioTracks, setRemoteAudioTracks] = useState(0);
|
const [, setRemoteAudioTracks] = useState(0);
|
||||||
const [micOn, setMicOn] = useState(false);
|
const [micOn, setMicOn] = useState(false);
|
||||||
const [historyMember, setHistoryMember] = useState<string | null>(null);
|
const [historyMember, setHistoryMember] = useState<string | null>(null);
|
||||||
const [history, setHistory] = useState<MemberWorkHistory | null>(null);
|
const [history, setHistory] = useState<MemberWorkHistory | null>(null);
|
||||||
@@ -567,8 +566,6 @@ export function PodView({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const podmanPresent = participants.some((p) => p.name.toLowerCase() === 'podman');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarProvider
|
<SidebarProvider
|
||||||
open={leftStreamOpen}
|
open={leftStreamOpen}
|
||||||
@@ -586,7 +583,7 @@ export function PodView({
|
|||||||
title="My stream"
|
title="My stream"
|
||||||
collapsedLabel="Mine"
|
collapsedLabel="Mine"
|
||||||
testId="my-stream-sidebar"
|
testId="my-stream-sidebar"
|
||||||
description={`${me}'s live screen, git, intervention, and conflict log.`}
|
description={`${me}'s activity feed.`}
|
||||||
events={activity.mine}
|
events={activity.mine}
|
||||||
connected={activity.connected}
|
connected={activity.connected}
|
||||||
open={leftStreamOpen}
|
open={leftStreamOpen}
|
||||||
@@ -681,12 +678,6 @@ export function PodView({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-3 sm:grid-cols-3">
|
|
||||||
<Metric label="Participants" value={participants.length || 1} />
|
|
||||||
<Metric label="Screen" value={sharing ? 'sharing' : 'idle'} />
|
|
||||||
<Metric label="PodMan" value={podmanPresent ? 'online' : 'waiting'} />
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{devMode && (
|
{devMode && (
|
||||||
@@ -922,41 +913,6 @@ export function PodView({
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Status</CardTitle>
|
|
||||||
<CardDescription>Connection, media, and agent presence.</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<LiveWaveform
|
|
||||||
processing={!!room || beat.on}
|
|
||||||
active={false}
|
|
||||||
height={32}
|
|
||||||
barWidth={2}
|
|
||||||
barGap={3}
|
|
||||||
className="mb-4 px-3 py-2 text-muted-foreground"
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
<StatusLine label="LiveKit" value={room ? 'connected' : 'offline'} />
|
|
||||||
<StatusLine label="Screen" value={sharing ? 'published' : 'not shared'} />
|
|
||||||
<StatusLine
|
|
||||||
label="Audio"
|
|
||||||
value={
|
|
||||||
audioBlocked
|
|
||||||
? 'blocked'
|
|
||||||
: remoteAudioTracks > 0
|
|
||||||
? `${remoteAudioTracks} remote track${remoteAudioTracks === 1 ? '' : 's'}`
|
|
||||||
: beat.on
|
|
||||||
? beat.mine
|
|
||||||
? 'publishing'
|
|
||||||
: `${beat.by} playing`
|
|
||||||
: 'ready'
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<StatusLine label="Agent" value={podmanPresent ? 'watching' : 'waiting'} />
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</aside>
|
</aside>
|
||||||
</main>
|
</main>
|
||||||
<div
|
<div
|
||||||
@@ -980,7 +936,7 @@ export function PodView({
|
|||||||
title="Team stream"
|
title="Team stream"
|
||||||
collapsedLabel="Team"
|
collapsedLabel="Team"
|
||||||
testId="team-stream-sidebar"
|
testId="team-stream-sidebar"
|
||||||
description="Everyone else in this pod, merged into one realtime feed."
|
description="Team activity feed."
|
||||||
events={activity.team}
|
events={activity.team}
|
||||||
connected={activity.connected}
|
connected={activity.connected}
|
||||||
open={rightStreamOpen}
|
open={rightStreamOpen}
|
||||||
@@ -993,17 +949,6 @@ export function PodView({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Metric({ label, value }: { label: string; value: number | string }) {
|
|
||||||
return (
|
|
||||||
<Card size="sm">
|
|
||||||
<CardContent>
|
|
||||||
<p className="text-xs text-muted-foreground">{label}</p>
|
|
||||||
<p className="mt-1 text-lg font-medium">{value}</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Participant({
|
function Participant({
|
||||||
participant,
|
participant,
|
||||||
onOpenHistory,
|
onOpenHistory,
|
||||||
@@ -1548,7 +1493,7 @@ const ACTIVITY_CATEGORIES: {
|
|||||||
{
|
{
|
||||||
id: 'signal',
|
id: 'signal',
|
||||||
label: 'Signals',
|
label: 'Signals',
|
||||||
hint: 'Screen logs and local git activity routed to this stream.',
|
hint: 'Recent screen and git activity.',
|
||||||
icon: RadioTowerIcon,
|
icon: RadioTowerIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user