inspect-ai 0.3.108__py3-none-any.whl → 0.3.109__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. inspect_ai/_eval/task/log.py +1 -1
  2. inspect_ai/_eval/task/run.py +1 -1
  3. inspect_ai/_util/dateutil.py +40 -0
  4. inspect_ai/_view/schema.py +11 -0
  5. inspect_ai/_view/www/CLAUDE.md +1 -1
  6. inspect_ai/_view/www/dist/assets/index.css +2068 -1796
  7. inspect_ai/_view/www/dist/assets/index.js +7951 -3643
  8. inspect_ai/_view/www/package.json +3 -2
  9. inspect_ai/_view/www/src/@types/log.d.ts +5 -5
  10. inspect_ai/_view/www/src/app/App.css +71 -4
  11. inspect_ai/_view/www/src/app/App.tsx +7 -0
  12. inspect_ai/_view/www/src/app/appearance/icons.ts +18 -2
  13. inspect_ai/_view/www/src/app/content/RenderedContent.tsx +7 -9
  14. inspect_ai/_view/www/src/app/log-list/LogItem.ts +18 -0
  15. inspect_ai/_view/www/src/app/log-list/LogListFooter.module.css +55 -0
  16. inspect_ai/_view/www/src/app/log-list/LogListFooter.tsx +67 -0
  17. inspect_ai/_view/www/src/app/log-list/LogPager.module.css +29 -0
  18. inspect_ai/_view/www/src/app/log-list/LogPager.tsx +134 -0
  19. inspect_ai/_view/www/src/app/log-list/LogsFilterInput.module.css +5 -0
  20. inspect_ai/_view/www/src/app/log-list/LogsFilterInput.tsx +31 -0
  21. inspect_ai/_view/www/src/app/log-list/LogsPanel.module.css +12 -0
  22. inspect_ai/_view/www/src/app/log-list/LogsPanel.tsx +178 -0
  23. inspect_ai/_view/www/src/app/log-list/grid/LogListGrid.module.css +115 -0
  24. inspect_ai/_view/www/src/app/log-list/grid/LogListGrid.tsx +304 -0
  25. inspect_ai/_view/www/src/app/log-list/grid/columns/CompletedDate.module.css +6 -0
  26. inspect_ai/_view/www/src/app/log-list/grid/columns/CompletedDate.tsx +64 -0
  27. inspect_ai/_view/www/src/app/log-list/grid/columns/EmptyCell.module.css +3 -0
  28. inspect_ai/_view/www/src/app/log-list/grid/columns/EmptyCell.tsx +7 -0
  29. inspect_ai/_view/www/src/app/log-list/grid/columns/FileName.module.css +20 -0
  30. inspect_ai/_view/www/src/app/log-list/grid/columns/FileName.tsx +52 -0
  31. inspect_ai/_view/www/src/app/log-list/grid/columns/Icon.module.css +11 -0
  32. inspect_ai/_view/www/src/app/log-list/grid/columns/Icon.tsx +35 -0
  33. inspect_ai/_view/www/src/app/log-list/grid/columns/Model.module.css +6 -0
  34. inspect_ai/_view/www/src/app/log-list/grid/columns/Model.tsx +34 -0
  35. inspect_ai/_view/www/src/app/log-list/grid/columns/Score.module.css +6 -0
  36. inspect_ai/_view/www/src/app/log-list/grid/columns/Score.tsx +61 -0
  37. inspect_ai/_view/www/src/app/log-list/grid/columns/Status.module.css +15 -0
  38. inspect_ai/_view/www/src/app/log-list/grid/columns/Status.tsx +95 -0
  39. inspect_ai/_view/www/src/app/log-list/grid/columns/Task.module.css +20 -0
  40. inspect_ai/_view/www/src/app/log-list/grid/columns/Task.tsx +50 -0
  41. inspect_ai/_view/www/src/app/log-list/grid/columns/columns.ts +27 -0
  42. inspect_ai/_view/www/src/app/log-view/LogView.tsx +2 -5
  43. inspect_ai/_view/www/src/app/log-view/LogViewContainer.tsx +4 -30
  44. inspect_ai/_view/www/src/app/log-view/LogViewLayout.tsx +5 -30
  45. inspect_ai/_view/www/src/app/log-view/tabs/TaskTab.tsx +4 -7
  46. inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/PrimaryBar.module.css +2 -0
  47. inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/PrimaryBar.tsx +3 -31
  48. inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/ResultsPanel.tsx +7 -57
  49. inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/ScoreGrid.tsx +2 -2
  50. inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/SecondaryBar.tsx +7 -1
  51. inspect_ai/_view/www/src/app/log-view/{navbar/Navbar.tsx → title-view/TitleView.tsx} +3 -6
  52. inspect_ai/_view/www/src/app/navbar/Navbar.module.css +57 -0
  53. inspect_ai/_view/www/src/app/navbar/Navbar.tsx +117 -0
  54. inspect_ai/_view/www/src/app/navbar/useBreadcrumbTruncation.ts +128 -0
  55. inspect_ai/_view/www/src/app/plan/DatasetDetailView.tsx +3 -3
  56. inspect_ai/_view/www/src/app/plan/DetailStep.tsx +6 -6
  57. inspect_ai/_view/www/src/app/plan/PlanDetailView.module.css +1 -0
  58. inspect_ai/_view/www/src/app/plan/ScorerDetailView.tsx +1 -1
  59. inspect_ai/_view/www/src/app/routing/AppRouter.tsx +28 -4
  60. inspect_ai/_view/www/src/app/routing/RouteDispatcher.tsx +28 -0
  61. inspect_ai/_view/www/src/app/routing/sampleNavigation.ts +76 -7
  62. inspect_ai/_view/www/src/app/routing/url.ts +193 -20
  63. inspect_ai/_view/www/src/app/samples/SampleDisplay.tsx +3 -17
  64. inspect_ai/_view/www/src/app/samples/descriptor/score/ScoreDescriptor.tsx +1 -1
  65. inspect_ai/_view/www/src/app/samples/transcript/SubtaskEventView.tsx +2 -2
  66. inspect_ai/_view/www/src/app/samples/transcript/TranscriptPanel.tsx +2 -2
  67. inspect_ai/_view/www/src/app/samples/transcript/outline/tree-visitors.ts +5 -0
  68. inspect_ai/_view/www/src/app/samples/transcript/transform/treeify.ts +26 -10
  69. inspect_ai/_view/www/src/app/types.ts +21 -1
  70. inspect_ai/_view/www/src/client/api/api-http.ts +2 -1
  71. inspect_ai/_view/www/src/client/api/api-shared.ts +0 -32
  72. inspect_ai/_view/www/src/client/api/client-api.ts +1 -1
  73. inspect_ai/_view/www/src/client/remote/remoteLogFile.ts +38 -6
  74. inspect_ai/_view/www/src/components/TextInput.module.css +45 -0
  75. inspect_ai/_view/www/src/components/TextInput.tsx +52 -0
  76. inspect_ai/_view/www/src/constants.ts +18 -0
  77. inspect_ai/_view/www/src/img/inspect-16.svg +10 -0
  78. inspect_ai/_view/www/src/img/inspect-back.svg +5 -0
  79. inspect_ai/_view/www/src/img/inspect-file.svg +26 -0
  80. inspect_ai/_view/www/src/img/inspect-forward.svg +7 -0
  81. inspect_ai/_view/www/src/img/inspect-home.svg +18 -0
  82. inspect_ai/_view/www/src/scoring/metrics.ts +75 -0
  83. inspect_ai/_view/www/src/scoring/scores.ts +19 -0
  84. inspect_ai/_view/www/src/scoring/types.ts +11 -0
  85. inspect_ai/_view/www/src/state/appSlice.ts +27 -7
  86. inspect_ai/_view/www/src/state/clientEvents.ts +73 -0
  87. inspect_ai/_view/www/src/state/clientEventsService.ts +105 -0
  88. inspect_ai/_view/www/src/state/hooks.ts +118 -1
  89. inspect_ai/_view/www/src/state/log.ts +19 -0
  90. inspect_ai/_view/www/src/state/logPolling.ts +3 -1
  91. inspect_ai/_view/www/src/state/logSlice.ts +9 -0
  92. inspect_ai/_view/www/src/state/logsSlice.ts +157 -15
  93. inspect_ai/_view/www/src/state/samplePolling.ts +4 -2
  94. inspect_ai/_view/www/src/tests/utils/path.test.ts +3 -3
  95. inspect_ai/_view/www/src/utils/evallog.ts +31 -0
  96. inspect_ai/_view/www/src/utils/path.ts +28 -0
  97. inspect_ai/_view/www/src/utils/uri.ts +49 -0
  98. inspect_ai/_view/www/yarn.lock +54 -17
  99. inspect_ai/analysis/beta/_dataframe/util.py +106 -10
  100. inspect_ai/log/_recorders/buffer/database.py +55 -16
  101. inspect_ai/model/_model.py +1 -1
  102. inspect_ai/model/_providers/providers.py +2 -2
  103. inspect_ai/model/_providers/vertex.py +3 -0
  104. inspect_ai/tool/_mcp/_mcp.py +6 -1
  105. inspect_ai/tool/_mcp/sampling.py +8 -1
  106. inspect_ai/tool/_tools/_bash_session.py +3 -6
  107. inspect_ai/tool/_tools/_web_browser/_web_browser.py +3 -8
  108. inspect_ai/util/_anyio.py +12 -3
  109. {inspect_ai-0.3.108.dist-info → inspect_ai-0.3.109.dist-info}/METADATA +2 -2
  110. {inspect_ai-0.3.108.dist-info → inspect_ai-0.3.109.dist-info}/RECORD +124 -94
  111. inspect_ai/_util/datetime.py +0 -10
  112. inspect_ai/_view/www/src/app/content/MetaDataView.module.css +0 -35
  113. inspect_ai/_view/www/src/app/content/MetaDataView.tsx +0 -101
  114. inspect_ai/_view/www/src/app/log-view/utils.ts +0 -34
  115. inspect_ai/_view/www/src/app/sidebar/EvalStatus.module.css +0 -15
  116. inspect_ai/_view/www/src/app/sidebar/EvalStatus.tsx +0 -72
  117. inspect_ai/_view/www/src/app/sidebar/LogDirectoryTitleView.module.css +0 -16
  118. inspect_ai/_view/www/src/app/sidebar/LogDirectoryTitleView.tsx +0 -70
  119. inspect_ai/_view/www/src/app/sidebar/Sidebar.module.css +0 -77
  120. inspect_ai/_view/www/src/app/sidebar/Sidebar.tsx +0 -119
  121. inspect_ai/_view/www/src/app/sidebar/SidebarLogEntry.module.css +0 -29
  122. inspect_ai/_view/www/src/app/sidebar/SidebarLogEntry.tsx +0 -96
  123. inspect_ai/_view/www/src/app/sidebar/SidebarScoreView.module.css +0 -23
  124. inspect_ai/_view/www/src/app/sidebar/SidebarScoreView.tsx +0 -44
  125. inspect_ai/_view/www/src/app/sidebar/SidebarScoresView.module.css +0 -35
  126. inspect_ai/_view/www/src/app/sidebar/SidebarScoresView.tsx +0 -63
  127. inspect_ai/_view/www/src/state/logsPolling.ts +0 -118
  128. /inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/ModelRolesView.module.css +0 -0
  129. /inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/ModelRolesView.tsx +0 -0
  130. /inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/ResultsPanel.module.css +0 -0
  131. /inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/RunningStatusPanel.module.css +0 -0
  132. /inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/RunningStatusPanel.tsx +0 -0
  133. /inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/ScoreGrid.module.css +0 -0
  134. /inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/SecondaryBar.module.css +0 -0
  135. /inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/StatusPanel.module.css +0 -0
  136. /inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/StatusPanel.tsx +0 -0
  137. /inspect_ai/_view/www/src/app/log-view/{navbar/Navbar.module.css → title-view/TitleView.module.css} +0 -0
  138. {inspect_ai-0.3.108.dist-info → inspect_ai-0.3.109.dist-info}/WHEEL +0 -0
  139. {inspect_ai-0.3.108.dist-info → inspect_ai-0.3.109.dist-info}/entry_points.txt +0 -0
  140. {inspect_ai-0.3.108.dist-info → inspect_ai-0.3.109.dist-info}/licenses/LICENSE +0 -0
  141. {inspect_ai-0.3.108.dist-info → inspect_ai-0.3.109.dist-info}/top_level.txt +0 -0
