SorrelC commited on
Commit
c83150a
·
verified ·
1 Parent(s): 2a047e0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +135 -336
app.py CHANGED
@@ -1,8 +1,8 @@
1
  import gradio as gr
2
- import plotly.graph_objects as go
3
  import networkx as nx
4
- import pandas as pd
5
- from collections import defaultdict
 
6
 
7
  # Entity type colors
8
  ENTITY_COLORS = {
@@ -75,141 +75,68 @@ class NetworkGraphBuilder:
75
 
76
  return G
77
 
78
- def create_plotly_graph(self, G, layout_type='spring'):
79
- """Create interactive Plotly visualization"""
80
  if len(G.nodes) == 0:
81
  return None
82
 
83
- # Choose layout
84
- if layout_type == 'spring':
85
- pos = nx.spring_layout(G, k=2, iterations=50)
86
- elif layout_type == 'circular':
87
- pos = nx.circular_layout(G)
88
- elif layout_type == 'kamada_kawai':
89
- pos = nx.kamada_kawai_layout(G)
90
- else:
91
- pos = nx.shell_layout(G)
92
-
93
- # Create edge traces
94
- edge_traces = []
95
- edge_labels = []
96
-
97
- for edge in G.edges(data=True):
98
- x0, y0 = pos[edge[0]]
99
- x1, y1 = pos[edge[1]]
100
-
101
- # Edge line
102
- edge_trace = go.Scatter(
103
- x=[x0, x1, None],
104
- y=[y0, y1, None],
105
- mode='lines',
106
- line=dict(width=2, color='#888'),
107
- hoverinfo='none',
108
- showlegend=False
109
- )
110
- edge_traces.append(edge_trace)
111
-
112
- # Edge label (relationship type)
113
- rel_type = edge[2].get('relationship', '')
114
- edge_label = go.Scatter(
115
- x=[(x0 + x1) / 2],
116
- y=[(y0 + y1) / 2],
117
- mode='text',
118
- text=[rel_type],
119
- textfont=dict(size=10, color='#555'),
120
- hoverinfo='text',
121
- hovertext=f"{edge[0]} → {rel_type} → {edge[1]}",
122
- showlegend=False
123
- )
124
- edge_labels.append(edge_label)
125
 
126
- # Create node traces (one per entity type for legend)
127
- node_traces = {}
128
  for node, data in G.nodes(data=True):
129
  entity_type = data.get('entity_type', 'UNKNOWN')
 
130
 
131
- if entity_type not in node_traces:
132
- node_traces[entity_type] = {
133
- 'x': [],
134
- 'y': [],
135
- 'text': [],
136
- 'hovertext': [],
137
- 'degree': []
138
- }
139
-
140
- x, y = pos[node]
141
- node_traces[entity_type]['x'].append(x)
142
- node_traces[entity_type]['y'].append(y)
143
- node_traces[entity_type]['text'].append(node)
144
 
145
- # Create hover text with connections
146
  connections = list(G.neighbors(node))
147
- hover_info = f"<b>{node}</b><br>"
148
- hover_info += f"Type: {entity_type}<br>"
149
- hover_info += f"Connections: {len(connections)}<br>"
150
  if connections:
151
- hover_info += f"Connected to: {', '.join(connections[:5])}"
152
  if len(connections) > 5:
153
- hover_info += f"... and {len(connections) - 5} more"
154
 
155
- node_traces[entity_type]['hovertext'].append(hover_info)
156
- node_traces[entity_type]['degree'].append(G.degree(node))
157
 
158
- # Create Plotly traces for each entity type
159
- data = edge_traces + edge_labels
 
 
160
 
161
- for entity_type, trace_data in node_traces.items():
162
- # Calculate node sizes based on degree
163
- max_degree = max(trace_data['degree']) if trace_data['degree'] else 1
164
- sizes = [20 + (degree / max_degree) * 30 for degree in trace_data['degree']]
165
-
166
- node_trace = go.Scatter(
167
- x=trace_data['x'],
168
- y=trace_data['y'],
169
- mode='markers+text',
170
- marker=dict(
171
- size=sizes,
172
- color=ENTITY_COLORS.get(entity_type, '#CCCCCC'),
173
- line=dict(width=2, color='white')
174
- ),
175
- text=trace_data['text'],
176
- textposition='top center',
177
- textfont=dict(size=10, color='#333'),
178
- hovertext=trace_data['hovertext'],
179
- hoverinfo='text',
180
- name=entity_type,
181
- showlegend=True
182
- )
183
- data.append(node_trace)
184
-
185
- # Create figure
186
- fig = go.Figure(
187
- data=data,
188
- layout=go.Layout(
189
- title=dict(
190
- text='<b>Entity Network Graph</b><br><sub>Node size indicates number of connections</sub>',
191
- x=0.5,
192
- xanchor='center'
193
- ),
194
- showlegend=True,
195
- hovermode='closest',
196
- margin=dict(b=20, l=5, r=5, t=80),
197
- xaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
198
- yaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
199
- plot_bgcolor='#fafafa',
200
- height=700,
201
- legend=dict(
202
- title=dict(text='<b>Entity Types</b>'),
203
- orientation='v',
204
- yanchor='top',
205
- y=1,
206
- xanchor='left',
207
- x=1.02
208
- )
209
- )
210
- )
211
 
212
- return fig
 
 
 
213
 
214
  def collect_entities_from_records(*args):
215
  """Collect all entities from the input fields"""
@@ -246,18 +173,18 @@ def collect_entities_from_records(*args):
246
 
247
  # Create summary
248
  summary = f"""
249
- ### 📊 Identified Entities
250
- - **Total entities:** {len(builder.entities)}
251
- - **People:** {sum(1 for e in builder.entities if e['type'] == 'PERSON')}
252
- - **Locations:** {sum(1 for e in builder.entities if e['type'] == 'LOCATION')}
253
- - **Events:** {sum(1 for e in builder.entities if e['type'] == 'EVENT')}
254
- - **Organizations:** {sum(1 for e in builder.entities if e['type'] == 'ORGANIZATION')}
255
- - **Dates:** {sum(1 for e in builder.entities if e['type'] == 'DATE')}
256
-
257
- Now define relationships between these entities on the right →
258
- """
259
 
260
- # Return summary and update all dropdowns (5 relationships × 2 dropdowns each = 10 updates)
261
  dropdown_updates = [gr.update(choices=entity_names, value=None)] * 10
262
  return [summary] + dropdown_updates
263
 
@@ -266,7 +193,7 @@ def generate_network_graph(*args):
266
  try:
267
  builder = NetworkGraphBuilder()
268
 
269
- # Collect entities (first 30 args: 6 records × 5 fields)
270
  num_records = 6
271
  fields_per_record = 5
272
 
@@ -291,7 +218,7 @@ def generate_network_graph(*args):
291
  if date:
292
  builder.add_entity(date, 'DATE', record_id)
293
 
294
- # Collect relationships (next args: 5 relationships × 3 fields)
295
  relationship_start = 30
296
  num_relationships = 5
297
 
@@ -304,33 +231,28 @@ def generate_network_graph(*args):
304
  if source and target:
305
  builder.add_relationship(source, target, rel_type)
306
 
307
- # Get layout type (last arg)
308
- layout_type = args[-1] if len(args) > relationship_start else 'spring'
309
-
310
  # Build graph
311
  G = builder.build_graph()
312
 
313
  if len(G.nodes) == 0:
314
  return None, "❌ **No entities to display.** Please enter entities in Step 1 and click 'Identify Entities' first."
315
 
316
- # Create visualization (even if no relationships, show isolated nodes)
317
- fig = builder.create_plotly_graph(G, layout_type)
318
 
319
  # Create statistics
320
  stats = f"""
321
- ### 📈 Network Statistics
322
- - **Nodes (Entities):** {G.number_of_nodes()}
323
- - **Edges (Relationships):** {G.number_of_edges()}
324
- """
325
 
326
  if len(G.edges) == 0:
327
  stats += "\n⚠️ **No relationships defined** - showing isolated nodes only.\n"
328
- stats += "\n*Define relationships in Step 2 to see connections between entities.*\n"
329
  else:
330
  stats += f"- **Network Density:** {nx.density(G):.3f}\n"
331
  stats += f"- **Average Connections per Node:** {sum(dict(G.degree()).values()) / G.number_of_nodes():.2f}\n"
332
-
333
- if G.number_of_edges() > 0:
334
  # Find most connected nodes
335
  degrees = dict(G.degree())
336
  top_nodes = sorted(degrees.items(), key=lambda x: x[1], reverse=True)[:3]
@@ -338,19 +260,21 @@ def generate_network_graph(*args):
338
  for node, degree in top_nodes:
339
  stats += f"- {node}: {degree} connections\n"
340
 
341
- return fig, stats
342
 
343
  except Exception as e:
 
 
344
  error_msg = f"""
345
- ### ❌ Error Generating Graph
346
-
347
- An error occurred: {str(e)}
348
-
349
- **Troubleshooting:**
350
- 1. Make sure you've clicked "Identify Entities" first
351
- 2. Check that you have at least one entity entered
352
- 3. If problem persists, try refreshing the page
353
- """
354
  return None, error_msg
355
 
356
  def create_interface():
@@ -359,232 +283,107 @@ def create_interface():
359
  # Basic Network Explorer
360
 
361
  Build interactive social network graphs by entering entities extracted through Named Entity Recognition (NER).
362
- This tool demonstrates how NER can be used to visualize relationships and connections in text data.
363
 
364
  ### How to use this tool:
365
- 1. **📝 Enter entities** in the records on the left (people, locations, events, organizations, dates)
366
- 2. **🔗 Click "Identify Entities"** to populate the relationship dropdowns
367
- 3. **🤝 Define relationships** on the right by selecting entities and connection types
368
- 4. **🎨 Click "Generate Network Graph"** to visualize your network
369
- 5. **👁️ Explore** the interactive graph - hover over nodes and edges for details
370
- 6. **🔄 Refresh the page** to start over with new data
371
  """)
372
 
373
- # Add tip box
374
  gr.HTML("""
375
  <div style="background-color: #fff3cd; border: 1px solid #ffeaa7; border-radius: 8px; padding: 12px; margin: 15px 0;">
376
- <strong style="color: #856404;">💡 Top tip:</strong> This tool works best when you have already identified entities from text using NER. Try the NER Explorer Tool first to extract entities automatically!
377
  </div>
378
  """)
379
 
380
- # Entity input section
381
  entity_inputs = []
382
 
383
- # Two-column layout: Entities on left, Relationships on right
384
  with gr.Row():
385
- # LEFT COLUMN: Entity Inputs
386
  with gr.Column(scale=1):
387
- with gr.Accordion("📚 Step 1: Enter Entities from Your Records", open=True):
388
- # First 4 records (always visible)
389
  for i in range(4):
390
  with gr.Group():
391
- gr.Markdown(f"### Record {i+1}")
392
- with gr.Row():
393
- person = gr.Textbox(label="👤 Person", placeholder="e.g., Winston Churchill")
394
- location = gr.Textbox(label="📍 Location", placeholder="e.g., London")
395
- with gr.Row():
396
- event = gr.Textbox(label="📅 Event", placeholder="e.g., Battle of Britain")
397
- org = gr.Textbox(label="🏢 Organization", placeholder="e.g., Royal Air Force")
398
  date = gr.Textbox(label="🗓️ Date", placeholder="e.g., 1940")
399
-
400
  entity_inputs.extend([person, location, event, org, date])
401
 
402
- # Additional records (collapsible)
403
- with gr.Accordion("➕ Additional Records (5-6)", open=False):
404
  for i in range(4, 6):
405
  with gr.Group():
406
- gr.Markdown(f"### Record {i+1}")
407
- with gr.Row():
408
- person = gr.Textbox(label="👤 Person", placeholder="e.g., Winston Churchill")
409
- location = gr.Textbox(label="📍 Location", placeholder="e.g., London")
410
- with gr.Row():
411
- event = gr.Textbox(label="📅 Event", placeholder="e.g., Battle of Britain")
412
- org = gr.Textbox(label="🏢 Organization", placeholder="e.g., Royal Air Force")
413
- date = gr.Textbox(label="🗓️ Date", placeholder="e.g., 1940")
414
-
415
  entity_inputs.extend([person, location, event, org, date])
416
 
417
  collect_btn = gr.Button("🔍 Identify Entities", variant="primary", size="lg")
418
  entity_summary = gr.Markdown()
419
 
420
- # RIGHT COLUMN: Relationship Builder (ALWAYS VISIBLE)
421
  with gr.Column(scale=1):
422
- with gr.Accordion("🤝 Step 2: Define Relationships Between Entities", open=True):
423
- gr.Markdown("*First identify entities, then define relationships below:*")
424
-
425
  relationship_inputs = []
426
-
427
  for i in range(5):
428
  with gr.Row():
429
- source = gr.Dropdown(label=f"From", choices=[], interactive=True, scale=2)
430
- rel_type = gr.Dropdown(
431
- label="Type",
432
- choices=RELATIONSHIP_TYPES,
433
- value="related_to",
434
- interactive=True,
435
- scale=2
436
- )
437
- target = gr.Dropdown(label=f"To", choices=[], interactive=True, scale=2)
438
-
439
  relationship_inputs.extend([source, rel_type, target])
440
 
441
- with gr.Accordion("🎨 Step 3: Customize and Generate", open=True):
442
- layout_type = gr.Dropdown(
443
- label="Graph Layout",
444
- choices=['spring', 'circular', 'kamada_kawai', 'shell'],
445
- value='spring',
446
- info="Choose how nodes are arranged"
447
- )
448
-
449
- generate_btn = gr.Button("🔍 Generate Network Graph", variant="primary", size="lg")
450
-
451
- # Output section
452
- gr.HTML("<hr style='margin: 30px 0;'>")
453
-
454
- with gr.Row():
455
- network_stats = gr.Markdown()
456
-
457
- with gr.Row():
458
- network_plot = gr.Plot(label="Interactive Network Graph")
459
 
460
  # Examples
461
- with gr.Column():
462
- gr.Markdown("""
463
- ### 💡 No example entities to test? No problem!
464
- Simply click on one of the examples provided below, and the fields will be populated for you.
465
- """, elem_id="examples-heading")
466
  gr.Examples(
467
- examples=[
468
- [
469
- # === ENTITY RECORDS ===
470
- # Record 1
471
- "Winston Churchill", "London", "Battle of Britain", "War Cabinet", "1940",
472
- # Record 2
473
- "Clement Attlee", "London", "Potsdam Conference", "Labour Party", "1945",
474
- # Record 3
475
- "Field Marshal Montgomery", "North Africa", "Battle of El Alamein", "Eighth Army", "1942",
476
- # Record 4
477
- "Winston Churchill", "Yalta", "Yalta Conference", "War Cabinet", "February 1945",
478
- # Record 5
479
- "King George VI", "London", "Victory in Europe Day", "British Monarchy", "May 1945",
480
- # Record 6
481
- "Field Marshal Montgomery", "Lüneburg Heath", "German Surrender", "British Army", "May 1945",
482
- # === RELATIONSHIPS ===
483
- # Relationship 1
484
- "Winston Churchill", "works_with", "Clement Attlee",
485
- # Relationship 2
486
- "Winston Churchill", "participated_in", "Battle of Britain",
487
- # Relationship 3
488
- "Field Marshal Montgomery", "participated_in", "Battle of El Alamein",
489
- # Relationship 4
490
- "Winston Churchill", "participated_in", "Yalta Conference",
491
- # Relationship 5
492
- "Clement Attlee", "participated_in", "Potsdam Conference",
493
- # Layout type
494
- "spring"
495
- ],
496
- [
497
- # === ENTITY RECORDS ===
498
- # Record 1 - Pride and Prejudice
499
- "Elizabeth Bennet", "Longbourn", "Meryton Assembly", "", "Autumn 1811",
500
- # Record 2
501
- "Mr Darcy", "Pemberley", "Meryton Assembly", "", "Autumn 1811",
502
- # Record 3
503
- "Jane Bennet", "Longbourn", "Netherfield Ball", "", "November 1811",
504
- # Record 4
505
- "Mr Bingley", "Netherfield", "Netherfield Ball", "", "November 1811",
506
- # Record 5
507
- "Elizabeth Bennet", "Rosings", "Easter Visit", "", "Spring 1812",
508
- # Record 6
509
- "Mr Darcy", "Rosings", "First Proposal", "", "Spring 1812",
510
- # === RELATIONSHIPS ===
511
- # Relationship 1
512
- "Elizabeth Bennet", "knows", "Mr Darcy",
513
- # Relationship 2
514
- "Jane Bennet", "knows", "Mr Bingley",
515
- # Relationship 3
516
- "Elizabeth Bennet", "located_in", "Longbourn",
517
- # Relationship 4
518
- "Mr Darcy", "located_in", "Pemberley",
519
- # Relationship 5
520
- "Elizabeth Bennet", "participated_in", "Meryton Assembly",
521
- # Layout type
522
- "spring"
523
- ]
524
- ],
525
- inputs=entity_inputs + relationship_inputs + [layout_type],
526
- label="Examples"
527
  )
528
 
529
- # Add custom CSS to match NER tool styling
530
- gr.HTML("""
531
- <style>
532
- /* Make the Examples label text black */
533
- .gradio-examples-label {
534
- color: black !important;
535
- }
536
- h4.examples-label, .examples-label {
537
- color: black !important;
538
- }
539
- #examples-heading + div label,
540
- #examples-heading + div .label-text {
541
- color: black !important;
542
- }
543
- </style>
544
- """)
545
-
546
- # Wire up the interface
547
- # Collect entities button - updates the relationship dropdowns
548
  collect_btn.click(
549
  fn=collect_entities_from_records,
550
  inputs=entity_inputs,
551
- outputs=[entity_summary] + relationship_inputs[::3] + relationship_inputs[2::3] # Update source and target dropdowns
552
  )
553
 
554
- # Generate graph button
555
- all_inputs = entity_inputs + relationship_inputs + [layout_type]
556
  generate_btn.click(
557
  fn=generate_network_graph,
558
  inputs=all_inputs,
559
  outputs=[network_plot, network_stats]
560
  )
561
 
562
- # Information footer
563
  gr.HTML("""
