Skip to content

Commit 55ce0fe

Browse files
committed
connected to graph
1 parent fd6bd27 commit 55ce0fe

8 files changed

Lines changed: 1034 additions & 53 deletions

File tree

backend/app/gexf/generated_nodes.gexf

Lines changed: 905 additions & 0 deletions
Large diffs are not rendered by default.

backend/app/main.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
from flask import Flask, jsonify, request
1+
from flask import Flask, jsonify, request, send_file, url_for
22
from flask_cors import CORS
33
from services.topic_service import TopicService
44
from services.ai_service import AITopicProcessor
5+
from services.gexy_node_service import GexfNodeGenerator
56
import os
67
import asyncio
78
import re
89

9-
app = Flask(__name__)
10+
app = Flask(__name__, static_folder='gexf', static_url_path='/gexf')
1011
CORS(
1112
app,
1213
resources={
@@ -20,6 +21,7 @@
2021

2122
topic_service = TopicService()
2223
ai_processor = AITopicProcessor()
24+
gexy_node_service = GexfNodeGenerator()
2325

2426

2527
@app.route("/api/process-topics", methods=["GET", "POST"])
@@ -235,6 +237,22 @@ def suggest_topics():
235237
}), 500
236238

237239

240+
@app.route("/api/generated-node-gexf", methods=["POST"])
241+
def finalized_node_gexf():
242+
data = request.get_json()
243+
topics = data.get("topics", [])
244+
gexf_path = gexy_node_service.generate_gexf_nodes_for_topics(topics)
245+
print(topics)
246+
# Read the GEXF file content
247+
with open(gexf_path, "r", encoding="utf-8") as f:
248+
gexf_content = f.read()
249+
250+
return jsonify({
251+
"success": True,
252+
"gexfContent": gexf_content
253+
})
254+
255+
238256
@app.route("/")
239257
def home():
240258
return "Hello World!"
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import os
2+
import duckdb
3+
import psutil
4+
import networkx as nx
5+
6+
class GexfNodeGenerator:
7+
def __init__(self):
8+
self.save_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'gexf')
9+
os.makedirs(self.save_dir, exist_ok=True)
10+
self.gexf_path = os.path.join(self.save_dir, 'generated_nodes.gexf')
11+
12+
# DuckDB connection (copied from TopicService)
13+
db_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))), 'public', 'data', 'github_meta.duckdb')
14+
if os.path.exists(db_path):
15+
self.con = duckdb.connect(database=db_path, read_only=True)
16+
available_memory = psutil.virtual_memory().available
17+
memory_limit = min(available_memory * 0.3, 0.5 * 1024 * 1024 * 1024)
18+
self.con.execute(f"SET memory_limit TO '{int(memory_limit)}B'")
19+
cpu_count = psutil.cpu_count(logical=False) or 1
20+
thread_count = max(1, min(cpu_count, 2))
21+
self.con.execute(f"SET threads TO {thread_count}")
22+
else:
23+
raise FileNotFoundError(f"Database not found at {db_path}. Please ensure the database file exists before running the application.")
24+
25+
def generate_gexf_nodes_for_topics(self, topics):
26+
"""
27+
Generate and store a GEXF file for all repos containing any of the given topics.
28+
Returns the path to the generated GEXF file.
29+
"""
30+
if not topics:
31+
return None
32+
topics_lower = [t.lower() for t in topics]
33+
placeholders = ','.join(['?'] * len(topics_lower))
34+
query = f'''
35+
SELECT DISTINCT r.nameWithOwner, r.stars, r.forks, r.watchers, r.isFork, r.isArchived, r.languageCount, r.pullRequests, r.issues, r.primaryLanguage, r.createdAt, r.license, r.codeOfConduct
36+
FROM repos r
37+
JOIN repo_topics t ON r.nameWithOwner = t.repo
38+
WHERE LOWER(t.topic) IN ({placeholders})
39+
'''
40+
result = self.con.execute(query, topics_lower).fetchall()
41+
columns = ["nameWithOwner", "stars", "forks", "watchers", "isFork", "isArchived", "languageCount", "pullRequests", "issues", "primaryLanguage", "createdAt", "license", "codeOfConduct"]
42+
G = nx.Graph()
43+
44+
# Define default values for each column type
45+
default_values = {
46+
"stars": 0,
47+
"forks": 0,
48+
"watchers": 0,
49+
"isFork": False,
50+
"isArchived": False,
51+
"languageCount": 0,
52+
"pullRequests": 0,
53+
"issues": 0,
54+
"primaryLanguage": "",
55+
"createdAt": "",
56+
"license": "",
57+
"codeOfConduct": ""
58+
}
59+
60+
for row in result:
61+
node_attrs = {}
62+
for col, val in zip(columns, row):
63+
if col == "nameWithOwner":
64+
repo_name = val
65+
else:
66+
# Use default value if the value is None
67+
node_attrs[col] = default_values[col] if val is None else val
68+
G.add_node(repo_name, **node_attrs)
69+
70+
nx.write_gexf(G, self.gexf_path)
71+
return self.gexf_path # Return the file path

