Skip to content

Total time and action timestamps #16

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jun 4, 2025
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
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"postcss": "^8.4.35",
"tailwind-scrollbar": "^3.0.5",
"tailwindcss": "^3.4.1",
"typescript": "^5.4.2",
"typescript": "^5.8.3",
"vite": "^6.2.1",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^3.0.8"
Expand Down
117 changes: 78 additions & 39 deletions src/components/jsonl-viewer/JsonlViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { JsonlEntry, parseJsonlFile } from '../../utils/jsonl-parser';
import JsonlViewerSettings, { JsonlViewerSettings as JsonlViewerSettingsType } from './JsonlViewerSettings';
import { getNestedValue, formatValueForDisplay } from '../../utils/object-utils';
import { TrajectoryItem } from '../../types/share';
import { TrajectoryHistoryEntry } from '../../types/trajectory';
import JsonVisualizer from '../json-visualizer/JsonVisualizer';
import { DEFAULT_JSONL_VIEWER_SETTINGS } from '../../config/jsonl-viewer-config';
import {
Expand Down Expand Up @@ -50,7 +51,7 @@ const JsonlViewer: React.FC<JsonlViewerProps> = ({ content }) => {
const [entries, setEntries] = useState<JsonlEntry[]>([]);
const [currentEntryIndex, setCurrentEntryIndex] = useState<number>(0);
const [error, setError] = useState<string | null>(null);
const [trajectoryItems, setTrajectoryItems] = useState<TrajectoryItem[]>([]);
const [trajectoryItems, setTrajectoryItems] = useState<TrajectoryHistoryEntry[]>([]);
const [settings, setSettings] = useState<JsonlViewerSettingsType>(DEFAULT_JSONL_VIEWER_SETTINGS);
const [originalEntries, setOriginalEntries] = useState<JsonlEntry[]>([]);

Expand All @@ -67,7 +68,7 @@ const JsonlViewer: React.FC<JsonlViewerProps> = ({ content }) => {
if (parsedEntries.length > 0) {
const currentEntry = parsedEntries[0];
if (currentEntry.history && Array.isArray(currentEntry.history)) {
setTrajectoryItems(currentEntry.history as TrajectoryItem[]);
setTrajectoryItems(currentEntry.history);
}
}
} catch (err) {
Expand All @@ -86,6 +87,13 @@ const JsonlViewer: React.FC<JsonlViewerProps> = ({ content }) => {

// Create a copy of the entries to sort
const sortedEntries = [...entriesToSort].sort((a, b) => {
// Special handling for duration sorting
if (currentSettings.sortField === 'duration') {
const durationA = a.history && Array.isArray(a.history) ? calculateDurationMs(a.history) : 0;
const durationB = b.history && Array.isArray(b.history) ? calculateDurationMs(b.history) : 0;
return currentSettings.sortDirection === 'asc' ? durationA - durationB : durationB - durationA;
}

// Get values using the sort field
const valueA = getNestedValue(a, currentSettings.sortField, null);
const valueB = getNestedValue(b, currentSettings.sortField, null);
Expand Down Expand Up @@ -135,7 +143,7 @@ const JsonlViewer: React.FC<JsonlViewerProps> = ({ content }) => {
if (entries.length > 0 && index < entries.length) {
const selectedEntry = entries[index];
if (selectedEntry.history && Array.isArray(selectedEntry.history)) {
setTrajectoryItems(selectedEntry.history as TrajectoryItem[]);
setTrajectoryItems(selectedEntry.history);
} else {
setTrajectoryItems([]);
}
Expand All @@ -149,6 +157,32 @@ const JsonlViewer: React.FC<JsonlViewerProps> = ({ content }) => {
return `Entry ${index + 1}`;
};

// Helper function to calculate duration in milliseconds
const calculateDurationMs = (history: TrajectoryHistoryEntry[]): number => {
if (!history || history.length === 0 || !history[0].timestamp) return 0;

const startTime = new Date(history[0].timestamp || new Date());
const endTime = new Date(history[history.length - 1].timestamp || new Date());
return endTime.getTime() - startTime.getTime();
};

// Helper function to format duration
const formatDuration = (durationMs: number): string => {
const durationSec = Math.round(durationMs / 1000);
const durationMin = Math.floor(durationSec / 60);
const remainingSec = durationSec % 60;

return durationMin > 0 ?
`${durationMin}m ${remainingSec}s` :
`${remainingSec}s`;
};

// Helper function to calculate duration string
const calculateDuration = (history: TrajectoryHistoryEntry[]): string | null => {
const durationMs = calculateDurationMs(history);
return durationMs > 0 ? formatDuration(durationMs) : null;
};

// Get a summary of the entry for the sidebar
const getEntrySummary = (entry: JsonlEntry): React.ReactNode => {
// If we have custom display fields, use those
Expand All @@ -157,7 +191,7 @@ const JsonlViewer: React.FC<JsonlViewerProps> = ({ content }) => {
<div className="space-y-1">
{settings.displayFields.map((field, idx) => {
const value = getNestedValue(entry, field, null);
const displayValue = formatValueForDisplay(value);
const displayValue = formatValueForDisplay(value, field);

// Format the field name for display
let displayField = field;
Expand Down Expand Up @@ -211,14 +245,14 @@ const JsonlViewer: React.FC<JsonlViewerProps> = ({ content }) => {
}, [entries, currentEntryIndex]);

// Function to filter out unwanted trajectory items
const shouldDisplayItem = (item: TrajectoryItem): boolean => {
const shouldDisplayItem = (item: TrajectoryHistoryEntry): boolean => {
// Filter out change_agent_state actions
if ("action" in item && item.action === "change_agent_state" as const) {
if (item.action === "change_agent_state") {
return false;
}

// Filter out null observations
if ("observation" in item && typeof item.observation === "string" && item.observation === "null") {
if (item.observation === "null") {
return false;
}

Expand Down Expand Up @@ -276,8 +310,12 @@ const JsonlViewer: React.FC<JsonlViewerProps> = ({ content }) => {
<div className="h-full flex flex-col border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 shadow-sm overflow-hidden">
{/* Timeline Header - fixed */}
<div className="flex-none h-10 px-3 py-2 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
<h3 className="text-sm font-medium text-gray-900 dark:text-white">
Trajectory ({filteredTrajectoryItems.length} steps)
<h3 className="text-sm font-medium text-gray-900 dark:text-white flex items-center">
<span>Trajectory ({filteredTrajectoryItems.length} steps)</span>
{(() => {
const duration = calculateDuration(filteredTrajectoryItems);
return duration && <span className="text-gray-500 dark:text-gray-400 ml-2">- {duration}</span>;
})()}
</h3>
<div className="text-xs text-gray-500 dark:text-gray-400">
{entries[currentEntryIndex] && (
Expand All @@ -293,36 +331,37 @@ const JsonlViewer: React.FC<JsonlViewerProps> = ({ content }) => {
{filteredTrajectoryItems.length > 0 ? (
<div className="flex flex-col items-center gap-4">
{filteredTrajectoryItems.map((item, index) => {
if (isAgentStateChange(item)) {
return <AgentStateChangeComponent key={index} state={item} />;
} else if (isUserMessage(item)) {
return <UserMessageComponent key={index} message={item} />;
} else if (isAssistantMessage(item)) {
return <AssistantMessageComponent key={index} message={item} />;
} else if (isCommandAction(item)) {
return <CommandActionComponent key={index} command={item} />;
} else if (isCommandObservation(item)) {
return <CommandObservationComponent key={index} observation={item} />;
} else if (isIPythonAction(item)) {
return <IPythonActionComponent key={index} action={item} />;
} else if (isIPythonObservation(item)) {
return <IPythonObservationComponent key={index} observation={item} />;
} else if (isFinishAction(item)) {
return <FinishActionComponent key={index} action={item} />;
} else if (isErrorObservation(item)) {
return <ErrorObservationComponent key={index} observation={item} />;
} else if (isReadAction(item)) {
return <ReadActionComponent key={index} item={item} />;
} else if (isReadObservation(item)) {
return <ReadObservationComponent key={index} observation={item} />;
} else if (isEditAction(item)) {
return <EditActionComponent key={index} item={item} />;
} else if (isEditObservation(item)) {
return <EditObservationComponent key={index} observation={item} />;
} else if (isThinkAction(item)) {
return <ThinkActionComponent key={index} action={item} />;
} else if (isThinkObservation(item)) {
return <ThinkObservationComponent key={index} observation={item} />;
const trajectoryItem = item as unknown as TrajectoryItem;
if (isAgentStateChange(trajectoryItem)) {
return <AgentStateChangeComponent key={index} state={trajectoryItem as any} />;
} else if (isUserMessage(trajectoryItem)) {
return <UserMessageComponent key={index} message={trajectoryItem as any} />;
} else if (isAssistantMessage(trajectoryItem)) {
return <AssistantMessageComponent key={index} message={trajectoryItem as any} />;
} else if (isCommandAction(trajectoryItem)) {
return <CommandActionComponent key={index} command={trajectoryItem as any} />;
} else if (isCommandObservation(trajectoryItem)) {
return <CommandObservationComponent key={index} observation={trajectoryItem as any} />;
} else if (isIPythonAction(trajectoryItem)) {
return <IPythonActionComponent key={index} action={trajectoryItem as any} />;
} else if (isIPythonObservation(trajectoryItem)) {
return <IPythonObservationComponent key={index} observation={trajectoryItem as any} />;
} else if (isFinishAction(trajectoryItem)) {
return <FinishActionComponent key={index} action={trajectoryItem as any} />;
} else if (isErrorObservation(trajectoryItem)) {
return <ErrorObservationComponent key={index} observation={trajectoryItem as any} />;
} else if (isReadAction(trajectoryItem)) {
return <ReadActionComponent key={index} item={trajectoryItem as any} />;
} else if (isReadObservation(trajectoryItem)) {
return <ReadObservationComponent key={index} observation={trajectoryItem as any} />;
} else if (isEditAction(trajectoryItem)) {
return <EditActionComponent key={index} item={trajectoryItem as any} />;
} else if (isEditObservation(trajectoryItem)) {
return <EditObservationComponent key={index} observation={trajectoryItem as any} />;
} else if (isThinkAction(trajectoryItem)) {
return <ThinkActionComponent key={index} action={trajectoryItem as any} />;
} else if (isThinkObservation(trajectoryItem)) {
return <ThinkObservationComponent key={index} observation={trajectoryItem as any} />;
} else {
return (
<TrajectoryCard key={index}>
Expand Down
19 changes: 14 additions & 5 deletions src/components/share/trajectory-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ interface TrajectoryTempateProps {
className?: React.HTMLAttributes<HTMLDivElement>["className"];
originalJson?: any;
defaultCollapsed?: boolean;
timestamp?: string;
}

interface TrajectoryCardType extends React.FC<TrajectoryTempateProps> {
Header: React.FC<TrajectoryCardHeaderProps>;
Body: React.FC<TrajectoryCardBodyProps>;
}

export const TrajectoryCard: TrajectoryCardType = ({ children, className, originalJson, defaultCollapsed = false }) => {
export const TrajectoryCard: TrajectoryCardType = ({ children, className, originalJson, defaultCollapsed = false, timestamp }) => {
const [isModalOpen, setIsModalOpen] = useState(false);
const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed);

Expand Down Expand Up @@ -45,7 +46,9 @@ export const TrajectoryCard: TrajectoryCardType = ({ children, className, origin
className="flex items-center justify-between cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700/30 transition-colors"
onClick={() => setIsCollapsed(!isCollapsed)}
>
<div className="flex-grow">{header}</div>
<div className="flex-grow">
{React.isValidElement(header) && React.cloneElement(header, { timestamp })}
</div>
<button
className="px-2 py-1 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300"
aria-label={isCollapsed ? "Expand" : "Collapse"}
Expand Down Expand Up @@ -99,17 +102,23 @@ export const TrajectoryCard: TrajectoryCardType = ({ children, className, origin
interface TrajectoryCardHeaderProps {
children: React.ReactNode;
className?: React.HTMLAttributes<HTMLDivElement>["className"];
timestamp?: string;
}

const TrajectoryCardHeader: React.FC<TrajectoryCardHeaderProps> = ({ children, className }) => {
const TrajectoryCardHeader: React.FC<TrajectoryCardHeaderProps> = ({ children, className, timestamp }) => {
return (
<div
className={clsx(
"rounded-t-md py-1 px-2 font-medium text-[10px]",
"rounded-t-md py-1 px-2 font-medium text-[10px] flex justify-between items-center",
className,
)}
>
{children}
<div>{children}</div>
{timestamp && (
<div className="text-[9px] opacity-80">
{new Date(timestamp).toLocaleString()}
</div>
)}
</div>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const AgentStateChangeComponent: React.FC<AgentStateChangeProps> = ({ sta
<TrajectoryCard
className="bg-gray-50 dark:bg-gray-800/30 border border-gray-200 dark:border-gray-700"
originalJson={state}
timestamp={state.timestamp}
>
<TrajectoryCard.Header className="bg-gray-100 dark:bg-gray-700/50 text-gray-700 dark:text-gray-200">Agent State: {stateText}</TrajectoryCard.Header>
<TrajectoryCard.Body>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const AssistantMessageComponent: React.FC<AssistantMessageProps> = ({ mes
<TrajectoryCard
className="bg-purple-50 dark:bg-purple-900/10 border border-purple-200 dark:border-purple-800"
originalJson={message}
timestamp={message.timestamp}
>
<TrajectoryCard.Header className="bg-purple-100 dark:bg-purple-800/50 text-purple-800 dark:text-purple-100">Assistant Message</TrajectoryCard.Header>
<TrajectoryCard.Body>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const CommandActionComponent: React.FC<CommandActionProps> = ({ command }
<TrajectoryCard
className="bg-green-50 dark:bg-green-900/10 border border-green-200 dark:border-green-800"
originalJson={command}
timestamp={command.timestamp}
>
<TrajectoryCard.Header className="bg-green-100 dark:bg-green-800/50 text-green-800 dark:text-green-100">Assistant Shell Action</TrajectoryCard.Header>
<TrajectoryCard.Body>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const CommandObservationComponent: React.FC<CommandObservationProps> = ({
<TrajectoryCard
className="bg-gray-50 dark:bg-gray-800/30 border border-gray-200 dark:border-gray-700"
originalJson={observation}
timestamp={observation.timestamp}
>
<TrajectoryCard.Header className="bg-gray-100 dark:bg-gray-700/50 text-gray-700 dark:text-gray-200">Shell Output</TrajectoryCard.Header>
<TrajectoryCard.Body>
Expand Down
1 change: 1 addition & 0 deletions src/components/share/trajectory-list-items/edit-action.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const EditActionComponent: React.FC<EditActionProps> = ({ item }) => {
<TrajectoryCard
className="bg-orange-50 dark:bg-orange-900/10 border border-orange-200 dark:border-orange-800"
originalJson={item}
timestamp={item.timestamp}
>
<TrajectoryCard.Header className="bg-orange-100 dark:bg-orange-800/50 text-orange-800 dark:text-orange-100">
<div className="flex justify-between items-center w-full">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export const EditObservationComponent: React.FC<EditObservationProps> = ({ obser
<TrajectoryCard
className="bg-gray-50 dark:bg-gray-800/30 border border-gray-200 dark:border-gray-700"
originalJson={observation}
timestamp={observation.timestamp}
>
<TrajectoryCard.Header className="bg-gray-100 dark:bg-gray-700/50 text-gray-700 dark:text-gray-200">
<div className="flex justify-between items-center w-full">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const ErrorObservationComponent: React.FC<ErrorObservationProps> = ({ obs
<TrajectoryCard
className="bg-red-50 dark:bg-red-900/10 border border-red-200 dark:border-red-800"
originalJson={observation}
timestamp={observation.timestamp}
>
<TrajectoryCard.Header className="bg-red-100 dark:bg-red-800/50 text-red-800 dark:text-red-100">Error</TrajectoryCard.Header>
<TrajectoryCard.Body>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const FinishActionComponent: React.FC<FinishActionProps> = ({ action }) =
<TrajectoryCard
className="bg-green-50 dark:bg-green-900/10 border border-green-200 dark:border-green-800"
originalJson={action}
timestamp={action.timestamp}
>
<TrajectoryCard.Header className="bg-green-100 dark:bg-green-800/50 text-green-800 dark:text-green-100">Finish</TrajectoryCard.Header>
<TrajectoryCard.Body>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const IPythonActionComponent: React.FC<IPythonActionProps> = ({ action })
<TrajectoryCard
className="bg-yellow-50 dark:bg-yellow-900/10 border border-yellow-200 dark:border-yellow-800"
originalJson={action}
timestamp={action.timestamp}
>
<TrajectoryCard.Header className="bg-yellow-100 dark:bg-yellow-800/50 text-yellow-800 dark:text-yellow-100">IPython Action</TrajectoryCard.Header>
<TrajectoryCard.Body>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const IPythonObservationComponent: React.FC<IPythonObservationProps> = ({
<TrajectoryCard
className="bg-gray-50 dark:bg-gray-800/30 border border-gray-200 dark:border-gray-700"
originalJson={observation}
timestamp={observation.timestamp}
>
<TrajectoryCard.Header className="bg-gray-100 dark:bg-gray-700/50 text-gray-700 dark:text-gray-200">IPython Output</TrajectoryCard.Header>
<TrajectoryCard.Body>
Expand Down
Loading