From 9215e23aae929a9ac5989c04ae6658300a8df2c0 Mon Sep 17 00:00:00 2001 From: Karishma Shukla Date: Sun, 21 Dec 2025 17:11:19 +0530 Subject: [PATCH 01/34] chore: -rm logout notif Comment out notification for successful logout. --- src/components/dashboard/NavBar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/dashboard/NavBar.tsx b/src/components/dashboard/NavBar.tsx index 4240d8af..f9b4bc90 100644 --- a/src/components/dashboard/NavBar.tsx +++ b/src/components/dashboard/NavBar.tsx @@ -113,7 +113,7 @@ export const NavBar: React.FC = ({ if (data.ok) { dispatch({ type: "LOGOUT" }); window.localStorage.removeItem("user"); - notify('success', t('navbar.notifications.success.logout')); + // notify('success', t('navbar.notifications.success.logout')); navigate("/login"); } } catch (error: any) { From b561e9e6a515b68d03a947ae0c88064c85dac0ea Mon Sep 17 00:00:00 2001 From: Karishma Shukla Date: Sun, 21 Dec 2025 17:12:06 +0530 Subject: [PATCH 02/34] fix: pass bgcolor --- src/components/ui/AlertSnackbar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ui/AlertSnackbar.tsx b/src/components/ui/AlertSnackbar.tsx index 3cf5c554..a9612963 100644 --- a/src/components/ui/AlertSnackbar.tsx +++ b/src/components/ui/AlertSnackbar.tsx @@ -32,7 +32,7 @@ export const AlertSnackbar = ({ severity, message, isOpen }: AlertSnackbarProps) return ( - + {message} From 12c36cfcb457c1f074d9f8d0f99c2f469ca8eb01 Mon Sep 17 00:00:00 2001 From: Karishma Shukla Date: Sun, 21 Dec 2025 17:12:33 +0530 Subject: [PATCH 03/34] feat: set variant outlined --- src/components/ui/AlertSnackbar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ui/AlertSnackbar.tsx b/src/components/ui/AlertSnackbar.tsx index a9612963..e59e637b 100644 --- a/src/components/ui/AlertSnackbar.tsx +++ b/src/components/ui/AlertSnackbar.tsx @@ -32,7 +32,7 @@ export const AlertSnackbar = ({ severity, message, isOpen }: AlertSnackbarProps) return ( - + {message} From fda36a3f574ce6b823d0258248eb389b8dfba774 Mon Sep 17 00:00:00 2001 From: Karishma Shukla Date: Sun, 21 Dec 2025 17:14:44 +0530 Subject: [PATCH 04/34] fix: change AlertSnackbar variant from 'filled' to 'outlined' --- src/components/ui/AlertSnackbar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ui/AlertSnackbar.tsx b/src/components/ui/AlertSnackbar.tsx index 3cf5c554..ad2f4d28 100644 --- a/src/components/ui/AlertSnackbar.tsx +++ b/src/components/ui/AlertSnackbar.tsx @@ -7,7 +7,7 @@ const Alert = React.forwardRef(function Alert( props, ref, ) { - return ; + return ; }); export interface AlertSnackbarProps { From a84fcb4a66e88cfb74f5b410ac4e53ab558df400 Mon Sep 17 00:00:00 2001 From: Karishma Shukla Date: Sun, 21 Dec 2025 17:15:35 +0530 Subject: [PATCH 05/34] feat: light mode outlinedInfo style for MuiAlert component --- src/context/theme-provider.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/context/theme-provider.tsx b/src/context/theme-provider.tsx index 5188f53e..b958b44f 100644 --- a/src/context/theme-provider.tsx +++ b/src/context/theme-provider.tsx @@ -65,6 +65,13 @@ const lightTheme = createTheme({ color: "#ff00c3", }, }, + outlinedInfo: { + color: '#ff00c3', + borderColor: '#ff00c3', + "& .MuiAlert-icon": { + color: "#ff00c3", + }, + }, }, }, MuiAlertTitle: { @@ -280,4 +287,4 @@ const ThemeModeProvider = ({ children }: { children: React.ReactNode }) => { ); }; -export default ThemeModeProvider; \ No newline at end of file +export default ThemeModeProvider; From d1adf42a70c7db856bb022231a7f5cb739aaccbf Mon Sep 17 00:00:00 2001 From: Karishma Shukla Date: Sun, 21 Dec 2025 17:17:48 +0530 Subject: [PATCH 06/34] feat: dark mode outlinedInfo style for MuiAlert --- src/context/theme-provider.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/context/theme-provider.tsx b/src/context/theme-provider.tsx index b958b44f..cf4e7bda 100644 --- a/src/context/theme-provider.tsx +++ b/src/context/theme-provider.tsx @@ -192,6 +192,13 @@ const darkTheme = createTheme({ color: "#ff66d9", }, }, + outlinedInfo: { + color: '#ff00c3', + borderColor: '#ff00c3', + "& .MuiAlert-icon": { + color: "#ff00c3", + }, + }, }, }, MuiAlertTitle: { From 5fb8e0ef3147a7206a2709e982129091cffc731d Mon Sep 17 00:00:00 2001 From: Karishma Shukla Date: Sun, 21 Dec 2025 17:23:51 +0530 Subject: [PATCH 07/34] feat: -rm rowsPerPageOptions from TablePagination --- src/components/robot/RecordingsTable.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/robot/RecordingsTable.tsx b/src/components/robot/RecordingsTable.tsx index 704a7f97..46d73574 100644 --- a/src/components/robot/RecordingsTable.tsx +++ b/src/components/robot/RecordingsTable.tsx @@ -618,13 +618,12 @@ export const RecordingsTable = ({ )} From 74c830c50e03cef3a5b97dc99aa2a75545fd5145 Mon Sep 17 00:00:00 2001 From: Karishma Shukla Date: Sun, 21 Dec 2025 17:24:42 +0530 Subject: [PATCH 08/34] feat: -rm pagination options in RunsTable --- src/components/run/RunsTable.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/run/RunsTable.tsx b/src/components/run/RunsTable.tsx index 65e57049..527ff703 100644 --- a/src/components/run/RunsTable.tsx +++ b/src/components/run/RunsTable.tsx @@ -630,11 +630,10 @@ export const RunsTable: React.FC = ({ count={data.length} rowsPerPage={getPaginationState(robotMetaId).rowsPerPage} page={getPaginationState(robotMetaId).page} - onPageChange={(_, newPage) => handleChangePage(robotMetaId, newPage)} - onRowsPerPageChange={(event) => - handleChangeRowsPerPage(robotMetaId, +event.target.value) + onPageChange={(_, newPage) => + handleChangePage(robotMetaId, newPage) } - rowsPerPageOptions={[10, 25, 50, 100]} + rowsPerPageOptions={[]} /> @@ -654,4 +653,4 @@ export const RunsTable: React.FC = ({ )} ); -}; \ No newline at end of file +}; From 3ef36d28cad032d9c3b1c59b4e507f633e1b40d0 Mon Sep 17 00:00:00 2001 From: Karishma Shukla Date: Sun, 21 Dec 2025 17:25:12 +0530 Subject: [PATCH 09/34] feat: -rm rowsPerPageOptions from runs table --- src/components/run/RunsTable.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/run/RunsTable.tsx b/src/components/run/RunsTable.tsx index 527ff703..9af72d14 100644 --- a/src/components/run/RunsTable.tsx +++ b/src/components/run/RunsTable.tsx @@ -646,8 +646,7 @@ export const RunsTable: React.FC = ({ page={accordionPage} rowsPerPage={accordionsPerPage} onPageChange={handleAccordionPageChange} - onRowsPerPageChange={handleAccordionsPerPageChange} - rowsPerPageOptions={[10, 25, 50, 100]} + rowsPerPageOptions={[]} /> )} From 6d43095eccc97552dbd023f2289d9751589d14c9 Mon Sep 17 00:00:00 2001 From: Karishma Shukla Date: Sun, 21 Dec 2025 17:29:12 +0530 Subject: [PATCH 10/34] fix: show formats selected for scrape Refactor output format selection logic and improve display. --- src/components/robot/pages/RobotCreate.tsx | 38 ++++++++++++++++++++-- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/src/components/robot/pages/RobotCreate.tsx b/src/components/robot/pages/RobotCreate.tsx index a64564e6..b45db930 100644 --- a/src/components/robot/pages/RobotCreate.tsx +++ b/src/components/robot/pages/RobotCreate.tsx @@ -611,14 +611,46 @@ const RobotCreate: React.FC = () => { value={outputFormats} label="Output Formats *" onChange={(e) => { - const value = typeof e.target.value === 'string' ? e.target.value.split(',') : e.target.value; + const value = + typeof e.target.value === 'string' + ? e.target.value.split(',') + : e.target.value; setOutputFormats(value); }} renderValue={(selected) => { if (selected.length === 0) { return Select formats; } - return `${selected.length} format${selected.length > 1 ? 's' : ''} selected`; + + const OUTPUT_FORMAT_LABELS: Record = { + markdown: 'Markdown', + html: 'HTML', + 'screenshot-visible': 'Screenshot (Visible)', + 'screenshot-fullpage': 'Screenshot (Full Page)', + }; + + const labels = selected.map( + (value) => OUTPUT_FORMAT_LABELS[value] ?? value + ); + + const MAX_ITEMS = 2; // Show only first 2, then ellipsis + + const display = + labels.length > MAX_ITEMS + ? `${labels.slice(0, MAX_ITEMS).join(', ')}…` + : labels.join(', '); + + return ( + + {display} + + ); }} MenuProps={{ PaperProps: { @@ -748,4 +780,4 @@ const modalStyle = { height: 'fit-content', display: 'block', padding: '20px', -}; \ No newline at end of file +}; From 5a192fdecc4c8c678bde16a0943ff493b150f710 Mon Sep 17 00:00:00 2001 From: Rohit Rajan Date: Sun, 21 Dec 2025 18:54:00 +0530 Subject: [PATCH 11/34] chore: pass prompt --- server/src/api/sdk.ts | 10 ++++++++-- server/src/routes/storage.ts | 1 + 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/server/src/api/sdk.ts b/server/src/api/sdk.ts index db6649ee..4eb64208 100644 --- a/server/src/api/sdk.ts +++ b/server/src/api/sdk.ts @@ -88,6 +88,7 @@ router.post("/sdk/robots", requireAPIKey, async (req: AuthenticatedRequest, res: type, url: extractedUrl, formats: (workflowFile.meta as any).formats || [], + isLLM: (workflowFile.meta as any).isLLM, }; const robot = await Robot.create({ @@ -102,10 +103,14 @@ router.post("/sdk/robots", requireAPIKey, async (req: AuthenticatedRequest, res: const eventName = robotMeta.isLLM ? "maxun-oss-llm-robot-created" : "maxun-oss-robot-created"; - capture(eventName, { + const telemetryData: any = { robot_meta: robot.recording_meta, recording: robot.recording, - }); + }; + if (robotMeta.isLLM && (workflowFile.meta as any).prompt) { + telemetryData.prompt = (workflowFile.meta as any).prompt; + } + capture(eventName, telemetryData); return res.status(201).json({ data: robot, @@ -698,6 +703,7 @@ router.post("/sdk/extract/llm", requireAPIKey, async (req: AuthenticatedRequest, capture("maxun-oss-llm-robot-created", { robot_meta: robot.recording_meta, recording: robot.recording, + prompt: prompt, }); return res.status(200).json({ diff --git a/server/src/routes/storage.ts b/server/src/routes/storage.ts index 0f84504e..896baf04 100644 --- a/server/src/routes/storage.ts +++ b/server/src/routes/storage.ts @@ -589,6 +589,7 @@ router.post('/recordings/llm', requireSignIn, async (req: AuthenticatedRequest, robot_meta: newRobot.recording_meta, recording: newRobot.recording, llm_provider: llmProvider || 'ollama', + prompt: prompt, }); return res.status(201).json({ From 1999672f1d2c8124d06239db10fa482b3042d40e Mon Sep 17 00:00:00 2001 From: Rohit Rajan Date: Wed, 24 Dec 2025 12:01:15 +0530 Subject: [PATCH 12/34] feat: add ai list naming --- server/src/sdk/workflowEnricher.ts | 173 ++++++++++++++++++++++++++++- 1 file changed, 172 insertions(+), 1 deletion(-) diff --git a/server/src/sdk/workflowEnricher.ts b/server/src/sdk/workflowEnricher.ts index f8ae4920..9052b362 100644 --- a/server/src/sdk/workflowEnricher.ts +++ b/server/src/sdk/workflowEnricher.ts @@ -1240,6 +1240,168 @@ Rules: } } + /** + * Generate semantic list name using LLM based on user prompt and field context + */ + private static async generateListName( + prompt: string, + url: string, + fieldNames: string[], + llmConfig?: { + provider?: 'anthropic' | 'openai' | 'ollama'; + model?: string; + apiKey?: string; + baseUrl?: string; + } + ): Promise { + try { + const provider = llmConfig?.provider || 'ollama'; + const axios = require('axios'); + + const fieldContext = fieldNames.length > 0 + ? `\n\nDetected fields in the list:\n${fieldNames.slice(0, 10).map((name, idx) => `${idx + 1}. ${name}`).join('\n')}` + : ''; + + const systemPrompt = `You are a list naming assistant. Your job is to generate a clear, concise name for a data list based on the user's extraction request and the fields being extracted. + +RULES FOR LIST NAMING: +1. Use 1-3 words maximum (prefer 2 words) +2. Use Title Case (e.g., "Product Listings", "Job Postings") +3. Be specific and descriptive +4. Match the user's terminology when possible +5. Adapt to the domain: e-commerce (Products, Listings), jobs (Jobs, Postings), articles (Articles, News), etc. +6. Avoid generic terms like "List", "Data", "Items" unless absolutely necessary +7. Focus on WHAT is being extracted, not HOW + +Examples: +- User wants "product listings" → "Product Listings" or "Products" +- User wants "job postings" → "Job Postings" or "Jobs" +- User wants "article titles" → "Articles" +- User wants "company information" → "Companies" +- User wants "quotes from page" → "Quotes" + +You must return ONLY the list name, nothing else. No JSON, no explanation, just the name.`; + + const userPrompt = `URL: ${url} + +User's extraction request: "${prompt}" +${fieldContext} + +TASK: Generate a concise, descriptive name for this list (1-3 words in Title Case). + +Return ONLY the list name, nothing else:`; + + let llmResponse: string; + + if (provider === 'ollama') { + const ollamaBaseUrl = llmConfig?.baseUrl || process.env.OLLAMA_BASE_URL || 'http://localhost:11434'; + const ollamaModel = llmConfig?.model || 'llama3.2-vision'; + + try { + const response = await axios.post(`${ollamaBaseUrl}/api/chat`, { + model: ollamaModel, + messages: [ + { + role: 'system', + content: systemPrompt + }, + { + role: 'user', + content: userPrompt + } + ], + stream: false, + options: { + temperature: 0.1, + top_p: 0.9, + num_predict: 20 + } + }); + + llmResponse = response.data.message.content; + } catch (ollamaError: any) { + logger.error(`Ollama request failed for list naming: ${ollamaError.message}`); + logger.info('Using fallback list name: "List 1"'); + return 'List 1'; + } + } else if (provider === 'anthropic') { + const anthropic = new Anthropic({ + apiKey: llmConfig?.apiKey || process.env.ANTHROPIC_API_KEY + }); + const anthropicModel = llmConfig?.model || 'claude-3-5-sonnet-20241022'; + + const response = await anthropic.messages.create({ + model: anthropicModel, + max_tokens: 20, + temperature: 0.1, + messages: [{ + role: 'user', + content: userPrompt + }], + system: systemPrompt + }); + + const textContent = response.content.find((c: any) => c.type === 'text'); + llmResponse = textContent?.type === 'text' ? textContent.text : ''; + + } else if (provider === 'openai') { + const openaiBaseUrl = llmConfig?.baseUrl || 'https://api.openai.com/v1'; + const openaiModel = llmConfig?.model || 'gpt-4o-mini'; + + const response = await axios.post(`${openaiBaseUrl}/chat/completions`, { + model: openaiModel, + messages: [ + { + role: 'system', + content: systemPrompt + }, + { + role: 'user', + content: userPrompt + } + ], + max_tokens: 20, + temperature: 0.1 + }, { + headers: { + 'Authorization': `Bearer ${llmConfig?.apiKey || process.env.OPENAI_API_KEY}`, + 'Content-Type': 'application/json' + } + }); + + llmResponse = response.data.choices[0].message.content; + } else { + throw new Error(`Unsupported LLM provider: ${provider}`); + } + + let listName = (llmResponse || '').trim(); + logger.info(`LLM List Naming Response: "${listName}"`); + + listName = listName.replace(/^["']|["']$/g, ''); + listName = listName.split('\n')[0]; + listName = listName.trim(); + + if (!listName || listName.length === 0) { + throw new Error('LLM returned empty list name'); + } + + if (listName.length > 50) { + throw new Error('LLM returned list name that is too long'); + } + + listName = listName.split(' ') + .map((word: string) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) + .join(' '); + + logger.info(`✓ Generated list name: "${listName}"`); + return listName; + } catch (error: any) { + logger.error(`Error in generateListName: ${error.message}`); + logger.info('Using fallback list name: "List 1"'); + return 'List 1'; + } + } + /** * Build workflow from LLM decision */ @@ -1333,10 +1495,19 @@ Rules: const limit = llmDecision.limit || 100; logger.info(`Using limit: ${limit}`); + logger.info('Generating semantic list name with LLM...'); + const listName = await this.generateListName( + prompt || 'Extract list data', + url, + Object.keys(finalFields), + llmConfig + ); + logger.info(`Using list name: "${listName}"`); + workflow[0].what.push({ action: 'scrapeList', actionId: `list-${uuid()}`, - name: 'List 1', + name: listName, args: [{ fields: finalFields, listSelector: autoDetectResult.listSelector, From 19de4ad5a9b4c1312c6955b2fb63de6dec2cb78d Mon Sep 17 00:00:00 2001 From: amhsirak Date: Fri, 26 Dec 2025 19:59:00 +0530 Subject: [PATCH 13/34] chore: cleanup code --- src/context/theme-provider.tsx | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/context/theme-provider.tsx b/src/context/theme-provider.tsx index cf4e7bda..1dcdda88 100644 --- a/src/context/theme-provider.tsx +++ b/src/context/theme-provider.tsx @@ -210,7 +210,6 @@ const darkTheme = createTheme({ }, }, }, - // Additional dark mode specific components MuiPaper: { styleOverrides: { root: { @@ -247,14 +246,6 @@ const darkTheme = createTheme({ }, }, }, - // MuiTextField:{ - // styleOverrides: { - // root: { - // '& .MuiInputBase-root': { - // backgroundColor: '#1d1c1cff', - // }, - // } - // }} }, }); From b61c21c2ab6c1d6b6a149c145eb9c57f700d1b91 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Fri, 26 Dec 2025 20:01:19 +0530 Subject: [PATCH 14/34] feat: use white for outlinedInfo alerts dark moded --- src/context/theme-provider.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/context/theme-provider.tsx b/src/context/theme-provider.tsx index 1dcdda88..cb8c365f 100644 --- a/src/context/theme-provider.tsx +++ b/src/context/theme-provider.tsx @@ -193,10 +193,10 @@ const darkTheme = createTheme({ }, }, outlinedInfo: { - color: '#ff00c3', - borderColor: '#ff00c3', + color: '#ffffff', + borderColor: '#ffffff', "& .MuiAlert-icon": { - color: "#ff00c3", + color: "#ffffff", }, }, }, From 0f909116b235594a27f0c98ced020e542663614c Mon Sep 17 00:00:00 2001 From: amhsirak Date: Fri, 26 Dec 2025 20:04:31 +0530 Subject: [PATCH 15/34] feat: use black for outlinedInfo alerts light mode --- src/context/theme-provider.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/context/theme-provider.tsx b/src/context/theme-provider.tsx index cb8c365f..34059705 100644 --- a/src/context/theme-provider.tsx +++ b/src/context/theme-provider.tsx @@ -66,10 +66,10 @@ const lightTheme = createTheme({ }, }, outlinedInfo: { - color: '#ff00c3', - borderColor: '#ff00c3', + color: '#000000ff', + borderColor: '#000000ff', "& .MuiAlert-icon": { - color: "#ff00c3", + color: "#000000ff", }, }, }, From 40326046aad8353f544f75350ef5a24909a28af5 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Fri, 26 Dec 2025 20:09:04 +0530 Subject: [PATCH 16/34] chore: -rm comment --- src/components/robot/RecordingsTable.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/robot/RecordingsTable.tsx b/src/components/robot/RecordingsTable.tsx index 704a7f97..9831a395 100644 --- a/src/components/robot/RecordingsTable.tsx +++ b/src/components/robot/RecordingsTable.tsx @@ -116,7 +116,6 @@ const LoadingRobotRow = memo(({ row, columns }: any) => { // Virtualized row component for efficient rendering const TableRowMemoized = memo(({ row, columns, handlers }: any) => { - // If robot is loading, show loading row if (row.isLoading) { return ; } From 339ac2b692a02824fdb0eb9accb3a91f01cfb8ec Mon Sep 17 00:00:00 2001 From: amhsirak Date: Fri, 26 Dec 2025 20:10:28 +0530 Subject: [PATCH 17/34] feat: dont wrap column names with table head --- src/components/robot/RecordingsTable.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/robot/RecordingsTable.tsx b/src/components/robot/RecordingsTable.tsx index 9831a395..94063663 100644 --- a/src/components/robot/RecordingsTable.tsx +++ b/src/components/robot/RecordingsTable.tsx @@ -591,7 +591,6 @@ export const RecordingsTable = ({ <> - {columns.map((column) => ( ))} - {visibleRows.map((row) => ( Date: Fri, 26 Dec 2025 20:11:04 +0530 Subject: [PATCH 18/34] chore: -rm TableHead import --- src/components/robot/RecordingsTable.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/robot/RecordingsTable.tsx b/src/components/robot/RecordingsTable.tsx index 94063663..8d29f8fb 100644 --- a/src/components/robot/RecordingsTable.tsx +++ b/src/components/robot/RecordingsTable.tsx @@ -5,7 +5,6 @@ import Table from '@mui/material/Table'; import TableBody from '@mui/material/TableBody'; import TableCell from '@mui/material/TableCell'; import TableContainer from '@mui/material/TableContainer'; -import TableHead from '@mui/material/TableHead'; import TablePagination from '@mui/material/TablePagination'; import TableRow from '@mui/material/TableRow'; import { memo, useCallback, useEffect, useMemo } from "react"; From 27b188170e868add9dd9a7df3070752cdb59d2c0 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Tue, 30 Dec 2025 00:27:47 +0530 Subject: [PATCH 19/34] feat: store api_key_created_at --- server/src/models/User.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/server/src/models/User.ts b/server/src/models/User.ts index 6664f381..1076bc07 100644 --- a/server/src/models/User.ts +++ b/server/src/models/User.ts @@ -7,6 +7,7 @@ interface UserAttributes { password: string; api_key_name?: string | null; api_key?: string | null; + api_key_created_at?: Date | null; proxy_url?: string | null; proxy_username?: string | null; proxy_password?: string | null; @@ -20,6 +21,7 @@ class User extends Model implements User public password!: string; public api_key_name!: string | null; public api_key!: string | null; + public api_key_created_at!: Date | null; public proxy_url!: string | null; public proxy_username!: string | null; public proxy_password!: string | null; @@ -53,6 +55,10 @@ User.init( type: DataTypes.STRING, allowNull: true, }, + api_key_created_at: { + type: DataTypes.DATE, + allowNull: true, + }, proxy_url: { type: DataTypes.STRING, allowNull: true, From afd4a63249cd53117ff56321ed905f16267accfb Mon Sep 17 00:00:00 2001 From: amhsirak Date: Tue, 30 Dec 2025 00:31:51 +0530 Subject: [PATCH 20/34] feat: create, store, fetch api key create date --- server/src/routes/auth.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index 5a758ee9..4ddbafe1 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -255,8 +255,9 @@ router.post( return res.status(400).json({ message: "API key already exists" }); } const apiKey = genAPIKey(); + const createdAt = new Date(); - await user.update({ api_key: apiKey }); + await user.update({ api_key: apiKey, api_key_created_at: createdAt }) capture("maxun-oss-api-key-created", { user_id: user.id, @@ -266,6 +267,7 @@ router.post( return res.status(200).json({ message: "API key generated successfully", api_key: apiKey, + api_key_created_at: createdAt, }); } catch (error) { return res @@ -290,7 +292,7 @@ router.get( const user = await User.findByPk(req.user.id, { raw: true, - attributes: ["api_key"], + attributes: ["api_key", "api_key_created_at"] }); if (!user) { @@ -305,6 +307,7 @@ router.get( ok: true, message: "API key fetched successfully", api_key: user.api_key || null, + api_key_created_at: user.api_key_created_at || null, }); } catch (error) { console.error('API Key fetch error:', error); @@ -336,7 +339,7 @@ router.delete( return res.status(404).json({ message: "API Key not found" }); } - await User.update({ api_key: null }, { where: { id: req.user.id } }); + await User.update({ api_key: null, api_key_created_at: null }, { where: { id: req.user.id } }); capture("maxun-oss-api-key-deleted", { user_id: user.id, From 495d440ce638bbb4755674d6c7ca8cddeed99a11 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Tue, 30 Dec 2025 00:34:10 +0530 Subject: [PATCH 21/34] feat: show api key created on --- src/components/api/ApiKey.tsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/components/api/ApiKey.tsx b/src/components/api/ApiKey.tsx index 27897169..73916d1e 100644 --- a/src/components/api/ApiKey.tsx +++ b/src/components/api/ApiKey.tsx @@ -34,6 +34,7 @@ const ApiKeyManager = () => { const { t } = useTranslation(); const [apiKey, setApiKey] = useState(null); const [apiKeyName, setApiKeyName] = useState(t('apikey.default_name')); + const [apiKeyCreatedAt, setApiKeyCreatedAt] = useState(null); const [loading, setLoading] = useState(true); const [showKey, setShowKey] = useState(false); const [copySuccess, setCopySuccess] = useState(false); @@ -44,6 +45,7 @@ const ApiKeyManager = () => { try { const { data } = await axios.get(`${apiUrl}/auth/api-key`); setApiKey(data.api_key); + setApiKeyCreatedAt(data.api_key_created_at); } catch (error: any) { notify('error', t('apikey.notifications.fetch_error', { error: error.message })); } finally { @@ -60,7 +62,7 @@ const ApiKeyManager = () => { try { const { data } = await axios.post(`${apiUrl}/auth/generate-api-key`); setApiKey(data.api_key); - + setApiKeyCreatedAt(data.api_key_created_at); notify('success', t('apikey.notifications.generate_success')); } catch (error: any) { notify('error', t('apikey.notifications.generate_error', { error: error.message })); @@ -74,6 +76,7 @@ const ApiKeyManager = () => { try { await axios.delete(`${apiUrl}/auth/delete-api-key`); setApiKey(null); + setApiKeyCreatedAt(null); notify('success', t('apikey.notifications.delete_success')); } catch (error: any) { notify('error', t('apikey.notifications.delete_error', { error: error.message })); @@ -133,6 +136,7 @@ const ApiKeyManager = () => { {t('apikey.table.name')} {t('apikey.table.key')} + Created On {t('apikey.table.actions')} @@ -144,6 +148,15 @@ const ApiKeyManager = () => { {showKey ? `${apiKey?.substring(0, 10)}...` : '**********'} + {apiKeyCreatedAt && ( + + {new Date(apiKeyCreatedAt).toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + })} + + )} From 7ef961d3da927221e28e0d39e33a1760e27f12e7 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Tue, 30 Dec 2025 00:34:36 +0530 Subject: [PATCH 22/34] fix: align all actions to right end --- src/components/api/ApiKey.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/api/ApiKey.tsx b/src/components/api/ApiKey.tsx index 73916d1e..413d866c 100644 --- a/src/components/api/ApiKey.tsx +++ b/src/components/api/ApiKey.tsx @@ -157,7 +157,7 @@ const ApiKeyManager = () => { })} )} - + From 1bc3f02d9b8f11cac69c32360aecab3fed89f756 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Tue, 30 Dec 2025 00:34:58 +0530 Subject: [PATCH 23/34] fix: align all actions to right end --- src/components/api/ApiKey.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/api/ApiKey.tsx b/src/components/api/ApiKey.tsx index 413d866c..3feb590d 100644 --- a/src/components/api/ApiKey.tsx +++ b/src/components/api/ApiKey.tsx @@ -131,7 +131,7 @@ const ApiKeyManager = () => { {apiKey ? ( -
+
{t('apikey.table.name')} From d1e0164047cccdddc2f858e7fb1a2a40bcc63b80 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Tue, 30 Dec 2025 00:35:34 +0530 Subject: [PATCH 24/34] fix: align actions name table cell to center --- src/components/api/ApiKey.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/api/ApiKey.tsx b/src/components/api/ApiKey.tsx index 3feb590d..fc81aecb 100644 --- a/src/components/api/ApiKey.tsx +++ b/src/components/api/ApiKey.tsx @@ -137,7 +137,7 @@ const ApiKeyManager = () => { {t('apikey.table.name')} {t('apikey.table.key')} Created On - {t('apikey.table.actions')} + {t('apikey.table.actions')} From 0006165f1ac166707dafc97eded87215d5db517b Mon Sep 17 00:00:00 2001 From: amhsirak Date: Tue, 30 Dec 2025 00:37:37 +0530 Subject: [PATCH 25/34] fix: show created on col if apiKeyCreatedAt exists --- src/components/api/ApiKey.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/api/ApiKey.tsx b/src/components/api/ApiKey.tsx index fc81aecb..73f95dc7 100644 --- a/src/components/api/ApiKey.tsx +++ b/src/components/api/ApiKey.tsx @@ -136,7 +136,7 @@ const ApiKeyManager = () => { {t('apikey.table.name')} {t('apikey.table.key')} - Created On + {apiKeyCreatedAt && Created On} {t('apikey.table.actions')} From 983b8ab1bc40858af5a012ec7da12f5457510406 Mon Sep 17 00:00:00 2001 From: Karishma Shukla Date: Thu, 1 Jan 2026 22:56:24 +0530 Subject: [PATCH 26/34] feat: custom scrollbar styles for table container dark mode Add custom scrollbar styles for MuiTableContainer component. --- src/context/theme-provider.tsx | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/context/theme-provider.tsx b/src/context/theme-provider.tsx index 5188f53e..9b8b3164 100644 --- a/src/context/theme-provider.tsx +++ b/src/context/theme-provider.tsx @@ -102,6 +102,29 @@ const darkTheme = createTheme({ }, }, components: { + MuiTableContainer: { + styleOverrides: { + root: { + overflow: 'auto', + /* Firefox */ + scrollbarWidth: 'thin', + scrollbarColor: 'currentColor transparent', + + /* WebKit (Chrome, Edge, Safari) */ + '&::-webkit-scrollbar': { + width: '5px', + height: '5px', + }, + '&::-webkit-scrollbar-track': { + background: 'transparent', + }, + '&::-webkit-scrollbar-thumb': { + backgroundColor: 'currentColor', + borderRadius: '8px', + }, + }, + }, + }, MuiButton: { styleOverrides: { root: { @@ -280,4 +303,4 @@ const ThemeModeProvider = ({ children }: { children: React.ReactNode }) => { ); }; -export default ThemeModeProvider; \ No newline at end of file +export default ThemeModeProvider; From 39c27dc4f84e6e564cb8f1b539f5ffdbb99c2eaa Mon Sep 17 00:00:00 2001 From: Karishma Shukla Date: Thu, 1 Jan 2026 22:57:26 +0530 Subject: [PATCH 27/34] feat: custom scrollbar styles table light mode Add custom scrollbar styles for MuiTableContainer. --- src/context/theme-provider.tsx | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/context/theme-provider.tsx b/src/context/theme-provider.tsx index 9b8b3164..553d70a5 100644 --- a/src/context/theme-provider.tsx +++ b/src/context/theme-provider.tsx @@ -10,6 +10,29 @@ const lightTheme = createTheme({ }, }, components: { + MuiTableContainer: { + styleOverrides: { + root: { + overflow: 'auto', + /* Firefox */ + scrollbarWidth: 'thin', + scrollbarColor: 'gray transparent', + + /* WebKit (Chrome, Edge, Safari) */ + '&::-webkit-scrollbar': { + width: '5px', + height: '5px', + }, + '&::-webkit-scrollbar-track': { + background: 'transparent', + }, + '&::-webkit-scrollbar-thumb': { + backgroundColor: 'gray', + borderRadius: '8px', + }, + }, + }, + }, MuiButton: { styleOverrides: { root: { From 36b80da631f16238765385973eabaed2bc2c081d Mon Sep 17 00:00:00 2001 From: amhsirak Date: Fri, 2 Jan 2026 11:18:23 +0530 Subject: [PATCH 28/34] feat(core): real time progress update for runs --- maxun-core/src/interpret.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/maxun-core/src/interpret.ts b/maxun-core/src/interpret.ts index b909376a..e6bd62f9 100644 --- a/maxun-core/src/interpret.ts +++ b/maxun-core/src/interpret.ts @@ -48,6 +48,7 @@ interface InterpreterOptions { debugMessage: (msg: string) => void, setActionType: (type: string) => void, incrementScrapeListIndex: () => void, + progressUpdate: (current: number, total: number, percentage: number) => void, }> } @@ -84,6 +85,10 @@ export default class Interpreter extends EventEmitter { private scrapeListCounter: number = 0; + private totalActions: number = 0; + + private executedActions: number = 0; + constructor(workflow: WorkflowFile, options?: Partial) { super(); this.workflow = workflow.workflow; @@ -1596,6 +1601,17 @@ export default class Interpreter extends EventEmitter { workflowCopy.splice(actionId, 1); console.log(`Action with ID ${action.id} removed from the workflow copy.`); + + this.executedActions++; + const percentage = Math.round((this.executedActions / this.totalActions) * 100); + + if (this.options.debugChannel?.progressUpdate) { + this.options.debugChannel.progressUpdate( + this.executedActions, + this.totalActions, + percentage + ); + } // const newSelectors = this.getPreviousSelectors(workflow, actionId); // const newSelectors = this.getSelectors(workflowCopy); @@ -1686,6 +1702,13 @@ export default class Interpreter extends EventEmitter { */ this.initializedWorkflow = Preprocessor.initWorkflow(this.workflow, params); + this.totalActions = this.initializedWorkflow.length; + this.executedActions = 0; + + if (this.options.debugChannel?.progressUpdate) { + this.options.debugChannel.progressUpdate(0, this.totalActions, 0); + } + await this.ensureScriptsLoaded(page); this.stopper = () => { From 2f42914cc422a2f05ebecdffbc83481ece539884 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Fri, 2 Jan 2026 11:35:32 +0530 Subject: [PATCH 29/34] feat: real time progress update for runs --- server/src/workflow-management/classes/Interpreter.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/server/src/workflow-management/classes/Interpreter.ts b/server/src/workflow-management/classes/Interpreter.ts index 03e9ef87..1bd6efe5 100644 --- a/server/src/workflow-management/classes/Interpreter.ts +++ b/server/src/workflow-management/classes/Interpreter.ts @@ -580,6 +580,13 @@ export class WorkflowInterpreter { setActionName: (name: string) => { this.currentActionName = name; }, + progressUpdate: (current: number, total: number, percentage: number) => { + this.socket.nsp.emit('workflowProgress', { + current, + total, + percentage + }); + }, }, serializableCallback: async (data: any) => { try { From 0e3d430f41bfa0d3ee1514b71246dc9c0b14d7be Mon Sep 17 00:00:00 2001 From: amhsirak Date: Fri, 2 Jan 2026 11:38:16 +0530 Subject: [PATCH 30/34] chore: remove scrollToLogBottom() --- src/components/run/ColapsibleRow.tsx | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/components/run/ColapsibleRow.tsx b/src/components/run/ColapsibleRow.tsx index 67e82bf0..bf5116e5 100644 --- a/src/components/run/ColapsibleRow.tsx +++ b/src/components/run/ColapsibleRow.tsx @@ -54,12 +54,6 @@ export const CollapsibleRow = ({ row, handleDelete, isOpen, onToggleExpanded, cu const logEndRef = useRef(null); - const scrollToLogBottom = () => { - if (logEndRef.current) { - logEndRef.current.scrollIntoView({ behavior: "smooth" }); - } - } - const handleAbort = () => { abortRunHandler(row.runId, row.name, row.browserId); } @@ -67,12 +61,7 @@ export const CollapsibleRow = ({ row, handleDelete, isOpen, onToggleExpanded, cu const handleRowExpand = () => { const newOpen = !isOpen; onToggleExpanded(newOpen); - //scrollToLogBottom(); }; - - // useEffect(() => { - // scrollToLogBottom(); - // }, [currentLog]) useEffect(() => { const fetchUserEmail = async () => { From 8a32c0b2d2d0c7b245175ccbd17af603f8304a57 Mon Sep 17 00:00:00 2001 From: amhsirak Date: Fri, 2 Jan 2026 11:42:32 +0530 Subject: [PATCH 31/34] feat: real time progress update for runs --- src/components/run/ColapsibleRow.tsx | 89 +++++++++++++++++++++++++++- src/components/run/RunContent.tsx | 34 ++++++++++- 2 files changed, 119 insertions(+), 4 deletions(-) diff --git a/src/components/run/ColapsibleRow.tsx b/src/components/run/ColapsibleRow.tsx index bf5116e5..7307985e 100644 --- a/src/components/run/ColapsibleRow.tsx +++ b/src/components/run/ColapsibleRow.tsx @@ -12,6 +12,45 @@ import { GenericModal } from "../ui/GenericModal"; import { getUserById } from "../../api/auth"; import { useTranslation } from "react-i18next"; import { useTheme } from "@mui/material/styles"; +import { io, Socket } from "socket.io-client"; +import { remoteBrowserApiUrl } from "../../apiConfig"; + +const socketCache = new Map(); +const progressCallbacks = new Map void>>(); + +function getOrCreateSocket(browserId: string): Socket { + if (socketCache.has(browserId)) { + return socketCache.get(browserId)!; + } + + const socket = io(`${remoteBrowserApiUrl}/${browserId}`, { + transports: ["websocket"], + rejectUnauthorized: false + }); + + socket.on('workflowProgress', (data: any) => { + const callbacks = progressCallbacks.get(browserId); + if (callbacks) { + callbacks.forEach(cb => cb(data)); + } + }); + + socketCache.set(browserId, socket); + return socket; +} + +function cleanupSocketIfUnused(browserId: string) { + const callbacks = progressCallbacks.get(browserId); + + if (!callbacks || callbacks.size === 0) { + const socket = socketCache.get(browserId); + if (socket) { + socket.disconnect(); + socketCache.delete(browserId); + progressCallbacks.delete(browserId); + } + } +} interface RunTypeChipProps { runByUserId?: string; @@ -54,6 +93,53 @@ export const CollapsibleRow = ({ row, handleDelete, isOpen, onToggleExpanded, cu const logEndRef = useRef(null); + const [workflowProgress, setWorkflowProgress] = useState<{ + current: number; + total: number; + percentage: number; + } | null>(null); + + // Subscribe to progress updates using module-level socket cache + useEffect(() => { + if (!row.browserId) return; + + // Get or create socket (from module cache) + getOrCreateSocket(row.browserId); + + // Register callback + if (!progressCallbacks.has(row.browserId)) { + progressCallbacks.set(row.browserId, new Set()); + } + + const callback = (data: any) => { + setWorkflowProgress(data); + }; + + progressCallbacks.get(row.browserId)!.add(callback); + + // Cleanup: remove callback and cleanup socket if no callbacks remain + return () => { + const callbacks = progressCallbacks.get(row.browserId); + if (callbacks) { + callbacks.delete(callback); + // Cleanup socket if this was the last callback + cleanupSocketIfUnused(row.browserId); + } + }; + }, [row.browserId]); + + // Clear progress UI when run completes and trigger socket cleanup + useEffect(() => { + if (row.status !== 'running' && row.status !== 'queued') { + setWorkflowProgress(null); + // Attempt to cleanup socket when run completes + // (will only cleanup if no other callbacks exist) + if (row.browserId) { + cleanupSocketIfUnused(row.browserId); + } + } + }, [row.status, row.browserId]); + const handleAbort = () => { abortRunHandler(row.runId, row.name, row.browserId); } @@ -185,7 +271,8 @@ export const CollapsibleRow = ({ row, handleDelete, isOpen, onToggleExpanded, cu + logEndRef={logEndRef} interpretationInProgress={runningRecordingName === row.name} + workflowProgress={workflowProgress} /> diff --git a/src/components/run/RunContent.tsx b/src/components/run/RunContent.tsx index 0cb3e71a..6a50b098 100644 --- a/src/components/run/RunContent.tsx +++ b/src/components/run/RunContent.tsx @@ -23,6 +23,8 @@ import TableHead from '@mui/material/TableHead'; import TableRow from '@mui/material/TableRow'; import { useTranslation } from "react-i18next"; import { useThemeMode } from "../../context/theme-provider"; +import { remoteBrowserApiUrl } from "../../apiConfig"; +import { io } from "socket.io-client"; interface RunContentProps { row: Data, @@ -30,10 +32,14 @@ interface RunContentProps { interpretationInProgress: boolean, logEndRef: React.RefObject, abortRunHandler: () => void, + workflowProgress: { + current: number; + total: number; + percentage: number; + } | null, } -export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRef, abortRunHandler }: RunContentProps) => { - const { t } = useTranslation(); +export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRef, abortRunHandler, workflowProgress }: RunContentProps) => { const { t } = useTranslation(); const [tab, setTab] = React.useState('output'); const [markdownContent, setMarkdownContent] = useState(''); const [htmlContent, setHtmlContent] = useState(''); @@ -63,6 +69,15 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe setTab(tab); }, [interpretationInProgress]); + const getProgressMessage = (percentage: number): string => { + if (percentage === 0) return 'Initializing workflow...'; + if (percentage < 25) return 'Starting execution...'; + if (percentage < 50) return 'Processing actions...'; + if (percentage < 75) return 'Extracting data...'; + if (percentage < 100) return 'Finalizing results...'; + return 'Completing...'; + }; + useEffect(() => { setMarkdownContent(''); setHtmlContent(''); @@ -810,7 +825,20 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe {row.status === 'running' || row.status === 'queued' ? ( <> - + {workflowProgress ? ( + <> + + {getProgressMessage(workflowProgress.percentage)} + + ) : ( + <> + + {t('run_content.loading')} + + )} {t('run_content.loading')}