feat: ui enhancements and fixes

This commit is contained in:
Rohit Rajan
2025-12-11 13:40:49 +05:30
parent c8c58eef54
commit c9085d61b4
5 changed files with 435 additions and 372 deletions

View File

@@ -542,12 +542,12 @@ router.post('/recordings/llm', requireSignIn, async (req: AuthenticatedRequest,
logger.log('info', `Starting LLM workflow generation for URL: ${url}`); logger.log('info', `Starting LLM workflow generation for URL: ${url}`);
const workflowResult = await WorkflowEnricher.generateWorkflowFromPrompt(url, prompt, { const workflowResult = await WorkflowEnricher.generateWorkflowFromPrompt(url, prompt, req.user.id, {
provider: llmProvider || 'ollama', provider: llmProvider || 'ollama',
model: llmModel, model: llmModel,
apiKey: llmApiKey, apiKey: llmApiKey,
baseUrl: llmBaseUrl baseUrl: llmBaseUrl
}, req.user.id); });
if (!workflowResult.success || !workflowResult.workflow) { if (!workflowResult.success || !workflowResult.workflow) {
logger.log('error', `Failed to generate workflow: ${JSON.stringify(workflowResult.errors)}`); logger.log('error', `Failed to generate workflow: ${JSON.stringify(workflowResult.errors)}`);
@@ -1321,4 +1321,4 @@ export async function recoverOrphanedRuns() {
} }
} }
export { processQueuedRuns }; export { processQueuedRuns };

View File

@@ -21,7 +21,7 @@ import {
MenuItem, MenuItem,
InputLabel InputLabel
} from '@mui/material'; } from '@mui/material';
import { ArrowBack, PlayCircleOutline, Article, Code, Description } from '@mui/icons-material'; import { ArrowBack, PlayCircleOutline, Article, Code, Description, SmartToy } from '@mui/icons-material';
import { useGlobalInfoStore, useCacheInvalidation } from '../../../context/globalInfo'; import { useGlobalInfoStore, useCacheInvalidation } from '../../../context/globalInfo';
import { canCreateBrowserInState, getActiveBrowserId, stopRecording } from '../../../api/recording'; import { canCreateBrowserInState, getActiveBrowserId, stopRecording } from '../../../api/recording';
import { createScrapeRobot, createLLMRobot, createAndRunRecording } from "../../../api/storage"; import { createScrapeRobot, createLLMRobot, createAndRunRecording } from "../../../api/storage";
@@ -59,11 +59,13 @@ const RobotCreate: React.FC = () => {
const [tabValue, setTabValue] = useState(0); const [tabValue, setTabValue] = useState(0);
const [url, setUrl] = useState(''); const [url, setUrl] = useState('');
const [scrapeRobotName, setScrapeRobotName] = useState(''); const [scrapeRobotName, setScrapeRobotName] = useState('');
const [extractRobotName, setExtractRobotName] = useState('');
const [needsLogin, setNeedsLogin] = useState(false); const [needsLogin, setNeedsLogin] = useState(false);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isWarningModalOpen, setWarningModalOpen] = useState(false); const [isWarningModalOpen, setWarningModalOpen] = useState(false);
const [activeBrowserId, setActiveBrowserId] = useState(''); const [activeBrowserId, setActiveBrowserId] = useState('');
const [outputFormats, setOutputFormats] = useState<string[]>([]); const [outputFormats, setOutputFormats] = useState<string[]>([]);
const [generationMode, setGenerationMode] = useState<'agent' | 'recorder' | null>(null);
// AI Extract tab state // AI Extract tab state
const [aiPrompt, setAiPrompt] = useState(''); const [aiPrompt, setAiPrompt] = useState('');
@@ -75,7 +77,7 @@ const RobotCreate: React.FC = () => {
const { state } = React.useContext(AuthContext); const { state } = React.useContext(AuthContext);
const { user } = state; const { user } = state;
const { addOptimisticRobot, removeOptimisticRobot, invalidateRecordings } = useCacheInvalidation(); const { addOptimisticRobot, removeOptimisticRobot, invalidateRecordings, invalidateRuns, addOptimisticRun } = useCacheInvalidation();
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
setTabValue(newValue); setTabValue(newValue);
@@ -215,190 +217,16 @@ const RobotCreate: React.FC = () => {
}} }}
> >
<Tab label="Extract" id="extract-robot" aria-controls="extract-robot" /> <Tab label="Extract" id="extract-robot" aria-controls="extract-robot" />
<Tab label="AI Extract" id="ai-extract-robot" aria-controls="ai-extract-robot" />
<Tab label="Scrape" id="scrape-robot" aria-controls="scrape-robot" /> <Tab label="Scrape" id="scrape-robot" aria-controls="scrape-robot" />
</Tabs> </Tabs>
</Box> </Box>
<TabPanel value={tabValue} index={0}> <TabPanel value={tabValue} index={0}>
<Card sx={{ mb: 4, p: 4, textAlign: 'center' }}> <Card sx={{ mb: 4, p: 4 }}>
<Box display="flex" flexDirection="column" alignItems="center"> <Box display="flex" flexDirection="column" alignItems="center">
{/* Logo (kept as original) */} {/* Website URL */}
<img <Box sx={{ width: '100%', maxWidth: 700, mb: 3 }}>
src="https://ik.imagekit.io/ys1blv5kv/maxunlogo.png"
width={73}
height={65}
style={{
borderRadius: '5px',
marginBottom: '30px'
}}
alt="Maxun Logo"
/>
<Typography variant="body2" color="text.secondary" mb={3}>
Extract structured data from websites in a few clicks.
</Typography>
{/* Origin URL Input */}
<Box sx={{ width: '100%', maxWidth: 700, mb: 2 }}>
<TextField
placeholder="Example: https://www.ycombinator.com/companies/"
variant="outlined"
fullWidth
value={url}
onChange={(e) => setUrl(e.target.value)}
/>
</Box>
{/* Checkbox */}
<Box sx={{ width: '100%', maxWidth: 700, mb: 3, textAlign: 'left' }}>
<FormControlLabel
control={
<Checkbox
checked={needsLogin}
onChange={(e) => setNeedsLogin(e.target.checked)}
color="primary"
/>
}
label="This website needs logging in."
/>
</Box>
{/* Button */}
<Button
variant="contained"
fullWidth
onClick={handleStartRecording}
disabled={!url.trim() || isLoading}
sx={{
bgcolor: '#ff00c3',
py: 1.4,
fontSize: '1rem',
textTransform: 'none',
maxWidth: 700,
borderRadius: 2
}}
startIcon={isLoading ? <CircularProgress size={20} color="inherit" /> : null}
>
{isLoading ? 'Starting...' : 'Start Recording'}
</Button>
</Box>
</Card>
<Box mt={6} textAlign="center">
<Typography variant="h6" gutterBottom>
First time creating a robot?
</Typography>
<Typography variant="body2" color="text.secondary" mb={3}>
Get help and learn how to use Maxun effectively.
</Typography>
<Grid container spacing={3} justifyContent="center">
{/* YouTube Tutorials */}
<Grid item xs={12} sm={6} md={4}>
<Card
sx={{
height: 140,
cursor: "pointer",
}}
onClick={() => window.open("https://www.youtube.com/@MaxunOSS/videos", "_blank")}
>
<CardContent
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center", // center content
height: "100%",
textAlign: "center",
p: 2,
color: (theme) =>
theme.palette.mode === 'light' ? 'rgba(0, 0, 0, 0.54)' : '',
}}
>
<PlayCircleOutline sx={{ fontSize: "32px", mb: 2 }} />
<Box sx={{ textAlign: "center" }}>
<Typography variant="body1" fontWeight="600" sx={{ lineHeight: 1.2 }}>
Video Tutorials
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ lineHeight: 1.4, mt: 1 }}>
Watch step-by-step guides
</Typography>
</Box>
</CardContent>
</Card>
</Grid>
{/* Documentation */}
<Grid item xs={12} sm={6} md={4}>
<Card
sx={{
height: 140,
cursor: "pointer",
}}
onClick={() => window.open("https://docs.maxun.dev", "_blank")}
>
<CardContent
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center", // center everything
height: "100%",
textAlign: "center",
p: 2,
color: (theme) =>
theme.palette.mode === 'light' ? 'rgba(0, 0, 0, 0.54)' : '',
}}
>
<Article sx={{ fontSize: "32px", mb: 2 }} />
<Box sx={{ textAlign: "center" }}>
<Typography variant="body1" fontWeight="600" sx={{ lineHeight: 1.2 }}>
Documentation
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ lineHeight: 1.4, mt: 1 }}>
Explore detailed guides
</Typography>
</Box>
</CardContent>
</Card>
</Grid>
</Grid>
</Box>
</TabPanel>
<TabPanel value={tabValue} index={1}>
<Card sx={{ mb: 4, p: 4, textAlign: 'center' }}>
<Box display="flex" flexDirection="column" alignItems="center">
<img
src="https://ik.imagekit.io/ys1blv5kv/maxunlogo.png"
width={73}
height={65}
style={{
borderRadius: '5px',
marginBottom: '30px'
}}
alt="Maxun Logo"
/>
<Typography variant="body2" color="text.secondary" mb={3}>
AI-powered extraction: Describe what you want to extract in natural language.
</Typography>
<Box sx={{ width: '100%', maxWidth: 700, mb: 2 }}>
<TextField
placeholder="Example: AI Product Extractor"
variant="outlined"
fullWidth
value={aiRobotName}
onChange={(e) => setAiRobotName(e.target.value)}
sx={{ mb: 2 }}
label="Robot Name (Optional)"
/>
<TextField <TextField
placeholder="Example: https://www.ycombinator.com/companies/" placeholder="Example: https://www.ycombinator.com/companies/"
variant="outlined" variant="outlined"
@@ -406,207 +234,325 @@ const RobotCreate: React.FC = () => {
value={url} value={url}
onChange={(e) => setUrl(e.target.value)} onChange={(e) => setUrl(e.target.value)}
label="Website URL" label="Website URL"
sx={{ mb: 2 }}
/> />
<TextField
placeholder="Example: Extract first 15 company names, descriptions, and batch information"
variant="outlined"
fullWidth
multiline
rows={3}
value={aiPrompt}
onChange={(e) => setAiPrompt(e.target.value)}
label="Extraction Prompt"
sx={{ mb: 2 }}
/>
<Box sx={{ width: '100%', display: 'flex', gap: 2, mb: 2 }}>
<FormControl sx={{ flex: 1 }}>
<InputLabel id="llm-provider-label">LLM Provider</InputLabel>
<Select
labelId="llm-provider-label"
id="llm-provider"
value={llmProvider}
label="LLM Provider"
onChange={(e) => {
const provider = e.target.value as 'anthropic' | 'openai' | 'ollama';
setLlmProvider(provider);
setLlmModel('');
if (provider === 'ollama') {
setLlmBaseUrl('http://localhost:11434');
} else {
setLlmBaseUrl('');
}
}}
>
<MenuItem value="ollama">Ollama (Local)</MenuItem>
<MenuItem value="anthropic">Anthropic (Claude)</MenuItem>
<MenuItem value="openai">OpenAI (GPT-4)</MenuItem>
</Select>
</FormControl>
<FormControl sx={{ flex: 1 }}>
<InputLabel id="llm-model-label">Model (Optional)</InputLabel>
<Select
labelId="llm-model-label"
id="llm-model"
value={llmModel}
label="Model (Optional)"
onChange={(e) => setLlmModel(e.target.value)}
>
{llmProvider === 'ollama' && (
<>
<MenuItem value="">Default (llama3.2-vision)</MenuItem>
<MenuItem value="llama3.2-vision">llama3.2-vision</MenuItem>
<MenuItem value="llama3.2">llama3.2</MenuItem>
</>
)}
{llmProvider === 'anthropic' && (
<>
<MenuItem value="">Default (claude-3-5-sonnet)</MenuItem>
<MenuItem value="claude-3-5-sonnet-20241022">claude-3-5-sonnet-20241022</MenuItem>
<MenuItem value="claude-3-opus-20240229">claude-3-opus-20240229</MenuItem>
</>
)}
{llmProvider === 'openai' && (
<>
<MenuItem value="">Default (gpt-4-vision-preview)</MenuItem>
<MenuItem value="gpt-4-vision-preview">gpt-4-vision-preview</MenuItem>
<MenuItem value="gpt-4o">gpt-4o</MenuItem>
</>
)}
</Select>
</FormControl>
</Box>
{llmProvider !== 'ollama' && (
<TextField
placeholder={`${llmProvider === 'anthropic' ? 'Anthropic' : 'OpenAI'} API Key (or set in .env)`}
variant="outlined"
fullWidth
type="password"
value={llmApiKey}
onChange={(e) => setLlmApiKey(e.target.value)}
label="API Key (Optional if set in .env)"
sx={{ mb: 2 }}
/>
)}
{llmProvider === 'ollama' && (
<TextField
placeholder="http://localhost:11434"
variant="outlined"
fullWidth
value={llmBaseUrl}
onChange={(e) => setLlmBaseUrl(e.target.value)}
label="Ollama Base URL (Optional)"
sx={{ mb: 2 }}
/>
)}
</Box> </Box>
<Button {/* Generation Mode Selection */}
variant="contained" <Box sx={{ width: '100%', maxWidth: 700, mb: 3 }}>
fullWidth <Typography variant="subtitle1" gutterBottom sx={{ fontWeight: 600, mb: 2 }}>
onClick={async () => { Generation Mode
if (!url.trim()) { </Typography>
notify('error', 'Please enter a valid URL');
return;
}
if (!aiPrompt.trim()) {
notify('error', 'Please enter an extraction prompt');
return;
}
const tempRobotId = `temp-${Date.now()}`; <Box sx={{ display: 'flex', gap: 2 }}>
const robotDisplayName = aiRobotName || `LLM Extract: ${aiPrompt.substring(0, 50)}`; <Card
onClick={() => setGenerationMode('agent')}
sx={{
flex: 1,
cursor: 'pointer',
border: '2px solid',
borderColor: generationMode === 'agent' ? '#ff00c3' : 'divider',
transition: 'all 0.2s',
'&:hover': {
borderColor: '#ff00c3',
}
}}
>
<CardContent sx={{ textAlign: 'center', py: 3 }}>
<SmartToy sx={{ fontSize: 32, mb: 1 }} />
<Typography variant="h6" gutterBottom>
AI Powered
</Typography>
<Typography variant="body2" color="text.secondary">
AI will take care of everything
</Typography>
</CardContent>
</Card>
const optimisticRobot = { <Card
id: tempRobotId, onClick={() => setGenerationMode('recorder')}
recording_meta: { sx={{
id: tempRobotId, flex: 1,
name: robotDisplayName, cursor: 'pointer',
createdAt: new Date().toISOString(), border: '2px solid',
updatedAt: new Date().toISOString(), borderColor: generationMode === 'recorder' ? '#ff00c3' : 'divider',
pairs: 0, transition: 'all 0.2s',
params: [], '&:hover': {
type: 'extract', borderColor: '#ff00c3',
url: url, }
}, }}
recording: { workflow: [] }, >
isLoading: true, <CardContent sx={{ textAlign: 'center', py: 3 }}>
isOptimistic: true <PlayCircleOutline sx={{ fontSize: 32, mb: 1 }} />
}; <Typography variant="h6" gutterBottom>
Record yourself
</Typography>
<Typography variant="body2" color="text.secondary">
You control the browser
</Typography>
</CardContent>
</Card>
</Box>
</Box>
addOptimisticRobot(optimisticRobot); {/* Conditional rendering based on generation mode */}
{generationMode === 'agent' && (
<Box sx={{ width: '100%', maxWidth: 700 }}>
{/* Robot Name */}
<Box sx={{ mb: 3 }}>
<TextField
placeholder="Robot Name"
variant="outlined"
fullWidth
value={extractRobotName}
onChange={(e) => setExtractRobotName(e.target.value)}
label="Robot Name"
/>
</Box>
notify('info', `Robot ${robotDisplayName} creation started (AI Powered)`); {/* Extraction Prompt */}
navigate('/robots'); <Box sx={{ mb: 3 }}>
<TextField
placeholder="Example: Extract first 15 company names, descriptions, and batch information"
variant="outlined"
fullWidth
multiline
rows={3}
value={aiPrompt}
onChange={(e) => setAiPrompt(e.target.value)}
label="Extraction Prompt"
/>
</Box>
try { {/* LLM Provider and Model */}
const result = await createLLMRobot( <Box sx={{ display: 'flex', gap: 2, mb: 3 }}>
url, <FormControl sx={{ flex: 1 }}>
aiPrompt, <InputLabel>LLM Provider</InputLabel>
llmProvider, <Select
llmModel || undefined, value={llmProvider}
llmApiKey || undefined, label="LLM Provider"
llmBaseUrl || undefined, onChange={(e) => {
aiRobotName || undefined const provider = e.target.value as 'anthropic' | 'openai' | 'ollama';
); setLlmProvider(provider);
setLlmModel('');
if (provider === 'ollama') {
setLlmBaseUrl('http://localhost:11434');
} else {
setLlmBaseUrl('');
}
}}
>
<MenuItem value="ollama">Ollama (Local)</MenuItem>
<MenuItem value="anthropic">Anthropic (Claude)</MenuItem>
<MenuItem value="openai">OpenAI (GPT-4)</MenuItem>
</Select>
</FormControl>
removeOptimisticRobot(tempRobotId); <FormControl sx={{ flex: 1 }}>
<InputLabel>Model</InputLabel>
<Select
value={llmModel}
label="Model"
onChange={(e) => setLlmModel(e.target.value)}
>
{llmProvider === 'ollama' && (
<>
<MenuItem value="">Default (llama3.2-vision)</MenuItem>
<MenuItem value="llama3.2-vision">llama3.2-vision</MenuItem>
<MenuItem value="llama3.2">llama3.2</MenuItem>
</>
)}
{llmProvider === 'anthropic' && (
<>
<MenuItem value="">Default (claude-3-5-sonnet)</MenuItem>
<MenuItem value="claude-3-5-sonnet-20241022">claude-3-5-sonnet-20241022</MenuItem>
<MenuItem value="claude-3-opus-20240229">claude-3-opus-20240229</MenuItem>
</>
)}
{llmProvider === 'openai' && (
<>
<MenuItem value="">Default (gpt-4-vision-preview)</MenuItem>
<MenuItem value="gpt-4-vision-preview">gpt-4-vision-preview</MenuItem>
<MenuItem value="gpt-4o">gpt-4o</MenuItem>
</>
)}
</Select>
</FormControl>
</Box>
if (!result || !result.robot) { {/* API Key for non-Ollama providers */}
notify('error', 'Failed to create AI robot. Please check your LLM configuration.'); {llmProvider !== 'ollama' && (
invalidateRecordings(); <Box sx={{ mb: 3 }}>
return; <TextField
} placeholder={`${llmProvider === 'anthropic' ? 'Anthropic' : 'OpenAI'} API Key`}
variant="outlined"
fullWidth
type="password"
value={llmApiKey}
onChange={(e) => setLlmApiKey(e.target.value)}
label="API Key (Optional if set in .env)"
/>
</Box>
)}
const robotMetaId = result.robot.recording_meta.id; {/* Ollama Base URL */}
notify('success', `${result.robot.recording_meta.name} created successfully!`); {llmProvider === 'ollama' && (
<Box sx={{ mb: 3 }}>
<TextField
placeholder="http://localhost:11434"
variant="outlined"
fullWidth
value={llmBaseUrl}
onChange={(e) => setLlmBaseUrl(e.target.value)}
label="Ollama Base URL (Optional)"
/>
</Box>
)}
invalidateRecordings(); {/* Create & Run Button */}
<Button
variant="contained"
fullWidth
onClick={async () => {
if (!url.trim()) {
notify('error', 'Please enter a valid URL');
return;
}
if (!extractRobotName.trim()) {
notify('error', 'Please enter a robot name');
return;
}
if (!aiPrompt.trim()) {
notify('error', 'Please enter an extraction prompt');
return;
}
await new Promise(resolve => setTimeout(resolve, 500)); const tempRobotId = `temp-${Date.now()}`;
const robotDisplayName = extractRobotName;
notify('info', 'Starting robot execution...'); const optimisticRobot = {
const runResponse = await createAndRunRecording(robotMetaId, { id: tempRobotId,
maxConcurrency: 1, recording_meta: {
maxRepeats: 1, id: tempRobotId,
debug: true name: robotDisplayName,
}); createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
pairs: 0,
params: [],
type: 'extract',
url: url,
},
recording: { workflow: [] },
isLoading: true,
isOptimistic: true
};
if (runResponse && runResponse.runId) { addOptimisticRobot(optimisticRobot);
notify('success', 'Robot is now running!');
navigate(`/runs/${robotMetaId}/run/${runResponse.runId}`); notify('info', `Robot ${robotDisplayName} creation started`);
} else { navigate('/robots');
notify('warning', 'Robot created but failed to start execution. You can run it manually from the robots page.');
} try {
} catch (error: any) { const result = await createLLMRobot(
console.error('Error in AI robot creation:', error); url,
removeOptimisticRobot(tempRobotId); aiPrompt,
invalidateRecordings(); llmProvider,
notify('error', error?.message || 'Failed to create and run AI robot'); llmModel || undefined,
} llmApiKey || undefined,
}} llmBaseUrl || undefined,
disabled={!url.trim() || !aiPrompt.trim() || isLoading} extractRobotName
sx={{ );
bgcolor: '#ff00c3',
py: 1.4, removeOptimisticRobot(tempRobotId);
fontSize: '1rem',
textTransform: 'none', if (!result || !result.robot) {
maxWidth: 700, notify('error', 'Failed to create AI robot. Please check your LLM configuration.');
borderRadius: 2 invalidateRecordings();
}} return;
startIcon={isLoading ? <CircularProgress size={20} color="inherit" /> : null} }
>
{isLoading ? 'Creating & Running...' : 'Create & Run AI Robot'} const robotMetaId = result.robot.recording_meta.id;
</Button> const robotName = result.robot.recording_meta.name;
invalidateRecordings();
notify('success', `${robotName} created successfully!`);
const optimisticRun = {
id: robotMetaId,
runId: `temp-${Date.now()}`,
status: 'running',
name: robotName,
startedAt: new Date().toISOString(),
finishedAt: '',
robotMetaId: robotMetaId,
log: 'Starting...',
isOptimistic: true
};
addOptimisticRun(optimisticRun);
const runResponse = await createAndRunRecording(robotMetaId, {
maxConcurrency: 1,
maxRepeats: 1,
debug: false
});
invalidateRuns();
if (runResponse && runResponse.runId) {
await new Promise(resolve => setTimeout(resolve, 300));
navigate(`/runs/${robotMetaId}/run/${runResponse.runId}`);
notify('info', `Run started: ${robotName}`);
} else {
notify('warning', 'Robot created but failed to start execution.');
navigate('/robots');
}
} catch (error: any) {
console.error('Error in AI robot creation:', error);
removeOptimisticRobot(tempRobotId);
invalidateRecordings();
notify('error', error?.message || 'Failed to create and run AI robot');
}
}}
disabled={!url.trim() || !extractRobotName.trim() || !aiPrompt.trim() || isLoading}
sx={{
bgcolor: '#ff00c3',
py: 1.4,
fontSize: '1rem',
textTransform: 'none',
borderRadius: 2
}}
startIcon={isLoading ? <CircularProgress size={20} color="inherit" /> : null}
>
{isLoading ? 'Creating & Running...' : 'Create & Run Robot'}
</Button>
</Box>
)}
{generationMode === 'recorder' && (
<Box sx={{ width: '100%', maxWidth: 700 }}>
<Button
variant="contained"
fullWidth
onClick={handleStartRecording}
disabled={!url.trim() || isLoading}
sx={{
bgcolor: '#ff00c3',
py: 1.4,
fontSize: '1rem',
textTransform: 'none',
borderRadius: 2
}}
startIcon={isLoading ? <CircularProgress size={20} color="inherit" /> : null}
>
{isLoading ? 'Starting...' : 'Start Recording'}
</Button>
</Box>
)}
</Box> </Box>
</Card> </Card>
</TabPanel> </TabPanel>
<TabPanel value={tabValue} index={2}> <TabPanel value={tabValue} index={1}>
<Card sx={{ mb: 4, p: 4, textAlign: 'center' }}> <Card sx={{ mb: 4, p: 4, textAlign: 'center' }}>
<Box display="flex" flexDirection="column" alignItems="center"> <Box display="flex" flexDirection="column" alignItems="center">
<img <img

View File

@@ -1,5 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState, useRef } from "react";
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import Paper from '@mui/material/Paper'; import Paper from '@mui/material/Paper';
import Table from '@mui/material/Table'; import Table from '@mui/material/Table';
@@ -13,10 +13,12 @@ import { Accordion, AccordionSummary, AccordionDetails, Typography, Box, TextFie
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import SearchIcon from '@mui/icons-material/Search'; import SearchIcon from '@mui/icons-material/Search';
import { useLocation, useNavigate } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom';
import { useGlobalInfoStore, useCachedRuns } from "../../context/globalInfo"; import { useGlobalInfoStore, useCachedRuns, useCacheInvalidation } from "../../context/globalInfo";
import { RunSettings } from "./RunSettings"; import { RunSettings } from "./RunSettings";
import { CollapsibleRow } from "./ColapsibleRow"; import { CollapsibleRow } from "./ColapsibleRow";
import { ArrowDownward, ArrowUpward, UnfoldMore } from '@mui/icons-material'; import { ArrowDownward, ArrowUpward, UnfoldMore } from '@mui/icons-material';
import { io, Socket } from 'socket.io-client';
import { apiUrl } from '../../apiConfig';
export const columns: readonly Column[] = [ export const columns: readonly Column[] = [
{ id: 'runStatus', label: 'Status', minWidth: 80 }, { id: 'runStatus', label: 'Status', minWidth: 80 },
@@ -133,6 +135,9 @@ export const RunsTable: React.FC<RunsTableProps> = ({
const { notify, rerenderRuns, setRerenderRuns } = useGlobalInfoStore(); const { notify, rerenderRuns, setRerenderRuns } = useGlobalInfoStore();
const { data: rows = [], isLoading: isFetching, error, refetch } = useCachedRuns(); const { data: rows = [], isLoading: isFetching, error, refetch } = useCachedRuns();
const { invalidateRuns } = useCacheInvalidation();
const activeSocketsRef = useRef<Map<string, Socket>>(new Map());
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [paginationStates, setPaginationStates] = useState<PaginationState>({}); const [paginationStates, setPaginationStates] = useState<PaginationState>({});
@@ -285,6 +290,98 @@ export const RunsTable: React.FC<RunsTableProps> = ({
} }
}, [rerenderRuns, refetch, setRerenderRuns]); }, [rerenderRuns, refetch, setRerenderRuns]);
useEffect(() => {
if (!rows || rows.length === 0) return;
const activeRuns = rows.filter((row: Data) =>
row.status === 'running' && row.browserId && row.browserId.trim() !== ''
);
activeRuns.forEach((run: Data) => {
const { browserId, runId: currentRunId, name } = run;
if (activeSocketsRef.current.has(browserId)) {
return;
}
console.log(`[RunsTable] Connecting to browser socket: ${browserId} for run: ${currentRunId}`);
try {
const socket = io(`${apiUrl}/${browserId}`, {
transports: ['websocket'],
rejectUnauthorized: false
});
socket.on('connect', () => {
console.log(`[RunsTable] Connected to browser ${browserId}`);
});
socket.on('debugMessage', (msg: string) => {
console.log(`[RunsTable] Debug message for ${browserId}:`, msg);
// Optionally update logs in real-time here
});
socket.on('run-completed', (data: any) => {
console.log(`[RunsTable] Run completed for ${browserId}:`, data);
// Invalidate cache to show updated run status
invalidateRuns();
setRerenderRuns(true);
// Show notification
if (data.status === 'success') {
notify('success', t('main_page.notifications.interpretation_success', { name: data.robotName || name }));
} else {
notify('error', t('main_page.notifications.interpretation_failed', { name: data.robotName || name }));
}
socket.disconnect();
activeSocketsRef.current.delete(browserId);
});
socket.on('urlChanged', (url: string) => {
console.log(`[RunsTable] URL changed for ${browserId}:`, url);
});
socket.on('dom-snapshot-loading', () => {
console.log(`[RunsTable] DOM snapshot loading for ${browserId}`);
});
socket.on('connect_error', (error: Error) => {
console.error(`[RunsTable] Connection error for browser ${browserId}:`, error.message);
});
socket.on('disconnect', (reason: string) => {
console.log(`[RunsTable] Disconnected from browser ${browserId}:`, reason);
activeSocketsRef.current.delete(browserId);
});
activeSocketsRef.current.set(browserId, socket);
} catch (error) {
console.error(`[RunsTable] Error connecting to browser ${browserId}:`, error);
}
});
// Disconnect from sockets for runs that are no longer active
const activeBrowserIds = new Set(activeRuns.map((run: Data) => run.browserId));
activeSocketsRef.current.forEach((socket, browserId) => {
if (!activeBrowserIds.has(browserId)) {
console.log(`[RunsTable] Disconnecting from inactive browser: ${browserId}`);
socket.disconnect();
activeSocketsRef.current.delete(browserId);
}
});
// Cleanup on unmount
return () => {
console.log('[RunsTable] Cleaning up all socket connections');
activeSocketsRef.current.forEach((socket) => {
socket.disconnect();
});
activeSocketsRef.current.clear();
};
}, [rows, notify, t, invalidateRuns, setRerenderRuns]);
const handleDelete = useCallback(() => { const handleDelete = useCallback(() => {
notify('success', t('runstable.notifications.delete_success')); notify('success', t('runstable.notifications.delete_success'));
refetch(); refetch();

View File

@@ -204,31 +204,47 @@ export const useCachedRuns = () => {
export const useCacheInvalidation = () => { export const useCacheInvalidation = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const invalidateRuns = () => { const invalidateRuns = () => {
queryClient.invalidateQueries({ queryKey: dataCacheKeys.runs }); queryClient.invalidateQueries({ queryKey: dataCacheKeys.runs });
}; };
const invalidateRecordings = () => { const invalidateRecordings = () => {
queryClient.invalidateQueries({ queryKey: dataCacheKeys.recordings }); queryClient.invalidateQueries({ queryKey: dataCacheKeys.recordings });
}; };
const addOptimisticRun = (newRun: any) => { const addOptimisticRun = (newRun: any) => {
queryClient.setQueryData(dataCacheKeys.runs, (oldData: any) => { queryClient.setQueryData(dataCacheKeys.runs, (oldData: any) => {
if (!oldData) return [{ id: 0, ...newRun }]; if (!oldData) return [{ id: 0, ...newRun }];
return [{ id: oldData.length, ...newRun }, ...oldData]; return [{ id: oldData.length, ...newRun }, ...oldData];
}); });
}; };
const addOptimisticRobot = (newRobot: any) => {
queryClient.setQueryData(dataCacheKeys.recordings, (oldData: any) => {
if (!oldData) return [newRobot];
return [newRobot, ...oldData];
});
};
const removeOptimisticRobot = (tempId: string) => {
queryClient.setQueryData(dataCacheKeys.recordings, (oldData: any) => {
if (!oldData) return [];
return oldData.filter((robot: any) => robot.id !== tempId);
});
};
const invalidateAllCache = () => { const invalidateAllCache = () => {
invalidateRuns(); invalidateRuns();
invalidateRecordings(); invalidateRecordings();
}; };
return { return {
invalidateRuns, invalidateRuns,
invalidateRecordings, invalidateRecordings,
addOptimisticRun, addOptimisticRun,
addOptimisticRobot,
removeOptimisticRobot,
invalidateAllCache invalidateAllCache
}; };
}; };

View File

@@ -53,6 +53,10 @@ export const MainPage = ({ handleEditRecording, initialContent }: MainPageProps)
const { invalidateRuns, addOptimisticRun } = useCacheInvalidation(); const { invalidateRuns, addOptimisticRun } = useCacheInvalidation();
const navigate = useNavigate(); const navigate = useNavigate();
React.useEffect(() => {
setContent(initialContent);
}, [initialContent]);
const { state } = useContext(AuthContext); const { state } = useContext(AuthContext);
const { user } = state; const { user } = state;