Skip to content

Commit 9cbb5f9

Browse files
authored
Merge pull request #66 from deepraj21/main
feat: Update dependencies and add visualization feature
2 parents c579020 + 00894d8 commit 9cbb5f9

File tree

9 files changed

+964
-23
lines changed

9 files changed

+964
-23
lines changed

client/package-lock.json

Lines changed: 685 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,9 @@
3939
"clsx": "^2.1.1",
4040
"cmdk": "^1.0.0",
4141
"cobe": "^0.6.3",
42+
"d3": "^7.9.0",
4243
"framer-motion": "^11.3.8",
43-
"lucide-react": "^0.411.0",
44+
"lucide-react": "^0.414.0",
4445
"mini-svg-data-uri": "^1.4.4",
4546
"next-themes": "^0.3.0",
4647
"react": "^18.3.1",
@@ -61,6 +62,7 @@
6162
"zod": "^3.23.8"
6263
},
6364
"devDependencies": {
65+
"@types/d3": "^7.4.3",
6466
"@types/node": "^20.14.11",
6567
"@types/react": "^18.3.3",
6668
"@types/react-dom": "^18.3.0",

client/src/App.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import Profile from './pages/Profile';
88
import { Toaster } from "@/components/ui/sonner";
99
import EditProfileForm from './pages/EditProfileForm';
1010
import { MessagePage } from './pages/MessagePage';
11-
import Projects from './components/Projects/Projects';
11+
import Projects from './pages/Projects';
12+
import Visualization from './pages/Visualization';
1213

1314
const App = () => {
1415

@@ -24,6 +25,7 @@ const App = () => {
2425
<Route path="/message" element={<MessagePage/>} />
2526
<Route path="/projects/:username" element={<Projects />} />
2627
<Route path="/user/:username" element={<Profile />} />
28+
<Route path="/relations/:username" element={<Visualization />} />
2729
<Route path="*" element={<div>404</div>} />
2830
</Routes>
2931
</Router>
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import { useEffect, useRef } from 'react';
2+
import * as d3 from 'd3';
3+
4+
interface NodeData extends d3.SimulationNodeDatum {
5+
id: string | number;
6+
label: string;
7+
}
8+
9+
interface LinkData extends d3.SimulationLinkDatum<NodeData> {
10+
source: string | number | NodeData; // Can be a string, number, or NodeData
11+
target: string | number | NodeData;
12+
type: string;
13+
}
14+
15+
const backendUrl = import.meta.env.VITE_BACKEND_URL || 'http://localhost:5000';
16+
17+
const Relations = () => {
18+
const username = localStorage.getItem('devhub_username');
19+
const d3Container = useRef<HTMLDivElement>(null);
20+
21+
useEffect(() => {
22+
if (!username) {
23+
console.error("Username not found in localStorage");
24+
return;
25+
}
26+
27+
// Fetch the user relations data from the backend
28+
fetch(`${backendUrl}/profile/relations?username=${username}`)
29+
.then(response => {
30+
if (!response.ok) {
31+
throw new Error('Network response was not ok');
32+
}
33+
return response.json();
34+
})
35+
.then((data: { nodes: NodeData[], links: LinkData[] }) => {
36+
// Remove any existing SVG if it exists to avoid duplication
37+
d3.select(d3Container.current).select('svg').remove();
38+
39+
// Get the parent element's dimensions dynamically
40+
const container = d3Container.current;
41+
const width = container?.getBoundingClientRect().width || 800;
42+
const height = container?.getBoundingClientRect().height || 600;
43+
44+
// Create the SVG element inside the div
45+
const svg = d3.select(container).append('svg')
46+
.attr('width', width)
47+
.attr('height', height);
48+
49+
// Initialize the force simulation with nodes
50+
const simulation = d3.forceSimulation<NodeData>(data.nodes)
51+
.force('link', d3.forceLink<NodeData, LinkData>(data.links)
52+
.id((d: NodeData) => d.id.toString()) // Ensure id is a string
53+
.distance(150)) // You can control the distance between nodes
54+
.force('charge', d3.forceManyBody()) // Compatible after extending NodeData
55+
.force('center', d3.forceCenter(width / 2, height / 2));
56+
57+
const link = svg.append('g')
58+
.selectAll('line')
59+
.data(data.links)
60+
.enter().append('line')
61+
.attr('stroke-width', 2)
62+
.attr('stroke', '#999');
63+
64+
// Add labels to links (relationship type)
65+
const linkText = svg.append('g')
66+
.selectAll('text')
67+
.data(data.links)
68+
.enter().append('text')
69+
.attr('text-anchor', 'middle')
70+
.attr('fill', '#666')
71+
.style('font-size', '14px') // Larger font for better visibility
72+
.text((d: LinkData) => d.type);
73+
74+
// Create nodes with increased size
75+
const node = svg.append('g')
76+
.selectAll('circle')
77+
.data(data.nodes)
78+
.enter().append('circle')
79+
.attr('r', 20) // Increased radius for bigger nodes
80+
.attr('fill', '#69b3a2')
81+
.call(
82+
d3.drag<SVGCircleElement, NodeData>() // Type the drag behavior with <SVGCircleElement, NodeData>
83+
.on('start', dragstarted)
84+
.on('drag', dragged)
85+
.on('end', dragended)
86+
);
87+
88+
// Add node labels
89+
const nodeLabels = svg.append('g')
90+
.selectAll('text')
91+
.data(data.nodes)
92+
.enter().append('text')
93+
.attr('dy', -25) // Position label above node
94+
.attr('text-anchor', 'middle')
95+
.attr('fill', '#333')
96+
.style('font-size', '14px') // Adjust font size as needed
97+
.text((d: NodeData) => d.label);
98+
99+
node.append('title').text((d: NodeData) => d.id.toString());
100+
101+
102+
// Update positions on each tick
103+
simulation.on('tick', () => {
104+
link.attr('x1', (d: LinkData) => ((d.source as NodeData).x!))
105+
.attr('y1', (d: LinkData) => ((d.source as NodeData).y!))
106+
.attr('x2', (d: LinkData) => ((d.target as NodeData).x!))
107+
.attr('y2', (d: LinkData) => ((d.target as NodeData).y!));
108+
109+
node.attr('cx', (d: NodeData) => d.x!)
110+
.attr('cy', (d: NodeData) => d.y!);
111+
112+
// Update node labels position
113+
nodeLabels.attr('x', (d: NodeData) => d.x!)
114+
.attr('y', (d: NodeData) => d.y!);
115+
116+
// Update link labels position
117+
linkText.attr('x', (d: LinkData) => {
118+
const x1 = (d.source as NodeData).x!;
119+
const x2 = (d.target as NodeData).x!;
120+
return (x1 + x2) / 2; // Position at the midpoint of the link
121+
})
122+
.attr('y', (d: LinkData) => {
123+
const y1 = (d.source as NodeData).y!;
124+
const y2 = (d.target as NodeData).y!;
125+
return (y1 + y2) / 2; // Position at the midpoint of the link
126+
});
127+
});
128+
129+
function dragstarted(event: d3.D3DragEvent<SVGCircleElement, NodeData, unknown>, d: NodeData) {
130+
if (!event.active) simulation.alphaTarget(0.3).restart();
131+
d.fx = d.x;
132+
d.fy = d.y;
133+
}
134+
135+
function dragged(event: d3.D3DragEvent<SVGCircleElement, NodeData, unknown>, d: NodeData) {
136+
d.fx = event.x;
137+
d.fy = event.y;
138+
}
139+
140+
function dragended(event: d3.D3DragEvent<SVGCircleElement, NodeData, unknown>, d: NodeData) {
141+
if (!event.active) simulation.alphaTarget(0);
142+
d.fx = null;
143+
d.fy = null;
144+
}
145+
})
146+
.catch(error => {
147+
console.error('Error fetching user relations:', error);
148+
});
149+
}, [username]);
150+
151+
return (
152+
<div style={{ width: '100%', height: '100%' }}>
153+
<div ref={d3Container} style={{ width: '100%', height: '100%' }}></div>
154+
</div>
155+
);
156+
};
157+
158+
export default Relations;

client/src/components/Sidebar/Sidebar.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,11 @@ import {
44
Settings2,
55
Sparkles,
66
CircleUser,
7-
Trash2,
87
LogOut,
98
MessagesSquare,
9+
ChartNetwork,
1010
type LucideIcon,
1111
} from "lucide-react"
12-
1312
import { Separator } from "@/components/ui/separator"
1413
import {
1514
Sidebar,
@@ -76,13 +75,14 @@ export function SidebarLeft({ ...props }: React.ComponentProps<typeof Sidebar>)
7675
url: username ? `/projects/${username}` : "#",
7776
icon: Inbox,
7877
},
78+
{
79+
title: "KGs",
80+
url: `/relations/${username}`,
81+
icon: ChartNetwork,
82+
}
7983
],
8084
navSecondary: [
81-
{
82-
title: "Trash",
83-
url: "#",
84-
icon: Trash2,
85-
},
85+
8686
{
8787
title: "Help",
8888
url: "#",

client/src/components/Projects/Projects.tsx renamed to client/src/pages/Projects.tsx

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import { SidebarLeft } from '@/components/Sidebar/Sidebar'
77
import { useEffect, useState } from "react";
88
import { ProjectCard } from "@/components/Projects/ProjectCard";
99
import { useParams } from "react-router-dom";
10-
import AddProject from "./AddProject";
10+
import AddProject from "../components/Projects/AddProject";
11+
import { Skeleton } from '@/components/ui/skeleton';
1112

1213
const backendUrl = import.meta.env.VITE_BACKEND_URL || 'http://localhost:5000';
1314

@@ -53,14 +54,6 @@ const Projects = () => {
5354
setRefresh(!refresh); // Toggle refresh state to re-trigger useEffect
5455
};
5556

56-
if (loading) {
57-
return <div>Loading projects...</div>;
58-
}
59-
60-
if (!projects.length) {
61-
return <div>No projects found.</div>;
62-
}
63-
6457
return (
6558
<SidebarProvider>
6659
<SidebarLeft />
@@ -72,13 +65,26 @@ const Projects = () => {
7265
</header>
7366
<main className="flex flex-col flex-grow p-4 overflow-hidden">
7467
<h1 className="ml-5 text-5xl mt-2">Projects</h1>
75-
<AddProject onProjectChange={handleRefresh} /> {/* Pass the callback */}
68+
<AddProject onProjectChange={handleRefresh} />
7669
<div className="flex flex-1 flex-col gap-4 p-4">
70+
{!projects.length ? (
71+
<div className="flex flex-col justify-center items-center">
72+
<h1 className="text-3xl">No Projects yet.</h1>
73+
</div>
74+
) :
75+
loading ? (
76+
<div className="grid auto-rows-min gap-4 md:grid-cols-3">
77+
{Array.from({ length: 3 }).map((_, index) => (
78+
<Skeleton key={index} className="h-32 w-100"/>
79+
))}
80+
</div>
81+
) : (
7782
<div className="grid auto-rows-min gap-4 md:grid-cols-3">
7883
{projects.map((project) => (
7984
<ProjectCard key={project.projectId} project={project} onProjectChange={handleRefresh} />
8085
))}
8186
</div>
87+
)}
8288
</div>
8389
</main>
8490
</SidebarInset>

client/src/pages/Visualization.tsx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import {
2+
SidebarInset,
3+
SidebarProvider,
4+
SidebarTrigger,
5+
} from "@/components/ui/sidebar"
6+
import { SidebarLeft } from '@/components/Sidebar/Sidebar'
7+
import Relations from "@/components/Relations/Relations"
8+
import {
9+
Tabs,
10+
TabsContent,
11+
TabsList,
12+
TabsTrigger,
13+
} from "@/components/ui/tabs"
14+
15+
const Visualization = () => {
16+
const username = localStorage.getItem('devhub_username');
17+
return (
18+
<SidebarProvider>
19+
<SidebarLeft />
20+
<SidebarInset>
21+
<header className="sticky top-0 flex h-14 shrink-0 items-center gap-2 bg-background">
22+
<div className="flex flex-1 items-center gap-2 px-3">
23+
<SidebarTrigger />
24+
</div>
25+
</header>
26+
<main className="flex flex-col flex-grow p-4 overflow-hidden">
27+
<h1 className="ml-5 text-5xl mt-2">Neo4j KGs</h1>
28+
<div className="flex flex-1 flex-col gap-4 p-4">
29+
<Tabs defaultValue="userRelation" className="mt-5 w-full">
30+
<TabsList className="grid w-full grid-cols-2">
31+
<TabsTrigger value="userRelation">{username} Relations</TabsTrigger>
32+
<TabsTrigger value="seeRelation">New Neo4j KGs</TabsTrigger>
33+
</TabsList>
34+
<TabsContent value="userRelation">
35+
<Relations />
36+
</TabsContent>
37+
<TabsContent value="seeRelation">
38+
<p>Coming soon...</p>
39+
</TabsContent>
40+
</Tabs>
41+
</div>
42+
</main>
43+
</SidebarInset>
44+
</SidebarProvider>
45+
)
46+
}
47+
48+
export default Visualization
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from flask import request, jsonify
2+
from extensions import neo4j_db
3+
4+
def get_user_relations():
5+
username = request.args.get('username')
6+
query = """
7+
MATCH (u:User {username: $username})-[r]->(n)
8+
RETURN u.username AS userUsername, r.type AS relationshipType,
9+
COALESCE(n.id, n.username, n.name, id(n)) AS nodeId, labels(n) AS nodeLabels
10+
"""
11+
try:
12+
with neo4j_db.driver.session() as session:
13+
result = session.run(query, username=username)
14+
nodes = []
15+
links = []
16+
node_ids = set() # To avoid adding duplicate nodes
17+
18+
for record in result:
19+
u = record["userUsername"]
20+
r = record["relationshipType"]
21+
n_id = str(record["nodeId"]) # Convert nodeId to string
22+
n_labels = record["nodeLabels"][0] if record["nodeLabels"] else "Unknown"
23+
24+
# Add the user node if not already added
25+
if u not in node_ids:
26+
nodes.append({"id": u, "label": "User"})
27+
node_ids.add(u)
28+
29+
# Add the related node if not already added
30+
if n_id not in node_ids:
31+
nodes.append({"id": n_id, "label": n_labels})
32+
node_ids.add(n_id)
33+
34+
# Add the link/relationship
35+
links.append({"source": u, "target": n_id, "type": r})
36+
37+
return jsonify({"nodes": nodes, "links": links})
38+
39+
except Exception as e:
40+
return jsonify({"error": str(e)}), 500

server/api/urls.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from api.handlers.query.querymodel import chat,chat_history
77
from api.handlers.user.friends import friends_bp
88
from api.handlers.message.message import search_users, send_message, get_messages
9+
from api.handlers.visualization.visualization import get_user_relations
910

1011
def register_routes(app):
1112
# Authentication routes
@@ -49,6 +50,9 @@ def register_routes(app):
4950
app.add_url_rule('/send_message', 'send_message', send_message, methods=['POST'])
5051
app.add_url_rule('/get_messages/<username>', 'get_messages', get_messages, methods=['GET'])
5152

53+
# Visualization route
54+
app.add_url_rule('/profile/relations','get_user_relations',get_user_relations, methods=['GET'])
55+
5256
# Landing page route
5357
app.add_url_rule('/', 'index', index)
5458

0 commit comments

Comments
 (0)