Spaces:
Running
Running
| import React, { useState, useCallback } from "react"; | |
| import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; | |
| import { Badge } from "@/components/ui/badge"; | |
| import { Progress } from "@/components/ui/progress"; | |
| import { Button } from "@/components/ui/button"; | |
| import { | |
| Collapsible, | |
| CollapsibleContent, | |
| CollapsibleTrigger, | |
| } from "@/components/ui/collapsible"; | |
| import { | |
| BarChart, | |
| Bar, | |
| XAxis, | |
| YAxis, | |
| CartesianGrid, | |
| Tooltip, | |
| ResponsiveContainer, | |
| } from "recharts"; | |
| import { | |
| TrendingUp, | |
| Network, | |
| Target, | |
| GitBranch, | |
| AlertTriangle, | |
| Search, | |
| BarChart3, | |
| ChevronDown, | |
| ChevronUp, | |
| Info, | |
| Brain, | |
| Shuffle, | |
| } from "lucide-react"; | |
| interface CausalResult { | |
| id: number; | |
| causal_score: number; | |
| analysis_method: string; | |
| created_at: string; | |
| updated_at: string; | |
| perturbation_set_id: string; | |
| metadata: { | |
| timestamp: string; | |
| method_specific_metadata: Record<string, any>; | |
| }; | |
| raw_analysis: { | |
| scores: Record<string, any>; | |
| metadata: Record<string, any>; | |
| }; | |
| } | |
| interface CausalResultsData { | |
| causal_results: CausalResult[]; | |
| causal_results_by_set: Record<string, CausalResult[]>; | |
| perturbation_set_types?: Record<string, string>; | |
| perturbation_set_metadata?: Record< | |
| string, | |
| { | |
| created_at: string | null; | |
| test_metadata: Record<string, any>; | |
| } | |
| >; | |
| summary: { | |
| total_causal_relations: number; | |
| total_perturbation_sets: number; | |
| }; | |
| } | |
| interface KnowledgeGraphData { | |
| entities: Array<{ id: string; name: string; type: string }>; | |
| relations: Array<{ | |
| id: string; | |
| type: string; | |
| source: string; | |
| target: string; | |
| }>; | |
| } | |
| interface CausalResultsProps { | |
| data: CausalResultsData; | |
| knowledgeGraphData?: KnowledgeGraphData; | |
| } | |
| const methodMetadata = { | |
| graph: { | |
| name: "Graph Analysis", | |
| description: | |
| "Shows which parts of your agent system have the strongest causal influence on overall performance", | |
| explanation: | |
| "This method analyzes your agent system like a network to find which components (agents, tools, tasks) most strongly affect the final outcomes. Higher scores mean stronger causal influence.", | |
| icon: Network, | |
| color: "blue", | |
| metrics: ["ACE", "Shapley"], | |
| }, | |
| component: { | |
| name: "Component Analysis", | |
| description: | |
| "Uses machine learning to rank which components matter most for system performance", | |
| explanation: | |
| "Think of this as a 'feature importance' analysis for your agent system. It trains a model to predict outcomes and tells you which components are most critical for success.", | |
| icon: BarChart3, | |
| color: "green", | |
| metrics: ["Feature_Importance", "Model_Metrics", "Key_Components"], | |
| }, | |
| dowhy: { | |
| name: "DoWhy Analysis", | |
| description: "Rigorous causal inference using Microsoft's DoWhy framework", | |
| icon: Target, | |
| color: "purple", | |
| metrics: [ | |
| "Effect_Estimate", | |
| "Refutation_Results", | |
| "Interaction_Effects", | |
| "Confounders", | |
| ], | |
| }, | |
| confounder: { | |
| name: "Confounder Detection", | |
| description: | |
| "Detects confounding variables that create spurious correlations", | |
| icon: AlertTriangle, | |
| color: "orange", | |
| metrics: ["Confounders", "Impact_Analysis", "Summary"], | |
| }, | |
| mscd: { | |
| name: "Multi-Signal Confounder Detection", | |
| description: "Advanced confounder detection using multiple signal types", | |
| icon: Search, | |
| color: "red", | |
| metrics: ["Confounders", "Method_Results", "Summary"], | |
| }, | |
| ate: { | |
| name: "Average Treatment Effect", | |
| description: | |
| "Measures direct causal effects and Granger causality between components", | |
| icon: GitBranch, | |
| color: "indigo", | |
| metrics: ["Effect_Strengths", "Granger_Results", "ATE_Results"], | |
| }, | |
| }; | |
| const CausalResults: React.FC<CausalResultsProps> = ({ | |
| data, | |
| knowledgeGraphData, | |
| }) => { | |
| const [selectedSet, setSelectedSet] = useState<string | null>(null); | |
| const [expandedMethods, setExpandedMethods] = useState<Set<string>>( | |
| new Set() | |
| ); | |
| // Helper function to convert ID to user-friendly name | |
| const getDisplayName = useCallback( | |
| (id: string): string => { | |
| if (!knowledgeGraphData) { | |
| // Fallback: clean up the ID for better readability | |
| return id.replace(/^(entity_|relation_)/, "").replace(/_/g, " "); | |
| } | |
| // Try to find the entity first | |
| const entity = knowledgeGraphData.entities.find((e) => e.id === id); | |
| if (entity && entity.name) { | |
| return entity.name; | |
| } | |
| // Try to find the relation | |
| const relation = knowledgeGraphData.relations.find((r) => r.id === id); | |
| if (relation) { | |
| // For relations, show the type as it's more meaningful | |
| return ( | |
| relation.type || | |
| relation.id.replace(/^relation_/, "").replace(/_/g, " ") | |
| ); | |
| } | |
| // Enhanced fallback - clean up common ID patterns | |
| return id | |
| .replace(/^(entity_|relation_)/, "") | |
| .replace(/relation_/g, "") | |
| .replace(/_/g, " ") | |
| .replace(/\b\w/g, (l) => l.toUpperCase()); // Capitalize first letter of each word | |
| }, | |
| [knowledgeGraphData] | |
| ); | |
| // Helper function to get component type and add appropriate context | |
| const getComponentInfo = useCallback( | |
| (id: string): { name: string; type: string; description: string } => { | |
| if (!knowledgeGraphData) { | |
| const type = id.startsWith("entity_") ? "Entity" : "Relation"; | |
| return { | |
| name: getDisplayName(id), | |
| type, | |
| description: `${type}: ${getDisplayName(id)}`, | |
| }; | |
| } | |
| const entity = knowledgeGraphData.entities.find((e) => e.id === id); | |
| if (entity) { | |
| return { | |
| name: entity.name || getDisplayName(id), | |
| type: entity.type || "Entity", | |
| description: `${entity.type || "Entity"}: ${ | |
| entity.name || getDisplayName(id) | |
| }`, | |
| }; | |
| } | |
| const relation = knowledgeGraphData.relations.find((r) => r.id === id); | |
| if (relation) { | |
| const sourceEntity = knowledgeGraphData.entities.find( | |
| (e) => e.id === relation.source | |
| ); | |
| const targetEntity = knowledgeGraphData.entities.find( | |
| (e) => e.id === relation.target | |
| ); | |
| const relationName = relation.type || getDisplayName(id); | |
| if (sourceEntity && targetEntity) { | |
| return { | |
| name: relationName, | |
| type: "Relation", | |
| description: `${relationName}: ${ | |
| sourceEntity.name || sourceEntity.id | |
| } → ${targetEntity.name || targetEntity.id}`, | |
| }; | |
| } | |
| return { | |
| name: relationName, | |
| type: "Relation", | |
| description: `Relation: ${relationName}`, | |
| }; | |
| } | |
| // Fallback | |
| const type = id.startsWith("entity_") ? "Entity" : "Relation"; | |
| return { | |
| name: getDisplayName(id), | |
| type, | |
| description: `${type}: ${getDisplayName(id)}`, | |
| }; | |
| }, | |
| [knowledgeGraphData, getDisplayName] | |
| ); | |
| // Helper function to get user-friendly name for perturbation set | |
| const getPerturbationSetDisplayName = useCallback( | |
| (setId: string): string => { | |
| if (!data.perturbation_set_types) { | |
| return setId.slice(0, 8) + "..."; | |
| } | |
| const testType = data.perturbation_set_types[setId]; | |
| if (!testType) { | |
| return setId.slice(0, 8) + "..."; | |
| } | |
| // Convert test type to user-friendly name | |
| const friendlyNames: Record<string, string> = { | |
| jailbreak: "Jailbreak Tests", | |
| counterfactual_bias: "Bias Tests", | |
| jailbreaking: "Jailbreak Tests", | |
| counterfactual: "Bias Tests", | |
| bias: "Bias Tests", | |
| safety: "Safety Tests", | |
| robustness: "Robustness Tests", | |
| }; | |
| const baseName = | |
| friendlyNames[testType.toLowerCase()] || | |
| testType.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase()); | |
| // Add timestamp differentiation if metadata is available | |
| if ( | |
| data.perturbation_set_metadata && | |
| data.perturbation_set_metadata[setId] | |
| ) { | |
| const metadata = data.perturbation_set_metadata[setId]; | |
| if (metadata.created_at) { | |
| try { | |
| const date = new Date(metadata.created_at); | |
| const timeStr = date.toLocaleTimeString("en-US", { | |
| hour: "2-digit", | |
| minute: "2-digit", | |
| hour12: false, | |
| }); | |
| const dateStr = date.toLocaleDateString("en-US", { | |
| month: "short", | |
| day: "numeric", | |
| }); | |
| return `${baseName} (${dateStr} ${timeStr})`; | |
| } catch (e) { | |
| // If date parsing fails, use short ID | |
| return `${baseName} (${setId.slice(0, 6)})`; | |
| } | |
| } | |
| } | |
| return baseName; | |
| }, | |
| [data.perturbation_set_types, data.perturbation_set_metadata] | |
| ); | |
| if (!data || !data.causal_results || data.causal_results.length === 0) { | |
| return ( | |
| <Card className="border-l-4 border-l-indigo-500"> | |
| <CardHeader> | |
| <CardTitle className="flex items-center gap-2"> | |
| <Brain className="h-5 w-5 text-indigo-600" /> | |
| Causal Analysis Results | |
| </CardTitle> | |
| <div className="mt-3 p-4 bg-blue-50 border border-blue-200 rounded-lg"> | |
| <div className="flex items-start gap-2"> | |
| <Info className="h-4 w-4 text-blue-600 mt-0.5 flex-shrink-0" /> | |
| <div className="text-sm text-blue-800"> | |
| <p className="font-medium mb-1"> | |
| Understanding Causal Analysis | |
| </p> | |
| <p> | |
| This analysis reveals which components in your agent system | |
| actually <strong>cause</strong> better or worse outcomes, not | |
| just correlations. Use these insights to identify the most | |
| impactful parts of your system for optimization. | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| </CardHeader> | |
| <CardContent> | |
| <div className="text-center py-8 text-muted-foreground"> | |
| <Brain className="h-12 w-12 mx-auto mb-4 opacity-50" /> | |
| <h3 className="text-lg font-medium mb-2"> | |
| No Causal Analysis Results | |
| </h3> | |
| <p> | |
| No causal analysis data is available for this knowledge graph. | |
| </p> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| ); | |
| } | |
| // Get available perturbation sets | |
| const perturbationSets = Object.keys(data.causal_results_by_set || {}); | |
| // Filter results based on selected perturbation set | |
| const filteredResults = | |
| selectedSet === null | |
| ? data.causal_results | |
| : data.causal_results_by_set[selectedSet] || []; | |
| // Group filtered results by method | |
| const resultsByMethod = filteredResults.reduce((acc, result) => { | |
| if (!acc[result.analysis_method]) { | |
| acc[result.analysis_method] = []; | |
| } | |
| acc[result.analysis_method]!.push(result); | |
| return acc; | |
| }, {} as Record<string, CausalResult[]>); | |
| // Calculate overall statistics from filtered results | |
| const overallStats = { | |
| totalMethods: Object.keys(resultsByMethod).length, | |
| avgCausalScore: | |
| filteredResults.length > 0 | |
| ? filteredResults.reduce((sum, r) => sum + r.causal_score, 0) / | |
| filteredResults.length | |
| : 0, | |
| maxCausalScore: | |
| filteredResults.length > 0 | |
| ? Math.max(...filteredResults.map((r) => r.causal_score)) | |
| : 0, | |
| activeComponents: new Set( | |
| filteredResults.flatMap((r) => | |
| Object.keys(r.raw_analysis.scores).flatMap((scoreType) => | |
| Object.keys(r.raw_analysis.scores[scoreType] || {}) | |
| ) | |
| ) | |
| ).size, | |
| }; | |
| const toggleMethodExpansion = (method: string) => { | |
| const newExpanded = new Set(expandedMethods); | |
| if (newExpanded.has(method)) { | |
| newExpanded.delete(method); | |
| } else { | |
| newExpanded.add(method); | |
| } | |
| setExpandedMethods(newExpanded); | |
| }; | |
| const renderMethodSummary = (method: string, results: CausalResult[]) => { | |
| const metadata = methodMetadata[method as keyof typeof methodMetadata]; | |
| if (!metadata) return null; | |
| const IconComponent = metadata.icon; | |
| const latestResult = results[results.length - 1]; | |
| if (!latestResult) return null; | |
| const avgScore = | |
| results.reduce((sum, r) => sum + r.causal_score, 0) / results.length; | |
| return ( | |
| <Collapsible | |
| key={method} | |
| open={expandedMethods.has(method)} | |
| onOpenChange={() => toggleMethodExpansion(method)} | |
| > | |
| <Card className={`border-l-4 border-l-${metadata.color}-500`}> | |
| <CollapsibleTrigger asChild> | |
| <CardHeader className="cursor-pointer hover:bg-muted/50 transition-colors"> | |
| <div className="flex items-center justify-between"> | |
| <div className="flex items-center gap-3"> | |
| <div className={`p-2 bg-${metadata.color}-100 rounded-lg`}> | |
| <IconComponent | |
| className={`h-5 w-5 text-${metadata.color}-600`} | |
| /> | |
| </div> | |
| <div> | |
| <CardTitle className="text-lg">{metadata.name}</CardTitle> | |
| <p className="text-sm text-muted-foreground mt-1"> | |
| {metadata.description} | |
| </p> | |
| </div> | |
| </div> | |
| <div className="flex items-center gap-3"> | |
| <div className="text-right"> | |
| <div className="text-2xl font-bold"> | |
| {avgScore.toFixed(3)} | |
| </div> | |
| <div className="text-xs text-muted-foreground"> | |
| Avg Score | |
| </div> | |
| </div> | |
| <Badge variant="secondary">{results.length} runs</Badge> | |
| {expandedMethods.has(method) ? ( | |
| <ChevronUp className="h-4 w-4" /> | |
| ) : ( | |
| <ChevronDown className="h-4 w-4" /> | |
| )} | |
| </div> | |
| </div> | |
| </CardHeader> | |
| </CollapsibleTrigger> | |
| <CollapsibleContent> | |
| <CardContent className="pt-0"> | |
| {renderMethodDetails(method, latestResult, metadata)} | |
| </CardContent> | |
| </CollapsibleContent> | |
| </Card> | |
| </Collapsible> | |
| ); | |
| }; | |
| const renderMethodDetails = ( | |
| method: string, | |
| result: CausalResult, | |
| _metadata: any | |
| ) => { | |
| const scores = result.raw_analysis.scores; | |
| switch (method) { | |
| case "graph": | |
| return renderGraphAnalysis(scores); | |
| case "component": | |
| return renderComponentAnalysis(scores); | |
| case "dowhy": | |
| return renderDoWhyAnalysis(scores); | |
| case "confounder": | |
| case "mscd": | |
| return renderConfounderAnalysis(scores, method); | |
| case "ate": | |
| return renderATEAnalysis(scores); | |
| default: | |
| return renderGenericAnalysis(scores); | |
| } | |
| }; | |
| const renderGraphAnalysis = (scores: any) => { | |
| const aceData = Object.entries(scores.ACE || {}) | |
| .map(([key, value]) => { | |
| const componentInfo = getComponentInfo(key); | |
| return { | |
| name: componentInfo.name, | |
| value: value as number, | |
| type: componentInfo.type, | |
| description: componentInfo.description, | |
| fullId: key, | |
| }; | |
| }) | |
| .sort((a, b) => Math.abs(b.value) - Math.abs(a.value)) | |
| .slice(0, 10); | |
| const shapleyData = Object.entries(scores.Shapley || {}) | |
| .map(([key, value]) => { | |
| const componentInfo = getComponentInfo(key); | |
| return { | |
| name: componentInfo.name, | |
| value: value as number, | |
| type: componentInfo.type, | |
| description: componentInfo.description, | |
| fullId: key, | |
| }; | |
| }) | |
| .sort((a, b) => Math.abs(b.value) - Math.abs(a.value)) | |
| .slice(0, 10); | |
| return ( | |
| <div className="space-y-6"> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| {/* ACE Scores */} | |
| <div> | |
| <h4 className="text-sm font-semibold mb-3 flex items-center gap-2"> | |
| <TrendingUp className="h-4 w-4" /> | |
| Average Causal Effect (ACE) | |
| </h4> | |
| <div className="h-64"> | |
| <ResponsiveContainer width="100%" height="100%"> | |
| <BarChart data={aceData}> | |
| <CartesianGrid strokeDasharray="3 3" /> | |
| <XAxis | |
| dataKey="name" | |
| angle={-45} | |
| textAnchor="end" | |
| height={60} | |
| fontSize={10} | |
| /> | |
| <YAxis fontSize={10} /> | |
| <Tooltip | |
| labelFormatter={(name, _payload) => { | |
| const item = aceData.find((d) => d.name === name); | |
| return item ? item.description : `Component: ${name}`; | |
| }} | |
| formatter={(value: number) => [ | |
| value.toFixed(4), | |
| "ACE Score", | |
| ]} | |
| /> | |
| <Bar dataKey="value" fill="#8884d8" /> | |
| </BarChart> | |
| </ResponsiveContainer> | |
| </div> | |
| </div> | |
| {/* Shapley Values */} | |
| <div> | |
| <h4 className="text-sm font-semibold mb-3 flex items-center gap-2"> | |
| <Shuffle className="h-4 w-4" /> | |
| Shapley Values | |
| </h4> | |
| <div className="h-64"> | |
| <ResponsiveContainer width="100%" height="100%"> | |
| <BarChart data={shapleyData}> | |
| <CartesianGrid strokeDasharray="3 3" /> | |
| <XAxis | |
| dataKey="name" | |
| angle={-45} | |
| textAnchor="end" | |
| height={60} | |
| fontSize={10} | |
| /> | |
| <YAxis fontSize={10} /> | |
| <Tooltip | |
| labelFormatter={(name, _payload) => { | |
| const item = shapleyData.find((d) => d.name === name); | |
| return item ? item.description : `Component: ${name}`; | |
| }} | |
| formatter={(value: number) => [ | |
| value.toFixed(6), | |
| "Shapley Value", | |
| ]} | |
| /> | |
| <Bar dataKey="value" fill="#82ca9d" /> | |
| </BarChart> | |
| </ResponsiveContainer> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Top Components Summary */} | |
| <div className="mt-4"> | |
| <h4 className="text-sm font-semibold mb-3">Top Causal Components</h4> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| {aceData.slice(0, 5).map((item, index) => ( | |
| <div | |
| key={`ace-${index}`} | |
| className="flex items-center justify-between p-3 bg-muted/50 rounded-lg" | |
| title={item.description} | |
| > | |
| <div className="flex items-center gap-2"> | |
| <Badge | |
| variant={item.type === "Entity" ? "default" : "secondary"} | |
| className="text-xs" | |
| > | |
| {item.type} | |
| </Badge> | |
| <span className="text-sm font-medium">{item.name}</span> | |
| </div> | |
| <div className="text-right"> | |
| <div | |
| className={`text-sm font-bold ${ | |
| item.value >= 0 ? "text-green-600" : "text-red-600" | |
| }`} | |
| > | |
| {item.value >= 0 ? "+" : ""} | |
| {item.value.toFixed(4)} | |
| </div> | |
| <div className="text-xs text-muted-foreground">ACE</div> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| const renderComponentAnalysis = (scores: any) => { | |
| const featureImportance = scores.Feature_Importance || {}; | |
| const modelMetrics = scores.Model_Metrics || {}; | |
| const keyComponents = scores.Key_Components || []; | |
| const importanceData = Object.entries(featureImportance) | |
| .map(([key, value]) => { | |
| const componentInfo = getComponentInfo(key); | |
| return { | |
| name: componentInfo.name, | |
| value: Math.abs(value as number), | |
| direction: (value as number) >= 0 ? "positive" : "negative", | |
| type: componentInfo.type, | |
| description: componentInfo.description, | |
| fullId: key, | |
| }; | |
| }) | |
| .sort((a, b) => b.value - a.value) | |
| .slice(0, 10); | |
| return ( | |
| <div className="space-y-6"> | |
| {/* Model Performance Metrics */} | |
| <div className="grid grid-cols-1 md:grid-cols-3 gap-4"> | |
| <Card className="p-4"> | |
| <div className="text-center"> | |
| <div className="text-2xl font-bold text-blue-600"> | |
| {(modelMetrics.r2 || 0).toFixed(3)} | |
| </div> | |
| <div className="text-sm text-muted-foreground">R² Score</div> | |
| <Progress | |
| value={(modelMetrics.r2 || 0) * 100} | |
| className="mt-2 h-2" | |
| /> | |
| </div> | |
| </Card> | |
| <Card className="p-4"> | |
| <div className="text-center"> | |
| <div className="text-2xl font-bold text-green-600"> | |
| {(modelMetrics.rmse || 0).toFixed(4)} | |
| </div> | |
| <div className="text-sm text-muted-foreground">RMSE</div> | |
| </div> | |
| </Card> | |
| <Card className="p-4"> | |
| <div className="text-center"> | |
| <div className="text-2xl font-bold text-purple-600"> | |
| {keyComponents.length} | |
| </div> | |
| <div className="text-sm text-muted-foreground"> | |
| Key Components | |
| </div> | |
| </div> | |
| </Card> | |
| </div> | |
| {/* Feature Importance Chart */} | |
| <div> | |
| <h4 className="text-sm font-semibold mb-3 flex items-center gap-2"> | |
| <BarChart3 className="h-4 w-4" /> | |
| Feature Importance | |
| </h4> | |
| <div className="h-64"> | |
| <ResponsiveContainer width="100%" height="100%"> | |
| <BarChart data={importanceData}> | |
| <CartesianGrid strokeDasharray="3 3" /> | |
| <XAxis | |
| dataKey="name" | |
| angle={-45} | |
| textAnchor="end" | |
| height={60} | |
| fontSize={10} | |
| /> | |
| <YAxis fontSize={10} /> | |
| <Tooltip | |
| labelFormatter={(name) => `Component: ${name}`} | |
| formatter={(value: number, name, props) => [ | |
| `${value.toFixed(4)} (${props.payload.direction})`, | |
| "Importance", | |
| ]} | |
| /> | |
| <Bar dataKey="value" fill="#8884d8" /> | |
| </BarChart> | |
| </ResponsiveContainer> | |
| </div> | |
| </div> | |
| {/* Key Components List */} | |
| {keyComponents.length > 0 && ( | |
| <div> | |
| <h4 className="text-sm font-semibold mb-3"> | |
| Key Components Identified | |
| </h4> | |
| <div className="flex flex-wrap gap-2"> | |
| {keyComponents.map((component: string, index: number) => { | |
| const componentInfo = getComponentInfo(component); | |
| return ( | |
| <Badge | |
| key={index} | |
| variant="outline" | |
| className="text-xs" | |
| title={componentInfo.description} | |
| > | |
| {componentInfo.name} | |
| </Badge> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| }; | |
| const renderDoWhyAnalysis = (scores: any) => { | |
| const effectEstimates = scores.Effect_Estimate || {}; | |
| const confounders = scores.Confounders || {}; | |
| const interactionEffects = scores.Interaction_Effects || {}; | |
| const effectData = Object.entries(effectEstimates) | |
| .map(([key, value]) => ({ | |
| name: key.replace(/^(entity_|relation_)/, ""), | |
| value: value as number, | |
| type: key.startsWith("entity_") ? "Entity" : "Relation", | |
| })) | |
| .sort((a, b) => Math.abs(b.value) - Math.abs(a.value)) | |
| .slice(0, 10); | |
| const confounderCount = Object.values(confounders).flat().length; | |
| const interactionCount = Object.values(interactionEffects).flat().length; | |
| return ( | |
| <div className="space-y-6"> | |
| {/* Summary Stats */} | |
| <div className="grid grid-cols-1 md:grid-cols-3 gap-4"> | |
| <Card className="p-4"> | |
| <div className="text-center"> | |
| <div className="text-2xl font-bold text-purple-600"> | |
| {Object.keys(effectEstimates).length} | |
| </div> | |
| <div className="text-sm text-muted-foreground"> | |
| Effect Estimates | |
| </div> | |
| </div> | |
| </Card> | |
| <Card className="p-4"> | |
| <div className="text-center"> | |
| <div className="text-2xl font-bold text-orange-600"> | |
| {confounderCount} | |
| </div> | |
| <div className="text-sm text-muted-foreground"> | |
| Confounders Detected | |
| </div> | |
| </div> | |
| </Card> | |
| <Card className="p-4"> | |
| <div className="text-center"> | |
| <div className="text-2xl font-bold text-blue-600"> | |
| {interactionCount} | |
| </div> | |
| <div className="text-sm text-muted-foreground"> | |
| Interaction Effects | |
| </div> | |
| </div> | |
| </Card> | |
| </div> | |
| {/* Effect Estimates Chart */} | |
| <div> | |
| <h4 className="text-sm font-semibold mb-3 flex items-center gap-2"> | |
| <Target className="h-4 w-4" /> | |
| Causal Effect Estimates | |
| </h4> | |
| <div className="h-64"> | |
| <ResponsiveContainer width="100%" height="100%"> | |
| <BarChart data={effectData}> | |
| <CartesianGrid strokeDasharray="3 3" /> | |
| <XAxis | |
| dataKey="name" | |
| angle={-45} | |
| textAnchor="end" | |
| height={60} | |
| fontSize={10} | |
| /> | |
| <YAxis fontSize={10} /> | |
| <Tooltip | |
| labelFormatter={(name) => `Component: ${name}`} | |
| formatter={(value: number) => [ | |
| value.toFixed(4), | |
| "Effect Size", | |
| ]} | |
| /> | |
| <Bar dataKey="value" fill="#8b5cf6" /> | |
| </BarChart> | |
| </ResponsiveContainer> | |
| </div> | |
| </div> | |
| {/* Confounders Summary */} | |
| {confounderCount > 0 && ( | |
| <div> | |
| <h4 className="text-sm font-semibold mb-3 flex items-center gap-2"> | |
| <AlertTriangle className="h-4 w-4" /> | |
| Detected Confounders | |
| </h4> | |
| <div className="space-y-2"> | |
| {Object.entries(confounders) | |
| .slice(0, 5) | |
| .map(([component, confounderList]: [string, any]) => ( | |
| <div | |
| key={component} | |
| className="p-3 bg-orange-50 border border-orange-200 rounded-lg" | |
| > | |
| <div className="font-medium text-sm"> | |
| {component.replace(/^(entity_|relation_)/, "")} | |
| </div> | |
| <div className="text-xs text-muted-foreground mt-1"> | |
| {Array.isArray(confounderList) | |
| ? confounderList.length | |
| : 0}{" "} | |
| confounder(s) detected | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| }; | |
| const renderConfounderAnalysis = (scores: any, method: string) => { | |
| const confounders = scores.Confounders || {}; | |
| const confounderCount = Object.keys(confounders).length; | |
| const affectedComponents = Object.values(confounders).flat().length; | |
| return ( | |
| <div className="space-y-6"> | |
| {/* Summary */} | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <Card className="p-4"> | |
| <div className="text-center"> | |
| <div className="text-2xl font-bold text-orange-600"> | |
| {confounderCount} | |
| </div> | |
| <div className="text-sm text-muted-foreground"> | |
| Confounders Found | |
| </div> | |
| </div> | |
| </Card> | |
| <Card className="p-4"> | |
| <div className="text-center"> | |
| <div className="text-2xl font-bold text-red-600"> | |
| {affectedComponents} | |
| </div> | |
| <div className="text-sm text-muted-foreground"> | |
| Affected Components | |
| </div> | |
| </div> | |
| </Card> | |
| </div> | |
| {confounderCount === 0 ? ( | |
| <Card className="p-6"> | |
| <div className="text-center text-muted-foreground"> | |
| <Search className="h-12 w-12 mx-auto mb-4 opacity-50" /> | |
| <h3 className="text-lg font-medium mb-2"> | |
| No Confounders Detected | |
| </h3> | |
| <p> | |
| The {method === "mscd" ? "multi-signal" : "basic"} analysis | |
| found no significant confounding relationships. | |
| </p> | |
| </div> | |
| </Card> | |
| ) : ( | |
| <div> | |
| <h4 className="text-sm font-semibold mb-3 flex items-center gap-2"> | |
| <AlertTriangle className="h-4 w-4" /> | |
| Confounder Relationships | |
| </h4> | |
| <div className="space-y-2"> | |
| {Object.entries(confounders).map( | |
| ([component, _details]: [string, any]) => ( | |
| <div | |
| key={component} | |
| className="p-3 bg-orange-50 border border-orange-200 rounded-lg" | |
| > | |
| <div className="font-medium text-sm"> | |
| {component.replace(/^(entity_|relation_)/, "")} | |
| </div> | |
| <div className="text-xs text-muted-foreground mt-1"> | |
| Potential confounding detected • Impact analysis available | |
| </div> | |
| </div> | |
| ) | |
| )} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| }; | |
| const renderATEAnalysis = (scores: any) => { | |
| const effectStrengths = scores.Effect_Strengths || {}; | |
| const ateResults = scores.ATE_Results || {}; | |
| const grangerResults = scores.Granger_Results || {}; | |
| const strengthData = Object.entries(effectStrengths) | |
| .map(([key, value]) => ({ | |
| name: key.replace(/^(entity_|relation_)/, ""), | |
| value: Math.abs(value as number), | |
| direction: (value as number) >= 0 ? "positive" : "negative", | |
| type: key.startsWith("entity_") ? "Entity" : "Relation", | |
| })) | |
| .sort((a, b) => b.value - a.value) | |
| .slice(0, 10); | |
| const significantEffects = Object.entries(ateResults).filter( | |
| ([_, result]: [string, any]) => Math.abs(result.ate || 0) > 0.01 | |
| ).length; | |
| const causalDirections: Record<string, number> = Object.values( | |
| grangerResults | |
| ).reduce((acc: Record<string, number>, result: any) => { | |
| const direction = result.causal_direction || "none"; | |
| acc[direction] = (acc[direction] || 0) + 1; | |
| return acc; | |
| }, {}); | |
| return ( | |
| <div className="space-y-6"> | |
| {/* Summary Metrics */} | |
| <div className="grid grid-cols-1 md:grid-cols-3 gap-4"> | |
| <Card className="p-4"> | |
| <div className="text-center"> | |
| <div className="text-2xl font-bold text-indigo-600"> | |
| {Object.keys(effectStrengths).length} | |
| </div> | |
| <div className="text-sm text-muted-foreground"> | |
| Components Analyzed | |
| </div> | |
| </div> | |
| </Card> | |
| <Card className="p-4"> | |
| <div className="text-center"> | |
| <div className="text-2xl font-bold text-green-600"> | |
| {significantEffects} | |
| </div> | |
| <div className="text-sm text-muted-foreground"> | |
| Significant Effects | |
| </div> | |
| </div> | |
| </Card> | |
| <Card className="p-4"> | |
| <div className="text-center"> | |
| <div className="text-2xl font-bold text-purple-600"> | |
| {causalDirections.bidirectional || 0} | |
| </div> | |
| <div className="text-sm text-muted-foreground"> | |
| Bidirectional Effects | |
| </div> | |
| </div> | |
| </Card> | |
| </div> | |
| {/* Effect Strengths Chart */} | |
| <div> | |
| <h4 className="text-sm font-semibold mb-3 flex items-center gap-2"> | |
| <GitBranch className="h-4 w-4" /> | |
| Treatment Effect Strengths | |
| </h4> | |
| <div className="h-64"> | |
| <ResponsiveContainer width="100%" height="100%"> | |
| <BarChart data={strengthData}> | |
| <CartesianGrid strokeDasharray="3 3" /> | |
| <XAxis | |
| dataKey="name" | |
| angle={-45} | |
| textAnchor="end" | |
| height={60} | |
| fontSize={10} | |
| /> | |
| <YAxis fontSize={10} /> | |
| <Tooltip | |
| labelFormatter={(name) => `Component: ${name}`} | |
| formatter={(value: number, name, props) => [ | |
| `${value.toFixed(4)} (${props.payload.direction})`, | |
| "Effect Strength", | |
| ]} | |
| /> | |
| <Bar dataKey="value" fill="#8884d8" /> | |
| </BarChart> | |
| </ResponsiveContainer> | |
| </div> | |
| </div> | |
| {/* Causal Direction Distribution */} | |
| {Object.keys(causalDirections).length > 0 && ( | |
| <div> | |
| <h4 className="text-sm font-semibold mb-3 flex items-center gap-2"> | |
| <Shuffle className="h-4 w-4" /> | |
| Causal Direction Distribution | |
| </h4> | |
| <div className="grid grid-cols-1 md:grid-cols-3 gap-4"> | |
| {Object.entries(causalDirections).map( | |
| ([direction, count]: [string, any]) => ( | |
| <div | |
| key={direction} | |
| className="p-3 bg-muted/50 rounded-lg text-center" | |
| > | |
| <div className="text-2xl font-bold">{count}</div> | |
| <div className="text-sm text-muted-foreground capitalize"> | |
| {direction} | |
| </div> | |
| </div> | |
| ) | |
| )} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| }; | |
| const renderGenericAnalysis = (scores: any) => { | |
| return ( | |
| <div className="space-y-4"> | |
| {Object.entries(scores).map(([key, value]) => ( | |
| <div key={key} className="p-3 bg-muted/50 rounded-lg"> | |
| <div className="font-medium text-sm mb-2"> | |
| {key.replace(/_/g, " ")} | |
| </div> | |
| <pre className="text-xs text-muted-foreground overflow-auto max-h-32"> | |
| {JSON.stringify(value, null, 2)} | |
| </pre> | |
| </div> | |
| ))} | |
| </div> | |
| ); | |
| }; | |
| return ( | |
| <div className="space-y-6"> | |
| {/* Header */} | |
| <Card className="border-l-4 border-l-indigo-500"> | |
| <CardHeader> | |
| <CardTitle className="flex items-center gap-2"> | |
| <Brain className="h-5 w-5 text-indigo-600" /> | |
| Causal Analysis Results | |
| </CardTitle> | |
| </CardHeader> | |
| <CardContent> | |
| {/* Overall Summary */} | |
| <div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6"> | |
| <div className="text-center p-4 bg-blue-50 border border-blue-200 rounded-lg"> | |
| <div className="text-2xl font-bold text-blue-600"> | |
| {overallStats.totalMethods} | |
| </div> | |
| <div className="text-sm text-blue-700">Analysis Methods</div> | |
| </div> | |
| <div className="text-center p-4 bg-green-50 border border-green-200 rounded-lg"> | |
| <div className="text-2xl font-bold text-green-600"> | |
| {overallStats.avgCausalScore.toFixed(3)} | |
| </div> | |
| <div className="text-sm text-green-700">Avg Causal Score</div> | |
| </div> | |
| <div className="text-center p-4 bg-purple-50 border border-purple-200 rounded-lg"> | |
| <div className="text-2xl font-bold text-purple-600"> | |
| {overallStats.maxCausalScore.toFixed(3)} | |
| </div> | |
| <div className="text-sm text-purple-700">Max Causal Score</div> | |
| </div> | |
| <div className="text-center p-4 bg-orange-50 border border-orange-200 rounded-lg"> | |
| <div className="text-2xl font-bold text-orange-600"> | |
| {overallStats.activeComponents} | |
| </div> | |
| <div className="text-sm text-orange-700">Active Components</div> | |
| </div> | |
| </div> | |
| {/* Perturbation Set Selector */} | |
| {perturbationSets.length > 1 && ( | |
| <div className="mb-6"> | |
| <h4 className="text-sm font-semibold mb-3"> | |
| Perturbation Sets | |
| {selectedSet && ( | |
| <span className="ml-2 text-xs font-normal text-muted-foreground"> | |
| (Showing results for:{" "} | |
| {getPerturbationSetDisplayName(selectedSet)}) | |
| </span> | |
| )} | |
| </h4> | |
| <div className="flex flex-wrap gap-2"> | |
| <Button | |
| variant={selectedSet === null ? "default" : "outline"} | |
| size="sm" | |
| onClick={() => setSelectedSet(null)} | |
| className={selectedSet === null ? "ring-2 ring-blue-500" : ""} | |
| > | |
| All Sets | |
| </Button> | |
| {perturbationSets.map((setId) => ( | |
| <Button | |
| key={setId} | |
| variant={selectedSet === setId ? "default" : "outline"} | |
| size="sm" | |
| onClick={() => setSelectedSet(setId)} | |
| className={ | |
| selectedSet === setId ? "ring-2 ring-blue-500" : "" | |
| } | |
| title={`${getPerturbationSetDisplayName(setId)} (${setId})`} | |
| > | |
| {getPerturbationSetDisplayName(setId)} | |
| </Button> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| </CardContent> | |
| </Card> | |
| {/* Method Results */} | |
| <div className="space-y-4"> | |
| {Object.keys(resultsByMethod).length === 0 ? ( | |
| <Card className="border-l-4 border-l-orange-500"> | |
| <CardContent className="py-8"> | |
| <div className="text-center text-muted-foreground"> | |
| <AlertTriangle className="h-12 w-12 mx-auto mb-4 opacity-50" /> | |
| <h3 className="text-lg font-medium mb-2"> | |
| No Results for Selected Set | |
| </h3> | |
| <p> | |
| {selectedSet | |
| ? `No causal analysis results found for "${getPerturbationSetDisplayName( | |
| selectedSet | |
| )}". Try selecting a different set or view all sets.` | |
| : "No causal analysis results are available."} | |
| </p> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| ) : ( | |
| Object.entries(resultsByMethod).map(([method, results]) => | |
| renderMethodSummary(method, results) | |
| ) | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| export default CausalResults; | |