inspect-ai 0.3.55__py3-none-any.whl → 0.3.57__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 (131) hide show
  1. inspect_ai/__init__.py +1 -0
  2. inspect_ai/_cli/common.py +1 -1
  3. inspect_ai/_cli/trace.py +33 -20
  4. inspect_ai/_display/core/active.py +1 -1
  5. inspect_ai/_display/core/display.py +1 -1
  6. inspect_ai/_display/core/footer.py +1 -1
  7. inspect_ai/_display/core/panel.py +1 -1
  8. inspect_ai/_display/core/progress.py +0 -6
  9. inspect_ai/_display/core/rich.py +1 -1
  10. inspect_ai/_display/rich/display.py +2 -2
  11. inspect_ai/_display/textual/app.py +15 -17
  12. inspect_ai/_display/textual/widgets/clock.py +3 -3
  13. inspect_ai/_display/textual/widgets/samples.py +6 -13
  14. inspect_ai/_eval/context.py +9 -1
  15. inspect_ai/_eval/run.py +16 -11
  16. inspect_ai/_eval/score.py +4 -10
  17. inspect_ai/_eval/task/results.py +5 -4
  18. inspect_ai/_eval/task/run.py +6 -12
  19. inspect_ai/_eval/task/task.py +10 -0
  20. inspect_ai/_util/ansi.py +31 -0
  21. inspect_ai/_util/datetime.py +1 -1
  22. inspect_ai/_util/deprecation.py +1 -1
  23. inspect_ai/_util/format.py +7 -0
  24. inspect_ai/_util/json.py +11 -1
  25. inspect_ai/_util/logger.py +14 -13
  26. inspect_ai/_util/throttle.py +10 -1
  27. inspect_ai/_util/trace.py +79 -47
  28. inspect_ai/_util/transcript.py +37 -4
  29. inspect_ai/_util/vscode.py +51 -0
  30. inspect_ai/_view/notify.py +2 -1
  31. inspect_ai/_view/www/.prettierrc.js +12 -0
  32. inspect_ai/_view/www/App.css +22 -1
  33. inspect_ai/_view/www/dist/assets/index.css +2374 -2
  34. inspect_ai/_view/www/dist/assets/index.js +29752 -24492
  35. inspect_ai/_view/www/log-schema.json +262 -215
  36. inspect_ai/_view/www/package.json +1 -0
  37. inspect_ai/_view/www/src/App.mjs +19 -9
  38. inspect_ai/_view/www/src/Types.mjs +0 -1
  39. inspect_ai/_view/www/src/api/Types.mjs +15 -4
  40. inspect_ai/_view/www/src/api/api-http.mjs +2 -0
  41. inspect_ai/_view/www/src/appearance/Icons.mjs +2 -0
  42. inspect_ai/_view/www/src/components/AsciiCinemaPlayer.mjs +74 -0
  43. inspect_ai/_view/www/src/components/CopyButton.mjs +0 -1
  44. inspect_ai/_view/www/src/components/ExpandablePanel.mjs +2 -2
  45. inspect_ai/_view/www/src/components/FindBand.mjs +5 -4
  46. inspect_ai/_view/www/src/components/HumanBaselineView.mjs +168 -0
  47. inspect_ai/_view/www/src/components/LargeModal.mjs +1 -1
  48. inspect_ai/_view/www/src/components/LightboxCarousel.mjs +217 -0
  49. inspect_ai/_view/www/src/components/MessageContent.mjs +1 -1
  50. inspect_ai/_view/www/src/components/TabSet.mjs +1 -1
  51. inspect_ai/_view/www/src/components/Tools.mjs +28 -5
  52. inspect_ai/_view/www/src/components/VirtualList.mjs +15 -17
  53. inspect_ai/_view/www/src/log/remoteLogFile.mjs +2 -1
  54. inspect_ai/_view/www/src/navbar/Navbar.mjs +44 -32
  55. inspect_ai/_view/www/src/samples/SampleDisplay.mjs +1 -2
  56. inspect_ai/_view/www/src/samples/SampleList.mjs +35 -4
  57. inspect_ai/_view/www/src/samples/SampleScoreView.mjs +13 -2
  58. inspect_ai/_view/www/src/samples/SampleScores.mjs +11 -2
  59. inspect_ai/_view/www/src/samples/SamplesDescriptor.mjs +238 -178
  60. inspect_ai/_view/www/src/samples/SamplesTab.mjs +4 -2
  61. inspect_ai/_view/www/src/samples/tools/SampleFilter.mjs +5 -5
  62. inspect_ai/_view/www/src/samples/tools/SelectScorer.mjs +7 -0
  63. inspect_ai/_view/www/src/samples/tools/SortFilter.mjs +3 -3
  64. inspect_ai/_view/www/src/samples/transcript/ModelEventView.mjs +3 -2
  65. inspect_ai/_view/www/src/samples/transcript/ToolEventView.mjs +1 -1
  66. inspect_ai/_view/www/src/samples/transcript/TranscriptView.mjs +1 -0
  67. inspect_ai/_view/www/src/samples/transcript/state/StateEventRenderers.mjs +56 -0
  68. inspect_ai/_view/www/src/samples/transcript/state/StateEventView.mjs +17 -5
  69. inspect_ai/_view/www/src/types/asciicinema-player.d.ts +26 -0
  70. inspect_ai/_view/www/src/types/log.d.ts +28 -20
  71. inspect_ai/_view/www/src/workspace/WorkSpace.mjs +1 -1
  72. inspect_ai/_view/www/yarn.lock +44 -0
  73. inspect_ai/approval/_apply.py +4 -0
  74. inspect_ai/approval/_human/panel.py +5 -8
  75. inspect_ai/dataset/_dataset.py +51 -10
  76. inspect_ai/dataset/_util.py +31 -3
  77. inspect_ai/log/__init__.py +2 -0
  78. inspect_ai/log/_log.py +30 -2
  79. inspect_ai/log/_recorders/eval.py +2 -0
  80. inspect_ai/model/_call_tools.py +31 -7
  81. inspect_ai/model/_chat_message.py +3 -0
  82. inspect_ai/model/_model.py +42 -1
  83. inspect_ai/model/_providers/anthropic.py +4 -0
  84. inspect_ai/model/_providers/google.py +24 -6
  85. inspect_ai/model/_providers/openai.py +17 -3
  86. inspect_ai/model/_providers/openai_o1.py +10 -12
  87. inspect_ai/model/_render.py +9 -2
  88. inspect_ai/scorer/_metric.py +12 -1
  89. inspect_ai/solver/__init__.py +2 -0
  90. inspect_ai/solver/_human_agent/agent.py +83 -0
  91. inspect_ai/solver/_human_agent/commands/__init__.py +36 -0
  92. inspect_ai/solver/_human_agent/commands/clock.py +70 -0
  93. inspect_ai/solver/_human_agent/commands/command.py +59 -0
  94. inspect_ai/solver/_human_agent/commands/instructions.py +74 -0
  95. inspect_ai/solver/_human_agent/commands/note.py +42 -0
  96. inspect_ai/solver/_human_agent/commands/score.py +80 -0
  97. inspect_ai/solver/_human_agent/commands/status.py +62 -0
  98. inspect_ai/solver/_human_agent/commands/submit.py +151 -0
  99. inspect_ai/solver/_human_agent/install.py +222 -0
  100. inspect_ai/solver/_human_agent/panel.py +252 -0
  101. inspect_ai/solver/_human_agent/service.py +45 -0
  102. inspect_ai/solver/_human_agent/state.py +55 -0
  103. inspect_ai/solver/_human_agent/view.py +24 -0
  104. inspect_ai/solver/_task_state.py +28 -2
  105. inspect_ai/tool/_tool.py +10 -2
  106. inspect_ai/tool/_tool_info.py +2 -1
  107. inspect_ai/tool/_tools/_web_browser/_resources/dm_env_servicer.py +9 -9
  108. inspect_ai/tool/_tools/_web_browser/_web_browser.py +16 -13
  109. inspect_ai/util/__init__.py +12 -4
  110. inspect_ai/{_util/display.py → util/_display.py} +6 -0
  111. inspect_ai/util/_panel.py +31 -9
  112. inspect_ai/util/_sandbox/__init__.py +0 -3
  113. inspect_ai/util/_sandbox/context.py +5 -1
  114. inspect_ai/util/_sandbox/docker/compose.py +17 -13
  115. inspect_ai/util/_sandbox/docker/docker.py +9 -6
  116. inspect_ai/util/_sandbox/docker/internal.py +1 -1
  117. inspect_ai/util/_sandbox/docker/util.py +3 -2
  118. inspect_ai/util/_sandbox/environment.py +6 -5
  119. inspect_ai/util/_sandbox/local.py +1 -1
  120. inspect_ai/util/_sandbox/self_check.py +18 -18
  121. inspect_ai/util/_sandbox/service.py +22 -7
  122. inspect_ai/util/_store.py +7 -8
  123. inspect_ai/util/_store_model.py +110 -0
  124. inspect_ai/util/_subprocess.py +3 -3
  125. inspect_ai/util/_throttle.py +32 -0
  126. {inspect_ai-0.3.55.dist-info → inspect_ai-0.3.57.dist-info}/METADATA +3 -3
  127. {inspect_ai-0.3.55.dist-info → inspect_ai-0.3.57.dist-info}/RECORD +131 -108
  128. {inspect_ai-0.3.55.dist-info → inspect_ai-0.3.57.dist-info}/WHEEL +1 -1
  129. {inspect_ai-0.3.55.dist-info → inspect_ai-0.3.57.dist-info}/LICENSE +0 -0
  130. {inspect_ai-0.3.55.dist-info → inspect_ai-0.3.57.dist-info}/entry_points.txt +0 -0
  131. {inspect_ai-0.3.55.dist-info → inspect_ai-0.3.57.dist-info}/top_level.txt +0 -0