@@ -11,7 +11,7 @@ import {
11
11
  bySample,
12
12
  sortSamples,
13
13
  } from "../app/samples/sample-tools/SortFilter";
14
- import { SampleSummary } from "../client/api/types";
14
+ import { LogFile, SampleSummary } from "../client/api/types";
15
15
  import { kEpochAscVal, kSampleAscVal, kScoreAscVal } from "../constants";
16
16
  import { createLogger } from "../utils/logger";
17
17
  import { getAvailableScorers, getDefaultScorer } from "./scoring";
@@ -556,3 +556,120 @@ export const useSamplePopover = (id: string) => {
556
556
  isShowing,
557
557
  };
558
558
  };
559
+
560
+ export const useLogs = () => {
561
+ // Loading logs
562
+ const load = useStore((state) => state.logsActions.loadLogs);
563
+ const setLogs = useStore((state) => state.logsActions.setLogs);
564
+ const setStatus = useStore((state) => state.appActions.setStatus);
565
+
566
+ const loadLogs = useCallback(async () => {
567
+ const exec = async () => {
568
+ setStatus({ loading: true, error: undefined });
569
+ const logs = await load();
570
+ setLogs(logs);
571
+ setStatus({ loading: false, error: undefined });
572
+ };
573
+ exec().catch((e) => {
574
+ log.error("Error loading logs", e);
575
+ setStatus({ loading: false, error: e });
576
+ });
577
+ }, [load, setLogs, setStatus]);
578
+
579
+ // Loading headers
580
+ const storeLoadHeaders = useStore((state) => state.logsActions.loadHeaders);
581
+ const existingHeaders = useStore((state) => state.logs.logHeaders);
582
+ const allLogFiles = useStore((state) => state.logs.logs.files);
583
+
584
+ const loadHeaders = useCallback(
585
+ async (logFiles: LogFile[] = allLogFiles) => {
586
+ await storeLoadHeaders(logFiles);
587
+ },
588
+ [storeLoadHeaders, allLogFiles],
589
+ );
590
+
591
+ const loadAllHeaders = useCallback(async () => {
592
+ const logsToLoad = allLogFiles.filter((logFile) => {
593
+ const existingHeader = existingHeaders[logFile.name];
594
+ return !existingHeader || existingHeader.status === "started";
595
+ });
596
+
597
+ if (logsToLoad.length > 0) {
598
+ await storeLoadHeaders(logsToLoad);
599
+ }
600
+ }, [storeLoadHeaders, allLogFiles, existingHeaders]);
601
+
602
+ return { loadLogs, loadHeaders, loadAllHeaders };
603
+ };
604
+
605
+ export const usePagination = (name: string, defaultPageSize: number) => {
606
+ const page = useStore((state) => state.app.pagination[name]?.page || 0);
607
+ const itemsPerPage = useStore(
608
+ (state) => state.app.pagination[name]?.pageSize || defaultPageSize,
609
+ );
610
+ const setPagination = useStore((state) => state.appActions.setPagination);
611
+
612
+ const setPage = useCallback(
613
+ (newPage: number) => {
614
+ setPagination(name, { page: newPage, pageSize: itemsPerPage });
615
+ },
616
+ [name, setPagination, itemsPerPage],
617
+ );
618
+
619
+ const setPageSize = useCallback(
620
+ (newPageSize: number) => {
621
+ setPagination(name, { page, pageSize: newPageSize });
622
+ },
623
+ [name, setPagination, page],
624
+ );
625
+
626
+ return {
627
+ page,
628
+ itemsPerPage,
629
+ setPage,
630
+ setPageSize,
631
+ };
632
+ };
633
+
634
+ export const useLogsListing = () => {
635
+ const sorting = useStore((state) => state.logs.listing.sorting);
636
+ const setSorting = useStore((state) => state.logsActions.setSorting);
637
+
638
+ const filtering = useStore((state) => state.logs.listing.filtering);
639
+ const setFiltering = useStore((state) => state.logsActions.setFiltering);
640
+
641
+ const globalFilter = useStore((state) => state.logs.listing.globalFilter);
642
+ const setGlobalFilter = useStore(
643
+ (state) => state.logsActions.setGlobalFilter,
644
+ );
645
+
646
+ const columnResizeMode = useStore(
647
+ (state) => state.logs.listing.columnResizeMode,
648
+ );
649
+ const setColumnResizeMode = useStore(
650
+ (state) => state.logsActions.setColumnResizeMode,
651
+ );
652
+
653
+ const columnSizes = useStore((state) => state.logs.listing.columnSizes);
654
+ const setColumnSize = useStore((state) => state.logsActions.setColumnSize);
655
+
656
+ const filteredCount = useStore((state) => state.logs.listing.filteredCount);
657
+ const setFilteredCount = useStore(
658
+ (state) => state.logsActions.setFilteredCount,
659
+ );
660
+
661
+ return {
662
+ sorting,
663
+ setSorting,
664
+ filtering,
665
+ setFiltering,
666
+ globalFilter,
667
+ setGlobalFilter,
668
+ columnResizeMode,
669
+ setColumnResizeMode,
670
+ columnSizes,
671
+ setColumnSize,
672
+ filteredCount,
673
+ setFilteredCount,
674
+ };
675
+ };
@@ -0,0 +1,19 @@
1
+ import { useCallback } from "react";
2
+ import { useStore } from "./store";
3
+
4
+ export const useUnloadLog = () => {
5
+ const clearSelectedLogSummary = useStore(
6
+ (state) => state.logActions.clearSelectedLogSummary,
7
+ );
8
+ const setSelectedLogIndex = useStore(
9
+ (state) => state.logsActions.setSelectedLogIndex,
10
+ );
11
+ const clearLog = useStore((state) => state.logActions.clearLog);
12
+
13
+ const unloadLog = useCallback(() => {
14
+ clearSelectedLogSummary();
15
+ setSelectedLogIndex(-1);
16
+ clearLog();
17
+ }, [clearLog, clearSelectedLogSummary, setSelectedLogIndex]);
18
+ return { unloadLog };
19
+ };
@@ -184,7 +184,9 @@ export function createLogPolling(
184
184
  // Method to call when component unmounts
185
185
  const cleanup = () => {
186
186
  log.debug(`Cleanup`);
187
- abortController.abort();
187
+ if (abortController) {
188
+ abortController.abort();
189
+ }
188
190
  stopPolling();
189
191
  };
190
192
 
@@ -54,6 +54,9 @@ export interface LogSlice {
54
54
 
55
55
  // Poll the currently selected log
56
56
  pollLog: () => Promise<void>;
57
+
58
+ // Clear the currently loaded log
59
+ clearLog: () => void;
57
60
  };
58
61
  }
