diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index 7801a20ec..4f5dca33f 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -10,7 +10,8 @@ import { getChildSelectors, getNonUniqueSelectors, isRuleOvershadowing, - selectorAlreadyInWorkflow + selectorAlreadyInWorkflow, + extractChildData } from "../selector"; import { CustomActions } from "../../../../src/shared/types"; import { workflow } from "../../routes"; @@ -61,6 +62,8 @@ export class WorkflowGenerator { */ private getList: boolean = false; + // private getListAuto: boolean = false; + private listSelector: string = ''; /** @@ -116,6 +119,9 @@ export class WorkflowGenerator { this.socket.on('setGetList', (data: { getList: boolean }) => { this.getList = data.getList; }); + // this.socket.on('setGetListAuto', (data: { getListAuto: boolean }) => { + // this.getListAuto = data.getListAuto; + // }); this.socket.on('listSelector', (data: { selector: string }) => { this.listSelector = data.selector; }) @@ -559,12 +565,22 @@ export class WorkflowGenerator { if (this.listSelector !== '') { const childSelectors = await getChildSelectors(page, this.listSelector || ''); this.socket.emit('highlighter', { rect, selector: displaySelector, elementInfo, childSelectors }) - console.log(`Child Selectors: ${childSelectors}`) - console.log(`Parent Selector: ${this.listSelector}`) } else { this.socket.emit('highlighter', { rect, selector: displaySelector, elementInfo }); } - } else { + } + // else if (this.getListAuto === true) { + // if (this.listSelector !== '') { + // console.log(`list selector is: ${this.listSelector}`) + // const childData = await extractChildData(page, this.listSelector || ''); + // console.log(`Data From Backend: ${JSON.stringify({ rect, selector: displaySelector, elementInfo, childData })}`) + // this.socket.emit('highlighter', { rect, selector: displaySelector, elementInfo, childData }); + // } + // else { + // this.socket.emit('highlighter', { ayo:'ayo', rect, selector: displaySelector, elementInfo }); + // } + // } + else { this.socket.emit('highlighter', { rect, selector: displaySelector, elementInfo }); } } diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 193de8910..8f52dc3ac 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -790,6 +790,147 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates } }; +export const extractChildData = async ( + page: Page, + parentSelector: string +): Promise<{ data: string; selector: string; attribute: string; tag: string }[]> => { + try { + const baseURL = new URL(page.url()); + + const uniqueData = await page.evaluate(({ parentSelector, baseHref }: { parentSelector: string, baseHref: string }) => { + interface ElementData { + data: string; + selector: string; + attribute: string; // New field for the attribute + tag: string; // New field for the tag name + importance: number; // Used internally but excluded in the return + } + + function getNonUniqueSelector(element: HTMLElement): string { + let selector = element.tagName.toLowerCase(); + + const className = typeof element.className === 'string' ? element.className : ''; + if (className) { + const classes = className.split(/\s+/).filter((cls: string) => Boolean(cls)); + if (classes.length > 0) { + const validClasses = classes.filter((cls: string) => !cls.startsWith('!') && !cls.includes(':')); + if (validClasses.length > 0) { + selector += '.' + validClasses.map(cls => CSS.escape(cls)).join('.'); + } + } + } + + return selector; + } + + function determineImportance(element: HTMLElement): number { + if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(element.tagName.toLowerCase())) { + return 2; + } else if (['p', 'span', 'a', 'img'].includes(element.tagName.toLowerCase())) { + return 1; + } + return 0; + } + + function cleanText(text: string): string { + return text.replace(/\s+/g, ' ').trim(); + } + + function resolveURL(url: string | null, baseHref: string): string | null { + if (!url) return null; + try { + return new URL(url, baseHref).href; + } catch { + return url; + } + } + + function extractElementData(element: HTMLElement, baseHref: string): ElementData[] { + const selector = getNonUniqueSelector(element); + const importance = determineImportance(element); + const tag = element.tagName.toLowerCase(); + const results: ElementData[] = []; + + // Include text content if importance is sufficient + if (importance >= 1) { + const textContent = cleanText(element.textContent || ''); + if (textContent) { + results.push({ data: textContent, selector, attribute: 'innerText', tag, importance }); + } + } + + // Handle links (a tags) + if (tag === 'a') { + const href = element.getAttribute('href'); + if (href) { + const resolvedHref = resolveURL(href, baseHref); + if (resolvedHref) { + results.push({ data: resolvedHref, selector, attribute: 'href', tag, importance }); + } + } + } + + // Handle images (img tags) + if (tag === 'img') { + const src = element.getAttribute('src'); + if (src) { + const resolvedSrc = resolveURL(src, baseHref); + if (resolvedSrc) { + results.push({ data: resolvedSrc, selector, attribute: 'src', tag, importance }); + } + } + } + + return results; + } + + function getAllDescendantData(element: HTMLElement): ElementData[] { + let data: ElementData[] = []; + const children = Array.from(element.children) as HTMLElement[]; + + for (const child of children) { + data = data.concat(extractElementData(child, baseHref)); + data = data.concat(getAllDescendantData(child)); + } + + return data; + } + + const parentElement = document.querySelector(parentSelector) as HTMLElement; + if (!parentElement) return []; + + const allData = getAllDescendantData(parentElement); + + // Deduplicate and prioritize by importance + const uniqueData = Array.from( + allData.reduce((map, item) => { + if (!map.has(item.data)) { + map.set(item.data, item); // Add new data + } else { + const existing = map.get(item.data); + if (existing && item.importance > existing.importance) { + map.set(item.data, item); // Replace with higher importance + } + } + return map; + }, new Map()) + ); + + // Remove importance field before returning + return uniqueData.map(([_, item]) => ({ + data: item.data, + selector: item.selector, + attribute: item.attribute, + tag: item.tag, + })); + }, { parentSelector, baseHref: baseURL.href }); + + return uniqueData; + } catch (error) { + console.error('Error in extractChildData:', error); + return []; + } +}; export const getChildSelectors = async (page: Page, parentSelector: string): Promise => { try { diff --git a/src/components/atoms/canvas.tsx b/src/components/atoms/canvas.tsx index 1dd88e190..1555e6369 100644 --- a/src/components/atoms/canvas.tsx +++ b/src/components/atoms/canvas.tsx @@ -27,9 +27,10 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => { const canvasRef = useRef(null); const { socket } = useSocketStore(); const { setLastAction, lastAction } = useGlobalInfoStore(); - const { getText, getList } = useActionContext(); + const { getText, getList, getListAuto } = useActionContext(); const getTextRef = useRef(getText); const getListRef = useRef(getList); + const getListAutoRef = useRef(getListAuto); const notifyLastAction = (action: string) => { if (lastAction !== action) { @@ -42,7 +43,8 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => { useEffect(() => { getTextRef.current = getText; getListRef.current = getList; - }, [getText, getList]); + getListAutoRef.current = getListAuto; + }, [getText, getList, getListAuto]); const onMouseEvent = useCallback((event: MouseEvent) => { if (socket && canvasRef.current) { @@ -59,7 +61,10 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => { console.log('Capturing Text...'); } else if (getListRef.current === true) { console.log('Capturing List...'); - } else { + } else if (getListAutoRef.current === true) { + console.log('Capturing List Automatically...'); + } + else { socket.emit('input:mousedown', clickCoordinates); } notifyLastAction('click'); diff --git a/src/components/molecules/ActionDescriptionBox.tsx b/src/components/molecules/ActionDescriptionBox.tsx index cad962c76..b85a78618 100644 --- a/src/components/molecules/ActionDescriptionBox.tsx +++ b/src/components/molecules/ActionDescriptionBox.tsx @@ -44,10 +44,11 @@ const Content = styled.div` `; const ActionDescriptionBox = () => { - const { getText, getScreenshot, getList, captureStage } = useActionContext() as { + const { getText, getScreenshot, getList, captureStage, getListAuto } = useActionContext() as { getText: boolean; getScreenshot: boolean; getList: boolean; + getListAuto: boolean; captureStage: 'initial' | 'pagination' | 'limit' | 'complete'; }; @@ -99,7 +100,32 @@ const ActionDescriptionBox = () => { ); - } else { + } else if (getListAuto) { + return ( + <> + Capture List + + Hover over the list you want to extract + + + {messages.map(({ stage, text }, index) => ( + + } + label={{text}} + /> + ))} + + + ); + } + + else { return ( <> What data do you want to extract? diff --git a/src/components/molecules/RobotEdit.tsx b/src/components/molecules/RobotEdit.tsx index 26317260a..f68beee24 100644 --- a/src/components/molecules/RobotEdit.tsx +++ b/src/components/molecules/RobotEdit.tsx @@ -58,7 +58,6 @@ interface RobotSettingsProps { } export const RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettings }: RobotSettingsProps) => { - console.log("robot edit"); const [robot, setRobot] = useState(null); const { recordingId, notify } = useGlobalInfoStore(); diff --git a/src/components/organisms/BrowserWindow.tsx b/src/components/organisms/BrowserWindow.tsx index 697b4adb1..b5aca1a85 100644 --- a/src/components/organisms/BrowserWindow.tsx +++ b/src/components/organisms/BrowserWindow.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState, useRef } from 'react'; import { useSocketStore } from '../../context/socket'; import { Button } from '@mui/material'; import Canvas from "../atoms/canvas"; @@ -25,6 +25,12 @@ interface AttributeOption { value: string; } +interface ChildData { + data: string; + selector: string; + importance?: number; // Add other properties if needed +} + const getAttributeOptions = (tagName: string, elementInfo: ElementInfo | null): AttributeOption[] => { if (!elementInfo) return []; switch (tagName.toLowerCase()) { @@ -54,19 +60,23 @@ const getAttributeOptions = (tagName: string, elementInfo: ElementInfo | null): export const BrowserWindow = () => { const [canvasRef, setCanvasReference] = useState | undefined>(undefined); const [screenShot, setScreenShot] = useState(""); - const [highlighterData, setHighlighterData] = useState<{ rect: DOMRect, selector: string, elementInfo: ElementInfo | null, childSelectors?: string[] } | null>(null); + const [highlighterData, setHighlighterData] = useState<{ rect: DOMRect, selector: string, elementInfo: ElementInfo | null, childSelectors?: string[], childData?: ChildData[] } | null>(null); const [showAttributeModal, setShowAttributeModal] = useState(false); + const [showAutoExtractModal, setShowAutoExtractModal] = useState(false); const [attributeOptions, setAttributeOptions] = useState([]); const [selectedElement, setSelectedElement] = useState<{ selector: string, info: ElementInfo | null } | null>(null); const [currentListId, setCurrentListId] = useState(null); + const [isChildDataAvailable, setIsChildDataAvailable] = useState(false); const [listSelector, setListSelector] = useState(null); const [fields, setFields] = useState>({}); const [paginationSelector, setPaginationSelector] = useState(''); + const listSelectorRef = useRef(null); + const { socket } = useSocketStore(); const { notify } = useGlobalInfoStore(); - const { getText, getList, paginationMode, paginationType, limitMode } = useActionContext(); + const { getText, getList, getListAuto, paginationMode, paginationType, limitMode } = useActionContext(); const { addTextStep, addListStep } = useBrowserSteps(); const onMouseMove = (e: MouseEvent) => { @@ -86,15 +96,16 @@ export const BrowserWindow = () => { const resetListState = useCallback(() => { setListSelector(null); + listSelectorRef.current = null; setFields({}); setCurrentListId(null); }, []); useEffect(() => { - if (!getList) { + if (!getList || !getListAuto) { resetListState(); } - }, [getList, resetListState]); + }, [getList, getListAuto, resetListState]); const screencastHandler = useCallback((data: string) => { setScreenShot(data); @@ -114,36 +125,44 @@ export const BrowserWindow = () => { } }, [screenShot, canvasRef, socket, screencastHandler]); - const highlighterHandler = useCallback((data: { rect: DOMRect, selector: string, elementInfo: ElementInfo | null, childSelectors?: string[] }) => { - if (getList === true) { - if (listSelector) { - socket?.emit('listSelector', { selector: listSelector }); + const highlighterHandler = useCallback((data: { rect: DOMRect, selector: string, elementInfo: ElementInfo | null, childSelectors?: string[], childData?: ChildData[] }) => { + if (getList === true || getListAuto === true) { + if (listSelectorRef.current) { + socket?.emit('listSelector', { selector: listSelectorRef.current }); + if (limitMode) { setHighlighterData(null); } else if (paginationMode) { - // only set highlighterData if type is not empty, 'none', 'scrollDown', or 'scrollUp' + // Only set highlighterData if type is valid for pagination if (paginationType !== '' && !['none', 'scrollDown', 'scrollUp'].includes(paginationType)) { setHighlighterData(data); } else { setHighlighterData(null); } - } else if (data.childSelectors && data.childSelectors.includes(data.selector)) { - // highlight only valid child elements within the listSelector + } else if (getList && data.childSelectors && data.childSelectors.includes(data.selector)) { + // For `getList`, highlight only valid child elements within the listSelector setHighlighterData(data); + } else if (getListAuto && data.childData) { + // For `getListAuto`, set highlighterData if childData is present + //onst { childSelectors, ...rest } = data; + setHighlighterData({ rect: data.rect, selector: data.selector, elementInfo: data.elementInfo, childData: data.childData }); + setIsChildDataAvailable(true); } else { - // if !valid child in normal mode, clear the highlighter + // Clear the highlighter if not valid setHighlighterData(null); } } else { - // set highlighterData for the initial listSelector selection + // Set highlighterData for the initial listSelector selection setHighlighterData(data); } } else { - // for non-list steps + // For non-list steps setHighlighterData(data); } - }, [highlighterData, getList, socket, listSelector, paginationMode, paginationType]); - + }, [highlighterData, getList, getListAuto, socket, listSelector, paginationMode, paginationType]); + + // console.log('highlighterData', highlighterData); + console.log('is child data available', isChildDataAvailable); useEffect(() => { document.addEventListener('mousemove', onMouseMove, false); @@ -196,66 +215,126 @@ export const BrowserWindow = () => { } } - if (paginationMode && getList) { + if (paginationMode && (getList || getListAuto)) { // Only allow selection in pagination mode if type is not empty, 'scrollDown', or 'scrollUp' if (paginationType !== '' && paginationType !== 'scrollDown' && paginationType !== 'scrollUp' && paginationType !== 'none') { setPaginationSelector(highlighterData.selector); notify(`info`, `Pagination element selected successfully.`); - addListStep(listSelector!, fields, currentListId || 0, { type: paginationType, selector: highlighterData.selector }); + addListStep(listSelectorRef.current!, fields, currentListId || 0, { type: paginationType, selector: highlighterData.selector }); } return; } - if (getList === true && !listSelector) { - setListSelector(highlighterData.selector); - notify(`info`, `List selected succesfully. Select the text data for extraction.`) + if ((getList === true || getListAuto === true) && !listSelectorRef.current) { + // Set listSelectorRef and state + const newSelector = highlighterData.selector; + listSelectorRef.current = newSelector; + setListSelector(newSelector); + notify(`info`, `List selected successfully. Proceed to extract data.`); + setCurrentListId(Date.now()); setFields({}); - } else if (getList === true && listSelector && currentListId) { - const attribute = options[0].value; - const data = attribute === 'href' ? highlighterData.elementInfo?.url || '' : - attribute === 'src' ? highlighterData.elementInfo?.imageUrl || '' : - highlighterData.elementInfo?.innerText || ''; - // Add fields to the list - if (options.length === 1) { - const attribute = options[0].value; - const newField: TextStep = { - id: Date.now(), - type: 'text', - label: `Label ${Object.keys(fields).length + 1}`, - data: data, - selectorObj: { - selector: highlighterData.selector, - tag: highlighterData.elementInfo?.tagName, - attribute - } - }; - - setFields(prevFields => { - const updatedFields = { - ...prevFields, - [newField.id]: newField - }; - return updatedFields; - }); - if (listSelector) { - addListStep(listSelector, { ...fields, [newField.id]: newField }, currentListId, { type: '', selector: paginationSelector }); + if (getListAuto) { + notify(`info`, `List container selected! Now click on any item inside the list to auto-extract all similar items.`); + if (highlighterData.childData) { + handleAutoFieldPopulation(highlighterData.childData, newSelector); + } else { + notify(`error`, `No child data found for auto-extraction.`); } - + } + + // Automatically handle field population for getListAuto mode + // if (getListAuto === true && highlighterData.childData) { + // handleAutoFieldPopulation(highlighterData.childData, newSelector); + // } + } else if ((getList === true || getListAuto === true) && listSelectorRef.current && currentListId) { + if (getListAuto === true && highlighterData.childData) { + handleAutoFieldPopulation(highlighterData.childData, listSelectorRef.current); } else { - setAttributeOptions(options); - setSelectedElement({ - selector: highlighterData.selector, - info: highlighterData.elementInfo - }); - setShowAttributeModal(true); + handleManualFieldPopulation(options, highlighterData, listSelectorRef.current); } } + + } } }; + const handleAutoFieldPopulation = (childData: Array, listSelector: string) => { + setShowAutoExtractModal(true); + setTimeout(() => { + setShowAutoExtractModal(false); + }, 3000); + + const newFields: Record = {}; + childData.forEach((child, index) => { + const newField: TextStep = { + id: Date.now() + index, + type: 'text', + label: `Label ${index + 1}`, + data: child.data, + selectorObj: { + selector: child.selector, + tag: child.tag, + attribute: child.attribute + }, + }; + + newFields[newField.id] = newField; + }); + + setFields(prevFields => ({ + ...prevFields, + ...newFields, + })); + + addListStep(listSelector, newFields, currentListId || Date.now(), { type: '', selector: paginationSelector }); + }; + + const handleManualFieldPopulation = ( + options: AttributeOption[], + highlighterData: any, + listSelector: string + ) => { + const attribute = options[0].value; + const data = + attribute === 'href' + ? highlighterData.elementInfo?.url || '' + : attribute === 'src' + ? highlighterData.elementInfo?.imageUrl || '' + : highlighterData.elementInfo?.innerText || ''; + + if (options.length === 1) { + const newField: TextStep = { + id: Date.now(), + type: 'text', + label: `Label ${Object.keys(fields).length + 1}`, + data: data, + selectorObj: { + selector: highlighterData.selector, + tag: highlighterData.elementInfo?.tagName, + attribute, + }, + }; + + setFields(prevFields => ({ + ...prevFields, + [newField.id]: newField, + })); + + addListStep(listSelector, { ...fields, [newField.id]: newField }, currentListId || Date.now(), { type: '', selector: paginationSelector }); + } else { + setAttributeOptions(options); + setSelectedElement({ + selector: highlighterData.selector, + info: highlighterData.elementInfo, + }); + setShowAttributeModal(true); + } + }; + + const handleAttributeSelection = (attribute: string) => { if (selectedElement) { let data = ''; @@ -277,7 +356,7 @@ export const BrowserWindow = () => { attribute: attribute }); } - if (getList === true && listSelector && currentListId) { + if ((getList === true) && listSelector && currentListId) { const newField: TextStep = { id: Date.now(), type: 'text', @@ -317,9 +396,54 @@ export const BrowserWindow = () => { } }, [paginationMode, resetPaginationSelector]); + // useEffect(() => { + // // Automatically populate fields when listSelector and childData are available + // if (listSelector && getListAuto && highlighterData?.childData && currentListId) { + // notify(`info`, `Auto extracting data...`); + + // const newFields: Record = {}; + // highlighterData.childData.forEach(child => { + // const newField: TextStep = { + // id: Date.now(), + // type: 'text', + // label: `Label ${Object.keys(fields).length + 1}`, + // data: child.data, + // selectorObj: { + // selector: child.selector, + // tag: '', + // attribute: '' + // } + // }; + + // newFields[newField.id] = newField; + // }); + + // setFields(prevFields => ({ ...prevFields, ...newFields })); + + // if (listSelector) { + // addListStep(listSelector, { ...fields, ...newFields }, currentListId, { type: '', selector: paginationSelector }); + // } + // } + // }, [listSelector, getListAuto, highlighterData?.childData, currentListId, fields]); + + return (
+ {getListAuto === true ? ( + { }} + canBeClosed={false} + modalStyle={modalStyle} + > +
+

Auto Extracting

+

Most useful data, please wait for a few seconds...

+

Found {highlighterData?.childData?.length} items...

+
+
+ ): null} { getText === true || getList === true ? ( { ) : null }
- {((getText === true || getList === true) && !showAttributeModal && highlighterData?.rect != null && highlighterData?.rect.top != null) && canvasRef?.current ? + {((getText === true || getList === true || getListAuto === true) && !showAttributeModal && highlighterData?.rect != null && highlighterData?.rect.top != null) && canvasRef?.current ? = ({ onFinishCapture const [showCaptureScreenshot, setShowCaptureScreenshot] = useState(true); const [showCaptureText, setShowCaptureText] = useState(true); const [hoverStates, setHoverStates] = useState<{ [id: string]: boolean }>({}); + const [showCaptureListOptions, setShowCaptureListOptions] = useState(false); const { lastAction, notify, currentWorkflowActionsState, setCurrentWorkflowActionsState } = useGlobalInfoStore(); - const { getText, startGetText, stopGetText, getScreenshot, startGetScreenshot, stopGetScreenshot, getList, startGetList, stopGetList, startPaginationMode, stopPaginationMode, paginationType, updatePaginationType, limitType, customLimit, updateLimitType, updateCustomLimit, stopLimitMode, startLimitMode, captureStage, setCaptureStage } = useActionContext(); + const { getText, startGetText, stopGetText, getScreenshot, startGetScreenshot, stopGetScreenshot, getList, startGetList, stopGetList, getListAuto, startGetListAuto, stopGetListAuto, startPaginationMode, stopPaginationMode, paginationType, updatePaginationType, limitType, customLimit, updateLimitType, updateCustomLimit, stopLimitMode, startLimitMode, captureStage, setCaptureStage } = useActionContext(); const { browserSteps, updateBrowserTextStepLabel, deleteBrowserStep, addScreenshotStep, updateListTextFieldLabel, removeListTextField } = useBrowserSteps(); const { id, socket } = useSocketStore(); @@ -125,8 +126,6 @@ export const RightSidePanel: React.FC = ({ onFinishCapture setHoverStates(prev => ({ ...prev, [id]: false })); }; - const handlePairDelete = () => { } - const handleTextLabelChange = (id: number, label: string, listId?: number, fieldKey?: string) => { if (listId !== undefined && fieldKey !== undefined) { // Prevent editing if the field is confirmed @@ -218,6 +217,32 @@ export const RightSidePanel: React.FC = ({ onFinishCapture onFinishCapture(); }, [stopGetText, getTextSettingsObject, socket, browserSteps, confirmedTextSteps]); + const getListAutoSettingsObject = useCallback(() => { + let settings: { + listSelector?: string; + } = {}; + + 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, + }; + } + }); + + settings = { + listSelector: step.listSelector, + }; + } + }); + + return settings; + }, [browserSteps]); + const getListSettingsObject = useCallback(() => { let settings: { listSelector?: string; @@ -265,6 +290,11 @@ export const RightSidePanel: React.FC = ({ onFinishCapture resetListState(); }, [stopGetList, resetListState]); + const handleStopGetListAuto = useCallback(() => { + stopGetListAuto(); + resetListState(); + }, [stopGetListAuto, resetListState]); + const stopCaptureAndEmitGetListSettings = useCallback(() => { const settings = getListSettingsObject(); if (settings) { @@ -276,6 +306,17 @@ export const RightSidePanel: React.FC = ({ onFinishCapture onFinishCapture(); }, [stopGetList, getListSettingsObject, socket, notify, handleStopGetList]); + const captureAndEmitGetListAutoSettings = useCallback(() => { + const settings = getListAutoSettingsObject(); + if (settings) { + socket?.emit('action', { action: 'scrapeListAuto', settings }); + } else { + notify('error', 'Unable to create list settings. Make sure you have defined a field for the list.'); + } + handleStopGetListAuto(); + onFinishCapture(); + }, [getListAutoSettingsObject, socket, notify]); + const hasUnconfirmedListTextFields = browserSteps.some(step => step.type === 'list' && Object.values(step.fields).some(field => !confirmedListTextFields[step.id]?.[field.id])); const handleConfirmListCapture = useCallback(() => { @@ -343,7 +384,7 @@ export const RightSidePanel: React.FC = ({ onFinishCapture notify('error', 'Capture Text Discarded'); }, [browserSteps, stopGetText, deleteBrowserStep]); - const discardGetList = useCallback(() => { + const discardGetListManual = useCallback(() => { stopGetList(); browserSteps.forEach(step => { if (step.type === 'list') { @@ -358,6 +399,21 @@ export const RightSidePanel: React.FC = ({ onFinishCapture notify('error', 'Capture List Discarded'); }, [browserSteps, stopGetList, deleteBrowserStep, resetListState]); + const discardGetListAuto = useCallback(() => { + stopGetListAuto(); + browserSteps.forEach(step => { + if (step.type === 'list') { + deleteBrowserStep(step.id); + } + }); + resetListState(); + setShowPaginationOptions(false); + setShowLimitOptions(false); + setCaptureStage('initial'); + setConfirmedListTextFields({}); + notify('error', 'Capture List Discarded'); + }, [browserSteps, stopGetListAuto, deleteBrowserStep, resetListState]); + const captureScreenshot = (fullPage: boolean) => { const screenshotSettings: ScreenshotSettings = { @@ -387,6 +443,22 @@ export const RightSidePanel: React.FC = ({ onFinishCapture return !hasValidListSelector || hasUnconfirmedListTextFields; }, [captureStage, browserSteps, hasUnconfirmedListTextFields]); + const handleCaptureListClick = () => { + setShowCaptureListOptions(true); + setShowCaptureText(false); + setShowCaptureScreenshot(false); + }; + + const handleManualCapture = () => { + startGetList(); + setShowCaptureListOptions(false); + }; + + const handleAutoCapture = () => { + startGetListAuto(); + setShowCaptureListOptions(false); + }; + return ( {/* @@ -394,7 +466,45 @@ export const RightSidePanel: React.FC = ({ onFinishCapture */} - {!getText && !getScreenshot && !getList && showCaptureList && } + {!getText && !getScreenshot && !getList && !getListAuto && showCaptureList && !showCaptureListOptions && ( + + )} + {/* Show Manual and Auto Capture options when "Capture List" is clicked */} + {!getText && !getScreenshot && !getList && !getListAuto && showCaptureList && showCaptureListOptions && ( + + + + + + )} + {getListAuto && ( + <> + + + + + + )} {getList && ( <> @@ -407,7 +517,7 @@ export const RightSidePanel: React.FC = ({ onFinishCapture captureStage === 'pagination' ? 'Confirm Pagination' : captureStage === 'limit' ? 'Confirm Limit' : 'Finish Capture'} - + )} @@ -459,7 +569,7 @@ export const RightSidePanel: React.FC = ({ onFinishCapture )} - {!getText && !getScreenshot && !getList && showCaptureText && } + {!getText && !getScreenshot && !getList && !getListAuto && showCaptureText && !showCaptureListOptions && } {getText && <> @@ -468,7 +578,7 @@ export const RightSidePanel: React.FC = ({ onFinishCapture } - {!getText && !getScreenshot && !getList && showCaptureScreenshot && } + {!getText && !getScreenshot && !getList && !getListAuto && showCaptureScreenshot && !showCaptureListOptions && } {getScreenshot && ( diff --git a/src/context/browserActions.tsx b/src/context/browserActions.tsx index ef303f822..1698845b1 100644 --- a/src/context/browserActions.tsx +++ b/src/context/browserActions.tsx @@ -8,6 +8,7 @@ export type CaptureStage = 'initial' | 'pagination' | 'limit' | 'complete' | ''; interface ActionContextProps { getText: boolean; getList: boolean; + getListAuto: boolean; getScreenshot: boolean; paginationMode: boolean; limitMode: boolean; @@ -21,6 +22,8 @@ interface ActionContextProps { stopGetText: () => void; startGetList: () => void; stopGetList: () => void; + startGetListAuto: () => void; + stopGetListAuto: () => void; startGetScreenshot: () => void; stopGetScreenshot: () => void; stopPaginationMode: () => void; @@ -36,6 +39,7 @@ const ActionContext = createContext(undefined); export const ActionProvider = ({ children }: { children: ReactNode }) => { const [getText, setGetText] = useState(false); const [getList, setGetList] = useState(false); + const [getListAuto, setGetListAuto] = useState(false); const [getScreenshot, setGetScreenshot] = useState(false); const [paginationMode, setPaginationMode] = useState(false); const [limitMode, setLimitMode] = useState(false); @@ -82,6 +86,23 @@ export const ActionProvider = ({ children }: { children: ReactNode }) => { setCaptureStage('complete'); }; + const startGetListAuto = () => { + setGetListAuto(true); + // socket?.emit('setGetListAuto', { getListAuto: true }); + socket?.emit('setGetList', { getList: true }); + setCaptureStage('initial'); + }; + + const stopGetListAuto = () => { + setGetListAuto(false); + // socket?.emit('setGetListAuto', { getListAuto: false }); + socket?.emit('setGetList', { getList: true }); + setPaginationType(''); + setLimitType(''); + setCustomLimit(''); + setCaptureStage('complete'); + }; + const startGetScreenshot = () => setGetScreenshot(true); const stopGetScreenshot = () => setGetScreenshot(false); @@ -89,6 +110,7 @@ export const ActionProvider = ({ children }: { children: ReactNode }) => { { stopGetText, startGetList, stopGetList, + startGetListAuto, + stopGetListAuto, startGetScreenshot, stopGetScreenshot, startPaginationMode,