inspect-ai 0.3.80__py3-none-any.whl → 0.3.82__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/_cli/eval.py +35 -2
- inspect_ai/_cli/util.py +44 -1
- inspect_ai/_display/core/config.py +1 -1
- inspect_ai/_display/core/display.py +13 -4
- inspect_ai/_display/core/results.py +1 -1
- inspect_ai/_display/textual/widgets/task_detail.py +5 -4
- inspect_ai/_eval/eval.py +38 -1
- inspect_ai/_eval/evalset.py +5 -0
- inspect_ai/_eval/run.py +5 -2
- inspect_ai/_eval/task/log.py +53 -6
- inspect_ai/_eval/task/run.py +51 -10
- inspect_ai/_util/constants.py +2 -0
- inspect_ai/_util/file.py +17 -1
- inspect_ai/_util/json.py +36 -1
- inspect_ai/_view/server.py +113 -1
- inspect_ai/_view/www/App.css +1 -1
- inspect_ai/_view/www/dist/assets/index.css +518 -296
- inspect_ai/_view/www/dist/assets/index.js +38803 -36307
- inspect_ai/_view/www/eslint.config.mjs +1 -1
- inspect_ai/_view/www/log-schema.json +13 -0
- inspect_ai/_view/www/node_modules/flatted/python/flatted.py +149 -0
- inspect_ai/_view/www/package.json +8 -2
- inspect_ai/_view/www/src/App.tsx +151 -855
- inspect_ai/_view/www/src/api/api-browser.ts +176 -5
- inspect_ai/_view/www/src/api/api-vscode.ts +75 -1
- inspect_ai/_view/www/src/api/client-api.ts +66 -10
- inspect_ai/_view/www/src/api/jsonrpc.ts +2 -0
- inspect_ai/_view/www/src/api/types.ts +107 -2
- inspect_ai/_view/www/src/appearance/icons.ts +1 -0
- inspect_ai/_view/www/src/components/AsciinemaPlayer.tsx +3 -3
- inspect_ai/_view/www/src/components/DownloadPanel.tsx +2 -2
- inspect_ai/_view/www/src/components/ExpandablePanel.tsx +56 -61
- inspect_ai/_view/www/src/components/FindBand.tsx +17 -9
- inspect_ai/_view/www/src/components/HumanBaselineView.tsx +1 -1
- inspect_ai/_view/www/src/components/JsonPanel.tsx +14 -24
- inspect_ai/_view/www/src/components/LargeModal.tsx +2 -35
- inspect_ai/_view/www/src/components/LightboxCarousel.tsx +27 -11
- inspect_ai/_view/www/src/components/LiveVirtualList.module.css +11 -0
- inspect_ai/_view/www/src/components/LiveVirtualList.tsx +177 -0
- inspect_ai/_view/www/src/components/MarkdownDiv.tsx +3 -3
- inspect_ai/_view/www/src/components/MessageBand.tsx +14 -9
- inspect_ai/_view/www/src/components/MorePopOver.tsx +3 -3
- inspect_ai/_view/www/src/components/NavPills.tsx +20 -8
- inspect_ai/_view/www/src/components/NoContentsPanel.module.css +12 -0
- inspect_ai/_view/www/src/components/NoContentsPanel.tsx +20 -0
- inspect_ai/_view/www/src/components/ProgressBar.module.css +5 -4
- inspect_ai/_view/www/src/components/ProgressBar.tsx +3 -2
- inspect_ai/_view/www/src/components/PulsingDots.module.css +81 -0
- inspect_ai/_view/www/src/components/PulsingDots.tsx +45 -0
- inspect_ai/_view/www/src/components/TabSet.tsx +4 -37
- inspect_ai/_view/www/src/components/ToolButton.tsx +3 -4
- inspect_ai/_view/www/src/index.tsx +26 -94
- inspect_ai/_view/www/src/logfile/remoteLogFile.ts +9 -1
- inspect_ai/_view/www/src/logfile/remoteZipFile.ts +30 -4
- inspect_ai/_view/www/src/metadata/RenderedContent.tsx +4 -6
- inspect_ai/_view/www/src/plan/ScorerDetailView.tsx +1 -1
- inspect_ai/_view/www/src/samples/InlineSampleDisplay.module.css +9 -1
- inspect_ai/_view/www/src/samples/InlineSampleDisplay.tsx +67 -28
- inspect_ai/_view/www/src/samples/SampleDialog.tsx +51 -22
- inspect_ai/_view/www/src/samples/SampleDisplay.module.css +4 -0
- inspect_ai/_view/www/src/samples/SampleDisplay.tsx +144 -90
- inspect_ai/_view/www/src/samples/SampleSummaryView.module.css +4 -0
- inspect_ai/_view/www/src/samples/SampleSummaryView.tsx +82 -35
- inspect_ai/_view/www/src/samples/SamplesTools.tsx +23 -30
- inspect_ai/_view/www/src/samples/chat/ChatMessage.tsx +2 -1
- inspect_ai/_view/www/src/samples/chat/ChatMessageRenderer.tsx +1 -1
- inspect_ai/_view/www/src/samples/chat/ChatViewVirtualList.tsx +45 -53
- inspect_ai/_view/www/src/samples/chat/MessageContent.tsx +4 -1
- inspect_ai/_view/www/src/samples/chat/MessageContents.tsx +3 -0
- inspect_ai/_view/www/src/samples/chat/messages.ts +34 -0
- inspect_ai/_view/www/src/samples/chat/tools/ToolCallView.module.css +3 -0
- inspect_ai/_view/www/src/samples/chat/tools/ToolCallView.tsx +10 -1
- inspect_ai/_view/www/src/samples/chat/tools/ToolInput.tsx +22 -46
- inspect_ai/_view/www/src/samples/descriptor/samplesDescriptor.tsx +25 -17
- inspect_ai/_view/www/src/samples/descriptor/score/ObjectScoreDescriptor.tsx +2 -1
- inspect_ai/_view/www/src/samples/descriptor/types.ts +6 -5
- inspect_ai/_view/www/src/samples/list/SampleFooter.module.css +21 -3
- inspect_ai/_view/www/src/samples/list/SampleFooter.tsx +20 -1
- inspect_ai/_view/www/src/samples/list/SampleList.tsx +105 -85
- inspect_ai/_view/www/src/samples/list/SampleRow.module.css +6 -0
- inspect_ai/_view/www/src/samples/list/SampleRow.tsx +27 -14
- inspect_ai/_view/www/src/samples/sample-tools/SelectScorer.tsx +29 -18
- inspect_ai/_view/www/src/samples/sample-tools/SortFilter.tsx +28 -28
- inspect_ai/_view/www/src/samples/sample-tools/sample-filter/SampleFilter.tsx +19 -9
- inspect_ai/_view/www/src/samples/sampleDataAdapter.ts +33 -0
- inspect_ai/_view/www/src/samples/sampleLimit.ts +2 -2
- inspect_ai/_view/www/src/samples/scores/SampleScoreView.tsx +7 -9
- inspect_ai/_view/www/src/samples/scores/SampleScores.tsx +7 -11
- inspect_ai/_view/www/src/samples/transcript/ErrorEventView.tsx +0 -13
- inspect_ai/_view/www/src/samples/transcript/InfoEventView.tsx +0 -13
- inspect_ai/_view/www/src/samples/transcript/InputEventView.tsx +0 -13
- inspect_ai/_view/www/src/samples/transcript/ModelEventView.module.css +4 -0
- inspect_ai/_view/www/src/samples/transcript/ModelEventView.tsx +10 -24
- inspect_ai/_view/www/src/samples/transcript/SampleInitEventView.tsx +0 -13
- inspect_ai/_view/www/src/samples/transcript/SampleLimitEventView.tsx +4 -22
- inspect_ai/_view/www/src/samples/transcript/SandboxEventView.tsx +15 -24
- inspect_ai/_view/www/src/samples/transcript/ScoreEventView.tsx +0 -13
- inspect_ai/_view/www/src/samples/transcript/StepEventView.tsx +6 -28
- inspect_ai/_view/www/src/samples/transcript/SubtaskEventView.tsx +24 -34
- inspect_ai/_view/www/src/samples/transcript/ToolEventView.module.css +4 -0
- inspect_ai/_view/www/src/samples/transcript/ToolEventView.tsx +8 -13
- inspect_ai/_view/www/src/samples/transcript/TranscriptView.tsx +197 -338
- inspect_ai/_view/www/src/samples/transcript/TranscriptVirtualListComponent.module.css +16 -0
- inspect_ai/_view/www/src/samples/transcript/TranscriptVirtualListComponent.tsx +44 -0
- inspect_ai/_view/www/src/samples/transcript/event/EventNav.tsx +7 -4
- inspect_ai/_view/www/src/samples/transcript/event/EventPanel.tsx +52 -58
- inspect_ai/_view/www/src/samples/transcript/event/EventProgressPanel.module.css +23 -0
- inspect_ai/_view/www/src/samples/transcript/event/EventProgressPanel.tsx +27 -0
- inspect_ai/_view/www/src/samples/transcript/state/StateEventRenderers.tsx +30 -1
- inspect_ai/_view/www/src/samples/transcript/state/StateEventView.tsx +102 -72
- inspect_ai/_view/www/src/scoring/utils.ts +87 -0
- inspect_ai/_view/www/src/state/appSlice.ts +244 -0
- inspect_ai/_view/www/src/state/hooks.ts +397 -0
- inspect_ai/_view/www/src/state/logPolling.ts +196 -0
- inspect_ai/_view/www/src/state/logSlice.ts +214 -0
- inspect_ai/_view/www/src/state/logsPolling.ts +118 -0
- inspect_ai/_view/www/src/state/logsSlice.ts +181 -0
- inspect_ai/_view/www/src/state/samplePolling.ts +311 -0
- inspect_ai/_view/www/src/state/sampleSlice.ts +127 -0
- inspect_ai/_view/www/src/state/sampleUtils.ts +21 -0
- inspect_ai/_view/www/src/state/scrolling.ts +206 -0
- inspect_ai/_view/www/src/state/store.ts +168 -0
- inspect_ai/_view/www/src/state/store_filter.ts +84 -0
- inspect_ai/_view/www/src/state/utils.ts +23 -0
- inspect_ai/_view/www/src/storage/index.ts +26 -0
- inspect_ai/_view/www/src/types/log.d.ts +2 -0
- inspect_ai/_view/www/src/types.ts +94 -32
- inspect_ai/_view/www/src/utils/attachments.ts +58 -23
- inspect_ai/_view/www/src/utils/logger.ts +52 -0
- inspect_ai/_view/www/src/utils/polling.ts +100 -0
- inspect_ai/_view/www/src/utils/react.ts +30 -0
- inspect_ai/_view/www/src/utils/vscode.ts +1 -1
- inspect_ai/_view/www/src/workspace/WorkSpace.tsx +181 -216
- inspect_ai/_view/www/src/workspace/WorkSpaceView.tsx +11 -53
- inspect_ai/_view/www/src/workspace/navbar/Navbar.tsx +8 -18
- inspect_ai/_view/www/src/workspace/navbar/PrimaryBar.module.css +1 -0
- inspect_ai/_view/www/src/workspace/navbar/PrimaryBar.tsx +40 -22
- inspect_ai/_view/www/src/workspace/navbar/ResultsPanel.module.css +0 -1
- inspect_ai/_view/www/src/workspace/navbar/ResultsPanel.tsx +98 -39
- inspect_ai/_view/www/src/workspace/navbar/RunningStatusPanel.module.css +32 -0
- inspect_ai/_view/www/src/workspace/navbar/RunningStatusPanel.tsx +32 -0
- inspect_ai/_view/www/src/workspace/navbar/SecondaryBar.tsx +11 -13
- inspect_ai/_view/www/src/workspace/navbar/StatusPanel.tsx +6 -2
- inspect_ai/_view/www/src/workspace/sidebar/LogDirectoryTitleView.tsx +4 -4
- inspect_ai/_view/www/src/workspace/sidebar/Sidebar.tsx +28 -13
- inspect_ai/_view/www/src/workspace/tabs/InfoTab.tsx +5 -10
- inspect_ai/_view/www/src/workspace/tabs/JsonTab.tsx +4 -4
- inspect_ai/_view/www/src/workspace/tabs/RunningNoSamples.module.css +22 -0
- inspect_ai/_view/www/src/workspace/tabs/RunningNoSamples.tsx +19 -0
- inspect_ai/_view/www/src/workspace/tabs/SamplesTab.tsx +110 -115
- inspect_ai/_view/www/src/workspace/tabs/grouping.ts +37 -5
- inspect_ai/_view/www/src/workspace/tabs/types.ts +4 -0
- inspect_ai/_view/www/src/workspace/types.ts +4 -3
- inspect_ai/_view/www/src/workspace/utils.ts +4 -4
- inspect_ai/_view/www/vite.config.js +6 -0
- inspect_ai/_view/www/yarn.lock +370 -354
- inspect_ai/log/_condense.py +26 -0
- inspect_ai/log/_log.py +6 -3
- inspect_ai/log/_recorders/buffer/__init__.py +14 -0
- inspect_ai/log/_recorders/buffer/buffer.py +30 -0
- inspect_ai/log/_recorders/buffer/database.py +685 -0
- inspect_ai/log/_recorders/buffer/filestore.py +259 -0
- inspect_ai/log/_recorders/buffer/types.py +84 -0
- inspect_ai/log/_recorders/eval.py +2 -11
- inspect_ai/log/_recorders/types.py +30 -0
- inspect_ai/log/_transcript.py +27 -1
- inspect_ai/model/_call_tools.py +1 -0
- inspect_ai/model/_generate_config.py +2 -2
- inspect_ai/model/_model.py +1 -0
- inspect_ai/tool/_tool_support_helpers.py +4 -4
- inspect_ai/tool/_tools/_web_browser/_web_browser.py +3 -1
- inspect_ai/util/_subtask.py +1 -0
- {inspect_ai-0.3.80.dist-info → inspect_ai-0.3.82.dist-info}/METADATA +2 -2
- {inspect_ai-0.3.80.dist-info → inspect_ai-0.3.82.dist-info}/RECORD +178 -138
- inspect_ai/_view/www/src/samples/transcript/SampleTranscript.tsx +0 -22
- {inspect_ai-0.3.80.dist-info → inspect_ai-0.3.82.dist-info}/WHEEL +0 -0
- {inspect_ai-0.3.80.dist-info → inspect_ai-0.3.82.dist-info}/entry_points.txt +0 -0
- {inspect_ai-0.3.80.dist-info → inspect_ai-0.3.82.dist-info}/licenses/LICENSE +0 -0
- {inspect_ai-0.3.80.dist-info → inspect_ai-0.3.82.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,311 @@
|
|
1
|
+
import {
|
2
|
+
AttachmentData,
|
3
|
+
EventData,
|
4
|
+
SampleData,
|
5
|
+
SampleSummary,
|
6
|
+
} from "../api/types";
|
7
|
+
import { Event } from "../types";
|
8
|
+
import { resolveAttachments } from "../utils/attachments";
|
9
|
+
import { createLogger } from "../utils/logger";
|
10
|
+
import { createPolling } from "../utils/polling";
|
11
|
+
import { resolveSample } from "./sampleUtils"; // Import the shared utility
|
12
|
+
import { StoreState } from "./store";
|
13
|
+
|
14
|
+
const log = createLogger("samplePolling");
|
15
|
+
|
16
|
+
const kNoId = -1;
|
17
|
+
const kPollingInterval = 2;
|
18
|
+
const kPollingMaxRetries = 10;
|
19
|
+
|
20
|
+
// Keeps the state for polling (the last ids for events
|
21
|
+
// and attachments, the attachments and events, and
|
22
|
+
// a mapping from eventIds to event indexes to enable
|
23
|
+
// replacing events)
|
24
|
+
interface PollingState {
|
25
|
+
eventId: number;
|
26
|
+
attachmentId: number;
|
27
|
+
|
28
|
+
attachments: Record<string, string>;
|
29
|
+
|
30
|
+
eventMapping: Record<string, number>;
|
31
|
+
events: Event[];
|
32
|
+
}
|
33
|
+
|
34
|
+
export function createSamplePolling(
|
35
|
+
get: () => StoreState,
|
36
|
+
set: (fn: (state: StoreState) => void) => void,
|
37
|
+
) {
|
38
|
+
// The polling function that will be returned
|
39
|
+
let currentPolling: ReturnType<typeof createPolling> | null = null;
|
40
|
+
|
41
|
+
// handle aborts
|
42
|
+
let abortController: AbortController;
|
43
|
+
|
44
|
+
// The inintial polling state
|
45
|
+
const pollingState: PollingState = {
|
46
|
+
eventId: kNoId,
|
47
|
+
attachmentId: kNoId,
|
48
|
+
|
49
|
+
eventMapping: {},
|
50
|
+
attachments: {},
|
51
|
+
events: [],
|
52
|
+
};
|
53
|
+
|
54
|
+
// Function to start polling for a specific log file
|
55
|
+
const startPolling = (logFile: string, summary: SampleSummary) => {
|
56
|
+
// Create a unique identifier for this polling session
|
57
|
+
const pollingId = `${logFile}:${summary.id}-${summary.epoch}`;
|
58
|
+
log.debug(`Start Polling ${pollingId}`);
|
59
|
+
|
60
|
+
// If we're already polling this resource, don't restart
|
61
|
+
if (currentPolling && currentPolling.name === pollingId) {
|
62
|
+
log.debug(`Aleady polling, ignoring start`);
|
63
|
+
return;
|
64
|
+
}
|
65
|
+
|
66
|
+
// Stop any existing polling first
|
67
|
+
if (currentPolling) {
|
68
|
+
log.debug(`Resetting existing polling`);
|
69
|
+
currentPolling.stop();
|
70
|
+
|
71
|
+
// Clear any current running events
|
72
|
+
set((state) => {
|
73
|
+
state.sample.runningEvents = [];
|
74
|
+
});
|
75
|
+
|
76
|
+
// Reset the current polling state
|
77
|
+
resetPollingState(pollingState);
|
78
|
+
}
|
79
|
+
abortController = new AbortController();
|
80
|
+
|
81
|
+
// Create the polling callback
|
82
|
+
log.debug(`Polling sample: ${summary.id}-${summary.epoch}`);
|
83
|
+
const pollCallback = async () => {
|
84
|
+
const state = get();
|
85
|
+
|
86
|
+
// Get the api
|
87
|
+
const api = state.api;
|
88
|
+
if (!api) {
|
89
|
+
throw new Error("Required API is missing");
|
90
|
+
}
|
91
|
+
|
92
|
+
if (!api.get_log_sample_data) {
|
93
|
+
throw new Error("Required API get_log_sample_data is undefined.");
|
94
|
+
}
|
95
|
+
|
96
|
+
if (abortController.signal.aborted) {
|
97
|
+
return false;
|
98
|
+
}
|
99
|
+
|
100
|
+
// Fetch sample data
|
101
|
+
const eventId = pollingState.eventId;
|
102
|
+
const attachmentId = pollingState.attachmentId;
|
103
|
+
const sampleDataResponse = await api.get_log_sample_data(
|
104
|
+
logFile,
|
105
|
+
summary.id,
|
106
|
+
summary.epoch,
|
107
|
+
eventId,
|
108
|
+
attachmentId,
|
109
|
+
);
|
110
|
+
|
111
|
+
if (abortController.signal.aborted) {
|
112
|
+
return false;
|
113
|
+
}
|
114
|
+
|
115
|
+
if (sampleDataResponse?.status === "NotFound") {
|
116
|
+
// A 404 from the server means that this sample
|
117
|
+
// has been flushed to the main eval file, no events
|
118
|
+
// are available and we should retrieve the data from the
|
119
|
+
// sample file itself.
|
120
|
+
|
121
|
+
// Stop polling since we now have the complete sample
|
122
|
+
stopPolling();
|
123
|
+
|
124
|
+
// Also fetch a fresh sample and clear the runnning Events
|
125
|
+
// (if there were ever running events)
|
126
|
+
if (state.sample.runningEvents.length > 0) {
|
127
|
+
try {
|
128
|
+
log.debug(
|
129
|
+
`LOADING COMPLETED SAMPLE AFTER FLUSH: ${summary.id}-${summary.epoch}`,
|
130
|
+
);
|
131
|
+
const sample = await api.get_log_sample(
|
132
|
+
logFile,
|
133
|
+
summary.id,
|
134
|
+
summary.epoch,
|
135
|
+
);
|
136
|
+
|
137
|
+
if (sample) {
|
138
|
+
const migratedSample = resolveSample(sample);
|
139
|
+
|
140
|
+
// Update the store with the completed sample
|
141
|
+
set((state) => {
|
142
|
+
state.sample.selectedSample = migratedSample;
|
143
|
+
state.sample.sampleStatus = "ok";
|
144
|
+
state.sample.runningEvents = [];
|
145
|
+
});
|
146
|
+
} else {
|
147
|
+
set((state) => {
|
148
|
+
state.sample.sampleStatus = "error";
|
149
|
+
state.sample.sampleError = new Error(
|
150
|
+
"Unable to load sample - an unknown error occurred",
|
151
|
+
);
|
152
|
+
state.sample.runningEvents = [];
|
153
|
+
});
|
154
|
+
}
|
155
|
+
} catch (e) {
|
156
|
+
set((state) => {
|
157
|
+
state.sample.sampleError = e as Error;
|
158
|
+
state.sample.sampleStatus = "error";
|
159
|
+
state.sample.runningEvents = [];
|
160
|
+
});
|
161
|
+
}
|
162
|
+
}
|
163
|
+
|
164
|
+
return false;
|
165
|
+
}
|
166
|
+
|
167
|
+
if (
|
168
|
+
sampleDataResponse?.status === "OK" &&
|
169
|
+
sampleDataResponse.sampleData
|
170
|
+
) {
|
171
|
+
if (abortController.signal.aborted) {
|
172
|
+
return false;
|
173
|
+
}
|
174
|
+
|
175
|
+
if (sampleDataResponse.sampleData) {
|
176
|
+
// Process attachments
|
177
|
+
processAttachments(sampleDataResponse.sampleData, pollingState);
|
178
|
+
|
179
|
+
// Process events
|
180
|
+
const processedEvents = processEvents(
|
181
|
+
sampleDataResponse.sampleData,
|
182
|
+
pollingState,
|
183
|
+
);
|
184
|
+
|
185
|
+
// update max attachment id
|
186
|
+
if (sampleDataResponse.sampleData.attachments.length > 0) {
|
187
|
+
const maxAttachment = findMaxId(
|
188
|
+
sampleDataResponse.sampleData.attachments,
|
189
|
+
pollingState.attachmentId,
|
190
|
+
);
|
191
|
+
log.debug(`New max attachment ${maxAttachment}`);
|
192
|
+
pollingState.attachmentId = maxAttachment;
|
193
|
+
}
|
194
|
+
|
195
|
+
// update max event id
|
196
|
+
if (sampleDataResponse.sampleData.events.length > 0) {
|
197
|
+
const maxEvent = findMaxId(
|
198
|
+
sampleDataResponse.sampleData.events,
|
199
|
+
pollingState.eventId,
|
200
|
+
);
|
201
|
+
log.debug(`New max event ${maxEvent}`);
|
202
|
+
pollingState.eventId = maxEvent;
|
203
|
+
}
|
204
|
+
|
205
|
+
// Update the running events (ensure identity of runningEvents fails equality)
|
206
|
+
if (processedEvents) {
|
207
|
+
set((state) => {
|
208
|
+
state.sample.runningEvents = [...pollingState.events];
|
209
|
+
});
|
210
|
+
}
|
211
|
+
}
|
212
|
+
}
|
213
|
+
|
214
|
+
// Continue polling
|
215
|
+
return true;
|
216
|
+
};
|
217
|
+
|
218
|
+
// Create the polling instance
|
219
|
+
const polling = createPolling(pollingId, pollCallback, {
|
220
|
+
maxRetries: kPollingMaxRetries,
|
221
|
+
interval: kPollingInterval,
|
222
|
+
});
|
223
|
+
|
224
|
+
// Store the polling instance and start it
|
225
|
+
currentPolling = polling;
|
226
|
+
polling.start();
|
227
|
+
};
|
228
|
+
|
229
|
+
// Stop polling
|
230
|
+
const stopPolling = () => {
|
231
|
+
if (currentPolling) {
|
232
|
+
currentPolling.stop();
|
233
|
+
currentPolling = null;
|
234
|
+
}
|
235
|
+
};
|
236
|
+
|
237
|
+
const cleanup = () => {
|
238
|
+
log.debug(`CLEANUP`);
|
239
|
+
abortController.abort();
|
240
|
+
stopPolling();
|
241
|
+
};
|
242
|
+
|
243
|
+
return {
|
244
|
+
startPolling,
|
245
|
+
stopPolling,
|
246
|
+
cleanup,
|
247
|
+
};
|
248
|
+
}
|
249
|
+
|
250
|
+
const resetPollingState = (state: PollingState) => {
|
251
|
+
state.eventId = -1;
|
252
|
+
state.attachmentId = -1;
|
253
|
+
state.eventMapping = {};
|
254
|
+
state.attachments = {};
|
255
|
+
state.events = [];
|
256
|
+
};
|
257
|
+
|
258
|
+
function processAttachments(
|
259
|
+
sampleData: SampleData,
|
260
|
+
pollingState: PollingState,
|
261
|
+
) {
|
262
|
+
log.debug(`Processing ${sampleData.attachments.length} attachments`);
|
263
|
+
Object.values(sampleData.attachments).forEach((v) => {
|
264
|
+
pollingState.attachments[v.hash] = v.content;
|
265
|
+
});
|
266
|
+
}
|
267
|
+
|
268
|
+
function processEvents(sampleData: SampleData, pollingState: PollingState) {
|
269
|
+
// Go through each event and resolve it, either appending or replacing
|
270
|
+
log.debug(`Processing ${sampleData.events.length} events`);
|
271
|
+
if (sampleData.events.length === 0) {
|
272
|
+
return false;
|
273
|
+
}
|
274
|
+
|
275
|
+
for (const eventData of sampleData.events) {
|
276
|
+
// Identify if this event id already has an event in the event list
|
277
|
+
const existingIndex = pollingState.eventMapping[eventData.event_id];
|
278
|
+
|
279
|
+
// Resolve attachments within this event
|
280
|
+
const resolvedEvent = resolveAttachments<Event>(
|
281
|
+
eventData.event,
|
282
|
+
pollingState.attachments,
|
283
|
+
);
|
284
|
+
|
285
|
+
if (existingIndex) {
|
286
|
+
// There is an existing event in the stream, replace it
|
287
|
+
log.debug(`Replace event ${existingIndex}`);
|
288
|
+
pollingState.events[existingIndex] = resolvedEvent;
|
289
|
+
} else {
|
290
|
+
// This is a new event, add to the event list and note
|
291
|
+
// its position
|
292
|
+
log.debug(`New event ${pollingState.events.length}`);
|
293
|
+
|
294
|
+
const currentIndex = pollingState.events.length;
|
295
|
+
pollingState.eventMapping[eventData.event_id] = currentIndex;
|
296
|
+
pollingState.events.push(resolvedEvent);
|
297
|
+
}
|
298
|
+
}
|
299
|
+
return true;
|
300
|
+
}
|
301
|
+
|
302
|
+
const findMaxId = (
|
303
|
+
items: EventData[] | AttachmentData[],
|
304
|
+
currentMax: number,
|
305
|
+
) => {
|
306
|
+
if (items.length > 0) {
|
307
|
+
const newMax = Math.max(...items.map((i) => i.id), currentMax);
|
308
|
+
return newMax;
|
309
|
+
}
|
310
|
+
return currentMax;
|
311
|
+
};
|
@@ -0,0 +1,127 @@
|
|
1
|
+
import { SampleSummary } from "../api/types";
|
2
|
+
import { kSampleMessagesTabId } from "../constants";
|
3
|
+
import { SampleState, SampleStatus } from "../types";
|
4
|
+
import { EvalSample } from "../types/log";
|
5
|
+
import { createLogger } from "../utils/logger";
|
6
|
+
import { createSamplePolling } from "./samplePolling";
|
7
|
+
import { resolveSample } from "./sampleUtils"; // Import the shared utility
|
8
|
+
import { StoreState } from "./store";
|
9
|
+
|
10
|
+
const log = createLogger("sampleSlice");
|
11
|
+
|
12
|
+
export interface SampleSlice {
|
13
|
+
sample: SampleState;
|
14
|
+
sampleActions: {
|
15
|
+
// The actual sample data
|
16
|
+
setSelectedSample: (sample: EvalSample) => void;
|
17
|
+
clearSelectedSample: () => void;
|
18
|
+
setSampleStatus: (status: SampleStatus) => void;
|
19
|
+
setSampleError: (error: Error | undefined) => void;
|
20
|
+
|
21
|
+
// Loading
|
22
|
+
loadSample: (
|
23
|
+
logFile: string,
|
24
|
+
sampleSummary: SampleSummary,
|
25
|
+
) => Promise<void>;
|
26
|
+
};
|
27
|
+
}
|
28
|
+
|
29
|
+
const initialState: SampleState = {
|
30
|
+
selectedSample: undefined,
|
31
|
+
sampleStatus: "ok",
|
32
|
+
sampleError: undefined,
|
33
|
+
|
34
|
+
// The resolved events
|
35
|
+
runningEvents: [],
|
36
|
+
};
|
37
|
+
|
38
|
+
export const createSampleSlice = (
|
39
|
+
set: (fn: (state: StoreState) => void) => void,
|
40
|
+
get: () => StoreState,
|
41
|
+
_store: any,
|
42
|
+
): [SampleSlice, () => void] => {
|
43
|
+
// The sample poller
|
44
|
+
const samplePolling = createSamplePolling(get, set);
|
45
|
+
|
46
|
+
const slice = {
|
47
|
+
// Actions
|
48
|
+
sample: initialState,
|
49
|
+
sampleActions: {
|
50
|
+
setSelectedSample: (sample: EvalSample) => {
|
51
|
+
set((state) => {
|
52
|
+
state.sample.selectedSample = sample;
|
53
|
+
});
|
54
|
+
if (sample.events.length < 1) {
|
55
|
+
// If there are no events, use the messages tab as the default
|
56
|
+
get().appActions.setSampleTab(kSampleMessagesTabId);
|
57
|
+
}
|
58
|
+
},
|
59
|
+
clearSelectedSample: () =>
|
60
|
+
set((state) => {
|
61
|
+
state.sample.selectedSample = undefined;
|
62
|
+
}),
|
63
|
+
setSampleStatus: (status: SampleStatus) =>
|
64
|
+
set((state) => {
|
65
|
+
state.sample.sampleStatus = status;
|
66
|
+
}),
|
67
|
+
setSampleError: (error: Error | undefined) =>
|
68
|
+
set((state) => {
|
69
|
+
state.sample.sampleError = error;
|
70
|
+
}),
|
71
|
+
loadSample: async (logFile: string, sampleSummary: SampleSummary) => {
|
72
|
+
const sampleActions = get().sampleActions;
|
73
|
+
|
74
|
+
sampleActions.setSampleError(undefined);
|
75
|
+
sampleActions.setSampleStatus("loading");
|
76
|
+
try {
|
77
|
+
if (sampleSummary.completed !== false) {
|
78
|
+
log.debug(
|
79
|
+
`LOADING COMPLETED SAMPLE: ${sampleSummary.id}-${sampleSummary.epoch}`,
|
80
|
+
);
|
81
|
+
const sample = await get().api?.get_log_sample(
|
82
|
+
logFile,
|
83
|
+
sampleSummary.id,
|
84
|
+
sampleSummary.epoch,
|
85
|
+
);
|
86
|
+
if (sample) {
|
87
|
+
const migratedSample = resolveSample(sample);
|
88
|
+
sampleActions.setSelectedSample(migratedSample);
|
89
|
+
sampleActions.setSampleStatus("ok");
|
90
|
+
} else {
|
91
|
+
sampleActions.setSampleStatus("error");
|
92
|
+
throw new Error(
|
93
|
+
"Unable to load sample - an unknown error occurred",
|
94
|
+
);
|
95
|
+
}
|
96
|
+
} else {
|
97
|
+
log.debug(
|
98
|
+
`POLLING RUNNING SAMPLE: ${sampleSummary.id}-${sampleSummary.epoch}`,
|
99
|
+
);
|
100
|
+
|
101
|
+
// Poll running sample
|
102
|
+
samplePolling.startPolling(logFile, sampleSummary);
|
103
|
+
sampleActions.setSampleStatus("streaming");
|
104
|
+
}
|
105
|
+
} catch (e) {
|
106
|
+
sampleActions.setSampleError(e as Error);
|
107
|
+
sampleActions.setSampleStatus("error");
|
108
|
+
}
|
109
|
+
},
|
110
|
+
},
|
111
|
+
} as const;
|
112
|
+
|
113
|
+
const cleanup = () => {
|
114
|
+
samplePolling.cleanup();
|
115
|
+
};
|
116
|
+
return [slice, cleanup];
|
117
|
+
};
|
118
|
+
|
119
|
+
export const initializeSampleSlice = (
|
120
|
+
set: (fn: (state: StoreState) => void) => void,
|
121
|
+
) => {
|
122
|
+
set((state) => {
|
123
|
+
if (!state.sample) {
|
124
|
+
state.sample = initialState;
|
125
|
+
}
|
126
|
+
});
|
127
|
+
};
|
@@ -0,0 +1,21 @@
|
|
1
|
+
import { EvalSample } from "../types/log";
|
2
|
+
import { resolveAttachments } from "../utils/attachments";
|
3
|
+
|
4
|
+
/**
|
5
|
+
* Migrates and resolves attachments for a sample
|
6
|
+
*/
|
7
|
+
export const resolveSample = (sample: any): EvalSample => {
|
8
|
+
sample = { ...sample };
|
9
|
+
|
10
|
+
// Migrates old versions of samples to the new structure
|
11
|
+
if (sample.transcript) {
|
12
|
+
sample.events = sample.transcript.events;
|
13
|
+
sample.attachments = sample.transcript.content;
|
14
|
+
}
|
15
|
+
sample.attachments = sample.attachments || {};
|
16
|
+
sample.input = resolveAttachments(sample.input, sample.attachments);
|
17
|
+
sample.messages = resolveAttachments(sample.messages, sample.attachments);
|
18
|
+
sample.events = resolveAttachments(sample.events, sample.attachments);
|
19
|
+
sample.attachments = {};
|
20
|
+
return sample;
|
21
|
+
};
|
@@ -0,0 +1,206 @@
|
|
1
|
+
import { RefObject, useCallback, useEffect, useRef } from "react";
|
2
|
+
import { StateCallback, StateSnapshot, VirtuosoHandle } from "react-virtuoso";
|
3
|
+
import { createLogger } from "../utils/logger";
|
4
|
+
import { debounce } from "../utils/sync";
|
5
|
+
import { useStore } from "./store";
|
6
|
+
|
7
|
+
const log = createLogger("scrolling");
|
8
|
+
|
9
|
+
export function useStatefulScrollPosition<
|
10
|
+
T extends HTMLElement = HTMLDivElement,
|
11
|
+
>(
|
12
|
+
elementRef: RefObject<T | null>,
|
13
|
+
elementKey: string,
|
14
|
+
delay = 500,
|
15
|
+
scrollable = true,
|
16
|
+
) {
|
17
|
+
const getScrollPosition = useStore(
|
18
|
+
(state) => state.appActions.getScrollPosition,
|
19
|
+
);
|
20
|
+
const setScrollPosition = useStore(
|
21
|
+
(state) => state.appActions.setScrollPosition,
|
22
|
+
);
|
23
|
+
|
24
|
+
// Create debounced scroll handler
|
25
|
+
const handleScroll = useCallback(
|
26
|
+
debounce((e: Event) => {
|
27
|
+
const target = e.target as HTMLElement;
|
28
|
+
const position = target.scrollTop;
|
29
|
+
log.debug(`Storing scroll position`, elementKey, position);
|
30
|
+
setScrollPosition(elementKey, position);
|
31
|
+
}, delay),
|
32
|
+
[elementKey, setScrollPosition, delay],
|
33
|
+
);
|
34
|
+
|
35
|
+
// Function to manually restore scroll position
|
36
|
+
const restoreScrollPosition = useCallback(() => {
|
37
|
+
const element = elementRef.current;
|
38
|
+
const savedPosition = getScrollPosition(elementKey);
|
39
|
+
|
40
|
+
if (element && savedPosition !== undefined) {
|
41
|
+
requestAnimationFrame(() => {
|
42
|
+
element.scrollTop = savedPosition;
|
43
|
+
|
44
|
+
requestAnimationFrame(() => {
|
45
|
+
if (element.scrollTop !== savedPosition) {
|
46
|
+
element.scrollTop = savedPosition;
|
47
|
+
}
|
48
|
+
});
|
49
|
+
});
|
50
|
+
}
|
51
|
+
}, [elementKey, getScrollPosition, elementRef]);
|
52
|
+
|
53
|
+
// Set up scroll listener and restore position on mount
|
54
|
+
useEffect(() => {
|
55
|
+
const element = elementRef.current;
|
56
|
+
if (!element || !scrollable) {
|
57
|
+
return;
|
58
|
+
}
|
59
|
+
log.debug(`Restore Scroll Hook`, elementKey);
|
60
|
+
|
61
|
+
// Restore scroll position on mount
|
62
|
+
const savedPosition = getScrollPosition(elementKey);
|
63
|
+
if (savedPosition !== undefined) {
|
64
|
+
log.debug(`Restoring scroll position`, savedPosition);
|
65
|
+
// Ensure the element has fully rendered
|
66
|
+
requestAnimationFrame(() => {
|
67
|
+
if (element.scrollTop !== savedPosition) {
|
68
|
+
element.scrollTop = savedPosition;
|
69
|
+
}
|
70
|
+
});
|
71
|
+
}
|
72
|
+
|
73
|
+
// Set up scroll listener
|
74
|
+
if (element.addEventListener) {
|
75
|
+
element.addEventListener("scroll", handleScroll);
|
76
|
+
} else {
|
77
|
+
log.warn("Element has no way to add event listener", element);
|
78
|
+
}
|
79
|
+
|
80
|
+
// Clean up
|
81
|
+
return () => {
|
82
|
+
if (element.removeEventListener) {
|
83
|
+
element.removeEventListener("scroll", handleScroll);
|
84
|
+
} else {
|
85
|
+
log.warn("Element has no way to remove event listener", element);
|
86
|
+
}
|
87
|
+
};
|
88
|
+
}, [elementKey, elementRef, handleScroll]);
|
89
|
+
|
90
|
+
return { restoreScrollPosition };
|
91
|
+
}
|
92
|
+
|
93
|
+
// Define a type for the debounced function that includes the cancel method
|
94
|
+
type DebouncedFunction<T extends (...args: any[]) => any> = T & {
|
95
|
+
cancel: () => void;
|
96
|
+
flush: () => void;
|
97
|
+
};
|
98
|
+
|
99
|
+
export const useVirtuosoState = (
|
100
|
+
virtuosoRef: RefObject<VirtuosoHandle | null>,
|
101
|
+
elementKey: string,
|
102
|
+
delay = 1000,
|
103
|
+
) => {
|
104
|
+
// Use useCallback to stabilize the selectors
|
105
|
+
const restoreState = useStore(
|
106
|
+
useCallback((state) => state.app.listPositions[elementKey], [elementKey]),
|
107
|
+
);
|
108
|
+
|
109
|
+
const setListPosition = useStore(
|
110
|
+
useCallback((state) => state.appActions.setListPosition, []),
|
111
|
+
);
|
112
|
+
|
113
|
+
const clearListPosition = useStore(
|
114
|
+
useCallback((state) => state.appActions.clearListPosition, []),
|
115
|
+
);
|
116
|
+
|
117
|
+
// Properly type the debounced function ref
|
118
|
+
const debouncedFnRef = useRef<DebouncedFunction<
|
119
|
+
(isScrolling: boolean) => void
|
120
|
+
> | null>(null);
|
121
|
+
|
122
|
+
// Create the state change handler
|
123
|
+
const handleStateChange: StateCallback = useCallback(
|
124
|
+
(state: StateSnapshot) => {
|
125
|
+
log.debug(`Storing list state: [${elementKey}]`, state);
|
126
|
+
setListPosition(elementKey, state);
|
127
|
+
},
|
128
|
+
[elementKey, setListPosition],
|
129
|
+
);
|
130
|
+
|
131
|
+
// Setup the debounced function once
|
132
|
+
useEffect(() => {
|
133
|
+
debouncedFnRef.current = debounce((isScrolling: boolean) => {
|
134
|
+
log.debug("List scroll", isScrolling);
|
135
|
+
const element = virtuosoRef.current;
|
136
|
+
if (!element) {
|
137
|
+
return;
|
138
|
+
}
|
139
|
+
element.getState(handleStateChange);
|
140
|
+
}, delay) as DebouncedFunction<(isScrolling: boolean) => void>;
|
141
|
+
|
142
|
+
return () => {
|
143
|
+
// Clear the stored position when component unmounts
|
144
|
+
clearListPosition(elementKey);
|
145
|
+
};
|
146
|
+
}, [delay, elementKey, handleStateChange, clearListPosition, virtuosoRef]);
|
147
|
+
|
148
|
+
// Return a stable function reference that uses the ref internally
|
149
|
+
const isScrolling = useCallback((scrolling: boolean) => {
|
150
|
+
if (!scrolling) {
|
151
|
+
return;
|
152
|
+
}
|
153
|
+
|
154
|
+
if (debouncedFnRef.current) {
|
155
|
+
debouncedFnRef.current(scrolling);
|
156
|
+
}
|
157
|
+
}, []);
|
158
|
+
|
159
|
+
// Use a state to prevent re-rendering just because the restore
|
160
|
+
// state changes
|
161
|
+
const stateRef = useRef(restoreState);
|
162
|
+
useEffect(() => {
|
163
|
+
stateRef.current = restoreState;
|
164
|
+
}, [restoreState]);
|
165
|
+
|
166
|
+
const getRestoreState = useCallback(() => stateRef.current, []);
|
167
|
+
|
168
|
+
return { getRestoreState, isScrolling };
|
169
|
+
};
|
170
|
+
|
171
|
+
export function useRafThrottle<T extends (...args: any[]) => any>(
|
172
|
+
callback: T,
|
173
|
+
dependencies: any[] = [],
|
174
|
+
): (...args: Parameters<T>) => void {
|
175
|
+
const rafRef = useRef<number | null>(null);
|
176
|
+
const callbackRef = useRef<T>(callback);
|
177
|
+
|
178
|
+
// Update the callback ref when the callback changes
|
179
|
+
useEffect(() => {
|
180
|
+
callbackRef.current = callback;
|
181
|
+
}, [callback, ...dependencies]);
|
182
|
+
|
183
|
+
const throttledCallback = useCallback((...args: Parameters<T>) => {
|
184
|
+
// Skip if we already have a frame queued
|
185
|
+
if (rafRef.current) {
|
186
|
+
return;
|
187
|
+
}
|
188
|
+
|
189
|
+
rafRef.current = requestAnimationFrame(() => {
|
190
|
+
callbackRef.current(...args);
|
191
|
+
rafRef.current = null;
|
192
|
+
});
|
193
|
+
}, []);
|
194
|
+
|
195
|
+
// Clean up any pending animation frame on unmount
|
196
|
+
useEffect(() => {
|
197
|
+
return () => {
|
198
|
+
if (rafRef.current) {
|
199
|
+
cancelAnimationFrame(rafRef.current);
|
200
|
+
rafRef.current = null;
|
201
|
+
}
|
202
|
+
};
|
203
|
+
}, []);
|
204
|
+
|
205
|
+
return throttledCallback;
|
206
|
+
}
|