inspect-ai 0.3.107__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 (142) 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/_anthropic_citations.py +1 -4
  103. inspect_ai/model/_providers/providers.py +2 -2
  104. inspect_ai/model/_providers/vertex.py +3 -0
  105. inspect_ai/tool/_mcp/_mcp.py +6 -1
  106. inspect_ai/tool/_mcp/sampling.py +8 -1
  107. inspect_ai/tool/_tools/_bash_session.py +3 -6
  108. inspect_ai/tool/_tools/_web_browser/_web_browser.py +3 -8
  109. inspect_ai/util/_anyio.py +12 -3
  110. {inspect_ai-0.3.107.dist-info → inspect_ai-0.3.109.dist-info}/METADATA +2 -2
  111. {inspect_ai-0.3.107.dist-info → inspect_ai-0.3.109.dist-info}/RECORD +125 -95
  112. inspect_ai/_util/datetime.py +0 -10
  113. inspect_ai/_view/www/src/app/content/MetaDataView.module.css +0 -35
  114. inspect_ai/_view/www/src/app/content/MetaDataView.tsx +0 -101
  115. inspect_ai/_view/www/src/app/log-view/utils.ts +0 -34
  116. inspect_ai/_view/www/src/app/sidebar/EvalStatus.module.css +0 -15
  117. inspect_ai/_view/www/src/app/sidebar/EvalStatus.tsx +0 -72
  118. inspect_ai/_view/www/src/app/sidebar/LogDirectoryTitleView.module.css +0 -16
  119. inspect_ai/_view/www/src/app/sidebar/LogDirectoryTitleView.tsx +0 -70
  120. inspect_ai/_view/www/src/app/sidebar/Sidebar.module.css +0 -77
  121. inspect_ai/_view/www/src/app/sidebar/Sidebar.tsx +0 -119
  122. inspect_ai/_view/www/src/app/sidebar/SidebarLogEntry.module.css +0 -29
  123. inspect_ai/_view/www/src/app/sidebar/SidebarLogEntry.tsx +0 -96
  124. inspect_ai/_view/www/src/app/sidebar/SidebarScoreView.module.css +0 -23
  125. inspect_ai/_view/www/src/app/sidebar/SidebarScoreView.tsx +0 -44
  126. inspect_ai/_view/www/src/app/sidebar/SidebarScoresView.module.css +0 -35
  127. inspect_ai/_view/www/src/app/sidebar/SidebarScoresView.tsx +0 -63
  128. inspect_ai/_view/www/src/state/logsPolling.ts +0 -118
  129. /inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/ModelRolesView.module.css +0 -0
  130. /inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/ModelRolesView.tsx +0 -0
  131. /inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/ResultsPanel.module.css +0 -0
  132. /inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/RunningStatusPanel.module.css +0 -0
  133. /inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/RunningStatusPanel.tsx +0 -0
  134. /inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/ScoreGrid.module.css +0 -0
  135. /inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/SecondaryBar.module.css +0 -0
  136. /inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/StatusPanel.module.css +0 -0
  137. /inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/StatusPanel.tsx +0 -0
  138. /inspect_ai/_view/www/src/app/log-view/{navbar/Navbar.module.css → title-view/TitleView.module.css} +0 -0
  139. {inspect_ai-0.3.107.dist-info → inspect_ai-0.3.109.dist-info}/WHEEL +0 -0
  140. {inspect_ai-0.3.107.dist-info → inspect_ai-0.3.109.dist-info}/entry_points.txt +0 -0
  141. {inspect_ai-0.3.107.dist-info → inspect_ai-0.3.109.dist-info}/licenses/LICENSE +0 -0
  142. {inspect_ai-0.3.107.dist-info → inspect_ai-0.3.109.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,178 @@
1
+ import clsx from "clsx";
2
+ import { FC, useEffect, useMemo, useRef } from "react";
3
+
4
+ import { ProgressBar } from "../../components/ProgressBar";
5
+ import { useClientEvents } from "../../state/clientEvents";
6
+ import { useLogs } from "../../state/hooks";
7
+ import { useUnloadLog } from "../../state/log";
8
+ import { useStore } from "../../state/store";
9
+ import { dirname, isInDirectory } from "../../utils/path";
10
+ import { directoryRelativeUrl, join } from "../../utils/uri";
11
+ import { Navbar } from "../navbar/Navbar";
12
+ import { logUrl, useLogRouteParams } from "../routing/url";
13
+ import { LogListGrid } from "./grid/LogListGrid";
14
+ import { FileLogItem, FolderLogItem } from "./LogItem";
15
+ import { LogListFooter } from "./LogListFooter";
16
+ import { LogsFilterInput } from "./LogsFilterInput";
17
+ import styles from "./LogsPanel.module.css";
18
+
19
+ const rootName = (relativePath: string) => {
20
+ const parts = relativePath.split("/");
21
+ if (parts.length === 0) {
22
+ return "";
23
+ }
24
+ return parts[0];
25
+ };
26
+
27
+ export const kLogsPaginationId = "logs-list-pagination";
28
+ export const kDefaultPageSize = 30;
29
+
30
+ interface LogsPanelProps {}
31
+
32
+ export const LogsPanel: FC<LogsPanelProps> = () => {
33
+ // Get the logs from the store
34
+ const loading = useStore((state) => state.app.status.loading);
35
+
36
+ const { loadLogs } = useLogs();
37
+ const logs = useStore((state) => state.logs.logs);
38
+ const logHeaders = useStore((state) => state.logs.logHeaders);
39
+ const headersLoading = useStore((state) => state.logs.headersLoading);
40
+ const watchedLogs = useStore((state) => state.logs.listing.watchedLogs);
41
+
42
+ // Unload the load when this is mounted. This prevents the old log
43
+ // data from being displayed when navigating back to the logs panel
44
+ // and also ensures that we reload logs when freshly navigating to them.
45
+ const { unloadLog } = useUnloadLog();
46
+ useEffect(() => {
47
+ unloadLog();
48
+ }, []);
49
+
50
+ const { logPath } = useLogRouteParams();
51
+
52
+ const currentDir = join(logPath || "", logs.log_dir);
53
+
54
+ // Polling for client events
55
+ const { startPolling, stopPolling } = useClientEvents();
56
+
57
+ const previousWatchedLogs = useRef<typeof watchedLogs>(undefined);
58
+
59
+ useEffect(() => {
60
+ // Only restart polling if the watched logs have actually changed
61
+ const current =
62
+ watchedLogs
63
+ ?.map((log) => log.name)
64
+ .sort()
65
+ .join(",") || "";
66
+ const previous =
67
+ previousWatchedLogs.current === undefined
68
+ ? undefined
69
+ : previousWatchedLogs.current
70
+ ?.map((log) => log.name)
71
+ .sort()
72
+ .join(",") || "";
73
+
74
+ if (current !== previous) {
75
+ // Always stop current polling first when logs change
76
+ stopPolling();
77
+
78
+ if (watchedLogs !== undefined) {
79
+ startPolling(watchedLogs);
80
+ }
81
+ previousWatchedLogs.current = watchedLogs;
82
+ }
83
+ }, [watchedLogs]);
84
+
85
+ // All the items visible in the current directory (might span
86
+ // multiple pages)
87
+ const logItems: Array<FileLogItem | FolderLogItem> = useMemo(() => {
88
+ // Build the list of files / folders that for the current directory
89
+ const logItems: Array<FileLogItem | FolderLogItem> = [];
90
+
91
+ // Track process folders to avoid duplicates
92
+ const processedFolders = new Set<string>();
93
+
94
+ for (const logFile of logs.files) {
95
+ // The file name
96
+ const name = logFile.name;
97
+
98
+ // Process paths in the current directory
99
+ const cleanDir = currentDir.endsWith("/")
100
+ ? currentDir.slice(0, -1)
101
+ : currentDir;
102
+
103
+ if (isInDirectory(logFile.name, cleanDir)) {
104
+ // This is a file within the current directory
105
+ const dirName = directoryRelativeUrl(currentDir, logs.log_dir);
106
+ const relativePath = directoryRelativeUrl(name, currentDir);
107
+
108
+ const fileOrFolderName = decodeURIComponent(rootName(relativePath));
109
+ const path = join(
110
+ decodeURIComponent(relativePath),
111
+ decodeURIComponent(dirName),
112
+ );
113
+
114
+ logItems.push({
115
+ id: fileOrFolderName,
116
+ name: fileOrFolderName,
117
+ type: "file",
118
+ url: logUrl(path, logs.log_dir),
119
+ logFile: logFile,
120
+ header: logHeaders[logFile.name],
121
+ });
122
+ } else if (name.startsWith(currentDir)) {
123
+ // This is file that is next level (or deeper) child
124
+ // of the current directory, extract the top level folder name
125
+
126
+ const relativePath = directoryRelativeUrl(name, currentDir);
127
+
128
+ const dirName = decodeURIComponent(rootName(relativePath));
129
+ const currentDirRelative = directoryRelativeUrl(
130
+ currentDir,
131
+ logs.log_dir,
132
+ );
133
+ const url = join(dirName, decodeURIComponent(currentDirRelative));
134
+ if (!processedFolders.has(dirName)) {
135
+ logItems.push({
136
+ id: dirName,
137
+ name: dirName,
138
+ type: "folder",
139
+ url: logUrl(url, logs.log_dir),
140
+ itemCount: logs.files.filter((file) =>
141
+ file.name.startsWith(dirname(name)),
142
+ ).length,
143
+ });
144
+ processedFolders.add(dirName);
145
+ }
146
+ }
147
+ }
148
+
149
+ return logItems;
150
+ }, [logPath, logs.files, logHeaders]);
151
+
152
+ useEffect(() => {
153
+ const exec = async () => {
154
+ await loadLogs();
155
+ };
156
+ exec();
157
+ }, [loadLogs]);
158
+
159
+ return (
160
+ <div className={clsx(styles.panel)}>
161
+ <Navbar>
162
+ <LogsFilterInput />
163
+ </Navbar>
164
+
165
+ <ProgressBar animating={loading || headersLoading} />
166
+ <div className={clsx(styles.list, "text-size-smaller")}>
167
+ <LogListGrid items={logItems} />
168
+ </div>
169
+ <LogListFooter
170
+ logDir={currentDir}
171
+ itemCount={logItems.length}
172
+ progressText={
173
+ loading ? "Loading logs" : headersLoading ? "Loading data" : undefined
174
+ }
175
+ />
176
+ </div>
177
+ );
178
+ };
@@ -0,0 +1,115 @@
1
+ .gridContainer {
2
+ height: 100%;
3
+ overflow: auto;
4
+ padding: 0;
5
+ }
6
+
7
+ .grid {
8
+ display: grid;
9
+ grid-template-rows: auto 1fr;
10
+ height: 100%;
11
+ min-height: 0;
12
+ }
13
+
14
+ /* Header Styles */
15
+ .headerRow {
16
+ display: grid;
17
+ background: var(--bs-light);
18
+ border-bottom: 1px solid var(--bs-border-color);
19
+ position: sticky;
20
+ top: 0;
21
+ z-index: 1;
22
+ width: fit-content;
23
+ min-width: 100%;
24
+ }
25
+
26
+ .headerCell {
27
+ padding: 0.1em 0.1em 0.1em 0.6em;
28
+ font-weight: 600;
29
+ font-size: 0.875rem;
30
+ display: flex;
31
+ align-items: center;
32
+ justify-content: flex-start;
33
+ border-right: 1px solid var(--bs-border-color-translucent);
34
+ position: relative;
35
+ box-sizing: border-box;
36
+ }
37
+
38
+ .headerCell:last-child {
39
+ border-right: none;
40
+ }
41
+
42
+ .sortable {
43
+ cursor: pointer;
44
+ user-select: none;
45
+ }
46
+
47
+ .sortable:hover {
48
+ background-color: var(--bs-secondary-bg-subtle);
49
+ }
50
+
51
+ .sortIndicator {
52
+ margin-left: 0.25em;
53
+ font-size: 0.75rem;
54
+ color: var(--bs-link-color);
55
+ }
56
+
57
+ /* Resizer Styles */
58
+ .resizer {
59
+ position: absolute;
60
+ right: 0;
61
+ top: 0;
62
+ height: 100%;
63
+ width: 5px;
64
+ background-color: var(--bs-secondary-bg-subtle);
65
+
66
+ cursor: col-resize;
67
+ user-select: none;
68
+ touch-action: none;
69
+ opacity: 0;
70
+ transition: opacity 0.2s;
71
+ }
72
+
73
+ .resizer:hover,
74
+ .isResizing {
75
+ opacity: 1;
76
+ }
77
+
78
+ .headerCell:hover .resizer {
79
+ opacity: 0.3;
80
+ }
81
+
82
+ /* Body Styles */
83
+ .bodyContainer {
84
+ overflow-y: auto;
85
+ overflow-x: hidden;
86
+ min-height: 0;
87
+ min-width: 100%;
88
+ width: fit-content;
89
+ }
90
+
91
+ .bodyRow {
92
+ display: grid;
93
+ transition: background-color 0.15s ease-in-out;
94
+ width: fit-content;
95
+ min-width: 100%;
96
+ }
97
+
98
+ /* Cell Styles */
99
+ .bodyCell {
100
+ padding: 0.1em 0.1em 0.1em 0.6em;
101
+ display: flex;
102
+ align-items: center;
103
+ box-sizing: border-box;
104
+ overflow: hidden;
105
+ }
106
+
107
+ .bodyCell:last-child {
108
+ border-right: none;
109
+ }
110
+
111
+ .emptyMessage {
112
+ width: 100%;
113
+ text-align: center;
114
+ padding-top: 4em;
115
+ }
@@ -0,0 +1,304 @@
1
+ import {
2
+ ColumnFiltersState,
3
+ flexRender,
4
+ getCoreRowModel,
5
+ getFilteredRowModel,
6
+ getPaginationRowModel,
7
+ getSortedRowModel,
8
+ PaginationState,
9
+ SortingState,
10
+ Updater,
11
+ useReactTable,
12
+ } from "@tanstack/react-table";
13
+ import clsx from "clsx";
14
+ import { FC, useCallback, useEffect, useMemo, useRef } from "react";
15
+
16
+ import { useLogs, useLogsListing, usePagination } from "../../../state/hooks";
17
+ import { useStore } from "../../../state/store";
18
+ import { FileLogItem, FolderLogItem } from "../LogItem";
19
+ import { kDefaultPageSize, kLogsPaginationId } from "../LogsPanel";
20
+ import styles from "./LogListGrid.module.css";
21
+ import { getColumns } from "./columns/columns";
22
+
23
+ interface LogListGridProps {
24
+ items: Array<FileLogItem | FolderLogItem>;
25
+ }
26
+
27
+ export const LogListGrid: FC<LogListGridProps> = ({ items }) => {
28
+ const {
29
+ sorting,
30
+ setSorting,
31
+ filtering,
32
+ setFiltering,
33
+ globalFilter,
34
+ setGlobalFilter,
35
+ columnResizeMode,
36
+ setFilteredCount,
37
+ columnSizes,
38
+ setColumnSize,
39
+ } = useLogsListing();
40
+
41
+ const { loadHeaders } = useLogs();
42
+
43
+ const { page, itemsPerPage, setPage } = usePagination(
44
+ kLogsPaginationId,
45
+ kDefaultPageSize,
46
+ );
47
+ const headersLoading = useStore((state) => state.logs.headersLoading);
48
+ const loading = useStore((state) => state.app.status.loading);
49
+ const setWatchedLogs = useStore((state) => state.logsActions.setWatchedLogs);
50
+
51
+ const logHeaders = useStore((state) => state.logs.logHeaders);
52
+ const sortingRef = useRef(sorting);
53
+
54
+ // Load all headers when needed (store handles deduplication)
55
+ const loadAllHeadersForItems = useCallback(async () => {
56
+ const logFiles = items
57
+ .filter((item) => item.type === "file")
58
+ .map((item) => item.logFile)
59
+ .filter((file) => file !== undefined);
60
+
61
+ await loadHeaders(logFiles);
62
+ setWatchedLogs(logFiles);
63
+ }, [loadHeaders, items, setWatchedLogs]);
64
+
65
+ // Keep ref updated
66
+ useEffect(() => {
67
+ sortingRef.current = sorting;
68
+ }, [sorting]);
69
+
70
+ // Initial sort
71
+ useEffect(() => {
72
+ setSorting([{ id: "icon", desc: true }]);
73
+ }, []);
74
+
75
+ // Force re-sort when logHeaders change (affects task column sorting)
76
+ useEffect(() => {
77
+ // Only re-sort if we're currently sorting by a column that depends on logHeaders
78
+ const currentSort = sortingRef.current?.find(
79
+ (sort) =>
80
+ sort.id === "task" || sort.id === "model" || sort.id === "score",
81
+ );
82
+ if (currentSort) {
83
+ // Trigger a re-sort by updating the sorting state
84
+ setSorting([...(sortingRef.current || [])]);
85
+ }
86
+ }, [logHeaders]);
87
+
88
+ const columns = useMemo(() => {
89
+ return getColumns();
90
+ }, []);
91
+
92
+ const table = useReactTable({
93
+ data: items,
94
+ columns,
95
+ columnResizeMode: columnResizeMode || "onChange",
96
+ state: {
97
+ sorting,
98
+ columnFilters: filtering,
99
+ globalFilter,
100
+ pagination: {
101
+ pageIndex: page,
102
+ pageSize: itemsPerPage,
103
+ },
104
+ columnSizing: columnSizes || {},
105
+ },
106
+ rowCount: items.length,
107
+ onSortingChange: async (updater: Updater<SortingState>) => {
108
+ await loadAllHeadersForItems();
109
+ setSorting(
110
+ typeof updater === "function" ? updater(sorting || []) : updater,
111
+ );
112
+ },
113
+ onColumnFiltersChange: async (updater: Updater<ColumnFiltersState>) => {
114
+ await loadAllHeadersForItems();
115
+ setFiltering(
116
+ typeof updater === "function" ? updater(filtering || []) : updater,
117
+ );
118
+ },
119
+ onGlobalFilterChange: (updater: Updater<string>) => {
120
+ setGlobalFilter(
121
+ typeof updater === "function" ? updater(globalFilter || "") : updater,
122
+ );
123
+ },
124
+ onPaginationChange: (updater: Updater<PaginationState>) => {
125
+ const newPagination =
126
+ typeof updater === "function"
127
+ ? updater({ pageIndex: page, pageSize: itemsPerPage })
128
+ : updater;
129
+ setPage(newPagination.pageIndex);
130
+ },
131
+ onColumnSizingChange: (updater: Updater<Record<string, number>>) => {
132
+ const newSizes =
133
+ typeof updater === "function"
134
+ ? updater(table.getState().columnSizing || {})
135
+ : updater;
136
+ for (const [columnId, size] of Object.entries(newSizes)) {
137
+ setColumnSize(columnId, size);
138
+ }
139
+ },
140
+ getCoreRowModel: getCoreRowModel(),
141
+ getSortedRowModel: getSortedRowModel(),
142
+ getFilteredRowModel: getFilteredRowModel(),
143
+ getPaginationRowModel: getPaginationRowModel(),
144
+ enableColumnResizing: true,
145
+ autoResetPageIndex: false,
146
+ });
147
+
148
+ // Update filtered count in store when table filtering changes
149
+ useEffect(() => {
150
+ const filteredRowCount = table.getFilteredRowModel().rows.length;
151
+ setFilteredCount(filteredRowCount);
152
+ }, [table.getFilteredRowModel().rows.length, setFilteredCount]);
153
+
154
+ // Load all headers when globalFilter changes
155
+ useEffect(() => {
156
+ if (globalFilter && globalFilter.trim()) {
157
+ loadAllHeadersForItems();
158
+ }
159
+ }, [globalFilter, loadAllHeadersForItems]);
160
+
161
+ // Load headers for current page (demand loading)
162
+ useEffect(() => {
163
+ const exec = async () => {
164
+ const startIndex = page * itemsPerPage;
165
+ const endIndex = startIndex + itemsPerPage;
166
+ const currentPageItems = items.slice(startIndex, endIndex);
167
+
168
+ const fileItems = currentPageItems.filter((item) => item.type === "file");
169
+ const logFiles = fileItems
170
+ .map((item) => item.logFile)
171
+ .filter((file) => file !== undefined);
172
+
173
+ // Only load headers for files that don't already have headers loaded
174
+ const filesToLoad = logFiles.filter((file) => !logHeaders[file.name]);
175
+
176
+ if (filesToLoad.length > 0) {
177
+ await loadHeaders(filesToLoad);
178
+ }
179
+
180
+ setWatchedLogs(logFiles);
181
+ };
182
+ exec();
183
+ }, [page, itemsPerPage, items, loadHeaders, setWatchedLogs, logHeaders]);
184
+
185
+ const placeholderText = useMemo(() => {
186
+ if (headersLoading || loading) {
187
+ if (globalFilter) {
188
+ return "searching...";
189
+ } else {
190
+ return "loading...";
191
+ }
192
+ } else {
193
+ if (globalFilter) {
194
+ return "no matching logs";
195
+ } else {
196
+ return "no logs";
197
+ }
198
+ }
199
+ }, [headersLoading, loading, globalFilter]);
200
+
201
+ return (
202
+ <div className={styles.gridContainer}>
203
+ <div className={styles.grid}>
204
+ {/* Header */}
205
+ <div
206
+ className={styles.headerRow}
207
+ style={{
208
+ gridTemplateColumns:
209
+ table
210
+ .getHeaderGroups()[0]
211
+ ?.headers.map((header) => `${header.getSize()}px`)
212
+ .join(" ") || "40px 0.5fr 0.25fr 0.25fr 0.1fr",
213
+ }}
214
+ >
215
+ {table.getHeaderGroups().map((headerGroup) =>
216
+ headerGroup.headers.map((header) => (
217
+ <div
218
+ key={header.id}
219
+ className={clsx(styles.headerCell, {
220
+ [styles.sortable]: header.column.getCanSort(),
221
+ [styles.resizing]: header.column.getIsResizing(),
222
+ })}
223
+ onClick={(event) => {
224
+ header.column.getToggleSortingHandler()?.(event);
225
+ }}
226
+ style={{
227
+ width: header.getSize(),
228
+ position: "relative",
229
+ }}
230
+ >
231
+ {header.isPlaceholder
232
+ ? null
233
+ : flexRender(
234
+ header.column.columnDef.header,
235
+ header.getContext(),
236
+ )}
237
+ {header.column.getCanSort() && (
238
+ <span className={styles.sortIndicator}>
239
+ {{
240
+ asc: " ↑",
241
+ desc: " ↓",
242
+ }[header.column.getIsSorted() as string] ?? ""}
243
+ </span>
244
+ )}
245
+ {header.column.getCanResize() && (
246
+ <div
247
+ onMouseDown={(e) => {
248
+ e.stopPropagation();
249
+ header.getResizeHandler()(e);
250
+ }}
251
+ onTouchStart={(e) => {
252
+ e.stopPropagation();
253
+ header.getResizeHandler()(e);
254
+ }}
255
+ onClick={(e) => {
256
+ e.stopPropagation();
257
+ }}
258
+ className={clsx(styles.resizer, {
259
+ [styles.isResizing]: header.column.getIsResizing(),
260
+ })}
261
+ />
262
+ )}
263
+ </div>
264
+ )),
265
+ )}
266
+ </div>
267
+
268
+ {/* Body */}
269
+ <div className={styles.bodyContainer}>
270
+ {table.getRowModel().rows.length === 0 && (
271
+ <div className={styles.emptyMessage}>{placeholderText}</div>
272
+ )}
273
+ {table.getRowModel().rows.map((row) => (
274
+ <div
275
+ key={row.id}
276
+ className={styles.bodyRow}
277
+ style={{
278
+ gridTemplateColumns: row
279
+ .getVisibleCells()
280
+ .map((cell) => `${cell.column.getSize()}px`)
281
+ .join(" "),
282
+ }}
283
+ >
284
+ {row.getVisibleCells().map((cell) => (
285
+ <div
286
+ key={cell.id}
287
+ className={clsx(
288
+ styles.bodyCell,
289
+ styles[`${cell.column.id}Cell`],
290
+ )}
291
+ style={{
292
+ width: cell.column.getSize(),
293
+ }}
294
+ >
295
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
296
+ </div>
297
+ ))}
298
+ </div>
299
+ ))}
300
+ </div>
301
+ </div>
302
+ </div>
303
+ );
304
+ };
@@ -0,0 +1,6 @@
1
+ .dateCell {
2
+ display: flex;
3
+ align-items: center;
4
+ min-width: 0;
5
+ flex: 1;
6
+ }
@@ -0,0 +1,64 @@
1
+ import { FileLogItem, FolderLogItem } from "../../LogItem";
2
+ import { columnHelper } from "./columns";
3
+
4
+ import styles from "./CompletedDate.module.css";
5
+ import { EmptyCell } from "./EmptyCell";
6
+
7
+ export const completedDateColumn = () => {
8
+ return columnHelper.accessor(
9
+ (row) => {
10
+ const completed = itemCompletedAt(row);
11
+ if (!completed) return "";
12
+ const time = new Date(completed);
13
+ return `${time.toDateString()} ${time.toLocaleTimeString([], {
14
+ hour: "2-digit",
15
+ minute: "2-digit",
16
+ })}`;
17
+ },
18
+ {
19
+ id: "completed",
20
+ header: "Completed",
21
+ cell: (info) => {
22
+ const item = info.row.original;
23
+ const completed = itemCompletedAt(item);
24
+ const time = completed ? new Date(completed) : undefined;
25
+ const timeStr = time
26
+ ? `${time.toDateString()}
27
+ ${time.toLocaleTimeString([], {
28
+ hour: "2-digit",
29
+ minute: "2-digit",
30
+ })}`
31
+ : "";
32
+
33
+ if (!timeStr) {
34
+ return <EmptyCell />;
35
+ }
36
+
37
+ return <div className={styles.dateCell}>{timeStr}</div>;
38
+ },
39
+ sortingFn: (rowA, rowB) => {
40
+ const itemA = rowA.original as FileLogItem | FolderLogItem;
41
+ const itemB = rowB.original as FileLogItem | FolderLogItem;
42
+
43
+ const completedA = itemCompletedAt(itemA);
44
+ const completedB = itemCompletedAt(itemB);
45
+
46
+ const timeA = new Date(completedA || 0);
47
+ const timeB = new Date(completedB || 0);
48
+ return timeA.getTime() - timeB.getTime();
49
+ },
50
+
51
+ enableSorting: true,
52
+ enableGlobalFilter: true,
53
+ size: 200,
54
+ minSize: 120,
55
+ maxSize: 300,
56
+ enableResizing: true,
57
+ },
58
+ );
59
+ };
60
+
61
+ const itemCompletedAt = (item: FileLogItem | FolderLogItem) => {
62
+ if (item.type !== "file") return undefined;
63
+ return item.header?.stats?.completed_at;
64
+ };
@@ -0,0 +1,3 @@
1
+ .emptyCell {
2
+ display: block;
3
+ }
@@ -0,0 +1,7 @@
1
+ import { FC } from "react";
2
+
3
+ import styles from "./EmptyCell.module.css";
4
+
5
+ export const EmptyCell: FC = () => {
6
+ return <div className={styles.emptyCell}>-</div>;
7
+ };
@@ -0,0 +1,20 @@
1
+ .nameCell {
2
+ display: flex;
3
+ align-items: center;
4
+ min-width: 0;
5
+ flex: 1;
6
+ }
7
+
8
+ .fileLink {
9
+ color: var(--bs-body-color);
10
+ text-decoration: none;
11
+ overflow: hidden;
12
+ text-overflow: ellipsis;
13
+ white-space: nowrap;
14
+ max-width: 100%;
15
+ }
16
+
17
+ .fileLink:hover {
18
+ color: var(--bs-link-hover-color);
19
+ text-decoration: underline;
20
+ }