const edgeInfos_dataset = { "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 été utilisé dans ce modèle": { color: "#f50f0f",name : "A été utilisé dans ce modèle", tooltip: "Ce dataset a servi pour l'entraînement de ce modèle" }, "A généré": { color: "#8f7340",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: "#d9c00d",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: "#9af17c",name : "Adapter", tooltip: "Adaptation : méthode d’ajustement qui peut être utilisée avec peu de ressources de calcul." }, "quantized": { color: "#1bc3c6",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: "#e407e4",name : "Merge", tooltip:"Fusion : méthode visant à mélanger des couches de différents modèles pour améliorer leur performance." } }; function buildEdgeLegend() { const legendContainer = document.getElementById("legend-edges"); legendContainer.innerHTML = ""; // reset Object.entries(edgeInfos_dataset).forEach(([relation, {color, name,tooltip}]) => { 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)); } /** * 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 common_dt_options = { "language": { "url": "https://cdn.datatables.net/plug-ins/1.13.4/i18n/fr-FR.json" }, "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 ?? "Inconnu"), node.task || "Inconnue", String(node.likes ?? "0"), String(node.createdAt ?? "Inconnue"), node.dataset || "Inconnu", node.license || "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(); } });