feat: add mindmap components from jarvis frontend
- Copied mindmap visualization components (ReactFlow-based interactive graph) - Added MindmapViewer, ReactFlowEditor, MermaidViewer - Included all node types: Concept, Task, Idea, Project - Added controls: NodeCreateModal, ExportButton - Created mindmap route at /mindmap - Added useGraphData hook for knowledge graph API - Copied auth-client and api utilities (dependencies) Note: Requires better-auth packages to be installed for full compilation
This commit is contained in:
308
apps/web/src/components/mindmap/hooks/useGraphData.ts
Normal file
308
apps/web/src/components/mindmap/hooks/useGraphData.ts
Normal file
@@ -0,0 +1,308 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useSession } from '@/lib/auth-client';
|
||||
import { handleSessionExpired, isSessionExpiring } from '@/lib/api';
|
||||
|
||||
export interface KnowledgeNode {
|
||||
id: string;
|
||||
title: string;
|
||||
node_type: string;
|
||||
content: string | null;
|
||||
tags: string[];
|
||||
domain: string | null;
|
||||
metadata: Record<string, unknown>;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
/** Input type for creating a new node (without server-generated fields) */
|
||||
export type NodeCreateInput = Omit<KnowledgeNode, 'id' | 'created_at' | 'updated_at'>;
|
||||
|
||||
/** Input type for creating a new edge (without server-generated fields) */
|
||||
export type EdgeCreateInput = Omit<KnowledgeEdge, 'created_at'>;
|
||||
|
||||
export interface KnowledgeEdge {
|
||||
source_id: string;
|
||||
target_id: string;
|
||||
relation_type: string;
|
||||
weight: number;
|
||||
metadata: Record<string, unknown>;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface GraphData {
|
||||
nodes: KnowledgeNode[];
|
||||
edges: KnowledgeEdge[];
|
||||
}
|
||||
|
||||
export interface MermaidData {
|
||||
diagram: string;
|
||||
style: string;
|
||||
}
|
||||
|
||||
export interface GraphStatistics {
|
||||
node_count: number;
|
||||
edge_count: number;
|
||||
nodes_by_type: Record<string, number>;
|
||||
edges_by_type: Record<string, number>;
|
||||
}
|
||||
|
||||
interface UseGraphDataOptions {
|
||||
rootId?: string;
|
||||
maxDepth?: number;
|
||||
autoFetch?: boolean;
|
||||
}
|
||||
|
||||
interface UseGraphDataResult {
|
||||
graph: GraphData | null;
|
||||
mermaid: MermaidData | null;
|
||||
statistics: GraphStatistics | null;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
fetchGraph: () => Promise<void>;
|
||||
fetchMermaid: (style?: 'flowchart' | 'mindmap') => Promise<void>;
|
||||
fetchStatistics: () => Promise<void>;
|
||||
createNode: (node: Omit<KnowledgeNode, 'id' | 'created_at' | 'updated_at'>) => Promise<KnowledgeNode | null>;
|
||||
updateNode: (id: string, updates: Partial<KnowledgeNode>) => Promise<KnowledgeNode | null>;
|
||||
deleteNode: (id: string) => Promise<boolean>;
|
||||
createEdge: (edge: Omit<KnowledgeEdge, 'created_at'>) => Promise<KnowledgeEdge | null>;
|
||||
deleteEdge: (sourceId: string, targetId: string, relationType: string) => Promise<boolean>;
|
||||
}
|
||||
|
||||
const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';
|
||||
|
||||
async function apiFetch<T>(
|
||||
endpoint: string,
|
||||
accessToken: string | null,
|
||||
options: RequestInit = {}
|
||||
): Promise<T> {
|
||||
// Skip request if session is already expiring (prevents request storms)
|
||||
if (isSessionExpiring()) {
|
||||
throw new Error('Session expired');
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers as Record<string, string>,
|
||||
};
|
||||
|
||||
// Add Authorization header if we have a token
|
||||
if (accessToken) {
|
||||
headers['Authorization'] = `Bearer ${accessToken}`;
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE}/api/v1/knowledge${endpoint}`, {
|
||||
...options,
|
||||
credentials: 'include',
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
// Handle session expiration
|
||||
if (response.status === 401) {
|
||||
handleSessionExpired();
|
||||
throw new Error('Session expired');
|
||||
}
|
||||
const error = await response.json().catch(() => ({ detail: response.statusText }));
|
||||
throw new Error(error.detail || 'API request failed');
|
||||
}
|
||||
|
||||
if (response.status === 204) {
|
||||
return undefined as T;
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export function useGraphData(options: UseGraphDataOptions = {}): UseGraphDataResult {
|
||||
const { rootId, maxDepth = 3, autoFetch = true } = options;
|
||||
|
||||
// Get access token from BetterAuth session
|
||||
const { data: sessionData } = useSession();
|
||||
const accessToken = (sessionData?.user as { accessToken?: string } | undefined)?.accessToken ?? null;
|
||||
|
||||
const [graph, setGraph] = useState<GraphData | null>(null);
|
||||
const [mermaid, setMermaid] = useState<MermaidData | null>(null);
|
||||
const [statistics, setStatistics] = useState<GraphStatistics | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchGraph = useCallback(async () => {
|
||||
if (!accessToken) {
|
||||
setError('Not authenticated');
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (rootId) params.set('root_id', rootId);
|
||||
params.set('max_depth', maxDepth.toString());
|
||||
|
||||
const data = await apiFetch<GraphData>(`/graph?${params}`, accessToken);
|
||||
setGraph(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to fetch graph');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [rootId, maxDepth, accessToken]);
|
||||
|
||||
const fetchMermaid = useCallback(async (style: 'flowchart' | 'mindmap' = 'flowchart') => {
|
||||
if (!accessToken) {
|
||||
setError('Not authenticated');
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (rootId) params.set('root_id', rootId);
|
||||
params.set('style', style);
|
||||
params.set('max_depth', maxDepth.toString());
|
||||
params.set('style_by_type', 'true');
|
||||
|
||||
const data = await apiFetch<MermaidData>(`/mermaid?${params}`, accessToken);
|
||||
setMermaid(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to fetch diagram');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [rootId, maxDepth, accessToken]);
|
||||
|
||||
const fetchStatistics = useCallback(async () => {
|
||||
if (!accessToken) return;
|
||||
try {
|
||||
const data = await apiFetch<GraphStatistics>('/graph/statistics', accessToken);
|
||||
setStatistics(data);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch statistics:', err);
|
||||
}
|
||||
}, [accessToken]);
|
||||
|
||||
const createNode = useCallback(async (
|
||||
node: Omit<KnowledgeNode, 'id' | 'created_at' | 'updated_at'>
|
||||
): Promise<KnowledgeNode | null> => {
|
||||
if (!accessToken) {
|
||||
setError('Not authenticated');
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const created = await apiFetch<KnowledgeNode>('/nodes', accessToken, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(node),
|
||||
});
|
||||
await fetchGraph();
|
||||
return created;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to create node');
|
||||
return null;
|
||||
}
|
||||
}, [fetchGraph, accessToken]);
|
||||
|
||||
const updateNode = useCallback(async (
|
||||
id: string,
|
||||
updates: Partial<KnowledgeNode>
|
||||
): Promise<KnowledgeNode | null> => {
|
||||
if (!accessToken) {
|
||||
setError('Not authenticated');
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const updated = await apiFetch<KnowledgeNode>(`/nodes/${id}`, accessToken, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(updates),
|
||||
});
|
||||
await fetchGraph();
|
||||
return updated;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to update node');
|
||||
return null;
|
||||
}
|
||||
}, [fetchGraph, accessToken]);
|
||||
|
||||
const deleteNode = useCallback(async (id: string): Promise<boolean> => {
|
||||
if (!accessToken) {
|
||||
setError('Not authenticated');
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
await apiFetch(`/nodes/${id}`, accessToken, { method: 'DELETE' });
|
||||
await fetchGraph();
|
||||
return true;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to delete node');
|
||||
return false;
|
||||
}
|
||||
}, [fetchGraph, accessToken]);
|
||||
|
||||
const createEdge = useCallback(async (
|
||||
edge: Omit<KnowledgeEdge, 'created_at'>
|
||||
): Promise<KnowledgeEdge | null> => {
|
||||
if (!accessToken) {
|
||||
setError('Not authenticated');
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const created = await apiFetch<KnowledgeEdge>('/edges', accessToken, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(edge),
|
||||
});
|
||||
await fetchGraph();
|
||||
return created;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to create edge');
|
||||
return null;
|
||||
}
|
||||
}, [fetchGraph, accessToken]);
|
||||
|
||||
const deleteEdge = useCallback(async (
|
||||
sourceId: string,
|
||||
targetId: string,
|
||||
relationType: string
|
||||
): Promise<boolean> => {
|
||||
if (!accessToken) {
|
||||
setError('Not authenticated');
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
source_id: sourceId,
|
||||
target_id: targetId,
|
||||
relation_type: relationType,
|
||||
});
|
||||
await apiFetch(`/edges?${params}`, accessToken, { method: 'DELETE' });
|
||||
await fetchGraph();
|
||||
return true;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to delete edge');
|
||||
return false;
|
||||
}
|
||||
}, [fetchGraph, accessToken]);
|
||||
|
||||
// Initial data fetch - only run when autoFetch is true and we have an access token
|
||||
useEffect(() => {
|
||||
if (autoFetch && accessToken) {
|
||||
void fetchGraph();
|
||||
void fetchStatistics();
|
||||
}
|
||||
}, [autoFetch, accessToken, fetchGraph, fetchStatistics]);
|
||||
|
||||
return {
|
||||
graph,
|
||||
mermaid,
|
||||
statistics,
|
||||
isLoading,
|
||||
error,
|
||||
fetchGraph,
|
||||
fetchMermaid,
|
||||
fetchStatistics,
|
||||
createNode,
|
||||
updateNode,
|
||||
deleteNode,
|
||||
createEdge,
|
||||
deleteEdge,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user