Merge branch 'develop' into crawl-search

This commit is contained in:
Rohit
2026-01-04 18:21:47 +05:30
committed by GitHub
17 changed files with 462 additions and 53 deletions

View File

@@ -34,6 +34,7 @@ const ApiKeyManager = () => {
const { t } = useTranslation();
const [apiKey, setApiKey] = useState<string | null>(null);
const [apiKeyName, setApiKeyName] = useState<string>(t('apikey.default_name'));
const [apiKeyCreatedAt, setApiKeyCreatedAt] = useState<string | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [showKey, setShowKey] = useState<boolean>(false);
const [copySuccess, setCopySuccess] = useState<boolean>(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 }));
@@ -128,12 +131,13 @@ const ApiKeyManager = () => {
</Typography>
{apiKey ? (
<TableContainer component={Paper} sx={{ width: '100%', overflow: 'hidden' }}>
<Table>
<Table sx={{ tableLayout: 'fixed', width: '100%' }}>
<TableHead>
<TableRow>
<TableCell>{t('apikey.table.name')}</TableCell>
<TableCell>{t('apikey.table.key')}</TableCell>
<TableCell>{t('apikey.table.actions')}</TableCell>
{apiKeyCreatedAt && <TableCell>Created On</TableCell>}
<TableCell align="center" sx={{ width: 160 }}>{t('apikey.table.actions')}</TableCell>
</TableRow>
</TableHead>
<TableBody>
@@ -144,7 +148,16 @@ const ApiKeyManager = () => {
{showKey ? `${apiKey?.substring(0, 10)}...` : '**********'}
</Box>
</TableCell>
<TableCell>
{apiKeyCreatedAt && (
<TableCell>
{new Date(apiKeyCreatedAt).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
})}
</TableCell>
)}
<TableCell align="right" sx={{ width: 160 }}>
<Tooltip title={t('apikey.actions.copy')}>
<IconButton onClick={copyToClipboard}>
<ContentCopy />

View File

@@ -113,7 +113,7 @@ export const NavBar: React.FC<NavBarProps> = ({
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) {

View File

@@ -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";
@@ -116,7 +115,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 <LoadingRobotRow row={row} columns={columns} />;
}
@@ -592,7 +590,6 @@ export const RecordingsTable = ({
<>
<TableContainer component={Paper} sx={{ width: '100%', overflow: 'hidden', marginTop: '15px' }}>
<Table stickyHeader aria-label="sticky table">
<TableHead>
<TableRow>
{columns.map((column) => (
<MemoizedTableCell
@@ -603,7 +600,6 @@ export const RecordingsTable = ({
</MemoizedTableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{visibleRows.map((row) => (
<TableRowMemoized
@@ -618,13 +614,12 @@ export const RecordingsTable = ({
</TableContainer>
<TablePagination
rowsPerPageOptions={[10, 25, 50, 100]}
component="div"
count={filteredRows.length}
rowsPerPage={rowsPerPage}
page={page}
rowsPerPage={rowsPerPage}
onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage}
rowsPerPageOptions={[]}
/>
</>
)}

View File

@@ -704,14 +704,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 <em style={{ color: '#999' }}>Select formats</em>;
}
return `${selected.length} format${selected.length > 1 ? 's' : ''} selected`;
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: {
@@ -1097,4 +1129,4 @@ const modalStyle = {
height: 'fit-content',
display: 'block',
padding: '20px',
};
};

View File

@@ -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 { apiUrl } from "../../apiConfig";
const socketCache = new Map<string, Socket>();
const progressCallbacks = new Map<string, Set<(data: any) => void>>();
function getOrCreateSocket(browserId: string): Socket {
if (socketCache.has(browserId)) {
return socketCache.get(browserId)!;
}
const socket = io(`${apiUrl}/${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,11 +93,52 @@ export const CollapsibleRow = ({ row, handleDelete, isOpen, onToggleExpanded, cu
const logEndRef = useRef<HTMLDivElement | null>(null);
const scrollToLogBottom = () => {
if (logEndRef.current) {
logEndRef.current.scrollIntoView({ behavior: "smooth" });
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);
@@ -67,12 +147,7 @@ export const CollapsibleRow = ({ row, handleDelete, isOpen, onToggleExpanded, cu
const handleRowExpand = () => {
const newOpen = !isOpen;
onToggleExpanded(newOpen);
//scrollToLogBottom();
};
// useEffect(() => {
// scrollToLogBottom();
// }, [currentLog])
useEffect(() => {
const fetchUserEmail = async () => {
@@ -196,7 +271,8 @@ export const CollapsibleRow = ({ row, handleDelete, isOpen, onToggleExpanded, cu
<TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={6}>
<Collapse in={isOpen} timeout="auto" unmountOnExit>
<RunContent row={row} abortRunHandler={handleAbort} currentLog={currentLog}
logEndRef={logEndRef} interpretationInProgress={runningRecordingName === row.name} />
logEndRef={logEndRef} interpretationInProgress={runningRecordingName === row.name}
workflowProgress={workflowProgress} />
</Collapse>
</TableCell>
</TableRow>

View File

@@ -30,9 +30,14 @@ interface RunContentProps {
interpretationInProgress: boolean,
logEndRef: React.RefObject<HTMLDivElement>,
abortRunHandler: () => void,
workflowProgress: {
current: number;
total: number;
percentage: number;
} | null,
}
export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRef, abortRunHandler }: RunContentProps) => {
export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRef, abortRunHandler, workflowProgress }: RunContentProps) => {
const { t } = useTranslation();
const { darkMode } = useThemeMode();
const [tab, setTab] = React.useState<string>('output');
@@ -73,6 +78,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('');
@@ -925,7 +939,20 @@ export const RunContent = ({ row, currentLog, interpretationInProgress, logEndRe
{row.status === 'running' || row.status === 'queued' ? (
<>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<CircularProgress size={22} sx={{ marginRight: '10px' }} />
{workflowProgress ? (
<>
<CircularProgress
size={22}
sx={{ marginRight: '10px' }}
/>
{getProgressMessage(workflowProgress.percentage)}
</>
) : (
<>
<CircularProgress size={22} sx={{ marginRight: '10px' }} />
{t('run_content.loading')}
</>
)}
{t('run_content.loading')}
</Box>
<Button color="error" onClick={abortRunHandler} sx={{ mt: 1 }}>

View File

@@ -631,11 +631,10 @@ export const RunsTable: React.FC<RunsTableProps> = ({
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={[]}
/>
</AccordionDetails>
</Accordion>
@@ -648,11 +647,10 @@ export const RunsTable: React.FC<RunsTableProps> = ({
page={accordionPage}
rowsPerPage={accordionsPerPage}
onPageChange={handleAccordionPageChange}
onRowsPerPageChange={handleAccordionsPerPageChange}
rowsPerPageOptions={[10, 25, 50, 100]}
rowsPerPageOptions={[]}
/>
</>
)}
</React.Fragment>
);
};
};

View File

@@ -7,7 +7,7 @@ const Alert = React.forwardRef<HTMLDivElement, AlertProps>(function Alert(
props,
ref,
) {
return <MuiAlert elevation={6} ref={ref} variant="filled" {...props} />;
return <MuiAlert elevation={6} ref={ref} variant="outlined" {...props} />;
});
export interface AlertSnackbarProps {
@@ -32,7 +32,7 @@ export const AlertSnackbar = ({ severity, message, isOpen }: AlertSnackbarProps)
return (
<Snackbar anchorOrigin={{ vertical: 'top', horizontal: 'center' }} open={open} autoHideDuration={5000} onClose={handleClose}>
<Alert onClose={handleClose} severity={severity} sx={{ width: '100%' }}>
<Alert onClose={handleClose} severity={severity} sx={{ width: '100%', bgcolor: 'background.paper' }} variant="outlined">
{message}
</Alert>
</Snackbar>

View File

@@ -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: {
@@ -65,6 +88,13 @@ const lightTheme = createTheme({
color: "#ff00c3",
},
},
outlinedInfo: {
color: '#000000ff',
borderColor: '#000000ff',
"& .MuiAlert-icon": {
color: "#000000ff",
},
},
},
},
MuiAlertTitle: {
@@ -102,6 +132,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: {
@@ -185,6 +238,13 @@ const darkTheme = createTheme({
color: "#ff66d9",
},
},
outlinedInfo: {
color: '#ffffff',
borderColor: '#ffffff',
"& .MuiAlert-icon": {
color: "#ffffff",
},
},
},
},
MuiAlertTitle: {
@@ -196,7 +256,6 @@ const darkTheme = createTheme({
},
},
},
// Additional dark mode specific components
MuiPaper: {
styleOverrides: {
root: {
@@ -233,14 +292,6 @@ const darkTheme = createTheme({
},
},
},
// MuiTextField:{
// styleOverrides: {
// root: {
// '& .MuiInputBase-root': {
// backgroundColor: '#1d1c1cff',
// },
// }
// }}
},
});
@@ -280,4 +331,4 @@ const ThemeModeProvider = ({ children }: { children: React.ReactNode }) => {
);
};
export default ThemeModeProvider;
export default ThemeModeProvider;