784 lines
30 KiB
TypeScript
784 lines
30 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { useTranslation } from 'react-i18next';
|
|
import {
|
|
Box,
|
|
Typography,
|
|
TextField,
|
|
Button,
|
|
Checkbox,
|
|
IconButton,
|
|
Card,
|
|
CircularProgress,
|
|
Container,
|
|
CardContent,
|
|
Tabs,
|
|
Tab,
|
|
FormControl,
|
|
Select,
|
|
MenuItem,
|
|
InputLabel
|
|
} from '@mui/material';
|
|
import { ArrowBack, AutoAwesome, HighlightAlt } from '@mui/icons-material';
|
|
import { useGlobalInfoStore, useCacheInvalidation } from '../../../context/globalInfo';
|
|
import { canCreateBrowserInState, getActiveBrowserId, stopRecording } from '../../../api/recording';
|
|
import { createScrapeRobot, createLLMRobot, createAndRunRecording } from "../../../api/storage";
|
|
import { AuthContext } from '../../../context/auth';
|
|
import { GenericModal } from '../../ui/GenericModal';
|
|
|
|
|
|
interface TabPanelProps {
|
|
children?: React.ReactNode;
|
|
index: number;
|
|
value: number;
|
|
}
|
|
|
|
function TabPanel(props: TabPanelProps) {
|
|
const { children, value, index, ...other } = props;
|
|
|
|
return (
|
|
<div
|
|
role="tabpanel"
|
|
hidden={value !== index}
|
|
id={`robot-tabpanel-${index}`}
|
|
aria-labelledby={`robot-tab-${index}`}
|
|
{...other}
|
|
>
|
|
{value === index && <Box>{children}</Box>}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const RobotCreate: React.FC = () => {
|
|
const { t } = useTranslation();
|
|
const navigate = useNavigate();
|
|
const { setBrowserId, setRecordingUrl, notify, setRecordingId, setRerenderRobots } = useGlobalInfoStore();
|
|
|
|
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<string[]>([]);
|
|
const [generationMode, setGenerationMode] = useState<'agent' | 'recorder' | null>(null);
|
|
|
|
const [aiPrompt, setAiPrompt] = useState('');
|
|
const [llmProvider, setLlmProvider] = useState<'anthropic' | 'openai' | 'ollama'>('ollama');
|
|
const [llmModel, setLlmModel] = useState('default');
|
|
const [llmApiKey, setLlmApiKey] = useState('');
|
|
const [llmBaseUrl, setLlmBaseUrl] = useState('');
|
|
const [aiRobotName, setAiRobotName] = useState('');
|
|
|
|
const { state } = React.useContext(AuthContext);
|
|
const { user } = state;
|
|
const { addOptimisticRobot, removeOptimisticRobot, invalidateRecordings, invalidateRuns, addOptimisticRun } = useCacheInvalidation();
|
|
|
|
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
|
|
setTabValue(newValue);
|
|
};
|
|
|
|
|
|
const handleStartRecording = async () => {
|
|
if (!url.trim()) {
|
|
notify('error', 'Please enter a valid URL');
|
|
return;
|
|
}
|
|
|
|
setIsLoading(true);
|
|
|
|
try {
|
|
const canCreateRecording = await canCreateBrowserInState("recording");
|
|
|
|
if (!canCreateRecording) {
|
|
const activeBrowser = await getActiveBrowserId();
|
|
if (activeBrowser) {
|
|
setActiveBrowserId(activeBrowser);
|
|
setWarningModalOpen(true);
|
|
} else {
|
|
notify('warning', t('recordingtable.notifications.browser_limit_warning'));
|
|
}
|
|
setIsLoading(false);
|
|
return;
|
|
}
|
|
|
|
setBrowserId('new-recording');
|
|
setRecordingUrl(url);
|
|
|
|
window.sessionStorage.setItem('browserId', 'new-recording');
|
|
window.sessionStorage.setItem('recordingUrl', url);
|
|
window.sessionStorage.setItem('initialUrl', url);
|
|
window.sessionStorage.setItem('needsLogin', needsLogin.toString());
|
|
|
|
const sessionId = Date.now().toString();
|
|
window.sessionStorage.setItem('recordingSessionId', sessionId);
|
|
|
|
window.open(`/recording-setup?session=${sessionId}`, '_blank');
|
|
window.sessionStorage.setItem('nextTabIsRecording', 'true');
|
|
|
|
// Reset loading state immediately after opening new tab
|
|
setIsLoading(false);
|
|
navigate('/robots');
|
|
} catch (error) {
|
|
console.error('Error starting recording:', error);
|
|
notify('error', 'Failed to start recording. Please try again.');
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleDiscardAndCreate = async () => {
|
|
if (activeBrowserId) {
|
|
await stopRecording(activeBrowserId);
|
|
notify('warning', t('browser_recording.notifications.terminated'));
|
|
}
|
|
|
|
setWarningModalOpen(false);
|
|
setIsLoading(false);
|
|
|
|
// Continue with the original Recording logic
|
|
setBrowserId('new-recording');
|
|
setRecordingUrl(url);
|
|
|
|
window.sessionStorage.setItem('browserId', 'new-recording');
|
|
window.sessionStorage.setItem('recordingUrl', url);
|
|
window.sessionStorage.setItem('initialUrl', url);
|
|
window.sessionStorage.setItem('needsLogin', needsLogin.toString());
|
|
|
|
const sessionId = Date.now().toString();
|
|
window.sessionStorage.setItem('recordingSessionId', sessionId);
|
|
|
|
window.open(`/recording-setup?session=${sessionId}`, '_blank');
|
|
window.sessionStorage.setItem('nextTabIsRecording', 'true');
|
|
|
|
navigate('/robots');
|
|
};
|
|
|
|
return (
|
|
<Container maxWidth="md" sx={{ py: 4 }}>
|
|
<Box>
|
|
<Box display="flex" alignItems="center" mb={3}>
|
|
<IconButton
|
|
onClick={() => navigate('/robots')}
|
|
sx={{
|
|
ml: -1,
|
|
mr: 1,
|
|
color: theme => theme.palette.text.primary,
|
|
backgroundColor: 'transparent !important',
|
|
'&:hover': {
|
|
backgroundColor: 'transparent !important',
|
|
},
|
|
'&:active': {
|
|
backgroundColor: 'transparent !important',
|
|
},
|
|
'&:focus': {
|
|
backgroundColor: 'transparent !important',
|
|
},
|
|
'&:focus-visible': {
|
|
backgroundColor: 'transparent !important',
|
|
},
|
|
}}
|
|
disableRipple
|
|
aria-label="Go back"
|
|
>
|
|
<ArrowBack />
|
|
</IconButton>
|
|
<Typography variant="h5" component="h1">
|
|
Create New Robot
|
|
</Typography>
|
|
</Box>
|
|
|
|
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 2, mt: "-30px" }}>
|
|
<Tabs
|
|
value={tabValue}
|
|
centered
|
|
onChange={handleTabChange}
|
|
aria-label="robot type tabs"
|
|
sx={{
|
|
minHeight: 36,
|
|
'& .MuiTab-root': {
|
|
minHeight: 36,
|
|
paddingX: 2,
|
|
paddingY: 1.5,
|
|
minWidth: 0,
|
|
},
|
|
'& .MuiTabs-indicator': {
|
|
height: 2,
|
|
},
|
|
}}
|
|
>
|
|
<Tab label="Extract" id="extract-robot" aria-controls="extract-robot" />
|
|
<Tab label="Scrape" id="scrape-robot" aria-controls="scrape-robot" />
|
|
</Tabs>
|
|
</Box>
|
|
|
|
<TabPanel value={tabValue} index={0}>
|
|
<Card sx={{ mb: 4, p: 4 }}>
|
|
<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}>
|
|
Extract structured data from websites using AI or record your own extraction workflow.
|
|
</Typography>
|
|
<Box sx={{ width: '100%', maxWidth: 700, mb: 3 }}>
|
|
<TextField
|
|
placeholder="Example: https://www.ycombinator.com/companies/"
|
|
variant="outlined"
|
|
fullWidth
|
|
value={url}
|
|
onChange={(e) => setUrl(e.target.value)}
|
|
label="Website URL"
|
|
/>
|
|
</Box>
|
|
|
|
<Box sx={{ width: '100%', maxWidth: 700, mb: 3 }}>
|
|
<Typography variant="subtitle1" gutterBottom sx={{ mb: 2 }} color="text.secondary">
|
|
Choose How to Build
|
|
</Typography>
|
|
|
|
<Box sx={{ display: 'flex', gap: 2 }}>
|
|
<Card
|
|
onClick={() => setGenerationMode('recorder')}
|
|
sx={{
|
|
flex: 1,
|
|
cursor: 'pointer',
|
|
border: '2px solid',
|
|
borderColor: generationMode === 'recorder' ? '#ff00c3' : 'divider',
|
|
transition: 'all 0.2s',
|
|
'&:hover': {
|
|
borderColor: '#ff00c3',
|
|
}
|
|
}}
|
|
>
|
|
<CardContent sx={{ textAlign: 'center', py: 3, color:"text.secondary" }}>
|
|
<HighlightAlt sx={{ fontSize: 32, mb: 1 }} />
|
|
<Typography variant="h6" gutterBottom>
|
|
Recorder Mode
|
|
</Typography>
|
|
<Typography variant="body2">
|
|
Record your actions into a workflow.
|
|
</Typography>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card
|
|
onClick={() => setGenerationMode('agent')}
|
|
sx={{
|
|
flex: 1,
|
|
cursor: 'pointer',
|
|
border: '2px solid',
|
|
borderColor: generationMode === 'agent' ? '#ff00c3' : 'divider',
|
|
transition: 'all 0.2s',
|
|
'&:hover': {
|
|
borderColor: '#ff00c3',
|
|
},
|
|
position: 'relative'
|
|
}}
|
|
>
|
|
<Box
|
|
sx={{
|
|
position: 'absolute',
|
|
top: 8,
|
|
right: 8,
|
|
background: '#ff00c3',
|
|
color: '#fff',
|
|
px: 1,
|
|
py: 0.3,
|
|
borderRadius: '10px',
|
|
fontSize: '0.7rem',
|
|
}}
|
|
>
|
|
Beta
|
|
</Box>
|
|
|
|
<CardContent sx={{ textAlign: 'center', py: 3, color:"text.secondary" }}>
|
|
<AutoAwesome sx={{ fontSize: 32, mb: 1 }} />
|
|
<Typography variant="h6" gutterBottom>
|
|
AI Mode
|
|
</Typography>
|
|
<Typography variant="body2">
|
|
Describe the task. It builds it for you.
|
|
</Typography>
|
|
</CardContent>
|
|
</Card>
|
|
</Box>
|
|
</Box>
|
|
{generationMode === 'agent' && (
|
|
<Box sx={{ width: '100%', maxWidth: 700 }}>
|
|
<Box sx={{ mb: 3 }}>
|
|
<TextField
|
|
placeholder="Robot Name"
|
|
variant="outlined"
|
|
fullWidth
|
|
value={extractRobotName}
|
|
onChange={(e) => setExtractRobotName(e.target.value)}
|
|
label="Robot Name"
|
|
/>
|
|
</Box>
|
|
|
|
<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>
|
|
|
|
<Box sx={{ display: 'flex', gap: 2, mb: 3 }}>
|
|
<FormControl sx={{ flex: 1 }}>
|
|
<InputLabel>LLM Provider</InputLabel>
|
|
<Select
|
|
value={llmProvider}
|
|
label="LLM Provider"
|
|
onChange={(e) => {
|
|
const provider = e.target.value as 'anthropic' | 'openai' | 'ollama';
|
|
setLlmProvider(provider);
|
|
setLlmModel('default');
|
|
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>Model</InputLabel>
|
|
<Select
|
|
value={llmModel}
|
|
label="Model"
|
|
onChange={(e) => setLlmModel(e.target.value)}
|
|
>
|
|
{llmProvider === 'ollama' ? (
|
|
[
|
|
<MenuItem key="default" value="default">Default (llama3.2-vision)</MenuItem>,
|
|
<MenuItem key="llama3.2-vision" value="llama3.2-vision">llama3.2-vision</MenuItem>,
|
|
<MenuItem key="llama3.2" value="llama3.2">llama3.2</MenuItem>
|
|
]
|
|
) : llmProvider === 'anthropic' ? (
|
|
[
|
|
<MenuItem key="default" value="default">Default (claude-3-5-sonnet)</MenuItem>,
|
|
<MenuItem key="claude-3-5-sonnet-20241022" value="claude-3-5-sonnet-20241022">claude-3-5-sonnet-20241022</MenuItem>,
|
|
<MenuItem key="claude-3-opus-20240229" value="claude-3-opus-20240229">claude-3-opus-20240229</MenuItem>
|
|
]
|
|
) : (
|
|
[
|
|
<MenuItem key="default" value="default">Default (gpt-4-vision-preview)</MenuItem>,
|
|
<MenuItem key="gpt-4-vision-preview" value="gpt-4-vision-preview">gpt-4-vision-preview</MenuItem>,
|
|
<MenuItem key="gpt-4o" value="gpt-4o">gpt-4o</MenuItem>
|
|
]
|
|
)}
|
|
</Select>
|
|
</FormControl>
|
|
</Box>
|
|
|
|
{/* API Key for non-Ollama providers */}
|
|
{llmProvider !== 'ollama' && (
|
|
<Box sx={{ mb: 3 }}>
|
|
<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>
|
|
)}
|
|
|
|
{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>
|
|
)}
|
|
|
|
<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;
|
|
}
|
|
|
|
const tempRobotId = `temp-${Date.now()}`;
|
|
const robotDisplayName = extractRobotName;
|
|
|
|
const optimisticRobot = {
|
|
id: tempRobotId,
|
|
recording_meta: {
|
|
id: tempRobotId,
|
|
name: robotDisplayName,
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
pairs: 0,
|
|
params: [],
|
|
type: 'extract',
|
|
url: url,
|
|
},
|
|
recording: { workflow: [] },
|
|
isLoading: true,
|
|
isOptimistic: true
|
|
};
|
|
|
|
addOptimisticRobot(optimisticRobot);
|
|
|
|
notify('info', `Robot ${robotDisplayName} creation started`);
|
|
navigate('/robots');
|
|
|
|
try {
|
|
const result = await createLLMRobot(
|
|
url,
|
|
aiPrompt,
|
|
llmProvider,
|
|
llmModel === 'default' ? undefined : llmModel,
|
|
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 ? <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>
|
|
</Card>
|
|
</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}>
|
|
Turn websites into LLM-ready Markdown, clean HTML, or screenshots for AI apps.
|
|
</Typography>
|
|
|
|
<Box sx={{ width: '100%', maxWidth: 700, mb: 2 }}>
|
|
<TextField
|
|
placeholder="Example: YC Companies Scraper"
|
|
variant="outlined"
|
|
fullWidth
|
|
value={scrapeRobotName}
|
|
onChange={(e) => setScrapeRobotName(e.target.value)}
|
|
sx={{ mb: 2 }}
|
|
label="Robot Name"
|
|
/>
|
|
<TextField
|
|
placeholder="Example: https://www.ycombinator.com/companies/"
|
|
variant="outlined"
|
|
fullWidth
|
|
value={url}
|
|
onChange={(e) => setUrl(e.target.value)}
|
|
label="Website URL"
|
|
sx={{ mb: 2 }}
|
|
/>
|
|
|
|
<Box sx={{ width: '100%', display: 'flex', justifyContent: 'flex-start' }}>
|
|
<FormControl sx={{ mb: 2, width: '300px' }}>
|
|
<InputLabel id="output-formats-label">Output Formats *</InputLabel>
|
|
<Select
|
|
labelId="output-formats-label"
|
|
id="output-formats"
|
|
multiple
|
|
value={outputFormats}
|
|
label="Output Formats *"
|
|
onChange={(e) => {
|
|
const value =
|
|
typeof e.target.value === 'string'
|
|
? e.target.value.split(',')
|
|
: e.target.value;
|
|
setOutputFormats(value);
|
|
}}
|
|
renderValue={(selected) => {
|
|
if (selected.length === 0) {
|
|
return <em style={{ color: '#999' }}>Select formats</em>;
|
|
}
|
|
|
|
const OUTPUT_FORMAT_LABELS: Record<string, string> = {
|
|
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 (
|
|
<Box
|
|
sx={{
|
|
whiteSpace: 'nowrap',
|
|
overflow: 'hidden',
|
|
textOverflow: 'ellipsis',
|
|
}}
|
|
>
|
|
{display}
|
|
</Box>
|
|
);
|
|
}}
|
|
MenuProps={{
|
|
PaperProps: {
|
|
style: {
|
|
maxHeight: 300,
|
|
},
|
|
},
|
|
}}
|
|
>
|
|
<MenuItem value="markdown">
|
|
<Checkbox checked={outputFormats.includes('markdown')} />
|
|
Markdown
|
|
</MenuItem>
|
|
<MenuItem value="html">
|
|
<Checkbox checked={outputFormats.includes('html')} />
|
|
HTML
|
|
</MenuItem>
|
|
<MenuItem value="screenshot-visible">
|
|
<Checkbox checked={outputFormats.includes('screenshot-visible')} />
|
|
Screenshot - Visible Viewport
|
|
</MenuItem>
|
|
<MenuItem value="screenshot-fullpage">
|
|
<Checkbox checked={outputFormats.includes('screenshot-fullpage')} />
|
|
Screenshot - Full Page
|
|
</MenuItem>
|
|
</Select>
|
|
</FormControl>
|
|
</Box>
|
|
</Box>
|
|
|
|
<Button
|
|
variant="contained"
|
|
fullWidth
|
|
onClick={async () => {
|
|
if (!url.trim()) {
|
|
notify('error', 'Please enter a valid URL');
|
|
return;
|
|
}
|
|
if (!scrapeRobotName.trim()) {
|
|
notify('error', 'Please enter a robot name');
|
|
return;
|
|
}
|
|
if (outputFormats.length === 0) {
|
|
notify('error', 'Please select at least one output format');
|
|
return;
|
|
}
|
|
|
|
setIsLoading(true);
|
|
const result = await createScrapeRobot(url, scrapeRobotName, outputFormats);
|
|
setIsLoading(false);
|
|
|
|
if (result) {
|
|
setRerenderRobots(true);
|
|
notify('success', `${scrapeRobotName} created successfully!`);
|
|
navigate('/robots');
|
|
} else {
|
|
notify('error', 'Failed to create scrape robot');
|
|
}
|
|
}}
|
|
disabled={!url.trim() || !scrapeRobotName.trim() || outputFormats.length === 0 || isLoading}
|
|
sx={{
|
|
bgcolor: '#ff00c3',
|
|
py: 1.4,
|
|
fontSize: '1rem',
|
|
textTransform: 'none',
|
|
maxWidth: 700,
|
|
borderRadius: 2
|
|
}}
|
|
startIcon={isLoading ? <CircularProgress size={20} color="inherit" /> : null}
|
|
>
|
|
{isLoading
|
|
? "Creating..."
|
|
: `Create Robot`
|
|
}
|
|
</Button>
|
|
</Box>
|
|
</Card>
|
|
</TabPanel>
|
|
</Box>
|
|
|
|
|
|
<GenericModal isOpen={isWarningModalOpen} onClose={() => {
|
|
setWarningModalOpen(false);
|
|
setIsLoading(false);
|
|
}} modalStyle={modalStyle}>
|
|
<div style={{ padding: '10px' }}>
|
|
<Typography variant="h6" gutterBottom>{t('recordingtable.warning_modal.title')}</Typography>
|
|
<Typography variant="body1" style={{ marginBottom: '20px' }}>
|
|
{t('recordingtable.warning_modal.message')}
|
|
</Typography>
|
|
|
|
<Box display="flex" justifyContent="space-between" mt={2}>
|
|
<Button
|
|
onClick={handleDiscardAndCreate}
|
|
variant="contained"
|
|
color="error"
|
|
>
|
|
{t('recordingtable.warning_modal.discard_and_create')}
|
|
</Button>
|
|
<Button
|
|
onClick={() => {
|
|
setWarningModalOpen(false);
|
|
setIsLoading(false);
|
|
}}
|
|
variant="outlined"
|
|
>
|
|
{t('recordingtable.warning_modal.cancel')}
|
|
</Button>
|
|
</Box>
|
|
</div>
|
|
</GenericModal>
|
|
|
|
|
|
</Container>
|
|
);
|
|
};
|
|
|
|
export default RobotCreate;
|
|
|
|
const modalStyle = {
|
|
top: '50%',
|
|
left: '50%',
|
|
transform: 'translate(-50%, -50%)',
|
|
width: '30%',
|
|
backgroundColor: 'background.paper',
|
|
p: 4,
|
|
height: 'fit-content',
|
|
display: 'block',
|
|
padding: '20px',
|
|
};
|