Quentin Lhoest
commited on
Commit
·
f0806e2
1
Parent(s):
c46832a
add app
Browse files- Dockerfile +19 -0
- application_neo4j/.DS_Store +0 -0
- application_neo4j/README.md +122 -0
- application_neo4j/app.py +431 -0
- application_neo4j/app_algorithms.py +304 -0
- application_neo4j/extract_dump.md +21 -0
- application_neo4j/load_data.py +64 -0
- application_neo4j/requirements.txt +5 -0
- application_neo4j/static/.DS_Store +0 -0
- application_neo4j/static/css/style.css +271 -0
- application_neo4j/static/graph_export.html +67 -0
- application_neo4j/static/js/.DS_Store +0 -0
- application_neo4j/static/js/script.js +351 -0
- application_neo4j/static/js/script_dataset.js +357 -0
- application_neo4j/static/js/script_expert.js +377 -0
- application_neo4j/static/js/utils.js +233 -0
- application_neo4j/templates/expert.html +253 -0
- application_neo4j/templates/index.html +174 -0
- application_neo4j/templates/infos.html +18 -0
- application_neo4j/templates/search.html +437 -0
- application_neo4j/templates/search_dataset.html +236 -0
- application_neo4j/utils.py +246 -0
- requirements.txt +25 -0
- start.sh +15 -0
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 $?
|