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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
- Inspect View: Render custom tool view when viewing messages.
- Inspect View: Fix cmd+click on tasks/samples to open in new tab.
- Inspect View: Only stream log bytes when requested chunks are large (>50MB)
- Inspect View: Add Show Retried Logs button when inside an eval set and some logs were retried (both Tasks and Samples are now de-duplicated by default).
- Bugfix: Prevent component not found error during Human Agent transition.
- Bugfix: Use `builtins` module rather than `__builtins__` when parsing tool function types.

Expand Down
343 changes: 200 additions & 143 deletions src/inspect_ai/_view/www/dist/assets/index.js

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions src/inspect_ai/_view/www/src/app/appearance/icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,11 @@ export const ApplicationIcons = {
step: "bi bi-fast-forward-btn",
subtask: "bi bi-subtract",
success: "bi bi-check-circle-fill",
toggle: {
// combination of toggle-on and toggle2-off looked best for our default button font size
on: "bi bi-toggle-on",
off: "bi bi-toggle2-off",
},
transcript: "bi bi-list-columns-reverse",
tree: {
open: "bi bi-caret-down-fill",
Expand Down
5 changes: 1 addition & 4 deletions src/inspect_ai/_view/www/src/app/flow/FlowPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,7 @@ export const FlowPanel: FC = () => {
// Get the logs from the store
const { loadLogs } = useLogs();
useEffect(() => {
const exec = async () => {
await loadLogs(flowDir);
};
exec();
loadLogs(flowDir);
}, [loadLogs, flowDir]);

// Retrieve flow data
Expand Down
236 changes: 90 additions & 146 deletions src/inspect_ai/_view/www/src/app/log-list/LogsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ import { useNavigate } from "react-router-dom";
import { EvalSet } from "../../@types/log";
import { ProgressBar } from "../../components/ProgressBar";
import { useClientEvents } from "../../state/clientEvents";
import { useDocumentTitle, useLogs, useLogsListing } from "../../state/hooks";
import {
useDocumentTitle,
useLogs,
useLogsListing,
useLogsWithretried,
} from "../../state/hooks";
import { useStore } from "../../state/store";
import { dirname, isInDirectory } from "../../utils/path";
import { directoryRelativeUrl, join } from "../../utils/uri";
Expand Down Expand Up @@ -43,8 +48,12 @@ export const LogsPanel: FC<LogsPanelProps> = ({ maybeShowSingleLog }) => {
const [showColumnSelector, setShowColumnSelector] = useState(false);
const columnButtonRef = useRef<HTMLButtonElement>(null);

const showRetriedLogs = useStore((state) => state.logs.showRetriedLogs);
const setShowRetriedLogs = useStore(
(state) => state.logsActions.setShowRetriedLogs,
);
const logDir = useStore((state) => state.logs.logDir);
const logFiles = useStore((state) => state.logs.logs);
const logFiles = useLogsWithretried();
const evalSet = useStore((state) => state.logs.evalSet);
const logPreviews = useStore((state) => state.logs.logPreviews);
const { filteredCount } = useLogsListing();
Expand Down Expand Up @@ -98,42 +107,49 @@ export const LogsPanel: FC<LogsPanelProps> = ({ maybeShowSingleLog }) => {
}
}, [watchedLogs, startPolling, stopPolling]);

const logItems: Array<FileLogItem | FolderLogItem | PendingTaskItem> =
useMemo(() => {
const folderItems: Array<FileLogItem | FolderLogItem | PendingTaskItem> =
[];
const fileItems: Array<FileLogItem | FolderLogItem | PendingTaskItem> =
[];
const [logItems, hasRetriedLogs]: [
Array<FileLogItem | FolderLogItem | PendingTaskItem>,
boolean,
] = useMemo(() => {
const folderItems: Array<FileLogItem | FolderLogItem | PendingTaskItem> =
[];
const fileItems: Array<FileLogItem | FolderLogItem | PendingTaskItem> = [];

// Track processed folders to avoid duplicates
const processedFolders = new Set<string>();
const existingLogTaskIds = new Set<string>();
let _hasRetriedLogs = false;

for (const logFile of logFiles) {
if (logFile.task_id) {
existingLogTaskIds.add(logFile.task_id);
}

// Track processed folders to avoid duplicates
const processedFolders = new Set<string>();
const existingLogTaskIds = new Set<string>();
const name = logFile.name;

for (const logFile of logFiles) {
if (logFile.task_id) {
existingLogTaskIds.add(logFile.task_id);
}
const cleanDir = currentDir.endsWith("/")
? currentDir.slice(0, -1)
: currentDir;

const name = logFile.name;
const dirWithSlash = !currentDir.endsWith("/")
? currentDir + "/"
: currentDir;

const cleanDir = currentDir.endsWith("/")
? currentDir.slice(0, -1)
: currentDir;
if (isInDirectory(name, cleanDir)) {
const dirName = directoryRelativeUrl(currentDir, logDir);
const relativePath = directoryRelativeUrl(name, currentDir);

const dirWithSlash = !currentDir.endsWith("/")
? currentDir + "/"
: currentDir;
const fileOrFolderName = decodeURIComponent(rootName(relativePath));
const path = join(
decodeURIComponent(relativePath),
decodeURIComponent(dirName),
);

if (isInDirectory(name, cleanDir)) {
const dirName = directoryRelativeUrl(currentDir, logDir);
const relativePath = directoryRelativeUrl(name, currentDir);

const fileOrFolderName = decodeURIComponent(rootName(relativePath));
const path = join(
decodeURIComponent(relativePath),
decodeURIComponent(dirName),
);
if (logFile.retried) {
_hasRetriedLogs = true;
}

if (showRetriedLogs || !logFile.retried) {
fileItems.push({
id: fileOrFolderName,
name: fileOrFolderName,
Expand All @@ -142,39 +158,39 @@ export const LogsPanel: FC<LogsPanelProps> = ({ maybeShowSingleLog }) => {
log: logFile,
logPreview: logPreviews[logFile.name],
});
} else if (name.startsWith(dirWithSlash)) {
// This is file that is next level (or deeper) child of the current directory
const relativePath = directoryRelativeUrl(name, currentDir);

const dirName = decodeURIComponent(rootName(relativePath));
const currentDirRelative = directoryRelativeUrl(currentDir, logDir);
const url = join(dirName, decodeURIComponent(currentDirRelative));
if (!processedFolders.has(dirName)) {
folderItems.push({
id: dirName,
name: dirName,
type: "folder",
url: logsUrl(url, logDir),
itemCount: logFiles.filter((file) =>
file.name.startsWith(dirname(name)),
).length,
});
processedFolders.add(dirName);
}
}
} else if (name.startsWith(dirWithSlash)) {
// This is file that is next level (or deeper) child of the current directory
const relativePath = directoryRelativeUrl(name, currentDir);

const dirName = decodeURIComponent(rootName(relativePath));
const currentDirRelative = directoryRelativeUrl(currentDir, logDir);
const url = join(dirName, decodeURIComponent(currentDirRelative));
if (!processedFolders.has(dirName)) {
folderItems.push({
id: dirName,
name: dirName,
type: "folder",
url: logsUrl(url, logDir),
itemCount: logFiles.filter((file) =>
file.name.startsWith(dirname(name)),
).length,
});
processedFolders.add(dirName);
}
}
}

const orderedItems = [...folderItems, ...fileItems];
const orderedItems = [...folderItems, ...fileItems];

// Ensure there is only one entry for each task id, preferring to
// always show running or complete tasks (over error tasks). Ensure that the
// order of all items isn't changed
const collapsedLogItems: Array<
FileLogItem | FolderLogItem | PendingTaskItem
> = collapseLogItems(evalSet, orderedItems);
const _logFiles = appendPendingItems(
evalSet,
existingLogTaskIds,
orderedItems,
);

return appendPendingItems(evalSet, existingLogTaskIds, collapsedLogItems);
}, [evalSet, logFiles, currentDir, logDir, logPreviews]);
return [_logFiles, _hasRetriedLogs];
}, [evalSet, logFiles, currentDir, logDir, logPreviews, showRetriedLogs]);

const { columns, setColumnVisibility } = useLogListColumns();

Expand Down Expand Up @@ -225,10 +241,7 @@ export const LogsPanel: FC<LogsPanelProps> = ({ maybeShowSingleLog }) => {
}, [logItems]);

useEffect(() => {
const exec = async () => {
await loadLogs(logPath);
};
exec();
loadLogs(logPath);
}, [loadLogs, logPath]);

const handleResetFilters = () => {
Expand Down Expand Up @@ -266,6 +279,20 @@ export const LogsPanel: FC<LogsPanelProps> = ({ maybeShowSingleLog }) => {
/>
)}

{hasRetriedLogs && (
<NavbarButton
key="show-retried"
label="Show Retried Logs"
icon={
showRetriedLogs
? ApplicationIcons.toggle.on
: ApplicationIcons.toggle.off
}
latched={showRetriedLogs}
onClick={() => setShowRetriedLogs(!showRetriedLogs)}
/>
)}

<NavbarButton
key="choose-columns"
ref={columnButtonRef}
Expand Down Expand Up @@ -318,89 +345,6 @@ export const LogsPanel: FC<LogsPanelProps> = ({ maybeShowSingleLog }) => {
);
};

export const collapseLogItems = (
evalSet: EvalSet | undefined,
logItems: (FileLogItem | FolderLogItem | PendingTaskItem)[],
): (FileLogItem | FolderLogItem | PendingTaskItem)[] => {
if (!evalSet) {
return logItems;
}

const running = logItems.some(
(l) => l.type === "file" && l.logPreview?.status === "started",
);
if (!running) {
return logItems;
}

// Group file items by task_id
const taskIdToItems = new Map<string, FileLogItem[]>();
const itemsWithoutTaskId: Array<FolderLogItem | FileLogItem> = [];

for (const item of logItems) {
if (item.type === "file" && item.log.task_id) {
const taskId = item.log.task_id;
if (!taskIdToItems.has(taskId)) {
taskIdToItems.set(taskId, []);
}
taskIdToItems.get(taskId)!.push(item);
} else if (item.type === "folder" || item.type === "file") {
itemsWithoutTaskId.push(item);
}
}

// For each task_id, select the best item (prefer running/complete over error)
const selectedItems = new Map<string, FileLogItem>();
for (const [taskId, items] of taskIdToItems) {
// Sort by status priority: started > success > error
// If same priority, take the last one
let bestItem = items[0];
for (const item of items) {
const currentStatus = item.logPreview?.status;
const currentMtime = item.log.mtime ?? 0;
const bestStatus = bestItem.logPreview?.status;
const bestMtime = bestItem.log.mtime ?? 0;

// Prefer started over everything
if (currentStatus === "started" && bestStatus !== "started") {
bestItem = item;
}
// Prefer success over error
else if (currentStatus === "success" && bestStatus === "error") {
bestItem = item;
}
// If same status or current is error, prefer most recent
else if (currentStatus === bestStatus && currentMtime > bestMtime) {
bestItem = item;
}
}
selectedItems.set(taskId, bestItem);
}

// Rebuild logItems maintaining order, replacing duplicates with selected item
const collapsedLogItems: Array<
FileLogItem | FolderLogItem | PendingTaskItem
> = [];
const processedTaskIds = new Set<string>();

for (const item of logItems) {
if (item.type === "file" && item.log.task_id) {
const taskId = item.log.task_id;
if (!processedTaskIds.has(taskId)) {
const selectedItem = selectedItems.get(taskId);
if (selectedItem) {
collapsedLogItems.push(selectedItem);
}
processedTaskIds.add(taskId);
}
} else {
// Include folders and files without task_id
collapsedLogItems.push(item);
}
}
return collapsedLogItems;
};

const appendPendingItems = (
evalSet: EvalSet | undefined,
tasksWithLogFiles: Set<string>,
Expand Down
Loading