Spaces:
Runtime error
Runtime error
| 'use client'; | |
| import React, { useState, useEffect } from 'react'; | |
| import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; | |
| import { Button } from '@/components/ui/button'; | |
| import { Badge } from '@/components/ui/badge'; | |
| import { Progress } from '@/components/ui/progress'; | |
| import { Input } from '@/components/ui/input'; | |
| import { SafeTimestamp } from './ClientTimeStampProvider'; | |
| import { | |
| Select, | |
| SelectContent, | |
| SelectItem, | |
| SelectTrigger, | |
| SelectValue, | |
| } from '@/components/ui/select'; | |
| import { | |
| Activity, | |
| CheckCircle, | |
| AlertCircle, | |
| Clock, | |
| Zap, | |
| Filter, | |
| Search, | |
| RefreshCw, | |
| Eye, | |
| Trash2, | |
| TrendingUp, | |
| Brain | |
| } from 'lucide-react'; | |
| interface WorkflowStatus { | |
| workflow_id: string; | |
| status: string; | |
| current_stage: number; | |
| stages: Array<{ | |
| name: string; | |
| status: string; | |
| description: string; | |
| started_at?: string; | |
| completed_at?: string; | |
| error?: string; | |
| }>; | |
| progress_percentage: number; | |
| created_at: string; | |
| metrics?: { | |
| consciousness_scores?: number[]; | |
| accuracy_scores?: number[]; | |
| total_time?: number; | |
| error_count?: number; | |
| }; | |
| error?: string; | |
| } | |
| interface WorkflowMonitorProps { | |
| workflows?: WorkflowStatus[]; | |
| } | |
| export default function WorkflowMonitor({ workflows: propWorkflows }: WorkflowMonitorProps) { | |
| const [workflows, setWorkflows] = useState<WorkflowStatus[]>(propWorkflows || []); | |
| const [filteredWorkflows, setFilteredWorkflows] = useState<WorkflowStatus[]>([]); | |
| const [statusFilter, setStatusFilter] = useState<string>('all'); | |
| const [searchTerm, setSearchTerm] = useState(''); | |
| const [isLoading, setIsLoading] = useState(false); | |
| const [selectedWorkflow, setSelectedWorkflow] = useState<WorkflowStatus | null>(null); | |
| // Fetch workflows on component mount and periodically | |
| useEffect(() => { | |
| fetchWorkflows(); | |
| const interval = setInterval(fetchWorkflows, 5000); // Refresh every 5 seconds | |
| return () => clearInterval(interval); | |
| }, []); | |
| // Update filtered workflows when filters change | |
| useEffect(() => { | |
| let filtered = workflows; | |
| // Apply status filter | |
| if (statusFilter !== 'all') { | |
| filtered = filtered.filter(w => w.status === statusFilter); | |
| } | |
| // Apply search filter | |
| if (searchTerm) { | |
| filtered = filtered.filter(w => | |
| w.workflow_id.toLowerCase().includes(searchTerm.toLowerCase()) || | |
| w.stages.some(stage => stage.name.toLowerCase().includes(searchTerm.toLowerCase())) | |
| ); | |
| } | |
| setFilteredWorkflows(filtered); | |
| }, [workflows, statusFilter, searchTerm]); | |
| const fetchWorkflows = async () => { | |
| if (propWorkflows) return; // Don't fetch if workflows are provided as props | |
| setIsLoading(true); | |
| try { | |
| const response = await fetch('/api/ai-architect/workflows'); | |
| if (response.ok) { | |
| const data = await response.json(); | |
| setWorkflows(data); | |
| } | |
| } catch (error) { | |
| console.error('Failed to fetch workflows:', error); | |
| } finally { | |
| setIsLoading(false); | |
| } | |
| }; | |
| const getStatusIcon = (status: string) => { | |
| switch (status) { | |
| case 'completed': return <CheckCircle className="w-4 h-4 text-green-500" />; | |
| case 'running': return <Activity className="w-4 h-4 text-blue-500 animate-pulse" />; | |
| case 'failed': return <AlertCircle className="w-4 h-4 text-red-500" />; | |
| case 'initializing': return <Clock className="w-4 h-4 text-yellow-500" />; | |
| default: return <Clock className="w-4 h-4 text-gray-400" />; | |
| } | |
| }; | |
| const getStatusBadge = (status: string) => { | |
| const variants = { | |
| completed: 'border-green-500 text-green-400', | |
| running: 'border-blue-500 text-blue-400 animate-pulse', | |
| failed: 'border-red-500 text-red-400', | |
| initializing: 'border-yellow-500 text-yellow-400' | |
| }; | |
| return ( | |
| <Badge variant="outline" className={variants[status as keyof typeof variants] || 'border-gray-500 text-gray-400'}> | |
| {status.toUpperCase()} | |
| </Badge> | |
| ); | |
| }; | |
| const formatDuration = (startTime: string, endTime?: string) => { | |
| const start = new Date(startTime); | |
| const end = endTime ? new Date(endTime) : new Date(); | |
| const duration = Math.round((end.getTime() - start.getTime()) / 1000 / 60); // minutes | |
| if (duration < 60) { | |
| return `${duration}m`; | |
| } else { | |
| const hours = Math.floor(duration / 60); | |
| const minutes = duration % 60; | |
| return `${hours}h ${minutes}m`; | |
| } | |
| }; | |
| const deleteWorkflow = async (workflowId: string) => { | |
| if (!confirm('Are you sure you want to delete this workflow?')) return; | |
| try { | |
| // This would be an actual delete endpoint | |
| setWorkflows(prev => prev.filter(w => w.workflow_id !== workflowId)); | |
| } catch (error) { | |
| console.error('Failed to delete workflow:', error); | |
| } | |
| }; | |
| const getOverallStats = () => { | |
| const total = workflows.length; | |
| const completed = workflows.filter(w => w.status === 'completed').length; | |
| const running = workflows.filter(w => w.status === 'running').length; | |
| const failed = workflows.filter(w => w.status === 'failed').length; | |
| const avgConsciousness = workflows.length > 0 | |
| ? workflows.reduce((sum, w) => { | |
| const scores = w.metrics?.consciousness_scores || []; | |
| return sum + (scores.length > 0 ? scores[0] : 0); | |
| }, 0) / workflows.length | |
| : 0; | |
| const avgAccuracy = workflows.length > 0 | |
| ? workflows.reduce((sum, w) => { | |
| const scores = w.metrics?.accuracy_scores || []; | |
| return sum + (scores.length > 0 ? scores[0] : 0); | |
| }, 0) / workflows.length | |
| : 0; | |
| return { total, completed, running, failed, avgConsciousness, avgAccuracy }; | |
| }; | |
| const stats = getOverallStats(); | |
| return ( | |
| <div className="w-full h-full flex flex-col space-y-6"> | |
| {/* Stats Overview */} | |
| <div className="grid grid-cols-2 md:grid-cols-6 gap-4"> | |
| <Card className="bg-gradient-to-r from-blue-600/20 to-blue-800/20"> | |
| <CardContent className="p-4"> | |
| <div className="text-2xl font-bold text-blue-300">{stats.total}</div> | |
| <div className="text-sm text-blue-400">Total Workflows</div> | |
| </CardContent> | |
| </Card> | |
| <Card className="bg-gradient-to-r from-green-600/20 to-green-800/20"> | |
| <CardContent className="p-4"> | |
| <div className="text-2xl font-bold text-green-300">{stats.completed}</div> | |
| <div className="text-sm text-green-400">Completed</div> | |
| </CardContent> | |
| </Card> | |
| <Card className="bg-gradient-to-r from-blue-600/20 to-blue-800/20"> | |
| <CardContent className="p-4"> | |
| <div className="text-2xl font-bold text-blue-300">{stats.running}</div> | |
| <div className="text-sm text-blue-400">Running</div> | |
| </CardContent> | |
| </Card> | |
| <Card className="bg-gradient-to-r from-red-600/20 to-red-800/20"> | |
| <CardContent className="p-4"> | |
| <div className="text-2xl font-bold text-red-300">{stats.failed}</div> | |
| <div className="text-sm text-red-400">Failed</div> | |
| </CardContent> | |
| </Card> | |
| <Card className="bg-gradient-to-r from-purple-600/20 to-purple-800/20"> | |
| <CardContent className="p-4"> | |
| <div className="text-2xl font-bold text-purple-300"> | |
| {(stats.avgConsciousness * 100).toFixed(1)}% | |
| </div> | |
| <div className="text-sm text-purple-400">Avg Consciousness</div> | |
| </CardContent> | |
| </Card> | |
| <Card className="bg-gradient-to-r from-orange-600/20 to-orange-800/20"> | |
| <CardContent className="p-4"> | |
| <div className="text-2xl font-bold text-orange-300"> | |
| {(stats.avgAccuracy * 100).toFixed(1)}% | |
| </div> | |
| <div className="text-sm text-orange-400">Avg Accuracy</div> | |
| </CardContent> | |
| </Card> | |
| </div> | |
| {/* Filters and Controls */} | |
| <Card> | |
| <CardHeader> | |
| <CardTitle className="flex items-center justify-between"> | |
| <div className="flex items-center space-x-2"> | |
| <Activity className="w-5 h-5" /> | |
| <span>Workflow Monitor</span> | |
| <Badge variant="outline">{filteredWorkflows.length} workflows</Badge> | |
| </div> | |
| <Button | |
| variant="outline" | |
| size="sm" | |
| onClick={fetchWorkflows} | |
| disabled={isLoading} | |
| > | |
| <RefreshCw className={`w-4 h-4 mr-1 ${isLoading ? 'animate-spin' : ''}`} /> | |
| Refresh | |
| </Button> | |
| </CardTitle> | |
| </CardHeader> | |
| <CardContent className="space-y-4"> | |
| <div className="flex items-center space-x-4"> | |
| <div className="flex items-center space-x-2"> | |
| <Search className="w-4 h-4 text-muted-foreground" /> | |
| <Input | |
| placeholder="Search workflows..." | |
| value={searchTerm} | |
| onChange={(e) => setSearchTerm(e.target.value)} | |
| className="w-64" | |
| /> | |
| </div> | |
| <div className="flex items-center space-x-2"> | |
| <Filter className="w-4 h-4 text-muted-foreground" /> | |
| <Select value={statusFilter} onValueChange={setStatusFilter}> | |
| <SelectTrigger className="w-40"> | |
| <SelectValue placeholder="Filter by status" /> | |
| </SelectTrigger> | |
| <SelectContent> | |
| <SelectItem value="all">All Status</SelectItem> | |
| <SelectItem value="running">Running</SelectItem> | |
| <SelectItem value="completed">Completed</SelectItem> | |
| <SelectItem value="failed">Failed</SelectItem> | |
| <SelectItem value="initializing">Initializing</SelectItem> | |
| </SelectContent> | |
| </Select> | |
| </div> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| {/* Workflows List */} | |
| <div className="flex-1 space-y-4 overflow-auto"> | |
| {filteredWorkflows.length === 0 ? ( | |
| <Card> | |
| <CardContent className="p-8 text-center"> | |
| <Activity className="w-12 h-12 text-muted-foreground mx-auto mb-4" /> | |
| <h3 className="text-lg font-semibold mb-2">No workflows found</h3> | |
| <p className="text-muted-foreground"> | |
| {workflows.length === 0 | |
| ? "No workflows have been created yet. Create your first workflow in the AI Architect panel." | |
| : "No workflows match your current filters. Try adjusting the search or filter criteria." | |
| } | |
| </p> | |
| </CardContent> | |
| </Card> | |
| ) : ( | |
| filteredWorkflows.map((workflow) => ( | |
| <Card key={workflow.workflow_id} className="hover:shadow-lg transition-shadow"> | |
| <CardContent className="p-6"> | |
| <div className="flex items-start justify-between"> | |
| <div className="flex-1 space-y-3"> | |
| {/* Header */} | |
| <div className="flex items-center space-x-3"> | |
| {getStatusIcon(workflow.status)} | |
| <span className="font-medium text-lg">{workflow.workflow_id}</span> | |
| {getStatusBadge(workflow.status)} | |
| <span className="text-sm text-muted-foreground"> | |
| Created {formatDuration(workflow.created_at)} ago | |
| </span> | |
| </div> | |
| {/* Progress */} | |
| <div className="flex items-center space-x-4"> | |
| <Progress value={workflow.progress_percentage} className="flex-1" /> | |
| <span className="text-sm font-medium"> | |
| {Math.round(workflow.progress_percentage)}% | |
| </span> | |
| <span className="text-sm text-muted-foreground"> | |
| Stage {workflow.current_stage + 1}/{workflow.stages.length} | |
| </span> | |
| </div> | |
| {/* Current Stage */} | |
| {workflow.stages.length > 0 && workflow.current_stage < workflow.stages.length && ( | |
| <div className="flex items-center space-x-2"> | |
| <span className="text-sm text-muted-foreground">Current:</span> | |
| <span className="text-sm font-medium"> | |
| {workflow.stages[workflow.current_stage]?.name.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase())} | |
| </span> | |
| </div> | |
| )} | |
| {/* Metrics */} | |
| {workflow.metrics && ( | |
| <div className="flex items-center space-x-6"> | |
| {workflow.metrics.consciousness_scores && workflow.metrics.consciousness_scores.length > 0 && ( | |
| <div className="flex items-center space-x-2"> | |
| <Brain className="w-4 h-4 text-purple-400" /> | |
| <span className="text-sm"> | |
| Consciousness: {(workflow.metrics.consciousness_scores[0] * 100).toFixed(1)}% | |
| </span> | |
| </div> | |
| )} | |
| {workflow.metrics.accuracy_scores && workflow.metrics.accuracy_scores.length > 0 && ( | |
| <div className="flex items-center space-x-2"> | |
| <TrendingUp className="w-4 h-4 text-green-400" /> | |
| <span className="text-sm"> | |
| Accuracy: {(workflow.metrics.accuracy_scores[0] * 100).toFixed(1)}% | |
| </span> | |
| </div> | |
| )} | |
| {workflow.metrics.total_time && ( | |
| <div className="flex items-center space-x-2"> | |
| <Clock className="w-4 h-4 text-blue-400" /> | |
| <span className="text-sm"> | |
| Duration: {Math.round(workflow.metrics.total_time / 60)}m | |
| </span> | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| {/* Error Display */} | |
| {workflow.error && ( | |
| <div className="p-3 bg-red-500/10 border border-red-500/20 rounded-lg"> | |
| <div className="flex items-center space-x-2"> | |
| <AlertCircle className="w-4 h-4 text-red-400" /> | |
| <span className="text-sm text-red-400 font-medium">Error:</span> | |
| </div> | |
| <p className="text-sm text-red-300 mt-1">{workflow.error}</p> | |
| </div> | |
| )} | |
| </div> | |
| {/* Actions */} | |
| <div className="flex items-center space-x-2 ml-4"> | |
| <Button | |
| variant="outline" | |
| size="sm" | |
| onClick={() => setSelectedWorkflow(workflow)} | |
| > | |
| <Eye className="w-4 h-4 mr-1" /> | |
| Details | |
| </Button> | |
| <Button | |
| variant="outline" | |
| size="sm" | |
| onClick={() => deleteWorkflow(workflow.workflow_id)} | |
| className="text-red-400 hover:text-red-300" | |
| > | |
| <Trash2 className="w-4 h-4" /> | |
| </Button> | |
| </div> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| )) | |
| )} | |
| </div> | |
| {/* Detailed View Modal (Simple version) */} | |
| {selectedWorkflow && ( | |
| <Card className="fixed inset-4 z-50 bg-background/95 backdrop-blur-sm border shadow-lg overflow-auto"> | |
| <CardHeader> | |
| <div className="flex items-center justify-between"> | |
| <CardTitle className="flex items-center space-x-2"> | |
| <Zap className="w-5 h-5 text-purple-400" /> | |
| <span>Workflow Details: {selectedWorkflow.workflow_id}</span> | |
| </CardTitle> | |
| <Button | |
| variant="outline" | |
| size="sm" | |
| onClick={() => setSelectedWorkflow(null)} | |
| > | |
| Close | |
| </Button> | |
| </div> | |
| </CardHeader> | |
| <CardContent className="space-y-6"> | |
| {/* Status and Progress */} | |
| <div className="flex items-center justify-between"> | |
| {getStatusBadge(selectedWorkflow.status)} | |
| <div className="flex items-center space-x-4"> | |
| <Progress value={selectedWorkflow.progress_percentage} className="w-32" /> | |
| <span className="text-sm font-medium"> | |
| {Math.round(selectedWorkflow.progress_percentage)}% | |
| </span> | |
| </div> | |
| </div> | |
| {/* All Stages */} | |
| <div className="space-y-3"> | |
| <h3 className="text-lg font-semibold">Workflow Stages</h3> | |
| {selectedWorkflow.stages.map((stage, index) => ( | |
| <div | |
| key={index} | |
| className={`p-4 rounded-lg border ${ | |
| index === selectedWorkflow.current_stage ? 'border-blue-500 bg-blue-500/10' : 'border-border' | |
| }`} | |
| > | |
| <div className="flex items-center space-x-3"> | |
| {getStatusIcon(stage.status)} | |
| <span className="font-medium"> | |
| {stage.name.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase())} | |
| </span> | |
| {getStatusBadge(stage.status)} | |
| </div> | |
| <p className="text-sm text-muted-foreground mt-2">{stage.description}</p> | |
| {stage.error && ( | |
| <p className="text-sm text-red-400 mt-2">Error: {stage.error}</p> | |
| )} | |
| <div className="flex items-center space-x-4 mt-2 text-xs text-muted-foreground"> | |
| {stage.started_at && ( | |
| <span>Started: <SafeTimestamp timestamp={new Date(stage.started_at)} format="datetime" /></span> | |
| )} | |
| {stage.completed_at && ( | |
| <span>Completed: <SafeTimestamp timestamp={new Date(stage.completed_at)} format="datetime" /></span> | |
| )} | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| {/* Detailed Metrics */} | |
| {selectedWorkflow.metrics && ( | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div className="space-y-2"> | |
| <h4 className="font-medium">Performance Metrics</h4> | |
| {selectedWorkflow.metrics.consciousness_scores && ( | |
| <div className="flex justify-between"> | |
| <span className="text-sm text-muted-foreground">Consciousness Score:</span> | |
| <span className="text-sm font-medium"> | |
| {(selectedWorkflow.metrics.consciousness_scores[0] * 100).toFixed(2)}% | |
| </span> | |
| </div> | |
| )} | |
| {selectedWorkflow.metrics.accuracy_scores && ( | |
| <div className="flex justify-between"> | |
| <span className="text-sm text-muted-foreground">Model Accuracy:</span> | |
| <span className="text-sm font-medium"> | |
| {(selectedWorkflow.metrics.accuracy_scores[0] * 100).toFixed(2)}% | |
| </span> | |
| </div> | |
| )} | |
| {selectedWorkflow.metrics.total_time && ( | |
| <div className="flex justify-between"> | |
| <span className="text-sm text-muted-foreground">Total Time:</span> | |
| <span className="text-sm font-medium"> | |
| {Math.round(selectedWorkflow.metrics.total_time / 60)} minutes | |
| </span> | |
| </div> | |
| )} | |
| </div> | |
| <div className="space-y-2"> | |
| <h4 className="font-medium">System Metrics</h4> | |
| <div className="flex justify-between"> | |
| <span className="text-sm text-muted-foreground">Error Count:</span> | |
| <span className="text-sm font-medium"> | |
| {selectedWorkflow.metrics.error_count || 0} | |
| </span> | |
| </div> | |
| <div className="flex justify-between"> | |
| <span className="text-sm text-muted-foreground">Created:</span> | |
| <span className="text-sm font-medium"> | |
| <SafeTimestamp timestamp={new Date(selectedWorkflow.created_at)} format="datetime" /> | |
| </span> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| </CardContent> | |
| </Card> | |
| )} | |
| </div> | |
| ); | |
| } |