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.
Files changed (138) hide show
  1. inspect_ai/_cli/eval.py +2 -1
  2. inspect_ai/_display/core/config.py +11 -5
  3. inspect_ai/_display/core/panel.py +66 -2
  4. inspect_ai/_display/core/textual.py +5 -2
  5. inspect_ai/_display/plain/display.py +1 -0
  6. inspect_ai/_display/rich/display.py +2 -2
  7. inspect_ai/_display/textual/widgets/transcript.py +37 -9
  8. inspect_ai/_eval/eval.py +13 -1
  9. inspect_ai/_eval/evalset.py +3 -2
  10. inspect_ai/_eval/run.py +2 -0
  11. inspect_ai/_eval/score.py +2 -4
  12. inspect_ai/_eval/task/log.py +3 -1
  13. inspect_ai/_eval/task/run.py +59 -81
  14. inspect_ai/_util/content.py +11 -6
  15. inspect_ai/_util/interrupt.py +2 -2
  16. inspect_ai/_util/text.py +7 -0
  17. inspect_ai/_util/working.py +8 -37
  18. inspect_ai/_view/__init__.py +0 -0
  19. inspect_ai/_view/schema.py +2 -1
  20. inspect_ai/_view/www/CLAUDE.md +15 -0
  21. inspect_ai/_view/www/dist/assets/index.css +307 -171
  22. inspect_ai/_view/www/dist/assets/index.js +24733 -21641
  23. inspect_ai/_view/www/log-schema.json +77 -3
  24. inspect_ai/_view/www/package.json +9 -5
  25. inspect_ai/_view/www/src/@types/log.d.ts +9 -0
  26. inspect_ai/_view/www/src/app/App.tsx +1 -15
  27. inspect_ai/_view/www/src/app/appearance/icons.ts +4 -1
  28. inspect_ai/_view/www/src/app/content/MetaDataGrid.tsx +24 -6
  29. inspect_ai/_view/www/src/app/content/MetadataGrid.module.css +0 -5
  30. inspect_ai/_view/www/src/app/content/RenderedContent.tsx +220 -205
  31. inspect_ai/_view/www/src/app/log-view/LogViewContainer.tsx +2 -1
  32. inspect_ai/_view/www/src/app/log-view/tabs/SamplesTab.tsx +5 -0
  33. inspect_ai/_view/www/src/app/log-view/tabs/grouping.ts +4 -4
  34. inspect_ai/_view/www/src/app/routing/navigationHooks.ts +22 -25
  35. inspect_ai/_view/www/src/app/routing/url.ts +84 -4
  36. inspect_ai/_view/www/src/app/samples/InlineSampleDisplay.module.css +0 -5
  37. inspect_ai/_view/www/src/app/samples/SampleDialog.module.css +1 -1
  38. inspect_ai/_view/www/src/app/samples/SampleDisplay.module.css +7 -0
  39. inspect_ai/_view/www/src/app/samples/SampleDisplay.tsx +24 -17
  40. inspect_ai/_view/www/src/app/samples/SampleSummaryView.module.css +1 -2
  41. inspect_ai/_view/www/src/app/samples/chat/ChatMessage.tsx +8 -6
  42. inspect_ai/_view/www/src/app/samples/chat/ChatMessageRow.tsx +0 -4
  43. inspect_ai/_view/www/src/app/samples/chat/ChatViewVirtualList.tsx +3 -2
  44. inspect_ai/_view/www/src/app/samples/chat/MessageContent.tsx +2 -0
  45. inspect_ai/_view/www/src/app/samples/chat/MessageContents.tsx +2 -0
  46. inspect_ai/_view/www/src/app/samples/chat/messages.ts +1 -0
  47. inspect_ai/_view/www/src/app/samples/chat/tools/ToolCallView.tsx +1 -0
  48. inspect_ai/_view/www/src/app/samples/list/SampleList.tsx +17 -5
  49. inspect_ai/_view/www/src/app/samples/list/SampleRow.tsx +1 -1
  50. inspect_ai/_view/www/src/app/samples/transcript/ErrorEventView.tsx +1 -2
  51. inspect_ai/_view/www/src/app/samples/transcript/InfoEventView.tsx +1 -1
  52. inspect_ai/_view/www/src/app/samples/transcript/InputEventView.tsx +1 -2
  53. inspect_ai/_view/www/src/app/samples/transcript/ModelEventView.module.css +1 -1
  54. inspect_ai/_view/www/src/app/samples/transcript/ModelEventView.tsx +1 -1
  55. inspect_ai/_view/www/src/app/samples/transcript/SampleInitEventView.tsx +1 -1
  56. inspect_ai/_view/www/src/app/samples/transcript/SampleLimitEventView.tsx +3 -2
  57. inspect_ai/_view/www/src/app/samples/transcript/SandboxEventView.tsx +4 -5
  58. inspect_ai/_view/www/src/app/samples/transcript/ScoreEventView.tsx +1 -1
  59. inspect_ai/_view/www/src/app/samples/transcript/SpanEventView.tsx +1 -2
  60. inspect_ai/_view/www/src/app/samples/transcript/StepEventView.tsx +1 -3
  61. inspect_ai/_view/www/src/app/samples/transcript/SubtaskEventView.tsx +1 -2
  62. inspect_ai/_view/www/src/app/samples/transcript/ToolEventView.tsx +3 -4
  63. inspect_ai/_view/www/src/app/samples/transcript/TranscriptPanel.module.css +42 -0
  64. inspect_ai/_view/www/src/app/samples/transcript/TranscriptPanel.tsx +77 -0
  65. inspect_ai/_view/www/src/app/samples/transcript/TranscriptVirtualList.tsx +27 -71
  66. inspect_ai/_view/www/src/app/samples/transcript/TranscriptVirtualListComponent.module.css +13 -3
  67. inspect_ai/_view/www/src/app/samples/transcript/TranscriptVirtualListComponent.tsx +27 -2
  68. inspect_ai/_view/www/src/app/samples/transcript/event/EventPanel.module.css +1 -0
  69. inspect_ai/_view/www/src/app/samples/transcript/event/EventPanel.tsx +21 -22
  70. inspect_ai/_view/www/src/app/samples/transcript/outline/OutlineRow.module.css +45 -0
  71. inspect_ai/_view/www/src/app/samples/transcript/outline/OutlineRow.tsx +223 -0
  72. inspect_ai/_view/www/src/app/samples/transcript/outline/TranscriptOutline.module.css +10 -0
  73. inspect_ai/_view/www/src/app/samples/transcript/outline/TranscriptOutline.tsx +258 -0
  74. inspect_ai/_view/www/src/app/samples/transcript/outline/tree-visitors.ts +187 -0
  75. inspect_ai/_view/www/src/app/samples/transcript/state/StateEventRenderers.tsx +8 -1
  76. inspect_ai/_view/www/src/app/samples/transcript/state/StateEventView.tsx +3 -4
  77. inspect_ai/_view/www/src/app/samples/transcript/transform/hooks.ts +78 -0
  78. inspect_ai/_view/www/src/app/samples/transcript/transform/treeify.ts +340 -135
  79. inspect_ai/_view/www/src/app/samples/transcript/transform/utils.ts +3 -0
  80. inspect_ai/_view/www/src/app/samples/transcript/types.ts +2 -0
  81. inspect_ai/_view/www/src/app/types.ts +5 -1
  82. inspect_ai/_view/www/src/client/api/api-browser.ts +2 -2
  83. inspect_ai/_view/www/src/components/LiveVirtualList.tsx +6 -1
  84. inspect_ai/_view/www/src/components/MarkdownDiv.tsx +1 -1
  85. inspect_ai/_view/www/src/components/PopOver.tsx +422 -0
  86. inspect_ai/_view/www/src/components/PulsingDots.module.css +9 -9
  87. inspect_ai/_view/www/src/components/PulsingDots.tsx +4 -1
  88. inspect_ai/_view/www/src/components/StickyScroll.tsx +183 -0
  89. inspect_ai/_view/www/src/components/TabSet.tsx +4 -0
  90. inspect_ai/_view/www/src/state/hooks.ts +52 -2
  91. inspect_ai/_view/www/src/state/logSlice.ts +4 -3
  92. inspect_ai/_view/www/src/state/samplePolling.ts +8 -0
  93. inspect_ai/_view/www/src/state/sampleSlice.ts +53 -9
  94. inspect_ai/_view/www/src/state/scrolling.ts +152 -0
  95. inspect_ai/_view/www/src/utils/attachments.ts +7 -0
  96. inspect_ai/_view/www/src/utils/python.ts +18 -0
  97. inspect_ai/_view/www/yarn.lock +290 -33
  98. inspect_ai/agent/_react.py +12 -7
  99. inspect_ai/agent/_run.py +2 -3
  100. inspect_ai/analysis/beta/__init__.py +2 -0
  101. inspect_ai/analysis/beta/_dataframe/samples/table.py +19 -18
  102. inspect_ai/dataset/_sources/csv.py +2 -6
  103. inspect_ai/dataset/_sources/hf.py +2 -6
  104. inspect_ai/dataset/_sources/json.py +2 -6
  105. inspect_ai/dataset/_util.py +23 -0
  106. inspect_ai/log/_log.py +1 -1
  107. inspect_ai/log/_recorders/eval.py +4 -3
  108. inspect_ai/log/_recorders/file.py +2 -9
  109. inspect_ai/log/_recorders/json.py +1 -0
  110. inspect_ai/log/_recorders/recorder.py +1 -0
  111. inspect_ai/log/_transcript.py +1 -1
  112. inspect_ai/model/_call_tools.py +6 -2
  113. inspect_ai/model/_openai.py +1 -1
  114. inspect_ai/model/_openai_responses.py +85 -41
  115. inspect_ai/model/_openai_web_search.py +38 -0
  116. inspect_ai/model/_providers/azureai.py +72 -3
  117. inspect_ai/model/_providers/openai.py +4 -1
  118. inspect_ai/model/_providers/openai_responses.py +5 -1
  119. inspect_ai/scorer/_metric.py +1 -2
  120. inspect_ai/scorer/_reducer/reducer.py +1 -1
  121. inspect_ai/solver/_task_state.py +2 -2
  122. inspect_ai/tool/_tool.py +6 -2
  123. inspect_ai/tool/_tool_def.py +27 -4
  124. inspect_ai/tool/_tool_info.py +2 -0
  125. inspect_ai/tool/_tools/_web_search/_google.py +43 -15
  126. inspect_ai/tool/_tools/_web_search/_tavily.py +46 -13
  127. inspect_ai/tool/_tools/_web_search/_web_search.py +214 -45
  128. inspect_ai/util/__init__.py +4 -0
  129. inspect_ai/util/_json.py +3 -0
  130. inspect_ai/util/_limit.py +230 -20
  131. inspect_ai/util/_sandbox/docker/compose.py +20 -11
  132. inspect_ai/util/_span.py +1 -1
  133. {inspect_ai-0.3.99.dist-info → inspect_ai-0.3.101.dist-info}/METADATA +3 -3
  134. {inspect_ai-0.3.99.dist-info → inspect_ai-0.3.101.dist-info}/RECORD +138 -124
  135. {inspect_ai-0.3.99.dist-info → inspect_ai-0.3.101.dist-info}/WHEEL +1 -1
  136. {inspect_ai-0.3.99.dist-info → inspect_ai-0.3.101.dist-info}/entry_points.txt +0 -0
  137. {inspect_ai-0.3.99.dist-info → inspect_ai-0.3.101.dist-info}/licenses/LICENSE +0 -0
  138. {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
- // Start polling for pending samples
192
- logPolling.startPolling(logFileName);
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: (collapsed: Record<string, true>) => void;
29
- collapseEvent: (id: string, collapsed: boolean) => void;
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: (collapsed: Record<string, true>) => {
146
+ setCollapsedEvents: (
147
+ scope: string,
148
+ collapsed: Record<string, boolean>,
149
+ ) => {
134
150
  set((state) => {
135
- state.sample.collapsedEvents = collapsed;
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 = null;
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
+ }