564
- <hr style="margin-top: 40px; margin-bottom: 20px;">
565
- <div style="background-color: #f8f9fa; padding: 20px; border-radius: 8px; margin-top: 20px;">
566
- <h4 style="margin-top: 0;">ℹ️ About This Tool</h4>
567
- <p style="font-size: 14px; line-height: 1.8;">
568
- This tool demonstrates how <strong>Named Entity Recognition (NER)</strong> can be combined with
569
- <strong>network analysis</strong> to visualize relationships in text data. In real-world applications,
570
- entities would be automatically extracted from text using NER models, and relationships could be
571
- identified through co-occurrence analysis, dependency parsing, or machine learning.
572
- </p>
573
- <p style="font-size: 14px; line-height: 1.8; margin-bottom: 0;">
574
- <strong>Built with:</strong> Gradio, NetworkX, and Plotly |
575
- <strong>Graph Layouts:</strong> Spring (force-directed), Circular, Kamada-Kawai, Shell
576
- </p>
577
- </div>
578
-
579
- <br>
580
- <hr style="margin-top: 40px; margin-bottom: 20px;">
581
- <div style="background-color: #f8f9fa; padding: 20px; border-radius: 8px; margin-top: 20px; text-align: center;">
582
- <p style="font-size: 14px; line-height: 1.8; margin: 0;">
583
- This <strong>Basic Network Explorer</strong> tool was created as part of a Bodleian Libraries (University of Oxford) Sassoon Research Fellowship.
584
- </p><br><br>
585
- <p style="font-size: 14px; line-height: 1.8; margin: 0;">
586
- The code for this tool was built with the aid of Claude Sonnet 4.5.
587
- </p>
588
  </div>
