From 313bd7885dc5fd2ca940135067532f28cd776cb0 Mon Sep 17 00:00:00 2001 From: Rohit Rajan Date: Mon, 14 Sep 2026 21:13:52 +0530 Subject: [PATCH] fix: rm ss comparison logic --- package.backend.json | 1 - package.json | 1 - server/src/api/record.ts | 25 +- server/src/routes/storage.ts | 17 -- server/src/task-runner.ts | 21 +- server/src/utils/run-comparison.ts | 46 +--- server/src/utils/screenshot-comparison.ts | 237 ------------------ .../workflow-management/scheduler/index.ts | 21 +- src/api/storage.ts | 16 -- .../robot/pages/RobotSettingsPage.tsx | 2 +- src/hooks/useRunDiff.ts | 28 +-- src/pages/RunDiffPage.tsx | 106 +------- 12 files changed, 18 insertions(+), 503 deletions(-) delete mode 100644 server/src/utils/screenshot-comparison.ts diff --git a/package.backend.json b/package.backend.json index a37bcfe1..db155369 100644 --- a/package.backend.json +++ b/package.backend.json @@ -54,7 +54,6 @@ "rrweb": "2.0.0-alpha.4", "sequelize": "6.37.7", "sequelize-typescript": "2.1.6", - "sharp": "0.35.3", "socket.io": "4.8.1", "socket.io-client": "4.8.1", "swagger-jsdoc": "6.2.8", diff --git a/package.json b/package.json index b8d9b73e..1f247459 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,6 @@ "rrweb": "^2.0.0-alpha.4", "sequelize": "^6.37.3", "sequelize-typescript": "^2.1.6", - "sharp": "^0.35.3", "socket.io": "^4.4.1", "socket.io-client": "^4.4.1", "styled-components": "^5.3.3", diff --git a/server/src/api/record.ts b/server/src/api/record.ts index 31cfd83a..96b15e08 100644 --- a/server/src/api/record.ts +++ b/server/src/api/record.ts @@ -955,21 +955,10 @@ async function executeRun(id: string, userId: string) { const binaryOutputService = new BinaryOutputService('maxun-run-screenshots'); if ((recording.recording_meta as any).compareRuns) { try { - const comparison = await compareRunOutputsWithPrevious(run, serializableOutput, binaryOutput); + const comparison = await compareRunOutputsWithPrevious(run, serializableOutput); hasChanges = comparison.hasChanges; serializableOutput._comparison = { changedFormats: comparison.changedFormats, - screenshots: Object.fromEntries(await Promise.all( - Object.entries(comparison.screenshotComparisons).map(async ([key, value]) => { - if (!value) return [key, null]; - const { diff: _diff, ...metadata } = value; - const diff = comparison.screenshotDiffs[`${key}-diff`]; - const diffUrl = diff - ? await binaryOutputService.uploadBinaryOutputItem(plainRun.runId, `${key}-diff`, diff, 'image/png') - : null; - return [key, { ...metadata, diff: diffUrl || (diff ? `data:image/png;base64,${diff.toString('base64')}` : null) }]; - }), - )), }; } catch (comparisonError: any) { logger.warn(`Monitoring comparison failed for API scrape run ${plainRun.runId}: ${comparisonError.message}`); @@ -1197,20 +1186,10 @@ async function executeRun(id: string, userId: string) { let hasChanges = false; if (robotType === 'extract' && (recording.recording_meta as any).compareRuns) { try { - const comparison = await compareExtractRunWithPrevious(run, finalSerializableOutput, postBinaryOutput); + const comparison = await compareExtractRunWithPrevious(run, finalSerializableOutput); hasChanges = comparison.hasChanges; finalSerializableOutput._comparison = { changedFormats: comparison.changedFormats, - screenshots: Object.fromEntries(await Promise.all( - Object.entries(comparison.screenshotComparisons).map(async ([key, value]) => { - const { diff: _diff, ...metadata } = value; - const diff = comparison.screenshotDiffs[`${key}-diff`]; - const diffUrl = diff - ? await binaryOutputService.uploadBinaryOutputItem(plainRun.runId, `${key}-diff`, diff, 'image/png') - : null; - return [key, { ...metadata, diff: diffUrl || (diff ? `data:image/png;base64,${diff.toString('base64')}` : null) }]; - }), - )), }; } catch (comparisonError: any) { logger.warn(`Monitoring comparison failed for API extract run ${plainRun.runId}: ${comparisonError.message}`); diff --git a/server/src/routes/storage.ts b/server/src/routes/storage.ts index 350d64bc..71bb8a45 100644 --- a/server/src/routes/storage.ts +++ b/server/src/routes/storage.ts @@ -1306,7 +1306,6 @@ router.get('/runs/:id/diff', requireSignIn, async (req: AuthenticatedRequest, re const currentText = formats.text?.current || ''; const previousText = formats.text?.previous || ''; - const screenshotMetadata = currentOutput?._comparison?.screenshots || {}; const isExtract = robot.recording_meta.type === 'extract'; const currentCapturedText = currentOutput?.scrapeSchema || {}; const previousCapturedText = previousOutput?.scrapeSchema || {}; @@ -1323,21 +1322,6 @@ router.get('/runs/:id/diff', requireSignIn, async (req: AuthenticatedRequest, re && (Object.keys(currentCapturedLists).length > 0 || Object.keys(previousCapturedLists).length > 0) ? { current: currentCapturedLists, previous: previousCapturedLists } : null; - const screenshotNames = isExtract - ? Object.keys(run.binaryOutput || {}).filter((name) => !name.endsWith('-diff')) - : ['screenshot-visible', 'screenshot-fullpage']; - const screenshotFormats = screenshotNames.reduce((result, format) => { - const current = run.binaryOutput?.[format]; - if (!current) return result; - result[format] = { - current, - previous: previousRun.binaryOutput?.[format] || null, - diff: screenshotMetadata[format]?.diff || run.binaryOutput?.[`${format}-diff`] || null, - metadata: screenshotMetadata[format] || null, - }; - return result; - }, {} as Record); - return res.json({ currentRunId: run.runId, previousRunId: previousRun.runId, @@ -1346,7 +1330,6 @@ router.get('/runs/:id/diff', requireSignIn, async (req: AuthenticatedRequest, re formats, capturedText, capturedLists, - screenshots: screenshotFormats, changedFormats: currentOutput?._comparison?.changedFormats || [], }); } catch (e) { diff --git a/server/src/task-runner.ts b/server/src/task-runner.ts index 5a2c093e..4567dfa2 100644 --- a/server/src/task-runner.ts +++ b/server/src/task-runner.ts @@ -341,22 +341,11 @@ async function processRunExecution(data: ExecuteRunData): Promise { const binaryOutputService = new BinaryOutputService('maxun-run-screenshots'); if ((recording.recording_meta as any).compareRuns) { try { - const comparison = await compareRunOutputsWithPrevious(run, serializableOutput, binaryOutput); + const comparison = await compareRunOutputsWithPrevious(run, serializableOutput); hasChanges = comparison.hasChanges; serializableOutput._comparison = { changedFormats: comparison.changedFormats, - screenshots: Object.fromEntries(await Promise.all( - Object.entries(comparison.screenshotComparisons).map(async ([key, value]) => { - if (!value) return [key, null]; - const { diff: _diff, ...metadata } = value; - const diff = comparison.screenshotDiffs[`${key}-diff`]; - const diffUrl = diff - ? await binaryOutputService.uploadBinaryOutputItem(plainRun.runId, `${key}-diff`, diff, 'image/png') - : null; - return [key, { ...metadata, diff: diffUrl || (diff ? `data:image/png;base64,${diff.toString('base64')}` : null) }]; - }), - )), }; if (comparison.previousRun) { @@ -498,16 +487,10 @@ async function processRunExecution(data: ExecuteRunData): Promise { let hasChanges = false; const binarySvc = new BinaryOutputService('maxun-run-screenshots'); if (robotType === 'extract' && (recording.recording_meta as any).compareRuns) { - const comparison = await compareExtractRunWithPrevious(run, finalSerializableOutput, binaryOutput); + const comparison = await compareExtractRunWithPrevious(run, finalSerializableOutput); hasChanges = comparison.hasChanges; finalSerializableOutput._comparison = { changedFormats: comparison.changedFormats, - screenshots: Object.fromEntries(await Promise.all(Object.entries(comparison.screenshotComparisons).map(async ([key, value]) => { - const { diff: _diff, ...metadata } = value; - const diff = comparison.screenshotDiffs[`${key}-diff`]; - const diffUrl = diff ? await binarySvc.uploadBinaryOutputItem(plainRun.runId, `${key}-diff`, diff, 'image/png') : null; - return [key, { ...metadata, diff: diffUrl || (diff ? `data:image/png;base64,${diff.toString('base64')}` : null) }]; - }))), }; } diff --git a/server/src/utils/run-comparison.ts b/server/src/utils/run-comparison.ts index 927cdd23..6d8e09bd 100644 --- a/server/src/utils/run-comparison.ts +++ b/server/src/utils/run-comparison.ts @@ -1,9 +1,4 @@ import Run from '../models/Run'; -import { - COMPARABLE_SCREENSHOT_FORMATS, - compareScreenshots, - ScreenshotComparisonResult, -} from './screenshot-comparison'; export const COMPARABLE_RUN_FORMATS = ['text', 'markdown', 'html'] as const; export type ComparableRunFormat = (typeof COMPARABLE_RUN_FORMATS)[number]; @@ -27,7 +22,7 @@ export async function findPreviousSuccessfulRun(currentRun: any) { robotMetaId: currentRun.robotMetaId, status: 'success', }, - attributes: ['runId', 'finishedAt', 'startedAt', 'serializableOutput', 'binaryOutput'], + attributes: ['runId', 'finishedAt', 'startedAt', 'serializableOutput'], }); const currentTimestamp = getRunTimestamp(currentRun); @@ -62,15 +57,13 @@ export async function compareRunTextWithPrevious(currentRun: any, currentText: s * Compares every text-based output produced by the current run against the * matching output from the previous successful run. */ -export async function compareRunOutputsWithPrevious(currentRun: any, currentOutput: any, currentBinaryOutput?: any) { +export async function compareRunOutputsWithPrevious(currentRun: any, currentOutput: any) { const previousRun = await findPreviousSuccessfulRun(currentRun); if (!previousRun) { return { previousRun: null, hasChanges: false, changedFormats: [] as string[], - screenshotComparisons: {} as Partial>, - screenshotDiffs: {} as Record, }; } @@ -83,25 +76,10 @@ export async function compareRunOutputsWithPrevious(currentRun: any, currentOutp return normalizeComparableText(previousContent) !== normalizeComparableText(currentContent); }); - const screenshotComparisons: Partial> = {}; - const screenshotDiffs: Record = {}; - for (const format of COMPARABLE_SCREENSHOT_FORMATS) { - if (!currentBinaryOutput?.[format]) continue; - const comparison = await compareScreenshots(currentBinaryOutput[format], previousRun.binaryOutput?.[format]); - if (!comparison) continue; - screenshotComparisons[format] = comparison; - if (comparison.changed) { - changedFormats.push(format); - if (comparison.diff) screenshotDiffs[`${format}-diff`] = comparison.diff; - } - } - return { previousRun, hasChanges: changedFormats.length > 0, changedFormats, - screenshotComparisons, - screenshotDiffs, }; } @@ -130,16 +108,14 @@ const canonicalizeCapturedLists = (value: any): any => { export const serializeCapturedLists = (value: any) => JSON.stringify(canonicalizeCapturedLists(value || {})); -/** Compares named text and screenshot captures produced by an extract robot. */ -export async function compareExtractRunWithPrevious(currentRun: any, currentOutput: any, binaryOutput: any) { +/** Compares captured text and lists produced by an extract robot. */ +export async function compareExtractRunWithPrevious(currentRun: any, currentOutput: any) { const previousRun = await findPreviousSuccessfulRun(currentRun); if (!previousRun) { return { previousRun: null, hasChanges: false, changedFormats: [] as string[], - screenshotComparisons: {} as Record, - screenshotDiffs: {} as Record, }; } @@ -151,23 +127,9 @@ export async function compareExtractRunWithPrevious(currentRun: any, currentOutp changedFormats.push('captured-list'); } - const screenshotComparisons: Record = {}; - const screenshotDiffs: Record = {}; - for (const name of Object.keys(binaryOutput || {}).filter((key) => !key.endsWith('-diff'))) { - const comparison = await compareScreenshots(binaryOutput[name], previousRun.binaryOutput?.[name]); - if (!comparison) continue; - screenshotComparisons[name] = comparison; - if (comparison.changed) { - changedFormats.push(`screenshot:${name}`); - if (comparison.diff) screenshotDiffs[`${name}-diff`] = comparison.diff; - } - } - return { previousRun, hasChanges: changedFormats.length > 0, changedFormats, - screenshotComparisons, - screenshotDiffs, }; } diff --git a/server/src/utils/screenshot-comparison.ts b/server/src/utils/screenshot-comparison.ts deleted file mode 100644 index 9e90ff96..00000000 --- a/server/src/utils/screenshot-comparison.ts +++ /dev/null @@ -1,237 +0,0 @@ -import sharp from 'sharp'; -import { BinaryOutputService } from '../storage/mino'; - -export const COMPARABLE_SCREENSHOT_FORMATS = ['screenshot-visible', 'screenshot-fullpage'] as const; -export type ComparableScreenshotFormat = (typeof COMPARABLE_SCREENSHOT_FORMATS)[number]; - -const PIXEL_CHANNEL_THRESHOLD = 32; -const HORIZONTAL_POSITION_TOLERANCE = 2; -const VERTICAL_POSITION_TOLERANCE = 3; -const CHANGED_PERCENTAGE_THRESHOLD = 0.01; -const MAX_COMPARISON_PIXELS = 4_000_000; - -export interface ScreenshotComparisonResult { - changed: boolean; - changedPixels: number; - changedPercentage: number; - previousWidth: number; - previousHeight: number; - currentWidth: number; - currentHeight: number; - comparedWidth: number; - comparedHeight: number; - diff?: Buffer; -} - -function binaryEntryToBuffer(entry: any): Buffer | null { - const value = entry?.data ?? entry; - if (Buffer.isBuffer(value)) return value; - if (value?.type === 'Buffer' && Array.isArray(value.data)) return Buffer.from(value.data); - if (typeof value !== 'string' || /^https?:\/\//.test(value)) return null; - - const dataUri = value.match(/^data:[^;]+;base64,(.+)$/); - return Buffer.from(dataUri?.[1] ?? value, 'base64'); -} - -async function loadStoredScreenshot(entry: any): Promise { - const inline = binaryEntryToBuffer(entry); - if (inline) return inline; - - const value = entry?.data ?? entry; - if (typeof value !== 'string' || !/^https?:\/\//.test(value)) return null; - - try { - const url = new URL(value); - const bucketSegment = '/maxun-run-screenshots/'; - const bucketIndex = url.pathname.indexOf(bucketSegment); - if (bucketIndex === -1) return null; - const key = decodeURIComponent(url.pathname.slice(bucketIndex + bucketSegment.length)); - return await new BinaryOutputService('maxun-run-screenshots').getBinaryOutputFromMinioBucket(key); - } catch { - return null; - } -} - -async function renderComparableImage( - buffer: Buffer, - imageWidth: number, - imageHeight: number, - canvasWidth: number, - canvasHeight: number, -): Promise { - return sharp(buffer) - .resize(imageWidth, imageHeight, { fit: 'fill' }) - .extend({ - top: 0, - left: 0, - right: canvasWidth - imageWidth, - bottom: canvasHeight - imageHeight, - background: { r: 255, g: 255, b: 255, alpha: 255 }, - }) - .ensureAlpha() - .raw() - .toBuffer(); -} - -const ROW_FINGERPRINT_BINS = 24; - -function createRowFingerprints(buffer: Buffer, canvasWidth: number, imageWidth: number, imageHeight: number) { - const fingerprints = new Float32Array(imageHeight * ROW_FINGERPRINT_BINS); - for (let y = 0; y < imageHeight; y += 1) { - for (let bin = 0; bin < ROW_FINGERPRINT_BINS; bin += 1) { - const start = Math.floor(bin * imageWidth / ROW_FINGERPRINT_BINS); - const end = Math.max(start + 1, Math.floor((bin + 1) * imageWidth / ROW_FINGERPRINT_BINS)); - const step = Math.max(1, Math.floor((end - start) / 6)); - let total = 0; - let samples = 0; - for (let x = start; x < end; x += step) { - const offset = (y * canvasWidth + x) * 4; - total += buffer[offset] * 0.299 + buffer[offset + 1] * 0.587 + buffer[offset + 2] * 0.114; - samples += 1; - } - fingerprints[y * ROW_FINGERPRINT_BINS + bin] = samples ? total / samples : 255; - } - } - return fingerprints; -} - -function alignRows(current: Buffer, previous: Buffer, width: number, currentWidth: number, currentHeight: number, previousWidth: number, previousHeight: number) { - const currentRows = createRowFingerprints(current, width, currentWidth, currentHeight); - const previousRows = createRowFingerprints(previous, width, previousWidth, previousHeight); - const searchRadius = Math.min(250, Math.max(40, Math.abs(currentHeight - previousHeight) + 30)); - const rowMap = new Int32Array(currentHeight); - const blockHeight = 24; - let previousOffset = 0; - - for (let blockStart = 0; blockStart < currentHeight; blockStart += blockHeight) { - const blockEnd = Math.min(currentHeight, blockStart + blockHeight); - const minimumOffset = Math.max(-searchRadius, -blockStart); - const maximumOffset = Math.min(searchRadius, previousHeight - blockEnd); - let bestOffset = Math.max(minimumOffset, Math.min(maximumOffset, previousOffset)); - let bestScore = Number.POSITIVE_INFINITY; - for (let offset = minimumOffset; offset <= maximumOffset; offset += 1) { - let score = Math.abs(offset - previousOffset) * 2 + Math.abs(offset) * 0.05; - for (let currentY = blockStart; currentY < blockEnd; currentY += 3) { - const previousY = currentY + offset; - for (let bin = 0; bin < ROW_FINGERPRINT_BINS; bin += 1) { - score += Math.abs( - currentRows[currentY * ROW_FINGERPRINT_BINS + bin] - - previousRows[previousY * ROW_FINGERPRINT_BINS + bin], - ); - } - } - if (score < bestScore) { - bestScore = score; - bestOffset = offset; - } - } - previousOffset = bestOffset; - for (let currentY = blockStart; currentY < blockEnd; currentY += 1) { - rowMap[currentY] = Math.max(0, Math.min(previousHeight - 1, currentY + bestOffset)); - } - } - return rowMap; -} - -export async function compareScreenshots(currentEntry: any, previousEntry: any): Promise { - const [currentBuffer, previousBuffer] = await Promise.all([ - loadStoredScreenshot(currentEntry), - loadStoredScreenshot(previousEntry), - ]); - if (!currentBuffer || !previousBuffer) return null; - - const [currentMetadata, previousMetadata] = await Promise.all([ - sharp(currentBuffer).metadata(), - sharp(previousBuffer).metadata(), - ]); - if (!currentMetadata.width || !currentMetadata.height || !previousMetadata.width || !previousMetadata.height) return null; - - const sourceWidth = Math.max(currentMetadata.width, previousMetadata.width); - const sourceHeight = Math.max(currentMetadata.height, previousMetadata.height); - const scale = Math.min(1, Math.sqrt(MAX_COMPARISON_PIXELS / (sourceWidth * sourceHeight))); - const width = Math.max(1, Math.round(sourceWidth * scale)); - const height = Math.max(1, Math.round(sourceHeight * scale)); - const currentWidth = Math.max(1, Math.round(currentMetadata.width * scale)); - const currentHeight = Math.max(1, Math.round(currentMetadata.height * scale)); - const previousWidth = Math.max(1, Math.round(previousMetadata.width * scale)); - const previousHeight = Math.max(1, Math.round(previousMetadata.height * scale)); - const [currentPixels, previousPixels] = await Promise.all([ - renderComparableImage(currentBuffer, currentWidth, currentHeight, width, height), - renderComparableImage(previousBuffer, previousWidth, previousHeight, width, height), - ]); - - const previousRowForCurrent = alignRows( - currentPixels, previousPixels, width, currentWidth, currentHeight, previousWidth, previousHeight, - ); - const changedMask = new Uint8Array(width * currentHeight); - let changedPixels = 0; - for (let y = 0; y < currentHeight; y += 1) { - const previousY = previousRowForCurrent[y]; - for (let x = 0; x < currentWidth; x += 1) { - const pixelIndex = y * width + x; - const currentOffset = pixelIndex * 4; - let delta = 255; - for (let dy = -VERTICAL_POSITION_TOLERANCE; dy <= VERTICAL_POSITION_TOLERANCE; dy += 1) { - const candidatePreviousY = previousY + dy; - if (candidatePreviousY < 0 || candidatePreviousY >= previousHeight) continue; - for (let dx = -HORIZONTAL_POSITION_TOLERANCE; dx <= HORIZONTAL_POSITION_TOLERANCE; dx += 1) { - const previousX = x + dx; - if (previousX < 0 || previousX >= previousWidth) continue; - const previousOffset = (candidatePreviousY * width + previousX) * 4; - delta = Math.min(delta, Math.max( - Math.abs(currentPixels[currentOffset] - previousPixels[previousOffset]), - Math.abs(currentPixels[currentOffset + 1] - previousPixels[previousOffset + 1]), - Math.abs(currentPixels[currentOffset + 2] - previousPixels[previousOffset + 2]), - Math.abs(currentPixels[currentOffset + 3] - previousPixels[previousOffset + 3]), - )); - } - } - if (delta > PIXEL_CHANNEL_THRESHOLD) { - changedPixels += 1; - changedMask[pixelIndex] = 1; - } - } - } - - const changedPercentage = (changedPixels / (currentWidth * currentHeight)) * 100; - const changed = changedPercentage >= CHANGED_PERCENTAGE_THRESHOLD; - let diff: Buffer | undefined; - if (changed) { - const highlightedMask = new Uint8Array(changedMask.length); - const radius = 1; - for (let index = 0; index < changedMask.length; index += 1) { - if (!changedMask[index]) continue; - const centerX = index % width; - const centerY = Math.floor(index / width); - for (let y = Math.max(0, centerY - radius); y <= Math.min(currentHeight - 1, centerY + radius); y += 1) { - for (let x = Math.max(0, centerX - radius); x <= Math.min(width - 1, centerX + radius); x += 1) { - highlightedMask[y * width + x] = 1; - } - } - } - - const diffPixels = Buffer.from(currentPixels.subarray(0, width * currentHeight * 4)); - for (let index = 0; index < highlightedMask.length; index += 1) { - if (!highlightedMask[index]) continue; - const offset = index * 4; - diffPixels[offset] = Math.round(diffPixels[offset] * 0.25 + 255 * 0.75); - diffPixels[offset + 1] = Math.round(diffPixels[offset + 1] * 0.25); - diffPixels[offset + 2] = Math.round(diffPixels[offset + 2] * 0.25 + 195 * 0.75); - diffPixels[offset + 3] = 255; - } - diff = await sharp(diffPixels, { raw: { width, height: currentHeight, channels: 4 } }).png().toBuffer(); - } - - return { - changed, - changedPixels, - changedPercentage, - previousWidth: previousMetadata.width, - previousHeight: previousMetadata.height, - currentWidth: currentMetadata.width, - currentHeight: currentMetadata.height, - comparedWidth: width, - comparedHeight: height, - diff, - }; -} diff --git a/server/src/workflow-management/scheduler/index.ts b/server/src/workflow-management/scheduler/index.ts index dbb851d9..8a00c878 100644 --- a/server/src/workflow-management/scheduler/index.ts +++ b/server/src/workflow-management/scheduler/index.ts @@ -447,22 +447,11 @@ async function executeRun(id: string, userId: string) { const binaryOutputService = new BinaryOutputService('maxun-run-screenshots'); if ((recording.recording_meta as any).compareRuns) { try { - const comparison = await compareRunOutputsWithPrevious(run, serializableOutput, binaryOutput); + const comparison = await compareRunOutputsWithPrevious(run, serializableOutput); hasChanges = comparison.hasChanges; serializableOutput._comparison = { changedFormats: comparison.changedFormats, - screenshots: Object.fromEntries(await Promise.all( - Object.entries(comparison.screenshotComparisons).map(async ([key, value]) => { - if (!value) return [key, null]; - const { diff: _diff, ...metadata } = value; - const diff = comparison.screenshotDiffs[`${key}-diff`]; - const diffUrl = diff - ? await binaryOutputService.uploadBinaryOutputItem(plainRun.runId, `${key}-diff`, diff, 'image/png') - : null; - return [key, { ...metadata, diff: diffUrl || (diff ? `data:image/png;base64,${diff.toString('base64')}` : null) }]; - }), - )), }; if (comparison.previousRun) { @@ -689,16 +678,10 @@ async function executeRun(id: string, userId: string) { let hasChanges = false; const binaryOutputService = new BinaryOutputService('maxun-run-screenshots'); if (robotType === 'extract' && (recording.recording_meta as any).compareRuns) { - const comparison = await compareExtractRunWithPrevious(run, finalSerializableOutput, binaryOutput); + const comparison = await compareExtractRunWithPrevious(run, finalSerializableOutput); hasChanges = comparison.hasChanges; finalSerializableOutput._comparison = { changedFormats: comparison.changedFormats, - screenshots: Object.fromEntries(await Promise.all(Object.entries(comparison.screenshotComparisons).map(async ([key, value]) => { - const { diff: _diff, ...metadata } = value; - const diff = comparison.screenshotDiffs[`${key}-diff`]; - const diffUrl = diff ? await binaryOutputService.uploadBinaryOutputItem(plainRun.runId, `${key}-diff`, diff, 'image/png') : null; - return [key, { ...metadata, diff: diffUrl || (diff ? `data:image/png;base64,${diff.toString('base64')}` : null) }]; - }))), }; } diff --git a/src/api/storage.ts b/src/api/storage.ts index 00046bec..93408318 100644 --- a/src/api/storage.ts +++ b/src/api/storage.ts @@ -184,22 +184,6 @@ export interface RunDiffResponse { }>>; capturedText: { current: string; previous: string } | null; capturedLists: { current: Record; previous: Record } | null; - screenshots: Record; changedFormats: string[]; } diff --git a/src/components/robot/pages/RobotSettingsPage.tsx b/src/components/robot/pages/RobotSettingsPage.tsx index f4b77402..f19c91d1 100644 --- a/src/components/robot/pages/RobotSettingsPage.tsx +++ b/src/components/robot/pages/RobotSettingsPage.tsx @@ -243,7 +243,7 @@ export const RobotSettingsPage = ({ handleStart }: RobotSettingsProps) => { {(robot.recording_meta.type === 'scrape' || robot.recording_meta.type === 'extract') && ( <> Monitoring -

When enabled, each run is compared against the previous one to detect changes. The changed values can be viewed in the run comparison results.

+

When enabled, each run is compared against the previous one for text content, Markdown, HTML, captured text, and captured lists. Screenshots are not compared. Changed values can be viewed in the run comparison results.

= { text: 'Text Content', markdown: 'Markdown', html: 'HTML', - 'screenshot-visible': 'Visible Screenshot', - 'screenshot-fullpage': 'Full-page Screenshot', }; export type CapturedListRow = Record; @@ -112,11 +110,9 @@ export function useRunDiff(runId: string | undefined) { ...TEXT_DIFF_FORMATS.filter((format) => data.formats?.[format]), ...(data.capturedText ? ['captured-text'] : []), ...(data.capturedLists ? ['captured-list'] : []), - ...Object.keys(data.screenshots || {}).map((name) => `screenshot:${name}`), ] : []; const firstChangedFormat = availableFormats.find((format) => { - const changedKey = format.startsWith('screenshot:') ? format.slice('screenshot:'.length) : format; - return data?.changedFormats?.includes(format) || data?.changedFormats?.includes(changedKey); + return data?.changedFormats?.includes(format); }); const initialFormat = firstChangedFormat || availableFormats[0]; if (initialFormat) setSelectedDiffFormat(initialFormat); @@ -145,7 +141,7 @@ export function useRunDiff(runId: string | undefined) { }, [runId]); const diffParts = useMemo(() => { - if (!diffData || selectedDiffFormat.startsWith('screenshot:')) return []; + if (!diffData) return []; const selectedOutput = selectedDiffFormat === 'captured-text' ? diffData.capturedText : diffData.formats?.[selectedDiffFormat as TextDiffFormat]; @@ -155,28 +151,12 @@ export function useRunDiff(runId: string | undefined) { }, [diffData, selectedDiffFormat]); const hasDiff = diffParts.some((part) => part.added || part.removed); - const selectedScreenshotName = selectedDiffFormat.startsWith('screenshot:') - ? selectedDiffFormat.slice('screenshot:'.length) - : undefined; - const selectedScreenshot = selectedScreenshotName ? diffData?.screenshots?.[selectedScreenshotName] : undefined; - - const getDiffImageSrc = (entry: string | { data?: string } | null | undefined) => { - const value = typeof entry === 'object' && entry !== null ? entry.data : entry; - if (!value) return ''; - if (value.startsWith('http') || value.startsWith('data:')) return value; - return `data:image/png;base64,${value}`; - }; - const diffOptions = useMemo(() => { if (!diffData) return []; return [ ...TEXT_DIFF_FORMATS.filter((format) => diffData.formats?.[format]).map((format) => ({ key: format, label: DIFF_FORMAT_LABELS[format] })), ...(diffData.capturedText ? [{ key: 'captured-text', label: 'Captured Text' }] : []), ...(diffData.capturedLists ? [{ key: 'captured-list', label: 'Captured Lists' }] : []), - ...Object.keys(diffData.screenshots || {}).map((name) => ({ - key: `screenshot:${name}`, - label: DIFF_FORMAT_LABELS[name] || name, - })), ]; }, [diffData]); @@ -254,13 +234,11 @@ export function useRunDiff(runId: string | undefined) { setSelectedCapturedList, diffParts, hasDiff, - selectedScreenshot, diffOptions, capturedGroups, capturedTableRows, capturedListGroups, capturedListRows, capturedListColumns, - getDiffImageSrc, }; -} \ No newline at end of file +} diff --git a/src/pages/RunDiffPage.tsx b/src/pages/RunDiffPage.tsx index f1582863..8336a7cb 100644 --- a/src/pages/RunDiffPage.tsx +++ b/src/pages/RunDiffPage.tsx @@ -19,35 +19,10 @@ export const RunDiffPage: React.FC = () => { diffData, isDiffLoading, selectedDiffFormat, setSelectedDiffFormat, selectedCapturedGroup, setSelectedCapturedGroup, selectedCapturedList, setSelectedCapturedList, - diffParts, hasDiff, selectedScreenshot, diffOptions, + diffParts, hasDiff, diffOptions, capturedGroups, capturedTableRows, capturedListGroups, capturedListRows, capturedListColumns, - getDiffImageSrc, } = useRunDiff(runId); - // Group all individual "screenshot:" entries the hook produces into one - // top-level "Screenshots" tab, with the individual names becoming a sub-tab row — - // same pattern as the captured-list groups below. - const screenshotOptions = diffOptions.filter((option) => option.key.startsWith('screenshot:')); - const nonScreenshotOptions = diffOptions.filter((option) => !option.key.startsWith('screenshot:')); - const topLevelOptions = [ - ...nonScreenshotOptions, - ...(screenshotOptions.length > 0 ? [{ key: 'screenshots', label: 'Screenshots' }] : []), - ]; - const isScreenshotsActive = selectedDiffFormat.startsWith('screenshot:'); - const topLevelValue = isScreenshotsActive ? 'screenshots' : selectedDiffFormat; - - const handleTopLevelChange = (_: React.SyntheticEvent, value: string) => { - if (value === 'screenshots') { - // Only switch format if we weren't already showing a screenshot; otherwise - // keep whichever one was selected. - if (!isScreenshotsActive && screenshotOptions.length > 0) { - setSelectedDiffFormat(screenshotOptions[0].key); - } - return; - } - setSelectedDiffFormat(value); - }; - return ( @@ -73,8 +48,8 @@ export const RunDiffPage: React.FC = () => { ) : ( <> setSelectedDiffFormat(value)} centered sx={{ minHeight: 36, @@ -87,34 +62,11 @@ export const RunDiffPage: React.FC = () => { '& .MuiTabs-indicator': { height: 2 }, }} > - {topLevelOptions.map((option) => ( + {diffOptions.map((option) => ( ))} - {isScreenshotsActive && screenshotOptions.length > 1 && ( - setSelectedDiffFormat(value)} - variant="scrollable" - scrollButtons="auto" - sx={{ - minHeight: 36, - mb: 3, - flexShrink: 0, - '& .MuiTab-root': { - minHeight: 36, paddingX: 2, paddingY: 1.5, minWidth: 0, - color: theme => `${theme.palette.mode === 'dark' ? '#fff' : '#000'} !important`, - }, - '& .MuiTabs-indicator': { height: 2 }, - }} - > - {screenshotOptions.map((option) => ( - - ))} - - )} - {selectedDiffFormat === 'captured-text' ? ( @@ -300,56 +252,6 @@ export const RunDiffPage: React.FC = () => { )} - ) : selectedScreenshot ? ( - - {selectedScreenshot.metadata && ( - - {selectedScreenshot.metadata.changedPercentage.toFixed(2)}% of compared pixels changed - {' · '} - Previous {selectedScreenshot.metadata.previousWidth}×{selectedScreenshot.metadata.previousHeight} - {' · '} - Current {selectedScreenshot.metadata.currentWidth}×{selectedScreenshot.metadata.currentHeight} - - )} - - {[ - { label: 'Previous Run', source: selectedScreenshot.previous }, - { label: 'Current Run', source: selectedScreenshot.current }, - ].map((image) => ( - - {image.label} - {getDiffImageSrc(image.source) ? ( - - ) : ( - Screenshot unavailable - )} - - ))} - - - Changes Highlighted - - {selectedScreenshot.diff ? ( - <> - - - - Magenta highlights show changed areas on the current screenshot. - - - ) : ( - - A highlighted visual diff was not generated for this run. Run the robot again to create one with the updated comparison. - - )} - - - ) : !hasDiff ? (