mirror of
https://github.com/james-m-jordan/morphik-core.git
synced 2025-05-09 19:32:38 +00:00
793 lines
30 KiB
TypeScript
793 lines
30 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
|
import dynamic from 'next/dynamic';
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Textarea } from '@/components/ui/textarea';
|
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
|
import {
|
|
AlertCircle,
|
|
Share2,
|
|
Database,
|
|
Plus,
|
|
Network,
|
|
Tag,
|
|
Link
|
|
} from 'lucide-react';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Switch } from '@/components/ui/switch';
|
|
|
|
// Dynamically import ForceGraphComponent to avoid SSR issues
|
|
const ForceGraphComponent = dynamic(() => import('@/components/ForceGraphComponent'), { ssr: false });
|
|
|
|
// Define interfaces
|
|
interface Graph {
|
|
id: string;
|
|
name: string;
|
|
entities: Entity[];
|
|
relationships: Relationship[];
|
|
metadata: Record<string, unknown>;
|
|
document_ids: string[];
|
|
filters?: Record<string, unknown>;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
interface Entity {
|
|
id: string;
|
|
label: string;
|
|
type: string;
|
|
properties: Record<string, unknown>;
|
|
chunk_sources: Record<string, number[]>;
|
|
}
|
|
|
|
interface Relationship {
|
|
id: string;
|
|
type: string;
|
|
source_id: string;
|
|
target_id: string;
|
|
}
|
|
|
|
interface GraphSectionProps {
|
|
apiBaseUrl: string;
|
|
onSelectGraph?: (graphName: string | undefined) => void;
|
|
authToken?: string | null;
|
|
}
|
|
|
|
// Map entity types to colors
|
|
const entityTypeColors: Record<string, string> = {
|
|
'person': '#4f46e5', // Indigo
|
|
'organization': '#06b6d4', // Cyan
|
|
'location': '#10b981', // Emerald
|
|
'date': '#f59e0b', // Amber
|
|
'concept': '#8b5cf6', // Violet
|
|
'event': '#ec4899', // Pink
|
|
'product': '#ef4444', // Red
|
|
'default': '#6b7280' // Gray
|
|
};
|
|
|
|
const GraphSection: React.FC<GraphSectionProps> = ({ apiBaseUrl, onSelectGraph, authToken }) => {
|
|
// Create auth headers for API requests if auth token is available
|
|
const createHeaders = useCallback((contentType?: string): HeadersInit => {
|
|
const headers: HeadersInit = {};
|
|
|
|
if (authToken) {
|
|
headers['Authorization'] = `Bearer ${authToken}`;
|
|
}
|
|
|
|
if (contentType) {
|
|
headers['Content-Type'] = contentType;
|
|
}
|
|
|
|
return headers;
|
|
}, [authToken]);
|
|
// State variables
|
|
const [graphs, setGraphs] = useState<Graph[]>([]);
|
|
const [selectedGraph, setSelectedGraph] = useState<Graph | null>(null);
|
|
const [graphName, setGraphName] = useState('');
|
|
const [graphDocuments, setGraphDocuments] = useState<string[]>([]);
|
|
const [graphFilters, setGraphFilters] = useState('{}');
|
|
const [additionalDocuments, setAdditionalDocuments] = useState<string[]>([]);
|
|
const [additionalFilters, setAdditionalFilters] = useState('{}');
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [activeTab, setActiveTab] = useState('list');
|
|
const [showNodeLabels, setShowNodeLabels] = useState(true);
|
|
const [showLinkLabels, setShowLinkLabels] = useState(true);
|
|
|
|
// Refs for graph visualization
|
|
const graphContainerRef = useRef<HTMLDivElement>(null);
|
|
const graphInstance = useRef<{ width: (width: number) => unknown } | null>(null);
|
|
|
|
// Prepare data for force-graph
|
|
const prepareGraphData = useCallback((graph: Graph | null) => {
|
|
if (!graph) return { nodes: [], links: [] };
|
|
|
|
const nodes = graph.entities.map(entity => ({
|
|
id: entity.id,
|
|
label: entity.label,
|
|
type: entity.type,
|
|
properties: entity.properties,
|
|
color: entityTypeColors[entity.type.toLowerCase()] || entityTypeColors.default
|
|
}));
|
|
|
|
// Create a Set of all entity IDs for faster lookups
|
|
const nodeIdSet = new Set(graph.entities.map(entity => entity.id));
|
|
|
|
// Filter relationships to only include those where both source and target nodes exist
|
|
const links = graph.relationships
|
|
.filter(rel => nodeIdSet.has(rel.source_id) && nodeIdSet.has(rel.target_id))
|
|
.map(rel => ({
|
|
source: rel.source_id,
|
|
target: rel.target_id,
|
|
type: rel.type
|
|
}));
|
|
|
|
return { nodes, links };
|
|
}, []);
|
|
|
|
// Initialize force-graph visualization
|
|
const initializeGraph = useCallback(() => {
|
|
// No need for implementation as ForceGraphComponent handles this
|
|
}, []);
|
|
|
|
// Handle window resize for responsive graph
|
|
useEffect(() => {
|
|
const handleResize = () => {
|
|
if (graphContainerRef.current && graphInstance.current) {
|
|
graphInstance.current.width(graphContainerRef.current.clientWidth);
|
|
}
|
|
};
|
|
|
|
window.addEventListener('resize', handleResize);
|
|
return () => window.removeEventListener('resize', handleResize);
|
|
}, []);
|
|
|
|
// Fetch all graphs
|
|
const fetchGraphs = useCallback(async () => {
|
|
try {
|
|
setLoading(true);
|
|
const headers = createHeaders();
|
|
const response = await fetch(`${apiBaseUrl}/graphs`, { headers });
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to fetch graphs: ${response.statusText}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
setGraphs(data);
|
|
} catch (err: unknown) {
|
|
const error = err as Error;
|
|
setError(`Error fetching graphs: ${error.message}`);
|
|
console.error('Error fetching graphs:', err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [apiBaseUrl, createHeaders]);
|
|
|
|
// Fetch graphs on component mount
|
|
useEffect(() => {
|
|
fetchGraphs();
|
|
}, [fetchGraphs]);
|
|
|
|
// Fetch a specific graph
|
|
const fetchGraph = async (graphName: string) => {
|
|
try {
|
|
setLoading(true);
|
|
const headers = createHeaders();
|
|
const response = await fetch(
|
|
`${apiBaseUrl}/graph/${encodeURIComponent(graphName)}`,
|
|
{ headers }
|
|
);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to fetch graph: ${response.statusText}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
setSelectedGraph(data);
|
|
|
|
// Call the callback if provided
|
|
if (onSelectGraph) {
|
|
onSelectGraph(graphName);
|
|
}
|
|
|
|
// Change to visualize tab if we're not on the list tab
|
|
if (activeTab !== 'list' && activeTab !== 'create') {
|
|
setActiveTab('visualize');
|
|
}
|
|
|
|
return data;
|
|
} catch (err: unknown) {
|
|
const error = err as Error;
|
|
setError(`Error fetching graph: ${error.message}`);
|
|
console.error('Error fetching graph:', err);
|
|
return null;
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
// Handle graph click
|
|
const handleGraphClick = (graph: Graph) => {
|
|
fetchGraph(graph.name);
|
|
};
|
|
|
|
// Create a new graph
|
|
const handleCreateGraph = async () => {
|
|
if (!graphName.trim()) {
|
|
setError('Please enter a graph name');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
// Parse filters
|
|
let parsedFilters = {};
|
|
try {
|
|
parsedFilters = JSON.parse(graphFilters);
|
|
} catch {
|
|
throw new Error('Invalid JSON in filters field');
|
|
}
|
|
|
|
const headers = createHeaders('application/json');
|
|
const response = await fetch(`${apiBaseUrl}/graph/create`, {
|
|
method: 'POST',
|
|
headers,
|
|
body: JSON.stringify({
|
|
name: graphName,
|
|
filters: Object.keys(parsedFilters).length > 0 ? parsedFilters : undefined,
|
|
documents: graphDocuments.length > 0 ? graphDocuments : undefined,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json();
|
|
throw new Error(errorData.detail || `Failed to create graph: ${response.statusText}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
setSelectedGraph(data);
|
|
|
|
// Refresh the graphs list
|
|
await fetchGraphs();
|
|
|
|
// Reset form
|
|
setGraphName('');
|
|
setGraphDocuments([]);
|
|
setGraphFilters('{}');
|
|
|
|
// Switch to visualize tab
|
|
setActiveTab('visualize');
|
|
|
|
} catch (err: unknown) {
|
|
const error = err as Error;
|
|
setError(`Error creating graph: ${error.message}`);
|
|
console.error('Error creating graph:', err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
// Update an existing graph
|
|
const handleUpdateGraph = async () => {
|
|
if (!selectedGraph) {
|
|
setError('No graph selected for update');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
// Parse additional filters
|
|
let parsedFilters = {};
|
|
try {
|
|
parsedFilters = JSON.parse(additionalFilters);
|
|
} catch {
|
|
throw new Error('Invalid JSON in additional filters field');
|
|
}
|
|
|
|
const headers = createHeaders('application/json');
|
|
const response = await fetch(`${apiBaseUrl}/graph/${encodeURIComponent(selectedGraph.name)}/update`, {
|
|
method: 'POST',
|
|
headers,
|
|
body: JSON.stringify({
|
|
additional_filters: Object.keys(parsedFilters).length > 0 ? parsedFilters : undefined,
|
|
additional_documents: additionalDocuments.length > 0 ? additionalDocuments : undefined,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json();
|
|
throw new Error(errorData.detail || `Failed to update graph: ${response.statusText}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
setSelectedGraph(data);
|
|
|
|
// Refresh the graphs list
|
|
await fetchGraphs();
|
|
|
|
// Reset form
|
|
setAdditionalDocuments([]);
|
|
setAdditionalFilters('{}');
|
|
|
|
// Switch to visualize tab
|
|
setActiveTab('visualize');
|
|
|
|
} catch (err: unknown) {
|
|
const error = err as Error;
|
|
setError(`Error updating graph: ${error.message}`);
|
|
console.error('Error updating graph:', err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
// Initialize or update graph visualization when the selected graph changes
|
|
useEffect(() => {
|
|
if (selectedGraph && activeTab === 'visualize') {
|
|
// Use setTimeout to ensure the container is rendered
|
|
setTimeout(() => {
|
|
initializeGraph();
|
|
}, 100);
|
|
}
|
|
}, [selectedGraph, activeTab, initializeGraph, showNodeLabels, showLinkLabels]);
|
|
|
|
// Handle tab change
|
|
const handleTabChange = (value: string) => {
|
|
setActiveTab(value);
|
|
// Initialize graph if switching to visualize tab and a graph is selected
|
|
if (value === 'visualize' && selectedGraph) {
|
|
setTimeout(() => {
|
|
initializeGraph();
|
|
}, 100);
|
|
}
|
|
};
|
|
|
|
// Render the graph visualization tab
|
|
const renderVisualization = () => {
|
|
if (!selectedGraph) {
|
|
return (
|
|
<div className="flex items-center justify-center h-[900px] border rounded-md">
|
|
<div className="text-center p-8">
|
|
<Network className="h-16 w-16 mx-auto mb-4 text-muted-foreground" />
|
|
<h3 className="text-lg font-medium mb-2">No Graph Selected</h3>
|
|
<p className="text-muted-foreground">
|
|
Select a graph from the list to visualize it here.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="border rounded-md">
|
|
<div className="p-4 border-b flex justify-between items-center">
|
|
<div>
|
|
<h3 className="text-lg font-medium">{selectedGraph.name}</h3>
|
|
<p className="text-sm text-gray-500">
|
|
{selectedGraph.entities.length} entities, {selectedGraph.relationships.length} relationships
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Tag className="h-4 w-4" />
|
|
<Label htmlFor="show-node-labels" className="text-sm cursor-pointer">Show Node Labels</Label>
|
|
<Switch
|
|
id="show-node-labels"
|
|
checked={showNodeLabels}
|
|
onCheckedChange={setShowNodeLabels}
|
|
/>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Link className="h-4 w-4" />
|
|
<Label htmlFor="show-link-labels" className="text-sm cursor-pointer">Show Relationship Labels</Label>
|
|
<Switch
|
|
id="show-link-labels"
|
|
checked={showLinkLabels}
|
|
onCheckedChange={setShowLinkLabels}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div ref={graphContainerRef} className="h-[900px]">
|
|
<ForceGraphComponent
|
|
data={prepareGraphData(selectedGraph)}
|
|
width={graphContainerRef.current?.clientWidth || 800}
|
|
height={900}
|
|
showNodeLabels={showNodeLabels}
|
|
showLinkLabels={showLinkLabels}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6 p-4">
|
|
<div className="space-y-2">
|
|
<div className="flex justify-between items-center">
|
|
<h2 className="text-2xl font-bold flex items-center">
|
|
<Network className="mr-2 h-6 w-6" />
|
|
Knowledge Graphs
|
|
</h2>
|
|
{selectedGraph && (
|
|
<div className="flex items-center">
|
|
<Badge variant="outline" className="text-md px-3 py-1 bg-blue-50">
|
|
Current Graph: {selectedGraph.name}
|
|
</Badge>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<p className="text-muted-foreground">
|
|
Knowledge graphs represent relationships between entities extracted from your documents.
|
|
Use them to enhance your queries with structured information and improve retrieval quality.
|
|
</p>
|
|
</div>
|
|
|
|
<Tabs defaultValue="list" value={activeTab} onValueChange={handleTabChange}>
|
|
<TabsList className="mb-4">
|
|
<TabsTrigger value="list">Available Graphs</TabsTrigger>
|
|
<TabsTrigger value="create">Create New Graph</TabsTrigger>
|
|
<TabsTrigger value="update" disabled={!selectedGraph}>Update Graph</TabsTrigger>
|
|
<TabsTrigger value="visualize" disabled={!selectedGraph}>Visualize Graph</TabsTrigger>
|
|
</TabsList>
|
|
|
|
{/* Graph List Tab */}
|
|
<TabsContent value="list">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center">
|
|
<Database className="mr-2 h-5 w-5" />
|
|
Available Knowledge Graphs
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Select a graph to view its details or visualize it.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{loading ? (
|
|
<div className="flex justify-center items-center p-8">
|
|
<div className="animate-spin rounded-full h-10 w-10 border-b-2 border-blue-500"></div>
|
|
</div>
|
|
) : graphs.length === 0 ? (
|
|
<div className="text-center p-8 border-2 border-dashed rounded-lg">
|
|
<Network className="mx-auto h-12 w-12 mb-3 text-muted-foreground" />
|
|
<p className="text-muted-foreground mb-3">No graphs available.</p>
|
|
<Button onClick={() => setActiveTab('create')} variant="default">
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
Create Your First Graph
|
|
</Button>
|
|
</div>
|
|
) : (
|
|
<ScrollArea className="h-[400px] pr-4">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{graphs.map((graph) => (
|
|
<Card
|
|
key={graph.id}
|
|
className={`cursor-pointer hover:shadow-md transition-shadow ${
|
|
selectedGraph?.id === graph.id ? 'border-2 border-blue-500' : ''
|
|
}`}
|
|
onClick={() => handleGraphClick(graph)}
|
|
>
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="flex justify-between items-center">
|
|
<span>{graph.name}</span>
|
|
<Badge variant="outline">
|
|
{new Date(graph.created_at).toLocaleDateString()}
|
|
</Badge>
|
|
</CardTitle>
|
|
<CardDescription>
|
|
{graph.entities.length} entities, {graph.relationships.length} relationships
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="flex flex-wrap gap-2 mt-2">
|
|
{Array.from(new Set(graph.entities.map(e => e.type))).slice(0, 5).map(type => (
|
|
<Badge
|
|
key={type}
|
|
style={{ backgroundColor: entityTypeColors[type.toLowerCase()] || entityTypeColors.default }}
|
|
className="text-white"
|
|
>
|
|
{type}
|
|
</Badge>
|
|
))}
|
|
{Array.from(new Set(graph.entities.map(e => e.type))).length > 5 && (
|
|
<Badge variant="outline">+{Array.from(new Set(graph.entities.map(e => e.type))).length - 5} more</Badge>
|
|
)}
|
|
</div>
|
|
<div className="mt-3 text-sm text-muted-foreground">
|
|
{graph.document_ids.length} document{graph.document_ids.length !== 1 ? 's' : ''}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
</ScrollArea>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{selectedGraph && (
|
|
<Card className="mt-4">
|
|
<CardHeader>
|
|
<CardTitle>{selectedGraph.name}</CardTitle>
|
|
<CardDescription>
|
|
Graph Details and Statistics
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4">
|
|
<div className="bg-blue-50 dark:bg-blue-900/20 p-4 rounded-lg">
|
|
<h4 className="font-medium text-blue-700 dark:text-blue-300 mb-1">Documents</h4>
|
|
<div className="text-2xl font-bold">{selectedGraph.document_ids.length}</div>
|
|
<div className="text-sm text-muted-foreground">source documents</div>
|
|
</div>
|
|
|
|
<div className="bg-emerald-50 dark:bg-emerald-900/20 p-4 rounded-lg">
|
|
<h4 className="font-medium text-emerald-700 dark:text-emerald-300 mb-1">Entities</h4>
|
|
<div className="text-2xl font-bold">{selectedGraph.entities.length}</div>
|
|
<div className="text-sm text-muted-foreground">unique elements</div>
|
|
</div>
|
|
|
|
<div className="bg-amber-50 dark:bg-amber-900/20 p-4 rounded-lg">
|
|
<h4 className="font-medium text-amber-700 dark:text-amber-300 mb-1">Relationships</h4>
|
|
<div className="text-2xl font-bold">{selectedGraph.relationships.length}</div>
|
|
<div className="text-sm text-muted-foreground">connections</div>
|
|
</div>
|
|
|
|
<div className="bg-purple-50 dark:bg-purple-900/20 p-4 rounded-lg">
|
|
<h4 className="font-medium text-purple-700 dark:text-purple-300 mb-1">Created</h4>
|
|
<div className="text-xl font-bold">{new Date(selectedGraph.created_at).toLocaleDateString()}</div>
|
|
<div className="text-sm text-muted-foreground">
|
|
{new Date(selectedGraph.created_at).toLocaleTimeString()}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-6 grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div>
|
|
<h4 className="font-medium mb-2">Entity Types</h4>
|
|
<div className="bg-gray-50 p-3 rounded-md">
|
|
{Object.entries(
|
|
selectedGraph.entities.reduce((acc, entity) => {
|
|
acc[entity.type] = (acc[entity.type] || 0) + 1;
|
|
return acc;
|
|
}, {} as Record<string, number>)
|
|
).map(([type, count]) => (
|
|
<div key={type} className="flex justify-between mb-2">
|
|
<div className="flex items-center">
|
|
<div
|
|
className="w-3 h-3 rounded-full mr-2"
|
|
style={{ backgroundColor: entityTypeColors[type.toLowerCase()] || entityTypeColors.default }}
|
|
></div>
|
|
<span>{type}</span>
|
|
</div>
|
|
<Badge variant="outline">{count}</Badge>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<h4 className="font-medium mb-2">Relationship Types</h4>
|
|
<div className="bg-gray-50 p-3 rounded-md">
|
|
{Object.entries(
|
|
selectedGraph.relationships.reduce((acc, rel) => {
|
|
acc[rel.type] = (acc[rel.type] || 0) + 1;
|
|
return acc;
|
|
}, {} as Record<string, number>)
|
|
).map(([type, count]) => (
|
|
<div key={type} className="flex justify-between mb-2">
|
|
<span>{type}</span>
|
|
<Badge variant="outline">{count}</Badge>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-6 flex justify-end">
|
|
<Button onClick={() => setActiveTab('visualize')}>
|
|
<Share2 className="mr-2 h-4 w-4" />
|
|
Visualize Graph
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</TabsContent>
|
|
|
|
{/* Create Graph Tab */}
|
|
<TabsContent value="create">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center">
|
|
<Plus className="mr-2 h-5 w-5" />
|
|
Create New Knowledge Graph
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Create a knowledge graph from documents in your Morphik collection to enhance your queries.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="graph-name">Graph Name</Label>
|
|
<Input
|
|
id="graph-name"
|
|
placeholder="Enter a unique name for your graph"
|
|
value={graphName}
|
|
onChange={(e) => setGraphName(e.target.value)}
|
|
/>
|
|
<p className="text-sm text-gray-500">
|
|
Give your graph a descriptive name that helps you identify its purpose.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="border-t pt-4 mt-4">
|
|
<h3 className="text-md font-medium mb-3">Document Selection</h3>
|
|
<p className="text-sm text-gray-500 mb-3">
|
|
Choose which documents to include in your graph. You can specify document IDs directly or use metadata filters.
|
|
</p>
|
|
|
|
<div className="space-y-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="graph-documents">Document IDs (Optional)</Label>
|
|
<Textarea
|
|
id="graph-documents"
|
|
placeholder="Enter document IDs separated by commas"
|
|
value={graphDocuments.join(', ')}
|
|
onChange={(e) => setGraphDocuments(e.target.value.split(',').map(id => id.trim()).filter(id => id))}
|
|
className="min-h-[80px]"
|
|
/>
|
|
<p className="text-xs text-gray-500">
|
|
Specify document IDs to include in the graph, or leave empty and use filters below.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="graph-filters">Metadata Filters (Optional)</Label>
|
|
<Textarea
|
|
id="graph-filters"
|
|
placeholder='{"category": "research", "author": "Jane Doe"}'
|
|
value={graphFilters}
|
|
onChange={(e) => setGraphFilters(e.target.value)}
|
|
className="min-h-[80px] font-mono"
|
|
/>
|
|
<p className="text-xs text-gray-500">
|
|
JSON object with metadata filters to select documents. All documents matching these filters will be included.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Button
|
|
onClick={handleCreateGraph}
|
|
disabled={!graphName || loading}
|
|
className="w-full"
|
|
>
|
|
{loading ? (
|
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
|
) : null}
|
|
Create Knowledge Graph
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
|
|
{/* Update Graph Tab */}
|
|
<TabsContent value="update">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center">
|
|
<Network className="mr-2 h-5 w-5" />
|
|
Update Knowledge Graph: {selectedGraph?.name}
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Update your knowledge graph with new documents to add more entities and relationships.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{selectedGraph ? (
|
|
<div className="space-y-4">
|
|
<div className="bg-blue-50 dark:bg-blue-900/20 p-4 rounded-lg">
|
|
<h4 className="font-medium mb-2">Current Graph Information</h4>
|
|
<div className="grid grid-cols-3 gap-2 text-sm">
|
|
<div>
|
|
<span className="font-medium">Documents:</span> {selectedGraph.document_ids.length}
|
|
</div>
|
|
<div>
|
|
<span className="font-medium">Entities:</span> {selectedGraph.entities.length}
|
|
</div>
|
|
<div>
|
|
<span className="font-medium">Relationships:</span> {selectedGraph.relationships.length}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="border-t pt-4 mt-4">
|
|
<h3 className="text-md font-medium mb-3">Add New Documents</h3>
|
|
<p className="text-sm text-gray-500 mb-3">
|
|
Choose additional documents to include in your graph. You can specify document IDs directly or use metadata filters.
|
|
</p>
|
|
|
|
<div className="space-y-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="additional-documents">Additional Document IDs</Label>
|
|
<Textarea
|
|
id="additional-documents"
|
|
placeholder="Enter document IDs separated by commas"
|
|
value={additionalDocuments.join(', ')}
|
|
onChange={(e) => setAdditionalDocuments(e.target.value.split(',').map(id => id.trim()).filter(id => id))}
|
|
className="min-h-[80px]"
|
|
/>
|
|
<p className="text-xs text-gray-500">
|
|
Specify additional document IDs to include in the graph, or use filters below.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="additional-filters">Additional Metadata Filters</Label>
|
|
<Textarea
|
|
id="additional-filters"
|
|
placeholder='{"category": "research", "author": "Jane Doe"}'
|
|
value={additionalFilters}
|
|
onChange={(e) => setAdditionalFilters(e.target.value)}
|
|
className="min-h-[80px] font-mono"
|
|
/>
|
|
<p className="text-xs text-gray-500">
|
|
JSON object with metadata filters to select additional documents.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Button
|
|
onClick={handleUpdateGraph}
|
|
disabled={loading}
|
|
className="w-full"
|
|
>
|
|
{loading ? (
|
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
|
) : null}
|
|
Update Knowledge Graph
|
|
</Button>
|
|
</div>
|
|
) : (
|
|
<div className="text-center p-8 border-2 border-dashed rounded-lg">
|
|
<Network className="mx-auto h-12 w-12 mb-3 text-muted-foreground" />
|
|
<p className="text-muted-foreground mb-3">Please select a graph to update.</p>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
|
|
{/* Visualize Graph Tab */}
|
|
<TabsContent value="visualize">
|
|
{renderVisualization()}
|
|
</TabsContent>
|
|
</Tabs>
|
|
|
|
{error && (
|
|
<Alert variant="destructive">
|
|
<AlertCircle className="h-4 w-4" />
|
|
<AlertTitle>Error</AlertTitle>
|
|
<AlertDescription>{error}</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default GraphSection;
|