SorrelC commited on
Commit
e7e67b0
·
verified ·
1 Parent(s): 06e2423

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +426 -340
app.py CHANGED
@@ -1,9 +1,11 @@
1
  import gradio as gr
2
  import networkx as nx
3
- import plotly.graph_objects as go
4
- import numpy as np
 
 
5
 
6
- # Entity type colors
7
  ENTITY_COLORS = {
8
  'PERSON': '#00B894', # Green
9
  'LOCATION': '#A0E7E5', # Light Cyan
@@ -14,6 +16,8 @@ ENTITY_COLORS = {
14
 
15
  # Relationship types for dropdown
16
  RELATIONSHIP_TYPES = [
 
 
17
  'works_with',
18
  'located_in',
19
  'participated_in',
@@ -22,17 +26,19 @@ RELATIONSHIP_TYPES = [
22
  'employed_by',
23
  'founded',
24
  'attended',
25
- 'knows',
26
- 'related_to',
27
  'collaborates_with',
28
  'married_to',
29
  'sibling_of',
30
  'parent_of',
31
  'lives_at',
32
  'wrote',
 
 
 
33
  'other'
34
  ]
35
 
 
36
  class NetworkGraphBuilder:
37
  def __init__(self):
38
  self.entities = []
@@ -82,129 +88,133 @@ class NetworkGraphBuilder:
82
 
83
  return G
84
 
85
- def create_plotly_graph(self, G):
86
- """Create interactive Plotly visualization"""
87
  if len(G.nodes) == 0:
88
  return None
89
 
90
- # Use spring layout for positioning
91
- pos = nx.spring_layout(G, k=2, iterations=50, seed=42)
92
-
93
- # Create edge traces
94
- edge_x = []
95
- edge_y = []
96
- edge_labels = []
97
- edge_mid_x = []
98
- edge_mid_y = []
99
-
100
- for edge in G.edges(data=True):
101
- x0, y0 = pos[edge[0]]
102
- x1, y1 = pos[edge[1]]
103
- edge_x.extend([x0, x1, None])
104
- edge_y.extend([y0, y1, None])
105
-
106
- # Midpoint for edge label
107
- mid_x = (x0 + x1) / 2
108
- mid_y = (y0 + y1) / 2
109
- edge_mid_x.append(mid_x)
110
- edge_mid_y.append(mid_y)
111
- edge_labels.append(edge[2].get('relationship', ''))
112
-
113
- edge_trace = go.Scatter(
114
- x=edge_x, y=edge_y,
115
- line=dict(width=2, color='#888'),
116
- hoverinfo='none',
117
- mode='lines'
118
- )
119
-
120
- # Edge labels trace
121
- edge_label_trace = go.Scatter(
122
- x=edge_mid_x, y=edge_mid_y,
123
- mode='text',
124
- text=edge_labels,
125
- textposition='middle center',
126
- textfont=dict(size=10, color='#666'),
127
- hoverinfo='none'
128
  )
129
 
130
- # Create node traces - one per entity type for legend
131
- node_traces = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
 
133
- for entity_type, color in ENTITY_COLORS.items():
134
- nodes_of_type = [n for n, d in G.nodes(data=True) if d.get('entity_type') == entity_type]
135
- if not nodes_of_type:
136
- continue
137
-
138
- node_x = []
139
- node_y = []
140
- node_text = []
141
- node_hover = []
142
- node_sizes = []
143
 
144
- for node in nodes_of_type:
145
- x, y = pos[node]
146
- node_x.append(x)
147
- node_y.append(y)
148
- node_text.append(node)
149
-
150
- # Calculate connections
151
- connections = list(G.neighbors(node))
152
- degree = len(connections)
153
- node_sizes.append(30 + (degree * 10))
154
-
155
- # Hover text
156
- hover = f"<b>{node}</b><br>Type: {entity_type}<br>Connections: {degree}"
157
- if connections:
158
- hover += f"<br>Connected to: {', '.join(connections[:5])}"
159
- if len(connections) > 5:
160
- hover += f"<br>... +{len(connections) - 5} more"
161
- node_hover.append(hover)
162
 
163
- node_trace = go.Scatter(
164
- x=node_x, y=node_y,
165
- mode='markers+text',
166
- name=entity_type,
167
- text=node_text,
168
- textposition='top center',
169
- textfont=dict(size=12, color='#333'),
170
- hoverinfo='text',
171
- hovertext=node_hover,
172
- marker=dict(
173
- size=node_sizes,
174
- color=color,
175
- line=dict(width=2, color='white'),
176
- symbol='circle'
177
- )
178
  )
179
- node_traces.append(node_trace)
180
-
181
- # Create figure
182
- fig = go.Figure(
183
- data=[edge_trace, edge_label_trace] + node_traces,
184
- layout=go.Layout(
185
- title=dict(
186
- text='Interactive Network Graph',
187
- font=dict(size=20)
188
- ),
189
- showlegend=True,
190
- legend=dict(
191
- yanchor="top",
192
- y=0.99,
193
- xanchor="left",
194
- x=0.01,
195
- bgcolor="rgba(255,255,255,0.8)"
196
- ),
197
- hovermode='closest',
198
- margin=dict(b=20, l=5, r=5, t=50),
199
- xaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
200
- yaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
201
- plot_bgcolor='#fafafa',
202
- paper_bgcolor='#fafafa',
203
- height=600
204
  )
205
- )
206
 
207
- return fig
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
 
209
 
210
  def collect_entities_from_records(
@@ -252,26 +262,43 @@ def collect_entities_from_records(
252
  'DATE': sum(1 for e in builder.entities if e['type'] == 'DATE'),
253
  }
254
 
255
- # Create summary
256
- summary = f"""
257
- ### 📊 Identified Entities ({len(builder.entities)} total)
258
- | Type | Count |
259
- |------|-------|
260
- | 👤 People | {counts['PERSON']} |
261
- | 📍 Locations | {counts['LOCATION']} |
262
- | 📅 Events | {counts['EVENT']} |
263
- | 🏢 Organizations | {counts['ORGANIZATION']} |
264
- | 🗓️ Dates | {counts['DATE']} |
265
-
266
- **Entities found:** {', '.join(entity_names) if entity_names else 'None'}
267
-
268
- ➡️ Now define relationships between these entities below
269
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
 
271
  # Return summary and update all 10 dropdowns (5 source + 5 target)
272
- # Each dropdown gets updated with the entity names
273
  return (
274
- summary,
275
  gr.update(choices=entity_names, value=None), # source 1
276
  gr.update(choices=entity_names, value=None), # target 1
277
  gr.update(choices=entity_names, value=None), # source 2
@@ -341,76 +368,87 @@ def generate_network_graph(
341
  G = builder.build_graph()
342
 
343
  if len(G.nodes) == 0:
344
- empty_fig = go.Figure()
345
- empty_fig.add_annotation(
346
- text="No entities to display.<br>Please enter entities in Step 1 and click 'Identify Entities' first.",
347
- xref="paper", yref="paper",
348
- x=0.5, y=0.5, showarrow=False,
349
- font=dict(size=16, color="#666")
350
- )
351
- empty_fig.update_layout(
352
- height=400,
353
- plot_bgcolor='#fafafa',
354
- paper_bgcolor='#fafafa'
355
- )
356
- return empty_fig, "❌ **No entities to display.** Please enter entities in Step 1 first."
357
 
358
  # Create visualization
359
- fig = builder.create_plotly_graph(G)
360
 
361
  # Create statistics
362
- stats = f"""
363
- ### 📈 Network Statistics
364
- | Metric | Value |
365
- |--------|-------|
366
- | **Nodes (Entities)** | {G.number_of_nodes()} |
367
- | **Edges (Relationships)** | {G.number_of_edges()} |
368
- """
369
-
370
- if len(G.edges) == 0:
371
- stats += "\n⚠️ **No relationships defined** - showing isolated nodes. Add relationships in Step 2!\n"
372
- else:
 
 
 
 
373
  density = nx.density(G)
374
  avg_degree = sum(dict(G.degree()).values()) / G.number_of_nodes()
375
- stats += f"| **Network Density** | {density:.3f} |\n"
376
- stats += f"| **Avg. Connections** | {avg_degree:.2f} |\n"
 
 
 
 
 
 
 
 
 
377
 
378
- # Find most connected nodes
379
  degrees = dict(G.degree())
380
  top_nodes = sorted(degrees.items(), key=lambda x: x[1], reverse=True)[:3]
381
- stats += "\n**Most Connected Entities:**\n"
 
 
 
 
382
  for node, degree in top_nodes:
383
- stats += f"- **{node}**: {degree} connections\n"
 
 
 
 
 
 
 
 
 
 
384
 
385
- return fig, stats
386
 
387
  except Exception as e:
388
  import traceback
389
  error_trace = traceback.format_exc()
390
 
391
- error_fig = go.Figure()
392
- error_fig.add_annotation(
393
- text=f"Error: {str(e)}",
394
- xref="paper", yref="paper",
395
- x=0.5, y=0.5, showarrow=False,
396
- font=dict(size=14, color="red")
397
- )
398
- error_fig.update_layout(height=300, plot_bgcolor='#fafafa', paper_bgcolor='#fafafa')
399
-
400
- error_msg = f"""
401
- ### ❌ Error Generating Graph
402
-
403
- {str(e)}
404
-
405
- <details>
406
- <summary>Technical details</summary>
407
-
408
- ```
409
- {error_trace}
410
- ```
411
- </details>
412
- """
413
- return error_fig, error_msg
414
 
415
 
416
  def load_austen_example():
@@ -419,11 +457,11 @@ def load_austen_example():
419
  # Record 1
420
  "Elizabeth Bennet", "Longbourn", "Meryton Ball", "Bennet Family", "1811",
421
  # Record 2
422
- "Mr. Darcy", "Pemberley", "Netherfield Ball", "Darcy Estate", "1811",
423
  # Record 3
424
- "Jane Bennet", "Netherfield", "", "Bennet Family", "",
425
  # Record 4
426
- "Mr. Bingley", "Netherfield", "", "", "",
427
  # Record 5
428
  "Mr. Wickham", "Meryton", "", "Militia", "",
429
  # Record 6
@@ -437,11 +475,11 @@ def load_wwii_example():
437
  # Record 1
438
  "Winston Churchill", "London", "Battle of Britain", "War Cabinet", "1940",
439
  # Record 2
440
- "Clement Attlee", "London", "Potsdam Conference", "Labour Party", "1945",
441
  # Record 3
442
  "Field Marshal Montgomery", "North Africa", "Battle of El Alamein", "Eighth Army", "1942",
443
  # Record 4
444
- "Franklin D. Roosevelt", "Washington D.C.", "D-Day", "Allied Forces", "1944",
445
  # Record 5
446
  "", "", "", "", "",
447
  # Record 6
@@ -450,136 +488,213 @@ def load_wwii_example():
450
 
451
 
452
  def create_interface():
453
- with gr.Blocks(title="Basic Network Explorer", theme=gr.themes.Soft()) as demo:
 
 
 
 
 
 
 
 
 
 
454
  gr.Markdown("""
455
- # 🕸️ Basic Network Explorer
456
-
457
- Build interactive network graphs by entering entities extracted through Named Entity Recognition (NER).
458
- Explore relationships between people, places, events, organizations and dates.
459
 
460
- ### How to use this tool:
461
- 1. **📝 Enter entities** in the records below (or load an example)
462
- 2. **🔍 Click "Identify Entities"** to collect and list all entities
463
- 3. **🤝 Define relationships** between entities using the dropdowns
464
- 4. **🎨 Click "Generate Network Graph"** to visualize
465
- 5. **👁️ Explore** - hover over nodes for details, zoom and pan the graph
466
  """)
467
 
468
- gr.HTML("""
469
- <div style="background-color: #fff3cd; border: 1px solid #ffeaa7; border-radius: 8px; padding: 12px; margin: 15px 0;">
470
- <strong style="color: #856404;">💡 Top tip:</strong> Start with just a few entities and relationships to see how it works!
471
- </div>
472
- """)
 
 
 
 
 
473
 
474
- # Store all entity input components
475
  entity_inputs = []
476
 
477
- # Example buttons
478
  with gr.Row():
479
- gr.Markdown("### 💡 Quick Start - Load an Example:")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
480
  with gr.Row():
481
- austen_btn = gr.Button("📚 Jane Austen (Pride & Prejudice)", variant="secondary")
482
- wwii_btn = gr.Button("⚔️ WWII History", variant="secondary")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
483
 
484
  gr.HTML("<hr style='margin: 20px 0;'>")
485
 
 
 
 
 
 
 
 
486
  with gr.Row():
487
- # LEFT COLUMN: Entity Inputs
488
- with gr.Column(scale=1):
489
- gr.Markdown("## 📚 Step 1: Enter Entities")
490
-
491
- with gr.Accordion("Records 1-4", open=True):
492
- for i in range(4):
493
- with gr.Group():
494
- gr.Markdown(f"**Record {i+1}**")
495
- with gr.Row():
496
- person = gr.Textbox(label="👤 Person", placeholder="e.g., Elizabeth Bennet", scale=1)
497
- location = gr.Textbox(label="📍 Location", placeholder="e.g., Longbourn", scale=1)
498
- with gr.Row():
499
- event = gr.Textbox(label="📅 Event", placeholder="e.g., Meryton Ball", scale=1)
500
- org = gr.Textbox(label="🏢 Organization", placeholder="e.g., Bennet Family", scale=1)
501
- date = gr.Textbox(label="🗓️ Date", placeholder="e.g., 1811")
502
- entity_inputs.extend([person, location, event, org, date])
503
-
504
- with gr.Accordion("Records 5-6 (Optional)", open=False):
505
- for i in range(4, 6):
506
- with gr.Group():
507
- gr.Markdown(f"**Record {i+1}**")
508
- with gr.Row():
509
- person = gr.Textbox(label="👤 Person", scale=1)
510
- location = gr.Textbox(label="📍 Location", scale=1)
511
- with gr.Row():
512
- event = gr.Textbox(label="📅 Event", scale=1)
513
- org = gr.Textbox(label="🏢 Organization", scale=1)
514
- date = gr.Textbox(label="🗓️ Date")
515
- entity_inputs.extend([person, location, event, org, date])
516
-
517
- collect_btn = gr.Button("🔍 Identify Entities", variant="primary", size="lg")
518
- entity_summary = gr.Markdown()
519
 
520
- # RIGHT COLUMN: Relationships
521
- with gr.Column(scale=1):
522
- gr.Markdown("## 🤝 Step 2: Define Relationships")
523
- gr.Markdown("*First click 'Identify Entities' to populate the dropdowns*")
524
-
525
- # Create relationship inputs with explicit variable names
526
- source1 = gr.Dropdown(label="From", choices=[], scale=2)
527
- with gr.Row():
528
- rel_type1 = gr.Dropdown(label="Relationship Type", choices=RELATIONSHIP_TYPES, value="related_to", scale=2)
529
- target1 = gr.Dropdown(label="To", choices=[], scale=2)
530
-
531
- gr.HTML("<hr style='margin: 10px 0; border-top: 1px dashed #ccc;'>")
532
-
533
- source2 = gr.Dropdown(label="From", choices=[], scale=2)
534
- with gr.Row():
535
- rel_type2 = gr.Dropdown(label="Relationship Type", choices=RELATIONSHIP_TYPES, value="related_to", scale=2)
536
- target2 = gr.Dropdown(label="To", choices=[], scale=2)
537
-
538
- gr.HTML("<hr style='margin: 10px 0; border-top: 1px dashed #ccc;'>")
539
-
540
- source3 = gr.Dropdown(label="From", choices=[], scale=2)
541
- with gr.Row():
542
- rel_type3 = gr.Dropdown(label="Relationship Type", choices=RELATIONSHIP_TYPES, value="related_to", scale=2)
543
- target3 = gr.Dropdown(label="To", choices=[], scale=2)
544
-
545
- gr.HTML("<hr style='margin: 10px 0; border-top: 1px dashed #ccc;'>")
546
-
547
- source4 = gr.Dropdown(label="From", choices=[], scale=2)
548
- with gr.Row():
549
- rel_type4 = gr.Dropdown(label="Relationship Type", choices=RELATIONSHIP_TYPES, value="related_to", scale=2)
550
- target4 = gr.Dropdown(label="To", choices=[], scale=2)
551
-
552
- gr.HTML("<hr style='margin: 10px 0; border-top: 1px dashed #ccc;'>")
553
-
554
- source5 = gr.Dropdown(label="From", choices=[], scale=2)
555
- with gr.Row():
556
- rel_type5 = gr.Dropdown(label="Relationship Type", choices=RELATIONSHIP_TYPES, value="related_to", scale=2)
557
- target5 = gr.Dropdown(label="To", choices=[], scale=2)
558
-
559
- # Collect relationship inputs
560
- relationship_inputs = [
561
- source1, rel_type1, target1,
562
- source2, rel_type2, target2,
563
- source3, rel_type3, target3,
564
- source4, rel_type4, target4,
565
- source5, rel_type5, target5
566
- ]
567
 
568
- gr.HTML("<hr style='margin: 30px 0;'>")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
569
 
570
- # Generate button
571
- generate_btn = gr.Button("🎨 Generate Network Graph", variant="primary", size="lg")
572
 
573
- # Output section
574
- gr.Markdown("## 📊 Step 3: View Results")
575
 
 
 
 
576
  with gr.Row():
577
- with gr.Column(scale=2):
578
- network_plot = gr.Plot(label="Interactive Network Graph")
579
  with gr.Column(scale=1):
580
- network_stats = gr.Markdown()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
581
 
582
- # Wire up the example buttons
 
 
583
  austen_btn.click(
584
  fn=load_austen_example,
585
  inputs=[],
@@ -592,21 +707,21 @@ def create_interface():
592
  outputs=entity_inputs
593
  )
594
 
595
- # Wire up collect entities button
596
  collect_btn.click(
597
  fn=collect_entities_from_records,
598
  inputs=entity_inputs,
599
  outputs=[
600
  entity_summary,
601
- source1, target1,
602
- source2, target2,
603
- source3, target3,
604
- source4, target4,
605
- source5, target5
606
  ]
607
  )
608
 
609
- # Wire up generate graph button
610
  all_inputs = entity_inputs + relationship_inputs
611
  generate_btn.click(
612
  fn=generate_network_graph,
@@ -614,41 +729,12 @@ def create_interface():
614
  outputs=[network_plot, network_stats]
615
  )
616
 
617
- # Color legend
618
- gr.HTML("""
619
- <div style="background-color: #f8f9fa; padding: 15px; border-radius: 8px; margin-top: 20px;">
620
- <h4 style="margin-top: 0;">🎨 Entity Color Legend</h4>
621
- <div style="display: flex; flex-wrap: wrap; gap: 15px; align-items: center;">
622
- <span style="display: flex; align-items: center; gap: 5px;">
623
- <span style="width: 20px; height: 20px; border-radius: 50%; background-color: #00B894; display: inline-block;"></span>
624
- <strong>Person</strong>
625
- </span>
626
- <span style="display: flex; align-items: center; gap: 5px;">
627
- <span style="width: 20px; height: 20px; border-radius: 50%; background-color: #A0E7E5; display: inline-block;"></span>
628
- <strong>Location</strong>
629
- </span>
630
- <span style="display: flex; align-items: center; gap: 5px;">
631
- <span style="width: 20px; height: 20px; border-radius: 50%; background-color: #4ECDC4; display: inline-block;"></span>
632
- <strong>Event</strong>
633
- </span>
634
- <span style="display: flex; align-items: center; gap: 5px;">
635
- <span style="width: 20px; height: 20px; border-radius: 50%; background-color: #55A3FF; display: inline-block;"></span>
636
- <strong>Organization</strong>
637
- </span>
638
- <span style="display: flex; align-items: center; gap: 5px;">
639
- <span style="width: 20px; height: 20px; border-radius: 50%; background-color: #FF6B6B; display: inline-block;"></span>
640
- <strong>Date</strong>
641
- </span>
642
- </div>
643
- </div>
644
- """)
645
-
646
  # Footer
647
  gr.HTML("""
648
  <hr style="margin: 40px 0 20px 0;">
649
  <div style="text-align: center; color: #666; font-size: 14px; padding: 20px;">
650
- <p><strong>Basic Network Explorer</strong> | Bodleian Libraries (University of Oxford) Sassoon Research Fellowship</p>
651
- <p>Built with the aid of Claude</p>
652
  </div>
653
  """)
654
 
 
1
  import gradio as gr
2
  import networkx as nx
3
+ from pyvis.network import Network
4
+ import tempfile
5
+ import os
6
+ import base64
7
 
8
+ # Entity type colors (matching your NER tool)
9
  ENTITY_COLORS = {
10
  'PERSON': '#00B894', # Green
11
  'LOCATION': '#A0E7E5', # Light Cyan
 
16
 
17
  # Relationship types for dropdown
18
  RELATIONSHIP_TYPES = [
19
+ 'related_to',
20
+ 'knows',
21
  'works_with',
22
  'located_in',
23
  'participated_in',
 
26
  'employed_by',
27
  'founded',
28
  'attended',
 
 
29
  'collaborates_with',
30
  'married_to',
31
  'sibling_of',
32
  'parent_of',
33
  'lives_at',
34
  'wrote',
35
+ 'visited',
36
+ 'born_in',
37
+ 'died_in',
38
  'other'
39
  ]
40
 
41
+
42
  class NetworkGraphBuilder:
43
  def __init__(self):
44
  self.entities = []
 
88
 
89
  return G
90
 
91
+ def create_pyvis_graph(self, G):
92
+ """Create interactive PyVis visualization"""
93
  if len(G.nodes) == 0:
94
  return None
95
 
96
+ # Create PyVis network with dark theme like the example
97
+ net = Network(
98
+ height="600px",
99
+ width="100%",
100
+ bgcolor="#1a1a2e",
101
+ font_color="white",
102
+ directed=False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  )
104
 
105
+ # Configure physics for interactive dragging
106
+ net.set_options("""
107
+ {
108
+ "nodes": {
109
+ "borderWidth": 2,
110
+ "borderWidthSelected": 4,
111
+ "font": {
112
+ "size": 14,
113
+ "face": "arial",
114
+ "color": "white"
115
+ },
116
+ "shadow": {
117
+ "enabled": true,
118
+ "color": "rgba(0,0,0,0.5)",
119
+ "size": 10
120
+ }
121
+ },
122
+ "edges": {
123
+ "color": {
124
+ "inherit": false
125
+ },
126
+ "smooth": {
127
+ "enabled": true,
128
+ "type": "continuous"
129
+ },
130
+ "shadow": {
131
+ "enabled": true,
132
+ "color": "rgba(0,0,0,0.3)"
133
+ }
134
+ },
135
+ "physics": {
136
+ "enabled": true,
137
+ "barnesHut": {
138
+ "gravitationalConstant": -8000,
139
+ "centralGravity": 0.3,
140
+ "springLength": 150,
141
+ "springConstant": 0.04,
142
+ "damping": 0.09
143
+ },
144
+ "stabilization": {
145
+ "enabled": true,
146
+ "iterations": 100
147
+ }
148
+ },
149
+ "interaction": {
150
+ "hover": true,
151
+ "tooltipDelay": 100,
152
+ "dragNodes": true,
153
+ "dragView": true,
154
+ "zoomView": true
155
+ }
156
+ }
157
+ """)
158
 
159
+ # Add nodes with styling based on entity type and degree
160
+ for node in G.nodes():
161
+ data = G.nodes[node]
162
+ entity_type = data.get('entity_type', 'UNKNOWN')
163
+ color = ENTITY_COLORS.get(entity_type, '#CCCCCC')
 
 
 
 
 
164
 
165
+ # Size based on connections (degree)
166
+ degree = G.degree(node)
167
+ size = 25 + (degree * 8)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
 
169
+ # Create tooltip
170
+ connections = list(G.neighbors(node))
171
+ title = f"<b>{node}</b><br>Type: {entity_type}<br>Connections: {len(connections)}"
172
+ if connections:
173
+ title += f"<br>Connected to: {', '.join(connections[:5])}"
174
+ if len(connections) > 5:
175
+ title += f"<br>... +{len(connections) - 5} more"
176
+
177
+ net.add_node(
178
+ node,
179
+ label=node,
180
+ color=color,
181
+ size=size,
182
+ title=title,
183
+ font={'size': 12, 'color': 'white'}
184
  )
185
+
186
+ # Add edges with relationship labels
187
+ for edge in G.edges(data=True):
188
+ rel_type = edge[2].get('relationship', '')
189
+ net.add_edge(
190
+ edge[0],
191
+ edge[1],
192
+ title=rel_type,
193
+ label=rel_type,
194
+ color='#888888',
195
+ width=2,
196
+ font={'size': 10, 'color': '#cccccc', 'align': 'middle'}
 
 
 
 
 
 
 
 
 
 
 
 
 
197
  )
 
198
 
199
+ # Generate HTML
200
+ html = net.generate_html()
201
+
202
+ # Wrap in iframe-compatible format
203
+ # Encode as base64 data URI for iframe src
204
+ html_bytes = html.encode('utf-8')
205
+ b64_html = base64.b64encode(html_bytes).decode('utf-8')
206
+
207
+ iframe_html = f'''
208
+ <iframe
209
+ src="data:text/html;base64,{b64_html}"
210
+ width="100%"
211
+ height="620px"
212
+ style="border: 2px solid #333; border-radius: 10px; background-color: #1a1a2e;"
213
+ sandbox="allow-scripts allow-same-origin"
214
+ ></iframe>
215
+ '''
216
+
217
+ return iframe_html
218
 
219
 
220
  def collect_entities_from_records(
 
262
  'DATE': sum(1 for e in builder.entities if e['type'] == 'DATE'),
263
  }
264
 
265
+ # Create HTML summary that spans full width
266
+ summary_html = f'''
267
+ <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 20px; border-radius: 12px; margin: 10px 0;">
268
+ <h3 style="color: white; margin: 0 0 15px 0; font-size: 18px;">📊 Identified Entities ({len(builder.entities)} total)</h3>
269
+ <div style="display: flex; flex-wrap: wrap; gap: 15px; margin-bottom: 15px;">
270
+ <div style="background: rgba(255,255,255,0.2); padding: 10px 20px; border-radius: 8px; text-align: center;">
271
+ <div style="font-size: 24px; font-weight: bold; color: white;">{counts['PERSON']}</div>
272
+ <div style="color: rgba(255,255,255,0.9); font-size: 12px;">👤 People</div>
273
+ </div>
274
+ <div style="background: rgba(255,255,255,0.2); padding: 10px 20px; border-radius: 8px; text-align: center;">
275
+ <div style="font-size: 24px; font-weight: bold; color: white;">{counts['LOCATION']}</div>
276
+ <div style="color: rgba(255,255,255,0.9); font-size: 12px;">📍 Locations</div>
277
+ </div>
278
+ <div style="background: rgba(255,255,255,0.2); padding: 10px 20px; border-radius: 8px; text-align: center;">
279
+ <div style="font-size: 24px; font-weight: bold; color: white;">{counts['EVENT']}</div>
280
+ <div style="color: rgba(255,255,255,0.9); font-size: 12px;">📅 Events</div>
281
+ </div>
282
+ <div style="background: rgba(255,255,255,0.2); padding: 10px 20px; border-radius: 8px; text-align: center;">
283
+ <div style="font-size: 24px; font-weight: bold; color: white;">{counts['ORGANIZATION']}</div>
284
+ <div style="color: rgba(255,255,255,0.9); font-size: 12px;">🏢 Organizations</div>
285
+ </div>
286
+ <div style="background: rgba(255,255,255,0.2); padding: 10px 20px; border-radius: 8px; text-align: center;">
287
+ <div style="font-size: 24px; font-weight: bold; color: white;">{counts['DATE']}</div>
288
+ <div style="color: rgba(255,255,255,0.9); font-size: 12px;">🗓️ Dates</div>
289
+ </div>
290
+ </div>
291
+ <div style="background: rgba(255,255,255,0.15); padding: 12px; border-radius: 8px;">
292
+ <div style="color: rgba(255,255,255,0.9); font-size: 13px; word-wrap: break-word;">
293
+ <strong>Entities:</strong> {', '.join(entity_names) if entity_names else 'None found'}
294
+ </div>
295
+ </div>
296
+ </div>
297
+ '''
298
 
299
  # Return summary and update all 10 dropdowns (5 source + 5 target)
 
300
  return (
301
+ summary_html,
302
  gr.update(choices=entity_names, value=None), # source 1
303
  gr.update(choices=entity_names, value=None), # target 1
304
  gr.update(choices=entity_names, value=None), # source 2
 
368
  G = builder.build_graph()
369
 
370
  if len(G.nodes) == 0:
371
+ empty_html = '''
372
+ <div style="background-color: #1a1a2e; height: 400px; display: flex; align-items: center; justify-content: center; border-radius: 10px; border: 2px solid #333;">
373
+ <div style="text-align: center; color: white;">
374
+ <div style="font-size: 48px; margin-bottom: 20px;">📊</div>
375
+ <h3>No entities to display</h3>
376
+ <p style="color: #888;">Enter entities above and click "Identify Entities" first</p>
377
+ </div>
378
+ </div>
379
+ '''
380
+ return empty_html, "❌ **No entities to display.** Please enter entities in Step 1 first."
 
 
 
381
 
382
  # Create visualization
383
+ graph_html = builder.create_pyvis_graph(G)
384
 
385
  # Create statistics
386
+ stats_html = f'''
387
+ <div style="background: #f8f9fa; padding: 15px; border-radius: 10px; border: 1px solid #ddd;">
388
+ <h4 style="margin: 0 0 10px 0;">📈 Network Statistics</h4>
389
+ <table style="width: 100%; border-collapse: collapse;">
390
+ <tr>
391
+ <td style="padding: 8px; border-bottom: 1px solid #eee;"><strong>Nodes</strong></td>
392
+ <td style="padding: 8px; border-bottom: 1px solid #eee; text-align: right;">{G.number_of_nodes()}</td>
393
+ </tr>
394
+ <tr>
395
+ <td style="padding: 8px; border-bottom: 1px solid #eee;"><strong>Edges</strong></td>
396
+ <td style="padding: 8px; border-bottom: 1px solid #eee; text-align: right;">{G.number_of_edges()}</td>
397
+ </tr>
398
+ '''
399
+
400
+ if len(G.edges) > 0:
401
  density = nx.density(G)
402
  avg_degree = sum(dict(G.degree()).values()) / G.number_of_nodes()
403
+ stats_html += f'''
404
+ <tr>
405
+ <td style="padding: 8px; border-bottom: 1px solid #eee;"><strong>Density</strong></td>
406
+ <td style="padding: 8px; border-bottom: 1px solid #eee; text-align: right;">{density:.3f}</td>
407
+ </tr>
408
+ <tr>
409
+ <td style="padding: 8px;"><strong>Avg. Connections</strong></td>
410
+ <td style="padding: 8px; text-align: right;">{avg_degree:.2f}</td>
411
+ </tr>
412
+ </table>
413
+ '''
414
 
415
+ # Most connected
416
  degrees = dict(G.degree())
417
  top_nodes = sorted(degrees.items(), key=lambda x: x[1], reverse=True)[:3]
418
+ stats_html += '''
419
+ <div style="margin-top: 15px;">
420
+ <strong>Most Connected:</strong>
421
+ <ul style="margin: 5px 0; padding-left: 20px;">
422
+ '''
423
  for node, degree in top_nodes:
424
+ stats_html += f'<li>{node}: {degree}</li>'
425
+ stats_html += '</ul></div>'
426
+ else:
427
+ stats_html += '''
428
+ </table>
429
+ <div style="margin-top: 10px; padding: 10px; background: #fff3cd; border-radius: 5px; color: #856404;">
430
+ ⚠️ No relationships defined - nodes are isolated
431
+ </div>
432
+ '''
433
+
434
+ stats_html += '</div>'
435
 
436
+ return graph_html, stats_html
437
 
438
  except Exception as e:
439
  import traceback
440
  error_trace = traceback.format_exc()
441
 
442
+ error_html = f'''
443
+ <div style="background-color: #f8d7da; height: 200px; display: flex; align-items: center; justify-content: center; border-radius: 10px; border: 2px solid #f5c6cb;">
444
+ <div style="text-align: center; color: #721c24; padding: 20px;">
445
+ <div style="font-size: 48px; margin-bottom: 10px;">⚠️</div>
446
+ <h3>Error generating graph</h3>
447
+ <p>{str(e)}</p>
448
+ </div>
449
+ </div>
450
+ '''
451
+ return error_html, f"❌ Error: {str(e)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
452
 
453
 
454
  def load_austen_example():
 
457
  # Record 1
458
  "Elizabeth Bennet", "Longbourn", "Meryton Ball", "Bennet Family", "1811",
459
  # Record 2
460
+ "Mr. Darcy", "Pemberley", "Netherfield Ball", "Darcy Estate", "",
461
  # Record 3
462
+ "Jane Bennet", "Netherfield", "", "", "",
463
  # Record 4
464
+ "Mr. Bingley", "London", "", "", "",
465
  # Record 5
466
  "Mr. Wickham", "Meryton", "", "Militia", "",
467
  # Record 6
 
475
  # Record 1
476
  "Winston Churchill", "London", "Battle of Britain", "War Cabinet", "1940",
477
  # Record 2
478
+ "Franklin D. Roosevelt", "Washington D.C.", "D-Day", "Allied Forces", "1944",
479
  # Record 3
480
  "Field Marshal Montgomery", "North Africa", "Battle of El Alamein", "Eighth Army", "1942",
481
  # Record 4
482
+ "Clement Attlee", "Potsdam", "Potsdam Conference", "Labour Party", "1945",
483
  # Record 5
484
  "", "", "", "", "",
485
  # Record 6
 
488
 
489
 
490
  def create_interface():
491
+ with gr.Blocks(title="Network Explorer", theme=gr.themes.Soft(), css="""
492
+ .record-box {
493
+ border: 1px solid #e0e0e0;
494
+ border-radius: 8px;
495
+ padding: 12px;
496
+ background: #fafafa;
497
+ }
498
+ .full-width {
499
+ width: 100%;
500
+ }
501
+ """) as demo:
502
  gr.Markdown("""
503
+ # 🕸️ Network Explorer
 
 
 
504
 
505
+ Build interactive network graphs from named entities. Enter people, places, events,
506
+ organizations and dates, then define relationships to visualize connections.
 
 
 
 
507
  """)
508
 
509
+ # Quick start buttons
510
+ gr.Markdown("### 💡 Quick Start - Load an Example:")
511
+ with gr.Row():
512
+ austen_btn = gr.Button("📚 Jane Austen (Pride & Prejudice)", variant="secondary", size="sm")
513
+ wwii_btn = gr.Button("⚔️ WWII History", variant="secondary", size="sm")
514
+
515
+ gr.HTML("<hr style='margin: 15px 0;'>")
516
+
517
+ # ==================== STEP 1: ENTITY INPUT (2x2 Grid) ====================
518
+ gr.Markdown("## 📝 Step 1: Enter Entities")
519
 
 
520
  entity_inputs = []
521
 
522
+ # First row: Records 1 & 2
523
  with gr.Row():
524
+ with gr.Column():
525
+ gr.Markdown("**Record 1**")
526
+ with gr.Group():
527
+ p1 = gr.Textbox(label="👤 Person", placeholder="e.g., Elizabeth Bennet")
528
+ l1 = gr.Textbox(label="📍 Location", placeholder="e.g., Longbourn")
529
+ e1 = gr.Textbox(label="📅 Event", placeholder="e.g., Meryton Ball")
530
+ o1 = gr.Textbox(label="🏢 Organization", placeholder="e.g., Bennet Family")
531
+ d1 = gr.Textbox(label="🗓️ Date", placeholder="e.g., 1811")
532
+ entity_inputs.extend([p1, l1, e1, o1, d1])
533
+
534
+ with gr.Column():
535
+ gr.Markdown("**Record 2**")
536
+ with gr.Group():
537
+ p2 = gr.Textbox(label="👤 Person", placeholder="e.g., Mr. Darcy")
538
+ l2 = gr.Textbox(label="📍 Location", placeholder="e.g., Pemberley")
539
+ e2 = gr.Textbox(label="📅 Event", placeholder="e.g., Netherfield Ball")
540
+ o2 = gr.Textbox(label="🏢 Organization", placeholder="e.g., Darcy Estate")
541
+ d2 = gr.Textbox(label="🗓️ Date", placeholder="")
542
+ entity_inputs.extend([p2, l2, e2, o2, d2])
543
+
544
+ # Second row: Records 3 & 4
545
  with gr.Row():
546
+ with gr.Column():
547
+ gr.Markdown("**Record 3**")
548
+ with gr.Group():
549
+ p3 = gr.Textbox(label="👤 Person", placeholder="e.g., Jane Bennet")
550
+ l3 = gr.Textbox(label="📍 Location", placeholder="e.g., Netherfield")
551
+ e3 = gr.Textbox(label="📅 Event", placeholder="")
552
+ o3 = gr.Textbox(label="🏢 Organization", placeholder="")
553
+ d3 = gr.Textbox(label="🗓️ Date", placeholder="")
554
+ entity_inputs.extend([p3, l3, e3, o3, d3])
555
+
556
+ with gr.Column():
557
+ gr.Markdown("**Record 4**")
558
+ with gr.Group():
559
+ p4 = gr.Textbox(label="👤 Person", placeholder="e.g., Mr. Bingley")
560
+ l4 = gr.Textbox(label="📍 Location", placeholder="e.g., London")
561
+ e4 = gr.Textbox(label="📅 Event", placeholder="")
562
+ o4 = gr.Textbox(label="🏢 Organization", placeholder="")
563
+ d4 = gr.Textbox(label="🗓️ Date", placeholder="")
564
+ entity_inputs.extend([p4, l4, e4, o4, d4])
565
+
566
+ # Optional records 5-6
567
+ with gr.Accordion("➕ Additional Records (5-6)", open=False):
568
+ with gr.Row():
569
+ with gr.Column():
570
+ gr.Markdown("**Record 5**")
571
+ with gr.Group():
572
+ p5 = gr.Textbox(label="👤 Person")
573
+ l5 = gr.Textbox(label="📍 Location")
574
+ e5 = gr.Textbox(label="📅 Event")
575
+ o5 = gr.Textbox(label="🏢 Organization")
576
+ d5 = gr.Textbox(label="🗓️ Date")
577
+ entity_inputs.extend([p5, l5, e5, o5, d5])
578
+
579
+ with gr.Column():
580
+ gr.Markdown("**Record 6**")
581
+ with gr.Group():
582
+ p6 = gr.Textbox(label="👤 Person")
583
+ l6 = gr.Textbox(label="📍 Location")
584
+ e6 = gr.Textbox(label="📅 Event")
585
+ o6 = gr.Textbox(label="🏢 Organization")
586
+ d6 = gr.Textbox(label="🗓️ Date")
587
+ entity_inputs.extend([p6, l6, e6, o6, d6])
588
+
589
+ # Identify button
590
+ collect_btn = gr.Button("🔍 Identify Entities", variant="primary", size="lg")
591
+
592
+ # Full-width entity summary
593
+ entity_summary = gr.HTML()
594
 
595
  gr.HTML("<hr style='margin: 20px 0;'>")
596
 
597
+ # ==================== STEP 2: RELATIONSHIPS (Grid) ====================
598
+ gr.Markdown("## 🤝 Step 2: Define Relationships")
599
+ gr.Markdown("*Select entities from the dropdowns to create connections*")
600
+
601
+ # Relationship inputs in a grid
602
+ relationship_inputs = []
603
+
604
  with gr.Row():
605
+ with gr.Column():
606
+ gr.Markdown("**Relationship 1**")
607
+ src1 = gr.Dropdown(label="From", choices=[])
608
+ rel1 = gr.Dropdown(label="Type", choices=RELATIONSHIP_TYPES, value="related_to")
609
+ tgt1 = gr.Dropdown(label="To", choices=[])
610
+ relationship_inputs.extend([src1, rel1, tgt1])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
611
 
612
+ with gr.Column():
613
+ gr.Markdown("**Relationship 2**")
614
+ src2 = gr.Dropdown(label="From", choices=[])
615
+ rel2 = gr.Dropdown(label="Type", choices=RELATIONSHIP_TYPES, value="related_to")
616
+ tgt2 = gr.Dropdown(label="To", choices=[])
617
+ relationship_inputs.extend([src2, rel2, tgt2])
618
+
619
+ with gr.Column():
620
+ gr.Markdown("**Relationship 3**")
621
+ src3 = gr.Dropdown(label="From", choices=[])
622
+ rel3 = gr.Dropdown(label="Type", choices=RELATIONSHIP_TYPES, value="related_to")
623
+ tgt3 = gr.Dropdown(label="To", choices=[])
624
+ relationship_inputs.extend([src3, rel3, tgt3])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
625
 
626
+ with gr.Row():
627
+ with gr.Column():
628
+ gr.Markdown("**Relationship 4**")
629
+ src4 = gr.Dropdown(label="From", choices=[])
630
+ rel4 = gr.Dropdown(label="Type", choices=RELATIONSHIP_TYPES, value="related_to")
631
+ tgt4 = gr.Dropdown(label="To", choices=[])
632
+ relationship_inputs.extend([src4, rel4, tgt4])
633
+
634
+ with gr.Column():
635
+ gr.Markdown("**Relationship 5**")
636
+ src5 = gr.Dropdown(label="From", choices=[])
637
+ rel5 = gr.Dropdown(label="Type", choices=RELATIONSHIP_TYPES, value="related_to")
638
+ tgt5 = gr.Dropdown(label="To", choices=[])
639
+ relationship_inputs.extend([src5, rel5, tgt5])
640
+
641
+ with gr.Column():
642
+ # Empty column for balance, or tips
643
+ gr.HTML("""
644
+ <div style="background: #e7f3ff; padding: 15px; border-radius: 8px; margin-top: 25px;">
645
+ <strong>💡 Tip:</strong> Click "Identify Entities" first to populate these dropdowns!
646
+ </div>
647
+ """)
648
 
649
+ gr.HTML("<hr style='margin: 20px 0;'>")
 
650
 
651
+ # ==================== STEP 3: GENERATE & VIEW ====================
652
+ gr.Markdown("## 🎨 Step 3: Generate Network Graph")
653
 
654
+ generate_btn = gr.Button("🎨 Generate Network Graph", variant="primary", size="lg")
655
+
656
+ # Full-width network graph
657
  with gr.Row():
658
+ with gr.Column(scale=3):
659
+ network_plot = gr.HTML(label="Interactive Network Graph")
660
  with gr.Column(scale=1):
661
+ network_stats = gr.HTML()
662
+
663
+ # Color legend
664
+ gr.HTML("""
665
+ <div style="background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); padding: 20px; border-radius: 10px; margin-top: 20px;">
666
+ <h4 style="color: white; margin: 0 0 15px 0;">🎨 Entity Color Legend</h4>
667
+ <div style="display: flex; flex-wrap: wrap; gap: 20px;">
668
+ <span style="display: flex; align-items: center; gap: 8px; color: white;">
669
+ <span style="width: 20px; height: 20px; border-radius: 50%; background-color: #00B894; display: inline-block; border: 2px solid white;"></span>
670
+ Person
671
+ </span>
672
+ <span style="display: flex; align-items: center; gap: 8px; color: white;">
673
+ <span style="width: 20px; height: 20px; border-radius: 50%; background-color: #A0E7E5; display: inline-block; border: 2px solid white;"></span>
674
+ Location
675
+ </span>
676
+ <span style="display: flex; align-items: center; gap: 8px; color: white;">
677
+ <span style="width: 20px; height: 20px; border-radius: 50%; background-color: #4ECDC4; display: inline-block; border: 2px solid white;"></span>
678
+ Event
679
+ </span>
680
+ <span style="display: flex; align-items: center; gap: 8px; color: white;">
681
+ <span style="width: 20px; height: 20px; border-radius: 50%; background-color: #55A3FF; display: inline-block; border: 2px solid white;"></span>
682
+ Organization
683
+ </span>
684
+ <span style="display: flex; align-items: center; gap: 8px; color: white;">
685
+ <span style="width: 20px; height: 20px; border-radius: 50%; background-color: #FF6B6B; display: inline-block; border: 2px solid white;"></span>
686
+ Date
687
+ </span>
688
+ </div>
689
+ <p style="color: #888; margin: 15px 0 0 0; font-size: 13px;">
690
+ 🖱️ <strong>Interaction:</strong> Drag nodes to rearrange • Scroll to zoom • Hover for details
691
+ </p>
692
+ </div>
693
+ """)
694
 
695
+ # ==================== WIRE UP EVENTS ====================
696
+
697
+ # Example buttons
698
  austen_btn.click(
699
  fn=load_austen_example,
700
  inputs=[],
 
707
  outputs=entity_inputs
708
  )
709
 
710
+ # Collect entities
711
  collect_btn.click(
712
  fn=collect_entities_from_records,
713
  inputs=entity_inputs,
714
  outputs=[
715
  entity_summary,
716
+ src1, tgt1,
717
+ src2, tgt2,
718
+ src3, tgt3,
719
+ src4, tgt4,
720
+ src5, tgt5
721
  ]
722
  )
723
 
724
+ # Generate graph
725
  all_inputs = entity_inputs + relationship_inputs
726
  generate_btn.click(
727
  fn=generate_network_graph,
 
729
  outputs=[network_plot, network_stats]
730
  )
731
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
732
  # Footer
733
  gr.HTML("""
734
  <hr style="margin: 40px 0 20px 0;">
735
  <div style="text-align: center; color: #666; font-size: 14px; padding: 20px;">
736
+ <p><strong>Network Explorer</strong> | Bodleian Libraries, University of Oxford</p>
737
+ <p style="color: #888; font-size: 12px;">Built with the aid of Claude</p>
738
  </div>
739
  """)
740