Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 48 additions & 1 deletion apps/studio/src/components/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,65 @@
*
* The sidebar provides navigation, breadcrumbs show the current
* location, and the main area renders the active route via Outlet.
*
* Responsive behavior:
* - md+ (≥768px): sidebar always visible as a fixed left panel.
* - <md: sidebar hidden by default; a hamburger in the mobile top bar toggles it.
*/

import { Outlet } from '@tanstack/react-router';

import { SidebarProvider, useSidebarContext } from '~/lib/sidebar-context';

import { Breadcrumbs } from './Breadcrumbs';
import { Sidebar } from './Sidebar';

export function Layout() {
return (
<SidebarProvider>
<LayoutInner />
</SidebarProvider>
);
}

function LayoutInner() {
const { toggle } = useSidebarContext();

return (
<div className="flex h-screen overflow-hidden">
<Sidebar />
<div className="flex flex-1 flex-col overflow-hidden">

<div className="flex min-w-0 flex-1 flex-col overflow-hidden">
{/* Mobile top bar — only visible below md breakpoint */}
<header className="flex items-center gap-3 border-b border-gray-800 bg-gray-900/50 px-4 py-3 md:hidden">
<button
type="button"
onClick={toggle}
className="text-gray-400 hover:text-gray-200"
aria-label="Toggle navigation"
>
{/* Hamburger icon */}
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
role="img"
aria-label="Toggle navigation"
>
<line x1="3" y1="6" x2="21" y2="6" />
<line x1="3" y1="12" x2="21" y2="12" />
<line x1="3" y1="18" x2="21" y2="18" />
</svg>
</button>
<span className="text-sm font-semibold text-white">AgentV Studio</span>
</header>

<Breadcrumbs />
<main className="flex-1 overflow-y-auto p-6">
<Outlet />
Expand Down
73 changes: 58 additions & 15 deletions apps/studio/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,15 @@
* - At eval detail: shows list of evals in the current run with pass/fail indicators
* - At suite detail: shows evals filtered to that suite
* - At experiment detail: shows list of experiments
*
* Responsive behavior is handled by SidebarShell:
* - md+ (≥768px): always-visible fixed left panel
* - <md: hidden by default, slides in as an overlay when toggled via the hamburger
*/

import { Link, useMatchRoute } from '@tanstack/react-router';
import { type ReactNode, useEffect } from 'react';

import { Link, useLocation, useMatchRoute } from '@tanstack/react-router';

import {
isPassing,
Expand All @@ -22,6 +28,43 @@ import {
useRunList,
useStudioConfig,
} from '~/lib/api';
import { useSidebarContext } from '~/lib/sidebar-context';

/** Responsive <aside> wrapper. Handles mobile overlay and desktop static placement. */
function SidebarShell({ children }: { children: ReactNode }) {
const { isOpen, close } = useSidebarContext();
const location = useLocation();

// Close sidebar on navigation (mobile UX)
// biome-ignore lint/correctness/useExhaustiveDependencies: location.pathname is the intended trigger
useEffect(() => {
close();
}, [close, location.pathname]);

return (
<>
{/* Backdrop — mobile only, shown when sidebar is open */}
{isOpen && (
<div
className="fixed inset-0 z-30 bg-black/50 md:hidden"
onClick={close}
onKeyDown={(e) => e.key === 'Escape' && close()}
role="button"
tabIndex={-1}
aria-label="Close navigation"
/>
)}

<aside
className={`fixed inset-y-0 left-0 z-40 flex w-64 flex-col border-r border-gray-800 bg-gray-900/50 transition-transform duration-200 ease-in-out md:static md:z-auto md:translate-x-0 ${
isOpen ? 'translate-x-0' : '-translate-x-full'
}`}
>
{children}
</aside>
</>
);
}

export function Sidebar() {
const matchRoute = useMatchRoute();
Expand Down Expand Up @@ -120,7 +163,7 @@ function RunSidebar() {
const data = useAggregated ? aggregatedData : localData;

return (
<aside className="flex w-64 flex-col border-r border-gray-800 bg-gray-900/50">
<SidebarShell>
<div className="flex items-center gap-2 border-b border-gray-800 px-4 py-4">
<Link to="/" className="text-lg font-semibold text-white hover:text-cyan-400">
AgentV Studio
Expand Down Expand Up @@ -181,7 +224,7 @@ function RunSidebar() {
Settings
</Link>
</div>
</aside>
</SidebarShell>
);
}

Expand All @@ -191,7 +234,7 @@ function EvalSidebar({ runId, currentEvalId }: { runId: string; currentEvalId: s
const passThreshold = config?.threshold ?? config?.pass_threshold ?? 0.8;

return (
<aside className="flex w-64 flex-col border-r border-gray-800 bg-gray-900/50">
<SidebarShell>
<div className="flex items-center gap-2 border-b border-gray-800 px-4 py-4">
<Link to="/" className="text-lg font-semibold text-white hover:text-cyan-400">
AgentV Studio
Expand Down Expand Up @@ -238,7 +281,7 @@ function EvalSidebar({ runId, currentEvalId }: { runId: string; currentEvalId: s
);
})}
</nav>
</aside>
</SidebarShell>
);
}

Expand All @@ -249,7 +292,7 @@ function SuiteSidebar({ runId, suite }: { runId: string; suite: string }) {
const suiteResults = (data?.results ?? []).filter((r) => (r.suite ?? 'Uncategorized') === suite);

return (
<aside className="flex w-64 flex-col border-r border-gray-800 bg-gray-900/50">
<SidebarShell>
<div className="flex items-center gap-2 border-b border-gray-800 px-4 py-4">
<Link to="/" className="text-lg font-semibold text-white hover:text-cyan-400">
AgentV Studio
Expand Down Expand Up @@ -292,7 +335,7 @@ function SuiteSidebar({ runId, suite }: { runId: string; suite: string }) {
);
})}
</nav>
</aside>
</SidebarShell>
);
}

Expand All @@ -301,7 +344,7 @@ function CategorySidebar({ runId, category }: { runId: string; category: string
const suites = data?.suites ?? [];

return (
<aside className="flex w-64 flex-col border-r border-gray-800 bg-gray-900/50">
<SidebarShell>
<div className="flex items-center gap-2 border-b border-gray-800 px-4 py-4">
<Link to="/" className="text-lg font-semibold text-white hover:text-cyan-400">
AgentV Studio
Expand Down Expand Up @@ -341,7 +384,7 @@ function CategorySidebar({ runId, category }: { runId: string; category: string
</Link>
))}
</nav>
</aside>
</SidebarShell>
);
}

Expand All @@ -357,7 +400,7 @@ function ProjectRunDetailSidebar({
const { data } = useProjectRunList(projectId);

return (
<aside className="flex w-64 flex-col border-r border-gray-800 bg-gray-900/50">
<SidebarShell>
<div className="flex items-center gap-2 border-b border-gray-800 px-4 py-4">
<Link to="/" className="text-lg font-semibold text-white hover:text-cyan-400">
AgentV Studio
Expand Down Expand Up @@ -393,7 +436,7 @@ function ProjectRunDetailSidebar({
);
})}
</nav>
</aside>
</SidebarShell>
);
}