589
  """)
590
 
 
1
  import gradio as gr
 
2
  import networkx as nx
3
+ from pyvis.network import Network
4
+ import tempfile
5
+ import os
6
 
7
  # Entity type colors
8
  ENTITY_COLORS = {
 
75
 
76
  return G
77
 
78
+ def create_pyvis_graph(self, G):
79
+ """Create interactive pyvis visualization"""
80
  if len(G.nodes) == 0:
81
  return None
82
 
83
+ # Create pyvis network
84
+ net = Network(height="600px", width="100%", bgcolor="#fafafa", font_color="#333")
85
+ net.set_options("""
86
+ {
87
+ "physics": {
88
+ "enabled": true,
89
+ "barnesHut": {
90
+ "gravitationalConstant": -8000,
91
+ "springLength": 150,
92
+ "springConstant": 0.04
93
+ }
94
+ },
95
+ "nodes": {
96
+ "font": {
97
+ "size": 16
98
+ }
99
+ }
100
+ }
101
+ """)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
 
103
+ # Add nodes
 
104
  for node, data in G.nodes(data=True):
105
  entity_type = data.get('entity_type', 'UNKNOWN')
106
+ color = ENTITY_COLORS.get(entity_type, '#CCCCCC')
107
 
108
+ # Node size based on degree
109
+ degree = G.degree(node)
110
+ size = 20 + (degree * 5)
 
 
 
 
 
 
 
 
 
 
111
 
112
+ # Create title (tooltip)
113
  connections = list(G.neighbors(node))
114
+ title = f"{node}\nType: {entity_type}\nConnections: {len(connections)}"
 
 
115
  if connections:
116
+ title += f"\nConnected to: {', '.join(connections[:5])}"
117
  if len(connections) > 5:
118
+ title += f"... +{len(connections) - 5} more"
119
 
120
+ net.add_node(node, label=node, color=color, size=size, title=title)
 
121
 
122
+ # Add edges
123
+ for edge in G.edges(data=True):
124
+ rel_type = edge[2].get('relationship', '')
125
+ net.add_edge(edge[0], edge[1], title=rel_type, label=rel_type)
126
 
127
+ # Save to temporary file
128
+ temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.html', mode='w')
129
+ net.save_graph(temp_file.name)
130
+ temp_file.close()
131
+
132
+ # Read the HTML content
133
+ with open(temp_file.name, 'r', encoding='utf-8') as f:
134
+ html_content = f.read()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
 
136
+ # Clean up
137
+ os.unlink(temp_file.name)
138
+
139
+ return html_content
140
 
141
  def collect_entities_from_records(*args):
142
  """Collect all entities from the input fields"""
 
173
 
174
  # Create summary
175
  summary = f"""
