diff --git a/server/src/browser-management/classes/RemoteBrowser.ts b/server/src/browser-management/classes/RemoteBrowser.ts index fe7822105..277760fad 100644 --- a/server/src/browser-management/classes/RemoteBrowser.ts +++ b/server/src/browser-management/classes/RemoteBrowser.ts @@ -1866,6 +1866,63 @@ export class RemoteBrowser { ) as Array>; } + /** + * Captures a screenshot directly without running the workflow interpreter + * @param settings Screenshot settings containing fullPage, type, etc. + * @returns Promise + */ + public captureDirectScreenshot = async (settings: { + fullPage: boolean; + type: 'png' | 'jpeg'; + timeout?: number; + animations?: 'disabled' | 'allow'; + caret?: 'hide' | 'initial'; + scale?: 'css' | 'device'; + }): Promise => { + if (!this.currentPage) { + logger.error("No current page available for screenshot"); + this.socket.emit('screenshotError', { + userId: this.userId, + error: 'No active page available' + }); + return; + } + + try { + this.socket.emit('screenshotCaptureStarted', { + userId: this.userId, + fullPage: settings.fullPage + }); + + const screenshotBuffer = await this.currentPage.screenshot({ + fullPage: settings.fullPage, + type: settings.type || 'png', + timeout: settings.timeout || 30000, + animations: settings.animations || 'allow', + caret: settings.caret || 'hide', + scale: settings.scale || 'device' + }); + + const base64Data = screenshotBuffer.toString('base64'); + const mimeType = `image/${settings.type || 'png'}`; + const dataUrl = `data:${mimeType};base64,${base64Data}`; + + this.socket.emit('directScreenshotCaptured', { + userId: this.userId, + screenshot: dataUrl, + mimeType: mimeType, + fullPage: settings.fullPage, + timestamp: Date.now() + }); + } catch (error) { + logger.error('Failed to capture direct screenshot:', error); + this.socket.emit('screenshotError', { + userId: this.userId, + error: error instanceof Error ? error.message : 'Unknown error occurred' + }); + } + }; + /** * Registers all event listeners needed for the recording editor session. * Should be called only once after the full initialization of the remote browser. @@ -1874,6 +1931,16 @@ export class RemoteBrowser { public registerEditorEvents = (): void => { // For each event, include userId to make sure events are handled for the correct browser logger.log('debug', `Registering editor events for user: ${this.userId}`); + + this.socket.on(`captureDirectScreenshot:${this.userId}`, async (settings) => { + logger.debug(`Direct screenshot capture requested for user ${this.userId}`); + await this.captureDirectScreenshot(settings); + }); + + // For backward compatibility + this.socket.on('captureDirectScreenshot', async (settings) => { + await this.captureDirectScreenshot(settings); + }); // Listen for specific events for this user this.socket.on(`rerender:${this.userId}`, async () => { diff --git a/src/components/recorder/DOMBrowserRenderer.tsx b/src/components/recorder/DOMBrowserRenderer.tsx index c18c98854..a7cc2d266 100644 --- a/src/components/recorder/DOMBrowserRenderer.tsx +++ b/src/components/recorder/DOMBrowserRenderer.tsx @@ -856,7 +856,7 @@ export const DOMBrowserRenderer: React.FC = ({ /* Make everything interactive */ * { - cursor: ${isInCaptureMode ? "crosshair" : "pointer"} !important; + cursor: "pointer" !important; } /* Additional CSS from resources */ @@ -1127,7 +1127,7 @@ export const DOMBrowserRenderer: React.FC = ({ left: 0, right: 0, bottom: 0, - cursor: "pointer", + cursor: "pointer !important", pointerEvents: "none", zIndex: 999, borderRadius: "0px 0px 5px 5px", diff --git a/src/components/recorder/RightSidePanel.tsx b/src/components/recorder/RightSidePanel.tsx index c1f6ce30d..3fd3fcf6c 100644 --- a/src/components/recorder/RightSidePanel.tsx +++ b/src/components/recorder/RightSidePanel.tsx @@ -72,7 +72,7 @@ export const RightSidePanel: React.FC = ({ onFinishCapture startAction, finishAction } = useActionContext(); - const { browserSteps, updateBrowserTextStepLabel, deleteBrowserStep, addScreenshotStep, updateListTextFieldLabel, removeListTextField, updateListStepLimit, deleteStepsByActionId, updateListStepData } = useBrowserSteps(); + const { browserSteps, updateBrowserTextStepLabel, deleteBrowserStep, addScreenshotStep, updateListTextFieldLabel, removeListTextField, updateListStepLimit, deleteStepsByActionId, updateListStepData, updateScreenshotStepData } = useBrowserSteps(); const { id, socket } = useSocketStore(); const { t } = useTranslation(); @@ -183,6 +183,29 @@ export const RightSidePanel: React.FC = ({ onFinishCapture }; }, [socket, updateListStepData, isDOMMode]); + useEffect(() => { + if (socket) { + const handleDirectScreenshot = (data: any) => { + const screenshotSteps = browserSteps.filter(step => + step.type === 'screenshot' && step.actionId === currentScreenshotActionId + ); + + if (screenshotSteps.length > 0) { + const latestStep = screenshotSteps[screenshotSteps.length - 1]; + updateScreenshotStepData(latestStep.id, data.screenshot); + } + + setCurrentScreenshotActionId(''); + }; + + socket.on('directScreenshotCaptured', handleDirectScreenshot); + + return () => { + socket.off('directScreenshotCaptured', handleDirectScreenshot); + }; + } + }, [socket, id, notify, t, currentScreenshotActionId, updateScreenshotStepData, setCurrentScreenshotActionId]); + const extractDataClientSide = useCallback( ( listSelector: string, @@ -649,14 +672,15 @@ export const RightSidePanel: React.FC = ({ onFinishCapture }, [currentListActionId, browserSteps, stopGetList, deleteStepsByActionId, resetListState, setShowPaginationOptions, setShowLimitOptions, setCaptureStage, notify, t]); const captureScreenshot = (fullPage: boolean) => { - const screenshotSettings: ScreenshotSettings = { + const screenshotSettings = { fullPage, - type: 'png', + type: 'png' as const, timeout: 30000, - animations: 'allow', - caret: 'hide', - scale: 'device', + animations: 'allow' as const, + caret: 'hide' as const, + scale: 'device' as const, }; + socket?.emit('captureDirectScreenshot', screenshotSettings); socket?.emit('action', { action: 'screenshot', settings: screenshotSettings }); addScreenshotStep(fullPage, currentScreenshotActionId); stopGetScreenshot(); diff --git a/src/components/recorder/SidePanelHeader.tsx b/src/components/recorder/SidePanelHeader.tsx index 41b33ae7f..889a51e30 100644 --- a/src/components/recorder/SidePanelHeader.tsx +++ b/src/components/recorder/SidePanelHeader.tsx @@ -2,7 +2,11 @@ import React, { FC, useState } from 'react'; import { InterpretationButtons } from "../run/InterpretationButtons"; import { useSocketStore } from "../../context/socket"; -export const SidePanelHeader = () => { +interface SidePanelHeaderProps { + onPreviewClick?: () => void; +} + +export const SidePanelHeader = ({ onPreviewClick }: SidePanelHeaderProps) => { const [steppingIsDisabled, setSteppingIsDisabled] = useState(true); @@ -14,7 +18,10 @@ export const SidePanelHeader = () => { return (
- setSteppingIsDisabled(!isPaused)} /> + setSteppingIsDisabled(!isPaused)} + onPreviewComplete={onPreviewClick} + /> {/*