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
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,31 @@

import { useEffect, useRef, useState } from "react";
import { Loader2 } from "lucide-react";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { ScrollArea } from "@/components/ui/scroll-area";

type TrajectoryPageProps = {
title: string;
trajectoryUrl: string | null;
trajectoryUrl: string;
fallbackUrl: string;
stderrText: string | null;
verifierText: string | null;
topOffsetClassName?: string;
};

export function TrajectoryPage({ title, trajectoryUrl, fallbackUrl }: TrajectoryPageProps) {
const [iframeLoading, setIframeLoading] = useState(true);
export function TrajectoryPage({
trajectoryUrl,
fallbackUrl,
stderrText,
verifierText,
topOffsetClassName = "top-28",
}: TrajectoryPageProps) {
const [iframeLoading, setIframeLoading] = useState(false);
const [activeTab, setActiveTab] = useState("trajectory");
const iframeRef = useRef<HTMLIFrameElement>(null);

useEffect(() => {
if (!trajectoryUrl) {
if (fallbackUrl) {
window.location.replace(fallbackUrl);
}
return;
}

setIframeLoading(true);
}, [trajectoryUrl, fallbackUrl]);
}, [trajectoryUrl]);

const handleIframeLoad = () => {
setIframeLoading(false);
Expand All @@ -34,44 +38,92 @@ export function TrajectoryPage({ title, trajectoryUrl, fallbackUrl }: Trajectory
};

const handleIframeError = () => {
if (fallbackUrl) {
window.location.replace(fallbackUrl);
window.location.replace(fallbackUrl);
};

const renderLogContent = (text: string | null, emptyMessage: string) => {
if (!text) {
return <p className="text-sm text-muted-foreground">{emptyMessage}</p>;
}
}

if (trajectoryUrl) {
return (
<div className="fixed inset-0 w-full h-full">
{iframeLoading && (
<div className="flex flex-col items-center justify-center h-full space-y-6">
<div className="relative flex items-center justify-center">
<Loader2 className="w-12 h-12 text-primary animate-spin relative z-10" />
</div>
<div className="space-y-2 text-center">
<h2 className="text-lg font-semibold tracking-tight text-foreground">Loading</h2>
</div>
</div>
)}
<iframe
ref={iframeRef}
src={trajectoryUrl}
className="fixed inset-0 w-full h-full border-0 opacity-0"
title={title || "Trial Details"}
onLoad={handleIframeLoad}
onError={handleIframeError}
/>
</div>
<pre className="w-max min-w-full whitespace-pre font-mono text-xs leading-5 text-foreground/95">
{text}
</pre>
);
}
};

return (
<div className="flex flex-col items-center justify-center h-screen space-y-6">
<div className="relative flex items-center justify-center">
<Loader2 className="w-12 h-12 text-primary animate-spin relative z-10" />
</div>
<div className="space-y-2 text-center">
<h2 className="text-lg font-semibold tracking-tight text-foreground">Loading</h2>
<div className="fixed inset-0 w-full h-full">
<div className={`absolute inset-x-0 ${topOffsetClassName} bottom-4 sm:bottom-6`}>
<div className="mx-auto h-full w-full max-w-[1400px] px-4 sm:px-7 lg:px-10">
<Tabs
value={activeTab}
onValueChange={setActiveTab}
className="h-full gap-0 overflow-hidden rounded-xl border border-border/60 bg-background/70 backdrop-blur-sm shadow-sm"
>
<div className="border-b border-border/50 bg-background/50 px-3 py-3 sm:px-4">
<TabsList className="grid h-11 w-[300px] max-w-full grid-cols-3 items-stretch gap-1 border border-border/40 bg-background/60 p-1">
<TabsTrigger
value="trajectory"
className="h-full w-full cursor-pointer border border-transparent py-0 leading-none text-muted-foreground transition-colors hover:bg-primary/10 hover:text-foreground data-[state=active]:border-primary/35 data-[state=active]:bg-primary/18 data-[state=active]:text-foreground data-[state=active]:shadow-none"
>
Trajectory
</TabsTrigger>
<TabsTrigger
value="log"
className="h-full w-full cursor-pointer border border-transparent py-0 leading-none text-muted-foreground transition-colors hover:bg-primary/10 hover:text-foreground data-[state=active]:border-primary/35 data-[state=active]:bg-primary/18 data-[state=active]:text-foreground data-[state=active]:shadow-none"
>
Log
</TabsTrigger>
<TabsTrigger
value="test"
className="h-full w-full cursor-pointer border border-transparent py-0 leading-none text-muted-foreground transition-colors hover:bg-primary/10 hover:text-foreground data-[state=active]:border-primary/35 data-[state=active]:bg-primary/18 data-[state=active]:text-foreground data-[state=active]:shadow-none"
>
Test
</TabsTrigger>
</TabsList>
</div>

<TabsContent value="trajectory" className="relative min-h-0 overflow-hidden" forceMount>
{iframeLoading && (
<div className="absolute inset-0 z-10 flex flex-col items-center justify-center space-y-6 bg-background/80 backdrop-blur-sm">
<div className="relative flex items-center justify-center">
<Loader2 className="relative z-10 h-12 w-12 animate-spin text-primary" />
</div>
<div className="space-y-2 text-center">
<h2 className="text-lg font-semibold tracking-tight text-foreground">Loading</h2>
</div>
</div>
)}
<iframe
ref={iframeRef}
src={trajectoryUrl}
className="h-full w-full border-0 opacity-0 transition-opacity duration-300"
title="Trial Details"
onLoad={handleIframeLoad}
onError={handleIframeError}
/>
</TabsContent>

<TabsContent value="log" className="min-h-0 overflow-hidden" forceMount>
<ScrollArea className="h-full w-full">
<div className="px-3 pb-4 pt-2 sm:px-4 sm:pb-5 sm:pt-3">
{renderLogContent(stderrText, "No stderr content available for this trial.")}
</div>
</ScrollArea>
</TabsContent>

<TabsContent value="test" className="min-h-0 overflow-hidden" forceMount>
<ScrollArea className="h-full w-full">
<div className="px-3 pb-4 pt-2 sm:px-4 sm:pb-5 sm:pt-3">
{renderLogContent(verifierText, "No verifier test output available for this trial.")}
</div>
</ScrollArea>
</TabsContent>
</Tabs>
</div>
</div>
</div>
);
}
}
185 changes: 179 additions & 6 deletions site/app/tasks/[name]/[jobId]/trajectory/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import type { CSSProperties } from "react";
import tasksData from "@/tasks.json";
import { TrajectoryPage } from "./components/trajectory-page";
import zealtConfig from "@/../zealt.json";
import { redirect } from "next/navigation";
import { AlertTriangle, Check, HelpCircle, X as XIcon } from "lucide-react";


