Spaces:
Runtime error
Runtime error
| import pandas as pd | |
| import json | |
| def generate_interactive_graph(docs_df): | |
| if docs_df is None or docs_df.empty: | |
| return '''<div style="text-align:center; padding:40px; color:#6b7280; background:var(--glass, rgba(17,24,39,0.4)); border-radius:12px; border:1px solid rgba(255,255,255,0.05);"> | |
| <div style="font-size:36px; margin-bottom:12px;">🌐</div> | |
| <div style="font-weight:600;">Sin datos para el grafo</div> | |
| <div style="font-size:12px; opacity:0.7;">Ejecuta una búsqueda primero.</div> | |
| </div>''' | |
| try: | |
| from sklearn.feature_extraction.text import TfidfVectorizer | |
| from sklearn.metrics.pairwise import cosine_similarity | |
| has_sklearn = True | |
| except ImportError: | |
| has_sklearn = False | |
| nodes = [] | |
| edges = [] | |
| docs = docs_df.to_dict(orient="records") | |
| # Create Nodes | |
| for i, row in enumerate(docs): | |
| node_id = i + 1 | |
| titulo = str(row.get("Título", "")) | |
| fuente = str(row.get("Fuente", "")) | |
| autores = str(row.get("Autores", "")) | |
| año = str(row.get("Año", "")) | |
| grade = str(row.get("GRADE", "")) | |
| # Color based on source | |
| color = "#3b82f6" # default blue | |
| if "PubMed" in fuente: color = "#10b981" # green | |
| elif "ArXiv" in fuente: color = "#f59e0b" # orange | |
| elif "Semantic" in fuente: color = "#8b5cf6" # purple | |
| elif "Crossref" in fuente: color = "#ef4444" # red | |
| # Format label (APA style short) | |
| parts = [a.strip() for a in autores.split(",")] | |
| surnames = [p.split()[-1] for p in parts if p and "..." not in p] | |
| if len(surnames) == 1: cite = f"{surnames[0]} ({año})" | |
| elif len(surnames) == 2: cite = f"{surnames[0]} y {surnames[1]} ({año})" | |
| elif len(surnames) > 2: cite = f"{surnames[0]} et al. ({año})" | |
| else: cite = f"Doc {node_id}" | |
| safe_row = json.dumps(row, ensure_ascii=False).replace("'", "'").replace('"', """) | |
| nodes.append({ | |
| "id": node_id, | |
| "label": cite, | |
| "title": f"<b>{cite}</b><br>{titulo[:80]}...", | |
| "color": color, | |
| "value": 10, # default size | |
| "group": fuente, | |
| "paperData": safe_row | |
| }) | |
| # Create Edges using TF-IDF similarity on titles | |
| if len(docs) > 1 and has_sklearn: | |
| titles = [str(r.get("Título", "")) for r in docs] | |
| vectorizer = TfidfVectorizer(stop_words='english') | |
| try: | |
| tfidf_matrix = vectorizer.fit_transform(titles) | |
| sim_matrix = cosine_similarity(tfidf_matrix) | |
| for i in range(len(docs)): | |
| for j in range(i+1, len(docs)): | |
| sim = sim_matrix[i][j] | |
| if sim > 0.2: # Threshold for similarity | |
| edges.append({ | |
| "from": i + 1, | |
| "to": j + 1, | |
| "value": float(sim), | |
| "title": f"Similitud: {sim:.2f}" | |
| }) | |
| nodes[i]["value"] += (sim * 5) | |
| nodes[j]["value"] += (sim * 5) | |
| except Exception: | |
| pass # fallback if tfidf fails | |
| nodes_json = json.dumps(nodes) | |
| edges_json = json.dumps(edges) | |
| html = f""" | |
| <div style="position:relative; width:100%; height:650px; background:var(--bg, #0f172a); border-radius:12px; overflow:hidden; border:1px solid var(--glass-border, rgba(255,255,255,0.08));"> | |
| <!-- Controls --> | |
| <div style="position:absolute; top:16px; left:16px; z-index:10; background:rgba(15,23,42,0.8); backdrop-filter:blur(8px); padding:8px; border-radius:8px; border:1px solid rgba(255,255,255,0.1); display:flex; gap:8px;"> | |
| <button onclick="window.network.fit()" style="background:var(--primary, #8b5cf6); color:white; border:none; padding:6px 12px; border-radius:6px; cursor:pointer; font-weight:600; font-size:12px; transition:0.2s;" onmouseover="this.style.opacity=0.8" onmouseout="this.style.opacity=1">🔍 Centrar Grafo</button> | |
| </div> | |
| <!-- Network Container --> | |
| <div id="mynetwork" style="width:100%; height:100%;"></div> | |
| <!-- Side Panel --> | |
| <div id="graph-sidepanel" style="position:absolute; top:0; right:-400px; width:350px; height:100%; background:rgba(15,23,42,0.95); backdrop-filter:blur(16px); border-left:1px solid rgba(255,255,255,0.1); transition:right 0.3s cubic-bezier(0.4, 0, 0.2, 1); box-shadow:-10px 0 30px rgba(0,0,0,0.5); z-index:20; display:flex; flex-direction:column;"> | |
| <div style="padding:16px; border-bottom:1px solid rgba(255,255,255,0.1); display:flex; justify-content:space-between; align-items:center;"> | |
| <h3 style="margin:0; font-size:16px; font-weight:600; color:white;">Detalles del Documento</h3> | |
| <button onclick="document.getElementById('graph-sidepanel').style.right='-400px'" style="background:transparent; border:none; color:#9ca3af; cursor:pointer; font-size:24px; display:flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:16px; transition:0.2s;" onmouseover="this.style.background='rgba(255,255,255,0.1)'" onmouseout="this.style.background='transparent'">×</button> | |
| </div> | |
| <div id="graph-panel-content" style="padding:20px; overflow-y:auto; flex-grow:1; color:#e2e8f0; font-size:13px; line-height:1.6;"> | |
| Selecciona un nodo para ver los detalles. | |
| </div> | |
| </div> | |
| <script type="text/javascript" src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script> | |
| <script type="text/javascript"> | |
| // Use setTimeout to ensure DOM is ready in Gradio | |
| setTimeout(function() {{ | |
| if(typeof vis === 'undefined') return; | |
| var nodes = new vis.DataSet({nodes_json}); | |
| var edges = new vis.DataSet({edges_json}); | |
| var container = document.getElementById('mynetwork'); | |
| var data = {{ nodes: nodes, edges: edges }}; | |
| var options = {{ | |
| nodes: {{ | |
| shape: 'dot', | |
| scaling: {{ min: 10, max: 35 }}, | |
| font: {{ color: '#e2e8f0', size: 12, face: 'system-ui, -apple-system, sans-serif' }}, | |
| borderWidth: 2, | |
| shadow: {{ enabled: true, color: 'rgba(0,0,0,0.4)', size: 10 }} | |
| }}, | |
| edges: {{ | |
| color: {{ color: 'rgba(148, 163, 184, 0.2)', highlight: '#8b5cf6' }}, | |
| smooth: {{ type: 'continuous' }} | |
| }}, | |
| physics: {{ | |
| barnesHut: {{ gravitationalConstant: -2000, centralGravity: 0.2, springLength: 150 }}, | |
| stabilization: {{ iterations: 150 }} | |
| }}, | |
| interaction: {{ hover: true, tooltipDelay: 200, zoomView: true, dragView: true }} | |
| }}; | |
| window.network = new vis.Network(container, data, options); | |
| window.network.on("click", function (params) {{ | |
| if (params.nodes.length > 0) {{ | |
| var nodeId = params.nodes[0]; | |
| var node = nodes.get(nodeId); | |
| if(node && node.paperData) {{ | |
| var paper = JSON.parse(node.paperData.replace(/"/g, '"').replace(/'/g, "'")); | |
| var panel = document.getElementById('graph-sidepanel'); | |
| var content = document.getElementById('graph-panel-content'); | |
| var html = '<div style="margin-bottom:16px; display:flex; gap:8px;">'; | |
| html += '<span style="background:rgba(255,255,255,0.1); padding:4px 10px; border-radius:12px; font-size:11px; font-weight:600;">' + paper.Fuente + '</span>'; | |
| if(paper.GRADE) {{ | |
| var gradeColor = "#6b7280"; | |
| var gradeLvl = paper.GRADE.split(" - ")[0].trim().toUpperCase(); | |
| if(gradeLvl === "HIGH") gradeColor = "#10b981"; | |
| else if(gradeLvl === "MODERATE") gradeColor = "#f59e0b"; | |
| else if(gradeLvl === "LOW" || gradeLvl === "VERY LOW") gradeColor = "#ef4444"; | |
| html += '<span style="background:'+gradeColor+'15; color:'+gradeColor+'; border:1px solid '+gradeColor+'40; padding:4px 10px; border-radius:12px; font-size:11px; font-weight:600;">' + paper.GRADE + '</span>'; | |
| }} | |
| html += '</div>'; | |
| html += '<h4 style="margin:0 0 16px 0; color:white; font-size:16px; line-height:1.4;">' + paper.Título + '</h4>'; | |
| html += '<p style="color:#94a3b8; margin-bottom:8px;"><b>Autores:</b> ' + paper.Autores + '</p>'; | |
| html += '<p style="color:#94a3b8; margin-bottom:24px;"><b>Año:</b> ' + paper.Año + '</p>'; | |
| if(paper.DOI) html += '<a href="https://doi.org/' + paper.DOI + '" target="_blank" style="display:inline-block; background:rgba(59,130,246,0.1); color:#3b82f6; border:1px solid rgba(59,130,246,0.2); padding:8px 16px; border-radius:8px; text-decoration:none; margin-bottom:8px; width:100%; text-align:center; box-sizing:border-box; transition:0.2s;" onmouseover="this.style.background=\'rgba(59,130,246,0.2)\'" onmouseout="this.style.background=\'rgba(59,130,246,0.1)\'">🔗 Ver en DOI</a>'; | |
| if(paper['PDF URL']) html += '<a href="' + paper['PDF URL'] + '" target="_blank" style="display:inline-block; background:rgba(239,68,68,0.1); color:#ef4444; border:1px solid rgba(239,68,68,0.2); padding:8px 16px; border-radius:8px; text-decoration:none; margin-bottom:8px; width:100%; text-align:center; box-sizing:border-box; transition:0.2s;" onmouseover="this.style.background=\'rgba(239,68,68,0.2)\'" onmouseout="this.style.background=\'rgba(239,68,68,0.1)\'">📄 Abrir PDF</a>'; | |
| content.innerHTML = html; | |
| panel.style.right = '0'; | |
| }} | |
| }} else {{ | |
| document.getElementById('graph-sidepanel').style.right = '-400px'; | |
| }} | |
| }}); | |
| // Close panel on empty click | |
| container.addEventListener("click", function(e) {{ | |
| if(window.network.getSelection().nodes.length === 0) {{ | |
| document.getElementById('graph-sidepanel').style.right = '-400px'; | |
| }} | |
| }}); | |
| }}, 500); | |
| </script> | |
| </div> | |
| """ | |
| return html | |