Spaces:
Running
Running
| // --- FONCTIONS UTILITAIRES GLOBALES --- | |
| // Ces fonctions sont utilisées à plusieurs endroits et sont donc définies en premier. | |
| // ── I18N runtime (peut être surchargé par le template) ── | |
| const I18N = window.__I18N_DATA || {}; | |
| const I18N_LANG = window.__I18N_LANG || 'fr'; | |
| function _(key) { | |
| return I18N[key] !== undefined ? I18N[key] : key; | |
| } | |
| // ── Edge labels translations (Neo4j relation name → display name) ── | |
| const EDGE_LABEL_MAP = { | |
| fr: { | |
| "POSTED": "A publié", | |
| "IS_IN": "Fait partie de cette organisation", | |
| "USED_IN": "A été utilisé dans ce modèle", | |
| "A publié": "A publié", | |
| "Fait partie de cette organisation": "Fait partie de cette organisation", | |
| "A été utilisé dans ce modèle": "A été utilisé dans ce modèle", | |
| "A généré": "A généré", | |
| "finetune": "Finetune", | |
| "adapter": "Adapter", | |
| "quantized": "Quantized", | |
| "merge": "Merge", | |
| "other": "Autre", | |
| "unknown": "Autre", | |
| }, | |
| en: { | |
| "POSTED": "Published", | |
| "IS_IN": "Member of this organization", | |
| "USED_IN": "Used in this model", | |
| "A publié": "Published", | |
| "Fait partie de cette organisation": "Member of this organization", | |
| "A été utilisé dans ce modèle": "Used in this model", | |
| "A généré": "Generated", | |
| "finetune": "Fine-tuned", | |
| "adapter": "Adapted", | |
| "quantized": "Quantized", | |
| "merge": "Merged", | |
| "other": "Other", | |
| "unknown": "Other", | |
| }, | |
| }; | |
| function edgeDisplayName(relation) { | |
| const map = EDGE_LABEL_MAP[I18N_LANG] || EDGE_LABEL_MAP.fr; | |
| return map[relation] || relation; | |
| } | |
| // Mapping relations → {couleur, tooltip} | |
| const edgeInfos = { | |
| "Fait partie de cette organisation": { | |
| color: "#a05195", | |
| tooltip_fr: "Cette personne est membre de cette organisation", | |
| tooltip_en: "This person is a member of this organization" | |
| }, | |
| "A publié": { | |
| color: "#003f5c", | |
| tooltip_fr: "Un auteur (personne ou organisation) a publié un modèle", | |
| tooltip_en: "An author (person or organization) published a model" | |
| }, | |
| "A généré": { | |
| color: "#f50f0f", | |
| 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)", | |
| tooltip_en: "The source model was downloaded and modified to create the target model (transformation type unknown)" | |
| }, | |
| "finetune": { | |
| color: "#cd6700", | |
| 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.", | |
| tooltip_en: "Fine-tuning: the source model is retrained on a specific dataset to perform well on a precise task." | |
| }, | |
| "adapter": { | |
| color: "#238a00", | |
| tooltip_fr: "Adaptation : méthode d'ajustement qui peut être utilisée avec peu de ressources de calcul.", | |
| tooltip_en: "Adaptation: a fine-tuning method that can be used with limited computing resources." | |
| }, | |
| "quantized": { | |
| color: "#009194", | |
| tooltip_fr: "Quantisation : la précision des poids du modèle source est réduite afin de diminuer son empreinte en mémoire.", | |
| tooltip_en: "Quantization: the precision of the source model's weights is reduced to decrease its memory footprint." | |
| }, | |
| "merge": { | |
| color: "#c29bc2", | |
| tooltip_fr: "Fusion : méthode visant à mélanger des couches de différents modèles pour améliorer leur performance.", | |
| tooltip_en: "Merge: a method aiming to mix layers of different models to improve their performance." | |
| }, | |
| "other": { | |
| color: "#8f7340", | |
| tooltip_fr: "Autre type de relation", | |
| tooltip_en: "Other type of relation" | |
| }, | |
| "unknown": { | |
| color: "#8f7340", | |
| tooltip_fr: "Autre type de relation", | |
| tooltip_en: "Other type of relation" | |
| } | |
| }; | |
| function buildEdgeLegend() { | |
| const legendContainer = document.getElementById("legend-edges"); | |
| legendContainer.innerHTML = ""; // reset | |
| Object.entries(edgeInfos).forEach(([relation, {color, tooltip_fr, tooltip_en}]) => { | |
| if (relation == "unknown") return; | |
| const li = document.createElement("li"); | |
| li.className = "list-group-item d-flex align-items-center"; | |
| // ajout Bootstrap tooltip | |
| li.setAttribute("data-bs-toggle", "tooltip"); | |
| li.setAttribute("data-bs-placement", "top"); | |
| li.setAttribute("data-bs-html", "true"); | |
| const tooltipKey = I18N_LANG === 'en' ? 'tooltip_en' : 'tooltip_fr'; | |
| li.setAttribute("title", tooltip_fr !== undefined ? (I18N_LANG === 'en' ? tooltip_en : tooltip_fr) : (tooltip_en || tooltip_fr || "")); | |
| const span = document.createElement("span"); | |
| span.className = "legend-color edge me-2"; | |
| span.style.backgroundColor = color; | |
| li.appendChild(span); | |
| li.appendChild(document.createTextNode(edgeDisplayName(relation))); | |
| legendContainer.appendChild(li); | |
| }); | |
| // nécessaire pour activer les tooltips Bootstrap dynamiques | |
| const tooltipTriggerList = [].slice.call(legendContainer.querySelectorAll('[data-bs-toggle="tooltip"]')) | |
| tooltipTriggerList.map(el => new bootstrap.Tooltip(el)); | |
| } | |
| /** | |
| * Affiche la carte d'information pour un nœud donné. | |
| * @param {object} attr - Les attributs du nœud à afficher. | |
| */ | |
| function showNodeInfo(attr) { | |
| const infoCard = document.getElementById("node-info-card"); | |
| const cardBody = infoCard.querySelector('.card-body'); | |
| const detailsContainer = document.getElementById("node-details"); | |
| if (!infoCard || !detailsContainer || !cardBody) return; | |
| // --- ÉTAPE 1: Nettoyage --- | |
| const existingLink = cardBody.querySelector('.stretched-link'); | |
| if (existingLink) { | |
| existingLink.remove(); | |
| } | |
| // On stocke l'ID du nœud pour l'animation au survol | |
| infoCard.dataset.nodeId = attr.id; | |
| // --- ÉTAPE 2: Construction des détails --- | |
| const labels = { | |
| name: I18N['js.node_info.name'] || "Nom :", | |
| type: I18N['js.node_info.type'] || "Type :", | |
| followers: I18N['js.node_info.followers'] || "Abonnés :", | |
| downloads: I18N['js.node_info.downloads'] || "Téléchargements :", | |
| created: I18N['js.node_info.created'] || "Créé le :", | |
| task: I18N['js.node_info.task'] || "Tâche :", | |
| dataset: I18N['js.node_info.dataset'] || "Dataset utilisé :", | |
| undefined: I18N['js.node_info.undefined'] || "Non défini", | |
| }; | |
| let infosHtml = `<p><strong>${labels.name}</strong> ${attr.id || labels.undefined}</p><p><strong>${labels.type}</strong> ${attr.dataCat}</p>`; | |
| // ... (Le reste de la construction de infosHtml ne change pas) ... | |
| if (["personne", "organisation", "Author"].includes(attr.dataCat)) { | |
| if (attr.followers) infosHtml += `<p><strong>${labels.followers}</strong> ${formatNumberShort(attr.followers)}</p>`; | |
| } else if (["Modèle", "Model"].includes(attr.dataCat)) { | |
| if (attr.downloads) infosHtml += `<p><strong>${labels.downloads}</strong> ${formatNumberShort(attr.downloads)}</p>`; | |
| if (attr.createdAt) infosHtml += `<p><strong>${labels.created}</strong> ${formatDateFr(attr.createdAt)}</p>`; | |
| if (attr.task) infosHtml += `<p><strong>${labels.task}</strong> ${attr.task}</p>`; | |
| if (attr.dataset) infosHtml += `<p><strong>${labels.dataset}</strong> ${attr.dataset}</p>`; | |
| } else if (attr.dataCat === "Dataset") { | |
| if (attr.downloads) infosHtml += `<p><strong>${labels.downloads}</strong> ${formatNumberShort(attr.downloads)}</p>`; | |
| if (attr.createdAt_dataset) infosHtml += `<p><strong>${labels.created}</strong> ${formatDateFr(attr.createdAt_dataset)}</p>`; | |
| } | |
| // --- ÉTAPE 3: Création du lien --- | |
| let huggingFaceUrl = null; | |
| if (["Modèle", "Model","personne", "organisation", "Author"].includes(attr.dataCat)) { | |
| huggingFaceUrl = `https://huggingface.co/${attr.id}`; | |
| } else if (attr.dataCat === "Dataset") { | |
| huggingFaceUrl = `https://huggingface.co/datasets/${attr.id}`; | |
| } | |
| if (huggingFaceUrl) { | |
| const link = document.createElement('a'); | |
| link.href = huggingFaceUrl; | |
| link.target = '_blank'; | |
| link.rel = 'noopener noreferrer'; | |
| link.className = 'stretched-link'; | |
| const seeLabel = (I18N['js.node_info.see_on_hf'] || "Voir {name} sur Hugging Face").replace('{name}', attr.id); | |
| link.setAttribute('aria-label', seeLabel); | |
| cardBody.appendChild(link); | |
| } | |
| detailsContainer.innerHTML = infosHtml; | |
| // Affichage et défilement de la carte | |
| infoCard.style.display = 'block'; | |
| infoCard.scrollIntoView({ behavior: 'smooth', block: 'center' }); | |
| } | |
| /** | |
| * Formate une chaîne de date ISO. | |
| * @param {string} dateString - La date à formater. | |
| * @returns {string} La date formatée ou "Date inconnue" / "Unknown date". | |
| */ | |
| function formatDateFr(dateString) { | |
| if (!dateString || dateString === "inconnue") return I18N['js.node_info.unknown_date'] || "Date inconnue"; | |
| try { | |
| const locale = I18N_LANG === 'en' ? 'en-GB' : 'fr-FR'; | |
| const options = { year: 'numeric', month: 'long', day: 'numeric' }; | |
| return new Date(dateString).toLocaleDateString(locale, options); | |
| } catch (e) { | |
| return I18N['js.node_info.unknown_date'] || "Date inconnue"; | |
| } | |
| } | |
| // --- PLUGIN DE TRI PERSONNALISÉ POUR DATATABLES --- | |
| // Gère les colonnes avec des nombres et des chaînes de caractères (ex: "Inconnu") | |
| // Place toujours les chaînes après les nombres lors d'un tri ascendant. | |
| // Fonction pour parser la valeur : enlève les espaces et convertit en nombre si possible | |
| function parseNumericValue(value) { | |
| if (typeof value === 'string') { | |
| // Enlève les espaces (pour les nombres comme "12 345") et les virgules | |
| const cleanedValue = value.replace(/[\s,]/g, ''); | |
| const num = parseInt(cleanedValue, 10); | |
| if (!isNaN(num)) { | |
| return { isNumber: true, value: num }; | |
| } | |
| } | |
| // Si ce n'est pas une chaîne numérique, on le traite comme du texte | |
| return { isNumber: false, value: value }; | |
| } | |
| // Définition du tri ascendant | |
| jQuery.fn.dataTable.ext.order['numeric-string-asc'] = function (a, b) { | |
| const valA = parseNumericValue(a); | |
| const valB = parseNumericValue(b); | |
| if (valA.isNumber && valB.isNumber) { | |
| return valA.value - valB.value; // Tri numérique standard | |
| } else if (valA.isNumber && !valB.isNumber) { | |
| return -1; // Les nombres viennent AVANT les chaînes | |
| } else if (!valA.isNumber && valB.isNumber) { | |
| return 1; // Les chaînes viennent APRÈS les nombres | |
| } else { | |
| // Les deux sont des chaînes, on fait un tri alphabétique | |
| return String(valA.value).localeCompare(String(valB.value)); | |
| } | |
| }; | |
| // Le tri descendant est simplement l'inverse de l'ascendant | |
| jQuery.fn.dataTable.ext.order['numeric-string-desc'] = function (a, b) { | |
| return jQuery.fn.dataTable.ext.order['numeric-string-asc'](a, b) * -1; | |
| }; | |
| /** | |
| * Formate un grand nombre en une version courte (ex: 1.2M, 50k). | |
| * @param {number} n - Le nombre à formater. | |
| * @returns {string} Le nombre formaté. | |
| */ | |
| function formatNumberShort(n) { | |
| if (!n || n === 0) return ""; | |
| if (n >= 1000000000) return (n / 1000000000).toFixed(1) + 'B'; | |
| if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M'; | |
| if (n >= 1000) return (n / 1000).toFixed(0) + 'k'; | |
| return n.toString(); | |
| } | |
| function getAllPredecessors(graph, nodeId) { | |
| const visited = new Set(); | |
| const stack = [nodeId]; | |
| while (stack.length > 0) { | |
| const current = stack.pop(); | |
| if (!visited.has(current)) { | |
| visited.add(current); | |
| // On parcourt les prédécesseurs directs | |
| graph.forEachInNeighbor(current, (pred) => { | |
| if (!visited.has(pred)) { | |
| stack.push(pred); | |
| } | |
| }); | |
| } | |
| } | |
| return visited; | |
| } | |