pbordescnil commited on
Commit
b372d3c
·
1 Parent(s): 23bf45f

add english version

Browse files
application_neo4j/app.py CHANGED
@@ -1,15 +1,17 @@
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"
@@ -99,6 +101,54 @@ def ensure_graph_projected(gds: GraphDataScience, graph_name):
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")
@@ -177,7 +227,7 @@ def findnode():
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:
@@ -189,28 +239,28 @@ def findnode():
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
 
@@ -250,7 +300,7 @@ def findnode_expert():
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:
@@ -262,22 +312,22 @@ def findnode_expert():
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
 
 
1
  # --- Import et initialisation Flask ---
2
  import os
3
+ from flask import Flask, request, render_template, jsonify, session, redirect, url_for
4
  from neo4j import GraphDatabase, basic_auth
5
  from graphdatascience import GraphDataScience
6
  import app_algorithms as algo
7
+ from translations import t
8
  import pandas as pd
9
  from typing import Dict
10
  from collections import defaultdict
11
  import argparse
12
 
13
  app = Flask(__name__, static_url_path="/static/") # Application Flask
14
+ app.secret_key = os.urandom(24)
15
 
16
  # --- Connexion à Neo4j et GDS ---
17
  NEO4J_URI = "bolt://localhost:7687"
 
101
  """)
102
  print(f"Graphe '{reverse_graph_name}' projeté.")
103
 
104
+ @app.before_request
105
+ def set_language():
106
+ """Determine current language from session or URL query param."""
107
+ if "lang" not in session:
108
+ session["lang"] = "fr"
109
+ lang = request.args.get("lang")
110
+ if lang in ("fr", "en"):
111
+ session["lang"] = lang
112
+
113
+
114
+ @app.route("/set_language")
115
+ def set_language_route():
116
+ """Endpoint used by the language selector in the navbar."""
117
+ lang = request.args.get("lang", "fr")
118
+ if lang in ("fr", "en"):
119
+ session["lang"] = lang
120
+ referrer = request.headers.get("Referer", "/")
121
+ if referrer and "//" in referrer:
122
+ from urllib.parse import urlparse, parse_qs, urlencode
123
+ parsed = urlparse(referrer)
124
+ qs = parse_qs(parsed.query, keep_blank_values=True)
125
+ qs["lang"] = [lang]
126
+ return redirect(parsed.path + ("?" + urlencode(qs, doseq=True) if qs else ""))
127
+ return redirect("/")
128
+
129
+
130
+ @app.context_processor
131
+ def inject_i18n():
132
+ """Inject t() and current_lang into all Jinja2 templates."""
133
+ def _t(key, **kwargs):
134
+ return t(key, session.get("lang", "fr"), **kwargs)
135
+ # Dictionnaire JS pour les clés utilisées côté client
136
+ js_i18n_keys = [
137
+ "js.node_info.name", "js.node_info.type", "js.node_info.followers",
138
+ "js.node_info.downloads", "js.node_info.created", "js.node_info.task",
139
+ "js.node_info.dataset", "js.node_info.undefined", "js.node_info.unknown_date",
140
+ "js.node_info.see_on_hf",
141
+ "js.unknown", "js.unknown_f", "js.na",
142
+ ]
143
+ lang = session.get("lang", "fr")
144
+ js_i18n_data = {k: t(k, lang) for k in js_i18n_keys}
145
+ return {
146
+ "t": _t,
147
+ "current_lang": lang,
148
+ "js_i18n_data": js_i18n_data,
149
+ }
150
+
151
+
152
  @app.route("/") # Page d'accueil
153
  def home():
154
  return render_template("index.html")
 
227
  search_info = {"name": name, "depth": request.form.get("depth", 3), "unlimited": is_unlimited}
228
 
229
  if not name:
230
+ message = t("error.empty_name", session.get("lang", "fr"))
231
  return render_template("search.html", message=message, search=search_info, graph_data=graph_data, highlights=highlights)
232
 
233
  try:
 
239
  # Appeler la fonction GDS BFS
240
  gds_result = algo.run_gds_bfs(gds, natural_graph_name,reverse_graph_name, name, depth,False)
241
  if not gds_result :
242
+ message = t("error.model_not_found", session.get("lang", "fr"), name=name)
243
  return render_template("search.html", message=message, search=search_info, graph_data=graph_data,highlights=highlights)
244
 
245
  process_gds_bfs_results(gds_result, graph_data, name)
246
  if gds_result["source_label"] == "Model" :
247
+ highlights = algo.get_genealogy_highlights(gds, name, lang=session.get("lang", "fr"))
248
  return render_template("search.html", message=message, search=search_info, graph_data=graph_data,highlights=highlights)
249
 
250
  if gds_result["source_label"] == "Dataset" :
251
  return render_template("search_dataset.html", message=message, search=search_info, graph_data=graph_data)
252
 
253
  if not graph_data["nodes"] and not graph_data["edges"]:
254
+ message = t("error.no_neighbors", session.get("lang", "fr"), name=name)
255
  return render_template("search.html", message=message, search=search_info, graph_data=graph_data,highlights=highlights)
256
 
257
  except Exception as e:
258
  # Gérer le cas où le nœud source n'existe pas du tout
259
  if "Failed to find a node" in str(e):
260
+ message = t("error.node_not_found", session.get("lang", "fr"), name=name)
261
  else:
262
  print(f"GDS BFS Error: {e}")
263
+ message = t("error.gds", session.get("lang", "fr"), error=str(e))
264
  return render_template("search.html", message=message, search=search_info, graph_data=graph_data,highlights=highlights)
265
 
266
 
 
300
  search_info = {"name": name, "depth": request.form.get("depth", 3), "unlimited": is_unlimited,"filters": current_filters }
301
 
302
  if not name:
303
+ message = t("error.empty_name", session.get("lang", "fr"))
304
  return render_template("expert.html", message=message, search=search_info, graph_data=graph_data)
305
 
306
  try:
 
312
  # Appeler la fonction GDS BFS
313
  gds_result = algo.run_gds_bfs(gds, natural_graph_name,reverse_graph_name, name, depth,True)
314
  if not gds_result :
315
+ message = t("error.node_not_found_expert", session.get("lang", "fr"), name=name)
316
  return render_template("expert.html", message=message, search=search_info, graph_data=graph_data)
317
 
318
 
319
  process_gds_bfs_results(gds_result, graph_data, name)
320
 
321
  if not graph_data["nodes"] and not graph_data["edges"]:
322
+ message = t("error.no_neighbors", session.get("lang", "fr"), name=name)
323
 
324
  except Exception as e:
325
  # Gérer le cas où le nœud source n'existe pas du tout
326
  if "Failed to find a node" in str(e):
327
+ message = t("error.node_not_found", session.get("lang", "fr"), name=name)
328
  else:
329
  print(f"GDS BFS Error: {e}")
330
+ message = t("error.gds", session.get("lang", "fr"), error=str(e))
331
  return render_template("expert.html", message=message, search=search_info, graph_data=graph_data)
332
 
333
 
application_neo4j/app_algorithms.py CHANGED
@@ -2,6 +2,7 @@
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
  """
@@ -78,7 +79,7 @@ def run_gds_bfs(gds: GraphDataScience, natural_graph_name: str, reverse_graph_na
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
 
@@ -100,50 +101,50 @@ def get_genealogy_highlights(gds: "GraphDataScience", model_name: str, num_highl
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
 
@@ -272,13 +273,13 @@ def create_node_data(node_props, label):
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",
 
2
  from graphdatascience import GraphDataScience
3
  from typing import Dict, List, Any
4
  import pandas as pd
5
+ from translations import t
6
 
7
  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]:
8
  """
 
79
 
80
 
81
 
82
+ def get_genealogy_highlights(gds: "GraphDataScience", model_name: str, num_highlights: int = 2, lang: str = "fr") -> Dict:
83
  """
84
  Trouve les modèles clés dans l'ascendance et la descendance (1er/2e plus cités/téléchargés).
85
 
 
101
  # Les classes CSS sont directement des classes Bootstrap 5 pour simplifier le rendu dans le template HTML.
102
  badges_info = {
103
  'desc_cited_1': {
104
+ 'text': t('badge.desc_cited_1.text', lang),
105
  'class': 'bg-success',
106
+ 'title': t('badge.desc_cited_1.title', lang)
107
  },
108
  'desc_cited_2': {
109
+ 'text': t('badge.desc_cited_2.text', lang),
110
  'class': 'bg-success bg-opacity-75',
111
+ 'title': t('badge.desc_cited_2.title', lang)
112
  },
113
  'desc_downloaded_1': {
114
+ 'text': t('badge.desc_downloaded_1.text', lang),
115
  'class': 'beta',
116
+ 'title': t('badge.desc_downloaded_1.title', lang)
117
  },
118
  'desc_downloaded_2': {
119
+ 'text': t('badge.desc_downloaded_2.text', lang),
120
  'class': 'alpha',
121
+ 'title': t('badge.desc_downloaded_2.title', lang)
122
  },
123
 
124
  'asc_foundation': {
125
+ 'text': t('badge.asc_foundation.text', lang),
126
  'class': 'bg-warning text-dark',
127
+ 'title': t('badge.asc_foundation.title', lang)
128
  },
129
  'asc_cited_1': {
130
+ 'text': t('badge.asc_cited_1.text', lang),
131
  'class': 'bg-success',
132
+ 'title': t('badge.asc_cited_1.title', lang)
133
  },
134
  'asc_cited_2': {
135
+ 'text': t('badge.asc_cited_2.text', lang),
136
  'class': 'bg-success bg-opacity-75',
137
+ 'title': t('badge.asc_cited_2.title', lang)
138
  },
139
  'asc_downloaded_1': {
140
+ 'text': t('badge.asc_downloaded_1.text', lang),
141
  'class': 'beta',
142
+ 'title': t('badge.asc_downloaded_1.title', lang)
143
  },
144
  'asc_downloaded_2': {
145
+ 'text': t('badge.asc_downloaded_2.text', lang),
146
  'class': 'alpha',
147
+ 'title': t('badge.asc_downloaded_2.title', lang)
148
  },
149
  }
150
 
 
273
  "followers": node_props.get("followers", 1)
274
  }
