Spaces:
Running
Running
| import React, { useState, useCallback } from "react"; | |
| import { Button } from "@/components/ui/button"; | |
| import { Badge } from "@/components/ui/badge"; | |
| import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; | |
| import { | |
| Dialog, | |
| DialogContent, | |
| DialogDescription, | |
| DialogHeader, | |
| DialogTitle, | |
| } from "@/components/ui/dialog"; | |
| import { | |
| AlertCircle, | |
| CheckCircle, | |
| Clock, | |
| Play, | |
| Eye, | |
| Zap, | |
| Shield, | |
| GitBranch, | |
| Sparkles, | |
| Settings, | |
| } from "lucide-react"; | |
| import { KnowledgeGraph, PerturbationConfig } from "@/types"; | |
| import { useNotification } from "@/context/NotificationContext"; | |
| import { api } from "@/lib/api"; | |
| import { EnrichResults } from "./EnrichResults"; | |
| import { PerturbResults } from "./PerturbResults"; | |
| import { PerturbationTestConfig } from "./PerturbationTestConfig"; | |
| interface OptionalPipelineSectionProps { | |
| knowledgeGraph: KnowledgeGraph; | |
| onStageComplete?: (stage: string) => 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 function OptionalPipelineSection({ | |
| knowledgeGraph, | |
| onStageComplete, | |
| }: OptionalPipelineSectionProps) { | |
| const { showNotification } = useNotification(); | |
| 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 [selectedStageResults, setSelectedStageResults] = useState<{ | |
| stage: string; | |
| data: any; | |
| } | null>(null); | |
| const [showPerturbConfig, setShowPerturbConfig] = useState(false); | |
| const getStageStatusColor = (status: StageState["status"]) => { | |
| switch (status) { | |
| case "completed": | |
| return "bg-green-500/10 text-green-700 border-green-200"; | |
| case "running": | |
| return "bg-blue-500/10 text-blue-700 border-blue-200"; | |
| case "error": | |
| return "bg-red-500/10 text-red-700 border-red-200"; | |
| default: | |
| return "bg-gray-500/10 text-gray-700 border-gray-200"; | |
| } | |
| }; | |
| const pollTaskStatus = useCallback( | |
| async (stageId: string, taskId: string) => { | |
| const maxAttempts = 60; // 5 minutes at 5-second intervals | |
| let attempts = 0; | |
| const poll = async () => { | |
| try { | |
| const status = await api.tasks.get(taskId); | |
| setStageStates((prev) => { | |
| const currentState = prev[stageId] || { | |
| status: "idle" as const, | |
| progress: 0, | |
| }; | |
| return { | |
| ...prev, | |
| [stageId]: { | |
| ...currentState, | |
| progress: status.progress || currentState.progress, | |
| }, | |
| }; | |
| }); | |
| if (status.status === "completed" || status.status === "COMPLETED") { | |
| setStageStates((prev) => ({ | |
| ...prev, | |
| [stageId]: { status: "completed", progress: 100 }, | |
| })); | |
| onStageComplete?.(stageId); | |
| showNotification({ | |
| type: "success", | |
| title: "Stage Completed", | |
| message: `${ | |
| PIPELINE_STAGES.find((s) => s.id === stageId)?.name | |
| } has finished successfully.`, | |
| }); | |
| return; | |
| } else if (status.status === "failed" || status.status === "FAILED") { | |
| setStageStates((prev) => ({ | |
| ...prev, | |
| [stageId]: { | |
| status: "error", | |
| progress: 0, | |
| error: status.error || "Unknown error", | |
| }, | |
| })); | |
| showNotification({ | |
| type: "error", | |
| title: "Stage Failed", | |
| message: status.error || "Processing failed", | |
| }); | |
| return; | |
| } | |
| // Continue polling | |
| attempts++; | |
| if (attempts < maxAttempts) { | |
| setTimeout(poll, 5000); // Poll every 5 seconds | |
| } else { | |
| // Timeout | |
| setStageStates((prev) => ({ | |
| ...prev, | |
| [stageId]: { | |
| status: "error", | |
| progress: 0, | |
| error: "Processing timeout", | |
| }, | |
| })); | |
| } | |
| } catch (error) { | |
| console.error("Error polling task status:", error); | |
| // Continue polling on error, might be temporary | |
| attempts++; | |
| if (attempts < maxAttempts) { | |
| setTimeout(poll, 5000); | |
| } | |
| } | |
| }; | |
| // Start polling | |
| setTimeout(poll, 2000); // First check after 2 seconds | |
| }, | |
| [onStageComplete, showNotification] | |
| ); | |
| const runPerturbWithConfig = useCallback( | |
| async (config: PerturbationConfig) => { | |
| const stageConfig = PIPELINE_STAGES.find((s) => s.id === "perturb")!; | |
| setStageStates((prev) => ({ | |
| ...prev, | |
| perturb: { status: "running", progress: 0 }, | |
| })); | |
| try { | |
| const response = await api.knowledgeGraphs.perturb( | |
| knowledgeGraph.kg_id, | |
| config | |
| ); | |
| if (response.task_id) { | |
| pollTaskStatus("perturb", response.task_id); | |
| showNotification({ | |
| type: "info", | |
| title: `${stageConfig.name} Started`, | |
| message: "Processing has begun with custom configuration.", | |
| }); | |
| } | |
| } catch (error) { | |
| console.error("Error running perturb stage:", error); | |
| setStageStates((prev) => ({ | |
| ...prev, | |
| perturb: { | |
| status: "error", | |
| progress: 0, | |
| error: error instanceof Error ? error.message : "Unknown error", | |
| }, | |
| })); | |
| showNotification({ | |
| type: "error", | |
| title: `${stageConfig.name} Failed`, | |
| message: error instanceof Error ? error.message : "An error occurred", | |
| }); | |
| } | |
| }, | |
| [knowledgeGraph.kg_id, showNotification, pollTaskStatus] | |
| ); | |
| const runStage = useCallback( | |
| async (stageConfig: PipelineStageConfig) => { | |
| const { id } = stageConfig; | |
| // For perturb stage, open the configuration dialog instead | |
| if (id === "perturb") { | |
| setShowPerturbConfig(true); | |
| return; | |
| } | |
| setStageStates((prev) => ({ | |
| ...prev, | |
| [id]: { status: "running", progress: 0 }, | |
| })); | |
| try { | |
| // Call the API endpoint based on stage | |
| let response; | |
| if (id === "enrich") { | |
| response = await api.knowledgeGraphs.enrich(knowledgeGraph.kg_id); | |
| } else if (id === "causal") { | |
| response = await api.knowledgeGraphs.analyze(knowledgeGraph.kg_id); | |
| } else { | |
| throw new Error(`Unknown stage: ${id}`); | |
| } | |
| if (response.task_id) { | |
| // Start polling for task status | |
| pollTaskStatus(id, response.task_id); | |
| showNotification({ | |
| type: "info", | |
| title: `${stageConfig.name} Started`, | |
| message: "Processing has begun. This may take several minutes.", | |
| }); | |
| } else { | |
| // Mark as completed immediately if no task ID | |
| setStageStates((prev) => ({ | |
| ...prev, | |
| [id]: { status: "completed", progress: 100 }, | |
| })); | |
| onStageComplete?.(id); | |
| } | |
| } catch (error) { | |
| console.error(`Error running ${id} stage:`, error); | |
| setStageStates((prev) => ({ | |
| ...prev, | |
| [id]: { | |
| status: "error", | |
| progress: 0, | |
| error: error instanceof Error ? error.message : "Unknown error", | |
| }, | |
| })); | |
| showNotification({ | |
| type: "error", | |
| title: `${stageConfig.name} Failed`, | |
| message: error instanceof Error ? error.message : "An error occurred", | |
| }); | |
| } | |
| }, | |
| [knowledgeGraph.kg_id, showNotification, onStageComplete, pollTaskStatus] | |
| ); | |
| const viewStageResults = useCallback( | |
| async (stageId: string) => { | |
| try { | |
| const results = await api.knowledgeGraphs.getStageResults( | |
| knowledgeGraph.kg_id, | |
| stageId | |
| ); | |
| setSelectedStageResults({ | |
| stage: stageId, | |
| data: results, | |
| }); | |
| } catch (error) { | |
| showNotification({ | |
| type: "error", | |
| title: "Failed to Load Results", | |
| message: "Could not retrieve stage results", | |
| }); | |
| } | |
| }, | |
| [knowledgeGraph.kg_id, showNotification] | |
| ); | |
| const anyStageRunning = Object.values(stageStates).some( | |
| (state) => state.status === "running" | |
| ); | |
| return ( | |
| <> | |
| <div className="space-y-6"> | |
| {/* Advanced Processing - Unified */} | |
| <Card> | |
| <CardHeader> | |
| <CardTitle className="flex items-center gap-2 text-xl"> | |
| <Zap className="h-6 w-6" /> | |
| Advanced Processing | |
| </CardTitle> | |
| </CardHeader> | |
| <CardContent> | |
| <div className="space-y-6"> | |
| {/* Description */} | |
| <div className="text-muted-foreground"> | |
| <p className="text-base"> | |
| Enhance your knowledge graph with advanced analysis | |
| techniques. These optional stages provide deeper insights but | |
| are not required for basic graph functionality. | |
| </p> | |
| </div> | |
| {/* Horizontal Stepper */} | |
| <div className="relative"> | |
| {/* Step Circles - Evenly Distributed */} | |
| <div className="flex items-center justify-between"> | |
| {PIPELINE_STAGES.map((stage, index) => { | |
| const state = stageStates[stage.id] || { | |
| status: "idle" as const, | |
| progress: 0, | |
| }; | |
| const StageIcon = stage.icon; | |
| return ( | |
| <div | |
| key={stage.id} | |
| className="flex flex-col items-center flex-1" | |
| > | |
| {/* Step Circle */} | |
| <div | |
| className={` | |
| w-12 h-12 rounded-full border-2 flex items-center justify-center transition-all | |
| ${ | |
| state.status === "completed" | |
| ? "bg-green-500 border-green-500 text-white" | |
| : state.status === "running" | |
| ? "bg-blue-500 border-blue-500 text-white animate-pulse" | |
| : state.status === "error" | |
| ? "bg-red-500 border-red-500 text-white" | |
| : "bg-background border-muted-foreground text-muted-foreground" | |
| } | |
| `} | |
| > | |
| {state.status === "completed" ? ( | |
| <CheckCircle className="h-6 w-6" /> | |
| ) : state.status === "running" ? ( | |
| <Clock className="h-6 w-6" /> | |
| ) : state.status === "error" ? ( | |
| <AlertCircle className="h-6 w-6" /> | |
| ) : ( | |
| <StageIcon className="h-6 w-6" /> | |
| )} | |
| </div> | |
| {/* Step Number */} | |
| <span className="text-xs font-medium mt-1 text-muted-foreground"> | |
| Step {index + 1} | |
| </span> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| {/* Connector Lines - Positioned Absolutely */} | |
| <div className="absolute top-6 left-0 right-0 flex items-center justify-between px-12"> | |
| {PIPELINE_STAGES.slice(0, -1).map((stage, _index) => { | |
| const state = stageStates[stage.id] || { | |
| status: "idle" as const, | |
| progress: 0, | |
| }; | |
| return ( | |
| <div | |
| key={`connector-${stage.id}`} | |
| className="flex-1 mx-8" | |
| > | |
| <div | |
| className={` | |
| h-0.5 transition-all | |
| ${ | |
| state.status === "completed" | |
| ? "bg-green-500" | |
| : "bg-muted-foreground/30" | |
| } | |
| `} | |
| /> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| {/* Stage Labels */} | |
| <div className="flex items-start justify-between"> | |
| {PIPELINE_STAGES.map((stage, _index) => { | |
| const state = stageStates[stage.id] || { | |
| status: "idle" as const, | |
| progress: 0, | |
| }; | |
| return ( | |
| <div | |
| key={`${stage.id}-label`} | |
| className="flex-1 text-center" | |
| > | |
| <h3 className="font-medium text-sm">{stage.name}</h3> | |
| <p className="text-xs text-muted-foreground mt-1 leading-relaxed"> | |
| {stage.description} | |
| </p> | |
| <Badge | |
| variant="outline" | |
| className={`${getStageStatusColor( | |
| state.status | |
| )} font-medium mt-2`} | |
| > | |
| {state.status === "idle" | |
| ? "Ready" | |
| : state.status === "running" | |
| ? "Running" | |
| : state.status === "completed" | |
| ? "Completed" | |
| : "Error"} | |
| </Badge> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| {/* Error Display */} | |
| {Object.values(stageStates).some((s) => s.error) && ( | |
| <div className="p-4 bg-red-50 border border-red-200 rounded-lg"> | |
| <p className="text-red-700 font-medium"> | |
| {Object.values(stageStates).find((s) => s.error)?.error} | |
| </p> | |
| </div> | |
| )} | |
| {/* Action Buttons - Aligned with Step Circles */} | |
| <div className="flex items-center justify-between"> | |
| {PIPELINE_STAGES.map((stage) => { | |
| const state = stageStates[stage.id] || { | |
| status: "idle" as const, | |
| progress: 0, | |
| }; | |
| return ( | |
| <div | |
| key={`${stage.id}-action`} | |
| className="flex-1 flex justify-center" | |
| > | |
| {state.status === "idle" && ( | |
| <Button | |
| onClick={() => runStage(stage)} | |
| disabled={anyStageRunning} | |
| className="gap-2" | |
| > | |
| {stage.id === "perturb" ? ( | |
| <> | |
| <Settings className="h-4 w-4" /> | |
| Configure & Run | |
| </> | |
| ) : ( | |
| <> | |
| <Play className="h-4 w-4" /> | |
| Run {stage.name} | |
| </> | |
| )} | |
| </Button> | |
| )} | |
| {state.status === "completed" && ( | |
| <Button | |
| variant="outline" | |
| onClick={() => viewStageResults(stage.id)} | |
| className="gap-2 bg-green-50 border-green-200 hover:bg-green-100" | |
| > | |
| <Eye className="h-4 w-4" /> | |
| View Results | |
| </Button> | |
| )} | |
| {state.status === "error" && ( | |
| <Button | |
| variant="outline" | |
| onClick={() => runStage(stage)} | |
| disabled={anyStageRunning} | |
| className="gap-2" | |
| > | |
| <Play className="h-4 w-4" /> | |
| Retry | |
| </Button> | |
| )} | |
| {state.status === "running" && ( | |
| <Button disabled className="gap-2"> | |
| <Clock className="h-4 w-4" /> | |
| Running... | |
| </Button> | |
| )} | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| </div> | |
| {/* Stage Results Dialog */} | |
| {selectedStageResults && ( | |
| <Dialog | |
| open={!!selectedStageResults} | |
| onOpenChange={() => setSelectedStageResults(null)} | |
| > | |
| <DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto"> | |
| <DialogHeader> | |
| <DialogTitle> | |
| { | |
| PIPELINE_STAGES.find( | |
| (s) => s.id === selectedStageResults.stage | |
| )?.name | |
| }{" "} | |
| Results | |
| </DialogTitle> | |
| <DialogDescription> | |
| Analysis results for knowledge graph {knowledgeGraph.kg_id} | |
| </DialogDescription> | |
| </DialogHeader> | |
| <div className="mt-4"> | |
| {/* Render appropriate results component based on stage */} | |
| {selectedStageResults.stage === "enrich" && ( | |
| <EnrichResults | |
| data={selectedStageResults.data} | |
| knowledgeGraphId={knowledgeGraph.kg_id} | |
| /> | |
| )} | |
| {selectedStageResults.stage === "perturb" && ( | |
| <PerturbResults | |
| data={selectedStageResults.data} | |
| knowledgeGraphId={knowledgeGraph.kg_id} | |
| /> | |
| )} | |
| {selectedStageResults.stage === "causal" && ( | |
| <div className="p-8 text-center"> | |
| <h3 className="text-lg font-semibold mb-2"> | |
| Causal Analysis Results | |
| </h3> | |
| <p className="text-muted-foreground mb-4"> | |
| Rich causal analysis visualization is in development. | |
| </p> | |
| <pre className="bg-muted p-4 rounded-lg text-sm overflow-auto text-left max-h-96"> | |
| {JSON.stringify(selectedStageResults.data, null, 2)} | |
| </pre> | |
| </div> | |
| )} | |
| </div> | |
| </DialogContent> | |
| </Dialog> | |
| )} | |
| {/* Perturbation Test Configuration Dialog */} | |
| <PerturbationTestConfig | |
| open={showPerturbConfig} | |
| onOpenChange={setShowPerturbConfig} | |
| onRun={runPerturbWithConfig} | |
| /> | |
| </> | |
| ); | |
| } | |