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.
- inspect_ai/_eval/task/log.py +1 -1
- inspect_ai/_eval/task/run.py +1 -1
- inspect_ai/_util/dateutil.py +40 -0
- inspect_ai/_view/schema.py +11 -0
- inspect_ai/_view/www/CLAUDE.md +1 -1
- inspect_ai/_view/www/dist/assets/index.css +2068 -1796
- inspect_ai/_view/www/dist/assets/index.js +7951 -3643
- inspect_ai/_view/www/package.json +3 -2
- inspect_ai/_view/www/src/@types/log.d.ts +5 -5
- inspect_ai/_view/www/src/app/App.css +71 -4
- inspect_ai/_view/www/src/app/App.tsx +7 -0
- inspect_ai/_view/www/src/app/appearance/icons.ts +18 -2
- inspect_ai/_view/www/src/app/content/RenderedContent.tsx +7 -9
- inspect_ai/_view/www/src/app/log-list/LogItem.ts +18 -0
- inspect_ai/_view/www/src/app/log-list/LogListFooter.module.css +55 -0
- inspect_ai/_view/www/src/app/log-list/LogListFooter.tsx +67 -0
- inspect_ai/_view/www/src/app/log-list/LogPager.module.css +29 -0
- inspect_ai/_view/www/src/app/log-list/LogPager.tsx +134 -0
- inspect_ai/_view/www/src/app/log-list/LogsFilterInput.module.css +5 -0
- inspect_ai/_view/www/src/app/log-list/LogsFilterInput.tsx +31 -0
- inspect_ai/_view/www/src/app/log-list/LogsPanel.module.css +12 -0
- inspect_ai/_view/www/src/app/log-list/LogsPanel.tsx +178 -0
- inspect_ai/_view/www/src/app/log-list/grid/LogListGrid.module.css +115 -0
- inspect_ai/_view/www/src/app/log-list/grid/LogListGrid.tsx +304 -0
- inspect_ai/_view/www/src/app/log-list/grid/columns/CompletedDate.module.css +6 -0
- inspect_ai/_view/www/src/app/log-list/grid/columns/CompletedDate.tsx +64 -0
- inspect_ai/_view/www/src/app/log-list/grid/columns/EmptyCell.module.css +3 -0
- inspect_ai/_view/www/src/app/log-list/grid/columns/EmptyCell.tsx +7 -0
- inspect_ai/_view/www/src/app/log-list/grid/columns/FileName.module.css +20 -0
- inspect_ai/_view/www/src/app/log-list/grid/columns/FileName.tsx +52 -0
- inspect_ai/_view/www/src/app/log-list/grid/columns/Icon.module.css +11 -0
- inspect_ai/_view/www/src/app/log-list/grid/columns/Icon.tsx +35 -0
- inspect_ai/_view/www/src/app/log-list/grid/columns/Model.module.css +6 -0
- inspect_ai/_view/www/src/app/log-list/grid/columns/Model.tsx +34 -0
- inspect_ai/_view/www/src/app/log-list/grid/columns/Score.module.css +6 -0
- inspect_ai/_view/www/src/app/log-list/grid/columns/Score.tsx +61 -0
- inspect_ai/_view/www/src/app/log-list/grid/columns/Status.module.css +15 -0
- inspect_ai/_view/www/src/app/log-list/grid/columns/Status.tsx +95 -0
- inspect_ai/_view/www/src/app/log-list/grid/columns/Task.module.css +20 -0
- inspect_ai/_view/www/src/app/log-list/grid/columns/Task.tsx +50 -0
- inspect_ai/_view/www/src/app/log-list/grid/columns/columns.ts +27 -0
- inspect_ai/_view/www/src/app/log-view/LogView.tsx +2 -5
- inspect_ai/_view/www/src/app/log-view/LogViewContainer.tsx +4 -30
- inspect_ai/_view/www/src/app/log-view/LogViewLayout.tsx +5 -30
- inspect_ai/_view/www/src/app/log-view/tabs/TaskTab.tsx +4 -7
- inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/PrimaryBar.module.css +2 -0
- inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/PrimaryBar.tsx +3 -31
- inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/ResultsPanel.tsx +7 -57
- inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/ScoreGrid.tsx +2 -2
- inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/SecondaryBar.tsx +7 -1
- inspect_ai/_view/www/src/app/log-view/{navbar/Navbar.tsx → title-view/TitleView.tsx} +3 -6
- inspect_ai/_view/www/src/app/navbar/Navbar.module.css +57 -0
- inspect_ai/_view/www/src/app/navbar/Navbar.tsx +117 -0
- inspect_ai/_view/www/src/app/navbar/useBreadcrumbTruncation.ts +128 -0
- inspect_ai/_view/www/src/app/plan/DatasetDetailView.tsx +3 -3
- inspect_ai/_view/www/src/app/plan/DetailStep.tsx +6 -6
- inspect_ai/_view/www/src/app/plan/PlanDetailView.module.css +1 -0
- inspect_ai/_view/www/src/app/plan/ScorerDetailView.tsx +1 -1
- inspect_ai/_view/www/src/app/routing/AppRouter.tsx +28 -4
- inspect_ai/_view/www/src/app/routing/RouteDispatcher.tsx +28 -0
- inspect_ai/_view/www/src/app/routing/sampleNavigation.ts +76 -7
- inspect_ai/_view/www/src/app/routing/url.ts +193 -20
- inspect_ai/_view/www/src/app/samples/SampleDisplay.tsx +3 -17
- inspect_ai/_view/www/src/app/samples/descriptor/score/ScoreDescriptor.tsx +1 -1
- inspect_ai/_view/www/src/app/samples/transcript/SubtaskEventView.tsx +2 -2
- inspect_ai/_view/www/src/app/samples/transcript/TranscriptPanel.tsx +2 -2
- inspect_ai/_view/www/src/app/samples/transcript/outline/tree-visitors.ts +5 -0
- inspect_ai/_view/www/src/app/samples/transcript/transform/treeify.ts +26 -10
- inspect_ai/_view/www/src/app/types.ts +21 -1
- inspect_ai/_view/www/src/client/api/api-http.ts +2 -1
- inspect_ai/_view/www/src/client/api/api-shared.ts +0 -32
- inspect_ai/_view/www/src/client/api/client-api.ts +1 -1
- inspect_ai/_view/www/src/client/remote/remoteLogFile.ts +38 -6
- inspect_ai/_view/www/src/components/TextInput.module.css +45 -0
- inspect_ai/_view/www/src/components/TextInput.tsx +52 -0
- inspect_ai/_view/www/src/constants.ts +18 -0
- inspect_ai/_view/www/src/img/inspect-16.svg +10 -0
- inspect_ai/_view/www/src/img/inspect-back.svg +5 -0
- inspect_ai/_view/www/src/img/inspect-file.svg +26 -0
- inspect_ai/_view/www/src/img/inspect-forward.svg +7 -0
- inspect_ai/_view/www/src/img/inspect-home.svg +18 -0
- inspect_ai/_view/www/src/scoring/metrics.ts +75 -0
- inspect_ai/_view/www/src/scoring/scores.ts +19 -0
- inspect_ai/_view/www/src/scoring/types.ts +11 -0
- inspect_ai/_view/www/src/state/appSlice.ts +27 -7
- inspect_ai/_view/www/src/state/clientEvents.ts +73 -0
- inspect_ai/_view/www/src/state/clientEventsService.ts +105 -0
- inspect_ai/_view/www/src/state/hooks.ts +118 -1
- inspect_ai/_view/www/src/state/log.ts +19 -0
- inspect_ai/_view/www/src/state/logPolling.ts +3 -1
- inspect_ai/_view/www/src/state/logSlice.ts +9 -0
- inspect_ai/_view/www/src/state/logsSlice.ts +157 -15
- inspect_ai/_view/www/src/state/samplePolling.ts +4 -2
- inspect_ai/_view/www/src/tests/utils/path.test.ts +3 -3
- inspect_ai/_view/www/src/utils/evallog.ts +31 -0
- inspect_ai/_view/www/src/utils/path.ts +28 -0
- inspect_ai/_view/www/src/utils/uri.ts +49 -0
- inspect_ai/_view/www/yarn.lock +54 -17
- inspect_ai/analysis/beta/_dataframe/util.py +106 -10
- inspect_ai/log/_recorders/buffer/database.py +55 -16
- inspect_ai/model/_model.py +1 -1
- inspect_ai/model/_providers/providers.py +2 -2
- inspect_ai/model/_providers/vertex.py +3 -0
- inspect_ai/tool/_mcp/_mcp.py +6 -1
- inspect_ai/tool/_mcp/sampling.py +8 -1
- inspect_ai/tool/_tools/_bash_session.py +3 -6
- inspect_ai/tool/_tools/_web_browser/_web_browser.py +3 -8
- inspect_ai/util/_anyio.py +12 -3
- {inspect_ai-0.3.108.dist-info → inspect_ai-0.3.109.dist-info}/METADATA +2 -2
- {inspect_ai-0.3.108.dist-info → inspect_ai-0.3.109.dist-info}/RECORD +124 -94
- inspect_ai/_util/datetime.py +0 -10
- inspect_ai/_view/www/src/app/content/MetaDataView.module.css +0 -35
- inspect_ai/_view/www/src/app/content/MetaDataView.tsx +0 -101
- inspect_ai/_view/www/src/app/log-view/utils.ts +0 -34
- inspect_ai/_view/www/src/app/sidebar/EvalStatus.module.css +0 -15
- inspect_ai/_view/www/src/app/sidebar/EvalStatus.tsx +0 -72
- inspect_ai/_view/www/src/app/sidebar/LogDirectoryTitleView.module.css +0 -16
- inspect_ai/_view/www/src/app/sidebar/LogDirectoryTitleView.tsx +0 -70
- inspect_ai/_view/www/src/app/sidebar/Sidebar.module.css +0 -77
- inspect_ai/_view/www/src/app/sidebar/Sidebar.tsx +0 -119
- inspect_ai/_view/www/src/app/sidebar/SidebarLogEntry.module.css +0 -29
- inspect_ai/_view/www/src/app/sidebar/SidebarLogEntry.tsx +0 -96
- inspect_ai/_view/www/src/app/sidebar/SidebarScoreView.module.css +0 -23
- inspect_ai/_view/www/src/app/sidebar/SidebarScoreView.tsx +0 -44
- inspect_ai/_view/www/src/app/sidebar/SidebarScoresView.module.css +0 -35
- inspect_ai/_view/www/src/app/sidebar/SidebarScoresView.tsx +0 -63
- inspect_ai/_view/www/src/state/logsPolling.ts +0 -118
- /inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/ModelRolesView.module.css +0 -0
- /inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/ModelRolesView.tsx +0 -0
- /inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/ResultsPanel.module.css +0 -0
- /inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/RunningStatusPanel.module.css +0 -0
- /inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/RunningStatusPanel.tsx +0 -0
- /inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/ScoreGrid.module.css +0 -0
- /inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/SecondaryBar.module.css +0 -0
- /inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/StatusPanel.module.css +0 -0
- /inspect_ai/_view/www/src/app/log-view/{navbar → title-view}/StatusPanel.tsx +0 -0
- /inspect_ai/_view/www/src/app/log-view/{navbar/Navbar.module.css → title-view/TitleView.module.css} +0 -0
- {inspect_ai-0.3.108.dist-info → inspect_ai-0.3.109.dist-info}/WHEEL +0 -0
- {inspect_ai-0.3.108.dist-info → inspect_ai-0.3.109.dist-info}/entry_points.txt +0 -0
- {inspect_ai-0.3.108.dist-info → inspect_ai-0.3.109.dist-info}/licenses/LICENSE +0 -0
- {inspect_ai-0.3.108.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,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,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
|
+
}
|