@@ -26,6 +26,7 @@
26
26
  },
27
27
  "dependencies": {
28
28
  "@popperjs/core": "^2.11.8",
29
+ "asciinema-player": "^3.8.1",
29
30
  "bootstrap": "^5.3.3",
30
31
  "bootstrap-icons": "^1.11.3",
31
32
  "clipboard": "^2.0.11",
@@ -3,6 +3,7 @@ import "bootstrap-icons/font/bootstrap-icons.css";
3
3
  import "prismjs/themes/prism.css";
4
4
  import "prismjs";
5
5
  import "../App.css";
6
+ import "asciinema-player/dist/bundle/asciinema-player.css";
6
7
 
7
8
  import { default as ClipboardJS } from "clipboard";
8
9
  // @ts-ignore
@@ -31,7 +32,10 @@ import { FindBand } from "./components/FindBand.mjs";
31
32
  import { isVscode } from "./utils/Html.mjs";
32
33
  import { getVscodeApi } from "./utils/vscode.mjs";
33
34
  import { kDefaultSort } from "./constants.mjs";
34
- import { createsSamplesDescriptor } from "./samples/SamplesDescriptor.mjs";
35
+ import {
36
+ createEvalDescriptor,
37
+ createSamplesDescriptor,
38
+ } from "./samples/SamplesDescriptor.mjs";
35
39
  import { byEpoch, bySample, sortSamples } from "./samples/tools/SortFilter.mjs";
36
40
  import { resolveAttachments } from "./utils/attachments.mjs";
37
41
  import { filterFnForType } from "./samples/tools/filters.mjs";
@@ -75,7 +79,7 @@ export function App({
75
79
  initialState?.headersLoading || false,
76
80
  );
77
81
 
78
- // Selected Log
82
+ /** @type {[import("./Types.mjs").CurrentLog, function(import("./Types.mjs").CurrentLog): void]} */
79
83
  const [selectedLog, setSelectedLog] = useState(
80
84
  initialState?.selectedLog || {
81
85
  contents: undefined,
@@ -94,6 +98,7 @@ export function App({
94
98
  ? initialState.selectedSampleIndex
95
99
  : -1,
96
100
  );
101
+ /** @type {[import("./types/log").EvalSample, function(import("./types/log").EvalSample): void]} */
97
102
  const [selectedSample, setSelectedSample] = useState(
98
103
  initialState?.selectedSample,
99
104
  );
@@ -325,7 +330,7 @@ export function App({
325
330
 
326
331
  // Set the grouping
327
332
  let grouping = "none";
328
- if (samplesDescriptor?.epochs > 1) {
333
+ if (samplesDescriptor?.evalDescriptor?.epochs > 1) {
329
334
  if (byEpoch(sort) || epoch !== "all") {
330
335
  grouping = "epoch";
331
336
  } else if (bySample(sort)) {
@@ -338,14 +343,17 @@ export function App({
338
343
  setGroupByOrder(order);
339
344
  }, [selectedLog, filter, sort, epoch]);
340
345
 
341
- const samplesDescriptor = useMemo(() => {
342
- return createsSamplesDescriptor(
346
+ const evalDescriptor = useMemo(() => {
347
+ return createEvalDescriptor(
343
348
  scores,
344
349
  selectedLog.contents?.sampleSummaries,
345
350
  selectedLog.contents?.eval?.config?.epochs || 1,
346
- score,
347
351
  );
348
- }, [selectedLog, scores, score]);
352
+ }, [selectedLog, scores]);
353
+
354
+ const samplesDescriptor = useMemo(() => {
355
+ return createSamplesDescriptor(evalDescriptor, score);
356
+ }, [evalDescriptor, score]);
349
357
 
350
358
  const refreshSampleTab = useCallback(
351
359
  (sample) => {
@@ -512,9 +520,11 @@ export function App({
512
520
  // Reset the workspace tab
513
521
  const hasSamples =
514
522
  !!log.sampleSummaries && log.sampleSummaries.length > 0;
515
- const showSamples = log.status !== "error" && hasSamples;
523
+ const showSamples = hasSamples;
516
524
  setSelectedWorkspaceTab(
517
- showSamples ? kEvalWorkspaceTabId : kInfoWorkspaceTabId,
525
+ log.status !== "error" && hasSamples
526
+ ? kEvalWorkspaceTabId
527
+ : kInfoWorkspaceTabId,
518
528
  );
519
529
 
520
530
  // Select the default scorer to use
@@ -8,7 +8,6 @@
8
8
  * @typedef {Object} CurrentLog
9
9
  * @property {string} name
10
10
  * @property {import("./api/Types.mjs").EvalSummary} contents
11
- * @property {string} raw
12
11
  */
13
12
 
14
13
  /**
@@ -30,15 +30,26 @@
30
30
  * @property { import("../types/log").Input } input
31
31
  * @property { import("../types/log").Target } target
32
32
  * @property { import("../types/log").Scores1 } scores
33
+ * @property { string } [error]
33
34
  * @property { import("../types/log").Type11 } [limit]
34
35
  */
35
36
 
36
37
  /**
37
- * @typedef {Object} Capabilities
38
- * @property {boolean} downloadFiles - Indicates if file downloads are supported.
39
- * @property {boolean} webWorkers - Indicates if web workers are supported.
40
- *
38
+ * Fields shared by EvalSample and SampleSummary.
39
+ * Contains only fields that are copied verbatim in src/inspect_ai/log/_recorders/eval.py.
40
+ *
41
+ * @typedef {Object} BasicSampleData
42
+ * @property { number | string } id
43
+ * @property { number } epoch
44
+ * @property { import("../types/log").Target } target
45
+ * @property { import("../types/log").Scores1 } scores
46
+ */
41
47
 
48
+ /**
49
+ * @typedef {Object} Capabilities
50
+ * @property {boolean} downloadFiles - Indicates if file downloads are supported.
51
+ * @property {boolean} webWorkers - Indicates if web workers are supported.
52
+ */
42
53
 
43
54
  /**
44
55
  * @typedef {Object} LogViewAPI
@@ -56,6 +56,7 @@ function simpleHttpAPI(logInfo) {
56
56
  });
57
57
  return Promise.resolve({
58
58
  files: logs,
59
+ log_dir,
59
60
  });
60
61
  } else if (log_file) {
61
62
  // Check the cache
@@ -76,6 +77,7 @@ function simpleHttpAPI(logInfo) {
76
77
 
77
78
  return {
78
79
  files: [result],
80
+ log_dir,
79
81
  };
80
82
  } else {
81
83
  // No log.json could be found, and there isn't a log file,
@@ -123,6 +123,7 @@
123
123
  * @property {string} toggle-right
124
124
  * @property {string} more
125
125
  * @property {string} next
126
+ * @property {string} play
126
127
  * @property {string} previous
127
128
  * @property {string} refresh
128
129
  * @property {RoleIcons} role
@@ -209,6 +210,7 @@ export const ApplicationIcons = {
209
210
  more: "bi bi-zoom-in",
210
211
  "multiple-choice": "bi bi-card-list",
211
212
  next: "bi bi-chevron-right",
213
+ play: "bi bi-play-fill",
212
214
  previous: "bi bi-chevron-left",
213
215
  refresh: "bi bi-arrow-clockwise",
214
216
  role: {
@@ -0,0 +1,74 @@
1
+ // @ts-check
2
+ import { html } from "htm/preact";
3
+ import { useEffect, useRef } from "preact/hooks";
4
+
5
+ // Import the asciinema player library
6
+ import * as AsciinemaPlayer from "asciinema-player";
7
+
8
+ /**
9
+ * Renders the ChatView component.
10
+ *
11
+ * @param {Object} props - The properties passed to the component.
12
+ * @param {string} props.id - The ID for the chat view.
13
+ * @param {string} props.inputUrl - The input url for the player
14
+ * @param {string} props.outputUrl - The output url for the player
15
+ * @param {string} props.timingUrl - The timing url for the player
16
+ * @param {number} [props.rows] - The rows for the player's initial size
17
+ * @param {number} [props.cols] - The cols for the player's initial size
18
+ * @param {string} [props.fit] - how to fit the player
19
+ * @param {Object} [props.style] - Inline styles for the chat view.
20
+ * @param {number} [props.speed] - The speed to play (1 = 1x, 1.5 = 1.5x...)
21
+ * @param {boolean} [props.autoPlay] - Whether to autoplay
22
+ * @param {boolean} [props.loop] - Whether to loop
23
+ * @param {string} [props.theme] - The terminal theme (e.g. "solarized-dark")
24
+ * @param {number} [props.idleTimeLimit] - The amount to compress idle time to
25
+ * @returns {import("preact").JSX.Element} The component.
26
+ */
27
+ export const AsciiCinemaPlayer = ({
28
+ id,
29
+ rows,
30
+ cols,
31
+ inputUrl,
32
+ outputUrl,
33
+ timingUrl,
34
+ fit,
35
+ speed,
36
+ autoPlay,
37
+ loop,
38
+ theme,
39
+ idleTimeLimit = 2,
40
+ style,
41
+ }) => {
42
+ const playerContainerRef = useRef();
43
+ useEffect(() => {
44
+ const player = AsciinemaPlayer.create(
45
+ {
46
+ url: [timingUrl, outputUrl, inputUrl],
47
+ parser: "typescript",
48
+ },
49
+ playerContainerRef.current,
50
+ {
51
+ rows: rows,
52
+ cols: cols,
53
+ autoPlay,
54
+ loop,
55
+ theme,
56
+ speed,
57
+ idleTimeLimit,
58
+ fit,
59
+ },
60
+ );
61
+ player.play();
62
+ return () => {
63
+ player.dispose();
64
+ };
65
+ }, []);
66
+
67
+ return html`
68
+ <div
69
+ id="asciinema-player-${id || "default"}"
70
+ ref=${playerContainerRef}
71
+ style=${{ ...style }}
72
+ ></div>
73
+ `;
74
+ };
@@ -32,7 +32,6 @@ export const CopyButton = ({ value }) => {
32
32
  if (iEl.tagName === "BUTTON") {
33
33
  iEl = iEl.firstChild;
34
34
  }
35
- console.log({ iEl });
36
35
  if (iEl) {
37
36
  if (iEl) {
38
37
  iEl.className = `${ApplicationIcons.confirm} primary`;
@@ -14,8 +14,8 @@ export const ExpandablePanel = ({
14
14
  const [collapsed, setCollapsed] = useState(collapse);
15
15
  const [showToggle, setShowToggle] = useState(false);
16
16
 
17
- const contentsRef = useRef();
18
- const observerRef = useRef();
17
+ const contentsRef = useRef(/** @type {HTMLElement|null} */ (null));
18
+ const observerRef = useRef(/** @type {IntersectionObserver|null} */ (null));
19
19
 
20
20
  // Ensure that when content changes, we reset the collapse state.
21
21
  useEffect(() => {
@@ -4,7 +4,7 @@ import { ApplicationIcons } from "../appearance/Icons.mjs";
4
4
  import { FontSize } from "../appearance/Fonts.mjs";
5
5
 
6
6
  export const FindBand = ({ hideBand }) => {
7
- const searchBoxRef = useRef();
7
+ const searchBoxRef = useRef(/** @type {HTMLInputElement|null} */ (null));
8
8
  useEffect(() => {
9
9
  searchBoxRef.current.focus();
10
10
  }, []);
@@ -31,13 +31,14 @@ export const FindBand = ({ hideBand }) => {
31
31
  };
32
32
 
33
33
  // capture what is focused
34
- const focusedElement = document.activeElement;
34
+ const focusedElement = /** @type {HTMLElement} */ (document.activeElement);
35
+ // @ts-expect-error: `Window.find` is non-standard
35
36
  const result = window.find(term, false, !!back, false, false, true, false);
36
37
  const noResultEl = window.document.getElementById(
37
38
  "inspect-find-no-results",
38
39
  );
39
40
  if (result) {
40
- noResultEl.style.opacity = 0;
41
+ noResultEl.style.opacity = "0";
41
42
  const selection = window.getSelection();
42
43
  if (selection.rangeCount > 0) {
43
44
  // See if the parent is an expandable panel and expand it
@@ -58,7 +59,7 @@ export const FindBand = ({ hideBand }) => {
58
59
  }, 100);
59
60
  }
60
61
  } else {
61
- noResultEl.style.opacity = 1;
62
+ noResultEl.style.opacity = "1";
62
63
  }
63
64
 
64
65
  // Return focus to the previously focused element
@@ -0,0 +1,168 @@
1
+ // @ts-check
2
+ import { html } from "htm/preact";
3
+ import { useEffect } from "preact/hooks";
4
+ import { formatDateTime, formatTime } from "../utils/Format.mjs";
5
+ import { AsciiCinemaPlayer } from "./AsciiCinemaPlayer.mjs";
6
+ import { TextStyle } from "../appearance/Fonts.mjs";
7
+ import { LightboxCarousel } from "./LightboxCarousel.mjs";
8
+
9
+ /**
10
+ * @typedef {Object} SessionLog
11
+ * @property {string} name - The name of this session
12
+ * @property {string} user - The user for this session
13
+ * @property {string} input - The input for this session
14
+ * @property {string} output - The output for this session
15
+ * @property {string} timing - The timing for this session
16
+ */
17
+
18
+ /**
19
+ * Renders the HumanBaselineView component.
20
+ *
21
+ * @param {Object} props - The properties passed to the component.
22
+ * @param {Date} props.started - When the baselining started
23
+ * @param {boolean} props.running - Whether the baselining is running
24
+ * @param {boolean} [props.completed] - Whether the baselining was completed
25
+ * @param {number} [props.runtime] - Duration of baselining in seconds
26
+ * @param {string} [props.answer] - The answer for the baselining
27
+ * @param {SessionLog[]} [props.sessionLogs] - The session logs for the baselining
28
+ * @returns {import("preact").JSX.Element} The component.
29
+ */
30
+ export const HumanBaselineView = ({
31
+ started,
32
+ runtime,
33
+ answer,
34
+ completed,
35
+ running,
36
+ sessionLogs,
37
+ }) => {
38
+ const player_fns = [];
39
+
40
+ // handle creation and revoking of these URLs
41
+ const revokableUrls = [];
42
+ const revokableUrl = (data) => {
43
+ const blob = new Blob([data], { type: "text/plain" });
44
+ const url = URL.createObjectURL(blob);
45
+ revokableUrls.push(url);
46
+ return url;
47
+ };
48
+
49
+ useEffect(() => {
50
+ return () => {
51
+ revokableUrls.forEach((url) => URL.revokeObjectURL(url));
52
+ };
53
+ }, []);
54
+
55
+ // Make a player for each session log
56
+ let count = 1;
57
+ let maxCols = 0;
58
+
59
+ for (const sessionLog of sessionLogs) {
60
+ const rows = extractSize(sessionLog.output, "LINES");
61
+ const cols = extractSize(sessionLog.output, "COLUMNS");
62
+ maxCols = Math.max(maxCols, parseInt(cols));
63
+
64
+ const currentCount = count;
65
+ const title =
66
+ sessionLogs.length === 1
67
+ ? "Terminal Session"
68
+ : `Terminal Session ${currentCount}`;
69
+
70
+ player_fns.push({
71
+ label: title,
72
+ render: () => html`
73
+ <${AsciiCinemaPlayer}
74
+ id=${`player-${currentCount}`}
75
+ inputUrl=${revokableUrl(sessionLog.input)}
76
+ outputUrl=${revokableUrl(sessionLog.output)}
77
+ timingUrl=${revokableUrl(sessionLog.timing)}
78
+ rows=${rows}
79
+ cols=${cols}
80
+ style=${{
81
+ maxHeight: "100vh",
82
+ maxWidth: "100vw",
83
+ height: `${parseInt(rows) * 2}em`,
84
+ width: `${parseInt(cols) * 2}em`,
85
+ }}
86
+ fit="both"
87
+ />
88
+ `,
89
+ });
90
+ count += 1;
91
+ }
92
+
93
+ const StatusMessage = ({ completed, running, answer }) => {
94
+ if (running) {
95
+ return html`<span style=${{ ...TextStyle.label }}>Running</span>`;
96
+ } else if (completed) {
97
+ return html`<span style=${{ ...TextStyle.label, marginRight: "0.5em" }}
98
+ >Answer</span
99
+ ><span>${answer}</span>`;
100
+ } else {
101
+ return "Unknown status";
102
+ }
103
+ };
104
+
105
+ return html`<div style=${{ display: "flex", justifyContent: "center" }}>
106
+ <div
107
+ style=${{
108
+ display: "grid",
109
+ gridTemplateColumns: "1fr 1fr 1fr",
110
+ gridTemplateRows: "auto auto",
111
+ width: "100%",
112
+ }}
113
+ >
114
+ <div
115
+ style=${{
116
+ justifySelf: "start",
117
+ ...TextStyle.label,
118
+ }}
119
+ >
120
+ ${started ? formatDateTime(started) : ""}${runtime
121
+ ? ` (${formatTime(Math.floor(runtime))})`
122
+ : ""}
123
+ </div>
124
+ <div
125
+ style=${{
126
+ justifySelf: "center",
127
+ ...TextStyle.label,
128
+ }}
129
+ ></div>
130
+ <div
131
+ style=${{
132
+ justifySelf: "end",
133
+ }}
134
+ >
135
+ <${StatusMessage}
136
+ completed=${completed}
137
+ running=${running}
138
+ answer=${answer}
139
+ />
140
+ </div>
141
+ <div
142
+ style=${{
143
+ gridColumn: "span 3",
144
+ width: "100%",
145
+ }}
146
+ >
147
+ <${LightboxCarousel} slides=${player_fns} />
148
+ </div>
149
+ </div>
150
+ </div>`;
151
+ };
152
+
153
+ /**
154
+ * Extracts a numeric size value from a string based on a given label.
155
+ *
156
+ * Searches the input string for a pattern matching the format `LABEL="VALUE"`,
157
+ * where `LABEL` is the provided label and `VALUE` is a numeric value.
158
+ *
159
+ * @param {string} value - The input string to search within.
160
+ * @param {string} label - The label to look for in the string.
161
+ * @returns {string | undefined} The extracted size as a string if found, otherwise `undefined`.
162
+ */
163
+ const extractSize = (value, label) => {
164
+ const regex = new RegExp(`${label}="(\\d+)"`);
165
+ const match = value.match(regex);
166
+ const size = match ? match[1] : undefined;
167
+ return size;
168
+ };
@@ -31,7 +31,7 @@ export const LargeModal = (props) => {
31
31
 
32
32
  // Support restoring the scroll position
33
33
  // but only do this for the first time that the children are set
34
- const scrollRef = useRef();
34
+ const scrollRef = useRef(/** @type {HTMLElement|null} */ (null));
35
35
  useEffect(() => {
36
36
  if (scrollRef.current) {
37
37
  setTimeout(() => {
@@ -0,0 +1,217 @@
1
+ import { html } from "htm/preact";
2
+ import { useState, useCallback, useEffect } from "preact/hooks";
3
+ import { ApplicationIcons } from "../appearance/Icons.mjs";
4
+
5
+ /**
6
+ * @typedef {Object} Slide
7
+ * @property {string} label - The label for the slide.
8
+ * @property {() => import("preact").JSX.Element} render - A function that returns another function to render the slide as a JSX element.
9
+ */
10
+
11
+ /**
12
+ * LightboxCarousel component provides a carousel with lightbox functionality.
13
+ * @param {Object} props - Component properties.
14
+ * @property {Array<Slide>} props.slides - Array of slide render functions.
15
+ * @returns {import("preact").JSX.Element} LightboxCarousel component.
16
+ */
17
+ export const LightboxCarousel = ({ slides }) => {
18
+ const [isOpen, setIsOpen] = useState(false);
19
+ const [showOverlay, setShowOverlay] = useState(false);
20
+ const [currentIndex, setCurrentIndex] = useState(0);
21
+
22
+ const openLightbox = (index) => {
23
+ setCurrentIndex(index);
24
+ setShowOverlay(true);
25
+
26
+ // Slight delay before setting isOpen so the fade-in starts from opacity: 0
27
+ setTimeout(() => setIsOpen(true), 10);
28
+ };
29
+
30
+ const closeLightbox = () => {
31
+ setIsOpen(false);
32
+ };
33
+
34
+ // Remove the overlay from the DOM after fade-out completes
35
+ useEffect(() => {
36
+ if (!isOpen && showOverlay) {
37
+ const timer = setTimeout(() => {
38
+ setShowOverlay(false);
39
+ }, 300); // match your transition duration
40
+ return () => clearTimeout(timer);
41
+ }
42
+ }, [isOpen, showOverlay]);
43
+
44
+ const showNext = useCallback(() => {
45
+ setCurrentIndex((prev) => {
46
+ return (prev + 1) % slides.length;
47
+ });
48
+ }, [slides]);
49
+
50
+ const showPrev = useCallback(() => {
51
+ setCurrentIndex((prev) => (prev - 1 + slides.length) % slides.length);
52
+ }, [slides]);
53
+
54
+ // Keyboard Navigation
55
+ useEffect(() => {
56
+ if (!isOpen) return;
57
+ const handleKeyUp = (e) => {
58
+ if (e.key === "Escape") {
59
+ closeLightbox();
60
+ } else if (e.key === "ArrowRight") {
61
+ showNext();
62
+ } else if (e.key === "ArrowLeft") {
63
+ showPrev();
64
+ }
65
+ e.preventDefault();
66
+ e.stopPropagation();
67
+ };
68
+ window.addEventListener("keyup", handleKeyUp, true);
69
+ return () => window.removeEventListener("keyup", handleKeyUp);
70
+ }, [isOpen, showNext, showPrev]);
71
+
72
+ // Common button style
73
+ const buttonStyle = {
74
+ position: "absolute",
75
+ top: "50%",
76
+ transform: "translateY(-50%)",
77
+ background: "none",
78
+ color: "#fff",
79
+ border: "none",
80
+ padding: "0.5em",
81
+ fontSize: "3em",
82
+ cursor: "pointer",
83
+ zIndex: "9999",
84
+ };
85
+
86
+ const prevButtonStyle = {
87
+ ...buttonStyle,
88
+ left: "10px",
89
+ };
90
+
91
+ const nextButtonStyle = {
92
+ ...buttonStyle,
93
+ right: "10px",
94
+ };
95
+
96
+ // Overlay style (fixed to fill the screen, flex for centering)
97
+ const overlayStyle = {
98
+ position: "fixed",
99
+ top: 0,
100
+ left: 0,
101
+ width: "100vw",
102
+ height: "100vh",
103
+ background: "rgba(0,0,0,0.8)",
104
+ display: "flex",
105
+ alignItems: "center",
106
+ justifyContent: "center",
107
+ opacity: isOpen ? "1" : "0",
108
+ visibility: isOpen ? "visible" : "hidden",
109
+ transition: "opacity 0.3s ease, visibility 0.3s ease",
110
+ zIndex: 9998,
111
+ };
112
+
113
+ // Close button style
114
+ const closeButtonWrapperStyle = {
115
+ position: "absolute",
116
+ top: "10px",
117
+ right: "10px",
118
+ };
119
+
120
+ const closeButtonStyle = {
121
+ border: "none",
122
+ background: "none",
123
+ color: "#fff",
124
+ fontSize: "3em",
125
+ fontWeight: "500",
126
+ cursor: "pointer",
127
+ paddingLeft: "1em",
128
+ paddingBottom: "1em",
129
+ zIndex: "10000",
130
+ };
131
+
132
+ // Lightbox content container style
133
+ const contentStyle = {
134
+ maxWidth: "90%",
135
+ maxHeight: "90%",
136
+ display: "flex",
137
+ alignItems: "center",
138
+ justifyContent: "center",
139
+ position: "relative",
140
+ // fade in/out
141
+ opacity: isOpen ? "1" : "0",
142
+ visibility: isOpen ? "visible" : "hidden",
143
+ transition: "opacity 0.3s ease, visibility 0.3s ease",
144
+ zIndex: 9999,
145
+ };
146
+
147
+ return html`
148
+ <div className="lightbox-carousel-container">
149
+ <!-- Thumbnails -->
150
+ <div
151
+ className="carousel-thumbs"
152
+ style=${{
153
+ display: "grid",
154
+ gridTemplateColumns: "auto auto auto auto",
155
+ }}
156
+ >
157
+ ${slides.map((slide, index) => {
158
+ return html`
159
+ <div
160
+ key=${index}
161
+ className="carousel-thumb"
162
+ onClick=${() => openLightbox(index)}
163
+ style=${{
164
+ background: "black",
165
+ color: "white",
166
+ padding: "4em 0",
167
+ border: "0",
168
+ margin: "5px",
169
+ cursor: "pointer",
170
+ textAlign: "center",
171
+ }}
172
+ >
173
+ <div>${slide.label}</div>
174
+ <div>
175
+ <i
176
+ class=${ApplicationIcons.play}
177
+ style=${{ fontSize: "4em" }}
178
+ ></i>
179
+ </div>
180
+ </div>
181
+ `;
182
+ })}
183
+ </div>
184
+
185
+ <!-- Lightbox Overlay -->
186
+ ${showOverlay &&
187
+ html`
188
+ <div className="lightbox-overlay" style=${overlayStyle}>
189
+ <div style=${closeButtonWrapperStyle}>
190
+ <button style=${closeButtonStyle} onClick=${closeLightbox}>
191
+ <i class=${ApplicationIcons.close}></i>
192
+ </button>
193
+ </div>
194
+
195
+ ${slides.length > 1
196
+ ? html` <button style=${prevButtonStyle} onClick=${showPrev}>
197
+ <i class=${ApplicationIcons.previous}></i>
198
+ </button>`
199
+ : ""}
200
+ ${slides.length > 1
201
+ ? html` <button style=${nextButtonStyle} onClick=${showNext}>
202
+ <i class=${ApplicationIcons.next}></i>
203
+ </button>`
204
+ : ""}
205
+
206
+ <div
207
+ key=${`carousel-slide-${currentIndex}`}
208
+ className="lightbox-content"
209
+ style=${contentStyle}
210
+ >
211
+ ${slides[currentIndex].render()}
212
+ </div>
213
+ </div>
214
+ `}
215
+ </div>
216
+ `;
217
+ };