From 4345fc29fe0e1b6623382dc69d7b97bc2e6f4b3a Mon Sep 17 00:00:00 2001 From: Rohit Rajan Date: Sun, 19 Oct 2025 22:48:28 +0530 Subject: [PATCH] feat: recorder revamp ui changes --- src/components/browser/BrowserWindow.tsx | 103 +- src/components/recorder/RightSidePanel.tsx | 582 ++-------- src/components/run/InterpretationLog.tsx | 1114 ++++++++++++++++---- src/components/run/RunContent.tsx | 765 +++++++++----- src/context/browserSteps.tsx | 461 ++++++-- src/context/globalInfo.tsx | 6 + src/helpers/capturedElementHighlighter.ts | 85 ++ 7 files changed, 2044 insertions(+), 1072 deletions(-) create mode 100644 src/helpers/capturedElementHighlighter.ts diff --git a/src/components/browser/BrowserWindow.tsx b/src/components/browser/BrowserWindow.tsx index 5bb84101..e18a1b89 100644 --- a/src/components/browser/BrowserWindow.tsx +++ b/src/components/browser/BrowserWindow.tsx @@ -5,13 +5,14 @@ import Canvas from "../recorder/Canvas"; import { Highlighter } from "../recorder/Highlighter"; import { GenericModal } from '../ui/GenericModal'; import { useActionContext } from '../../context/browserActions'; -import { useBrowserSteps, TextStep } from '../../context/browserSteps'; +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"; import DatePicker from "../pickers/DatePicker"; import Dropdown from "../pickers/Dropdown"; import TimePicker from "../pickers/TimePicker"; @@ -182,10 +183,13 @@ export const BrowserWindow = () => { count?: number; } | null>(null); + const [initialAutoFieldIds, setInitialAutoFieldIds] = useState>(new Set()); + const [manuallyAddedFieldIds, setManuallyAddedFieldIds] = useState>(new Set()); + const { socket } = useSocketStore(); const { notify, currentTextActionId, currentListActionId, updateDOMMode, isDOMMode, currentSnapshot } = useGlobalInfoStore(); const { getText, getList, paginationMode, paginationType, limitMode, captureStage } = useActionContext(); - const { addTextStep, addListStep } = useBrowserSteps(); + const { addTextStep, addListStep, browserSteps } = useBrowserSteps(); const [currentGroupInfo, setCurrentGroupInfo] = useState<{ isGroupElement: boolean; @@ -1159,6 +1163,7 @@ export const BrowserWindow = () => { if (Object.keys(autoFields).length > 0) { setFields(autoFields); + setInitialAutoFieldIds(new Set(Object.keys(autoFields).map(id => parseInt(id)))); addListStep( listSelector, @@ -1195,6 +1200,11 @@ export const BrowserWindow = () => { cachedListSelector, pendingNotification, notify, + createFieldsFromChildSelectors, + currentListId, + currentListActionId, + paginationSelector, + addListStep ]); useEffect(() => { @@ -1203,6 +1213,77 @@ export const BrowserWindow = () => { } }, [listSelector]); + useEffect(() => { + if (!getList || !listSelector || initialAutoFieldIds.size === 0 || !currentListActionId) return; + + const currentListStep = browserSteps.find( + step => step.type === 'list' && step.actionId === currentListActionId + ); + + if (!currentListStep || currentListStep.type !== 'list' || !currentListStep.fields) return; + + const currentFieldIds = new Set(Object.keys(currentListStep.fields).map(id => parseInt(id))); + const newManualIds = new Set(); + + currentFieldIds.forEach(fieldId => { + if (!initialAutoFieldIds.has(fieldId)) { + newManualIds.add(fieldId); + } + }); + + if (newManualIds.size !== manuallyAddedFieldIds.size || + ![...newManualIds].every(id => manuallyAddedFieldIds.has(id))) { + setManuallyAddedFieldIds(newManualIds); + } + }, [browserSteps, getList, listSelector, initialAutoFieldIds, currentListActionId, manuallyAddedFieldIds]); + + useEffect(() => { + if (!isDOMMode) { + capturedElementHighlighter.clearHighlights(); + return; + } + + const capturedSelectors: Array<{ selector: string }> = []; + + if (getText && currentTextActionId) { + const textSteps = browserSteps.filter( + (step): step is TextStep => step.type === 'text' && step.actionId === currentTextActionId + ); + + textSteps.forEach(step => { + if (step.selectorObj?.selector) { + capturedSelectors.push({ + selector: step.selectorObj.selector, + }); + } + }); + } + + if (getList && listSelector && currentListActionId && manuallyAddedFieldIds.size > 0) { + const listSteps = browserSteps.filter( + step => step.type === 'list' && step.actionId === currentListActionId + ) as ListStep[]; + + listSteps.forEach(listStep => { + if (listStep.fields) { + Object.entries(listStep.fields).forEach(([fieldId, field]: [string, any]) => { + if (manuallyAddedFieldIds.has(parseInt(fieldId)) && field.selectorObj?.selector) { + capturedSelectors.push({ + selector: field.selectorObj.selector, + }); + } + }); + } + }); + } + + if (capturedSelectors.length > 0) { + capturedElementHighlighter.applyHighlights(capturedSelectors); + } else { + capturedElementHighlighter.clearHighlights(); + } + }, [browserSteps, getText, getList, listSelector, currentTextActionId, currentListActionId, isDOMMode, manuallyAddedFieldIds]); + useEffect(() => { coordinateMapper.updateDimensions(dimensions.width, dimensions.height, viewportInfo.width, viewportInfo.height); }, [viewportInfo, dimensions.width, dimensions.height]); @@ -1216,7 +1297,6 @@ export const BrowserWindow = () => { useEffect(() => { const storedListSelector = sessionStorage.getItem('recordingListSelector'); - // Only restore state if it exists in sessionStorage if (storedListSelector && !listSelector) { setListSelector(storedListSelector); } @@ -1225,7 +1305,6 @@ export const BrowserWindow = () => { const onMouseMove = (e: MouseEvent) => { if (canvasRef && canvasRef.current && highlighterData) { const canvasRect = canvasRef.current.getBoundingClientRect(); - // mousemove outside the browser window if ( e.pageX < canvasRect.left || e.pageX > canvasRect.right @@ -1242,6 +1321,8 @@ export const BrowserWindow = () => { setFields({}); setCurrentListId(null); setCachedChildSelectors([]); + setInitialAutoFieldIds(new Set()); + setManuallyAddedFieldIds(new Set()); }, []); useEffect(() => { @@ -1262,7 +1343,7 @@ export const BrowserWindow = () => { } } } - }, [screenShot, user?.id]); + }, [user?.id]); useEffect(() => { if (socket) { @@ -1456,7 +1537,6 @@ export const BrowserWindow = () => { } highlighterUpdateRef.current = now; - // Map the incoming DOMRect from browser coordinates to canvas coordinates const mappedRect = new DOMRect( data.rect.x, data.rect.y, @@ -1477,17 +1557,14 @@ export const BrowserWindow = () => { if (limitMode) { setHighlighterData(null); } else if (paginationMode) { - // Only set highlighterData if type is not empty, 'none', 'scrollDown', or 'scrollUp' if (paginationType !== '' && !['none', 'scrollDown', 'scrollUp'].includes(paginationType)) { setHighlighterData(mappedData); } else { setHighlighterData(null); } } else if (mappedData.childSelectors && mappedData.childSelectors.includes(mappedData.selector)) { - // Highlight only valid child elements within the listSelector setHighlighterData(mappedData); } else if (mappedData.elementInfo?.isIframeContent && mappedData.childSelectors) { - // Handle iframe elements const isIframeChild = mappedData.childSelectors.some(childSelector => mappedData.selector.includes(':>>') && childSelector.split(':>>').some(part => @@ -1496,7 +1573,6 @@ export const BrowserWindow = () => { ); setHighlighterData(isIframeChild ? mappedData : null); } else if (mappedData.selector.includes(':>>') && hasValidChildSelectors) { - // Handle mixed DOM cases with iframes const selectorParts = mappedData.selector.split(':>>').map(part => part.trim()); const isValidMixedSelector = selectorParts.some(part => mappedData.childSelectors!.some(childSelector => @@ -1505,7 +1581,6 @@ export const BrowserWindow = () => { ); setHighlighterData(isValidMixedSelector ? mappedData : null); } else if (mappedData.elementInfo?.isShadowRoot && mappedData.childSelectors) { - // Handle Shadow DOM elements const isShadowChild = mappedData.childSelectors.some(childSelector => mappedData.selector.includes('>>') && childSelector.split('>>').some(part => @@ -1514,7 +1589,6 @@ export const BrowserWindow = () => { ); setHighlighterData(isShadowChild ? mappedData : null); } else if (mappedData.selector.includes('>>') && hasValidChildSelectors) { - // Handle mixed DOM cases const selectorParts = mappedData.selector.split('>>').map(part => part.trim()); const isValidMixedSelector = selectorParts.some(part => mappedData.childSelectors!.some(childSelector => @@ -1523,15 +1597,12 @@ export const BrowserWindow = () => { ); setHighlighterData(isValidMixedSelector ? mappedData : null); } else { - // If not a valid child in normal mode, clear the highlighter setHighlighterData(null); } } else { - // Set highlighterData for the initial listSelector selection setHighlighterData(mappedData); } } else { - // For non-list steps setHighlighterData(mappedData); } }, [getList, socket, listSelector, paginationMode, paginationType, limitMode]); @@ -2114,7 +2185,7 @@ export const BrowserWindow = () => { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } - `} + `}j {(getText === true || getList === true) && !showAttributeModal && diff --git a/src/components/recorder/RightSidePanel.tsx b/src/components/recorder/RightSidePanel.tsx index bf0a10c4..e22ab08b 100644 --- a/src/components/recorder/RightSidePanel.tsx +++ b/src/components/recorder/RightSidePanel.tsx @@ -1,19 +1,13 @@ import React, { useState, useCallback, useEffect, useMemo } from 'react'; import { Button, Paper, Box, TextField, IconButton, Tooltip } from "@mui/material"; -import EditIcon from '@mui/icons-material/Edit'; -import TextFieldsIcon from '@mui/icons-material/TextFields'; -import DocumentScannerIcon from '@mui/icons-material/DocumentScanner'; import { WorkflowFile } from "maxun-core"; import Typography from "@mui/material/Typography"; import { useGlobalInfoStore } from "../../context/globalInfo"; import { PaginationType, useActionContext, LimitType } from '../../context/browserActions'; import { BrowserStep, useBrowserSteps } from '../../context/browserSteps'; import { useSocketStore } from '../../context/socket'; -import { ScreenshotSettings } from '../../shared/types'; -import InputAdornment from '@mui/material/InputAdornment'; import FormControlLabel from '@mui/material/FormControlLabel'; import FormControl from '@mui/material/FormControl'; -import FormLabel from '@mui/material/FormLabel'; import Radio from '@mui/material/Radio'; import RadioGroup from '@mui/material/RadioGroup'; import { getActiveWorkflow } from "../../api/workflow"; @@ -49,13 +43,9 @@ export const RightSidePanel: React.FC = ({ onFinishCapture const [showCaptureList, setShowCaptureList] = useState(true); const [showCaptureScreenshot, setShowCaptureScreenshot] = useState(true); const [showCaptureText, setShowCaptureText] = useState(true); - const [hoverStates, setHoverStates] = useState<{ [id: string]: boolean }>({}); - const [browserStepIdList, setBrowserStepIdList] = useState([]); - const [isCaptureTextConfirmed, setIsCaptureTextConfirmed] = useState(false); - const [isCaptureListConfirmed, setIsCaptureListConfirmed] = useState(false); const { panelHeight } = useBrowserDimensionsStore(); - const { lastAction, notify, currentWorkflowActionsState, setCurrentWorkflowActionsState, resetInterpretationLog, currentListActionId, setCurrentListActionId, currentTextActionId, setCurrentTextActionId, currentScreenshotActionId, setCurrentScreenshotActionId, isDOMMode, setIsDOMMode, currentSnapshot, setCurrentSnapshot, updateDOMMode, initialUrl, setRecordingUrl } = useGlobalInfoStore(); + const { lastAction, notify, currentWorkflowActionsState, setCurrentWorkflowActionsState, resetInterpretationLog, currentListActionId, setCurrentListActionId, currentTextActionId, setCurrentTextActionId, currentScreenshotActionId, setCurrentScreenshotActionId, isDOMMode, setIsDOMMode, currentSnapshot, setCurrentSnapshot, updateDOMMode, initialUrl, setRecordingUrl, currentTextGroupName } = useGlobalInfoStore(); const { getText, startGetText, stopGetText, getList, startGetList, stopGetList, @@ -72,7 +62,7 @@ export const RightSidePanel: React.FC = ({ onFinishCapture startAction, finishAction } = useActionContext(); - const { browserSteps, updateBrowserTextStepLabel, deleteBrowserStep, addScreenshotStep, updateListTextFieldLabel, removeListTextField, updateListStepLimit, deleteStepsByActionId, updateListStepData, updateScreenshotStepData } = useBrowserSteps(); + const { browserSteps, updateBrowserTextStepLabel, deleteBrowserStep, addScreenshotStep, updateListTextFieldLabel, removeListTextField, updateListStepLimit, deleteStepsByActionId, updateListStepData, updateScreenshotStepData, emitActionForStep } = useBrowserSteps(); const { id, socket } = useSocketStore(); const { t } = useTranslation(); @@ -183,6 +173,7 @@ export const RightSidePanel: React.FC = ({ onFinishCapture if (screenshotSteps.length > 0) { const latestStep = screenshotSteps[screenshotSteps.length - 1]; updateScreenshotStepData(latestStep.id, data.screenshot); + emitActionForStep(latestStep); } setCurrentScreenshotActionId(''); @@ -194,7 +185,7 @@ export const RightSidePanel: React.FC = ({ onFinishCapture socket.off('directScreenshotCaptured', handleDirectScreenshot); }; } - }, [socket, id, notify, t, currentScreenshotActionId, updateScreenshotStepData, setCurrentScreenshotActionId]); + }, [socket, id, notify, t, currentScreenshotActionId, updateScreenshotStepData, setCurrentScreenshotActionId, emitActionForStep, browserSteps]); const extractDataClientSide = useCallback( ( @@ -271,26 +262,41 @@ export const RightSidePanel: React.FC = ({ onFinishCapture } } }, - [isDOMMode, currentSnapshot, updateListStepData, socket, notify] + [isDOMMode, currentSnapshot, updateListStepData, socket, notify, currentWorkflowActionsState] ); - const handleMouseEnter = (id: number) => { - setHoverStates(prev => ({ ...prev, [id]: true })); - }; + useEffect(() => { + if (!getList) return; - const handleMouseLeave = (id: number) => { - setHoverStates(prev => ({ ...prev, [id]: false })); - }; + const currentListStep = browserSteps.find( + step => step.type === 'list' && step.actionId === currentListActionId + ) as (BrowserStep & { type: 'list'; listSelector?: string; fields?: Record }) | undefined; + + if (!currentListStep || !currentListStep.listSelector || !currentListStep.fields) return; + + const fieldCount = Object.keys(currentListStep.fields).length; + + if (fieldCount > 0) { + extractDataClientSide( + currentListStep.listSelector, + currentListStep.fields, + currentListStep.id + ); + + setCurrentWorkflowActionsState({ + ...currentWorkflowActionsState, + hasScrapeListAction: true + }); + } + }, [browserSteps, currentListActionId, getList, extractDataClientSide, setCurrentWorkflowActionsState, currentWorkflowActionsState]); const handleStartGetText = () => { - setIsCaptureTextConfirmed(false); const newActionId = `text-${crypto.randomUUID()}`; setCurrentTextActionId(newActionId); startGetText(); } const handleStartGetList = () => { - setIsCaptureListConfirmed(false); const newActionId = `list-${crypto.randomUUID()}`; setCurrentListActionId(newActionId); startGetList(); @@ -302,230 +308,24 @@ export const RightSidePanel: React.FC = ({ onFinishCapture startGetScreenshot(); }; - const handleTextLabelChange = (id: number, label: string, listId?: number, fieldKey?: string) => { - if (listId !== undefined && fieldKey !== undefined) { - // Prevent editing if the field is confirmed - if (confirmedListTextFields[listId]?.[fieldKey]) { - return; - } - updateListTextFieldLabel(listId, fieldKey, label); - } else { - setTextLabels(prevLabels => ({ ...prevLabels, [id]: label })); - } - if (!label.trim()) { - setErrors(prevErrors => ({ ...prevErrors, [id]: t('right_panel.errors.label_required') })); - } else { - setErrors(prevErrors => ({ ...prevErrors, [id]: '' })); - } - }; - - const handleTextStepConfirm = (id: number) => { - const label = textLabels[id]?.trim(); - if (!label) { - setErrors(prevErrors => ({ ...prevErrors, [id]: t('right_panel.errors.label_required') })); - return; - } - - const existingLabels = browserSteps - .filter(step => - step.type === 'text' && - step.id !== id && - confirmedTextSteps[step.id] && - 'label' in step && - step.label - ) - .map(step => (step as any).label); - - if (existingLabels.includes(label)) { - setErrors(prevErrors => ({ - ...prevErrors, - [id]: t('right_panel.errors.duplicate_label') || `Label "${label}" already exists. Please use a unique label.` - })); - return; - } - - updateBrowserTextStepLabel(id, label); - setConfirmedTextSteps(prev => ({ ...prev, [id]: true })); - setErrors(prevErrors => ({ ...prevErrors, [id]: '' })); - }; - - const handleTextStepDiscard = (id: number) => { - deleteBrowserStep(id); - setTextLabels(prevLabels => { - const { [id]: _, ...rest } = prevLabels; - return rest; - }); - setErrors(prevErrors => { - const { [id]: _, ...rest } = prevErrors; - return rest; - }); - }; - - const handleTextStepDelete = (id: number) => { - deleteBrowserStep(id); - setTextLabels(prevLabels => { - const { [id]: _, ...rest } = prevLabels; - return rest; - }); - setConfirmedTextSteps(prev => { - const { [id]: _, ...rest } = prev; - return rest; - }); - setErrors(prevErrors => { - const { [id]: _, ...rest } = prevErrors; - return rest; - }); - }; - - const handleListTextFieldConfirm = (listId: number, fieldKey: string) => { - setConfirmedListTextFields(prev => ({ - ...prev, - [listId]: { - ...(prev[listId] || {}), - [fieldKey]: true - } - })); - }; - - const handleListTextFieldDiscard = (listId: number, fieldKey: string) => { - removeListTextField(listId, fieldKey); - setConfirmedListTextFields(prev => { - const updatedListFields = { ...(prev[listId] || {}) }; - delete updatedListFields[fieldKey]; - return { - ...prev, - [listId]: updatedListFields - }; - }); - setErrors(prev => { - const { [fieldKey]: _, ...rest } = prev; - return rest; - }); - }; - - const handleListTextFieldDelete = (listId: number, fieldKey: string) => { - removeListTextField(listId, fieldKey); - setConfirmedListTextFields(prev => { - const updatedListFields = { ...(prev[listId] || {}) }; - delete updatedListFields[fieldKey]; - return { - ...prev, - [listId]: updatedListFields - }; - }); - setErrors(prev => { - const { [fieldKey]: _, ...rest } = prev; - return rest; - }); - }; - - const getTextSettingsObject = useCallback(() => { - const settings: Record = {}; - - browserSteps.forEach(step => { - if (browserStepIdList.includes(step.id)) { - return; - } - - if (step.type === 'text' && step.label && step.selectorObj?.selector) { - settings[step.label] = { - ...step.selectorObj, - selector: step.selectorObj.selector - }; - } - setBrowserStepIdList(prevList => [...prevList, step.id]); - }); - - return settings; - }, [browserSteps, browserStepIdList]); - const stopCaptureAndEmitGetTextSettings = useCallback(() => { - const hasTextStepsForCurrentAction = browserSteps.some(step => step.type === 'text' && step.actionId === currentTextActionId); - if (!hasTextStepsForCurrentAction) { + const currentTextActionStep = browserSteps.find(step => step.type === 'text' && step.actionId === currentTextActionId); + if (!currentTextActionStep) { notify('error', t('right_panel.errors.no_text_captured')); return; } - const hasUnconfirmedTextStepsForCurrentAction = browserSteps.some(step => - step.type === 'text' && - step.actionId === currentTextActionId && - !confirmedTextSteps[step.id] - ); - if (hasUnconfirmedTextStepsForCurrentAction) { - notify('error', t('right_panel.errors.confirm_text_fields')); - return; - } stopGetText(); - const settings = getTextSettingsObject(); - if (hasTextStepsForCurrentAction) { - socket?.emit('action', { action: 'scrapeSchema', settings }); + if (currentTextActionStep) { + emitActionForStep(currentTextActionStep); } - setIsCaptureTextConfirmed(true); setCurrentTextActionId(''); resetInterpretationLog(); finishAction('text'); onFinishCapture(); clientSelectorGenerator.cleanup(); - }, [stopGetText, getTextSettingsObject, socket, browserSteps, confirmedTextSteps, resetInterpretationLog, finishAction, notify, onFinishCapture, t]); + }, [stopGetText, socket, browserSteps, resetInterpretationLog, finishAction, notify, onFinishCapture, t, currentTextActionId, currentTextGroupName, emitActionForStep]); - const getListSettingsObject = useCallback(() => { - let settings: { - listSelector?: string; - fields?: Record; - pagination?: { - type: string; - selector?: string; - isShadow?: boolean; - }; - limit?: number; - isShadow?: boolean; - } = {}; - - browserSteps.forEach(step => { - if (step.type === 'list' && step.listSelector && Object.keys(step.fields).length > 0) { - const fields: Record = {}; - - Object.entries(step.fields).forEach(([id, field]) => { - if (field.selectorObj?.selector) { - fields[field.label] = { - selector: field.selectorObj.selector, - tag: field.selectorObj.tag, - attribute: field.selectorObj.attribute, - isShadow: field.selectorObj.isShadow - }; - } - }); - - settings = { - listSelector: step.listSelector, - fields: fields, - pagination: { - type: paginationType, - selector: step.pagination?.selector, - isShadow: step.isShadow - }, - limit: parseInt(limitType === 'custom' ? customLimit : limitType), - isShadow: step.isShadow - }; - } - }); - - return settings; - }, [browserSteps, paginationType, limitType, customLimit]); const resetListState = useCallback(() => { setShowPaginationOptions(false); @@ -541,32 +341,33 @@ export const RightSidePanel: React.FC = ({ onFinishCapture }, [stopGetList, resetListState]); const stopCaptureAndEmitGetListSettings = useCallback(() => { - const settings = getListSettingsObject(); - const latestListStep = getLatestListStep(browserSteps); - if (latestListStep && settings) { + if (latestListStep) { extractDataClientSide(latestListStep.listSelector!, latestListStep.fields, latestListStep.id); + + setCurrentWorkflowActionsState({ + ...currentWorkflowActionsState, + hasScrapeListAction: true + }); - socket?.emit('action', { action: 'scrapeList', settings }); + emitActionForStep(latestListStep); + + handleStopGetList(); + setCurrentListActionId(''); + resetInterpretationLog(); + finishAction('list'); + onFinishCapture(); + clientSelectorGenerator.cleanup(); } else { notify('error', t('right_panel.errors.unable_create_settings')); + handleStopGetList(); + setCurrentListActionId(''); + resetInterpretationLog(); + finishAction('list'); + onFinishCapture(); + clientSelectorGenerator.cleanup(); } - - handleStopGetList(); - setCurrentListActionId(''); - resetInterpretationLog(); - finishAction('list'); - onFinishCapture(); - clientSelectorGenerator.cleanup(); - }, [getListSettingsObject, socket, notify, handleStopGetList, resetInterpretationLog, finishAction, onFinishCapture, t, browserSteps, extractDataClientSide]); - - const hasUnconfirmedListTextFields = browserSteps.some(step => - step.type === 'list' && - step.actionId === currentListActionId && - Object.entries(step.fields).some(([fieldKey]) => - !confirmedListTextFields[step.id]?.[fieldKey] - ) - ); + }, [socket, notify, handleStopGetList, resetInterpretationLog, finishAction, onFinishCapture, t, browserSteps, extractDataClientSide, setCurrentWorkflowActionsState, currentWorkflowActionsState, emitActionForStep]); const getLatestListStep = (steps: BrowserStep[]) => { const listSteps = steps.filter(step => step.type === 'list'); @@ -590,19 +391,6 @@ export const RightSidePanel: React.FC = ({ onFinishCapture return; } - const hasUnconfirmedListTextFieldsForCurrentAction = browserSteps.some(step => - step.type === 'list' && - step.actionId === currentListActionId && - Object.entries(step.fields).some(([fieldKey]) => - !confirmedListTextFields[step.id]?.[fieldKey] - ) - ); - - if (hasUnconfirmedListTextFieldsForCurrentAction) { - notify('error', t('right_panel.errors.confirm_all_list_fields')); - return; - } - startPaginationMode(); setShowPaginationOptions(true); setCaptureStage('pagination'); @@ -613,11 +401,17 @@ export const RightSidePanel: React.FC = ({ onFinishCapture notify('error', t('right_panel.errors.select_pagination')); return; } - const settings = getListSettingsObject(); - const paginationSelector = settings.pagination?.selector; - if (['clickNext', 'clickLoadMore'].includes(paginationType) && !paginationSelector) { - notify('error', t('right_panel.errors.select_pagination_element')); - return; + + const currentListStepForPagination = browserSteps.find( + step => step.type === 'list' && step.actionId === currentListActionId + ) as (BrowserStep & { type: 'list' }) | undefined; + + if (currentListStepForPagination) { + const paginationSelector = currentListStepForPagination.pagination?.selector; + if (['clickNext', 'clickLoadMore'].includes(paginationType) && !paginationSelector) { + notify('error', t('right_panel.errors.select_pagination_element')); + return; + } } stopPaginationMode(); setShowPaginationOptions(false); @@ -644,7 +438,6 @@ export const RightSidePanel: React.FC = ({ onFinishCapture stopLimitMode(); setShowLimitOptions(false); - setIsCaptureListConfirmed(true); stopCaptureAndEmitGetListSettings(); setCaptureStage('complete'); break; @@ -653,7 +446,7 @@ export const RightSidePanel: React.FC = ({ onFinishCapture setCaptureStage('initial'); break; } - }, [captureStage, paginationType, limitType, customLimit, startPaginationMode, setShowPaginationOptions, setCaptureStage, getListSettingsObject, notify, stopPaginationMode, startLimitMode, setShowLimitOptions, stopLimitMode, setIsCaptureListConfirmed, stopCaptureAndEmitGetListSettings, t, browserSteps, currentListActionId, confirmedListTextFields]); + }, [captureStage, paginationType, limitType, customLimit, startPaginationMode, setShowPaginationOptions, setCaptureStage, notify, stopPaginationMode, startLimitMode, setShowLimitOptions, stopLimitMode, stopCaptureAndEmitGetListSettings, t, browserSteps, currentListActionId, updateListStepLimit]); const handleBackCaptureList = useCallback(() => { switch (captureStage) { @@ -679,61 +472,28 @@ export const RightSidePanel: React.FC = ({ onFinishCapture const discardGetText = useCallback(() => { stopGetText(); - if (currentTextActionId) { - const stepsToDelete = browserSteps - .filter(step => step.type === 'text' && step.actionId === currentTextActionId) - .map(step => step.id); - + if (currentTextActionId) { deleteStepsByActionId(currentTextActionId); - setTextLabels(prevLabels => { - const newLabels = { ...prevLabels }; - stepsToDelete.forEach(id => { - delete newLabels[id]; - }); - return newLabels; - }); - - setErrors(prevErrors => { - const newErrors = { ...prevErrors }; - stepsToDelete.forEach(id => { - delete newErrors[id]; - }); - return newErrors; - }); - - setConfirmedTextSteps(prev => { - const newConfirmed = { ...prev }; - stepsToDelete.forEach(id => { - delete newConfirmed[id]; - }); - return newConfirmed; - }); + if (socket) { + socket.emit('removeAction', { actionId: currentTextActionId }); + } } setCurrentTextActionId(''); - setIsCaptureTextConfirmed(false); clientSelectorGenerator.cleanup(); notify('error', t('right_panel.errors.capture_text_discarded')); - }, [currentTextActionId, browserSteps, stopGetText, deleteStepsByActionId, notify, t]); + }, [currentTextActionId, browserSteps, stopGetText, deleteStepsByActionId, notify, t, socket]); const discardGetList = useCallback(() => { stopGetList(); if (currentListActionId) { - const listStepsToDelete = browserSteps - .filter(step => step.type === 'list' && step.actionId === currentListActionId) - .map(step => step.id); - deleteStepsByActionId(currentListActionId); - setConfirmedListTextFields(prev => { - const newConfirmed = { ...prev }; - listStepsToDelete.forEach(id => { - delete newConfirmed[id]; - }); - return newConfirmed; - }); + if (socket) { + socket.emit('removeAction', { actionId: currentListActionId }); + } } resetListState(); @@ -743,12 +503,14 @@ export const RightSidePanel: React.FC = ({ onFinishCapture setShowLimitOptions(false); setCaptureStage('initial'); setCurrentListActionId(''); - setIsCaptureListConfirmed(false); clientSelectorGenerator.cleanup(); notify('error', t('right_panel.errors.capture_list_discarded')); - }, [currentListActionId, browserSteps, stopGetList, deleteStepsByActionId, resetListState, setShowPaginationOptions, setShowLimitOptions, setCaptureStage, notify, t]); + }, [currentListActionId, browserSteps, stopGetList, deleteStepsByActionId, resetListState, setShowPaginationOptions, setShowLimitOptions, setCaptureStage, notify, t, stopPaginationMode, stopLimitMode, socket]); const captureScreenshot = (fullPage: boolean) => { + const screenshotCount = browserSteps.filter(s => s.type === 'screenshot').length + 1; + const screenshotName = `Screenshot ${screenshotCount}`; + const screenshotSettings = { fullPage, type: 'png' as const, @@ -756,38 +518,18 @@ export const RightSidePanel: React.FC = ({ onFinishCapture animations: 'allow' as const, caret: 'hide' as const, scale: 'device' as const, + name: screenshotName, + actionId: currentScreenshotActionId }; socket?.emit('captureDirectScreenshot', screenshotSettings); - socket?.emit('action', { action: 'screenshot', settings: screenshotSettings }); addScreenshotStep(fullPage, currentScreenshotActionId); stopGetScreenshot(); resetInterpretationLog(); finishAction('screenshot'); - clientSelectorGenerator.cleanup(); onFinishCapture(); + clientSelectorGenerator.cleanup(); }; - const isConfirmCaptureDisabled = useMemo(() => { - if (captureStage !== 'initial') return false; - - const hasValidListSelectorForCurrentAction = browserSteps.some(step => - step.type === 'list' && - step.actionId === currentListActionId && - step.listSelector && - Object.keys(step.fields).length > 0 - ); - - const hasUnconfirmedListTextFieldsForCurrentAction = browserSteps.some(step => - step.type === 'list' && - step.actionId === currentListActionId && - Object.entries(step.fields).some(([fieldKey]) => - !confirmedListTextFields[step.id]?.[fieldKey] - ) - ); - - return !hasValidListSelectorForCurrentAction || hasUnconfirmedListTextFieldsForCurrentAction; - }, [captureStage, browserSteps, currentListActionId, confirmedListTextFields]); - const theme = useThemeMode(); const isDarkMode = theme.darkMode; @@ -842,33 +584,20 @@ export const RightSidePanel: React.FC = ({ onFinishCapture {t('right_panel.buttons.back')} )} - - - - - + - - - ) : !isCaptureTextConfirmed && ( - - - - )} - - )} - {step.type === 'screenshot' && ( - - - - {step.fullPage ? - t('right_panel.screenshot.display_fullpage') : - t('right_panel.screenshot.display_visible')} - - - )} - {step.type === 'list' && ( - Object.entries(step.fields).length === 0 ? ( - {t('right_panel.messages.list_empty')} - ) : ( - <> - {t('right_panel.messages.list_selected')} - {Object.entries(step.fields).map(([key, field]) => ( - - handleTextLabelChange(field.id, e.target.value, step.id, key)} - fullWidth - margin="normal" - InputProps={{ - readOnly: confirmedListTextFields[field.id]?.[key], - startAdornment: ( - - - - ) - }} - /> - - - - ) - }} - /> - {!confirmedListTextFields[step.id]?.[key] && ( - - - - - )} - - ))} - - ) - )} - - ))} - ); }; diff --git a/src/components/run/InterpretationLog.tsx b/src/components/run/InterpretationLog.tsx index faf965e9..96fd2867 100644 --- a/src/components/run/InterpretationLog.tsx +++ b/src/components/run/InterpretationLog.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import SwipeableDrawer from '@mui/material/SwipeableDrawer'; import Typography from '@mui/material/Typography'; -import { Button, Grid, Box } from '@mui/material'; +import { Button, Grid, Box, TextField, IconButton, Tooltip } from '@mui/material'; import { useCallback, useEffect, useRef, useState } from "react"; import { useBrowserDimensionsStore } from "../../context/browserDimensions"; import Table from '@mui/material/Table'; @@ -13,11 +13,14 @@ import TableRow from '@mui/material/TableRow'; import Paper from '@mui/material/Paper'; import StorageIcon from '@mui/icons-material/Storage'; import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; +import CloseIcon from '@mui/icons-material/Close'; +import CheckIcon from '@mui/icons-material/Check'; import { SidePanelHeader } from '../recorder/SidePanelHeader'; import { useGlobalInfoStore } from '../../context/globalInfo'; import { useThemeMode } from '../../context/theme-provider'; import { useTranslation } from 'react-i18next'; import { useBrowserSteps } from '../../context/browserSteps'; +import { useActionContext } from '../../context/browserActions'; interface InterpretationLogProps { isOpen: boolean; @@ -26,24 +29,48 @@ interface InterpretationLogProps { export const InterpretationLog: React.FC = ({ isOpen, setIsOpen }) => { const { t } = useTranslation(); - + const [captureListData, setCaptureListData] = useState([]); const [captureTextData, setCaptureTextData] = useState([]); const [screenshotData, setScreenshotData] = useState([]); - const [captureListPage, setCaptureListPage] = useState(0); - const [screenshotPage, setScreenshotPage] = useState(0); - const [activeTab, setActiveTab] = useState(0); - - const logEndRef = useRef(null); + const [activeListTab, setActiveListTab] = useState(0); + const [activeScreenshotTab, setActiveScreenshotTab] = useState(0); + + const [editingField, setEditingField] = useState<{listId: number, fieldKey: string} | null>(null); + const [editingValue, setEditingValue] = useState(''); + + const [editingListName, setEditingListName] = useState(null); + const [editingListNameValue, setEditingListNameValue] = useState(''); + + const [editingTextGroupName, setEditingTextGroupName] = useState(false); + const [editingTextGroupNameValue, setEditingTextGroupNameValue] = useState('Text Data'); + + const [editingTextLabel, setEditingTextLabel] = useState(null); + const [editingTextLabelValue, setEditingTextLabelValue] = useState(''); + + const [editingScreenshotName, setEditingScreenshotName] = useState(null); + const [editingScreenshotNameValue, setEditingScreenshotNameValue] = useState(''); + + const logEndRef = useRef(null); + const autoFocusedListIds = useRef>(new Set()); + const previousDataLengths = useRef>(new Map()); + const hasAutoFocusedTextTab = useRef(false); + const previousGetText = useRef(false); + const autoFocusedScreenshotIndices = useRef>(new Set()); + + const { browserSteps, updateListTextFieldLabel, removeListTextField, updateListStepName, updateScreenshotStepName, updateBrowserTextStepLabel, deleteBrowserStep, emitForStepId } = useBrowserSteps(); + const { captureStage, getText } = useActionContext(); - const { browserSteps } = useBrowserSteps(); - const { browserWidth, outputPreviewHeight, outputPreviewWidth } = useBrowserDimensionsStore(); - const { currentWorkflowActionsState, shouldResetInterpretationLog } = useGlobalInfoStore(); + const { currentWorkflowActionsState, shouldResetInterpretationLog, currentTextGroupName, setCurrentTextGroupName } = useGlobalInfoStore(); const [showPreviewData, setShowPreviewData] = useState(false); + const userClosedDrawer = useRef(false); + const lastListDataLength = useRef(0); + const lastTextDataLength = useRef(0); + const lastScreenshotDataLength = useRef(0); const toggleDrawer = (newOpen: boolean) => (event: React.KeyboardEvent | React.MouseEvent) => { if ( @@ -53,50 +80,216 @@ export const InterpretationLog: React.FC = ({ isOpen, se ) { return; } + if (!newOpen && isOpen) { + userClosedDrawer.current = true; + } setIsOpen(newOpen); }; + const handleStartEdit = (listId: number, fieldKey: string, currentLabel: string) => { + setEditingField({ listId, fieldKey }); + setEditingValue(currentLabel); + }; + + const handleSaveEdit = () => { + if (editingField && editingValue.trim()) { + const listStep = browserSteps.find(step => step.id === editingField.listId); + const actionId = listStep?.actionId; + + updateListTextFieldLabel(editingField.listId, editingField.fieldKey, editingValue.trim()); + + if (actionId) { + setTimeout(() => emitForStepId(actionId), 0); + } + + setEditingField(null); + setEditingValue(''); + } + }; + + const handleCancelEdit = () => { + setEditingField(null); + setEditingValue(''); + }; + + const handleDeleteField = (listId: number, fieldKey: string) => { + const listStep = browserSteps.find(step => step.id === listId); + const actionId = listStep?.actionId; + + removeListTextField(listId, fieldKey); + + if (actionId) { + setTimeout(() => emitForStepId(actionId), 0); + } + }; + + const handleStartEditListName = (listId: number, currentName: string) => { + setEditingListName(listId); + setEditingListNameValue(currentName); + }; + + const handleSaveListName = () => { + if (editingListName !== null) { + const trimmedName = editingListNameValue.trim(); + const finalName = trimmedName || `List Data ${captureListData.findIndex(l => l.id === editingListName) + 1}`; + + updateListStepName(editingListName, finalName); + + const listStep = browserSteps.find(step => step.id === editingListName); + if (listStep?.actionId) { + setTimeout(() => emitForStepId(listStep.actionId!), 0); + } + + setEditingListName(null); + setEditingListNameValue(''); + } + }; + + const handleStartEditTextGroupName = () => { + setEditingTextGroupName(true); + setEditingTextGroupNameValue(currentTextGroupName); + }; + + const handleSaveTextGroupName = () => { + const trimmedName = editingTextGroupNameValue.trim(); + const finalName = trimmedName || 'Text Data'; + + setCurrentTextGroupName(finalName); + setEditingTextGroupName(false); + + setTimeout(() => { + const activeTextStep = captureTextData.find(step => step.actionId); + if (activeTextStep?.actionId) emitForStepId(activeTextStep.actionId); + }, 0); + }; + + + const handleStartEditTextLabel = (textId: number, currentLabel: string) => { + setEditingTextLabel(textId); + setEditingTextLabelValue(currentLabel); + }; + + const handleSaveTextLabel = () => { + if (editingTextLabel !== null && editingTextLabelValue.trim()) { + const textStep = browserSteps.find(step => step.id === editingTextLabel); + const actionId = textStep?.actionId; + + updateBrowserTextStepLabel(editingTextLabel, editingTextLabelValue.trim()); + + if (actionId) { + setTimeout(() => emitForStepId(actionId), 0); + } + + setEditingTextLabel(null); + setEditingTextLabelValue(''); + } + }; + + const handleCancelTextLabel = () => { + setEditingTextLabel(null); + setEditingTextLabelValue(''); + }; + + const handleDeleteTextStep = (textId: number) => { + const textStep = browserSteps.find(step => step.id === textId); + const actionId = textStep?.actionId; + + deleteBrowserStep(textId); + + if (actionId) { + setTimeout(() => emitForStepId(actionId), 0); + } + }; + + const handleStartEditScreenshotName = (screenshotStepId: number, currentName: string) => { + setEditingScreenshotName(screenshotStepId); + setEditingScreenshotNameValue(currentName); + }; + + const handleSaveScreenshotName = () => { + if (editingScreenshotName !== null) { + const trimmedName = editingScreenshotNameValue.trim(); + const screenshotSteps = browserSteps.filter(step => step.type === 'screenshot'); + const screenshotIndex = screenshotSteps.findIndex(s => s.id === editingScreenshotName); + const finalName = trimmedName || `Screenshot ${screenshotIndex + 1}`; + + updateScreenshotStepName(editingScreenshotName, finalName); + + const screenshotStep = browserSteps.find(step => step.id === editingScreenshotName); + if (screenshotStep?.actionId) { + const originalName = screenshotStep.name?.trim() || ""; + const trimmedName = editingScreenshotNameValue.trim(); + + if (trimmedName && trimmedName !== originalName) { + setTimeout(() => emitForStepId(screenshotStep.actionId!), 500); + } + } + + setEditingScreenshotName(null); + setEditingScreenshotNameValue(''); + } + }; + + + const previousTabsCount = useRef({ lists: 0, texts: 0, screenshots: 0 }); + const updateActiveTab = useCallback(() => { const availableTabs = getAvailableTabs(); - - if (captureListData.length > 0 && availableTabs.findIndex(tab => tab.id === 'captureList') !== -1) { + const hasNewListData = captureListData.length > previousTabsCount.current.lists; + const hasNewTextData = captureTextData.length > previousTabsCount.current.texts; + const hasNewScreenshotData = screenshotData.length > previousTabsCount.current.screenshots; + + previousTabsCount.current = { + lists: captureListData.length, + texts: captureTextData.length, + screenshots: screenshotData.length + }; + + if (hasNewListData && availableTabs.findIndex(tab => tab.id === 'captureList') !== -1) { setActiveTab(availableTabs.findIndex(tab => tab.id === 'captureList')); - } else if (captureTextData.length > 0 && availableTabs.findIndex(tab => tab.id === 'captureText') !== -1) { + } else if (hasNewTextData && availableTabs.findIndex(tab => tab.id === 'captureText') !== -1) { setActiveTab(availableTabs.findIndex(tab => tab.id === 'captureText')); - } else if (screenshotData.length > 0 && availableTabs.findIndex(tab => tab.id === 'captureScreenshot') !== -1) { + } else if (hasNewScreenshotData && availableTabs.findIndex(tab => tab.id === 'captureScreenshot') !== -1) { setActiveTab(availableTabs.findIndex(tab => tab.id === 'captureScreenshot')); + setActiveScreenshotTab(screenshotData.length - 1); } }, [captureListData.length, captureTextData.length, screenshotData.length]); + useEffect(() => { const textSteps = browserSteps.filter(step => step.type === 'text'); - if (textSteps.length > 0) { - const textDataRow: Record = {}; - - textSteps.forEach(step => { - textDataRow[step.label] = step.data; - }); - - setCaptureTextData([textDataRow]); - } - - const listSteps = browserSteps.filter(step => step.type === 'list'); - if (listSteps.length > 0) { - setCaptureListData(listSteps); + setCaptureTextData(textSteps); + + if (!getText && previousGetText.current && textSteps.length > 0) { + if (!hasAutoFocusedTextTab.current) { + hasAutoFocusedTextTab.current = true; + setTimeout(() => { + handleStartEditTextGroupName(); + }, 300); + } } - const screenshotSteps = browserSteps.filter(step => + previousGetText.current = getText; + + const listSteps = browserSteps.filter(step => step.type === 'list'); + setCaptureListData(listSteps); + + const screenshotSteps = browserSteps.filter(step => step.type === 'screenshot' - ) as Array<{ type: 'screenshot'; id: number; fullPage: boolean; actionId?: string; screenshotData?: string }>; + ) as Array<{ type: 'screenshot'; id: number; name?: string; fullPage: boolean; actionId?: string; screenshotData?: string }>; const screenshotsWithData = screenshotSteps.filter(step => step.screenshotData); - if (screenshotsWithData.length > 0) { - const screenshots = screenshotsWithData.map(step => step.screenshotData!); - setScreenshotData(screenshots); + const screenshots = screenshotsWithData.map(step => step.screenshotData!); + setScreenshotData(screenshots); + + if (textSteps.length > 0 || listSteps.length > 0 || screenshots.length > 0) { + setShowPreviewData(true); + } else { + setShowPreviewData(false); } - + updateActiveTab(); - }, [browserSteps, updateActiveTab]); + }, [browserSteps, updateActiveTab, getText]); useEffect(() => { if (shouldResetInterpretationLog) { @@ -104,32 +297,40 @@ export const InterpretationLog: React.FC = ({ isOpen, se setCaptureTextData([]); setScreenshotData([]); setActiveTab(0); - setCaptureListPage(0); - setScreenshotPage(0); setShowPreviewData(false); + autoFocusedListIds.current.clear(); + previousDataLengths.current.clear(); + autoFocusedScreenshotIndices.current.clear(); + userClosedDrawer.current = false; + lastListDataLength.current = 0; + lastTextDataLength.current = 0; + lastScreenshotDataLength.current = 0; + previousTabsCount.current = { lists: 0, texts: 0, screenshots: 0 }; + hasAutoFocusedTextTab.current = false; + previousGetText.current = false; } }, [shouldResetInterpretationLog]); const getAvailableTabs = useCallback(() => { const tabs = []; - + if (captureListData.length > 0) { tabs.push({ id: 'captureList', label: 'Lists' }); } - + if (captureTextData.length > 0) { tabs.push({ id: 'captureText', label: 'Texts' }); } - + if (screenshotData.length > 0) { tabs.push({ id: 'captureScreenshot', label: 'Screenshots' }); } - + return tabs; }, [captureListData.length, captureTextData.length, screenshotData.length, showPreviewData]); const availableTabs = getAvailableTabs(); - + useEffect(() => { if (activeTab >= availableTabs.length && availableTabs.length > 0) { setActiveTab(0); @@ -139,15 +340,105 @@ export const InterpretationLog: React.FC = ({ isOpen, se const { hasScrapeListAction, hasScreenshotAction, hasScrapeSchemaAction } = currentWorkflowActionsState; useEffect(() => { - if (hasScrapeListAction || hasScrapeSchemaAction || hasScreenshotAction) { - setIsOpen(true); + let shouldOpenDrawer = false; + let switchToTextTab = false; + let switchToScreenshotTab = false; + + if (hasScrapeListAction && captureListData.length > 0 && captureListData[0]?.data?.length > 0) { + setShowPreviewData(true); + if (captureListData.length > lastListDataLength.current) { + userClosedDrawer.current = false; + shouldOpenDrawer = true; + } + lastListDataLength.current = captureListData.length; } - }, [hasScrapeListAction, hasScrapeSchemaAction, hasScreenshotAction, setIsOpen]); + + if (hasScrapeSchemaAction && captureTextData.length > 0 && !getText) { + setShowPreviewData(true); + if (captureTextData.length > lastTextDataLength.current) { + userClosedDrawer.current = false; + shouldOpenDrawer = true; + switchToTextTab = true; + } + lastTextDataLength.current = captureTextData.length; + } + + if (hasScreenshotAction && screenshotData.length > 0) { + setShowPreviewData(true); + if (screenshotData.length > lastScreenshotDataLength.current) { + userClosedDrawer.current = false; + shouldOpenDrawer = true; + switchToScreenshotTab = true; + } + lastScreenshotDataLength.current = screenshotData.length; + } + + if (shouldOpenDrawer) { + setIsOpen(true); + if (switchToTextTab) { + setTimeout(() => { + const textTabIndex = getAvailableTabs().findIndex(tab => tab.id === 'captureText'); + if (textTabIndex !== -1) { + setActiveTab(textTabIndex); + } + }, 100); + } else if (switchToScreenshotTab) { + setTimeout(() => { + const screenshotTabIndex = getAvailableTabs().findIndex(tab => tab.id === 'captureScreenshot'); + if (screenshotTabIndex !== -1) { + setActiveTab(screenshotTabIndex); + const latestIndex = screenshotData.length - 1; + setActiveScreenshotTab(latestIndex); + + if (!autoFocusedScreenshotIndices.current.has(latestIndex)) { + autoFocusedScreenshotIndices.current.add(latestIndex); + setTimeout(() => { + const screenshotSteps = browserSteps.filter(step => step.type === 'screenshot') as Array<{ id: number; name?: string; type: 'screenshot' }>; + const latestScreenshotStep = screenshotSteps[latestIndex]; + if (latestScreenshotStep) { + const screenshotName = latestScreenshotStep.name || `Screenshot ${latestIndex + 1}`; + handleStartEditScreenshotName(latestScreenshotStep.id, screenshotName); + } + }, 300); + } + } + }, 100); + } + } + }, [hasScrapeListAction, hasScrapeSchemaAction, hasScreenshotAction, captureListData, captureTextData, screenshotData, setIsOpen, getText]); + + useEffect(() => { + if (captureListData.length > 0 && isOpen && captureStage === 'initial') { + const latestListIndex = captureListData.length - 1; + const latestList = captureListData[latestListIndex]; + if (latestList && latestList.data && latestList.data.length > 0 && !editingListName) { + const previousLength = previousDataLengths.current.get(latestList.id) || 0; + const currentLength = latestList.data.length; + + if (previousLength === 0 && currentLength > 0) { + if (!autoFocusedListIds.current.has(latestList.id)) { + autoFocusedListIds.current.add(latestList.id); + setActiveListTab(latestListIndex); + setTimeout(() => { + handleStartEditListName(latestList.id, latestList.name || `List Data ${latestListIndex + 1}`); + }, 300); + } + } + + previousDataLengths.current.set(latestList.id, currentLength); + } + } + }, [captureListData.length, isOpen, captureStage]); + + useEffect(() => { + if (screenshotData.length > 0 && isOpen) { + const latestScreenshotIndex = screenshotData.length - 1; + setActiveScreenshotTab(latestScreenshotIndex); + } + }, [screenshotData.length, isOpen]); const { darkMode } = useThemeMode(); - const getCaptureTextColumns = captureTextData.length > 0 ? Object.keys(captureTextData[0]) : []; - const shouldShowTabs = availableTabs.length > 1; const getSingleContentType = () => { @@ -193,10 +484,10 @@ export const InterpretationLog: React.FC = ({ isOpen, se onOpen={toggleDrawer(true)} PaperProps={{ sx: { - background: `${darkMode ? '#1e2124' : 'white'}`, + background: `${darkMode ? '#1d1c1cff' : 'white'}`, color: `${darkMode ? 'white' : 'black'}`, padding: '10px', - height: outputPreviewHeight, + height: "calc(100% - 140px)", width: outputPreviewWidth, display: 'flex', flexDirection: 'column', @@ -208,16 +499,16 @@ export const InterpretationLog: React.FC = ({ isOpen, se {t('interpretation_log.titles.output_preview')} - - {showPreviewData && availableTabs.length > 0 ? ( + + {showPreviewData && availableTabs.length > 0 && ( <> {shouldShowTabs && ( - {availableTabs.map((tab, index) => ( @@ -228,15 +519,14 @@ export const InterpretationLog: React.FC = ({ isOpen, se px: 4, py: 2, cursor: 'pointer', - borderBottom: activeTab === index ? '2px solid' : 'none', borderColor: activeTab === index ? (darkMode ? '#ff00c3' : '#ff00c3') : 'transparent', - backgroundColor: activeTab === index ? (darkMode ? '#34404d' : '#e9ecef') : 'transparent', + backgroundColor: activeTab === index ? (darkMode ? '#121111ff' : '#e9ecef') : 'transparent', color: darkMode ? 'white' : 'black', fontWeight: activeTab === index ? 500 : 400, textAlign: 'center', position: 'relative', '&:hover': { - backgroundColor: activeTab !== index ? (darkMode ? '#303b49' : '#e2e6ea') : undefined + backgroundColor: activeTab !== index ? (darkMode ? '#121111ff' : '#e2e6ea') : undefined } }} > @@ -247,79 +537,248 @@ export const InterpretationLog: React.FC = ({ isOpen, se ))} )} - - - {(activeTab === availableTabs.findIndex(tab => tab.id === 'captureList') || singleContentType === 'captureList') && captureListData.length > 0 && ( - - - - {`Table ${captureListPage + 1} of ${captureListData.length}`} - - - - - - - - - - - {Object.values(captureListData[captureListPage]?.fields || {}).map((field: any, index) => ( - + {(activeTab === availableTabs.findIndex(tab => tab.id === 'captureList') || + singleContentType === 'captureList') && + captureListData.length > 0 && ( + + + {captureListData.map((listItem, index) => { + const isEditing = editingListName === listItem.id; + const isActive = activeListTab === index; + + return ( + + { + if (!isEditing) { + setActiveListTab(index); + } + }} + onDoubleClick={() => { + handleStartEditListName( + listItem.id, + listItem.name || `List Data ${index + 1}` + ); + }} + sx={{ + px: 3, + py: 1.25, + cursor: isEditing ? 'text' : 'pointer', + borderRadius: '8px 8px 0 0', + backgroundColor: darkMode + ? '#131313ff' + : '#ffffff', + color: isActive + ? darkMode + ? '#ffffff' + : '#000000' + : darkMode + ? '#b0b0b0' + : '#555555', + fontWeight: isActive ? 600 : 400, + fontSize: '0.875rem', + border: '1px solid', + borderColor: darkMode ? '#2a2a2a' : '#d0d0d0', + borderBottom: isActive + ? darkMode + ? '2px solid #1c1c1c' + : '2px solid #ffffff' + : '2px solid transparent', + transition: 'all 0.2s ease', + '&:hover': { + backgroundColor: isActive + ? undefined + : darkMode + ? '#161616' + : '#e9ecef', + }, }} > - {field.label} - - ))} + {isEditing ? ( + setEditingListNameValue(e.target.value)} + onBlur={handleSaveListName} + onKeyDown={(e) => { + if (e.key === 'Enter') handleSaveListName(); + if (e.key === 'Escape') { + setEditingListName(null); + setEditingListNameValue(''); + } + }} + autoFocus + size="small" + variant="standard" + sx={{ + minWidth: '120px', + '& .MuiInputBase-input': { + color: darkMode ? '#fff' : '#000', + fontSize: 'inherit', + fontWeight: 'inherit', + padding: 0, + }, + '& .MuiInput-underline:before': { display: 'none' }, + '& .MuiInput-underline:after': { display: 'none' }, + '& .MuiInput-underline:hover:before': { display: 'none' }, + }} + /> + ) : ( + listItem.name || `List Data ${index + 1}` + )} + + + ); + })} + + + +
+ + + {Object.entries(captureListData[activeListTab]?.fields || {}).map(([fieldKey, field]: [string, any]) => { + const isEditing = editingField?.listId === captureListData[activeListTab]?.id && editingField?.fieldKey === fieldKey; + + return ( + + {isEditing ? ( + + setEditingValue(e.target.value)} + onBlur={handleSaveEdit} + onKeyDown={(e) => { + if (e.key === 'Enter') handleSaveEdit(); + if (e.key === 'Escape') handleCancelEdit(); + }} + autoFocus + size="small" + sx={{ + flex: 1, + minWidth: '150px', + '& .MuiInputBase-root': { + backgroundColor: darkMode ? '#2a2929' : '#fff' + } + }} + /> + + + + + ) : ( + + + handleStartEdit(captureListData[activeListTab]?.id, fieldKey, field.label)} + > + {field.label} + + + handleDeleteField(captureListData[activeListTab]?.id, fieldKey)} + sx={{ + position: 'absolute', + right: 4, + top: '50%', + transform: 'translateY(-50%)', + opacity: 0, + transition: 'opacity 0.2s', + color: darkMode ? '#999' : '#666', + padding: '4px', + '&:hover': { + color: '#f44336', + backgroundColor: darkMode ? 'rgba(244, 67, 54, 0.1)' : 'rgba(244, 67, 54, 0.05)' + } + }} + > + + + + )} + + ); + })} - {(captureListData[captureListPage]?.data || []) - .slice(0, Math.min(captureListData[captureListPage]?.limit || 10, 5)) + {(captureListData[activeListTab]?.data || []) + .slice(0, Math.min(captureListData[activeListTab]?.limit || 10, 5)) .map((row: any, rowIndex: any) => ( - - {Object.values(captureListData[captureListPage]?.fields || {}).map((field: any, colIndex) => ( - ( + - {row[field.label]} + {typeof row[field.label] === 'object' ? JSON.stringify(row[field.label]) : String(row[field.label] || '')} ))} @@ -328,76 +787,237 @@ export const InterpretationLog: React.FC = ({ isOpen, se
+
+ )} + + + {(activeTab === availableTabs.findIndex(tab => tab.id === 'captureScreenshot') || singleContentType === 'captureScreenshot') && screenshotData.length > 0 && ( + + + {(() => { + const screenshotSteps = browserSteps.filter(step => step.type === 'screenshot' && step.screenshotData) as Array<{ id: number; name?: string; type: 'screenshot'; screenshotData?: string }>; + return screenshotData.map((screenshot, index) => { + const screenshotStep = screenshotSteps[index]; + if (!screenshotStep) return null; + + const isActive = activeScreenshotTab === index; + const isEditing = editingScreenshotName === screenshotStep.id; + const screenshotName = screenshotStep.name || `Screenshot ${index + 1}`; + + return ( + + { + if (!isEditing) { + setActiveScreenshotTab(index); + } + }} + onDoubleClick={() => { + handleStartEditScreenshotName(screenshotStep.id, screenshotName); + }} + sx={{ + px: 3, + py: 1.25, + cursor: isEditing ? 'text' : 'pointer', + borderRadius: '8px 8px 0 0', + backgroundColor: darkMode ? '#131313ff' : '#ffffff', + color: isActive + ? darkMode + ? '#ffffff' + : '#000000' + : darkMode + ? '#b0b0b0' + : '#555555', + fontWeight: isActive ? 600 : 400, + fontSize: '0.875rem', + border: '1px solid', + borderColor: darkMode ? '#2a2a2a' : '#d0d0d0', + borderBottom: isActive + ? darkMode + ? '2px solid #1c1c1c' + : '2px solid #ffffff' + : '2px solid transparent', + transition: 'all 0.2s ease', + '&:hover': { + backgroundColor: isActive + ? undefined + : darkMode + ? '#161616' + : '#e9ecef', + }, + }} + > + {isEditing ? ( + setEditingScreenshotNameValue(e.target.value)} + onBlur={handleSaveScreenshotName} + onKeyDown={(e) => { + if (e.key === 'Enter') handleSaveScreenshotName(); + if (e.key === 'Escape') { + setEditingScreenshotName(null); + setEditingScreenshotNameValue(''); + } + }} + autoFocus + size="small" + variant="standard" + sx={{ + minWidth: '120px', + '& .MuiInputBase-input': { + color: darkMode ? '#fff' : '#000', + fontSize: 'inherit', + fontWeight: 'inherit', + padding: 0, + }, + '& .MuiInput-underline:before': { display: 'none' }, + '& .MuiInput-underline:after': { display: 'none' }, + '& .MuiInput-underline:hover:before': { display: 'none' }, + }} + /> + ) : ( + screenshotName + )} + + + ); + }); + })()} + + + + {`Screenshot + )} - {(activeTab === availableTabs.findIndex(tab => tab.id === 'captureScreenshot') || singleContentType === 'captureScreenshot') && screenshotData.length > 0 && ( - - {screenshotData.length > 1 && ( - - - {`Screenshot ${screenshotPage + 1} of ${screenshotData.length}`} - - - - - - - )} - {screenshotData.length > 0 && ( - - - {t('interpretation_log.titles.screenshot')} {screenshotPage + 1} - - {`${t('interpretation_log.titles.screenshot')} - - )} - - )} - {(activeTab === availableTabs.findIndex(tab => tab.id === 'captureText') || singleContentType === 'captureText') && captureTextData.length > 0 && ( - - - + + + + + {editingTextGroupName ? ( + setEditingTextGroupNameValue(e.target.value)} + onBlur={handleSaveTextGroupName} + onKeyDown={(e) => { + if (e.key === 'Enter') handleSaveTextGroupName(); + if (e.key === 'Escape') { + setEditingTextGroupName(false); + setEditingTextGroupNameValue(currentTextGroupName); + } + }} + autoFocus + size="small" + variant="standard" + sx={{ + minWidth: '120px', + '& .MuiInputBase-input': { + color: darkMode ? '#fff' : '#000', + fontSize: 'inherit', + fontWeight: 'inherit', + padding: 0, + }, + '& .MuiInput-underline:before': { display: 'none' }, + '& .MuiInput-underline:after': { display: 'none' }, + '& .MuiInput-underline:hover:before': { display: 'none' }, + }} + /> + ) : ( + currentTextGroupName + )} + + + + + +
- Label - Value @@ -405,33 +1025,112 @@ export const InterpretationLog: React.FC = ({ isOpen, se - {getCaptureTextColumns.map((column, index) => ( - - { + const isEditing = editingTextLabel === textStep.id; + + return ( + - {column} - - - {captureTextData[0][column]} - - - ))} + + {isEditing ? ( + + setEditingTextLabelValue(e.target.value)} + onBlur={handleSaveTextLabel} + onKeyDown={(e) => { + if (e.key === 'Enter') handleSaveTextLabel(); + if (e.key === 'Escape') handleCancelTextLabel(); + }} + autoFocus + size="small" + sx={{ + flex: 1, + minWidth: '150px', + '& .MuiInputBase-root': { + backgroundColor: darkMode ? '#2a2929' : '#fff' + } + }} + /> + + + + + ) : ( + + + handleStartEditTextLabel(textStep.id, textStep.label)} + > + {textStep.label} + + + handleDeleteTextStep(textStep.id)} + sx={{ + position: 'absolute', + right: 4, + top: '50%', + transform: 'translateY(-50%)', + opacity: 0, + transition: 'opacity 0.2s', + color: darkMode ? '#999' : '#666', + padding: '4px', + '&:hover': { + color: '#f44336', + backgroundColor: darkMode ? 'rgba(244, 67, 54, 0.1)' : 'rgba(244, 67, 54, 0.05)' + } + }} + > + + + + )} + + + {typeof textStep.data === 'object' ? JSON.stringify(textStep.data) : String(textStep.data || '')} + + + ); + })}
@@ -439,27 +1138,10 @@ export const InterpretationLog: React.FC = ({ isOpen, se )}
- ) : ( - - - {hasScrapeListAction || hasScrapeSchemaAction || hasScreenshotAction ? ( - <> - - {t('interpretation_log.messages.successful_training')} - - setShowPreviewData(true)} /> - - ) : ( - - {t('interpretation_log.messages.no_selection')} - - )} - - )}
); -}; \ No newline at end of file +}; diff --git a/src/components/run/RunContent.tsx b/src/components/run/RunContent.tsx index b23af630..ceffc025 100644 --- a/src/components/run/RunContent.tsx +++ b/src/components/run/RunContent.tsx @@ -8,16 +8,13 @@ import { CircularProgress, Accordion, AccordionSummary, - AccordionDetails, - ButtonGroup + AccordionDetails } from "@mui/material"; import Highlight from "react-highlight"; import * as React from "react"; import { Data } from "./RunsTable"; import { TabPanel, TabContext } from "@mui/lab"; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import ArrowBackIcon from '@mui/icons-material/ArrowBack'; -import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; import { useEffect, useState } from "react"; import Table from '@mui/material/Table'; import TableBody from '@mui/material/TableBody'; @@ -43,21 +40,25 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe const [schemaData, setSchemaData] = useState([]); const [schemaColumns, setSchemaColumns] = useState([]); + const [schemaKeys, setSchemaKeys] = useState([]); + const [schemaDataByKey, setSchemaDataByKey] = useState>({}); + const [schemaColumnsByKey, setSchemaColumnsByKey] = useState>({}); const [isSchemaTabular, setIsSchemaTabular] = useState(false); const [listData, setListData] = useState([]); const [listColumns, setListColumns] = useState([]); + const [listKeys, setListKeys] = useState([]); const [currentListIndex, setCurrentListIndex] = useState(0); const [screenshotKeys, setScreenshotKeys] = useState([]); + const [screenshotKeyMap, setScreenshotKeyMap] = useState>({}); const [currentScreenshotIndex, setCurrentScreenshotIndex] = useState(0); + const [currentSchemaIndex, setCurrentSchemaIndex] = useState(0); const [legacyData, setLegacyData] = useState([]); const [legacyColumns, setLegacyColumns] = useState([]); const [isLegacyData, setIsLegacyData] = useState(false); - const { darkMode } = useThemeMode(); - useEffect(() => { setTab(tab); }, [interpretationInProgress]); @@ -66,8 +67,12 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe if (row.status === 'running' || row.status === 'queued' || row.status === 'scheduled') { setSchemaData([]); setSchemaColumns([]); + setSchemaKeys([]); + setSchemaDataByKey({}); + setSchemaColumnsByKey({}); setListData([]); setListColumns([]); + setListKeys([]); setLegacyData([]); setLegacyColumns([]); setIsLegacyData(false); @@ -77,10 +82,11 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe if (!row.serializableOutput) return; - if (!row.serializableOutput.scrapeSchema && - !row.serializableOutput.scrapeList && - Object.keys(row.serializableOutput).length > 0) { + const hasLegacySchema = row.serializableOutput.scrapeSchema && Array.isArray(row.serializableOutput.scrapeSchema); + const hasLegacyList = row.serializableOutput.scrapeList && Array.isArray(row.serializableOutput.scrapeList); + const hasOldFormat = !row.serializableOutput.scrapeSchema && !row.serializableOutput.scrapeList && Object.keys(row.serializableOutput).length > 0; + if (hasLegacySchema || hasLegacyList || hasOldFormat) { setIsLegacyData(true); processLegacyData(row.serializableOutput); return; @@ -100,44 +106,106 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe useEffect(() => { if (row.status === 'running' || row.status === 'queued' || row.status === 'scheduled') { setScreenshotKeys([]); + setScreenshotKeyMap({}); setCurrentScreenshotIndex(0); return; } if (row.binaryOutput && Object.keys(row.binaryOutput).length > 0) { - setScreenshotKeys(Object.keys(row.binaryOutput)); + const rawKeys = Object.keys(row.binaryOutput); + + const isLegacyPattern = rawKeys.every(key => /^item-\d+-\d+$/.test(key)); + + if (isLegacyPattern) { + const renamedKeys = rawKeys.map((_, index) => `Screenshot ${index + 1}`); + const keyMap: Record = {}; + + renamedKeys.forEach((displayName, index) => { + keyMap[displayName] = rawKeys[index]; + }); + + setScreenshotKeys(renamedKeys); + setScreenshotKeyMap(keyMap); + } else { + const keyMap: Record = {}; + rawKeys.forEach(key => { + keyMap[key] = key; + }); + + setScreenshotKeys(rawKeys); + setScreenshotKeyMap(keyMap); + } + setCurrentScreenshotIndex(0); } else { setScreenshotKeys([]); + setScreenshotKeyMap({}); setCurrentScreenshotIndex(0); } }, [row.binaryOutput, row.status]); const processLegacyData = (legacyOutput: Record) => { - let allData: any[] = []; + const convertedSchema: Record = {}; + const convertedList: Record = {}; - Object.keys(legacyOutput).forEach(key => { + const keys = Object.keys(legacyOutput); + + keys.forEach((key) => { const data = legacyOutput[key]; + if (Array.isArray(data)) { - const filteredData = data.filter(row => - Object.values(row).some(value => value !== undefined && value !== "") - ); - allData = [...allData, ...filteredData]; + const isNestedArray = data.length > 0 && Array.isArray(data[0]); + + if (isNestedArray) { + data.forEach((subArray, index) => { + if (Array.isArray(subArray) && subArray.length > 0) { + const filteredData = subArray.filter(row => + row && typeof row === 'object' && Object.values(row).some(value => value !== undefined && value !== "") + ); + + if (filteredData.length > 0) { + const autoName = `List ${Object.keys(convertedList).length + 1}`; + convertedList[autoName] = filteredData; + } + } + }); + } else { + const filteredData = data.filter(row => + row && typeof row === 'object' && Object.values(row).some(value => value !== undefined && value !== "") + ); + + if (filteredData.length > 0) { + const schemaCount = Object.keys(convertedSchema).length; + const autoName = `Text ${schemaCount + 1}`; + convertedSchema[autoName] = filteredData; + } + } } }); - if (allData.length > 0) { - const allColumns = new Set(); - allData.forEach(item => { - Object.keys(item).forEach(key => allColumns.add(key)); - }); + if (Object.keys(convertedSchema).length === 1) { + const singleKey = Object.keys(convertedSchema)[0]; + const singleData = convertedSchema[singleKey]; + delete convertedSchema[singleKey]; + convertedSchema["Texts"] = singleData; + } - setLegacyData(allData); - setLegacyColumns(Array.from(allColumns)); + if (Object.keys(convertedSchema).length > 0) { + processSchemaData(convertedSchema); + } + + if (Object.keys(convertedList).length > 0) { + processScrapeList(convertedList); } }; const processSchemaData = (schemaOutput: any) => { + const keys = Object.keys(schemaOutput); + setSchemaKeys(keys); + + const dataByKey: Record = {}; + const columnsByKey: Record = {}; + if (Array.isArray(schemaOutput)) { const filteredData = schemaOutput.filter(row => row && Object.values(row).some(value => value !== undefined && value !== "") @@ -156,41 +224,32 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe } } - if (schemaOutput['schema-tabular']) { - const tabularData = schemaOutput['schema-tabular']; - if (Array.isArray(tabularData) && tabularData.length > 0) { - const filteredData = tabularData.filter(row => - Object.values(row).some(value => value !== undefined && value !== "") - ); - - if (filteredData.length > 0) { - const allColumns = new Set(); - filteredData.forEach(item => { - Object.keys(item).forEach(key => allColumns.add(key)); - }); - - setSchemaData(filteredData); - setSchemaColumns(Array.from(allColumns)); - setIsSchemaTabular(true); - return; - } - } - } - let allData: any[] = []; let hasMultipleEntries = false; - Object.keys(schemaOutput).forEach(key => { + keys.forEach(key => { const data = schemaOutput[key]; if (Array.isArray(data)) { const filteredData = data.filter(row => Object.values(row).some(value => value !== undefined && value !== "") ); + + dataByKey[key] = filteredData; + + const columnsForKey = new Set(); + filteredData.forEach(item => { + Object.keys(item).forEach(col => columnsForKey.add(col)); + }); + columnsByKey[key] = Array.from(columnsForKey); + allData = [...allData, ...filteredData]; if (filteredData.length > 1) hasMultipleEntries = true; } }); + setSchemaDataByKey(dataByKey); + setSchemaColumnsByKey(columnsByKey); + if (allData.length > 0) { const allColumns = new Set(); allData.forEach(item => { @@ -206,42 +265,22 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe const processScrapeList = (scrapeListData: any) => { const tablesList: any[][] = []; const columnsList: string[][] = []; + const keys: string[] = []; - if (Array.isArray(scrapeListData)) { - scrapeListData.forEach(tableData => { - if (Array.isArray(tableData) && tableData.length > 0) { - const filteredData = tableData.filter(row => - Object.values(row).some(value => value !== undefined && value !== "") - ); - - if (filteredData.length > 0) { - tablesList.push(filteredData); - - const tableColumns = new Set(); - filteredData.forEach(item => { - Object.keys(item).forEach(key => tableColumns.add(key)); - }); - - columnsList.push(Array.from(tableColumns)); - } - } - }); - } else if (typeof scrapeListData === 'object') { + if (typeof scrapeListData === 'object') { Object.keys(scrapeListData).forEach(key => { const tableData = scrapeListData[key]; if (Array.isArray(tableData) && tableData.length > 0) { const filteredData = tableData.filter(row => Object.values(row).some(value => value !== undefined && value !== "") ); - if (filteredData.length > 0) { tablesList.push(filteredData); - + keys.push(key); const tableColumns = new Set(); filteredData.forEach(item => { Object.keys(item).forEach(key => tableColumns.add(key)); }); - columnsList.push(Array.from(tableColumns)); } } @@ -250,6 +289,7 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe setListData(tablesList); setListColumns(columnsList); + setListKeys(keys); setCurrentListIndex(0); }; @@ -308,21 +348,6 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe }, 100); }; - const navigateListTable = (direction: 'next' | 'prev') => { - if (direction === 'next' && currentListIndex < listData.length - 1) { - setCurrentListIndex(currentListIndex + 1); - } else if (direction === 'prev' && currentListIndex > 0) { - setCurrentListIndex(currentListIndex - 1); - } - }; - - const navigateScreenshots = (direction: 'next' | 'prev') => { - if (direction === 'next' && currentScreenshotIndex < screenshotKeys.length - 1) { - setCurrentScreenshotIndex(currentScreenshotIndex + 1); - } else if (direction === 'prev' && currentScreenshotIndex > 0) { - setCurrentScreenshotIndex(currentScreenshotIndex - 1); - } - }; const renderDataTable = ( data: any[], @@ -333,15 +358,127 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe isPaginatedList: boolean = false, isSchemaData: boolean = false ) => { - if (!isPaginatedList && data.length === 0) return null; - if (isPaginatedList && (listData.length === 0 || currentListIndex >= listData.length)) return null; + if (data.length === 0) return null; - const currentData = isPaginatedList ? listData[currentListIndex] : data; - const currentColumns = isPaginatedList ? listColumns[currentListIndex] : columns; + const shouldShowAsKeyValue = isSchemaData && !isSchemaTabular && data.length === 1; - if (!currentData || currentData.length === 0) return null; + if (title === '') { + return ( + <> + + + + + + {shouldShowAsKeyValue ? ( + <> + theme.palette.mode === 'dark' ? '#11111' : '#f8f9fa' + }} + > + Label + + theme.palette.mode === 'dark' ? '#11111' : '#f8f9fa' + }} + > + Value + + + ) : ( + columns.map((column) => ( + theme.palette.mode === 'dark' ? '#11111' : '#f8f9fa' + }} + > + {column} + + )) + )} + + + + {shouldShowAsKeyValue ? ( + columns.map((column) => ( + + + {column} + + + {data[0][column] === undefined || data[0][column] === "" + ? "-" + : (typeof data[0][column] === 'object' + ? JSON.stringify(data[0][column]) + : String(data[0][column]))} + + + )) + ) : ( + data.map((row, index) => ( + + {columns.map((column) => ( + + {row[column] === undefined || row[column] === "" + ? "-" + : (typeof row[column] === 'object' + ? JSON.stringify(row[column]) + : String(row[column]))} + + ))} + + )) + )} + +
+
+
+ + + - const shouldShowAsKeyValue = isSchemaData && !isSchemaTabular && currentData.length === 1; + + + + + ); + } return ( @@ -359,11 +496,11 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe - - - - {isPaginatedList && listData.length > 1 && ( - - - - - )} @@ -434,33 +540,27 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe {shouldShowAsKeyValue ? ( <> - theme.palette.mode === 'dark' ? '#11111' : '#f8f9fa' }} > Label - theme.palette.mode === 'dark' ? '#11111' : '#f8f9fa' }} > Value ) : ( - (isPaginatedList ? currentColumns : columns).map((column) => ( - ( + theme.palette.mode === 'dark' ? '#11111' : '#f8f9fa' }} > {column} @@ -471,24 +571,30 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe {shouldShowAsKeyValue ? ( - // Single schema entry - show as key-value pairs - currentColumns.map((column) => ( + columns.map((column) => ( {column} - {currentData[0][column] === undefined || currentData[0][column] === "" ? "-" : currentData[0][column]} + {data[0][column] === undefined || data[0][column] === "" + ? "-" + : (typeof data[0][column] === 'object' + ? JSON.stringify(data[0][column]) + : String(data[0][column]))} )) ) : ( - // Multiple entries or list data - show as table - currentData.map((row, index) => ( + data.map((row, index) => ( - {(isPaginatedList ? currentColumns : columns).map((column) => ( + {columns.map((column) => ( - {row[column] === undefined || row[column] === "" ? "-" : row[column]} + {row[column] === undefined || row[column] === "" + ? "-" + : (typeof row[column] === 'object' + ? JSON.stringify(row[column]) + : String(row[column]))} ))} @@ -601,23 +707,201 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe {!isLegacyData && ( <> - {renderDataTable( - schemaData, - schemaColumns, - t('run_content.captured_data.schema_title', 'Captured Texts'), - 'schema_data.csv', - 'schema_data.json', - false, - true + {schemaData.length > 0 && ( + + }> + + + {t('run_content.captured_data.schema_title', 'Captured Texts')} + + + + + {schemaKeys.length > 0 && ( + + {schemaKeys.map((key, idx) => ( + setCurrentSchemaIndex(idx)} + sx={{ + px: 3, + py: 1, + cursor: 'pointer', + backgroundColor: + currentSchemaIndex === idx + ? (theme) => theme.palette.mode === 'dark' + ? '#121111ff' + : '#e9ecef' + : 'transparent', + fontWeight: currentSchemaIndex === idx ? 600 : 400, + color: (theme) => theme.palette.mode === 'dark' ? '#fff' : '#000', + }} + > + {key} + + ))} + + )} + + {renderDataTable( + schemaDataByKey[schemaKeys[currentSchemaIndex]] || schemaData, + schemaColumnsByKey[schemaKeys[currentSchemaIndex]] || schemaColumns, + '', + `${schemaKeys[currentSchemaIndex] || 'schema_data'}.csv`, + `${schemaKeys[currentSchemaIndex] || 'schema_data'}.json`, + false, + true + )} + + )} - {listData.length > 0 && renderDataTable( - [], - [], - t('run_content.captured_data.list_title', 'Captured Lists'), - 'list_data.csv', - 'list_data.json', - true + {listData.length > 0 && ( + + }> + + + {t('run_content.captured_data.list_title', 'Captured Lists')} + + + + + + {listKeys.map((key, idx) => ( + setCurrentListIndex(idx)} + sx={{ + px: 3, + py: 1, + cursor: 'pointer', + backgroundColor: + currentListIndex === idx + ? (theme) => theme.palette.mode === 'dark' + ? '#121111ff' + : '#e9ecef' + : 'transparent', + fontWeight: currentListIndex === idx ? 600 : 400, + color: (theme) => theme.palette.mode === 'dark' ? '#fff' : '#000', + }} + > + {key} + + ))} + + + +
+ + + {(listColumns[currentListIndex] || []).map((column) => ( + theme.palette.mode === 'dark' ? '#11111' : '#f8f9fa' + }} + > + {column} + + ))} + + + + + {(listData[currentListIndex] || []).map((rowItem, idx) => ( + + {(listColumns[currentListIndex] || []).map((column) => ( + + {rowItem[column] === undefined || rowItem[column] === '' + ? '-' + : typeof rowItem[column] === 'object' + ? JSON.stringify(rowItem[column]) + : String(rowItem[column])} + + ))} + + ))} + +
+
+ + + + + + + + +
+
)} )} @@ -625,102 +909,73 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe )} {hasScreenshots && ( - <> - - } - aria-controls="screenshot-content" - id="screenshot-header" + + }> + + + {t('run_content.captured_screenshot.title', 'Captured Screenshots')} + + + + + - - - {t('run_content.captured_screenshot.title', 'Screenshots')} - - - - - - - - {screenshotKeys.length > 1 && ( - - - - - )} - - - - - {`Screenshot + {key} + + ))} - - - - + )} + + + + {screenshotKeys.length > 0 && ( + {`Screenshot + )} + + + )} diff --git a/src/context/browserSteps.tsx b/src/context/browserSteps.tsx index 0931813e..de7a6460 100644 --- a/src/context/browserSteps.tsx +++ b/src/context/browserSteps.tsx @@ -1,4 +1,6 @@ -import React, { createContext, useContext, useState } from 'react'; +import React, { createContext, useContext, useEffect, useRef, useState } from 'react'; +import { useSocketStore } from "./socket"; +import { useGlobalInfoStore } from "./globalInfo"; export interface TextStep { id: number; @@ -8,11 +10,13 @@ export interface TextStep { isShadow?: boolean; selectorObj: SelectorObject; actionId?: string; + name?: string; } -interface ScreenshotStep { +export interface ScreenshotStep { id: number; type: 'screenshot'; + name?: string; fullPage: boolean; actionId?: string; screenshotData?: string; @@ -21,6 +25,7 @@ interface ScreenshotStep { export interface ListStep { id: number; type: 'list'; + name?: string; listSelector: string; isShadow?: boolean; fields: { [key: string]: TextStep }; @@ -31,112 +36,331 @@ export interface ListStep { }; limit?: number; actionId?: string; + data?: any[]; } export type BrowserStep = TextStep | ScreenshotStep | ListStep; export interface SelectorObject { selector: string; + isShadow?: boolean; tag?: string; attribute?: string; - isShadow?: boolean; [key: string]: any; } interface BrowserStepsContextType { browserSteps: BrowserStep[]; - addTextStep: (label: string, data: string, selectorObj: SelectorObject, actionId: string) => void; - addListStep: (listSelector: string, fields: { [key: string]: TextStep }, listId: number, actionId: string, pagination?: { type: string; selector: string, isShadow?: boolean }, limit?: number, isShadow?: boolean) => void + addTextStep: ( + label: string, + data: string, + selectorObj: SelectorObject, + actionId: string + ) => void; + addListStep: ( + listSelector: string, + fields: { [key: string]: TextStep }, + listId: number, + actionId: string, + pagination?: { + type: string; + selector: string; + isShadow?: boolean; + }, + limit?: number, + isShadow?: boolean + ) => void; addScreenshotStep: (fullPage: boolean, actionId: string) => void; deleteBrowserStep: (id: number) => void; updateBrowserTextStepLabel: (id: number, newLabel: string) => void; - updateListTextFieldLabel: (listId: number, fieldKey: string, newLabel: string) => void; + updateListTextFieldLabel: ( + listId: number, + fieldKey: string, + newLabel: string + ) => void; updateListStepLimit: (listId: number, limit: number) => void; updateListStepData: (listId: number, extractedData: any[]) => void; + updateListStepName: (listId: number, name: string) => void; + updateScreenshotStepName: (id: number, name: string) => void; removeListTextField: (listId: number, fieldKey: string) => void; deleteStepsByActionId: (actionId: string) => void; updateScreenshotStepData: (id: number, screenshotData: string) => void; + emitActionForStep: (step: BrowserStep) => void; + emitForStepId: (actionId: string, nameOverride?: string) => void; } const BrowserStepsContext = createContext(undefined); export const BrowserStepsProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const { socket } = useSocketStore(); + const { currentTextGroupName } = useGlobalInfoStore(); const [browserSteps, setBrowserSteps] = useState([]); const [discardedFields, setDiscardedFields] = useState>(new Set()); + const browserStepsRef = useRef(browserSteps); + useEffect(() => { + browserStepsRef.current = browserSteps; + }, [browserSteps]); + + const currentTextGroupNameRef = useRef(currentTextGroupName); + + useEffect(() => { + currentTextGroupNameRef.current = currentTextGroupName; + }, [currentTextGroupName]); + + const getListSettingsObject = (listStep: ListStep) => { + const fields: Record = {}; + + Object.entries(listStep.fields).forEach(([id, field]) => { + if (field.selectorObj?.selector) { + fields[field.label] = { + selector: field.selectorObj.selector, + tag: field.selectorObj.tag, + attribute: field.selectorObj.attribute, + isShadow: field.selectorObj.isShadow + }; + } + }); + + return { + listSelector: listStep.listSelector, + fields: fields, + pagination: { + type: listStep.pagination?.type || "", + selector: listStep.pagination?.selector, + isShadow: listStep.isShadow + }, + limit: listStep.limit, + isShadow: listStep.isShadow + }; + }; + + const emitActionForStep = (step: BrowserStep) => { + if (!socket) return; + if (!step.actionId) return; + if (!socket.connected) return; + + let action = ""; + let settings: any = {}; + + // Always read the latest steps from the ref to prevent stale data + const latestSteps = browserStepsRef.current; + + if (step.type === "list") { + action = "scrapeList"; + const baseSettings = getListSettingsObject(step); + settings = { + ...baseSettings, + name: step.name || `List Data ${latestSteps.filter(s => s.type === "list").length}`, + }; + + } else if (step.type === "text") { + action = "scrapeSchema"; + + const freshTextSteps = latestSteps.filter( + (s): s is TextStep => s.type === "text" && s.actionId === step.actionId + ); + + // Build schema settings from text steps + const fieldSettings: Record< + string, + { + selector: string; + tag?: string; + [key: string]: any; + } + > = {}; + + freshTextSteps.forEach((textStep) => { + if (textStep.selectorObj?.selector && textStep.label) { + fieldSettings[textStep.label] = { + selector: textStep.selectorObj.selector, + tag: textStep.selectorObj.tag, + attribute: textStep.selectorObj.attribute, + isShadow: textStep.selectorObj.isShadow, + }; + } + }); + + settings = { + ...fieldSettings, + name: currentTextGroupNameRef.current || "Text Data", + }; + + } else if (step.type === "screenshot") { + action = "screenshot"; + + const freshScreenshot = latestSteps.find( + (s) => s.type === "screenshot" && s.actionId === step.actionId + ) as ScreenshotStep | undefined; + + settings = { + name: + step.name || + freshScreenshot?.name || + `Screenshot ${latestSteps.filter((s) => s.type === "screenshot").length}`, + type: "png", + caret: "hide", + scale: "device", + timeout: 30000, + fullPage: freshScreenshot?.fullPage ?? step.fullPage ?? true, + animations: "allow", + }; + } + + socket.emit("action", { action, actionId: step.actionId, settings }); + }; + + const emitForStepId = (actionId: string, nameOverride?: string) => { + const step = browserStepsRef.current.find(s => s.actionId === actionId); + if (!step) return; + + let enrichedStep = { ...step }; + + if (step.type === "text") { + enrichedStep = { ...step, name: currentTextGroupNameRef.current }; + } + + if (step.type === "screenshot") { + const freshScreenshot = browserStepsRef.current.find( + s => s.type === "screenshot" && s.actionId === actionId + ) as ScreenshotStep | undefined; + + if (freshScreenshot) { + enrichedStep = { ...freshScreenshot }; + + if (nameOverride && freshScreenshot.name !== nameOverride) { + enrichedStep.name = nameOverride; + browserStepsRef.current = browserStepsRef.current.map(s => + s.id === freshScreenshot.id ? { ...s, name: nameOverride } : s + ); + setBrowserSteps(prev => + prev.map(s => + s.id === freshScreenshot.id ? { ...s, name: nameOverride } : s + ) + ); + } + } + } + + if (step.type === "list") { + const freshList = browserStepsRef.current.find( + s => s.type === "list" && s.actionId === actionId + ) as ListStep | undefined; + + if (freshList) { + enrichedStep = { ...freshList }; + } + } + + emitActionForStep(enrichedStep); + }; + const addTextStep = (label: string, data: string, selectorObj: SelectorObject, actionId: string) => { - setBrowserSteps(prevSteps => [ - ...prevSteps, - { id: Date.now(), type: 'text', label, data, selectorObj, actionId } - ]); + setBrowserSteps((prevSteps) => { + const textCount = prevSteps.filter(s => s.type === 'text').length + 1; + const generatedLabel = label || `Label ${textCount}`; + return [ + ...prevSteps, + { + id: Date.now(), + type: "text", + label: generatedLabel, + data, + selectorObj, + actionId, + }, + ]; + }); }; const addListStep = ( - listSelector: string, - newFields: { [key: string]: TextStep }, - listId: number, - actionId: string, - pagination?: { type: string; selector: string; isShadow?: boolean }, + listSelector: string, + newFields: { [key: string]: TextStep }, + listId: number, + actionId: string, + pagination?: { + type: string; + selector: string; + isShadow?: boolean; + }, limit?: number, isShadow?: boolean ) => { - setBrowserSteps(prevSteps => { - const existingListStepIndex = prevSteps.findIndex(step => step.type === 'list' && step.id === listId); - + setBrowserSteps((prevSteps) => { + const existingListStepIndex = prevSteps.findIndex( + (step) => step.type === "list" && step.id === listId + ); + if (existingListStepIndex !== -1) { const updatedSteps = [...prevSteps]; - const existingListStep = updatedSteps[existingListStepIndex] as ListStep; + const existingListStep = updatedSteps[ + existingListStepIndex + ] as ListStep; // Preserve existing labels for fields - const mergedFields = Object.entries(newFields).reduce((acc, [key, field]) => { - if (!discardedFields.has(`${listId}-${key}`)) { - // If field exists, preserve its label - if (existingListStep.fields[key]) { - acc[key] = { - ...field, - label: existingListStep.fields[key].label, - actionId - }; - } else { - acc[key] = { - ...field, - actionId - }; + const mergedFields = Object.entries(newFields).reduce( + (acc, [key, field]) => { + if (!discardedFields.has(`${listId}-${key}`)) { + // If field exists, preserve its label + if (existingListStep.fields[key]) { + acc[key] = { + ...field, + label: existingListStep.fields[key].label, + actionId, + }; + } else { + acc[key] = { + ...field, + actionId, + }; + } } - } - return acc; - }, {} as { [key: string]: TextStep }); + return acc; + }, + {} as { [key: string]: TextStep } + ); updatedSteps[existingListStepIndex] = { ...existingListStep, + listSelector, fields: mergedFields, pagination: pagination || existingListStep.pagination, limit: limit, + isShadow: isShadow !== undefined ? isShadow : existingListStep.isShadow, actionId, - isShadow: isShadow !== undefined ? isShadow : existingListStep.isShadow }; return updatedSteps; } else { - const fieldsWithActionId = Object.entries(newFields).reduce((acc, [key, field]) => { - acc[key] = { - ...field, - actionId - }; - return acc; - }, {} as { [key: string]: TextStep }); + const fieldsWithActionId = Object.entries(newFields).reduce( + (acc, [key, field]) => { + acc[key] = { + ...field, + actionId, + }; + return acc; + }, + {} as { [key: string]: TextStep } + ); + const listCount = prevSteps.filter(s => s.type === 'list').length + 1; return [ ...prevSteps, - { - id: listId, - type: 'list', - listSelector, - fields: fieldsWithActionId, - pagination, - limit, + { + id: listId, + type: "list", + name: `List Data ${listCount}`, + listSelector, + fields: fieldsWithActionId, + pagination, + limit, actionId, - isShadow - } + }, ]; } }); @@ -165,22 +389,39 @@ export const BrowserStepsProvider: React.FC<{ children: React.ReactNode }> = ({ ); }; - const updateListTextFieldLabel = (listId: number, fieldKey: string, newLabel: string) => { - setBrowserSteps(prevSteps => - prevSteps.map(step => { - if (step.type === 'list' && step.id === listId) { - // Ensure deep copy of the fields object + const updateListTextFieldLabel = ( + listId: number, + fieldKey: string, + newLabel: string + ) => { + setBrowserSteps((prevSteps) => + prevSteps.map((step) => { + if (step.type === "list" && step.id === listId) { + const oldLabel = step.fields[fieldKey].label; + const updatedFields = { ...step.fields, [fieldKey]: { ...step.fields[fieldKey], - label: newLabel - } + label: newLabel, + }, }; + const updatedData = step.data?.map((row: any) => { + if (row[oldLabel] !== undefined) { + const { [oldLabel]: value, ...rest } = row; + return { + ...rest, + [newLabel]: value, + }; + } + return row; + }); + return { ...step, - fields: updatedFields + fields: updatedFields, + data: updatedData, }; } return step; @@ -194,7 +435,7 @@ export const BrowserStepsProvider: React.FC<{ children: React.ReactNode }> = ({ if (step.type === 'list' && step.id === listId) { return { ...step, - data: extractedData // Add the extracted data to the step + data: extractedData }; } return step; @@ -217,49 +458,83 @@ export const BrowserStepsProvider: React.FC<{ children: React.ReactNode }> = ({ }; const updateListStepLimit = (listId: number, limit: number) => { - setBrowserSteps(prevSteps => - prevSteps.map(step => { - if (step.type === 'list' && step.id === listId) { - return { - ...step, - limit: limit - }; - } - return step; - }) - ); - }; - - const removeListTextField = (listId: number, fieldKey: string) => { setBrowserSteps(prevSteps => prevSteps.map(step => { if (step.type === 'list' && step.id === listId) { - const { [fieldKey]: _, ...remainingFields } = step.fields; return { ...step, - fields: remainingFields + limit: limit }; } return step; }) ); - setDiscardedFields(prevDiscarded => new Set(prevDiscarded).add(`${listId}-${fieldKey}`)); + }; + + const updateListStepName = (listId: number, name: string) => { + setBrowserSteps((prevSteps) => + prevSteps.map((step) => { + if (step.type === "list" && step.id === listId) { + return { + ...step, + name: name, + }; + } + return step; + }) + ); + }; + + const updateScreenshotStepName = (id: number, name: string) => { + setBrowserSteps(prevSteps => { + const updated = prevSteps.map(step => + step.id === id && step.type === 'screenshot' + ? { ...step, name } + : step + ); + browserStepsRef.current = updated; + return updated; + }); + }; + + const removeListTextField = (listId: number, fieldKey: string) => { + setBrowserSteps((prevSteps) => + prevSteps.map((step) => { + if (step.type === "list" && step.id === listId) { + const { [fieldKey]: _, ...remainingFields } = step.fields; + return { + ...step, + fields: remainingFields, + }; + } + return step; + }) + ); + setDiscardedFields((prevDiscarded) => + new Set(prevDiscarded).add(`${listId}-${fieldKey}`) + ); }; return ( - + {children} ); diff --git a/src/context/globalInfo.tsx b/src/context/globalInfo.tsx index a9b20bfa..69969a09 100644 --- a/src/context/globalInfo.tsx +++ b/src/context/globalInfo.tsx @@ -139,6 +139,8 @@ interface GlobalInfo { setCurrentListActionId: (actionId: string) => void; currentScreenshotActionId: string; setCurrentScreenshotActionId: (actionId: string) => void; + currentTextGroupName: string; + setCurrentTextGroupName: (name: string) => void; isDOMMode: boolean; setIsDOMMode: (isDOMMode: boolean) => void; currentSnapshot: ProcessedSnapshot | null; @@ -173,6 +175,7 @@ class GlobalInfoStore implements Partial { currentTextActionId = ''; currentListActionId = ''; currentScreenshotActionId = ''; + currentTextGroupName = 'Text Data'; isDOMMode = false; currentSnapshot = null; }; @@ -282,6 +285,7 @@ export const GlobalInfoProvider = ({ children }: { children: JSX.Element }) => { const [currentTextActionId, setCurrentTextActionId] = useState(''); const [currentListActionId, setCurrentListActionId] = useState(''); const [currentScreenshotActionId, setCurrentScreenshotActionId] = useState(''); + const [currentTextGroupName, setCurrentTextGroupName] = useState('Text Data'); const [isDOMMode, setIsDOMMode] = useState(globalInfoStore.isDOMMode); const [currentSnapshot, setCurrentSnapshot] = useState(globalInfoStore.currentSnapshot); @@ -363,6 +367,8 @@ export const GlobalInfoProvider = ({ children }: { children: JSX.Element }) => { setCurrentListActionId, currentScreenshotActionId, setCurrentScreenshotActionId, + currentTextGroupName, + setCurrentTextGroupName, isDOMMode, setIsDOMMode, currentSnapshot, diff --git a/src/helpers/capturedElementHighlighter.ts b/src/helpers/capturedElementHighlighter.ts new file mode 100644 index 00000000..4eb7aa3e --- /dev/null +++ b/src/helpers/capturedElementHighlighter.ts @@ -0,0 +1,85 @@ +/** + * Helper class for managing persistent highlights of captured elements. + * Shows dotted highlights for elements that have been captured but not yet confirmed. + */ +class CapturedElementHighlighter { + private static readonly STYLE_ID = 'maxun-captured-elements-style'; + + /** + * Apply persistent dotted highlights to captured elements in the DOM iframe + * @param selectors Array of captured element selectors + */ + public applyHighlights(selectors: Array<{ selector: string }>): void { + const iframeDoc = this.getIframeDocument(); + if (!iframeDoc) return; + + // Remove existing highlights + this.clearHighlights(); + + // Create CSS rules for each captured selector + const cssRules: string[] = []; + + selectors.forEach(({ selector }) => { + const cssSelector = this.getCSSSelector(selector); + + if (cssSelector) { + cssRules.push(` + ${cssSelector} { + outline: 2px dotted #ff00c3 !important; + outline-offset: 2px !important; + background-color: rgba(255, 0, 195, 0.08) !important; + box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.5) !important; + } + `); + } + }); + + // Inject style element + if (cssRules.length > 0) { + const styleElement = iframeDoc.createElement('style'); + styleElement.id = CapturedElementHighlighter.STYLE_ID; + styleElement.textContent = cssRules.join('\n'); + iframeDoc.head.appendChild(styleElement); + } + } + + /** + * Clear all persistent highlights from the DOM iframe + */ + public clearHighlights(): void { + const iframeDoc = this.getIframeDocument(); + if (!iframeDoc) return; + + const existingStyle = iframeDoc.getElementById(CapturedElementHighlighter.STYLE_ID); + if (existingStyle) { + existingStyle.remove(); + } + } + + /** + * Get the iframe document + */ + private getIframeDocument(): Document | null { + const iframeElement = document.querySelector('#dom-browser-iframe') as HTMLIFrameElement; + return iframeElement?.contentDocument || null; + } + + /** + * Convert selector to CSS format for highlighting + */ + private getCSSSelector(selector: string): string { + // Handle XPath selectors by extracting data-mx-id + if (selector.startsWith('//') || selector.startsWith('(//')) { + const mxIdMatch = selector.match(/data-mx-id='([^']+)'/); + if (mxIdMatch) { + return `[data-mx-id='${mxIdMatch[1]}']`; + } + return ''; + } + + // Already a CSS selector + return selector; + } +} + +export const capturedElementHighlighter = new CapturedElementHighlighter();