Spaces:
Sleeping
Sleeping
| import React, { useEffect, useMemo, useState } from "react"; | |
| import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; | |
| import { Badge } from "@/components/ui/badge"; | |
| import { Progress } from "@/components/ui/progress"; | |
| import { | |
| Accordion, | |
| AccordionContent, | |
| AccordionItem, | |
| AccordionTrigger, | |
| } from "@/components/ui/accordion"; | |
| import { BarChart3, Users, Network, GitBranch, TrendingUp, Lightbulb } from "lucide-react"; | |
| import { GraphComparisonResults, GraphDetailsResponse } from "@/types"; | |
| import { api } from "@/lib/api"; | |
| // Prefer system_name when available | |
| const getGraphDisplayName = (g?: { filename: string; system_name?: string }) => { | |
| if (!g) return ""; | |
| return g.system_name && g.system_name.trim().length > 0 ? g.system_name : g.filename; | |
| }; | |
| interface ComparisonResultsProps { | |
| results: GraphComparisonResults; | |
| } | |
| export const ComparisonResults: React.FC<ComparisonResultsProps> = ({ | |
| results, | |
| }) => { | |
| const graph1_info = results.metadata?.graph1; | |
| const graph2_info = results.metadata?.graph2; | |
| const [graph1Details, setGraph1Details] = useState<GraphDetailsResponse | null>(null); | |
| const [graph2Details, setGraph2Details] = useState<GraphDetailsResponse | null>(null); | |
| useEffect(() => { | |
| const loadDetails = async () => { | |
| try { | |
| if (graph1_info?.id) setGraph1Details(await api.graphComparison.getGraphDetails(graph1_info.id)); | |
| if (graph2_info?.id) setGraph2Details(await api.graphComparison.getGraphDetails(graph2_info.id)); | |
| } catch (e) { | |
| console.warn("ComparisonResults: failed to load graph details", e); | |
| } | |
| }; | |
| loadDetails(); | |
| }, [graph1_info?.id, graph2_info?.id]); | |
| const entityExamples = useMemo(() => { | |
| const createKey = (e: any) => `${(e?.type || "").toLowerCase()} ${(e?.name || "").toLowerCase()}`.trim(); | |
| const toLabel = (e: any) => `${e?.name || "(unnamed)"}${e?.type ? ` (${e.type})` : ""}`; | |
| const e1 = graph1Details?.entities || []; | |
| const e2 = graph2Details?.entities || []; | |
| const set1 = new Map<string, any>(); | |
| const set2 = new Map<string, any>(); | |
| e1.forEach((x) => set1.set(createKey(x), x)); | |
| e2.forEach((x) => set2.set(createKey(x), x)); | |
| const overlap: string[] = []; | |
| const unique1: string[] = []; | |
| const unique2: string[] = []; | |
| for (const [k, v] of set1) { if (k && set2.has(k)) overlap.push(toLabel(v)); else unique1.push(toLabel(v)); } | |
| for (const [k, v] of set2) { if (!k) continue; if (!set1.has(k)) unique2.push(toLabel(v)); } | |
| return { overlap: overlap.slice(0, 8), unique1: unique1.slice(0, 8), unique2: unique2.slice(0, 8) }; | |
| }, [graph1Details, graph2Details]); | |
| const relationExamples = useMemo(() => { | |
| const createKey = (r: any) => `${(r?.type || "").toLowerCase()} ${(r?.description || "").toLowerCase()}`.trim(); | |
| const toLabel = (r: any) => `${r?.type || "RELATION"}${r?.description ? ` — ${r.description}` : ""}`; | |
| const r1 = graph1Details?.relations || []; | |
| const r2 = graph2Details?.relations || []; | |
| const set1 = new Map<string, any>(); | |
| const set2 = new Map<string, any>(); | |
| r1.forEach((x) => set1.set(createKey(x), x)); | |
| r2.forEach((x) => set2.set(createKey(x), x)); | |
| const overlap: string[] = []; | |
| const unique1: string[] = []; | |
| const unique2: string[] = []; | |
| for (const [k, v] of set1) { if (k && set2.has(k)) overlap.push(toLabel(v)); else unique1.push(toLabel(v)); } | |
| for (const [k, v] of set2) { if (!k) continue; if (!set1.has(k)) unique2.push(toLabel(v)); } | |
| return { overlap: overlap.slice(0, 8), unique1: unique1.slice(0, 8), unique2: unique2.slice(0, 8) }; | |
| }, [graph1Details, graph2Details]); | |
| // Get entity and relation counts from the actual metrics | |
| const graph1_entity_count = | |
| results.entity_metrics.unique_to_graph1 + | |
| results.entity_metrics.overlap_count; | |
| const graph2_entity_count = | |
| results.entity_metrics.unique_to_graph2 + | |
| results.entity_metrics.overlap_count; | |
| const graph1_relation_count = | |
| results.relation_metrics.unique_to_graph1 + | |
| results.relation_metrics.overlap_count; | |
| const graph2_relation_count = | |
| results.relation_metrics.unique_to_graph2 + | |
| results.relation_metrics.overlap_count; | |
| const formatPercentage = (value: number | undefined | null) => { | |
| if (value === undefined || value === null || isNaN(value)) { | |
| return "N/A"; | |
| } | |
| return `${(value * 100).toFixed(1)}%`; | |
| }; | |
| const formatNumber = ( | |
| value: number | undefined | null, | |
| decimals: number = 3 | |
| ) => { | |
| if (value === undefined || value === null || isNaN(value)) { | |
| return "N/A"; | |
| } | |
| return value.toFixed(decimals); | |
| }; | |
| const safeValue = ( | |
| value: number | undefined | null, | |
| defaultValue: number = 0 | |
| ) => { | |
| if (value === undefined || value === null || isNaN(value)) { | |
| return defaultValue; | |
| } | |
| return value; | |
| }; | |
| const getScoreColor = (score: number | undefined | null) => { | |
| const safeScore = safeValue(score); | |
| if (safeScore >= 0.8) return "text-green-600"; | |
| if (safeScore >= 0.6) return "text-yellow-600"; | |
| return "text-red-600"; | |
| }; | |
| const getScoreVariant = (score: number | undefined | null) => { | |
| const safeScore = safeValue(score); | |
| if (safeScore >= 0.8) return "default"; | |
| if (safeScore >= 0.6) return "secondary"; | |
| return "destructive"; | |
| }; | |
| // Handle case where metadata is missing | |
| if (!graph1_info || !graph2_info) { | |
| return ( | |
| <div className="flex items-center justify-center p-8"> | |
| <p className="text-muted-foreground">Graph information not available</p> | |
| </div> | |
| ); | |
| } | |
| return ( | |
| <div className="space-y-6"> | |
| {/* Key Insights */} | |
| <Card> | |
| <CardHeader> | |
| <CardTitle className="flex items-center gap-2"> | |
| <Lightbulb className="h-5 w-5" /> | |
| Key Insights | |
| </CardTitle> | |
| </CardHeader> | |
| <CardContent> | |
| <ul className="text-sm space-y-2 list-disc pl-4"> | |
| <li> | |
| Entities overlap {formatPercentage(results.entity_metrics.overlap_ratio)} • shared {results.entity_metrics.overlap_count}; unique G1 {results.entity_metrics.unique_to_graph1}, G2 {results.entity_metrics.unique_to_graph2}. | |
| </li> | |
| <li> | |
| Relations overlap {formatPercentage(results.relation_metrics.overlap_ratio)} • shared {results.relation_metrics.overlap_count}; unique G1 {results.relation_metrics.unique_to_graph1}, G2 {results.relation_metrics.unique_to_graph2}. | |
| </li> | |
| <li> | |
| Structural density difference {formatNumber(Math.abs(results.structural_metrics.density_difference), 3)} • common patterns {results.structural_metrics.common_patterns_count}. | |
| </li> | |
| </ul> | |
| </CardContent> | |
| </Card> | |
| {/* Header Summary */} | |
| <Card> | |
| <CardHeader> | |
| <CardTitle className="flex items-center gap-2"> | |
| <BarChart3 className="h-5 w-5" /> | |
| Comparison Overview | |
| </CardTitle> | |
| </CardHeader> | |
| <CardContent> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| {/* Graph 1 Info */} | |
| <div className="space-y-2"> | |
| <h3 className="font-medium text-sm text-muted-foreground"> | |
| Graph 1 | |
| </h3> | |
| <div className="p-3 bg-muted/50 rounded-lg"> | |
| <h4 | |
| className="font-medium whitespace-normal break-words" | |
| title={getGraphDisplayName(graph1_info)} | |
| > | |
| {getGraphDisplayName(graph1_info)} | |
| </h4> | |
| <div className="text-sm text-muted-foreground mt-1"> | |
| {graph1_entity_count} entities • {graph1_relation_count}{" "} | |
| relations | |
| </div> | |
| </div> | |
| </div> | |
| {/* Graph 2 Info */} | |
| <div className="space-y-2"> | |
| <h3 className="font-medium text-sm text-muted-foreground"> | |
| Graph 2 | |
| </h3> | |
| <div className="p-3 bg-muted/50 rounded-lg"> | |
| <h4 | |
| className="font-medium whitespace-normal break-words" | |
| title={getGraphDisplayName(graph2_info)} | |
| > | |
| {getGraphDisplayName(graph2_info)} | |
| </h4> | |
| <div className="text-sm text-muted-foreground mt-1"> | |
| {graph2_entity_count} entities • {graph2_relation_count}{" "} | |
| relations | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Overall Similarity Scores */} | |
| <div className="mt-6 grid grid-cols-1 md:grid-cols-3 gap-4"> | |
| <div className="text-center p-4 bg-muted/30 rounded-lg"> | |
| <div | |
| className={`text-2xl font-bold ${getScoreColor( | |
| results.overall_metrics.structural_similarity | |
| )}`} | |
| > | |
| {formatPercentage( | |
| results.overall_metrics.structural_similarity | |
| )} | |
| </div> | |
| <div className="text-sm text-muted-foreground">Structural</div> | |
| </div> | |
| <div className="text-center p-4 bg-muted/30 rounded-lg"> | |
| <div | |
| className={`text-2xl font-bold ${getScoreColor( | |
| results.overall_metrics.content_similarity | |
| )}`} | |
| > | |
| {formatPercentage(results.overall_metrics.content_similarity)} | |
| </div> | |
| <div className="text-sm text-muted-foreground">Content</div> | |
| </div> | |
| <div className="text-center p-4 bg-muted/30 rounded-lg"> | |
| <div | |
| className={`text-2xl font-bold ${getScoreColor( | |
| results.overall_metrics.overall_similarity | |
| )}`} | |
| > | |
| {formatPercentage(results.overall_metrics.overall_similarity)} | |
| </div> | |
| <div className="text-sm text-muted-foreground">Overall</div> | |
| </div> | |
| </div> | |
| </CardContent> | |
| </Card> | |
| {/* Detailed Metrics */} | |
| <Card> | |
| <CardHeader> | |
| <CardTitle>Detailed Analysis</CardTitle> | |
| </CardHeader> | |
| <CardContent> | |
| <Accordion type="single" collapsible className="w-full"> | |
| {/* Entity Metrics */} | |
| <AccordionItem value="entities"> | |
| <AccordionTrigger className="hover:no-underline"> | |
| <div className="flex items-center gap-2"> | |
| <Users className="h-4 w-4" /> | |
| <span>Entity Analysis</span> | |
| <Badge | |
| variant={getScoreVariant( | |
| results.entity_metrics.semantic_similarity | |
| )} | |
| > | |
| {formatPercentage( | |
| results.entity_metrics.semantic_similarity | |
| )} | |
| </Badge> | |
| </div> | |
| </AccordionTrigger> | |
| <AccordionContent> | |
| <div className="space-y-4"> | |
| <div className="grid grid-cols-2 md:grid-cols-4 gap-4"> | |
| <div className="text-center p-3 bg-green-50 rounded-lg"> | |
| <div className="text-lg font-bold text-green-600"> | |
| {results.entity_metrics.overlap_count} | |
| </div> | |
| <div className="text-xs text-muted-foreground"> | |
| Overlapping | |
| </div> | |
| </div> | |
| <div className="text-center p-3 bg-blue-50 rounded-lg"> | |
| <div className="text-lg font-bold text-blue-600"> | |
| {results.entity_metrics.unique_to_graph1} | |
| </div> | |
| <div className="text-xs text-muted-foreground"> | |
| Unique to Graph 1 | |
| </div> | |
| </div> | |
| <div className="text-center p-3 bg-purple-50 rounded-lg"> | |
| <div className="text-lg font-bold text-purple-600"> | |
| {results.entity_metrics.unique_to_graph2} | |
| </div> | |
| <div className="text-xs text-muted-foreground"> | |
| Unique to Graph 2 | |
| </div> | |
| </div> | |
| <div className="text-center p-3 bg-muted/50 rounded-lg"> | |
| <div className="text-lg font-bold"> | |
| {formatPercentage(results.entity_metrics.overlap_ratio)} | |
| </div> | |
| <div className="text-xs text-muted-foreground"> | |
| Overlap Ratio | |
| </div> | |
| </div> | |
| </div> | |
| <div> | |
| <div className="flex justify-between text-sm mb-2"> | |
| <span>Semantic Similarity</span> | |
| <span | |
| className={getScoreColor( | |
| results.entity_metrics.semantic_similarity | |
| )} | |
| > | |
| {formatPercentage( | |
| results.entity_metrics.semantic_similarity | |
| )} | |
| </span> | |
| </div> | |
| <Progress | |
| value={ | |
| safeValue(results.entity_metrics.semantic_similarity) * | |
| 100 | |
| } | |
| className="h-2" | |
| /> | |
| </div> | |
| {/* Examples */} | |
| {(entityExamples.overlap.length || entityExamples.unique1.length || entityExamples.unique2.length) ? ( | |
| <div className="grid grid-cols-1 md:grid-cols-3 gap-4"> | |
| <div> | |
| <div className="text-xs font-semibold mb-2">Common examples</div> | |
| <div className="flex flex-wrap gap-1"> | |
| {entityExamples.overlap.map((e, i) => ( | |
| <Badge key={`e-ov-${i}`} variant="secondary">{e}</Badge> | |
| ))} | |
| {!entityExamples.overlap.length && <span className="text-xs text-muted-foreground">None</span>} | |
| </div> | |
| </div> | |
| <div> | |
| <div className="text-xs font-semibold mb-2">Unique to Graph 1</div> | |
| <div className="flex flex-wrap gap-1"> | |
| {entityExamples.unique1.map((e, i) => ( | |
| <Badge key={`e-u1-${i}`} variant="outline">{e}</Badge> | |
| ))} | |
| {!entityExamples.unique1.length && <span className="text-xs text-muted-foreground">None</span>} | |
| </div> | |
| </div> | |
| <div> | |
| <div className="text-xs font-semibold mb-2">Unique to Graph 2</div> | |
| <div className="flex flex-wrap gap-1"> | |
| {entityExamples.unique2.map((e, i) => ( | |
| <Badge key={`e-u2-${i}`} variant="outline">{e}</Badge> | |
| ))} | |
| {!entityExamples.unique2.length && <span className="text-xs text-muted-foreground">None</span>} | |
| </div> | |
| </div> | |
| </div> | |
| ) : null} | |
| </div> | |
| </AccordionContent> | |
| </AccordionItem> | |
| {/* Relation Metrics */} | |
| <AccordionItem value="relations"> | |
| <AccordionTrigger className="hover:no-underline"> | |
| <div className="flex items-center gap-2"> | |
| <Network className="h-4 w-4" /> | |
| <span>Relation Analysis</span> | |
| <Badge | |
| variant={getScoreVariant( | |
| results.relation_metrics.semantic_similarity | |
| )} | |
| > | |
| {formatPercentage( | |
| results.relation_metrics.semantic_similarity | |
| )} | |
| </Badge> | |
| </div> | |
| </AccordionTrigger> | |
| <AccordionContent> | |
| <div className="space-y-4"> | |
| <div className="grid grid-cols-2 md:grid-cols-4 gap-4"> | |
| <div className="text-center p-3 bg-green-50 rounded-lg"> | |
| <div className="text-lg font-bold text-green-600"> | |
| {results.relation_metrics.overlap_count} | |
| </div> | |
| <div className="text-xs text-muted-foreground"> | |
| Overlapping | |
| </div> | |
| </div> | |
| <div className="text-center p-3 bg-blue-50 rounded-lg"> | |
| <div className="text-lg font-bold text-blue-600"> | |
| {results.relation_metrics.unique_to_graph1} | |
| </div> | |
| <div className="text-xs text-muted-foreground"> | |
| Unique to Graph 1 | |
| </div> | |
| </div> | |
| <div className="text-center p-3 bg-purple-50 rounded-lg"> | |
| <div className="text-lg font-bold text-purple-600"> | |
| {results.relation_metrics.unique_to_graph2} | |
| </div> | |
| <div className="text-xs text-muted-foreground"> | |
| Unique to Graph 2 | |
| </div> | |
| </div> | |
| <div className="text-center p-3 bg-muted/50 rounded-lg"> | |
| <div className="text-lg font-bold"> | |
| {formatPercentage( | |
| results.relation_metrics.overlap_ratio | |
| )} | |
| </div> | |
| <div className="text-xs text-muted-foreground"> | |
| Overlap Ratio | |
| </div> | |
| </div> | |
| </div> | |
| <div> | |
| <div className="flex justify-between text-sm mb-2"> | |
| <span>Semantic Similarity</span> | |
| <span | |
| className={getScoreColor( | |
| results.relation_metrics.semantic_similarity | |
| )} | |
| > | |
| {formatPercentage( | |
| results.relation_metrics.semantic_similarity | |
| )} | |
| </span> | |
| </div> | |
| <Progress | |
| value={ | |
| safeValue( | |
| results.relation_metrics.semantic_similarity | |
| ) * 100 | |
| } | |
| className="h-2" | |
| /> | |
| </div> | |
| {/* Examples */} | |
| {(relationExamples.overlap.length || relationExamples.unique1.length || relationExamples.unique2.length) ? ( | |
| <div className="grid grid-cols-1 md:grid-cols-3 gap-4"> | |
| <div> | |
| <div className="text-xs font-semibold mb-2">Common examples</div> | |
| <div className="flex flex-wrap gap-1"> | |
| {relationExamples.overlap.map((e, i) => ( | |
| <Badge key={`r-ov-${i}`} variant="secondary">{e}</Badge> | |
| ))} | |
| {!relationExamples.overlap.length && <span className="text-xs text-muted-foreground">None</span>} | |
| </div> | |
| </div> | |
| <div> | |
| <div className="text-xs font-semibold mb-2">Unique to Graph 1</div> | |
| <div className="flex flex-wrap gap-1"> | |
| {relationExamples.unique1.map((e, i) => ( | |
| <Badge key={`r-u1-${i}`} variant="outline">{e}</Badge> | |
| ))} | |
| {!relationExamples.unique1.length && <span className="text-xs text-muted-foreground">None</span>} | |
| </div> | |
| </div> | |
| <div> | |
| <div className="text-xs font-semibold mb-2">Unique to Graph 2</div> | |
| <div className="flex flex-wrap gap-1"> | |
| {relationExamples.unique2.map((e, i) => ( | |
| <Badge key={`r-u2-${i}`} variant="outline">{e}</Badge> | |
| ))} | |
| {!relationExamples.unique2.length && <span className="text-xs text-muted-foreground">None</span>} | |
| </div> | |
| </div> | |
| </div> | |
| ) : null} | |
| </div> | |
| </AccordionContent> | |
| </AccordionItem> | |
| {/* Structural Metrics */} | |
| <AccordionItem value="structural"> | |
| <AccordionTrigger className="hover:no-underline"> | |
| <div className="flex items-center gap-2"> | |
| <GitBranch className="h-4 w-4" /> | |
| <span>Structural Analysis</span> | |
| <Badge | |
| variant={getScoreVariant( | |
| results.overall_metrics.structural_similarity | |
| )} | |
| > | |
| {formatPercentage( | |
| results.overall_metrics.structural_similarity | |
| )} | |
| </Badge> | |
| </div> | |
| </AccordionTrigger> | |
| <AccordionContent> | |
| <div className="space-y-4"> | |
| <div className="grid grid-cols-1 md:grid-cols-3 gap-4"> | |
| <div className="p-3 bg-muted/50 rounded-lg"> | |
| <div className="text-sm text-muted-foreground"> | |
| Graph 1 Density | |
| </div> | |
| <div className="text-lg font-bold"> | |
| {formatNumber( | |
| results.structural_metrics.graph1_density, | |
| 3 | |
| )} | |
| </div> | |
| </div> | |
| <div className="p-3 bg-muted/50 rounded-lg"> | |
| <div className="text-sm text-muted-foreground"> | |
| Graph 2 Density | |
| </div> | |
| <div className="text-lg font-bold"> | |
| {formatNumber( | |
| results.structural_metrics.graph2_density, | |
| 3 | |
| )} | |
| </div> | |
| </div> | |
| <div className="p-3 bg-muted/50 rounded-lg"> | |
| <div className="text-sm text-muted-foreground"> | |
| Density Difference | |
| </div> | |
| <div className="text-lg font-bold"> | |
| {formatNumber( | |
| Math.abs( | |
| safeValue( | |
| results.structural_metrics.density_difference | |
| ) | |
| ), | |
| 3 | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| <div className="p-3 bg-muted/50 rounded-lg"> | |
| <div className="text-sm text-muted-foreground"> | |
| Common Patterns | |
| </div> | |
| <div className="text-lg font-bold"> | |
| {results.structural_metrics.common_patterns_count} | |
| </div> | |
| </div> | |
| </div> | |
| </AccordionContent> | |
| </AccordionItem> | |
| {/* Type Distribution */} | |
| <AccordionItem value="types"> | |
| <AccordionTrigger className="hover:no-underline"> | |
| <div className="flex items-center gap-2"> | |
| <TrendingUp className="h-4 w-4" /> | |
| <span>Type Distribution</span> | |
| </div> | |
| </AccordionTrigger> | |
| <AccordionContent> | |
| <div className="space-y-4"> | |
| <div> | |
| <div className="flex justify-between text-sm mb-2"> | |
| <span>Entity Type Similarity</span> | |
| <span | |
| className={getScoreColor( | |
| results.type_distribution_metrics | |
| .entity_type_similarity | |
| )} | |
| > | |
| {formatPercentage( | |
| results.type_distribution_metrics | |
| .entity_type_similarity | |
| )} | |
| </span> | |
| </div> | |
| <Progress | |
| value={ | |
| safeValue( | |
| results.type_distribution_metrics | |
| .entity_type_similarity | |
| ) * 100 | |
| } | |
| className="h-2" | |
| /> | |
| </div> | |
| <div> | |
| <div className="flex justify-between text-sm mb-2"> | |
| <span>Relation Type Similarity</span> | |
| <span | |
| className={getScoreColor( | |
| results.type_distribution_metrics | |
| .relation_type_similarity | |
| )} | |
| > | |
| {formatPercentage( | |
| results.type_distribution_metrics | |
| .relation_type_similarity | |
| )} | |
| </span> | |
| </div> | |
| <Progress | |
| value={ | |
| safeValue( | |
| results.type_distribution_metrics | |
| .relation_type_similarity | |
| ) * 100 | |
| } | |
| className="h-2" | |
| /> | |
| </div> | |
| </div> | |
| </AccordionContent> | |
| </AccordionItem> | |
| </Accordion> | |
| </CardContent> | |
| </Card> | |
| {/* Comparison Metadata */} | |
| <Card> | |
| <CardHeader> | |
| <CardTitle>Comparison Details</CardTitle> | |
| </CardHeader> | |
| <CardContent> | |
| <div className="text-sm text-muted-foreground"> | |
| Comparison completed at:{" "} | |
| {results.metadata?.comparison_timestamp | |
| ? new Date(results.metadata.comparison_timestamp).toLocaleString() | |
| : "Unknown"} | |
| </div> | |
| </CardContent> | |
| </Card> | |
| </div> | |
| ); | |
| }; | |