Quentin Lhoest
add app
f0806e2
// --- FONCTIONS UTILITAIRES GLOBALES ---
// Ces fonctions sont utilisées à plusieurs endroits et sont donc définies en premier.
// Mapping relations → {couleur, tooltip}
const edgeInfos = {
"Fait partie de cette organisation": {
color: "#a05195",name : "Fait partie de cette organisation",
tooltip: "Cette personne est membre de cette organisation"
},
"A publié": {
color: "#003f5c",name : "A publié",
tooltip: "Un auteur (personne ou organistaion) a publié un modèle"
},
"A généré": {
color: "#f50f0f",name : "A permis de générer",
tooltip: "Le modèle source a été téléchargé et modifié afin de créer le modèle cible (type de transformation inconnu)"
},
"finetune": {
color: "#cd6700",name : "Finetune",
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."
},
"adapter": {
color: "#238a00",name : "Adapter",
tooltip: "Adaptation : méthode d’ajustement qui peut être utilisée avec peu de ressources de calcul."
},
"quantized": {
color: "#009194",name : "Quantized",
tooltip:"Quantisation : la précision des poids du modèle source est réduite afin de diminuer son empreinte en mémoire."
},
"merge": {
color: "#c29bc2",name : "Merge",
tooltip:"Fusion : méthode visant à mélanger des couches de différents modèles pour améliorer leur performance."
},
"other": {
color: "#8f7340",name : "Autre",
tooltip: "Autre type de relation"
}
, "unknown": {
color: "#8f7340",name : "Autre",
tooltip: "Autre type de relation"
}
};
function buildEdgeLegend() {
const legendContainer = document.getElementById("legend-edges");
legendContainer.innerHTML = ""; // reset
Object.entries(edgeInfos).forEach(([relation, {color, name,tooltip}]) => {
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");
li.setAttribute("title", tooltip);
const span = document.createElement("span");
span.className = "legend-color edge me-2";
span.style.backgroundColor = color;
li.appendChild(span);
li.appendChild(document.createTextNode(name));
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 ---
let infosHtml = `<p><strong>Nom :</strong> ${attr.id || "Non défini"}</p><p><strong>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>Abonnés :</strong> ${formatNumberShort(attr.followers)}</p>`;
} else if (["Modèle", "Model"].includes(attr.dataCat)) {
if (attr.downloads) infosHtml += `<p><strong>Téléchargements :</strong> ${formatNumberShort(attr.downloads)}</p>`;
if (attr.createdAt) infosHtml += `<p><strong>Créé le :</strong> ${formatDateFr(attr.createdAt)}</p>`;
if (attr.task) infosHtml += `<p><strong>Tâche :</strong> ${attr.task}</p>`;
if (attr.dataset) infosHtml += `<p><strong>Dataset utilisé :</strong> ${attr.dataset}</p>`;
} else if (attr.dataCat === "Dataset") {
if (attr.downloads) infosHtml += `<p><strong>Téléchargements :</strong> ${formatNumberShort(attr.downloads)}</p>`;
if (attr.createdAt_dataset) infosHtml += `<p><strong>Créé le :</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';
link.setAttribute('aria-label', `Voir ${attr.id} sur Hugging Face`);
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 en format français (ex: 1 janvier 2024).
* @param {string} dateString - La date à formater.
* @returns {string} La date formatée ou "Date inconnue".
*/
function formatDateFr(dateString) {
if (!dateString || dateString === "inconnue") return "Date inconnue";
try {
const options = { year: 'numeric', month: 'long', day: 'numeric' };
return new Date(dateString).toLocaleDateString('fr-FR', options);
} catch (e) {
return "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;
}