pbordescnil's picture
add english version
b372d3c
Raw
History Blame
12 kB
// --- 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;
}