Expand All @@ -411,7 +454,7 @@ function ProjectEvalSidebar({
const passThreshold = config?.threshold ?? config?.pass_threshold ?? 0.8;

return (
<aside className="flex w-64 flex-col border-r border-gray-800 bg-gray-900/50">
<SidebarShell>
<div className="flex items-center gap-2 border-b border-gray-800 px-4 py-4">
<Link to="/" className="text-lg font-semibold text-white hover:text-cyan-400">
AgentV Studio
Expand Down Expand Up @@ -455,7 +498,7 @@ function ProjectEvalSidebar({
);
})}
</nav>
</aside>
</SidebarShell>
);
}

Expand All @@ -464,7 +507,7 @@ function ExperimentSidebar({ currentExperiment }: { currentExperiment: string })
const experiments = data?.experiments ?? [];

return (
<aside className="flex w-64 flex-col border-r border-gray-800 bg-gray-900/50">
<SidebarShell>
<div className="flex items-center gap-2 border-b border-gray-800 px-4 py-4">
<Link to="/" className="text-lg font-semibold text-white hover:text-cyan-400">
AgentV Studio
Expand Down Expand Up @@ -506,6 +549,6 @@ function ExperimentSidebar({ currentExperiment }: { currentExperiment: string })
);
})}
</nav>
</aside>
</SidebarShell>
);
}
37 changes: 37 additions & 0 deletions apps/studio/src/lib/sidebar-context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* Sidebar open/close state shared between Layout (hamburger button)
* and SidebarShell (the <aside> visibility).
*/

import { createContext, useContext, useState } from 'react';

interface SidebarContextValue {
isOpen: boolean;
toggle: () => void;
close: () => void;
}

const SidebarContext = createContext<SidebarContextValue>({
isOpen: false,
toggle: () => {},
close: () => {},
});

export function SidebarProvider({ children }: { children: React.ReactNode }) {
const [isOpen, setIsOpen] = useState(false);
return (
<SidebarContext.Provider
value={{
isOpen,
toggle: () => setIsOpen((o) => !o),
close: () => setIsOpen(false),
}}
>
{children}
</SidebarContext.Provider>
);
}

export function useSidebarContext() {
return useContext(SidebarContext);
}
Loading