176
+ ### 📊 Identified Entities
177
+ - **Total entities:** {len(builder.entities)}
178
+ - **People:** {sum(1 for e in builder.entities if e['type'] == 'PERSON')}
179
+ - **Locations:** {sum(1 for e in builder.entities if e['type'] == 'LOCATION')}
180
+ - **Events:** {sum(1 for e in builder.entities if e['type'] == 'EVENT')}
181
+ - **Organizations:** {sum(1 for e in builder.entities if e['type'] == 'ORGANIZATION')}
182
+ - **Dates:** {sum(1 for e in builder.entities if e['type'] == 'DATE')}
183
+
184
+ Now define relationships between these entities on the right →
185
+ """
186
 
187
+ # Return summary and update all dropdowns
188
  dropdown_updates = [gr.update(choices=entity_names, value=None)] * 10
189
  return [summary] + dropdown_updates
190
 
 
193
  try:
194
  builder = NetworkGraphBuilder()
195
 
196
+ # Collect entities
197
  num_records = 6
198
  fields_per_record = 5
199
 
 
218
  if date:
219
  builder.add_entity(date, 'DATE', record_id)
220
 
221
+ # Collect relationships
222
  relationship_start = 30
223
  num_relationships = 5
224
 
 
231
  if source and target:
232
  builder.add_relationship(source, target, rel_type)
233
 
 
 
 
234
  # Build graph
235
  G = builder.build_graph()
236
 
237
  if len(G.nodes) == 0:
238
  return None, "❌ **No entities to display.** Please enter entities in Step 1 and click 'Identify Entities' first."
239
 
240
+ # Create visualization
241
+ html_graph = builder.create_pyvis_graph(G)
242
 
243
  # Create statistics
244
  stats = f"""
