Skip to content

Commit 50dcca9

Browse files
feat(site): implement tab persistence and loading state for trajectory page (#48)
* feat(site): implement tab persistence and loading state for trajectory page - Added tab persistence using URL search parameters (`?tab=...`) - Dynamic tabs rendering based on `tabsConfig` prop - Added `Suspense` support and `TrajectoryPageFallback` for better loading state - Updated `.zealt/tasks.json` with a detailed instruction for the task 🤖 Generated with [Pochi](https://getpochi.com) | [Task](https://app.getpochi.com/share/p-d681e3f16fbf48f19020853021442223) Co-Authored-By: Pochi <noreply@getpochi.com> * update * update --------- Co-authored-by: Pochi <noreply@getpochi.com>
1 parent 4b27a3d commit 50dcca9

File tree

3 files changed

+59
-29
lines changed

3 files changed

+59
-29
lines changed

.zealt/tasks.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1086,7 +1086,7 @@
10861086
]
10871087
},
10881088
"template_author_metadata": {
1089-
"instruction": "",
1089+
"instruction": "You are a repo maintainer enforcing a metadata policy for your team's monorepo. You need to extract a custom-formatted log of the recent commits in the repository located at `/home/user/repo`.\n\nYour task is to:\n1. Navigate to `/home/user/repo`.\n2. Configure a repository-specific alias named `mylog` that runs `log` with a custom template. The template must output each commit's short ID (exactly 8 characters), author name, and the first line of the commit description, separated by a pipe character `|`, followed by a newline.\n3. Run your new `jj mylog` alias and redirect the output to a file named `/home/user/repo/formatted_log.txt`.\n\nNote: Do not use `sudo` or modify system-level configurations. Ensure the alias is saved in the repository's `.jj/repo/config.toml`.\n",
10901090
"trials": [
10911091
{
10921092
"job_name": "2026-03-19__17-16-51",

site/app/tasks/[name]/[jobId]/trajectory/components/trajectory-page.tsx

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,24 @@
33
import { useEffect, useMemo, useState } from "react";
44
import { useQuery } from "@tanstack/react-query";
55
import { useTheme } from "next-themes";
6+
import { useSearchParams, useRouter, usePathname } from "next/navigation";
67
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
78
import { ScrollArea } from "@/components/ui/scroll-area";
89
import { Skeleton } from "@/components/ui/skeleton";
910
import { Button } from "@/components/ui/button";
1011
import { HttpError } from "@/lib/http-error";
1112

13+
export type TabConfig = {
14+
value: string;
15+
label: React.ReactNode;
16+
};
17+
1218
type TrajectoryPageProps = {
1319
trajectoryUrl: string;
1420
fallbackUrl: string;
1521
stderrLogUrl: string | null;
1622
verifierLogUrl: string | null;
23+
tabsConfig: TabConfig[];
1724
};
1825

1926
async function fetchLogText(url: string): Promise<string> {
@@ -36,11 +43,24 @@ export function TrajectoryPage({
3643
fallbackUrl,
3744
stderrLogUrl,
3845
verifierLogUrl,
46+
tabsConfig,
3947
}: TrajectoryPageProps) {
4048
const { resolvedTheme } = useTheme();
49+
const searchParams = useSearchParams();
50+
const router = useRouter();
51+
const pathname = usePathname();
4152
const [mounted, setMounted] = useState(false);
4253
const [iframeLoading, setIframeLoading] = useState(true);
43-
const [activeTab, setActiveTab] = useState("trajectory");
54+
55+
const validTabs = tabsConfig.map((t) => t.value);
56+
const queryTab = searchParams.get("tab");
57+
const activeTab = queryTab && validTabs.includes(queryTab) ? queryTab : validTabs[0];
58+
59+
const handleTabChange = (value: string) => {
60+
const params = new URLSearchParams(searchParams.toString());
61+
params.set("tab", value);
62+
router.replace(`${pathname}?${params.toString()}`, { scroll: false });
63+
};
4464

4565
const iframeTheme = mounted && resolvedTheme === "light" ? "light" : "dark";
4666

@@ -55,7 +75,7 @@ export function TrajectoryPage({
5575
}, []);
5676

5777
useEffect(() => {
58-
if (!mounted) {
78+
if (!mounted || !iframeUrl) {
5979
return;
6080
}
6181

@@ -126,29 +146,20 @@ export function TrajectoryPage({
126146
<div className="mx-auto h-full w-full max-w-[1400px] px-4 sm:px-7 lg:px-10">
127147
<Tabs
128148
value={activeTab}
129-
onValueChange={setActiveTab}
149+
onValueChange={handleTabChange}
130150
className="flex h-full min-h-0 flex-col gap-0 overflow-hidden rounded-xl border border-border bg-background/70 backdrop-blur-sm shadow-sm"
131151
>
132152
<div className="border-b border-border bg-background/40 px-3 py-3 sm:px-4">
133153
<TabsList className="grid h-11 w-[300px] max-w-full grid-cols-3 items-stretch gap-1 rounded-xl bg-muted/55 p-1">
134-
<TabsTrigger
135-
value="trajectory"
136-
className="h-full w-full cursor-pointer rounded-lg border-0 py-0 leading-none text-muted-foreground transition-colors hover:bg-primary/10 hover:text-foreground data-[state=active]:bg-primary/18 data-[state=active]:text-foreground data-[state=active]:shadow-none"
137-
>
138-
Trajectory
139-
</TabsTrigger>
140-
<TabsTrigger
141-
value="log"
142-
className="h-full w-full cursor-pointer rounded-lg border-0 py-0 leading-none text-muted-foreground transition-colors hover:bg-primary/10 hover:text-foreground data-[state=active]:bg-primary/18 data-[state=active]:text-foreground data-[state=active]:shadow-none"
143-
>
144-
Log
145-
</TabsTrigger>
146-
<TabsTrigger
147-
value="test"
148-
className="h-full w-full cursor-pointer rounded-lg border-0 py-0 leading-none text-muted-foreground transition-colors hover:bg-primary/10 hover:text-foreground data-[state=active]:bg-primary/18 data-[state=active]:text-foreground data-[state=active]:shadow-none"
149-
>
150-
Test
151-
</TabsTrigger>
154+
{tabsConfig.map((tab) => (
155+
<TabsTrigger
156+
key={tab.value}
157+
value={tab.value}
158+
className="h-full w-full cursor-pointer rounded-lg border-0 py-0 leading-none text-muted-foreground transition-colors hover:bg-primary/10 hover:text-foreground data-[state=active]:bg-primary/18 data-[state=active]:text-foreground data-[state=active]:shadow-none"
159+
>
160+
{tab.label}
161+
</TabsTrigger>
162+
))}
152163
</TabsList>
153164
</div>
154165

site/app/tasks/[name]/[jobId]/trajectory/page.tsx

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import tasksData from "@/zealt/tasks.json";
2-
import { TrajectoryPage } from "./components/trajectory-page";
2+
import { TrajectoryPage, type TabConfig } from "./components/trajectory-page";
33
import zealtConfig from "@/zealt/config.json";
44
import { redirect } from "next/navigation";
55
import { AlertTriangle, Check, ExternalLink, HelpCircle, X as XIcon } from "lucide-react";
6+
import { Suspense } from "react";
67

78

89
type RouteParams = {
@@ -251,6 +252,21 @@ export default async function TrajectoryRoutePage({
251252
}) {
252253
const resolvedParams = await params;
253254

255+
const tabsConfig: TabConfig[] = [
256+
{
257+
value: "trajectory",
258+
label: "Trajectory",
259+
},
260+
{
261+
value: "log",
262+
label: "Log",
263+
},
264+
{
265+
value: "test",
266+
label: "Test",
267+
},
268+
];
269+
254270
const trialEntry = findTrialEntry(resolvedParams.name, resolvedParams.jobId);
255271
const fallbackUrl = trialEntry
256272
? buildFallbackUrl(trialEntry.job_name, trialEntry.trial_name)
@@ -313,12 +329,15 @@ export default async function TrajectoryRoutePage({
313329
</div>
314330
</div>
315331
<div className="min-h-0 flex-1">
316-
<TrajectoryPage
317-
trajectoryUrl={trajectoryUrl}
318-
fallbackUrl={fallbackUrl ?? ''}
319-
stderrLogUrl={stderrLogUrl}
320-
verifierLogUrl={verifierLogUrl}
321-
/>
332+
<Suspense fallback={null}>
333+
<TrajectoryPage
334+
trajectoryUrl={trajectoryUrl}
335+
fallbackUrl={fallbackUrl ?? ''}
336+
stderrLogUrl={stderrLogUrl}
337+
verifierLogUrl={verifierLogUrl}
338+
tabsConfig={tabsConfig}
339+
/>
340+
</Suspense>
322341
</div>
323342
</div>
324343
);

0 commit comments

Comments
 (0)