type RouteParams = {
Expand All @@ -11,9 +14,123 @@ type RouteParams = {
type TrialEntry = {
trial_name: string;
job_name: string;
agent: string;
passed?: boolean;
error?: boolean;
latency_sec?: number | null;
latency_breakdown?: {
env_setup?: number | null;
agent_setup?: number | null;
agent_exec?: number | null;
verifier?: number | null;
};
trajectory_id?: string;
stderr_text?: string | null;
stderr_line_count?: number;
verifier_text?: string | null;
verifier_line_count?: number;
};

function formatStartTime(jobName: string): string {
const match = jobName.match(/^(\d{4})-(\d{2})-(\d{2})__(\d{2})-(\d{2})-(\d{2})$/);
if (!match) {
return "Unknown";
}

const [, year, month, day, hour, minute, second] = match;
const localDate = new Date(Number(year), Number(month) - 1, Number(day), Number(hour), Number(minute), Number(second));
if (Number.isNaN(localDate.getTime())) {
return "Unknown";
}

return localDate.toLocaleString();
}

function formatStartTimeShort(jobName: string): string {
const match = jobName.match(/^(\d{4})-(\d{2})-(\d{2})__(\d{2})-(\d{2})-(\d{2})$/);
if (!match) {
return "Unknown";
}

const [, year, month, day, hour, minute, second] = match;
const localDate = new Date(Number(year), Number(month) - 1, Number(day), Number(hour), Number(minute), Number(second));
if (Number.isNaN(localDate.getTime())) {
return "Unknown";
}

const monthLabel = String(localDate.getMonth() + 1);
const dayLabel = String(localDate.getDate());
const hourLabel = String(localDate.getHours()).padStart(2, "0");
const minuteLabel = String(localDate.getMinutes()).padStart(2, "0");
return `${monthLabel}/${dayLabel} ${hourLabel}:${minuteLabel}`;
}

function formatDuration(durationSec: number | null | undefined): string {
if (durationSec == null || Number.isNaN(durationSec)) {
return "Unknown";
}

if (durationSec < 60) {
return `${durationSec.toFixed(1)}s`;
}

const minutes = Math.floor(durationSec / 60);
const seconds = durationSec % 60;
return `${minutes}m ${seconds.toFixed(1)}s`;
}

function getTrialStatus(trial: TrialEntry | null): "error" | "passed" | "failed" | "unknown" {
if (!trial) {
return "unknown";
}

if (trial.error) {
return "error";
}

if (trial.passed) {
return "passed";
}

if (trial.passed === false) {
return "failed";
}

return "unknown";
}

function getStatusMeta(status: "error" | "passed" | "failed" | "unknown") {
if (status === "error") {
return {
label: "Error",
className: "text-red-400",
Icon: AlertTriangle,
};
}

if (status === "passed") {
return {
label: "Passed",
className: "text-emerald-400",
Icon: Check,
};
}

if (status === "failed") {
return {
label: "Failed",
className: "text-amber-400",
Icon: XIcon,
};
}

return {
label: "Unknown",
className: "text-muted-foreground",
Icon: HelpCircle,
};
}

function buildFallbackUrl(jobName: string, trialName: string) {
return `${zealtConfig.github_repo}/blob/main/jobs/${jobName}/${trialName}/result.json`
}
Expand All @@ -40,7 +157,7 @@ function getGithubOwnerRepo(): string {
return match ? match[1] : repoUrl;
}

function buildClipUrl(clipId: string, jobName: string, trialName: string, title: string): string {
function buildClipUrl(jobName: string, trialName: string, title: string): string {
const ownerRepo = getGithubOwnerRepo();
const url = new URL(`/f/raw.githubusercontent.com/${ownerRepo}/refs/heads/main/jobs/${jobName}/${trialName}/agent/pochi/trajectory.jsonl`, getServerBaseUrl());
url.searchParams.set("title", title);
Expand All @@ -54,7 +171,11 @@ function isTrialEntry(value: unknown): value is TrialEntry {
}

const trial = value as Record<string, unknown>;
if (typeof trial.trial_name !== "string" || typeof trial.job_name !== "string") {
if (
typeof trial.trial_name !== "string" ||
typeof trial.job_name !== "string" ||
typeof trial.agent !== "string"
) {
return false;
}

Expand Down Expand Up @@ -138,17 +259,69 @@ export default async function TrajectoryRoutePage({
? buildFallbackUrl(trialEntry.job_name, trialEntry.trial_name)
: null;
const clipId = trialEntry?.trajectory_id?.trim() || null;
const headerTitle = `${resolvedParams.name}__${resolvedParams.jobId}`;
const startedAt = trialEntry ? formatStartTime(trialEntry.job_name) : "Unknown";
const startedAtShort = trialEntry ? formatStartTimeShort(trialEntry.job_name) : "Unknown";
const executionDurationLabel = formatDuration(trialEntry?.latency_breakdown?.agent_exec ?? null);
const verifyDurationLabel = formatDuration(trialEntry?.latency_breakdown?.verifier ?? null);
const trialStatus = getTrialStatus(trialEntry);
const statusMeta = getStatusMeta(trialStatus);
const StatusIcon = statusMeta.Icon;
const contentTopOffsetClassName = "top-36 sm:top-32 lg:top-28";

const trajectoryUrl = clipId && trialEntry
? buildClipUrl(clipId, trialEntry.job_name, trialEntry.trial_name, resolvedParams.name)
? buildClipUrl(trialEntry.job_name, trialEntry.trial_name, resolvedParams.name)
: null;

// FIXME
if (!trajectoryUrl) {
redirect(fallbackUrl ?? '/tasks');
}
const stderrText = trialEntry?.stderr_text ?? null;
const verifierText = trialEntry?.verifier_text ?? null;
const pageThemeVars = {
"--background": "oklch(0.268 0.004 106.643)",
"--border": "oklch(0.362 0.01 106.893)",
} as CSSProperties;

return (
<div className="w-full h-screen bg-background text-foreground font-sans selection:bg-primary/20 overflow-hidden">
<div style={pageThemeVars} className="w-full h-screen bg-background text-foreground font-sans selection:bg-primary/20 overflow-hidden">
<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>
<div className="fixed inset-x-0 top-0 z-40 border-b border-border/50 bg-background/85 backdrop-blur-sm">
<div className="mx-auto w-full max-w-[1400px] px-4 py-4 sm:px-7 lg:px-10">
<div className="flex flex-col gap-1">
<h1 className="truncate whitespace-nowrap font-bold text-2xl">
{headerTitle}
</h1>
<div className="mt-2 text-xs sm:text-sm">
<div className="grid grid-cols-2 gap-x-4 gap-y-1.5 sm:hidden">
<div className={`font-medium ${statusMeta.className}`}>
Status: {statusMeta.label}
</div>
<div className="text-muted-foreground">Started: {startedAtShort}</div>
<div className="text-muted-foreground">Execution: {executionDurationLabel}</div>
<div className="text-muted-foreground">Test: {verifyDurationLabel}</div>
</div>

<div className="hidden items-center gap-4 sm:flex">
<span className={`inline-flex shrink-0 items-center gap-1.5 font-medium ${statusMeta.className}`}>
<StatusIcon className="h-3.5 w-3.5" />
<span>Status: {statusMeta.label}</span>
</span>
<span className="text-muted-foreground">Started: {startedAt}</span>
<span className="text-muted-foreground truncate">Execution: {executionDurationLabel}</span>
<span className="text-muted-foreground truncate">Test: {verifyDurationLabel}</span>
</div>
</div>
</div>
</div>
</div>
<TrajectoryPage
title={resolvedParams.name}
trajectoryUrl={trajectoryUrl}
fallbackUrl={fallbackUrl ?? ""}
fallbackUrl={fallbackUrl ?? ''}
stderrText={stderrText}
verifierText={verifierText}
topOffsetClassName={contentTopOffsetClassName}
/>
</div>
);
Expand Down
Loading
Loading