Ashutosh1975270 commited on
Commit
678f5a2
·
1 Parent(s): 410ebbb

feat: simplify graph visualization to 3D only, implement 3D camera controls, and persist document states globally

Browse files
frontend/src/app/(dashboard)/documents/page.tsx CHANGED
@@ -13,7 +13,6 @@ import { cn } from '@/lib/utils';
13
  import { useGraphData } from '@/hooks/useGraphData';
14
 
15
  export default function DocumentsPage() {
16
- useDocumentPolling();
17
  useGraphData(); // Keep graph data updated
18
 
19
  const documents = useDocumentsStore((s) => s.documents);
 
13
  import { useGraphData } from '@/hooks/useGraphData';
14
 
15
  export default function DocumentsPage() {
 
16
  useGraphData(); // Keep graph data updated
17
 
18
  const documents = useDocumentsStore((s) => s.documents);
frontend/src/components/graph/GraphControls.tsx CHANGED
@@ -2,21 +2,10 @@
2
 
3
  import { Plus, Minus, Maximize, Crosshair } from 'lucide-react';
4
  import { graphController } from './graphController';
5
- import { useGraphStore } from '@/store/graph';
6
 
7
  export function GraphControls() {
8
- const dim = useGraphStore((s) => s.dim);
9
- const setDim = useGraphStore((s) => s.setDim);
10
-
11
  return (
12
  <div className="absolute right-3 top-3 flex flex-col gap-1.5">
13
- <button
14
- onClick={() => setDim(dim === 2 ? 3 : 2)}
15
- className="flex h-9 w-9 items-center justify-center rounded-md border border-border bg-bg-elevated/90 text-xs font-bold text-accent-cyan shadow-sm backdrop-blur hover:bg-bg-surface hover:text-text-primary transition-all duration-200"
16
- aria-label="Toggle 2D/3D"
17
- >
18
- {dim === 2 ? '3D' : '2D'}
19
- </button>
20
  <button
21
  onClick={() => graphController.zoomIn?.()}
22
  className="flex h-9 w-9 items-center justify-center rounded-md border border-border bg-bg-elevated/90 text-text-secondary shadow-sm backdrop-blur hover:text-text-primary"
 
2
 
3
  import { Plus, Minus, Maximize, Crosshair } from 'lucide-react';
4
  import { graphController } from './graphController';
 
5
 
6
  export function GraphControls() {
 
 
 
7
  return (
8
  <div className="absolute right-3 top-3 flex flex-col gap-1.5">
 
 
 
 
 
 
 
9
  <button
10
  onClick={() => graphController.zoomIn?.()}
11
  className="flex h-9 w-9 items-center justify-center rounded-md border border-border bg-bg-elevated/90 text-text-secondary shadow-sm backdrop-blur hover:text-text-primary"
frontend/src/components/graph/GraphVisualization.tsx CHANGED
@@ -43,12 +43,32 @@ export function GraphVisualization({ height = '100%' }: { height?: string | numb
43
  const closeContextMenu = useCallback(() => setContextMenu(null), []);
44
 
45
  useEffect(() => {
46
- graphController.zoomIn = () => fgRef.current?.zoom?.(fgRef.current.zoom() * 1.4, 300);
47
- graphController.zoomOut = () => fgRef.current?.zoom?.(fgRef.current.zoom() / 1.4, 300);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  graphController.resetView = () => {
49
  fgRef.current?.zoomToFit?.(400, 40);
50
  };
51
- }, []);
52
 
53
  const pathSet = useMemo(() => new Set(paths), [paths]);
54
  const highlightSet = useMemo(() => new Set(highlighted), [highlighted]);
@@ -100,14 +120,26 @@ export function GraphVisualization({ height = '100%' }: { height?: string | numb
100
  );
101
  setSearchHit(match ? (match as any).id : null);
102
  if (match && fgRef.current) {
103
- if (fgRef.current.centerAt) {
104
- fgRef.current.centerAt((match as any).x, (match as any).y, 600);
105
- }
106
- if (fgRef.current.zoom) {
107
- fgRef.current.zoom(2.5, 600);
 
 
 
 
 
 
 
 
 
 
 
 
108
  }
109
  }
110
- }, [searchTerm, graphData.nodes]);
111
 
112
  return (
113
  <div className="graph-canvas relative h-full w-full" style={{ height }}>
@@ -163,8 +195,20 @@ export function GraphVisualization({ height = '100%' }: { height?: string | numb
163
  const node = n as GraphNode;
164
  setHighlighted([node.id], []);
165
  if (fgRef.current) {
166
- fgRef.current.centerAt((n as any).x, (n as any).y, 600);
167
- fgRef.current.zoom(2.5, 600);
 
 
 
 
 
 
 
 
 
 
 
 
168
  }
169
  }}
170
  onNodeHover={(n: any) => setHoverNode(n as FGNode)}
 
43
  const closeContextMenu = useCallback(() => setContextMenu(null), []);
44
 
45
  useEffect(() => {
46
+ graphController.zoomIn = () => {
47
+ if (dim === 3) {
48
+ const cam = fgRef.current?.camera?.();
49
+ if (cam && fgRef.current?.cameraPosition) {
50
+ const { x, y, z } = cam.position;
51
+ fgRef.current.cameraPosition({ x: x * 0.75, y: y * 0.75, z: z * 0.75 }, undefined, 300);
52
+ }
53
+ } else {
54
+ fgRef.current?.zoom?.(fgRef.current.zoom() * 1.4, 300);
55
+ }
56
+ };
57
+ graphController.zoomOut = () => {
58
+ if (dim === 3) {
59
+ const cam = fgRef.current?.camera?.();
60
+ if (cam && fgRef.current?.cameraPosition) {
61
+ const { x, y, z } = cam.position;
62
+ fgRef.current.cameraPosition({ x: x * 1.33, y: y * 1.33, z: z * 1.33 }, undefined, 300);
63
+ }
64
+ } else {
65
+ fgRef.current?.zoom?.(fgRef.current.zoom() / 1.4, 300);
66
+ }
67
+ };
68
  graphController.resetView = () => {
69
  fgRef.current?.zoomToFit?.(400, 40);
70
  };
71
+ }, [dim]);
72
 
73
  const pathSet = useMemo(() => new Set(paths), [paths]);
74
  const highlightSet = useMemo(() => new Set(highlighted), [highlighted]);
 
120
  );
121
  setSearchHit(match ? (match as any).id : null);
122
  if (match && fgRef.current) {
123
+ if (dim === 3) {
124
+ const distance = 40;
125
+ const distRatio = 1 + distance / Math.hypot((match as any).x || 1, (match as any).y || 1, (match as any).z || 1);
126
+ if (fgRef.current.cameraPosition) {
127
+ fgRef.current.cameraPosition(
128
+ { x: ((match as any).x || 0) * distRatio, y: ((match as any).y || 0) * distRatio, z: ((match as any).z || 0) * distRatio },
129
+ match, // lookAt
130
+ 600 // transitionMs
131
+ );
132
+ }
133
+ } else {
134
+ if (fgRef.current.centerAt) {
135
+ fgRef.current.centerAt((match as any).x, (match as any).y, 600);
136
+ }
137
+ if (fgRef.current.zoom) {
138
+ fgRef.current.zoom(2.5, 600);
139
+ }
140
  }
141
  }
142
+ }, [searchTerm, graphData.nodes, dim]);
143
 
144
  return (
145
  <div className="graph-canvas relative h-full w-full" style={{ height }}>
 
195
  const node = n as GraphNode;
196
  setHighlighted([node.id], []);
197
  if (fgRef.current) {
198
+ if (dim === 3) {
199
+ const distance = 40;
200
+ const distRatio = 1 + distance / Math.hypot(n.x || 1, n.y || 1, n.z || 1);
201
+ if (fgRef.current.cameraPosition) {
202
+ fgRef.current.cameraPosition(
203
+ { x: (n.x || 0) * distRatio, y: (n.y || 0) * distRatio, z: (n.z || 0) * distRatio },
204
+ n, // lookAt
205
+ 600 // transitionMs
206
+ );
207
+ }
208
+ } else {
209
+ fgRef.current.centerAt((n as any).x, (n as any).y, 600);
210
+ fgRef.current.zoom(2.5, 600);
211
+ }
212
  }
213
  }}
214
  onNodeHover={(n: any) => setHoverNode(n as FGNode)}
frontend/src/components/layout/DashboardLayout.tsx CHANGED
@@ -1,7 +1,10 @@
1
  import { Sidebar } from './Sidebar';
2
  import { TopBar } from './TopBar';
 
3
 
4
  export function DashboardLayout({ children }: { children: React.ReactNode }) {
 
 
5
  return (
6
  <div className="flex h-screen overflow-hidden bg-bg-base">
7
  <Sidebar />
 
1
  import { Sidebar } from './Sidebar';
2
  import { TopBar } from './TopBar';
3
+ import { useDocumentPolling } from '@/hooks/useDocumentPolling';
4
 
5
  export function DashboardLayout({ children }: { children: React.ReactNode }) {
6
+ useDocumentPolling(); // Poll documents globally to keep Query page select boxes in sync
7
+
8
  return (
9
  <div className="flex h-screen overflow-hidden bg-bg-base">
10
  <Sidebar />
frontend/src/store/graph.ts CHANGED
@@ -51,7 +51,7 @@ export const useGraphStore = create<GraphState>((set) => ({
51
  visibleTypes: {},
52
  visibleRelationships: {},
53
  selectedEntity: null,
54
- dim: 2,
55
  setData: (data) => set({ data }),
56
  setHighlighted: (entities, paths = []) =>
57
  set({ highlightedEntities: entities, highlightedPaths: paths }),
 
51
  visibleTypes: {},
52
  visibleRelationships: {},
53
  selectedEntity: null,
54
+ dim: 3,
55
  setData: (data) => set({ data }),
56
  setHighlighted: (entities, paths = []) =>
57
  set({ highlightedEntities: entities, highlightedPaths: paths }),