diff --git a/frontend/src/components/dashboard/MonitorDashboard.tsx b/frontend/src/components/dashboard/MonitorDashboard.tsx new file mode 100644 index 000000000..fd1abe83b --- /dev/null +++ b/frontend/src/components/dashboard/MonitorDashboard.tsx @@ -0,0 +1,412 @@ +import React, { useState, useEffect } from 'react'; +import { + RefreshCw, + Clock, + CheckCircle, + XCircle, + Loader, + ExternalLink, + FileText, + GitBranch, + Calendar +} from 'lucide-react'; +import { codegenApi } from '@/services/api'; +import { AgentRun, RunStatus } from '@/types'; + +const MonitorDashboard: React.FC = () => { + const [runs, setRuns] = useState([]); + const [selectedRun, setSelectedRun] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [refreshing, setRefreshing] = useState(false); + + const orgId = 'default-org'; // TODO: Get from auth context + const apiKey = import.meta.env.VITE_API_KEY || 'demo-key'; + + useEffect(() => { + fetchRuns(); + // Auto-refresh every 5 seconds + const interval = setInterval(() => { + fetchRuns(true); + }, 5000); + return () => clearInterval(interval); + }, []); + + const fetchRuns = async (isBackground = false) => { + try { + if (!isBackground) setLoading(true); + else setRefreshing(true); + + const fetchedRuns = await codegenApi.fetchAllRuns(orgId, apiKey); + setRuns(fetchedRuns); + setError(null); + + // Update selected run if it exists in new data + if (selectedRun) { + const updated = fetchedRuns.find(r => r.id === selectedRun.id); + if (updated) setSelectedRun(updated); + } + } catch (err: any) { + setError(err.message || 'Failed to fetch runs'); + console.error('[MonitorDashboard] Error fetching runs:', err); + } finally { + setLoading(false); + setRefreshing(false); + } + }; + + const getStatusIcon = (status: RunStatus) => { + switch (status) { + case 'running': + return ; + case 'completed': + return ; + case 'failed': + return ; + case 'pending': + return ; + default: + return ; + } + }; + + const getStatusBadge = (status: RunStatus) => { + const colors = { + running: 'bg-blue-100 text-blue-700', + completed: 'bg-green-100 text-green-700', + failed: 'bg-red-100 text-red-700', + pending: 'bg-yellow-100 text-yellow-700' + }; + return ( + + {status.toUpperCase()} + + ); + }; + + const formatDate = (dateString?: string) => { + if (!dateString) return 'N/A'; + try { + return new Date(dateString).toLocaleString(); + } catch { + return dateString; + } + }; + + if (loading && !runs.length) { + return ( +
+
+ +

Loading runs...

+
+
+ ); + } + + if (error && !runs.length) { + return ( +
+
+ +

Error Loading Runs

+

{error}

+ +
+
+ ); + } + + if (!runs.length) { + return ( +
+
+ +

No Runs Yet

+

Create your first agent run to get started

+
+
+ ); + } + + return ( +
+ {/* Header */} +
+
+

Agent Runs Monitor

+

+ Real-time monitoring of all agent executions +

+
+ +
+ + {/* Stats Cards */} +
+ + r.status === 'running').length} + icon={Loader} + color="blue" + /> + r.status === 'completed').length} + icon={CheckCircle} + color="green" + /> + r.status === 'failed').length} + icon={XCircle} + color="red" + /> +
+ + {/* Runs Table */} +
+
+ + + + + + + + + + + + + {runs.map((run) => ( + setSelectedRun(run)} + className={`hover:bg-gray-50 cursor-pointer transition-colors ${ + selectedRun?.id === run.id ? 'bg-blue-50' : '' + }`} + > + + + + + + + + ))} + +
+ Status + + Run ID + + Prompt + + Model + + Created + + Actions +
+
+ {getStatusIcon(run.status)} + {getStatusBadge(run.status)} +
+
+ {run.id.substring(0, 8)}... + + {run.prompt || 'No prompt'} + + {run.model || 'N/A'} + + {formatDate(run.created_at)} + + +
+
+
+ + {/* Selected Run Details */} + {selectedRun && ( +
+
+
+

Run Details

+
+ {getStatusIcon(selectedRun.status)} + {getStatusBadge(selectedRun.status)} + + ID: {selectedRun.id} + +
+
+ +
+ +
+ {/* Left Column */} +
+
+ +

{formatDate(selectedRun.created_at)}

+
+ +
+ +

{formatDate(selectedRun.updated_at)}

+
+ +
+ +

{selectedRun.model || 'Not specified'}

+
+
+ + {/* Right Column */} +
+
+ +
+ {selectedRun.prompt} +
+
+ + {selectedRun.pr_urls && selectedRun.pr_urls.length > 0 && ( +
+ +
+ {selectedRun.pr_urls.map((url, idx) => ( + + + {url} + + ))} +
+
+ )} +
+
+ + {/* Result/Error Section */} + {selectedRun.result && ( +
+ +
+ {selectedRun.result} +
+
+ )} + + {selectedRun.error && ( +
+ +
+ {selectedRun.error} +
+
+ )} + + {selectedRun.summary && ( +
+ +
+ {selectedRun.summary} +
+
+ )} +
+ )} +
+ ); +}; + +// Stat Card Component +interface StatCardProps { + label: string; + value: number; + icon: React.ComponentType<{ className?: string }>; + color: 'blue' | 'green' | 'red' | 'yellow'; +} + +const StatCard: React.FC = ({ label, value, icon: Icon, color }) => { + const colors = { + blue: 'bg-blue-50 text-blue-600', + green: 'bg-green-50 text-green-600', + red: 'bg-red-50 text-red-600', + yellow: 'bg-yellow-50 text-yellow-600' + }; + + return ( +
+
+
+

