From aa2775d744cd596317dfb2a2842723a0b6e66522 Mon Sep 17 00:00:00 2001 From: Rohit Date: Mon, 4 Aug 2025 22:40:33 +0530 Subject: [PATCH 01/38] feat: add page components for config actions --- src/components/robot/Recordings.tsx | 131 ++++++++++------------------ 1 file changed, 48 insertions(+), 83 deletions(-) diff --git a/src/components/robot/Recordings.tsx b/src/components/robot/Recordings.tsx index 1b3fc7b2..b57c9efe 100644 --- a/src/components/robot/Recordings.tsx +++ b/src/components/robot/Recordings.tsx @@ -2,11 +2,14 @@ import React, { useEffect, useState } from "react"; import { RecordingsTable } from "./RecordingsTable"; import { Grid } from "@mui/material"; import { RunSettings, RunSettingsModal } from "../run/RunSettings"; -import { ScheduleSettings, ScheduleSettingsModal } from "./ScheduleSettings"; -import { IntegrationSettingsModal } from "../integration/IntegrationSettings"; -import { RobotSettingsModal } from "./RobotSettings"; -import { RobotEditModal } from "./RobotEdit"; -import { RobotDuplicationModal } from "./RobotDuplicate"; +import { + ScheduleSettings, + ScheduleSettingsPage, +} from "./pages/ScheduleSettingsPage"; +import { RobotIntegrationPage } from "./pages/RobotIntegrationPage"; +import { RobotSettingsPage } from "./pages/RobotSettingsPage"; +import { RobotEditPage } from "./pages/RobotEditPage"; +import { RobotDuplicatePage } from "./pages/RobotDuplicatePage"; import { useNavigate, useLocation, useParams } from "react-router-dom"; import { useGlobalInfoStore } from "../../context/globalInfo"; import { useTranslation } from "react-i18next"; @@ -26,12 +29,16 @@ export const Recordings = ({ }: RecordingsProps) => { const navigate = useNavigate(); const location = useLocation(); - const { selectedRecordingId } = useParams(); const [params, setParams] = useState([]); const { notify } = useGlobalInfoStore(); const { t } = useTranslation(); - const handleNavigate = (path: string, id: string, name: string, params: string[]) => { + const handleNavigate = ( + path: string, + id: string, + name: string, + params: string[] + ) => { setParams(params); setRecordingInfo(id, name); navigate(path); @@ -47,35 +54,36 @@ export const Recordings = ({ // Helper function to get and clear a cookie const getAndClearCookie = (name: string) => { const value = document.cookie - .split('; ') - .find(row => row.startsWith(`${name}=`)) - ?.split('=')[1]; - + .split("; ") + .find((row) => row.startsWith(`${name}=`)) + ?.split("=")[1]; + if (value) { document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`; } - + return value; }; - const authStatus = getAndClearCookie('robot_auth_status'); - const airtableAuthStatus = getAndClearCookie('airtable_auth_status'); - const robotId = getAndClearCookie('robot_auth_robotId'); + const authStatus = getAndClearCookie("robot_auth_status"); + const airtableAuthStatus = getAndClearCookie("airtable_auth_status"); + const robotId = getAndClearCookie("robot_auth_robotId"); - if (airtableAuthStatus === 'success' && robotId) { + if (airtableAuthStatus === "success" && robotId) { console.log("Airtable Auth Status:", airtableAuthStatus); - notify(airtableAuthStatus, t("recordingtable.notifications.auth_success")); + notify( + airtableAuthStatus, + t("recordingtable.notifications.auth_success") + ); handleNavigate(`/robots/${robotId}/integrate/airtable`, robotId, "", []); - } - else if (authStatus === 'success' && robotId) { + } else if (authStatus === "success" && robotId) { console.log("Google Auth Status:", authStatus); notify(authStatus, t("recordingtable.notifications.auth_success")); handleNavigate(`/robots/${robotId}/integrate/google`, robotId, "", []); } }, []); - // Determine which modal to open based on the current route - const getCurrentModal = () => { + const getCurrentPageComponent = () => { const currentPath = location.pathname; if (currentPath.endsWith("/run")) { @@ -89,79 +97,36 @@ export const Recordings = ({ /> ); } else if (currentPath.endsWith("/schedule")) { + return ; + } else if (currentPath.includes("/integrate")) { return ( - - ); - } else if (currentPath.endsWith("/integrate/google")) { - return ( - {}} - preSelectedIntegrationType="googleSheets" - /> - ); - } else if (currentPath.endsWith("/integrate/airtable")) { - return ( - {}} - preSelectedIntegrationType="airtable" - /> - ); - } else if (currentPath.endsWith("/integrate/webhook")) { - return ( - {}} - preSelectedIntegrationType="webhook" - /> - ); - } else if (currentPath.endsWith("/integrate")) { - return ( - {}} - /> + {}} robotPath={"robots"} /> ); } else if (currentPath.endsWith("/settings")) { - return ( - {}} - /> - ); + return {}} />; } else if (currentPath.endsWith("/edit")) { - return ( - {}} - /> - ); + return {}} />; } else if (currentPath.endsWith("/duplicate")) { - return ( - {}} - /> - ); + return {}} />; } return null; }; + const currentPath = location.pathname; + const isConfigPage = + currentPath.includes("/schedule") || + currentPath.includes("/integrate") || + currentPath.includes("/settings") || + currentPath.includes("/edit") || + currentPath.includes("/duplicate") || + currentPath.includes("/run"); + + if (isConfigPage) { + return getCurrentPageComponent(); + } + return ( - {getCurrentModal()} Date: Mon, 4 Aug 2025 22:54:51 +0530 Subject: [PATCH 02/38] feat: add config action pages --- .../robot/pages/RobotConfigPage.tsx | 187 +++ .../robot/pages/RobotDuplicatePage.tsx | 191 +++ src/components/robot/pages/RobotEditPage.tsx | 643 ++++++++ .../robot/pages/RobotIntegrationPage.tsx | 1368 +++++++++++++++++ .../robot/pages/RobotSettingsPage.tsx | 192 +++ .../robot/pages/ScheduleSettingsPage.tsx | 373 +++++ 6 files changed, 2954 insertions(+) create mode 100644 src/components/robot/pages/RobotConfigPage.tsx create mode 100644 src/components/robot/pages/RobotDuplicatePage.tsx create mode 100644 src/components/robot/pages/RobotEditPage.tsx create mode 100644 src/components/robot/pages/RobotIntegrationPage.tsx create mode 100644 src/components/robot/pages/RobotSettingsPage.tsx create mode 100644 src/components/robot/pages/ScheduleSettingsPage.tsx diff --git a/src/components/robot/pages/RobotConfigPage.tsx b/src/components/robot/pages/RobotConfigPage.tsx new file mode 100644 index 00000000..e394ddca --- /dev/null +++ b/src/components/robot/pages/RobotConfigPage.tsx @@ -0,0 +1,187 @@ +import React from 'react'; +import { + Box, + Typography, + Button, + IconButton, + Divider, + useTheme +} from '@mui/material'; +import { ArrowBack } from '@mui/icons-material'; +import { useNavigate, useLocation } from 'react-router-dom'; + +interface RobotConfigPageProps { + title: string; + children: React.ReactNode; + onSave?: () => void; + onCancel?: () => void; + saveButtonText?: string; + cancelButtonText?: string; + showSaveButton?: boolean; + showCancelButton?: boolean; + isLoading?: boolean; + icon?: React.ReactNode; + onBackToSelection?: () => void; + backToSelectionText?: string; +} + +export const RobotConfigPage: React.FC = ({ + title, + children, + onSave, + onCancel, + saveButtonText = "Save", + cancelButtonText = "Cancel", + showSaveButton = true, + showCancelButton = true, + isLoading = false, + icon, + onBackToSelection, + backToSelectionText = "← Back" +}) => { + const theme = useTheme(); + + const handleBack = () => { + if (onCancel) { + onCancel(); + } + }; + + return ( + + + + + + {icon && ( + + {icon} + + )} + + {title} + + + + + + {children} + + + {(showSaveButton || showCancelButton || onBackToSelection) && ( + + {onBackToSelection && ( + + )} + + + {showCancelButton && ( + + )} + {showSaveButton && onSave && ( + + )} + + + )} + + ); +}; \ No newline at end of file diff --git a/src/components/robot/pages/RobotDuplicatePage.tsx b/src/components/robot/pages/RobotDuplicatePage.tsx new file mode 100644 index 00000000..e88be19d --- /dev/null +++ b/src/components/robot/pages/RobotDuplicatePage.tsx @@ -0,0 +1,191 @@ +import React, { useState, useEffect } from "react"; +import { + TextField, + Typography, + Box, + Button, +} from "@mui/material"; +import { useGlobalInfoStore } from "../../../context/globalInfo"; +import { + duplicateRecording, + getStoredRecording, + getStoredRecordings, +} from "../../../api/storage"; +import { WhereWhatPair } from "maxun-core"; +import { useTranslation } from "react-i18next"; +import { RobotConfigPage } from "./RobotConfigPage"; +import { useNavigate, useLocation } from "react-router-dom"; + +interface RobotMeta { + name: string; + id: string; + createdAt: string; + pairs: number; + updatedAt: string; + params: any[]; + url?: string; +} + +interface RobotWorkflow { + workflow: WhereWhatPair[]; +} + +interface ScheduleConfig { + runEvery: number; + runEveryUnit: "MINUTES" | "HOURS" | "DAYS" | "WEEKS" | "MONTHS"; + startFrom: + | "SUNDAY" + | "MONDAY" + | "TUESDAY" + | "WEDNESDAY" + | "THURSDAY" + | "FRIDAY" + | "SATURDAY"; + atTimeStart?: string; + atTimeEnd?: string; + timezone: string; + lastRunAt?: Date; + nextRunAt?: Date; + cronExpression?: string; +} + +export interface RobotSettings { + id: string; + userId?: number; + recording_meta: RobotMeta; + recording: RobotWorkflow; + google_sheet_email?: string | null; + google_sheet_name?: string | null; + google_sheet_id?: string | null; + google_access_token?: string | null; + google_refresh_token?: string | null; + schedule?: ScheduleConfig | null; +} + +interface RobotSettingsProps { + handleStart: (settings: RobotSettings) => void; +} + +export const RobotDuplicatePage = ({ handleStart }: RobotSettingsProps) => { + const { t } = useTranslation(); + const navigate = useNavigate(); + const location = useLocation(); + const [targetUrl, setTargetUrl] = useState(""); + const [robot, setRobot] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const { recordingId, notify, setRerenderRobots } = + useGlobalInfoStore(); + + useEffect(() => { + getRobot(); + }, []); + + useEffect(() => { + if (robot) { + let url = robot.recording_meta.url; + + if (!url) { + const lastPair = + robot?.recording.workflow[robot?.recording.workflow.length - 1]; + url = lastPair?.what.find((action) => action.action === "goto") + ?.args?.[0]; + } + + setTargetUrl(url); + } + }, [robot]); + + const getRobot = async () => { + if (recordingId) { + try { + const robot = await getStoredRecording(recordingId); + setRobot(robot); + } catch (error) { + notify("error", t("robot_duplication.notifications.robot_not_found")); + } + } else { + notify("error", t("robot_duplication.notifications.robot_not_found")); + } + }; + + const handleTargetUrlChange = (e: React.ChangeEvent) => { + setTargetUrl(e.target.value); + }; + + const handleSave = async () => { + if (!robot || !targetUrl) { + notify("error", t("robot_duplication.notifications.url_required")); + return; + } + + setIsLoading(true); + try { + const success = await duplicateRecording( + robot.recording_meta.id, + targetUrl + ); + + if (success) { + setRerenderRobots(true); + notify( + "success", + t("robot_duplication.notifications.duplicate_success") + ); + handleStart(robot); + navigate("/robots"); + } else { + notify("error", t("robot_duplication.notifications.duplicate_error")); + } + } catch (error) { + notify("error", t("robot_duplication.notifications.unknown_error")); + console.error("Error updating Target URL:", error); + } finally { + setIsLoading(false); + } + }; + + const handleCancel = () => { + navigate("/robots"); + }; + + return ( + + <> + + {robot && ( + <> + {t("robot_duplication.descriptions.purpose")} +
+ producthunt.com/topics/api", + url2: "producthunt.com/topics/database", + }), + }} + /> +
+ + {t("robot_duplication.descriptions.warning")} + + + + )} +
+ +
+ ); +}; diff --git a/src/components/robot/pages/RobotEditPage.tsx b/src/components/robot/pages/RobotEditPage.tsx new file mode 100644 index 00000000..deaf3d76 --- /dev/null +++ b/src/components/robot/pages/RobotEditPage.tsx @@ -0,0 +1,643 @@ +import React, { useState, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { + TextField, + Typography, + Box, + Button, + IconButton, + InputAdornment, +} from "@mui/material"; +import { Visibility, VisibilityOff } from "@mui/icons-material"; +import { useGlobalInfoStore } from "../../../context/globalInfo"; +import { getStoredRecording, updateRecording } from "../../../api/storage"; +import { WhereWhatPair } from "maxun-core"; +import { RobotConfigPage } from "./RobotConfigPage"; +import { useNavigate, useLocation } from "react-router-dom"; + +interface RobotMeta { + name: string; + id: string; + createdAt: string; + pairs: number; + updatedAt: string; + params: any[]; + url?: string; +} + +interface RobotWorkflow { + workflow: WhereWhatPair[]; +} + +interface ScheduleConfig { + runEvery: number; + runEveryUnit: "MINUTES" | "HOURS" | "DAYS" | "WEEKS" | "MONTHS"; + startFrom: + | "SUNDAY" + | "MONDAY" + | "TUESDAY" + | "WEDNESDAY" + | "THURSDAY" + | "FRIDAY" + | "SATURDAY"; + atTimeStart?: string; + atTimeEnd?: string; + timezone: string; + lastRunAt?: Date; + nextRunAt?: Date; + cronExpression?: string; +} + +export interface RobotSettings { + id: string; + userId?: number; + recording_meta: RobotMeta; + recording: RobotWorkflow; + google_sheet_email?: string | null; + google_sheet_name?: string | null; + google_sheet_id?: string | null; + google_access_token?: string | null; + google_refresh_token?: string | null; + schedule?: ScheduleConfig | null; +} + +interface RobotSettingsProps { + handleStart: (settings: RobotSettings) => void; +} + +interface CredentialInfo { + value: string; + type: string; +} + +interface Credentials { + [key: string]: CredentialInfo; +} + +interface CredentialVisibility { + [key: string]: boolean; +} + +interface GroupedCredentials { + passwords: string[]; + emails: string[]; + usernames: string[]; + others: string[]; +} + +interface ScrapeListLimit { + pairIndex: number; + actionIndex: number; + argIndex: number; + currentLimit: number; +} + +export const RobotEditPage = ({ handleStart }: RobotSettingsProps) => { + const { t } = useTranslation(); + const navigate = useNavigate(); + const location = useLocation(); + const [credentials, setCredentials] = useState({}); + const { recordingId, notify, setRerenderRobots } = useGlobalInfoStore(); + const [robot, setRobot] = useState(null); + const [credentialGroups, setCredentialGroups] = useState({ + passwords: [], + emails: [], + usernames: [], + others: [], + }); + const [showPasswords, setShowPasswords] = useState({}); + const [scrapeListLimits, setScrapeListLimits] = useState( + [] + ); + const [isLoading, setIsLoading] = useState(false); + + const isEmailPattern = (value: string): boolean => { + return value.includes("@"); + }; + + const isUsernameSelector = (selector: string): boolean => { + return ( + selector.toLowerCase().includes("username") || + selector.toLowerCase().includes("user") || + selector.toLowerCase().includes("email") + ); + }; + + const determineCredentialType = ( + selector: string, + info: CredentialInfo + ): "password" | "email" | "username" | "other" => { + if ( + info.type === "password" || + selector.toLowerCase().includes("password") + ) { + return "password"; + } + if ( + isEmailPattern(info.value) || + selector.toLowerCase().includes("email") + ) { + return "email"; + } + if (isUsernameSelector(selector)) { + return "username"; + } + return "other"; + }; + + useEffect(() => { + getRobot(); + }, []); + + useEffect(() => { + if (robot?.recording?.workflow) { + const extractedCredentials = extractInitialCredentials( + robot.recording.workflow + ); + setCredentials(extractedCredentials); + setCredentialGroups(groupCredentialsByType(extractedCredentials)); + + findScrapeListLimits(robot.recording.workflow); + } + }, [robot]); + + const findScrapeListLimits = (workflow: WhereWhatPair[]) => { + const limits: ScrapeListLimit[] = []; + + workflow.forEach((pair, pairIndex) => { + if (!pair.what) return; + + pair.what.forEach((action, actionIndex) => { + if ( + action.action === "scrapeList" && + action.args && + action.args.length > 0 + ) { + const arg = action.args[0]; + if (arg && typeof arg === "object" && "limit" in arg) { + limits.push({ + pairIndex, + actionIndex, + argIndex: 0, + currentLimit: arg.limit, + }); + } + } + }); + }); + + setScrapeListLimits(limits); + }; + + function extractInitialCredentials(workflow: any[]): Credentials { + const credentials: Credentials = {}; + + const isPrintableCharacter = (char: string): boolean => { + return char.length === 1 && !!char.match(/^[\x20-\x7E]$/); + }; + + workflow.forEach((step) => { + if (!step.what) return; + + let currentSelector = ""; + let currentValue = ""; + let currentType = ""; + let i = 0; + + while (i < step.what.length) { + const action = step.what[i]; + + if (!action.action || !action.args?.[0]) { + i++; + continue; + } + + const selector = action.args[0]; + + if ( + action.action === "type" && + action.args?.length >= 2 && + typeof action.args[1] === "string" && + action.args[1].length > 1 + ) { + if (!credentials[selector]) { + credentials[selector] = { + value: action.args[1], + type: action.args[2] || "text", + }; + } + i++; + continue; + } + + if ( + (action.action === "type" || action.action === "press") && + action.args?.length >= 2 && + typeof action.args[1] === "string" + ) { + if (selector !== currentSelector) { + if (currentSelector && currentValue) { + credentials[currentSelector] = { + value: currentValue, + type: currentType || "text", + }; + } + currentSelector = selector; + currentValue = credentials[selector]?.value || ""; + currentType = + action.args[2] || credentials[selector]?.type || "text"; + } + + const character = action.args[1]; + + if (isPrintableCharacter(character)) { + currentValue += character; + } else if (character === "Backspace") { + currentValue = currentValue.slice(0, -1); + } + + if (!currentType && action.args[2]?.toLowerCase() === "password") { + currentType = "password"; + } + + let j = i + 1; + while (j < step.what.length) { + const nextAction = step.what[j]; + if ( + !nextAction.action || + !nextAction.args?.[0] || + nextAction.args[0] !== selector || + (nextAction.action !== "type" && nextAction.action !== "press") + ) { + break; + } + if (nextAction.args[1] === "Backspace") { + currentValue = currentValue.slice(0, -1); + } else if (isPrintableCharacter(nextAction.args[1])) { + currentValue += nextAction.args[1]; + } + j++; + } + + credentials[currentSelector] = { + value: currentValue, + type: currentType, + }; + + i = j; + } else { + i++; + } + } + + if (currentSelector && currentValue) { + credentials[currentSelector] = { + value: currentValue, + type: currentType || "text", + }; + } + }); + + return credentials; + } + + const groupCredentialsByType = ( + credentials: Credentials + ): GroupedCredentials => { + return Object.entries(credentials).reduce( + (acc: GroupedCredentials, [selector, info]) => { + const credentialType = determineCredentialType(selector, info); + + switch (credentialType) { + case "password": + acc.passwords.push(selector); + break; + case "email": + acc.emails.push(selector); + break; + case "username": + acc.usernames.push(selector); + break; + default: + acc.others.push(selector); + } + + return acc; + }, + { passwords: [], emails: [], usernames: [], others: [] } + ); + }; + + const getRobot = async () => { + if (recordingId) { + try { + const robot = await getStoredRecording(recordingId); + setRobot(robot); + } catch (error) { + notify("error", t("robot_edit.notifications.update_failed")); + } + } else { + notify("error", t("robot_edit.notifications.update_failed")); + } + }; + + const handleClickShowPassword = (selector: string) => { + setShowPasswords((prev) => ({ + ...prev, + [selector]: !prev[selector], + })); + }; + + const handleRobotNameChange = (newName: string) => { + setRobot((prev) => + prev + ? { ...prev, recording_meta: { ...prev.recording_meta, name: newName } } + : prev + ); + }; + + const handleCredentialChange = (selector: string, value: string) => { + setCredentials((prev) => ({ + ...prev, + [selector]: { + ...prev[selector], + value, + }, + })); + }; + + const handleLimitChange = ( + pairIndex: number, + actionIndex: number, + argIndex: number, + newLimit: number + ) => { + setRobot((prev) => { + if (!prev) return prev; + + const updatedWorkflow = [...prev.recording.workflow]; + if ( + updatedWorkflow.length > pairIndex && + updatedWorkflow[pairIndex]?.what && + updatedWorkflow[pairIndex].what.length > actionIndex && + updatedWorkflow[pairIndex].what[actionIndex].args && + updatedWorkflow[pairIndex].what[actionIndex].args.length > argIndex + ) { + updatedWorkflow[pairIndex].what[actionIndex].args[argIndex].limit = + newLimit; + + setScrapeListLimits((prev) => { + return prev.map((item) => { + if ( + item.pairIndex === pairIndex && + item.actionIndex === actionIndex && + item.argIndex === argIndex + ) { + return { ...item, currentLimit: newLimit }; + } + return item; + }); + }); + } + + return { + ...prev, + recording: { ...prev.recording, workflow: updatedWorkflow }, + }; + }); + }; + + const handleTargetUrlChange = (newUrl: string) => { + setRobot((prev) => { + if (!prev) return prev; + + const updatedWorkflow = [...prev.recording.workflow]; + const lastPairIndex = updatedWorkflow.length - 1; + + if (lastPairIndex >= 0) { + const gotoAction = updatedWorkflow[lastPairIndex]?.what?.find( + (action) => action.action === "goto" + ); + if (gotoAction && gotoAction.args && gotoAction.args.length > 0) { + gotoAction.args[0] = newUrl; + } + } + + return { + ...prev, + recording_meta: { ...prev.recording_meta, url: newUrl }, + recording: { ...prev.recording, workflow: updatedWorkflow }, + }; + }); + }; + + const renderAllCredentialFields = () => { + return ( + <> + {renderCredentialFields( + credentialGroups.usernames, + t("Username"), + "text" + )} + + {renderCredentialFields(credentialGroups.emails, t("Email"), "text")} + + {renderCredentialFields( + credentialGroups.passwords, + t("Password"), + "password" + )} + + {renderCredentialFields(credentialGroups.others, t("Other"), "text")} + + ); + }; + + const renderScrapeListLimitFields = () => { + if (scrapeListLimits.length === 0) return null; + + return ( + <> + + {t("List Limits")} + + + {scrapeListLimits.map((limitInfo, index) => ( + { + const value = parseInt(e.target.value, 10); + if (value >= 1) { + handleLimitChange( + limitInfo.pairIndex, + limitInfo.actionIndex, + limitInfo.argIndex, + value + ); + } + }} + inputProps={{ min: 1 }} + style={{ marginBottom: "20px" }} + /> + ))} + + ); + }; + + const renderCredentialFields = ( + selectors: string[], + headerText: string, + defaultType: "text" | "password" = "text" + ) => { + if (selectors.length === 0) return null; + + return ( + <> + {selectors.map((selector, index) => { + const isVisible = showPasswords[selector]; + + return ( + handleCredentialChange(selector, e.target.value)} + fullWidth + style={{ marginBottom: "20px" }} + InputProps={{ + endAdornment: ( + + handleClickShowPassword(selector)} + edge="end" + disabled={!credentials[selector]?.value} + > + {isVisible ? : } + + + ), + }} + /> + ); + })} + + ); + }; + + const getTargetUrl = () => { + let url = robot?.recording_meta.url; + + if (!url) { + const lastPair = + robot?.recording.workflow[robot?.recording.workflow.length - 1]; + url = lastPair?.what.find((action) => action.action === "goto") + ?.args?.[0]; + } + + return url; + }; + + const handleSave = async () => { + if (!robot) return; + + setIsLoading(true); + try { + const credentialsForPayload = Object.entries(credentials).reduce( + (acc, [selector, info]) => { + const enforceType = info.type === "password" ? "password" : "text"; + + acc[selector] = { + value: info.value, + type: enforceType, + }; + return acc; + }, + {} as Record + ); + + const targetUrl = getTargetUrl(); + + const payload = { + name: robot.recording_meta.name, + limits: scrapeListLimits.map((limit) => ({ + pairIndex: limit.pairIndex, + actionIndex: limit.actionIndex, + argIndex: limit.argIndex, + limit: limit.currentLimit, + })), + credentials: credentialsForPayload, + targetUrl: targetUrl, + }; + + const success = await updateRecording(robot.recording_meta.id, payload); + + if (success) { + setRerenderRobots(true); + notify("success", t("robot_edit.notifications.update_success")); + handleStart(robot); + navigate("/robots"); + } else { + notify("error", t("robot_edit.notifications.update_failed")); + } + } catch (error) { + notify("error", t("robot_edit.notifications.update_error")); + console.error("Error updating robot:", error); + } finally { + setIsLoading(false); + } + }; + + const handleCancel = () => { + navigate("/robots"); + }; + + const lastPair = + robot?.recording.workflow[robot?.recording.workflow.length - 1]; + const targetUrl = lastPair?.what.find((action) => action.action === "goto") + ?.args?.[0]; + + return ( + + <> + + {robot && ( + <> + handleRobotNameChange(e.target.value)} + style={{ marginBottom: "20px" }} + /> + + handleTargetUrlChange(e.target.value)} + style={{ marginBottom: "20px" }} + /> + + {renderScrapeListLimitFields()} + + )} + + + + ); +}; diff --git a/src/components/robot/pages/RobotIntegrationPage.tsx b/src/components/robot/pages/RobotIntegrationPage.tsx new file mode 100644 index 00000000..723b5986 --- /dev/null +++ b/src/components/robot/pages/RobotIntegrationPage.tsx @@ -0,0 +1,1368 @@ +import React, { useState, useEffect } from "react"; +import { + MenuItem, + Typography, + CircularProgress, + Alert, + AlertTitle, + Button, + TextField, + IconButton, + Box, + Chip, + Card, + CardContent, + CardActions, + Switch, + FormControlLabel, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, +} from "@mui/material"; +import { + Add as AddIcon, + Delete as DeleteIcon, + Edit as EditIcon, + Science as ScienceIcon, +} from "@mui/icons-material"; +import axios from "axios"; +import { useGlobalInfoStore } from "../../../context/globalInfo"; +import { getStoredRecording } from "../../../api/storage"; +import { apiUrl } from "../../../apiConfig.js"; +import { v4 as uuid } from "uuid"; +import { useTranslation } from "react-i18next"; +import { useNavigate, useParams, useLocation } from "react-router-dom"; +import { + addWebhook, + updateWebhook, + removeWebhook, + getWebhooks, + testWebhook, + WebhookConfig, +} from "../../../api/webhook"; +import { RobotConfigPage } from "./RobotConfigPage"; + +interface IntegrationProps { + handleStart: (data: IntegrationSettings) => void; + robotPath?: string; + preSelectedIntegrationType?: "googleSheets" | "airtable" | "webhook" | null; +} + +export interface IntegrationSettings { + spreadsheetId?: string; + spreadsheetName?: string; + airtableBaseId?: string; + airtableBaseName?: string; + airtableTableName?: string; + airtableTableId?: string; + webhooks?: WebhookConfig[]; + data: string; + integrationType: "googleSheets" | "airtable" | "webhook"; +} + +const getCookie = (name: string): string | null => { + const value = `; ${document.cookie}`; + const parts = value.split(`; ${name}=`); + if (parts.length === 2) { + return parts.pop()?.split(";").shift() || null; + } + return null; +}; + +const removeCookie = (name: string): void => { + document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`; +}; + +export const RobotIntegrationPage = ({ + handleStart, + robotPath = "robots", + preSelectedIntegrationType = null, +}: IntegrationProps) => { + const { t } = useTranslation(); + const navigate = useNavigate(); + const location = useLocation(); + const params = useParams(); + + // Extract robotId and integrationType from URL manually since there's no specific route param defined + const pathSegments = location.pathname.split('/'); + const robotsIndex = pathSegments.findIndex(segment => segment === 'robots' || segment === 'prebuilt-robots'); + const integrateIndex = pathSegments.findIndex(segment => segment === 'integrate'); + + // Extract robotId from URL (more reliable than global store) + const robotIdFromUrl = robotsIndex !== -1 && robotsIndex + 1 < pathSegments.length + ? pathSegments[robotsIndex + 1] + : null; + + const integrationType = integrateIndex !== -1 && integrateIndex + 1 < pathSegments.length + ? pathSegments[integrateIndex + 1] as "googleSheets" | "airtable" | "webhook" + : preSelectedIntegrationType || null; + + + const [settings, setSettings] = useState({ + spreadsheetId: "", + spreadsheetName: "", + airtableBaseId: "", + airtableBaseName: "", + airtableTableName: "", + airtableTableId: "", + webhooks: [], + data: "", + integrationType: integrationType || "airtable", + }); + + const [spreadsheets, setSpreadsheets] = useState< + { id: string; name: string }[] + >([]); + const [airtableBases, setAirtableBases] = useState< + { id: string; name: string }[] + >([]); + const [airtableTables, setAirtableTables] = useState< + { id: string; name: string }[] + >([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [isLoading, setIsLoadingAction] = useState(false); + + const [showWebhookForm, setShowWebhookForm] = useState(false); + const [editingWebhook, setEditingWebhook] = useState(null); + const [newWebhook, setNewWebhook] = useState({ + id: "", + url: "", + events: ["run_completed"], + active: true, + }); + const [urlError, setUrlError] = useState(null); + + const { recordingId: recordingIdFromStore, notify, setRerenderRobots, setRecordingId } = useGlobalInfoStore(); + + // Use robotId from URL as primary source, fallback to global store + const recordingId = robotIdFromUrl || recordingIdFromStore; + + // Update global store if we extracted recordingId from URL + useEffect(() => { + if (robotIdFromUrl && robotIdFromUrl !== recordingIdFromStore) { + setRecordingId(robotIdFromUrl); + } + }, [robotIdFromUrl, recordingIdFromStore, setRecordingId]); + + const [recording, setRecording] = useState(null); + const [selectedIntegrationType, setSelectedIntegrationType] = useState< + "googleSheets" | "airtable" | "webhook" | null + >(integrationType); + + const authenticateWithAirtable = () => { + if (!recordingId) { + console.error("Cannot authenticate: recordingId is null"); + return; + } + + const basePath = robotPath === "prebuilt-robots" ? "/prebuilt-robots" : "/robots"; + const redirectUrl = `${window.location.origin}${basePath}/${recordingId}/integrate/airtable`; + window.location.href = `${apiUrl}/auth/airtable?robotId=${recordingId}&redirectUrl=${encodeURIComponent(redirectUrl)}`; + }; + + const validateWebhookData = ( + url: string, + events: string[], + excludeId?: string + ) => { + if (!url) { + setUrlError("Please provide webhook URL"); + return false; + } + + try { + new URL(url); + } catch { + setUrlError("Please provide a valid URL"); + return false; + } + + const existingWebhook = settings.webhooks?.find( + (webhook) => webhook.url === url && webhook.id !== excludeId + ); + + if (existingWebhook) { + setUrlError("This webhook URL is already in use"); + return false; + } + + if (!events || events.length === 0) { + setUrlError("Please select at least one event"); + return false; + } + + setUrlError(null); + return true; + }; + + const fetchWebhooks = async () => { + try { + setLoading(true); + if (!recordingId) return; + + const response = await getWebhooks(recordingId); + + if (response.ok && response.webhooks) { + setSettings((prev) => ({ + ...prev, + webhooks: response.webhooks, + })); + } + setLoading(false); + } catch (error: any) { + setLoading(false); + console.error("Error fetching webhooks:", error); + } + }; + + const addWebhookSetting = async () => { + if (!validateWebhookData(newWebhook.url, newWebhook.events)) { + if (!newWebhook.url) { + notify("error", "Please provide webhook URL"); + } else if (!newWebhook.events || newWebhook.events.length === 0) { + notify("error", "Please select at least one event"); + } + return; + } + + if (!recordingId) return; + + try { + setLoading(true); + const webhookWithId = { + ...newWebhook, + id: uuid(), + }; + + const response = await addWebhook(webhookWithId, recordingId); + + if (response.ok) { + setSettings((prev) => ({ + ...prev, + webhooks: [...(prev.webhooks || []), webhookWithId], + })); + + setNewWebhook({ + id: "", + url: "", + events: ["run_completed"], + active: true, + }); + setShowWebhookForm(false); + notify("success", "Webhook added successfully"); + } else { + notify("error", response.message || "Failed to add webhook"); + } + setLoading(false); + } catch (error: any) { + setLoading(false); + notify("error", "Failed to add webhook"); + console.error("Error adding webhook:", error); + } + }; + + const updateWebhookSetting = async () => { + if ( + !validateWebhookData( + newWebhook.url, + newWebhook.events, + editingWebhook || undefined + ) + ) { + return; + } + + if (!recordingId || !editingWebhook) return; + + try { + setLoading(true); + const response = await updateWebhook( + { ...newWebhook, id: editingWebhook }, + recordingId + ); + + if (response.ok) { + setSettings((prev) => ({ + ...prev, + webhooks: (prev.webhooks || []).map((webhook) => + webhook.id === editingWebhook + ? { ...newWebhook, id: editingWebhook } + : webhook + ), + })); + + setNewWebhook({ + id: "", + url: "", + events: ["run_completed"], + active: true, + }); + setEditingWebhook(null); + setShowWebhookForm(false); + notify("success", "Webhook updated successfully"); + } else { + notify("error", response.message || "Failed to update webhook"); + } + setLoading(false); + } catch (error: any) { + setLoading(false); + notify("error", "Failed to update webhook"); + console.error("Error updating webhook:", error); + } + }; + + const deleteWebhookSetting = async (webhookId: string) => { + if (!recordingId) return; + + try { + setLoading(true); + const response = await removeWebhook(webhookId, recordingId); + + if (response.ok) { + setSettings((prev) => ({ + ...prev, + webhooks: (prev.webhooks || []).filter( + (webhook) => webhook.id !== webhookId + ), + })); + + // Refresh recording data + if (recordingId) { + const updatedRecording = await getStoredRecording(recordingId); + setRecording(updatedRecording); + } + setRerenderRobots(true); + + notify("success", "Webhook removed successfully"); + } else { + notify("error", response.error || "Failed to remove webhook"); + } + setLoading(false); + } catch (error: any) { + setLoading(false); + notify("error", "Failed to remove webhook"); + console.error("Error removing webhook:", error); + } + }; + + const testWebhookSetting = async (webhookId: string) => { + if (!recordingId) return; + + const webhook = settings.webhooks?.find(w => w.id === webhookId); + if (!webhook) return; + + try { + setLoading(true); + const response = await testWebhook(webhook, recordingId); + + if (response.ok) { + notify("success", "Test webhook sent successfully"); + } else { + notify("error", response.message || "Failed to test webhook"); + } + setLoading(false); + } catch (error: any) { + setLoading(false); + notify("error", "Failed to test webhook"); + console.error("Error testing webhook:", error); + } + }; + + useEffect(() => { + setSelectedIntegrationType(integrationType); + setSettings(prev => ({ + ...prev, + integrationType: integrationType || "airtable" + })); + }, [integrationType]); + + useEffect(() => { + const fetchRecording = async () => { + if (recordingId) { + try { + const recording = await getStoredRecording(recordingId); + setRecording(recording); + } catch (error) { + console.error("Failed to fetch recording:", error); + } + } + }; + + fetchRecording(); + if (selectedIntegrationType === "webhook") { + fetchWebhooks(); + } + }, [recordingId, selectedIntegrationType]); + + const handleTabChange = (event: React.SyntheticEvent, newValue: string) => { + if (!recordingId) { + console.error("Cannot navigate: recordingId is null"); + return; + } + + const newIntegrationType = newValue as "googleSheets" | "airtable" | "webhook"; + setSelectedIntegrationType(newIntegrationType); + const basePath = + robotPath === "prebuilt-robots" ? "/prebuilt-robots" : "/robots"; + navigate(`${basePath}/${recordingId}/integrate/${newValue}`); + }; + + const handleCancel = () => { + const basePath = + robotPath === "prebuilt-robots" ? "/prebuilt-robots" : "/robots"; + navigate(basePath); + }; + + const handleSave = async () => { + setIsLoadingAction(true); + try { + await handleStart(settings); + const basePath = + robotPath === "prebuilt-robots" ? "/prebuilt-robots" : "/robots"; + navigate(basePath); + } catch (error) { + notify("error", "Failed to save integration settings"); + } finally { + setIsLoadingAction(false); + } + }; + + + // Fetch Airtable bases + const fetchAirtableBases = async () => { + try { + const response = await axios.get( + `${apiUrl}/auth/airtable/bases?robotId=${recordingId}`, + { withCredentials: true } + ); + setAirtableBases(response.data); + } catch (error: any) { + setLoading(false); + console.error("Error fetching Airtable bases:", error); + notify("error", t("integration_settings.airtable.errors.fetch_error", { + message: error.response?.data?.message || error.message, + })); + } + }; + + const fetchAirtableTables = async (baseId: string, recordingId: string) => { + try { + const response = await axios.get( + `${apiUrl}/auth/airtable/tables?robotId=${recordingId}&baseId=${baseId}`, + { withCredentials: true } + ); + setAirtableTables(response.data); + } catch (error: any) { + setLoading(false); + console.error("Error fetching Airtable tables:", error); + notify("error", t("integration_settings.airtable.errors.fetch_tables_error", { + message: error.response?.data?.message || error.message, + })); + } + }; + + // Handle Airtable base selection + const handleAirtableBaseSelect = async (e: React.ChangeEvent) => { + const selectedBase = airtableBases.find((base) => base.id === e.target.value); + + if (selectedBase) { + setSettings((prevSettings) => ({ + ...prevSettings, + airtableBaseId: selectedBase.id, + airtableBaseName: selectedBase.name, + })); + + if (recordingId) { + await fetchAirtableTables(selectedBase.id, recordingId); + } else { + console.error("Recording ID is null"); + } + } + }; + + const handleAirtabletableSelect = (e: React.ChangeEvent) => { + const selectedTable = airtableTables.find((table) => table.id === e.target.value); + if (selectedTable) { + setSettings((prevSettings) => ({ + ...prevSettings, + airtableTableId: e.target.value, + airtableTableName: selectedTable?.name || "", + })); + } + }; + + // Update Airtable integration + const updateAirtableBase = async () => { + try { + setLoading(true); + await axios.post( + `${apiUrl}/auth/airtable/update`, + { + baseId: settings.airtableBaseId, + baseName: settings.airtableBaseName, + robotId: recordingId, + tableName: settings.airtableTableName, + tableId: settings.airtableTableId, + }, + { withCredentials: true } + ); + + // Refresh recording data + if (recordingId) { + const updatedRecording = await getStoredRecording(recordingId); + setRecording(updatedRecording); + } + setRerenderRobots(true); + + notify("success", t("integration_settings.airtable.notifications.base_selected")); + setLoading(false); + } catch (error: any) { + setLoading(false); + console.error("Error updating Airtable base:", error); + notify("error", t("integration_settings.airtable.errors.update_error", { + message: error.response?.data?.message || error.message, + })); + } + }; + + // Remove Airtable integration + const removeAirtableIntegration = async () => { + try { + setLoading(true); + await axios.post( + `${apiUrl}/auth/airtable/remove`, + { robotId: recordingId }, + { withCredentials: true } + ); + + setAirtableBases([]); + setAirtableTables([]); + setSettings({ + ...settings, + airtableBaseId: "", + airtableBaseName: "", + airtableTableName: "", + airtableTableId: "", + }); + + // Refresh recording data + if (recordingId) { + const updatedRecording = await getStoredRecording(recordingId); + setRecording(updatedRecording); + } + setRerenderRobots(true); + + notify("success", t("integration_settings.airtable.notifications.integration_removed")); + setLoading(false); + } catch (error: any) { + setLoading(false); + console.error("Error removing Airtable integration:", error); + notify("error", t("integration_settings.airtable.errors.remove_error", { + message: error.response?.data?.message || error.message, + })); + } + }; + + const renderAirtableIntegration = () => ( + <> + + {t("integration_settings.airtable.title")} + + + {recording?.airtable_base_id ? ( + <> + + {t("integration_settings.airtable.alerts.success.title")} + {t("integration_settings.airtable.alerts.success.content", { + baseName: recording.airtable_base_name, + tableName: recording.airtable_table_name, + })} + + {t("integration_settings.airtable.alerts.success.here")} + + + + + ) : ( + <> + {!recording?.airtable_access_token ? ( + <> +

{t("integration_settings.airtable.descriptions.sync_info")}

+ + + ) : ( + <> + + {t("integration_settings.airtable.descriptions.authenticated_as")} + + {loading ? ( + + ) : error ? ( + {error} + ) : airtableBases.length === 0 ? ( + + + + + ) : ( + <> + + {airtableBases.map((base) => ( + + {base.name} + + ))} + + + {airtableTables.map((table) => ( + + {table.name} + + ))} + + + + )} + + )} + + )} + + ); + + const renderWebhookIntegration = () => ( + + + Webhook Integration + + + Configure webhooks to receive real-time notifications about robot events + + + {!showWebhookForm && ( + + )} + + {showWebhookForm && ( + + + + {editingWebhook ? "Edit Webhook" : "Add New Webhook"} + + + setNewWebhook((prev) => ({ ...prev, url: e.target.value })) + } + error={!!urlError} + helperText={urlError} + sx={{ mb: 2 }} + /> + + setNewWebhook((prev) => ({ + ...prev, + active: e.target.checked, + })) + } + /> + } + label="Active" + /> + + + + + + + )} + + {settings.webhooks && settings.webhooks.length > 0 && ( + + + + + URL + Status + Actions + + + + {settings.webhooks.map((webhook) => ( + + {webhook.url} + + + + + { + setNewWebhook(webhook); + setEditingWebhook(webhook.id); + setShowWebhookForm(true); + }} + size="small" + > + + + testWebhookSetting(webhook.id)} + size="small" + disabled={loading} + > + + + deleteWebhookSetting(webhook.id)} + size="small" + color="error" + > + + + + + ))} + +
+
+ )} +
+ ); + + const getIntegrationTitle = () => { + switch (selectedIntegrationType) { + case "airtable": + return "Airtable Integration"; + case "webhook": + return "Webhook Integration"; + default: + return "Integration Settings"; + } + }; + + const editWebhookSetting = (webhook: WebhookConfig) => { + setNewWebhook(webhook); + setEditingWebhook(webhook.id); + setShowWebhookForm(true); + }; + + const resetWebhookForm = () => { + setNewWebhook({ + id: "", + url: "", + events: ["run_completed"], + active: true, + }); + setShowWebhookForm(false); + setEditingWebhook(null); + setUrlError(null); + }; + + const toggleWebhookStatusSetting = async (webhookId: string) => { + if (!recordingId) return; + + try { + const webhook = settings.webhooks?.find((w) => w.id === webhookId); + if (!webhook) return; + + const updatedWebhook = { ...webhook, active: !webhook.active }; + + const response = await updateWebhook(updatedWebhook, recordingId); + + if (response.ok) { + const updatedWebhooks = (settings.webhooks || []).map((w) => + w.id === webhookId ? updatedWebhook : w + ); + setSettings({ ...settings, webhooks: updatedWebhooks }); + + // Refresh recording data + if (recordingId) { + const updatedRecording = await getStoredRecording(recordingId); + setRecording(updatedRecording); + } + setRerenderRobots(true); + + notify( + "success", + `Webhook ${updatedWebhook.active ? "enabled" : "disabled"}` + ); + } else { + notify("error", response.message || "Failed to update webhook"); + } + } catch (error: any) { + console.error("Error toggling webhook status:", error); + notify("error", "Failed to update webhook"); + } + }; + + const formatEventName = (event: string) => { + switch (event) { + case "run_completed": + return "Run finished"; + case "run_failed": + return "Run failed"; + default: + return event; + } + }; + + const formatLastCalled = (lastCalledAt?: string | null) => { + if (!lastCalledAt) { + return "Not called yet"; + } + + const date = new Date(lastCalledAt); + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); + const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); + const diffMinutes = Math.floor(diffMs / (1000 * 60)); + + if (diffMinutes < 1) { + return "Just now"; + } else if (diffMinutes < 60) { + return `${diffMinutes} minute${diffMinutes === 1 ? "" : "s"} ago`; + } else if (diffHours < 24) { + return `${diffHours} hour${diffHours === 1 ? "" : "s"} ago`; + } else if (diffDays < 7) { + return `${diffDays} day${diffDays === 1 ? "" : "s"} ago`; + } else { + return date.toLocaleDateString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }); + } + }; + + // Show integration selection if no type is selected + if (!selectedIntegrationType && !integrationType) { + return ( + +
+
+
+ + + + + + +
+ +
+
+
+
+
+ ); + } + + const handleBack = () => { + if (!recordingId) { + console.error("Cannot navigate: recordingId is null"); + return; + } + + setSelectedIntegrationType(null); + setSettings({ ...settings, integrationType: "airtable" }); + const basePath = robotPath === "prebuilt-robots" ? "/prebuilt-robots" : "/robots"; + navigate(`${basePath}/${recordingId}/integrate`); + }; + + return ( + +
+
+ {(selectedIntegrationType === "airtable" || integrationType === "airtable") && ( + <>{renderAirtableIntegration()} + )} + + {(selectedIntegrationType === "webhook" || integrationType === "webhook") && ( + <> + + Integrate using Webhooks + + + {settings.webhooks && settings.webhooks.length > 0 && ( + + + + + + Webhook URL + + + Call when + + + Last called + + + Status + + + Actions + + + + + {settings.webhooks.map((webhook) => ( + + {webhook.url} + + + {webhook.events.map((event) => ( + + ))} + + + + {formatLastCalled(webhook.lastCalledAt)} + + + toggleWebhookStatusSetting(webhook.id)} + size="small" + /> + + + + testWebhookSetting(webhook.id)} + disabled={loading || !webhook.active} + title="Test" + > + + + editWebhookSetting(webhook)} + disabled={loading} + title="Edit" + > + + + deleteWebhookSetting(webhook.id)} + disabled={loading} + title="Delete" + > + + + + + + ))} + +
+
+ )} + + {!showWebhookForm && ( + + + { + setNewWebhook({ ...newWebhook, url: e.target.value }); + if (urlError) setUrlError(null); + }} + error={!!urlError} + helperText={urlError} + required + aria-describedby="webhook-url-help" + /> + + setNewWebhook({ + ...newWebhook, + events: [e.target.value], + }) + } + sx={{ minWidth: "200px" }} + required + > + Run finished + Run failed + + + + + + Refer to the{" "} + + API documentation + {" "} + for examples and details. + + + + + )} + + {showWebhookForm && ( + + + + {editingWebhook ? "Edit Webhook" : "Add New Webhook"} + + + { + setNewWebhook({ ...newWebhook, url: e.target.value }); + if (urlError) setUrlError(null); + }} + sx={{ marginBottom: "15px" }} + placeholder="https://your-api.com/webhook/endpoint" + required + error={!!urlError} + helperText={urlError} + /> + + + setNewWebhook({ + ...newWebhook, + events: + typeof e.target.value === "string" + ? [e.target.value] + : e.target.value, + }) + } + SelectProps={{ + multiple: true, + renderValue: (selected) => ( + + {(selected as string[]).map((value) => ( + + ))} + + ), + }} + sx={{ marginBottom: "20px" }} + required + > + Run finished + Run failed + + + + setNewWebhook({ + ...newWebhook, + active: e.target.checked, + }) + } + /> + } + label="Active" + sx={{ marginBottom: "10px" }} + /> + + + + + + + + )} + + )} +
+
+
+ ); +}; diff --git a/src/components/robot/pages/RobotSettingsPage.tsx b/src/components/robot/pages/RobotSettingsPage.tsx new file mode 100644 index 00000000..7f0f15ae --- /dev/null +++ b/src/components/robot/pages/RobotSettingsPage.tsx @@ -0,0 +1,192 @@ +import React, { useState, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { TextField, Typography, Box, Card, CardContent } from "@mui/material"; +import { Settings, Info } from "@mui/icons-material"; +import { useGlobalInfoStore } from "../../../context/globalInfo"; +import { getStoredRecording } from "../../../api/storage"; +import { WhereWhatPair } from "maxun-core"; +import { getUserById } from "../../../api/auth"; +import { RobotConfigPage } from "./RobotConfigPage"; +import { useNavigate, useLocation } from "react-router-dom"; + +interface RobotMeta { + name: string; + id: string; + createdAt: string; + pairs: number; + updatedAt: string; + params: any[]; + url?: string; +} + +interface RobotWorkflow { + workflow: WhereWhatPair[]; +} + +interface ScheduleConfig { + runEvery: number; + runEveryUnit: "MINUTES" | "HOURS" | "DAYS" | "WEEKS" | "MONTHS"; + startFrom: + | "SUNDAY" + | "MONDAY" + | "TUESDAY" + | "WEDNESDAY" + | "THURSDAY" + | "FRIDAY" + | "SATURDAY"; + atTimeStart?: string; + atTimeEnd?: string; + timezone: string; + lastRunAt?: Date; + nextRunAt?: Date; + cronExpression?: string; +} + +export interface RobotSettings { + id: string; + userId?: number; + recording_meta: RobotMeta; + recording: RobotWorkflow; + google_sheet_email?: string | null; + google_sheet_name?: string | null; + google_sheet_id?: string | null; + google_access_token?: string | null; + google_refresh_token?: string | null; + schedule?: ScheduleConfig | null; +} + +interface RobotSettingsProps { + handleStart: (settings: RobotSettings) => void; +} + +export const RobotSettingsPage = ({ handleStart }: RobotSettingsProps) => { + const { t } = useTranslation(); + const navigate = useNavigate(); + const location = useLocation(); + const [userEmail, setUserEmail] = useState(null); + const [robot, setRobot] = useState(null); + const { recordingId, notify } = useGlobalInfoStore(); + + useEffect(() => { + getRobot(); + }, []); + + const getRobot = async () => { + if (recordingId) { + try { + const robot = await getStoredRecording(recordingId); + setRobot(robot); + } catch (error) { + notify("error", t("robot_settings.errors.robot_not_found")); + } + } else { + notify("error", t("robot_settings.errors.robot_not_found")); + } + }; + + const getTargetUrl = () => { + let url = robot?.recording_meta.url; + + if (!url) { + const lastPair = + robot?.recording.workflow[robot?.recording.workflow.length - 1]; + url = lastPair?.what.find((action) => action.action === "goto") + ?.args?.[0]; + } + + return url; + }; + + useEffect(() => { + const fetchUserEmail = async () => { + if (robot && robot.userId) { + try { + const userData = await getUserById(robot.userId.toString()); + if (userData && userData.user) { + setUserEmail(userData.user.email); + } + } catch (error) { + console.error("Failed to fetch user email:", error); + } + } + }; + fetchUserEmail(); + }, [robot?.userId]); + + const handleCancel = () => { + const basePath = location.pathname.includes("/prebuilt-robots") + ? "/prebuilt-robots" + : "/robots"; + navigate(basePath); + }; + + const targetUrl = getTargetUrl(); + + return ( + + <> + + {robot && ( + <> + + + {robot.recording.workflow?.[0]?.what?.[0]?.args?.[0]?.limit !== + undefined && ( + + )} + + + + )} + + + + ); +}; diff --git a/src/components/robot/pages/ScheduleSettingsPage.tsx b/src/components/robot/pages/ScheduleSettingsPage.tsx new file mode 100644 index 00000000..5108d485 --- /dev/null +++ b/src/components/robot/pages/ScheduleSettingsPage.tsx @@ -0,0 +1,373 @@ +import React, { useState, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { + MenuItem, + TextField, + Typography, + Box, + Button, +} from "@mui/material"; +import { Schedule } from "@mui/icons-material"; +import { Dropdown } from "../../ui/DropdownMui"; +import { validMomentTimezones } from "../../../constants/const"; +import { useGlobalInfoStore } from "../../../context/globalInfo"; +import { getSchedule, deleteSchedule } from "../../../api/storage"; +import { RobotConfigPage } from "./RobotConfigPage"; +import { useNavigate, useLocation } from "react-router-dom"; + +interface ScheduleSettingsProps { + handleStart: (settings: ScheduleSettings) => void; +} + +export interface ScheduleSettings { + runEvery: number; + runEveryUnit: string; + startFrom: string; + dayOfMonth?: string; + atTimeStart?: string; + atTimeEnd?: string; + timezone: string; +} + +export const ScheduleSettingsPage = ({ + handleStart, +}: ScheduleSettingsProps) => { + const { t } = useTranslation(); + const navigate = useNavigate(); + const location = useLocation(); + const [schedule, setSchedule] = useState(null); + const [settings, setSettings] = useState({ + runEvery: 1, + runEveryUnit: "HOURS", + startFrom: "MONDAY", + dayOfMonth: "1", + atTimeStart: "00:00", + atTimeEnd: "01:00", + timezone: "UTC", + }); + const [isLoading, setIsLoading] = useState(false); + + const handleChange = ( + field: keyof ScheduleSettings, + value: string | number | boolean + ) => { + setSettings((prev) => ({ ...prev, [field]: value })); + }; + + const units = ["MINUTES", "HOURS", "DAYS", "WEEKS", "MONTHS"]; + + const days = [ + "MONDAY", + "TUESDAY", + "WEDNESDAY", + "THURSDAY", + "FRIDAY", + "SATURDAY", + "SUNDAY", + ]; + + const { recordingId, notify } = useGlobalInfoStore(); + + const deleteRobotSchedule = async () => { + if (recordingId) { + setIsLoading(true); + try { + await deleteSchedule(recordingId); + setSchedule(null); + notify("success", t("Schedule deleted successfully")); + + setSettings({ + runEvery: 1, + runEveryUnit: "HOURS", + startFrom: "MONDAY", + dayOfMonth: "", + atTimeStart: "00:00", + atTimeEnd: "01:00", + timezone: "UTC", + }); + } catch (error) { + notify("error", "Failed to delete schedule"); + } finally { + setIsLoading(false); + } + } else { + console.error("No recording id provided"); + } + }; + + const getRobotSchedule = async () => { + if (recordingId) { + try { + const scheduleData = await getSchedule(recordingId); + setSchedule(scheduleData); + } catch (error) { + console.error("Failed to fetch robot schedule:", error); + } + } else { + console.error("No recording id provided"); + } + }; + + useEffect(() => { + const fetchSchedule = async () => { + await getRobotSchedule(); + }; + fetchSchedule(); + }, []); + + const handleSave = async () => { + setIsLoading(true); + try { + await handleStart(settings); + const basePath = location.pathname.includes("/prebuilt-robots") + ? "/prebuilt-robots" + : "/robots"; + navigate(basePath); + } catch (error) { + notify("error", "Failed to save schedule"); + } finally { + setIsLoading(false); + } + }; + + const handleCancel = () => { + const basePath = location.pathname.includes("/prebuilt-robots") + ? "/prebuilt-robots" + : "/robots"; + navigate(basePath); + }; + + const getDayOrdinal = (day: string | undefined) => { + if (!day) return ""; + const lastDigit = day.slice(-1); + const lastTwoDigits = day.slice(-2); + + // Special cases for 11, 12, 13 + if (["11", "12", "13"].includes(lastTwoDigits)) { + return t("schedule_settings.labels.on_day.th"); + } + + // Other cases + switch (lastDigit) { + case "1": + return t("schedule_settings.labels.on_day.st"); + case "2": + return t("schedule_settings.labels.on_day.nd"); + case "3": + return t("schedule_settings.labels.on_day.rd"); + default: + return t("schedule_settings.labels.on_day.th"); + } + }; + + const textStyle = { + width: "150px", + height: "52px", + marginRight: "10px", + }; + + const dropDownStyle = { + marginTop: "2px", + width: "150px", + height: "59px", + marginRight: "10px", + }; + + return ( + + *": { marginBottom: "20px" }, + }} + > + <> + {schedule !== null ? ( + <> + + {t("schedule_settings.run_every")}: {schedule.runEvery}{" "} + {schedule.runEveryUnit.toLowerCase()} + + + {["MONTHS", "WEEKS"].includes(settings.runEveryUnit) + ? t("schedule_settings.start_from") + : t("schedule_settings.start_from")} + : {schedule.startFrom.charAt(0).toUpperCase() + schedule.startFrom.slice(1).toLowerCase()} + + {schedule.runEveryUnit === "MONTHS" && ( + + {t("schedule_settings.on_day")}: {schedule.dayOfMonth} + {getDayOrdinal(schedule.dayOfMonth)} of the month + + )} + + {t("schedule_settings.at_around")}: {schedule.atTimeStart},{" "} + {schedule.timezone} {t("schedule_settings.timezone")} + + + + + + ) : ( + <> + + + {t("schedule_settings.labels.run_once_every")} + + + handleChange("runEvery", parseInt(e.target.value)) + } + sx={textStyle} + inputProps={{ min: 1 }} + /> + + handleChange("runEveryUnit", e.target.value) + } + sx={dropDownStyle} + > + {units.map((unit) => ( + + {" "} + {unit.charAt(0).toUpperCase() + + unit.slice(1).toLowerCase()} + + ))} + + + + + + {["MONTHS", "WEEKS"].includes(settings.runEveryUnit) + ? t("schedule_settings.labels.start_from_label") + : t("schedule_settings.labels.start_from_label")} + + + handleChange("startFrom", e.target.value) + } + sx={dropDownStyle} + > + {days.map((day) => ( + + {day.charAt(0).toUpperCase() + + day.slice(1).toLowerCase()} + + ))} + + + + {settings.runEveryUnit === "MONTHS" && ( + + + {t("schedule_settings.labels.on_day_of_month")} + + + handleChange("dayOfMonth", e.target.value) + } + sx={textStyle} + inputProps={{ min: 1, max: 31 }} + /> + + )} + + {["MINUTES", "HOURS"].includes(settings.runEveryUnit) ? ( + + + + {t("schedule_settings.labels.in_between")} + + + handleChange("atTimeStart", e.target.value) + } + sx={textStyle} + /> + + handleChange("atTimeEnd", e.target.value) + } + sx={textStyle} + /> + + + ) : ( + + + {t("schedule_settings.at_around")} + + + handleChange("atTimeStart", e.target.value) + } + sx={textStyle} + /> + + )} + + + + {t("schedule_settings.timezone")} + + + handleChange("timezone", e.target.value) + } + sx={dropDownStyle} + > + {validMomentTimezones.map((tz) => ( + + {tz.charAt(0).toUpperCase() + + tz.slice(1).toLowerCase()} + + ))} + + + + )} + + + + ); +}; From fa9b1bc5bb287d60bd58ba9f10689d727ef1f62c Mon Sep 17 00:00:00 2001 From: Rohit Date: Mon, 4 Aug 2025 23:04:12 +0530 Subject: [PATCH 03/38] feat: enable google sheets --- .../robot/pages/RobotIntegrationPage.tsx | 1135 +++++------------ 1 file changed, 324 insertions(+), 811 deletions(-) diff --git a/src/components/robot/pages/RobotIntegrationPage.tsx b/src/components/robot/pages/RobotIntegrationPage.tsx index 723b5986..ea8295c0 100644 --- a/src/components/robot/pages/RobotIntegrationPage.tsx +++ b/src/components/robot/pages/RobotIntegrationPage.tsx @@ -64,19 +64,6 @@ export interface IntegrationSettings { integrationType: "googleSheets" | "airtable" | "webhook"; } -const getCookie = (name: string): string | null => { - const value = `; ${document.cookie}`; - const parts = value.split(`; ${name}=`); - if (parts.length === 2) { - return parts.pop()?.split(";").shift() || null; - } - return null; -}; - -const removeCookie = (name: string): void => { - document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`; -}; - export const RobotIntegrationPage = ({ handleStart, robotPath = "robots", @@ -85,14 +72,12 @@ export const RobotIntegrationPage = ({ const { t } = useTranslation(); const navigate = useNavigate(); const location = useLocation(); - const params = useParams(); - // Extract robotId and integrationType from URL manually since there's no specific route param defined + // Extract robotId and integrationType from URL const pathSegments = location.pathname.split('/'); const robotsIndex = pathSegments.findIndex(segment => segment === 'robots' || segment === 'prebuilt-robots'); const integrateIndex = pathSegments.findIndex(segment => segment === 'integrate'); - // Extract robotId from URL (more reliable than global store) const robotIdFromUrl = robotsIndex !== -1 && robotsIndex + 1 < pathSegments.length ? pathSegments[robotsIndex + 1] : null; @@ -101,7 +86,6 @@ export const RobotIntegrationPage = ({ ? pathSegments[integrateIndex + 1] as "googleSheets" | "airtable" | "webhook" : preSelectedIntegrationType || null; - const [settings, setSettings] = useState({ spreadsheetId: "", spreadsheetName: "", @@ -114,15 +98,9 @@ export const RobotIntegrationPage = ({ integrationType: integrationType || "airtable", }); - const [spreadsheets, setSpreadsheets] = useState< - { id: string; name: string }[] - >([]); - const [airtableBases, setAirtableBases] = useState< - { id: string; name: string }[] - >([]); - const [airtableTables, setAirtableTables] = useState< - { id: string; name: string }[] - >([]); + const [spreadsheets, setSpreadsheets] = useState<{ id: string; name: string }[]>([]); + const [airtableBases, setAirtableBases] = useState<{ id: string; name: string }[]>([]); + const [airtableTables, setAirtableTables] = useState<{ id: string; name: string }[]>([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [isLoading, setIsLoadingAction] = useState(false); @@ -139,10 +117,8 @@ export const RobotIntegrationPage = ({ const { recordingId: recordingIdFromStore, notify, setRerenderRobots, setRecordingId } = useGlobalInfoStore(); - // Use robotId from URL as primary source, fallback to global store const recordingId = robotIdFromUrl || recordingIdFromStore; - // Update global store if we extracted recordingId from URL useEffect(() => { if (robotIdFromUrl && robotIdFromUrl !== recordingIdFromStore) { setRecordingId(robotIdFromUrl); @@ -154,17 +130,28 @@ export const RobotIntegrationPage = ({ "googleSheets" | "airtable" | "webhook" | null >(integrationType); + // --- AUTHENTICATION --- + const authenticateWithGoogle = () => { + if (!recordingId) { + console.error("Cannot authenticate: recordingId is null"); + return; + } + const basePath = robotPath === "prebuilt-robots" ? "/prebuilt-robots" : "/robots"; + const redirectUrl = `${window.location.origin}${basePath}/${recordingId}/integrate/googleSheets`; + window.location.href = `${apiUrl}/auth/google?robotId=${recordingId}&redirectUrl=${encodeURIComponent(redirectUrl)}`; + }; + const authenticateWithAirtable = () => { if (!recordingId) { console.error("Cannot authenticate: recordingId is null"); return; } - const basePath = robotPath === "prebuilt-robots" ? "/prebuilt-robots" : "/robots"; const redirectUrl = `${window.location.origin}${basePath}/${recordingId}/integrate/airtable`; window.location.href = `${apiUrl}/auth/airtable?robotId=${recordingId}&redirectUrl=${encodeURIComponent(redirectUrl)}`; }; + // --- WEBHOOKS --- const validateWebhookData = ( url: string, events: string[], @@ -174,28 +161,23 @@ export const RobotIntegrationPage = ({ setUrlError("Please provide webhook URL"); return false; } - try { new URL(url); } catch { setUrlError("Please provide a valid URL"); return false; } - const existingWebhook = settings.webhooks?.find( (webhook) => webhook.url === url && webhook.id !== excludeId ); - if (existingWebhook) { setUrlError("This webhook URL is already in use"); return false; } - if (!events || events.length === 0) { setUrlError("Please select at least one event"); return false; } - setUrlError(null); return true; }; @@ -204,14 +186,9 @@ export const RobotIntegrationPage = ({ try { setLoading(true); if (!recordingId) return; - const response = await getWebhooks(recordingId); - if (response.ok && response.webhooks) { - setSettings((prev) => ({ - ...prev, - webhooks: response.webhooks, - })); + setSettings((prev) => ({ ...prev, webhooks: response.webhooks })); } setLoading(false); } catch (error: any) { @@ -222,37 +199,18 @@ export const RobotIntegrationPage = ({ const addWebhookSetting = async () => { if (!validateWebhookData(newWebhook.url, newWebhook.events)) { - if (!newWebhook.url) { - notify("error", "Please provide webhook URL"); - } else if (!newWebhook.events || newWebhook.events.length === 0) { - notify("error", "Please select at least one event"); - } + if (!newWebhook.url) notify("error", "Please provide webhook URL"); + else if (!newWebhook.events || newWebhook.events.length === 0) notify("error", "Please select at least one event"); return; } - if (!recordingId) return; - try { setLoading(true); - const webhookWithId = { - ...newWebhook, - id: uuid(), - }; - + const webhookWithId = { ...newWebhook, id: uuid() }; const response = await addWebhook(webhookWithId, recordingId); - if (response.ok) { - setSettings((prev) => ({ - ...prev, - webhooks: [...(prev.webhooks || []), webhookWithId], - })); - - setNewWebhook({ - id: "", - url: "", - events: ["run_completed"], - active: true, - }); + setSettings((prev) => ({ ...prev, webhooks: [...(prev.webhooks || []), webhookWithId] })); + setNewWebhook({ id: "", url: "", events: ["run_completed"], active: true }); setShowWebhookForm(false); notify("success", "Webhook added successfully"); } else { @@ -267,41 +225,19 @@ export const RobotIntegrationPage = ({ }; const updateWebhookSetting = async () => { - if ( - !validateWebhookData( - newWebhook.url, - newWebhook.events, - editingWebhook || undefined - ) - ) { - return; - } - + if (!validateWebhookData(newWebhook.url, newWebhook.events, editingWebhook || undefined)) return; if (!recordingId || !editingWebhook) return; - try { setLoading(true); - const response = await updateWebhook( - { ...newWebhook, id: editingWebhook }, - recordingId - ); - + const response = await updateWebhook({ ...newWebhook, id: editingWebhook }, recordingId); if (response.ok) { setSettings((prev) => ({ ...prev, webhooks: (prev.webhooks || []).map((webhook) => - webhook.id === editingWebhook - ? { ...newWebhook, id: editingWebhook } - : webhook + webhook.id === editingWebhook ? { ...newWebhook, id: editingWebhook } : webhook ), })); - - setNewWebhook({ - id: "", - url: "", - events: ["run_completed"], - active: true, - }); + setNewWebhook({ id: "", url: "", events: ["run_completed"], active: true }); setEditingWebhook(null); setShowWebhookForm(false); notify("success", "Webhook updated successfully"); @@ -318,26 +254,16 @@ export const RobotIntegrationPage = ({ const deleteWebhookSetting = async (webhookId: string) => { if (!recordingId) return; - try { setLoading(true); const response = await removeWebhook(webhookId, recordingId); - if (response.ok) { - setSettings((prev) => ({ - ...prev, - webhooks: (prev.webhooks || []).filter( - (webhook) => webhook.id !== webhookId - ), - })); - - // Refresh recording data + setSettings((prev) => ({ ...prev, webhooks: (prev.webhooks || []).filter((webhook) => webhook.id !== webhookId) })); if (recordingId) { const updatedRecording = await getStoredRecording(recordingId); setRecording(updatedRecording); } setRerenderRobots(true); - notify("success", "Webhook removed successfully"); } else { notify("error", response.error || "Failed to remove webhook"); @@ -352,14 +278,11 @@ export const RobotIntegrationPage = ({ const testWebhookSetting = async (webhookId: string) => { if (!recordingId) return; - const webhook = settings.webhooks?.find(w => w.id === webhookId); if (!webhook) return; - try { setLoading(true); const response = await testWebhook(webhook, recordingId); - if (response.ok) { notify("success", "Test webhook sent successfully"); } else { @@ -373,74 +296,119 @@ export const RobotIntegrationPage = ({ } }; + // --- DATA FETCHING & STATE MANAGEMENT --- useEffect(() => { setSelectedIntegrationType(integrationType); - setSettings(prev => ({ - ...prev, - integrationType: integrationType || "airtable" - })); + setSettings(prev => ({ ...prev, integrationType: integrationType || "airtable" })); }, [integrationType]); useEffect(() => { const fetchRecording = async () => { if (recordingId) { try { - const recording = await getStoredRecording(recordingId); - setRecording(recording); + const recordingData = await getStoredRecording(recordingId); + setRecording(recordingData); } catch (error) { console.error("Failed to fetch recording:", error); } } }; - fetchRecording(); if (selectedIntegrationType === "webhook") { fetchWebhooks(); } }, [recordingId, selectedIntegrationType]); + // --- NAVIGATION & ACTIONS --- const handleTabChange = (event: React.SyntheticEvent, newValue: string) => { - if (!recordingId) { - console.error("Cannot navigate: recordingId is null"); - return; - } - + if (!recordingId) return; const newIntegrationType = newValue as "googleSheets" | "airtable" | "webhook"; setSelectedIntegrationType(newIntegrationType); - const basePath = - robotPath === "prebuilt-robots" ? "/prebuilt-robots" : "/robots"; + const basePath = robotPath === "prebuilt-robots" ? "/prebuilt-robots" : "/robots"; navigate(`${basePath}/${recordingId}/integrate/${newValue}`); }; const handleCancel = () => { - const basePath = - robotPath === "prebuilt-robots" ? "/prebuilt-robots" : "/robots"; + const basePath = robotPath === "prebuilt-robots" ? "/prebuilt-robots" : "/robots"; navigate(basePath); }; - const handleSave = async () => { - setIsLoadingAction(true); + // --- GOOGLE SHEETS --- + const fetchSpreadsheetFiles = async () => { try { - await handleStart(settings); - const basePath = - robotPath === "prebuilt-robots" ? "/prebuilt-robots" : "/robots"; - navigate(basePath); - } catch (error) { - notify("error", "Failed to save integration settings"); - } finally { - setIsLoadingAction(false); + setLoading(true); + const response = await axios.get(`${apiUrl}/auth/gsheets/files?robotId=${recordingId}`, { withCredentials: true }); + setSpreadsheets(response.data); + setLoading(false); + } catch (error: any) { + setLoading(false); + console.error("Error fetching spreadsheet files:", error); + notify("error", t("integration_settings.google.errors.fetch_error", { + message: error.response?.data?.message || error.message, + })); } }; + const handleSpreadsheetSelect = (e: React.ChangeEvent) => { + const selectedSheet = spreadsheets.find((sheet) => sheet.id === e.target.value); + if (selectedSheet) { + setSettings({ ...settings, spreadsheetId: selectedSheet.id, spreadsheetName: selectedSheet.name }); + } + }; - // Fetch Airtable bases + const updateGoogleSheetId = async () => { + try { + setLoading(true); + await axios.post(`${apiUrl}/auth/gsheets/update`, { + spreadsheetId: settings.spreadsheetId, + spreadsheetName: settings.spreadsheetName, + robotId: recordingId, + }, { withCredentials: true }); + if (recordingId) { + const updatedRecording = await getStoredRecording(recordingId); + setRecording(updatedRecording); + } + setRerenderRobots(true); + notify("success", t("integration_settings.google.notifications.sheet_selected")); + setLoading(false); + } catch (error: any) { + setLoading(false); + console.error("Error updating Google Sheet ID:", error); + notify("error", t("integration_settings.google.errors.update_error", { + message: error.response?.data?.message || error.message, + })); + } + }; + + const removeGoogleSheetsIntegration = async () => { + try { + setLoading(true); + await axios.post(`${apiUrl}/auth/gsheets/remove`, { robotId: recordingId }, { withCredentials: true }); + setSpreadsheets([]); + setSettings({ ...settings, spreadsheetId: "", spreadsheetName: "" }); + if (recordingId) { + const updatedRecording = await getStoredRecording(recordingId); + setRecording(updatedRecording); + } + setRerenderRobots(true); + notify("success", t("integration_settings.google.notifications.integration_removed")); + setLoading(false); + } catch (error: any) { + setLoading(false); + console.error("Error removing Google Sheets integration:", error); + notify("error", t("integration_settings.google.errors.remove_error", { + message: error.response?.data?.message || error.message, + })); + } + }; + + // --- AIRTABLE --- const fetchAirtableBases = async () => { try { - const response = await axios.get( - `${apiUrl}/auth/airtable/bases?robotId=${recordingId}`, - { withCredentials: true } - ); + setLoading(true); + const response = await axios.get(`${apiUrl}/auth/airtable/bases?robotId=${recordingId}`, { withCredentials: true }); setAirtableBases(response.data); + setLoading(false); } catch (error: any) { setLoading(false); console.error("Error fetching Airtable bases:", error); @@ -452,11 +420,10 @@ export const RobotIntegrationPage = ({ const fetchAirtableTables = async (baseId: string, recordingId: string) => { try { - const response = await axios.get( - `${apiUrl}/auth/airtable/tables?robotId=${recordingId}&baseId=${baseId}`, - { withCredentials: true } - ); + setLoading(true); + const response = await axios.get(`${apiUrl}/auth/airtable/tables?robotId=${recordingId}&baseId=${baseId}`, { withCredentials: true }); setAirtableTables(response.data); + setLoading(false); } catch (error: any) { setLoading(false); console.error("Error fetching Airtable tables:", error); @@ -466,59 +433,36 @@ export const RobotIntegrationPage = ({ } }; - // Handle Airtable base selection const handleAirtableBaseSelect = async (e: React.ChangeEvent) => { const selectedBase = airtableBases.find((base) => base.id === e.target.value); - if (selectedBase) { - setSettings((prevSettings) => ({ - ...prevSettings, - airtableBaseId: selectedBase.id, - airtableBaseName: selectedBase.name, - })); - - if (recordingId) { - await fetchAirtableTables(selectedBase.id, recordingId); - } else { - console.error("Recording ID is null"); - } + setSettings((prevSettings) => ({ ...prevSettings, airtableBaseId: selectedBase.id, airtableBaseName: selectedBase.name })); + if (recordingId) await fetchAirtableTables(selectedBase.id, recordingId); } }; const handleAirtabletableSelect = (e: React.ChangeEvent) => { const selectedTable = airtableTables.find((table) => table.id === e.target.value); if (selectedTable) { - setSettings((prevSettings) => ({ - ...prevSettings, - airtableTableId: e.target.value, - airtableTableName: selectedTable?.name || "", - })); + setSettings((prevSettings) => ({ ...prevSettings, airtableTableId: e.target.value, airtableTableName: selectedTable?.name || "" })); } }; - // Update Airtable integration const updateAirtableBase = async () => { try { setLoading(true); - await axios.post( - `${apiUrl}/auth/airtable/update`, - { - baseId: settings.airtableBaseId, - baseName: settings.airtableBaseName, - robotId: recordingId, - tableName: settings.airtableTableName, - tableId: settings.airtableTableId, - }, - { withCredentials: true } - ); - - // Refresh recording data + await axios.post(`${apiUrl}/auth/airtable/update`, { + baseId: settings.airtableBaseId, + baseName: settings.airtableBaseName, + robotId: recordingId, + tableName: settings.airtableTableName, + tableId: settings.airtableTableId, + }, { withCredentials: true }); if (recordingId) { const updatedRecording = await getStoredRecording(recordingId); setRecording(updatedRecording); } setRerenderRobots(true); - notify("success", t("integration_settings.airtable.notifications.base_selected")); setLoading(false); } catch (error: any) { @@ -530,33 +474,18 @@ export const RobotIntegrationPage = ({ } }; - // Remove Airtable integration const removeAirtableIntegration = async () => { try { setLoading(true); - await axios.post( - `${apiUrl}/auth/airtable/remove`, - { robotId: recordingId }, - { withCredentials: true } - ); - + await axios.post(`${apiUrl}/auth/airtable/remove`, { robotId: recordingId }, { withCredentials: true }); setAirtableBases([]); setAirtableTables([]); - setSettings({ - ...settings, - airtableBaseId: "", - airtableBaseName: "", - airtableTableName: "", - airtableTableId: "", - }); - - // Refresh recording data + setSettings({ ...settings, airtableBaseId: "", airtableBaseName: "", airtableTableName: "", airtableTableId: "" }); if (recordingId) { const updatedRecording = await getStoredRecording(recordingId); setRecording(updatedRecording); } setRerenderRobots(true); - notify("success", t("integration_settings.airtable.notifications.integration_removed")); setLoading(false); } catch (error: any) { @@ -568,36 +497,79 @@ export const RobotIntegrationPage = ({ } }; + // --- RENDER METHODS --- + const renderGoogleSheetsIntegration = () => ( + <> + + {t("integration_settings.google.title")} + + {recording?.google_sheet_id ? ( + <> + + {t("integration_settings.google.alerts.success.title")} + {t("integration_settings.google.alerts.success.content", { sheetName: recording.google_sheet_name })} + + {t("integration_settings.google.alerts.success.here")} + + + + + ) : ( + <> + {!recording?.google_sheet_email ? ( + <> +

{t("integration_settings.google.descriptions.sync_info")}

+ + + ) : ( + <> + + {t("integration_settings.google.descriptions.authenticated_as", { email: recording.google_sheet_email })} + + {loading ? ( + + ) : error ? ( + {error} + ) : spreadsheets.length === 0 ? ( + + ) : ( + <> + + {spreadsheets.map((sheet) => ({sheet.name}))} + + + + )} + + )} + + )} + + ); + const renderAirtableIntegration = () => ( <> {t("integration_settings.airtable.title")} - {recording?.airtable_base_id ? ( <> {t("integration_settings.airtable.alerts.success.title")} - {t("integration_settings.airtable.alerts.success.content", { - baseName: recording.airtable_base_name, - tableName: recording.airtable_table_name, - })} - + {t("integration_settings.airtable.alerts.success.content", { baseName: recording.airtable_base_name, tableName: recording.airtable_table_name })} + {t("integration_settings.airtable.alerts.success.here")} - @@ -606,12 +578,7 @@ export const RobotIntegrationPage = ({ {!recording?.airtable_access_token ? ( <>

{t("integration_settings.airtable.descriptions.sync_info")}

- @@ -626,62 +593,22 @@ export const RobotIntegrationPage = ({ {error} ) : airtableBases.length === 0 ? ( - - ) : ( <> - - {airtableBases.map((base) => ( - - {base.name} - - ))} + + {airtableBases.map((base) => ({base.name}))} - - {airtableTables.map((table) => ( - - {table.name} - - ))} + + {airtableTables.map((table) => ({table.name}))} - @@ -694,136 +621,38 @@ export const RobotIntegrationPage = ({ ); const renderWebhookIntegration = () => ( - - - Webhook Integration - - - Configure webhooks to receive real-time notifications about robot events - - + + Webhook Integration + Configure webhooks to receive real-time notifications about robot events {!showWebhookForm && ( - + )} - {showWebhookForm && ( - - {editingWebhook ? "Edit Webhook" : "Add New Webhook"} - - - setNewWebhook((prev) => ({ ...prev, url: e.target.value })) - } - error={!!urlError} - helperText={urlError} - sx={{ mb: 2 }} - /> - - setNewWebhook((prev) => ({ - ...prev, - active: e.target.checked, - })) - } - /> - } - label="Active" - /> + {editingWebhook ? "Edit Webhook" : "Add New Webhook"} + setNewWebhook((prev) => ({ ...prev, url: e.target.value }))} error={!!urlError} helperText={urlError} sx={{ mb: 2 }}/> + setNewWebhook((prev) => ({ ...prev, active: e.target.checked }))}/>} label="Active"/> - - + + )} - {settings.webhooks && settings.webhooks.length > 0 && ( - - - URL - Status - Actions - - + URLStatusActions {settings.webhooks.map((webhook) => ( {webhook.url} + - - - - { - setNewWebhook(webhook); - setEditingWebhook(webhook.id); - setShowWebhookForm(true); - }} - size="small" - > - - - testWebhookSetting(webhook.id)} - size="small" - disabled={loading} - > - - - deleteWebhookSetting(webhook.id)} - size="small" - color="error" - > - - + { setNewWebhook(webhook); setEditingWebhook(webhook.id); setShowWebhookForm(true); }} size="small"> + testWebhookSetting(webhook.id)} size="small" disabled={loading}> + deleteWebhookSetting(webhook.id)} size="small" color="error"> ))} @@ -836,6 +665,8 @@ export const RobotIntegrationPage = ({ const getIntegrationTitle = () => { switch (selectedIntegrationType) { + case "googleSheets": + return "Google Sheets Integration"; case "airtable": return "Airtable Integration"; case "webhook": @@ -852,12 +683,7 @@ export const RobotIntegrationPage = ({ }; const resetWebhookForm = () => { - setNewWebhook({ - id: "", - url: "", - events: ["run_completed"], - active: true, - }); + setNewWebhook({ id: "", url: "", events: ["run_completed"], active: true }); setShowWebhookForm(false); setEditingWebhook(null); setUrlError(null); @@ -865,32 +691,20 @@ export const RobotIntegrationPage = ({ const toggleWebhookStatusSetting = async (webhookId: string) => { if (!recordingId) return; - try { const webhook = settings.webhooks?.find((w) => w.id === webhookId); if (!webhook) return; - const updatedWebhook = { ...webhook, active: !webhook.active }; - const response = await updateWebhook(updatedWebhook, recordingId); - if (response.ok) { - const updatedWebhooks = (settings.webhooks || []).map((w) => - w.id === webhookId ? updatedWebhook : w - ); + const updatedWebhooks = (settings.webhooks || []).map((w) => w.id === webhookId ? updatedWebhook : w); setSettings({ ...settings, webhooks: updatedWebhooks }); - - // Refresh recording data if (recordingId) { const updatedRecording = await getStoredRecording(recordingId); setRecording(updatedRecording); } setRerenderRobots(true); - - notify( - "success", - `Webhook ${updatedWebhook.active ? "enabled" : "disabled"}` - ); + notify("success", `Webhook ${updatedWebhook.active ? "enabled" : "disabled"}`); } else { notify("error", response.message || "Failed to update webhook"); } @@ -902,464 +716,163 @@ export const RobotIntegrationPage = ({ const formatEventName = (event: string) => { switch (event) { - case "run_completed": - return "Run finished"; - case "run_failed": - return "Run failed"; - default: - return event; + case "run_completed": return "Run finished"; + case "run_failed": return "Run failed"; + default: return event; } }; const formatLastCalled = (lastCalledAt?: string | null) => { - if (!lastCalledAt) { - return "Not called yet"; - } - + if (!lastCalledAt) return "Not called yet"; const date = new Date(lastCalledAt); const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); const diffMinutes = Math.floor(diffMs / (1000 * 60)); - - if (diffMinutes < 1) { - return "Just now"; - } else if (diffMinutes < 60) { - return `${diffMinutes} minute${diffMinutes === 1 ? "" : "s"} ago`; - } else if (diffHours < 24) { - return `${diffHours} hour${diffHours === 1 ? "" : "s"} ago`; - } else if (diffDays < 7) { - return `${diffDays} day${diffDays === 1 ? "" : "s"} ago`; - } else { - return date.toLocaleDateString("en-US", { - year: "numeric", - month: "short", - day: "numeric", - hour: "2-digit", - minute: "2-digit", - }); - } + if (diffMinutes < 1) return "Just now"; + else if (diffMinutes < 60) return `${diffMinutes} minute${diffMinutes === 1 ? "" : "s"} ago`; + else if (diffHours < 24) return `${diffHours} hour${diffHours === 1 ? "" : "s"} ago`; + else if (diffDays < 7) return `${diffDays} day${diffDays === 1 ? "" : "s"} ago`; + else return date.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }); }; - // Show integration selection if no type is selected + // --- MAIN RENDER --- if (!selectedIntegrationType && !integrationType) { return ( - -
-
+ +
+
- - - - - - -
- + + +
-
); } const handleBack = () => { - if (!recordingId) { - console.error("Cannot navigate: recordingId is null"); - return; - } - + if (!recordingId) return; setSelectedIntegrationType(null); - setSettings({ ...settings, integrationType: "airtable" }); const basePath = robotPath === "prebuilt-robots" ? "/prebuilt-robots" : "/robots"; navigate(`${basePath}/${recordingId}/integrate`); }; return ( - -
+ +
+ {(selectedIntegrationType === "googleSheets" || integrationType === "googleSheets") && ( + <>{renderGoogleSheetsIntegration()} + )} + {(selectedIntegrationType === "airtable" || integrationType === "airtable") && ( <>{renderAirtableIntegration()} )} {(selectedIntegrationType === "webhook" || integrationType === "webhook") && ( - <> - - Integrate using Webhooks - - - {settings.webhooks && settings.webhooks.length > 0 && ( - -
- - - - Webhook URL - - - Call when - - - Last called - - - Status - - - Actions - - - - - {settings.webhooks.map((webhook) => ( - - {webhook.url} - - - {webhook.events.map((event) => ( - - ))} - - - - {formatLastCalled(webhook.lastCalledAt)} - - - toggleWebhookStatusSetting(webhook.id)} - size="small" - /> - - - - testWebhookSetting(webhook.id)} - disabled={loading || !webhook.active} - title="Test" - > - - - editWebhookSetting(webhook)} - disabled={loading} - title="Edit" - > - - - deleteWebhookSetting(webhook.id)} - disabled={loading} - title="Delete" - > - - - - + <> + Integrate using Webhooks + {settings.webhooks && settings.webhooks.length > 0 && ( + +
+ + + Webhook URL + Call when + Last called + Status + Actions - ))} - -
-
- )} - - {!showWebhookForm && ( - - - { - setNewWebhook({ ...newWebhook, url: e.target.value }); - if (urlError) setUrlError(null); - }} - error={!!urlError} - helperText={urlError} - required - aria-describedby="webhook-url-help" - /> - - setNewWebhook({ - ...newWebhook, - events: [e.target.value], - }) - } - sx={{ minWidth: "200px" }} - required - > - Run finished - Run failed - + + + {settings.webhooks.map((webhook) => ( + + {webhook.url} + {webhook.events.map((event) => ())} + {formatLastCalled(webhook.lastCalledAt)} + toggleWebhookStatusSetting(webhook.id)} size="small"/> + + testWebhookSetting(webhook.id)} disabled={loading || !webhook.active} title="Test"> + editWebhookSetting(webhook)} disabled={loading} title="Edit"> + deleteWebhookSetting(webhook.id)} disabled={loading} title="Delete"> + + + ))} + + + + )} + {!showWebhookForm && ( + + + { setNewWebhook({ ...newWebhook, url: e.target.value }); if (urlError) setUrlError(null); }} error={!!urlError} helperText={urlError} required aria-describedby="webhook-url-help"/> + setNewWebhook({ ...newWebhook, events: [e.target.value] })} sx={{ minWidth: "200px" }} required> + Run finished + Run failed + + + + Refer to the API documentation for examples and details. + + - - - - Refer to the{" "} - - API documentation - {" "} - for examples and details. - - - - - )} - - {showWebhookForm && ( - - - - {editingWebhook ? "Edit Webhook" : "Add New Webhook"} - - - { - setNewWebhook({ ...newWebhook, url: e.target.value }); - if (urlError) setUrlError(null); - }} - sx={{ marginBottom: "15px" }} - placeholder="https://your-api.com/webhook/endpoint" - required - error={!!urlError} - helperText={urlError} - /> - - - setNewWebhook({ - ...newWebhook, - events: - typeof e.target.value === "string" - ? [e.target.value] - : e.target.value, - }) - } - SelectProps={{ - multiple: true, - renderValue: (selected) => ( - - {(selected as string[]).map((value) => ( - - ))} - - ), - }} - sx={{ marginBottom: "20px" }} - required - > - Run finished - Run failed - - - - setNewWebhook({ - ...newWebhook, - active: e.target.checked, - }) - } - /> - } - label="Active" - sx={{ marginBottom: "10px" }} - /> - - - - - - - - )} - + )} + {showWebhookForm && ( + + + {editingWebhook ? "Edit Webhook" : "Add New Webhook"} + { setNewWebhook({ ...newWebhook, url: e.target.value }); if (urlError) setUrlError(null); }} sx={{ marginBottom: "15px" }} placeholder="https://your-api.com/webhook/endpoint" required error={!!urlError} helperText={urlError}/> + setNewWebhook({ ...newWebhook, events: typeof e.target.value === "string" ? [e.target.value] : e.target.value })} + SelectProps={{ multiple: true, renderValue: (selected) => ({(selected as string[]).map((value) => ())}),}} sx={{ marginBottom: "20px" }} required> + Run finished + Run failed + + setNewWebhook({ ...newWebhook, active: e.target.checked })}/>} label="Active" sx={{ marginBottom: "10px" }}/> + + + + + + + )} + )} From 150434ab66e1fb8a3b921aeefc6f05b20e0d437e Mon Sep 17 00:00:00 2001 From: Rohit Date: Mon, 4 Aug 2025 23:11:16 +0530 Subject: [PATCH 04/38] chore: cleanup --- .../robot/pages/RobotIntegrationPage.tsx | 60 ------------------- 1 file changed, 60 deletions(-) diff --git a/src/components/robot/pages/RobotIntegrationPage.tsx b/src/components/robot/pages/RobotIntegrationPage.tsx index ea8295c0..b18e1cd0 100644 --- a/src/components/robot/pages/RobotIntegrationPage.tsx +++ b/src/components/robot/pages/RobotIntegrationPage.tsx @@ -73,7 +73,6 @@ export const RobotIntegrationPage = ({ const navigate = useNavigate(); const location = useLocation(); - // Extract robotId and integrationType from URL const pathSegments = location.pathname.split('/'); const robotsIndex = pathSegments.findIndex(segment => segment === 'robots' || segment === 'prebuilt-robots'); const integrateIndex = pathSegments.findIndex(segment => segment === 'integrate'); @@ -103,7 +102,6 @@ export const RobotIntegrationPage = ({ const [airtableTables, setAirtableTables] = useState<{ id: string; name: string }[]>([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const [isLoading, setIsLoadingAction] = useState(false); const [showWebhookForm, setShowWebhookForm] = useState(false); const [editingWebhook, setEditingWebhook] = useState(null); @@ -130,7 +128,6 @@ export const RobotIntegrationPage = ({ "googleSheets" | "airtable" | "webhook" | null >(integrationType); - // --- AUTHENTICATION --- const authenticateWithGoogle = () => { if (!recordingId) { console.error("Cannot authenticate: recordingId is null"); @@ -151,7 +148,6 @@ export const RobotIntegrationPage = ({ window.location.href = `${apiUrl}/auth/airtable?robotId=${recordingId}&redirectUrl=${encodeURIComponent(redirectUrl)}`; }; - // --- WEBHOOKS --- const validateWebhookData = ( url: string, events: string[], @@ -296,7 +292,6 @@ export const RobotIntegrationPage = ({ } }; - // --- DATA FETCHING & STATE MANAGEMENT --- useEffect(() => { setSelectedIntegrationType(integrationType); setSettings(prev => ({ ...prev, integrationType: integrationType || "airtable" })); @@ -319,21 +314,11 @@ export const RobotIntegrationPage = ({ } }, [recordingId, selectedIntegrationType]); - // --- NAVIGATION & ACTIONS --- - const handleTabChange = (event: React.SyntheticEvent, newValue: string) => { - if (!recordingId) return; - const newIntegrationType = newValue as "googleSheets" | "airtable" | "webhook"; - setSelectedIntegrationType(newIntegrationType); - const basePath = robotPath === "prebuilt-robots" ? "/prebuilt-robots" : "/robots"; - navigate(`${basePath}/${recordingId}/integrate/${newValue}`); - }; - const handleCancel = () => { const basePath = robotPath === "prebuilt-robots" ? "/prebuilt-robots" : "/robots"; navigate(basePath); }; - // --- GOOGLE SHEETS --- const fetchSpreadsheetFiles = async () => { try { setLoading(true); @@ -402,7 +387,6 @@ export const RobotIntegrationPage = ({ } }; - // --- AIRTABLE --- const fetchAirtableBases = async () => { try { setLoading(true); @@ -497,7 +481,6 @@ export const RobotIntegrationPage = ({ } }; - // --- RENDER METHODS --- const renderGoogleSheetsIntegration = () => ( <> @@ -620,49 +603,6 @@ export const RobotIntegrationPage = ({ ); - const renderWebhookIntegration = () => ( - - Webhook Integration - Configure webhooks to receive real-time notifications about robot events - {!showWebhookForm && ( - - )} - {showWebhookForm && ( - - - {editingWebhook ? "Edit Webhook" : "Add New Webhook"} - setNewWebhook((prev) => ({ ...prev, url: e.target.value }))} error={!!urlError} helperText={urlError} sx={{ mb: 2 }}/> - setNewWebhook((prev) => ({ ...prev, active: e.target.checked }))}/>} label="Active"/> - - - - - - - )} - {settings.webhooks && settings.webhooks.length > 0 && ( - - - URLStatusActions - - {settings.webhooks.map((webhook) => ( - - {webhook.url} - - - { setNewWebhook(webhook); setEditingWebhook(webhook.id); setShowWebhookForm(true); }} size="small"> - testWebhookSetting(webhook.id)} size="small" disabled={loading}> - deleteWebhookSetting(webhook.id)} size="small" color="error"> - - - ))} - -
-
- )} -
- ); - const getIntegrationTitle = () => { switch (selectedIntegrationType) { case "googleSheets": From ee002a7a7cd823326408c2fd7f80dba8737fe70d Mon Sep 17 00:00:00 2001 From: Rohit Date: Mon, 4 Aug 2025 23:22:33 +0530 Subject: [PATCH 05/38] fix: cancel button dark theme UI --- src/components/robot/pages/RobotConfigPage.tsx | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/components/robot/pages/RobotConfigPage.tsx b/src/components/robot/pages/RobotConfigPage.tsx index e394ddca..665223dc 100644 --- a/src/components/robot/pages/RobotConfigPage.tsx +++ b/src/components/robot/pages/RobotConfigPage.tsx @@ -144,18 +144,10 @@ export const RobotConfigPage: React.FC = ({ onClick={handleBack} disabled={isLoading} sx={{ - color: '#ff00c3', - borderColor: '#ff00c3', - bgcolor: 'white', - '&:hover': { - borderColor: '#cc0099', - bgcolor: '#fff5fc', - }, - textTransform: 'none', - fontWeight: 500, - px: 3 - }} - > + color: '#ff00c3 !important', + borderColor: '#ff00c3 !important', + backgroundColor: 'whitesmoke !important', + }} > {cancelButtonText} )} From 2a5989ff99884f92382e814f541e923363208ce3 Mon Sep 17 00:00:00 2001 From: Rohit Date: Mon, 4 Aug 2025 23:25:43 +0530 Subject: [PATCH 06/38] feat: add white button --- src/components/robot/pages/RobotConfigPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/robot/pages/RobotConfigPage.tsx b/src/components/robot/pages/RobotConfigPage.tsx index 665223dc..fa9a2d0a 100644 --- a/src/components/robot/pages/RobotConfigPage.tsx +++ b/src/components/robot/pages/RobotConfigPage.tsx @@ -146,7 +146,7 @@ export const RobotConfigPage: React.FC = ({ sx={{ color: '#ff00c3 !important', borderColor: '#ff00c3 !important', - backgroundColor: 'whitesmoke !important', + backgroundColor: 'white !important', }} > {cancelButtonText} From 26d19a7b68d0a39f4f477bed958d344e441574bb Mon Sep 17 00:00:00 2001 From: Rohit Date: Mon, 4 Aug 2025 23:36:07 +0530 Subject: [PATCH 07/38] fix: translation back and cancel --- src/components/robot/pages/RobotIntegrationPage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/robot/pages/RobotIntegrationPage.tsx b/src/components/robot/pages/RobotIntegrationPage.tsx index b18e1cd0..bffe8da8 100644 --- a/src/components/robot/pages/RobotIntegrationPage.tsx +++ b/src/components/robot/pages/RobotIntegrationPage.tsx @@ -680,7 +680,7 @@ export const RobotIntegrationPage = ({ // --- MAIN RENDER --- if (!selectedIntegrationType && !integrationType) { return ( - + navigate(`/${robotPath}/${recordingId}/integrate`)}>
@@ -733,7 +733,7 @@ export const RobotIntegrationPage = ({ }; return ( - +
{(selectedIntegrationType === "googleSheets" || integrationType === "googleSheets") && ( From 080c0790b39bd0d4a8ebdc8e9bc37832e78fae06 Mon Sep 17 00:00:00 2001 From: Rohit Date: Mon, 4 Aug 2025 23:39:20 +0530 Subject: [PATCH 08/38] feat: add translations for close button --- public/locales/de.json | 3 +++ public/locales/en.json | 3 +++ public/locales/es.json | 3 +++ public/locales/ja.json | 3 +++ public/locales/tr.json | 3 +++ public/locales/zh.json | 3 +++ 6 files changed, 18 insertions(+) diff --git a/public/locales/de.json b/public/locales/de.json index eef6ace5..51e38314 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -426,6 +426,9 @@ "created_at": "Erstellungsdatum des Roboters", "errors": { "robot_not_found": "Roboterdetails konnten nicht gefunden werden. Bitte versuchen Sie es erneut." + }, + "buttons": { + "close": "Schließen" } }, "robot_edit": { diff --git a/public/locales/en.json b/public/locales/en.json index 7721231e..3a686a68 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -439,6 +439,9 @@ "created_at": "Robot Created At", "errors": { "robot_not_found": "Could not find robot details. Please try again." + }, + "buttons": { + "close": "Close" } }, "robot_edit": { diff --git a/public/locales/es.json b/public/locales/es.json index c98b2c8b..701cadb3 100644 --- a/public/locales/es.json +++ b/public/locales/es.json @@ -427,6 +427,9 @@ "created_at": "Fecha de Creación del Robot", "errors": { "robot_not_found": "No se pudieron encontrar los detalles del robot. Inténtelo de nuevo." + }, + "buttons": { + "close": "Cerrar" } }, "robot_edit": { diff --git a/public/locales/ja.json b/public/locales/ja.json index 32ca0654..cfbfe24e 100644 --- a/public/locales/ja.json +++ b/public/locales/ja.json @@ -427,6 +427,9 @@ "created_at": "作成日時", "errors": { "robot_not_found": "ロボットの詳細が見つかりませんでした。もう一度試してください。" + }, + "buttons": { + "close": "閉じる" } }, "robot_edit": { diff --git a/public/locales/tr.json b/public/locales/tr.json index 6c714fe0..8d30432b 100644 --- a/public/locales/tr.json +++ b/public/locales/tr.json @@ -439,6 +439,9 @@ "created_at": "Oluşturulma", "errors": { "robot_not_found": "Robot bulunamadı. Tekrar deneyin." + }, + "buttons": { + "close": "Kapat" } }, "robot_edit": { diff --git a/public/locales/zh.json b/public/locales/zh.json index 6bb6e9b3..c98b18e4 100644 --- a/public/locales/zh.json +++ b/public/locales/zh.json @@ -427,6 +427,9 @@ "created_at": "机器人创建时间", "errors": { "robot_not_found": "无法找到机器人详细信息。请重试。" + }, + "buttons": { + "close": "关闭" } }, "robot_edit": { From b04c5ef0cccb34744666b72d9600a3a2c64c9dfe Mon Sep 17 00:00:00 2001 From: Rohit Date: Mon, 4 Aug 2025 23:40:25 +0530 Subject: [PATCH 09/38] feat: add translation key --- src/components/robot/pages/RobotSettingsPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/robot/pages/RobotSettingsPage.tsx b/src/components/robot/pages/RobotSettingsPage.tsx index 7f0f15ae..b5355082 100644 --- a/src/components/robot/pages/RobotSettingsPage.tsx +++ b/src/components/robot/pages/RobotSettingsPage.tsx @@ -126,7 +126,7 @@ export const RobotSettingsPage = ({ handleStart }: RobotSettingsProps) => { <> From 8b81756fda4e0302c1c3745b98224dabcaa242e9 Mon Sep 17 00:00:00 2001 From: Rohit Date: Mon, 4 Aug 2025 23:43:36 +0530 Subject: [PATCH 10/38] fix dark theme back button ui --- src/components/robot/pages/RobotConfigPage.tsx | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/components/robot/pages/RobotConfigPage.tsx b/src/components/robot/pages/RobotConfigPage.tsx index fa9a2d0a..5923842a 100644 --- a/src/components/robot/pages/RobotConfigPage.tsx +++ b/src/components/robot/pages/RobotConfigPage.tsx @@ -124,15 +124,10 @@ export const RobotConfigPage: React.FC = ({ onClick={onBackToSelection} disabled={isLoading} sx={{ - color: '#ff00c3', - borderColor: '#ff00c3', - bgcolor: 'whitesmoke', - '&:hover': { - bgcolor: '#f0f0f0', - borderColor: '#cc0099' - } - }} - > + color: '#ff00c3 !important', + borderColor: '#ff00c3 !important', + backgroundColor: 'white !important', + }} > {backToSelectionText} )} From 7ae134dc4ec46db83d5022d3c172e4306803a860 Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 14 Aug 2025 15:57:01 +0530 Subject: [PATCH 11/38] feat: add queued run namespace --- server/src/server.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/server/src/server.ts b/server/src/server.ts index 7f2d04d3..11b25a37 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -162,6 +162,22 @@ app.use((req, res, next) => { next(); }); +io.of('/queued-run').on('connection', (socket) => { + const userId = socket.handshake.query.userId as string; + + if (userId) { + socket.join(`user-${userId}`); + logger.log('info', `Client joined queued-run namespace for user: ${userId}, socket: ${socket.id}`); + + socket.on('disconnect', () => { + logger.log('info', `Client disconnected from queued-run namespace: ${socket.id}`); + }); + } else { + logger.log('warn', `Client connected to queued-run namespace without userId: ${socket.id}`); + socket.disconnect(); + } +}); + server.listen(SERVER_PORT, '0.0.0.0', async () => { try { await connectDB(); From a2f41064840e9427ac46218f02e8adf5b8f46b54 Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 14 Aug 2025 16:03:54 +0530 Subject: [PATCH 12/38] feat: add queue socket conn logic --- src/context/socket.tsx | 68 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/src/context/socket.tsx b/src/context/socket.tsx index 4d9c95e1..05a8c989 100644 --- a/src/context/socket.tsx +++ b/src/context/socket.tsx @@ -1,4 +1,4 @@ -import React, { createContext, useCallback, useContext, useMemo, useState } from 'react'; +import React, { createContext, useCallback, useContext, useState, useRef, useEffect } from 'react'; import { io, Socket } from 'socket.io-client'; import { apiUrl } from "../apiConfig"; @@ -6,12 +6,16 @@ const SERVER_ENDPOINT = apiUrl; interface SocketState { socket: Socket | null; + queueSocket: Socket | null; id: string; setId: (id: string) => void; + connectToQueueSocket: (userId: string, onRunCompleted?: (data: any) => void) => void; + disconnectQueueSocket: () => void; }; class SocketStore implements Partial { - socket = null; + socket: Socket | null = null; + queueSocket: Socket | null = null; id = ''; }; @@ -22,7 +26,9 @@ export const useSocketStore = () => useContext(socketStoreContext); export const SocketProvider = ({ children }: { children: JSX.Element }) => { const [socket, setSocket] = useState(socketStore.socket); + const [queueSocket, setQueueSocket] = useState(socketStore.queueSocket); const [id, setActiveId] = useState(socketStore.id); + const runCompletedCallbackRef = useRef<((data: any) => void) | null>(null); const setId = useCallback((id: string) => { // the socket client connection is recomputed whenever id changes -> the new browser has been initialized @@ -39,12 +45,70 @@ export const SocketProvider = ({ children }: { children: JSX.Element }) => { setActiveId(id); }, [setSocket]); + const connectToQueueSocket = useCallback((userId: string, onRunCompleted?: (data: any) => void) => { + runCompletedCallbackRef.current = onRunCompleted || null; + + const newQueueSocket = io(`${SERVER_ENDPOINT}/queued-run`, { + transports: ["websocket"], + rejectUnauthorized: false, + query: { userId } + }); + + newQueueSocket.on('connect', () => { + console.log('Queue socket connected for user:', userId); + }); + + newQueueSocket.on('connect_error', (error) => { + console.log('Queue socket connection error:', error); + }); + + newQueueSocket.on('run-completed', (completionData) => { + console.log('Run completed event received:', completionData); + if (runCompletedCallbackRef.current) { + runCompletedCallbackRef.current(completionData); + } + }); + + setQueueSocket(currentSocket => { + if (currentSocket) { + currentSocket.disconnect(); + } + return newQueueSocket; + }); + + socketStore.queueSocket = newQueueSocket; + }, []); + + const disconnectQueueSocket = useCallback(() => { + setQueueSocket(currentSocket => { + if (currentSocket) { + currentSocket.disconnect(); + } + return null; + }); + + socketStore.queueSocket = null; + runCompletedCallbackRef.current = null; + }, []); + + // Cleanup on unmount + useEffect(() => { + return () => { + if (queueSocket) { + queueSocket.disconnect(); + } + }; + }, [queueSocket]); + return ( {children} From fa0d1533b7c78a55fd21214d7d91a4711ff47297 Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 14 Aug 2025 16:09:23 +0530 Subject: [PATCH 13/38] feat: add queued run completed notif --- src/pages/MainPage.tsx | 76 ++++++++++++++++++------------------------ 1 file changed, 33 insertions(+), 43 deletions(-) diff --git a/src/pages/MainPage.tsx b/src/pages/MainPage.tsx index ed223ba1..c71478b3 100644 --- a/src/pages/MainPage.tsx +++ b/src/pages/MainPage.tsx @@ -15,6 +15,7 @@ import { ScheduleSettings } from "../components/robot/ScheduleSettings"; import { apiUrl } from "../apiConfig"; import { useNavigate } from 'react-router-dom'; import { AuthContext } from '../context/auth'; +import { useSocketStore } from '../context/socket'; interface MainPageProps { handleEditRecording: (id: string, fileName: string) => void; @@ -54,6 +55,8 @@ export const MainPage = ({ handleEditRecording, initialContent }: MainPageProps) const { state } = useContext(AuthContext); const { user } = state; + const { connectToQueueSocket, disconnectQueueSocket } = useSocketStore(); + const abortRunHandler = (runId: string, robotName: string, browserId: string) => { notify('info', t('main_page.notifications.abort_initiated', { name: robotName })); @@ -138,50 +141,7 @@ export const MainPage = ({ handleEditRecording, initialContent }: MainPageProps) navigate(`/runs/${robotMetaId}/run/${runId}`); if (queued) { - console.log('Creating queue socket for queued run:', runId); - setQueuedRuns(prev => new Set([...prev, runId])); - - const queueSocket = io(`${apiUrl}/queued-run`, { - transports: ["websocket"], - rejectUnauthorized: false, - query: { userId: user?.id } - }); - - queueSocket.on('connect', () => { - console.log('Queue socket connected for user:', user?.id); - }); - - queueSocket.on('connect_error', (error) => { - console.log('Queue socket connection error:', error); - }); - - queueSocket.on('run-completed', (completionData) => { - if (completionData.runId === runId) { - setRunningRecordingName(''); - setCurrentInterpretationLog(''); - setRerenderRuns(true); - - setQueuedRuns(prev => { - const newSet = new Set(prev); - newSet.delete(runId); - return newSet; - }); - - const robotName = completionData.robotName || runningRecordingName; - - if (completionData.status === 'success') { - notify('success', t('main_page.notifications.interpretation_success', { name: robotName })); - } else { - notify('error', t('main_page.notifications.interpretation_failed', { name: robotName })); - } - - queueSocket.disconnect(); - } - }); - - setSockets(sockets => [...sockets, queueSocket]); - notify('info', `Run queued: ${runningRecordingName}`); } else { const socket = io(`${apiUrl}/${browserId}`, { @@ -245,6 +205,36 @@ export const MainPage = ({ handleEditRecording, initialContent }: MainPageProps) return message === 'success'; } + useEffect(() => { + if (user?.id) { + const handleRunCompleted = (completionData: any) => { + setRerenderRuns(true); + + if (queuedRuns.has(completionData.runId)) { + setQueuedRuns(prev => { + const newSet = new Set(prev); + newSet.delete(completionData.runId); + return newSet; + }); + } + + const robotName = completionData.robotName || 'Unknown Robot'; + + if (completionData.status === 'success') { + notify('success', t('main_page.notifications.interpretation_success', { name: robotName })); + } else { + notify('error', t('main_page.notifications.interpretation_failed', { name: robotName })); + } + }; + + connectToQueueSocket(user.id, handleRunCompleted); + + return () => { + disconnectQueueSocket(); + }; + } + }, [user?.id, connectToQueueSocket, disconnectQueueSocket, t, setRerenderRuns, queuedRuns, setQueuedRuns]); + const DisplayContent = () => { switch (content) { case 'robots': From 8b8a08c54a27ce0d2ab6b27b500a6b63724d58f3 Mon Sep 17 00:00:00 2001 From: Rohit Date: Tue, 19 Aug 2025 00:16:42 +0530 Subject: [PATCH 14/38] feat: rm state checks workflow validation --- maxun-core/src/interpret.ts | 66 ++++++++++++++++++++----------------- 1 file changed, 36 insertions(+), 30 deletions(-) diff --git a/maxun-core/src/interpret.ts b/maxun-core/src/interpret.ts index 5ed6fb12..faa473c2 100644 --- a/maxun-core/src/interpret.ts +++ b/maxun-core/src/interpret.ts @@ -1169,31 +1169,36 @@ export default class Interpreter extends EventEmitter { return; } - let pageState = {}; - try { - // Check if page is still valid before accessing state - if (p.isClosed()) { - this.log('Page was closed during execution', Level.WARN); - return; - } - - pageState = await this.getState(p, workflowCopy, selectors); - selectors = []; - console.log("Empty selectors:", selectors) - } catch (e: any) { - this.log(`Failed to get page state: ${e.message}`, Level.ERROR); - // If state access fails, attempt graceful recovery - if (p.isClosed()) { - this.log('Browser has been closed, terminating workflow', Level.WARN); - return; - } - // For other errors, continue with empty state to avoid complete failure - pageState = { url: p.url(), selectors: [], cookies: {} }; + if (workflowCopy.length === 0) { + this.log('All actions completed. Workflow finished.', Level.LOG); + return; } - if (this.options.debug) { - this.log(`Current state is: \n${JSON.stringify(pageState, null, 2)}`, Level.WARN); - } + // let pageState = {}; + // try { + // // Check if page is still valid before accessing state + // if (p.isClosed()) { + // this.log('Page was closed during execution', Level.WARN); + // return; + // } + + // pageState = await this.getState(p, workflowCopy, selectors); + // selectors = []; + // console.log("Empty selectors:", selectors) + // } catch (e: any) { + // this.log(`Failed to get page state: ${e.message}`, Level.ERROR); + // // If state access fails, attempt graceful recovery + // if (p.isClosed()) { + // this.log('Browser has been closed, terminating workflow', Level.WARN); + // return; + // } + // // For other errors, continue with empty state to avoid complete failure + // pageState = { url: p.url(), selectors: [], cookies: {} }; + // } + + // if (this.options.debug) { + // this.log(`Current state is: \n${JSON.stringify(pageState, null, 2)}`, Level.WARN); + // } // const actionId = workflow.findIndex((step) => { // const isApplicable = this.applicable(step.where, pageState, usedActions); @@ -1205,8 +1210,9 @@ export default class Interpreter extends EventEmitter { // return isApplicable; // }); - actionId = this.getMatchingActionId(workflowCopy, pageState, usedActions); + // actionId = this.getMatchingActionId(workflowCopy, pageState, usedActions); + const actionId = workflowCopy.length - 1; const action = workflowCopy[actionId]; console.log("MATCHED ACTION:", action); @@ -1235,12 +1241,12 @@ export default class Interpreter extends EventEmitter { console.log(`Action with ID ${action.id} removed from the workflow copy.`); // const newSelectors = this.getPreviousSelectors(workflow, actionId); - const newSelectors = this.getSelectors(workflowCopy); - newSelectors.forEach(selector => { - if (!selectors.includes(selector)) { - selectors.push(selector); - } - }); + // const newSelectors = this.getSelectors(workflowCopy); + // newSelectors.forEach(selector => { + // if (!selectors.includes(selector)) { + // selectors.push(selector); + // } + // }); // Reset loop iteration counter on successful action loopIterations = 0; From 93a644b1d848fc4e9cf1f39e785adf93ebbdae7f Mon Sep 17 00:00:00 2001 From: Karishma Shukla Date: Thu, 21 Aug 2025 20:23:48 +0530 Subject: [PATCH 15/38] fix: no cursor position if page closed --- server/src/workflow-management/classes/Generator.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server/src/workflow-management/classes/Generator.ts b/server/src/workflow-management/classes/Generator.ts index eb0e33a7..a5bc2edc 100644 --- a/server/src/workflow-management/classes/Generator.ts +++ b/server/src/workflow-management/classes/Generator.ts @@ -541,6 +541,10 @@ export class WorkflowGenerator { } if ((elementInfo?.tagName === 'INPUT' || elementInfo?.tagName === 'TEXTAREA') && selector) { + if (page.isClosed()) { + logger.log('debug', 'Page is closed, cannot get cursor position'); + return; + } const positionAndCursor = await page.evaluate( ({ selector, coords }) => { const getCursorPosition = (element: any, clickX: any) => { @@ -1342,4 +1346,4 @@ export class WorkflowGenerator { public clearLastIndex = () => { this.generatedData.lastIndex = null; } -} \ No newline at end of file +} From 28a6eb465fd09be23cb5cb6b87dd666fc9401ca2 Mon Sep 17 00:00:00 2001 From: Karishma Shukla Date: Thu, 21 Aug 2025 20:29:36 +0530 Subject: [PATCH 16/38] fix: if page closed !get element info --- server/src/workflow-management/selector.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index eddd1fb1..80e82c52 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -21,6 +21,10 @@ export const getElementInformation = async ( ) => { try { if (!getList || listSelector !== '') { + if (page.isClosed()) { + logger.debug('Page is closed, cannot get element information'); + return null; + } const elementInfo = await page.evaluate( async ({ x, y }) => { const getDeepestElementFromPoint = (x: number, y: number): HTMLElement | null => { From 1abfd6a8119836252763f381e621e417188e0a48 Mon Sep 17 00:00:00 2001 From: Karishma Shukla Date: Thu, 21 Aug 2025 20:30:49 +0530 Subject: [PATCH 17/38] fix: if page closed !get element info --- server/src/workflow-management/selector.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 80e82c52..d4cc3463 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -306,6 +306,10 @@ export const getElementInformation = async ( ); return elementInfo; } else { + if (page.isClosed()) { + logger.debug('Page is closed, cannot get element information (else branch)'); + return null; + } const elementInfo = await page.evaluate( async ({ x, y }) => { const getDeepestElementFromPoint = (x: number, y: number): HTMLElement | null => { From b6ee449b9ce949af2b62aac8571c73e1a4654346 Mon Sep 17 00:00:00 2001 From: Karishma Shukla Date: Thu, 21 Aug 2025 20:31:26 +0530 Subject: [PATCH 18/38] fix: if page closed !get element rect --- server/src/workflow-management/selector.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index d4cc3463..3dfd344f 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -619,6 +619,10 @@ export const getElementInformation = async ( export const getRect = async (page: Page, coordinates: Coordinates, listSelector: string, getList: boolean) => { try { if (!getList || listSelector !== '') { + if (page.isClosed()) { + logger.debug('Page is closed, cannot get element rect'); + return null; + } const rect = await page.evaluate( async ({ x, y }) => { const getDeepestElementFromPoint = (x: number, y: number): HTMLElement | null => { From 1abd277fc4fc3ea73cbd626e6cd953fd854230b8 Mon Sep 17 00:00:00 2001 From: Karishma Shukla Date: Thu, 21 Aug 2025 20:32:19 +0530 Subject: [PATCH 19/38] fix: if page closed !get element rect --- server/src/workflow-management/selector.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 3dfd344f..48918fe5 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -846,6 +846,10 @@ export const getRect = async (page: Page, coordinates: Coordinates, listSelector ); return rect; } else { + if (page.isClosed()) { + logger.debug('Page is closed, cannot get element rect (else branch)'); + return null; + } const rect = await page.evaluate( async ({ x, y }) => { const getDeepestElementFromPoint = (x: number, y: number): HTMLElement | null => { From d1c5a604174c34c4687f672f079af9d02d6d76ce Mon Sep 17 00:00:00 2001 From: Karishma Shukla Date: Thu, 21 Aug 2025 20:33:14 +0530 Subject: [PATCH 20/38] fix: if page closed !get element selector --- server/src/workflow-management/selector.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 48918fe5..a0cc9b6b 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -1092,6 +1092,10 @@ export const getRect = async (page: Page, coordinates: Coordinates, listSelector */ export const getSelectors = async (page: Page, coordinates: Coordinates) => { try { + if (page.isClosed()) { + logger.debug('Page is closed, cannot get selectors'); + return null; + } const selectors: any = await page.evaluate(async ({ x, y }) => { // version @medv/finder // https://github.com/antonmedv/finder/blob/master/finder.ts From 2081fbbee8a81c5394589266c7bf522305287823 Mon Sep 17 00:00:00 2001 From: Karishma Shukla Date: Thu, 21 Aug 2025 20:35:42 +0530 Subject: [PATCH 21/38] fix: if page closed !get non unique selectors --- server/src/workflow-management/selector.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index a0cc9b6b..21777f9c 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -2030,6 +2030,10 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates try { if (!listSelector) { + if (page.isClosed()) { + logger.debug('Page is closed, cannot get non-unique selectors'); + return { generalSelector: '' }; + } const selectors = await page.evaluate(({ x, y }: { x: number, y: number }) => { const getDeepestElementFromPoint = (x: number, y: number): HTMLElement | null => { let elements = document.elementsFromPoint(x, y) as HTMLElement[]; From 6fa4f88dee09bd8cbdfe9a8c08a4e6f099d1f2b9 Mon Sep 17 00:00:00 2001 From: Karishma Shukla Date: Thu, 21 Aug 2025 20:36:23 +0530 Subject: [PATCH 22/38] fix: if page closed !get list selectors --- server/src/workflow-management/selector.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index 21777f9c..f7cc5c72 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -2388,6 +2388,10 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates return selectors || { generalSelector: '' }; } else { // When we have a list selector, we need special handling while maintaining shadow DOM and frame support + if (page.isClosed()) { + logger.debug('Page is closed, cannot get list selectors'); + return { generalSelector: '' }; + } const selectors = await page.evaluate(({ x, y }: { x: number, y: number }) => { const getDeepestElementFromPoint = (x: number, y: number): HTMLElement | null => { let elements = document.elementsFromPoint(x, y) as HTMLElement[]; From 1eea6290dac00e4cbeb4657301235cefdedfef4e Mon Sep 17 00:00:00 2001 From: Karishma Shukla Date: Thu, 21 Aug 2025 20:37:01 +0530 Subject: [PATCH 23/38] fix: if page closed !get child selectors --- server/src/workflow-management/selector.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/src/workflow-management/selector.ts b/server/src/workflow-management/selector.ts index f7cc5c72..43c1d2f8 100644 --- a/server/src/workflow-management/selector.ts +++ b/server/src/workflow-management/selector.ts @@ -2753,6 +2753,10 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates export const getChildSelectors = async (page: Page, parentSelector: string): Promise => { try { + if (page.isClosed()) { + logger.debug('Page is closed, cannot get child selectors'); + return []; + } const childSelectors = await page.evaluate((parentSelector: string) => { // Function to get a non-unique selector based on tag and class (if present) function getNonUniqueSelector(element: HTMLElement): string { From 0d209c80142cb3088ad5d69c07f2df8263536ee8 Mon Sep 17 00:00:00 2001 From: Karishma Shukla Date: Sat, 23 Aug 2025 15:06:22 +0530 Subject: [PATCH 24/38] fix: remove deprecated server side screenshot highlighting that caused execution context destroyed error --- server/src/browser-management/inputHandlers.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/server/src/browser-management/inputHandlers.ts b/server/src/browser-management/inputHandlers.ts index da48f4fd..36bfd1c6 100644 --- a/server/src/browser-management/inputHandlers.ts +++ b/server/src/browser-management/inputHandlers.ts @@ -234,11 +234,11 @@ const handleMousemove = async (activeBrowser: RemoteBrowser, page: Page, { x, y const generator = activeBrowser.generator; await page.mouse.move(x, y); - throttle(async () => { - if (!page.isClosed()) { - await generator.generateDataForHighlighter(page, { x, y }); - } - }, 100)(); + // throttle(async () => { + // if (!page.isClosed()) { + // await generator.generateDataForHighlighter(page, { x, y }); + // } + // }, 100)(); logger.log("debug", `Moved over position x:${x}, y:${y}`); } catch (e) { const { message } = e as Error; From 96dea7f5c7d1e248c7adc776982a794161f7bd91 Mon Sep 17 00:00:00 2001 From: Rohit Date: Sat, 23 Aug 2025 18:44:20 +0530 Subject: [PATCH 25/38] feat: batching, caching, modal support --- src/helpers/clientSelectorGenerator.ts | 450 +++++++++++++++++++++---- 1 file changed, 382 insertions(+), 68 deletions(-) diff --git a/src/helpers/clientSelectorGenerator.ts b/src/helpers/clientSelectorGenerator.ts index 9ec690b7..3a4e1385 100644 --- a/src/helpers/clientSelectorGenerator.ts +++ b/src/helpers/clientSelectorGenerator.ts @@ -109,6 +109,11 @@ class ClientSelectorGenerator { private getList: boolean = false; private paginationMode: boolean = false; + private pathCache = new WeakMap(); + private descendantsCache = new WeakMap(); + private meaningfulCache = new WeakMap(); + private selectorCache = new Map(); + private elementGroups: Map = new Map(); private groupedElements: Set = new Set(); private lastAnalyzedDocument: Document | null = null; @@ -397,7 +402,22 @@ class ClientSelectorGenerator { this.lastAnalyzedDocument = iframeDoc; // Get all visible elements INCLUDING shadow DOM - const allElements = this.getAllVisibleElementsWithShadow(iframeDoc); + let allElements = this.getAllVisibleElementsWithShadow(iframeDoc); + + if (this.getList === true && this.listSelector === "") { + const dialogElements = this.findAllDialogElements(iframeDoc); + + if (dialogElements.length > 0) { + // Check if dialogs contain significant content worth analyzing + const dialogContentElements = this.getElementsFromDialogs(dialogElements); + + // Only switch to dialog-focused analysis if dialogs have substantial content + if (dialogContentElements.length > 5) { + allElements = dialogContentElements; + } + } + } + const processedInTables = new Set(); // 1. Specifically find and group rows within each table, bypassing normal similarity checks. @@ -517,16 +537,42 @@ class ClientSelectorGenerator { return meaningfulChildren; } + /** + * Check if element has meaningful content for extraction (cached version) + */ + private isMeaningfulElementCached(element: HTMLElement): boolean { + if (this.meaningfulCache.has(element)) { + return this.meaningfulCache.get(element)!; + } + + const result = this.isMeaningfulElement(element); + this.meaningfulCache.set(element, result); + return result; + } + /** * Check if element has meaningful content for extraction */ private isMeaningfulElement(element: HTMLElement): boolean { const tagName = element.tagName.toLowerCase(); + + // Fast path for common meaningful elements + if (["a", "img", "input", "button", "select"].includes(tagName)) { + return true; + } + const text = (element.textContent || "").trim(); const hasHref = element.hasAttribute("href"); const hasSrc = element.hasAttribute("src"); + + // Quick checks first + if (text.length > 0 || hasHref || hasSrc) { + return true; + } + const isCustomElement = tagName.includes("-"); + // For custom elements, be more lenient about what's considered meaningful if (isCustomElement) { const hasChildren = element.children.length > 0; const hasSignificantAttributes = Array.from(element.attributes).some( @@ -534,9 +580,6 @@ class ClientSelectorGenerator { ); return ( - text.length > 0 || - hasHref || - hasSrc || hasChildren || hasSignificantAttributes || element.hasAttribute("role") || @@ -544,12 +587,7 @@ class ClientSelectorGenerator { ); } - return ( - text.length > 0 || - hasHref || - hasSrc || - ["a", "img", "input", "button", "select"].includes(tagName) - ); + return false; } /** @@ -2457,6 +2495,13 @@ class ClientSelectorGenerator { parentSelector: string ): string[] => { try { + const cacheKey = `${parentSelector}_${iframeDoc.location?.href || 'doc'}`; + if (this.selectorCache.has(cacheKey)) { + return this.selectorCache.get(cacheKey)!; + } + + this.pathCache = new WeakMap(); + // Use XPath evaluation to find parent elements let parentElements: HTMLElement[] = this.evaluateXPath( parentSelector, @@ -2468,9 +2513,17 @@ class ClientSelectorGenerator { return []; } - const allChildSelectors = new Set(); + if (parentElements.length > 10) { + parentElements = parentElements.slice(0, 10); + } + + const allChildSelectors = new Set(); + const processedParents = new Set(); + + for (const parentElement of parentElements) { + if (processedParents.has(parentElement)) continue; + processedParents.add(parentElement); - parentElements.forEach((parentElement) => { const otherListElements = parentElements.filter( (el) => el !== parentElement ); @@ -2481,12 +2534,15 @@ class ClientSelectorGenerator { iframeDoc, otherListElements ); - childSelectors.forEach((selector) => allChildSelectors.add(selector)); - }); + + for (const selector of childSelectors) { + allChildSelectors.add(selector); + } + } - // Convert Set back to array and sort for consistency - const childSelectors = Array.from(allChildSelectors).sort(); - return childSelectors; + const result = Array.from(allChildSelectors).sort(); + this.selectorCache.set(cacheKey, result); + return result; } catch (error) { console.error("Error in getChildSelectors:", error); return []; @@ -2494,39 +2550,81 @@ class ClientSelectorGenerator { }; private getAllDescendantsIncludingShadow( - parentElement: HTMLElement, - maxDepth: number = 20 + parentElement: HTMLElement ): HTMLElement[] { - const allDescendants: HTMLElement[] = []; + if (this.descendantsCache.has(parentElement)) { + return this.descendantsCache.get(parentElement)!; + } + + const meaningfulDescendants: HTMLElement[] = []; + const queue: HTMLElement[] = [parentElement]; const visited = new Set(); + visited.add(parentElement); - const traverse = (element: HTMLElement, currentDepth: number) => { - if (currentDepth >= maxDepth || visited.has(element)) { - return; - } - visited.add(element); + const MAX_MEANINGFUL_ELEMENTS = 300; + const MAX_NODES_TO_CHECK = 1200; + const MAX_DEPTH = 12; + let nodesChecked = 0; - if (element !== parentElement) { - allDescendants.push(element); + let adjustedMaxDepth = MAX_DEPTH; + const elementDensityThreshold = 50; + + const depths: number[] = [0]; + let queueIndex = 0; + + while (queueIndex < queue.length) { + const element = queue[queueIndex]; + const currentDepth = depths[queueIndex]; + queueIndex++; + nodesChecked++; + + if (currentDepth <= 3 && meaningfulDescendants.length > elementDensityThreshold) { + adjustedMaxDepth = Math.max(6, adjustedMaxDepth - 2); } - // Traverse light DOM children - const children = Array.from(element.children) as HTMLElement[]; - for (const child of children) { - traverse(child, currentDepth + 1); + if ( + nodesChecked > MAX_NODES_TO_CHECK || + meaningfulDescendants.length >= MAX_MEANINGFUL_ELEMENTS || + currentDepth > adjustedMaxDepth + ) { + break; } - // Traverse shadow DOM if it exists - if (element.shadowRoot) { - const shadowChildren = Array.from(element.shadowRoot.children) as HTMLElement[]; - for (const shadowChild of shadowChildren) { - traverse(shadowChild, currentDepth + 1); + if (element !== parentElement && this.isMeaningfulElementCached(element)) { + meaningfulDescendants.push(element); + } + + if (currentDepth >= adjustedMaxDepth) { + continue; + } + + const children = element.children; + const childLimit = Math.min(children.length, 30); + for (let i = 0; i < childLimit; i++) { + const child = children[i] as HTMLElement; + if (!visited.has(child)) { + visited.add(child); + queue.push(child); + depths.push(currentDepth + 1); } } - }; - traverse(parentElement, 0); - return allDescendants; + if (element.shadowRoot && currentDepth < adjustedMaxDepth - 1) { + const shadowChildren = element.shadowRoot.children; + const shadowLimit = Math.min(shadowChildren.length, 20); + for (let i = 0; i < shadowLimit; i++) { + const child = shadowChildren[i] as HTMLElement; + if (!visited.has(child)) { + visited.add(child); + queue.push(child); + depths.push(currentDepth + 1); + } + } + } + } + + this.descendantsCache.set(parentElement, meaningfulDescendants); + return meaningfulDescendants; } private generateOptimizedChildXPaths( @@ -2541,24 +2639,35 @@ class ClientSelectorGenerator { // Get all meaningful descendants (not just direct children) const allDescendants = this.getAllDescendantsIncludingShadow(parentElement); - allDescendants.forEach((descendant, i) => { - if (processedElements.has(descendant)) return; - processedElements.add(descendant); + const batchSize = 25; + for (let i = 0; i < allDescendants.length; i += batchSize) { + const batch = allDescendants.slice(i, i + batchSize); + + for (const descendant of batch) { + if (processedElements.has(descendant)) continue; + processedElements.add(descendant); - if (!this.isMeaningfulElement(descendant)) return; + const absolutePath = this.buildOptimizedAbsoluteXPath( + descendant, + listSelector, + parentElement, + document, + otherListElements + ); - const absolutePath = this.buildOptimizedAbsoluteXPath( - descendant, - listSelector, - parentElement, - document, - otherListElements - ); + if (absolutePath) { + selectors.push(absolutePath); + } - if (absolutePath) { - selectors.push(absolutePath); + if (selectors.length >= 250) { + break; + } } - }); + + if (selectors.length >= 250) { + break; + } + } return [...new Set(selectors)]; } @@ -2736,6 +2845,10 @@ class ClientSelectorGenerator { rootElement: HTMLElement, otherListElements: HTMLElement[] = [] ): string | null { + if (this.pathCache.has(targetElement)) { + return this.pathCache.get(targetElement)!; + } + if ( !this.elementContains(rootElement, targetElement) || targetElement === rootElement @@ -2788,45 +2901,64 @@ class ClientSelectorGenerator { } if (current !== rootElement) { + this.pathCache.set(targetElement, null); return null; } - return pathParts.length > 0 ? "/" + pathParts.join("/") : null; + const result = pathParts.length > 0 ? "/" + pathParts.join("/") : null; + + this.pathCache.set(targetElement, result); + + return result; } private getCommonClassesAcrossLists( targetElement: HTMLElement, otherListElements: HTMLElement[] ): string[] { - const targetClasses = this.normalizeClasses(targetElement.classList).split(" ").filter(Boolean); + if (otherListElements.length === 0) { + return this.normalizeClasses(targetElement.classList).split(" ").filter(Boolean); + } - const otherListsKey = otherListElements.map(el => `${el.tagName}-${el.className}`).sort().join('|'); - const cacheKey = `${targetElement.tagName}-${targetClasses.sort().join(',')}-${otherListsKey}`; + const targetClasses = this.normalizeClasses(targetElement.classList).split(" ").filter(Boolean); + + if (targetClasses.length === 0) { + return []; + } + + const cacheKey = `${targetElement.tagName}_${targetClasses.join(',')}_${otherListElements.length}`; if (this.classCache.has(cacheKey)) { return this.classCache.get(cacheKey)!; } - if (otherListElements.length === 0) { - this.classCache.set(cacheKey, targetClasses); - return targetClasses; - } + const maxElementsToCheck = 100; + let checkedElements = 0; + const similarElements: HTMLElement[] = []; - const similarElements = otherListElements.flatMap(listEl => - this.getAllDescendantsIncludingShadow(listEl).filter(child => - child.tagName === targetElement.tagName - ) - ); + for (const listEl of otherListElements) { + if (checkedElements >= maxElementsToCheck) break; + + const descendants = this.getAllDescendantsIncludingShadow(listEl); + for (const child of descendants) { + if (checkedElements >= maxElementsToCheck) break; + if (child.tagName === targetElement.tagName) { + similarElements.push(child); + checkedElements++; + } + } + } if (similarElements.length === 0) { this.classCache.set(cacheKey, targetClasses); return targetClasses; } + const targetClassSet = new Set(targetClasses); const exactMatches = similarElements.filter(el => { const elClasses = this.normalizeClasses(el.classList).split(" ").filter(Boolean); - return targetClasses.length === elClasses.length && - targetClasses.every(cls => elClasses.includes(cls)); + if (elClasses.length !== targetClasses.length) return false; + return elClasses.every(cls => targetClassSet.has(cls)); }); if (exactMatches.length > 0) { @@ -3913,6 +4045,184 @@ class ClientSelectorGenerator { return depth; } + /** + * Find dialog element in the elements array + */ + private findDialogElement(elements: HTMLElement[]): HTMLElement | null { + let dialogElement = elements.find((el) => el.getAttribute("role") === "dialog"); + + if (!dialogElement) { + dialogElement = elements.find((el) => el.tagName.toLowerCase() === "dialog"); + } + + if (!dialogElement) { + dialogElement = elements.find((el) => { + const classList = el.classList.toString().toLowerCase(); + const id = (el.id || "").toLowerCase(); + + return ( + classList.includes("modal") || + classList.includes("dialog") || + classList.includes("popup") || + classList.includes("overlay") || + id.includes("modal") || + id.includes("dialog") || + id.includes("popup") + ); + }); + } + + return dialogElement || null; + } + + /** + * Find the deepest element within a dialog + */ + private findDeepestInDialog( + dialogElements: HTMLElement[], + dialogElement: HTMLElement + ): HTMLElement | null { + if (!dialogElements.length) return null; + if (dialogElements.length === 1) return dialogElements[0]; + + let deepestElement = dialogElements[0]; + let maxDepth = 0; + + for (const element of dialogElements) { + let depth = 0; + let current = element; + + // Calculate depth within the dialog context + while ( + current && + current.parentElement && + current !== dialogElement.parentElement + ) { + depth++; + current = current.parentElement; + } + + if (depth > maxDepth) { + maxDepth = depth; + deepestElement = element; + } + } + + return deepestElement; + } + + /** + * Find all dialog elements in the document + */ + private findAllDialogElements(doc: Document): HTMLElement[] { + const dialogElements: HTMLElement[] = []; + const allElements = Array.from(doc.querySelectorAll("*")) as HTMLElement[]; + + for (const element of allElements) { + if (element.getAttribute("role") === "dialog") { + dialogElements.push(element); + continue; + } + + if (element.tagName.toLowerCase() === "dialog") { + dialogElements.push(element); + continue; + } + + const classList = element.classList.toString().toLowerCase(); + const id = (element.id || "").toLowerCase(); + + if ( + classList.includes("modal") || + classList.includes("dialog") || + classList.includes("popup") || + classList.includes("overlay") || + id.includes("modal") || + id.includes("dialog") || + id.includes("popup") + ) { + dialogElements.push(element); + continue; + } + } + + return dialogElements; + } + + /** + * Get all visible elements from within dialog elements + */ + private getElementsFromDialogs(dialogElements: HTMLElement[]): HTMLElement[] { + const elements: HTMLElement[] = []; + const visited = new Set(); + + for (const dialog of dialogElements) { + const dialogChildren = Array.from(dialog.querySelectorAll("*")).filter( + (el) => { + const rect = el.getBoundingClientRect(); + return rect.width > 0 && rect.height > 0; + } + ) as HTMLElement[]; + + // Add dialog itself if it's visible + const dialogRect = dialog.getBoundingClientRect(); + if (dialogRect.width > 0 && dialogRect.height > 0 && !visited.has(dialog)) { + visited.add(dialog); + elements.push(dialog); + } + + // Add all visible children + dialogChildren.forEach((element) => { + if (!visited.has(element)) { + visited.add(element); + elements.push(element); + + // Traverse shadow DOM if it exists within dialog + if (element.shadowRoot) { + const shadowElements = this.getElementsFromShadowRoot(element.shadowRoot); + shadowElements.forEach(shadowEl => { + if (!visited.has(shadowEl)) { + visited.add(shadowEl); + elements.push(shadowEl); + } + }); + } + } + }); + } + + return elements; + } + + /** + * Get elements from shadow root (helper for dialog analysis) + */ + private getElementsFromShadowRoot(shadowRoot: ShadowRoot): HTMLElement[] { + const elements: HTMLElement[] = []; + try { + const shadowChildren = Array.from(shadowRoot.querySelectorAll("*")).filter( + (el) => { + const rect = el.getBoundingClientRect(); + return rect.width > 0 && rect.height > 0; + } + ) as HTMLElement[]; + + shadowChildren.forEach((element) => { + elements.push(element); + + // Recursively traverse nested shadow DOMs + if (element.shadowRoot) { + const nestedShadowElements = this.getElementsFromShadowRoot(element.shadowRoot); + elements.push(...nestedShadowElements); + } + }); + } catch (error) { + console.warn("Could not access shadow root:", error); + } + + return elements; + } + /** * Clean up when component unmounts or mode changes @@ -3926,6 +4236,10 @@ class ClientSelectorGenerator { this.spatialIndex.clear(); this.lastCachedDocument = null; this.classCache.clear(); + this.selectorCache.clear(); + this.pathCache = new WeakMap(); + this.descendantsCache = new WeakMap(); + this.meaningfulCache = new WeakMap(); } // Update generateSelector to use instance variables From 0fba0f0e91053506cd867e6bff0012248fc84d71 Mon Sep 17 00:00:00 2001 From: Rohit Date: Sat, 23 Aug 2025 19:45:21 +0530 Subject: [PATCH 26/38] feat: strict dialog check --- src/helpers/clientSelectorGenerator.ts | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/helpers/clientSelectorGenerator.ts b/src/helpers/clientSelectorGenerator.ts index 3a4e1385..184a199b 100644 --- a/src/helpers/clientSelectorGenerator.ts +++ b/src/helpers/clientSelectorGenerator.ts @@ -4128,22 +4128,6 @@ class ClientSelectorGenerator { dialogElements.push(element); continue; } - - const classList = element.classList.toString().toLowerCase(); - const id = (element.id || "").toLowerCase(); - - if ( - classList.includes("modal") || - classList.includes("dialog") || - classList.includes("popup") || - classList.includes("overlay") || - id.includes("modal") || - id.includes("dialog") || - id.includes("popup") - ) { - dialogElements.push(element); - continue; - } } return dialogElements; From b134bd1c29548e69696c07ef2ba7aea54ccdadc3 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Mon, 25 Aug 2025 19:37:49 +0530 Subject: [PATCH 27/38] fix: ui style --- .../robot/pages/RobotConfigPage.tsx | 154 +++++++++++------- 1 file changed, 91 insertions(+), 63 deletions(-) diff --git a/src/components/robot/pages/RobotConfigPage.tsx b/src/components/robot/pages/RobotConfigPage.tsx index 5923842a..902518eb 100644 --- a/src/components/robot/pages/RobotConfigPage.tsx +++ b/src/components/robot/pages/RobotConfigPage.tsx @@ -1,14 +1,15 @@ import React from 'react'; -import { - Box, - Typography, - Button, +import { + Box, + Typography, + Button, IconButton, Divider, useTheme } from '@mui/material'; import { ArrowBack } from '@mui/icons-material'; import { useNavigate, useLocation } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; interface RobotConfigPageProps { title: string; @@ -23,6 +24,7 @@ interface RobotConfigPageProps { icon?: React.ReactNode; onBackToSelection?: () => void; backToSelectionText?: string; + onArrowBack?: () => void; // Optional prop for custom back action } export const RobotConfigPage: React.FC = ({ @@ -30,50 +32,72 @@ export const RobotConfigPage: React.FC = ({ children, onSave, onCancel, - saveButtonText = "Save", - cancelButtonText = "Cancel", + saveButtonText, + cancelButtonText, showSaveButton = true, showCancelButton = true, isLoading = false, icon, onBackToSelection, - backToSelectionText = "← Back" + backToSelectionText, + onArrowBack, }) => { + const navigate = useNavigate(); + const location = useLocation(); const theme = useTheme(); + const { t } = useTranslation(); const handleBack = () => { if (onCancel) { onCancel(); + } else { + // Try to determine the correct path based on current URL + const currentPath = location.pathname; + const basePath = currentPath.includes('/prebuilt-robots') ? '/prebuilt-robots' : '/robots'; + navigate(basePath); } }; return ( - - @@ -82,9 +106,9 @@ export const RobotConfigPage: React.FC = ({ {icon} )} - = ({ - {children} + {/* Action Buttons */} {(showSaveButton || showCancelButton || onBackToSelection) && ( + {/* Left side - Back to Selection button */} {onBackToSelection && ( )} + {/* Right side - Save/Cancel buttons */} - {showCancelButton && ( - - )} - {showSaveButton && onSave && ( - + )} + {showSaveButton && onSave && ( + - )} + }} + > + {isLoading ? t("buttons.saving") : (saveButtonText || t("buttons.save"))} + + )} )} From 53d8b81fc82e67416aaa425344e932d575b269db Mon Sep 17 00:00:00 2001 From: amhsirak Date: Mon, 25 Aug 2025 19:40:53 +0530 Subject: [PATCH 28/38] feat: dont show cancel button --- .../robot/pages/RobotDuplicatePage.tsx | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/components/robot/pages/RobotDuplicatePage.tsx b/src/components/robot/pages/RobotDuplicatePage.tsx index e88be19d..b02cecde 100644 --- a/src/components/robot/pages/RobotDuplicatePage.tsx +++ b/src/components/robot/pages/RobotDuplicatePage.tsx @@ -19,10 +19,17 @@ import { useNavigate, useLocation } from "react-router-dom"; interface RobotMeta { name: string; id: string; + prebuiltId?: string; createdAt: string; pairs: number; updatedAt: string; params: any[]; + type?: string; + description?: string; + usedByUsers?: number[]; + subscriptionLevel?: number; + access?: string; + sample?: any[]; url?: string; } @@ -73,7 +80,7 @@ export const RobotDuplicatePage = ({ handleStart }: RobotSettingsProps) => { const [targetUrl, setTargetUrl] = useState(""); const [robot, setRobot] = useState(null); const [isLoading, setIsLoading] = useState(false); - const { recordingId, notify, setRerenderRobots } = + const { recordingId, notify, setRerenderRobots} = useGlobalInfoStore(); useEffect(() => { @@ -132,7 +139,10 @@ export const RobotDuplicatePage = ({ handleStart }: RobotSettingsProps) => { t("robot_duplication.notifications.duplicate_success") ); handleStart(robot); - navigate("/robots"); + const basePath = location.pathname.includes("/prebuilt-robots") + ? "/prebuilt-robots" + : "/robots"; + navigate(basePath); } else { notify("error", t("robot_duplication.notifications.duplicate_error")); } @@ -145,7 +155,10 @@ export const RobotDuplicatePage = ({ handleStart }: RobotSettingsProps) => { }; const handleCancel = () => { - navigate("/robots"); + const basePath = location.pathname.includes("/prebuilt-robots") + ? "/prebuilt-robots" + : "/robots"; + navigate(basePath); }; return ( @@ -156,6 +169,7 @@ export const RobotDuplicatePage = ({ handleStart }: RobotSettingsProps) => { saveButtonText={t("robot_duplication.buttons.duplicate")} cancelButtonText={t("robot_duplication.buttons.cancel")} isLoading={isLoading} + showCancelButton={false} > <> @@ -188,4 +202,4 @@ export const RobotDuplicatePage = ({ handleStart }: RobotSettingsProps) => { ); -}; +}; \ No newline at end of file From 0bd666e37240dd929680bc537eaf918c1ba7ae20 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Mon, 25 Aug 2025 19:44:30 +0530 Subject: [PATCH 29/38] feat: dont show cancel button --- src/components/robot/pages/RobotEditPage.tsx | 33 ++++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/src/components/robot/pages/RobotEditPage.tsx b/src/components/robot/pages/RobotEditPage.tsx index deaf3d76..bce3a5ea 100644 --- a/src/components/robot/pages/RobotEditPage.tsx +++ b/src/components/robot/pages/RobotEditPage.tsx @@ -18,10 +18,17 @@ import { useNavigate, useLocation } from "react-router-dom"; interface RobotMeta { name: string; id: string; + prebuiltId?: string; createdAt: string; pairs: number; updatedAt: string; params: any[]; + type?: string; + description?: string; + usedByUsers?: number[]; + subscriptionLevel?: number; + access?: string; + sample?: any[]; url?: string; } @@ -33,13 +40,13 @@ interface ScheduleConfig { runEvery: number; runEveryUnit: "MINUTES" | "HOURS" | "DAYS" | "WEEKS" | "MONTHS"; startFrom: - | "SUNDAY" - | "MONDAY" - | "TUESDAY" - | "WEDNESDAY" - | "THURSDAY" - | "FRIDAY" - | "SATURDAY"; + | "SUNDAY" + | "MONDAY" + | "TUESDAY" + | "WEDNESDAY" + | "THURSDAY" + | "FRIDAY" + | "SATURDAY"; atTimeStart?: string; atTimeEnd?: string; timezone: string; @@ -173,6 +180,7 @@ export const RobotEditPage = ({ handleStart }: RobotSettingsProps) => { action.args && action.args.length > 0 ) { + // Check if first argument has a limit property const arg = action.args[0]; if (arg && typeof arg === "object" && "limit" in arg) { limits.push({ @@ -214,6 +222,7 @@ export const RobotEditPage = ({ handleStart }: RobotSettingsProps) => { const selector = action.args[0]; + // Handle full word type actions first if ( action.action === "type" && action.args?.length >= 2 && @@ -230,6 +239,7 @@ export const RobotEditPage = ({ handleStart }: RobotSettingsProps) => { continue; } + // Handle character-by-character sequences (both type and press) if ( (action.action === "type" || action.action === "press") && action.args?.length >= 2 && @@ -582,7 +592,8 @@ export const RobotEditPage = ({ handleStart }: RobotSettingsProps) => { setRerenderRobots(true); notify("success", t("robot_edit.notifications.update_success")); handleStart(robot); - navigate("/robots"); + const basePath = "/robots"; + navigate(basePath); } else { notify("error", t("robot_edit.notifications.update_failed")); } @@ -595,7 +606,8 @@ export const RobotEditPage = ({ handleStart }: RobotSettingsProps) => { }; const handleCancel = () => { - navigate("/robots"); + const basePath = "/robots"; + navigate(basePath); }; const lastPair = @@ -610,6 +622,7 @@ export const RobotEditPage = ({ handleStart }: RobotSettingsProps) => { onCancel={handleCancel} saveButtonText={t("robot_edit.save")} cancelButtonText={t("robot_edit.cancel")} + showCancelButton={false} isLoading={isLoading} > <> @@ -640,4 +653,4 @@ export const RobotEditPage = ({ handleStart }: RobotSettingsProps) => { ); -}; +}; \ No newline at end of file From c8dc0de6d585730b86005d4e1130d9bdec0a999f Mon Sep 17 00:00:00 2001 From: amhsirak Date: Mon, 25 Aug 2025 19:45:05 +0530 Subject: [PATCH 30/38] feat: dont show cancel button --- src/components/robot/pages/RobotSettingsPage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/robot/pages/RobotSettingsPage.tsx b/src/components/robot/pages/RobotSettingsPage.tsx index b5355082..11832935 100644 --- a/src/components/robot/pages/RobotSettingsPage.tsx +++ b/src/components/robot/pages/RobotSettingsPage.tsx @@ -128,6 +128,7 @@ export const RobotSettingsPage = ({ handleStart }: RobotSettingsProps) => { onCancel={handleCancel} cancelButtonText={t("robot_settings.buttons.close")} showSaveButton={false} + showCancelButton={false} > <> From 10b2a1b568f4d99ae185e8f4f656d7a4d58dcc50 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Mon, 25 Aug 2025 19:46:22 +0530 Subject: [PATCH 31/38] feat: rename to integrations --- src/components/robot/pages/RobotIntegrationPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/robot/pages/RobotIntegrationPage.tsx b/src/components/robot/pages/RobotIntegrationPage.tsx index bffe8da8..4e42fd04 100644 --- a/src/components/robot/pages/RobotIntegrationPage.tsx +++ b/src/components/robot/pages/RobotIntegrationPage.tsx @@ -612,7 +612,7 @@ export const RobotIntegrationPage = ({ case "webhook": return "Webhook Integration"; default: - return "Integration Settings"; + return "Integration"; } }; From f06763d5126fe90919e365a0d24d2324f905296b Mon Sep 17 00:00:00 2001 From: amhsirak Date: Mon, 25 Aug 2025 19:46:46 +0530 Subject: [PATCH 32/38] feat: rename to integrations --- src/components/robot/pages/RobotIntegrationPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/robot/pages/RobotIntegrationPage.tsx b/src/components/robot/pages/RobotIntegrationPage.tsx index 4e42fd04..2e2c9990 100644 --- a/src/components/robot/pages/RobotIntegrationPage.tsx +++ b/src/components/robot/pages/RobotIntegrationPage.tsx @@ -680,7 +680,7 @@ export const RobotIntegrationPage = ({ // --- MAIN RENDER --- if (!selectedIntegrationType && !integrationType) { return ( - navigate(`/${robotPath}/${recordingId}/integrate`)}> + navigate(`/${robotPath}/${recordingId}/integrate`)}>
From 411fe1ab18579d310d739a78af5c5ddeb51446ca Mon Sep 17 00:00:00 2001 From: amhsirak Date: Mon, 25 Aug 2025 19:51:33 +0530 Subject: [PATCH 33/38] feat: align integrations --- .../robot/pages/RobotIntegrationPage.tsx | 96 ++++++++++++------- 1 file changed, 60 insertions(+), 36 deletions(-) diff --git a/src/components/robot/pages/RobotIntegrationPage.tsx b/src/components/robot/pages/RobotIntegrationPage.tsx index 2e2c9990..7adfd12a 100644 --- a/src/components/robot/pages/RobotIntegrationPage.tsx +++ b/src/components/robot/pages/RobotIntegrationPage.tsx @@ -72,16 +72,16 @@ export const RobotIntegrationPage = ({ const { t } = useTranslation(); const navigate = useNavigate(); const location = useLocation(); - + const pathSegments = location.pathname.split('/'); const robotsIndex = pathSegments.findIndex(segment => segment === 'robots' || segment === 'prebuilt-robots'); const integrateIndex = pathSegments.findIndex(segment => segment === 'integrate'); - - const robotIdFromUrl = robotsIndex !== -1 && robotsIndex + 1 < pathSegments.length - ? pathSegments[robotsIndex + 1] + + const robotIdFromUrl = robotsIndex !== -1 && robotsIndex + 1 < pathSegments.length + ? pathSegments[robotsIndex + 1] : null; - - const integrationType = integrateIndex !== -1 && integrateIndex + 1 < pathSegments.length + + const integrationType = integrateIndex !== -1 && integrateIndex + 1 < pathSegments.length ? pathSegments[integrateIndex + 1] as "googleSheets" | "airtable" | "webhook" : preSelectedIntegrationType || null; @@ -114,7 +114,7 @@ export const RobotIntegrationPage = ({ const [urlError, setUrlError] = useState(null); const { recordingId: recordingIdFromStore, notify, setRerenderRobots, setRecordingId } = useGlobalInfoStore(); - + const recordingId = robotIdFromUrl || recordingIdFromStore; useEffect(() => { @@ -137,7 +137,7 @@ export const RobotIntegrationPage = ({ const redirectUrl = `${window.location.origin}${basePath}/${recordingId}/integrate/googleSheets`; window.location.href = `${apiUrl}/auth/google?robotId=${recordingId}&redirectUrl=${encodeURIComponent(redirectUrl)}`; }; - + const authenticateWithAirtable = () => { if (!recordingId) { console.error("Cannot authenticate: recordingId is null"); @@ -251,7 +251,7 @@ export const RobotIntegrationPage = ({ const deleteWebhookSetting = async (webhookId: string) => { if (!recordingId) return; try { - setLoading(true); + setLoading(true); const response = await removeWebhook(webhookId, recordingId); if (response.ok) { setSettings((prev) => ({ ...prev, webhooks: (prev.webhooks || []).filter((webhook) => webhook.id !== webhookId) })); @@ -681,36 +681,60 @@ export const RobotIntegrationPage = ({ if (!selectedIntegrationType && !integrationType) { return ( navigate(`/${robotPath}/${recordingId}/integrate`)}> -
-
-
+
+
+
@@ -763,9 +787,9 @@ export const RobotIntegrationPage = ({ {settings.webhooks.map((webhook) => ( {webhook.url} - {webhook.events.map((event) => ())} + {webhook.events.map((event) => ())} {formatLastCalled(webhook.lastCalledAt)} - toggleWebhookStatusSetting(webhook.id)} size="small"/> + toggleWebhookStatusSetting(webhook.id)} size="small" /> testWebhookSetting(webhook.id)} disabled={loading || !webhook.active} title="Test"> editWebhookSetting(webhook)} disabled={loading} title="Edit"> @@ -780,7 +804,7 @@ export const RobotIntegrationPage = ({ {!showWebhookForm && ( - { setNewWebhook({ ...newWebhook, url: e.target.value }); if (urlError) setUrlError(null); }} error={!!urlError} helperText={urlError} required aria-describedby="webhook-url-help"/> + { setNewWebhook({ ...newWebhook, url: e.target.value }); if (urlError) setUrlError(null); }} error={!!urlError} helperText={urlError} required aria-describedby="webhook-url-help" /> setNewWebhook({ ...newWebhook, events: [e.target.value] })} sx={{ minWidth: "200px" }} required> Run finished Run failed @@ -796,13 +820,13 @@ export const RobotIntegrationPage = ({ {editingWebhook ? "Edit Webhook" : "Add New Webhook"} - { setNewWebhook({ ...newWebhook, url: e.target.value }); if (urlError) setUrlError(null); }} sx={{ marginBottom: "15px" }} placeholder="https://your-api.com/webhook/endpoint" required error={!!urlError} helperText={urlError}/> + { setNewWebhook({ ...newWebhook, url: e.target.value }); if (urlError) setUrlError(null); }} sx={{ marginBottom: "15px" }} placeholder="https://your-api.com/webhook/endpoint" required error={!!urlError} helperText={urlError} /> setNewWebhook({ ...newWebhook, events: typeof e.target.value === "string" ? [e.target.value] : e.target.value })} - SelectProps={{ multiple: true, renderValue: (selected) => ({(selected as string[]).map((value) => ())}),}} sx={{ marginBottom: "20px" }} required> + SelectProps={{ multiple: true, renderValue: (selected) => ({(selected as string[]).map((value) => ())}), }} sx={{ marginBottom: "20px" }} required> Run finished Run failed - setNewWebhook({ ...newWebhook, active: e.target.checked })}/>} label="Active" sx={{ marginBottom: "10px" }}/> + setNewWebhook({ ...newWebhook, active: e.target.checked })} />} label="Active" sx={{ marginBottom: "10px" }} />