wu981526092's picture
🚀 Deploy AgentGraph: Complete agent monitoring and knowledge graph system
c2ea5ed
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>
);
};