inspect-ai 0.3.99__py3-none-any.whl → 0.3.101__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 +2 -1
- inspect_ai/_display/core/config.py +11 -5
- inspect_ai/_display/core/panel.py +66 -2
- inspect_ai/_display/core/textual.py +5 -2
- inspect_ai/_display/plain/display.py +1 -0
- inspect_ai/_display/rich/display.py +2 -2
- inspect_ai/_display/textual/widgets/transcript.py +37 -9
- inspect_ai/_eval/eval.py +13 -1
- inspect_ai/_eval/evalset.py +3 -2
- inspect_ai/_eval/run.py +2 -0
- inspect_ai/_eval/score.py +2 -4
- inspect_ai/_eval/task/log.py +3 -1
- inspect_ai/_eval/task/run.py +59 -81
- inspect_ai/_util/content.py +11 -6
- inspect_ai/_util/interrupt.py +2 -2
- inspect_ai/_util/text.py +7 -0
- inspect_ai/_util/working.py +8 -37
- inspect_ai/_view/__init__.py +0 -0
- inspect_ai/_view/schema.py +2 -1
- inspect_ai/_view/www/CLAUDE.md +15 -0
- inspect_ai/_view/www/dist/assets/index.css +307 -171
- inspect_ai/_view/www/dist/assets/index.js +24733 -21641
- inspect_ai/_view/www/log-schema.json +77 -3
- inspect_ai/_view/www/package.json +9 -5
- inspect_ai/_view/www/src/@types/log.d.ts +9 -0
- inspect_ai/_view/www/src/app/App.tsx +1 -15
- inspect_ai/_view/www/src/app/appearance/icons.ts +4 -1
- inspect_ai/_view/www/src/app/content/MetaDataGrid.tsx +24 -6
- inspect_ai/_view/www/src/app/content/MetadataGrid.module.css +0 -5
- inspect_ai/_view/www/src/app/content/RenderedContent.tsx +220 -205
- inspect_ai/_view/www/src/app/log-view/LogViewContainer.tsx +2 -1
- inspect_ai/_view/www/src/app/log-view/tabs/SamplesTab.tsx +5 -0
- inspect_ai/_view/www/src/app/log-view/tabs/grouping.ts +4 -4
- inspect_ai/_view/www/src/app/routing/navigationHooks.ts +22 -25
- inspect_ai/_view/www/src/app/routing/url.ts +84 -4
- inspect_ai/_view/www/src/app/samples/InlineSampleDisplay.module.css +0 -5
- inspect_ai/_view/www/src/app/samples/SampleDialog.module.css +1 -1
- inspect_ai/_view/www/src/app/samples/SampleDisplay.module.css +7 -0
- inspect_ai/_view/www/src/app/samples/SampleDisplay.tsx +24 -17
- inspect_ai/_view/www/src/app/samples/SampleSummaryView.module.css +1 -2
- inspect_ai/_view/www/src/app/samples/chat/ChatMessage.tsx +8 -6
- inspect_ai/_view/www/src/app/samples/chat/ChatMessageRow.tsx +0 -4
- inspect_ai/_view/www/src/app/samples/chat/ChatViewVirtualList.tsx +3 -2
- inspect_ai/_view/www/src/app/samples/chat/MessageContent.tsx +2 -0
- inspect_ai/_view/www/src/app/samples/chat/MessageContents.tsx +2 -0
- inspect_ai/_view/www/src/app/samples/chat/messages.ts +1 -0
- inspect_ai/_view/www/src/app/samples/chat/tools/ToolCallView.tsx +1 -0
- inspect_ai/_view/www/src/app/samples/list/SampleList.tsx +17 -5
- inspect_ai/_view/www/src/app/samples/list/SampleRow.tsx +1 -1
- inspect_ai/_view/www/src/app/samples/transcript/ErrorEventView.tsx +1 -2
- inspect_ai/_view/www/src/app/samples/transcript/InfoEventView.tsx +1 -1
- inspect_ai/_view/www/src/app/samples/transcript/InputEventView.tsx +1 -2
- inspect_ai/_view/www/src/app/samples/transcript/ModelEventView.module.css +1 -1
- inspect_ai/_view/www/src/app/samples/transcript/ModelEventView.tsx +1 -1
- inspect_ai/_view/www/src/app/samples/transcript/SampleInitEventView.tsx +1 -1
- inspect_ai/_view/www/src/app/samples/transcript/SampleLimitEventView.tsx +3 -2
- inspect_ai/_view/www/src/app/samples/transcript/SandboxEventView.tsx +4 -5
- inspect_ai/_view/www/src/app/samples/transcript/ScoreEventView.tsx +1 -1
- inspect_ai/_view/www/src/app/samples/transcript/SpanEventView.tsx +1 -2
- inspect_ai/_view/www/src/app/samples/transcript/StepEventView.tsx +1 -3
- inspect_ai/_view/www/src/app/samples/transcript/SubtaskEventView.tsx +1 -2
- inspect_ai/_view/www/src/app/samples/transcript/ToolEventView.tsx +3 -4
- inspect_ai/_view/www/src/app/samples/transcript/TranscriptPanel.module.css +42 -0
- inspect_ai/_view/www/src/app/samples/transcript/TranscriptPanel.tsx +77 -0
- inspect_ai/_view/www/src/app/samples/transcript/TranscriptVirtualList.tsx +27 -71
- inspect_ai/_view/www/src/app/samples/transcript/TranscriptVirtualListComponent.module.css +13 -3
- inspect_ai/_view/www/src/app/samples/transcript/TranscriptVirtualListComponent.tsx +27 -2
- inspect_ai/_view/www/src/app/samples/transcript/event/EventPanel.module.css +1 -0
- inspect_ai/_view/www/src/app/samples/transcript/event/EventPanel.tsx +21 -22
- inspect_ai/_view/www/src/app/samples/transcript/outline/OutlineRow.module.css +45 -0
- inspect_ai/_view/www/src/app/samples/transcript/outline/OutlineRow.tsx +223 -0
- inspect_ai/_view/www/src/app/samples/transcript/outline/TranscriptOutline.module.css +10 -0
- inspect_ai/_view/www/src/app/samples/transcript/outline/TranscriptOutline.tsx +258 -0
- inspect_ai/_view/www/src/app/samples/transcript/outline/tree-visitors.ts +187 -0
- inspect_ai/_view/www/src/app/samples/transcript/state/StateEventRenderers.tsx +8 -1
- inspect_ai/_view/www/src/app/samples/transcript/state/StateEventView.tsx +3 -4
- inspect_ai/_view/www/src/app/samples/transcript/transform/hooks.ts +78 -0
- inspect_ai/_view/www/src/app/samples/transcript/transform/treeify.ts +340 -135
- inspect_ai/_view/www/src/app/samples/transcript/transform/utils.ts +3 -0
- inspect_ai/_view/www/src/app/samples/transcript/types.ts +2 -0
- inspect_ai/_view/www/src/app/types.ts +5 -1
- inspect_ai/_view/www/src/client/api/api-browser.ts +2 -2
- inspect_ai/_view/www/src/components/LiveVirtualList.tsx +6 -1
- inspect_ai/_view/www/src/components/MarkdownDiv.tsx +1 -1
- inspect_ai/_view/www/src/components/PopOver.tsx +422 -0
- inspect_ai/_view/www/src/components/PulsingDots.module.css +9 -9
- inspect_ai/_view/www/src/components/PulsingDots.tsx +4 -1
- inspect_ai/_view/www/src/components/StickyScroll.tsx +183 -0
- inspect_ai/_view/www/src/components/TabSet.tsx +4 -0
- inspect_ai/_view/www/src/state/hooks.ts +52 -2
- inspect_ai/_view/www/src/state/logSlice.ts +4 -3
- inspect_ai/_view/www/src/state/samplePolling.ts +8 -0
- inspect_ai/_view/www/src/state/sampleSlice.ts +53 -9
- inspect_ai/_view/www/src/state/scrolling.ts +152 -0
- inspect_ai/_view/www/src/utils/attachments.ts +7 -0
- inspect_ai/_view/www/src/utils/python.ts +18 -0
- inspect_ai/_view/www/yarn.lock +290 -33
- inspect_ai/agent/_react.py +12 -7
- inspect_ai/agent/_run.py +2 -3
- inspect_ai/analysis/beta/__init__.py +2 -0
- inspect_ai/analysis/beta/_dataframe/samples/table.py +19 -18
- inspect_ai/dataset/_sources/csv.py +2 -6
- inspect_ai/dataset/_sources/hf.py +2 -6
- inspect_ai/dataset/_sources/json.py +2 -6
- inspect_ai/dataset/_util.py +23 -0
- inspect_ai/log/_log.py +1 -1
- inspect_ai/log/_recorders/eval.py +4 -3
- inspect_ai/log/_recorders/file.py +2 -9
- inspect_ai/log/_recorders/json.py +1 -0
- inspect_ai/log/_recorders/recorder.py +1 -0
- inspect_ai/log/_transcript.py +1 -1
- inspect_ai/model/_call_tools.py +6 -2
- inspect_ai/model/_openai.py +1 -1
- inspect_ai/model/_openai_responses.py +85 -41
- inspect_ai/model/_openai_web_search.py +38 -0
- inspect_ai/model/_providers/azureai.py +72 -3
- inspect_ai/model/_providers/openai.py +4 -1
- inspect_ai/model/_providers/openai_responses.py +5 -1
- inspect_ai/scorer/_metric.py +1 -2
- inspect_ai/scorer/_reducer/reducer.py +1 -1
- inspect_ai/solver/_task_state.py +2 -2
- inspect_ai/tool/_tool.py +6 -2
- inspect_ai/tool/_tool_def.py +27 -4
- inspect_ai/tool/_tool_info.py +2 -0
- inspect_ai/tool/_tools/_web_search/_google.py +43 -15
- inspect_ai/tool/_tools/_web_search/_tavily.py +46 -13
- inspect_ai/tool/_tools/_web_search/_web_search.py +214 -45
- inspect_ai/util/__init__.py +4 -0
- inspect_ai/util/_json.py +3 -0
- inspect_ai/util/_limit.py +230 -20
- inspect_ai/util/_sandbox/docker/compose.py +20 -11
- inspect_ai/util/_span.py +1 -1
- {inspect_ai-0.3.99.dist-info → inspect_ai-0.3.101.dist-info}/METADATA +3 -3
- {inspect_ai-0.3.99.dist-info → inspect_ai-0.3.101.dist-info}/RECORD +138 -124
- {inspect_ai-0.3.99.dist-info → inspect_ai-0.3.101.dist-info}/WHEEL +1 -1
- {inspect_ai-0.3.99.dist-info → inspect_ai-0.3.101.dist-info}/entry_points.txt +0 -0
- {inspect_ai-0.3.99.dist-info → inspect_ai-0.3.101.dist-info}/licenses/LICENSE +0 -0
- {inspect_ai-0.3.99.dist-info → inspect_ai-0.3.101.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,183 @@
|
|
1
|
+
import {
|
2
|
+
CSSProperties,
|
3
|
+
FC,
|
4
|
+
ReactNode,
|
5
|
+
RefObject,
|
6
|
+
useEffect,
|
7
|
+
useRef,
|
8
|
+
useState,
|
9
|
+
} from "react";
|
10
|
+
|
11
|
+
interface StickyScrollProps {
|
12
|
+
children: ReactNode;
|
13
|
+
scrollRef: RefObject<HTMLElement | null>;
|
14
|
+
offsetTop?: number;
|
15
|
+
zIndex?: number;
|
16
|
+
className?: string;
|
17
|
+
stickyClassName?: string;
|
18
|
+
onStickyChange?: (isSticky: boolean) => void;
|
19
|
+
}
|
20
|
+
|
21
|
+
export const StickyScroll: FC<StickyScrollProps> = ({
|
22
|
+
children,
|
23
|
+
scrollRef,
|
24
|
+
offsetTop = 0,
|
25
|
+
zIndex = 100,
|
26
|
+
className = "",
|
27
|
+
stickyClassName = "is-sticky",
|
28
|
+
onStickyChange,
|
29
|
+
}) => {
|
30
|
+
const wrapperRef = useRef<HTMLDivElement>(null);
|
31
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
32
|
+
const [isSticky, setIsSticky] = useState(false);
|
33
|
+
const [dimensions, setDimensions] = useState({
|
34
|
+
width: 0,
|
35
|
+
height: 0,
|
36
|
+
left: 0,
|
37
|
+
stickyTop: 0, // Store the position where the element should stick
|
38
|
+
});
|
39
|
+
|
40
|
+
useEffect(() => {
|
41
|
+
const wrapper = wrapperRef.current;
|
42
|
+
const content = contentRef.current;
|
43
|
+
const scrollContainer = scrollRef.current;
|
44
|
+
|
45
|
+
if (!wrapper || !content || !scrollContainer) {
|
46
|
+
return;
|
47
|
+
}
|
48
|
+
|
49
|
+
// Create a sentinel element that will be positioned at the desired sticky point
|
50
|
+
const sentinel = document.createElement("div");
|
51
|
+
sentinel.style.position = "absolute";
|
52
|
+
sentinel.style.top = "0px"; // Position at the top of the wrapper
|
53
|
+
sentinel.style.left = "0";
|
54
|
+
sentinel.style.width = "1px";
|
55
|
+
sentinel.style.height = "1px";
|
56
|
+
sentinel.style.pointerEvents = "none";
|
57
|
+
wrapper.prepend(sentinel);
|
58
|
+
|
59
|
+
// Create a width tracker element that always has the same width as the wrapper
|
60
|
+
// This helps us know what width to apply to the fixed element
|
61
|
+
const widthTracker = document.createElement("div");
|
62
|
+
widthTracker.style.position = "absolute";
|
63
|
+
widthTracker.style.top = "0";
|
64
|
+
widthTracker.style.left = "0";
|
65
|
+
widthTracker.style.width = "100%";
|
66
|
+
widthTracker.style.height = "0";
|
67
|
+
widthTracker.style.pointerEvents = "none";
|
68
|
+
widthTracker.style.visibility = "hidden";
|
69
|
+
wrapper.prepend(widthTracker);
|
70
|
+
|
71
|
+
// Measure element dimensions and calculate sticky position
|
72
|
+
const updateDimensions = () => {
|
73
|
+
if (wrapper && scrollContainer) {
|
74
|
+
const contentRect = content.getBoundingClientRect();
|
75
|
+
const containerRect = scrollContainer.getBoundingClientRect();
|
76
|
+
const trackerRect = widthTracker.getBoundingClientRect();
|
77
|
+
|
78
|
+
// Calculate where the top of the content should be when sticky
|
79
|
+
// This is the distance from the top of the scroll container
|
80
|
+
// plus any additional offsetTop
|
81
|
+
const stickyTop = containerRect.top + offsetTop;
|
82
|
+
|
83
|
+
setDimensions({
|
84
|
+
// Use the width tracker to get the right width that respects
|
85
|
+
// the parent container's current width, rather than the content's width
|
86
|
+
width: trackerRect.width,
|
87
|
+
height: contentRect.height,
|
88
|
+
left: trackerRect.left,
|
89
|
+
stickyTop,
|
90
|
+
});
|
91
|
+
}
|
92
|
+
};
|
93
|
+
|
94
|
+
// Initial measurement
|
95
|
+
updateDimensions();
|
96
|
+
|
97
|
+
// Monitor size changes
|
98
|
+
const resizeObserver = new ResizeObserver(() => {
|
99
|
+
// Use animationFrame to ensure dimensions are updated after DOM has settled
|
100
|
+
requestAnimationFrame(() => {
|
101
|
+
updateDimensions();
|
102
|
+
// If sticky, force a re-measurement of position to update layout
|
103
|
+
if (isSticky) {
|
104
|
+
handleScroll();
|
105
|
+
}
|
106
|
+
});
|
107
|
+
});
|
108
|
+
|
109
|
+
resizeObserver.observe(wrapper);
|
110
|
+
resizeObserver.observe(scrollContainer);
|
111
|
+
resizeObserver.observe(content);
|
112
|
+
|
113
|
+
// Add scroll event listener for more precise control
|
114
|
+
const handleScroll = () => {
|
115
|
+
const sentinelRect = sentinel.getBoundingClientRect();
|
116
|
+
const containerRect = scrollContainer.getBoundingClientRect();
|
117
|
+
|
118
|
+
// Check if sentinel is above the top of the viewport + offset
|
119
|
+
const shouldBeSticky = sentinelRect.top < containerRect.top + offsetTop;
|
120
|
+
|
121
|
+
if (shouldBeSticky !== isSticky) {
|
122
|
+
updateDimensions();
|
123
|
+
setIsSticky(shouldBeSticky);
|
124
|
+
|
125
|
+
if (onStickyChange) {
|
126
|
+
onStickyChange(shouldBeSticky);
|
127
|
+
}
|
128
|
+
}
|
129
|
+
};
|
130
|
+
|
131
|
+
scrollContainer.addEventListener("scroll", handleScroll);
|
132
|
+
|
133
|
+
// Trigger initial check
|
134
|
+
handleScroll();
|
135
|
+
|
136
|
+
// Clean up
|
137
|
+
return () => {
|
138
|
+
resizeObserver.disconnect();
|
139
|
+
scrollContainer.removeEventListener("scroll", handleScroll);
|
140
|
+
if (sentinel.parentNode) {
|
141
|
+
sentinel.parentNode.removeChild(sentinel);
|
142
|
+
}
|
143
|
+
if (widthTracker.parentNode) {
|
144
|
+
widthTracker.parentNode.removeChild(widthTracker);
|
145
|
+
}
|
146
|
+
};
|
147
|
+
}, [scrollRef, offsetTop, onStickyChange, isSticky]);
|
148
|
+
|
149
|
+
// Wrapper styles - this div serves as the placeholder
|
150
|
+
// When sticky, we need to ensure the wrapper has the right dimensions
|
151
|
+
// to prevent content jumping when the element is detached from normal flow
|
152
|
+
const wrapperStyle: CSSProperties = {
|
153
|
+
position: "relative",
|
154
|
+
height: isSticky ? `${dimensions.height}px` : "auto",
|
155
|
+
// Don't constrain width - let it flow naturally with the content
|
156
|
+
};
|
157
|
+
|
158
|
+
// Content styles - position at the calculated stickyTop when sticky
|
159
|
+
// For sticky mode, use fixed positioning but maintain the original width
|
160
|
+
const contentStyle: CSSProperties = isSticky
|
161
|
+
? {
|
162
|
+
position: "fixed",
|
163
|
+
top: `${dimensions.stickyTop}px`,
|
164
|
+
left: `${dimensions.left}px`,
|
165
|
+
width: `${dimensions.width}px`, // Keep explicit width to prevent expanding to 100%
|
166
|
+
maxHeight: `calc(100vh - ${dimensions.stickyTop}px)`,
|
167
|
+
zIndex,
|
168
|
+
}
|
169
|
+
: {};
|
170
|
+
|
171
|
+
const contentClassName =
|
172
|
+
isSticky && stickyClassName
|
173
|
+
? `${className} ${stickyClassName}`.trim()
|
174
|
+
: className;
|
175
|
+
|
176
|
+
return (
|
177
|
+
<div ref={wrapperRef} style={wrapperStyle}>
|
178
|
+
<div ref={contentRef} className={contentClassName} style={contentStyle}>
|
179
|
+
{children}
|
180
|
+
</div>
|
181
|
+
</div>
|
182
|
+
);
|
183
|
+
};
|
@@ -16,6 +16,7 @@ import moduleStyles from "./TabSet.module.css";
|
|
16
16
|
|
17
17
|
interface TabSetProps {
|
18
18
|
id: string;
|
19
|
+
tabsRef?: RefObject<HTMLUListElement | null>;
|
19
20
|
type?: "tabs" | "pills";
|
20
21
|
className?: string | string[];
|
21
22
|
tabPanelsClassName?: string | string[];
|
@@ -33,6 +34,7 @@ interface TabPanelProps {
|
|
33
34
|
style?: CSSProperties;
|
34
35
|
scrollable?: boolean;
|
35
36
|
scrollRef?: RefObject<HTMLDivElement | null>;
|
37
|
+
|
36
38
|
className?: string | string[];
|
37
39
|
children?: ReactNode;
|
38
40
|
title: string;
|
@@ -47,6 +49,7 @@ export const TabSet: FC<TabSetProps> = ({
|
|
47
49
|
tabPanelsClassName,
|
48
50
|
tabControlsClassName,
|
49
51
|
tools,
|
52
|
+
tabsRef,
|
50
53
|
children,
|
51
54
|
}) => {
|
52
55
|
const validTabs = flattenChildren(children);
|
@@ -55,6 +58,7 @@ export const TabSet: FC<TabSetProps> = ({
|
|
55
58
|
return (
|
56
59
|
<Fragment>
|
57
60
|
<ul
|
61
|
+
ref={tabsRef}
|
58
62
|
id={id}
|
59
63
|
className={clsx(
|
60
64
|
"nav",
|
@@ -270,16 +270,17 @@ export const useLogSelection = () => {
|
|
270
270
|
};
|
271
271
|
|
272
272
|
export const useCollapseSampleEvent = (
|
273
|
+
scope: string,
|
273
274
|
id: string,
|
274
275
|
): [boolean, (collapsed: boolean) => void] => {
|
275
276
|
const collapsed = useStore((state) => state.sample.collapsedEvents);
|
276
277
|
const collapseEvent = useStore((state) => state.sampleActions.collapseEvent);
|
277
278
|
|
278
279
|
return useMemo(() => {
|
279
|
-
const isCollapsed = collapsed !== null && collapsed[id] === true;
|
280
|
+
const isCollapsed = collapsed !== null && collapsed[scope]?.[id] === true;
|
280
281
|
const set = (value: boolean) => {
|
281
282
|
log.debug("Set collapsed", id, value);
|
282
|
-
collapseEvent(id, value);
|
283
|
+
collapseEvent(scope, id, value);
|
283
284
|
};
|
284
285
|
return [isCollapsed, set];
|
285
286
|
}, [collapsed, collapseEvent, id]);
|
@@ -506,3 +507,52 @@ export const useSetSelectedLogIndex = () => {
|
|
506
507
|
],
|
507
508
|
);
|
508
509
|
};
|
510
|
+
|
511
|
+
export const useSamplePopover = (id: string) => {
|
512
|
+
const setVisiblePopover = useStore(
|
513
|
+
(store) => store.sampleActions.setVisiblePopover,
|
514
|
+
);
|
515
|
+
const clearVisiblePopover = useStore(
|
516
|
+
(store) => store.sampleActions.clearVisiblePopover,
|
517
|
+
);
|
518
|
+
const visiblePopover = useStore((store) => store.sample.visiblePopover);
|
519
|
+
const timerRef = useRef<number | null>(null);
|
520
|
+
|
521
|
+
const show = useCallback(() => {
|
522
|
+
if (timerRef.current) {
|
523
|
+
return; // Timer already running
|
524
|
+
}
|
525
|
+
|
526
|
+
timerRef.current = window.setTimeout(() => {
|
527
|
+
setVisiblePopover(id);
|
528
|
+
timerRef.current = null;
|
529
|
+
}, 250);
|
530
|
+
}, [id, setVisiblePopover]);
|
531
|
+
|
532
|
+
const hide = useCallback(() => {
|
533
|
+
if (timerRef.current) {
|
534
|
+
clearTimeout(timerRef.current);
|
535
|
+
timerRef.current = null;
|
536
|
+
}
|
537
|
+
clearVisiblePopover();
|
538
|
+
}, [clearVisiblePopover]);
|
539
|
+
|
540
|
+
// Clear the timeout when component unmounts
|
541
|
+
useEffect(() => {
|
542
|
+
return () => {
|
543
|
+
if (timerRef.current) {
|
544
|
+
clearTimeout(timerRef.current);
|
545
|
+
}
|
546
|
+
};
|
547
|
+
}, []);
|
548
|
+
|
549
|
+
const isShowing = useMemo(() => {
|
550
|
+
return visiblePopover === id;
|
551
|
+
}, [id, visiblePopover]);
|
552
|
+
|
553
|
+
return {
|
554
|
+
show,
|
555
|
+
hide,
|
556
|
+
isShowing,
|
557
|
+
};
|
558
|
+
};
|
@@ -187,9 +187,10 @@ export const createLogSlice = (
|
|
187
187
|
state.logsActions.updateLogHeaders(header);
|
188
188
|
set((state) => {
|
189
189
|
state.log.loadedLog = logFileName;
|
190
|
-
})
|
191
|
-
|
192
|
-
|
190
|
+
});
|
191
|
+
|
192
|
+
// Start polling for pending samples
|
193
|
+
logPolling.startPolling(logFileName);
|
193
194
|
} catch (error) {
|
194
195
|
log.error("Error loading log:", error);
|
195
196
|
throw error;
|
@@ -283,6 +283,14 @@ function processEvents(sampleData: SampleData, pollingState: PollingState) {
|
|
283
283
|
const resolvedEvent = resolveAttachments<Event>(
|
284
284
|
eventData.event,
|
285
285
|
pollingState.attachments,
|
286
|
+
(attachmentId: string) => {
|
287
|
+
const snapshot = {
|
288
|
+
eventId: eventData.event_id,
|
289
|
+
attachmentId,
|
290
|
+
available_attachments: Object.keys(pollingState.attachments),
|
291
|
+
};
|
292
|
+
console.warn(`Unable to resolve attachment ${attachmentId}`, snapshot);
|
293
|
+
},
|
286
294
|
);
|
287
295
|
|
288
296
|
if (existingIndex) {
|
@@ -22,17 +22,27 @@ export interface SampleSlice {
|
|
22
22
|
setSelectedSample: (sample: EvalSample) => void;
|
23
23
|
getSelectedSample: () => EvalSample | undefined;
|
24
24
|
clearSelectedSample: () => void;
|
25
|
+
|
25
26
|
setSampleStatus: (status: SampleStatus) => void;
|
26
27
|
setSampleError: (error: Error | undefined) => void;
|
27
28
|
|
28
|
-
setCollapsedEvents: (
|
29
|
-
|
29
|
+
setCollapsedEvents: (
|
30
|
+
scope: string,
|
31
|
+
collapsed: Record<string, boolean>,
|
32
|
+
) => void;
|
33
|
+
collapseEvent: (scope: string, id: string, collapsed: boolean) => void;
|
30
34
|
clearCollapsedEvents: () => void;
|
31
35
|
|
32
36
|
setCollapsedIds: (key: string, collapsed: Record<string, true>) => void;
|
33
37
|
collapseId: (key: string, id: string, collapsed: boolean) => void;
|
34
38
|
clearCollapsedIds: (key: string) => void;
|
35
39
|
|
40
|
+
setVisiblePopover: (id: string) => void;
|
41
|
+
clearVisiblePopover: () => void;
|
42
|
+
|
43
|
+
setSelectedOutlineId: (id: string) => void;
|
44
|
+
clearSelectedOutlineId: () => void;
|
45
|
+
|
36
46
|
// Loading
|
37
47
|
loadSample: (
|
38
48
|
logFile: string,
|
@@ -56,6 +66,8 @@ const initialState: SampleState = {
|
|
56
66
|
sampleStatus: "ok",
|
57
67
|
sampleError: undefined,
|
58
68
|
|
69
|
+
visiblePopover: undefined,
|
70
|
+
|
59
71
|
// signals that the sample needs to be reloaded
|
60
72
|
sampleNeedsReload: 0,
|
61
73
|
|
@@ -64,6 +76,7 @@ const initialState: SampleState = {
|
|
64
76
|
collapsedEvents: null,
|
65
77
|
|
66
78
|
collapsedIdBuckets: {},
|
79
|
+
selectedOutlineId: undefined,
|
67
80
|
};
|
68
81
|
|
69
82
|
export const createSampleSlice = (
|
@@ -130,25 +143,37 @@ export const createSampleSlice = (
|
|
130
143
|
set((state) => {
|
131
144
|
state.sample.sampleError = error;
|
132
145
|
}),
|
133
|
-
setCollapsedEvents: (
|
146
|
+
setCollapsedEvents: (
|
147
|
+
scope: string,
|
148
|
+
collapsed: Record<string, boolean>,
|
149
|
+
) => {
|
134
150
|
set((state) => {
|
135
|
-
state.sample.collapsedEvents
|
151
|
+
if (state.sample.collapsedEvents === null) {
|
152
|
+
state.sample.collapsedEvents = {};
|
153
|
+
}
|
154
|
+
state.sample.collapsedEvents[scope] = collapsed;
|
136
155
|
});
|
137
156
|
},
|
138
157
|
clearCollapsedEvents: () => {
|
139
158
|
set((state) => {
|
140
|
-
state.sample.collapsedEvents
|
159
|
+
if (state.sample.collapsedEvents !== null) {
|
160
|
+
state.sample.collapsedEvents = null;
|
161
|
+
}
|
141
162
|
});
|
142
163
|
},
|
143
|
-
collapseEvent: (id: string, collapsed: boolean) => {
|
164
|
+
collapseEvent: (scope: string, id: string, collapsed: boolean) => {
|
144
165
|
set((state) => {
|
145
166
|
if (state.sample.collapsedEvents === null) {
|
146
167
|
state.sample.collapsedEvents = {};
|
147
168
|
}
|
169
|
+
if (!state.sample.collapsedEvents[scope]) {
|
170
|
+
state.sample.collapsedEvents[scope] = {};
|
171
|
+
}
|
172
|
+
|
148
173
|
if (collapsed) {
|
149
|
-
state.sample.collapsedEvents[id] = true;
|
174
|
+
state.sample.collapsedEvents[scope][id] = true;
|
150
175
|
} else {
|
151
|
-
delete state.sample.collapsedEvents[id];
|
176
|
+
delete state.sample.collapsedEvents[scope][id];
|
152
177
|
}
|
153
178
|
});
|
154
179
|
},
|
@@ -174,7 +199,26 @@ export const createSampleSlice = (
|
|
174
199
|
delete state.sample.collapsedIdBuckets[key];
|
175
200
|
});
|
176
201
|
},
|
177
|
-
|
202
|
+
setVisiblePopover: (id: string) => {
|
203
|
+
set((state) => {
|
204
|
+
state.sample.visiblePopover = id;
|
205
|
+
});
|
206
|
+
},
|
207
|
+
clearVisiblePopover: () => {
|
208
|
+
set((state) => {
|
209
|
+
state.sample.visiblePopover = undefined;
|
210
|
+
});
|
211
|
+
},
|
212
|
+
setSelectedOutlineId: (id: string) => {
|
213
|
+
set((state) => {
|
214
|
+
state.sample.selectedOutlineId = id;
|
215
|
+
});
|
216
|
+
},
|
217
|
+
clearSelectedOutlineId: () => {
|
218
|
+
set((state) => {
|
219
|
+
state.sample.selectedOutlineId = undefined;
|
220
|
+
});
|
221
|
+
},
|
178
222
|
pollSample: async (logFile: string, sampleSummary: SampleSummary) => {
|
179
223
|
// Poll running sample
|
180
224
|
const state = get();
|
@@ -237,3 +237,155 @@ export function useRafThrottle<T extends (...args: any[]) => any>(
|
|
237
237
|
|
238
238
|
return throttledCallback;
|
239
239
|
}
|
240
|
+
|
241
|
+
export function useScrollTrack(
|
242
|
+
elementIds: string[],
|
243
|
+
onElementVisible: (id: string) => void,
|
244
|
+
scrollRef?: RefObject<HTMLElement | null>,
|
245
|
+
options?: { topOffset?: number; checkInterval?: number },
|
246
|
+
) {
|
247
|
+
const currentVisibleRef = useRef<string | null>(null);
|
248
|
+
const lastCheckRef = useRef<number>(0);
|
249
|
+
const rafRef = useRef<number | null>(null);
|
250
|
+
|
251
|
+
const findTopmostVisibleElement = useCallback(() => {
|
252
|
+
const container = scrollRef?.current;
|
253
|
+
const containerRect = container?.getBoundingClientRect();
|
254
|
+
const topOffset = options?.topOffset ?? 50;
|
255
|
+
|
256
|
+
// Define viewport bounds
|
257
|
+
const viewportTop = containerRect
|
258
|
+
? containerRect.top + topOffset
|
259
|
+
: topOffset;
|
260
|
+
const viewportBottom = containerRect
|
261
|
+
? containerRect.bottom
|
262
|
+
: window.innerHeight;
|
263
|
+
const viewportHeight = viewportBottom - viewportTop;
|
264
|
+
|
265
|
+
// Calculate dynamic threshold based on scroll position
|
266
|
+
let detectionPoint = viewportTop;
|
267
|
+
|
268
|
+
if (container) {
|
269
|
+
// This will track the scroll position and select which element is 'showing'
|
270
|
+
// This is generally the item at the the top of the viewport (threshold).
|
271
|
+
// As we get to the bottom of the scroll area, though, we will actually start
|
272
|
+
// sliding the detection point down to the bottom of the viewport so that every
|
273
|
+
// item can be selected.
|
274
|
+
const scrollHeight = container.scrollHeight;
|
275
|
+
const scrollTop = container.scrollTop;
|
276
|
+
const clientHeight = container.clientHeight;
|
277
|
+
const maxScroll = scrollHeight - clientHeight;
|
278
|
+
|
279
|
+
// Calculate how close we are to the bottom (0 = at top, 1 = at bottom)
|
280
|
+
const scrollProgress = maxScroll > 0 ? scrollTop / maxScroll : 0;
|
281
|
+
|
282
|
+
// Start sliding only in the last 20% of scroll
|
283
|
+
const slideThreshold = 0.8;
|
284
|
+
if (scrollProgress > slideThreshold) {
|
285
|
+
// Calculate how far through the slide zone we are (0 to 1)
|
286
|
+
const slideProgress =
|
287
|
+
(scrollProgress - slideThreshold) / (1 - slideThreshold);
|
288
|
+
// Use a steeper curve (power of 3) for faster transition
|
289
|
+
const easedProgress = Math.pow(slideProgress, 3);
|
290
|
+
// Slide all the way from top to bottom of viewport
|
291
|
+
detectionPoint = viewportTop + viewportHeight * 0.9 * easedProgress;
|
292
|
+
}
|
293
|
+
|
294
|
+
// When fully scrolled to bottom, use bottom of viewport
|
295
|
+
if (scrollProgress >= 0.99) {
|
296
|
+
detectionPoint = viewportBottom - 50; // Slight offset from absolute bottom
|
297
|
+
}
|
298
|
+
}
|
299
|
+
|
300
|
+
let closestId: string | null = null;
|
301
|
+
let closestDistance = Infinity;
|
302
|
+
|
303
|
+
// Create a Set for O(1) lookup
|
304
|
+
const elementIdSet = new Set(elementIds);
|
305
|
+
|
306
|
+
// Find all elements that are actually in the DOM and check if they're our tracked elements
|
307
|
+
const elements = container
|
308
|
+
? container.querySelectorAll("[id]")
|
309
|
+
: document.querySelectorAll("[id]");
|
310
|
+
|
311
|
+
for (const element of elements) {
|
312
|
+
const id = element.id;
|
313
|
+
|
314
|
+
// Check if this element is one we're tracking
|
315
|
+
if (elementIdSet.has(id)) {
|
316
|
+
const rect = element.getBoundingClientRect();
|
317
|
+
|
318
|
+
// Check if element is in viewport
|
319
|
+
if (rect.bottom >= viewportTop && rect.top <= viewportBottom) {
|
320
|
+
// Calculate distance from detection point to element's vertical center
|
321
|
+
const elementCenter = rect.top + rect.height / 2;
|
322
|
+
const distance = Math.abs(elementCenter - detectionPoint);
|
323
|
+
|
324
|
+
if (distance < closestDistance) {
|
325
|
+
closestDistance = distance;
|
326
|
+
closestId = id;
|
327
|
+
}
|
328
|
+
}
|
329
|
+
}
|
330
|
+
}
|
331
|
+
|
332
|
+
return closestId;
|
333
|
+
}, [elementIds, scrollRef, options?.topOffset]);
|
334
|
+
|
335
|
+
const checkVisibility = useCallback(() => {
|
336
|
+
const now = Date.now();
|
337
|
+
const checkInterval = options?.checkInterval ?? 100;
|
338
|
+
|
339
|
+
// Throttle checks
|
340
|
+
if (now - lastCheckRef.current < checkInterval) {
|
341
|
+
return;
|
342
|
+
}
|
343
|
+
|
344
|
+
lastCheckRef.current = now;
|
345
|
+
const topmostId = findTopmostVisibleElement();
|
346
|
+
|
347
|
+
if (topmostId !== currentVisibleRef.current) {
|
348
|
+
currentVisibleRef.current = topmostId;
|
349
|
+
if (topmostId) {
|
350
|
+
onElementVisible(topmostId);
|
351
|
+
}
|
352
|
+
}
|
353
|
+
}, [findTopmostVisibleElement, onElementVisible, options?.checkInterval]);
|
354
|
+
|
355
|
+
const handleScroll = useCallback(() => {
|
356
|
+
// Cancel any pending animation frame
|
357
|
+
if (rafRef.current !== null) {
|
358
|
+
cancelAnimationFrame(rafRef.current);
|
359
|
+
}
|
360
|
+
|
361
|
+
// Schedule visibility check on next animation frame
|
362
|
+
rafRef.current = requestAnimationFrame(() => {
|
363
|
+
checkVisibility();
|
364
|
+
rafRef.current = null;
|
365
|
+
});
|
366
|
+
}, [checkVisibility]);
|
367
|
+
|
368
|
+
useEffect(() => {
|
369
|
+
if (elementIds.length === 0) return;
|
370
|
+
|
371
|
+
const scrollElement = scrollRef?.current || window;
|
372
|
+
|
373
|
+
// Initial check
|
374
|
+
checkVisibility();
|
375
|
+
|
376
|
+
// Add scroll listener
|
377
|
+
scrollElement.addEventListener("scroll", handleScroll, { passive: true });
|
378
|
+
|
379
|
+
// Also check periodically for virtual elements that may have appeared
|
380
|
+
const intervalId = setInterval(checkVisibility, 1000);
|
381
|
+
|
382
|
+
// Cleanup
|
383
|
+
return () => {
|
384
|
+
scrollElement.removeEventListener("scroll", handleScroll);
|
385
|
+
clearInterval(intervalId);
|
386
|
+
if (rafRef.current !== null) {
|
387
|
+
cancelAnimationFrame(rafRef.current);
|
388
|
+
}
|
389
|
+
};
|
390
|
+
}, [elementIds, scrollRef, handleScroll, checkVisibility]);
|
391
|
+
}
|
@@ -1,6 +1,7 @@
|
|
1
1
|
export const resolveAttachments = <T>(
|
2
2
|
value: T,
|
3
3
|
attachments: Record<string, string>,
|
4
|
+
onFailedResolve?: (attachmentId: string) => void,
|
4
5
|
): T => {
|
5
6
|
const CONTENT_PROTOCOL = "tc://";
|
6
7
|
const ATTACHMENT_PROTOCOL = "attachment://";
|
@@ -56,6 +57,9 @@ export const resolveAttachments = <T>(
|
|
56
57
|
const attachment = attachments[attachmentId];
|
57
58
|
|
58
59
|
// Return the attachment content if it exists, otherwise return the original string
|
60
|
+
if (attachment === undefined && onFailedResolve) {
|
61
|
+
onFailedResolve(attachmentId);
|
62
|
+
}
|
59
63
|
return (attachment !== undefined ? attachment : value) as unknown as T;
|
60
64
|
}
|
61
65
|
|
@@ -66,6 +70,9 @@ export const resolveAttachments = <T>(
|
|
66
70
|
if (value.startsWith(ATTACHMENT_PROTOCOL)) {
|
67
71
|
const attachmentId = value.slice(ATTACHMENT_PROTOCOL.length);
|
68
72
|
const attachment = attachments[attachmentId];
|
73
|
+
if (attachment === undefined && onFailedResolve) {
|
74
|
+
onFailedResolve(attachmentId);
|
75
|
+
}
|
69
76
|
|
70
77
|
// Return the attachment content if it exists, otherwise return the original string
|
71
78
|
return (attachment !== undefined ? attachment : value) as unknown as T;
|
@@ -0,0 +1,18 @@
|
|
1
|
+
/**
|
2
|
+
* Extracts the package and module names from a fully qualified Python module path
|
3
|
+
*
|
4
|
+
* @param name - A Python import path that may include a package name
|
5
|
+
* @returns An object containing the package and module names
|
6
|
+
*/
|
7
|
+
export const parsePackageName = (name: string): PythonName => {
|
8
|
+
if (name.includes("/")) {
|
9
|
+
const [packageName, moduleName] = name.split("/", 2);
|
10
|
+
return { package: packageName, module: moduleName };
|
11
|
+
}
|
12
|
+
return { package: "", module: name };
|
13
|
+
};
|
14
|
+
|
15
|
+
export interface PythonName {
|
16
|
+
package: string;
|
17
|
+
module: string;
|
18
|
+
}
|