245
+ ### 📈 Network Statistics
246
+ - **Nodes (Entities):** {G.number_of_nodes()}
247
+ - **Edges (Relationships):** {G.number_of_edges()}
248
+ """
249
 
250
  if len(G.edges) == 0:
251
  stats += "\n⚠️ **No relationships defined** - showing isolated nodes only.\n"
 
252
  else:
253
  stats += f"- **Network Density:** {nx.density(G):.3f}\n"
254
  stats += f"- **Average Connections per Node:** {sum(dict(G.degree()).values()) / G.number_of_nodes():.2f}\n"
255
+
 
256
  # Find most connected nodes
257
  degrees = dict(G.degree())
258
  top_nodes = sorted(degrees.items(), key=lambda x: x[1], reverse=True)[:3]
 
260
  for node, degree in top_nodes:
261
  stats += f"- {node}: {degree} connections\n"
262
 
263
+ return html_graph, stats
264
 
265
  except Exception as e:
266
+ import traceback
267
+ error_trace = traceback.format_exc()
268
  error_msg = f"""
269
+ ### ❌ Error Generating Graph
270
+
271
+ {str(e)}
272
+
273
+ **Technical details:**
274
+ ```
275
+ {error_trace}
276
+ ```
277
+ """
278
  return None, error_msg
279
 
280
  def create_interface():
 
283
  # Basic Network Explorer
284
 
285
  Build interactive social network graphs by entering entities extracted through Named Entity Recognition (NER).
 
286
 
287
  ### How to use this tool:
288
+ 1. **📝 Enter entities** in the records on the left
289
+ 2. **🔗 Click "Identify Entities"** to populate the dropdowns
290
+ 3. **🤝 Define relationships** on the right
291
+ 4. **🎨 Click "Generate Network Graph"** to visualize
292
+ 5. **👁️ Explore** - drag nodes, zoom, hover for details
 
293
  """)
