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.
- inspect_ai/__init__.py +1 -0
- inspect_ai/_cli/common.py +1 -1
- inspect_ai/_cli/trace.py +33 -20
- inspect_ai/_display/core/active.py +1 -1
- inspect_ai/_display/core/display.py +1 -1
- inspect_ai/_display/core/footer.py +1 -1
- inspect_ai/_display/core/panel.py +1 -1
- inspect_ai/_display/core/progress.py +0 -6
- inspect_ai/_display/core/rich.py +1 -1
- inspect_ai/_display/rich/display.py +2 -2
- inspect_ai/_display/textual/app.py +15 -17
- inspect_ai/_display/textual/widgets/clock.py +3 -3
- inspect_ai/_display/textual/widgets/samples.py +6 -13
- inspect_ai/_eval/context.py +9 -1
- inspect_ai/_eval/run.py +16 -11
- inspect_ai/_eval/score.py +4 -10
- inspect_ai/_eval/task/results.py +5 -4
- inspect_ai/_eval/task/run.py +6 -12
- inspect_ai/_eval/task/task.py +10 -0
- inspect_ai/_util/ansi.py +31 -0
- inspect_ai/_util/datetime.py +1 -1
- inspect_ai/_util/deprecation.py +1 -1
- inspect_ai/_util/format.py +7 -0
- inspect_ai/_util/json.py +11 -1
- inspect_ai/_util/logger.py +14 -13
- inspect_ai/_util/throttle.py +10 -1
- inspect_ai/_util/trace.py +79 -47
- inspect_ai/_util/transcript.py +37 -4
- inspect_ai/_util/vscode.py +51 -0
- inspect_ai/_view/notify.py +2 -1
- inspect_ai/_view/www/.prettierrc.js +12 -0
- inspect_ai/_view/www/App.css +22 -1
- inspect_ai/_view/www/dist/assets/index.css +2374 -2
- inspect_ai/_view/www/dist/assets/index.js +29752 -24492
- inspect_ai/_view/www/log-schema.json +262 -215
- inspect_ai/_view/www/package.json +1 -0
- inspect_ai/_view/www/src/App.mjs +19 -9
- inspect_ai/_view/www/src/Types.mjs +0 -1
- inspect_ai/_view/www/src/api/Types.mjs +15 -4
- inspect_ai/_view/www/src/api/api-http.mjs +2 -0
- inspect_ai/_view/www/src/appearance/Icons.mjs +2 -0
- inspect_ai/_view/www/src/components/AsciiCinemaPlayer.mjs +74 -0
- inspect_ai/_view/www/src/components/CopyButton.mjs +0 -1
- inspect_ai/_view/www/src/components/ExpandablePanel.mjs +2 -2
- inspect_ai/_view/www/src/components/FindBand.mjs +5 -4
- inspect_ai/_view/www/src/components/HumanBaselineView.mjs +168 -0
- inspect_ai/_view/www/src/components/LargeModal.mjs +1 -1
- inspect_ai/_view/www/src/components/LightboxCarousel.mjs +217 -0
- inspect_ai/_view/www/src/components/MessageContent.mjs +1 -1
- inspect_ai/_view/www/src/components/TabSet.mjs +1 -1
- inspect_ai/_view/www/src/components/Tools.mjs +28 -5
- inspect_ai/_view/www/src/components/VirtualList.mjs +15 -17
- inspect_ai/_view/www/src/log/remoteLogFile.mjs +2 -1
- inspect_ai/_view/www/src/navbar/Navbar.mjs +44 -32
- inspect_ai/_view/www/src/samples/SampleDisplay.mjs +1 -2
- inspect_ai/_view/www/src/samples/SampleList.mjs +35 -4
- inspect_ai/_view/www/src/samples/SampleScoreView.mjs +13 -2
- inspect_ai/_view/www/src/samples/SampleScores.mjs +11 -2
- inspect_ai/_view/www/src/samples/SamplesDescriptor.mjs +238 -178
- inspect_ai/_view/www/src/samples/SamplesTab.mjs +4 -2
- inspect_ai/_view/www/src/samples/tools/SampleFilter.mjs +5 -5
- inspect_ai/_view/www/src/samples/tools/SelectScorer.mjs +7 -0
- inspect_ai/_view/www/src/samples/tools/SortFilter.mjs +3 -3
- inspect_ai/_view/www/src/samples/transcript/ModelEventView.mjs +3 -2
- inspect_ai/_view/www/src/samples/transcript/ToolEventView.mjs +1 -1
- inspect_ai/_view/www/src/samples/transcript/TranscriptView.mjs +1 -0
- inspect_ai/_view/www/src/samples/transcript/state/StateEventRenderers.mjs +56 -0
- inspect_ai/_view/www/src/samples/transcript/state/StateEventView.mjs +17 -5
- inspect_ai/_view/www/src/types/asciicinema-player.d.ts +26 -0
- inspect_ai/_view/www/src/types/log.d.ts +28 -20
- inspect_ai/_view/www/src/workspace/WorkSpace.mjs +1 -1
- inspect_ai/_view/www/yarn.lock +44 -0
- inspect_ai/approval/_apply.py +4 -0
- inspect_ai/approval/_human/panel.py +5 -8
- inspect_ai/dataset/_dataset.py +51 -10
- inspect_ai/dataset/_util.py +31 -3
- inspect_ai/log/__init__.py +2 -0
- inspect_ai/log/_log.py +30 -2
- inspect_ai/log/_recorders/eval.py +2 -0
- inspect_ai/model/_call_tools.py +31 -7
- inspect_ai/model/_chat_message.py +3 -0
- inspect_ai/model/_model.py +42 -1
- inspect_ai/model/_providers/anthropic.py +4 -0
- inspect_ai/model/_providers/google.py +24 -6
- inspect_ai/model/_providers/openai.py +17 -3
- inspect_ai/model/_providers/openai_o1.py +10 -12
- inspect_ai/model/_render.py +9 -2
- inspect_ai/scorer/_metric.py +12 -1
- inspect_ai/solver/__init__.py +2 -0
- inspect_ai/solver/_human_agent/agent.py +83 -0
- inspect_ai/solver/_human_agent/commands/__init__.py +36 -0
- inspect_ai/solver/_human_agent/commands/clock.py +70 -0
- inspect_ai/solver/_human_agent/commands/command.py +59 -0
- inspect_ai/solver/_human_agent/commands/instructions.py +74 -0
- inspect_ai/solver/_human_agent/commands/note.py +42 -0
- inspect_ai/solver/_human_agent/commands/score.py +80 -0
- inspect_ai/solver/_human_agent/commands/status.py +62 -0
- inspect_ai/solver/_human_agent/commands/submit.py +151 -0
- inspect_ai/solver/_human_agent/install.py +222 -0
- inspect_ai/solver/_human_agent/panel.py +252 -0
- inspect_ai/solver/_human_agent/service.py +45 -0
- inspect_ai/solver/_human_agent/state.py +55 -0
- inspect_ai/solver/_human_agent/view.py +24 -0
- inspect_ai/solver/_task_state.py +28 -2
- inspect_ai/tool/_tool.py +10 -2
- inspect_ai/tool/_tool_info.py +2 -1
- inspect_ai/tool/_tools/_web_browser/_resources/dm_env_servicer.py +9 -9
- inspect_ai/tool/_tools/_web_browser/_web_browser.py +16 -13
- inspect_ai/util/__init__.py +12 -4
- inspect_ai/{_util/display.py → util/_display.py} +6 -0
- inspect_ai/util/_panel.py +31 -9
- inspect_ai/util/_sandbox/__init__.py +0 -3
- inspect_ai/util/_sandbox/context.py +5 -1
- inspect_ai/util/_sandbox/docker/compose.py +17 -13
- inspect_ai/util/_sandbox/docker/docker.py +9 -6
- inspect_ai/util/_sandbox/docker/internal.py +1 -1
- inspect_ai/util/_sandbox/docker/util.py +3 -2
- inspect_ai/util/_sandbox/environment.py +6 -5
- inspect_ai/util/_sandbox/local.py +1 -1
- inspect_ai/util/_sandbox/self_check.py +18 -18
- inspect_ai/util/_sandbox/service.py +22 -7
- inspect_ai/util/_store.py +7 -8
- inspect_ai/util/_store_model.py +110 -0
- inspect_ai/util/_subprocess.py +3 -3
- inspect_ai/util/_throttle.py +32 -0
- {inspect_ai-0.3.55.dist-info → inspect_ai-0.3.57.dist-info}/METADATA +3 -3
- {inspect_ai-0.3.55.dist-info → inspect_ai-0.3.57.dist-info}/RECORD +131 -108
- {inspect_ai-0.3.55.dist-info → inspect_ai-0.3.57.dist-info}/WHEEL +1 -1
- {inspect_ai-0.3.55.dist-info → inspect_ai-0.3.57.dist-info}/LICENSE +0 -0
- {inspect_ai-0.3.55.dist-info → inspect_ai-0.3.57.dist-info}/entry_points.txt +0 -0
- {inspect_ai-0.3.55.dist-info → inspect_ai-0.3.57.dist-info}/top_level.txt +0 -0
inspect_ai/_view/www/src/App.mjs
CHANGED
@@ -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 {
|
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
|
-
|
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
|
342
|
-
return
|
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
|
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 =
|
523
|
+
const showSamples = hasSamples;
|
516
524
|
setSelectedWorkspaceTab(
|
517
|
-
|
525
|
+
log.status !== "error" && hasSamples
|
526
|
+
? kEvalWorkspaceTabId
|
527
|
+
: kInfoWorkspaceTabId,
|
518
528
|
);
|
519
529
|
|
520
530
|
// Select the default scorer to use
|
@@ -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
|
-
*
|
38
|
-
*
|
39
|
-
*
|
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
|
+
};
|
@@ -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
|
+
};
|