Skip to content

Commit 6f6e952

Browse files
feat(site): add theme support (light/dark mode)
- Added `next-themes` for theme management. - Created `ThemeProvider` and `ThemeToggle` components. - Integrated theme support into the root layout. - Updated the trajectory page to handle theme-aware iframe rendering. - Improved overall UI styling for the trajectory page. 🤖 Generated with [Pochi](https://getpochi.com) | [Task](https://app.getpochi.com/share/p-e1d7b6023162482cbc9352dc92b945fd) Co-Authored-By: Pochi <noreply@getpochi.com>
1 parent 1285fac commit 6f6e952

File tree

7 files changed

+6424
-36
lines changed

7 files changed

+6424
-36
lines changed

site/app/layout.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import "./globals.css";
33
import type { Metadata } from 'next';
44
import { Inter } from "next/font/google";
55
import zealtConfig from "@/zealt/config.json";
6+
import { ThemeProvider } from "@/components/theme-provider";
7+
import { ThemeToggle } from "@/components/theme-toggle";
68

79
const inter = Inter({ subsets: ["latin"] });
810

@@ -18,8 +20,15 @@ export default function RootLayout({
1820
children: React.ReactNode;
1921
}>) {
2022
return (
21-
<html lang="en" className="dark">
22-
<body className={`${inter.className} antialiased`}>{children}</body>
23+
<html lang="en" suppressHydrationWarning>
24+
<body className={`${inter.className} antialiased`}>
25+
<ThemeProvider defaultTheme="dark">
26+
<div className="fixed right-4 top-6 z-50 sm:top-8">
27+
<ThemeToggle />
28+
</div>
29+
{children}
30+
</ThemeProvider>
31+
</body>
2332
</html>
2433
);
2534
}

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

Lines changed: 34 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use client";
22

3-
import { useEffect, useRef, useState } from "react";
3+
import { useEffect, useMemo, useState } from "react";
4+
import { useTheme } from "next-themes";
45
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
56
import { ScrollArea } from "@/components/ui/scroll-area";
67
import { Skeleton } from "@/components/ui/skeleton";
@@ -18,31 +19,33 @@ export function TrajectoryPage({
1819
stderrText,
1920
verifierText,
2021
}: TrajectoryPageProps) {
22+
const { resolvedTheme } = useTheme();
23+
const [mounted, setMounted] = useState(false);
2124
const [iframeLoading, setIframeLoading] = useState(true);
2225
const [activeTab, setActiveTab] = useState("trajectory");
23-
const iframeRef = useRef<HTMLIFrameElement>(null);
24-
const hideSkeletonTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
26+
27+
const iframeTheme = mounted && resolvedTheme === "light" ? "light" : "dark";
28+
29+
const iframeUrl = useMemo(() => {
30+
const url = new URL(trajectoryUrl);
31+
url.searchParams.set("theme", iframeTheme);
32+
return url.toString();
33+
}, [trajectoryUrl, iframeTheme]);
2534

2635
useEffect(() => {
27-
return () => {
28-
if (hideSkeletonTimeoutRef.current) {
29-
clearTimeout(hideSkeletonTimeoutRef.current);
30-
}
31-
};
36+
setMounted(true);
3237
}, []);
3338

34-
const handleIframeLoad = () => {
35-
if (hideSkeletonTimeoutRef.current) {
36-
clearTimeout(hideSkeletonTimeoutRef.current);
39+
useEffect(() => {
40+
if (!mounted) {
41+
return;
3742
}
3843

39-
hideSkeletonTimeoutRef.current = setTimeout(() => {
40-
setIframeLoading(false);
41-
if (iframeRef.current) {
42-
iframeRef.current.style.opacity = "1";
43-
}
44-
hideSkeletonTimeoutRef.current = null;
45-
}, 300);
44+
setIframeLoading(true);
45+
}, [iframeUrl, mounted]);
46+
47+
const handleIframeLoad = () => {
48+
setIframeLoading(false);
4649
};
4750

4851
const handleIframeError = () => {
@@ -93,19 +96,20 @@ export function TrajectoryPage({
9396
</div>
9497

9598
<TabsContent value="trajectory" className="relative min-h-0 flex-1 overflow-hidden px-2" forceMount>
96-
{iframeLoading && (
97-
<div className="absolute inset-0 z-10 overflow-auto bg-background/80 backdrop-blur-sm">
98-
<TrajectorySkeleton />
99-
</div>
99+
<div
100+
className={`absolute inset-0 z-10 overflow-auto bg-background/80 backdrop-blur-sm transition-opacity duration-500 delay-150 ${!mounted || iframeLoading ? "opacity-100" : "pointer-events-none opacity-0"}`}
101+
>
102+
<TrajectorySkeleton />
103+
</div>
104+
{mounted && (
105+
<iframe
106+
src={iframeUrl}
107+
className={`h-full w-full border-0 transition-opacity duration-300 ${iframeLoading ? "opacity-0" : "opacity-100"}`}
108+
title="Trial Details"
109+
onLoad={handleIframeLoad}
110+
onError={handleIframeError}
111+
/>
100112
)}
101-
<iframe
102-
ref={iframeRef}
103-
src={trajectoryUrl}
104-
className="h-full w-full border-0 opacity-0 transition-opacity duration-300"
105-
title="Trial Details"
106-
onLoad={handleIframeLoad}
107-
onError={handleIframeError}
108-
/>
109113
</TabsContent>
110114

111115
<TabsContent value="log" className="min-h-0 flex-1 overflow-hidden" forceMount>

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

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,6 @@ function buildClipUrl(jobName: string, trialName: string, title: string): string
164164
const ownerRepo = getGithubOwnerRepo();
165165
const url = new URL(`/f/raw.githubusercontent.com/${ownerRepo}/refs/heads/main/jobs/${jobName}/${trialName}/agent/pochi/trajectory.jsonl`, getServerBaseUrl());
166166
url.searchParams.set("title", title);
167-
url.searchParams.set("theme", "dark");
168167
return url.toString();
169168
}
170169

@@ -288,10 +287,10 @@ export default async function TrajectoryRoutePage({
288287
<div className="flex h-screen w-full flex-col overflow-hidden bg-background text-foreground font-sans selection:bg-primary/20">
289288
<div className="fixed inset-0 -z-10 h-full w-full bg-background bg-[radial-gradient(#2a2a2a_1px,transparent_1px)] [background-size:16px_16px] [mask-image:radial-gradient(ellipse_50%_50%_at_50%_50%,#000_70%,transparent_100%)] opacity-20 dark:opacity-40"></div>
290289
<div className="z-40 shrink-0 bg-background/85 backdrop-blur-sm">
291-
<div className="mx-auto w-full max-w-[1400px] px-4 py-4 sm:px-7 lg:px-10">
290+
<div className="mx-auto w-full max-w-[1400px] px-4 py-6 sm:px-7 sm:py-8 lg:px-10">
292291
<div className="flex flex-col gap-1">
293-
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between sm:gap-3">
294-
<h1 className="min-w-0 truncate whitespace-nowrap font-bold text-2xl sm:flex-1 sm:pr-4">
292+
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-start sm:gap-2">
293+
<h1 className="min-w-0 truncate whitespace-nowrap font-bold text-2xl">
295294
{headerTitle}
296295
</h1>
297296
<a

site/components/theme-provider.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"use client";
2+
3+
import type { ReactNode } from "react";
4+
import { ThemeProvider as NextThemesProvider } from "next-themes";
5+
6+
export type ThemeMode = "light" | "dark" | "system";
7+
8+
export function ThemeProvider({
9+
children,
10+
defaultTheme = "dark",
11+
}: {
12+
children: ReactNode;
13+
defaultTheme?: ThemeMode;
14+
}) {
15+
return (
16+
<NextThemesProvider
17+
attribute="class"
18+
defaultTheme={defaultTheme}
19+
enableSystem
20+
// Keep iframe transparency rendering stable across theme switches.
21+
enableColorScheme={false}
22+
disableTransitionOnChange
23+
>
24+
{children}
25+
</NextThemesProvider>
26+
);
27+
}

site/components/theme-toggle.tsx

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
"use client";
2+
3+
import * as React from "react";
4+
import { Check, Monitor, Moon, Sun } from "lucide-react";
5+
import { useTheme } from "next-themes";
6+
7+
import { Button } from "@/components/ui/button";
8+
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
9+
import { cn } from "@/lib/utils";
10+
11+
type ThemeMode = "light" | "dark" | "system";
12+
13+
const themeOptions: Array<{ value: ThemeMode; label: string }> = [
14+
{ value: "light", label: "Light" },
15+
{ value: "dark", label: "Dark" },
16+
{ value: "system", label: "System" },
17+
];
18+
19+
function ThemeIcon({ theme }: { theme: ThemeMode }) {
20+
if (theme === "light") {
21+
return <Sun className="h-4 w-4" />;
22+
}
23+
if (theme === "dark") {
24+
return <Moon className="h-4 w-4" />;
25+
}
26+
return <Monitor className="h-4 w-4" />;
27+
}
28+
29+
export function ThemeToggle() {
30+
const { theme, setTheme } = useTheme();
31+
const [open, setOpen] = React.useState(false);
32+
const [mounted, setMounted] = React.useState(false);
33+
34+
React.useEffect(() => {
35+
setMounted(true);
36+
}, []);
37+
38+
const activeTheme = (mounted ? theme : "dark") as ThemeMode;
39+
40+
return (
41+
<Popover open={open} onOpenChange={setOpen}>
42+
<PopoverTrigger asChild>
43+
<Button
44+
type="button"
45+
variant="outline"
46+
size="icon-sm"
47+
aria-label="Switch theme"
48+
className="size-8 rounded-full border-border/70 bg-background/90 shadow-sm backdrop-blur supports-backdrop-filter:bg-background/70"
49+
>
50+
<ThemeIcon theme={activeTheme} />
51+
<span className="sr-only">Current theme: {activeTheme}</span>
52+
</Button>
53+
</PopoverTrigger>
54+
<PopoverContent align="end" className="w-48 rounded-xl border-border/70 bg-popover/95 p-2 shadow-xl">
55+
<div className="flex flex-col gap-1">
56+
{themeOptions.map((option) => {
57+
const active = mounted && activeTheme === option.value;
58+
return (
59+
<button
60+
key={option.value}
61+
type="button"
62+
onClick={() => {
63+
setTheme(option.value);
64+
setOpen(false);
65+
}}
66+
className={cn(
67+
"flex items-center justify-between rounded-lg border px-2.5 py-2 text-sm transition-colors",
68+
active
69+
? "border-primary/30 bg-primary/10 text-foreground"
70+
: "border-transparent hover:border-border/70 hover:bg-accent/70 hover:text-accent-foreground"
71+
)}
72+
aria-label={`Use ${option.label} theme`}
73+
>
74+
<span className="flex items-center gap-2">
75+
<ThemeIcon theme={option.value} />
76+
<span>{option.label}</span>
77+
</span>
78+
<Check className={cn("h-4 w-4", active ? "opacity-100" : "opacity-0")} />
79+
</button>
80+
);
81+
})}
82+
</div>
83+
</PopoverContent>
84+
</Popover>
85+
);
86+
}

0 commit comments

Comments
 (0)