wu981526092's picture
add
b9fa64b
import React, { useState } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import {
Database,
ChevronDown,
Clock,
Activity,
GitBranch,
GitCompare,
} from "lucide-react";
import { AvailableGraph } from "@/types";
interface GraphSelectorProps {
availableGraphs: AvailableGraph[];
selectedGraph1: AvailableGraph | null;
selectedGraph2: AvailableGraph | null;
onSelectionChange: (
graph1: AvailableGraph | null,
graph2: AvailableGraph | null
) => void;
onCompareGraphs: () => Promise<void>;
isLoading: boolean;
}
export const GraphSelector: React.FC<GraphSelectorProps> = ({
availableGraphs,
selectedGraph1,
selectedGraph2,
onSelectionChange,
onCompareGraphs,
isLoading,
}) => {
const [expandedGraphs, setExpandedGraphs] = useState<Set<number>>(new Set());
const toggleGraphExpansion = (graphId: number) => {
const newExpanded = new Set(expandedGraphs);
if (newExpanded.has(graphId)) {
newExpanded.delete(graphId);
} else {
newExpanded.add(graphId);
}
setExpandedGraphs(newExpanded);
};
const handleGraphSelect = (graph: AvailableGraph) => {
// If clicking on already selected graph, deselect it
if (selectedGraph1?.id === graph.id) {
onSelectionChange(null, selectedGraph2);
return;
}
if (selectedGraph2?.id === graph.id) {
onSelectionChange(selectedGraph1, null);
return;
}
// Select in first available slot
if (!selectedGraph1) {
onSelectionChange(graph, selectedGraph2);
} else if (!selectedGraph2) {
onSelectionChange(selectedGraph1, graph);
} else {
// Both slots filled, replace the first one
onSelectionChange(graph, selectedGraph2);
}
};
const clearAllSelections = () => {
onSelectionChange(null, null);
};
const getSelectionClass = (graph: AvailableGraph) => {
if (selectedGraph1?.id === graph.id) return "selected selected-1";
if (selectedGraph2?.id === graph.id) return "selected selected-2";
return "";
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString();
};
const getGraphTypeIcon = (type: string) => {
switch (type) {
case "final":
return Activity;
case "chunk":
return Clock;
default:
return GitBranch;
}
};
const truncateGraphName = (
name: string,
maxLength: number = 35
): string => {
if (!name) return "";
if (name.length <= maxLength) return name;
// Smart truncation - keep beginning and end if possible
const start = name.substring(0, Math.floor(maxLength * 0.6));
const end = name.substring(name.length - Math.floor(maxLength * 0.3));
return `${start}...${end}`;
};
// Prefer a human-friendly system name if available
const getGraphDisplayName = (graph: AvailableGraph | null | undefined) => {
if (!graph) return "";
return graph.system_name && graph.system_name.trim().length > 0
? graph.system_name
: graph.filename;
};
const getSelectionNumber = (graph: AvailableGraph) => {
if (selectedGraph1?.id === graph.id) return "1";
if (selectedGraph2?.id === graph.id) return "2";
return null;
};
const canCompare = selectedGraph1 && selectedGraph2;
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold">Select Graphs to Compare</h2>
<p className="text-sm text-muted-foreground">
Choose exactly 2 knowledge graphs for comparison
</p>
</div>
<div className="flex items-center gap-2">
{canCompare && (
<Button
onClick={onCompareGraphs}
disabled={isLoading}
className="flex items-center gap-2"
>
<GitCompare className="h-4 w-4" />
{isLoading ? "Comparing..." : "Compare Graphs"}
</Button>
)}
{(selectedGraph1 || selectedGraph2) && (
<Button variant="outline" onClick={clearAllSelections}>
Clear Selection
</Button>
)}
</div>
</div>
{/* Selection Indicators */}
<div className="grid grid-cols-2 gap-4">
<Card
className={`transition-all ${
selectedGraph1 ? "ring-2 ring-blue-500" : "border-dashed"
}`}
>
<CardContent className="p-4">
<div className="flex items-center gap-2">
<div className="w-6 h-6 rounded-full bg-blue-500 text-white text-xs flex items-center justify-center font-medium">
1
</div>
<div className="flex-1">
{selectedGraph1 ? (
<div>
<div
className="font-medium text-sm whitespace-normal break-words"
title={getGraphDisplayName(selectedGraph1)}
>
{getGraphDisplayName(selectedGraph1)}
</div>
<div className="text-xs text-muted-foreground">
{selectedGraph1.graph_type} •{" "}
{selectedGraph1.entity_count} entities
</div>
</div>
) : (
<div className="text-sm text-muted-foreground">
Select first graph
</div>
)}
</div>
</div>
</CardContent>
</Card>
<Card
className={`transition-all ${
selectedGraph2 ? "ring-2 ring-green-500" : "border-dashed"
}`}
>
<CardContent className="p-4">
<div className="flex items-center gap-2">
<div className="w-6 h-6 rounded-full bg-green-500 text-white text-xs flex items-center justify-center font-medium">
2
</div>
<div className="flex-1">
{selectedGraph2 ? (
<div>
<div
className="font-medium text-sm whitespace-normal break-words"
title={getGraphDisplayName(selectedGraph2)}
>
{getGraphDisplayName(selectedGraph2)}
</div>
<div className="text-xs text-muted-foreground">
{selectedGraph2.graph_type} •{" "}
{selectedGraph2.entity_count} entities
</div>
</div>
) : (
<div className="text-sm text-muted-foreground">
Select second graph
</div>
)}
</div>
</div>
</CardContent>
</Card>
</div>
{/* Graph List */}
<div className="space-y-4">
{availableGraphs.length === 0 ? (
<Card>
<CardContent className="p-8 text-center">
<Database className="h-12 w-12 mx-auto text-muted-foreground mb-4" />
<h3 className="text-lg font-semibold mb-2">
No Graphs Available
</h3>
<p className="text-muted-foreground">
No knowledge graphs found for comparison. Upload traces and
generate graphs first.
</p>
</CardContent>
</Card>
) : (
<Card>
<CardHeader>
<CardTitle className="text-base flex items-center gap-2">
<Database className="h-4 w-4" />
Available Graphs
<Badge variant="outline">{availableGraphs.length}</Badge>
</CardTitle>
</CardHeader>
<CardContent className="p-0">
<div className="space-y-0">
{availableGraphs.map((graph) => {
const hasChunks =
graph.chunk_graphs && graph.chunk_graphs.length > 0;
const isExpanded = expandedGraphs.has(graph.id);
const IconComponent = getGraphTypeIcon(graph.graph_type);
return (
<div key={graph.id} className="border-b last:border-b-0">
{/* Final Graph Item */}
<div
className={`relative p-5 cursor-pointer hover:bg-muted/50 transition-all duration-200 ${getSelectionClass(
graph
)}`}
onClick={() => handleGraphSelect(graph)}
>
<div className="flex items-center gap-4">
<div className="flex items-center gap-3 flex-1 min-w-0">
<IconComponent className="h-5 w-5 text-primary flex-shrink-0" />
<div className="flex-1 min-w-0">
<div
className="font-semibold text-base text-foreground mb-1 whitespace-normal break-words"
title={getGraphDisplayName(graph)}
>
{getGraphDisplayName(graph)}
</div>
<div className="flex items-center gap-3 flex-wrap">
<Badge
variant={
graph.graph_type === "final"
? "default"
: "secondary"
}
className="text-xs font-medium px-2 py-1"
>
{graph.graph_type}
</Badge>
{graph.trace_title && (
<span className="text-xs text-muted-foreground truncate">
from{" "}
{truncateGraphName(graph.trace_title, 25)}
</span>
)}
</div>
</div>
</div>
<div className="flex items-center gap-6 text-sm">
<div className="text-center">
<div className="font-bold text-foreground">
{graph.entity_count}
</div>
<div className="text-xs text-muted-foreground">
entities
</div>
</div>
<div className="text-center">
<div className="font-bold text-foreground">
{graph.relation_count}
</div>
<div className="text-xs text-muted-foreground">
relations
</div>
</div>
{hasChunks && (
<div className="text-center">
<div className="font-bold text-foreground">
{graph.chunk_graphs!.length}
</div>
<div className="text-xs text-muted-foreground">
chunks
</div>
</div>
)}
<div className="text-center">
<div className="font-bold text-foreground">
{formatDate(graph.creation_timestamp)}
</div>
<div className="text-xs text-muted-foreground">
created
</div>
</div>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
{hasChunks && (
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation();
toggleGraphExpansion(graph.id);
}}
className="p-1 h-8 w-8 hover:bg-muted"
>
<ChevronDown
className={`h-4 w-4 transition-transform ${
isExpanded ? "rotate-180" : ""
}`}
/>
</Button>
)}
</div>
</div>
{/* Selection Number Badge */}
{getSelectionNumber(graph) && (
<div
className={`absolute top-3 right-3 w-7 h-7 rounded-full text-white text-sm font-bold flex items-center justify-center shadow-lg transition-all duration-300 ${
getSelectionNumber(graph) === "1"
? "bg-blue-500 shadow-blue-500/30"
: "bg-green-500 shadow-green-500/30"
}`}
style={{
animation: "selectionPulse 0.5s ease",
}}
>
{getSelectionNumber(graph)}
</div>
)}
</div>
{/* Chunk Graphs */}
{hasChunks && isExpanded && (
<div className="bg-muted/20 border-t">
{graph.chunk_graphs!.map((chunk) => {
const ChunkIcon = getGraphTypeIcon(
chunk.graph_type
);
return (
<div
key={chunk.id}
className={`relative p-4 pl-12 cursor-pointer hover:bg-muted/40 transition-all duration-200 border-t border-muted/40 ${getSelectionClass(
chunk
)}`}
onClick={() => handleGraphSelect(chunk)}
>
<div className="flex items-center gap-4">
<div className="flex items-center gap-3 flex-1 min-w-0">
<ChunkIcon className="h-4 w-4 text-orange-500 flex-shrink-0" />
<div className="flex-1 min-w-0">
<div className="font-semibold text-sm text-foreground mb-1">
Window{" "}
{(chunk.window_info?.index || 0) + 1}/
{chunk.window_info?.total || "?"}
</div>
<div className="flex items-center gap-2">
<Badge
variant="secondary"
className="text-xs font-medium px-2 py-1"
>
{chunk.graph_type}
</Badge>
{chunk.window_info && (
<span className="text-xs text-muted-foreground">
{chunk.window_info.start_char?.toLocaleString() ||
"N/A"}{" "}
-{" "}
{chunk.window_info.end_char?.toLocaleString() ||
"N/A"}{" "}
chars
</span>
)}
</div>
</div>
</div>
<div className="flex items-center gap-6 text-sm">
<div className="text-center">
<div className="font-bold text-foreground">
{chunk.entity_count}
</div>
<div className="text-xs text-muted-foreground">
entities
</div>
</div>
<div className="text-center">
<div className="font-bold text-foreground">
{chunk.relation_count}
</div>
<div className="text-xs text-muted-foreground">
relations
</div>
</div>
</div>
</div>
{/* Selection Number Badge for Chunks */}
{getSelectionNumber(chunk) && (
<div
className={`absolute top-3 right-3 w-6 h-6 rounded-full text-white text-xs font-bold flex items-center justify-center shadow-lg transition-all duration-300 ${
getSelectionNumber(chunk) === "1"
? "bg-blue-500 shadow-blue-500/30"
: "bg-green-500 shadow-green-500/30"
}`}
style={{
animation: "selectionPulse 0.5s ease",
}}
>
{getSelectionNumber(chunk)}
</div>
)}
</div>
);
})}
</div>
)}
</div>
);
})}
</div>
</CardContent>
</Card>
)}
</div>
{/* Selection Summary removed */}
</div>
);
};