275
  elif label == "Model":
276
+ licens_ =str(node_props.get("license", t("node.unknown", "fr"))).strip("[]")
277
  if licens_ =="\'other\'" or pd.isna(licens_) or licens_ =="nan":
278
+ licens_ = t("node.other", "fr")
279
 
280
  tache = node_props.get("task", "")
281
  if tache =="unknown":
282
+ tache = t("node.unknown", "fr")
283
  return {
284
  **base_data,
285
  "label": "Modèle",
application_neo4j/static/js/script.js CHANGED
@@ -187,8 +187,11 @@ document.addEventListener("DOMContentLoaded", () => {
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": [
@@ -227,12 +230,12 @@ document.addEventListener("DOMContentLoaded", () => {
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"),
 
187
  return;
188
  }
189
 
190
+ const datatablesLangUrl = (window.__I18N_LANG === 'en')
191
+ ? "https://cdn.datatables.net/plug-ins/1.13.4/i18n/en.json"
192
+ : "https://cdn.datatables.net/plug-ins/1.13.4/i18n/fr-FR.json";
193
  const common_dt_options = {
194
+ "language": { "url": datatablesLangUrl },
195
  "pageLength": 10,
196
  "responsive": true,"scrollX": true , "scrollY":true,
197
  "columnDefs": [
 
230
  node.id || "N/A",
231
  node.author || "Inconnu",
232
  // La conversion en string via la condition est parfaite ici
233
+ String(node.downloads ?? (window.__I18N['js.unknown'] || "Inconnu")),
234
+ node.task || (window.__I18N['js.unknown_f'] || "Inconnue"),
235
  String(node.likes ?? "0"),
236
+ String(node.createdAt ?? (window.__I18N['js.unknown_f'] || "Inconnue")),
237
+ node.dataset || (window.__I18N['js.unknown'] || "Inconnu"),
238
+ node.license || (window.__I18N['js.unknown_f'] || "Inconnue"),
239
  distance > 0 ? `+${distance}` : String(distance ?? 0),
240
  String(node.ascendantsCount ?? "0"),
241
  String(node.descendantsCount ?? "0"),
application_neo4j/static/js/script_dataset.js CHANGED
@@ -1,39 +1,43 @@
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
 
@@ -42,7 +46,7 @@ const edgeInfos_dataset = {
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
 
@@ -50,15 +54,15 @@ const edgeInfos_dataset = {
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
 
@@ -199,8 +203,11 @@ document.addEventListener("DOMContentLoaded", () => {
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": [
@@ -236,12 +243,12 @@ document.addEventListener("DOMContentLoaded", () => {
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")
 
1
  const edgeInfos_dataset = {
2
  "Fait partie de cette organisation": {
3
+ color: "#a05195",
4
+ tooltip_fr: "Cette personne est membre de cette organisation",
5
+ tooltip_en: "This person is a member of this organization"
6
  },
7
  "A publié": {
8
+ color: "#003f5c",
9
+ tooltip_fr: "Un auteur (personne ou organisation) a publié un modèle",
10
+ tooltip_en: "An author (person or organization) published a model"
11
  },
12
  "A été utilisé dans ce modèle": {
13
+ color: "#f50f0f",
14
+ tooltip_fr: "Ce dataset a servi pour l'entraînement de ce modèle",
15
+ tooltip_en: "This dataset was used for training this model"
16
  },
17
  "A généré": {
18
+ color: "#8f7340",
19
+ tooltip_fr: "Le modèle source a été téléchargé et modifié afin de créer le modèle cible (type de transformation inconnu)",
20
+ tooltip_en: "The source model was downloaded and modified to create the target model (transformation type unknown)"
21
  },
22
  "finetune": {
23
+ color: "#d9c00d",
24
+ tooltip_fr: "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.",
25
+ tooltip_en: "Fine-tuning: the source model is retrained on a specific dataset to perform well on a precise task."
26
  },
27
  "adapter": {
28
+ color: "#9af17c",
29
+ tooltip_fr: "Adaptation : méthode d'ajustement qui peut être utilisée avec peu de ressources de calcul.",
30
+ tooltip_en: "Adaptation: a fine-tuning method that can be used with limited computing resources."
31
  },
32
  "quantized": {
33
+ color: "#1bc3c6",
34
+ tooltip_fr: "Quantisation : la précision des poids du modèle source est réduite afin de diminuer son empreinte en mémoire.",
35
+ tooltip_en: "Quantization: the precision of the source model's weights is reduced to decrease its memory footprint."
36
  },
37
  "merge": {
38
+ color: "#e407e4",
39
+ tooltip_fr: "Fusion : méthode visant à mélanger des couches de différents modèles pour améliorer leur performance.",
40
+ tooltip_en: "Merge: a method aiming to mix layers of different models to improve their performance."
41
  }
42
  };
43
 
 
46
  const legendContainer = document.getElementById("legend-edges");
47
  legendContainer.innerHTML = ""; // reset
48
 
49
+ Object.entries(edgeInfos_dataset).forEach(([relation, {color, tooltip_fr, tooltip_en}]) => {
50
  const li = document.createElement("li");
51
  li.className = "list-group-item d-flex align-items-center";
52
 
 
54
  li.setAttribute("data-bs-toggle", "tooltip");
55
  li.setAttribute("data-bs-placement", "top");
56
  li.setAttribute("data-bs-html", "true");
57
+ li.setAttribute("title", I18N_LANG === 'en' ? (tooltip_en || tooltip_fr) : (tooltip_fr || tooltip_en || ""));
58
+
59
  const span = document.createElement("span");
60
  span.className = "legend-color edge me-2";
61
  span.style.backgroundColor = color;
62
+
63
  li.appendChild(span);
64
+ li.appendChild(document.createTextNode(edgeDisplayName(relation)));
65
+
66
  legendContainer.appendChild(li);
67
  });
68
 
 
203
  return;
204
  }
205
 
206
+ const datatablesLangUrl = (window.__I18N_LANG === 'en')
207
+ ? "https://cdn.datatables.net/plug-ins/1.13.4/i18n/en.json"
208
+ : "https://cdn.datatables.net/plug-ins/1.13.4/i18n/fr-FR.json";
209
  const common_dt_options = {
210
+ "language": { "url": datatablesLangUrl },
211
  "pageLength": 10,
212
  "responsive": true,"scrollX": true , "scrollY":true,
213
  "columnDefs": [
 
243
  node.id || "N/A",
244
  node.author || "Inconnu",
245
  // La conversion en string via la condition est parfaite ici
246
+ String(node.downloads ?? (window.__I18N['js.unknown'] || "Inconnu")),
247
+ node.task || (window.__I18N['js.unknown_f'] || "Inconnue"),
248
  String(node.likes ?? "0"),
249
+ String(node.createdAt ?? (window.__I18N['js.unknown_f'] || "Inconnue")),
250
+ node.dataset || (window.__I18N['js.unknown'] || "Inconnu"),
251
+ node.license || (window.__I18N['js.unknown_f'] || "Inconnue"),
252
  String(node.ascendantsCount ?? "0"),
253
  String(node.descendantsCount ?? "0"),
254
  String(node.citationCount ?? "0")
application_neo4j/static/js/script_expert.js CHANGED
@@ -6,7 +6,7 @@ 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');
@@ -18,7 +18,7 @@ function buildEdgeFilters() {
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);
@@ -34,16 +34,19 @@ function populateGraphModelsTable(graphData, table) {
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"),
@@ -204,8 +207,11 @@ function initializeSigmaGraph(graphData) {
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
 
 
6
  const container = document.getElementById('edge-filters-container');
7
  if (!container) return;
8
  container.innerHTML = '';
9
+ Object.entries(edgeInfos).forEach(([key, { color }]) => {
10
  const div = document.createElement('div');
11
  div.className = 'form-check form-switch';
12
  const input = document.createElement('input');
 
18
  const label = document.createElement('label');
19
  label.className = 'form-check-label';
20
  label.htmlFor = input.id;
21
+ label.textContent = edgeDisplayName(key);
22
  div.appendChild(input);
23
  div.appendChild(label);
24
  container.appendChild(div);
 
34
 
35
 
36
  // Construction de la ligne avec les nouvelles données
37
+ const unknown = window.__I18N['js.unknown'] || "Inconnu";
38
+ const unknownF = window.__I18N['js.unknown_f'] || "Inconnue";
39
+ const na = window.__I18N['js.na'] || "N/A";
40
  const rowData = [
41
+ node.id || na,
42
+ node.author || unknown,
43
  // La conversion en string via la condition est parfaite ici
44
+ String(node.downloads ?? unknown),
45
+ node.task || unknownF,
46
  String(node.likes ?? "0"),
47
+ String(node.createdAt ?? unknownF),
48
+ node.dataset || unknown,
49
+ node.license || unknownF,
50
  distance > 0 ? `+${distance}` : String(distance ?? 0),
51
  String(node.ascendantsCount ?? "0"),
52
  String(node.descendantsCount ?? "0"),
 
207
  buildEdgeLegend();
208
  buildEdgeFilters();
209
 
210
+ const datatablesLangUrl = (window.__I18N_LANG === 'en')
211
+ ? "https://cdn.datatables.net/plug-ins/1.13.4/i18n/en.json"
212
+ : "https://cdn.datatables.net/plug-ins/1.13.4/i18n/fr-FR.json";
213
  const graphModelsTable = $('#graph-models-table').DataTable({
214
+ language: { "url": datatablesLangUrl },
215
  pageLength: 5, responsive: true, scrollX: true
216
  });
217
 
application_neo4j/static/js/utils.js CHANGED
@@ -1,45 +1,99 @@
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
 
@@ -49,8 +103,8 @@ const edgeInfos = {
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";
@@ -59,18 +113,19 @@ const edgeInfos = {
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));
@@ -97,19 +152,30 @@ function showNodeInfo(attr) {
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 ---
@@ -126,7 +192,8 @@ function showNodeInfo(attr) {
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
 
@@ -143,17 +210,18 @@ function showNodeInfo(attr) {
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
 
 
1
  // --- FONCTIONS UTILITAIRES GLOBALES ---
2
  // Ces fonctions sont utilisées à plusieurs endroits et sont donc définies en premier.
3
+
4
+ // ── I18N runtime (peut être surchargé par le template) ──
5
+ const I18N = window.__I18N_DATA || {};
6
+ const I18N_LANG = window.__I18N_LANG || 'fr';
7
+
8
+ function _(key) {
9
+ return I18N[key] !== undefined ? I18N[key] : key;
10
+ }
11
+
12
+ // ── Edge labels translations (Neo4j relation name → display name) ──
13
+ const EDGE_LABEL_MAP = {
14
+ fr: {
15
+ "POSTED": "A publié",
16
+ "IS_IN": "Fait partie de cette organisation",
17
+ "USED_IN": "A été utilisé dans ce modèle",
18
+ "A publié": "A publié",
19
+ "Fait partie de cette organisation": "Fait partie de cette organisation",
20
+ "A été utilisé dans ce modèle": "A été utilisé dans ce modèle",
21
+ "A généré": "A généré",
22
+ "finetune": "Finetune",
23
+ "adapter": "Adapter",
24
+ "quantized": "Quantized",
25
+ "merge": "Merge",
26
+ "other": "Autre",
27
+ "unknown": "Autre",
28
+ },
29
+ en: {
30
+ "POSTED": "Published",
31
+ "IS_IN": "Member of this organization",
32
+ "USED_IN": "Used in this model",
33
+ "A publié": "Published",
34
+ "Fait partie de cette organisation": "Member of this organization",
35
+ "A été utilisé dans ce modèle": "Used in this model",
36
+ "A généré": "Generated",
37
+ "finetune": "Fine-tuned",
38
+ "adapter": "Adapted",
39
+ "quantized": "Quantized",
40
+ "merge": "Merged",
41
+ "other": "Other",
42
+ "unknown": "Other",
43
+ },
44
+ };
45
+
46
+ function edgeDisplayName(relation) {
47
+ const map = EDGE_LABEL_MAP[I18N_LANG] || EDGE_LABEL_MAP.fr;
48
+ return map[relation] || relation;
49
+ }
50
+
51
  // Mapping relations → {couleur, tooltip}
52
  const edgeInfos = {
53
  "Fait partie de cette organisation": {
54
+ color: "#a05195",
55
+ tooltip_fr: "Cette personne est membre de cette organisation",
56
+ tooltip_en: "This person is a member of this organization"
57
  },
58
  "A publié": {
59
+ color: "#003f5c",
60
+ tooltip_fr: "Un auteur (personne ou organisation) a publié un modèle",
61
+ tooltip_en: "An author (person or organization) published a model"
62
  },
63
  "A généré": {
64
+ color: "#f50f0f",
65
+ tooltip_fr: "Le modèle source a été téléchargé et modifié afin de créer le modèle cible (type de transformation inconnu)",
66
+ tooltip_en: "The source model was downloaded and modified to create the target model (transformation type unknown)"
67
  },
68
  "finetune": {
69
+ color: "#cd6700",
70
+ tooltip_fr: "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.",
71
+ tooltip_en: "Fine-tuning: the source model is retrained on a specific dataset to perform well on a precise task."
72
  },
73
  "adapter": {
74
+ color: "#238a00",
75
+ tooltip_fr: "Adaptation : méthode d'ajustement qui peut être utilisée avec peu de ressources de calcul.",
76
+ tooltip_en: "Adaptation: a fine-tuning method that can be used with limited computing resources."
77
  },
78
  "quantized": {
79
+ color: "#009194",
80
+ tooltip_fr: "Quantisation : la précision des poids du modèle source est réduite afin de diminuer son empreinte en mémoire.",
81
+ tooltip_en: "Quantization: the precision of the source model's weights is reduced to decrease its memory footprint."
82
  },
83
  "merge": {
84
+ color: "#c29bc2",
85
+ tooltip_fr: "Fusion : méthode visant à mélanger des couches de différents modèles pour améliorer leur performance.",
86
+ tooltip_en: "Merge: a method aiming to mix layers of different models to improve their performance."
87
  },
88
  "other": {
89
+ color: "#8f7340",
90
+ tooltip_fr: "Autre type de relation",
91
+ tooltip_en: "Other type of relation"
92
+ },
93
+ "unknown": {
94
+ color: "#8f7340",
95
+ tooltip_fr: "Autre type de relation",
96
+ tooltip_en: "Other type of relation"
97
  }
98
  };
99
 
 
103
  function buildEdgeLegend() {
104
  const legendContainer = document.getElementById("legend-edges");
105
  legendContainer.innerHTML = ""; // reset
106
+
107
+ Object.entries(edgeInfos).forEach(([relation, {color, tooltip_fr, tooltip_en}]) => {
108
  if (relation == "unknown") return;
109
  const li = document.createElement("li");
110
  li.className = "list-group-item d-flex align-items-center";
 
113
  li.setAttribute("data-bs-toggle", "tooltip");
114
  li.setAttribute("data-bs-placement", "top");
115
  li.setAttribute("data-bs-html", "true");
116
+ const tooltipKey = I18N_LANG === 'en' ? 'tooltip_en' : 'tooltip_fr';
117
+ li.setAttribute("title", tooltip_fr !== undefined ? (I18N_LANG === 'en' ? tooltip_en : tooltip_fr) : (tooltip_en || tooltip_fr || ""));
118
+
119
  const span = document.createElement("span");
120
  span.className = "legend-color edge me-2";
121
  span.style.backgroundColor = color;
122
+
123
  li.appendChild(span);
124
+ li.appendChild(document.createTextNode(edgeDisplayName(relation)));
125
+
126
  legendContainer.appendChild(li);
127
  });
128
+
129
  // nécessaire pour activer les tooltips Bootstrap dynamiques
130
  const tooltipTriggerList = [].slice.call(legendContainer.querySelectorAll('[data-bs-toggle="tooltip"]'))
131
  tooltipTriggerList.map(el => new bootstrap.Tooltip(el));
 
152
  infoCard.dataset.nodeId = attr.id;
153
 
154
  // --- ÉTAPE 2: Construction des détails ---
155
+ const labels = {
156
+ name: I18N['js.node_info.name'] || "Nom :",
157
+ type: I18N['js.node_info.type'] || "Type :",
158
+ followers: I18N['js.node_info.followers'] || "Abonnés :",
159
+ downloads: I18N['js.node_info.downloads'] || "Téléchargements :",
160
+ created: I18N['js.node_info.created'] || "Créé le :",
161
+ task: I18N['js.node_info.task'] || "Tâche :",
162
+ dataset: I18N['js.node_info.dataset'] || "Dataset utilisé :",
163
+ undefined: I18N['js.node_info.undefined'] || "Non défini",
164
+ };
165
+
166
+ let infosHtml = `<p><strong>${labels.name}</strong> ${attr.id || labels.undefined}</p><p><strong>${labels.type}</strong> ${attr.dataCat}</p>`;
167
 
168
  // ... (Le reste de la construction de infosHtml ne change pas) ...
169
  if (["personne", "organisation", "Author"].includes(attr.dataCat)) {
170
+ if (attr.followers) infosHtml += `<p><strong>${labels.followers}</strong> ${formatNumberShort(attr.followers)}</p>`;
171
  } else if (["Modèle", "Model"].includes(attr.dataCat)) {
172
+ if (attr.downloads) infosHtml += `<p><strong>${labels.downloads}</strong> ${formatNumberShort(attr.downloads)}</p>`;
173
+ if (attr.createdAt) infosHtml += `<p><strong>${labels.created}</strong> ${formatDateFr(attr.createdAt)}</p>`;
174
+ if (attr.task) infosHtml += `<p><strong>${labels.task}</strong> ${attr.task}</p>`;
175
+ if (attr.dataset) infosHtml += `<p><strong>${labels.dataset}</strong> ${attr.dataset}</p>`;
176
  } else if (attr.dataCat === "Dataset") {
177
+ if (attr.downloads) infosHtml += `<p><strong>${labels.downloads}</strong> ${formatNumberShort(attr.downloads)}</p>`;
178
+ if (attr.createdAt_dataset) infosHtml += `<p><strong>${labels.created}</strong> ${formatDateFr(attr.createdAt_dataset)}</p>`;
179
  }
180
 
181
  // --- ÉTAPE 3: Création du lien ---
 
192
  link.target = '_blank';
193
  link.rel = 'noopener noreferrer';
194
  link.className = 'stretched-link';
195
+ const seeLabel = (I18N['js.node_info.see_on_hf'] || "Voir {name} sur Hugging Face").replace('{name}', attr.id);
196
+ link.setAttribute('aria-label', seeLabel);
197
  cardBody.appendChild(link);
198
  }
199
 
 
210
 
211
 
212
  /**
213
+ * Formate une chaîne de date ISO.
214
  * @param {string} dateString - La date à formater.
215
+ * @returns {string} La date formatée ou "Date inconnue" / "Unknown date".
216
  */
217
  function formatDateFr(dateString) {
218
+ if (!dateString || dateString === "inconnue") return I18N['js.node_info.unknown_date'] || "Date inconnue";
219
  try {
220
+ const locale = I18N_LANG === 'en' ? 'en-GB' : 'fr-FR';
221
  const options = { year: 'numeric', month: 'long', day: 'numeric' };
222
+ return new Date(dateString).toLocaleDateString(locale, options);
223
  } catch (e) {
224
+ return I18N['js.node_info.unknown_date'] || "Date inconnue";
225
  }
226
  }
227
 
application_neo4j/templates/expert.html CHANGED
@@ -1,9 +1,9 @@
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">
@@ -21,17 +21,21 @@
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">
@@ -42,44 +46,44 @@
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>
@@ -89,7 +93,7 @@
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>
@@ -97,7 +101,7 @@
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 %}
@@ -106,7 +110,7 @@
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">
@@ -118,7 +122,7 @@
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"
@@ -131,27 +135,27 @@
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>
@@ -163,7 +167,7 @@
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>
@@ -171,27 +175,27 @@
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">
@@ -199,23 +203,23 @@
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>
@@ -231,7 +235,7 @@
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
 
@@ -242,6 +246,10 @@
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 () {
@@ -250,4 +258,4 @@
250
  });
251
  </script>
252
  </body>
253
- </html>
 
1
  <!DOCTYPE html>
2
+ <html lang="{{ current_lang }}">
3
  <head>
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{{ t('site.title_expert') }}</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">
 
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">{{ t('site.nav_brand_model_expert') }}</span>
25
+ <small class="d-block text-muted">{{ t('site.nav_subtitle') }}</small>
26
  </a>
27
+ <div class="d-flex gap-2">
28
+ <a href="{{ url_for('set_language_route', lang='fr') }}" class="btn btn-sm {{ 'btn-primary' if current_lang == 'fr' else 'btn-outline-primary' }}">{{ t('lang.fr') }}</a>
29
+ <a href="{{ url_for('set_language_route', lang='en') }}" class="btn btn-sm {{ 'btn-primary' if current_lang == 'en' else 'btn-outline-primary' }}">{{ t('lang.en') }}</a>
30
+ </div>
31
  </div>
32
  </header>
33
 
34
  <main class="container mt-4 flex-grow-1">
35
  <div class="row">
36
  <div class="col-12">
37
+ <h1 class="display-5">{{ t('expert.page_title') }}</h1>
38
+ <p class="lead">{{ t('expert.page_lead') }}</p>
39
  </div>
40
  </div>
41
  <div class="row g-3 mb-4">
 
46
  <div class="row g-3 align-items-end">
47
  <!-- Champ de recherche -->
48
  <div class="col-12 col-md-5">
49
+ <label for="search-input" class="form-label fw-bold">{{ t('search.label_name') }}</label>
50
  <div class="position-relative">
51
  <input type="text" name="name" id="search-input" class="form-control"
52
+ placeholder="{{ t('expert.placeholder') }}"
53
  value="{{ request.form.name or '' }}" required autocomplete="off" />
54
  <ul id="suggestions-list" class="list-group position-absolute w-100" style="display: none; z-index: 1000;"></ul>
55
  </div>
56
  </div>
57
  <!-- Filtres -->
58
  <div class="col-12 col-md-3">
59
+ <label class="form-label fw-bold">{{ t('search.label_filters') }}</label>
60
  <div class="d-flex gap-3">
61
  <div class="form-check">
62
  <input class="form-check-input" type="checkbox" name="filters" value="Model" id="filter-model"
63
  {% if 'Model' in search.filters %}checked{% endif %}>
64
+ <label class="form-check-label" for="filter-model">{{ t('search.filter_model') }}</label>
65
  </div>
66
  <div class="form-check">
67
  <input class="form-check-input" type="checkbox" name="filters" value="Dataset" id="filter-dataset"
68
  {% if 'Dataset' in search.filters %}checked{% endif %}>
69
+ <label class="form-check-label" for="filter-dataset">{{ t('search.filter_dataset') }}</label>
70
  </div>
71
  <div class="form-check">
72
  <input class="form-check-input" type="checkbox" name="filters" value="Author" id="filter-author" {% if 'Author' in search.filters %}checked{% endif %}>
73
+ <label class="form-check-label" for="filter-author">{{ t('expert.filter_author') }}</label>
74
  </div>
75
  </div>
76
  </div>
77
  <!-- Profondeur de recherche -->
78
  <div class="col-12 col-md-4">
79
+ <label class="form-label fw-bold">{{ t('search.label_depth') }}</label>
80
  <div class="d-flex align-items-center gap-3">
81
  <div class="form-check form-switch">
82
  <input class="form-check-input" type="checkbox" id="depth-unlimited" name="depth_unlimited" checked>
83
+ <label class="form-check-label" for="depth-unlimited">{{ t('search.depth_unlimited') }}</label>
84
  </div>
85
  <div class="input-group input-group-sm">
86
+ <span class="input-group-text">{{ t('search.depth_limited') }}</span>
87
  <input class="form-control" type="number" name="depth" id="depth"
88
  value="{{ request.form.depth }}" min="1" max="5" disabled>
89
  </div>
 
93
  <div class="row mt-4">
94
  <div class="col-12">
95
  <button type="submit" name="submit" value="findnode_expert" class="btn btn-primary w-100">
96
+ <i class="bi bi-search me-1"></i>{{ t('search.btn_search_simple') }}
97
  </button>
98
  </div>
99
  </div>
 
101
  </div>
102
  </div>
103
 
104
+
105
  {% if message %}
106
  <div class="alert alert-info mt-3"><p class="mb-0">{{ message }}</p></div>
107
  {% endif %}
 
110
  {% if graph_data and graph_data.nodes %}
111
  <div id="genealogy-graph-section" class="mt-4">
112
  <div class="row g-4">
113
+ <div class="card-header"><h5 class="card-title mb-0">{{ t('expert.connected_component') }}</h5></div>
114
  <!-- Colonne de gauche pour le graphe et la carte d'info -->
115
  <div class="col-12 col-lg-8">
116
  <div class="card position-relative">
 
122
  class="card p-2 position-absolute top-0 start-0 shadow-sm"
123
  style="width: 220px; background-color: rgba(255,255,255,0.95);">
124
  <div class="d-flex justify-content-between align-items-center mb-2">
125
+ <strong>{{ t('expert.legend_title') }}</strong>
126
  <button class="btn btn-sm btn-light p-0"
127
  type="button"
128
  data-bs-toggle="collapse"
 
135
  <!-- Contenu de la légende -->
136
  <div id="legend-content" class="collapse show">
137
  <div class="card mb-2">
138
+ <div class="card-header py-1"><h6 class="card-title mb-0">{{ t('expert.legend_nodes_title') }}</h6></div>
139
  <div class="card-body p-2">
140
  <ul id="legend-nodes" class="list-group list-group-flush small">
141
  <li class="list-group-item d-flex align-items-center py-1">
142
+ <span class="legend-color me-2" style="background-color: hsl(45, 65%, 52%);"></span>{{ t('expert.legend_dataset') }}
143
  </li>
144
  <li class="list-group-item d-flex align-items-center py-1">
145
+ <span class="legend-color me-2" style="background-color: #007bff;"></span>{{ t('expert.legend_person') }}
146
  </li>
147
  <li class="list-group-item d-flex align-items-center py-1">
148
+ <span class="legend-color me-2" style="background-color: #092d53;"></span>{{ t('expert.legend_org') }}
149
  </li>
150
  <li class="list-group-item d-flex align-items-center py-1">
151
+ <span class="legend-color me-2" style="background-color:#7D7D7D;"></span>{{ t('expert.legend_model') }}
152
  </li>
153
  </ul>
154
  </div>
155
  </div>
156
 
157
  <div class="card">
158
+ <div class="card-header py-1"><h6 class="card-title mb-0">{{ t('expert.legend_edges_title') }}</h6></div>
159
  <div class="card-body p-2">
160
  <ul id="legend-edges" class="list-group list-group-flush small">
161
  </ul>
 
167
  </div>
168
  <div id="node-info-card" class="card mt-3 card-interactive" style="display: none;">
169
  <div class="card-body">
170
+ <h5 class="card-title">{{ t('search.node_info_title') }}</h5>
171
  <div id="node-details" class="mt-2"></div>
172
  </div>
173
  </div>
 
175
  <div class="col-12 col-lg-4">
176
  <!-- CORRECTION : Chaque section est dans sa propre carte -->
177
  <div class="card mb-3">
178
+ <div class="card-header"><h5 class="card-title mb-0">{{ t('expert.graph_filters') }}</h5></div>
179
  <div class="card-body" id="graph-filters-container">
180
+ <h6>{{ t('expert.node_types') }}</h6>
181
  <div class="form-check form-switch">
182
  <input class="form-check-input" type="checkbox" id="filter-node-model" value="Modèle" checked>
183
+ <label class="form-check-label" for="filter-node-model">{{ t('expert.model_label') }}</label>
184
  </div>
185
  <div class="form-check form-switch">
186
  <input class="form-check-input" type="checkbox" id="filter-node-person" value="personne" checked>
187
+ <label class="form-check-label" for="filter-node-person">{{ t('expert.person_label') }}</label>
188
  </div>
189
  <div class="form-check form-switch">
190
  <input class="form-check-input" type="checkbox" id="filter-node-org" value="organisation" checked>
191
+ <label class="form-check-label" for="filter-node-org">{{ t('expert.org_label') }}</label>
192
  </div>
193
  <hr>
194
+ <h6>{{ t('expert.rel_types') }}</h6>
195
  <div id="edge-filters-container"></div>
196
  </div>
197
  </div>
198
+ <button id="export-btn" class="btn-modern">{{ t('expert.btn_export') }}</button>
199
  </div>
200
  </div>
201
  <hr class="my-5">
 
203
  <!-- CORRECTION : Tableau des modèles présents dans le graphe -->
204
  <div class="row mt-4">
205
  <div class="col-12">
206
+ <h4 class="h4">{{ t('expert.models_in_graph') }}</h4>
207
  <div class="table-responsive">
208
  <table id="graph-models-table" class="table table-bordered table-striped table-hover" style="width:100%">
209
  <thead>
210
  <tr>
211
+ <th scope="col">{{ t('search.table_model') }}</th>
212
+ <th scope="col">{{ t('search.table_author') }}</th>
213
+ <th scope="col">{{ t('search.table_downloads') }}</th>
214
+ <th scope="col">{{ t('search.table_task') }}</th>
215
+ <th scope="col">{{ t('search.table_likes') }}</th>
216
+ <th scope="col">{{ t('search.table_date') }}</th>
217
+ <th scope="col">{{ t('search.table_dataset') }}</th>
218
+ <th scope="col">{{ t('search.table_license') }}</th>
219
+ <th scope="col">{{ t('search.table_distance') }}</th>
220
+ <th scope="col">{{ t('search.table_ascendants') }}</th>
221
+ <th scope="col">{{ t('search.table_descendants') }}</th>
222
+ <th scope="col">{{ t('search.table_citations') }}</th>
223
  </tr>
224
  </thead>
225
  <tbody>
 
235
 
236
  <footer class="bg-dark text-white text-center p-4 mt-auto">
237
  <div class="container">
238
+ <p class="mb-0">{{ t('site.footer_expert') }}</p>
239
  </div>
240
  </footer>
241
 
 
246
  <script src="https://unpkg.com/graphology@0.25.1/dist/graphology.umd.min.js"></script>
247
  <script src="https://cdn.jsdelivr.net/npm/graphology-library/dist/graphology-library.min.js"></script>
248
  <script src="{{ url_for('static', filename='js/utils.js') }}"></script>
249
+ <script>
250
+ window.__I18N_DATA = {{ js_i18n_data | tojson }};
251
+ window.__I18N_LANG = "{{ current_lang }}";
252
+ </script>
253
  <script src="{{ url_for('static', filename='js/script_expert.js') }}"></script>
254
  <script>
255
  document.addEventListener('DOMContentLoaded', function () {
 
258
  });
259
  </script>
260
  </body>
261
+ </html>
application_neo4j/templates/index.html CHANGED
@@ -1,9 +1,9 @@
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
 
@@ -95,59 +95,69 @@
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('static', filename='notice/notice.pdf') }}" class="btn-modern" target="_blank">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>
@@ -155,40 +165,40 @@
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
- <p><br/> <br/> <br/>Date de téléchargement de la base de donnée : 01/09/2025</p>
171
  </div>
172
  </div>
173
  <div class="welcome-container">
174
- <h2>Mentions d'information sur les traitements de données à caractère personnel</h2>
175
 
176
- <p>Afin d’étudier le développement de la communauté de l’IA open source, et de préparer la possibilité d’exercices de droits des citoyens, le projet vise à étudier la base de données des jeux de données et modèles présents sur la plateforme HuggingFace. Cette base de données permet d’établir un arbre généalogique des modèles.</p>
177
 
178
- <p>Les données traitées sont le pseudonyme de l’auteur (quand il apparaît dans les métadonnées), le nom du modèle/jeu de données et plusieurs informations inhérentes à ce modèle/jeu de données telles que la date de publication, la licence utilisée ou encore le nombre de téléchargements.</p>
179
 
180
- <p>Ce projet relève de la mission d’intérêt public dont est investie la CNIL en application du règlement général sur la protection des données et de la loi Informatique et Libertés modifiée (article 8).</p>
181
 
182
- <p>Les données sont publiées à partir de l’espace CNIL sur HuggingFace.</p>
183
 
184
- <p>Une première phase d’évaluation de l’utilité de l’outil interviendra après 6 mois de publication. A cette échéance, l’outil pourra être modifié et le projet reconduit.</p>
185
 
186
- <p>Vous pouvez accéder et obtenir une copie de vos données, vous opposer au traitement de ces données, les faire rectifier ou effacer. Vous disposez également du droit de limiter le traitement de vos données.</p>
187
 
188
- <p>Vous pouvez exercer vos droits ou poser vos questions sur ce projet en contactant le service IA de la CNIL : <a href="mailto:ia@cnil.fr">ia@cnil.fr</a>.</p>
189
 
190
- <p>Si vous estimez, après nous avoir contactés, que vos droits « Informatique et Libertés » ne sont pas respectés, vous pouvez contacter le DPO de la CNIL ou adresser une réclamation à la CNIL.</p>
191
 
192
  </div>
193
  </body>
194
- </html>
 
1
  <!DOCTYPE html>
2
+ <html lang="{{ current_lang }}">
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{{ t('site.title_home') }}</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
 
 
95
  margin-top: 20px;
96
  }
97
 
98
+ /* Language selector */
99
+ .lang-switch {
100
+ position: absolute;
101
+ top: 15px;
102
+ right: 20px;
103
+ display: flex;
104
+ gap: 5px;
105
+ }
106
+ .lang-switch a {
107
+ padding: 4px 10px;
108
+ border-radius: 4px;
109
+ text-decoration: none;
110
+ font-size: 0.85rem;
111
+ font-weight: bold;
112
+ color: #555;
113
+ background: #eee;
114
+ }
115
+ .lang-switch a.active {
116
+ background: #3498db;
117
+ color: #fff;
118
+ }
119
 
120
  </style>
121
  </head>
122
 
123
  <body>
124
+ <!-- Language selector -->
125
+ <div class="lang-switch">
126
+ <a href="{{ url_for('set_language_route', lang='fr') }}" class="{{ 'active' if current_lang == 'fr' else '' }}">{{ t('lang.fr') }}</a>
127
+ <a href="{{ url_for('set_language_route', lang='en') }}" class="{{ 'active' if current_lang == 'en' else '' }}">{{ t('lang.en') }}</a>
128
+ </div>
129
+
130
  <div class="welcome-container">
131
+ <h1>{{ t('home.welcome_title') }}</h1>
132
+ <a href="{{ url_for('static', filename='notice/notice.pdf') }}" class="btn-modern" target="_blank">{{ t('home.more_info') }}</a>
133
  </div>
134
  <div class="container content-section">
135
  <div class="row g-4">
136
  <div class="col-md-6">
137
  <div class="content-card">
138
+ <h4>{{ t('home.what_is_model') }}</h4>
139
  <p>
140
+ {{ t('home.what_is_model_text') | safe }}
 
 
 
 
141
  </p>
142
  </div>
143
  </div>
144
  <div class="col-md-6">
145
  <div class="content-card">
146
+ <h4>{{ t('home.what_is_dataset') }}</h4>
147
  <p>
148
+ {{ t('home.what_is_dataset_text') | safe }}
 
 
 
 
 
149
  </p>
150
  </div>
151
  </div>
152
  <div class="col-md-6">
153
  <div class="content-card">
154
+ <h4>{{ t('home.purpose') }}</h4>
155
  <p>
156
+ {{ t('home.purpose_text') | safe }}
 
 
157
 
158
+ <h6>{{ t('home.alice_title') }}</h6>
159
  <span class="highlight">
160
+ {{ t('home.alice_text') | safe }}
 
 
 
 
 
 
161
  </span>
162
  </p>
163
  </div>
 
165
 
166
  <div class="col-md-6">
167
  <div class="content-card">
168
+ <h4>{{ t('home.find_label') }}</h4>
169
  <div class="buttons-container">
170
  <!-- Ce bouton mène à la page de recherche -->
171
+ <a href="{{ url_for('findnode', filter='Model') }}" class="btn-modern">{{ t('home.btn_model') }}</a>
172
+ <a href="{{ url_for('findnode', filter='Dataset') }}" class="btn-modern">{{ t('home.btn_dataset') }}</a>
173
+ <a href="{{ url_for('findnode') }}" class="btn-modern">{{ t('home.btn_unsure') }}"><br><br>
174
+ <h4>{{ t('home.expert_label') }}</h4>
175
  <div class="buttons-container">
176
  <!-- Ce bouton mène à la page de recherche -->
177
+ <a href="{{ url_for('findnode_expert', filter='Model') }}" class="btn-modern">{{ t('home.btn_expert') }}</a>
178
  </div>
179
  </div>
180
+ <p><br/> <br/> <br/>{{ t('home.download_date') }}</p>
181
  </div>
182
  </div>
183
  <div class="welcome-container">
184
+ <h2>{{ t('home.notice_title') }}</h2>
185
 
186
+ <p>{{ t('home.notice_p1') }}</p>
187
 
188
+ <p>{{ t('home.notice_p2') }}</p>
189
 
190
+ <p>{{ t('home.notice_p3') }}</p>
191
 
192
+ <p>{{ t('home.notice_p4') }}</p>
193
 
194
+ <p>{{ t('home.notice_p5') }}</p>
195
 
196
+ <p>{{ t('home.notice_p6') }}</p>
197
 
198
+ <p>{{ t('home.notice_p7') | safe }}</p>
199
 
200
+ <p>{{ t('home.notice_p8') }}</p>
201
 
202
  </div>
203
  </body>
204
+ </html>
application_neo4j/templates/search.html CHANGED
@@ -1,9 +1,9 @@
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">
@@ -36,9 +36,13 @@
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 (base de données actualisée le 01/09/2025)</small>
41
  </a>
 
 
 
 
42
  </div>
43
  </header>
44
 
@@ -46,8 +50,8 @@
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
 
@@ -59,10 +63,10 @@
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 ou l'id de son repo Hugging Face."
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>
@@ -70,31 +74,31 @@
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>
@@ -106,7 +110,7 @@
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 (le chargement peut prendre jusqu'à une minute pour les grandes généalogies)
110
  </button>
111
  </div>
112
  </div>
@@ -127,19 +131,19 @@
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 ">
@@ -161,20 +165,20 @@
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>
@@ -192,7 +196,7 @@
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">
@@ -204,46 +208,46 @@
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>
@@ -256,12 +260,12 @@
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>
@@ -293,26 +297,26 @@
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>
@@ -325,23 +329,23 @@
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>
@@ -349,23 +353,23 @@
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>
@@ -378,30 +382,30 @@
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>
@@ -415,7 +419,7 @@
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
 
@@ -426,6 +430,10 @@
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 () {
@@ -434,4 +442,4 @@
434
  });
435
  </script>
436
  </body>
437
- </html>
 
1
  <!DOCTYPE html>
2
+ <html lang="{{ current_lang }}">
3
  <head>
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{{ t('site.title_search') }}</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">
 
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">{{ t('site.nav_brand') }}</span>
40
+ <small class="d-block text-muted">{{ t('site.nav_subtitle_full') }}</small>
41
  </a>
42
+ <div class="d-flex gap-2">
43
+ <a href="{{ url_for('set_language_route', lang='fr') }}" class="btn btn-sm {{ 'btn-primary' if current_lang == 'fr' else 'btn-outline-primary' }}">{{ t('lang.fr') }}</a>
44
+ <a href="{{ url_for('set_language_route', lang='en') }}" class="btn btn-sm {{ 'btn-primary' if current_lang == 'en' else 'btn-outline-primary' }}">{{ t('lang.en') }}</a>
45
+ </div>
46
  </div>
47
  </header>
48
 
 
50
  <!-- Titre principal et Formulaire (inchangés) -->
51
  <div class="row">
52
  <div class="col-12">
53
+ <h1 class="display-5">{{ t('search.page_title') }}</h1>
54
+ <p class="lead">{{ t('search.page_lead_model') }}</p>
55
  </div>
56
  </div>
57
 
 
63
  <div class="row g-3 align-items-end">
64
  <!-- Champ de recherche -->
65
  <div class="col-12 col-md-5">
66
+ <label for="search-input" class="form-label fw-bold">{{ t('search.label_name') }}</label>
67
  <div class="position-relative">
68
  <input type="text" name="name" id="search-input" class="form-control"
69
+ placeholder="{{ t('search.placeholder_model') }}"
70
  value="{{ request.form.name or '' }}" required autocomplete="off" />
71
  <ul id="suggestions-list" class="list-group position-absolute w-100" style="display: none; z-index: 1000;"></ul>
72
  </div>
 
74
 
75
  <!-- Filtres -->
76
  <div class="col-12 col-md-3">
77
+ <label class="form-label fw-bold">{{ t('search.label_filters') }}</label>
78
  <div class="d-flex gap-3">
79
  <div class="form-check">
80
  <input class="form-check-input" type="checkbox" name="filters" value="Model" id="filter-model"
81
  {% if 'Model' in search.filters %}checked{% endif %}>
82
+ <label class="form-check-label" for="filter-model">{{ t('search.filter_model') }}</label>
83
  </div>
84
  <div class="form-check">
85
  <input class="form-check-input" type="checkbox" name="filters" value="Dataset" id="filter-dataset"
86
  {% if 'Dataset' in search.filters %}checked{% endif %}>
87
+ <label class="form-check-label" for="filter-dataset">{{ t('search.filter_dataset') }}</label>
88
  </div>
89
  </div>
90
  </div>
91
 
92
  <!-- Profondeur de recherche -->
93
  <div class="col-12 col-md-4">
94
+ <label class="form-label fw-bold">{{ t('search.label_depth') }}</label>
95
  <div class="d-flex align-items-center gap-3">
96
  <div class="form-check form-switch">
97
  <input class="form-check-input" type="checkbox" id="depth-unlimited" name="depth_unlimited" checked>
98
+ <label class="form-check-label" for="depth-unlimited">{{ t('search.depth_unlimited') }}</label>
99
  </div>
100
  <div class="input-group input-group-sm">
101
+ <span class="input-group-text">{{ t('search.depth_limited') }}</span>
102
  <input class="form-control" type="number" name="depth" id="depth"
103
  value="{{ request.form.depth }}" min="1" max="5" disabled>
104
  </div>
 
110
  <div class="row mt-4">
111
  <div class="col-12">
112
  <button type="submit" name="submit" value="find_node" class="btn btn-primary w-100">
113
+ <i class="bi bi-search me-1"></i>{{ t('search.btn_search') }}
114
  </button>
115
  </div>
116
  </div>
 
131
  <!-- COLONNE ASCENDANCE -->
132
  <div class="col-12 col-lg-3">
133
  <div class="d-flex justify-content-center gap-2 mb-3">
134
+ <h4 class="h5 text-center mb-3">{{ t('search.asc_section') }}
135
  <!-- L'icône avec les attributs pour le tooltip -->
136
  <i class="bi bi-info-circle-fill text-secondary align-middle ms-2"
137
  data-bs-toggle="tooltip"
138
  data-bs-placement="right"
139
+ title="{{ t('search.asc_tooltip') }}">
140
  </i>
141
  </h4>
142
  </div>
143
  <div class="card">
144
  <div class="card-body p-2">
145
  {% if not highlights.asc_unique_models %}
146
+ <p class="text-muted small m-2">{{ t('search.no_parent') }}</p>
147
  {% else %}
148
  {% for model in highlights.asc_unique_models %}
149
  <div class="card mb-2 position-relative ">
 
165
  <!-- MODIFICATION : Ajout de la classe "text-break" pour empêcher le débordement -->
166
  <h6 class="card-title small mb-1 text-break card-interactive">
167
  <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"
168
+ title="<b>{{ t('search.info_label') }}</b>
169
+ <br>{{ t('search.info_author') }} : {{ (model.name.split('/') | first) if '/' in model.name else t('search.info_unknown') }}<br>
170
+ {{ t('search.info_citations') }} : {{ '{:,.0f}'.format((model.citation_count or 0) | int).replace(',', ' ') }}<br>
171
+ {{ t('search.info_likes') }} : {{ model.likes or t('search.info_unknown') }}<br>
172
+ {{ t('search.info_date') }} : {{ model.createdAt or 'N/A' }}<br>
173
+ {{ t('search.info_task') }} : {{ model.task or t('search.info_unknown_task') }}<br>
174
+ {{ t('search.info_license') }} : {{ model.license or t('search.info_unknown_task') }}"
175
  >
176
  {{ model.name }}
177
  </a>
178
  </h6>
179
  <p class="card-text" style="font-size: 0.75em; line-height: 1.2;">
180
+ {{ "{:,.0f}".format((model.downloads or 0) | int).replace(',', ' ') }} {{ t('search.downloads') }}<br>
181
+ {{ "{:,.0f}".format((model.citation_count or 0) | int).replace(',', ' ') }} {{ t('search.citations') }}
182
  </p>
183
  </div>
184
  </div>
 
196
 
197
  <!-- COLONNE MODÈLE RECHERCHÉ (CENTRE) (inchangée) -->
198
  <div class="col-12 col-lg-4 " id="center-column">
199
+ <h3 class="h5 text-center mb-3">{{ t('search.searched_model') }}</h3>
200
  <a href="https://huggingface.co/{{ searched_node.id }}" target="_blank" rel="noopener noreferrer" class="text-white">
201
  <div class="card card-recherche text-center border-2 ">
202
  <div class="card-header card-recherche-header text-white">
 
208
  <div class="row">
209
  <div class="col">
210
  <p class="fw-bold mb-0">{{ "{:,.0f}".format((searched_node.ascendantsCount or 0) | int).replace(',', ' ') }}</p>
211
+ <p class="small text-muted">{{ t('search.ascendants') }}
212
 
213
  <i class="bi bi-info-circle-fill ms-2"
214
  data-bs-toggle="tooltip"
215
  data-bs-placement="right"
216
+ title="{{ t('search.ascendants_tooltip') }}">
217
  </i>
218
  </p>
219
 
220
  </div>
221
  <div class="col">
222
  <p class="fw-bold mb-0">{{ "{:,.0f}".format((searched_node.descendantsCount or 0) | int).replace(',', ' ') }}</p>
223
+ <p class="small text-muted">{{ t('search.descendants') }}
224
  <i class="bi bi-info-circle-fill ms-2"
225
  data-bs-toggle="tooltip"
226
  data-bs-placement="right"
227
+ title="{{ t('search.descendants_tooltip') }}">
228
  </i>
229
  </p>
230
  </div>
231
  </div>
232
  <div class="row mt-2">
233
+ <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('search.downloads') }}</p></div>
234
  <div class="col"><p class="fw-bold mb-0">{{ "{:,.0f}".format((searched_node.citationCount or 0) | int).replace(',', ' ') }}</p>
235
+ <p class="small text-muted">{{ t('search.citations') }}
236
  <i class="bi bi-info-circle-fill ms-2"
237
  data-bs-toggle="tooltip"
238
  data-bs-placement="right"
239
+ title="{{ t('search.citations_tooltip') }}">
240
  </i>
241
  </p></div>
242
+ <div class="col"><p class="fw-bold mb-0">{{ "{:,.0f}".format((searched_node.likes or 0) | int).replace(',', ' ') }}</p><p class="small text-muted">{{ t('search.likes') }}</p></div>
243
  </div>
244
  <div class="mt-2">
245
+ {% if searched_node.createdAt %}<span class="badge bg-light text-dark">{{ t('search.published') }} {{ searched_node.createdAt }}</span>{% endif %}
246
+ {% if searched_node.task %}<span class="badge bg-light text-dark">{{ t('search.task') }} {{ searched_node.task }}</span>{% endif %}
247
  </div>
248
  <hr>
249
  </a>
250
+ <button id="show-graph-btn" class="btn btn-secondary"><i class="bi bi-diagram-3 me-1"></i>{{ t('search.btn_graph') }}</button>
251
  </div>
252
  </div>
253
  </div>
 
260
  <!-- COLONNE DESCENDANCE (DROITE) -->
261
  <div class="col-12 col-lg-3">
262
  <div class="d-flex justify-content-center gap-2 mb-3">
263
+ <h4 class="h5 text-center mb-3">{{ t('search.desc_section') }}
264
  <!-- L'icône avec les attributs pour le tooltip -->
265
  <i class="bi bi-info-circle-fill text-secondary align-middle ms-2"
266
  data-bs-toggle="tooltip"
267
  data-bs-placement="right"
268
+ title="{{ t('search.desc_tooltip') }}">
269
  </i>
270
  </h4>
271
  </div>
 
297
  data-bs-toggle="tooltip"
298
  data-bs-placement="top"
299
  data-bs-html="true"
300
+ title="<b>{{ t('search.info_label') }}</b><br>
301
+ {{ t('search.info_author') }} : {{ (model.name.split('/') | first) if '/' in model.name else t('search.info_unknown') }}<br>
302
+ {{ t('search.info_downloads') }} : {{ '{:,.0f}'.format((model.downloads or 0) | int).replace(',', ' ') }}<br>
303
+ {{ t('search.info_likes') }} : {{ model.likes or t('search.info_unknown') }}<br>
304
+ {{ t('search.info_date') }} : {{ model.createdAt or 'N/A' }}<br>
305
+ {{ t('search.info_task') }} : {{ model.task or t('search.info_unknown_task') }}<br>
306
+ {{ t('search.info_license') }} : {{ model.license or t('search.info_unknown_task') }}">
307
  {{ model.name }}
308
  </a>
309
  </h6>
310
  <p class="card-text" style="font-size: 0.75em; line-height: 1.2;">
311
+ {{ "{:,.0f}".format((model.downloads or 0) | int).replace(',', ' ') }} {{ t('search.downloads') }}<br>
312
+ {{ "{:,.0f}".format((model.citation_count or 0) | int).replace(',', ' ') }} {{ t('search.citations') }}
313
  </p>
314
  </div>
315
  </div>
316
  </div>
317
  {% endfor %}
318
  {% else %}
319
+ <p class="text-muted small m-2">{{ t('search.no_derived') }}</p>
320
  {% endif %}
321
  </div>
322
  </div>
 
329
  <hr class="mb-5">
330
  <div class="col-12">
331
  <div class="mb-5">
332
+ <h4 class="h4">{{ t('search.desc_detail_title') }}</h4>
333
  <div class="table-responsive">
334
  <table id="descendance-table" class="table table-bordered table-striped table-hover">
335
  <thead>
336
  <tr>
337
+ <th scope="col">{{ t('search.table_model') }}</th>
338
+ <th scope="col">{{ t('search.table_author') }}</th>
339
+ <th scope="col">{{ t('search.table_downloads') }}</th>
340
+ <th scope="col">{{ t('search.table_task') }}</th>
341
+ <th scope="col">{{ t('search.table_likes') }}</th>
342
+ <th scope="col">{{ t('search.table_date') }}</th>
343
+ <th scope="col">{{ t('search.table_dataset') }}</th>
344
+ <th scope="col">{{ t('search.table_license') }}</th>
345
+ <th scope="col">{{ t('search.table_distance') }}</th>
346
+ <th scope="col">{{ t('search.table_ascendants') }}</th>
347
+ <th scope="col">{{ t('search.table_descendants') }}</th>
348
+ <th scope="col">{{ t('search.table_citations') }}</th>
349
  </tr>
350
  </thead>
351
  </table>
 
353
  </div>
354
  <hr class="my-5">
355
  <div>
356
+ <h4 class="h4">{{ t('search.asc_detail_title') }}</h4>
357
  <div class="table-responsive">
358
  <table id="ascendance-table" class="table table-bordered table-striped table-hover">
359
  <thead>
360
  <tr>
361
+ <th scope="col">{{ t('search.table_model') }}</th>
362
+ <th scope="col">{{ t('search.table_author') }}</th>
363
+ <th scope="col">{{ t('search.table_downloads') }}</th>
364
+ <th scope="col">{{ t('search.table_task') }}</th>
365
+ <th scope="col">{{ t('search.table_likes') }}</th>
366
+ <th scope="col">{{ t('search.table_date') }}</th>
367
+ <th scope="col">{{ t('search.table_dataset') }}</th>
368
+ <th scope="col">{{ t('search.table_license') }}</th>
369
+ <th scope="col">{{ t('search.table_distance') }}</th>
370
+ <th scope="col">{{ t('search.table_ascendants') }}</th>
371
+ <th scope="col">{{ t('search.table_descendants') }}</th>
372
+ <th scope="col">{{ t('search.table_citations') }}</th>
373
  </tr>
374
  </thead>
375
  </table>
 
382
  <div class="row g-4">
383
  <div class="col-12 col-lg-8">
384
  <div class="card">
385
+ <div class="card-header"><h5 class="card-title mb-0">{{ t('search.graph_visualization') }}</h5></div>
386
  <div class="card-body">
387
  <div id="sigma-container" data-graph='{{ graph_data | tojson | safe }}'></div>
388
  </div>
389
  </div>
390
  <div id="node-info-card" class="card mt-3 card-interactive" style="display: none;"> <div class="card-body">
391
+ <h5 class="card-title">{{ t('search.node_info_title') }}</h5>
392
  <div id="node-details" class="mt-2"></div>
393
  </div>
394
  </div>
395
  </div>
396
  <div class="col-12 col-lg-4">
397
  <div class="card mb-3">
398
+ <div class="card-header"><h5 class="card-title mb-0">{{ t('search.legend_nodes') }}</h5></div>
399
  <div class="card-body">
400
  <ul id="legend-nodes" class="list-group list-group-flush">
401
+ <li class="list-group-item d-flex align-items-center"><span class="legend-color me-2" style="background-color: #007bff;"></span>{{ t('search.legend_person') }}</li>
402
+ <li class="list-group-item d-flex align-items-center"><span class="legend-color me-2" style="background-color: #092d53;"></span>{{ t('search.legend_org') }}</li>
403
+ <li class="list-group-item d-flex align-items-center"><span class="legend-color me-2" style="background-color:#7D7D7D;"></span>{{ t('search.legend_model') }}</li>
404
  </ul>
405
  </div>
406
  </div>
407
  <div class="card">
408
+ <div class="card-header"><h5 class="card-title mb-0">{{ t('search.legend_edges') }}</h5></div>
409
  <div class="card-body">
410
  <ul id="legend-edges" class="list-group list-group-flush"></ul>
411
  </div>
 
419
  <!-- Pied de page et scripts (inchangés) -->
420
  <footer class="bg-dark text-white text-center p-4 mt-auto">
421
  <div class="container">
422
+ <p class="mb-0">{{ t('site.footer') }}</p>
423
  </div>
424
  </footer>
425
 
 
430
  <script src="https://unpkg.com/graphology@0.25.1/dist/graphology.umd.min.js"></script>
431
  <script src="https://cdn.jsdelivr.net/npm/graphology-library/dist/graphology-library.min.js"></script>
432
  <script src="{{ url_for('static', filename='js/utils.js') }}"></script>
433
+ <script>
434
+ window.__I18N_DATA = {{ js_i18n_data | tojson }};
435
+ window.__I18N_LANG = "{{ current_lang }}";
436
+ </script>
437
  <script src="{{ url_for('static', filename='js/script.js') }}"></script>
438
  <script>
439
  document.addEventListener('DOMContentLoaded', function () {
 
442
  });
443
  </script>
444
  </body>
445
+ </html>
application_neo4j/templates/search_dataset.html CHANGED
@@ -1,9 +1,9 @@
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">
@@ -36,9 +36,13 @@
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
 
@@ -46,8 +50,8 @@
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
 
@@ -59,10 +63,10 @@
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>
@@ -70,31 +74,31 @@
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>
@@ -102,7 +106,7 @@
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 %}
@@ -110,7 +114,7 @@
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">
@@ -122,20 +126,20 @@
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>
@@ -147,22 +151,22 @@
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>
@@ -177,31 +181,31 @@
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>
@@ -214,7 +218,7 @@
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
 
@@ -225,6 +229,10 @@
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 () {
@@ -233,4 +241,4 @@
233
  });
234
  </script>
235
  </body>
236
- </html>
 
1
  <!DOCTYPE html>
2
+ <html lang="{{ current_lang }}">
3
  <head>
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{{ t('site.title_search') }}</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">
 
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">{{ t('site.nav_brand') }}</span>
40
+ <small class="d-block text-muted">{{ t('site.nav_subtitle') }}</small>
41
  </a>
42
+ <div class="d-flex gap-2">
43
+ <a href="{{ url_for('set_language_route', lang='fr') }}" class="btn btn-sm {{ 'btn-primary' if current_lang == 'fr' else 'btn-outline-primary' }}">{{ t('lang.fr') }}</a>
44
+ <a href="{{ url_for('set_language_route', lang='en') }}" class="btn btn-sm {{ 'btn-primary' if current_lang == 'en' else 'btn-outline-primary' }}">{{ t('lang.en') }}</a>
45
+ </div>
46
  </div>
47
  </header>
48
 
 
50
  <!-- Titre principal et Formulaire (inchangés) -->
51
  <div class="row">
52
  <div class="col-12">
53
+ <h1 class="display-5">{{ t('search.page_title') }}</h1>
54
+ <p class="lead">{{ t('search.page_lead_dataset') }}</p>
55
  </div>
56
  </div>
57
 
 
63
  <div class="row g-3 align-items-end">
64
  <!-- Champ de recherche -->
65
  <div class="col-12 col-md-5">
66
+ <label for="search-input" class="form-label fw-bold">{{ t('search.label_name') }}</label>
67
  <div class="position-relative">
68
  <input type="text" name="name" id="search-input" class="form-control"
69
+ placeholder="{{ t('search.placeholder_dataset') }}"
70
  value="{{ request.form.name or '' }}" required autocomplete="off" />
71
  <ul id="suggestions-list" class="list-group position-absolute w-100" style="display: none; z-index: 1000;"></ul>
72
  </div>
 
74
 
75
  <!-- Filtres -->
76
  <div class="col-12 col-md-3">
77
+ <label class="form-label fw-bold">{{ t('search.label_filters') }}</label>
78
  <div class="d-flex gap-3">
79
  <div class="form-check">
80
  <input class="form-check-input" type="checkbox" name="filters" value="Model" id="filter-model"
81
  {% if 'Model' in search.filters %}checked{% endif %}>
82
+ <label class="form-check-label" for="filter-model">{{ t('search.filter_model') }}</label>
83
  </div>
84
  <div class="form-check">
85
  <input class="form-check-input" type="checkbox" name="filters" value="Dataset" id="filter-dataset"
86
  {% if 'Dataset' in search.filters %}checked{% endif %}>
87
+ <label class="form-check-label" for="filter-dataset">{{ t('search.filter_dataset') }}</label>
88
  </div>
89
  </div>
90
  </div>
91
 
92
  <!-- Profondeur de recherche -->
93
  <div class="col-12 col-md-4">
94
+ <label class="form-label fw-bold">{{ t('search.label_depth') }}</label>
95
  <div class="d-flex align-items-center gap-3">
96
  <div class="form-check form-switch">
97
  <input class="form-check-input" type="checkbox" id="depth-unlimited" name="depth_unlimited" checked>
98
+ <label class="form-check-label" for="depth-unlimited">{{ t('search.depth_unlimited') }}</label>
99
  </div>
100
  <div class="input-group input-group-sm">
101
+ <span class="input-group-text">{{ t('search.depth_limited') }}</span>
102
  <input class="form-control" type="number" name="depth" id="depth"
103
  value="{{ request.form.depth }}" min="1" max="5" disabled>
104
  </div>
 
106
  </div>
107
  </div>
108
 
109
+
110
  {% if message %}
111
  <div class="alert alert-info mt-3"><p class="mb-0">{{ message }}</p></div>
112
  {% endif %}
 
114
  {% set searched_node = (graph_data.nodes | selectattr('id', 'equalto', search.name) | list | first) or {} %}
115
  <div class="row justify-content-center align-items-stretch g-4">
116
  <div class="col-12 col-lg-4 " id="center-column">
117
+ <h4 class="h5 text-center mb-3">{{ t('dataset.searched_title') }}</h4>
118
  <a href="https://huggingface.co/datasets/{{ searched_node.id }}" target="_blank" rel="noopener noreferrer" class="text-white">
119
  <div class="card card-recherche text-center border-primary border-2 ">
120
  <div class="card-header bg-primary text-white">
 
126
  <div class="row">
127
  <div class="col">
128
  <p class="fw-bold mb-0">{{ "{:,.0f}".format((graph_data.models_count[0] or 0) | int).replace(',', ' ') }}</p>
129
+ <p class="small text-muted">{{ t('dataset.models_using') }}
130
  <i class="bi bi-info-circle-fill ms-2"></i>
131
  </p>
132
  </div>
133
  </div>
134
  <div class="row mt-2">
135
+ <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('search.downloads') }}</p></div>
136
  </div>
137
  <div class="mt-2">
138
+ {% if searched_node.createdAt_dataset %}<span class="badge bg-light text-dark">{{ t('search.published') }} {{ searched_node.createdAt_dataset }}</span>{% endif %}
139
  </div>
140
  <hr>
141
  </a>
142
+ <button id="show-graph-btn" class="btn btn-secondary"><i class="bi bi-diagram-3 me-1"></i>{{ t('search.btn_graph') }}</button>
143
  </div>
144
  </div>
145
  </div>
 
151
  <hr class="mb-5">
152
  <div class="col-12">
153
  <div class="mb-5">
154
+ <h4 class="h4">{{ t('dataset.trained_title') }}</h4>
155
  <div class="table-responsive">
156
  <table id="train-table" class="table table-bordered table-striped table-hover">
157
  <thead>
158
  <tr>
159
+ <th scope="col">{{ t('search.table_model') }}</th>
160
+ <th scope="col">{{ t('search.table_author') }}</th>
161
+ <th scope="col">{{ t('search.table_downloads') }}</th>
162
+ <th scope="col">{{ t('search.table_task') }}</th>
163
+ <th scope="col">{{ t('search.table_likes') }}</th>
164
+ <th scope="col">{{ t('search.table_date') }}</th>
165
+ <th scope="col">{{ t('search.table_dataset') }}</th>
166
+ <th scope="col">{{ t('search.table_license') }}</th>
167
+ <th scope="col">{{ t('search.table_ascendants') }}</th>
168
+ <th scope="col">{{ t('search.table_descendants') }}</th>
169
+ <th scope="col">{{ t('search.table_citations') }}</th>
170
  </tr>
171
  </thead>
172
  <tbody>
 
181
  <div class="row g-4">
182
  <div class="col-12 col-lg-8">
183
  <div class="card">
184
+ <div class="card-header"><h5 class="card-title mb-0">{{ t('search.graph_visualization') }}</h5></div>
185
  <div class="card-body">
186
  <div id="sigma-container" data-graph='{{ graph_data | tojson | safe }}'></div>
187
  </div>
188
  </div>
189
  <div id="node-info-card" class="card mt-3 card-interactive" style="display: none;"> <div class="card-body">
190
+ <h5 class="card-title">{{ t('search.node_info_title') }}</h5>
191
  <div id="node-details" class="mt-2"></div>
192
  </div>
193
  </div>
194
  </div>
195
  <div class="col-12 col-lg-4">
196
  <div class="card mb-3">
197
+ <div class="card-header"><h5 class="card-title mb-0">{{ t('search.legend_nodes') }}</h5></div>
198
  <div class="card-body">
199
  <ul id="legend-nodes" class="list-group list-group-flush">
200
+ <li class="list-group-item d-flex align-items-center"><span class="legend-color me-2" style="background-color: #ffc107;"></span>{{ t('search.legend_dataset') }}</li>
201
+ <li class="list-group-item d-flex align-items-center"><span class="legend-color me-2" style="background-color: #007bff;"></span>{{ t('search.legend_person') }}</li>
202
+ <li class="list-group-item d-flex align-items-center"><span class="legend-color me-2" style="background-color: #092d53;"></span>{{ t('search.legend_org') }}</li>
203
+ <li class="list-group-item d-flex align-items-center"><span class="legend-color me-2" style="background-color:#7D7D7D;"></span>{{ t('search.legend_model') }}</li>
204
  </ul>
205
  </div>
206
  </div>
207
  <div class="card">
208
+ <div class="card-header"><h5 class="card-title mb-0">{{ t('search.legend_edges') }}</h5></div>
209
  <div class="card-body">
210
  <ul id="legend-edges" class="list-group list-group-flush"></ul>
211
  </div>
 
218
  <!-- Pied de page et scripts (inchangés) -->
219
  <footer class="bg-dark text-white text-center p-4 mt-auto">
220
  <div class="container">
221
+ <p class="mb-0">{{ t('site.footer') }}</p>
222
  </div>
223
  </footer>
224
 
 
229
  <script src="https://unpkg.com/graphology@0.25.1/dist/graphology.umd.min.js"></script>
230
  <script src="https://cdn.jsdelivr.net/npm/graphology-library/dist/graphology-library.min.js"></script>
231
  <script src="{{ url_for('static', filename='js/utils.js') }}"></script>
232
+ <script>
233
+ window.__I18N_DATA = {{ js_i18n_data | tojson }};
234
+ window.__I18N_LANG = "{{ current_lang }}";
235
+ </script>
236
  <script src="{{ url_for('static', filename='js/script_dataset.js') }}"></script>
237
  <script>
238
  document.addEventListener('DOMContentLoaded', function () {
 
241
  });
242
  </script>
243
  </body>
244
+ </html>