File size: 11,286 Bytes
68fb5e2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
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("'", "&#39;").replace('"', "&quot;")

        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(/&quot;/g, '"').replace(/&#39;/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