diff --git a/maxun-core/src/preprocessor.ts b/maxun-core/src/preprocessor.ts index 3d4307a9..f56f9d38 100644 --- a/maxun-core/src/preprocessor.ts +++ b/maxun-core/src/preprocessor.ts @@ -55,7 +55,7 @@ export default class Preprocessor { */ static getParams(workflow: WorkflowFile): string[] { const getParamsRecurse = (object: any): string[] => { - if (typeof object === 'object') { + if (typeof object === 'object' && object !== null) { // Recursion base case if (object.$param) { return [object.$param]; @@ -141,14 +141,24 @@ export default class Preprocessor { } const out = object; - // for every key (child) of the object + Object.keys(object!).forEach((key) => { - // if the field has only one key, which is `k` - if (Object.keys((object)[key]).length === 1 && (object)[key][k]) { - // process the current special tag (init param, hydrate regex...) - (out)[key] = f((object)[key][k]); - } else { - initSpecialRecurse((object)[key], k, f); + const childValue = (object)[key]; + + if (!childValue || typeof childValue !== 'object') { + return; + } + + try { + const childKeys = Object.keys(childValue); + + if (childKeys.length === 1 && childValue[k]) { + (out)[key] = f(childValue[k]); + } else { + initSpecialRecurse(childValue, k, f); + } + } catch (error) { + console.warn(`Error processing key "${key}" in initSpecialRecurse:`, error); } }); return out; diff --git a/server/src/workflow-management/classes/Interpreter.ts b/server/src/workflow-management/classes/Interpreter.ts index f9a48921..71cac8b7 100644 --- a/server/src/workflow-management/classes/Interpreter.ts +++ b/server/src/workflow-management/classes/Interpreter.ts @@ -116,6 +116,16 @@ export class WorkflowInterpreter { */ private currentScrapeListIndex: number = 0; + /** + * Track action counts to generate unique names + */ + private actionCounts: Record = {}; + + /** + * Track used action names to prevent duplicates + */ + private usedActionNames: Set = new Set(); + /** * Current run ID for real-time persistence */ @@ -379,6 +389,8 @@ export class WorkflowInterpreter { }; this.binaryData = []; this.currentScrapeListIndex = 0; + this.actionCounts = {}; + this.usedActionNames = new Set(); this.currentRunId = null; this.persistenceBuffer = []; this.persistenceInProgress = false; @@ -394,6 +406,43 @@ export class WorkflowInterpreter { logger.log('debug', `Set run ID for real-time persistence: ${runId}`); }; + /** + * Generates a unique action name for data storage + * @param actionType The type of action (scrapeList, scrapeSchema, etc.) + * @param providedName Optional name provided by the action + * @returns A unique action name + */ + private getUniqueActionName = (actionType: string, providedName?: string | null): string => { + if (providedName && providedName.trim() !== '' && !this.usedActionNames.has(providedName)) { + this.usedActionNames.add(providedName); + return providedName; + } + + if (!this.actionCounts[actionType]) { + this.actionCounts[actionType] = 0; + } + + let uniqueName: string; + let counter = this.actionCounts[actionType]; + + do { + counter++; + if (actionType === 'scrapeList') { + uniqueName = `List ${counter}`; + } else if (actionType === 'scrapeSchema') { + uniqueName = `Text ${counter}`; + } else if (actionType === 'screenshot') { + uniqueName = `Screenshot ${counter}`; + } else { + uniqueName = `${actionType} ${counter}`; + } + } while (this.usedActionNames.has(uniqueName)); + + this.actionCounts[actionType] = counter; + this.usedActionNames.add(uniqueName); + return uniqueName; + }; + /** * Persists extracted data to database with intelligent batching for performance * Falls back to immediate persistence for critical operations @@ -525,20 +574,8 @@ export class WorkflowInterpreter { } let actionName = this.currentActionName || ""; - - if (!actionName) { - if (!Array.isArray(data) && Object.keys(data).length === 1) { - const soleKey = Object.keys(data)[0]; - const soleValue = data[soleKey]; - if (Array.isArray(soleValue) || typeof soleValue === "object") { - actionName = soleKey; - data = soleValue; - } - } - } - - if (!actionName) { - actionName = "Unnamed Action"; + if (typeKey === "scrapeList") { + actionName = this.getUniqueActionName(typeKey, this.currentActionName); } const flattened = Array.isArray(data) @@ -570,9 +607,10 @@ export class WorkflowInterpreter { const { name, data, mimeType } = payload; const base64Data = data.toString("base64"); + const uniqueName = this.getUniqueActionName('screenshot', name); const binaryItem = { - name, + name: uniqueName, mimeType, data: base64Data }; @@ -582,7 +620,7 @@ export class WorkflowInterpreter { await this.persistBinaryDataToDatabase(binaryItem); this.socket.emit("binaryCallback", { - name, + name: uniqueName, data: base64Data, mimeType }); diff --git a/src/components/robot/pages/RobotEditPage.tsx b/src/components/robot/pages/RobotEditPage.tsx index fa745160..80671c1f 100644 --- a/src/components/robot/pages/RobotEditPage.tsx +++ b/src/components/robot/pages/RobotEditPage.tsx @@ -552,16 +552,12 @@ export const RobotEditPage = ({ handleStart }: RobotSettingsProps) => { pair.what.forEach((action, actionIndex) => { if (!editableActions.has(String(action.action))) return; - let currentName = - action.name || - (action.args && action.args[0] && typeof action.args[0] === 'object') || - ''; + let currentName = action.name || ''; if (!currentName) { switch (action.action) { case 'scrapeSchema': - textCount++; - currentName = `Text ${textCount}`; + currentName = 'Texts'; break; case 'screenshot': screenshotCount++; @@ -574,9 +570,6 @@ export const RobotEditPage = ({ handleStart }: RobotSettingsProps) => { } } else { switch (action.action) { - case 'scrapeSchema': - textCount++; - break; case 'screenshot': screenshotCount++; break; @@ -599,10 +592,7 @@ export const RobotEditPage = ({ handleStart }: RobotSettingsProps) => { switch (action.action) { case 'scrapeSchema': { - const existingName = - currentName || - (action.args && action.args[0] && typeof action.args[0] === "object") || - "Texts"; + const existingName = currentName || "Texts"; if (!textInputs.length) { textInputs.push( diff --git a/src/components/run/RunContent.tsx b/src/components/run/RunContent.tsx index 197f4b1f..b005e4e5 100644 --- a/src/components/run/RunContent.tsx +++ b/src/components/run/RunContent.tsx @@ -115,27 +115,29 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe const rawKeys = Object.keys(row.binaryOutput); const isLegacyPattern = rawKeys.every(key => /^item-\d+-\d+$/.test(key)); + + let normalizedScreenshotKeys: string[]; if (isLegacyPattern) { - const renamedKeys = rawKeys.map((_, index) => `Screenshot ${index + 1}`); - const keyMap: Record = {}; - - renamedKeys.forEach((displayName, index) => { - keyMap[displayName] = rawKeys[index]; - }); - - setScreenshotKeys(renamedKeys); - setScreenshotKeyMap(keyMap); + // Legacy unnamed screenshots → Screenshot 1, Screenshot 2... + normalizedScreenshotKeys = rawKeys.map((_, index) => `Screenshot ${index + 1}`); } else { - const keyMap: Record = {}; - rawKeys.forEach(key => { - keyMap[key] = key; + // Same rule as captured lists: if name missing or generic, auto-label + normalizedScreenshotKeys = rawKeys.map((key, index) => { + if (!key || key.toLowerCase().includes("screenshot")) { + return `Screenshot ${index + 1}`; + } + return key; }); - - setScreenshotKeys(rawKeys); - setScreenshotKeyMap(keyMap); } + const keyMap: Record = {}; + normalizedScreenshotKeys.forEach((displayName, index) => { + keyMap[displayName] = rawKeys[index]; + }); + + setScreenshotKeys(normalizedScreenshotKeys); + setScreenshotKeyMap(keyMap); setCurrentScreenshotIndex(0); } else { setScreenshotKeys([]); @@ -202,7 +204,14 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe const processSchemaData = (schemaOutput: any) => { const keys = Object.keys(schemaOutput); - setSchemaKeys(keys); + const normalizedKeys = keys.map((key, index) => { + if (!key || key.toLowerCase().includes("scrapeschema")) { + return keys.length === 1 ? "Texts" : `Text ${index + 1}`; + } + return key; + }); + + setSchemaKeys(normalizedKeys); const dataByKey: Record = {}; const columnsByKey: Record = {}; @@ -248,8 +257,17 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe } }); - setSchemaDataByKey(dataByKey); - setSchemaColumnsByKey(columnsByKey); + const remappedDataByKey: Record = {}; + const remappedColumnsByKey: Record = {}; + + normalizedKeys.forEach((newKey, idx) => { + const oldKey = keys[idx]; + remappedDataByKey[newKey] = dataByKey[oldKey]; + remappedColumnsByKey[newKey] = columnsByKey[oldKey]; + }); + + setSchemaDataByKey(remappedDataByKey); + setSchemaColumnsByKey(remappedColumnsByKey); if (allData.length > 0) { const allColumns = new Set(); @@ -290,7 +308,14 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe setListData(tablesList); setListColumns(columnsList); - setListKeys(keys); + const normalizedListKeys = keys.map((key, index) => { + if (!key || key.toLowerCase().includes("scrapelist")) { + return `List ${index + 1}`; + } + return key; + }); + + setListKeys(normalizedListKeys); setCurrentListIndex(0); };