Spaces:
Running
Running
| import React, { useState, useCallback, useEffect } from "react"; | |
| import { KnowledgeGraph } from "@/types"; | |
| import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; | |
| import { Card, CardContent } from "@/components/ui/card"; | |
| import { Button } from "@/components/ui/button"; | |
| import { | |
| Tooltip, | |
| TooltipContent, | |
| TooltipProvider, | |
| TooltipTrigger, | |
| } from "@/components/ui/tooltip"; | |
| import { | |
| Sparkles, | |
| Shield, | |
| GitBranch, | |
| Play, | |
| Clock, | |
| CheckCircle, | |
| AlertCircle, | |
| Trash2, | |
| } from "lucide-react"; | |
| import { useNotification } from "@/context/NotificationContext"; | |
| import { EnrichResults } from "../traces/EnrichResults"; | |
| import { PerturbResults } from "../traces/PerturbResults"; | |
| import CausalResults from "../traces/CausalResults"; | |
| const API_BASE = "/api"; | |
| async function fetchApi<T>( | |
| endpoint: string, | |
| options?: RequestInit | |
| ): Promise<T> { | |
| const response = await fetch(`${API_BASE}${endpoint}`, { | |
| headers: { | |
| "Content-Type": "application/json", | |
| ...options?.headers, | |
| }, | |
| ...options, | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`API Error: ${response.statusText}`); | |
| } | |
| return response.json(); | |
| } | |
| interface AdvancedProcessingViewProps { | |
| knowledgeGraph: KnowledgeGraph; | |
| onBack?: () => void; | |
| } | |
| interface StageState { | |
| status: "idle" | "running" | "completed" | "error"; | |
| progress: number; | |
| taskId?: string; | |
| error?: string; | |
| } | |
| interface PipelineStageConfig { | |
| id: string; | |
| name: string; | |
| description: string; | |
| icon: React.ComponentType<{ className?: string }>; | |
| apiEndpoint: (kgId: string) => string; | |
| benefit: string; | |
| } | |
| const PIPELINE_STAGES: PipelineStageConfig[] = [ | |
| { | |
| id: "enrich", | |
| name: "Prompt Reconstruction", | |
| description: "Reconstruct original prompts from conversation patterns", | |
| icon: Sparkles, | |
| apiEndpoint: (kgId) => `/knowledge-graphs/${kgId}/enrich`, | |
| benefit: "Understand conversation context and intent", | |
| }, | |
| { | |
| id: "perturb", | |
| name: "Perturbation Testing", | |
| description: "Test graph robustness through systematic variations", | |
| icon: Shield, | |
| apiEndpoint: (kgId) => `/knowledge-graphs/${kgId}/perturb`, | |
| benefit: "Validate relationship reliability and stability", | |
| }, | |
| { | |
| id: "causal", | |
| name: "Causal Analysis", | |
| description: "Analyze causal relationships and dependencies", | |
| icon: GitBranch, | |
| apiEndpoint: (kgId) => `/knowledge-graphs/${kgId}/analyze`, | |
| benefit: "Discover cause-and-effect patterns", | |
| }, | |
| ]; | |
| export const AdvancedProcessingView: React.FC<AdvancedProcessingViewProps> = ({ | |
| knowledgeGraph: initialKnowledgeGraph, | |
| onBack: _onBack, | |
| }) => { | |
| const { showNotification } = useNotification(); | |
| const [knowledgeGraph, setKnowledgeGraph] = useState(initialKnowledgeGraph); | |
| const [stageStates, setStageStates] = useState<Record<string, StageState>>({ | |
| enrich: { | |
| status: knowledgeGraph.is_enriched ? "completed" : "idle", | |
| progress: knowledgeGraph.is_enriched ? 100 : 0, | |
| }, | |
| perturb: { | |
| status: knowledgeGraph.is_perturbed ? "completed" : "idle", | |
| progress: knowledgeGraph.is_perturbed ? 100 : 0, | |
| }, | |
| causal: { | |
| status: knowledgeGraph.is_analyzed ? "completed" : "idle", | |
| progress: knowledgeGraph.is_analyzed ? 100 : 0, | |
| }, | |
| }); | |
| const [stageResults, setStageResults] = useState<Record<string, any>>({}); | |
| const [loadingResults, setLoadingResults] = useState<Record<string, boolean>>( | |
| {} | |
| ); | |
| const [knowledgeGraphData, setKnowledgeGraphData] = useState<any>(null); | |
| const [progressTimers, setProgressTimers] = useState< | |
| Record<string, NodeJS.Timeout> | |
| >({}); | |
| const anyStageRunning = Object.values(stageStates).some( | |
| (state) => state.status === "running" | |
| ); | |
| // Estimated durations for each stage (in seconds) | |
| const STAGE_DURATIONS = { | |
| enrich: 45, // Prompt Reconstruction typically takes ~45 seconds | |
| perturb: 90, // Perturbation Testing takes ~90 seconds | |
| causal: 60, // Causal Analysis takes ~60 seconds | |
| }; | |
| // Start estimated progress animation | |
| const startEstimatedProgress = useCallback( | |
| (stageId: string) => { | |
| // Clear any existing timer | |
| if (progressTimers[stageId]) { | |
| clearInterval(progressTimers[stageId]); | |
| } | |
| const duration = | |
| STAGE_DURATIONS[stageId as keyof typeof STAGE_DURATIONS] || 60; | |
| const interval = 1000; // Update every second | |
| const increment = 100 / ((duration * 1000) / interval); // Progress per interval | |
| let currentProgress = 0; | |
| const timer = setInterval(() => { | |
| // Add some randomness to make progress feel more realistic | |
| const randomVariation = (Math.random() - 0.5) * 0.5; // ±0.25% variation | |
| currentProgress += increment + randomVariation; | |
| // Cap at 95% to avoid reaching 100% before completion | |
| if (currentProgress >= 95) { | |
| currentProgress = 95; | |
| } | |
| // Ensure progress doesn't go backwards | |
| if (currentProgress < 0) { | |
| currentProgress = 0; | |
| } | |
| setStageStates((prev) => ({ | |
| ...prev, | |
| [stageId]: { | |
| ...prev[stageId], | |
| progress: Math.round(currentProgress), | |
| } as StageState, | |
| })); | |
| }, interval); | |
| setProgressTimers((prev) => ({ | |
| ...prev, | |
| [stageId]: timer, | |
| })); | |
| }, | |
| [progressTimers] | |
| ); | |
| // Stop progress animation | |
| const stopEstimatedProgress = useCallback( | |
| (stageId: string) => { | |
| if (progressTimers[stageId]) { | |
| clearInterval(progressTimers[stageId]); | |
| setProgressTimers((prev) => { | |
| const updated = { ...prev }; | |
| delete updated[stageId]; | |
| return updated; | |
| }); | |
| } | |
| }, | |
| [progressTimers] | |
| ); | |
| // Cleanup timers on unmount | |
| useEffect(() => { | |
| return () => { | |
| Object.values(progressTimers).forEach((timer) => clearInterval(timer)); | |
| }; | |
| }, [progressTimers]); | |
| // Function to refresh knowledge graph data | |
| const refreshKnowledgeGraph = useCallback(async () => { | |
| try { | |
| console.log("🔄 Refreshing knowledge graph status..."); | |
| const response = await fetchApi<any>( | |
| `/knowledge-graphs/${knowledgeGraph.kg_id}/status` | |
| ); | |
| console.log("📡 KG Status API Response:", response); | |
| setKnowledgeGraph((prev) => { | |
| console.log("📝 Previous KG state:", prev); | |
| const updated = { | |
| ...prev, | |
| is_enriched: response.is_enriched, | |
| is_perturbed: response.is_perturbed, | |
| is_analyzed: response.is_analyzed, | |
| status: response.status, | |
| }; | |
| console.log("✅ Updated KG state:", updated); | |
| return updated; | |
| }); | |
| } catch (error) { | |
| console.error("❌ Error refreshing knowledge graph status:", error); | |
| } | |
| }, [knowledgeGraph.kg_id]); | |
| const pollTaskStatus = useCallback( | |
| async (stageId: string, taskId: string) => { | |
| const maxAttempts = 60; // 5 minutes at 5-second intervals | |
| let attempts = 0; | |
| const poll = async (): Promise<void> => { | |
| try { | |
| const response = await fetchApi<any>(`/tasks/${taskId}/status`); | |
| const { status } = response; | |
| // Note: We're using estimated progress instead of backend progress | |
| // The progress is managed by the startEstimatedProgress function | |
| if (status === "COMPLETED") { | |
| console.log(`🎉 Stage ${stageId} completed! Updating state...`); | |
| // Stop estimated progress animation and set to 100% | |
| stopEstimatedProgress(stageId); | |
| setStageStates((prev) => ({ | |
| ...prev, | |
| [stageId]: { | |
| ...prev[stageId], | |
| status: "completed", | |
| progress: 100, | |
| } as StageState, | |
| })); | |
| showNotification({ | |
| title: `${ | |
| PIPELINE_STAGES.find((s) => s.id === stageId)?.name | |
| } completed successfully`, | |
| message: "The processing stage has been completed successfully.", | |
| type: "success", | |
| }); | |
| // Refresh the knowledge graph data to get updated status flags | |
| console.log( | |
| `🔄 Calling refreshKnowledgeGraph for stage ${stageId}...` | |
| ); | |
| await refreshKnowledgeGraph(); | |
| console.log(`✅ Finished refreshing KG for stage ${stageId}`); | |
| return; | |
| } | |
| if (status === "FAILED") { | |
| // Stop estimated progress animation on failure | |
| stopEstimatedProgress(stageId); | |
| setStageStates((prev) => ({ | |
| ...prev, | |
| [stageId]: { | |
| ...prev[stageId], | |
| status: "error", | |
| error: response.error || "Task failed", | |
| progress: 0, | |
| } as StageState, | |
| })); | |
| showNotification({ | |
| title: `${ | |
| PIPELINE_STAGES.find((s) => s.id === stageId)?.name | |
| } failed`, | |
| message: "The processing stage encountered an error.", | |
| type: "error", | |
| }); | |
| return; | |
| } | |
| attempts++; | |
| if (attempts < maxAttempts && status === "RUNNING") { | |
| setTimeout(poll, 5000); | |
| } | |
| } catch (error) { | |
| console.error("Error polling task status:", error); | |
| attempts++; | |
| if (attempts < maxAttempts) { | |
| setTimeout(poll, 5000); | |
| } | |
| } | |
| }; | |
| poll(); | |
| }, | |
| [showNotification, refreshKnowledgeGraph] | |
| ); | |
| const runStage = async (stage: PipelineStageConfig) => { | |
| try { | |
| setStageStates((prev) => ({ | |
| ...prev, | |
| [stage.id]: { | |
| ...prev[stage.id], | |
| status: "running", | |
| progress: 0, | |
| error: undefined, | |
| }, | |
| })); | |
| // Start estimated progress animation | |
| startEstimatedProgress(stage.id); | |
| const response = await fetchApi<any>( | |
| stage.apiEndpoint(knowledgeGraph.kg_id), | |
| { | |
| method: "POST", | |
| } | |
| ); | |
| const { task_id } = response; | |
| if (task_id) { | |
| setStageStates((prev) => ({ | |
| ...prev, | |
| [stage.id]: { | |
| ...prev[stage.id], | |
| taskId: task_id, | |
| } as StageState, | |
| })); | |
| pollTaskStatus(stage.id, task_id); | |
| } | |
| } catch (error: any) { | |
| console.error(`Error running ${stage.name}:`, error); | |
| // Stop progress animation on error | |
| stopEstimatedProgress(stage.id); | |
| setStageStates((prev) => ({ | |
| ...prev, | |
| [stage.id]: { | |
| ...prev[stage.id], | |
| status: "error", | |
| error: error.message, | |
| progress: 0, | |
| } as StageState, | |
| })); | |
| showNotification({ | |
| title: `Failed to start ${stage.name}`, | |
| message: "Unable to start the processing stage. Please try again.", | |
| type: "error", | |
| }); | |
| } | |
| }; | |
| const clearStage = async (stage: PipelineStageConfig) => { | |
| try { | |
| const response = await fetchApi<any>( | |
| `/knowledge-graphs/${knowledgeGraph.kg_id}/stage-results/${stage.id}`, | |
| { | |
| method: "DELETE", | |
| } | |
| ); | |
| const { cleared_stages, new_status } = response; | |
| // Update knowledge graph status | |
| setKnowledgeGraph((prev) => ({ | |
| ...prev, | |
| status: new_status, | |
| is_enriched: | |
| new_status === "enriched" || | |
| new_status === "perturbed" || | |
| new_status === "analyzed", | |
| is_perturbed: new_status === "perturbed" || new_status === "analyzed", | |
| is_analyzed: new_status === "analyzed", | |
| })); | |
| // Clear results and reset stage states for cleared stages | |
| const updatedStageStates: Record<string, StageState> = {}; | |
| const updatedStageResults: Record<string, any> = {}; | |
| cleared_stages.forEach((clearedStage: string) => { | |
| updatedStageStates[clearedStage] = { | |
| status: "idle", | |
| progress: 0, | |
| error: undefined, | |
| }; | |
| updatedStageResults[clearedStage] = undefined; | |
| }); | |
| setStageStates((prev) => ({ ...prev, ...updatedStageStates })); | |
| setStageResults((prev) => { | |
| const newResults = { ...prev }; | |
| cleared_stages.forEach((clearedStage: string) => { | |
| delete newResults[clearedStage]; | |
| }); | |
| return newResults; | |
| }); | |
| showNotification({ | |
| title: `${stage.name} cleared successfully`, | |
| message: `Cleared ${cleared_stages.join(", ")} stage(s) as requested.`, | |
| type: "success", | |
| }); | |
| } catch (error: any) { | |
| console.error(`Error clearing ${stage.name}:`, error); | |
| showNotification({ | |
| title: `Failed to clear ${stage.name}`, | |
| message: "Unable to clear the stage results. Please try again.", | |
| type: "error", | |
| }); | |
| } | |
| }; | |
| const fetchStageResults = useCallback( | |
| async (stageId: string) => { | |
| if (loadingResults[stageId]) return; | |
| setLoadingResults((prev) => ({ ...prev, [stageId]: true })); | |
| try { | |
| const response = await fetchApi<any>( | |
| `/knowledge-graphs/${knowledgeGraph.kg_id}/stage-results/${stageId}` | |
| ); | |
| setStageResults((prev) => ({ ...prev, [stageId]: response })); | |
| } catch (error) { | |
| console.error("Error fetching stage results:", error); | |
| showNotification({ | |
| title: "Failed to fetch results", | |
| message: "Unable to retrieve the results for this stage.", | |
| type: "error", | |
| }); | |
| } finally { | |
| setLoadingResults((prev) => ({ ...prev, [stageId]: false })); | |
| } | |
| }, | |
| [loadingResults, knowledgeGraph.kg_id, showNotification] | |
| ); | |
| // Fetch knowledge graph data for component name mapping | |
| useEffect(() => { | |
| const fetchKnowledgeGraphData = async () => { | |
| try { | |
| const response = await fetchApi<{ | |
| entities: Array<{ id: string; name: string; type: string }>; | |
| relations: Array<{ | |
| id: string; | |
| type: string; | |
| source: string; | |
| target: string; | |
| }>; | |
| }>(`/knowledge-graphs/${knowledgeGraph.kg_id}`); | |
| setKnowledgeGraphData(response); | |
| } catch (error) { | |
| console.error("Error fetching knowledge graph data:", error); | |
| // Don't show notification for this as it's not critical - causal analysis will still work with IDs | |
| } | |
| }; | |
| fetchKnowledgeGraphData(); | |
| }, [knowledgeGraph.kg_id]); | |
| // Auto-fetch results when stage completes | |
| useEffect(() => { | |
| console.log("🔍 Auto-fetch effect triggered. StageStates:", stageStates); | |
| PIPELINE_STAGES.forEach((stage) => { | |
| const state = stageStates[stage.id]; | |
| console.log( | |
| `📊 Stage ${stage.id} state:`, | |
| state, | |
| `Results exist:`, | |
| !!stageResults[stage.id], | |
| `Loading:`, | |
| !!loadingResults[stage.id] | |
| ); | |
| if ( | |
| state?.status === "completed" && | |
| !stageResults[stage.id] && | |
| !loadingResults[stage.id] | |
| ) { | |
| console.log(`🚀 Fetching results for completed stage: ${stage.id}`); | |
| fetchStageResults(stage.id); | |
| } | |
| }); | |
| }, [stageStates, stageResults, loadingResults, fetchStageResults]); | |
| const renderStageContent = (stage: PipelineStageConfig) => { | |
| const state = stageStates[stage.id] || { | |
| status: "idle" as const, | |
| progress: 0, | |
| }; | |
| return ( | |
| <div className="space-y-6"> | |
| {/* Progress */} | |
| {state.status === "running" && ( | |
| <Card> | |
| <CardContent className="pt-6"> | |
| <div className="space-y-2"> | |
| <div className="flex items-center justify-between"> | |
| <span className="font-medium">Progress</span> | |
| <span className="font-mono"> | |
| {Math.round(state.progress)}% | |
| </span> | |
| </div> | |
| <div className="w-full bg-gray-200 rounded-full h-2"> | |
| <div | |
| className="bg-blue-600 h-2 rounded-full transition-all duration-300" | |
| style={{ width: `${state.progress}%` }} | |
| /> | |
| </div> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| )} | |
| {/* Error Display */} | |
| {state.error && ( | |
| <Card> | |
| <CardContent className="pt-6"> | |
| <div className="p-4 bg-red-50 border border-red-200 rounded-lg"> | |
| <p className="text-red-700 font-medium">{state.error}</p> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| )} | |
| {/* Results Section */} | |
| {state.status === "completed" && stageResults[stage.id] ? ( | |
| <div className="space-y-6"> | |
| {/* Results Header with Actions */} | |
| <Card className="border-t-2 border-t-green-500 bg-gradient-to-r from-green-50 to-emerald-50"> | |
| <CardContent className="p-4"> | |
| <div className="flex items-center justify-between"> | |
| <div className="flex items-center gap-3"> | |
| <div className="flex items-center justify-center w-8 h-8 bg-green-100 rounded-full"> | |
| <CheckCircle className="h-4 w-4 text-green-600" /> | |
| </div> | |
| <div> | |
| <h4 className="font-semibold text-green-900"> | |
| {stage.name} Completed | |
| </h4> | |
| <p className="text-sm text-green-700"> | |
| Results are ready to view | |
| </p> | |
| </div> | |
| </div> | |
| {/* Action Buttons */} | |
| <div className="flex items-center gap-3"> | |
| <Button | |
| onClick={() => runStage(stage)} | |
| disabled={anyStageRunning} | |
| size="sm" | |
| variant="outline" | |
| className="gap-2 border-green-200 text-green-700 hover:bg-green-100" | |
| > | |
| <Play className="h-4 w-4" /> | |
| Run Again | |
| </Button> | |
| <Button | |
| variant="outline" | |
| onClick={() => clearStage(stage)} | |
| disabled={anyStageRunning} | |
| size="sm" | |
| className="gap-2 text-red-600 border-red-200 hover:bg-red-50 hover:border-red-300" | |
| > | |
| <Trash2 className="h-4 w-4" /> | |
| Clear | |
| </Button> | |
| </div> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| {/* Results Content */} | |
| <div> | |
| {stage.id === "enrich" && ( | |
| <EnrichResults | |
| data={stageResults[stage.id]} | |
| knowledgeGraphId={knowledgeGraph.kg_id} | |
| knowledgeGraphData={knowledgeGraphData} | |
| /> | |
| )} | |
| {stage.id === "perturb" && ( | |
| <PerturbResults | |
| data={stageResults[stage.id]} | |
| knowledgeGraphId={knowledgeGraph.kg_id} | |
| /> | |
| )} | |
| {stage.id === "causal" && ( | |
| <CausalResults | |
| data={stageResults[stage.id]} | |
| knowledgeGraphData={knowledgeGraphData} | |
| /> | |
| )} | |
| </div> | |
| </div> | |
| ) : ( | |
| /* Actions for non-completed stages */ | |
| <Card className="border-dashed border-2 border-gray-200"> | |
| <CardContent className="p-6"> | |
| <div className="text-center space-y-4"> | |
| {state.status === "idle" && ( | |
| <> | |
| <div className="flex items-center justify-center w-12 h-12 bg-blue-50 rounded-full mx-auto"> | |
| <Play className="h-6 w-6 text-blue-600" /> | |
| </div> | |
| <div> | |
| <h4 className="font-medium text-gray-900 mb-1"> | |
| Ready to Run {stage.name} | |
| </h4> | |
| <p className="text-sm text-gray-600 mb-4"> | |
| {stage.description} | |
| </p> | |
| <Button | |
| onClick={() => runStage(stage)} | |
| disabled={anyStageRunning} | |
| size="lg" | |
| className="gap-2" | |
| > | |
| <Play className="h-5 w-5" /> | |
| Run {stage.name} | |
| </Button> | |
| </div> | |
| </> | |
| )} | |
| {state.status === "completed" && !stageResults[stage.id] && ( | |
| <> | |
| <div className="flex items-center justify-center w-12 h-12 bg-orange-50 rounded-full mx-auto"> | |
| <AlertCircle className="h-6 w-6 text-orange-600" /> | |
| </div> | |
| <div> | |
| <h4 className="font-medium text-gray-900 mb-1"> | |
| No Results Available | |
| </h4> | |
| <p className="text-sm text-gray-600 mb-4"> | |
| The stage completed but no results were found. Try | |
| running again. | |
| </p> | |
| <Button | |
| onClick={() => runStage(stage)} | |
| disabled={anyStageRunning} | |
| size="lg" | |
| className="gap-2" | |
| > | |
| <Play className="h-5 w-5" /> | |
| Run Again | |
| </Button> | |
| </div> | |
| </> | |
| )} | |
| {state.status === "error" && ( | |
| <> | |
| <div className="flex items-center justify-center w-12 h-12 bg-red-50 rounded-full mx-auto"> | |
| <AlertCircle className="h-6 w-6 text-red-600" /> | |
| </div> | |
| <div> | |
| <h4 className="font-medium text-red-900 mb-1"> | |
| Error Occurred | |
| </h4> | |
| <p className="text-sm text-red-600 mb-4"> | |
| {state.error || "An error occurred during processing"} | |
| </p> | |
| <Button | |
| variant="outline" | |
| onClick={() => runStage(stage)} | |
| disabled={anyStageRunning} | |
| size="lg" | |
| className="gap-2 border-red-200 text-red-700 hover:bg-red-50" | |
| > | |
| <Play className="h-5 w-5" /> | |
| Retry | |
| </Button> | |
| </div> | |
| </> | |
| )} | |
| {state.status === "running" && ( | |
| <> | |
| <div className="flex items-center justify-center w-12 h-12 bg-blue-50 rounded-full mx-auto"> | |
| <Clock className="h-6 w-6 text-blue-600 animate-spin" /> | |
| </div> | |
| <div> | |
| <h4 className="font-medium text-blue-900 mb-1"> | |
| Running {stage.name} | |
| </h4> | |
| <p className="text-sm text-blue-600 mb-4"> | |
| Processing in progress... {Math.round(state.progress)}% | |
| </p> | |
| <Button disabled size="lg" className="gap-2"> | |
| <Clock className="h-5 w-5" /> | |
| Running... | |
| </Button> | |
| </div> | |
| </> | |
| )} | |
| {loadingResults[stage.id] && ( | |
| <> | |
| <div className="flex items-center justify-center w-12 h-12 bg-purple-50 rounded-full mx-auto"> | |
| <Clock className="h-6 w-6 text-purple-600 animate-pulse" /> | |
| </div> | |
| <div> | |
| <h4 className="font-medium text-purple-900 mb-1"> | |
| Loading Results | |
| </h4> | |
| <p className="text-sm text-purple-600 mb-4"> | |
| Fetching {stage.name.toLowerCase()} results... | |
| </p> | |
| <Button disabled size="lg" className="gap-2"> | |
| <Clock className="h-5 w-5" /> | |
| Loading Results... | |
| </Button> | |
| </div> | |
| </> | |
| )} | |
| </div> | |
| </CardContent> | |
| </Card> | |
| )} | |
| </div> | |
| ); | |
| }; | |
| return ( | |
| <TooltipProvider> | |
| <div className="flex flex-col h-screen bg-background"> | |
| <div className="flex-1 overflow-hidden"> | |
| <div className="h-full p-6"> | |
| <Tabs defaultValue="enrich" className="h-full flex flex-col"> | |
| <TabsList className="grid w-full grid-cols-3 mb-6"> | |
| {PIPELINE_STAGES.map((stage) => { | |
| const state = stageStates[stage.id]; | |
| const StageIcon = stage.icon; | |
| return ( | |
| <Tooltip key={stage.id}> | |
| <TooltipTrigger asChild> | |
| <TabsTrigger | |
| value={stage.id} | |
| className="flex items-center gap-2 relative" | |
| > | |
| <StageIcon className="h-4 w-4" /> | |
| {stage.name} | |
| {state?.status === "completed" && ( | |
| <CheckCircle className="h-3 w-3 text-green-600 absolute -top-1 -right-1" /> | |
| )} | |
| {state?.status === "running" && ( | |
| <Clock className="h-3 w-3 text-blue-600 absolute -top-1 -right-1 animate-pulse" /> | |
| )} | |
| {state?.status === "error" && ( | |
| <AlertCircle className="h-3 w-3 text-red-600 absolute -top-1 -right-1" /> | |
| )} | |
| </TabsTrigger> | |
| </TooltipTrigger> | |
| <TooltipContent> | |
| <p>{stage.description}</p> | |
| </TooltipContent> | |
| </Tooltip> | |
| ); | |
| })} | |
| </TabsList> | |
| <div className="flex-1 overflow-y-auto"> | |
| {PIPELINE_STAGES.map((stage) => ( | |
| <TabsContent | |
| key={stage.id} | |
| value={stage.id} | |
| className="h-full mt-0" | |
| > | |
| <div className="max-w-5xl mx-auto"> | |
| {renderStageContent(stage)} | |
| </div> | |
| </TabsContent> | |
| ))} | |
| </div> | |
| </Tabs> | |
| </div> | |
| </div> | |
| </div> | |
| </TooltipProvider> | |
| ); | |
| }; | |