@midscene/visualizer 1.7.6 → 1.7.7-beta-20260428102047.0

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.
@@ -7,6 +7,7 @@ import { Dropdown, Spin, Switch, Tooltip, message } from "antd";
7
7
  import global_perspective from "../../icons/global-perspective.mjs";
8
8
  import player_setting from "../../icons/player-setting.mjs";
9
9
  import { useGlobalPreference } from "../../store/store.mjs";
10
+ import { triggerReportDownload } from "./report-download.mjs";
10
11
  import { StepsTimeline } from "./scenes/StepScene.mjs";
11
12
  import { exportBrandedVideo } from "./scenes/export-branded-video.mjs";
12
13
  import { calculateFrameMap } from "./scenes/frame-calculator.mjs";
@@ -38,22 +39,6 @@ function _async_to_generator(fn) {
38
39
  });
39
40
  };
40
41
  }
41
- const downloadReport = (content)=>{
42
- const blob = new Blob([
43
- content
44
- ], {
45
- type: 'text/html'
46
- });
47
- const url = URL.createObjectURL(blob);
48
- const a = document.createElement('a');
49
- a.href = url;
50
- a.download = 'midscene_report.html';
51
- a.style.display = 'none';
52
- document.body.appendChild(a);
53
- a.click();
54
- document.body.removeChild(a);
55
- setTimeout(()=>URL.revokeObjectURL(url), 0);
56
- };
57
42
  function deriveTaskId(scriptFrames, stepsFrame) {
58
43
  let taskId = null;
59
44
  for (const sf of scriptFrames){
@@ -168,6 +153,21 @@ function Player(props) {
168
153
  frameMap,
169
154
  player.currentFrame
170
155
  ]);
156
+ const handleDownloadReport = useCallback(()=>_async_to_generator(function*() {
157
+ if (!(null == props ? void 0 : props.reportFileContent)) return;
158
+ try {
159
+ yield triggerReportDownload({
160
+ content: props.reportFileContent,
161
+ onDownloadReport: props.onDownloadReport
162
+ });
163
+ } catch (error) {
164
+ const errorMessage = error instanceof Error ? error.message : String(error);
165
+ message.error(`Failed to download report: ${errorMessage}`);
166
+ }
167
+ })(), [
168
+ null == props ? void 0 : props.onDownloadReport,
169
+ null == props ? void 0 : props.reportFileContent
170
+ ]);
171
171
  const subtitle = useMemo(()=>{
172
172
  if (!currentFrameState) return null;
173
173
  if (!currentFrameState.title && !currentFrameState.subTitle) return null;
@@ -249,7 +249,9 @@ function Player(props) {
249
249
  setIsExporting(true);
250
250
  setExportProgress(0);
251
251
  try {
252
- yield exportBrandedVideo(frameMap, (pct)=>setExportProgress(Math.round(100 * pct)));
252
+ yield exportBrandedVideo(frameMap, {
253
+ autoZoom
254
+ }, (pct)=>setExportProgress(Math.round(100 * pct)));
253
255
  message.success('Video exported');
254
256
  } catch (e) {
255
257
  console.error('Export failed:', e);
@@ -259,6 +261,7 @@ function Player(props) {
259
261
  setExportProgress(0);
260
262
  }
261
263
  })(), [
264
+ autoZoom,
262
265
  frameMap,
263
266
  isExporting
264
267
  ]);
@@ -423,7 +426,9 @@ function Player(props) {
423
426
  title: "Download Report",
424
427
  children: /*#__PURE__*/ jsx("div", {
425
428
  className: "status-icon",
426
- onClick: ()=>downloadReport(props.reportFileContent),
429
+ onClick: ()=>{
430
+ handleDownloadReport();
431
+ },
427
432
  children: /*#__PURE__*/ jsx(DownloadOutlined, {})
428
433
  })
429
434
  }) : null,
@@ -0,0 +1,61 @@
1
+ function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
2
+ try {
3
+ var info = gen[key](arg);
4
+ var value = info.value;
5
+ } catch (error) {
6
+ reject(error);
7
+ return;
8
+ }
9
+ if (info.done) resolve(value);
10
+ else Promise.resolve(value).then(_next, _throw);
11
+ }
12
+ function _async_to_generator(fn) {
13
+ return function() {
14
+ var self = this, args = arguments;
15
+ return new Promise(function(resolve, reject) {
16
+ var gen = fn.apply(self, args);
17
+ function _next(value) {
18
+ asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
19
+ }
20
+ function _throw(err) {
21
+ asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
22
+ }
23
+ _next(void 0);
24
+ });
25
+ };
26
+ }
27
+ const DEFAULT_REPORT_FILE_NAME = 'midscene_report.html';
28
+ function triggerReportDownload(options) {
29
+ return _async_to_generator(function*() {
30
+ const { content, defaultFileName = DEFAULT_REPORT_FILE_NAME, onDownloadReport, documentRef, urlRef, blobFactory, scheduleRevoke } = options;
31
+ if (onDownloadReport) return void (yield onDownloadReport({
32
+ content,
33
+ defaultFileName
34
+ }));
35
+ const activeDocument = null != documentRef ? documentRef : globalThis.document;
36
+ if (!activeDocument) throw new Error('Report download requires a document context.');
37
+ const activeUrl = null != urlRef ? urlRef : globalThis.URL;
38
+ if (!(null == activeUrl ? void 0 : activeUrl.createObjectURL) || !(null == activeUrl ? void 0 : activeUrl.revokeObjectURL)) throw new Error('Report download requires URL.createObjectURL support.');
39
+ const createBlob = null != blobFactory ? blobFactory : (parts, blobOptions)=>new Blob(parts, blobOptions);
40
+ const blob = createBlob([
41
+ content
42
+ ], {
43
+ type: 'text/html'
44
+ });
45
+ const url = activeUrl.createObjectURL(blob);
46
+ const anchor = activeDocument.createElement('a');
47
+ anchor.href = url;
48
+ anchor.download = defaultFileName;
49
+ anchor.style.display = 'none';
50
+ activeDocument.body.appendChild(anchor);
51
+ try {
52
+ anchor.click();
53
+ } finally{
54
+ activeDocument.body.removeChild(anchor);
55
+ (null != scheduleRevoke ? scheduleRevoke : (callback)=>setTimeout(callback, 0))(()=>{
56
+ activeUrl.revokeObjectURL(url);
57
+ });
58
+ }
59
+ })();
60
+ }
61
+ export { DEFAULT_REPORT_FILE_NAME, triggerReportDownload };
@@ -38,6 +38,18 @@ function clamp(v, lo, hi) {
38
38
  function lerp(a, b, t) {
39
39
  return a + (b - a) * t;
40
40
  }
41
+ function resolveExportCamera(prevCamera, camera, imageWidth, progress, autoZoom) {
42
+ if (!autoZoom) return {
43
+ camLeft: 0,
44
+ camTop: 0,
45
+ camWidth: imageWidth
46
+ };
47
+ return {
48
+ camLeft: lerp(prevCamera.left, camera.left, progress),
49
+ camTop: lerp(prevCamera.top, camera.top, progress),
50
+ camWidth: lerp(prevCamera.width, camera.width, progress)
51
+ };
52
+ }
41
53
  function loadImage(src) {
42
54
  return new Promise((resolve, reject)=>{
43
55
  const img = new Image();
@@ -95,16 +107,14 @@ function drawSpinningPointer(ctx, img, x, y, elapsedMs) {
95
107
  ctx.drawImage(img, -11, -14, 22, 28);
96
108
  ctx.restore();
97
109
  }
98
- function drawSteps(ctx, stepsFrame, frameMap, imgCache, cursorImg, spinnerImg) {
110
+ function drawSteps(ctx, stepsFrame, frameMap, imgCache, cursorImg, spinnerImg, autoZoom) {
99
111
  const { scriptFrames, imageWidth: baseW, imageHeight: baseH, fps } = frameMap;
100
112
  const st = deriveFrameState(scriptFrames, stepsFrame, baseW, baseH, fps);
101
113
  if (!st.img) return;
102
114
  const { img, prevImg, imageWidth: imgW, imageHeight: imgH, camera, prevCamera, pointerMoved, imageChanged, rawProgress, frameInScript: fInScript, spinning, spinningElapsedMs, insights } = st;
103
115
  const pT = pointerMoved ? Math.min(rawProgress / POINTER_PHASE, 1) : rawProgress;
104
116
  const cT = pointerMoved ? rawProgress <= POINTER_PHASE ? 0 : Math.min((rawProgress - POINTER_PHASE) / (1 - POINTER_PHASE), 1) : rawProgress;
105
- const camL = lerp(prevCamera.left, camera.left, cT);
106
- const camT2 = lerp(prevCamera.top, camera.top, cT);
107
- const camW = lerp(prevCamera.width, camera.width, cT);
117
+ const { camLeft: camL, camTop: camT2, camWidth: camW } = resolveExportCamera(prevCamera, camera, imgW, cT, autoZoom);
108
118
  const ptrX = lerp(prevCamera.pointerLeft, camera.pointerLeft, pT);
109
119
  const ptrY = lerp(prevCamera.pointerTop, camera.pointerTop, pT);
110
120
  const zoom = imgW / camW;
@@ -141,9 +151,11 @@ function drawSteps(ctx, stepsFrame, frameMap, imgCache, cursorImg, spinnerImg) {
141
151
  if (spinning && spinnerImg) drawSpinningPointer(ctx, spinnerImg, sX, sY, spinningElapsedMs);
142
152
  if (!spinning && hasPtrData && cursorImg) ctx.drawImage(cursorImg, sX - 3, sY - 2, 22, 28);
143
153
  }
144
- function exportBrandedVideo(frameMap, onProgress) {
154
+ function exportBrandedVideo(frameMap, options, onProgress) {
145
155
  return _async_to_generator(function*() {
146
156
  const { totalDurationInFrames: total, fps } = frameMap;
157
+ var _options_autoZoom;
158
+ const autoZoom = null != (_options_autoZoom = null == options ? void 0 : options.autoZoom) ? _options_autoZoom : true;
147
159
  const imgSrcs = new Set();
148
160
  for (const sf of frameMap.scriptFrames)if (sf.img) imgSrcs.add(sf.img);
149
161
  const imgCache = new Map();
@@ -200,7 +212,7 @@ function exportBrandedVideo(frameMap, onProgress) {
200
212
  if (targetFrame > lastFrame) {
201
213
  lastFrame = targetFrame;
202
214
  ctx.clearRect(0, 0, W, H);
203
- drawSteps(ctx, targetFrame, frameMap, imgCache, cursorImg, spinnerImg);
215
+ drawSteps(ctx, targetFrame, frameMap, imgCache, cursorImg, spinnerImg, autoZoom);
204
216
  null == onProgress || onProgress((targetFrame + 1) / total);
205
217
  }
206
218
  if (targetFrame < total - 1) requestAnimationFrame(tick);
@@ -210,4 +222,4 @@ function exportBrandedVideo(frameMap, onProgress) {
210
222
  });
211
223
  })();
212
224
  }
213
- export { exportBrandedVideo };
225
+ export { exportBrandedVideo, resolveExportCamera };
@@ -6,7 +6,7 @@ import { emptyResultTip, serverLaunchTip } from "../misc/index.mjs";
6
6
  import { Player } from "../player/index.mjs";
7
7
  import shiny_text from "../shiny-text/index.mjs";
8
8
  import "./index.css";
9
- const PlaygroundResultView = ({ result, loading, serverValid, serviceMode, replayScriptsInfo, replayCounter, loadingProgressText, verticalMode = false, notReadyMessage, fitMode, autoZoom, actionType, canDownloadReport })=>{
9
+ const PlaygroundResultView = ({ result, loading, serverValid, serviceMode, replayScriptsInfo, replayCounter, loadingProgressText, verticalMode = false, notReadyMessage, fitMode, autoZoom, actionType, canDownloadReport, onDownloadReport })=>{
10
10
  let resultWrapperClassName = 'result-wrapper';
11
11
  if (verticalMode) resultWrapperClassName += ' vertical-mode-result';
12
12
  if (replayScriptsInfo && verticalMode) resultWrapperClassName += ' result-wrapper-compact';
@@ -78,7 +78,8 @@ const PlaygroundResultView = ({ result, loading, serverValid, serviceMode, repla
78
78
  reportFileContent: result.reportHTML || null,
79
79
  fitMode: fitMode,
80
80
  autoZoom: autoZoom,
81
- canDownloadReport: null != canDownloadReport ? canDownloadReport : 'In-Browser' !== serviceMode
81
+ canDownloadReport: null != canDownloadReport ? canDownloadReport : 'In-Browser' !== serviceMode,
82
+ onDownloadReport: onDownloadReport
82
83
  }, replayCounter)
83
84
  })
84
85
  ]
@@ -129,7 +130,8 @@ const PlaygroundResultView = ({ result, loading, serverValid, serviceMode, repla
129
130
  reportFileContent: reportContent,
130
131
  fitMode: fitMode,
131
132
  autoZoom: autoZoom,
132
- canDownloadReport: null != canDownloadReport ? canDownloadReport : 'In-Browser' !== serviceMode
133
+ canDownloadReport: null != canDownloadReport ? canDownloadReport : 'In-Browser' !== serviceMode,
134
+ onDownloadReport: onDownloadReport
133
135
  }, replayCounter)
134
136
  })
135
137
  ]
@@ -145,7 +147,8 @@ const PlaygroundResultView = ({ result, loading, serverValid, serviceMode, repla
145
147
  reportFileContent: reportContent,
146
148
  fitMode: fitMode,
147
149
  autoZoom: autoZoom,
148
- canDownloadReport: null != canDownloadReport ? canDownloadReport : 'In-Browser' !== serviceMode
150
+ canDownloadReport: null != canDownloadReport ? canDownloadReport : 'In-Browser' !== serviceMode,
151
+ onDownloadReport: onDownloadReport
149
152
  }, replayCounter);
150
153
  } else if (shouldPrioritizeResult && (null == result ? void 0 : result.result) !== void 0 && (null == result ? void 0 : result.reportHTML)) {
151
154
  const resultOutput = 'string' == typeof (null == result ? void 0 : result.result) ? /*#__PURE__*/ jsx("pre", {
@@ -187,7 +190,8 @@ const PlaygroundResultView = ({ result, loading, serverValid, serviceMode, repla
187
190
  reportFileContent: result.reportHTML,
188
191
  fitMode: fitMode,
189
192
  autoZoom: autoZoom,
190
- canDownloadReport: null != canDownloadReport ? canDownloadReport : 'In-Browser' !== serviceMode
193
+ canDownloadReport: null != canDownloadReport ? canDownloadReport : 'In-Browser' !== serviceMode,
194
+ onDownloadReport: onDownloadReport
191
195
  }, replayCounter)
192
196
  })
193
197
  ]
@@ -203,7 +207,8 @@ const PlaygroundResultView = ({ result, loading, serverValid, serviceMode, repla
203
207
  reportFileContent: result.reportHTML,
204
208
  fitMode: fitMode,
205
209
  autoZoom: autoZoom,
206
- canDownloadReport: null != canDownloadReport ? canDownloadReport : 'In-Browser' !== serviceMode
210
+ canDownloadReport: null != canDownloadReport ? canDownloadReport : 'In-Browser' !== serviceMode,
211
+ onDownloadReport: onDownloadReport
207
212
  }, replayCounter);
208
213
  else if ((null == result ? void 0 : result.result) !== void 0) resultDataToShow = 'string' == typeof (null == result ? void 0 : result.result) ? /*#__PURE__*/ jsx("pre", {
209
214
  children: null == result ? void 0 : result.result
@@ -4,6 +4,23 @@
4
4
  display: flex;
5
5
  }
6
6
 
7
+ .screenshot-viewer.screen-only {
8
+ min-height: 0;
9
+ }
10
+
11
+ .screenshot-viewer.screen-only > .screenshot-content {
12
+ flex: 1;
13
+ min-height: 0;
14
+ }
15
+
16
+ .screenshot-viewer.screen-only > .screenshot-content .screenshot-image {
17
+ border-radius: 0;
18
+ width: 100%;
19
+ max-width: none;
20
+ height: 100%;
21
+ max-height: none;
22
+ }
23
+
7
24
  .screenshot-viewer.offline, .screenshot-viewer.loading, .screenshot-viewer.error {
8
25
  text-align: center;
9
26
  color: #666;
@@ -29,13 +29,18 @@ function _async_to_generator(fn) {
29
29
  });
30
30
  };
31
31
  }
32
- function ScreenshotViewer({ getScreenshot, getInterfaceInfo, serverOnline, isUserOperating = false, mjpegUrl }) {
32
+ function ScreenshotViewer({ getScreenshot, getInterfaceInfo, serverOnline, isUserOperating = false, mjpegUrl, mode = 'default' }) {
33
33
  const [screenshot, setScreenshot] = useState(null);
34
34
  const [loading, setLoading] = useState(false);
35
35
  const [error, setError] = useState(null);
36
36
  const [lastUpdateTime, setLastUpdateTime] = useState(0);
37
37
  const [interfaceInfo, setInterfaceInfo] = useState(null);
38
38
  const isMjpeg = Boolean(mjpegUrl && serverOnline);
39
+ const showChrome = 'screen-only' !== mode;
40
+ const rootClassName = [
41
+ 'screenshot-viewer',
42
+ 'screen-only' === mode && 'screen-only'
43
+ ].filter(Boolean).join(' ');
39
44
  const pollingIntervalRef = useRef(null);
40
45
  const isPollingPausedRef = useRef(false);
41
46
  const fetchScreenshot = useCallback((isManual = false)=>_async_to_generator(function*() {
@@ -148,7 +153,7 @@ function ScreenshotViewer({ getScreenshot, getInterfaceInfo, serverOnline, isUse
148
153
  stopPolling
149
154
  ]);
150
155
  if (!serverOnline) return /*#__PURE__*/ jsx("div", {
151
- className: "screenshot-viewer offline",
156
+ className: `${rootClassName} offline`,
152
157
  children: /*#__PURE__*/ jsxs("div", {
153
158
  className: "screenshot-placeholder",
154
159
  children: [
@@ -162,7 +167,7 @@ function ScreenshotViewer({ getScreenshot, getInterfaceInfo, serverOnline, isUse
162
167
  })
163
168
  });
164
169
  if (!isMjpeg && loading && !screenshot) return /*#__PURE__*/ jsxs("div", {
165
- className: "screenshot-viewer loading",
170
+ className: `${rootClassName} loading`,
166
171
  children: [
167
172
  /*#__PURE__*/ jsx(Spin, {
168
173
  size: "large"
@@ -173,7 +178,7 @@ function ScreenshotViewer({ getScreenshot, getInterfaceInfo, serverOnline, isUse
173
178
  ]
174
179
  });
175
180
  if (!isMjpeg && error && !screenshot) return /*#__PURE__*/ jsx("div", {
176
- className: "screenshot-viewer error",
181
+ className: `${rootClassName} error`,
177
182
  children: /*#__PURE__*/ jsxs("div", {
178
183
  className: "screenshot-placeholder",
179
184
  children: [
@@ -195,8 +200,35 @@ function ScreenshotViewer({ getScreenshot, getInterfaceInfo, serverOnline, isUse
195
200
  if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
196
201
  return new Date(timestamp).toLocaleTimeString();
197
202
  };
203
+ const screenshotContent = /*#__PURE__*/ jsx("div", {
204
+ className: "screenshot-content",
205
+ children: isMjpeg ? /*#__PURE__*/ jsx("img", {
206
+ src: mjpegUrl,
207
+ alt: "Device Live Stream",
208
+ className: "screenshot-image"
209
+ }) : screenshot ? /*#__PURE__*/ jsx("img", {
210
+ src: screenshot.startsWith('data:image/') ? screenshot : `data:image/png;base64,${screenshot}`,
211
+ alt: "Device Screenshot",
212
+ className: "screenshot-image",
213
+ onLoad: ()=>console.log('Screenshot image loaded successfully'),
214
+ onError: (e)=>{
215
+ console.error('Screenshot image load error:', e);
216
+ console.error('Screenshot data preview:', screenshot.substring(0, 100));
217
+ setError('Failed to load screenshot image');
218
+ }
219
+ }) : /*#__PURE__*/ jsx("div", {
220
+ className: "screenshot-placeholder",
221
+ children: /*#__PURE__*/ jsx("p", {
222
+ children: "No screenshot available"
223
+ })
224
+ })
225
+ });
226
+ if (!showChrome) return /*#__PURE__*/ jsx("div", {
227
+ className: rootClassName,
228
+ children: screenshotContent
229
+ });
198
230
  return /*#__PURE__*/ jsxs("div", {
199
- className: "screenshot-viewer",
231
+ className: rootClassName,
200
232
  children: [
201
233
  /*#__PURE__*/ jsx("div", {
202
234
  className: "screenshot-header",
@@ -258,29 +290,7 @@ function ScreenshotViewer({ getScreenshot, getInterfaceInfo, serverOnline, isUse
258
290
  })
259
291
  ]
260
292
  }),
261
- /*#__PURE__*/ jsx("div", {
262
- className: "screenshot-content",
263
- children: isMjpeg ? /*#__PURE__*/ jsx("img", {
264
- src: mjpegUrl,
265
- alt: "Device Live Stream",
266
- className: "screenshot-image"
267
- }) : screenshot ? /*#__PURE__*/ jsx("img", {
268
- src: screenshot.startsWith('data:image/') ? screenshot : `data:image/png;base64,${screenshot}`,
269
- alt: "Device Screenshot",
270
- className: "screenshot-image",
271
- onLoad: ()=>console.log('Screenshot image loaded successfully'),
272
- onError: (e)=>{
273
- console.error('Screenshot image load error:', e);
274
- console.error('Screenshot data preview:', screenshot.substring(0, 100));
275
- setError('Failed to load screenshot image');
276
- }
277
- }) : /*#__PURE__*/ jsx("div", {
278
- className: "screenshot-placeholder",
279
- children: /*#__PURE__*/ jsx("p", {
280
- children: "No screenshot available"
281
- })
282
- })
283
- })
293
+ screenshotContent
284
294
  ]
285
295
  })
286
296
  ]
@@ -335,7 +335,8 @@ function UniversalPlayground({ playgroundSDK, storage, contextProvider, config:
335
335
  loadingProgressText: item.loadingProgressText || '',
336
336
  verticalMode: item.verticalMode || false,
337
337
  fitMode: "width",
338
- actionType: item.actionType
338
+ actionType: item.actionType,
339
+ onDownloadReport: componentConfig.onDownloadReport
339
340
  }) : /*#__PURE__*/ jsxs(Fragment, {
340
341
  children: [
341
342
  /*#__PURE__*/ jsx("div", {
@@ -46,6 +46,7 @@ var global_perspective_js_default = /*#__PURE__*/ __webpack_require__.n(global_p
46
46
  const player_setting_js_namespaceObject = require("../../icons/player-setting.js");
47
47
  var player_setting_js_default = /*#__PURE__*/ __webpack_require__.n(player_setting_js_namespaceObject);
48
48
  const store_js_namespaceObject = require("../../store/store.js");
49
+ const external_report_download_js_namespaceObject = require("./report-download.js");
49
50
  const StepScene_js_namespaceObject = require("./scenes/StepScene.js");
50
51
  const export_branded_video_js_namespaceObject = require("./scenes/export-branded-video.js");
51
52
  const frame_calculator_js_namespaceObject = require("./scenes/frame-calculator.js");
@@ -77,22 +78,6 @@ function _async_to_generator(fn) {
77
78
  });
78
79
  };
79
80
  }
80
- const downloadReport = (content)=>{
81
- const blob = new Blob([
82
- content
83
- ], {
84
- type: 'text/html'
85
- });
86
- const url = URL.createObjectURL(blob);
87
- const a = document.createElement('a');
88
- a.href = url;
89
- a.download = 'midscene_report.html';
90
- a.style.display = 'none';
91
- document.body.appendChild(a);
92
- a.click();
93
- document.body.removeChild(a);
94
- setTimeout(()=>URL.revokeObjectURL(url), 0);
95
- };
96
81
  function deriveTaskId(scriptFrames, stepsFrame) {
97
82
  let taskId = null;
98
83
  for (const sf of scriptFrames){
@@ -207,6 +192,21 @@ function Player(props) {
207
192
  frameMap,
208
193
  player.currentFrame
209
194
  ]);
195
+ const handleDownloadReport = (0, external_react_namespaceObject.useCallback)(()=>_async_to_generator(function*() {
196
+ if (!(null == props ? void 0 : props.reportFileContent)) return;
197
+ try {
198
+ yield (0, external_report_download_js_namespaceObject.triggerReportDownload)({
199
+ content: props.reportFileContent,
200
+ onDownloadReport: props.onDownloadReport
201
+ });
202
+ } catch (error) {
203
+ const errorMessage = error instanceof Error ? error.message : String(error);
204
+ external_antd_namespaceObject.message.error(`Failed to download report: ${errorMessage}`);
205
+ }
206
+ })(), [
207
+ null == props ? void 0 : props.onDownloadReport,
208
+ null == props ? void 0 : props.reportFileContent
209
+ ]);
210
210
  const subtitle = (0, external_react_namespaceObject.useMemo)(()=>{
211
211
  if (!currentFrameState) return null;
212
212
  if (!currentFrameState.title && !currentFrameState.subTitle) return null;
@@ -288,7 +288,9 @@ function Player(props) {
288
288
  setIsExporting(true);
289
289
  setExportProgress(0);
290
290
  try {
291
- yield (0, export_branded_video_js_namespaceObject.exportBrandedVideo)(frameMap, (pct)=>setExportProgress(Math.round(100 * pct)));
291
+ yield (0, export_branded_video_js_namespaceObject.exportBrandedVideo)(frameMap, {
292
+ autoZoom
293
+ }, (pct)=>setExportProgress(Math.round(100 * pct)));
292
294
  external_antd_namespaceObject.message.success('Video exported');
293
295
  } catch (e) {
294
296
  console.error('Export failed:', e);
@@ -298,6 +300,7 @@ function Player(props) {
298
300
  setExportProgress(0);
299
301
  }
300
302
  })(), [
303
+ autoZoom,
301
304
  frameMap,
302
305
  isExporting
303
306
  ]);
@@ -462,7 +465,9 @@ function Player(props) {
462
465
  title: "Download Report",
463
466
  children: /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)("div", {
464
467
  className: "status-icon",
465
- onClick: ()=>downloadReport(props.reportFileContent),
468
+ onClick: ()=>{
469
+ handleDownloadReport();
470
+ },
466
471
  children: /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)(icons_namespaceObject.DownloadOutlined, {})
467
472
  })
468
473
  }) : null,
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ var __webpack_require__ = {};
3
+ (()=>{
4
+ __webpack_require__.d = (exports1, definition)=>{
5
+ for(var key in definition)if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
6
+ enumerable: true,
7
+ get: definition[key]
8
+ });
9
+ };
10
+ })();
11
+ (()=>{
12
+ __webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop);
13
+ })();
14
+ (()=>{
15
+ __webpack_require__.r = (exports1)=>{
16
+ if ('undefined' != typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
17
+ value: 'Module'
18
+ });
19
+ Object.defineProperty(exports1, '__esModule', {
20
+ value: true
21
+ });
22
+ };
23
+ })();
24
+ var __webpack_exports__ = {};
25
+ __webpack_require__.r(__webpack_exports__);
26
+ __webpack_require__.d(__webpack_exports__, {
27
+ DEFAULT_REPORT_FILE_NAME: ()=>DEFAULT_REPORT_FILE_NAME,
28
+ triggerReportDownload: ()=>triggerReportDownload
29
+ });
30
+ function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
31
+ try {
32
+ var info = gen[key](arg);
33
+ var value = info.value;
34
+ } catch (error) {
35
+ reject(error);
36
+ return;
37
+ }
38
+ if (info.done) resolve(value);
39
+ else Promise.resolve(value).then(_next, _throw);
40
+ }
41
+ function _async_to_generator(fn) {
42
+ return function() {
43
+ var self = this, args = arguments;
44
+ return new Promise(function(resolve, reject) {
45
+ var gen = fn.apply(self, args);
46
+ function _next(value) {
47
+ asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
48
+ }
49
+ function _throw(err) {
50
+ asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
51
+ }
52
+ _next(void 0);
53
+ });
54
+ };
55
+ }
56
+ const DEFAULT_REPORT_FILE_NAME = 'midscene_report.html';
57
+ function triggerReportDownload(options) {
58
+ return _async_to_generator(function*() {
59
+ const { content, defaultFileName = DEFAULT_REPORT_FILE_NAME, onDownloadReport, documentRef, urlRef, blobFactory, scheduleRevoke } = options;
60
+ if (onDownloadReport) return void (yield onDownloadReport({
61
+ content,
62
+ defaultFileName
63
+ }));
64
+ const activeDocument = null != documentRef ? documentRef : globalThis.document;
65
+ if (!activeDocument) throw new Error('Report download requires a document context.');
66
+ const activeUrl = null != urlRef ? urlRef : globalThis.URL;
67
+ if (!(null == activeUrl ? void 0 : activeUrl.createObjectURL) || !(null == activeUrl ? void 0 : activeUrl.revokeObjectURL)) throw new Error('Report download requires URL.createObjectURL support.');
68
+ const createBlob = null != blobFactory ? blobFactory : (parts, blobOptions)=>new Blob(parts, blobOptions);
69
+ const blob = createBlob([
70
+ content
71
+ ], {
72
+ type: 'text/html'
73
+ });
74
+ const url = activeUrl.createObjectURL(blob);
75
+ const anchor = activeDocument.createElement('a');
76
+ anchor.href = url;
77
+ anchor.download = defaultFileName;
78
+ anchor.style.display = 'none';
79
+ activeDocument.body.appendChild(anchor);
80
+ try {
81
+ anchor.click();
82
+ } finally{
83
+ activeDocument.body.removeChild(anchor);
84
+ (null != scheduleRevoke ? scheduleRevoke : (callback)=>setTimeout(callback, 0))(()=>{
85
+ activeUrl.revokeObjectURL(url);
86
+ });
87
+ }
88
+ })();
89
+ }
90
+ exports.DEFAULT_REPORT_FILE_NAME = __webpack_exports__.DEFAULT_REPORT_FILE_NAME;
91
+ exports.triggerReportDownload = __webpack_exports__.triggerReportDownload;
92
+ for(var __rspack_i in __webpack_exports__)if (-1 === [
93
+ "DEFAULT_REPORT_FILE_NAME",
94
+ "triggerReportDownload"
95
+ ].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
96
+ Object.defineProperty(exports, '__esModule', {
97
+ value: true
98
+ });
@@ -24,6 +24,7 @@ var __webpack_require__ = {};
24
24
  var __webpack_exports__ = {};
25
25
  __webpack_require__.r(__webpack_exports__);
26
26
  __webpack_require__.d(__webpack_exports__, {
27
+ resolveExportCamera: ()=>resolveExportCamera,
27
28
  exportBrandedVideo: ()=>exportBrandedVideo
28
29
  });
29
30
  const index_js_namespaceObject = require("../../../utils/index.js");
@@ -66,6 +67,18 @@ function clamp(v, lo, hi) {
66
67
  function lerp(a, b, t) {
67
68
  return a + (b - a) * t;
68
69
  }
70
+ function resolveExportCamera(prevCamera, camera, imageWidth, progress, autoZoom) {
71
+ if (!autoZoom) return {
72
+ camLeft: 0,
73
+ camTop: 0,
74
+ camWidth: imageWidth
75
+ };
76
+ return {
77
+ camLeft: lerp(prevCamera.left, camera.left, progress),
78
+ camTop: lerp(prevCamera.top, camera.top, progress),
79
+ camWidth: lerp(prevCamera.width, camera.width, progress)
80
+ };
81
+ }
69
82
  function loadImage(src) {
70
83
  return new Promise((resolve, reject)=>{
71
84
  const img = new Image();
@@ -123,16 +136,14 @@ function drawSpinningPointer(ctx, img, x, y, elapsedMs) {
123
136
  ctx.drawImage(img, -11, -14, 22, 28);
124
137
  ctx.restore();
125
138
  }
126
- function drawSteps(ctx, stepsFrame, frameMap, imgCache, cursorImg, spinnerImg) {
139
+ function drawSteps(ctx, stepsFrame, frameMap, imgCache, cursorImg, spinnerImg, autoZoom) {
127
140
  const { scriptFrames, imageWidth: baseW, imageHeight: baseH, fps } = frameMap;
128
141
  const st = (0, external_derive_frame_state_js_namespaceObject.deriveFrameState)(scriptFrames, stepsFrame, baseW, baseH, fps);
129
142
  if (!st.img) return;
130
143
  const { img, prevImg, imageWidth: imgW, imageHeight: imgH, camera, prevCamera, pointerMoved, imageChanged, rawProgress, frameInScript: fInScript, spinning, spinningElapsedMs, insights } = st;
131
144
  const pT = pointerMoved ? Math.min(rawProgress / POINTER_PHASE, 1) : rawProgress;
132
145
  const cT = pointerMoved ? rawProgress <= POINTER_PHASE ? 0 : Math.min((rawProgress - POINTER_PHASE) / (1 - POINTER_PHASE), 1) : rawProgress;
133
- const camL = lerp(prevCamera.left, camera.left, cT);
134
- const camT2 = lerp(prevCamera.top, camera.top, cT);
135
- const camW = lerp(prevCamera.width, camera.width, cT);
146
+ const { camLeft: camL, camTop: camT2, camWidth: camW } = resolveExportCamera(prevCamera, camera, imgW, cT, autoZoom);
136
147
  const ptrX = lerp(prevCamera.pointerLeft, camera.pointerLeft, pT);
137
148
  const ptrY = lerp(prevCamera.pointerTop, camera.pointerTop, pT);
138
149
  const zoom = imgW / camW;
@@ -169,9 +180,11 @@ function drawSteps(ctx, stepsFrame, frameMap, imgCache, cursorImg, spinnerImg) {
169
180
  if (spinning && spinnerImg) drawSpinningPointer(ctx, spinnerImg, sX, sY, spinningElapsedMs);
170
181
  if (!spinning && hasPtrData && cursorImg) ctx.drawImage(cursorImg, sX - 3, sY - 2, 22, 28);
171
182
  }
172
- function exportBrandedVideo(frameMap, onProgress) {
183
+ function exportBrandedVideo(frameMap, options, onProgress) {
173
184
  return _async_to_generator(function*() {
174
185
  const { totalDurationInFrames: total, fps } = frameMap;
186
+ var _options_autoZoom;
187
+ const autoZoom = null != (_options_autoZoom = null == options ? void 0 : options.autoZoom) ? _options_autoZoom : true;
175
188
  const imgSrcs = new Set();
176
189
  for (const sf of frameMap.scriptFrames)if (sf.img) imgSrcs.add(sf.img);
177
190
  const imgCache = new Map();
@@ -228,7 +241,7 @@ function exportBrandedVideo(frameMap, onProgress) {
228
241
  if (targetFrame > lastFrame) {
229
242
  lastFrame = targetFrame;
230
243
  ctx.clearRect(0, 0, W, H);
231
- drawSteps(ctx, targetFrame, frameMap, imgCache, cursorImg, spinnerImg);
244
+ drawSteps(ctx, targetFrame, frameMap, imgCache, cursorImg, spinnerImg, autoZoom);
232
245
  null == onProgress || onProgress((targetFrame + 1) / total);
233
246
  }
234
247
  if (targetFrame < total - 1) requestAnimationFrame(tick);
@@ -239,8 +252,10 @@ function exportBrandedVideo(frameMap, onProgress) {
239
252
  })();
240
253
  }
241
254
  exports.exportBrandedVideo = __webpack_exports__.exportBrandedVideo;
255
+ exports.resolveExportCamera = __webpack_exports__.resolveExportCamera;
242
256
  for(var __rspack_i in __webpack_exports__)if (-1 === [
243
- "exportBrandedVideo"
257
+ "exportBrandedVideo",
258
+ "resolveExportCamera"
244
259
  ].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
245
260
  Object.defineProperty(exports, '__esModule', {
246
261
  value: true
@@ -44,7 +44,7 @@ const external_player_index_js_namespaceObject = require("../player/index.js");
44
44
  const external_shiny_text_index_js_namespaceObject = require("../shiny-text/index.js");
45
45
  var external_shiny_text_index_js_default = /*#__PURE__*/ __webpack_require__.n(external_shiny_text_index_js_namespaceObject);
46
46
  require("./index.css");
47
- const PlaygroundResultView = ({ result, loading, serverValid, serviceMode, replayScriptsInfo, replayCounter, loadingProgressText, verticalMode = false, notReadyMessage, fitMode, autoZoom, actionType, canDownloadReport })=>{
47
+ const PlaygroundResultView = ({ result, loading, serverValid, serviceMode, replayScriptsInfo, replayCounter, loadingProgressText, verticalMode = false, notReadyMessage, fitMode, autoZoom, actionType, canDownloadReport, onDownloadReport })=>{
48
48
  let resultWrapperClassName = 'result-wrapper';
49
49
  if (verticalMode) resultWrapperClassName += ' vertical-mode-result';
50
50
  if (replayScriptsInfo && verticalMode) resultWrapperClassName += ' result-wrapper-compact';
@@ -116,7 +116,8 @@ const PlaygroundResultView = ({ result, loading, serverValid, serviceMode, repla
116
116
  reportFileContent: result.reportHTML || null,
117
117
  fitMode: fitMode,
118
118
  autoZoom: autoZoom,
119
- canDownloadReport: null != canDownloadReport ? canDownloadReport : 'In-Browser' !== serviceMode
119
+ canDownloadReport: null != canDownloadReport ? canDownloadReport : 'In-Browser' !== serviceMode,
120
+ onDownloadReport: onDownloadReport
120
121
  }, replayCounter)
121
122
  })
122
123
  ]
@@ -167,7 +168,8 @@ const PlaygroundResultView = ({ result, loading, serverValid, serviceMode, repla
167
168
  reportFileContent: reportContent,
168
169
  fitMode: fitMode,
169
170
  autoZoom: autoZoom,
170
- canDownloadReport: null != canDownloadReport ? canDownloadReport : 'In-Browser' !== serviceMode
171
+ canDownloadReport: null != canDownloadReport ? canDownloadReport : 'In-Browser' !== serviceMode,
172
+ onDownloadReport: onDownloadReport
171
173
  }, replayCounter)
172
174
  })
173
175
  ]
@@ -183,7 +185,8 @@ const PlaygroundResultView = ({ result, loading, serverValid, serviceMode, repla
183
185
  reportFileContent: reportContent,
184
186
  fitMode: fitMode,
185
187
  autoZoom: autoZoom,
186
- canDownloadReport: null != canDownloadReport ? canDownloadReport : 'In-Browser' !== serviceMode
188
+ canDownloadReport: null != canDownloadReport ? canDownloadReport : 'In-Browser' !== serviceMode,
189
+ onDownloadReport: onDownloadReport
187
190
  }, replayCounter);
188
191
  } else if (shouldPrioritizeResult && (null == result ? void 0 : result.result) !== void 0 && (null == result ? void 0 : result.reportHTML)) {
189
192
  const resultOutput = 'string' == typeof (null == result ? void 0 : result.result) ? /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)("pre", {
@@ -225,7 +228,8 @@ const PlaygroundResultView = ({ result, loading, serverValid, serviceMode, repla
225
228
  reportFileContent: result.reportHTML,
226
229
  fitMode: fitMode,
227
230
  autoZoom: autoZoom,
228
- canDownloadReport: null != canDownloadReport ? canDownloadReport : 'In-Browser' !== serviceMode
231
+ canDownloadReport: null != canDownloadReport ? canDownloadReport : 'In-Browser' !== serviceMode,
232
+ onDownloadReport: onDownloadReport
229
233
  }, replayCounter)
230
234
  })
231
235
  ]
@@ -241,7 +245,8 @@ const PlaygroundResultView = ({ result, loading, serverValid, serviceMode, repla
241
245
  reportFileContent: result.reportHTML,
242
246
  fitMode: fitMode,
243
247
  autoZoom: autoZoom,
244
- canDownloadReport: null != canDownloadReport ? canDownloadReport : 'In-Browser' !== serviceMode
248
+ canDownloadReport: null != canDownloadReport ? canDownloadReport : 'In-Browser' !== serviceMode,
249
+ onDownloadReport: onDownloadReport
245
250
  }, replayCounter);
246
251
  else if ((null == result ? void 0 : result.result) !== void 0) resultDataToShow = 'string' == typeof (null == result ? void 0 : result.result) ? /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)("pre", {
247
252
  children: null == result ? void 0 : result.result
@@ -4,6 +4,23 @@
4
4
  display: flex;
5
5
  }
6
6
 
7
+ .screenshot-viewer.screen-only {
8
+ min-height: 0;
9
+ }
10
+
11
+ .screenshot-viewer.screen-only > .screenshot-content {
12
+ flex: 1;
13
+ min-height: 0;
14
+ }
15
+
16
+ .screenshot-viewer.screen-only > .screenshot-content .screenshot-image {
17
+ border-radius: 0;
18
+ width: 100%;
19
+ max-width: none;
20
+ height: 100%;
21
+ max-height: none;
22
+ }
23
+
7
24
  .screenshot-viewer.offline, .screenshot-viewer.loading, .screenshot-viewer.error {
8
25
  text-align: center;
9
26
  color: #666;
@@ -57,13 +57,18 @@ function _async_to_generator(fn) {
57
57
  });
58
58
  };
59
59
  }
60
- function ScreenshotViewer({ getScreenshot, getInterfaceInfo, serverOnline, isUserOperating = false, mjpegUrl }) {
60
+ function ScreenshotViewer({ getScreenshot, getInterfaceInfo, serverOnline, isUserOperating = false, mjpegUrl, mode = 'default' }) {
61
61
  const [screenshot, setScreenshot] = (0, external_react_namespaceObject.useState)(null);
62
62
  const [loading, setLoading] = (0, external_react_namespaceObject.useState)(false);
63
63
  const [error, setError] = (0, external_react_namespaceObject.useState)(null);
64
64
  const [lastUpdateTime, setLastUpdateTime] = (0, external_react_namespaceObject.useState)(0);
65
65
  const [interfaceInfo, setInterfaceInfo] = (0, external_react_namespaceObject.useState)(null);
66
66
  const isMjpeg = Boolean(mjpegUrl && serverOnline);
67
+ const showChrome = 'screen-only' !== mode;
68
+ const rootClassName = [
69
+ 'screenshot-viewer',
70
+ 'screen-only' === mode && 'screen-only'
71
+ ].filter(Boolean).join(' ');
67
72
  const pollingIntervalRef = (0, external_react_namespaceObject.useRef)(null);
68
73
  const isPollingPausedRef = (0, external_react_namespaceObject.useRef)(false);
69
74
  const fetchScreenshot = (0, external_react_namespaceObject.useCallback)((isManual = false)=>_async_to_generator(function*() {
@@ -176,7 +181,7 @@ function ScreenshotViewer({ getScreenshot, getInterfaceInfo, serverOnline, isUse
176
181
  stopPolling
177
182
  ]);
178
183
  if (!serverOnline) return /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)("div", {
179
- className: "screenshot-viewer offline",
184
+ className: `${rootClassName} offline`,
180
185
  children: /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsxs)("div", {
181
186
  className: "screenshot-placeholder",
182
187
  children: [
@@ -190,7 +195,7 @@ function ScreenshotViewer({ getScreenshot, getInterfaceInfo, serverOnline, isUse
190
195
  })
191
196
  });
192
197
  if (!isMjpeg && loading && !screenshot) return /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsxs)("div", {
193
- className: "screenshot-viewer loading",
198
+ className: `${rootClassName} loading`,
194
199
  children: [
195
200
  /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)(external_antd_namespaceObject.Spin, {
196
201
  size: "large"
@@ -201,7 +206,7 @@ function ScreenshotViewer({ getScreenshot, getInterfaceInfo, serverOnline, isUse
201
206
  ]
202
207
  });
203
208
  if (!isMjpeg && error && !screenshot) return /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)("div", {
204
- className: "screenshot-viewer error",
209
+ className: `${rootClassName} error`,
205
210
  children: /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsxs)("div", {
206
211
  className: "screenshot-placeholder",
207
212
  children: [
@@ -223,8 +228,35 @@ function ScreenshotViewer({ getScreenshot, getInterfaceInfo, serverOnline, isUse
223
228
  if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
224
229
  return new Date(timestamp).toLocaleTimeString();
225
230
  };
231
+ const screenshotContent = /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)("div", {
232
+ className: "screenshot-content",
233
+ children: isMjpeg ? /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)("img", {
234
+ src: mjpegUrl,
235
+ alt: "Device Live Stream",
236
+ className: "screenshot-image"
237
+ }) : screenshot ? /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)("img", {
238
+ src: screenshot.startsWith('data:image/') ? screenshot : `data:image/png;base64,${screenshot}`,
239
+ alt: "Device Screenshot",
240
+ className: "screenshot-image",
241
+ onLoad: ()=>console.log('Screenshot image loaded successfully'),
242
+ onError: (e)=>{
243
+ console.error('Screenshot image load error:', e);
244
+ console.error('Screenshot data preview:', screenshot.substring(0, 100));
245
+ setError('Failed to load screenshot image');
246
+ }
247
+ }) : /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)("div", {
248
+ className: "screenshot-placeholder",
249
+ children: /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)("p", {
250
+ children: "No screenshot available"
251
+ })
252
+ })
253
+ });
254
+ if (!showChrome) return /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)("div", {
255
+ className: rootClassName,
256
+ children: screenshotContent
257
+ });
226
258
  return /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsxs)("div", {
227
- className: "screenshot-viewer",
259
+ className: rootClassName,
228
260
  children: [
229
261
  /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)("div", {
230
262
  className: "screenshot-header",
@@ -286,29 +318,7 @@ function ScreenshotViewer({ getScreenshot, getInterfaceInfo, serverOnline, isUse
286
318
  })
287
319
  ]
288
320
  }),
289
- /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)("div", {
290
- className: "screenshot-content",
291
- children: isMjpeg ? /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)("img", {
292
- src: mjpegUrl,
293
- alt: "Device Live Stream",
294
- className: "screenshot-image"
295
- }) : screenshot ? /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)("img", {
296
- src: screenshot.startsWith('data:image/') ? screenshot : `data:image/png;base64,${screenshot}`,
297
- alt: "Device Screenshot",
298
- className: "screenshot-image",
299
- onLoad: ()=>console.log('Screenshot image loaded successfully'),
300
- onError: (e)=>{
301
- console.error('Screenshot image load error:', e);
302
- console.error('Screenshot data preview:', screenshot.substring(0, 100));
303
- setError('Failed to load screenshot image');
304
- }
305
- }) : /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)("div", {
306
- className: "screenshot-placeholder",
307
- children: /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)("p", {
308
- children: "No screenshot available"
309
- })
310
- })
311
- })
321
+ screenshotContent
312
322
  ]
313
323
  })
314
324
  ]
@@ -376,7 +376,8 @@ function UniversalPlayground({ playgroundSDK, storage, contextProvider, config:
376
376
  loadingProgressText: item.loadingProgressText || '',
377
377
  verticalMode: item.verticalMode || false,
378
378
  fitMode: "width",
379
- actionType: item.actionType
379
+ actionType: item.actionType,
380
+ onDownloadReport: componentConfig.onDownloadReport
380
381
  }) : /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsxs)(jsx_runtime_namespaceObject.Fragment, {
381
382
  children: [
382
383
  /*#__PURE__*/ (0, jsx_runtime_namespaceObject.jsx)("div", {
@@ -1,4 +1,5 @@
1
1
  import './index.less';
2
+ import type { ReportDownloadHandler } from '../../types';
2
3
  import type { AnimationScript } from '../../utils/replay-scripts';
3
4
  export declare function Player(props?: {
4
5
  replayScripts?: AnimationScript[];
@@ -9,5 +10,6 @@ export declare function Player(props?: {
9
10
  fitMode?: 'width' | 'height';
10
11
  autoZoom?: boolean;
11
12
  canDownloadReport?: boolean;
13
+ onDownloadReport?: ReportDownloadHandler;
12
14
  onTaskChange?: (taskId: string | null) => void;
13
15
  }): import("react").JSX.Element;
@@ -0,0 +1,32 @@
1
+ import type { ReportDownloadHandler } from '../../types';
2
+ export declare const DEFAULT_REPORT_FILE_NAME = "midscene_report.html";
3
+ interface AnchorLike {
4
+ href: string;
5
+ download: string;
6
+ style: {
7
+ display: string;
8
+ };
9
+ click: () => void;
10
+ }
11
+ interface DocumentLike {
12
+ body: {
13
+ appendChild: (node: AnchorLike) => void;
14
+ removeChild: (node: AnchorLike) => void;
15
+ };
16
+ createElement: (tagName: string) => AnchorLike;
17
+ }
18
+ interface UrlLike {
19
+ createObjectURL: (blob: Blob) => string;
20
+ revokeObjectURL: (url: string) => void;
21
+ }
22
+ interface TriggerReportDownloadOptions {
23
+ content: string;
24
+ defaultFileName?: string;
25
+ onDownloadReport?: ReportDownloadHandler;
26
+ documentRef?: DocumentLike;
27
+ urlRef?: UrlLike;
28
+ blobFactory?: (parts: BlobPart[], options: BlobPropertyBag) => Blob;
29
+ scheduleRevoke?: (callback: () => void) => void;
30
+ }
31
+ export declare function triggerReportDownload(options: TriggerReportDownloadOptions): Promise<void>;
32
+ export {};
@@ -1,2 +1,17 @@
1
1
  import type { FrameMap } from './frame-calculator';
2
- export declare function exportBrandedVideo(frameMap: FrameMap, onProgress?: (pct: number) => void): Promise<void>;
2
+ export declare function resolveExportCamera(prevCamera: {
3
+ left: number;
4
+ top: number;
5
+ width: number;
6
+ }, camera: {
7
+ left: number;
8
+ top: number;
9
+ width: number;
10
+ }, imageWidth: number, progress: number, autoZoom: boolean): {
11
+ camLeft: number;
12
+ camTop: number;
13
+ camWidth: number;
14
+ };
15
+ export declare function exportBrandedVideo(frameMap: FrameMap, options?: {
16
+ autoZoom?: boolean;
17
+ }, onProgress?: (pct: number) => void): Promise<void>;
@@ -1,6 +1,5 @@
1
1
  import type React from 'react';
2
- import type { PlaygroundResult as PlaygroundResultType } from '../../types';
3
- import type { ServiceModeType } from '../../types';
2
+ import type { PlaygroundResult as PlaygroundResultType, ReportDownloadHandler, ServiceModeType } from '../../types';
4
3
  import type { ReplayScriptsInfo } from '../../utils/replay-scripts';
5
4
  import './index.less';
6
5
  interface PlaygroundResultProps {
@@ -17,6 +16,7 @@ interface PlaygroundResultProps {
17
16
  autoZoom?: boolean;
18
17
  actionType?: string;
19
18
  canDownloadReport?: boolean;
19
+ onDownloadReport?: ReportDownloadHandler;
20
20
  }
21
21
  export declare const PlaygroundResultView: React.FC<PlaygroundResultProps>;
22
22
  export {};
@@ -1,4 +1,6 @@
1
+ import React from 'react';
1
2
  import './index.less';
3
+ export type ScreenshotViewerMode = 'default' | 'screen-only';
2
4
  interface ScreenshotViewerProps {
3
5
  getScreenshot: () => Promise<{
4
6
  screenshot: string;
@@ -11,6 +13,7 @@ interface ScreenshotViewerProps {
11
13
  serverOnline: boolean;
12
14
  isUserOperating?: boolean;
13
15
  mjpegUrl?: string;
16
+ mode?: ScreenshotViewerMode;
14
17
  }
15
- export default function ScreenshotViewer({ getScreenshot, getInterfaceInfo, serverOnline, isUserOperating, mjpegUrl, }: ScreenshotViewerProps): import("react").JSX.Element;
18
+ export default function ScreenshotViewer({ getScreenshot, getInterfaceInfo, serverOnline, isUserOperating, mjpegUrl, mode, }: ScreenshotViewerProps): React.JSX.Element;
16
19
  export {};
@@ -18,10 +18,11 @@ export { PromptInput } from './component/prompt-input';
18
18
  export { Player } from './component/player';
19
19
  export { Blackboard } from './component/blackboard';
20
20
  export { default as ScreenshotViewer } from './component/screenshot-viewer';
21
+ export type { ScreenshotViewerMode } from './component/screenshot-viewer';
21
22
  export { actionNameForType, staticAgentFromContext, getPlaceholderForType, } from './utils/playground-utils';
22
23
  export { timeStr, filterBase64Value } from './utils';
23
24
  export { default as ShinyText } from './component/shiny-text';
24
25
  export { UniversalPlayground, default as UniversalPlaygroundDefault, } from './component/universal-playground';
25
- export type { UniversalPlaygroundProps, PlaygroundSDKLike, StorageProvider, ContextProvider, UniversalPlaygroundConfig, PlaygroundBranding, InfoListItem, FormValue, ExecutionOptions, ProgressCallback, DeviceType, ExecutionUxHint, ExecutionUxConfig, PromptInputChromeConfig, } from './types';
26
+ export type { UniversalPlaygroundProps, PlaygroundSDKLike, StorageProvider, ContextProvider, UniversalPlaygroundConfig, PlaygroundBranding, InfoListItem, FormValue, ExecutionOptions, ProgressCallback, DeviceType, ExecutionUxHint, ExecutionUxConfig, PromptInputChromeConfig, ReportDownloadHandler, ReportDownloadRequest, } from './types';
26
27
  export { LocalStorageProvider, MemoryStorageProvider, NoOpStorageProvider, IndexedDBStorageProvider, createStorageProvider, detectBestStorageType, StorageType, } from './component/universal-playground/providers/storage-provider';
27
28
  export { BaseContextProvider, AgentContextProvider, StaticContextProvider, NoOpContextProvider, } from './component/universal-playground/providers/context-provider';
@@ -163,6 +163,11 @@ export interface InfoListItem {
163
163
  */
164
164
  actionKind?: string;
165
165
  }
166
+ export interface ReportDownloadRequest {
167
+ content: string;
168
+ defaultFileName: string;
169
+ }
170
+ export type ReportDownloadHandler = (request: ReportDownloadRequest) => void | Promise<void>;
166
171
  export interface UniversalPlaygroundConfig {
167
172
  showContextPreview?: boolean;
168
173
  storageNamespace?: string;
@@ -193,6 +198,11 @@ export interface UniversalPlaygroundConfig {
193
198
  * grouping, no connector) so existing hosts keep their behaviour.
194
199
  */
195
200
  executionFlow?: ExecutionFlowConfig;
201
+ /**
202
+ * Optional host-provided report download hook.
203
+ * Defaults to the browser Blob download flow when omitted.
204
+ */
205
+ onDownloadReport?: ReportDownloadHandler;
196
206
  }
197
207
  export interface ExecutionFlowConfig {
198
208
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@midscene/visualizer",
3
- "version": "1.7.6",
3
+ "version": "1.7.7-beta-20260428102047.0",
4
4
  "repository": "https://github.com/web-infra-dev/midscene",
5
5
  "homepage": "https://midscenejs.com/",
6
6
  "types": "./dist/types/index.d.ts",
@@ -58,10 +58,10 @@
58
58
  "antd": "^5.21.6",
59
59
  "buffer": "6.0.3",
60
60
  "dayjs": "^1.11.11",
61
- "@midscene/core": "1.7.6",
62
- "@midscene/playground": "1.7.6",
63
- "@midscene/web": "1.7.6",
64
- "@midscene/shared": "1.7.6"
61
+ "@midscene/playground": "1.7.7-beta-20260428102047.0",
62
+ "@midscene/core": "1.7.7-beta-20260428102047.0",
63
+ "@midscene/web": "1.7.7-beta-20260428102047.0",
64
+ "@midscene/shared": "1.7.7-beta-20260428102047.0"
65
65
  },
66
66
  "license": "MIT",
67
67
  "scripts": {