294
 
 
295
  gr.HTML("""
296
  <div style="background-color: #fff3cd; border: 1px solid #ffeaa7; border-radius: 8px; padding: 12px; margin: 15px 0;">
297
+ <strong style="color: #856404;">💡 Top tip:</strong> Start with just a few entities and relationships to see how it works!
298
  </div>
299
  """)
300
 
 
301
  entity_inputs = []
302
 
 
303
  with gr.Row():
304
+ # LEFT: Entity Inputs
305
  with gr.Column(scale=1):
306
+ with gr.Accordion("📚 Step 1: Enter Entities", open=True):
 
307
  for i in range(4):
308
  with gr.Group():
309
+ gr.Markdown(f"**Record {i+1}**")
310
+ person = gr.Textbox(label="👤 Person", placeholder="e.g., Winston Churchill")
311
+ location = gr.Textbox(label="📍 Location", placeholder="e.g., London")
312
+ event = gr.Textbox(label="📅 Event", placeholder="e.g., Battle of Britain")
313
+ org = gr.Textbox(label="🏢 Organization", placeholder="e.g., War Cabinet")
 
 
314
  date = gr.Textbox(label="🗓️ Date", placeholder="e.g., 1940")
 
315
  entity_inputs.extend([person, location, event, org, date])
316
 
