| <!DOCTYPE html> |
| <html> |
| <head> |
| <meta charset="utf-8"> |
| <title>Vue Agrégée - Doc2GL</title> |
| <script src="https://unpkg.com/vis-network@9.1.0/dist/vis-network.min.js"></script> |
| <style> |
| html, body { |
| margin: 0; |
| padding: 0; |
| height: 100%; |
| background: #f5f5f5; |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
| } |
| |
| #container { |
| width: 100vw; |
| height: 100vh; |
| background: white; |
| position: relative; |
| } |
| |
| #controls { |
| position: absolute; |
| top: 20px; |
| right: 20px; |
| background: rgba(255, 255, 255, 0.95); |
| padding: 20px; |
| border-radius: 12px; |
| box-shadow: 0 4px 16px rgba(0,0,0,0.2); |
| z-index: 1000; |
| width: 320px; |
| max-height: 90vh; |
| overflow-y: auto; |
| transition: transform 0.3s ease; |
| } |
| |
| #controls.closed { |
| transform: translateX(calc(100% + 40px)); |
| } |
| |
| #controls h3 { |
| margin: 0 0 20px 0; |
| color: #667eea; |
| font-size: 18px; |
| display: flex; |
| align-items: center; |
| gap: 8px; |
| border-bottom: 2px solid #667eea; |
| padding-bottom: 10px; |
| } |
| |
| |
| #toggleControls { |
| position: absolute; |
| top: 10px; |
| right: 10px; |
| background: transparent; |
| border: none; |
| font-size: 20px; |
| cursor: pointer; |
| padding: 5px 8px; |
| width: auto; |
| margin: 0; |
| color: #667eea; |
| transition: transform 0.2s ease; |
| border-radius: 4px; |
| } |
| |
| #toggleControls:hover { |
| transform: scale(1.2); |
| background: rgba(102, 126, 234, 0.1); |
| box-shadow: none; |
| } |
| |
| |
| #reopenButton { |
| position: fixed; |
| top: 20px; |
| right: 20px; |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| color: white; |
| border: none; |
| border-radius: 50%; |
| width: 56px; |
| height: 56px; |
| font-size: 24px; |
| cursor: pointer; |
| box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); |
| z-index: 999; |
| display: none; |
| align-items: center; |
| justify-content: center; |
| transition: all 0.3s ease; |
| } |
| |
| #reopenButton:hover { |
| transform: scale(1.1); |
| box-shadow: 0 6px 16px rgba(102, 126, 234, 0.6); |
| } |
| |
| #reopenButton.visible { |
| display: flex; |
| } |
| |
| .control-group { |
| margin-bottom: 20px; |
| } |
| |
| .control-group label { |
| display: block; |
| font-size: 13px; |
| color: #555; |
| margin-bottom: 8px; |
| font-weight: 600; |
| } |
| |
| .slider-container { |
| display: flex; |
| align-items: center; |
| gap: 10px; |
| } |
| |
| input[type="range"] { |
| flex: 1; |
| height: 6px; |
| border-radius: 3px; |
| outline: none; |
| -webkit-appearance: none; |
| } |
| |
| #thresholdSlider { |
| background: linear-gradient(to right, #e74c3c 0%, #f39c12 25%, #3498db 50%, #27ae60 75%, #27ae60 100%); |
| } |
| |
| #fuzzySlider, #semanticSlider { |
| background: linear-gradient(to right, #e74c3c 0%, #f39c12 50%, #27ae60 100%); |
| } |
| |
| input[type="range"]::-webkit-slider-thumb { |
| -webkit-appearance: none; |
| appearance: none; |
| width: 20px; |
| height: 20px; |
| border-radius: 50%; |
| background: #667eea; |
| cursor: pointer; |
| box-shadow: 0 2px 6px rgba(0,0,0,0.3); |
| transition: all 0.2s ease; |
| } |
| |
| input[type="range"]::-webkit-slider-thumb:hover { |
| transform: scale(1.2); |
| background: #5568d3; |
| } |
| |
| input[type="range"]::-moz-range-thumb { |
| width: 20px; |
| height: 20px; |
| border-radius: 50%; |
| background: #667eea; |
| cursor: pointer; |
| border: none; |
| box-shadow: 0 2px 6px rgba(0,0,0,0.3); |
| transition: all 0.2s ease; |
| } |
| |
| input[type="range"]::-moz-range-thumb:hover { |
| transform: scale(1.2); |
| background: #5568d3; |
| } |
| |
| .value-display { |
| min-width: 60px; |
| text-align: right; |
| font-weight: bold; |
| color: #667eea; |
| font-size: 14px; |
| } |
| |
| #legend { |
| margin-top: 20px; |
| padding-top: 15px; |
| border-top: 1px solid #e0e0e0; |
| } |
| |
| #legend h4 { |
| margin: 0 0 12px 0; |
| font-size: 14px; |
| color: #555; |
| font-weight: 600; |
| } |
| |
| .legend-item { |
| display: flex; |
| align-items: center; |
| margin: 8px 0; |
| font-size: 12px; |
| } |
| |
| .legend-color { |
| width: 18px; |
| height: 18px; |
| border-radius: 50%; |
| margin-right: 10px; |
| border: 2px solid #fff; |
| box-shadow: 0 2px 4px rgba(0,0,0,0.2); |
| } |
| |
| #info { |
| position: absolute; |
| bottom: 20px; |
| left: 20px; |
| background: rgba(255, 255, 255, 0.95); |
| padding: 15px 20px; |
| border-radius: 12px; |
| box-shadow: 0 4px 16px rgba(0,0,0,0.2); |
| z-index: 1000; |
| font-size: 13px; |
| } |
| |
| #info p { |
| margin: 5px 0; |
| color: #555; |
| } |
| |
| #info strong { |
| color: #333; |
| } |
| |
| #loading { |
| position: absolute; |
| top: 50%; |
| left: 50%; |
| transform: translate(-50%, -50%); |
| font-size: 24px; |
| color: #667eea; |
| z-index: 999; |
| } |
| |
| #error { |
| position: absolute; |
| top: 50%; |
| left: 50%; |
| transform: translate(-50%, -50%); |
| background: #f8d7da; |
| border: 1px solid #f5c6cb; |
| border-radius: 12px; |
| padding: 30px; |
| text-align: center; |
| color: #721c24; |
| display: none; |
| max-width: 450px; |
| box-shadow: 0 4px 16px rgba(0,0,0,0.2); |
| } |
| |
| .stats { |
| display: flex; |
| gap: 15px; |
| margin-top: 15px; |
| padding-top: 15px; |
| border-top: 1px solid #e0e0e0; |
| } |
| |
| .stat-item { |
| flex: 1; |
| text-align: center; |
| padding: 10px; |
| background: #f8f9fa; |
| border-radius: 8px; |
| } |
| |
| .stat-value { |
| font-size: 22px; |
| font-weight: bold; |
| color: #667eea; |
| } |
| |
| .stat-label { |
| font-size: 11px; |
| color: #888; |
| margin-top: 4px; |
| text-transform: uppercase; |
| letter-spacing: 0.5px; |
| } |
| |
| .help-text { |
| font-size: 11px; |
| color: #888; |
| margin-top: 4px; |
| font-style: italic; |
| } |
| |
| button { |
| width: 100%; |
| padding: 12px; |
| margin-top: 15px; |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| color: white; |
| border: none; |
| border-radius: 8px; |
| font-weight: 600; |
| cursor: pointer; |
| font-size: 14px; |
| transition: all 0.3s ease; |
| } |
| |
| button:hover { |
| transform: translateY(-2px); |
| box-shadow: 0 6px 16px rgba(102, 126, 234, 0.4); |
| } |
| |
| button:active { |
| transform: translateY(0); |
| } |
| </style> |
| </head> |
| <body> |
| <div id="loading">⏳ Chargement...</div> |
| <div id="error"></div> |
| <div id="container"></div> |
|
|
| |
| <button id="reopenButton" onclick="toggleControlPanel()" title="Ouvrir les contrôles"> |
| ⚙️ |
| </button> |
|
|
| |
| <div id="controls"> |
| <button id="toggleControls" onclick="toggleControlPanel()" title="Fermer le panneau">✕</button> |
|
|
| <h3> |
| <span>🎛️</span> |
| <span>Contrôles</span> |
| </h3> |
|
|
| |
| <div class="control-group"> |
| <label for="thresholdSlider">🎯 Seuil de fréquence</label> |
| <div class="slider-container"> |
| <input type="range" id="thresholdSlider" min="0" max="100" value="50" step="5"> |
| <span class="value-display" id="thresholdValue">50%</span> |
| </div> |
| <div class="help-text">Masque les nœuds/arêtes en dessous du seuil</div> |
| </div> |
|
|
| |
| <div class="control-group"> |
| <label for="fuzzySlider">🔤 Seuil Fuzzy (clustering)</label> |
| <div class="slider-container"> |
| <input type="range" id="fuzzySlider" min="50" max="100" value="70" step="5"> |
| <span class="value-display" id="fuzzyValue">70%</span> |
| </div> |
| <div class="help-text">Tolérance orthographique (70 = flexible)</div> |
| </div> |
|
|
| |
| <div class="control-group"> |
| <label for="semanticSlider">🧠 Seuil Sémantique (clustering)</label> |
| <div class="slider-container"> |
| <input type="range" id="semanticSlider" min="50" max="100" value="70" step="5"> |
| <span class="value-display" id="semanticValue">70%</span> |
| </div> |
| <div class="help-text">Tolérance de sens (70 = flexible)</div> |
| </div> |
|
|
| |
| <button onclick="recomputeAggregation()">🔄 Recalculer l'agrégation</button> |
|
|
| |
| <div class="stats"> |
| <div class="stat-item"> |
| <div class="stat-value" id="visibleNodes">-</div> |
| <div class="stat-label">Nœuds</div> |
| </div> |
| <div class="stat-item"> |
| <div class="stat-value" id="visibleEdges">-</div> |
| <div class="stat-label">Arêtes</div> |
| </div> |
| </div> |
|
|
| |
| <div id="legend"> |
| <h4>🎨 Code couleur (fréquence)</h4> |
| <div class="legend-item"> |
| <div class="legend-color" style="background: #27ae60;"></div> |
| <span><strong>≥75%</strong> - Universels</span> |
| </div> |
| <div class="legend-item"> |
| <div class="legend-color" style="background: #3498db;"></div> |
| <span><strong>50-74%</strong> - Fréquents</span> |
| </div> |
| <div class="legend-item"> |
| <div class="legend-color" style="background: #f39c12;"></div> |
| <span><strong>25-49%</strong> - Moyens</span> |
| </div> |
| <div class="legend-item"> |
| <div class="legend-color" style="background: #e74c3c;"></div> |
| <span><strong><25%</strong> - Rares</span> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="info"> |
| <p><strong>Documents:</strong> <span id="doc-count">-</span></p> |
| <p><strong>Clustering:</strong> <span id="clustering-info">-</span></p> |
| </div> |
|
|
| <script> |
| console.log("🚀 Script démarré"); |
| |
| |
| let allNodes = []; |
| let allEdges = []; |
| let network = null; |
| let nodesDataset = null; |
| let edgesDataset = null; |
| let metadata = {}; |
| |
| |
| |
| |
| function toggleControlPanel() { |
| const controls = document.getElementById('controls'); |
| const reopenButton = document.getElementById('reopenButton'); |
| const isClosing = !controls.classList.contains('closed'); |
| |
| if (isClosing) { |
| |
| controls.classList.add('closed'); |
| reopenButton.classList.add('visible'); |
| console.log("📦 Panneau de contrôle fermé"); |
| } else { |
| |
| controls.classList.remove('closed'); |
| reopenButton.classList.remove('visible'); |
| console.log("📦 Panneau de contrôle ouvert"); |
| } |
| } |
| |
| |
| |
| |
| document.addEventListener('keydown', (e) => { |
| if (e.key === 'c' || e.key === 'C') { |
| toggleControlPanel(); |
| } |
| }); |
| |
| |
| |
| |
| function getNodeColor(freq) { |
| if (freq >= 75) return { background: '#27ae60', border: '#229954' }; |
| if (freq >= 50) return { background: '#3498db', border: '#2980b9' }; |
| if (freq >= 25) return { background: '#f39c12', border: '#e67e22' }; |
| return { background: '#e74c3c', border: '#c0392b' }; |
| } |
| |
| |
| |
| |
| function updateGraph(threshold) { |
| console.log(`🔄 Filtrage dynamique avec seuil: ${threshold}%`); |
| |
| const filteredNodes = allNodes.filter(n => n.freq >= threshold); |
| const filteredNodeIds = new Set(filteredNodes.map(n => n.id)); |
| |
| const filteredEdges = allEdges.filter(e => |
| e.freq >= threshold && |
| filteredNodeIds.has(e.source) && |
| filteredNodeIds.has(e.target) |
| ); |
| |
| console.log(`📊 Nœuds affichés: ${filteredNodes.length} (≥${threshold}%)`); |
| console.log(`📊 Arêtes affichées: ${filteredEdges.length} (≥${threshold}%)`); |
| |
| const edgeMap = new Map(); |
| filteredEdges.forEach(e => { |
| const key = `${e.source}→${e.target}`; |
| if (edgeMap.has(key)) { |
| const existing = edgeMap.get(key); |
| if (e.freq > existing.freq) { |
| edgeMap.set(key, e); |
| } |
| } else { |
| edgeMap.set(key, e); |
| } |
| }); |
| |
| const uniqueEdges = Array.from(edgeMap.values()); |
| |
| const visNodes = filteredNodes.map(n => { |
| const colors = getNodeColor(n.freq); |
| return { |
| id: n.id, |
| label: n.label, |
| title: `${n.label}\nFréquence: ${n.freq}%`, |
| value: n.freq, |
| color: colors, |
| font: { |
| size: Math.max(16, 18 + n.freq / 8), |
| face: "Segoe UI, Arial", |
| color: "#2c3e50", |
| bold: { face: "Segoe UI, Arial", mod: "bold" }, |
| strokeWidth: 0, |
| strokeColor: "transparent" |
| }, |
| margin: 12 |
| }; |
| }); |
| |
| const visEdges = uniqueEdges.map(e => { |
| const width = Math.max(2, e.freq / 12); |
| return { |
| from: e.source, |
| to: e.target, |
| label: `${e.freq}%`, |
| font: { |
| size: 13, |
| align: "top", |
| color: "#34495e", |
| background: "rgba(255, 255, 255, 0.92)", |
| strokeWidth: 0, |
| bold: true |
| }, |
| width: width, |
| color: { |
| color: "#4a5568", |
| highlight: "#667eea", |
| hover: "#667eea", |
| opacity: 0.85 |
| }, |
| arrows: { |
| to: { |
| enabled: true, |
| scaleFactor: 0.9, |
| type: "arrow" |
| } |
| }, |
| smooth: { |
| enabled: true, |
| type: "cubicBezier", |
| forceDirection: "vertical", |
| roundness: 0.5 |
| }, |
| shadow: { |
| enabled: true, |
| color: "rgba(0,0,0,0.12)", |
| size: 5, |
| x: 2, |
| y: 2 |
| } |
| }; |
| }); |
| |
| nodesDataset.clear(); |
| edgesDataset.clear(); |
| nodesDataset.add(visNodes); |
| edgesDataset.add(visEdges); |
| |
| document.getElementById('visibleNodes').textContent = filteredNodes.length; |
| document.getElementById('visibleEdges').textContent = uniqueEdges.length; |
| |
| if (filteredNodes.length === 0) { |
| document.getElementById('error').innerHTML = |
| `<h3>⚠️ Graphe vide</h3> |
| <p>Aucun nœud au-dessus de ${threshold}%</p> |
| <p style="font-size:13px; color:#95a5a6;">→ Diminuez le seuil avec le curseur</p>`; |
| document.getElementById('error').style.display = 'block'; |
| } else { |
| document.getElementById('error').style.display = 'none'; |
| } |
| } |
| |
| |
| |
| |
| function recomputeAggregation() { |
| const fuzzy = document.getElementById('fuzzySlider').value; |
| const semantic = document.getElementById('semanticSlider').value; |
| const threshold = document.getElementById('thresholdSlider').value; |
| |
| console.log(`🔄 Recalcul avec fuzzy=${fuzzy}%, semantic=${semantic}%, threshold=${threshold}%`); |
| |
| alert(`🔄 Recalcul demandé !\n\nPour recalculer l'agrégation avec:\n• Fuzzy: ${fuzzy}%\n• Sémantique: ${semantic}%\n• Seuil: ${threshold}%\n\n→ Cliquez à nouveau sur "Agréger les mindmaps" dans l'interface Gradio`); |
| } |
| |
| |
| |
| |
| document.getElementById('thresholdSlider').addEventListener('input', (e) => { |
| const value = parseInt(e.target.value); |
| document.getElementById('thresholdValue').textContent = `${value}%`; |
| updateGraph(value); |
| }); |
| |
| document.getElementById('fuzzySlider').addEventListener('input', (e) => { |
| document.getElementById('fuzzyValue').textContent = `${e.target.value}%`; |
| }); |
| |
| document.getElementById('semanticSlider').addEventListener('input', (e) => { |
| document.getElementById('semanticValue').textContent = `${e.target.value}%`; |
| }); |
| |
| |
| |
| |
| const jsonPath = "./aggregate.json"; |
| |
| fetch(jsonPath) |
| .then(response => { |
| console.log("📡 Status:", response.status); |
| if (!response.ok) { |
| throw new Error(`HTTP ${response.status}`); |
| } |
| return response.json(); |
| }) |
| .then(graph => { |
| console.log("✅ JSON chargé:", graph); |
| |
| document.getElementById("loading").style.display = "none"; |
| |
| metadata = graph.metadata || {}; |
| allNodes = graph.nodes || []; |
| allEdges = graph.edges || []; |
| |
| console.log(`📊 ${allNodes.length} nœuds, ${allEdges.length} arêtes`); |
| |
| document.getElementById("doc-count").textContent = metadata.doc_count || "?"; |
| |
| const clusteringInfo = metadata.clustering_enabled |
| ? `Hybride (F:${metadata.fuzzy_threshold}% + S:${metadata.semantic_threshold}%)` |
| : "Désactivé"; |
| document.getElementById("clustering-info").textContent = clusteringInfo; |
| |
| if (metadata.fuzzy_threshold) { |
| document.getElementById('fuzzySlider').value = metadata.fuzzy_threshold; |
| document.getElementById('fuzzyValue').textContent = `${metadata.fuzzy_threshold}%`; |
| } |
| if (metadata.semantic_threshold) { |
| document.getElementById('semanticSlider').value = metadata.semantic_threshold; |
| document.getElementById('semanticValue').textContent = `${metadata.semantic_threshold}%`; |
| } |
| |
| if (allNodes.length === 0) { |
| document.getElementById("error").innerHTML = |
| "<h3>⚠️ Aucune donnée</h3><p>Le fichier aggregate.json est vide</p>"; |
| document.getElementById("error").style.display = "block"; |
| return; |
| } |
| |
| nodesDataset = new vis.DataSet(); |
| edgesDataset = new vis.DataSet(); |
| |
| const container = document.getElementById("container"); |
| const data = { nodes: nodesDataset, edges: edgesDataset }; |
| |
| const options = { |
| nodes: { |
| shape: 'dot', |
| scaling: { |
| min: 20, |
| max: 70, |
| label: { enabled: true, min: 12, max: 20 } |
| }, |
| shadow: { |
| enabled: true, |
| color: 'rgba(0,0,0,0.2)', |
| size: 10, |
| x: 2, |
| y: 2 |
| }, |
| borderWidth: 3, |
| borderWidthSelected: 5 |
| }, |
| edges: { |
| smooth: { |
| type: "continuous", |
| roundness: 0.2 |
| }, |
| shadow: false |
| }, |
| physics: { |
| enabled: true, |
| stabilization: { |
| enabled: true, |
| iterations: 1000, |
| updateInterval: 25 |
| }, |
| barnesHut: { |
| gravitationalConstant: -8000, |
| springLength: 200, |
| springConstant: 0.001, |
| damping: 0.5, |
| avoidOverlap: 1 |
| } |
| }, |
| layout: { |
| improvedLayout: true, |
| hierarchical: false |
| }, |
| interaction: { |
| hover: true, |
| tooltipDelay: 100, |
| zoomView: true, |
| dragView: true, |
| dragNodes: true, |
| navigationButtons: false |
| } |
| }; |
| |
| network = new vis.Network(container, data, options); |
| |
| network.once('stabilizationIterationsDone', function() { |
| console.log("✅ Graphe stabilisé"); |
| network.setOptions({ physics: false }); |
| }); |
| |
| const initialThreshold = metadata.threshold || 50; |
| document.getElementById('thresholdSlider').value = initialThreshold; |
| document.getElementById('thresholdValue').textContent = `${initialThreshold}%`; |
| |
| updateGraph(initialThreshold); |
| |
| console.log("✅ Visualisation créée avec succès"); |
| }) |
| .catch(err => { |
| console.error("❌ ERREUR:", err); |
| document.getElementById("loading").style.display = "none"; |
| document.getElementById("error").innerHTML = |
| `<h3>❌ Erreur de chargement</h3> |
| <p>${err.message}</p> |
| <p style="font-size:11px;">Fichier: ${jsonPath}</p> |
| <p style="font-size:11px;">Vérifiez que le serveur HTTP est lancé</p>`; |
| document.getElementById("error").style.display = "block"; |
| }); |
| </script> |
| </body> |
| </html> |