SorrelC commited on
Commit
66bba48
·
verified ·
1 Parent(s): f8219f6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +574 -61
app.py CHANGED
@@ -1,68 +1,581 @@
1
- # Basic Network Explorer - Updates
 
 
 
 
2
 
3
- ## Changes Made Based on Feedback
 
 
 
 
 
 
 
4
 
5
- ### 1. Name Change
6
- - Changed from "Basic Networks Explorer" to **"Basic Network Explorer"** (singular)
7
- - Updated throughout code and documentation
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
- ### 2. ✅ Footer Attribution Updated
10
- **Old:**
11
- - DiSc project at Oxford
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
- **New:**
14
- - "This Basic Network Explorer tool was created as part of a Bodleian Libraries (University of Oxford) Sassoon Research Fellowship."
15
- - "The code for this tool was built with the aid of Claude Sonnet 4.5."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
- ### 3. ✅ Terminology Change
18
- - Changed "Collected Entities" to **"Identified Entities"**
19
- - Updated button text from "Collect Entities" to **"Identify Entities"**
20
- - Updated instructions accordingly
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
- ### 4. ✅ Layout Restructured
23
- **Before:** Stacked vertically (busy at top)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
- **After:** Two-column layout
26
- - **Left Column:** Entity input records (Step 1)
27
- - **Right Column:** Relationship builder (Step 2) and customization (Step 3)
28
-
29
- This provides more breathing space and clearer visual organization.
30
-
31
- ### 5. ✅ Bug Fix: Network Graph Generation
32
- **Issue:** Graph wasn't generating
33
-
34
- **Root Cause:** Incorrect argument order in relationship parsing
35
- - Expected: source, target, rel_type
36
- - Actual input: source, rel_type, target
37
-
38
- **Fix:** Corrected the argument extraction order to match the actual input order
39
-
40
- **Additional Improvement:** Graph now displays even without relationships (showing isolated nodes with a warning)
41
-
42
- ### 6. ✅ British Examples with Auto-populated Relationships
43
-
44
- **Example 1: British WWII**
45
- - Winston Churchill, Clement Attlee, Field Marshal Montgomery, King George VI
46
- - Locations: London, North Africa, Yalta, Lüneburg Heath
47
- - Events: Battle of Britain, Battle of El Alamein, Yalta Conference, VE Day
48
- - **Relationships auto-populate:** works_with, participated_in connections
49
-
50
- **Example 2: Pride and Prejudice** (Already British)
51
- - Elizabeth Bennet, Mr Darcy, Jane Bennet, Mr Bingley
52
- - Locations: Longbourn, Pemberley, Rosings, Netherfield
53
- - Events: Meryton Assembly, Netherfield Ball, First Proposal
54
- - **Relationships auto-populate:** knows, located_in, participated_in connections
55
-
56
- ### Additional Improvements
57
- - Simplified relationship dropdown labels (shortened from "From Entity 1" to just "From")
58
- - Better placeholder text in entity fields (British examples)
59
- - Graph now shows isolated nodes with warning if no relationships defined
60
- - More robust error handling
61
-
62
- ## Files Updated
63
- 1. `app.py` - Main application file with all changes
64
- 2. `requirements.txt` - No changes needed
65
- 3. `README.md` - Updated with new tool name
66
-
67
- ## Ready for Deployment
68
- All files are updated and ready to upload to your Hugging Face Space: **Basic-Network-Explorer**
 
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 = {
9
+ 'PERSON': '#00B894', # Green
10
+ 'LOCATION': '#A0E7E5', # Light Cyan
11
+ 'EVENT': '#4ECDC4', # Teal
12
+ 'ORGANIZATION': '#55A3FF', # Light Blue
13
+ 'DATE': '#FF6B6B' # Red
14
+ }
15
 
16
+ # Relationship types for dropdown
17
+ RELATIONSHIP_TYPES = [
18
+ 'works_with',
19
+ 'located_in',
20
+ 'participated_in',
21
+ 'member_of',
22
+ 'occurred_at',
23
+ 'employed_by',
24
+ 'founded',
25
+ 'attended',
26
+ 'knows',
27
+ 'related_to',
28
+ 'collaborates_with',
29
+ 'other'
30
+ ]
31
 
