Skip to content

Commit f2dab8b

Browse files
committed
feat(report): add agent markdown export view
1 parent b45f2d6 commit f2dab8b

25 files changed

Lines changed: 2365 additions & 164 deletions

apps/report/src/App.less

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,9 @@ footer.mt-8 {
119119
.page-nav-left {
120120
display: flex;
121121
flex-direction: row;
122+
align-items: center;
123+
gap: 12px;
124+
min-width: 0;
122125

123126
.page-nav-title {
124127
padding-left: 4px;
@@ -132,6 +135,32 @@ footer.mt-8 {
132135
font-size: 12px;
133136
}
134137
}
138+
139+
.report-view-mode-switch {
140+
flex-shrink: 0;
141+
142+
.ant-segmented-item-label {
143+
min-height: 26px;
144+
line-height: 26px;
145+
padding-inline: 12px;
146+
}
147+
}
148+
149+
.report-markdown-actions {
150+
display: flex;
151+
align-items: center;
152+
gap: 6px;
153+
min-width: 0;
154+
155+
.ant-btn {
156+
height: 28px;
157+
}
158+
159+
.ant-btn:not(.ant-btn-icon-only) {
160+
padding-inline: 10px;
161+
font-size: 13px;
162+
}
163+
}
135164
}
136165

137166
.page-nav-right {
@@ -178,6 +207,24 @@ footer.mt-8 {
178207
}
179208
}
180209

210+
@media (max-width: 960px) {
211+
.page-nav {
212+
.page-nav-left {
213+
gap: 8px;
214+
215+
.report-view-mode-switch .ant-segmented-item-label {
216+
padding-inline: 8px;
217+
}
218+
219+
.report-markdown-actions .ant-btn:not(.ant-btn-icon-only) {
220+
max-width: 148px;
221+
overflow: hidden;
222+
text-overflow: ellipsis;
223+
}
224+
}
225+
}
226+
}
227+
181228
.cost-str {
182229
color: @weak-text;
183230
}

apps/report/src/App.tsx

Lines changed: 158 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,24 @@
11
import './App.less';
22

3-
import { Alert, App as AntdApp, ConfigProvider, Empty, theme } from 'antd';
3+
import { CopyOutlined, DownloadOutlined } from '@ant-design/icons';
4+
import {
5+
Alert,
6+
App as AntdApp,
7+
Button,
8+
ConfigProvider,
9+
Empty,
10+
Segmented,
11+
Tooltip,
12+
message,
13+
theme,
14+
} from 'antd';
415
import { useEffect, useMemo, useRef, useState } from 'react';
516
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
617

718
import {
819
GroupedActionDump,
920
dedupeExecutionsKeepLatest,
21+
reportToMarkdown,
1022
restoreImageReferences,
1123
} from '@midscene/core';
1224
import { antiEscapeScriptTag } from '@midscene/shared/utils';
@@ -16,6 +28,7 @@ import {
1628
globalThemeConfig,
1729
useGlobalPreference,
1830
} from '@midscene/visualizer';
31+
import AgentScreenshotView from './components/agent-screenshot-view';
1932
import DetailPanel from './components/detail-panel';
2033
import DetailSide from './components/detail-side';
2134
import GlobalHoverPreview from './components/global-hover-preview';
@@ -27,8 +40,15 @@ import ThemeLightIcon from './icons/theme-light.svg?react';
2740
import type {
2841
PlaywrightTaskAttributes,
2942
PlaywrightTasks,
43+
ReportViewMode,
3044
VisualizerProps,
3145
} from './types';
46+
import {
47+
downloadMarkdownZip,
48+
getReportMarkdownView,
49+
markdownArchiveBaseName,
50+
markdownZipDownloadTooltip,
51+
} from './utils/markdown-export';
3252
import { formatModelBriefText } from './utils/model-brief';
3353
import {
3454
getEmptyDumpDescription,
@@ -97,6 +117,12 @@ function Visualizer(props: VisualizerProps): JSX.Element {
97117
});
98118
const dump = useExecutionDump((store) => store.dump);
99119
const [timelineCollapsed, setTimelineCollapsed] = useState(false);
120+
const [reportViewMode, setReportViewMode] = useState<ReportViewMode>('human');
121+
const [selectedMarkdownImagePath, setSelectedMarkdownImagePath] = useState<
122+
string | null
123+
>(null);
124+
const [selectedMarkdownImageRequestId, setSelectedMarkdownImageRequestId] =
125+
useState(0);
100126
const {
101127
modelCallDetailsEnabled: proModeEnabled,
102128
setModelCallDetailsEnabled: setProModeEnabled,
@@ -147,6 +173,62 @@ function Visualizer(props: VisualizerProps): JSX.Element {
147173
dumps.every((d) => d.attributes?.playwright_test_status === 'skipped'),
148174
[dumps],
149175
);
176+
const reportMarkdownView = useMemo(() => {
177+
if (reportViewMode !== 'markdown') {
178+
return null;
179+
}
180+
return getReportMarkdownView(dump, reportToMarkdown);
181+
}, [dump, reportViewMode]);
182+
const readyReportMarkdown =
183+
reportMarkdownView?.status === 'ready' ? reportMarkdownView : null;
184+
const reportArchiveBaseName = useMemo(
185+
() => markdownArchiveBaseName(dump),
186+
[dump],
187+
);
188+
189+
const handleCopyReportMarkdown = async () => {
190+
if (!readyReportMarkdown) {
191+
return;
192+
}
193+
194+
try {
195+
await navigator.clipboard.writeText(readyReportMarkdown.markdown);
196+
message.success('Markdown copied');
197+
} catch {
198+
message.error('Copy failed');
199+
}
200+
};
201+
202+
const handleDownloadReportMarkdownZip = async () => {
203+
if (!readyReportMarkdown) {
204+
return;
205+
}
206+
207+
try {
208+
const result = await downloadMarkdownZip(
209+
readyReportMarkdown.markdown,
210+
readyReportMarkdown.attachments,
211+
reportArchiveBaseName,
212+
);
213+
if (result.missingAttachmentCount > 0) {
214+
message.warning(
215+
`${result.missingAttachmentCount} screenshot link${
216+
result.missingAttachmentCount === 1 ? '' : 's'
217+
} kept as markdown reference${
218+
result.missingAttachmentCount === 1 ? '' : 's'
219+
} but not packaged.`,
220+
);
221+
} else {
222+
message.success('Markdown and images downloaded');
223+
}
224+
} catch {
225+
message.error('Download failed');
226+
}
227+
};
228+
const handleMarkdownImageClick = (markdownPath: string) => {
229+
setSelectedMarkdownImagePath(markdownPath);
230+
setSelectedMarkdownImageRequestId((current) => current + 1);
231+
};
150232

151233
const renderContent = () => {
152234
if (dump && dump.executions.length === 0) {
@@ -228,6 +310,9 @@ function Visualizer(props: VisualizerProps): JSX.Element {
228310
onProModeChange={setProModeEnabled}
229311
replayAllScripts={replayAllScripts}
230312
setReplayAllMode={setReplayAllMode}
313+
reportViewMode={reportViewMode}
314+
reportMarkdownView={reportMarkdownView}
315+
onMarkdownImageClick={handleMarkdownImageClick}
231316
/>
232317
</div>
233318
<div
@@ -252,28 +337,38 @@ function Visualizer(props: VisualizerProps): JSX.Element {
252337
}}
253338
/>
254339
<div className="main-right">
255-
<div
256-
className="main-right-header"
257-
onClick={() => setTimelineCollapsed(!timelineCollapsed)}
258-
style={{ cursor: 'pointer', userSelect: 'none' }}
259-
>
260-
<span
261-
className="timeline-collapse-icon"
262-
style={{
263-
display: 'inline-block',
264-
marginRight: 8,
265-
transition: 'transform 0.2s',
266-
transform: timelineCollapsed
267-
? 'rotate(-90deg)'
268-
: 'rotate(0deg)',
269-
}}
270-
>
271-
272-
</span>
273-
Record
274-
</div>
275-
{!timelineCollapsed && <Timeline key={mainLayoutChangeFlag} />}
276-
<div className="main-content">{content}</div>
340+
{reportViewMode === 'markdown' ? (
341+
<AgentScreenshotView
342+
markdownView={reportMarkdownView}
343+
selectedMarkdownImagePath={selectedMarkdownImagePath}
344+
selectedMarkdownImageRequestId={selectedMarkdownImageRequestId}
345+
/>
346+
) : (
347+
<>
348+
<div
349+
className="main-right-header"
350+
onClick={() => setTimelineCollapsed(!timelineCollapsed)}
351+
style={{ cursor: 'pointer', userSelect: 'none' }}
352+
>
353+
<span
354+
className="timeline-collapse-icon"
355+
style={{
356+
display: 'inline-block',
357+
marginRight: 8,
358+
transition: 'transform 0.2s',
359+
transform: timelineCollapsed
360+
? 'rotate(-90deg)'
361+
: 'rotate(0deg)',
362+
}}
363+
>
364+
365+
</span>
366+
Record
367+
</div>
368+
{!timelineCollapsed && <Timeline key={mainLayoutChangeFlag} />}
369+
<div className="main-content">{content}</div>
370+
</>
371+
)}
277372
</div>
278373
</div>
279374
);
@@ -327,6 +422,46 @@ function Visualizer(props: VisualizerProps): JSX.Element {
327422
<div className="page-nav">
328423
<div className="page-nav-left">
329424
<Logo />
425+
{executionDump && (
426+
<Segmented
427+
size="small"
428+
className="report-view-mode-switch"
429+
value={reportViewMode}
430+
options={[
431+
{ label: 'Human View', value: 'human' },
432+
{ label: 'Markdown View', value: 'markdown' },
433+
]}
434+
onChange={(value) => {
435+
setReportViewMode(value as ReportViewMode);
436+
setSelectedMarkdownImagePath(null);
437+
setSelectedMarkdownImageRequestId(0);
438+
}}
439+
/>
440+
)}
441+
{reportViewMode === 'markdown' && (
442+
<div className="report-markdown-actions">
443+
<Tooltip title="Copy report.md markdown">
444+
<Button
445+
type="text"
446+
size="small"
447+
icon={<CopyOutlined />}
448+
disabled={!readyReportMarkdown}
449+
onClick={() => void handleCopyReportMarkdown()}
450+
aria-label="Copy report markdown"
451+
/>
452+
</Tooltip>
453+
<Tooltip title={markdownZipDownloadTooltip}>
454+
<Button
455+
type="text"
456+
size="small"
457+
icon={<DownloadOutlined />}
458+
disabled={!readyReportMarkdown}
459+
onClick={() => void handleDownloadReportMarkdownZip()}
460+
aria-label="Download markdown and images ZIP"
461+
/>
462+
</Tooltip>
463+
</div>
464+
)}
330465
</div>
331466
<div className="page-nav-right">
332467
<div className="page-nav-version">

0 commit comments

Comments
 (0)