2026-04-27 11:20:47 -05:00
|
|
|
import { Link, useNavigate, useParams } from "@tanstack/react-router";
|
|
|
|
|
import { motion, type Variants } from "framer-motion";
|
|
|
|
|
import { ArrowLeft, ChevronRight, CircleDot, Clock, MessageSquare } from "lucide-react";
|
|
|
|
|
import { useMemo, useState } from "react";
|
2026-04-24 21:30:48 -05:00
|
|
|
import { useSessions } from "@/api/queries";
|
2026-04-24 13:47:22 -05:00
|
|
|
import type { components } from "@/api/schema.d.ts";
|
|
|
|
|
import { EmptyState } from "@/components/shared/EmptyState";
|
2026-04-24 21:30:48 -05:00
|
|
|
import { ErrorAlert } from "@/components/shared/ErrorAlert";
|
2026-04-24 13:47:22 -05:00
|
|
|
import { PageLoader } from "@/components/shared/LoadingSpinner";
|
2026-04-24 21:30:48 -05:00
|
|
|
import { Pagination } from "@/components/shared/Pagination";
|
2026-04-24 16:52:40 -05:00
|
|
|
import { SortControl, type SortDir } from "@/components/shared/SortControl";
|
|
|
|
|
import { MonoCaption, PageTitle } from "@/components/ui/typography";
|
2026-04-24 13:56:13 -05:00
|
|
|
import { COLOR } from "@/lib/constants";
|
2026-04-24 21:30:48 -05:00
|
|
|
|
|
|
|
|
type Session = components["schemas"]["Session"];
|
|
|
|
|
|
2026-04-24 16:52:40 -05:00
|
|
|
const SORT_OPTIONS = [
|
|
|
|
|
{ value: "created_at", label: "Newest" },
|
|
|
|
|
{ value: "active", label: "Active" },
|
|
|
|
|
{ value: "id", label: "ID" },
|
|
|
|
|
];
|
|
|
|
|
|
2026-04-24 21:30:48 -05:00
|
|
|
const container: Variants = {
|
|
|
|
|
hidden: { opacity: 0 },
|
|
|
|
|
show: { opacity: 1, transition: { staggerChildren: 0.05 } },
|
|
|
|
|
};
|
|
|
|
|
const item: Variants = {
|
|
|
|
|
hidden: { opacity: 0, y: 10 },
|
|
|
|
|
show: { opacity: 1, y: 0, transition: { type: "spring", stiffness: 280, damping: 24 } },
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export function SessionList() {
|
|
|
|
|
const { workspaceId } = useParams({ strict: false }) as { workspaceId: string };
|
|
|
|
|
const [page, setPage] = useState(1);
|
2026-04-24 16:52:40 -05:00
|
|
|
const [sortField, setSortField] = useState("created_at");
|
|
|
|
|
const [sortDir, setSortDir] = useState<SortDir>("desc");
|
2026-04-24 21:30:48 -05:00
|
|
|
const navigate = useNavigate();
|
|
|
|
|
const { data, isLoading, error } = useSessions(workspaceId, page);
|
|
|
|
|
|
|
|
|
|
const sessions: Session[] = (data as { items?: Session[] } | undefined)?.items ?? [];
|
|
|
|
|
const totalPages = (data as { pages?: number } | undefined)?.pages ?? 1;
|
|
|
|
|
const total = (data as { total?: number } | undefined)?.total ?? 0;
|
|
|
|
|
|
2026-04-24 16:52:40 -05:00
|
|
|
const sorted = useMemo(() => {
|
|
|
|
|
return [...sessions].sort((a, b) => {
|
|
|
|
|
let cmp = 0;
|
|
|
|
|
if (sortField === "created_at") {
|
|
|
|
|
cmp = new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
|
|
|
|
|
} else if (sortField === "active") {
|
|
|
|
|
// active sessions first (true > false)
|
|
|
|
|
cmp = Number(a.is_active) - Number(b.is_active);
|
|
|
|
|
} else if (sortField === "id") {
|
|
|
|
|
cmp = a.id.localeCompare(b.id);
|
|
|
|
|
}
|
|
|
|
|
return sortDir === "asc" ? cmp : -cmp;
|
|
|
|
|
});
|
|
|
|
|
}, [sessions, sortField, sortDir]);
|
|
|
|
|
|
|
|
|
|
function handleSort(field: string, dir: SortDir) {
|
|
|
|
|
setSortField(field);
|
|
|
|
|
setSortDir(dir);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-24 21:30:48 -05:00
|
|
|
return (
|
2026-04-24 16:52:40 -05:00
|
|
|
<div className="page-container">
|
|
|
|
|
<motion.div initial={{ opacity: 0, y: -8 }} animate={{ opacity: 1, y: 0 }} className="mb-6">
|
2026-04-24 21:30:48 -05:00
|
|
|
<Link
|
|
|
|
|
to="/workspaces/$workspaceId"
|
feat: wire all remaining API endpoints
- src/api/keys.ts: QK singleton for all 20+ query key patterns
- src/api/queries.ts: rewritten with QK + full mutation coverage:
deleteWorkspace, updateWorkspace, scheduleDream,
updatePeer, setPeerCard, searchPeer,
deleteSession, updateSession, cloneSession, searchSession,
createMessages, updateMessage,
useSessionPeers, addPeersToSession, setSessionPeers,
removePeersFromSession, setPeerConfig,
createConclusion, deleteConclusion,
useWebhooks, createWebhook, deleteWebhook, testWebhook,
createKey
Shared primitives (just-in-time):
- ConfirmDialog: animated modal with keyboard/click-out dismiss
- FormModal: reusable modal shell
- InlineEditor: click-to-edit with commit/cancel
New surfaces:
- WorkspaceDetail: delete + schedule dream buttons
- ScheduleDreamModal: Zod-validated observer/session form
- WebhookManager: full CRUD + test emit (new /webhooks route)
- SessionDetail: delete, clone, session search, peer membership tab
- PeerDetail: editable peer card, semantic search tab
- ConclusionBrowser: create conclusion modal (Zod), per-row delete
2026-04-24 11:25:19 -05:00
|
|
|
params={{ workspaceId } as never}
|
2026-04-24 21:30:48 -05:00
|
|
|
className="inline-flex items-center gap-1.5 text-xs mb-4 transition-colors"
|
2026-04-24 16:52:40 -05:00
|
|
|
style={{ color: COLOR.dimText }}
|
2026-04-24 21:30:48 -05:00
|
|
|
>
|
|
|
|
|
<ArrowLeft className="w-3 h-3" strokeWidth={1.5} />
|
|
|
|
|
{workspaceId}
|
|
|
|
|
</Link>
|
|
|
|
|
<div className="flex items-center gap-2 mb-1">
|
2026-04-24 16:52:40 -05:00
|
|
|
<MessageSquare className="w-5 h-5" style={{ color: COLOR.accent }} strokeWidth={1.5} />
|
2026-04-24 13:56:13 -05:00
|
|
|
<PageTitle>Sessions</PageTitle>
|
2026-04-24 21:30:48 -05:00
|
|
|
{total > 0 && (
|
|
|
|
|
<span
|
2026-04-24 16:52:40 -05:00
|
|
|
className="ml-1 text-xs font-mono px-2 py-0.5 rounded-full"
|
2026-04-24 21:30:48 -05:00
|
|
|
style={{
|
2026-04-24 13:56:13 -05:00
|
|
|
background: COLOR.accentSubtle,
|
|
|
|
|
color: COLOR.accentText,
|
|
|
|
|
border: `1px solid ${COLOR.accentBorder}`,
|
2026-04-24 21:30:48 -05:00
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{total}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
2026-04-24 16:52:40 -05:00
|
|
|
<div className="ml-auto">
|
|
|
|
|
<SortControl
|
|
|
|
|
options={SORT_OPTIONS}
|
|
|
|
|
field={sortField}
|
|
|
|
|
dir={sortDir}
|
|
|
|
|
onChange={handleSort}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2026-04-24 21:30:48 -05:00
|
|
|
</div>
|
2026-04-24 16:52:40 -05:00
|
|
|
<MonoCaption className="mt-0.5" as="p">
|
|
|
|
|
{workspaceId}
|
|
|
|
|
</MonoCaption>
|
2026-04-24 21:30:48 -05:00
|
|
|
</motion.div>
|
|
|
|
|
|
|
|
|
|
<ErrorAlert error={error instanceof Error ? error : null} />
|
|
|
|
|
{isLoading && <PageLoader />}
|
|
|
|
|
|
|
|
|
|
{!isLoading && sessions.length === 0 && (
|
|
|
|
|
<EmptyState
|
|
|
|
|
icon={MessageSquare}
|
|
|
|
|
title="No sessions found"
|
|
|
|
|
description="No sessions exist in this workspace."
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-04-24 16:52:40 -05:00
|
|
|
{!isLoading && sorted.length > 0 && (
|
2026-04-24 21:30:48 -05:00
|
|
|
<>
|
|
|
|
|
<motion.div variants={container} initial="hidden" animate="show" className="space-y-2">
|
2026-04-24 16:52:40 -05:00
|
|
|
{sorted.map((session) => (
|
2026-04-24 21:30:48 -05:00
|
|
|
<motion.button
|
|
|
|
|
key={session.id}
|
|
|
|
|
variants={item}
|
|
|
|
|
onClick={() =>
|
|
|
|
|
navigate({
|
|
|
|
|
to: "/workspaces/$workspaceId/sessions/$sessionId",
|
|
|
|
|
params: { workspaceId, sessionId: session.id } as never,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
className="w-full text-left rounded-xl px-5 py-4 group"
|
|
|
|
|
style={{
|
2026-04-24 16:52:40 -05:00
|
|
|
background: COLOR.cardBaseBg,
|
|
|
|
|
border: `1px solid ${COLOR.cardBaseBorder}`,
|
2026-04-24 21:30:48 -05:00
|
|
|
}}
|
|
|
|
|
whileHover={{
|
2026-04-24 16:52:40 -05:00
|
|
|
background: COLOR.accentDimHover,
|
|
|
|
|
borderColor: COLOR.accentBorder,
|
2026-04-24 21:30:48 -05:00
|
|
|
x: 2,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-center justify-between">
|
2026-04-24 13:47:22 -05:00
|
|
|
<span
|
|
|
|
|
className="font-mono text-sm font-medium truncate"
|
2026-04-24 16:52:40 -05:00
|
|
|
style={{ color: COLOR.accentSoft }}
|
2026-04-24 13:47:22 -05:00
|
|
|
>
|
2026-04-24 21:30:48 -05:00
|
|
|
{session.id}
|
|
|
|
|
</span>
|
|
|
|
|
<div className="flex items-center gap-2 shrink-0 ml-2">
|
|
|
|
|
{session.is_active && (
|
|
|
|
|
<div className="flex items-center gap-1">
|
|
|
|
|
<motion.div
|
|
|
|
|
animate={{ opacity: [0.5, 1, 0.5] }}
|
2026-04-24 13:47:22 -05:00
|
|
|
transition={{ duration: 2, repeat: Number.POSITIVE_INFINITY }}
|
2026-04-24 21:30:48 -05:00
|
|
|
>
|
2026-04-24 13:47:22 -05:00
|
|
|
<CircleDot
|
|
|
|
|
className="w-3 h-3"
|
2026-04-24 13:56:13 -05:00
|
|
|
style={{ color: COLOR.success }}
|
2026-04-24 13:47:22 -05:00
|
|
|
strokeWidth={2}
|
|
|
|
|
/>
|
2026-04-24 21:30:48 -05:00
|
|
|
</motion.div>
|
2026-04-24 13:56:13 -05:00
|
|
|
<span className="text-xs" style={{ color: COLOR.success }}>
|
2026-04-24 13:47:22 -05:00
|
|
|
Active
|
|
|
|
|
</span>
|
2026-04-24 21:30:48 -05:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
<ChevronRight
|
|
|
|
|
className="w-4 h-4 opacity-30 group-hover:opacity-70 transition-opacity"
|
2026-04-24 16:52:40 -05:00
|
|
|
style={{ color: COLOR.accent }}
|
2026-04-24 21:30:48 -05:00
|
|
|
strokeWidth={1.5}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-04-24 13:47:22 -05:00
|
|
|
<div className="flex items-center gap-2 mt-2">
|
|
|
|
|
{session.created_at && (
|
|
|
|
|
<div className="flex items-center gap-1.5">
|
|
|
|
|
<Clock
|
|
|
|
|
className="w-3 h-3"
|
2026-04-24 16:52:40 -05:00
|
|
|
style={{ color: COLOR.dimIcon }}
|
2026-04-24 13:47:22 -05:00
|
|
|
strokeWidth={1.5}
|
|
|
|
|
/>
|
2026-04-24 13:56:13 -05:00
|
|
|
<MonoCaption>{new Date(session.created_at).toLocaleString()}</MonoCaption>
|
2026-04-24 13:47:22 -05:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{(session.metadata as Record<string, string> | null)?.source && (
|
|
|
|
|
<span
|
|
|
|
|
className="text-xs font-mono px-1.5 py-0.5 rounded"
|
|
|
|
|
style={{
|
2026-04-24 13:56:13 -05:00
|
|
|
background: COLOR.accentDim,
|
|
|
|
|
border: `1px solid ${COLOR.accentBorderStrong}`,
|
2026-04-24 16:52:40 -05:00
|
|
|
color: COLOR.dimText,
|
2026-04-24 13:47:22 -05:00
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{(session.metadata as Record<string, string>).source}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2026-04-24 21:30:48 -05:00
|
|
|
</motion.button>
|
|
|
|
|
))}
|
|
|
|
|
</motion.div>
|
|
|
|
|
<Pagination page={page} totalPages={totalPages} onPageChange={setPage} />
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|