{label}

+

{value}

+
+
+ +
+
+
+ ); +}; + +export default MonitorDashboard; + diff --git a/frontend/src/components/dashboard/WorkflowControl.tsx b/frontend/src/components/dashboard/WorkflowControl.tsx new file mode 100644 index 000000000..b81200b50 --- /dev/null +++ b/frontend/src/components/dashboard/WorkflowControl.tsx @@ -0,0 +1,380 @@ +import React, { useState, useEffect } from 'react'; +import { + Play, + Pause, + RefreshCw, + AlertCircle, + CheckCircle, + Clock, + Loader, + XCircle, + Search, + Filter +} from 'lucide-react'; +import { codegenApi } from '@/services/api'; +import { AgentRun, RunStatus } from '@/types'; +import toast from 'react-hot-toast'; + +const WorkflowControl: React.FC = () => { + const [runs, setRuns] = useState([]); + const [filteredRuns, setFilteredRuns] = useState([]); + const [loading, setLoading] = useState(true); + const [searchTerm, setSearchTerm] = useState(''); + const [statusFilter, setStatusFilter] = useState('all'); + const [isResumeModalOpen, setIsResumeModalOpen] = useState(false); + const [selectedRun, setSelectedRun] = useState(null); + const [resumePrompt, setResumePrompt] = useState(''); + const [resuming, setResuming] = useState(false); + + const orgId = 'default-org'; // TODO: Get from auth context + const apiKey = import.meta.env.VITE_API_KEY || 'demo-key'; + + useEffect(() => { + fetchRuns(); + // Auto-refresh every 5 seconds + const interval = setInterval(fetchRuns, 5000); + return () => clearInterval(interval); + }, []); + + useEffect(() => { + // Apply filters + let filtered = runs; + + if (statusFilter !== 'all') { + filtered = filtered.filter(run => run.status === statusFilter); + } + + if (searchTerm) { + filtered = filtered.filter( + run => + run.id.toLowerCase().includes(searchTerm.toLowerCase()) || + run.prompt?.toLowerCase().includes(searchTerm.toLowerCase()) + ); + } + + setFilteredRuns(filtered); + }, [runs, statusFilter, searchTerm]); + + const fetchRuns = async () => { + try { + setLoading(true); + const fetchedRuns = await codegenApi.fetchAllRuns(orgId, apiKey); + setRuns(fetchedRuns); + } catch (err: any) { + console.error('[WorkflowControl] Error fetching runs:', err); + toast.error('Failed to fetch runs'); + } finally { + setLoading(false); + } + }; + + const handleResumeClick = (run: AgentRun) => { + setSelectedRun(run); + setResumePrompt(''); + setIsResumeModalOpen(true); + }; + + const handleResumeSubmit = async () => { + if (!selectedRun || !resumePrompt.trim()) { + toast.error('Please provide instructions for resuming the run'); + return; + } + + try { + setResuming(true); + await codegenApi.resumeRun(orgId, apiKey, selectedRun.id, resumePrompt); + toast.success('Run resumed successfully!'); + setIsResumeModalOpen(false); + setSelectedRun(null); + setResumePrompt(''); + // Refresh runs list + setTimeout(fetchRuns, 1000); + } catch (err: any) { + console.error('[WorkflowControl] Error resuming run:', err); + toast.error(err.message || 'Failed to resume run'); + } finally { + setResuming(false); + } + }; + + const getStatusIcon = (status: RunStatus) => { + switch (status) { + case 'running': + return ; + case 'completed': + return ; + case 'failed': + return ; + case 'pending': + return ; + default: + return ; + } + }; + + const getStatusColor = (status: RunStatus) => { + switch (status) { + case 'running': + return 'bg-blue-100 text-blue-700 border-blue-200'; + case 'completed': + return 'bg-green-100 text-green-700 border-green-200'; + case 'failed': + return 'bg-red-100 text-red-700 border-red-200'; + case 'pending': + return 'bg-yellow-100 text-yellow-700 border-yellow-200'; + default: + return 'bg-gray-100 text-gray-700 border-gray-200'; + } + }; + + if (loading && !runs.length) { + return ( +
+
+ +

Loading workflow controls...

+
+
+ ); + } + + return ( +
+ {/* Header */} +
+

Workflow Control

+

+ Manage and control agent run executions +

+
+ + {/* Filters and Search */} +
+
+ {/* Search */} +
+ + setSearchTerm(e.target.value)} + className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> +
+ + {/* Status Filter */} +
+ + +
+ + {/* Refresh Button */} + +
+ + {/* Filter Stats */} +
+ Total: {runs.length} + + Filtered: {filteredRuns.length} +
+
+ + {/* Runs List */} + {filteredRuns.length === 0 ? ( +
+ +

No runs found

+

+ {runs.length === 0 + ? 'Create your first agent run to get started' + : 'Try adjusting your filters'} +

+
+ ) : ( +
+ {filteredRuns.map((run) => ( +
+
+
+ {getStatusIcon(run.status)} +
+
+ + {run.id.substring(0, 12)}... + + + {run.status} + +
+

+ {run.model || 'No model specified'} +

+
+
+ + {/* Actions */} +
+ {(run.status === 'failed' || run.status === 'completed') && ( + + )} + {run.status === 'running' && ( + + )} +
+
+ + {/* Prompt */} +
+

Prompt:

+

{run.prompt}

+
+ + {/* Result/Error */} + {run.result && ( +
+

Result:

+

{run.result}

+
+ )} + + {run.error && ( +
+

Error:

+

{run.error}

+
+ )} + + {/* Timestamps */} +
+ Created: {new Date(run.created_at || '').toLocaleString()} + {run.updated_at && ( + <> + + Updated: {new Date(run.updated_at).toLocaleString()} + + )} +
+
+ ))} +
+ )} + + {/* Resume Modal */} + {isResumeModalOpen && selectedRun && ( +
+
+
+
+

Resume Run

+

+ Provide additional instructions to continue this run +

+
+ +
+ + {/* Original Run Info */} +
+

Original Prompt:

+

{selectedRun.prompt}

+
+ + {selectedRun.status} + + + ID: {selectedRun.id.substring(0, 12)}... + +
+
+ + {/* Resume Instructions */} +
+ +