2026-05-08 19:53:21 -04:00
|
|
|
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
|
|
|
|
import { listSessions } from '../services/api'
|
|
|
|
|
import { AlertCircle, Monitor, RefreshCw, Cpu, MessageSquare, Clock, Hash } from 'lucide-react'
|
|
|
|
|
|
2026-05-07 20:15:30 -04:00
|
|
|
export default function SessionsPage() {
|
2026-05-08 19:53:21 -04:00
|
|
|
const queryClient = useQueryClient()
|
|
|
|
|
|
|
|
|
|
const { data, isLoading, error } = useQuery({
|
|
|
|
|
queryKey: ['sessions'],
|
|
|
|
|
queryFn: listSessions,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const sessions = data?.data ?? []
|
|
|
|
|
|
|
|
|
|
if (isLoading) return <SessionsSkeleton />
|
|
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex flex-col items-center justify-center h-96 gap-4">
|
|
|
|
|
<AlertCircle size={48} className="text-danger" />
|
|
|
|
|
<p className="text-danger text-lg">Failed to load sessions</p>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => queryClient.invalidateQueries({ queryKey: ['sessions'] })}
|
|
|
|
|
className="flex items-center gap-2 px-4 py-2 rounded-lg bg-primary/10 text-primary hover:bg-primary/20 transition-colors"
|
|
|
|
|
>
|
|
|
|
|
<RefreshCw size={16} /> Retry
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-07 20:15:30 -04:00
|
|
|
return (
|
2026-05-08 19:53:21 -04:00
|
|
|
<div className="space-y-6">
|
|
|
|
|
<header>
|
|
|
|
|
<h1 className="text-2xl font-bold">Sessions</h1>
|
|
|
|
|
<p className="text-on-surface-variant">Active and recent agent sessions</p>
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
{sessions.length === 0 ? (
|
|
|
|
|
<div className="flex flex-col items-center justify-center h-48 gap-3 border border-dashed border-surface-light rounded-xl">
|
|
|
|
|
<Monitor size={40} className="text-on-surface-muted" />
|
|
|
|
|
<p className="text-on-surface-muted text-lg">No active sessions</p>
|
|
|
|
|
<p className="text-on-surface-muted text-sm">Sessions will appear when agents connect.</p>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
{/* Desktop: Table view */}
|
|
|
|
|
<div className="hidden lg:block overflow-x-auto rounded-xl border border-surface-light">
|
|
|
|
|
<table className="w-full text-sm">
|
|
|
|
|
<thead>
|
|
|
|
|
<tr className="border-b border-surface-light bg-surface-dark">
|
|
|
|
|
<th className="text-left p-4 font-medium text-on-surface-variant">Agent</th>
|
|
|
|
|
<th className="text-left p-4 font-medium text-on-surface-variant">Session Key</th>
|
|
|
|
|
<th className="text-left p-4 font-medium text-on-surface-variant">Channel</th>
|
|
|
|
|
<th className="text-left p-4 font-medium text-on-surface-variant">Model</th>
|
|
|
|
|
<th className="text-right p-4 font-medium text-on-surface-variant">Context Tokens</th>
|
|
|
|
|
<th className="text-right p-4 font-medium text-on-surface-variant">Started</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
{sessions.map((s) => (
|
|
|
|
|
<tr
|
|
|
|
|
key={s.id}
|
|
|
|
|
className="border-b border-surface-light hover:bg-surface-dark/50 transition-colors"
|
|
|
|
|
>
|
|
|
|
|
<td className="p-4 font-medium">{s.agentId}</td>
|
|
|
|
|
<td className="p-4">
|
|
|
|
|
<code className="text-xs bg-surface-light px-2 py-1 rounded text-on-surface-variant">
|
|
|
|
|
{s.sessionKey}
|
|
|
|
|
</code>
|
|
|
|
|
</td>
|
|
|
|
|
<td className="p-4 text-on-surface-variant">{s.channel}</td>
|
|
|
|
|
<td className="p-4 text-on-surface-variant">{s.model}</td>
|
|
|
|
|
<td className="p-4 text-right tabular-nums text-on-surface-variant">
|
|
|
|
|
{s.contextTokens.toLocaleString()}
|
|
|
|
|
</td>
|
|
|
|
|
<td className="p-4 text-right text-on-surface-muted whitespace-nowrap">
|
|
|
|
|
{formatDateTime(s.startedAt)}
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
))}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Mobile: Card view */}
|
|
|
|
|
<div className="lg:hidden grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
|
|
|
{sessions.map((s) => (
|
|
|
|
|
<div
|
|
|
|
|
key={s.id}
|
|
|
|
|
className="p-4 rounded-xl border border-surface-light bg-surface-dark"
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-center gap-2 mb-3">
|
|
|
|
|
<Monitor size={16} className="text-primary" />
|
|
|
|
|
<span className="font-medium text-sm">{s.agentId}</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2 text-sm">
|
|
|
|
|
<Row icon={Hash} label="Session">{s.sessionKey}</Row>
|
|
|
|
|
<Row icon={MessageSquare} label="Channel">{s.channel}</Row>
|
|
|
|
|
<Row icon={Cpu} label="Model">{s.model}</Row>
|
|
|
|
|
<Row icon={Hash} label="Tokens">{s.contextTokens.toLocaleString()}</Row>
|
|
|
|
|
<Row icon={Clock} label="Started">{formatDateTime(s.startedAt)}</Row>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function Row({ icon: Icon, label, children }: { icon: React.ElementType; label: string; children: React.ReactNode }) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<Icon size={14} className="text-on-surface-muted shrink-0" />
|
|
|
|
|
<span className="text-on-surface-muted text-xs w-14 shrink-0">{label}</span>
|
|
|
|
|
<span className="text-on-surface-variant truncate">{children}</span>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatDateTime(iso: string): string {
|
|
|
|
|
try {
|
|
|
|
|
const d = new Date(iso)
|
|
|
|
|
return d.toLocaleString(undefined, {
|
|
|
|
|
month: 'short',
|
|
|
|
|
day: 'numeric',
|
|
|
|
|
hour: 'numeric',
|
|
|
|
|
minute: '2-digit',
|
|
|
|
|
})
|
|
|
|
|
} catch {
|
|
|
|
|
return iso
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function SessionsSkeleton() {
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-6 animate-pulse">
|
|
|
|
|
<div>
|
|
|
|
|
<div className="h-8 w-40 skeleton mb-2" />
|
|
|
|
|
<div className="h-4 w-56 skeleton" />
|
|
|
|
|
</div>
|
|
|
|
|
{/* Desktop skeleton */}
|
|
|
|
|
<div className="hidden lg:block rounded-xl border border-surface-light overflow-hidden">
|
|
|
|
|
<div className="bg-surface-dark p-4 border-b border-surface-light">
|
|
|
|
|
<div className="grid grid-cols-6 gap-4">
|
|
|
|
|
{[...Array(6)].map((_, i) => (
|
|
|
|
|
<div key={i} className="h-3 skeleton" />
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{[...Array(5)].map((_, i) => (
|
|
|
|
|
<div key={i} className="border-b border-surface-light p-4">
|
|
|
|
|
<div className="grid grid-cols-6 gap-4">
|
|
|
|
|
{[...Array(6)].map((_, j) => (
|
|
|
|
|
<div key={j} className="h-4 skeleton" />
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
{/* Mobile skeleton */}
|
|
|
|
|
<div className="lg:hidden grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
|
|
|
{[...Array(4)].map((_, i) => (
|
|
|
|
|
<div key={i} className="p-4 rounded-xl border border-surface-light bg-surface-dark">
|
|
|
|
|
<div className="h-4 w-24 skeleton mb-3" />
|
|
|
|
|
{[...Array(5)].map((_, j) => (
|
|
|
|
|
<div key={j} className="h-4 w-full skeleton mb-2" />
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
2026-05-07 20:15:30 -04:00
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|