317
+ with gr.Accordion("➕ Records 5-6", open=False):
 
318
  for i in range(4, 6):
319
  with gr.Group():
320
+ gr.Markdown(f"**Record {i+1}**")
321
+ person = gr.Textbox(label="👤 Person")
322
+ location = gr.Textbox(label="📍 Location")
323
+ event = gr.Textbox(label="📅 Event")
324
+ org = gr.Textbox(label="🏢 Organization")
325
+ date = gr.Textbox(label="🗓️ Date")
 
 
 
326
  entity_inputs.extend([person, location, event, org, date])
327
 
328
  collect_btn = gr.Button("🔍 Identify Entities", variant="primary", size="lg")
329
  entity_summary = gr.Markdown()
330
 
331
+ # RIGHT: Relationships & Graph
332
  with gr.Column(scale=1):
333
+ with gr.Accordion("🤝 Step 2: Define Relationships", open=True):
 
 
334
  relationship_inputs = []
 
335
  for i in range(5):
336
  with gr.Row():
337
+ source = gr.Dropdown(label="From", choices=[], scale=2)
338
+ rel_type = gr.Dropdown(label="Type", choices=RELATIONSHIP_TYPES, value="related_to", scale=2)
339
+ target = gr.Dropdown(label="To", choices=[], scale=2)
 
 
 
 
 
 
 
