feat: add replayer func client side
This commit is contained in:
@@ -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<string, string>;
|
||||
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<AttributeOption[]>([]);
|
||||
const [selectedElement, setSelectedElement] = useState<{ selector: string, info: ElementInfo | null } | null>(null);
|
||||
const [currentListId, setCurrentListId] = useState<number | null>(null);
|
||||
const [viewportInfo, setViewportInfo] = useState<ViewportInfo>({ width: browserWidth, height: browserHeight });
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [cachedChildSelectors, setCachedChildSelectors] = useState<string[]>([]);
|
||||
const [processingGroupCoordinates, setProcessingGroupCoordinates] = useState<Array<{ element: HTMLElement; rect: DOMRect }>>([]);
|
||||
const [listSelector, setListSelector] = useState<string | null>(null);
|
||||
@@ -177,7 +101,7 @@ export const BrowserWindow = () => {
|
||||
const [manuallyAddedFieldIds, setManuallyAddedFieldIds] = useState<Set<number>>(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<HTMLDivElement>) => {
|
||||
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) && (
|
||||
<GenericModal
|
||||
isOpen={showAttributeModal}
|
||||
@@ -1906,17 +1709,15 @@ export const BrowserWindow = () => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Main content area */}
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
height: dimensions.height,
|
||||
overflow: "hidden",
|
||||
borderRadius: "0px 0px 5px 5px",
|
||||
}}
|
||||
>
|
||||
{/* Add CSS for the spinner animation */}
|
||||
style={{
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
height: dimensions.height,
|
||||
overflow: "hidden",
|
||||
borderRadius: "0px 0px 5px 5px",
|
||||
}}
|
||||
>
|
||||
<style>{`
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
@@ -1933,13 +1734,12 @@ export const BrowserWindow = () => {
|
||||
id="dom-highlight-overlay"
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0, // top:0; right:0; bottom:0; left:0
|
||||
overflow: "hidden", // clip everything within iframe area
|
||||
inset: 0,
|
||||
overflow: "hidden",
|
||||
pointerEvents: "none",
|
||||
zIndex: 1000,
|
||||
}}
|
||||
>
|
||||
{/* Individual element highlight (for non-group or hovered element) */}
|
||||
{((getText && !listSelector) ||
|
||||
(getList && paginationMode && !paginationSelector && paginationType !== "" &&
|
||||
!["none", "scrollDown", "scrollUp"].includes(paginationType))) && (
|
||||
@@ -1960,7 +1760,6 @@ export const BrowserWindow = () => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Grouped list element highlights */}
|
||||
{getList &&
|
||||
!listSelector &&
|
||||
currentGroupInfo?.isGroupElement &&
|
||||
@@ -2029,7 +1828,6 @@ export const BrowserWindow = () => {
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Label for similar element */}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
@@ -2054,23 +1852,22 @@ export const BrowserWindow = () => {
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{/* --- Main DOM Renderer Section --- */}
|
||||
|
||||
<div
|
||||
id="iframe-wrapper"
|
||||
style={{
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
overflow: "hidden", // key: confine everything below
|
||||
overflow: "hidden",
|
||||
borderRadius: "0px 0px 5px 5px",
|
||||
}}
|
||||
>
|
||||
{currentSnapshot ? (
|
||||
{isDOMMode ? (
|
||||
<>
|
||||
<DOMBrowserRenderer
|
||||
width={dimensions.width}
|
||||
height={dimensions.height}
|
||||
snapshot={currentSnapshot}
|
||||
getList={getList}
|
||||
getText={getText}
|
||||
listSelector={listSelector}
|
||||
@@ -2088,7 +1885,6 @@ export const BrowserWindow = () => {
|
||||
onShowDateTimePicker={handleShowDateTimePicker}
|
||||
/>
|
||||
|
||||
{/* --- Loading overlay --- */}
|
||||
{isCachingChildSelectors && (
|
||||
<>
|
||||
<div
|
||||
@@ -2209,7 +2005,6 @@ export const BrowserWindow = () => {
|
||||
|
||||
const DOMLoadingIndicator: React.FC = () => {
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [pendingRequests, setPendingRequests] = useState(0);
|
||||
const [hasStartedLoading, setHasStartedLoading] = useState(false);
|
||||
const { socket } = useSocketStore();
|
||||
const { state } = useContext(AuthContext);
|
||||
@@ -2225,15 +2020,12 @@ const DOMLoadingIndicator: React.FC = () => {
|
||||
userId: string;
|
||||
}) => {
|
||||
if (!data.userId || data.userId === user?.id) {
|
||||
// Once loading has started, never reset progress to 0
|
||||
if (!hasStartedLoading && data.progress > 0) {
|
||||
setHasStartedLoading(true);
|
||||
}
|
||||
|
||||
// Only update progress if we haven't started or if new progress is higher
|
||||
|
||||
if (!hasStartedLoading || data.progress >= progress) {
|
||||
setProgress(data.progress);
|
||||
setPendingRequests(data.pendingRequests);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -2259,7 +2051,6 @@ const DOMLoadingIndicator: React.FC = () => {
|
||||
gap: "15px",
|
||||
}}
|
||||
>
|
||||
{/* Loading text with percentage */}
|
||||
<div
|
||||
style={{
|
||||
fontSize: "18px",
|
||||
@@ -2270,7 +2061,6 @@ const DOMLoadingIndicator: React.FC = () => {
|
||||
Loading {progress}%
|
||||
</div>
|
||||
|
||||
{/* Progress bar */}
|
||||
<div
|
||||
style={{
|
||||
width: "240px",
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useSocketStore } from '../../context/socket';
|
||||
import { Coordinates } from '../recorder/Canvas';
|
||||
|
||||
interface Coordinates {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
interface DatePickerProps {
|
||||
coordinates: Coordinates;
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useSocketStore } from '../../context/socket';
|
||||
import { Coordinates } from '../recorder/Canvas';
|
||||
|
||||
interface Coordinates {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
interface DateTimeLocalPickerProps {
|
||||
coordinates: Coordinates;
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useSocketStore } from '../../context/socket';
|
||||
import { Coordinates } from '../recorder/Canvas';
|
||||
|
||||
interface Coordinates {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
interface DropdownProps {
|
||||
coordinates: Coordinates;
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useSocketStore } from '../../context/socket';
|
||||
import { Coordinates } from '../recorder/Canvas';
|
||||
|
||||
interface Coordinates {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
interface TimePickerProps {
|
||||
coordinates: Coordinates;
|
||||
|
||||
@@ -1,310 +0,0 @@
|
||||
import React, { memo, useCallback, useEffect, useRef } from 'react';
|
||||
import { useSocketStore } from '../../context/socket';
|
||||
import { useGlobalInfoStore } from "../../context/globalInfo";
|
||||
import { useActionContext } from '../../context/browserActions';
|
||||
import DatePicker from '../pickers/DatePicker';
|
||||
import Dropdown from '../pickers/Dropdown';
|
||||
import TimePicker from '../pickers/TimePicker';
|
||||
import DateTimeLocalPicker from '../pickers/DateTimeLocalPicker';
|
||||
import { coordinateMapper } from '../../helpers/coordinateMapper';
|
||||
|
||||
interface CreateRefCallback {
|
||||
(ref: React.RefObject<HTMLCanvasElement>): void;
|
||||
}
|
||||
|
||||
interface CanvasProps {
|
||||
width: number;
|
||||
height: number;
|
||||
onCreateRef: CreateRefCallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for mouse's x,y coordinates
|
||||
*/
|
||||
export interface Coordinates {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
const Canvas = ({ width, height, onCreateRef }: CanvasProps) => {
|
||||
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const contextRef = useRef<CanvasRenderingContext2D | null>(null);
|
||||
const imageDataRef = useRef<ImageData | null>(null);
|
||||
const animationFrameRef = useRef<number | null>(null);
|
||||
|
||||
const { socket } = useSocketStore();
|
||||
const { setLastAction, lastAction } = useGlobalInfoStore();
|
||||
const { getText, getList } = useActionContext();
|
||||
const getTextRef = useRef(getText);
|
||||
const getListRef = useRef(getList);
|
||||
|
||||
const MOUSE_MOVE_THROTTLE = 8;
|
||||
const lastMouseMoveTime = useRef(0);
|
||||
|
||||
const [datePickerInfo, setDatePickerInfo] = React.useState<{
|
||||
coordinates: Coordinates;
|
||||
selector: string;
|
||||
} | null>(null);
|
||||
|
||||
const [dropdownInfo, setDropdownInfo] = React.useState<{
|
||||
coordinates: Coordinates;
|
||||
selector: string;
|
||||
options: Array<{
|
||||
value: string;
|
||||
text: string;
|
||||
disabled: boolean;
|
||||
selected: boolean;
|
||||
}>;
|
||||
} | null>(null);
|
||||
|
||||
const [timePickerInfo, setTimePickerInfo] = React.useState<{
|
||||
coordinates: Coordinates;
|
||||
selector: string;
|
||||
} | null>(null);
|
||||
|
||||
const [dateTimeLocalInfo, setDateTimeLocalInfo] = React.useState<{
|
||||
coordinates: Coordinates;
|
||||
selector: string;
|
||||
} | null>(null);
|
||||
|
||||
const notifyLastAction = (action: string) => {
|
||||
if (lastAction !== action) {
|
||||
setLastAction(action);
|
||||
}
|
||||
};
|
||||
|
||||
const lastMousePosition = useRef<Coordinates>({ x: 0, y: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
if (canvasRef.current && !contextRef.current) {
|
||||
const ctx = canvasRef.current.getContext('2d', {
|
||||
alpha: false,
|
||||
desynchronized: true,
|
||||
willReadFrequently: false
|
||||
});
|
||||
|
||||
if (ctx) {
|
||||
contextRef.current = ctx;
|
||||
|
||||
imageDataRef.current = ctx.createImageData(width, height);
|
||||
}
|
||||
}
|
||||
}, [width, height]);
|
||||
|
||||
useEffect(() => {
|
||||
getTextRef.current = getText;
|
||||
getListRef.current = getList;
|
||||
}, [getText, getList]);
|
||||
|
||||
useEffect(() => {
|
||||
if (socket) {
|
||||
const handleDatePicker = (info: { coordinates: Coordinates, selector: string }) => {
|
||||
const canvasCoords = coordinateMapper.mapBrowserToCanvas(info.coordinates);
|
||||
setDatePickerInfo({ ...info, coordinates: canvasCoords });
|
||||
};
|
||||
|
||||
const handleDropdown = (info: {
|
||||
coordinates: Coordinates,
|
||||
selector: string,
|
||||
options: Array<{ value: string; text: string; disabled: boolean; selected: boolean; }>;
|
||||
}) => {
|
||||
const canvasCoords = coordinateMapper.mapBrowserToCanvas(info.coordinates);
|
||||
setDropdownInfo({ ...info, coordinates: canvasCoords });
|
||||
};
|
||||
|
||||
const handleTimePicker = (info: { coordinates: Coordinates, selector: string }) => {
|
||||
const canvasCoords = coordinateMapper.mapBrowserToCanvas(info.coordinates);
|
||||
setTimePickerInfo({ ...info, coordinates: canvasCoords });
|
||||
};
|
||||
|
||||
const handleDateTimePicker = (info: { coordinates: Coordinates, selector: string }) => {
|
||||
const canvasCoords = coordinateMapper.mapBrowserToCanvas(info.coordinates);
|
||||
setDateTimeLocalInfo({ ...info, coordinates: canvasCoords });
|
||||
};
|
||||
|
||||
socket.on('showDatePicker', handleDatePicker);
|
||||
socket.on('showDropdown', handleDropdown);
|
||||
socket.on('showTimePicker', handleTimePicker);
|
||||
socket.on('showDateTimePicker', handleDateTimePicker);
|
||||
|
||||
return () => {
|
||||
socket.off('showDatePicker', handleDatePicker);
|
||||
socket.off('showDropdown', handleDropdown);
|
||||
socket.off('showTimePicker', handleTimePicker);
|
||||
socket.off('showDateTimePicker', handleDateTimePicker);
|
||||
};
|
||||
}
|
||||
}, [socket]);
|
||||
|
||||
const onMouseEvent = useCallback((event: MouseEvent) => {
|
||||
if (!socket || !canvasRef.current) return;
|
||||
|
||||
const rect = canvasRef.current.getBoundingClientRect();
|
||||
const clickCoordinates = {
|
||||
x: event.clientX - rect.left,
|
||||
y: event.clientY - rect.top,
|
||||
};
|
||||
|
||||
const browserCoordinates = coordinateMapper.mapCanvasToBrowser(clickCoordinates);
|
||||
|
||||
switch (event.type) {
|
||||
case 'mousedown':
|
||||
if (getTextRef.current === true) {
|
||||
console.log('Capturing Text...');
|
||||
} else if (getListRef.current === true) {
|
||||
console.log('Capturing List...');
|
||||
} else {
|
||||
socket.emit('input:mousedown', browserCoordinates);
|
||||
}
|
||||
notifyLastAction('click');
|
||||
break;
|
||||
|
||||
case 'mousemove': {
|
||||
const now = performance.now();
|
||||
if (now - lastMouseMoveTime.current < MOUSE_MOVE_THROTTLE) {
|
||||
return;
|
||||
}
|
||||
lastMouseMoveTime.current = now;
|
||||
|
||||
const dx = Math.abs(lastMousePosition.current.x - clickCoordinates.x);
|
||||
const dy = Math.abs(lastMousePosition.current.y - clickCoordinates.y);
|
||||
|
||||
if (dx > 0.5 || dy > 0.5) {
|
||||
lastMousePosition.current = clickCoordinates;
|
||||
socket.emit('input:mousemove', browserCoordinates);
|
||||
notifyLastAction('move');
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'wheel': {
|
||||
const wheelEvent = event as WheelEvent;
|
||||
const deltaX = Math.round(wheelEvent.deltaX / 5) * 5;
|
||||
const deltaY = Math.round(wheelEvent.deltaY / 5) * 5;
|
||||
|
||||
if (Math.abs(deltaX) > 2 || Math.abs(deltaY) > 2) {
|
||||
socket.emit('input:wheel', { deltaX, deltaY });
|
||||
notifyLastAction('scroll');
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}, [socket, notifyLastAction]);
|
||||
|
||||
const onKeyboardEvent = useCallback((event: KeyboardEvent) => {
|
||||
if (socket) {
|
||||
const browserCoordinates = coordinateMapper.mapCanvasToBrowser(lastMousePosition.current);
|
||||
|
||||
switch (event.type) {
|
||||
case 'keydown':
|
||||
socket.emit('input:keydown', { key: event.key, coordinates: browserCoordinates });
|
||||
notifyLastAction(`${event.key} pressed`);
|
||||
break;
|
||||
case 'keyup':
|
||||
socket.emit('input:keyup', event.key);
|
||||
break;
|
||||
default:
|
||||
console.log('Default keyEvent registered');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, [socket, notifyLastAction]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
|
||||
onCreateRef(canvasRef);
|
||||
|
||||
const options = { passive: true };
|
||||
|
||||
canvas.addEventListener('mousedown', onMouseEvent, options);
|
||||
canvas.addEventListener('mousemove', onMouseEvent, options);
|
||||
canvas.addEventListener('wheel', onMouseEvent, options);
|
||||
canvas.addEventListener('keydown', onKeyboardEvent);
|
||||
canvas.addEventListener('keyup', onKeyboardEvent);
|
||||
|
||||
return () => {
|
||||
canvas.removeEventListener('mousedown', onMouseEvent);
|
||||
canvas.removeEventListener('mousemove', onMouseEvent);
|
||||
canvas.removeEventListener('wheel', onMouseEvent);
|
||||
canvas.removeEventListener('keydown', onKeyboardEvent);
|
||||
canvas.removeEventListener('keyup', onKeyboardEvent);
|
||||
};
|
||||
}, [onMouseEvent, onKeyboardEvent, onCreateRef]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (animationFrameRef.current) {
|
||||
cancelAnimationFrame(animationFrameRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const containerStyle = React.useMemo<React.CSSProperties>(() => ({
|
||||
borderRadius: '0px 0px 5px 5px',
|
||||
overflow: 'hidden',
|
||||
backgroundColor: 'white',
|
||||
contain: 'layout style paint',
|
||||
isolation: 'isolate' as React.CSSProperties['isolation']
|
||||
}), []);
|
||||
|
||||
const canvasStyle = React.useMemo(() => ({
|
||||
display: 'block',
|
||||
imageRendering: 'crisp-edges' as const,
|
||||
willChange: 'contents',
|
||||
backfaceVisibility: 'hidden' as const,
|
||||
transform: 'translateZ(0)',
|
||||
maxWidth: '100%',
|
||||
maxHeight: '100%'
|
||||
}), []);
|
||||
|
||||
return (
|
||||
<div style={containerStyle}>
|
||||
<canvas
|
||||
tabIndex={0}
|
||||
ref={canvasRef}
|
||||
height={height}
|
||||
width={width}
|
||||
style={canvasStyle}
|
||||
/>
|
||||
{datePickerInfo && (
|
||||
<DatePicker
|
||||
coordinates={datePickerInfo.coordinates}
|
||||
selector={datePickerInfo.selector}
|
||||
onClose={() => setDatePickerInfo(null)}
|
||||
/>
|
||||
)}
|
||||
{dropdownInfo && (
|
||||
<Dropdown
|
||||
coordinates={dropdownInfo.coordinates}
|
||||
selector={dropdownInfo.selector}
|
||||
options={dropdownInfo.options}
|
||||
onClose={() => setDropdownInfo(null)}
|
||||
/>
|
||||
)}
|
||||
{timePickerInfo && (
|
||||
<TimePicker
|
||||
coordinates={timePickerInfo.coordinates}
|
||||
selector={timePickerInfo.selector}
|
||||
onClose={() => setTimePickerInfo(null)}
|
||||
/>
|
||||
)}
|
||||
{dateTimeLocalInfo && (
|
||||
<DateTimeLocalPicker
|
||||
coordinates={dateTimeLocalInfo.coordinates}
|
||||
selector={dateTimeLocalInfo.selector}
|
||||
onClose={() => setDateTimeLocalInfo(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
|
||||
export default memo(Canvas);
|
||||
@@ -7,9 +7,8 @@ import React, {
|
||||
} from "react";
|
||||
import { useSocketStore } from "../../context/socket";
|
||||
import { useGlobalInfoStore } from "../../context/globalInfo";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AuthContext } from "../../context/auth";
|
||||
import { rebuild, createMirror } from "rrweb-snapshot";
|
||||
import { Replayer } from "rrweb"
|
||||
import {
|
||||
ActionType,
|
||||
clientSelectorGenerator,
|
||||
@@ -29,72 +28,9 @@ interface ElementInfo {
|
||||
isDOMMode?: boolean;
|
||||
}
|
||||
|
||||
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 RRWebSnapshot {
|
||||
type: number;
|
||||
childNodes?: RRWebSnapshot[];
|
||||
tagName?: string;
|
||||
attributes?: Record<string, string>;
|
||||
textContent: string;
|
||||
id: number;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface RRWebDOMBrowserRendererProps {
|
||||
width: number;
|
||||
height: number;
|
||||
snapshot: ProcessedSnapshot;
|
||||
getList?: boolean;
|
||||
getText?: boolean;
|
||||
listSelector?: string | null;
|
||||
@@ -148,7 +84,6 @@ interface RRWebDOMBrowserRendererProps {
|
||||
export const DOMBrowserRenderer: React.FC<RRWebDOMBrowserRendererProps> = ({
|
||||
width,
|
||||
height,
|
||||
snapshot,
|
||||
getList = false,
|
||||
getText = false,
|
||||
listSelector = null,
|
||||
@@ -165,8 +100,9 @@ export const DOMBrowserRenderer: React.FC<RRWebDOMBrowserRendererProps> = ({
|
||||
onShowTimePicker,
|
||||
onShowDateTimePicker,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const replayerIframeRef = useRef<HTMLIFrameElement | null>(null);
|
||||
const replayerRef = useRef<any>(null);
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
const [isRendered, setIsRendered] = useState(false);
|
||||
const [lastMousePosition, setLastMousePosition] = useState({ x: 0, y: 0 });
|
||||
@@ -184,7 +120,7 @@ export const DOMBrowserRenderer: React.FC<RRWebDOMBrowserRendererProps> = ({
|
||||
const { state } = useContext(AuthContext);
|
||||
const { user } = state;
|
||||
|
||||
const MOUSE_MOVE_THROTTLE = 16; // ~60fps
|
||||
const MOUSE_MOVE_THROTTLE = 16;
|
||||
const lastMouseMoveTime = useRef(0);
|
||||
|
||||
const notifyLastAction = (action: string) => {
|
||||
@@ -313,7 +249,7 @@ export const DOMBrowserRenderer: React.FC<RRWebDOMBrowserRendererProps> = ({
|
||||
isShadow,
|
||||
childSelectors,
|
||||
groupInfo,
|
||||
similarElements, // Pass similar elements data
|
||||
similarElements,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -337,12 +273,14 @@ export const DOMBrowserRenderer: React.FC<RRWebDOMBrowserRendererProps> = ({
|
||||
getList,
|
||||
listSelector,
|
||||
paginationMode,
|
||||
paginationSelector,
|
||||
cachedChildSelectors,
|
||||
paginationType,
|
||||
limitMode,
|
||||
onHighlight,
|
||||
]
|
||||
);
|
||||
|
||||
/**
|
||||
* Set up enhanced interaction handlers for DOM mode
|
||||
*/
|
||||
@@ -379,7 +317,7 @@ export const DOMBrowserRenderer: React.FC<RRWebDOMBrowserRendererProps> = ({
|
||||
const iframeX = mouseEvent.clientX;
|
||||
const iframeY = mouseEvent.clientY;
|
||||
|
||||
const iframe = iframeRef.current;
|
||||
const iframe = replayerIframeRef.current;
|
||||
if (iframe) {
|
||||
const iframeRect = iframe.getBoundingClientRect();
|
||||
setLastMousePosition({
|
||||
@@ -407,7 +345,6 @@ export const DOMBrowserRenderer: React.FC<RRWebDOMBrowserRendererProps> = ({
|
||||
e.stopPropagation();
|
||||
|
||||
if (currentHighlight && onElementSelect) {
|
||||
// Get the group info for the current highlight
|
||||
const highlighterData =
|
||||
clientSelectorGenerator.generateDataForHighlighter(
|
||||
{ x: iframeX, y: iframeY },
|
||||
@@ -438,6 +375,7 @@ export const DOMBrowserRenderer: React.FC<RRWebDOMBrowserRendererProps> = ({
|
||||
e.stopPropagation();
|
||||
|
||||
const href = linkElement.href;
|
||||
const originalTarget = linkElement.target;
|
||||
|
||||
if (linkElement.target) {
|
||||
linkElement.target = "";
|
||||
@@ -447,12 +385,17 @@ export const DOMBrowserRenderer: React.FC<RRWebDOMBrowserRendererProps> = ({
|
||||
linkElement.removeAttribute("href");
|
||||
|
||||
setTimeout(() => {
|
||||
linkElement.setAttribute("href", originalHref);
|
||||
try {
|
||||
linkElement.setAttribute("href", originalHref);
|
||||
if (originalTarget) {
|
||||
linkElement.setAttribute("target", originalTarget);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Could not restore link attributes:", error);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
const isSPALink =
|
||||
href.endsWith("#") ||
|
||||
(href.includes("#") && new URL(href).hash !== "");
|
||||
const isSPALink = href.startsWith('#');
|
||||
|
||||
const selector = clientSelectorGenerator.generateSelector(
|
||||
iframeDoc,
|
||||
@@ -470,7 +413,6 @@ export const DOMBrowserRenderer: React.FC<RRWebDOMBrowserRendererProps> = ({
|
||||
if (selector && socket) {
|
||||
socket.emit("dom:click", {
|
||||
selector,
|
||||
url: snapshot.baseUrl,
|
||||
userId: user?.id || "unknown",
|
||||
elementInfo,
|
||||
coordinates: undefined,
|
||||
@@ -592,17 +534,14 @@ export const DOMBrowserRenderer: React.FC<RRWebDOMBrowserRendererProps> = ({
|
||||
|
||||
socket.emit("dom:click", {
|
||||
selector,
|
||||
url: snapshot.baseUrl,
|
||||
userId: user?.id || "unknown",
|
||||
elementInfo,
|
||||
coordinates: { x: relativeX, y: relativeY },
|
||||
isSPA: false,
|
||||
});
|
||||
} else if (elementInfo?.tagName !== "SELECT") {
|
||||
// Handle other elements normally
|
||||
socket.emit("dom:click", {
|
||||
selector,
|
||||
url: snapshot.baseUrl,
|
||||
userId: user?.id || "unknown",
|
||||
elementInfo,
|
||||
coordinates: { x: iframeX, y: iframeY },
|
||||
@@ -632,21 +571,19 @@ export const DOMBrowserRenderer: React.FC<RRWebDOMBrowserRendererProps> = ({
|
||||
const keyboardEvent = e as KeyboardEvent;
|
||||
const target = keyboardEvent.target as HTMLElement;
|
||||
|
||||
if (!isInCaptureMode && socket && snapshot?.baseUrl) {
|
||||
const iframe = iframeRef.current;
|
||||
if (!isInCaptureMode && socket) {
|
||||
const iframe = replayerIframeRef.current;
|
||||
if (iframe) {
|
||||
const focusedElement = iframeDoc.activeElement as HTMLElement;
|
||||
let coordinates = { x: 0, y: 0 };
|
||||
|
||||
if (focusedElement && focusedElement !== iframeDoc.body) {
|
||||
// Get coordinates from the focused element
|
||||
const rect = focusedElement.getBoundingClientRect();
|
||||
coordinates = {
|
||||
x: rect.left + rect.width / 2,
|
||||
y: rect.top + rect.height / 2
|
||||
};
|
||||
} else {
|
||||
// Fallback to last mouse position if no focused element
|
||||
const iframeRect = iframe.getBoundingClientRect();
|
||||
coordinates = {
|
||||
x: lastMousePosition.x - iframeRect.left,
|
||||
@@ -671,7 +608,6 @@ export const DOMBrowserRenderer: React.FC<RRWebDOMBrowserRendererProps> = ({
|
||||
socket.emit("dom:keypress", {
|
||||
selector,
|
||||
key: keyboardEvent.key,
|
||||
url: snapshot.baseUrl,
|
||||
userId: user?.id || "unknown",
|
||||
inputType: elementInfo?.attributes?.type || "text",
|
||||
});
|
||||
@@ -768,12 +704,11 @@ export const DOMBrowserRenderer: React.FC<RRWebDOMBrowserRendererProps> = ({
|
||||
iframeDoc.addEventListener(event, handler, options);
|
||||
});
|
||||
|
||||
// Store handlers for cleanup
|
||||
(iframeDoc as any)._domRendererHandlers = handlers;
|
||||
|
||||
// Make iframe focusable for keyboard events
|
||||
if (iframeRef.current) {
|
||||
iframeRef.current.tabIndex = 0;
|
||||
const iframe = replayerIframeRef.current;
|
||||
if (iframe) {
|
||||
iframe.tabIndex = 0;
|
||||
}
|
||||
},
|
||||
[
|
||||
@@ -784,287 +719,219 @@ export const DOMBrowserRenderer: React.FC<RRWebDOMBrowserRendererProps> = ({
|
||||
currentHighlight,
|
||||
onElementSelect,
|
||||
isInCaptureMode,
|
||||
snapshot,
|
||||
user?.id,
|
||||
onShowDatePicker,
|
||||
onShowDropdown,
|
||||
onShowTimePicker,
|
||||
onShowDateTimePicker,
|
||||
cachedChildSelectors
|
||||
]
|
||||
);
|
||||
|
||||
/**
|
||||
* Render DOM snapshot using rrweb
|
||||
* Cleanup replayer on unmount
|
||||
*/
|
||||
const renderRRWebSnapshot = useCallback(
|
||||
(snapshotData: ProcessedSnapshot) => {
|
||||
if (!iframeRef.current) {
|
||||
console.warn("No iframe reference available");
|
||||
return;
|
||||
}
|
||||
|
||||
if (isInCaptureMode || isCachingChildSelectors) {
|
||||
return; // Skip rendering in capture mode
|
||||
}
|
||||
|
||||
try {
|
||||
setIsRendered(false);
|
||||
|
||||
const iframe = iframeRef.current!;
|
||||
let iframeDoc: Document;
|
||||
|
||||
try {
|
||||
iframeDoc = iframe.contentDocument!;
|
||||
if (!iframeDoc) {
|
||||
throw new Error("Cannot access iframe document");
|
||||
}
|
||||
} catch (crossOriginError) {
|
||||
console.warn("Cross-origin iframe access blocked, recreating iframe");
|
||||
|
||||
const newIframe = document.createElement('iframe');
|
||||
newIframe.style.cssText = iframe.style.cssText;
|
||||
newIframe.sandbox = iframe.sandbox.value;
|
||||
newIframe.title = iframe.title;
|
||||
newIframe.tabIndex = iframe.tabIndex;
|
||||
newIframe.id = iframe.id;
|
||||
|
||||
iframe.parentNode?.replaceChild(newIframe, iframe);
|
||||
Object.defineProperty(iframeRef, 'current', {
|
||||
value: newIframe,
|
||||
writable: false,
|
||||
enumerable: true,
|
||||
configurable: true
|
||||
});
|
||||
|
||||
iframeDoc = newIframe.contentDocument!;
|
||||
if (!iframeDoc) {
|
||||
throw new Error("Cannot access new iframe document");
|
||||
}
|
||||
}
|
||||
|
||||
const styleTags = Array.from(
|
||||
document.querySelectorAll('link[rel="stylesheet"], style')
|
||||
)
|
||||
.map((tag) => tag.outerHTML)
|
||||
.join("\n");
|
||||
|
||||
const enhancedCSS = `
|
||||
/* rrweb rebuilt content styles */
|
||||
html, body {
|
||||
margin: 0 !important;
|
||||
padding: 8px !important;
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
||||
html::-webkit-scrollbar,
|
||||
body::-webkit-scrollbar {
|
||||
display: none !important;
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* Hide scrollbars for all elements */
|
||||
*::-webkit-scrollbar {
|
||||
display: none !important;
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
* {
|
||||
scrollbar-width: none !important; /* Firefox */
|
||||
-ms-overflow-style: none !important; /* Internet Explorer 10+ */
|
||||
}
|
||||
|
||||
/* Make everything interactive */
|
||||
* {
|
||||
cursor: "pointer" !important;
|
||||
}
|
||||
`;
|
||||
|
||||
const skeleton = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<base href="${snapshotData.baseUrl}">
|
||||
${styleTags}
|
||||
<style>${enhancedCSS}</style>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
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 (
|
||||
<div
|
||||
id="mirror-container"
|
||||
ref={containerRef}
|
||||
style={{
|
||||
width: width,
|
||||
height: height,
|
||||
overflow: "hidden !important",
|
||||
position: "relative",
|
||||
borderRadius: "0px 0px 5px 5px",
|
||||
backgroundColor: "white",
|
||||
backgroundColor: "#ffffff",
|
||||
overflow: "hidden",
|
||||
isolation: "isolate",
|
||||
}}
|
||||
>
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
id="dom-browser-iframe"
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
border: "none",
|
||||
display: "block",
|
||||
overflow: isCachingChildSelectors ? "hidden !important" : "hidden !important",
|
||||
pointerEvents: isCachingChildSelectors ? "none" : "auto",
|
||||
}}
|
||||
sandbox="allow-same-origin allow-forms allow-scripts"
|
||||
title="DOM Browser Content"
|
||||
tabIndex={0}
|
||||
/>
|
||||
|
||||
{/* Loading indicator */}
|
||||
{!isRendered && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: "rgba(255, 255, 255, 0.9)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontSize: "18px",
|
||||
color: "#666",
|
||||
flexDirection: "column",
|
||||
gap: "10px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: "40px",
|
||||
height: "40px",
|
||||
border: "3px solid #ff00c3",
|
||||
borderTop: "3px solid transparent",
|
||||
borderRadius: "50%",
|
||||
animation: "spin 1s linear infinite",
|
||||
}}
|
||||
/>
|
||||
<div>Loading website...</div>
|
||||
<style>{`
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Capture mode overlay */}
|
||||
{isInCaptureMode && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
cursor: "pointer",
|
||||
pointerEvents: "none",
|
||||
zIndex: 999,
|
||||
borderRadius: "0px 0px 5px 5px",
|
||||
}}
|
||||
/>
|
||||
<DOMLoadingIndicator />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const DOMLoadingIndicator: React.FC = () => {
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [hasStartedLoading, setHasStartedLoading] = useState(false);
|
||||
const { socket } = useSocketStore();
|
||||
const { state } = useContext(AuthContext);
|
||||
const { user } = state;
|
||||
|
||||
useEffect(() => {
|
||||
if (!socket) return;
|
||||
|
||||
const handleLoadingProgress = (data: {
|
||||
progress: number;
|
||||
pendingRequests: number;
|
||||
userId: string;
|
||||
}) => {
|
||||
if (!data.userId || data.userId === user?.id) {
|
||||
if (!hasStartedLoading && data.progress > 0) {
|
||||
setHasStartedLoading(true);
|
||||
}
|
||||
|
||||
if (!hasStartedLoading || data.progress >= progress) {
|
||||
setProgress(data.progress);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
socket.on("domLoadingProgress", handleLoadingProgress);
|
||||
|
||||
return () => {
|
||||
socket.off("domLoadingProgress", handleLoadingProgress);
|
||||
};
|
||||
}, [socket, user?.id, hasStartedLoading, progress]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: "#f5f5f5",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexDirection: "column",
|
||||
gap: "15px",
|
||||
zIndex: 9999,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "18px",
|
||||
fontWeight: "500",
|
||||
color: "#333",
|
||||
}}
|
||||
>
|
||||
Loading {progress}%
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
width: "240px",
|
||||
height: "6px",
|
||||
background: "#e0e0e0",
|
||||
borderRadius: "3px",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: `${progress}%`,
|
||||
height: "100%",
|
||||
background: "linear-gradient(90deg, #ff00c3, #ff66d9)",
|
||||
borderRadius: "3px",
|
||||
transition: "width 0.3s ease-out",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import styled from "styled-components";
|
||||
import { coordinateMapper } from '../../helpers/coordinateMapper';
|
||||
|
||||
interface HighlighterProps {
|
||||
unmodifiedRect: DOMRect;
|
||||
displayedSelector: string;
|
||||
width: number;
|
||||
height: number;
|
||||
canvasRect: DOMRect;
|
||||
};
|
||||
|
||||
const HighlighterComponent = ({ unmodifiedRect, displayedSelector = '', width, height, canvasRect }: HighlighterProps) => {
|
||||
if (!unmodifiedRect) {
|
||||
return null;
|
||||
} else {
|
||||
const rect = useMemo(() => {
|
||||
const mappedRect = coordinateMapper.mapBrowserRectToCanvas(unmodifiedRect);
|
||||
return {
|
||||
top: mappedRect.top + canvasRect.top + window.scrollY,
|
||||
left: mappedRect.left + canvasRect.left + window.scrollX,
|
||||
width: mappedRect.width,
|
||||
height: mappedRect.height,
|
||||
};
|
||||
}, [unmodifiedRect, canvasRect.top, canvasRect.left]);
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<HighlighterOutline
|
||||
id="Highlighter-outline"
|
||||
top={rect.top}
|
||||
left={rect.left}
|
||||
width={rect.width}
|
||||
height={rect.height}
|
||||
/>
|
||||
{/* <HighlighterLabel
|
||||
id="Highlighter-label"
|
||||
top={rect.top + rect.height + 8}
|
||||
left={rect.left}
|
||||
>
|
||||
{displayedSelector}
|
||||
</HighlighterLabel> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const Highlighter = React.memo(HighlighterComponent);
|
||||
|
||||
const HighlighterOutline = styled.div<HighlighterOutlineProps>`
|
||||
box-sizing: border-box;
|
||||
pointer-events: none !important;
|
||||
position: fixed !important;
|
||||
background: #ff5d5b26 !important;
|
||||
outline: 2px solid #ff00c3 !important;
|
||||
z-index: 2147483647 !important;
|
||||
top: ${(p: HighlighterOutlineProps) => p.top}px;
|
||||
left: ${(p: HighlighterOutlineProps) => p.left}px;
|
||||
width: ${(p: HighlighterOutlineProps) => p.width}px;
|
||||
height: ${(p: HighlighterOutlineProps) => p.height}px;
|
||||
`;
|
||||
|
||||
const HighlighterLabel = styled.div<HighlighterLabelProps>`
|
||||
pointer-events: none !important;
|
||||
position: fixed !important;
|
||||
background: #080a0b !important;
|
||||
color: white !important;
|
||||
padding: 8px !important;
|
||||
font-family: monospace !important;
|
||||
border-radius: 5px !important;
|
||||
z-index: 2147483647 !important;
|
||||
top: ${(p: HighlighterLabelProps) => p.top}px;
|
||||
left: ${(p: HighlighterLabelProps) => p.left}px;
|
||||
`;
|
||||
|
||||
interface HighlighterLabelProps {
|
||||
top: number;
|
||||
left: number;
|
||||
}
|
||||
|
||||
interface HighlighterOutlineProps {
|
||||
top: number;
|
||||
left: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
@@ -36,10 +36,6 @@ interface RightSidePanelProps {
|
||||
}
|
||||
|
||||
export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture }) => {
|
||||
const [textLabels, setTextLabels] = useState<{ [id: string]: string }>({});
|
||||
const [errors, setErrors] = useState<{ [id: string]: string }>({});
|
||||
const [confirmedTextSteps, setConfirmedTextSteps] = useState<{ [id: string]: boolean }>({});
|
||||
const [confirmedListTextFields, setConfirmedListTextFields] = useState<{ [listId: string]: { [fieldKey: string]: boolean } }>({});
|
||||
const [showCaptureList, setShowCaptureList] = useState(true);
|
||||
const [showCaptureScreenshot, setShowCaptureScreenshot] = useState(true);
|
||||
const [showCaptureText, setShowCaptureText] = useState(true);
|
||||
@@ -52,7 +48,7 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
} | null>(null);
|
||||
const autoDetectionRunRef = useRef<string | null>(null);
|
||||
|
||||
const { lastAction, notify, currentWorkflowActionsState, setCurrentWorkflowActionsState, resetInterpretationLog, currentListActionId, setCurrentListActionId, currentTextActionId, setCurrentTextActionId, currentScreenshotActionId, setCurrentScreenshotActionId, isDOMMode, setIsDOMMode, currentSnapshot, setCurrentSnapshot, updateDOMMode, initialUrl, setRecordingUrl, currentTextGroupName } = useGlobalInfoStore();
|
||||
const { notify, currentWorkflowActionsState, setCurrentWorkflowActionsState, resetInterpretationLog, currentListActionId, setCurrentListActionId, currentTextActionId, setCurrentTextActionId, currentScreenshotActionId, setCurrentScreenshotActionId, isDOMMode, updateDOMMode, currentTextGroupName } = useGlobalInfoStore();
|
||||
const {
|
||||
getText, startGetText, stopGetText,
|
||||
getList, startGetList, stopGetList,
|
||||
@@ -65,8 +61,7 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
showPaginationOptions, setShowPaginationOptions,
|
||||
showLimitOptions, setShowLimitOptions,
|
||||
workflow, setWorkflow,
|
||||
activeAction, setActiveAction,
|
||||
startAction, finishAction
|
||||
activeAction, setActiveAction, finishAction
|
||||
} = useActionContext();
|
||||
|
||||
const { browserSteps, addScreenshotStep, updateListStepLimit, updateListStepPagination, deleteStepsByActionId, updateListStepData, updateScreenshotStepData, emitActionForStep } = useBrowserSteps();
|
||||
@@ -154,20 +149,10 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
}
|
||||
};
|
||||
|
||||
const domcastHandler = (data: any) => {
|
||||
if (!data.userId || data.userId === id) {
|
||||
if (data.snapshotData && data.snapshotData.snapshot) {
|
||||
updateDOMMode(true, data.snapshotData);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
socket.on("dom-mode-enabled", domModeHandler);
|
||||
socket.on("domcast", domcastHandler);
|
||||
|
||||
return () => {
|
||||
socket.off("dom-mode-enabled", domModeHandler);
|
||||
socket.off("domcast", domcastHandler);
|
||||
};
|
||||
}
|
||||
}, [socket, id, updateDOMMode]);
|
||||
@@ -176,11 +161,9 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
if (socket) {
|
||||
socket.on("workflow", workflowHandler);
|
||||
}
|
||||
// fetch the workflow every time the id changes
|
||||
if (id) {
|
||||
fetchWorkflow(id, workflowHandler);
|
||||
}
|
||||
// fetch workflow in 15min intervals
|
||||
let interval = setInterval(() => {
|
||||
if (id) {
|
||||
fetchWorkflow(id, workflowHandler);
|
||||
@@ -267,7 +250,7 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
fields: Record<string, any>,
|
||||
currentListId: number
|
||||
) => {
|
||||
if (isDOMMode && currentSnapshot) {
|
||||
if (isDOMMode) {
|
||||
try {
|
||||
let iframeElement = document.querySelector(
|
||||
"#dom-browser-iframe"
|
||||
@@ -318,25 +301,9 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
||||
console.error("Error in client-side data extraction:", error);
|
||||
notify("error", "Failed to extract data client-side");
|
||||
}
|
||||
} else {
|
||||
if (!socket) {
|
||||
console.error("Socket not available for backend extraction");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
socket.emit("extractListData", {
|
||||
listSelector,
|
||||
fields,
|
||||
currentListId,
|
||||
pagination: { type: "", selector: "" },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error in backend data extraction:", error);
|
||||
}
|
||||
}
|
||||
},
|
||||
[isDOMMode, currentSnapshot, updateListStepData, socket, notify, currentWorkflowActionsState]
|
||||
[isDOMMode, updateListStepData, socket, notify, currentWorkflowActionsState]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { createContext, useContext, useState } from "react";
|
||||
import { createContext, useContext, useState } from "react";
|
||||
import { AlertSnackbarProps } from "../components/ui/AlertSnackbar";
|
||||
import { WhereWhatPair } from "maxun-core";
|
||||
import { QueryClient, QueryClientProvider, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
@@ -49,41 +49,6 @@ interface ScheduleConfig {
|
||||
cronExpression?: string;
|
||||
}
|
||||
|
||||
interface ProcessedSnapshot {
|
||||
snapshot: any;
|
||||
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: any;
|
||||
}
|
||||
|
||||
export interface RobotSettings {
|
||||
id: string;
|
||||
userId?: number;
|
||||
@@ -147,9 +112,7 @@ interface GlobalInfo {
|
||||
setCurrentTextGroupName: (name: string) => void;
|
||||
isDOMMode: boolean;
|
||||
setIsDOMMode: (isDOMMode: boolean) => void;
|
||||
currentSnapshot: ProcessedSnapshot | null;
|
||||
setCurrentSnapshot: (snapshot: ProcessedSnapshot | null) => void;
|
||||
updateDOMMode: (isDOMMode: boolean, snapshot?: ProcessedSnapshot | null) => void;
|
||||
updateDOMMode: (isDOMMode: boolean) => void;
|
||||
};
|
||||
|
||||
class GlobalInfoStore implements Partial<GlobalInfo> {
|
||||
@@ -181,7 +144,6 @@ class GlobalInfoStore implements Partial<GlobalInfo> {
|
||||
currentScreenshotActionId = '';
|
||||
currentTextGroupName = 'Text Data';
|
||||
isDOMMode = false;
|
||||
currentSnapshot = null;
|
||||
};
|
||||
|
||||
const globalInfoStore = new GlobalInfoStore();
|
||||
@@ -272,8 +234,7 @@ export const GlobalInfoProvider = ({ children }: { children: JSX.Element }) => {
|
||||
const [rerenderRuns, setRerenderRuns] = useState<boolean>(globalInfoStore.rerenderRuns);
|
||||
const [rerenderRobots, setRerenderRobots] = useState<boolean>(globalInfoStore.rerenderRobots);
|
||||
const [recordingLength, setRecordingLength] = useState<number>(globalInfoStore.recordingLength);
|
||||
// const [recordingId, setRecordingId] = useState<string | null>(globalInfoStore.recordingId);
|
||||
const [recordingId, setRecordingId] = useState<string | null>(() => {
|
||||
const [recordingId, setRecordingId] = useState<string | null>(() => {
|
||||
try {
|
||||
const stored = sessionStorage.getItem('recordingId');
|
||||
return stored ? JSON.parse(stored) : globalInfoStore.recordingId;
|
||||
@@ -282,7 +243,6 @@ export const GlobalInfoProvider = ({ children }: { children: JSX.Element }) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Create a wrapped setter that persists to sessionStorage
|
||||
const setPersistedRecordingId = (newRecordingId: string | null) => {
|
||||
setRecordingId(newRecordingId);
|
||||
try {
|
||||
@@ -307,7 +267,6 @@ export const GlobalInfoProvider = ({ children }: { children: JSX.Element }) => {
|
||||
const [currentScreenshotActionId, setCurrentScreenshotActionId] = useState<string>('');
|
||||
const [currentTextGroupName, setCurrentTextGroupName] = useState<string>('Text Data');
|
||||
const [isDOMMode, setIsDOMMode] = useState<boolean>(globalInfoStore.isDOMMode);
|
||||
const [currentSnapshot, setCurrentSnapshot] = useState<ProcessedSnapshot | null>(globalInfoStore.currentSnapshot);
|
||||
|
||||
const notify = (severity: 'error' | 'warning' | 'info' | 'success', message: string) => {
|
||||
setNotification({ severity, message, isOpen: true });
|
||||
@@ -326,22 +285,13 @@ export const GlobalInfoProvider = ({ children }: { children: JSX.Element }) => {
|
||||
|
||||
const resetInterpretationLog = () => {
|
||||
setShouldResetInterpretationLog(true);
|
||||
// Reset the flag after a short delay to allow components to respond
|
||||
setTimeout(() => {
|
||||
setShouldResetInterpretationLog(false);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
const updateDOMMode = (mode: boolean, snapshot?: ProcessedSnapshot | null) => {
|
||||
const updateDOMMode = (mode: boolean) => {
|
||||
setIsDOMMode(mode);
|
||||
|
||||
if (snapshot !== undefined) {
|
||||
setCurrentSnapshot(snapshot);
|
||||
}
|
||||
|
||||
if (!mode) {
|
||||
setCurrentSnapshot(null);
|
||||
}
|
||||
}
|
||||
|
||||
const [dataCacheClient] = useState(() => createDataCacheClient());
|
||||
@@ -391,8 +341,6 @@ export const GlobalInfoProvider = ({ children }: { children: JSX.Element }) => {
|
||||
setCurrentTextGroupName,
|
||||
isDOMMode,
|
||||
setIsDOMMode,
|
||||
currentSnapshot,
|
||||
setCurrentSnapshot,
|
||||
updateDOMMode,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -59,7 +59,12 @@ class CapturedElementHighlighter {
|
||||
* Get the iframe document
|
||||
*/
|
||||
private getIframeDocument(): Document | null {
|
||||
const iframeElement = document.querySelector('#dom-browser-iframe') as HTMLIFrameElement;
|
||||
let iframeElement = document.querySelector('#dom-browser-iframe') as HTMLIFrameElement;
|
||||
|
||||
if (!iframeElement) {
|
||||
iframeElement = document.querySelector('.replayer-wrapper iframe') as HTMLIFrameElement;
|
||||
}
|
||||
|
||||
return iframeElement?.contentDocument || null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
import { BROWSER_DEFAULT_HEIGHT, BROWSER_DEFAULT_WIDTH } from "../constants/const";
|
||||
import { getResponsiveDimensions } from "./dimensionUtils";
|
||||
|
||||
export class CoordinateMapper {
|
||||
private canvasWidth: number;
|
||||
private canvasHeight: number;
|
||||
private browserWidth: number;
|
||||
private browserHeight: number;
|
||||
|
||||
private lastBrowserRect: { left: number, top: number, right: number, bottom: number } | null = null;
|
||||
private lastCanvasRect: DOMRect | null = null;
|
||||
|
||||
constructor() {
|
||||
const dimensions = getResponsiveDimensions();
|
||||
this.canvasWidth = dimensions.canvasWidth;
|
||||
this.canvasHeight = dimensions.canvasHeight;
|
||||
this.browserWidth = BROWSER_DEFAULT_WIDTH;
|
||||
this.browserHeight = BROWSER_DEFAULT_HEIGHT;
|
||||
}
|
||||
|
||||
mapCanvasToBrowser(coord: { x: number, y: number }): { x: number, y: number } {
|
||||
return {
|
||||
x: (coord.x / this.canvasWidth) * this.browserWidth,
|
||||
y: (coord.y / this.canvasHeight) * this.browserHeight
|
||||
};
|
||||
}
|
||||
|
||||
mapBrowserToCanvas(coord: { x: number, y: number }): { x: number, y: number } {
|
||||
return {
|
||||
x: (coord.x / this.browserWidth) * this.canvasWidth,
|
||||
y: (coord.y / this.browserHeight) * this.canvasHeight
|
||||
};
|
||||
}
|
||||
|
||||
mapBrowserRectToCanvas(rect: DOMRect): DOMRect {
|
||||
if (this.lastBrowserRect &&
|
||||
this.lastBrowserRect.left === rect.left &&
|
||||
this.lastBrowserRect.top === rect.top &&
|
||||
this.lastBrowserRect.right === rect.right &&
|
||||
this.lastBrowserRect.bottom === rect.bottom) {
|
||||
return this.lastCanvasRect!;
|
||||
}
|
||||
|
||||
const topLeft = this.mapBrowserToCanvas({ x: rect.left, y: rect.top });
|
||||
const bottomRight = this.mapBrowserToCanvas({ x: rect.right, y: rect.bottom });
|
||||
|
||||
const width = bottomRight.x - topLeft.x;
|
||||
const height = bottomRight.y - topLeft.y;
|
||||
|
||||
const result = new DOMRect(
|
||||
topLeft.x,
|
||||
topLeft.y,
|
||||
width,
|
||||
height
|
||||
);
|
||||
|
||||
this.lastBrowserRect = {
|
||||
left: rect.left,
|
||||
top: rect.top,
|
||||
right: rect.right,
|
||||
bottom: rect.bottom
|
||||
};
|
||||
this.lastCanvasRect = result;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
mapCanvasRectToBrowser(rect: DOMRect): DOMRect {
|
||||
const topLeft = this.mapCanvasToBrowser({ x: rect.left, y: rect.top });
|
||||
const bottomRight = this.mapCanvasToBrowser({ x: rect.right, y: rect.bottom });
|
||||
|
||||
const width = bottomRight.x - topLeft.x;
|
||||
const height = bottomRight.y - topLeft.y;
|
||||
|
||||
return new DOMRect(
|
||||
topLeft.x,
|
||||
topLeft.y,
|
||||
width,
|
||||
height
|
||||
);
|
||||
}
|
||||
|
||||
updateDimensions(canvasWidth?: number, canvasHeight?: number, browserWidth?: number, browserHeight?: number) {
|
||||
if (canvasWidth) this.canvasWidth = canvasWidth;
|
||||
if (canvasHeight) this.canvasHeight = canvasHeight;
|
||||
if (browserWidth) this.browserWidth = browserWidth;
|
||||
if (browserHeight) this.browserHeight = browserHeight;
|
||||
|
||||
this.lastBrowserRect = null;
|
||||
this.lastCanvasRect = null;
|
||||
}
|
||||
}
|
||||
|
||||
export const coordinateMapper = new CoordinateMapper();
|
||||
@@ -1,139 +0,0 @@
|
||||
import {
|
||||
ONE_PERCENT_OF_VIEWPORT_H,
|
||||
ONE_PERCENT_OF_VIEWPORT_W,
|
||||
} from "../constants/const";
|
||||
import { Coordinates } from '../components/recorder/Canvas';
|
||||
|
||||
export const throttle = (callback: any, limit: number) => {
|
||||
let wait = false;
|
||||
return (...args: any[]) => {
|
||||
if (!wait) {
|
||||
callback(...args);
|
||||
wait = true;
|
||||
setTimeout(function () {
|
||||
wait = false;
|
||||
}, limit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const getMappedCoordinates = (
|
||||
event: MouseEvent,
|
||||
canvas: HTMLCanvasElement | null,
|
||||
browserWidth: number,
|
||||
browserHeight: number,
|
||||
): Coordinates => {
|
||||
const clientCoordinates = getCoordinates(event, canvas);
|
||||
const mappedX = mapPixelFromSmallerToLarger(
|
||||
browserWidth / 100,
|
||||
ONE_PERCENT_OF_VIEWPORT_W,
|
||||
clientCoordinates.x,
|
||||
);
|
||||
const mappedY = mapPixelFromSmallerToLarger(
|
||||
browserHeight / 100,
|
||||
ONE_PERCENT_OF_VIEWPORT_H,
|
||||
clientCoordinates.y,
|
||||
);
|
||||
|
||||
return {
|
||||
x: mappedX,
|
||||
y: mappedY
|
||||
};
|
||||
};
|
||||
|
||||
const getCoordinates = (event: MouseEvent, canvas: HTMLCanvasElement | null): Coordinates => {
|
||||
if (!canvas) {
|
||||
return { x: 0, y: 0 };
|
||||
}
|
||||
return {
|
||||
x: event.pageX - canvas.offsetLeft,
|
||||
y: event.pageY - canvas.offsetTop
|
||||
};
|
||||
};
|
||||
|
||||
export const mapRect = (
|
||||
rect: DOMRect,
|
||||
browserWidth: number,
|
||||
browserHeight: number,
|
||||
) => {
|
||||
const mappedX = mapPixelFromSmallerToLarger(
|
||||
browserWidth / 100,
|
||||
ONE_PERCENT_OF_VIEWPORT_W,
|
||||
rect.x,
|
||||
);
|
||||
const mappedLeft = mapPixelFromSmallerToLarger(
|
||||
browserWidth / 100,
|
||||
ONE_PERCENT_OF_VIEWPORT_W,
|
||||
rect.left,
|
||||
);
|
||||
const mappedRight = mapPixelFromSmallerToLarger(
|
||||
browserWidth / 100,
|
||||
ONE_PERCENT_OF_VIEWPORT_W,
|
||||
rect.right,
|
||||
);
|
||||
const mappedWidth = mapPixelFromSmallerToLarger(
|
||||
browserWidth / 100,
|
||||
ONE_PERCENT_OF_VIEWPORT_W,
|
||||
rect.width,
|
||||
);
|
||||
const mappedY = mapPixelFromSmallerToLarger(
|
||||
browserHeight / 100,
|
||||
ONE_PERCENT_OF_VIEWPORT_H,
|
||||
rect.y,
|
||||
);
|
||||
const mappedTop = mapPixelFromSmallerToLarger(
|
||||
browserHeight / 100,
|
||||
ONE_PERCENT_OF_VIEWPORT_H,
|
||||
rect.top,
|
||||
);
|
||||
const mappedBottom = mapPixelFromSmallerToLarger(
|
||||
browserHeight / 100,
|
||||
ONE_PERCENT_OF_VIEWPORT_H,
|
||||
rect.bottom,
|
||||
);
|
||||
const mappedHeight = mapPixelFromSmallerToLarger(
|
||||
browserHeight / 100,
|
||||
ONE_PERCENT_OF_VIEWPORT_H,
|
||||
rect.height,
|
||||
);
|
||||
|
||||
console.log('Mapped:', {
|
||||
x: mappedX,
|
||||
y: mappedY,
|
||||
width: mappedWidth,
|
||||
height: mappedHeight,
|
||||
top: mappedTop,
|
||||
right: mappedRight,
|
||||
bottom: mappedBottom,
|
||||
left: mappedLeft,
|
||||
})
|
||||
|
||||
return {
|
||||
x: mappedX,
|
||||
y: mappedY,
|
||||
width: mappedWidth,
|
||||
height: mappedHeight,
|
||||
top: mappedTop,
|
||||
right: mappedRight,
|
||||
bottom: mappedBottom,
|
||||
left: mappedLeft,
|
||||
};
|
||||
};
|
||||
|
||||
const mapPixelFromSmallerToLarger = (
|
||||
onePercentOfSmallerScreen: number,
|
||||
onePercentOfLargerScreen: number,
|
||||
pixel: number
|
||||
): number => {
|
||||
const xPercentOfScreen = pixel / onePercentOfSmallerScreen;
|
||||
return xPercentOfScreen * onePercentOfLargerScreen;
|
||||
};
|
||||
|
||||
const mapPixelFromLargerToSmaller = (
|
||||
onePercentOfSmallerScreen: number,
|
||||
onePercentOfLargerScreen: number,
|
||||
pixel: number
|
||||
): number => {
|
||||
const xPercentOfScreen = pixel / onePercentOfLargerScreen;
|
||||
return Math.round(xPercentOfScreen * onePercentOfSmallerScreen);
|
||||
};
|
||||
@@ -56,8 +56,6 @@ export const RecordingPage = ({ recordingName }: RecordingPageProps) => {
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => changeBrowserDimensions(), [isLoaded])
|
||||
|
||||
useEffect(() => {
|
||||
if (darkMode) {
|
||||
|
||||
@@ -118,20 +116,6 @@ export const RecordingPage = ({ recordingName }: RecordingPageProps) => {
|
||||
}
|
||||
}, [setId, recordingUrl, setRecordingUrl, setRecordingName, setRetrainRobotId]);
|
||||
|
||||
const changeBrowserDimensions = useCallback(() => {
|
||||
if (browserContentRef.current) {
|
||||
const currentWidth = Math.floor(browserContentRef.current.getBoundingClientRect().width);
|
||||
const innerHeightWithoutNavBar = window.innerHeight - 54.5;
|
||||
if (innerHeightWithoutNavBar <= (currentWidth / 1.6)) {
|
||||
setWidth(currentWidth - 10);
|
||||
setHasScrollbar(true);
|
||||
} else {
|
||||
setWidth(currentWidth);
|
||||
}
|
||||
socket?.emit("rerender");
|
||||
}
|
||||
}, [socket]);
|
||||
|
||||
const handleLoaded = useCallback(() => {
|
||||
if (recordingName && browserId && recordingId) {
|
||||
editRecordingFromStorage(browserId, recordingId).then(() => setIsLoaded(true));
|
||||
|
||||
Reference in New Issue
Block a user