wu981526092's picture
add
9c5dc16
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>
);
};