340
  relationship_inputs.extend([source, rel_type, target])
341
 
342
+ generate_btn = gr.Button("🔍 Generate Network Graph", variant="primary", size="lg")
343
+
344
+ gr.HTML("<hr style='margin: 20px 0;'>")
345
+ network_stats = gr.Markdown()
346
+ network_plot = gr.HTML(label="Interactive Network Graph")
 
 
 
 
 
 
 
 
 
 
 
 
 
347
 
348
  # Examples
349
+ gr.Markdown("### 💡 Try an example:")
 
 
 
 
350
  gr.Examples(
351
+ examples=[[
352
+ "Winston Churchill", "London", "Battle of Britain", "War Cabinet", "1940",
353
+ "Clement Attlee", "London", "Potsdam Conference", "Labour Party", "1945",
354
+ "Field Marshal Montgomery", "North Africa", "Battle of El Alamein", "Eighth Army", "1942",
355
+ "Winston Churchill", "Yalta", "Yalta Conference", "War Cabinet", "February 1945",
356
+ "", "", "", "", "",
357
+ "", "", "", "", "",
358
+ "Winston Churchill", "works_with", "Clement Attlee",
359
+ "Winston Churchill", "participated_in", "Battle of Britain",
360
+ "Field Marshal Montgomery", "participated_in", "Battle of El Alamein",
361
+ "", "", "",
362
+ "", "", "",
363
+ ]],
364
+ inputs=entity_inputs + relationship_inputs,
365
+ label="WWII Example"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
366
  )
367
 
368
+ # Wire up
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
369
  collect_btn.click(
370
  fn=collect_entities_from_records,
371
  inputs=entity_inputs,
372
+ outputs=[entity_summary] + relationship_inputs[::3] + relationship_inputs[2::3]
373
  )
374
 
375
+ all_inputs = entity_inputs + relationship_inputs
 
376
  generate_btn.click(
377
  fn=generate_network_graph,
378
  inputs=all_inputs,
379
  outputs=[network_plot, network_stats]
380
  )
381
 
 
382
  gr.HTML("""
383
+ <hr style="margin: 40px 0;">
384
+ <div style="text-align: center; color: #666; font-size: 14px;">
385
+ <p>Basic Network Explorer | Bodleian Libraries (University of Oxford) Sassoon Research Fellowship</p>
386
+ <p>Built with the aid of Claude Sonnet 4.5</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
387
  </div>
388
  """)
389