32
+ class NetworkGraphBuilder:
33
+ def __init__(self):
34
+ self.entities = []
35
+ self.relationships = []
36
+
37
+ def add_entity(self, name, entity_type, record_id):
38
+ """Add an entity to the collection"""
39
+ if name.strip():
40
+ self.entities.append({
41
+ 'name': name.strip(),
42
+ 'type': entity_type,
43
+ 'record_id': record_id
44
+ })
45
+
46
+ def add_relationship(self, source, target, rel_type):
47
+ """Add a relationship between entities"""
48
+ if source and target and source != target:
49
+ self.relationships.append({
50
+ 'source': source.strip(),
51
+ 'target': target.strip(),
52
+ 'type': rel_type
53
+ })
54
+
55
+ def build_graph(self):
56
+ """Build NetworkX graph from entities and relationships"""
57
+ G = nx.Graph()
58
+
59
+ # Add nodes with attributes
60
+ for entity in self.entities:
61
+ G.add_node(
62
+ entity['name'],
63
+ entity_type=entity['type'],
64
+ record_id=entity['record_id']
65
+ )
66
+
67
+ # Add edges
68
+ for rel in self.relationships:
69
+ if rel['source'] in G.nodes and rel['target'] in G.nodes:
70
+ G.add_edge(
71
+ rel['source'],
72
+ rel['target'],
73
+ relationship=rel['type']
74
+ )
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"""
216
+ builder = NetworkGraphBuilder()
217
+
218
+ # Each record has 5 entity fields (person, location, event, org, date)
219
+ num_records = 6
220
+ fields_per_record = 5
221
+
222
+ for i in range(num_records):
223
+ record_id = i + 1
224
+ base_idx = i * fields_per_record
225
+
226
+ # Extract entities for this record
227
+ person = args[base_idx] if base_idx < len(args) else ""
228
+ location = args[base_idx + 1] if base_idx + 1 < len(args) else ""
229
+ event = args[base_idx + 2] if base_idx + 2 < len(args) else ""
230
+ org = args[base_idx + 3] if base_idx + 3 < len(args) else ""
231
+ date = args[base_idx + 4] if base_idx + 4 < len(args) else ""
232
+
233
+ if person:
234
+ builder.add_entity(person, 'PERSON', record_id)
235
+ if location:
236
+ builder.add_entity(location, 'LOCATION', record_id)
237
+ if event:
238
+ builder.add_entity(event, 'EVENT', record_id)
239
+ if org:
240
+ builder.add_entity(org, 'ORGANIZATION', record_id)
241
+ if date:
242
+ builder.add_entity(date, 'DATE', record_id)
243
+
244
+ # Create list of all entity names for relationship dropdowns
245
+ entity_names = [e['name'] for e in builder.entities]
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 below.
258
+ """
259
+
260
+ # Return summary and update dropdowns
261
+ return (
262
+ summary,
263
+ gr.update(visible=True), # Show relationship section
264
+ gr.update(choices=entity_names, value=None), # Update all relationship dropdowns
265
+ gr.update(choices=entity_names, value=None),
266
+ gr.update(choices=entity_names, value=None),
267
+ gr.update(choices=entity_names, value=None),
268
+ gr.update(choices=entity_names, value=None),
269
+ gr.update(choices=entity_names, value=None),
270
+ gr.update(choices=entity_names, value=None),
271
+ gr.update(choices=entity_names, value=None),
272
+ gr.update(choices=entity_names, value=None),
273
+ gr.update(choices=entity_names, value=None)
274
+ )
275
 