src/components/TopicRefiner.tsx

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { FaArrowLeft } from "react-icons/fa";
1616
import { API_ENDPOINTS } from "../lib/config";
1717
import debounce from 'lodash/debounce';
1818
import { Tooltip } from 'bootstrap';
19+
import { useNavigate } from 'react-router-dom';
1920

2021
interface AIModel {
2122
id: string;
@@ -69,10 +70,7 @@ export const TopicRefiner: FC<Omit<TopicRefinerProps, 'isLlmProcessing'>> = ({
6970
setLlmSuggestions,
7071
selectedTopics = [],
7172
setSelectedTopics,
72-
newTopic = "",
73-
setNewTopic,
7473
prevStep,
75-
handleSubmit,
7674
searchTerm,
7775
onRequestSuggestions
7876
}) => {
@@ -92,6 +90,7 @@ export const TopicRefiner: FC<Omit<TopicRefinerProps, 'isLlmProcessing'>> = ({
9290
const [suggestionsByModel, setSuggestionsByModel] = useState<TopicWithModel[]>([]);
9391
const tooltipRefs = useRef<{ [key: string]: Tooltip }>({});
9492
const [isGettingSuggestions, setIsGettingSuggestions] = useState(false);
93+
const navigate = useNavigate();
9594

9695
const moveToRightColumn = (topic: string) => {
9796
setFinalizedTopics(prev => [...prev, topic]);
@@ -340,6 +339,33 @@ export const TopicRefiner: FC<Omit<TopicRefinerProps, 'isLlmProcessing'>> = ({
340339
// setLlmSuggestions([]);
341340
}, []);
342341

342+
const handleSubmitFinalizedTopics = async () => {
343+
try {
344+
const response = await fetch(`${API_ENDPOINTS.GENERATED_NODES}`, {
345+
method: 'POST',
346+
headers: {
347+
'Content-Type': 'application/json',
348+
},
349+
body: JSON.stringify({ topics: finalizedTopics }),
350+
});
351+
352+
if (!response.ok) {
353+
throw new Error('Failed to generate GEXF file');
354+
}
355+
356+
const data = await response.json();
357+
if (!data.success) {
358+
throw new Error('Failed to generate GEXF file');
359+
}
360+
361+
const file = new File([data.gexfContent], "generated_nodes.gexf", { type: "application/xml" });
362+
navigate('/graph?l=1', { state: { file } });
363+
} catch (error) {
364+
console.error('Error submitting finalized topics:', error);
365+
alert('Failed to generate graph. Please try again.');
366+
}
367+
};
368+
343369
return (
344370
<main className="container-fluid py-4" style={{ height: '100vh', overflowY: 'auto' }}>
345371
{/* Navigation/Header Row */}
@@ -619,10 +645,7 @@ export const TopicRefiner: FC<Omit<TopicRefinerProps, 'isLlmProcessing'>> = ({
619645
<div className="d-flex justify-content-end mt-4">
620646
<button
621647
className="btn btn-success d-flex align-items-center"
622-
onClick={() => {
623-
// Pass finalized topics to parent component
624-
handleSubmit();
625-
}}
648+
onClick={handleSubmitFinalizedTopics}
626649
disabled={finalizedTopics.length === 0}
627650
>
628651
<ThumbsUp size={16} className="me-2" />

src/lib/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ export const API_ENDPOINTS = {
77
AI_PROCESS: `${API_BASE_URL}/api/ai-process`,
88
EXPLAIN_TOPIC: `${API_BASE_URL}/api/explain-topic`,
99
SUGGEST_TOPICS: `${API_BASE_URL}/api/suggest-topics`,
10+
GENERATED_NODES: `${API_BASE_URL}/api/generated-node-gexf`,
1011
} as const;

src/lib/errors.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,7 @@ const REPORT_DICT: Record<
5858
missingNodeLabels: {
5959
level: "warning",
6060
log: (n) =>
61-
`${n > 1 ? n : "One"} node${n > 1 ? "s have" : " has"} no label. ${
62-
n > 1 ? "Their keys are" : "Its key is"
61+
`${n > 1 ? n : "One"} node${n > 1 ? "s have" : " has"} no label. ${n > 1 ? "Their keys are" : "Its key is"
6362
} used instead (${n > 1 ? "they are" : "it is"} italic in the graph).`,
6463
},
6564
missingNodePositions: {

src/views/GraphView.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,10 @@ const GraphView: FC<{ embed?: boolean }> = ({ embed = false }) => {
200200
setGraphFile({ name, extension, textContent });
201201
return readGraph({ name, extension, textContent });
202202
})
203-
.then((rawGraph) => prepareGraph(rawGraph))
203+
.then((rawGraph) => {
204+
if (!rawGraph) throw new Error("Parsed graph is empty or invalid (possibly no edges).");
205+
return prepareGraph(rawGraph);
206+
})
204207
.then(({ graph, report }) => {
205208
const notif = getReportNotification(report, /*rawNavState.role !== "d"*/ true);
206209
if (notif) notify(notif);

src/views/LocalWarningBanner.tsx

Lines changed: 2 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,7 @@
1-
import React, { FC, useContext } from "react";
2-
import { AiFillQuestionCircle } from "react-icons/ai";
3-
4-
import { GraphContext } from "../lib/context";
5-
import { queryURLToNavState } from "../lib/navState";
6-
import { useBlocker } from "../utils/useBlocker";
1+
import { FC } from "react";
72

83
const LocalWarningBanner: FC = () => {
9-
const { navState, openModal } = useContext(GraphContext);
10-
11-
useBlocker((tx) => {
12-
const newNavState = queryURLToNavState(tx.location.search);
13-
14-
if (
15-
navState.preventBlocker ||
16-
!!navState.local === !!newNavState.local ||
17-
window.confirm(
18-
"You are working on a local file, and you cannot share your visualizations yet. Are you sure you want to leave that page?",
19-
)
20-
)
21-
tx.retry();
22-
}, navState.local);
23-
24-
return (
25-
<>
26-
{navState.local && (
27-
<div className="bg-warning text-center d-flex flex-column p-2 border-bottom border-dark align-items-stretch text-md-start flex-md-row align-items-md-center">
28-
<div className="flex-grow-1">
29-
<div>
30-
You are currently using a <strong>local file</strong>, that only <strong>you</strong> can access.
31-
</div>
32-
<div>
33-
To be able to share your visualizations online, you need to first{" "}
34-
<strong>publish your graph file online</strong>.
35-
</div>
36-
</div>
37-
<button className="btn btn-outline-dark ms-md-2 flex-shrink-0" onClick={() => openModal("publish")}>
38-
<AiFillQuestionCircle /> Publish the graph online
39-
</button>
40-
</div>
41-
)}
42-
</>
43-
);
4+
return null;
445
};
456

467
export default LocalWarningBanner;

0 commit comments

Comments
 (0)