// --- 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 = `
${labels.name} ${attr.id || labels.undefined}
${labels.type} ${attr.dataCat}
`; // ... (Le reste de la construction de infosHtml ne change pas) ... if (["personne", "organisation", "Author"].includes(attr.dataCat)) { if (attr.followers) infosHtml += `${labels.followers} ${formatNumberShort(attr.followers)}
`; } else if (["Modèle", "Model"].includes(attr.dataCat)) { if (attr.downloads) infosHtml += `${labels.downloads} ${formatNumberShort(attr.downloads)}
`; if (attr.createdAt) infosHtml += `${labels.created} ${formatDateFr(attr.createdAt)}
`; if (attr.task) infosHtml += `${labels.task} ${attr.task}
`; if (attr.dataset) infosHtml += `${labels.dataset} ${attr.dataset}
`; } else if (attr.dataCat === "Dataset") { if (attr.downloads) infosHtml += `${labels.downloads} ${formatNumberShort(attr.downloads)}
`; if (attr.createdAt_dataset) infosHtml += `${labels.created} ${formatDateFr(attr.createdAt_dataset)}
`; } // --- É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; }