276
+ def generate_network_graph(*args):
277
+ """Generate the network graph from all inputs"""
278
+ builder = NetworkGraphBuilder()
279
+
280
+ # Collect entities (first 30 args: 6 records × 5 fields)
281
+ num_records = 6
282
+ fields_per_record = 5
283
+
284
+ for i in range(num_records):
285
+ record_id = i + 1
286
+ base_idx = i * fields_per_record
287
+
288
+ person = args[base_idx] if base_idx < len(args) else ""
289
+ location = args[base_idx + 1] if base_idx + 1 < len(args) else ""
290
+ event = args[base_idx + 2] if base_idx + 2 < len(args) else ""
291
+ org = args[base_idx + 3] if base_idx + 3 < len(args) else ""
292
+ date = args[base_idx + 4] if base_idx + 4 < len(args) else ""
293
+
294
+ if person:
295
+ builder.add_entity(person, 'PERSON', record_id)
296
+ if location:
297
+ builder.add_entity(location, 'LOCATION', record_id)
298
+ if event:
299
+ builder.add_entity(event, 'EVENT', record_id)
300
+ if org:
301
+ builder.add_entity(org, 'ORGANIZATION', record_id)
302
+ if date:
303
+ builder.add_entity(date, 'DATE', record_id)
304
+
305
+ # Collect relationships (next args: 5 relationships × 3 fields)
306
+ relationship_start = 30
307
+ num_relationships = 5
308
+
309
+ for i in range(num_relationships):
310
+ base_idx = relationship_start + (i * 3)
311
+ source = args[base_idx] if base_idx < len(args) else None
312
+ rel_type = args[base_idx + 1] if base_idx + 1 < len(args) else None
313
+ target = args[base_idx + 2] if base_idx + 2 < len(args) else None
314
+
315
+ if source and target:
316
+ builder.add_relationship(source, target, rel_type)
317
+
318
+ # Get layout type (last arg)
319
+ layout_type = args[-1] if len(args) > relationship_start else 'spring'
320
+
321
+ # Build graph
322
+ G = builder.build_graph()
323
+
324
+ if len(G.nodes) == 0:
325
+ return None, "❌ No entities to display. Please add some entities first."
326
+
327
+ # Create visualization (even if no relationships, show isolated nodes)
328
+ fig = builder.create_plotly_graph(G, layout_type)
329
+
330
+ # Create statistics
331
+ stats = f"""
332
+ ### 📈 Network Statistics
333
+ - **Nodes (Entities):** {G.number_of_nodes()}
334
+ - **Edges (Relationships):** {G.number_of_edges()}
335
+ """
336
+
337
+ if len(G.edges) == 0:
338
+ stats += "\n⚠️ **No relationships defined** - showing isolated nodes only.\n"
339
+ else:
340
+ stats += f"- **Network Density:** {nx.density(G):.3f}\n"
341
+ stats += f"- **Average Connections per Node:** {sum(dict(G.degree()).values()) / G.number_of_nodes():.2f}\n"
342
+
343
+ if G.number_of_edges() > 0:
344
+ # Find most connected nodes
345
+ degrees = dict(G.degree())
346
+ top_nodes = sorted(degrees.items(), key=lambda x: x[1], reverse=True)[:3]
347
+ stats += "\n**Most Connected Entities:**\n"
348
+ for node, degree in top_nodes:
349
+ stats += f"- {node}: {degree} connections\n"
350
+
351
+ return fig, stats
352
 
