diff --git a/src/components/recorder/Canvas.tsx b/legacy/src/Canvas.tsx similarity index 100% rename from src/components/recorder/Canvas.tsx rename to legacy/src/Canvas.tsx diff --git a/src/components/recorder/Highlighter.tsx b/legacy/src/Highlighter.tsx similarity index 100% rename from src/components/recorder/Highlighter.tsx rename to legacy/src/Highlighter.tsx diff --git a/src/helpers/coordinateMapper.ts b/legacy/src/coordinateMapper.ts similarity index 100% rename from src/helpers/coordinateMapper.ts rename to legacy/src/coordinateMapper.ts diff --git a/src/helpers/inputHelpers.ts b/legacy/src/inputHelpers.ts similarity index 100% rename from src/helpers/inputHelpers.ts rename to legacy/src/inputHelpers.ts diff --git a/server/src/browser-management/classes/RemoteBrowser.ts b/server/src/browser-management/classes/RemoteBrowser.ts index eed60616..a6aa1ccf 100644 --- a/server/src/browser-management/classes/RemoteBrowser.ts +++ b/server/src/browser-management/classes/RemoteBrowser.ts @@ -640,7 +640,6 @@ export class RemoteBrowser { private removeAllSocketListeners(): void { try { this.socket.removeAllListeners('captureDirectScreenshot'); - this.socket.removeAllListeners('rerender'); this.socket.removeAllListeners('settings'); this.socket.removeAllListeners('changeTab'); this.socket.removeAllListeners('addTab'); diff --git a/src/components/browser/BrowserWindow.tsx b/src/components/browser/BrowserWindow.tsx index af9d61a9..013d5a94 100644 --- a/src/components/browser/BrowserWindow.tsx +++ b/src/components/browser/BrowserWindow.tsx @@ -7,7 +7,6 @@ import { useBrowserSteps, TextStep, ListStep } from '../../context/browserSteps' import { useGlobalInfoStore } from '../../context/globalInfo'; import { useTranslation } from 'react-i18next'; import { AuthContext } from '../../context/auth'; -import { coordinateMapper } from '../../helpers/coordinateMapper'; import { useBrowserDimensionsStore } from '../../context/browserDimensions'; import { clientSelectorGenerator, ElementFingerprint } from "../../helpers/clientSelectorGenerator"; import { capturedElementHighlighter } from "../../helpers/capturedElementHighlighter"; @@ -36,79 +35,6 @@ interface AttributeOption { value: string; } -interface ViewportInfo { - width: number; - height: number; -} - -interface RRWebSnapshot { - type: number; - childNodes?: RRWebSnapshot[]; - tagName?: string; - attributes?: Record; - textContent: string; - id: number; - [key: string]: any; -} - -interface ProcessedSnapshot { - snapshot: RRWebSnapshot; - resources: { - stylesheets: Array<{ - href: string; - content: string; - media?: string; - }>; - images: Array<{ - src: string; - dataUrl: string; - alt?: string; - }>; - fonts: Array<{ - url: string; - dataUrl: string; - format?: string; - }>; - scripts: Array<{ - src: string; - content: string; - type?: string; - }>; - media: Array<{ - src: string; - dataUrl: string; - type: string; - }>; - }; - baseUrl: string; - viewport: { width: number; height: number }; - timestamp: number; - processingStats: { - totalReplacements: number; - discoveredResources: { - images: number; - stylesheets: number; - scripts: number; - fonts: number; - media: number; - }; - cachedResources: { - stylesheets: number; - images: number; - fonts: number; - scripts: number; - media: number; - }; - totalCacheSize: number; - }; -} - -interface RRWebDOMCastData { - snapshotData: ProcessedSnapshot; - userId: string; - timestamp: number; -} - const getAttributeOptions = (tagName: string, elementInfo: ElementInfo | null): AttributeOption[] => { if (!elementInfo) return []; switch (tagName.toLowerCase()) { @@ -154,8 +80,6 @@ export const BrowserWindow = () => { const [attributeOptions, setAttributeOptions] = useState([]); const [selectedElement, setSelectedElement] = useState<{ selector: string, info: ElementInfo | null } | null>(null); const [currentListId, setCurrentListId] = useState(null); - const [viewportInfo, setViewportInfo] = useState({ width: browserWidth, height: browserHeight }); - const [isLoading, setIsLoading] = useState(false); const [cachedChildSelectors, setCachedChildSelectors] = useState([]); const [processingGroupCoordinates, setProcessingGroupCoordinates] = useState>([]); const [listSelector, setListSelector] = useState(null); @@ -177,7 +101,7 @@ export const BrowserWindow = () => { const [manuallyAddedFieldIds, setManuallyAddedFieldIds] = useState>(new Set()); const { socket } = useSocketStore(); - const { notify, currentTextActionId, currentListActionId, updateDOMMode, isDOMMode, currentSnapshot } = useGlobalInfoStore(); + const { notify, currentTextActionId, currentListActionId, updateDOMMode, isDOMMode } = useGlobalInfoStore(); const { getText, getList, paginationMode, paginationType, limitMode, captureStage } = useActionContext(); const { addTextStep, addListStep, browserSteps } = useBrowserSteps(); @@ -258,27 +182,11 @@ export const BrowserWindow = () => { [] ); - const rrwebSnapshotHandler = useCallback( - (data: RRWebDOMCastData) => { - if (!data.userId || data.userId === user?.id) { - if (data.snapshotData && data.snapshotData.snapshot) { - updateDOMMode(true, data.snapshotData); - socket?.emit("dom-mode-enabled"); - setIsLoading(false); - } else { - setIsLoading(false); - } - } - }, - [user?.id, socket, updateDOMMode] - ); - const domModeHandler = useCallback( (data: any) => { if (!data.userId || data.userId === user?.id) { updateDOMMode(true); socket?.emit("dom-mode-enabled"); - setIsLoading(false); } }, [user?.id, socket, updateDOMMode] @@ -288,18 +196,21 @@ export const BrowserWindow = () => { (data: any) => { if (!data.userId || data.userId === user?.id) { updateDOMMode(false); - setIsLoading(false); + + if (data.error) { + notify("error", data.error); + } } }, - [user?.id, updateDOMMode] + [user?.id, updateDOMMode, notify] ); useEffect(() => { - if (isDOMMode) { + if (isDOMMode) { clientSelectorGenerator.setGetList(getList); clientSelectorGenerator.setListSelector(listSelector || ""); clientSelectorGenerator.setPaginationMode(paginationMode); - } + } }, [isDOMMode, getList, listSelector, paginationMode]); const createFieldsFromChildSelectors = useCallback( @@ -698,7 +609,7 @@ export const BrowserWindow = () => { return finalFields; }, - [currentSnapshot] + [] ); const removeParentChildDuplicates = ( @@ -798,7 +709,7 @@ export const BrowserWindow = () => { clientSelectorGenerator.setListSelector(listSelector); - if (currentSnapshot && cachedListSelector !== listSelector) { + if (cachedListSelector !== listSelector) { setCachedChildSelectors([]); setIsCachingChildSelectors(true); setCachedListSelector(listSelector); @@ -878,7 +789,6 @@ export const BrowserWindow = () => { listSelector, socket, getList, - currentSnapshot, cachedListSelector, pendingNotification, notify, @@ -989,10 +899,6 @@ export const BrowserWindow = () => { } }, [browserSteps, getText, getList, listSelector, currentTextActionId, currentListActionId, isDOMMode, manuallyAddedFieldIds]); - useEffect(() => { - coordinateMapper.updateDimensions(dimensions.width, dimensions.height, viewportInfo.width, viewportInfo.height); - }, [viewportInfo, dimensions.width, dimensions.height]); - useEffect(() => { if (listSelector) { sessionStorage.setItem('recordingListSelector', listSelector); @@ -1027,21 +933,18 @@ export const BrowserWindow = () => { useEffect(() => { if (socket) { - socket.on("domcast", rrwebSnapshotHandler); socket.on("dom-mode-enabled", domModeHandler); socket.on("dom-mode-error", domModeErrorHandler); } return () => { if (socket) { - socket.off("domcast", rrwebSnapshotHandler); socket.off("dom-mode-enabled", domModeHandler); socket.off("dom-mode-error", domModeErrorHandler); } }; }, [ socket, - rrwebSnapshotHandler, domModeHandler, domModeErrorHandler, ]); @@ -1066,14 +969,15 @@ export const BrowserWindow = () => { isDOMMode?: boolean; }) => { if (paginationMode && paginationSelector) { - return; + return; } + if (!getText && !getList) { setHighlighterData(null); return; } - if (!isDOMMode || !currentSnapshot) { + if (!isDOMMode) { return; } @@ -1187,7 +1091,6 @@ export const BrowserWindow = () => { }, [ isDOMMode, - currentSnapshot, getText, getList, socket, @@ -1200,105 +1103,6 @@ export const BrowserWindow = () => { ] ); - const highlighterHandler = useCallback((data: { rect: DOMRect, selector: string, elementInfo: ElementInfo | null, childSelectors?: string[], isDOMMode?: boolean; }) => { - if (paginationMode && paginationSelector) { - return; - } - if (isDOMMode || data.isDOMMode) { - domHighlighterHandler(data); - return; - } - - const now = performance.now(); - if (now - highlighterUpdateRef.current < 16) { - return; - } - highlighterUpdateRef.current = now; - - const mappedRect = new DOMRect( - data.rect.x, - data.rect.y, - data.rect.width, - data.rect.height - ); - - const mappedData = { - ...data, - rect: mappedRect - }; - - if (getList === true) { - if (listSelector) { - socket?.emit('listSelector', { selector: listSelector }); - const hasValidChildSelectors = Array.isArray(mappedData.childSelectors) && mappedData.childSelectors.length > 0; - - if (limitMode) { - setHighlighterData(null); - } else if (paginationMode) { - if (paginationType !== '' && !['none', 'scrollDown', 'scrollUp'].includes(paginationType)) { - setHighlighterData(mappedData); - } else { - setHighlighterData(null); - } - } else if (mappedData.childSelectors && mappedData.childSelectors.includes(mappedData.selector)) { - setHighlighterData(mappedData); - } else if (mappedData.elementInfo?.isIframeContent && mappedData.childSelectors) { - const isIframeChild = mappedData.childSelectors.some(childSelector => - mappedData.selector.includes(':>>') && - childSelector.split(':>>').some(part => - mappedData.selector.includes(part.trim()) - ) - ); - setHighlighterData(isIframeChild ? mappedData : null); - } else if (mappedData.selector.includes(':>>') && hasValidChildSelectors) { - const selectorParts = mappedData.selector.split(':>>').map(part => part.trim()); - const isValidMixedSelector = selectorParts.some(part => - mappedData.childSelectors!.some(childSelector => - childSelector.includes(part) - ) - ); - setHighlighterData(isValidMixedSelector ? mappedData : null); - } else if (mappedData.elementInfo?.isShadowRoot && mappedData.childSelectors) { - const isShadowChild = mappedData.childSelectors.some(childSelector => - mappedData.selector.includes('>>') && - childSelector.split('>>').some(part => - mappedData.selector.includes(part.trim()) - ) - ); - setHighlighterData(isShadowChild ? mappedData : null); - } else if (mappedData.selector.includes('>>') && hasValidChildSelectors) { - const selectorParts = mappedData.selector.split('>>').map(part => part.trim()); - const isValidMixedSelector = selectorParts.some(part => - mappedData.childSelectors!.some(childSelector => - childSelector.includes(part) - ) - ); - setHighlighterData(isValidMixedSelector ? mappedData : null); - } else { - setHighlighterData(null); - } - } else { - setHighlighterData(mappedData); - } - } else { - setHighlighterData(mappedData); - } - }, [getList, socket, listSelector, paginationMode, paginationType, limitMode]); - - useEffect(() => { - document.addEventListener("mousemove", onMouseMove, false); - if (socket) { - socket.off("highlighter", highlighterHandler); - socket.on("highlighter", highlighterHandler); - } - return () => { - document.removeEventListener("mousemove", onMouseMove); - if (socket) { - socket.off("highlighter", highlighterHandler); - } - }; - }, [socket, highlighterHandler, getList, listSelector]); - useEffect(() => { if (socket && listSelector) { socket.emit('setGetList', { getList: true }); @@ -1533,7 +1337,6 @@ export const BrowserWindow = () => { ] ); - const handleClick = (e: React.MouseEvent) => { if (highlighterData) { const shouldProcessClick = true; @@ -1720,49 +1523,50 @@ export const BrowserWindow = () => { default: data = selectedElement.info?.innerText || ''; } - { - if (getText === true) { - addTextStep('', data, { + + if (getText === true) { + addTextStep('', data, { + selector: selectedElement.selector, + tag: selectedElement.info?.tagName, + isShadow: highlighterData?.isShadow || selectedElement.info?.isShadowRoot, + attribute: attribute + }, currentTextActionId || `text-${crypto.randomUUID()}`); + } + + if (getList === true && listSelector && currentListId) { + const newField: TextStep = { + id: Date.now(), + type: 'text', + label: `Label ${Object.keys(fields).length + 1}`, + data: data, + selectorObj: { selector: selectedElement.selector, tag: selectedElement.info?.tagName, - isShadow: highlighterData?.isShadow || selectedElement.info?.isShadowRoot, + isShadow: highlighterData?.isShadow || highlighterData?.elementInfo?.isShadowRoot, attribute: attribute - }, currentTextActionId || `text-${crypto.randomUUID()}`); - } - if (getList === true && listSelector && currentListId) { - const newField: TextStep = { - id: Date.now(), - type: 'text', - label: `Label ${Object.keys(fields).length + 1}`, - data: data, - selectorObj: { - selector: selectedElement.selector, - tag: selectedElement.info?.tagName, - isShadow: highlighterData?.isShadow || highlighterData?.elementInfo?.isShadowRoot, - attribute: attribute - } - }; - - const updatedFields = { - ...fields, - [newField.id]: newField - }; - - setFields(updatedFields); - - if (listSelector) { - addListStep( - listSelector, - updatedFields, - currentListId, - currentListActionId || `list-${crypto.randomUUID()}`, - { type: "", selector: paginationSelector, isShadow: highlighterData?.isShadow }, - undefined, - highlighterData?.isShadow - ); } + }; + + const updatedFields = { + ...fields, + [newField.id]: newField + }; + + setFields(updatedFields); + + if (listSelector) { + addListStep( + listSelector, + updatedFields, + currentListId, + currentListActionId || `list-${crypto.randomUUID()}`, + { type: "", selector: paginationSelector, isShadow: highlighterData?.isShadow }, + undefined, + highlighterData?.isShadow + ); } } + } setShowAttributeModal(false); @@ -1817,7 +1621,6 @@ export const BrowserWindow = () => { style={{ width: browserWidth }} id="browser-window" > - {/* Attribute selection modal */} {(getText === true || getList === true) && ( { /> )} - {/* Main content area */}
- {/* Add CSS for the spinner animation */} + style={{ + position: "relative", + width: "100%", + height: dimensions.height, + overflow: "hidden", + borderRadius: "0px 0px 5px 5px", + }} + > - - - - `; - - if (!iframeDoc) { - throw new Error("Cannot access iframe document"); - } - - // Write the skeleton into the iframe - iframeDoc.open(); - iframeDoc.write(skeleton); - iframeDoc.close(); - - const mirror = createMirror(); - - try { - rebuild(snapshotData.snapshot, { - doc: iframeDoc, - mirror: mirror, - hackCss: false, - cache: { stylesWithHoverClass: new Map() }, - afterAppend: (node) => { - if (node.nodeType === Node.TEXT_NODE && node.textContent) { - const text = node.textContent.trim(); - - if ( - text.startsWith("<") && - text.includes(">") && - text.length > 50 - ) { - if (node.parentNode) { - node.parentNode.removeChild(node); - } - } - } - }, - }); - } catch (rebuildError) { - console.error("rrweb rebuild failed:", rebuildError); - throw new Error(`rrweb rebuild failed: ${rebuildError}`); - } - - setIsRendered(true); - setupIframeInteractions(iframeDoc); - } catch (error) { - console.error("Error rendering rrweb snapshot:", error); - } - }, - [setupIframeInteractions, isInCaptureMode, isCachingChildSelectors] - ); - - useEffect(() => { - if (snapshot && iframeRef.current) { - renderRRWebSnapshot(snapshot); - } - }, [snapshot]); - - useEffect(() => { - if (isRendered && iframeRef.current) { - const iframeDoc = iframeRef.current.contentDocument; - if (iframeDoc) { - setupIframeInteractions(iframeDoc); - } - } - }, [getText, getList, listSelector, isRendered, setupIframeInteractions]); - useEffect(() => { return () => { - if (iframeRef.current) { - const iframeDoc = iframeRef.current.contentDocument; - if (iframeDoc) { - const handlers = (iframeDoc as any)._domRendererHandlers; - if (handlers) { - Object.entries(handlers).forEach(([event, handler]) => { - const options: boolean | AddEventListenerOptions = ['wheel', 'touchstart', 'touchmove'].includes(event) - ? { passive: false } - : false; - iframeDoc.removeEventListener( - event, - handler as EventListener, - options - ); - }); - } - } + if (replayerRef.current) { + replayerRef.current.pause(); + replayerRef.current = null; } }; }, []); + /** + * Listen for rrweb events from backend and add to replayer + */ + useEffect(() => { + if (!socket) { + console.warn('No socket available, skipping event listener setup'); + return; + } + + const handleRRWebEvent = (event: any) => { + if (!replayerRef.current && event.type === 2) { + const container = document.getElementById('mirror-container'); + if (!container) { + console.warn('Container #mirror-container not found'); + return; + } + + const replayer = new Replayer([], { + root: container, + liveMode: true, + mouseTail: false + }); + + replayer.startLive(); + replayer.addEvent(event); + + replayerRef.current = replayer; + + setTimeout(() => { + const replayerWrapper = container.querySelector('.replayer-wrapper'); + const replayerIframe = replayerWrapper?.querySelector('iframe') as HTMLIFrameElement; + + if (replayerIframe) { + replayerIframe.style.width = '100%'; + replayerIframe.style.height = '100%'; + replayerIframe.style.border = 'none'; + replayerIframe.style.position = 'absolute'; + replayerIframe.style.top = '0'; + replayerIframe.style.left = '0'; + replayerIframe.style.backgroundColor = '#ffffff'; + replayerIframe.style.display = 'block'; + replayerIframe.style.pointerEvents = 'auto'; + + replayerIframe.id = 'dom-browser-iframe'; + + replayerIframeRef.current = replayerIframe; + + try { + const iframeDoc = replayerIframe.contentDocument; + if (iframeDoc) { + setupIframeInteractions(iframeDoc); + } + } catch (err) { + console.warn('Error accessing iframe:', err); + } + + replayer.on('fullsnapshot-rebuilded', () => { + const iframe = replayerIframeRef.current; + if (iframe && iframe.contentDocument) { + setupIframeInteractions(iframe.contentDocument); + + iframe.style.pointerEvents = 'auto'; + const wrapper = container.querySelector('.replayer-wrapper') as HTMLElement; + if(wrapper) wrapper.style.pointerEvents = 'auto'; + + setIsRendered(true); + } + }); + + } else { + console.warn('Could not find iframe in replayer-wrapper'); + } + }, 150); + } else if (replayerRef.current) { + replayerRef.current.addEvent(event); + } + }; + + socket.on('rrweb-event', handleRRWebEvent); + socket.emit('request-refresh'); + + return () => { + socket.off('rrweb-event', handleRRWebEvent); + }; + }, [socket, setupIframeInteractions]); + + useEffect(() => { + const iframe = replayerIframeRef.current; + if (iframe && iframe.contentDocument) { + setupIframeInteractions(iframe.contentDocument); + } + }, [setupIframeInteractions]); + return (
-