Spaces:
Running
Running
| const edgeInfos_dataset = { | |
| "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 été utilisé dans ce modèle": { | |
| color: "#f50f0f", | |
| tooltip_fr: "Ce dataset a servi pour l'entraînement de ce modèle", | |
| tooltip_en: "This dataset was used for training this model" | |
| }, | |
| "A généré": { | |
| color: "#8f7340", | |
| 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: "#d9c00d", | |
| 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: "#9af17c", | |
| 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: "#1bc3c6", | |
| 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: "#e407e4", | |
| 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." | |
| } | |
| }; | |
| function buildEdgeLegend() { | |
| const legendContainer = document.getElementById("legend-edges"); | |
| legendContainer.innerHTML = ""; // reset | |
| Object.entries(edgeInfos_dataset).forEach(([relation, {color, tooltip_fr, tooltip_en}]) => { | |
| 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", I18N_LANG === 'en' ? (tooltip_en || tooltip_fr) : (tooltip_fr || tooltip_en || "")); | |
| 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)); | |
| } | |
| /** | |
| * Fonction principale pour créer et afficher le graphe Sigma. | |
| * Appelée une seule fois lorsque l'utilisateur clique sur "Voir l'arbre". | |
| * @param {object} graphData - Les données des nœuds et arêtes. | |
| * @returns {Sigma} L'instance du renderer Sigma. | |
| */ | |
| function initializeSigmaGraph(graphData) { | |
| const container = document.getElementById("sigma-container"); | |
| if (!container) { | |
| console.error("Conteneur Sigma non trouvé. Initialisation annulée."); | |
| return null; | |
| } | |
| console.log("Initialisation du graphe Sigma..."); | |
| const Graph = window.graphology.Graph; | |
| const graph = new Graph(); | |
| const TYPE_COLORS = { | |
| personne: '#007bff', | |
| organisation: '#007', | |
| Modèle: '#b1b1b1', | |
| Dataset: '#ffc107', | |
| Author: '#007bff', | |
| Model: '#b1b1b1', | |
| Unknown: '#999', | |
| Défaut: '#999' | |
| }; | |
| const addedNodeIds = new Set(); | |
| graphData.nodes.forEach(node => { | |
| if (addedNodeIds.has(node.id)) return; | |
| const rawSize = Math.log10(node.downloads || node.followers || 1) * 2 + (depth === 0 ? 5 : 0); | |
| graph.addNode(node.id, { | |
| id: node.id, label: node.id, size: Math.max(5, rawSize), color: TYPE_COLORS[node.label] || '#999', | |
| dataCat: node.label, x: Math.random(), y: Math.random(), | |
| followers: node.followers, downloads: node.downloads, createdAt: node.createdAt, | |
| createdAt_dataset: node.createdAt_dataset, task: node.task, bfsDepth: node.distance | |
| }); | |
| addedNodeIds.add(node.id); | |
| }); | |
| graphData.edges.forEach(edge => { | |
| console.log(edge.relation); | |
| const edgeColor = edgeInfos_dataset[edge.relation]["color"] || "#ccc"; | |
| if (graph.hasNode(edge.source) && graph.hasNode(edge.target)) { | |
| graph.addEdge(edge.source, edge.target, { | |
| label: edge.relation || '', | |
| color: edgeColor || '#ccc', | |
| size: 3, | |
| type: 'arrow' | |
| }); | |
| } else { | |
| console.warn(`Arête ignorée: ${edge.source} -> ${edge.target} (nœuds manquants)`); | |
| } | |
| }); | |
| graphologyLibrary.layoutForceAtlas2.assign(graph, { iterations: 100 }); | |
| const renderer = new Sigma(graph, container); | |
| buildEdgeLegend(); | |
| // Mettre en place les interactions | |
| const filtersContainer = document.getElementById('graph-filters-container'); | |
| if (filtersContainer) { | |
| filtersContainer.addEventListener('change', (event) => { | |
| if (event.target.type === 'checkbox') { | |
| updateGraphVisibility(graph, renderer); | |
| } | |
| }); | |
| } | |
| renderer.on('clickNode', ({ node }) => { | |
| showNodeInfo(graph.getNodeAttributes(node)); | |
| }); | |
| setupTableRowClickListeners(renderer, graph); | |
| return renderer; | |
| } | |
| /** | |
| * Met en place l'écouteur de clic sur les lignes des tableaux pour interagir avec le graphe. | |
| * @param {Sigma} renderer - L'instance du renderer Sigma. | |
| * @param {Graph} graph - L'instance du graphe Graphology. | |
| */ | |
| function setupTableRowClickListeners(renderer, graph) { | |
| $('#train-table tbody').on('click', 'tr', function() { | |
| const nodeId = $(this).attr('data-node-id'); | |
| if (!nodeId || !renderer || !graph.hasNode(nodeId)) return; | |
| // Effet de "highlight" temporaire sur le nœud dans le graphe | |
| const originalSize = graph.getNodeAttribute(nodeId, "size"); | |
| const highlightSize = originalSize * 1.6; | |
| graph.setNodeAttribute(nodeId, "size", highlightSize); | |
| renderer.refresh(); // Nécessaire pour forcer le rendu | |
| setTimeout(() => { | |
| graph.setNodeAttribute(nodeId, "size", originalSize); | |
| renderer.refresh(); | |
| }, 1000); | |
| // Supprime la classe active des autres lignes | |
| $('#train-table tbody tr').removeClass('active'); | |
| $(this).addClass('active'); | |
| showNodeInfo(graph.getNodeAttributes(nodeId)); | |
| const pos = renderer.getNodeDisplayData(nodeId); | |
| if (pos) { | |
| renderer.getCamera().animate({ x: pos.x, y: pos.y, ratio: 0.2 }, { duration: 600 }); | |
| } | |
| }); | |
| } | |
| // =================================================================== | |
| // POINT D'ENTRÉE PRINCIPAL - EXÉCUTÉ AU CHARGEMENT DE LA PAGE | |
| // =================================================================== | |
| document.addEventListener("DOMContentLoaded", () => { | |
| // --- ÉTAPE 1: LECTURE DES DONNÉES ET INITIALISATION DES TABLEAUX --- | |
| const sigmaContainer = document.getElementById("sigma-container"); | |
| let graphData; | |
| if (sigmaContainer && sigmaContainer.dataset.graph) { | |
| try { | |
| graphData = JSON.parse(sigmaContainer.dataset.graph); | |
| } catch (e) { | |
| console.error("Erreur d'analyse JSON:", e); | |
| return; | |
| } | |
| const datatablesLangUrl = (window.__I18N_LANG === 'en') | |
| ? "https://cdn.datatables.net/plug-ins/1.13.4/i18n/en.json" | |
| : "https://cdn.datatables.net/plug-ins/1.13.4/i18n/fr-FR.json"; | |
| const common_dt_options = { | |
| "language": { "url": datatablesLangUrl }, | |
| "pageLength": 10, | |
| "responsive": true,"scrollX": true , "scrollY":true, | |
| "columnDefs": [ | |
| { | |
| // Appliquer notre plugin 'numeric-string' aux colonnes cibles | |
| "type": "numeric-string", | |
| "targets": [2, 4, 6, 7, 8, 9] // Indices des colonnes: Downloads, Likes, Distance, etc. | |
| } | |
| ] | |
| // "searching" est activé par défaut, on peut le customiser | |
| }; | |
| const tableTrain = $('#train-table').DataTable(common_dt_options); | |
| // Gérer la barre de recherche personnalisée | |
| const customSearch = document.getElementById('custom-table-search'); | |
| if (customSearch) { | |
| customSearch.addEventListener('keyup', function() { | |
| const query = this.value; | |
| tableTrain.search(query).draw(); | |
| }); | |
| } | |
| tableTrain.clear(); | |
| console.log("GraphData nodes:", graphData.nodes); | |
| graphData.nodes.forEach(node => { | |
| // Filtrer pour n'inclure que les modèles dans ces tableaux | |
| if (node.label !== 'Modèle' && node.label !== 'Model') return; | |
| const distance = node.distance; | |
| // Construction de la ligne avec les nouvelles données | |
| const rowData = [ | |
| node.id || "N/A", | |
| node.author || "Inconnu", | |
| // La conversion en string via la condition est parfaite ici | |
| String(node.downloads ?? (I18N['js.unknown'] || "Inconnu")), | |
| node.task || (I18N['js.unknown_f'] || "Inconnue"), | |
| String(node.likes ?? "0"), | |
| String(node.createdAt ?? (I18N['js.unknown_f'] || "Inconnue")), | |
| node.dataset || (I18N['js.unknown'] || "Inconnu"), | |
| node.license || (I18N['js.unknown_f'] || "Inconnue"), | |
| String(node.ascendantsCount ?? "0"), | |
| String(node.descendantsCount ?? "0"), | |
| String(node.citationCount ?? "0") | |
| ]; | |
| tableTrain.row.add(rowData).node().setAttribute("data-node-id", node.id); | |
| }); | |
| tableTrain.draw(); | |
| } | |
| // --- ÉTAPE 2: GESTION DES INTERACTIONS DE L'UTILISATEUR --- | |
| // Logique du bouton "Voir l'arbre généalogique" | |
| const showGraphBtn = document.getElementById('show-graph-btn'); | |
| const graphSection = document.getElementById('genealogy-graph-section'); | |
| let sigmaInstance = null; // Garde en mémoire l'instance du graphe | |
| if (showGraphBtn && graphSection && graphData) { | |
| showGraphBtn.addEventListener('click', () => { | |
| if (graphSection.style.display === 'none') { | |
| graphSection.style.display = 'block'; | |
| if (!sigmaInstance) { | |
| sigmaInstance = initializeSigmaGraph(graphData); | |
| } | |
| graphSection.scrollIntoView({ behavior: 'smooth', block: 'start' }); | |
| } else { | |
| graphSection.style.display = 'none'; | |
| document.getElementById("node-info-card").style.display = "none"; | |
| } | |
| }); | |
| } | |
| const input = document.getElementById('search-input'); | |
| const suggestionsList = document.getElementById('suggestions-list'); | |
| const modelCheckbox = document.getElementById('filter-model'); | |
| const datasetCheckbox = document.getElementById('filter-dataset'); | |
| // Cette fonction sera notre point central pour l'autocomplétion | |
| function fetchAutocomplete() { | |
| const query = input.value.trim(); | |
| // 1. Déterminer quel filtre est actif | |
| let activeFilter = ''; | |
| if (modelCheckbox.checked) { | |
| activeFilter = 'Model'; | |
| } else if (datasetCheckbox.checked) { | |
| activeFilter = 'Dataset'; | |
| } | |
| // Si le champ est vide, on cache les suggestions | |
| if (query.length < 2) { | |
| suggestionsList.style.display = 'none'; | |
| return; | |
| } | |
| // 2. Construire l'URL avec le filtre | |
| // On commence avec la requête de base | |
| let fetchUrl = `/autocomplete?q=${encodeURIComponent(query)}`; | |
| // On ajoute le paramètre 'filter' SEULEMENT s'il y en a un d'actif | |
| if (activeFilter) { | |
| fetchUrl += `&filter=${activeFilter}`; | |
| } | |
| // 3. Lancer la requête fetch avec la bonne URL | |
| fetch(fetchUrl) | |
| .then(response => response.json()) | |
| .then(data => { | |
| suggestionsList.innerHTML = ''; | |
| if (data.length > 0) { | |
| data.forEach(item => { | |
| const li = document.createElement('li'); | |
| li.textContent = item.name; | |
| li.addEventListener('click', () => { | |
| input.value = item.name; | |
| suggestionsList.style.display = 'none'; | |
| }); | |
| suggestionsList.appendChild(li); | |
| }); | |
| suggestionsList.style.display = 'block'; | |
| } else { | |
| suggestionsList.style.display = 'none'; | |
| } | |
| }); | |
| } | |
| // On écoute les événements sur le champ de saisie | |
| input.addEventListener('input', fetchAutocomplete); | |
| // AMÉLIORATION : On écoute aussi les changements sur les checkboxes ! | |
| // Si l'utilisateur change de filtre, on relance immédiatement une recherche. | |
| modelCheckbox.addEventListener('change', fetchAutocomplete); | |
| datasetCheckbox.addEventListener('change', fetchAutocomplete); | |
| // Logique pour cacher les suggestions si on clique ailleurs (inchangée) | |
| document.addEventListener('click', (e) => { | |
| if (!input.contains(e.target) && !suggestionsList.contains(e.target)) { | |
| suggestionsList.style.display = 'none'; | |
| } | |
| }); | |
| // Logique de la checkbox de profondeur (inchangée) | |
| const checkbox = document.getElementById('depth-unlimited'); | |
| if (checkbox) { | |
| const depthInput = document.getElementById('depth'); | |
| function toggleDepthInput() { if(depthInput) depthInput.disabled = checkbox.checked; } | |
| checkbox.addEventListener('change', toggleDepthInput); | |
| toggleDepthInput(); | |
| } | |
| }); | |