Quentin Lhoest commited on
Commit
f0806e2
·
1 Parent(s): c46832a
Dockerfile ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM neo4j:2025.09.0-community
2
+
3
+ # neo4j
4
+ ENV NEO4J_PLUGINS='["graph-data-science"]'
5
+ RUN chmod 400 /var/lib/neo4j/conf/neo4j.conf
6
+ RUN chmod 400 /var/lib/neo4j/conf/neo4j-admin.conf
7
+
8
+ # python
9
+ RUN apt-get update \
10
+ && apt-get install -y python3 python3-pip \
11
+ && rm -rf /var/lib/apt/lists/*
12
+
13
+ # app
14
+ ADD application_neo4j /application_neo4j
15
+ RUN pip install -r /application_neo4j/requirements.txt
16
+
17
+ COPY start.sh start.sh
18
+ ENTRYPOINT ["/bin/bash"]
19
+ CMD ["./start.sh"]
application_neo4j/.DS_Store ADDED
Binary file (6.15 kB). View file
 
application_neo4j/README.md ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ ## Dossier application_neo4j :
3
+ Cette application est une interface web construite avec **Flask** qui permet d'explorer un graphe de modèles et de datasets stocké dans **Neo4j** et exploité avec **Graph Data Science (GDS)**.
4
+
5
+ ## Prérequis
6
+
7
+ 1. **Python 3.10**
8
+ 2. Créer une instance Neo4j avec le plug-in GDS :
9
+ - suivre les instructions et télécharger la version de neo4j community adaptée à votre enironnement : https://neo4j.com/deployment-center/
10
+
11
+ <img width="1138" height="614" alt="Capture d’écran du 2025-09-09 16-33-46" src="https://github.com/user-attachments/assets/fcc6f82b-63eb-476c-af46-85e57cce3d97" />
12
+
13
+
14
+ - Configurer l'utilisation du plugin GDS en suivant les instructions pour la "Community Edition" : https://neo4j.com/docs/graph-data-science/current/installation/neo4j-server/
15
+ 4. **pip** pour installer les dépendances Python
16
+ 5. Clonez ce projet puis installez les dépendances :
17
+
18
+ ```bash
19
+ pip install -r requirements.txt
20
+ ```
21
+
22
+ 6. Récupérer les données formattés
23
+
24
+ Si la base n'a pas encore été chargée, il faut récupérer le fichier "model_with_all_information.csv" généré préalablement dans le dossier pre-processing/base_de_donnees.
25
+
26
+ 7. Le fichier `load_data.py` configuré avec les bons identifiants de connexion (URI, utilisateur, mot de passe de l'instance Neo4j).
27
+
28
+ 8. **Chargement des données dans Neo4j**
29
+
30
+ Ce script (`load_data.py`) est essentiel car il sert de pont entre votre fichier CSV et la base de données Neo4j. Le chargement de 1.8 M de modèles prend environ 30 min lorqu'il est effectué sur 8 threads parallèles.
31
+ Ouvrez un terminal et exécutez le script en choisissant l'une des deux options suivantes :
32
+
33
+ Les arguments **name_neo4j** et **password_neo4J** sont respectivement le nom et le mot de passe que vous avez donné à votre instance Neo4J lors de sa création.
34
+
35
+ *(Dans mon cas, name_neo4j = neo4j et password_neo4J = genealogiemodeles)*
36
+
37
+ **1. Pour une nouvelle installation ou une réinitialisation complète :**
38
+
39
+ Cette commande efface toutes les données existantes avant de tout importer.
40
+ ```bash
41
+ python load_data.py true name_neo4j password_neo4j
42
+ ```
43
+
44
+ **2. Pour une mise à jour (ajout de nouvelles données sans effacer les anciennes) :**
45
+
46
+ Cette commande ne charge que les modèles du CSV qui ne sont pas déjà dans la base.
47
+ ```bash
48
+ python load_data.py false name_neo4j password_neo4j
49
+ ```
50
+
51
+ À la fin de l'exécution, votre base de données Neo4j est peuplée et prête à être utilisée par l'application. Un fichier processed_ids.txt est mis à jour avec l'id de tous les modèles déjà chargés dans la base.
52
+
53
+ ### Utilisation : Lancement de l'application Flask
54
+
55
+ Une fois la base de données prête, vous pouvez lancer le serveur web.
56
+ Exécutez la commande suivante dans votre terminal :
57
+ ```bash
58
+ python app.py name_neo4j password_neo4j
59
+ ```
60
+ Les arguments **name_neo4j** et **password_neo4J** sont respectivement le nom et le mot de passe que vous avez donné à votre instance Neo4J lors de sa création.
61
+
62
+ *(Dans mon cas, name_neo4j = neo4j et password_neo4J =genealogiemodeles)*
63
+
64
+ Le terminal affichera un message indiquant que le serveur est en cours d'exécution, généralement sur `http://127.0.0.1:5000`. Vous pouvez maintenant ouvrir cette adresse dans votre navigateur web pour utiliser l'application.
65
+
66
+ ---
67
+
68
+ <img width="1864" height="912" alt="Capture d'écran 2025-09-09 155319" src="https://github.com/user-attachments/assets/6b92d363-9227-4331-912c-bcb524a592b1" />
69
+
70
+
71
+ ## Détails de l'Architecture de l'Application
72
+
73
+ * `app.py`
74
+ * **Rôle** : Fichier principal et point d'entrée de l'application.
75
+ * **Fonctionnalités** :
76
+ * Initialise l'application Flask.
77
+ * Établit la connexion au driver Neo4j et à la bibliothèque GDS.
78
+ * Définit les **routes** (URL) de l'application.
79
+
80
+
81
+ * `app_algorithms.py`
82
+ * **Rôle** : Module dédié à l'exécution des algorithmes d'analyse de graphe via la bibliothèque Neo4j Graph Data Science (GDS).
83
+ * **Fonctionnalités** :
84
+ * **Recherche exhaustive de la généalogie (`run_gds_bfs`)** : Exécute l'algorithme de parcours en largeur (BFS) pour trouver tous les nœuds accessibles
85
+ depuis un nœud de départ, à la fois en aval (descendants) et en amont (ascendants). Les résultats sont destinés aux tableaux de données détaillés et à la visualisation du graphe complet.
86
+ * **Identification des points clés (`get_genealogy_highlights`)** : Exécute des requêtes Cypher ciblées pour identifier les modèles plus importants dans l'ascendance et la descendance (ex: les plus téléchargés, les plus cités, les modèles "racines"). Les résultats sont enrichis avec des "badges".
87
+ * **`create_node_data`** : Fonction utilitaire de formatage. Traduit un dictionnaire de propriétés brutes venant de Neo4j en un dictionnaire structuré et propre pour le front-end.
88
+ La structure de sortie dépend du type (label) du nœud.
89
+ * **`project_gds_bfs_results`** : Elle prend les résultats bruts de l'algorithme BFS (une simple liste d'ID de nœuds) et reconstruit le sous-graphe complet avec toutes
90
+ les propriétés et les relations stockées dans Neo4j.
91
+
92
+ * Dossier `templates/`
93
+ * **Rôle** : Contient tous les fichiers HTML qui constituent les pages de l'application.
94
+ * **Fichiers principaux** :
95
+ * `search.html` : Il s'agit de la page principale de l'application. Elle sert à la fois de page d'accueil pour la recherche et de page d'affichage des résultats après une requête, SI le noeud cherché est un modèle.
96
+ * **Contenu principal :**
97
+ * **Formulaire de recherche :** Permet à l'utilisateur de rechercher un nœud (modèle, dataset) par son nom. Il inclut l'auto-complétion, le réglage de la profondeur de la recherche (illimitée ou limitée) et des filtres.
98
+ * **Vue "Highlights" :** Après une recherche réussie, cette section affiche une synthèse visuelle en trois colonnes : les modèles ascendants les plus importants, le modèle recherché au centre avec ses statistiques clés, et les modèles descendants les plus importants.
99
+ * **Tableaux détaillés :** Deux tableaux interactifs (motorisés par *DataTables*) listent de manière exhaustive tous les modèles de l'ascendance et de la descendance.
100
+ * **Visualisation du Graphe :** Une section, initialement masquée, contient un graphe interactif (généré avec *Sigma.js*) qui représente visuellement l'arbre généalogique complet.
101
+
102
+ * `search_dataset.html` : Page d'affichage des résultats après une requête, SI le noeud cherché est un dataset.
103
+ * **Contenu principal :**
104
+ * **Formulaire de recherche :** Permet à l'utilisateur de rechercher un nœud (modèle, dataset) par son nom. Il inclut l'auto-complétion, le réglage de la profondeur de la recherche (illimitée ou limitée) et des filtres.
105
+ * **Tableau détaillé :** Tableau interactif (motorisé par *DataTables*) liste de manière exhaustive tous les modèles entraînés sur le dataset recherché.
106
+ * **Visualisation du Graphe :** Une section, initialement masquée, contient un graphe interactif (généré avec *Sigma.js*) qui représente visuellement l'arbre généalogique complet.
107
+
108
+ * `expert.html` : Permet de chercher un noeud et afficher le graphe en choissisant les types de liens et de noeuds visibles + bouton pour télécharger le graphe au format HTML.
109
+
110
+ * `index.html` : Page d'accueil et porte d'entrée de l'application.
111
+ * `infos.html` : lien vers des slides avec plus d'informations.
112
+
113
+
114
+ * Dossier `static/`
115
+ * **Rôle** : Sert les fichiers statiques qui ne changent pas, comme le CSS, le JavaScript et les images.
116
+ * **Contenu** :
117
+ * `css/style.css` : Styles personnalisés qui complètent le framework Bootstrap pour affiner l'apparence de l'application.
118
+ * `js/script.js` : Pour l'interactivité de `search.html`.
119
+ * `js/script_expert.js` : Pour l'interactivité de `expert.html`.
120
+ * `js/script_dataset.js` : Pour l'interactivité de `search_dataset.html`.
121
+ * `js/utils.js` : regroupe les fonctions utilisées dans plusieurs scripts.
122
+ * `graph_export.html` : fichier pour créer l'export du graphe lorsque l'utilisateur souhaite télécharger le graphe dans le mode expert.
application_neo4j/app.py ADDED
@@ -0,0 +1,431 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # --- Import et initialisation Flask ---
2
+ import os
3
+ from flask import Flask, request, render_template, jsonify
4
+ from neo4j import GraphDatabase, basic_auth
5
+ from graphdatascience import GraphDataScience
6
+ import app_algorithms as algo
7
+ import pandas as pd
8
+ from typing import Dict
9
+ from collections import defaultdict
10
+ import argparse
11
+
12
+ app = Flask(__name__, static_url_path="/static/") # Application Flask
13
+
14
+ # --- Connexion à Neo4j et GDS ---
15
+ NEO4J_URI = "bolt://localhost:7687"
16
+ GDS_GRAPH_NAME = "genealogie_gds"
17
+
18
+ # --- Configuration des arguments du script ---
19
+ parser = argparse.ArgumentParser(description="Script pour lancer la web application.")
20
+
21
+ parser.add_argument(
22
+ 'neo4j_user', # Le nom de l'argument (positionnel, car sans tirets)
23
+ type=str, # On attend une chaîne de caractères
24
+ help="Nom de votre instance Neo4J"
25
+ )
26
+
27
+ parser.add_argument(
28
+ 'neo4j_password', # Le nom de l'argument (positionnel, car sans tirets)
29
+ type=str, # On attend une chaîne de caractères
30
+ help="Mot de passe de votre instance Neo4J"
31
+ )
32
+
33
+ args = parser.parse_args()
34
+
35
+ NEO4J_PASSWORD = args.neo4j_password
36
+ NEO4J_USER = args.neo4j_user
37
+
38
+ try:
39
+ driver = GraphDatabase.driver(NEO4J_URI, auth=basic_auth(NEO4J_USER, NEO4J_PASSWORD))
40
+ gds = GraphDataScience(NEO4J_URI, auth=(NEO4J_USER, NEO4J_PASSWORD))
41
+ except Exception as e:
42
+ print(f"FATAL: Could not connect to Neo4j/GDS at startup. Error: {e}")
43
+ exit() # Si la connexion échoue, l'application ne peut pas tourner
44
+
45
+ # --- Projection du graphe pour GDS ---
46
+ def ensure_graph_projected(gds: GraphDataScience, graph_name):
47
+ """
48
+ Projette deux graphes dans :
49
+ - Graphe "naturel" : relations descendant (source -> target)
50
+ - Graphe "inverse" : relations ascendant (target -> source)
51
+ Les projections sont nécessaires pour lancer des algorithmes GDS comme BFS
52
+ """
53
+
54
+ natural_graph_name = f"{graph_name}_natural"
55
+ reverse_graph_name = f"{graph_name}_reverse"
56
+
57
+ # Suppression de toute projection existante
58
+ g_natural_exists = gds.graph.exists(natural_graph_name).exists
59
+ if g_natural_exists:
60
+ gds.graph.get(natural_graph_name).drop()
61
+
62
+ g_reverse_exists = gds.graph.exists(reverse_graph_name).exists
63
+ if g_reverse_exists:
64
+ gds.graph.get(reverse_graph_name).drop()
65
+
66
+
67
+ # --- 1. Projection pour les descendants (sens normal) ---
68
+ # On sélectionne les relations et on les projette en gardant source -> target
69
+ print("Projection du graphe naturel (descendant)...")
70
+ gds.run_cypher(f"""
71
+ MATCH (source)-[r:IS_IN|POSTED|USED_IN]->(target)
72
+ WITH gds.graph.project(
73
+ '{natural_graph_name}',
74
+ source,
75
+ target,
76
+ {{
77
+ relationshipType: type(r)
78
+ }}
79
+ ) AS g
80
+ RETURN g.graphName AS graph, g.nodeCount AS nodes, g.relationshipCount AS rels
81
+ """)
82
+ print(f"Graphe '{natural_graph_name}' projeté.")
83
+
84
+
85
+ # --- 2. Projection pour les ascendants (sens inversé) ---
86
+ # On sélectionne les mêmes relations, mais on inverse source et target dans l'appel
87
+ print("Projection du graphe inversé (ascendant)...")
88
+ gds.run_cypher(f"""
89
+ MATCH (source)-[r:IS_IN|POSTED|USED_IN]->(target)
90
+ WITH gds.graph.project(
91
+ '{reverse_graph_name}',
92
+ target, // <<< Le 'target' devient la source dans la projection
93
+ source, // <<< Le 'source' devient la cible dans la projection
94
+ {{
95
+ relationshipType: type(r)
96
+ }}
97
+ ) AS g
98
+ RETURN g.graphName AS graph, g.nodeCount AS nodes, g.relationshipCount AS rels
99
+ """)
100
+ print(f"Graphe '{reverse_graph_name}' projeté.")
101
+
102
+ @app.route("/") # Page d'accueil
103
+ def home():
104
+ return render_template("index.html")
105
+
106
+ @app.route("/info") # Page d'informations
107
+ def info_page():
108
+ return render_template("infos.html")
109
+
110
+ # Suggestions pour l'autocomplétion
111
+ @app.route("/autocomplete")
112
+ def autocomplete():
113
+ query = request.args.get("q", "")
114
+ node_filter = request.args.get("filter")
115
+ if not query:
116
+ return jsonify([])
117
+
118
+ # Filtrage optionnel par label (Model ou Dataset)
119
+ label_cypher = ""
120
+ if node_filter and node_filter in ["Model", "Dataset"]: # Mesure de sécurité
121
+ label_cypher = f":{node_filter}"
122
+
123
+ # Récupère les noms commençant par le préfixe fourni
124
+ cypher = f"""
125
+ MATCH (n{label_cypher})
126
+ WHERE toLower(n.name) STARTS WITH toLower($prefix)
127
+ AND n.name IS NOT NULL
128
+ RETURN n.name AS name, labels(n)[0] as label
129
+ ORDER BY size(n.name) ASC
130
+ LIMIT 10
131
+ """
132
+ try:
133
+ results_df = gds.run_cypher(cypher, {"prefix": query})
134
+ suggestions = results_df.to_dict('records')
135
+ return jsonify(suggestions)
136
+
137
+ except Exception as e:
138
+ print(f"Autocomplete error: {e}")
139
+ return jsonify([])
140
+
141
+
142
+ @app.route("/search", methods=["GET", "POST"]) # Recherche d'un noeud
143
+ def findnode():
144
+ """
145
+ 1. Récupère le nom à chercher et la profondeur
146
+ 2. Appelle ensure_graph_projected pour s'assurer que les graphes GDS existent
147
+ 3. Lance l'algorithme BFS via algo.run_gds_bfs
148
+ 4. Traite les résultats et construit le sous-graphe à afficher
149
+ 5. Met à jour les données pour le template Flask
150
+ """
151
+
152
+ message = None
153
+ highlights = {}
154
+ graph_data = {"nodes": [], "edges": [], "models_count": []}
155
+ current_filters = []
156
+ if request.method == "GET":
157
+ # Pour les liens depuis la page d'accueil
158
+ filter_from_url = request.args.get('filter')
159
+ if filter_from_url:
160
+ current_filters.append(filter_from_url)
161
+ else: # POST
162
+ # Pour le formulaire soumis avec les checkboxes
163
+ current_filters = request.form.getlist('filters')
164
+
165
+ search_info = {
166
+ "name": request.form.get("name", ""),
167
+ "depth": int(request.form.get("depth", 3)),
168
+ "unlimited": 'unlimited_depth' in request.form,
169
+ "filters": current_filters # On passe la liste des filtres au template
170
+ }
171
+
172
+ if request.method == "POST" and request.form.get("submit") == "find_node":
173
+ name = request.form.get("name", "").strip()
174
+ is_unlimited = 'unlimited_depth' in request.form
175
+ depth = None if is_unlimited else int(request.form.get("depth", 3))
176
+
177
+ search_info = {"name": name, "depth": request.form.get("depth", 3), "unlimited": is_unlimited}
178
+
179
+ if not name:
180
+ message = "Veuillez entrer un nom à rechercher."
181
+ return render_template("search.html", message=message, search=search_info, graph_data=graph_data, highlights=highlights)
182
+
183
+ try:
184
+ # Projection des graphes ascendant et descendant
185
+ ensure_graph_projected(gds, GDS_GRAPH_NAME)
186
+ natural_graph_name = f"{GDS_GRAPH_NAME}_natural"
187
+ reverse_graph_name = f"{GDS_GRAPH_NAME}_reverse"
188
+
189
+ # Appeler la fonction GDS BFS
190
+ gds_result = algo.run_gds_bfs(gds, natural_graph_name,reverse_graph_name, name, depth,False)
191
+ if not gds_result :
192
+ message = f"Le modèle/dataset '{name}' n'a pas été trouvé."
193
+ return render_template("search.html", message=message, search=search_info, graph_data=graph_data,highlights=highlights)
194
+
195
+ process_gds_bfs_results(gds_result, graph_data, name)
196
+ if gds_result["source_label"] == "Model" :
197
+ highlights = algo.get_genealogy_highlights(gds, name)
198
+ return render_template("search.html", message=message, search=search_info, graph_data=graph_data,highlights=highlights)
199
+
200
+ if gds_result["source_label"] == "Dataset" :
201
+ return render_template("search_dataset.html", message=message, search=search_info, graph_data=graph_data)
202
+
203
+ if not graph_data["nodes"] and not graph_data["edges"]:
204
+ message = f"Le nœud '{name}' a été trouvé, mais il n'a pas de voisins dans la profondeur spécifiée."
205
+ return render_template("search.html", message=message, search=search_info, graph_data=graph_data,highlights=highlights)
206
+
207
+ except Exception as e:
208
+ # Gérer le cas où le nœud source n'existe pas du tout
209
+ if "Failed to find a node" in str(e):
210
+ message = f"Le nœud '{name}' n'a pas été trouvé dans le graphe."
211
+ else:
212
+ print(f"GDS BFS Error: {e}")
213
+ message = f"Erreur lors de la recherche GDS: {str(e)}"
214
+ return render_template("search.html", message=message, search=search_info, graph_data=graph_data,highlights=highlights)
215
+
216
+
217
+ @app.route("/expert", methods=["GET", "POST"]) # Recherche d'un noeud
218
+ def findnode_expert():
219
+ """
220
+ 1. Récupère le nom à chercher et la profondeur
221
+ 2. Appelle ensure_graph_projected pour s'assurer que les graphes GDS existent
222
+ 3. Lance l'algorithme BFS via algo.run_gds_bfs
223
+ 4. Traite les résultats et construit le sous-graphe à afficher
224
+ 5. Met à jour les données pour le template Flask
225
+ """
226
+ message = None
227
+ graph_data = {"nodes": [], "edges": [], "models_count": []}
228
+ current_filters = []
229
+ if request.method == "GET":
230
+ # Pour les liens depuis la page d'accueil
231
+ filter_from_url = request.args.get('filter')
232
+ if filter_from_url:
233
+ current_filters.append(filter_from_url)
234
+ else: # POST
235
+ # Pour le formulaire soumis avec les checkboxes
236
+ current_filters = request.form.getlist('filters')
237
+
238
+ search_info = {
239
+ "name": request.form.get("name", ""),
240
+ "depth": int(request.form.get("depth", 3)),
241
+ "unlimited": 'unlimited_depth' in request.form,
242
+ "filters": current_filters # On passe la liste des filtres au template
243
+ }
244
+
245
+ if request.method == "POST" and request.form.get("submit") == "findnode_expert":
246
+ name = request.form.get("name", "").strip()
247
+ is_unlimited = 'unlimited_depth' in request.form
248
+ depth = None if is_unlimited else int(request.form.get("depth", 3))
249
+
250
+ search_info = {"name": name, "depth": request.form.get("depth", 3), "unlimited": is_unlimited,"filters": current_filters }
251
+
252
+ if not name:
253
+ message = "Veuillez entrer un nom à rechercher."
254
+ return render_template("expert.html", message=message, search=search_info, graph_data=graph_data)
255
+
256
+ try:
257
+ # Projection des graphes ascendant et descendant
258
+ ensure_graph_projected(gds, GDS_GRAPH_NAME)
259
+ natural_graph_name = f"{GDS_GRAPH_NAME}_natural"
260
+ reverse_graph_name = f"{GDS_GRAPH_NAME}_reverse"
261
+
262
+ # Appeler la fonction GDS BFS
263
+ gds_result = algo.run_gds_bfs(gds, natural_graph_name,reverse_graph_name, name, depth,True)
264
+ if not gds_result :
265
+ message = f"Le noeud '{name}' n'a pas été trouvé."
266
+ return render_template("expert.html", message=message, search=search_info, graph_data=graph_data)
267
+
268
+
269
+ process_gds_bfs_results(gds_result, graph_data, name)
270
+
271
+ if not graph_data["nodes"] and not graph_data["edges"]:
272
+ message = f"Le nœud '{name}' a été trouvé, mais il n'a pas de voisins dans la profondeur spécifiée."
273
+
274
+ except Exception as e:
275
+ # Gérer le cas où le nœud source n'existe pas du tout
276
+ if "Failed to find a node" in str(e):
277
+ message = f"Le nœud '{name}' n'a pas été trouvé dans le graphe."
278
+ else:
279
+ print(f"GDS BFS Error: {e}")
280
+ message = f"Erreur lors de la recherche GDS: {str(e)}"
281
+ return render_template("expert.html", message=message, search=search_info, graph_data=graph_data)
282
+
283
+
284
+ def process_gds_bfs_results(gds_result: Dict, graph_data: Dict, origin_name: str):
285
+ """
286
+ Transforme les résultats d'un BFS GDS en un sous-graphe utilisable pour le front-end.
287
+
288
+ Étapes principales :
289
+ 1. Collecte des IDs de tous les nœuds parcourus par le BFS (ascendants et descendants).
290
+ 2. Récupération des nœuds et relations réels dans Neo4j via une requête Cypher.
291
+ 3. Formatage des nœuds et arêtes pour construire le dictionnaire `graph_data`.
292
+ """
293
+
294
+ # --- PHASE 1 : Collecte des IDs de tous les nœuds visités ---
295
+ all_discovered_node_ids = set()
296
+ # Ajouter le nœud source
297
+ source_id = gds_result.get("source_node")
298
+ if source_id is not None:
299
+ all_discovered_node_ids.add(source_id)
300
+
301
+ # Ajouter les descendants (profondeur positive)
302
+ desc_df = gds_result.get("descendant")
303
+ if desc_df is not None and not desc_df.empty:
304
+ node_ids = desc_df["nodeIds"].iloc[0]
305
+ all_discovered_node_ids.update(node_ids)
306
+
307
+ # Ajouter les ascendants (profondeur négative)
308
+ asc_df = gds_result.get("ascendant")
309
+ if asc_df is not None and not asc_df.empty:
310
+ node_ids = asc_df["nodeIds"].iloc[0]
311
+ all_discovered_node_ids.update(node_ids)
312
+
313
+ if not all_discovered_node_ids:
314
+ return # Aucun nœud découvert → rien à faire
315
+
316
+ # --- PHASE 2 : Récupération du sous-graphe réel dans Neo4j ---
317
+ with driver.session() as session:
318
+ # Requête Cypher : récupère les nœuds, auteurs, datasets et relations entre eux
319
+ results = session.run("""
320
+ MATCH (n) WHERE id(n) IN $ids
321
+ OPTIONAL MATCH (author:Author)-[:POSTED]->(n)
322
+ OPTIONAL MATCH (dataset:Dataset)-[:USED_IN]->(n)
323
+ OPTIONAL MATCH (o:Model) WHERE o.name = $origin_name
324
+ CALL {
325
+ WITH n,o
326
+ OPTIONAL MATCH p = (n)-[:USED_IN*1..]->(o)
327
+ WITH n,o, length(p) AS rel_asc
328
+ OPTIONAL MATCH p= (n)<-[r:USED_IN*1..]-(o)
329
+ WITH n,o, rel_asc, length(p) AS rel_desc
330
+ OPTIONAL MATCH (ancestor:Model)-[:USED_IN*1..]->(n)
331
+ WITH n, rel_asc, rel_desc, count(DISTINCT ancestor) AS ascendantsCount
332
+ OPTIONAL MATCH (descendant:Model)<-[:USED_IN*1..]-(n)
333
+ WITH n,rel_asc, rel_desc, ascendantsCount, count(DISTINCT descendant) AS descendantsCount
334
+ OPTIONAL MATCH (citation:Model)<-[:USED_IN]-(n)
335
+ RETURN ascendantsCount, descendantsCount, count(DISTINCT citation) AS citationCount,rel_asc, rel_desc
336
+ }
337
+ WITH n, author,dataset, ascendantsCount, descendantsCount, citationCount,rel_asc, rel_desc
338
+ WITH collect({
339
+ id: id(n),
340
+ node: n,
341
+ dataset: properties(dataset),
342
+ author: properties(author),
343
+ task: n.task,
344
+ license: n.license,
345
+ createdAt:n.createdAt,
346
+ likes:n.likes,
347
+ properties: properties(n),
348
+ labels: labels(n),
349
+ ascendantsCount: ascendantsCount,
350
+ descendantsCount: descendantsCount,
351
+ citationCount: citationCount,
352
+ distance: CASE
353
+ WHEN rel_asc IS NOT NULL THEN -rel_asc
354
+ WHEN rel_desc IS NOT NULL THEN rel_desc
355
+ ELSE 0
356
+ END
357
+ }) AS nodes_data
358
+ CALL {
359
+ WITH nodes_data
360
+ UNWIND [item IN nodes_data | item.node] AS n1
361
+ UNWIND [item IN nodes_data | item.node] AS n2
362
+ MATCH (n1)-[r]-(n2)
363
+ RETURN collect(r) AS rels
364
+ }
365
+ RETURN nodes_data, rels
366
+ """, {"ids": list(all_discovered_node_ids), "origin_name": origin_name})
367
+
368
+ subgraph = results.single()
369
+ if not subgraph:
370
+ return
371
+ count=0
372
+ # --- PHASE 3 : Formatage des nœuds et des arêtes ---
373
+ for node_obj in subgraph["nodes_data"]:
374
+ # Extraire et compléter les propriétés du nœud
375
+ node_properties = dict(node_obj["properties"])
376
+ node_properties.update({
377
+ "ascendantsCount": node_obj["ascendantsCount"],
378
+ "descendantsCount": node_obj["descendantsCount"],
379
+ "citationCount": node_obj["citationCount"],
380
+ "task": node_obj.get("task"),
381
+ "license": node_obj.get("license"),
382
+ "createdAt": node_obj.get("createdAt"),
383
+ "likes": node_obj.get("likes"),
384
+ "distance": node_obj.get("distance")
385
+ })
386
+ if node_obj.get("author"):
387
+ node_properties["author"] = node_obj["author"].get("name")
388
+ if node_obj.get("dataset"):
389
+ node_properties["dataset"] = node_obj["dataset"].get("name")
390
+
391
+ node_label = list(node_obj["labels"])[0]
392
+
393
+ if node_label == "Model" :
394
+ count = count +1
395
+
396
+ node_data = algo.create_node_data(node_properties, node_label)
397
+ graph_data["nodes"].append(node_data)
398
+
399
+ graph_data["models_count"].append(count)
400
+
401
+ # Construction des arêtes, en évitant les doublons
402
+ added_edges_canonical_keys = set()
403
+ for rel_obj in subgraph["rels"]:
404
+ source_name = rel_obj.start_node["name"]
405
+ target_name = rel_obj.end_node["name"]
406
+ if not source_name or not target_name:
407
+ continue
408
+
409
+ canonical_key = tuple(sorted((source_name, target_name)))
410
+ if canonical_key not in added_edges_canonical_keys:
411
+ added_edges_canonical_keys.add(canonical_key)
412
+ edge_id = f"{source_name}_{target_name}_{type(rel_obj).__name__}"
413
+ rel_props = dict(rel_obj.items())
414
+ graph_data["edges"].append({
415
+ "id": edge_id,
416
+ "source": source_name,
417
+ "target": target_name,
418
+ "relation": rel_props.get("name")
419
+ })
420
+
421
+
422
+ if __name__ == '__main__':
423
+ # Le script de démarrage n'a plus besoin de projeter le graphe.
424
+ # Il se contente de lancer l'application. La projection se fera à la demande.
425
+ app.run(host='0.0.0.0', port=80 if os.getenv("SPACE_ID") else 5500, debug=True)
426
+
427
+ # Le driver doit être fermé quand l'application s'arrête.
428
+ # Une manière simple est d'utiliser `atexit`
429
+ import atexit
430
+ atexit.register(lambda: driver.close())
431
+ atexit.register(lambda: gds.close())
application_neo4j/app_algorithms.py ADDED
@@ -0,0 +1,304 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Importation des bibliothèques nécessaires
2
+ from graphdatascience import GraphDataScience
3
+ from typing import Dict, List, Any
4
+ import pandas as pd
5
+
6
+ def run_gds_bfs(gds: GraphDataScience, natural_graph_name: str, reverse_graph_name: str, source_name: str, max_depth: int = None, expert = False) -> Dict[str, Any]:
7
+ """
8
+ Exécute un parcours en largeur (BFS) directionnel à l'aide de GDS pour trouver les descendants et les ascendants.
9
+
10
+ Cette fonction nécessite deux graphes pré-projetés en mémoire GDS :
11
+ - Un graphe "naturel" pour trouver les descendants (relations dans le sens source -> cible).
12
+ - Un graphe "inversé" pour trouver les ascendants (relations dans le sens cible -> source).
13
+
14
+ Args:
15
+ gds: L'objet de connexion à la bibliothèque Graph Data Science.
16
+ natural_graph_name: Le nom du graphe GDS projeté avec une orientation NATURELLE.
17
+ reverse_graph_name: Le nom du graphe GDS projeté avec une orientation INVERSÉE.
18
+ source_name: La propriété 'name' du nœud de départ de la recherche.
19
+ max_depth: La profondeur maximale de recherche. Si None, la recherche est illimitée.
20
+
21
+ Returns:
22
+ Un dictionnaire contenant l'ID du nœud source et deux DataFrames pandas :
23
+ un pour les chemins des descendants et un pour les chemins des ascendants.
24
+ """
25
+ # GDS fonctionne avec des identifiants de nœuds internes (des nombres), pas avec des noms.
26
+ # La première étape est donc de trouver l'ID numérique de notre nœud de départ à partir de son nom.
27
+ try:
28
+ source_id_result = gds.run_cypher(
29
+ """
30
+ MATCH (n {name: $source_name})
31
+ RETURN id(n) AS id , labels(n) as label
32
+ LIMIT 1
33
+ """,
34
+ {"source_name": source_name}
35
+ )
36
+
37
+ if source_id_result.empty or (source_id_result["label"][0]==["Author"] and not expert):
38
+ print(f"Le modèle ou dataset avec le nom '{source_name}' n'a pas été trouvé.")
39
+ return None # Retourne des DataFrames vides
40
+
41
+ # On récupère l'ID de la première ligne du résultat.
42
+ source_node_id = source_id_result['id'][0]
43
+ except Exception as e:
44
+ print(f"Erreur lors de la recherche de l'ID du nœud source pour '{source_name}': {e}")
45
+ return {"source_label": source_id_result["label"][0][0],"descendant": pd.DataFrame(), "ascendant": pd.DataFrame()}
46
+
47
+ # Préparation des paramètres pour l'algorithme BFS.
48
+ bfs_params = {'sourceNode': source_node_id}
49
+ print(bfs_params)
50
+ # Si une profondeur maximale est spécifiée, on l'ajoute aux paramètres.
51
+ if max_depth is not None:
52
+ bfs_params['maxDepth'] = max_depth
53
+
54
+ # --- Exécution du BFS pour trouver les DESCENDANTS sur le graphe NATUREL ---
55
+ # On récupère l'objet graphe depuis GDS.
56
+ g_natural = gds.graph.get(natural_graph_name)
57
+ # On exécute l'algorithme BFS en mode `stream` pour obtenir les chemins.
58
+ desc_df = gds.bfs.stream(g_natural, **bfs_params)
59
+ print("BFS pour les descendants sur le graphe naturel terminé.")
60
+
61
+ # --- Exécution du BFS pour trouver les ASCENDANTS sur le graphe INVERSÉ ---
62
+ # Utiliser un graphe inversé est très efficace pour trouver les parents/ancêtres.
63
+ g_reverse = gds.graph.get(reverse_graph_name)
64
+ asc_df = gds.bfs.stream(g_reverse, **bfs_params)
65
+ print("BFS pour les ascendants sur le graphe inversé terminé.")
66
+ print("DESC",desc_df)
67
+ print("ASC",asc_df)
68
+ print(source_id_result["label"][0][0])
69
+
70
+ # Retourne les résultats sous forme d'un dictionnaire structuré.
71
+ return {
72
+ "source_node": source_node_id,"source_label": source_id_result["label"][0][0],
73
+ "descendant": desc_df,
74
+ "ascendant": asc_df
75
+ }
76
+
77
+
78
+
79
+
80
+
81
+ def get_genealogy_highlights(gds: "GraphDataScience", model_name: str, num_highlights: int = 2) -> Dict:
82
+ """
83
+ Trouve les modèles clés dans l'ascendance et la descendance (1er/2e plus cités/téléchargés).
84
+
85
+ Args:
86
+ gds: L'instance de GraphDataScience.
87
+ model_name: Le nom du modèle de départ.
88
+ num_highlights: Le nombre de modèles à récupérer pour chaque catégorie (par défaut 2).
89
+
90
+ Returns:
91
+ Un dictionnaire contenant les listes de modèles unifiés pour l'affichage.
92
+ """
93
+ highlights = {
94
+ "desc_unique_models": [],
95
+ "asc_unique_models": []
96
+ }
97
+
98
+ # --- DÉFINITION CENTRALE DES BADGES ---
99
+ # Centraliser les badges ici rend le code beaucoup plus facile à modifier.
100
+ # Les classes CSS sont directement des classes Bootstrap 5 pour simplifier le rendu dans le template HTML.
101
+ badges_info = {
102
+ 'desc_cited_1': {
103
+ 'text': '1er + cité',
104
+ 'class': 'bg-success',
105
+ 'title': "Ce modèle est le plus cité parmi les modèles de la descendance."
106
+ },
107
+ 'desc_cited_2': {
108
+ 'text': '2e + cité',
109
+ 'class': 'bg-success bg-opacity-75',
110
+ 'title': "Ce modèle est le deuxième plus cité parmi les modèles de la descendance."
111
+ },
112
+ 'desc_downloaded_1': {
113
+ 'text': '1er + téléchargé',
114
+ 'class': 'beta',
115
+ 'title': "Ce modèle est le plus téléchargé parmi les modèles de la descendance."
116
+ },
117
+ 'desc_downloaded_2': {
118
+ 'text': '2e + téléchargé',
119
+ 'class': 'alpha',
120
+ 'title': "Ce modèle est le deuxième plus téléchargé parmi les modèles de la descendance."
121
+ },
122
+
123
+ 'asc_foundation': {
124
+ 'text': 'Modèle racine',
125
+ 'class': 'bg-warning text-dark',
126
+ 'title': "Ce modèle n'a pas de parent connu."
127
+ },
128
+ 'asc_cited_1': {
129
+ 'text': '1er + cité',
130
+ 'class': 'bg-success',
131
+ 'title': "Ce modèle est le plus cité parmi les modèles de l'ascendance."
132
+ },
133
+ 'asc_cited_2': {
134
+ 'text': '2e + cité',
135
+ 'class': 'bg-success bg-opacity-75',
136
+ 'title': "Ce modèle est le deuxième plus cité parmi les modèles de l'ascendance."
137
+ },
138
+ 'asc_downloaded_1': {
139
+ 'text': '1er + téléchargé',
140
+ 'class': 'beta',
141
+ 'title': "Ce modèle est le plus téléchargé parmi les modèles de l'ascendance."
142
+ },
143
+ 'asc_downloaded_2': {
144
+ 'text': '2e + téléchargé',
145
+ 'class': 'alpha',
146
+ 'title': "Ce modèle est le deuxième plus téléchargé parmi les modèles de l'ascendance."
147
+ },
148
+ }
149
+
150
+
151
+ def process_and_assign_badges(
152
+ unified_dict: Dict,
153
+ model_list: List[Dict],
154
+ badge_keys: List[str]
155
+ ):
156
+ """
157
+ Fonction utilitaire pour ajouter des modèles et leurs badges à un dictionnaire unifié.
158
+ Cela évite la duplication de code pour chaque catégorie (cité, téléchargé, etc.).
159
+ """
160
+ for i, model in enumerate(model_list):
161
+ if i < len(badge_keys): # S'assurer qu'on a un badge défini pour ce rang
162
+ model_name_key = model['name']
163
+ badge_key = badge_keys[i]
164
+
165
+ # Ajouter le modèle au dictionnaire s'il n'y est pas déjà
166
+ if model_name_key not in unified_dict:
167
+ unified_dict[model_name_key] = model.copy()
168
+ unified_dict[model_name_key]['badges'] = []
169
+
170
+ # Ajouter le badge correspondant
171
+ badge_to_add = badges_info[badge_key]
172
+ if badge_to_add not in unified_dict[model_name_key]['badges']:
173
+ unified_dict[model_name_key]['badges'].append(badge_to_add)
174
+
175
+ # ==========================================================================
176
+ # 1. GESTION DE LA DESCENDANCE
177
+ # ==========================================================================
178
+ desc_downloads_query = """
179
+ MATCH (start:Model {name: $model_name})-[:USED_IN*1..]->(descendant:Model)
180
+ WHERE start <> descendant
181
+ WITH descendant, size([(m:Model)<-[:USED_IN]-(descendant) | m]) AS citation_count
182
+ RETURN descendant.name AS name, citation_count, descendant.downloads AS downloads, descendant.task AS task, descendant.license AS license, descendant.likes AS likes, descendant.createdAt AS createdAt
183
+ ORDER BY descendant.downloads DESC, descendant.name ASC
184
+ LIMIT $limit
185
+ """
186
+ desc_cited_query = """
187
+ MATCH (start:Model {name: $model_name})-[:USED_IN*1..]->(descendant:Model)
188
+ WHERE start <> descendant
189
+ WITH descendant, size([(m:Model)<-[:USED_IN]-(descendant) | m]) AS citation_count
190
+ RETURN descendant.name AS name, citation_count, descendant.task AS task, descendant.downloads AS downloads, descendant.license AS license, descendant.likes AS likes, descendant.createdAt AS createdAt
191
+ ORDER BY citation_count DESC, descendant.name ASC
192
+ LIMIT $limit
193
+ """
194
+ try:
195
+ params = {"model_name": model_name, "limit": num_highlights}
196
+ desc_downloaded_list = gds.run_cypher(desc_downloads_query, params).to_dict('records')
197
+ desc_cited_list = gds.run_cypher(desc_cited_query, params).to_dict('records')
198
+
199
+ desc_unified_models = {}
200
+ process_and_assign_badges(desc_unified_models, desc_cited_list, ['desc_cited_1', 'desc_cited_2'])
201
+ process_and_assign_badges(desc_unified_models, desc_downloaded_list, ['desc_downloaded_1', 'desc_downloaded_2'])
202
+
203
+ highlights["desc_unique_models"] = list(desc_unified_models.values())
204
+
205
+ except Exception as e:
206
+ print(f"Erreur lors de la recherche des descendants: {e}")
207
+
208
+ # ==========================================================================
209
+ # 2. GESTION DE L'ASCENDANCE
210
+ # ==========================================================================
211
+ asc_downloads_query = """
212
+ MATCH (ascendant:Model)-[:USED_IN*1..]->(start:Model {name: $model_name})
213
+ WHERE start <> ascendant
214
+ WITH ascendant, size([(m:Model)<-[:USED_IN]-(ascendant) | m]) AS citation_count
215
+ RETURN ascendant.name AS name, citation_count, ascendant.downloads AS downloads, ascendant.task AS task,
216
+ ascendant.license AS license, ascendant.likes AS likes, ascendant.createdAt AS createdAt
217
+ ORDER BY ascendant.downloads DESC
218
+ LIMIT 1 // On ne veut que LE plus téléchargé
219
+ """
220
+ asc_cited_query = """
221
+ MATCH (ascendant:Model)-[:USED_IN*1..]->(start:Model {name: $model_name})
222
+ WHERE start <> ascendant
223
+ WITH ascendant, size([(m:Model)<-[:USED_IN]-(ascendant) | m]) AS citation_count
224
+ RETURN ascendant.name AS name, citation_count, ascendant.downloads AS downloads, ascendant.task AS task,
225
+ ascendant.license AS license, ascendant.likes AS likes, ascendant.createdAt AS createdAt
226
+ ORDER BY citation_count DESC
227
+ LIMIT 1 // On ne veut que LE plus cité
228
+ """
229
+
230
+ foundation_query = """
231
+ MATCH (foundation:Model)-[:USED_IN*1..]->(start:Model {name: $model_name})
232
+ WHERE NOT EXISTS( (:Model)-[:USED_IN]->(foundation) )
233
+ WITH foundation, size([(m:Model)<-[:USED_IN]-(foundation) | m]) AS citation_count
234
+ RETURN DISTINCT foundation.name AS name, citation_count, foundation.downloads AS downloads, foundation.task AS task,
235
+ foundation.license AS license, foundation.likes AS likes, foundation.createdAt AS createdAt
236
+ LIMIT $limit
237
+ """
238
+
239
+ try:
240
+ params = {"model_name": model_name, "limit": num_highlights}
241
+ asc_foundation_list = gds.run_cypher(foundation_query, params).to_dict('records')
242
+ asc_downloaded_list = gds.run_cypher(asc_downloads_query, params).to_dict('records')
243
+ asc_cited_list = gds.run_cypher(asc_cited_query, params).to_dict('records')
244
+
245
+ asc_unified_models = {}
246
+ # Ordre de priorité : Racine > Cité > Téléchargé
247
+ process_and_assign_badges(asc_unified_models, asc_foundation_list, ['asc_foundation'] * num_highlights) # Le badge racine s'applique à tous
248
+ process_and_assign_badges(asc_unified_models, asc_cited_list, ['asc_cited_1', 'asc_cited_2'])
249
+ process_and_assign_badges(asc_unified_models, asc_downloaded_list, ['asc_downloaded_1', 'asc_downloaded_2'])
250
+
251
+ highlights["asc_unique_models"] = list(asc_unified_models.values())
252
+
253
+ except Exception as e:
254
+ print(f"Erreur lors de la recherche des ascendants: {e}")
255
+
256
+ return highlights
257
+
258
+
259
+ def create_node_data(node_props, label):
260
+ """
261
+ Construit un dictionnaire de données pour chaque noeud
262
+ à afficher dans le graphe front-end.
263
+ """
264
+ base_data = {
265
+ "id": node_props.get("name", "")
266
+ }
267
+
268
+ if label == "Author":
269
+ return {
270
+ **base_data,
271
+ "label": node_props.get("type", "Unknown"),
272
+ "followers": node_props.get("followers", 1)
273
+ }
274
+ elif label == "Model":
275
+ licens_ =str(node_props.get("license", "Inconnue")).strip("[]")
276
+ if licens_ =="\'other\'" or pd.isna(licens_) or licens_ =="nan":
277
+ licens_ = "Autre"
278
+
279
+ tache = node_props.get("task", "")
280
+ if tache =="unknown":
281
+ tache = "Inconnue"
282
+ return {
283
+ **base_data,
284
+ "label": "Modèle",
285
+ "downloads": node_props.get("downloads", 1),
286
+ "likes": node_props.get("likes", 0),
287
+ "license": licens_,
288
+ "createdAt": node_props.get("createdAt", "inconnue"),
289
+ "createdAt_dataset": node_props.get("createdAt_dataset", "inconnue"),
290
+ "task": tache,
291
+ "author": node_props.get("author", ""),"dataset": node_props.get("dataset", ""),
292
+ "ascendantsCount": node_props.get("ascendantsCount", 0),"descendantsCount": node_props.get("descendantsCount", 0),
293
+ "citationCount": node_props.get("citationCount", 0), "distance":node_props.get("distance", 0)
294
+ }
295
+ else: # Dataset or other
296
+ return {
297
+ **base_data,
298
+ "label": "Dataset",
299
+ "downloads": node_props.get("downloads", 1),
300
+ "createdAt_dataset": node_props.get("createdAt_dataset", "inconnue")
301
+ }
302
+
303
+ return { "id": node_props['name'], "label": label, **node_props }
304
+
application_neo4j/extract_dump.md ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Pour faire tourner le space il faut extraire le fichier .dump, créé par l'instance Neo4j qui contient la base de données graphe.
2
+
3
+ Pour cela il faut s'assurer que neo4j ne tourne plus sur le serveur :
4
+ ```bash
5
+ ps aux | grep neo4j
6
+ ```
7
+ On note les PID correspondant au processus de neo4J, puis, on les arrête :
8
+ ```bash
9
+ sudo kill -SIGTERM <PID>
10
+ ```
11
+
12
+ Ensuite on peut récupérer le dump, en ayant préalablement créé un dossier vide `backups` :
13
+ ```bash
14
+ sudo neo4j-admin database dump '*' --to-path=/home/acs/genealogie/backups/ --verbose
15
+ ```
16
+ On récupère ainsi 2 fichiers .dump, celui qui nous intéresse est neo4J.dump.
17
+
18
+ Ne pas oublier de relancer neo4j, pour pouvoir faire tourner l'app sur le serveur :
19
+ ```bash
20
+ sudo neo4j start
21
+ ```
application_neo4j/load_data.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from neo4j import GraphDatabase, basic_auth
2
+ from graphdatascience import GraphDataScience
3
+ import utils as u
4
+ import argparse
5
+ # --- Configuration ---
6
+ SOURCE_CSV = 'models_with_all_information.csv'
7
+ NEO4J_URI = "bolt://localhost:7687"
8
+
9
+ # --- Configuration des arguments du script ---
10
+ parser = argparse.ArgumentParser(description="Script pour peupler une base de données Neo4j avec des données sur des modèles.")
11
+ parser.add_argument(
12
+ 'reset', # Le nom de l'argument (positionnel, car sans tirets)
13
+ type=str, # On attend une chaîne de caractères
14
+ choices=['true', 'false'], # L'utilisateur DOIT choisir entre 'true' et 'false'
15
+ help="Spécifiez 'true' pour réinitialiser la base de données, 'false' pour continuer."
16
+ )
17
+
18
+ parser.add_argument(
19
+ 'neo4j_user', # Le nom de l'argument (positionnel, car sans tirets)
20
+ type=str, # On attend une chaîne de caractères
21
+ help="Nom de votre instance Neo4J"
22
+ )
23
+
24
+ parser.add_argument(
25
+ 'neo4j_password', # Le nom de l'argument (positionnel, car sans tirets)
26
+ type=str, # On attend une chaîne de caractères
27
+ help="Mot de passe de votre instance Neo4J"
28
+ )
29
+
30
+ args = parser.parse_args()
31
+
32
+ # Conversion de l'argument string 'true'/'false' en un vrai booléen Python True/False
33
+ RESET = args.reset.lower() == 'true'
34
+ NEO4J_PASSWORD = args.neo4j_password
35
+ NEO4J_USER = args.neo4j_user
36
+
37
+ # --- Initialisation et chargement de la base Neo4j ---
38
+ # Connexion au serveur Neo4j via le driver natif
39
+ # C'est une bonne pratique de garder le driver et de ne pas créer la session tout de suite
40
+ driver = GraphDatabase.driver(NEO4J_URI, auth=basic_auth(NEO4J_USER, NEO4J_PASSWORD))
41
+
42
+ # Réinitialisation de la base si activé
43
+ if RESET:
44
+ u.reset_database(driver) # Cette fonction supprime maintenant la DB + le fichier
45
+ processed_ids = set() # On part d'un set vide
46
+ print("Reset done")
47
+ else:
48
+ # Si on ne réinitialise pas, ALORS on charge les IDs existants pour continuer
49
+ processed_ids = u.load_processed_ids()
50
+
51
+ # Création des index pour améliorer les performances des requêtes
52
+ with driver.session() as session:
53
+ session.run("CREATE INDEX IF NOT EXISTS FOR (m:Model) ON (m.name);")
54
+ session.run("CREATE INDEX IF NOT EXISTS FOR (a:Author) ON (a.name);")
55
+ session.run("CREATE INDEX IF NOT EXISTS FOR (d:Dataset) ON (d.name);")
56
+ print("Indexes created.")
57
+
58
+ # Insertion des données dans la base Neo4j depuis le fichier CSV en parallèle
59
+ u.insert_parallel('models_with_all_information.csv', driver, processed_ids)
60
+
61
+ # --- Finalisation ---
62
+ # Fermer la connexion du driver à la toute fin du script
63
+ driver.close()
64
+ print("\nScript finished. Neo4j driver connection closed.")
application_neo4j/requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ Flask==3.1.0
2
+ neo4j==5.28.1
3
+ graphdatascience==1.15.1
4
+ pandas==2.2.3
5
+ Jinja2 ==3.1.6
application_neo4j/static/.DS_Store ADDED
Binary file (6.15 kB). View file
 
application_neo4j/static/css/style.css ADDED
@@ -0,0 +1,271 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ===================================================================
2
+ * STYLE GLOBAL & GRAPHIQUE SIGMA
3
+ * =================================================================== */
4
+
5
+ #sigma-container {
6
+ width: 100%;
7
+ height: 600px; /* Un peu plus de hauteur pour un meilleur confort */
8
+ border: 1px solid var(--bs-border-color); /* Variable Bootstrap */
9
+ margin-top: 1rem;
10
+ border-radius: var(--bs-border-radius); /* Variable Bootstrap */
11
+ background-color: var(--bs-light-bg-subtle); /* Fond légèrement teinté */
12
+ }
13
+
14
+ /* ===================================================================
15
+ * STYLE DES CARTES "HIGHLIGHTS"
16
+ * =================================================================== */
17
+
18
+ /* Applique une hauteur de 100% aux cartes dans les colonnes pour un alignement parfait */
19
+ .genealogy-column .card {
20
+ height: 100%;
21
+ }
22
+
23
+ /* Style spécifique pour la carte centrale du modèle recherché */
24
+ .card-searched {
25
+ border: 2px solid var(--bs-primary); /* Bordure primaire de Bootstrap */
26
+ box-shadow: 0 4px 12px rgba(0,0,0,0.1); /* Ombre plus prononcée */
27
+ background-color: var(--bs-light); /* Fond clair Bootstrap */
28
+ }
29
+
30
+
31
+
32
+ /* ===================================================================
33
+ * STYLE DES TABLES (DATATABLES)
34
+ * =================================================================== */
35
+ /* Permet au texte de revenir à la ligne naturellement */
36
+ #descendance-table th, #descendance-table td,
37
+ #ascendance-table th, #ascendance-table td {
38
+ white-space: normal; /* <-- La correction clé ! */
39
+ word-wrap: break-word; /* Force le retour à la ligne des mots longs */
40
+ vertical-align: middle; /* Garde le centrage vertical */
41
+ }
42
+
43
+ /* On peut donner une largeur minimale aux colonnes pour garder une bonne structure */
44
+ #descendance-table th, #ascendance-table th {
45
+ min-width: 120px; /* Ajustez cette valeur selon vos besoins */
46
+ }
47
+
48
+ /* Cas spécifique pour les colonnes potentiellement longues comme le nom du modèle */
49
+ #descendance-table th:first-child, #ascendance-table th:first-child {
50
+ min-width: 200px;
51
+ }
52
+
53
+ /* Empêche le texte de se couper dans les cellules, force le défilement horizontal */
54
+ #descendance-table th, #descendance-table td,
55
+ #ascendance-table th, #ascendance-table td {
56
+ white-space: nowrap;
57
+ vertical-align: middle; /* Centrage vertical pour un look plus propre */
58
+ }
59
+
60
+ /* Personnalisation de l'input de recherche de DataTables pour qu'il ressemble à un champ Bootstrap */
61
+ div.dataTables_wrapper div.dataTables_filter input {
62
+ border: 1px solid var(--bs-border-color);
63
+ border-radius: var(--bs-border-radius);
64
+ padding: 0.375rem 0.75rem;
65
+ margin-left: 0.5em;
66
+ }
67
+
68
+ /* Amélioration de la visibilité des flèches de tri de DataTables */
69
+ div.dataTables_wrapper .table thead th.sorting::before,
70
+ div.dataTables_wrapper .table thead th.sorting::after {
71
+ opacity: 0.4;
72
+ color: var(--bs-body-color);
73
+ }
74
+
75
+ div.dataTables_wrapper .table thead th.sorting_asc::after,
76
+ div.dataTables_wrapper .table thead th.sorting_desc::before {
77
+ opacity: 1;
78
+ color: var(--bs-primary); /* Couleur primaire de Bootstrap */
79
+ }
80
+
81
+ div.dataTables_wrapper .table thead th.sorting_asc::before,
82
+ div.dataTables_wrapper .table thead th.sorting_desc::after {
83
+ opacity: 0.2;
84
+ }
85
+
86
+
87
+ /* ===================================================================
88
+ * STYLE DES COMPOSANTS D'INTERFACE
89
+ * =================================================================== */
90
+
91
+ /* NOTE: La plupart des classes de layout comme .search-controls-container, .depth-controls
92
+ * ont été supprimées car la mise en page est maintenant gérée par la grille et
93
+ * les utilitaires Flexbox de Bootstrap directement dans le HTML (row, col, d-flex, gap-3).
94
+ */
95
+
96
+ /* Style pour la liste de suggestions de l'autocomplétion */
97
+ /* Le HTML utilise maintenant <ul class="list-group">, ce CSS est donc obsolète. */
98
+ /* Si vous n'utilisez pas list-group, ce style peut être adapté. */
99
+ #suggestions-list {
100
+ /* Ce style est pour si vous utilisez une <ul> simple.
101
+ Si vous avez bien une <ul class="list-group">, vous pouvez supprimer ce bloc. */
102
+ box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
103
+ border-radius: var(--bs-border-radius);
104
+ background-color: var(--bs-light);
105
+ }
106
+ #suggestions-list li:hover {
107
+ background-color: #94bfeb; ;
108
+ }
109
+
110
+
111
+ /* Légendes du graphe */
112
+
113
+ .legend-item {
114
+ /* Cette classe est maintenant principalement utilisée pour le JS (clic) */
115
+ cursor: pointer;
116
+ transition: background-color 0.2s;
117
+ }
118
+ .legend-item:hover {
119
+ background-color: var(--bs-light-bg-subtle);
120
+ }
121
+ .legend-item.active {
122
+ font-weight: bold;
123
+ background-color: var(--bs-primary-bg-subtle);
124
+ }
125
+
126
+ /* Le style de la puce colorée reste le même */
127
+ .legend-color {
128
+ width: 15px;
129
+ height: 15px;
130
+ border-radius: 50%;
131
+ margin-right: 0.75rem;
132
+ flex-shrink: 0;
133
+ }
134
+ .legend-color.edge {
135
+ height: 4px;
136
+ width: 20px;
137
+ border-radius: 2px;
138
+ }
139
+
140
+
141
+ /* ===================================================================
142
+ * CONNECTEURS VISUELS POUR LA VUE GÉNÉALOGIQUE
143
+ * (Cette logique est indépendante du framework et n'a besoin que de la mise à jour des couleurs)
144
+ * =================================================================== */
145
+
146
+ .genealogy-row {
147
+ position: relative;
148
+ }
149
+
150
+ /* On utilise la classe de la colonne centrale pour attacher les connecteurs */
151
+ #center-column {
152
+ position: static; /* Important pour que les connecteurs ne soient pas piégés dedans */
153
+ }
154
+
155
+ /* Média query pour n'afficher les connecteurs que sur les grands écrans (lg et plus) */
156
+ @media (min-width: 992px) {
157
+ /* Connecteur vers l'ascendance (gauche) */
158
+ #center-column::before {
159
+ content: '';
160
+ position: absolute;
161
+ z-index: -1;
162
+ top: 100px; /* Ajustez cette hauteur pour aligner avec vos cartes */
163
+
164
+ left: 20%; /* Commence à 20% de la largeur */
165
+ right: 60%; /* S'arrête à 40% de la largeur */
166
+
167
+ height: 3px;
168
+ background-color: var(--bs-secondary-border-subtle); /* Couleur Bootstrap subtile */
169
+ border-radius: 3px;
170
+ }
171
+
172
+ /* Connecteur vers la descendance (droite) */
173
+ #center-column::after {
174
+ content: '';
175
+ position: absolute;
176
+ z-index: -1;
177
+ top: 100px; /* Même hauteur */
178
+
179
+ left: 60%;
180
+ right: 20%;
181
+
182
+ height: 3px;
183
+ background-color: var(--bs-secondary-border-subtle);
184
+ border-radius: 3px;
185
+ }
186
+ }
187
+
188
+ /* Style de base pour la carte interactive */
189
+ .card-interactive {
190
+ /* Ajoute une transition douce pour tous les changements de style (couleur, ombre...) */
191
+ transition: background-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
192
+ }
193
+
194
+
195
+ .form-check-input:checked {
196
+ background-color: #6a11cb; /* Bleu primaire par défaut */
197
+ border-color:#6a11cb; /* Bleu primaire par défaut */
198
+ }
199
+
200
+ /* Change la couleur de fond des cases à cocher lorsqu'elles sont cochées */
201
+ .btn {
202
+ background-color: #6a11cb;
203
+ border-color:#6a11cb;
204
+ }
205
+
206
+ .stretched-link{
207
+ color: #6a11cb;
208
+ }
209
+ /* Style appliqué LORSQUE LA SOURIS SURVOLE la carte */
210
+ .card-interactive:hover {
211
+ cursor: pointer; /* Le curseur change en main, indiquant un clic possible */
212
+ background-color: #94bfeb; /* Une couleur de fond légèrement grise (standard Bootstrap) */
213
+
214
+ /* Optionnel : ajoute une ombre subtile pour faire "ressortir" la carte */
215
+ box-shadow: 0 4px 8px rgba(0,0,0,0.1);
216
+ }
217
+
218
+ .card-recherche p:not(.text-muted) {
219
+ color: #212529 !important; /* Couleur noire standard de Bootstrap */
220
+
221
+ }
222
+
223
+ .card-recherche{
224
+ border-color:#6a11cb;
225
+ }
226
+
227
+ .card-recherche-header{
228
+ border-color:#6a11cb;
229
+ background-color: #6a11cb;
230
+
231
+ }
232
+ .on-top {
233
+ position: relative;
234
+ z-index: 2; /* Doit être supérieur à 1 (le z-index du stretched-link) */
235
+ }
236
+
237
+ .beta {
238
+ background-color: #6a11cb;
239
+ }
240
+
241
+ .alpha {
242
+ background-color: #b476f7;
243
+ color : #000000;
244
+ }
245
+
246
+ main {
247
+ margin-bottom: 5rem; /* ajuste la valeur selon tes besoins */
248
+ }
249
+
250
+ #floating-legend .legend-color { width: 1em; height: 1em; display: inline-block; border-radius: 2px; }
251
+ #floating-legend .legend-color.edge { border-radius: 0; height: 0.25em; transform: translateY(-0.35em); }
252
+
253
+ .btn-modern {
254
+ min-width: 200px;
255
+ padding: 14px 24px;
256
+ font-weight: bold;
257
+ color: white;
258
+ background: linear-gradient(135deg, #6a11cb, #f8a5f8);
259
+ border: none;
260
+ border-radius: 50px;
261
+ cursor: pointer;
262
+ transition: all 0.3s ease;
263
+ box-shadow: 0 3px 8px rgba(0,0,0,0.15);
264
+ text-align: center;
265
+ text-decoration: none;
266
+ }
267
+
268
+ .btn-modern:hover {
269
+ background: linear-gradient(135deg, #5a0fbf, #1d63e1);
270
+ transform: translateY(-3px);
271
+ }
application_neo4j/static/graph_export.html ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Export Graphe Sigma</title>
6
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
7
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
8
+ <script src="https://unpkg.com/graphology@0.25.1/dist/graphology.umd.min.js"></script>
9
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/sigma.js/2.4.0/sigma.min.js"></script>
10
+ <style>
11
+ body { margin:0; font-family: Arial, sans-serif; }
12
+ #sigma-container { width: 100%; height: 70vh; border: 1px solid #ccc; border-radius: 5px; margin-bottom: 1rem; }
13
+ .legend-card { margin-bottom: 1rem; }
14
+ .legend-card .card-body { max-height: 40vh; overflow-y: auto; }
15
+ .legend-color { display:inline-block; width:20px; height:20px; border-radius:3px; }
16
+ </style>
17
+ </head>
18
+ <body>
19
+ <div class="container-fluid p-3">
20
+ <div class="row mb-2">
21
+ <div class="col">
22
+ <h3 class="text-center" id="node_name"></h3>
23
+ </div>
24
+ </div>
25
+
26
+ <div class="row">
27
+ <!-- Colonne des légendes -->
28
+ <div class="col-lg-3">
29
+ <div class="card legend-card">
30
+ <div class="card-header"><strong>Légende - Nœuds</strong></div>
31
+ <div class="card-body" id="legend-nodes"></div>
32
+ </div>
33
+ <div class="card legend-card">
34
+ <div class="card-header"><strong>Légende - Relations</strong></div>
35
+ <div class="card-body" id="legend-edges"></div>
36
+ </div>
37
+ </div>
38
+
39
+ <!-- Colonne du graphe -->
40
+ <div class="col-lg-9">
41
+ <div id="sigma-container"></div>
42
+ </div>
43
+ </div>
44
+ </div>
45
+ <script>
46
+ // Ces variables seront remplacées dynamiquement
47
+ const GRAPH_DATA = `{{GRAPH_DATA}}`;
48
+ const graphData = JSON.parse(GRAPH_DATA);
49
+ const LEGEND_HTML_NODES = `{{LEGEND_HTML_NODES}}`;
50
+ const LEGEND_HTML_EDGES = `{{LEGEND_HTML_EDGES}}`;
51
+ const NODE_NAME = `{{NODE_NAME}}`;
52
+
53
+ // On injecte les légendes
54
+ document.getElementById("legend-nodes").innerHTML = LEGEND_HTML_NODES;
55
+ document.getElementById("legend-edges").innerHTML = LEGEND_HTML_EDGES;
56
+ document.getElementById("node_name").textContent = NODE_NAME ? ("Composante connexe du noeud : " + NODE_NAME) : "";
57
+ // Reconstruction du graphe Sigma
58
+ const Graph = window.graphology.Graph;
59
+ const graph = new Graph();
60
+
61
+ graphData.nodes.forEach(n => graph.addNode(n.id, n));
62
+ graphData.edges.forEach(e => graph.addEdge(e.source, e.target, e));
63
+
64
+ new Sigma(graph, document.getElementById("sigma-container"));
65
+ </script>
66
+ </body>
67
+ </html>
application_neo4j/static/js/.DS_Store ADDED
Binary file (6.15 kB). View file
 
application_neo4j/static/js/script.js ADDED
@@ -0,0 +1,351 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ /**
3
+ * Fonction principale pour créer et afficher le graphe Sigma.
4
+ * Appelée une seule fois lorsque l'utilisateur clique sur "Voir l'arbre".
5
+ * @param {object} graphData - Les données des nœuds et arêtes.
6
+ * @returns {Sigma} L'instance du renderer Sigma.
7
+ */
8
+ function initializeSigmaGraph(graphData) {
9
+ const container = document.getElementById("sigma-container");
10
+ if (!container) {
11
+ console.error("Conteneur Sigma non trouvé. Initialisation annulée.");
12
+ return null;
13
+ }
14
+
15
+ console.log("Initialisation du graphe Sigma...");
16
+
17
+ const Graph = window.graphology.Graph;
18
+ const graph = new Graph();
19
+ const TYPE_COLORS = {
20
+ personne: '#007bff',
21
+ organisation: '#007',
22
+ Modèle: '#b1b1b1',
23
+ Dataset: '#ffc107',
24
+ Author: '#007bff',
25
+ Model: '#b1b1b1',
26
+ Unknown: '#999',
27
+ Défaut: '#999'
28
+ };
29
+ const addedNodeIds = new Set();
30
+ graphData.nodes.forEach(node => {
31
+ if (addedNodeIds.has(node.id)) return;
32
+ const rawSize = Math.log10(node.downloads || node.followers || 1) * 2 + (depth === 0 ? 5 : 0);
33
+ graph.addNode(node.id, {
34
+ id: node.id, label: node.id, size: Math.max(5, rawSize), color: TYPE_COLORS[node.label] || '#999',
35
+ dataCat: node.label, x: Math.random(), y: Math.random(),
36
+ followers: node.followers, downloads: node.downloads, createdAt: node.createdAt,
37
+ createdAt_dataset: node.createdAt_dataset, task: node.task, bfsDepth: node.distance
38
+ });
39
+ addedNodeIds.add(node.id);
40
+ });
41
+
42
+ graphData.edges.forEach(edge => {
43
+ console.log(edge.relation);
44
+ const edgeColor = edgeInfos[edge.relation]["color"] || "#ccc";
45
+
46
+ if (graph.hasNode(edge.source) && graph.hasNode(edge.target)) {
47
+ graph.addEdge(edge.source, edge.target, {
48
+ label: edge.relation || '',
49
+ color: edgeColor || '#ccc',
50
+ size: 3,
51
+ type: 'arrow'
52
+ });
53
+ } else {
54
+ console.warn(`Arête ignorée: ${edge.source} -> ${edge.target} (nœuds manquants)`);
55
+ }
56
+ });
57
+
58
+ graphologyLibrary.layoutForceAtlas2.assign(graph, {
59
+ iterations: 100,
60
+ settings: {
61
+ gravity: 0.01, // attraction faible vers le centre
62
+ scalingRatio: 10, // augmente fortement la répulsion
63
+ slowDown: 1, // vitesse normale
64
+ strongGravityMode: false,
65
+ barnesHutOptimize: true,
66
+ barnesHutTheta: 0.5 // précision de l’approximation
67
+ }
68
+ });
69
+ const renderer = new Sigma(graph, container);
70
+ // Juste après l'initialisation du renderer
71
+ const state = {
72
+ hoveredNode: null,
73
+ hoveredNeighbors: null
74
+ };
75
+ renderer.setSetting("nodeReducer", (node, data) => {
76
+ const res = { ...data };
77
+
78
+ if (state.hoveredNode && state.hoveredPredecessors) {
79
+ // Garder seulement le nœud survolé et ses prédécesseurs visibles en couleur
80
+ if (!state.hoveredPredecessors.has(node)) {
81
+ res.color = "#f1f1f1"; // gris
82
+ res.label = ""; // masquer le label
83
+ }
84
+ }
85
+
86
+ return res;
87
+ });
88
+
89
+ renderer.setSetting("edgeReducer", (edge, data) => {
90
+ const res = { ...data };
91
+
92
+ if (state.hoveredNode && state.hoveredPredecessors) {
93
+ const [source, target] = graph.extremities(edge);
94
+ // Montrer seulement les arêtes dans le chemin des prédécesseurs
95
+ if (!state.hoveredPredecessors.has(source) || !state.hoveredPredecessors.has(target)) {
96
+ res.hidden = true;
97
+ }
98
+ }
99
+
100
+ return res;
101
+ });
102
+
103
+ buildEdgeLegend();
104
+
105
+ // Mettre en place les interactions
106
+ const filtersContainer = document.getElementById('graph-filters-container');
107
+ if (filtersContainer) {
108
+ filtersContainer.addEventListener('change', (event) => {
109
+ if (event.target.type === 'checkbox') {
110
+ updateGraphVisibility(graph, renderer);
111
+ }
112
+ });
113
+ }
114
+
115
+ renderer.on('clickNode', ({ node }) => {
116
+ showNodeInfo(graph.getNodeAttributes(node));
117
+ });
118
+ // Quand on survole un nœud
119
+ renderer.on("enterNode", ({ node }) => {
120
+ state.hoveredNode = node;
121
+ state.hoveredPredecessors = getAllPredecessors(graph, node);
122
+ renderer.refresh({ skipIndexation: true });
123
+ });
124
+
125
+ // Quand on sort du nœud
126
+ renderer.on("leaveNode", () => {
127
+ state.hoveredNode = null;
128
+ state.hoveredPredecessors = null;
129
+ renderer.refresh({ skipIndexation: true });
130
+ });
131
+
132
+ setupTableRowClickListeners(renderer, graph);
133
+
134
+ return renderer;
135
+ }
136
+
137
+ /**
138
+ * Met en place l'écouteur de clic sur les lignes des tableaux pour interagir avec le graphe.
139
+ * @param {Sigma} renderer - L'instance du renderer Sigma.
140
+ * @param {Graph} graph - L'instance du graphe Graphology.
141
+ */
142
+ function setupTableRowClickListeners(renderer, graph) {
143
+ $('#descendance-table tbody, #ascendance-table tbody').on('click', 'tr', function() {
144
+ const nodeId = $(this).attr('data-node-id');
145
+ if (!nodeId || !renderer || !graph.hasNode(nodeId)) return;
146
+ // Effet de "highlight" temporaire sur le nœud dans le graphe
147
+ const originalSize = graph.getNodeAttribute(nodeId, "size");
148
+ const highlightSize = originalSize * 1.6;
149
+
150
+ graph.setNodeAttribute(nodeId, "size", highlightSize);
151
+ renderer.refresh(); // Nécessaire pour forcer le rendu
152
+
153
+ setTimeout(() => {
154
+ graph.setNodeAttribute(nodeId, "size", originalSize);
155
+ renderer.refresh();
156
+ }, 1000);
157
+ // Supprime la classe active des autres lignes
158
+ $('#descendance-table tbody tr, #ascendance-table tbody tr').removeClass('active');
159
+ $(this).addClass('active');
160
+
161
+ showNodeInfo(graph.getNodeAttributes(nodeId));
162
+
163
+
164
+ const pos = renderer.getNodeDisplayData(nodeId);
165
+ if (pos) {
166
+ renderer.getCamera().animate({ x: pos.x, y: pos.y, ratio: 0.2 }, { duration: 600 });
167
+ }
168
+ });
169
+
170
+ }
171
+
172
+ // ===================================================================
173
+ // POINT D'ENTRÉE PRINCIPAL - EXÉCUTÉ AU CHARGEMENT DE LA PAGE
174
+ // ===================================================================
175
+ document.addEventListener("DOMContentLoaded", () => {
176
+
177
+ // --- ÉTAPE 1: LECTURE DES DONNÉES ET INITIALISATION DES TABLEAUX ---
178
+
179
+ const sigmaContainer = document.getElementById("sigma-container");
180
+ let graphData;
181
+
182
+ if (sigmaContainer && sigmaContainer.dataset.graph) {
183
+ try {
184
+ graphData = JSON.parse(sigmaContainer.dataset.graph);
185
+ } catch (e) {
186
+ console.error("Erreur d'analyse JSON:", e);
187
+ return;
188
+ }
189
+
190
+ const common_dt_options = {
191
+ "language": { "url": "https://cdn.datatables.net/plug-ins/1.13.4/i18n/fr-FR.json" },
192
+ "pageLength": 10,
193
+ "responsive": true,"scrollX": true , "scrollY":true,
194
+ "columnDefs": [
195
+ {
196
+ // Appliquer notre plugin 'numeric-string' aux colonnes cibles
197
+ "type": "numeric-string",
198
+ "targets": [2, 4, 6, 7, 8, 9] // Indices des colonnes: Downloads, Likes, Distance, etc.
199
+ }
200
+ ]
201
+ // "searching" est activé par défaut, on peut le customiser
202
+ };
203
+ const tableDescendance = $('#descendance-table').DataTable(common_dt_options);
204
+ const tableAscendance = $('#ascendance-table').DataTable(common_dt_options);
205
+
206
+ // Gérer la barre de recherche personnalisée
207
+ const customSearch = document.getElementById('custom-table-search');
208
+ if (customSearch) {
209
+ customSearch.addEventListener('keyup', function() {
210
+ const query = this.value;
211
+ tableDescendance.search(query).draw();
212
+ tableAscendance.search(query).draw();
213
+ });
214
+ }
215
+
216
+ tableAscendance.clear();
217
+ tableDescendance.clear();
218
+
219
+ graphData.nodes.forEach(node => {
220
+ // Filtrer pour n'inclure que les modèles dans ces tableaux
221
+ if (node.label !== 'Modèle' && node.label !== 'Model') return;
222
+ const distance = node.distance;
223
+ if (distance === 0) return;
224
+
225
+ // Construction de la ligne avec les nouvelles données
226
+ const rowData = [
227
+ node.id || "N/A",
228
+ node.author || "Inconnu",
229
+ // La conversion en string via la condition est parfaite ici
230
+ String(node.downloads ?? "Inconnu"),
231
+ node.task || "Inconnue",
232
+ String(node.likes ?? "0"),
233
+ String(node.createdAt ?? "Inconnue"),
234
+ node.dataset || "Inconnu",
235
+ node.license || "Inconnue",
236
+ distance > 0 ? `+${distance}` : String(distance ?? 0),
237
+ String(node.ascendantsCount ?? "0"),
238
+ String(node.descendantsCount ?? "0"),
239
+ String(node.citationCount ?? "0")
240
+ ];
241
+
242
+ const tableToUpdate = distance > 0 ? tableDescendance : tableAscendance;
243
+ tableToUpdate.row.add(rowData).node().setAttribute("data-node-id", node.id);
244
+ });
245
+
246
+ tableAscendance.draw();
247
+ tableDescendance.draw();
248
+
249
+ }
250
+
251
+ // --- ÉTAPE 2: GESTION DES INTERACTIONS DE L'UTILISATEUR ---
252
+
253
+ // Logique du bouton "Voir l'arbre généalogique"
254
+ const showGraphBtn = document.getElementById('show-graph-btn');
255
+ const graphSection = document.getElementById('genealogy-graph-section');
256
+ let sigmaInstance = null; // Garde en mémoire l'instance du graphe
257
+
258
+ if (showGraphBtn && graphSection && graphData) {
259
+ showGraphBtn.addEventListener('click', () => {
260
+ if (graphSection.style.display === 'none') {
261
+ graphSection.style.display = 'block';
262
+
263
+ if (!sigmaInstance) {
264
+ sigmaInstance = initializeSigmaGraph(graphData);
265
+ }
266
+ graphSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
267
+ } else {
268
+ graphSection.style.display = 'none';
269
+ document.getElementById("node-info-card").style.display = "none";
270
+ }
271
+ });
272
+ }
273
+
274
+ const input = document.getElementById('search-input');
275
+ const suggestionsList = document.getElementById('suggestions-list');
276
+ const modelCheckbox = document.getElementById('filter-model');
277
+ const datasetCheckbox = document.getElementById('filter-dataset');
278
+
279
+ // Cette fonction sera notre point central pour l'autocomplétion
280
+ function fetchAutocomplete() {
281
+ const query = input.value.trim();
282
+
283
+ // 1. Déterminer quel filtre est actif
284
+ let activeFilter = '';
285
+ if (modelCheckbox.checked) {
286
+ activeFilter = 'Model';
287
+ } else if (datasetCheckbox.checked) {
288
+ activeFilter = 'Dataset';
289
+ }
290
+
291
+ // Si le champ est vide, on cache les suggestions
292
+ if (query.length < 2) {
293
+ suggestionsList.style.display = 'none';
294
+ return;
295
+ }
296
+
297
+ // 2. Construire l'URL avec le filtre
298
+ // On commence avec la requête de base
299
+ let fetchUrl = `/autocomplete?q=${encodeURIComponent(query)}`;
300
+ // On ajoute le paramètre 'filter' SEULEMENT s'il y en a un d'actif
301
+ if (activeFilter) {
302
+ fetchUrl += `&filter=${activeFilter}`;
303
+ }
304
+
305
+ // 3. Lancer la requête fetch avec la bonne URL
306
+ fetch(fetchUrl)
307
+ .then(response => response.json())
308
+ .then(data => {
309
+ suggestionsList.innerHTML = '';
310
+ if (data.length > 0) {
311
+ data.forEach(item => {
312
+ const li = document.createElement('li');
313
+ li.textContent = item.name;
314
+ li.addEventListener('click', () => {
315
+ input.value = item.name;
316
+ suggestionsList.style.display = 'none';
317
+ });
318
+ suggestionsList.appendChild(li);
319
+ });
320
+ suggestionsList.style.display = 'block';
321
+ } else {
322
+ suggestionsList.style.display = 'none';
323
+ }
324
+ });
325
+ }
326
+
327
+ // On écoute les événements sur le champ de saisie
328
+ input.addEventListener('input', fetchAutocomplete);
329
+
330
+ // AMÉLIORATION : On écoute aussi les changements sur les checkboxes !
331
+ // Si l'utilisateur change de filtre, on relance immédiatement une recherche.
332
+ modelCheckbox.addEventListener('change', fetchAutocomplete);
333
+ datasetCheckbox.addEventListener('change', fetchAutocomplete);
334
+
335
+ // Logique pour cacher les suggestions si on clique ailleurs (inchangée)
336
+ document.addEventListener('click', (e) => {
337
+ if (!input.contains(e.target) && !suggestionsList.contains(e.target)) {
338
+ suggestionsList.style.display = 'none';
339
+ }
340
+ });
341
+
342
+
343
+ // Logique de la checkbox de profondeur (inchangée)
344
+ const checkbox = document.getElementById('depth-unlimited');
345
+ if (checkbox) {
346
+ const depthInput = document.getElementById('depth');
347
+ function toggleDepthInput() { if(depthInput) depthInput.disabled = checkbox.checked; }
348
+ checkbox.addEventListener('change', toggleDepthInput);
349
+ toggleDepthInput();
350
+ }
351
+ });
application_neo4j/static/js/script_dataset.js ADDED
@@ -0,0 +1,357 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const edgeInfos_dataset = {
2
+ "Fait partie de cette organisation": {
3
+ color: "#a05195",name : "Fait partie de cette organisation",
4
+ tooltip: "Cette personne est membre de cette organisation"
5
+ },
6
+ "A publié": {
7
+ color: "#003f5c",name : "A publié",
8
+ tooltip: "Un auteur (personne ou organistaion) a publié un modèle"
9
+ },
10
+ "A été utilisé dans ce modèle": {
11
+ color: "#f50f0f",name : "A été utilisé dans ce modèle",
12
+ tooltip: "Ce dataset a servi pour l'entraînement de ce modèle"
13
+
14
+ },
15
+ "A généré": {
16
+ color: "#8f7340",name : "A permis de générer",
17
+ tooltip: "Le modèle source a été téléchargé et modifié afin de créer le modèle cible (type de transformation inconnu)"
18
+
19
+ },
20
+ "finetune": {
21
+ color: "#d9c00d",name : "Finetune",
22
+ tooltip: "Ajustement : le modèle source est ré-entraîné sur un jeu de données spécifique afin de pouvoir être performant pour une tâche précise."
23
+
24
+ },
25
+ "adapter": {
26
+ color: "#9af17c",name : "Adapter",
27
+
28
+ tooltip: "Adaptation : méthode d’ajustement qui peut être utilisée avec peu de ressources de calcul."
29
+ },
30
+ "quantized": {
31
+ color: "#1bc3c6",name : "Quantized",
32
+ tooltip:"Quantisation : la précision des poids du modèle source est réduite afin de diminuer son empreinte en mémoire."
33
+ },
34
+ "merge": {
35
+ color: "#e407e4",name : "Merge",
36
+ tooltip:"Fusion : méthode visant à mélanger des couches de différents modèles pour améliorer leur performance."
37
+ }
38
+ };
39
+
40
+
41
+ function buildEdgeLegend() {
42
+ const legendContainer = document.getElementById("legend-edges");
43
+ legendContainer.innerHTML = ""; // reset
44
+
45
+ Object.entries(edgeInfos_dataset).forEach(([relation, {color, name,tooltip}]) => {
46
+ const li = document.createElement("li");
47
+ li.className = "list-group-item d-flex align-items-center";
48
+
49
+ // ajout Bootstrap tooltip
50
+ li.setAttribute("data-bs-toggle", "tooltip");
51
+ li.setAttribute("data-bs-placement", "top");
52
+ li.setAttribute("data-bs-html", "true");
53
+ li.setAttribute("title", tooltip);
54
+
55
+ const span = document.createElement("span");
56
+ span.className = "legend-color edge me-2";
57
+ span.style.backgroundColor = color;
58
+
59
+ li.appendChild(span);
60
+ li.appendChild(document.createTextNode(name));
61
+
62
+ legendContainer.appendChild(li);
63
+ });
64
+
65
+ // nécessaire pour activer les tooltips Bootstrap dynamiques
66
+ const tooltipTriggerList = [].slice.call(legendContainer.querySelectorAll('[data-bs-toggle="tooltip"]'))
67
+ tooltipTriggerList.map(el => new bootstrap.Tooltip(el));
68
+ }
69
+
70
+ /**
71
+ * Fonction principale pour créer et afficher le graphe Sigma.
72
+ * Appelée une seule fois lorsque l'utilisateur clique sur "Voir l'arbre".
73
+ * @param {object} graphData - Les données des nœuds et arêtes.
74
+ * @returns {Sigma} L'instance du renderer Sigma.
75
+ */
76
+ function initializeSigmaGraph(graphData) {
77
+ const container = document.getElementById("sigma-container");
78
+ if (!container) {
79
+ console.error("Conteneur Sigma non trouvé. Initialisation annulée.");
80
+ return null;
81
+ }
82
+
83
+ console.log("Initialisation du graphe Sigma...");
84
+
85
+ const Graph = window.graphology.Graph;
86
+ const graph = new Graph();
87
+ const TYPE_COLORS = {
88
+ personne: '#007bff',
89
+ organisation: '#007',
90
+ Modèle: '#b1b1b1',
91
+ Dataset: '#ffc107',
92
+ Author: '#007bff',
93
+ Model: '#b1b1b1',
94
+ Unknown: '#999',
95
+ Défaut: '#999'
96
+ };
97
+ const addedNodeIds = new Set();
98
+ graphData.nodes.forEach(node => {
99
+ if (addedNodeIds.has(node.id)) return;
100
+ const rawSize = Math.log10(node.downloads || node.followers || 1) * 2 + (depth === 0 ? 5 : 0);
101
+ graph.addNode(node.id, {
102
+ id: node.id, label: node.id, size: Math.max(5, rawSize), color: TYPE_COLORS[node.label] || '#999',
103
+ dataCat: node.label, x: Math.random(), y: Math.random(),
104
+ followers: node.followers, downloads: node.downloads, createdAt: node.createdAt,
105
+ createdAt_dataset: node.createdAt_dataset, task: node.task, bfsDepth: node.distance
106
+ });
107
+ addedNodeIds.add(node.id);
108
+ });
109
+
110
+ graphData.edges.forEach(edge => {
111
+ console.log(edge.relation);
112
+ const edgeColor = edgeInfos_dataset[edge.relation]["color"] || "#ccc";
113
+
114
+ if (graph.hasNode(edge.source) && graph.hasNode(edge.target)) {
115
+ graph.addEdge(edge.source, edge.target, {
116
+ label: edge.relation || '',
117
+ color: edgeColor || '#ccc',
118
+ size: 3,
119
+ type: 'arrow'
120
+ });
121
+ } else {
122
+ console.warn(`Arête ignorée: ${edge.source} -> ${edge.target} (nœuds manquants)`);
123
+ }
124
+ });
125
+
126
+ graphologyLibrary.layoutForceAtlas2.assign(graph, { iterations: 100 });
127
+ const renderer = new Sigma(graph, container);
128
+ buildEdgeLegend();
129
+
130
+ // Mettre en place les interactions
131
+ const filtersContainer = document.getElementById('graph-filters-container');
132
+ if (filtersContainer) {
133
+ filtersContainer.addEventListener('change', (event) => {
134
+ if (event.target.type === 'checkbox') {
135
+ updateGraphVisibility(graph, renderer);
136
+ }
137
+ });
138
+ }
139
+
140
+ renderer.on('clickNode', ({ node }) => {
141
+ showNodeInfo(graph.getNodeAttributes(node));
142
+ });
143
+
144
+ setupTableRowClickListeners(renderer, graph);
145
+
146
+ return renderer;
147
+ }
148
+
149
+ /**
150
+ * Met en place l'écouteur de clic sur les lignes des tableaux pour interagir avec le graphe.
151
+ * @param {Sigma} renderer - L'instance du renderer Sigma.
152
+ * @param {Graph} graph - L'instance du graphe Graphology.
153
+ */
154
+ function setupTableRowClickListeners(renderer, graph) {
155
+ $('#train-table tbody').on('click', 'tr', function() {
156
+ const nodeId = $(this).attr('data-node-id');
157
+ if (!nodeId || !renderer || !graph.hasNode(nodeId)) return;
158
+ // Effet de "highlight" temporaire sur le nœud dans le graphe
159
+ const originalSize = graph.getNodeAttribute(nodeId, "size");
160
+ const highlightSize = originalSize * 1.6;
161
+
162
+ graph.setNodeAttribute(nodeId, "size", highlightSize);
163
+ renderer.refresh(); // Nécessaire pour forcer le rendu
164
+
165
+ setTimeout(() => {
166
+ graph.setNodeAttribute(nodeId, "size", originalSize);
167
+ renderer.refresh();
168
+ }, 1000);
169
+ // Supprime la classe active des autres lignes
170
+ $('#train-table tbody tr').removeClass('active');
171
+ $(this).addClass('active');
172
+
173
+ showNodeInfo(graph.getNodeAttributes(nodeId));
174
+
175
+
176
+ const pos = renderer.getNodeDisplayData(nodeId);
177
+ if (pos) {
178
+ renderer.getCamera().animate({ x: pos.x, y: pos.y, ratio: 0.2 }, { duration: 600 });
179
+ }
180
+ });
181
+
182
+ }
183
+
184
+ // ===================================================================
185
+ // POINT D'ENTRÉE PRINCIPAL - EXÉCUTÉ AU CHARGEMENT DE LA PAGE
186
+ // ===================================================================
187
+ document.addEventListener("DOMContentLoaded", () => {
188
+
189
+ // --- ÉTAPE 1: LECTURE DES DONNÉES ET INITIALISATION DES TABLEAUX ---
190
+
191
+ const sigmaContainer = document.getElementById("sigma-container");
192
+ let graphData;
193
+
194
+ if (sigmaContainer && sigmaContainer.dataset.graph) {
195
+ try {
196
+ graphData = JSON.parse(sigmaContainer.dataset.graph);
197
+ } catch (e) {
198
+ console.error("Erreur d'analyse JSON:", e);
199
+ return;
200
+ }
201
+
202
+ const common_dt_options = {
203
+ "language": { "url": "https://cdn.datatables.net/plug-ins/1.13.4/i18n/fr-FR.json" },
204
+ "pageLength": 10,
205
+ "responsive": true,"scrollX": true , "scrollY":true,
206
+ "columnDefs": [
207
+ {
208
+ // Appliquer notre plugin 'numeric-string' aux colonnes cibles
209
+ "type": "numeric-string",
210
+ "targets": [2, 4, 6, 7, 8, 9] // Indices des colonnes: Downloads, Likes, Distance, etc.
211
+ }
212
+ ]
213
+ // "searching" est activé par défaut, on peut le customiser
214
+ };
215
+ const tableTrain = $('#train-table').DataTable(common_dt_options);
216
+
217
+ // Gérer la barre de recherche personnalisée
218
+ const customSearch = document.getElementById('custom-table-search');
219
+ if (customSearch) {
220
+ customSearch.addEventListener('keyup', function() {
221
+ const query = this.value;
222
+ tableTrain.search(query).draw();
223
+ });
224
+ }
225
+
226
+ tableTrain.clear();
227
+ console.log("GraphData nodes:", graphData.nodes);
228
+
229
+ graphData.nodes.forEach(node => {
230
+ // Filtrer pour n'inclure que les modèles dans ces tableaux
231
+ if (node.label !== 'Modèle' && node.label !== 'Model') return;
232
+ const distance = node.distance;
233
+
234
+ // Construction de la ligne avec les nouvelles données
235
+ const rowData = [
236
+ node.id || "N/A",
237
+ node.author || "Inconnu",
238
+ // La conversion en string via la condition est parfaite ici
239
+ String(node.downloads ?? "Inconnu"),
240
+ node.task || "Inconnue",
241
+ String(node.likes ?? "0"),
242
+ String(node.createdAt ?? "Inconnue"),
243
+ node.dataset || "Inconnu",
244
+ node.license || "Inconnue",
245
+ String(node.ascendantsCount ?? "0"),
246
+ String(node.descendantsCount ?? "0"),
247
+ String(node.citationCount ?? "0")
248
+ ];
249
+
250
+ tableTrain.row.add(rowData).node().setAttribute("data-node-id", node.id);
251
+ });
252
+
253
+ tableTrain.draw();
254
+
255
+ }
256
+
257
+ // --- ÉTAPE 2: GESTION DES INTERACTIONS DE L'UTILISATEUR ---
258
+
259
+ // Logique du bouton "Voir l'arbre généalogique"
260
+ const showGraphBtn = document.getElementById('show-graph-btn');
261
+ const graphSection = document.getElementById('genealogy-graph-section');
262
+ let sigmaInstance = null; // Garde en mémoire l'instance du graphe
263
+
264
+ if (showGraphBtn && graphSection && graphData) {
265
+ showGraphBtn.addEventListener('click', () => {
266
+ if (graphSection.style.display === 'none') {
267
+ graphSection.style.display = 'block';
268
+
269
+ if (!sigmaInstance) {
270
+ sigmaInstance = initializeSigmaGraph(graphData);
271
+ }
272
+ graphSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
273
+ } else {
274
+ graphSection.style.display = 'none';
275
+ document.getElementById("node-info-card").style.display = "none";
276
+ }
277
+ });
278
+ }
279
+
280
+ const input = document.getElementById('search-input');
281
+ const suggestionsList = document.getElementById('suggestions-list');
282
+ const modelCheckbox = document.getElementById('filter-model');
283
+ const datasetCheckbox = document.getElementById('filter-dataset');
284
+
285
+ // Cette fonction sera notre point central pour l'autocomplétion
286
+ function fetchAutocomplete() {
287
+ const query = input.value.trim();
288
+
289
+ // 1. Déterminer quel filtre est actif
290
+ let activeFilter = '';
291
+ if (modelCheckbox.checked) {
292
+ activeFilter = 'Model';
293
+ } else if (datasetCheckbox.checked) {
294
+ activeFilter = 'Dataset';
295
+ }
296
+
297
+ // Si le champ est vide, on cache les suggestions
298
+ if (query.length < 2) {
299
+ suggestionsList.style.display = 'none';
300
+ return;
301
+ }
302
+
303
+ // 2. Construire l'URL avec le filtre
304
+ // On commence avec la requête de base
305
+ let fetchUrl = `/autocomplete?q=${encodeURIComponent(query)}`;
306
+ // On ajoute le paramètre 'filter' SEULEMENT s'il y en a un d'actif
307
+ if (activeFilter) {
308
+ fetchUrl += `&filter=${activeFilter}`;
309
+ }
310
+
311
+ // 3. Lancer la requête fetch avec la bonne URL
312
+ fetch(fetchUrl)
313
+ .then(response => response.json())
314
+ .then(data => {
315
+ suggestionsList.innerHTML = '';
316
+ if (data.length > 0) {
317
+ data.forEach(item => {
318
+ const li = document.createElement('li');
319
+ li.textContent = item.name;
320
+ li.addEventListener('click', () => {
321
+ input.value = item.name;
322
+ suggestionsList.style.display = 'none';
323
+ });
324
+ suggestionsList.appendChild(li);
325
+ });
326
+ suggestionsList.style.display = 'block';
327
+ } else {
328
+ suggestionsList.style.display = 'none';
329
+ }
330
+ });
331
+ }
332
+
333
+ // On écoute les événements sur le champ de saisie
334
+ input.addEventListener('input', fetchAutocomplete);
335
+
336
+ // AMÉLIORATION : On écoute aussi les changements sur les checkboxes !
337
+ // Si l'utilisateur change de filtre, on relance immédiatement une recherche.
338
+ modelCheckbox.addEventListener('change', fetchAutocomplete);
339
+ datasetCheckbox.addEventListener('change', fetchAutocomplete);
340
+
341
+ // Logique pour cacher les suggestions si on clique ailleurs (inchangée)
342
+ document.addEventListener('click', (e) => {
343
+ if (!input.contains(e.target) && !suggestionsList.contains(e.target)) {
344
+ suggestionsList.style.display = 'none';
345
+ }
346
+ });
347
+
348
+
349
+ // Logique de la checkbox de profondeur (inchangée)
350
+ const checkbox = document.getElementById('depth-unlimited');
351
+ if (checkbox) {
352
+ const depthInput = document.getElementById('depth');
353
+ function toggleDepthInput() { if(depthInput) depthInput.disabled = checkbox.checked; }
354
+ checkbox.addEventListener('change', toggleDepthInput);
355
+ toggleDepthInput();
356
+ }
357
+ });
application_neo4j/static/js/script_expert.js ADDED
@@ -0,0 +1,377 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ /**
3
+ * Construit les cases à cocher pour les filtres d'arêtes dynamiquement.
4
+ */
5
+ function buildEdgeFilters() {
6
+ const container = document.getElementById('edge-filters-container');
7
+ if (!container) return;
8
+ container.innerHTML = '';
9
+ Object.entries(edgeInfos).forEach(([key, { name }]) => {
10
+ const div = document.createElement('div');
11
+ div.className = 'form-check form-switch';
12
+ const input = document.createElement('input');
13
+ input.className = 'form-check-input';
14
+ input.type = 'checkbox';
15
+ input.id = `filter-edge-${key.replace(/\s+/g, '-')}`;
16
+ input.value = key;
17
+ input.checked = true;
18
+ const label = document.createElement('label');
19
+ label.className = 'form-check-label';
20
+ label.htmlFor = input.id;
21
+ label.textContent = name;
22
+ div.appendChild(input);
23
+ div.appendChild(label);
24
+ container.appendChild(div);
25
+ });
26
+ }
27
+
28
+ function populateGraphModelsTable(graphData, table) {
29
+ table.clear();
30
+ graphData.nodes.forEach(node => {
31
+ // Filtrer pour n'inclure que les modèles dans ces tableaux
32
+ if (node.label !== 'Modèle' && node.label !== 'Model') return;
33
+ const distance = node.distance;
34
+
35
+
36
+ // Construction de la ligne avec les nouvelles données
37
+ const rowData = [
38
+ node.id || "N/A",
39
+ node.author || "Inconnu",
40
+ // La conversion en string via la condition est parfaite ici
41
+ String(node.downloads ?? "Inconnu"),
42
+ node.task || "Inconnue",
43
+ String(node.likes ?? "0"),
44
+ String(node.createdAt ?? "Inconnue"),
45
+ node.dataset || "Inconnu",
46
+ node.license || "Inconnue",
47
+ distance > 0 ? `+${distance}` : String(distance ?? 0),
48
+ String(node.ascendantsCount ?? "0"),
49
+ String(node.descendantsCount ?? "0"),
50
+ String(node.citationCount ?? "0")
51
+ ];
52
+ table.row.add(rowData);
53
+ });
54
+ table.draw();
55
+ }
56
+
57
+ /**
58
+ * Met à jour la visibilité des nœuds et des arêtes dans le graphe.
59
+ * Cache les nœuds dont les liens sont tous cachés et met à jour la table des modèles.
60
+ * @param {Graph} graph - L'instance du graphe Graphology.
61
+ * @param {Sigma} renderer - L'instance du renderer Sigma.
62
+ * @param {DataTable} table - L'instance de la DataTable des modèles à mettre à jour.
63
+ */
64
+ function updateGraphVisibility(graphData,graph, renderer, table) {
65
+ const filtersContainer = document.getElementById('graph-filters-container');
66
+ if (!filtersContainer) return;
67
+
68
+ const visibleNodeTypes = new Set();
69
+ filtersContainer.querySelectorAll('input[id^="filter-node-"]:checked').forEach(c => {
70
+ visibleNodeTypes.add(c.value);
71
+ if (c.value === 'Modèle') visibleNodeTypes.add('Model');
72
+ // if (c.value === 'personne') visibleNodeTypes.add('Author');
73
+ });
74
+
75
+ const visibleEdgeTypes = new Set();
76
+ filtersContainer.querySelectorAll('input[id^="filter-edge-"]:checked').forEach(c => visibleEdgeTypes.add(c.value));
77
+
78
+ graph.forEachNode((node, attr) => graph.setNodeAttribute(node, 'hidden', !visibleNodeTypes.has(attr.dataCat)));
79
+ graph.forEachEdge((edge, attr, source, target) => {
80
+ const sourceHidden = graph.getNodeAttribute(source, 'hidden');
81
+ const targetHidden = graph.getNodeAttribute(target, 'hidden');
82
+ const typeHidden = !visibleEdgeTypes.has(attr.label);
83
+ graph.setEdgeAttribute(edge, 'hidden', sourceHidden || targetHidden || typeHidden);
84
+ });
85
+
86
+ // On repeuple la table en se basant sur la nouvelle visibilité des nœuds
87
+ populateGraphModelsTable(graphData, table);
88
+
89
+ renderer.refresh();
90
+ }
91
+ /**
92
+ * Met en place l'écouteur de clic sur les lignes des tableaux pour interagir avec le graphe.
93
+ * @param {Sigma} renderer - L'instance du renderer Sigma.
94
+ * @param {Graph} graph - L'instance du graphe Graphology.
95
+ */
96
+ function setupTableRowClickListeners(renderer, graph) {
97
+ $('#graph-models-table tbody').on('click', 'tr', function() {
98
+ const nodeId = $(this).attr('data-node-id');
99
+ if (!nodeId || !renderer || !graph.hasNode(nodeId)) return;
100
+ // Effet de "highlight" temporaire sur le nœud dans le graphe
101
+ const originalSize = graph.getNodeAttribute(nodeId, "size");
102
+ const highlightSize = originalSize * 1.6;
103
+
104
+ graph.setNodeAttribute(nodeId, "size", highlightSize);
105
+ renderer.refresh(); // Nécessaire pour forcer le rendu
106
+
107
+ setTimeout(() => {
108
+ graph.setNodeAttribute(nodeId, "size", originalSize);
109
+ renderer.refresh();
110
+ }, 1000);
111
+ // Supprime la classe active des autres lignes
112
+ $('#graph-models-table tbody tr').removeClass('active');
113
+ $(this).addClass('active');
114
+
115
+ showNodeInfo(graph.getNodeAttributes(nodeId));
116
+
117
+
118
+ const pos = renderer.getNodeDisplayData(nodeId);
119
+ if (pos) {
120
+ renderer.getCamera().animate({ x: pos.x, y: pos.y, ratio: 0.2 }, { duration: 600 });
121
+ }
122
+ });
123
+
124
+ }
125
+
126
+ /**
127
+ * Fonction principale pour créer et afficher le graphe Sigma.
128
+ * Appelée une seule fois lorsque l'utilisateur clique sur "Voir l'arbre".
129
+ * @param {object} graphData - Les données des nœuds et arêtes.
130
+ * @returns {Sigma} L'instance du renderer Sigma.
131
+ */
132
+ function initializeSigmaGraph(graphData) {
133
+ const container = document.getElementById("sigma-container");
134
+ const nodeName = document.getElementById("node_name")?.textContent?.trim() || "";
135
+ if (!container) return null;
136
+
137
+ console.log("Initialisation du graphe Sigma...");
138
+
139
+ const Graph = window.graphology.Graph;
140
+ const graph = new Graph();
141
+ const TYPE_COLORS = {
142
+ personne: '#007bff', organisation: '#007', Modèle: '#b1b1b1', Dataset: '#ffc107',
143
+ Author: '#007bff', Model: '#b1b1b1', Unknown: '#999'
144
+ };
145
+ const addedNodeIds = new Set();
146
+
147
+ graphData.nodes.forEach(node => {
148
+ if (addedNodeIds.has(node.id)) return;
149
+ const rawSize = Math.log10(node.downloads || node.followers || 1) * 2 + (depth === 0 ? 5 : 0);
150
+ graph.addNode(node.id, {
151
+ id: node.id, label: node.id, size: Math.max(5, rawSize), color: TYPE_COLORS[node.label] || '#999',
152
+ dataCat: node.label, x: Math.random(), y: Math.random(),
153
+ followers: node.followers, downloads: node.downloads, createdAt: node.createdAt,
154
+ createdAt_dataset: node.createdAt_dataset, task: node.task, bfsDepth: node.distance
155
+ });
156
+ addedNodeIds.add(node.id);
157
+
158
+ });
159
+
160
+ graphData.edges.forEach(edge => {
161
+ const info = edgeInfos[edge.relation];
162
+ const edgeColor = info ? info.color : '#ccc';
163
+
164
+ if (graph.hasNode(edge.source) && graph.hasNode(edge.target)) {
165
+ graph.addEdge(edge.source, edge.target, {
166
+ label: edge.relation || '', color: edgeColor, size: 3, type: 'arrow'
167
+ });
168
+ }
169
+ });
170
+
171
+ graphologyLibrary.layoutForceAtlas2.assign(graph, { iterations: 100 });
172
+ const renderer = new Sigma(graph, container);
173
+ const state = {
174
+ hoveredNode: null,
175
+ hoveredNeighbors: null
176
+ };
177
+ renderer.setSetting("nodeReducer", (node, data) => {
178
+ const res = { ...data };
179
+
180
+ if (state.hoveredNode && state.hoveredPredecessors) {
181
+ // Garder seulement le nœud survolé et ses prédécesseurs visibles en couleur
182
+ if (!state.hoveredPredecessors.has(node)) {
183
+ res.color = "#f1f1f1"; // gris
184
+ res.label = ""; // masquer le label
185
+ }
186
+ }
187
+
188
+ return res;
189
+ });
190
+
191
+ renderer.setSetting("edgeReducer", (edge, data) => {
192
+ const res = { ...data };
193
+
194
+ if (state.hoveredNode && state.hoveredPredecessors) {
195
+ const [source, target] = graph.extremities(edge);
196
+ // Montrer seulement les arêtes dans le chemin des prédécesseurs
197
+ if (!state.hoveredPredecessors.has(source) || !state.hoveredPredecessors.has(target)) {
198
+ res.hidden = true;
199
+ }
200
+ }
201
+
202
+ return res;
203
+ });
204
+ buildEdgeLegend();
205
+ buildEdgeFilters();
206
+
207
+ const graphModelsTable = $('#graph-models-table').DataTable({
208
+ language: { "url": "https://cdn.datatables.net/plug-ins/1.13.4/i18n/fr-FR.json" },
209
+ pageLength: 5, responsive: true, scrollX: true
210
+ });
211
+
212
+ // Remplir la table une première fois avec tous les modèles
213
+ populateGraphModelsTable(graphData, graphModelsTable);
214
+
215
+ const filtersContainer = document.getElementById('graph-filters-container');
216
+ if (filtersContainer) {
217
+ // L'écouteur appelle maintenant la fonction updateGraphVisibility qui est bien définie
218
+ filtersContainer.addEventListener('change', () => updateGraphVisibility(graphData,graph, renderer, graphModelsTable));
219
+ }
220
+
221
+ renderer.on('clickNode', ({ node }) => showNodeInfo(graph.getNodeAttributes(node)));
222
+ setupTableRowClickListeners(renderer, graph);
223
+ renderer.on('clickNode', ({ node }) => {
224
+ showNodeInfo(graph.getNodeAttributes(node));
225
+ });
226
+ // Quand on survole un nœud
227
+ renderer.on("enterNode", ({ node }) => {
228
+ state.hoveredNode = node;
229
+ state.hoveredPredecessors = getAllPredecessors(graph, node);
230
+ renderer.refresh({ skipIndexation: true });
231
+ });
232
+
233
+ // Quand on sort du nœud
234
+ renderer.on("leaveNode", () => {
235
+ state.hoveredNode = null;
236
+ state.hoveredPredecessors = null;
237
+ renderer.refresh({ skipIndexation: true });
238
+ });
239
+ document.getElementById("export-btn").addEventListener("click", () => {
240
+ const graph = renderer.getGraph(); // Graph actuel de Sigma
241
+ const graphData = {
242
+ nodes: graph.nodes().map(n => graph.getNodeAttributes(n)),
243
+ edges: graph.edges().map(e => ({
244
+ ...graph.getEdgeAttributes(e),
245
+ source: graph.source(e),
246
+ target: graph.target(e),
247
+ })),
248
+ };
249
+ console.log(graphData)
250
+ // Récupération du HTML des légendes
251
+ const legendHtmlnodes = document.getElementById("legend-nodes")?.innerHTML || "";
252
+ const legendHtmledges = document.getElementById("legend-edges")?.innerHTML || "";
253
+ fetch("/static/graph_export.html")
254
+ .then(res => res.text())
255
+ .then(template => {
256
+ // Remplacement des placeholders
257
+ const finalHtml = template
258
+ .replace("{{GRAPH_DATA}}", JSON.stringify(graphData))
259
+ .replaceAll("{{NODE_NAME}}", JSON.stringify(nodeName))
260
+ .replace("{{LEGEND_HTML_NODES}}", legendHtmlnodes)
261
+ .replace("{{LEGEND_HTML_EDGES}}", legendHtmledges);
262
+
263
+ const blob = new Blob([finalHtml], { type: "text/html" });
264
+ const url = URL.createObjectURL(blob);
265
+
266
+ const a = document.createElement("a");
267
+ a.href = url;
268
+ a.download = "graph_export.html";
269
+ a.click();
270
+
271
+ URL.revokeObjectURL(url);
272
+ });
273
+ });
274
+
275
+
276
+ return renderer;
277
+ }
278
+
279
+
280
+ // ===================================================================
281
+ // POINT D'ENTRÉE PRINCIPAL - EXÉCUTÉ AU CHARGEMENT DE LA PAGE
282
+ // ===================================================================
283
+ document.addEventListener("DOMContentLoaded", () => {
284
+
285
+ const sigmaContainer = document.getElementById("sigma-container");
286
+
287
+ if (sigmaContainer && sigmaContainer.dataset.graph) {
288
+ let graphData;
289
+ try {
290
+ graphData = JSON.parse(sigmaContainer.dataset.graph);
291
+ if (graphData && graphData.nodes && graphData.nodes.length > 0) {
292
+ initializeSigmaGraph(graphData);
293
+ }
294
+ } catch (e) {
295
+ console.error("Erreur d'analyse JSON des données du graphe:", e);
296
+ }
297
+ }
298
+
299
+ // --- Logique du formulaire (autocomplétion, etc.) ---
300
+ const input = document.getElementById('search-input');
301
+ const suggestionsList = document.getElementById('suggestions-list');
302
+
303
+ if(input && suggestionsList) {
304
+ const modelCheckbox = document.getElementById('filter-model');
305
+ const datasetCheckbox = document.getElementById('filter-dataset');
306
+ const authorCheckbox = document.getElementById('filter-author'); // Assurez-vous d'avoir cet ID
307
+
308
+ function fetchAutocomplete() {
309
+ const query = input.value.trim();
310
+
311
+ // 1. Déterminer quel filtre est actif
312
+ let activeFilter = '';
313
+ if (modelCheckbox.checked) {
314
+ activeFilter = 'Model';
315
+ } else if (datasetCheckbox.checked) {
316
+ activeFilter = 'Dataset';
317
+ }
318
+
319
+ // Si le champ est vide, on cache les suggestions
320
+ if (query.length < 2) {
321
+ suggestionsList.style.display = 'none';
322
+ return;
323
+ }
324
+
325
+ // 2. Construire l'URL avec le filtre
326
+ // On commence avec la requête de base
327
+ let fetchUrl = `/autocomplete?q=${encodeURIComponent(query)}`;
328
+ // On ajoute le paramètre 'filter' SEULEMENT s'il y en a un d'actif
329
+ if (activeFilter) {
330
+ fetchUrl += `&filter=${activeFilter}`;
331
+ }
332
+
333
+ // 3. Lancer la requête fetch avec la bonne URL
334
+ fetch(fetchUrl)
335
+ .then(response => response.json())
336
+ .then(data => {
337
+ suggestionsList.innerHTML = '';
338
+ if (data.length > 0) {
339
+ data.forEach(item => {
340
+ const li = document.createElement('li');
341
+ li.textContent = item.name;
342
+ li.addEventListener('click', () => {
343
+ input.value = item.name;
344
+ suggestionsList.style.display = 'none';
345
+ });
346
+ suggestionsList.appendChild(li);
347
+ });
348
+ suggestionsList.style.display = 'block';
349
+ } else {
350
+ suggestionsList.style.display = 'none';
351
+ }
352
+ });
353
+ }
354
+
355
+ input.addEventListener('input', fetchAutocomplete);
356
+ if (modelCheckbox) modelCheckbox.addEventListener('change', fetchAutocomplete);
357
+ if (datasetCheckbox) datasetCheckbox.addEventListener('change', fetchAutocomplete);
358
+ if (authorCheckbox) authorCheckbox.addEventListener('change', fetchAutocomplete);
359
+
360
+ document.addEventListener('click', (e) => {
361
+ if (!input.contains(e.target) && !suggestionsList.contains(e.target)) {
362
+ suggestionsList.style.display = 'none';
363
+ }
364
+ });
365
+ }
366
+ const checkbox = document.getElementById('depth-unlimited');
367
+ if (checkbox) {
368
+ const depthInput = document.getElementById('depth');
369
+ function toggleDepthInput() { if (depthInput) depthInput.disabled = checkbox.checked; }
370
+ checkbox.addEventListener('change', toggleDepthInput);
371
+ toggleDepthInput();
372
+ }
373
+
374
+
375
+
376
+ });
377
+
application_neo4j/static/js/utils.js ADDED
@@ -0,0 +1,233 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // --- FONCTIONS UTILITAIRES GLOBALES ---
2
+ // Ces fonctions sont utilisées à plusieurs endroits et sont donc définies en premier.
3
+ // Mapping relations → {couleur, tooltip}
4
+ const edgeInfos = {
5
+ "Fait partie de cette organisation": {
6
+ color: "#a05195",name : "Fait partie de cette organisation",
7
+ tooltip: "Cette personne est membre de cette organisation"
8
+ },
9
+ "A publié": {
10
+ color: "#003f5c",name : "A publié",
11
+ tooltip: "Un auteur (personne ou organistaion) a publié un modèle"
12
+ },
13
+ "A généré": {
14
+ color: "#f50f0f",name : "A permis de générer",
15
+ tooltip: "Le modèle source a été téléchargé et modifié afin de créer le modèle cible (type de transformation inconnu)"
16
+
17
+ },
18
+ "finetune": {
19
+ color: "#cd6700",name : "Finetune",
20
+ tooltip: "Ajustement : le modèle source est ré-entraîné sur un jeu de données spécifique afin de pouvoir être performant pour une tâche précise."
21
+
22
+ },
23
+ "adapter": {
24
+ color: "#238a00",name : "Adapter",
25
+
26
+ tooltip: "Adaptation : méthode d’ajustement qui peut être utilisée avec peu de ressources de calcul."
27
+ },
28
+ "quantized": {
29
+ color: "#009194",name : "Quantized",
30
+ tooltip:"Quantisation : la précision des poids du modèle source est réduite afin de diminuer son empreinte en mémoire."
31
+ },
32
+ "merge": {
33
+ color: "#c29bc2",name : "Merge",
34
+ tooltip:"Fusion : méthode visant à mélanger des couches de différents modèles pour améliorer leur performance."
35
+ },
36
+ "other": {
37
+ color: "#8f7340",name : "Autre",
38
+ tooltip: "Autre type de relation"
39
+ }
40
+ , "unknown": {
41
+ color: "#8f7340",name : "Autre",
42
+ tooltip: "Autre type de relation"
43
+ }
44
+ };
45
+
46
+
47
+
48
+
49
+ function buildEdgeLegend() {
50
+ const legendContainer = document.getElementById("legend-edges");
51
+ legendContainer.innerHTML = ""; // reset
52
+
53
+ Object.entries(edgeInfos).forEach(([relation, {color, name,tooltip}]) => {
54
+ if (relation == "unknown") return;
55
+ const li = document.createElement("li");
56
+ li.className = "list-group-item d-flex align-items-center";
57
+
58
+ // ajout Bootstrap tooltip
59
+ li.setAttribute("data-bs-toggle", "tooltip");
60
+ li.setAttribute("data-bs-placement", "top");
61
+ li.setAttribute("data-bs-html", "true");
62
+ li.setAttribute("title", tooltip);
63
+
64
+ const span = document.createElement("span");
65
+ span.className = "legend-color edge me-2";
66
+ span.style.backgroundColor = color;
67
+
68
+ li.appendChild(span);
69
+ li.appendChild(document.createTextNode(name));
70
+
71
+ legendContainer.appendChild(li);
72
+ });
73
+
74
+ // nécessaire pour activer les tooltips Bootstrap dynamiques
75
+ const tooltipTriggerList = [].slice.call(legendContainer.querySelectorAll('[data-bs-toggle="tooltip"]'))
76
+ tooltipTriggerList.map(el => new bootstrap.Tooltip(el));
77
+ }
78
+
79
+ /**
80
+ * Affiche la carte d'information pour un nœud donné.
81
+ * @param {object} attr - Les attributs du nœud à afficher.
82
+ */
83
+ function showNodeInfo(attr) {
84
+ const infoCard = document.getElementById("node-info-card");
85
+ const cardBody = infoCard.querySelector('.card-body');
86
+ const detailsContainer = document.getElementById("node-details");
87
+
88
+ if (!infoCard || !detailsContainer || !cardBody) return;
89
+
90
+ // --- ÉTAPE 1: Nettoyage ---
91
+ const existingLink = cardBody.querySelector('.stretched-link');
92
+ if (existingLink) {
93
+ existingLink.remove();
94
+ }
95
+
96
+ // On stocke l'ID du nœud pour l'animation au survol
97
+ infoCard.dataset.nodeId = attr.id;
98
+
99
+ // --- ÉTAPE 2: Construction des détails ---
100
+ let infosHtml = `<p><strong>Nom :</strong> ${attr.id || "Non défini"}</p><p><strong>Type :</strong> ${attr.dataCat}</p>`;
101
+
102
+ // ... (Le reste de la construction de infosHtml ne change pas) ...
103
+ if (["personne", "organisation", "Author"].includes(attr.dataCat)) {
104
+ if (attr.followers) infosHtml += `<p><strong>Abonnés :</strong> ${formatNumberShort(attr.followers)}</p>`;
105
+ } else if (["Modèle", "Model"].includes(attr.dataCat)) {
106
+ if (attr.downloads) infosHtml += `<p><strong>Téléchargements :</strong> ${formatNumberShort(attr.downloads)}</p>`;
107
+ if (attr.createdAt) infosHtml += `<p><strong>Créé le :</strong> ${formatDateFr(attr.createdAt)}</p>`;
108
+ if (attr.task) infosHtml += `<p><strong>Tâche :</strong> ${attr.task}</p>`;
109
+ if (attr.dataset) infosHtml += `<p><strong>Dataset utilisé :</strong> ${attr.dataset}</p>`;
110
+ } else if (attr.dataCat === "Dataset") {
111
+ if (attr.downloads) infosHtml += `<p><strong>Téléchargements :</strong> ${formatNumberShort(attr.downloads)}</p>`;
112
+ if (attr.createdAt_dataset) infosHtml += `<p><strong>Créé le :</strong> ${formatDateFr(attr.createdAt_dataset)}</p>`;
113
+ }
114
+
115
+ // --- ÉTAPE 3: Création du lien ---
116
+ let huggingFaceUrl = null;
117
+ if (["Modèle", "Model","personne", "organisation", "Author"].includes(attr.dataCat)) {
118
+ huggingFaceUrl = `https://huggingface.co/${attr.id}`;
119
+ } else if (attr.dataCat === "Dataset") {
120
+ huggingFaceUrl = `https://huggingface.co/datasets/${attr.id}`;
121
+ }
122
+
123
+ if (huggingFaceUrl) {
124
+ const link = document.createElement('a');
125
+ link.href = huggingFaceUrl;
126
+ link.target = '_blank';
127
+ link.rel = 'noopener noreferrer';
128
+ link.className = 'stretched-link';
129
+ link.setAttribute('aria-label', `Voir ${attr.id} sur Hugging Face`);
130
+ cardBody.appendChild(link);
131
+ }
132
+
133
+ detailsContainer.innerHTML = infosHtml;
134
+
135
+ // Affichage et défilement de la carte
136
+ infoCard.style.display = 'block';
137
+ infoCard.scrollIntoView({ behavior: 'smooth', block: 'center' });
138
+ }
139
+
140
+
141
+
142
+
143
+
144
+
145
+ /**
146
+ * Formate une chaîne de date ISO en format français (ex: 1 janvier 2024).
147
+ * @param {string} dateString - La date à formater.
148
+ * @returns {string} La date formatée ou "Date inconnue".
149
+ */
150
+ function formatDateFr(dateString) {
151
+ if (!dateString || dateString === "inconnue") return "Date inconnue";
152
+ try {
153
+ const options = { year: 'numeric', month: 'long', day: 'numeric' };
154
+ return new Date(dateString).toLocaleDateString('fr-FR', options);
155
+ } catch (e) {
156
+ return "Date inconnue";
157
+ }
158
+ }
159
+
160
+ // --- PLUGIN DE TRI PERSONNALISÉ POUR DATATABLES ---
161
+ // Gère les colonnes avec des nombres et des chaînes de caractères (ex: "Inconnu")
162
+ // Place toujours les chaînes après les nombres lors d'un tri ascendant.
163
+
164
+ // Fonction pour parser la valeur : enlève les espaces et convertit en nombre si possible
165
+ function parseNumericValue(value) {
166
+ if (typeof value === 'string') {
167
+ // Enlève les espaces (pour les nombres comme "12 345") et les virgules
168
+ const cleanedValue = value.replace(/[\s,]/g, '');
169
+ const num = parseInt(cleanedValue, 10);
170
+ if (!isNaN(num)) {
171
+ return { isNumber: true, value: num };
172
+ }
173
+ }
174
+ // Si ce n'est pas une chaîne numérique, on le traite comme du texte
175
+ return { isNumber: false, value: value };
176
+ }
177
+
178
+ // Définition du tri ascendant
179
+ jQuery.fn.dataTable.ext.order['numeric-string-asc'] = function (a, b) {
180
+ const valA = parseNumericValue(a);
181
+ const valB = parseNumericValue(b);
182
+
183
+ if (valA.isNumber && valB.isNumber) {
184
+ return valA.value - valB.value; // Tri numérique standard
185
+ } else if (valA.isNumber && !valB.isNumber) {
186
+ return -1; // Les nombres viennent AVANT les chaînes
187
+ } else if (!valA.isNumber && valB.isNumber) {
188
+ return 1; // Les chaînes viennent APRÈS les nombres
189
+ } else {
190
+ // Les deux sont des chaînes, on fait un tri alphabétique
191
+ return String(valA.value).localeCompare(String(valB.value));
192
+ }
193
+ };
194
+
195
+ // Le tri descendant est simplement l'inverse de l'ascendant
196
+ jQuery.fn.dataTable.ext.order['numeric-string-desc'] = function (a, b) {
197
+ return jQuery.fn.dataTable.ext.order['numeric-string-asc'](a, b) * -1;
198
+ };
199
+
200
+ /**
201
+ * Formate un grand nombre en une version courte (ex: 1.2M, 50k).
202
+ * @param {number} n - Le nombre à formater.
203
+ * @returns {string} Le nombre formaté.
204
+ */
205
+ function formatNumberShort(n) {
206
+ if (!n || n === 0) return "";
207
+ if (n >= 1000000000) return (n / 1000000000).toFixed(1) + 'B';
208
+ if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
209
+ if (n >= 1000) return (n / 1000).toFixed(0) + 'k';
210
+ return n.toString();
211
+ }
212
+
213
+
214
+ function getAllPredecessors(graph, nodeId) {
215
+ const visited = new Set();
216
+ const stack = [nodeId];
217
+
218
+ while (stack.length > 0) {
219
+ const current = stack.pop();
220
+ if (!visited.has(current)) {
221
+ visited.add(current);
222
+
223
+ // On parcourt les prédécesseurs directs
224
+ graph.forEachInNeighbor(current, (pred) => {
225
+ if (!visited.has(pred)) {
226
+ stack.push(pred);
227
+ }
228
+ });
229
+ }
230
+ }
231
+
232
+ return visited;
233
+ }
application_neo4j/templates/expert.html ADDED
@@ -0,0 +1,253 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="fr">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Généalogie des Modèles - Vue Experte</title>
7
+
8
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
9
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css">
10
+ <link rel="stylesheet" href="https://cdn.datatables.net/1.13.4/css/jquery.dataTables.min.css" />
11
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
12
+
13
+ <style>
14
+ body { font-size: 1.05rem; }
15
+ .legend-color { width: 1em; height: 1em; display: inline-block; border-radius: 2px; }
16
+ .legend-color.edge { border-radius: 0; height: 0.25em; transform: translateY(-0.35em); }
17
+ </style>
18
+ </head>
19
+
20
+ <body class="d-flex flex-column min-vh-100">
21
+ <header class="navbar navbar-expand-lg navbar-light bg-light border-bottom">
22
+ <div class="container">
23
+ <a class="navbar-brand" href="/">
24
+ <span class="fw-bold">Généalogie des Modèles</span>
25
+ <small class="d-block text-muted">Exploration des relations entre modèles et datasets</small>
26
+ </a>
27
+ </div>
28
+ </header>
29
+
30
+ <main class="container mt-4 flex-grow-1">
31
+ <div class="row">
32
+ <div class="col-12">
33
+ <h1 class="display-5">Recherche Experte</h1>
34
+ <p class="lead">Explorez et filtrez la généalogie des modèles</p>
35
+ </div>
36
+ </div>
37
+ <div class="row g-3 mb-4">
38
+ <div class="col-12">
39
+ <form method="POST" action="{{ url_for('findnode_expert') }}" autocomplete="off">
40
+ <input type="hidden" name="filter" value="{{ search.filter or '' }}">
41
+
42
+ <div class="row g-3 align-items-end">
43
+ <!-- Champ de recherche -->
44
+ <div class="col-12 col-md-5">
45
+ <label for="search-input" class="form-label fw-bold">Nom à rechercher</label>
46
+ <div class="position-relative">
47
+ <input type="text" name="name" id="search-input" class="form-control"
48
+ placeholder="Taper le nom du modèle suspecté."
49
+ value="{{ request.form.name or '' }}" required autocomplete="off" />
50
+ <ul id="suggestions-list" class="list-group position-absolute w-100" style="display: none; z-index: 1000;"></ul>
51
+ </div>
52
+ </div>
53
+ <!-- Filtres -->
54
+ <div class="col-12 col-md-3">
55
+ <label class="form-label fw-bold">Filtres</label>
56
+ <div class="d-flex gap-3">
57
+ <div class="form-check">
58
+ <input class="form-check-input" type="checkbox" name="filters" value="Model" id="filter-model"
59
+ {% if 'Model' in search.filters %}checked{% endif %}>
60
+ <label class="form-check-label" for="filter-model">Modèle</label>
61
+ </div>
62
+ <div class="form-check">
63
+ <input class="form-check-input" type="checkbox" name="filters" value="Dataset" id="filter-dataset"
64
+ {% if 'Dataset' in search.filters %}checked{% endif %}>
65
+ <label class="form-check-label" for="filter-dataset">Dataset</label>
66
+ </div>
67
+ <div class="form-check">
68
+ <input class="form-check-input" type="checkbox" name="filters" value="Author" id="filter-author" {% if 'Author' in search.filters %}checked{% endif %}>
69
+ <label class="form-check-label" for="filter-author">Auteur</label>
70
+ </div>
71
+ </div>
72
+ </div>
73
+ <!-- Profondeur de recherche -->
74
+ <div class="col-12 col-md-4">
75
+ <label class="form-label fw-bold">Profondeur de recherche</label>
76
+ <div class="d-flex align-items-center gap-3">
77
+ <div class="form-check form-switch">
78
+ <input class="form-check-input" type="checkbox" id="depth-unlimited" name="depth_unlimited" checked>
79
+ <label class="form-check-label" for="depth-unlimited">Illimitée</label>
80
+ </div>
81
+ <div class="input-group input-group-sm">
82
+ <span class="input-group-text">Limité à:</span>
83
+ <input class="form-control" type="number" name="depth" id="depth"
84
+ value="{{ request.form.depth }}" min="1" max="5" disabled>
85
+ </div>
86
+ </div>
87
+ </div>
88
+ </div>
89
+ <div class="row mt-4">
90
+ <div class="col-12">
91
+ <button type="submit" name="submit" value="findnode_expert" class="btn btn-primary w-100">
92
+ <i class="bi bi-search me-1"></i>Rechercher
93
+ </button>
94
+ </div>
95
+ </div>
96
+ </form>
97
+ </div>
98
+ </div>
99
+
100
+
101
+ {% if message %}
102
+ <div class="alert alert-info mt-3"><p class="mb-0">{{ message }}</p></div>
103
+ {% endif %}
104
+
105
+ <!-- CORRECTION : Cette section s'affiche seulement s'il y a des données -->
106
+ {% if graph_data and graph_data.nodes %}
107
+ <div id="genealogy-graph-section" class="mt-4">
108
+ <div class="row g-4">
109
+ <div class="card-header"><h5 class="card-title mb-0">Composante connexe du noeud recherché</h5></div>
110
+ <!-- Colonne de gauche pour le graphe et la carte d'info -->
111
+ <div class="col-12 col-lg-8">
112
+ <div class="card position-relative">
113
+ <!-- Graphe Sigma -->
114
+
115
+ <div id="sigma-container" data-graph='{{ graph_data | tojson | safe }}' style="width: 100%; height: 600px;">
116
+ <!-- Légende flottante -->
117
+ <div id="floating-legend"
118
+ class="card p-2 position-absolute top-0 start-0 shadow-sm"
119
+ style="width: 220px; background-color: rgba(255,255,255,0.95);">
120
+ <div class="d-flex justify-content-between align-items-center mb-2">
121
+ <strong>Légendes</strong>
122
+ <button class="btn btn-sm btn-light p-0"
123
+ type="button"
124
+ data-bs-toggle="collapse"
125
+ data-bs-target="#legend-content"
126
+ aria-expanded="true"
127
+ aria-controls="legend-content">
128
+ <i class="bi bi-chevron-up"></i>
129
+ </button>
130
+ </div>
131
+ <!-- Contenu de la légende -->
132
+ <div id="legend-content" class="collapse show">
133
+ <div class="card mb-2">
134
+ <div class="card-header py-1"><h6 class="card-title mb-0">Nœuds</h6></div>
135
+ <div class="card-body p-2">
136
+ <ul id="legend-nodes" class="list-group list-group-flush small">
137
+ <li class="list-group-item d-flex align-items-center py-1">
138
+ <span class="legend-color me-2" style="background-color: hsl(45, 65%, 52%);"></span>Dataset
139
+ </li>
140
+ <li class="list-group-item d-flex align-items-center py-1">
141
+ <span class="legend-color me-2" style="background-color: #007bff;"></span>Personne
142
+ </li>
143
+ <li class="list-group-item d-flex align-items-center py-1">
144
+ <span class="legend-color me-2" style="background-color: #092d53;"></span>Organisation
145
+ </li>
146
+ <li class="list-group-item d-flex align-items-center py-1">
147
+ <span class="legend-color me-2" style="background-color:#7D7D7D;"></span>Modèle
148
+ </li>
149
+ </ul>
150
+ </div>
151
+ </div>
152
+
153
+ <div class="card">
154
+ <div class="card-header py-1"><h6 class="card-title mb-0">Relations</h6></div>
155
+ <div class="card-body p-2">
156
+ <ul id="legend-edges" class="list-group list-group-flush small">
157
+ </ul>
158
+ </div>
159
+ </div>
160
+ </div>
161
+ </div>
162
+ </div>
163
+ </div>
164
+ <div id="node-info-card" class="card mt-3 card-interactive" style="display: none;">
165
+ <div class="card-body">
166
+ <h5 class="card-title">Informations du nœud sélectionné</h5>
167
+ <div id="node-details" class="mt-2"></div>
168
+ </div>
169
+ </div>
170
+ </div>
171
+ <div class="col-12 col-lg-4">
172
+ <!-- CORRECTION : Chaque section est dans sa propre carte -->
173
+ <div class="card mb-3">
174
+ <div class="card-header"><h5 class="card-title mb-0">Filtres du Graphe</h5></div>
175
+ <div class="card-body" id="graph-filters-container">
176
+ <h6>Types de Nœuds</h6>
177
+ <div class="form-check form-switch">
178
+ <input class="form-check-input" type="checkbox" id="filter-node-model" value="Modèle" checked>
179
+ <label class="form-check-label" for="filter-node-model">Modèle</label>
180
+ </div>
181
+ <div class="form-check form-switch">
182
+ <input class="form-check-input" type="checkbox" id="filter-node-person" value="personne" checked>
183
+ <label class="form-check-label" for="filter-node-person">Personne</label>
184
+ </div>
185
+ <div class="form-check form-switch">
186
+ <input class="form-check-input" type="checkbox" id="filter-node-org" value="organisation" checked>
187
+ <label class="form-check-label" for="filter-node-org">Organisation</label>
188
+ </div>
189
+ <hr>
190
+ <h6>Types de Relations</h6>
191
+ <div id="edge-filters-container"></div>
192
+ </div>
193
+ </div>
194
+ <button id="export-btn" class="btn-modern">Télécharger le graphe</button>
195
+ </div>
196
+ </div>
197
+ <hr class="my-5">
198
+
199
+ <!-- CORRECTION : Tableau des modèles présents dans le graphe -->
200
+ <div class="row mt-4">
201
+ <div class="col-12">
202
+ <h4 class="h4">Modèles présents dans le graphe</h4>
203
+ <div class="table-responsive">
204
+ <table id="graph-models-table" class="table table-bordered table-striped table-hover" style="width:100%">
205
+ <thead>
206
+ <tr>
207
+ <th scope="col">Modèle</th>
208
+ <th scope="col">Auteur</th>
209
+ <th scope="col">Téléchargements</th>
210
+ <th scope="col">Tâche</th>
211
+ <th scope="col">J'aime</th>
212
+ <th scope="col">Date de publication</th>
213
+ <th scope="col">Dataset utilisé</th>
214
+ <th scope="col">Licence</th>
215
+ <th scope="col">Distance au modèle recherché</th>
216
+ <th scope="col">Ascendants</th>
217
+ <th scope="col">Descendants</th>
218
+ <th scope="col">Citations</th>
219
+ </tr>
220
+ </thead>
221
+ <tbody>
222
+ <!-- Le contenu sera rempli par JavaScript -->
223
+ </tbody>
224
+ </table>
225
+ </div>
226
+ </div>
227
+ </div>
228
+ </div>
229
+ {% endif %}
230
+ </main>
231
+
232
+ <footer class="bg-dark text-white text-center p-4 mt-auto">
233
+ <div class="container">
234
+ <p class="mb-0">Application de recherche et visualisation... © 2025</p>
235
+ </div>
236
+ </footer>
237
+
238
+ <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
239
+ <script src="https://cdn.datatables.net/1.13.4/js/jquery.dataTables.min.js"></script>
240
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
241
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/sigma.js/2.4.0/sigma.min.js"></script>
242
+ <script src="https://unpkg.com/graphology@0.25.1/dist/graphology.umd.min.js"></script>
243
+ <script src="https://cdn.jsdelivr.net/npm/graphology-library/dist/graphology-library.min.js"></script>
244
+ <script src="{{ url_for('static', filename='js/utils.js') }}"></script>
245
+ <script src="{{ url_for('static', filename='js/script_expert.js') }}"></script>
246
+ <script>
247
+ document.addEventListener('DOMContentLoaded', function () {
248
+ const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
249
+ const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
250
+ });
251
+ </script>
252
+ </body>
253
+ </html>
application_neo4j/templates/index.html ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="fr">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Accueil - Généalogie des Modèles</title>
7
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
8
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
9
+
10
+ <style>/* Style simple pour la page d'accueil */
11
+ body {
12
+ font-family: sans-serif;
13
+ background-color: #f4f4f9;
14
+ color: #333;
15
+ margin: 0;
16
+ padding: 0;
17
+ }
18
+
19
+ /* En-tête avec titre + bouton */
20
+ .welcome-container {
21
+ display: flex;
22
+ flex-direction: column;
23
+ align-items: center;
24
+ text-align: center;
25
+ gap: 15px;
26
+ padding: 40px 20px;
27
+ }
28
+
29
+ .welcome-container h1 {
30
+ color: #2c3e50;
31
+ font-size: 2rem;
32
+ font-weight: 700;
33
+ margin: 0;
34
+ }
35
+
36
+ .info-btn {
37
+ background: linear-gradient(135deg, #3498db, #5dade2);
38
+ color: white;
39
+ padding: 12px 30px;
40
+ border-radius: 50px;
41
+ text-decoration: none;
42
+ font-weight: bold;
43
+ transition: all 0.3s ease;
44
+ box-shadow: 0 3px 8px rgba(0,0,0,0.15);
45
+ }
46
+
47
+ .info-btn:hover {
48
+ background: linear-gradient(135deg, #2980b9, #3498db);
49
+ transform: scale(1.05);
50
+ }
51
+
52
+ /* Section et cartes */
53
+ .content-section {
54
+ padding: 40px 20px;
55
+ }
56
+
57
+ .content-card {
58
+ background: white;
59
+ padding: 30px;
60
+ border-radius: 12px;
61
+ box-shadow: 0 4px 10px rgba(0,0,0,0.08);
62
+ transition: transform 0.2s ease-in-out;
63
+ height: 100%;
64
+ text-align: justify;
65
+ }
66
+
67
+ .content-card:hover {
68
+ transform: translateY(-5px);
69
+ }
70
+
71
+ .content-card h4, .content-card h6 {
72
+ color: #6a11cb;
73
+ margin-bottom: 15px;
74
+ }
75
+
76
+ /* Décalage des puces */
77
+ .content-card p {
78
+ margin-left: 15px;
79
+ }
80
+
81
+ /* Mise en forme spécifique pour Alice */
82
+ .highlight {
83
+ font-style: italic;
84
+ color: #6a11cb;
85
+ display: block;
86
+ margin-left: 15px; /* aligné avec les puces */
87
+ }
88
+
89
+ /* Boutons */
90
+ .buttons-container {
91
+ display: flex;
92
+ flex-direction: column; /* empilés verticalement */
93
+ align-items: center;
94
+ gap: 15px;
95
+ margin-top: 20px;
96
+ }
97
+
98
+
99
+ </style>
100
+ </head>
101
+
102
+ <body>
103
+ <div class="welcome-container">
104
+ <h1>Bienvenue sur l'explorateur de généalogie des modèles publiés sur HuggingFace</h1>
105
+ <a href="{{ url_for('info_page') }}" class="btn-modern">Plus d'infos</a>
106
+ </div>
107
+ <div class="container content-section">
108
+ <div class="row g-4">
109
+ <div class="col-md-6">
110
+ <div class="content-card">
111
+ <h4>Qu'est-ce que la généalogie d'un <strong>modèle</strong> ?</h4>
112
+ <p>
113
+ L'observation de la généalogie d’un modèle d’intelligence artificielle consiste à reconstituer l’historique de sa création et de ses transformations :<br><br>
114
+ • À partir de quel(s) modèle(s) il a été dérivé (<strong>ascendants</strong>) <br>
115
+ • Quelles <strong>modifications</strong> ont été apportées (ajustement,quantisation, adaptation, fusion…)<br>
116
+ • Quels nouveaux modèles ont été produits à partir de celui-ci (<strong>descendants</strong> )<br><br>
117
+ Cela peut être représenté sous forme d'<strong>arbre généalogique</strong>, retraçant l’ensemble des étapes qui ont conduit à un modèle open source donné.
118
+ </p>
119
+ </div>
120
+ </div>
121
+ <div class="col-md-6">
122
+ <div class="content-card">
123
+ <h4>Et dans le cas d'un <strong>dataset</strong> ?</h4>
124
+ <p>
125
+ L'observation de la généalogie d’un dataset consiste à reconstituer l’historique de sa création et de ses différentes utilisations : <br> <br>
126
+ • À partir de quelle(s) <strong>source(s) de données</strong> le dataset a été construit <br>
127
+ • Quels modèles ont été <strong>entrainés</strong> sur ce jeu de données<br>
128
+ • A quelles(s) organisation(s) appartient l'auteur qui a publié ce jeu de données<br> <br>
129
+
130
+ La généalogie retrace toutes les étapes et ramifications de l'utilisation d'un dataset donné.
131
+ </p>
132
+ </div>
133
+ </div>
134
+ <div class="col-md-6">
135
+ <div class="content-card">
136
+ <h4>Pour quoi faire ?</h4>
137
+ <p>
138
+ Visualiser la généalogie des modèles est une étape importante pour :<br> <br>
139
+ • Protéger la <strong>vie privée</strong> des personnes dont les données pourraient être mémorisées par un modèle ou contenues dans un dataset <br>
140
+ • Assurer la <strong>traçabilité</strong> sur les chaînes de modification des modèles<br> <br>
141
+
142
+ <h6>Pour mieux comprendre l'utilité de cet outil, prenons le cas d'Alice Dupont:</h6>
143
+ <span class="highlight">
144
+ • Alice utilise un chatbot issu d'un modèle publié sur HuggingFace. <br>
145
+ Sa requête est : "Qui est Alice Dupont ?", le chatbot renvoie son adresse et son numéro de téléphone.<br><br>
146
+ • Alice veut connaître l'<strong>impact de la mémorisation potentielle de ses données personnelles</strong>, ainsi que l'origine de ce modèle.<br><br>
147
+ Avec cet outil, Alice a accès :<br>
148
+ • aux modèles issus du modèle interrogé et publiés sur HuggingFace<br>
149
+ • aux modèles parents de ce modèle.<br><br>
150
+ (A condition que les liens entre modèles soient déclarés par les utiliateurs sur HuggingFace)
151
+ </span>
152
+ </p>
153
+ </div>
154
+ </div>
155
+
156
+ <div class="col-md-6">
157
+ <div class="content-card">
158
+ <h4>Vous souhaitez connaître la descendance et l'acsendance de ...</h4>
159
+ <div class="buttons-container">
160
+ <!-- Ce bouton mène à la page de recherche -->
161
+ <a href="{{ url_for('findnode', filter='Model') }}" class="btn-modern">Un modèle</a>
162
+ <a href="{{ url_for('findnode', filter='Dataset') }}" class="btn-modern">Un dataset</a>
163
+ <a href="{{ url_for('findnode') }}" class="btn-modern">Je ne sais pas</a><br><br>
164
+ <h4>Vous êtes un chercheur</h4>
165
+ <div class="buttons-container">
166
+ <!-- Ce bouton mène à la page de recherche -->
167
+ <a href="{{ url_for('findnode_expert', filter='Model') }}" class="btn-modern">Mode expert</a>
168
+ </div>
169
+ </div>
170
+ </div>
171
+ </div>
172
+
173
+ </body>
174
+ </html>
application_neo4j/templates/infos.html ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="fr">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Plus d'informations</title>
6
+ <style>
7
+ body, html { margin: 0; padding: 0; height: 100%; overflow: hidden; }
8
+ .pdf-container { width: 100%; height: 100%; }
9
+ .pdf-container iframe { border: none; width: 100%; height: 100%; }
10
+ </style>
11
+ </head>
12
+ <body>
13
+ <div class="pdf-container">
14
+ <!-- Cette balise iframe va afficher votre PDF -->
15
+ <iframe src="{{ url_for('static', filename='pdf/plus_infos_interface.pdf') }}"></iframe>
16
+ </div>
17
+ </body>
18
+ </html>
application_neo4j/templates/search.html ADDED
@@ -0,0 +1,437 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="fr">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Généalogie des Modèles</title>
7
+
8
+ <!-- CSS de Bootstrap (remplace DSFR) -->
9
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
10
+ <!-- Icônes Bootstrap (pour les boutons, etc.) -->
11
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css">
12
+
13
+ <!-- CSS de DataTables et votre CSS personnalisé (inchangés) -->
14
+ <link rel="stylesheet" href="https://cdn.datatables.net/1.13.4/css/jquery.dataTables.min.css" />
15
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
16
+
17
+ <style>
18
+ body {
19
+ font-size: 1.05rem;
20
+ }
21
+ .legend-color {
22
+ width: 1em;
23
+ height: 1em;
24
+ display: inline-block;
25
+ border-radius: 2px;
26
+ }
27
+ .legend-color.edge {
28
+ border-radius: 0;
29
+ height: 0.25em;
30
+ transform: translateY(-0.35em);
31
+ }
32
+ </style>
33
+ </head>
34
+
35
+ <body class="d-flex flex-column min-vh-100">
36
+ <header class="navbar navbar-expand-lg navbar-light bg-light border-bottom">
37
+ <div class="container">
38
+ <a class="navbar-brand" href="/">
39
+ <span class="fw-bold">Généalogie des Modèles</span>
40
+ <small class="d-block text-muted">Exploration des relations entre modèles et datasets</small>
41
+ </a>
42
+ </div>
43
+ </header>
44
+
45
+ <main class="container mt-4 flex-grow-1">
46
+ <!-- Titre principal et Formulaire (inchangés) -->
47
+ <div class="row">
48
+ <div class="col-12">
49
+ <h1 class="display-5">Recherche dans la base de données HuggingFace</h1>
50
+ <p class="lead">Explorez la généalogie des modèles</p>
51
+ </div>
52
+ </div>
53
+
54
+ <div class="row g-3 mb-4">
55
+ <div class="col-12">
56
+ <form method="POST" action="{{ url_for('findnode') }}" autocomplete="off">
57
+ <input type="hidden" name="filter" value="{{ search.filter or '' }}">
58
+
59
+ <div class="row g-3 align-items-end">
60
+ <!-- Champ de recherche -->
61
+ <div class="col-12 col-md-5">
62
+ <label for="search-input" class="form-label fw-bold">Nom à rechercher</label>
63
+ <div class="position-relative">
64
+ <input type="text" name="name" id="search-input" class="form-control"
65
+ placeholder="Taper le nom du modèle suspecté."
66
+ value="{{ request.form.name or '' }}" required autocomplete="off" />
67
+ <ul id="suggestions-list" class="list-group position-absolute w-100" style="display: none; z-index: 1000;"></ul>
68
+ </div>
69
+ </div>
70
+
71
+ <!-- Filtres -->
72
+ <div class="col-12 col-md-3">
73
+ <label class="form-label fw-bold">Filtres</label>
74
+ <div class="d-flex gap-3">
75
+ <div class="form-check">
76
+ <input class="form-check-input" type="checkbox" name="filters" value="Model" id="filter-model"
77
+ {% if 'Model' in search.filters %}checked{% endif %}>
78
+ <label class="form-check-label" for="filter-model">Modèle</label>
79
+ </div>
80
+ <div class="form-check">
81
+ <input class="form-check-input" type="checkbox" name="filters" value="Dataset" id="filter-dataset"
82
+ {% if 'Dataset' in search.filters %}checked{% endif %}>
83
+ <label class="form-check-label" for="filter-dataset">Dataset</label>
84
+ </div>
85
+ </div>
86
+ </div>
87
+
88
+ <!-- Profondeur de recherche -->
89
+ <div class="col-12 col-md-4">
90
+ <label class="form-label fw-bold">Profondeur de recherche</label>
91
+ <div class="d-flex align-items-center gap-3">
92
+ <div class="form-check form-switch">
93
+ <input class="form-check-input" type="checkbox" id="depth-unlimited" name="depth_unlimited" checked>
94
+ <label class="form-check-label" for="depth-unlimited">Illimitée</label>
95
+ </div>
96
+ <div class="input-group input-group-sm">
97
+ <span class="input-group-text">Limité à:</span>
98
+ <input class="form-control" type="number" name="depth" id="depth"
99
+ value="{{ request.form.depth }}" min="1" max="5" disabled>
100
+ </div>
101
+ </div>
102
+ </div>
103
+ </div>
104
+
105
+ <!-- Bouton rechercher -->
106
+ <div class="row mt-4">
107
+ <div class="col-12">
108
+ <button type="submit" name="submit" value="find_node" class="btn btn-primary w-100">
109
+ <i class="bi bi-search me-1"></i>Rechercher
110
+ </button>
111
+ </div>
112
+ </div>
113
+ </form>
114
+ </div>
115
+ </div>
116
+
117
+
118
+ {% if message %}
119
+ <div class="alert alert-info mt-3"><p class="mb-0">{{ message }}</p></div>
120
+ {% endif %}
121
+
122
+ <!-- SECTION 1: VUE "HIGHLIGHTS" -->
123
+ {% if highlights and graph_data.nodes %}
124
+ <div id="highlights-view" class="mt-5 bg-light border rounded-3 p-4">
125
+ {% set searched_node = (graph_data.nodes | selectattr('id', 'equalto', search.name) | list | first) or {} %}
126
+ <div class="row justify-content-center align-items-stretch g-4">
127
+ <!-- COLONNE ASCENDANCE -->
128
+ <div class="col-12 col-lg-3">
129
+ <div class="d-flex justify-content-center gap-2 mb-3">
130
+ <h4 class="h5 text-center mb-3">Modèles importants de l'ascendance
131
+ <!-- L'icône avec les attributs pour le tooltip -->
132
+ <i class="bi bi-info-circle-fill text-secondary align-middle ms-2"
133
+ data-bs-toggle="tooltip"
134
+ data-bs-placement="right"
135
+ title="Les modèles qui précèdent le modèle recherché dans la généalogie, c’est-à-dire, les modèles à partir desquels le modèle recherché a été constitué">
136
+ </i>
137
+ </h4>
138
+ </div>
139
+ <div class="card">
140
+ <div class="card-body p-2">
141
+ {% if not highlights.asc_unique_models %}
142
+ <p class="text-muted small m-2">Aucun modèle parent trouvé.</p>
143
+ {% else %}
144
+ {% for model in highlights.asc_unique_models %}
145
+ <div class="card mb-2 position-relative ">
146
+ <!-- MODIFICATION : Structure interne de la carte changée -->
147
+ <div class="card-body p-2">
148
+ <!-- Zone pour les badges, maintenant au-dessus, centrée et avec retour à la ligne -->
149
+ <div class="d-flex flex-wrap justify-content-center gap-1 mb-2">
150
+ {% for badge in model.badges %}
151
+ <span class="badge {{ badge.class }} on-top"
152
+ data-bs-toggle="tooltip"
153
+ data-bs-placement="top"
154
+ title="{{ badge.title }}">
155
+ {{ badge.text }}
156
+ </span>
157
+ {% endfor %}
158
+ </div>
159
+ <!-- Contenu principal de la carte -->
160
+ <div class="text-center">
161
+ <!-- MODIFICATION : Ajout de la classe "text-break" pour empêcher le débordement -->
162
+ <h6 class="card-title small mb-1 text-break card-interactive">
163
+ <a href="https://huggingface.co/{{ model.name }}" target="_blank" rel="noopener noreferrer" class="stretched-link" data-bs-toggle="tooltip" data-bs-placement="top" data-bs-html="true"
164
+ title="<b>Informations :</b>
165
+ <br>Auteur : {{ (model.name.split('/') | first) if '/' in model.name else 'Inconnu' }}<br>
166
+ Ciations : {{ '{:,.0f}'.format((model.citation_count or 0) | int).replace(',', ' ') }}<br>
167
+ J'aime : {{ model.likes or 'Inconnu' }}<br>
168
+ Date publication : {{ model.createdAt or 'N/A' }}<br>
169
+ Tâche : {{ model.task or 'Inconnue' }}<br>
170
+ License : {{ model.license or 'Inconnue' }}"
171
+ >
172
+ {{ model.name }}
173
+ </a>
174
+ </h6>
175
+ <p class="card-text" style="font-size: 0.75em; line-height: 1.2;">
176
+ {{ "{:,.0f}".format((model.downloads or 0) | int).replace(',', ' ') }} téléchargements<br>
177
+ {{ "{:,.0f}".format((model.citation_count or 0) | int).replace(',', ' ') }} citations
178
+ </p>
179
+ </div>
180
+ </div>
181
+ </div>
182
+ {% endfor %}
183
+ {% endif %}
184
+ </div>
185
+ </div>
186
+ </div>
187
+
188
+ <!-- Colonne pour la première flèche -->
189
+ <div class="col-lg-1 d-none d-lg-flex justify-content-center">
190
+ <i class="bi bi-arrow-right fs-1 text-secondary"></i>
191
+ </div>
192
+
193
+ <!-- COLONNE MODÈLE RECHERCHÉ (CENTRE) (inchangée) -->
194
+ <div class="col-12 col-lg-4 " id="center-column">
195
+ <h3 class="h5 text-center mb-3">Modèle recherché</h3>
196
+ <a href="https://huggingface.co/{{ searched_node.id }}" target="_blank" rel="noopener noreferrer" class="text-white">
197
+ <div class="card card-recherche text-center border-2 ">
198
+ <div class="card-header card-recherche-header text-white">
199
+ <h5 class="card-title h6 mb-0 ">
200
+ {{ searched_node.id }}
201
+ </h5>
202
+ </div>
203
+ <div class="card-body p-3">
204
+ <div class="row">
205
+ <div class="col">
206
+ <p class="fw-bold mb-0">{{ "{:,.0f}".format((searched_node.ascendantsCount or 0) | int).replace(',', ' ') }}</p>
207
+ <p class="small text-muted">ascendant(s)
208
+
209
+ <i class="bi bi-info-circle-fill ms-2"
210
+ data-bs-toggle="tooltip"
211
+ data-bs-placement="right"
212
+ title="Nombre de modèles qui précèdent le modèle recherché dans la généalogie">
213
+ </i>
214
+ </p>
215
+
216
+ </div>
217
+ <div class="col">
218
+ <p class="fw-bold mb-0">{{ "{:,.0f}".format((searched_node.descendantsCount or 0) | int).replace(',', ' ') }}</p>
219
+ <p class="small text-muted">descendant(s)
220
+ <i class="bi bi-info-circle-fill ms-2"
221
+ data-bs-toggle="tooltip"
222
+ data-bs-placement="right"
223
+ title="Nombre de modèles directement ou indirectement constitués à partir du modèle recherché">
224
+ </i>
225
+ </p>
226
+ </div>
227
+ </div>
228
+ <div class="row mt-2">
229
+ <div class="col"><p class="fw-bold mb-0">{{ "{:,.0f}".format((searched_node.downloads or 0) | int).replace(',', ' ') }}</p><p class="small text-muted">téléchargement(s)</p></div>
230
+ <div class="col"><p class="fw-bold mb-0">{{ "{:,.0f}".format((searched_node.citationCount or 0) | int).replace(',', ' ') }}</p>
231
+ <p class="small text-muted">citation(s)
232
+ <i class="bi bi-info-circle-fill ms-2"
233
+ data-bs-toggle="tooltip"
234
+ data-bs-placement="right"
235
+ title="Nombre de descendants directs du modèle recherché : les enfants du modèle recherché">
236
+ </i>
237
+ </p></div>
238
+ <div class="col"><p class="fw-bold mb-0">{{ "{:,.0f}".format((searched_node.likes or 0) | int).replace(',', ' ') }}</p><p class="small text-muted">J'aime</p></div>
239
+ </div>
240
+ <div class="mt-2">
241
+ {% if searched_node.createdAt %}<span class="badge bg-light text-dark">Publié le: {{ searched_node.createdAt }}</span>{% endif %}
242
+ {% if searched_node.task %}<span class="badge bg-light text-dark">Tâche: {{ searched_node.task }}</span>{% endif %}
243
+ </div>
244
+ <hr>
245
+ </a>
246
+ <button id="show-graph-btn" class="btn btn-secondary"><i class="bi bi-diagram-3 me-1"></i>Voir l'arbre généalogique</button>
247
+ </div>
248
+ </div>
249
+ </div>
250
+
251
+ <!-- Colonne pour la deuxième flèche -->
252
+ <div class="col-lg-1 d-none d-lg-flex justify-content-center">
253
+ <i class="bi bi-arrow-right fs-1 text-secondary"></i>
254
+ </div>
255
+
256
+ <!-- COLONNE DESCENDANCE (DROITE) -->
257
+ <div class="col-12 col-lg-3">
258
+ <div class="d-flex justify-content-center gap-2 mb-3">
259
+ <h4 class="h5 text-center mb-3">Modèles importants de la descendance
260
+ <!-- L'icône avec les attributs pour le tooltip -->
261
+ <i class="bi bi-info-circle-fill text-secondary align-middle ms-2"
262
+ data-bs-toggle="tooltip"
263
+ data-bs-placement="right"
264
+ title="Les modèles directement ou indirectement constitués à partir du modèle recherché">
265
+ </i>
266
+ </h4>
267
+ </div>
268
+ <div class="card">
269
+ <div class="card-body p-2">
270
+ {% if highlights.desc_unique_models %}
271
+ {% for model in highlights.desc_unique_models %}
272
+ <div class="card mb-2 position-relative ">
273
+ <div class="card-body p-2">
274
+ <!-- Zone pour les badges, maintenant au-dessus, centrée et avec retour à la ligne -->
275
+ <div class="d-flex flex-wrap justify-content-center gap-1 mb-2">
276
+ {% for badge in model.badges %}
277
+ <span class="badge {{ badge.class }} on-top"
278
+ data-bs-toggle="tooltip"
279
+ data-bs-placement="top"
280
+ title="{{ badge.title }}">
281
+ {{ badge.text }}
282
+ </span>
283
+ {% endfor %}
284
+ </div>
285
+ <!-- Contenu principal de la carte -->
286
+ <div class="text-center card-interactive">
287
+ <!-- MODIFICATION : Ajout de la classe "text-break" pour empêcher le débordement -->
288
+ <h6 class="card-title small mb-1 text-break">
289
+ <a href="https://huggingface.co/{{ model.name }}"
290
+ target="_blank"
291
+ rel="noopener noreferrer"
292
+ class="stretched-link"
293
+ data-bs-toggle="tooltip"
294
+ data-bs-placement="top"
295
+ data-bs-html="true"
296
+ title="<b>Informations :</b><br>
297
+ Auteur : {{ (model.name.split('/') | first) if '/' in model.name else 'Inconnu' }}<br>
298
+ Téléchargements : {{ '{:,.0f}'.format((model.downloads or 0) | int).replace(',', ' ') }}<br>
299
+ J'aime : {{ model.likes or 'Inconnu' }}<br>
300
+ Date publication : {{ model.createdAt or 'N/A' }}<br>
301
+ Tâche : {{ model.task or 'Inconnue' }}<br>
302
+ License : {{ model.license or 'Inconnue' }}">
303
+ {{ model.name }}
304
+ </a>
305
+ </h6>
306
+ <p class="card-text" style="font-size: 0.75em; line-height: 1.2;">
307
+ {{ "{:,.0f}".format((model.downloads or 0) | int).replace(',', ' ') }} téléchargements<br>
308
+ {{ "{:,.0f}".format((model.citation_count or 0) | int).replace(',', ' ') }} citations
309
+ </p>
310
+ </div>
311
+ </div>
312
+ </div>
313
+ {% endfor %}
314
+ {% else %}
315
+ <p class="text-muted small m-2">Aucun modèle dérivé trouvé.</p>
316
+ {% endif %}
317
+ </div>
318
+ </div>
319
+ </div>
320
+ </div>
321
+ </div>
322
+
323
+ <!-- SECTION 2 ET 3 (inchangées) -->
324
+ <div class="row mt-5 mb-5">
325
+ <hr class="mb-5">
326
+ <div class="col-12">
327
+ <div class="mb-5">
328
+ <h4 class="h4">Descendance détaillée</h4>
329
+ <div class="table-responsive">
330
+ <table id="descendance-table" class="table table-bordered table-striped table-hover">
331
+ <thead>
332
+ <tr>
333
+ <th scope="col">Modèle</th>
334
+ <th scope="col">Auteur</th>
335
+ <th scope="col">Téléchargements</th>
336
+ <th scope="col">Tâche</th>
337
+ <th scope="col">J'aime</th>
338
+ <th scope="col">Date de publication</th>
339
+ <th scope="col">Dataset utilisé</th>
340
+ <th scope="col">Licence</th>
341
+ <th scope="col">Distance au modèle recherché</th>
342
+ <th scope="col">Ascendants</th>
343
+ <th scope="col">Descendants</th>
344
+ <th scope="col">Citations</th>
345
+ </tr>
346
+ </thead>
347
+ </table>
348
+ </div>
349
+ </div>
350
+ <hr class="my-5">
351
+ <div>
352
+ <h4 class="h4">Ascendance détaillée</h4>
353
+ <div class="table-responsive">
354
+ <table id="ascendance-table" class="table table-bordered table-striped table-hover">
355
+ <thead>
356
+ <tr>
357
+ <th scope="col">Modèle</th>
358
+ <th scope="col">Auteur</th>
359
+ <th scope="col">Téléchargements</th>
360
+ <th scope="col">Tâche</th>
361
+ <th scope="col">J'aime</th>
362
+ <th scope="col">Date de publication</th>
363
+ <th scope="col">Dataset utilisé</th>
364
+ <th scope="col">Licence</th>
365
+ <th scope="col">Distance au modèle recherché</th>
366
+ <th scope="col">Ascendants</th>
367
+ <th scope="col">Descendants</th>
368
+ <th scope="col">Citations</th>
369
+ </tr>
370
+ </thead>
371
+ </table>
372
+ </div>
373
+ </div>
374
+ </div>
375
+ <hr class="mt-5">
376
+ </div>
377
+ <div id="genealogy-graph-section" style="display: none;" class="mt-5">
378
+ <div class="row g-4">
379
+ <div class="col-12 col-lg-8">
380
+ <div class="card">
381
+ <div class="card-header"><h5 class="card-title mb-0">Visualisation de l'arbre généalogique</h5></div>
382
+ <div class="card-body">
383
+ <div id="sigma-container" data-graph='{{ graph_data | tojson | safe }}'></div>
384
+ </div>
385
+ </div>
386
+ <div id="node-info-card" class="card mt-3 card-interactive" style="display: none;"> <div class="card-body">
387
+ <h5 class="card-title">Informations du nœud sélectionné</h5>
388
+ <div id="node-details" class="mt-2"></div>
389
+ </div>
390
+ </div>
391
+ </div>
392
+ <div class="col-12 col-lg-4">
393
+ <div class="card mb-3">
394
+ <div class="card-header"><h5 class="card-title mb-0">Légende - Nœuds</h5></div>
395
+ <div class="card-body">
396
+ <ul id="legend-nodes" class="list-group list-group-flush">
397
+ <li class="list-group-item d-flex align-items-center"><span class="legend-color me-2" style="background-color: #007bff;"></span>Personne (taille = nombre d'abonnés)</li>
398
+ <li class="list-group-item d-flex align-items-center"><span class="legend-color me-2" style="background-color: #092d53;"></span>Organisation (taille = nombre d'abonnés)</li>
399
+ <li class="list-group-item d-flex align-items-center"><span class="legend-color me-2" style="background-color:#7D7D7D;"></span>Modèle (taille = nombre de téléchargements)</li>
400
+ </ul>
401
+ </div>
402
+ </div>
403
+ <div class="card">
404
+ <div class="card-header"><h5 class="card-title mb-0">Légende - Relations</h5></div>
405
+ <div class="card-body">
406
+ <ul id="legend-edges" class="list-group list-group-flush"></ul>
407
+ </div>
408
+ </div>
409
+ </div>
410
+ </div>
411
+ </div>
412
+ {% endif %}
413
+ </main>
414
+
415
+ <!-- Pied de page et scripts (inchangés) -->
416
+ <footer class="bg-dark text-white text-center p-4 mt-auto">
417
+ <div class="container">
418
+ <p class="mb-0">Application de recherche et visualisation des relations entre modèles et jeux de données publiés sur la plateforme HuggingFace. © 2025</p>
419
+ </div>
420
+ </footer>
421
+
422
+ <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
423
+ <script src="https://cdn.datatables.net/1.13.4/js/jquery.dataTables.min.js"></script>
424
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
425
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/sigma.js/2.4.0/sigma.min.js"></script>
426
+ <script src="https://unpkg.com/graphology@0.25.1/dist/graphology.umd.min.js"></script>
427
+ <script src="https://cdn.jsdelivr.net/npm/graphology-library/dist/graphology-library.min.js"></script>
428
+ <script src="{{ url_for('static', filename='js/utils.js') }}"></script>
429
+ <script src="{{ url_for('static', filename='js/script.js') }}"></script>
430
+ <script>
431
+ document.addEventListener('DOMContentLoaded', function () {
432
+ const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
433
+ const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
434
+ });
435
+ </script>
436
+ </body>
437
+ </html>
application_neo4j/templates/search_dataset.html ADDED
@@ -0,0 +1,236 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="fr">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Généalogie des Modèles</title>
7
+
8
+ <!-- CSS de Bootstrap (remplace DSFR) -->
9
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
10
+ <!-- Icônes Bootstrap (pour les boutons, etc.) -->
11
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css">
12
+
13
+ <!-- CSS de DataTables et votre CSS personnalisé (inchangés) -->
14
+ <link rel="stylesheet" href="https://cdn.datatables.net/1.13.4/css/jquery.dataTables.min.css" />
15
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
16
+
17
+ <style>
18
+ body {
19
+ font-size: 1.05rem;
20
+ }
21
+ .legend-color {
22
+ width: 1em;
23
+ height: 1em;
24
+ display: inline-block;
25
+ border-radius: 2px;
26
+ }
27
+ .legend-color.edge {
28
+ border-radius: 0;
29
+ height: 0.25em;
30
+ transform: translateY(-0.35em);
31
+ }
32
+ </style>
33
+ </head>
34
+
35
+ <body class="d-flex flex-column min-vh-100">
36
+ <header class="navbar navbar-expand-lg navbar-light bg-light border-bottom">
37
+ <div class="container">
38
+ <a class="navbar-brand" href="/">
39
+ <span class="fw-bold">Généalogie des Modèles</span>
40
+ <small class="d-block text-muted">Exploration des relations entre modèles et datasets</small>
41
+ </a>
42
+ </div>
43
+ </header>
44
+
45
+ <main class="container mt-4 flex-grow-1">
46
+ <!-- Titre principal et Formulaire (inchangés) -->
47
+ <div class="row">
48
+ <div class="col-12">
49
+ <h1 class="display-5">Recherche dans la base de données HuggingFace</h1>
50
+ <p class="lead">Explorez la généalogie des modèles et datasets</p>
51
+ </div>
52
+ </div>
53
+
54
+ <div class="row g-3 mb-4">
55
+ <div class="col-12">
56
+ <form method="POST" action="{{ url_for('findnode') }}" autocomplete="off">
57
+ <input type="hidden" name="filter" value="{{ search.filter or '' }}">
58
+
59
+ <div class="row g-3 align-items-end">
60
+ <!-- Champ de recherche -->
61
+ <div class="col-12 col-md-5">
62
+ <label for="search-input" class="form-label fw-bold">Nom à rechercher</label>
63
+ <div class="position-relative">
64
+ <input type="text" name="name" id="search-input" class="form-control"
65
+ placeholder="Taper le nom du dataset suspecté."
66
+ value="{{ request.form.name or '' }}" required autocomplete="off" />
67
+ <ul id="suggestions-list" class="list-group position-absolute w-100" style="display: none; z-index: 1000;"></ul>
68
+ </div>
69
+ </div>
70
+
71
+ <!-- Filtres -->
72
+ <div class="col-12 col-md-3">
73
+ <label class="form-label fw-bold">Filtres</label>
74
+ <div class="d-flex gap-3">
75
+ <div class="form-check">
76
+ <input class="form-check-input" type="checkbox" name="filters" value="Model" id="filter-model"
77
+ {% if 'Model' in search.filters %}checked{% endif %}>
78
+ <label class="form-check-label" for="filter-model">Modèle</label>
79
+ </div>
80
+ <div class="form-check">
81
+ <input class="form-check-input" type="checkbox" name="filters" value="Dataset" id="filter-dataset"
82
+ {% if 'Dataset' in search.filters %}checked{% endif %}>
83
+ <label class="form-check-label" for="filter-dataset">Dataset</label>
84
+ </div>
85
+ </div>
86
+ </div>
87
+
88
+ <!-- Profondeur de recherche -->
89
+ <div class="col-12 col-md-4">
90
+ <label class="form-label fw-bold">Profondeur de recherche</label>
91
+ <div class="d-flex align-items-center gap-3">
92
+ <div class="form-check form-switch">
93
+ <input class="form-check-input" type="checkbox" id="depth-unlimited" name="depth_unlimited" checked>
94
+ <label class="form-check-label" for="depth-unlimited">Illimitée</label>
95
+ </div>
96
+ <div class="input-group input-group-sm">
97
+ <span class="input-group-text">Limité à:</span>
98
+ <input class="form-control" type="number" name="depth" id="depth"
99
+ value="{{ request.form.depth }}" min="1" max="5" disabled>
100
+ </div>
101
+ </div>
102
+ </div>
103
+ </div>
104
+
105
+
106
+ {% if message %}
107
+ <div class="alert alert-info mt-3"><p class="mb-0">{{ message }}</p></div>
108
+ {% endif %}
109
+ {% if graph_data.nodes %}
110
+ {% set searched_node = (graph_data.nodes | selectattr('id', 'equalto', search.name) | list | first) or {} %}
111
+ <div class="row justify-content-center align-items-stretch g-4">
112
+ <div class="col-12 col-lg-4 " id="center-column">
113
+ <h4 class="h5 text-center mb-3">DATASET RECHERCHÉ</h4>
114
+ <a href="https://huggingface.co/datasets/{{ searched_node.id }}" target="_blank" rel="noopener noreferrer" class="text-white">
115
+ <div class="card card-recherche text-center border-primary border-2 ">
116
+ <div class="card-header bg-primary text-white">
117
+ <h5 class="card-title h6 mb-0 ">
118
+ {{ searched_node.id }}
119
+ </h5>
120
+ </div>
121
+ <div class="card-body p-3">
122
+ <div class="row">
123
+ <div class="col">
124
+ <p class="fw-bold mb-0">{{ "{:,.0f}".format((graph_data.models_count[0] or 0) | int).replace(',', ' ') }}</p>
125
+ <p class="small text-muted">modèle(s) utilisent ce dataset
126
+ <i class="bi bi-info-circle-fill ms-2"></i>
127
+ </p>
128
+ </div>
129
+ </div>
130
+ <div class="row mt-2">
131
+ <div class="col"><p class="fw-bold mb-0">{{ "{:,.0f}".format((searched_node.downloads or 0) | int).replace(',', ' ') }}</p><p class="small text-muted">téléchargement(s)</p></div>
132
+ </div>
133
+ <div class="mt-2">
134
+ {% if searched_node.createdAt_dataset %}<span class="badge bg-light text-dark">Publié le: {{ searched_node.createdAt_dataset }}</span>{% endif %}
135
+ </div>
136
+ <hr>
137
+ </a>
138
+ <button id="show-graph-btn" class="btn btn-secondary"><i class="bi bi-diagram-3 me-1"></i>Voir l'arbre généalogique</button>
139
+ </div>
140
+ </div>
141
+ </div>
142
+ </div>
143
+ {% endif %}
144
+
145
+ <!-- SECTION 2 ET 3 (inchangées) -->
146
+ <div class="row mt-5 mb-5">
147
+ <hr class="mb-5">
148
+ <div class="col-12">
149
+ <div class="mb-5">
150
+ <h4 class="h4">Les modèles entraînés sur ce dataset</h4>
151
+ <div class="table-responsive">
152
+ <table id="train-table" class="table table-bordered table-striped table-hover">
153
+ <thead>
154
+ <tr>
155
+ <th scope="col">Modèle</th>
156
+ <th scope="col">Auteur</th>
157
+ <th scope="col">Téléchargements</th>
158
+ <th scope="col">Tâche</th>
159
+ <th scope="col">J'aime</th>
160
+ <th scope="col">Date de publication</th>
161
+ <th scope="col">Dataset utilisé</th>
162
+ <th scope="col">Licence</th>
163
+ <th scope="col">Ascendants</th>
164
+ <th scope="col">Descendants</th>
165
+ <th scope="col">Citations</th>
166
+ </tr>
167
+ </thead>
168
+ <tbody>
169
+ <!-- DataTables remplira ici -->
170
+ </tbody>
171
+ </table>
172
+ </div>
173
+ </div>
174
+ </div>
175
+ </div>
176
+ <div id="genealogy-graph-section" style="display: none;" class="mt-5">
177
+ <div class="row g-4">
178
+ <div class="col-12 col-lg-8">
179
+ <div class="card">
180
+ <div class="card-header"><h5 class="card-title mb-0">Visualisation de l'arbre généalogique</h5></div>
181
+ <div class="card-body">
182
+ <div id="sigma-container" data-graph='{{ graph_data | tojson | safe }}'></div>
183
+ </div>
184
+ </div>
185
+ <div id="node-info-card" class="card mt-3 card-interactive" style="display: none;"> <div class="card-body">
186
+ <h5 class="card-title">Informations du nœud sélectionné</h5>
187
+ <div id="node-details" class="mt-2"></div>
188
+ </div>
189
+ </div>
190
+ </div>
191
+ <div class="col-12 col-lg-4">
192
+ <div class="card mb-3">
193
+ <div class="card-header"><h5 class="card-title mb-0">Légende - Nœuds</h5></div>
194
+ <div class="card-body">
195
+ <ul id="legend-nodes" class="list-group list-group-flush">
196
+ <li class="list-group-item d-flex align-items-center"><span class="legend-color me-2" style="background-color: #ffc107;"></span>Dataset (taille = nombre de téléchargements)</li>
197
+ <li class="list-group-item d-flex align-items-center"><span class="legend-color me-2" style="background-color: #007bff;"></span>Personne (taille = nombre d'abonnés)</li>
198
+ <li class="list-group-item d-flex align-items-center"><span class="legend-color me-2" style="background-color: #092d53;"></span>Organisation (taille = nombre d'abonnés)</li>
199
+ <li class="list-group-item d-flex align-items-center"><span class="legend-color me-2" style="background-color:#7D7D7D;"></span>Modèle (taille = nombre de téléchargements)</li>
200
+ </ul>
201
+ </div>
202
+ </div>
203
+ <div class="card">
204
+ <div class="card-header"><h5 class="card-title mb-0">Légende - Relations</h5></div>
205
+ <div class="card-body">
206
+ <ul id="legend-edges" class="list-group list-group-flush"></ul>
207
+ </div>
208
+ </div>
209
+ </div>
210
+ </div>
211
+ </div>
212
+ </main>
213
+
214
+ <!-- Pied de page et scripts (inchangés) -->
215
+ <footer class="bg-dark text-white text-center p-4 mt-auto">
216
+ <div class="container">
217
+ <p class="mb-0">Application de recherche et visualisation des relations entre modèles et jeux de données publiés sur la plateforme HuggingFace. © 2025</p>
218
+ </div>
219
+ </footer>
220
+
221
+ <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
222
+ <script src="https://cdn.datatables.net/1.13.4/js/jquery.dataTables.min.js"></script>
223
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
224
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/sigma.js/2.4.0/sigma.min.js"></script>
225
+ <script src="https://unpkg.com/graphology@0.25.1/dist/graphology.umd.min.js"></script>
226
+ <script src="https://cdn.jsdelivr.net/npm/graphology-library/dist/graphology-library.min.js"></script>
227
+ <script src="{{ url_for('static', filename='js/utils.js') }}"></script>
228
+ <script src="{{ url_for('static', filename='js/script_dataset.js') }}"></script>
229
+ <script>
230
+ document.addEventListener('DOMContentLoaded', function () {
231
+ const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
232
+ const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
233
+ });
234
+ </script>
235
+ </body>
236
+ </html>
application_neo4j/utils.py ADDED
@@ -0,0 +1,246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+ import pandas as pd
3
+ from collections import namedtuple
4
+ from tqdm import tqdm
5
+ import ast
6
+ from concurrent.futures import ThreadPoolExecutor
7
+ import os
8
+ import multiprocessing
9
+ from neo4j.exceptions import TransientError
10
+
11
+
12
+ # --- Paramètres ---
13
+ BATCH_SIZE = 5000
14
+ NUM_THREADS = multiprocessing.cpu_count()
15
+ PROCESSED_IDS_FILE = "processed_ids.txt"
16
+
17
+
18
+ def reset_database(driver):
19
+ """
20
+ Efface TOUTES les données de la base Neo4j ET le fichier de suivi des IDs traités.
21
+ À n'utiliser que pour une réinitialisation complète.
22
+ """
23
+ # Étape 1 : Vider la base de données
24
+ with driver.session() as session:
25
+ result = session.run("RETURN 1 AS test")
26
+ print("Connexion OK, test result:", result.single()["test"])
27
+ session.run("MATCH (n) DETACH DELETE n")
28
+
29
+ # Étape 2 : Supprimer le fichier de suivi pour garantir une réimportation propre
30
+ if os.path.exists(PROCESSED_IDS_FILE):
31
+ os.remove(PROCESSED_IDS_FILE)
32
+
33
+
34
+ def parse_list_field(value):
35
+ """
36
+ Parse une chaîne de caractères qui représente une liste (ex: "['model1', 'model2']")
37
+ en une véritable liste Python. Gère les cas où la valeur est simple ou vide.
38
+ """
39
+ if isinstance(value, str) and pd.notna(value):
40
+ try:
41
+ parsed = ast.literal_eval(value)
42
+ if isinstance(parsed, list):
43
+ return parsed
44
+ except Exception:
45
+ pass
46
+ return [value] if value else []
47
+
48
+ def load_processed_ids():
49
+ """Charge l'ensemble des IDs déjà traités depuis le fichier de suivi."""
50
+ if os.path.exists(PROCESSED_IDS_FILE):
51
+ with open(PROCESSED_IDS_FILE, "r", encoding="utf-8") as f:
52
+ return set(line.strip() for line in f)
53
+ return set()
54
+
55
+ def append_processed_ids(ids):
56
+ """Ajoute une liste d'IDs au fichier de suivi."""
57
+ with open(PROCESSED_IDS_FILE, "a", encoding="utf-8") as f:
58
+ for i in ids:
59
+ f.write(f"{i}\n")
60
+
61
+ def run_with_retry(session, query, parameters=None, retries=3, delay=1):
62
+ """
63
+ Exécute une requête Cypher avec une logique de réessai en cas d'erreur transitoire
64
+ """
65
+ for attempt in range(retries):
66
+ try:
67
+ session.run(query, parameters)
68
+ return
69
+ except TransientError as e:
70
+ if attempt < retries - 1:
71
+ time.sleep(delay)
72
+ continue
73
+ else:
74
+ raise
75
+
76
+ def process_batch(rows, fieldnames, driver):
77
+ """
78
+ Traite un lot (batch) de lignes du CSV et les insère dans Neo4j.
79
+ C'est la fonction "worker" qui sera exécutée en parallèle.
80
+ """
81
+ normalized_fields = [f.strip().replace(" ", "_").replace("-", "_") for f in fieldnames]
82
+ Row = namedtuple("Row", normalized_fields)
83
+ ids_successfully_processed = []
84
+
85
+ with driver.session() as session:
86
+ for row in rows:
87
+ obj = Row(**{k: v if v != "" else None for k, v in row.items()})
88
+ data = obj._asdict()
89
+
90
+ if not data.get("id") or pd.isna(data.get("id")) or pd.isna(data.get("author")) or str(data.get("id")).strip() == "":
91
+ continue # Ignore si l'ID est manquant
92
+
93
+ base_models = parse_list_field(data.get("base_model"))
94
+ base_model_rels = parse_list_field(data.get("base_model_relation"))
95
+ datasets = parse_list_field(data.get("dataset"))
96
+ orgs_author_model = parse_list_field(data.get("organizations_author_model"))
97
+ orgs_author_dataset = parse_list_field(data.get("organizations_author_dataset"))
98
+ # Insertion du modèle et de son auteur
99
+ if data.get("id") and data.get("author"):
100
+ run_with_retry(session, """
101
+ MERGE (m:Model {name: $id})
102
+ SET m.downloads = $downloadsAllTime,
103
+ m.task = $pipeline_tag,
104
+ m.createdAt = $createdAt,
105
+ m.parameters = $total_parameters_formatted,
106
+ m.likes = $likes,
107
+ m.license = $license
108
+ MERGE (a:Author {name: $author})
109
+ SET a.type = $author_type,
110
+ a.followers = $followers_count_author_model
111
+ WITH a
112
+ MATCH (m:Model {name: $id})
113
+ MERGE (a)-[p:POSTED]->(m)
114
+ SET p.name = "A publié"
115
+ """, data)
116
+
117
+ # Lien entre l’auteur et ses organisations
118
+ orgs_data = [
119
+ {"org": org, "author": data["author"]}
120
+ for org in orgs_author_model
121
+ if pd.notna(org) and data.get("author")
122
+ ]
123
+ if orgs_data:
124
+ run_with_retry(session, """
125
+ UNWIND $orgs_data AS row
126
+ MERGE (o:Author {name: row.org})
127
+ WITH o, row
128
+ MATCH (a:Author {name: row.author})
129
+ MERGE (a)-[r:IS_IN]->(o)
130
+ SET r.name = "Fait partie de cette organisation", a.type = "personne",o.type = "organisation"
131
+ """, {"orgs_data": orgs_data})
132
+
133
+ # Lien entre modèles et base models
134
+
135
+ if len(base_models) == len(base_model_rels) :
136
+ base_model_data = [
137
+ {"bm": bm, "id": data["id"], "rel": rel}
138
+ for bm, rel in zip(base_models, base_model_rels)
139
+ if pd.notna(bm) and data.get("id")
140
+ ]
141
+ elif len(base_models) >len(base_model_rels) :
142
+ if base_model_rels==['merge'] :
143
+ base_model_data = [
144
+ {"bm": bm, "id": data["id"], "rel": "merge"}
145
+ for bm in base_models
146
+ if pd.notna(bm) and data.get("id")
147
+ ]
148
+ else :
149
+ base_model_data = [
150
+ {"bm": bm, "id": data["id"], "rel": "A généré"}
151
+ for bm in base_models
152
+ if pd.notna(bm) and data.get("id")
153
+ ]
154
+
155
+ if base_model_data:
156
+ run_with_retry(session, """
157
+ UNWIND $base_model_data AS row
158
+ MERGE (bm:Model {name: row.bm})
159
+ WITH bm, row
160
+ MATCH (m:Model {name: row.id})
161
+ MERGE (bm)-[r:USED_IN]->(m)
162
+ SET r.name = row.rel
163
+ """, {"base_model_data": base_model_data})
164
+
165
+ # Lien entre modèles et datasets
166
+ datasets_data = [
167
+ {"ds": ds, "downloads": data.get("downloads_dataset"),
168
+ "createdAt_dataset": data.get("createdAt_dataset"), "id": data["id"]}
169
+ for ds in datasets
170
+ if pd.notna(ds) and data.get("id")
171
+ ]
172
+ if datasets_data and data.get("author_dataset") and data.get("dataset") and pd.notna(data.get("author_dataset")):
173
+ run_with_retry(session, """
174
+ UNWIND $datasets_data AS row
175
+ MERGE (d:Dataset {name: row.ds})
176
+ SET d.downloads = row.downloads,
177
+ d.createdAt_dataset = row.createdAt_dataset
178
+ WITH d, row
179
+ MATCH (m:Model {name: row.id})
180
+ MERGE (d)-[r:USED_IN]->(m)
181
+ SET r.name = "A été utilisé dans ce modèle"
182
+ """, {"datasets_data": datasets_data})
183
+
184
+ # Insertion de l’auteur du dataset
185
+ run_with_retry(session, """
186
+ MERGE (ad:Author {name: $author_dataset})
187
+ SET ad.type = $author_dataset_type,
188
+ ad.followers = $followers_count_author_dataset
189
+ WITH ad
190
+ MATCH (d:Dataset {name: $dataset})
191
+ MERGE (ad)-[r:POSTED]->(d)
192
+ SET r.name = "A publié"
193
+ """, data)
194
+
195
+ # Lien entre l’auteur du dataset et ses organisations
196
+ orgs_dataset_data = [
197
+ {"org": org, "author_dataset": data["author_dataset"]}
198
+ for org in orgs_author_dataset
199
+ if pd.notna(org) and data.get("author_dataset")
200
+ ]
201
+ if orgs_dataset_data and pd.notna(data.get("author_dataset")):
202
+ run_with_retry(session, """
203
+ UNWIND $orgs_data AS row
204
+ MERGE (o:Author {name: row.org})
205
+ WITH o, row
206
+ MATCH (ad:Author {name: row.author_dataset})
207
+ MERGE (ad)-[r:IS_IN]->(o)
208
+ SET r.name = "Fait partie de cette organisation", ad.type = "personne",o.type = "organisation"
209
+ """, {"orgs_data": orgs_dataset_data})
210
+
211
+ ids_successfully_processed.append(data["id"])
212
+
213
+ if ids_successfully_processed:
214
+ append_processed_ids(ids_successfully_processed)
215
+
216
+ # Insère les données depuis un CSV en parallèle, par lots
217
+ def insert_parallel(csv_file_path, driver, processed_ids):
218
+ # Lecture et nettoyage via pandas
219
+ df = pd.read_csv(csv_file_path)
220
+ df = df.loc[:, ~df.columns.str.contains('^Unnamed')]
221
+ # Supprimer les lignes où 'id' est NaN ou vide
222
+ df = df[~df["id"].isnull()]
223
+ df = df[df["id"].astype(str).str.strip() != ""]
224
+
225
+ # Ne conserver que les lignes dont l'ID n’a pas encore été traitée
226
+ df = df[~df["id"].isin(processed_ids)]
227
+
228
+ records = df.to_dict(orient="records")
229
+ fieldnames = list(df.columns)
230
+
231
+ batch = []
232
+ futures = []
233
+
234
+ with ThreadPoolExecutor(max_workers=NUM_THREADS) as executor:
235
+ for row in tqdm(records, desc="Lecture CSV"):
236
+ batch.append(row)
237
+ if len(batch) == BATCH_SIZE:
238
+ futures.append(executor.submit(process_batch, batch.copy(), fieldnames, driver))
239
+ batch = []
240
+
241
+ if batch:
242
+ futures.append(executor.submit(process_batch, batch.copy(), fieldnames, driver))
243
+
244
+ for future in tqdm(futures, desc="Traitement parallélisé"):
245
+ future.result()
246
+
requirements.txt ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastjsonschema==2.21.1
2
+ flask==3.1.0
3
+ graphdatascience==1.17
4
+ httpx==0.28.1
5
+ huggingface-hub==0.31.4
6
+ ipysigma==0.24.5
7
+ ipywidgets==8.1.7
8
+ itables==2.3.0
9
+ jinja2==3.1.6
10
+ joblib==1.5.0
11
+ jsonschema==4.23.0
12
+ matplotlib==3.10.3
13
+ multimethod==2.0
14
+ neo4j==5.28.2
15
+ networkx==2.8.8
16
+ numpy==2.2.5
17
+ pandas==2.2.3
18
+ plotly==6.0.1
19
+ pyarrow==20.0.0
20
+ requests==2.32.3
21
+ scikit-learn==1.7.0
22
+ scipy==1.15.3
23
+ seaborn==0.13.2
24
+ tqdm==4.67.1
25
+ urllib3==2.4.0
start.sh ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # load database backup
2
+ mkdir -p /backups
3
+ wget --no-clobber --header="Authorization: Bearer $HF_TOKEN" https://huggingface.co/datasets/lhoestq/dump-neo4j/resolve/main/system.dump -P /backups/
4
+ wget --no-clobber --header="Authorization: Bearer $HF_TOKEN" https://huggingface.co/datasets/lhoestq/dump-neo4j/resolve/main/neo4j.dump -P /backups/
5
+ neo4j-admin database load --expand-commands system --from-path=/backups --overwrite-destination=true
6
+ neo4j-admin database load --expand-commands neo4j --from-path=/backups --overwrite-destination=true
7
+
8
+ # start database
9
+ /startup/docker-entrypoint.sh neo4j &
10
+ # start tool
11
+ python3 /application_neo4j/app.py neo4j genealogiemodeles &
12
+ # wait for any process to exit
13
+ wait -n
14
+ # exit with status of process that exited first
15
+ exit $?