353
+ def create_interface():
354
+ with gr.Blocks(title="Basic Network Explorer", theme=gr.themes.Soft()) as demo:
355
+ gr.Markdown("""
356
+ # Basic Network Explorer
357
+
358
+ Build interactive social network graphs by entering entities extracted through Named Entity Recognition (NER).
359
+ This tool demonstrates how NER can be used to visualize relationships and connections in text data.
360
+
361
+ ### How to use this tool:
362
+ 1. **📝 Enter entities** in the records below (people, locations, events, organizations, dates)
363
+ 2. **🔗 Click "Identify Entities"** to gather all your inputs
364
+ 3. **🤝 Define relationships** between entities in the relationship builder
365
+ 4. **🎨 Choose a layout style** and click "Generate Network Graph"
366
+ 5. **👁️ Explore** the interactive visualization
367
+ 6. **🔄 Refresh the page** to start over with new data
368
+ """)
369
+
370
+ # Add tip box
371
+ gr.HTML("""
372
+ <div style="background-color: #fff3cd; border: 1px solid #ffeaa7; border-radius: 8px; padding: 12px; margin: 15px 0;">
373
+ <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!
374
+ </div>
375
+ """)
376
+
377
+ # Entity input section
378
+ entity_inputs = []
379
+
380
+ # Two-column layout: Entities on left, Relationships on right
381
+ with gr.Row():
382
+ # LEFT COLUMN: Entity Inputs
383
+ with gr.Column(scale=1):
384
+ with gr.Accordion("📚 Step 1: Enter Entities from Your Records", open=True):
385
+ for i in range(6):
386
+ with gr.Group():
387
+ gr.Markdown(f"### Record {i+1}")
388
+ with gr.Row():
389
+ person = gr.Textbox(label="👤 Person", placeholder="e.g., Winston Churchill")
390
+ location = gr.Textbox(label="📍 Location", placeholder="e.g., London")
391
+ with gr.Row():
392
+ event = gr.Textbox(label="📅 Event", placeholder="e.g., Battle of Britain")
393
+ org = gr.Textbox(label="🏢 Organization", placeholder="e.g., Royal Air Force")
394
+ date = gr.Textbox(label="🗓️ Date", placeholder="e.g., 1940")
395
+
396
+ entity_inputs.extend([person, location, event, org, date])
397
+
398
+ collect_btn = gr.Button("🔍 Identify Entities", variant="primary", size="lg")
399
+ entity_summary = gr.Markdown()
400
+
401
+ # RIGHT COLUMN: Relationship Builder
402
+ with gr.Column(scale=1):
403
+ # Relationship section (initially hidden)
404
+ with gr.Column(visible=False) as relationship_section:
405
+ with gr.Accordion("🤝 Step 2: Define Relationships Between Entities", open=True):
406
+ gr.Markdown("Select entities and specify how they're connected:")
407
+
408
+ relationship_inputs = []
409
+
410
+ for i in range(5):
411
+ with gr.Row():
412
+ source = gr.Dropdown(label=f"From", choices=[], interactive=True, scale=2)
413
+ rel_type = gr.Dropdown(
414
+ label="Type",
415
+ choices=RELATIONSHIP_TYPES,
416
+ value="related_to",
417
+ interactive=True,
418
+ scale=2
419
+ )
420
+ target = gr.Dropdown(label=f"To", choices=[], interactive=True, scale=2)
421
+
422
+ relationship_inputs.extend([source, rel_type, target])
423
+
424
+ with gr.Accordion("🎨 Step 3: Customize and Generate", open=True):
425
+ layout_type = gr.Dropdown(
426
+ label="Graph Layout",
427
+ choices=['spring', 'circular', 'kamada_kawai', 'shell'],
428
+ value='spring',
429
+ info="Choose how nodes are arranged"
430
+ )
431
+
432
+ generate_btn = gr.Button("🔍 Generate Network Graph", variant="primary", size="lg")
433
+
434
+ # Output section
435
+ gr.HTML("<hr style='margin: 30px 0;'>")
436
+
437
+ with gr.Row():
438
+ network_stats = gr.Markdown()
439
+
440
+ with gr.Row():
441
+ network_plot = gr.Plot(label="Interactive Network Graph")
442
+
443
+ # Examples
444
+ with gr.Column():
445
+ gr.Markdown("""
446
+ ### 💡 No example entities to test? No problem!
447
+ Simply click on one of the examples provided below, and the fields will be populated for you.
448
+ """, elem_id="examples-heading")
449
+ gr.Examples(
450
+ examples=[
451
+ [
452
+ # === ENTITY RECORDS ===
453
+ # Record 1
454
+ "Winston Churchill", "London", "Battle of Britain", "War Cabinet", "1940",
455
+ # Record 2
456
+ "Clement Attlee", "London", "Potsdam Conference", "Labour Party", "1945",
457
+ # Record 3
458
+ "Field Marshal Montgomery", "North Africa", "Battle of El Alamein", "Eighth Army", "1942",
459
+ # Record 4
460
+ "Winston Churchill", "Yalta", "Yalta Conference", "War Cabinet", "February 1945",
461
+ # Record 5
462
+ "King George VI", "London", "Victory in Europe Day", "British Monarchy", "May 1945",
463
+ # Record 6
464
+ "Field Marshal Montgomery", "Lüneburg Heath", "German Surrender", "British Army", "May 1945",
465
+ # === RELATIONSHIPS ===
466
+ # Relationship 1
467
+ "Winston Churchill", "works_with", "Clement Attlee",
468
+ # Relationship 2
469
+ "Winston Churchill", "participated_in", "Battle of Britain",
470
+ # Relationship 3
471
+ "Field Marshal Montgomery", "participated_in", "Battle of El Alamein",
472
+ # Relationship 4
473
+ "Winston Churchill", "participated_in", "Yalta Conference",
474
+ # Relationship 5
475
+ "Clement Attlee", "participated_in", "Potsdam Conference",
476
+ # Layout type
477
+ "spring"
478
+ ],
479
+ [
480
+ # === ENTITY RECORDS ===
481
+ # Record 1 - Pride and Prejudice
482
+ "Elizabeth Bennet", "Longbourn", "Meryton Assembly", "", "Autumn 1811",
483
+ # Record 2
484
+ "Mr Darcy", "Pemberley", "Meryton Assembly", "", "Autumn 1811",
485
+ # Record 3
486
+ "Jane Bennet", "Longbourn", "Netherfield Ball", "", "November 1811",
487
+ # Record 4
488
+ "Mr Bingley", "Netherfield", "Netherfield Ball", "", "November 1811",
489
+ # Record 5
490
+ "Elizabeth Bennet", "Rosings", "Easter Visit", "", "Spring 1812",
491
+ # Record 6
492
+ "Mr Darcy", "Rosings", "First Proposal", "", "Spring 1812",
493
+ # === RELATIONSHIPS ===
494
+ # Relationship 1
495
+ "Elizabeth Bennet", "knows", "Mr Darcy",
496
+ # Relationship 2
497
+ "Jane Bennet", "knows", "Mr Bingley",
498
+ # Relationship 3
499
+ "Elizabeth Bennet", "located_in", "Longbourn",
500
+ # Relationship 4
501
+ "Mr Darcy", "located_in", "Pemberley",
502
+ # Relationship 5
503
+ "Elizabeth Bennet", "participated_in", "Meryton Assembly",
504
+ # Layout type
505
+ "spring"
506
+ ]
507
+ ],
508
+ inputs=entity_inputs + relationship_inputs + [layout_type],
509
+ label="Examples"
510
+ )
511
+
512
+ # Add custom CSS to match NER tool styling
513
+ gr.HTML("""
514
+ <style>
515
+ /* Make the Examples label text black */
516
+ .gradio-examples-label {
517
+ color: black !important;
518
+ }
519
+ h4.examples-label, .examples-label {
520
+ color: black !important;
521
+ }
522
+ #examples-heading + div label,
523
+ #examples-heading + div .label-text {
524
+ color: black !important;
525
+ }
526
+ </style>
527
+ """)
528
+
529
+ # Wire up the interface
530
+ # Collect entities button
531
+ collect_btn.click(
532
+ fn=collect_entities_from_records,
533
+ inputs=entity_inputs,
534
+ outputs=[
535
+ entity_summary,
536
+ relationship_section
537
+ ] + relationship_inputs[::3] + relationship_inputs[2::3] # Update source and target dropdowns
538
+ )
539
+
540
+ # Generate graph button
541
+ all_inputs = entity_inputs + relationship_inputs + [layout_type]
542
+ generate_btn.click(
543
+ fn=generate_network_graph,
544
+ inputs=all_inputs,
545
+ outputs=[network_plot, network_stats]
546
+ )
547
+
548
+ # Information footer
549
+ gr.HTML("""
550
+ <hr style="margin-top: 40px; margin-bottom: 20px;">
551
+ <div style="background-color: #f8f9fa; padding: 20px; border-radius: 8px; margin-top: 20px;">
552
+ <h4 style="margin-top: 0;">ℹ️ About This Tool</h4>
553
+ <p style="font-size: 14px; line-height: 1.8;">
554
+ This tool demonstrates how <strong>Named Entity Recognition (NER)</strong> can be combined with
555
+ <strong>network analysis</strong> to visualize relationships in text data. In real-world applications,
556
+ entities would be automatically extracted from text using NER models, and relationships could be
557
+ identified through co-occurrence analysis, dependency parsing, or machine learning.
558
+ </p>
559
+ <p style="font-size: 14px; line-height: 1.8; margin-bottom: 0;">
560
+ <strong>Built with:</strong> Gradio, NetworkX, and Plotly |
561
+ <strong>Graph Layouts:</strong> Spring (force-directed), Circular, Kamada-Kawai, Shell
562
+ </p>
563
+ </div>
564
+
565
+ <br>
566
+ <hr style="margin-top: 40px; margin-bottom: 20px;">
567
+ <div style="background-color: #f8f9fa; padding: 20px; border-radius: 8px; margin-top: 20px; text-align: center;">
568
+ <p style="font-size: 14px; line-height: 1.8; margin: 0;">
569
+ This <strong>Basic Network Explorer</strong> tool was created as part of a Bodleian Libraries (University of Oxford) Sassoon Research Fellowship.
570
+ </p><br><br>
571
+ <p style="font-size: 14px; line-height: 1.8; margin: 0;">
572
+ The code for this tool was built with the aid of Claude Sonnet 4.5.
573
+ </p>
574
+ </div>
575
+ """)
576
+
577
+ return demo
578
 
579
+ if __name__ == "__main__":
580
+ demo = create_interface()
581
+ demo.launch()