59
62
 
@@ -197,6 +200,12 @@ export const createLogSlice = (
197
200
  }
198
201
  },
199
202
 
203
+ clearLog: () => {
204
+ set((state) => {
205
+ state.log.loadedLog = undefined;
206
+ });
207
+ },
208
+
200
209
  pollLog: async () => {
201
210
  const currentLog = get().log.loadedLog;
202
211
  if (currentLog) {
@@ -1,7 +1,12 @@
1
+ import {
2
+ ColumnFiltersState,
3
+ ColumnResizeMode,
4
+ SortingState,
5
+ } from "@tanstack/react-table";
6
+ import { EvalLog } from "../@types/log";
1
7
  import { LogsState } from "../app/types";
2
- import { EvalLogHeader, LogFiles } from "../client/api/types";
8
+ import { EvalLogHeader, LogFile, LogFiles } from "../client/api/types";
3
9
  import { createLogger } from "../utils/logger";
4
- import { createLogsPolling } from "./logsPolling";
5
10
  import { StoreState } from "./store";
6
11
 
7
12
  const log = createLogger("Log Slice");
@@ -17,6 +22,7 @@ export interface LogsSlice {
17
22
  // Update State
18
23
  setLogs: (logs: LogFiles) => void;
19
24
  setLogHeaders: (headers: Record<string, EvalLogHeader>) => void;
25
+ loadHeaders: (logs: LogFile[]) => Promise<EvalLog[]>;
20
26
  setHeadersLoading: (loading: boolean) => void;
21
27
  setSelectedLogIndex: (index: number) => void;
22
28
  setSelectedLogFile: (logUrl: string) => void;
@@ -26,6 +32,15 @@ export interface LogsSlice {
26
32
  refreshLogs: () => Promise<void>;
27
33
  selectLogFile: (logUrl: string) => Promise<void>;
28
34
  loadLogs: () => Promise<LogFiles>;
35
+
36
+ setSorting: (sorting: SortingState) => void;
37
+ setFiltering: (filtering: ColumnFiltersState) => void;
38
+ setGlobalFilter: (globalFilter: string) => void;
39
+ setColumnResizeMode: (mode: ColumnResizeMode) => void;
40
+ setColumnSize: (columnId: string, size: number) => void;
41
+ setFilteredCount: (count: number) => void;
42
+ setWatchedLogs: (logs: LogFile[]) => void;
43
+ clearWatchedLogs: () => void;
29
44
  };
30
45
  }
31
46
 
@@ -35,6 +50,9 @@ const initialState: LogsState = {
35
50
  headersLoading: false,
36
51
  selectedLogIndex: -1,
37
52
  selectedLogFile: undefined as string | undefined,
53
+ listing: {},
54
+ loadingFiles: new Set<string>(),
55
+ pendingRequests: new Map<string, Promise<EvalLogHeader | null>>(),
38
56
  };
39
57
 
40
58
  export const createLogsSlice = (
@@ -42,8 +60,6 @@ export const createLogsSlice = (
42
60
  get: () => StoreState,
43
61
  _store: any,
44
62
  ): [LogsSlice, () => void] => {
45
- const logsPolling = createLogsPolling(get, set);
46
-
47
63
  const slice = {
48
64
  // State
49
65
  logs: initialState,
@@ -58,22 +74,105 @@ export const createLogsSlice = (
58
74
  ? logs.files[state.logs.selectedLogIndex]?.name
59
75
  : undefined;
60
76
  });
61
-
62
- // If we have files in the logs, load the headers
63
- if (logs.files.length > 0) {
64
- // ensure state is updated first
65
- setTimeout(() => {
66
- const currentState = get();
67
- if (!currentState.logs.headersLoading) {
68
- logsPolling.startPolling(logs);
69
- }
70
- }, 100);
71
- }
72
77
  },
73
78
  setLogHeaders: (headers: Record<string, EvalLogHeader>) =>
74
79
  set((state) => {
75
80
  state.logs.logHeaders = headers;
76
81
  }),
82
+ loadHeaders: async (logs: LogFile[]) => {
83
+ const state = get();
84
+ const api = state.api;
85
+ if (!api) {
86
+ console.error("API not initialized in LogsStore");
87
+ return [];
88
+ }
89
+
90
+ // Filter out files that are already loaded or currently loading
91
+ // reload headers with "started" status as they may have changed
92
+ const filesToLoad = logs.filter((logFile) => {
93
+ const existing = state.logs.logHeaders[logFile.name];
94
+ const isLoading = state.logs.loadingFiles.has(logFile.name);
95
+
96
+ // Always load if no existing header
97
+ if (!existing) {
98
+ return !isLoading;
99
+ }
100
+
101
+ // Reload if header status is "started" or "error" (but not if already loading)
102
+ if (existing.status === "started" || existing.status === "error") {
103
+ return !isLoading;
104
+ }
105
+
106
+ // Skip if already loaded with final status
107
+ return false;
108
+ });
109
+
110
+ if (filesToLoad.length === 0) {
111
+ return [];
112
+ }
113
+
114
+ // Mark files as loading
115
+ set((state) => {
116
+ filesToLoad.forEach((logFile) => {
117
+ state.logs.loadingFiles.add(logFile.name);
118
+ });
119
+ });
120
+
121
+ // Set global loading state if this is the first batch
122
+ const wasLoading = get().logs.headersLoading;
123
+ if (!wasLoading) {
124
+ set((state) => {
125
+ state.logs.headersLoading = true;
126
+ });
127
+ }
128
+
129
+ try {
130
+ log.debug(`LOADING LOG HEADERS for ${filesToLoad.length} files`);
131
+ const headers = await api.get_log_headers(
132
+ filesToLoad.map((log) => log.name),
133
+ );
134
+
135
+ // Process results and update store
136
+ const headerMap: Record<string, EvalLogHeader> = {};
137
+ for (let i = 0; i < filesToLoad.length; i++) {
138
+ const logFile = filesToLoad[i];
139
+ const header = headers[i];
140
+ if (header) {
141
+ headerMap[logFile.name] = header as EvalLogHeader;
142
+ }
143
+ }
144
+
145
+ // Update headers in store
146
+ set((state) => {
147
+ state.logs.logHeaders = { ...state.logs.logHeaders, ...headerMap };
148
+ // Remove from loading state
149
+ filesToLoad.forEach((logFile) => {
150
+ state.logs.loadingFiles.delete(logFile.name);
151
+ });
152
+ // Update global loading state if no more files are loading
153
+ if (state.logs.loadingFiles.size === 0) {
154
+ state.logs.headersLoading = false;
155
+ }
156
+ });
157
+
158
+ return headers;
159
+ } catch (error) {
160
+ log.error("Error loading log headers", error);
161
+
162
+ // Clear loading state on error
163
+ set((state) => {
164
+ filesToLoad.forEach((logFile) => {
165
+ state.logs.loadingFiles.delete(logFile.name);
166
+ });
167
+ if (state.logs.loadingFiles.size === 0) {
168
+ state.logs.headersLoading = false;
169
+ }
170
+ });
171
+
172
+ // Don't throw - just return empty array like the old implementation
173
+ return [];
174
+ }
175
+ },
77
176
  setHeadersLoading: (loading: boolean) =>
78
177
  set((state) => {
79
178
  state.logs.headersLoading = loading;
@@ -167,6 +266,49 @@ export const createLogsSlice = (
167
266
  );
168
267
  }
169
268
  },
269
+ setSorting: (sorting: SortingState) => {
270
+ set((state) => {
271
+ state.logs.listing.sorting = sorting;
272
+ });
273
+ },
274
+ setFiltering: (filtering: ColumnFiltersState) => {
275
+ set((state) => {
276
+ state.logs.listing.filtering = filtering;
277
+ });
278
+ },
279
+ setGlobalFilter: (globalFilter: string) => {
280
+ set((state) => {
281
+ state.logs.listing.globalFilter = globalFilter;
282
+ });
283
+ },
284
+ setColumnResizeMode: (mode: ColumnResizeMode) => {
285
+ set((state) => {
286
+ state.logs.listing.columnResizeMode = mode;
287
+ });
288
+ },
289
+ setColumnSize: (columnId: string, size: number) => {
290
+ set((state) => {
291
+ if (!state.logs.listing.columnSizes) {
292
+ state.logs.listing.columnSizes = {};
293
+ }
294
+ state.logs.listing.columnSizes[columnId] = size;
295
+ });
296
+ },
297
+ setFilteredCount: (count: number) => {
298
+ set((state) => {
299
+ state.logs.listing.filteredCount = count;
300
+ });
301
+ },
302
+ setWatchedLogs: (logs: LogFile[]) => {
303
+ set((state) => {
304
+ state.logs.listing.watchedLogs = logs;
305
+ });
306
+ },
307
+ clearWatchedLogs: () => {
308
+ set((state) => {
309
+ state.logs.listing.watchedLogs = undefined;
310
+ });
311
+ },
170
312
  },
171
313
  } as const;
172
314
 
@@ -241,8 +241,10 @@ export function createSamplePolling(
241
241
  };
242
242
 
243
243
  const cleanup = () => {
244
- log.debug(`CLEANUP`);
245
- abortController.abort();
244
+ log.debug(`Cleanup`);
245
+ if (abortController) {
246
+ abortController.abort();
247
+ }
246
248
  stopPolling();
247
249
  };
248
250
 
@@ -1,4 +1,4 @@
1
- import { filename, dirname } from "../../utils/path";
1
+ import { dirname, filename } from "../../utils/path";
2
2
 
3
3
  describe("filename", () => {
4
4
  test("extracts filename without extension from a path", () => {
@@ -30,7 +30,7 @@ describe("filename", () => {
30
30
  describe("dirname", () => {
31
31
  test("extracts directory name from a path", () => {
32
32
  expect(dirname("/path/to/file.txt")).toBe("/path/to");
33
- expect(dirname("/path/to/directory/")).toBe("/path/to/directory");
33
+ expect(dirname("/path/to/directory/")).toBe("/path/to");
34
34
  expect(dirname("/path/to/file")).toBe("/path/to");
35
35
  });
36
36
 
@@ -49,6 +49,6 @@ describe("dirname", () => {
49
49
  });
50
50
 
51
51
  test("handles paths with trailing slash", () => {
52
- expect(dirname("/path/to/directory/")).toBe("/path/to/directory");
52
+ expect(dirname("/path/to/directory/")).toBe("/path/to");
53
53
  });
54
54
  });
@@ -0,0 +1,31 @@
1
+ import { filename } from "./path";
2
+
3
+ const kLogFilePattern =
4
+ /^(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}[-+]\d{2}-\d{2})_(.+)_([0-9A-Za-z]+)\.(eval|json)$/;
5
+
6
+ export interface ParsedLogFileName {
7
+ timestamp?: Date;
8
+ name: string;
9
+ taskId?: string;
10
+ extension: "eval" | "json";
11
+ }
12
+
13
+ export const parseLogFileName = (logFileName: string): ParsedLogFileName => {
14
+ const match = logFileName.match(kLogFilePattern);
15
+ if (!match) {
16
+ // read the extension
17
+ return {
18
+ timestamp: undefined,
19
+ name: filename(logFileName),
20
+ taskId: undefined,
21
+ extension: logFileName.endsWith(".eval") ? "eval" : "json",
22
+ };
23
+ }
24
+
25
+ return {
26
+ timestamp: new Date(Date.parse(match[1])),
27
+ name: match[2],
28
+ taskId: match[3],
29
+ extension: match[4] as "eval" | "json",
30
+ };
31
+ };
@@ -5,6 +5,7 @@ export const filename = (path: string): string => {
5
5
  if (!path) {
6
6
  return "";
7
7
  }
8
+ path = path.endsWith("/") ? path.slice(0, -1) : path;
8
9
 
9
10
  const pathparts = path.split("/");
10
11
  const basename = pathparts.slice(-1)[0];
@@ -22,10 +23,21 @@ export const filename = (path: string): string => {
22
23
  }
23
24
  };
24
25
 
26
+ export const basename = (path: string): string => {
27
+ if (!path) {
28
+ return "";
29
+ }
30
+ path = path.endsWith("/") ? path.slice(0, -1) : path;
31
+ const pathparts = path.split("/");
32
+ return pathparts.slice(-1)[0];
33
+ };
34
+
25
35
  /**
26
36
  * Extracts the directory name from a given path.
27
37
  */
28
38
  export const dirname = (path: string): string => {
39
+ path = path.endsWith("/") ? path.slice(0, -1) : path;
40
+
29
41
  const pathparts = path.split("/");
30
42
 
31
43
  // If the path ends with a filename (or no slashes), remove the last part (filename)
@@ -38,3 +50,19 @@ export const dirname = (path: string): string => {
38
50
  // If no slashes, return empty string (no directory)
39
51
  return "";
40
52
  };
53
+
54
+ /**
55
+ * Tests whether the given path is in the specified directory.
56
+ */
57
+ export const isInDirectory = (path: string, directory: string): boolean => {
58
+ directory = directory.endsWith("/") ? directory.slice(0, -1) : directory;
59
+
60
+ return dirname(path) === directory;
61
+ };
62
+
63
+ export const ensureTrailingSlash = (path?: string): string => {
64
+ if (!path) {
65
+ return "";
66
+ }
67
+ return path.endsWith("/") ? path : path + "/";
68
+ };
@@ -30,3 +30,52 @@ export const directoryRelativeUrl = (file: string, dir?: string): string => {
30
30
  // If path can't be made relative, return undefined
31
31
  return encodeURIComponent(file);
32
32
  };
33
+
34
+ export const join = (file: string, dir?: string): string => {
35
+ if (!dir) {
36
+ return file;
37
+ }
38
+
39
+ // Normalize paths to ensure consistent directory separators
40
+ const normalizedFile = file.replace(/\\/g, "/");
41
+ const normalizedLogDir = dir.replace(/\\/g, "/");
42
+
43
+ // Ensure log_dir ends with a trailing slash
44
+ const dirWithSlash = normalizedLogDir.endsWith("/")
45
+ ? normalizedLogDir
46
+ : normalizedLogDir + "/";
47
+
48
+ return dirWithSlash + normalizedFile;
49
+ };
50
+
51
+ /**
52
+ * Encodes the path segments of a URL or relative path to ensure special characters
53
+ * (like `+`, spaces, etc.) are properly encoded without affecting legal characters like `/`.
54
+ *
55
+ * This function will encode file names and path portions of both absolute URLs and
56
+ * relative paths. It ensures that components of a full URL, such as the protocol and
57
+ * query parameters, remain intact, while only encoding the path.
58
+ */
59
+ export function encodePathParts(url: string): string {
60
+ if (!url) return url; // Handle empty strings
61
+
62
+ try {
63
+ // Parse a full Uri
64
+ const fullUrl = new URL(url);
65
+ fullUrl.pathname = fullUrl.pathname
66
+ .split("/")
67
+ .map((segment) =>
68
+ segment ? encodeURIComponent(decodeURIComponent(segment)) : "",
69
+ )
70
+ .join("/");
71
+ return fullUrl.toString();
72
+ } catch {
73
+ // This is a relative path that isn't parseable as Uri
74
+ return url
75
+ .split("/")
76
+ .map((segment) =>
77
+ segment ? encodeURIComponent(decodeURIComponent(segment)) : "",
78
+ )
79
+ .join("/");
80
+ }
81
+ }
@@ -920,6 +920,11 @@
920
920
  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.1.tgz#9fce313d12c9a77507f264de74626e87fd0dc541"
921
921
  integrity sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==
922
922
 
923
+ "@babel/runtime@^7.27.6":
924
+ version "7.27.6"
925
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.6.tgz#ec4070a04d76bae8ddbb10770ba55714a417b7c6"
926
+ integrity sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==
927
+
923
928
  "@babel/template@^7.25.9", "@babel/template@^7.26.9", "@babel/template@^7.27.0", "@babel/template@^7.3.3":
924
929
  version "7.27.0"
925
930
  resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.0.tgz#b253e5406cc1df1c57dcd18f11760c2dbf40c0b4"
@@ -1633,7 +1638,14 @@
1633
1638
  dependencies:
1634
1639
  "@babel/runtime" "^7.27.1"
1635
1640
 
1636
- "@mui/utils@^7.0.2", "@mui/utils@^7.1.0":
1641
+ "@mui/types@^7.4.3":
1642
+ version "7.4.3"
1643
+ resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.4.3.tgz#b205ee3404db0478cd93227fc21967e2cb8630fe"
1644
+ integrity sha512-2UCEiK29vtiZTeLdS2d4GndBKacVyxGvReznGXGr+CzW/YhjIX+OHUdCIczZjzcRAgKBGmE9zCIgoV9FleuyRQ==
1645
+ dependencies:
1646
+ "@babel/runtime" "^7.27.1"
1647
+
1648
+ "@mui/utils@^7.1.0":
1637
1649
  version "7.1.0"
1638
1650
  resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-7.1.0.tgz#464c0c1bc8f57c07d934ac674f3dcc81ac24f68b"
1639
1651
  integrity sha512-/OM3S8kSHHmWNOP+NH9xEtpYSG10upXeQ0wLZnfDgmgadTAk5F4MQfFLyZ5FCRJENB3eRzltMmaNl6UtDnPovw==
@@ -1645,22 +1657,35 @@
1645
1657
  prop-types "^15.8.1"
1646
1658
  react-is "^19.1.0"
1647
1659
 
1648
- "@mui/x-internals@8.3.0":
1649
- version "8.3.0"
1650
- resolved "https://registry.yarnpkg.com/@mui/x-internals/-/x-internals-8.3.0.tgz#0255ad37dcf2451e3f01d6820391cf368b68b7b5"
1651
- integrity sha512-wSVxg5aSO9xvJT7oarhsXqr03NeP355Whm7Qn6z3VvxdGwNc7K7vKpez3E+2KMxtdvywOmragwlSdTaO1K6qkg==
1660
+ "@mui/utils@^7.1.1":
1661
+ version "7.1.1"
1662
+ resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-7.1.1.tgz#de315ec45ac9e16c637dcc2b32cd7912edb4e234"
1663
+ integrity sha512-BkOt2q7MBYl7pweY2JWwfrlahhp+uGLR8S+EhiyRaofeRYUWL2YKbSGQvN4hgSN1i8poN0PaUiii1kEMrchvzg==
1652
1664
  dependencies:
1653
1665
  "@babel/runtime" "^7.27.1"
1654
- "@mui/utils" "^7.0.2"
1666
+ "@mui/types" "^7.4.3"
1667
+ "@types/prop-types" "^15.7.14"
1668
+ clsx "^2.1.1"
1669
+ prop-types "^15.8.1"
1670
+ react-is "^19.1.0"
1655
1671
 
1656
- "@mui/x-tree-view@^8.3.0":
1657
- version "8.3.0"
1658
- resolved "https://registry.yarnpkg.com/@mui/x-tree-view/-/x-tree-view-8.3.0.tgz#eaf401af4f8d88809c5683f4216168ac050a62b3"
1659
- integrity sha512-iK9xisIi0pYx8wMkY7C5o/eBQzOfSiy7Szpy8aNPaMO1mqTyYjVpv98fgQCkNsdluniqmFkLSnrN+W5xZEZqtQ==
1672
+ "@mui/x-internals@8.5.3":
1673
+ version "8.5.3"
1674
+ resolved "https://registry.yarnpkg.com/@mui/x-internals/-/x-internals-8.5.3.tgz#60756111fae9b5d5c56e6eb72ee0a869dc900441"
1675
+ integrity sha512-ImCg4E3DT3XoDIZO0pNCbB7iw14N+YCFY3J1V28POwCD7P2f3HSIz4jwzM006oYxI6bqeE6LMfpdPRDW6s6dQw==
1660
1676
  dependencies:
1661
- "@babel/runtime" "^7.27.1"
1662
- "@mui/utils" "^7.0.2"
1663
- "@mui/x-internals" "8.3.0"
1677
+ "@babel/runtime" "^7.27.6"
1678
+ "@mui/utils" "^7.1.1"
1679
+ reselect "^5.1.1"
1680
+
1681
+ "@mui/x-tree-view@^8.3.1":
1682
+ version "8.5.3"
1683
+ resolved "https://registry.yarnpkg.com/@mui/x-tree-view/-/x-tree-view-8.5.3.tgz#d2e2f3a2019dd88e11b3c64e35016a1cfada9e98"
1684
+ integrity sha512-mnorFyEtEshcj3AkDwBUu3UtydqsFCrqIoGAmVqO5YmwVEcgXJtg1PcQX3gkZmLDroolAUbBXrlUI8PwSLBtTQ==
1685
+ dependencies:
1686
+ "@babel/runtime" "^7.27.6"
1687
+ "@mui/utils" "^7.1.1"
1688
+ "@mui/x-internals" "8.5.3"
1664
1689
  "@types/react-transition-group" "^4.4.12"
1665
1690
  clsx "^2.1.1"
1666
1691
  prop-types "^15.8.1"
@@ -1813,6 +1838,18 @@
1813
1838
  dependencies:
1814
1839
  "@sinonjs/commons" "^3.0.0"
1815
1840
 
1841
+ "@tanstack/react-table@^8.21.3":
1842
+ version "8.21.3"
1843
+ resolved "https://registry.yarnpkg.com/@tanstack/react-table/-/react-table-8.21.3.tgz#2c38c747a5731c1a07174fda764b9c2b1fb5e91b"
1844
+ integrity sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==
1845
+ dependencies:
1846
+ "@tanstack/table-core" "8.21.3"
1847
+
1848
+ "@tanstack/table-core@8.21.3":
1849
+ version "8.21.3"
1850
+ resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.21.3.tgz#2977727d8fc8dfa079112d9f4d4c019110f1732c"
1851
+ integrity sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==
1852
+
1816
1853
  "@testing-library/jest-dom@^6.6.3":
1817
1854
  version "6.6.3"
1818
1855
  resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz#26ba906cf928c0f8172e182c6fe214eb4f9f2bd2"
@@ -2229,10 +2266,10 @@ aria-query@^5.0.0:
2229
2266
  resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.2.tgz#93f81a43480e33a338f19163a3d10a50c01dcd59"
2230
2267
  integrity sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==
2231
2268
 
2232
- asciinema-player@^3.9.0:
2233
- version "3.9.0"
2234
- resolved "https://registry.yarnpkg.com/asciinema-player/-/asciinema-player-3.9.0.tgz#c60742f85978e861b878fc7eb6289a5622c298af"
2235
- integrity sha512-SXVFImVzeNr8ZUdNIHABGuzlbnGWTKy245AquAjODsAnv+Lp6vxjYGN0LfA8ns30tnx/ag/bMrTbLq13TpHE6w==
2269
+ asciinema-player@^3.10.0:
2270
+ version "3.10.0"
2271
+ resolved "https://registry.yarnpkg.com/asciinema-player/-/asciinema-player-3.10.0.tgz#6b4b74b6ce85906b9930f0cf0dc71cf89e50e202"
2272
+ integrity sha512-shoOK6F606nDKZxDVM7JuGSCAyWLePoGRFNlV+FqiP5Sqvyn0BlE7wlbjZyd2X4P1iRhv/HKfVNtnQIxmgphRA==
2236
2273
  dependencies:
2237
2274
  "@babel/runtime" "^7.21.0"
2238
2275
  solid-js "^1.3.0"