diff --git a/server/src/routes/storage.ts b/server/src/routes/storage.ts index 7da96179..7eb0dd79 100644 --- a/server/src/routes/storage.ts +++ b/server/src/routes/storage.ts @@ -542,12 +542,12 @@ router.post('/recordings/llm', requireSignIn, async (req: AuthenticatedRequest, 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', model: llmModel, apiKey: llmApiKey, baseUrl: llmBaseUrl - }, req.user.id); + }); if (!workflowResult.success || !workflowResult.workflow) { logger.log('error', `Failed to generate workflow: ${JSON.stringify(workflowResult.errors)}`); @@ -1321,4 +1321,4 @@ export async function recoverOrphanedRuns() { } } -export { processQueuedRuns }; +export { processQueuedRuns }; \ No newline at end of file diff --git a/src/components/robot/pages/RobotCreate.tsx b/src/components/robot/pages/RobotCreate.tsx index a91a7314..4aacfc7c 100644 --- a/src/components/robot/pages/RobotCreate.tsx +++ b/src/components/robot/pages/RobotCreate.tsx @@ -21,7 +21,7 @@ import { MenuItem, InputLabel } 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 { canCreateBrowserInState, getActiveBrowserId, stopRecording } from '../../../api/recording'; import { createScrapeRobot, createLLMRobot, createAndRunRecording } from "../../../api/storage"; @@ -59,11 +59,13 @@ const RobotCreate: React.FC = () => { const [tabValue, setTabValue] = useState(0); const [url, setUrl] = useState(''); const [scrapeRobotName, setScrapeRobotName] = useState(''); + const [extractRobotName, setExtractRobotName] = useState(''); const [needsLogin, setNeedsLogin] = useState(false); const [isLoading, setIsLoading] = useState(false); const [isWarningModalOpen, setWarningModalOpen] = useState(false); const [activeBrowserId, setActiveBrowserId] = useState(''); const [outputFormats, setOutputFormats] = useState([]); + const [generationMode, setGenerationMode] = useState<'agent' | 'recorder' | null>(null); // AI Extract tab state const [aiPrompt, setAiPrompt] = useState(''); @@ -75,7 +77,7 @@ const RobotCreate: React.FC = () => { const { state } = React.useContext(AuthContext); const { user } = state; - const { addOptimisticRobot, removeOptimisticRobot, invalidateRecordings } = useCacheInvalidation(); + const { addOptimisticRobot, removeOptimisticRobot, invalidateRecordings, invalidateRuns, addOptimisticRun } = useCacheInvalidation(); const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { setTabValue(newValue); @@ -215,190 +217,16 @@ const RobotCreate: React.FC = () => { }} > - - + - {/* Logo (kept as original) */} - Maxun Logo - - - Extract structured data from websites in a few clicks. - - - {/* Origin URL Input */} - - setUrl(e.target.value)} - /> - - - {/* Checkbox */} - - setNeedsLogin(e.target.checked)} - color="primary" - /> - } - label="This website needs logging in." - /> - - - {/* Button */} - - - - - - - First time creating a robot? - - - Get help and learn how to use Maxun effectively. - - - - - {/* YouTube Tutorials */} - - window.open("https://www.youtube.com/@MaxunOSS/videos", "_blank")} - > - - theme.palette.mode === 'light' ? 'rgba(0, 0, 0, 0.54)' : '', - }} - > - - - - - Video Tutorials - - - Watch step-by-step guides - - - - - - - {/* Documentation */} - - window.open("https://docs.maxun.dev", "_blank")} - > - - theme.palette.mode === 'light' ? 'rgba(0, 0, 0, 0.54)' : '', - }} - > -
- - - - Documentation - - - Explore detailed guides - - - - - - - - - - - - - Maxun Logo - - - AI-powered extraction: Describe what you want to extract in natural language. - - - - setAiRobotName(e.target.value)} - sx={{ mb: 2 }} - label="Robot Name (Optional)" - /> + {/* Website URL */} + { value={url} onChange={(e) => setUrl(e.target.value)} label="Website URL" - sx={{ mb: 2 }} /> - setAiPrompt(e.target.value)} - label="Extraction Prompt" - sx={{ mb: 2 }} - /> - - - - LLM Provider - - - - - Model (Optional) - - - - - {llmProvider !== 'ollama' && ( - setLlmApiKey(e.target.value)} - label="API Key (Optional if set in .env)" - sx={{ mb: 2 }} - /> - )} - - {llmProvider === 'ollama' && ( - setLlmBaseUrl(e.target.value)} - label="Ollama Base URL (Optional)" - sx={{ mb: 2 }} - /> - )} - + addOptimisticRobot(optimisticRobot); + + notify('info', `Robot ${robotDisplayName} creation started`); + navigate('/robots'); + + try { + const result = await createLLMRobot( + url, + aiPrompt, + llmProvider, + llmModel || undefined, + llmApiKey || undefined, + llmBaseUrl || undefined, + extractRobotName + ); + + removeOptimisticRobot(tempRobotId); + + if (!result || !result.robot) { + notify('error', 'Failed to create AI robot. Please check your LLM configuration.'); + invalidateRecordings(); + return; + } + + const robotMetaId = result.robot.recording_meta.id; + 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 ? : null} + > + {isLoading ? 'Creating & Running...' : 'Create & Run Robot'} + + + )} + + {generationMode === 'recorder' && ( + + + + )} - + = ({ const { notify, rerenderRuns, setRerenderRuns } = useGlobalInfoStore(); const { data: rows = [], isLoading: isFetching, error, refetch } = useCachedRuns(); + const { invalidateRuns } = useCacheInvalidation(); + + const activeSocketsRef = useRef>(new Map()); const [searchTerm, setSearchTerm] = useState(''); const [paginationStates, setPaginationStates] = useState({}); @@ -285,6 +290,98 @@ export const RunsTable: React.FC = ({ } }, [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(() => { notify('success', t('runstable.notifications.delete_success')); refetch(); diff --git a/src/context/globalInfo.tsx b/src/context/globalInfo.tsx index 3dddab1c..c5bbc044 100644 --- a/src/context/globalInfo.tsx +++ b/src/context/globalInfo.tsx @@ -204,31 +204,47 @@ export const useCachedRuns = () => { export const useCacheInvalidation = () => { const queryClient = useQueryClient(); - + const invalidateRuns = () => { queryClient.invalidateQueries({ queryKey: dataCacheKeys.runs }); }; - + const invalidateRecordings = () => { queryClient.invalidateQueries({ queryKey: dataCacheKeys.recordings }); }; - + const addOptimisticRun = (newRun: any) => { queryClient.setQueryData(dataCacheKeys.runs, (oldData: any) => { if (!oldData) return [{ id: 0, ...newRun }]; 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 = () => { invalidateRuns(); invalidateRecordings(); }; - + return { invalidateRuns, - invalidateRecordings, + invalidateRecordings, addOptimisticRun, + addOptimisticRobot, + removeOptimisticRobot, invalidateAllCache }; }; diff --git a/src/pages/MainPage.tsx b/src/pages/MainPage.tsx index 4f302135..004785f7 100644 --- a/src/pages/MainPage.tsx +++ b/src/pages/MainPage.tsx @@ -53,6 +53,10 @@ export const MainPage = ({ handleEditRecording, initialContent }: MainPageProps) const { invalidateRuns, addOptimisticRun } = useCacheInvalidation(); const navigate = useNavigate(); + React.useEffect(() => { + setContent(initialContent); + }, [initialContent]); + const { state } = useContext(AuthContext); const { user } = state;