SorrelC commited on
Commit
d6d7693
Β·
verified Β·
1 Parent(s): cedf3bc

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +134 -103
app.py CHANGED
@@ -1,8 +1,6 @@
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)
@@ -93,7 +91,7 @@ class NetworkGraphBuilder:
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%",
@@ -102,16 +100,18 @@ class NetworkGraphBuilder:
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,
@@ -121,16 +121,22 @@ class NetworkGraphBuilder:
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,
@@ -148,7 +154,7 @@ class NetworkGraphBuilder:
148
  },
149
  "interaction": {
150
  "hover": true,
151
- "tooltipDelay": 100,
152
  "dragNodes": true,
153
  "dragView": true,
154
  "zoomView": true
@@ -166,13 +172,20 @@ class NetworkGraphBuilder:
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,
@@ -180,7 +193,7 @@ class NetworkGraphBuilder:
180
  color=color,
181
  size=size,
182
  title=title,
183
- font={'size': 12, 'color': 'white'}
184
  )
185
 
186
  # Add edges with relationship labels
@@ -193,13 +206,12 @@ class NetworkGraphBuilder:
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')
@@ -223,7 +235,9 @@ def collect_entities_from_records(
223
  p3, l3, e3, o3, d3,
224
  p4, l4, e4, o4, d4,
225
  p5, l5, e5, o5, d5,
226
- p6, l6, e6, o6, d6
 
 
227
  ):
228
  """Collect all entities from the input fields"""
229
  builder = NetworkGraphBuilder()
@@ -236,6 +250,8 @@ def collect_entities_from_records(
236
  (p4, l4, e4, o4, d4),
237
  (p5, l5, e5, o5, d5),
238
  (p6, l6, e6, o6, d6),
 
 
239
  ]
240
 
241
  for record_id, (person, location, event, org, date) in enumerate(records, 1):
@@ -319,6 +335,8 @@ def generate_network_graph(
319
  p4, l4, e4, o4, d4,
320
  p5, l5, e5, o5, d5,
321
  p6, l6, e6, o6, d6,
 
 
322
  src1, rel1, tgt1,
323
  src2, rel2, tgt2,
324
  src3, rel3, tgt3,
@@ -337,6 +355,8 @@ def generate_network_graph(
337
  (p4, l4, e4, o4, d4),
338
  (p5, l5, e5, o5, d5),
339
  (p6, l6, e6, o6, d6),
 
 
340
  ]
341
 
342
  for record_id, (person, location, event, org, date) in enumerate(records, 1):
@@ -466,6 +486,10 @@ def load_austen_example():
466
  "Mr. Wickham", "Meryton", "", "Militia", "",
467
  # Record 6
468
  "Charlotte Lucas", "Hunsford", "", "", "",
 
 
 
 
469
  )
470
 
471
 
@@ -484,26 +508,32 @@ def load_wwii_example():
484
  "", "", "", "", "",
485
  # Record 6
486
  "", "", "", "", "",
 
 
 
 
487
  )
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
@@ -514,77 +544,87 @@ def create_interface():
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")
@@ -596,55 +636,46 @@ def create_interface():
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
 
@@ -653,7 +684,7 @@ def create_interface():
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")
@@ -733,8 +764,8 @@ def create_interface():
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
 
 
1
  import gradio as gr
2
  import networkx as nx
3
  from pyvis.network import Network
 
 
4
  import base64
5
 
6
  # Entity type colors (matching your NER tool)
 
91
  if len(G.nodes) == 0:
92
  return None
93
 
94
+ # Create PyVis network with dark theme
95
  net = Network(
96
  height="600px",
97
  width="100%",
 
100
  directed=False
101
  )
102
 
103
+ # Configure physics and styling
104
  net.set_options("""
105
  {
106
  "nodes": {
107
  "borderWidth": 2,
108
  "borderWidthSelected": 4,
109
  "font": {
110
+ "size": 18,
111
  "face": "arial",
112
+ "color": "white",
113
+ "strokeWidth": 3,
114
+ "strokeColor": "#1a1a2e"
115
  },
116
  "shadow": {
117
  "enabled": true,
 
121
  },
122
  "edges": {
123
  "color": {
124
+ "color": "#888888",
125
+ "highlight": "#ffffff"
126
+ },
127
+ "font": {
128
+ "size": 14,
129
+ "face": "arial",
130
+ "color": "#ffffff",
131
+ "strokeWidth": 0,
132
+ "background": "rgba(26, 26, 46, 0.8)",
133
+ "align": "middle"
134
  },
135
  "smooth": {
136
  "enabled": true,
137
  "type": "continuous"
138
  },
139
+ "width": 2
 
 
 
140
  },
141
  "physics": {
142
  "enabled": true,
 
154
  },
155
  "interaction": {
156
  "hover": true,
157
+ "tooltipDelay": 200,
158
  "dragNodes": true,
159
  "dragView": true,
160
  "zoomView": true
 
172
  degree = G.degree(node)
173
  size = 25 + (degree * 8)
174
 
175
+ # Create plain text tooltip (no HTML)
176
  connections = list(G.neighbors(node))
177
+ title_lines = [
178
+ node,
179
+ f"Type: {entity_type}",
180
+ f"Connections: {len(connections)}"
181
+ ]
182
  if connections:
183
+ connected_to = ', '.join(connections[:5])
184
  if len(connections) > 5:
185
+ connected_to += f"... +{len(connections) - 5} more"
186
+ title_lines.append(f"Connected to: {connected_to}")
187
+
188
+ title = '\n'.join(title_lines)
189
 
190
  net.add_node(
191
  node,
 
193
  color=color,
194
  size=size,
195
  title=title,
196
+ font={'size': 18, 'color': 'white', 'strokeWidth': 3, 'strokeColor': '#1a1a2e'}
197
  )
198
 
199
  # Add edges with relationship labels
 
206
  label=rel_type,
207
  color='#888888',
208
  width=2,
209
+ font={'size': 14, 'color': '#ffffff', 'strokeWidth': 0, 'background': 'rgba(26,26,46,0.8)'}
210
  )
211
 
212
  # Generate HTML
213
  html = net.generate_html()
214
 
 
215
  # Encode as base64 data URI for iframe src
216
  html_bytes = html.encode('utf-8')
217
  b64_html = base64.b64encode(html_bytes).decode('utf-8')
 
235
  p3, l3, e3, o3, d3,
236
  p4, l4, e4, o4, d4,
237
  p5, l5, e5, o5, d5,
238
+ p6, l6, e6, o6, d6,
239
+ p7, l7, e7, o7, d7,
240
+ p8, l8, e8, o8, d8
241
  ):
242
  """Collect all entities from the input fields"""
243
  builder = NetworkGraphBuilder()
 
250
  (p4, l4, e4, o4, d4),
251
  (p5, l5, e5, o5, d5),
252
  (p6, l6, e6, o6, d6),
253
+ (p7, l7, e7, o7, d7),
254
+ (p8, l8, e8, o8, d8),
255
  ]
256
 
257
  for record_id, (person, location, event, org, date) in enumerate(records, 1):
 
335
  p4, l4, e4, o4, d4,
336
  p5, l5, e5, o5, d5,
337
  p6, l6, e6, o6, d6,
338
+ p7, l7, e7, o7, d7,
339
+ p8, l8, e8, o8, d8,
340
  src1, rel1, tgt1,
341
  src2, rel2, tgt2,
342
  src3, rel3, tgt3,
 
355
  (p4, l4, e4, o4, d4),
356
  (p5, l5, e5, o5, d5),
357
  (p6, l6, e6, o6, d6),
358
+ (p7, l7, e7, o7, d7),
359
+ (p8, l8, e8, o8, d8),
360
  ]
361
 
362
  for record_id, (person, location, event, org, date) in enumerate(records, 1):
 
486
  "Mr. Wickham", "Meryton", "", "Militia", "",
487
  # Record 6
488
  "Charlotte Lucas", "Hunsford", "", "", "",
489
+ # Record 7
490
+ "", "", "", "", "",
491
+ # Record 8
492
+ "", "", "", "", "",
493
  )
494
 
495
 
 
508
  "", "", "", "", "",
509
  # Record 6
510
  "", "", "", "", "",
511
+ # Record 7
512
+ "", "", "", "", "",
513
+ # Record 8
514
+ "", "", "", "", "",
515
  )
516
 
517
 
518
  def create_interface():
519
+ with gr.Blocks(title="Network Explorer", theme=gr.themes.Soft()) as demo:
 
 
 
 
 
 
 
 
 
 
520
  gr.Markdown("""
521
+ # πŸ•ΈοΈ Basic Network Explorer
522
+
523
+ Build interactive network graphs by entering entities extracted through Named Entity Recognition (NER).
524
+
525
+ ### How to use this tool:
526
+ 1. **πŸ“ Enter entities** in the records below (or load an example to get started)
527
+ 2. **πŸ” Click "Identify Entities"** to collect and list all entities
528
+ 3. **🀝 Define relationships** between entities using the dropdowns
529
+ 4. **🎨 Click "Generate Network Graph"** to visualize
530
+ 5. **πŸ‘οΈ Explore** - drag nodes to rearrange, scroll to zoom, hover for details
531
+ """)
532
 
533
+ gr.HTML("""
534
+ <div style="background-color: #fff3cd; border: 1px solid #ffeaa7; border-radius: 8px; padding: 12px; margin: 15px 0;">
535
+ <strong style="color: #856404;">πŸ’‘ Top tip:</strong> Start with just a few entities and relationships to see how it works!
536
+ </div>
537
  """)
538
 
539
  # Quick start buttons
 
544
 
545
  gr.HTML("<hr style='margin: 15px 0;'>")
546
 
547
+ # ==================== STEP 1: ENTITY INPUT (4 per row) ====================
548
  gr.Markdown("## πŸ“ Step 1: Enter Entities")
549
 
550
  entity_inputs = []
551
 
552
+ # First row: Records 1-4 (all on one line)
553
  with gr.Row():
554
+ with gr.Column(scale=1, min_width=200):
555
  gr.Markdown("**Record 1**")
556
+ p1 = gr.Textbox(label="πŸ‘€ Person", placeholder="e.g., Elizabeth Bennet")
557
+ l1 = gr.Textbox(label="πŸ“ Location", placeholder="e.g., Longbourn")
558
+ e1 = gr.Textbox(label="πŸ“… Event", placeholder="e.g., Meryton Ball")
559
+ o1 = gr.Textbox(label="🏒 Organization", placeholder="e.g., Bennet Family")
560
+ d1 = gr.Textbox(label="πŸ—“οΈ Date", placeholder="e.g., 1811")
 
561
  entity_inputs.extend([p1, l1, e1, o1, d1])
562
 
563
+ with gr.Column(scale=1, min_width=200):
564
  gr.Markdown("**Record 2**")
565
+ p2 = gr.Textbox(label="πŸ‘€ Person", placeholder="e.g., Mr. Darcy")
566
+ l2 = gr.Textbox(label="πŸ“ Location", placeholder="e.g., Pemberley")
567
+ e2 = gr.Textbox(label="πŸ“… Event", placeholder="e.g., Netherfield Ball")
568
+ o2 = gr.Textbox(label="🏒 Organization", placeholder="e.g., Darcy Estate")
569
+ d2 = gr.Textbox(label="πŸ—“οΈ Date", placeholder="")
 
570
  entity_inputs.extend([p2, l2, e2, o2, d2])
571
+
572
+ with gr.Column(scale=1, min_width=200):
 
 
573
  gr.Markdown("**Record 3**")
574
+ p3 = gr.Textbox(label="πŸ‘€ Person", placeholder="e.g., Jane Bennet")
575
+ l3 = gr.Textbox(label="πŸ“ Location", placeholder="e.g., Netherfield")
576
+ e3 = gr.Textbox(label="πŸ“… Event", placeholder="")
577
+ o3 = gr.Textbox(label="🏒 Organization", placeholder="")
578
+ d3 = gr.Textbox(label="πŸ—“οΈ Date", placeholder="")
 
579
  entity_inputs.extend([p3, l3, e3, o3, d3])
580
 
581
+ with gr.Column(scale=1, min_width=200):
582
  gr.Markdown("**Record 4**")
583
+ p4 = gr.Textbox(label="πŸ‘€ Person", placeholder="e.g., Mr. Bingley")
584
+ l4 = gr.Textbox(label="πŸ“ Location", placeholder="e.g., London")
585
+ e4 = gr.Textbox(label="πŸ“… Event", placeholder="")
586
+ o4 = gr.Textbox(label="🏒 Organization", placeholder="")
587
+ d4 = gr.Textbox(label="πŸ—“οΈ Date", placeholder="")
 
588
  entity_inputs.extend([p4, l4, e4, o4, d4])
589
 
590
+ # Additional records 5-8
591
+ with gr.Accordion("βž• Additional Records (5-8)", open=False):
592
  with gr.Row():
593
+ with gr.Column(scale=1, min_width=200):
594
  gr.Markdown("**Record 5**")
595
+ p5 = gr.Textbox(label="πŸ‘€ Person")
596
+ l5 = gr.Textbox(label="πŸ“ Location")
597
+ e5 = gr.Textbox(label="πŸ“… Event")
598
+ o5 = gr.Textbox(label="🏒 Organization")
599
+ d5 = gr.Textbox(label="πŸ—“οΈ Date")
 
600
  entity_inputs.extend([p5, l5, e5, o5, d5])
601
 
602
+ with gr.Column(scale=1, min_width=200):
603
  gr.Markdown("**Record 6**")
604
+ p6 = gr.Textbox(label="πŸ‘€ Person")
605
+ l6 = gr.Textbox(label="πŸ“ Location")
606
+ e6 = gr.Textbox(label="πŸ“… Event")
607
+ o6 = gr.Textbox(label="🏒 Organization")
608
+ d6 = gr.Textbox(label="πŸ—“οΈ Date")
 
609
  entity_inputs.extend([p6, l6, e6, o6, d6])
610
+
611
+ with gr.Column(scale=1, min_width=200):
612
+ gr.Markdown("**Record 7**")
613
+ p7 = gr.Textbox(label="πŸ‘€ Person")
614
+ l7 = gr.Textbox(label="πŸ“ Location")
615
+ e7 = gr.Textbox(label="πŸ“… Event")
616
+ o7 = gr.Textbox(label="🏒 Organization")
617
+ d7 = gr.Textbox(label="πŸ—“οΈ Date")
618
+ entity_inputs.extend([p7, l7, e7, o7, d7])
619
+
620
+ with gr.Column(scale=1, min_width=200):
621
+ gr.Markdown("**Record 8**")
622
+ p8 = gr.Textbox(label="πŸ‘€ Person")
623
+ l8 = gr.Textbox(label="πŸ“ Location")
624
+ e8 = gr.Textbox(label="πŸ“… Event")
625
+ o8 = gr.Textbox(label="🏒 Organization")
626
+ d8 = gr.Textbox(label="πŸ—“οΈ Date")
627
+ entity_inputs.extend([p8, l8, e8, o8, d8])
628
 
629
  # Identify button
630
  collect_btn = gr.Button("πŸ” Identify Entities", variant="primary", size="lg")
 
636
 
637
  # ==================== STEP 2: RELATIONSHIPS (Grid) ====================
638
  gr.Markdown("## 🀝 Step 2: Define Relationships")
639
+ gr.Markdown("*Select entities from the dropdowns to create connections (click 'Identify Entities' first)*")
640
 
641
+ # Relationship inputs in a grid (5 columns)
642
  relationship_inputs = []
643
 
644
  with gr.Row():
645
+ with gr.Column(scale=1, min_width=180):
646
  gr.Markdown("**Relationship 1**")
647
  src1 = gr.Dropdown(label="From", choices=[])
648
  rel1 = gr.Dropdown(label="Type", choices=RELATIONSHIP_TYPES, value="related_to")
649
  tgt1 = gr.Dropdown(label="To", choices=[])
650
  relationship_inputs.extend([src1, rel1, tgt1])
651
 
652
+ with gr.Column(scale=1, min_width=180):
653
  gr.Markdown("**Relationship 2**")
654
  src2 = gr.Dropdown(label="From", choices=[])
655
  rel2 = gr.Dropdown(label="Type", choices=RELATIONSHIP_TYPES, value="related_to")
656
  tgt2 = gr.Dropdown(label="To", choices=[])
657
  relationship_inputs.extend([src2, rel2, tgt2])
658
 
659
+ with gr.Column(scale=1, min_width=180):
660
  gr.Markdown("**Relationship 3**")
661
  src3 = gr.Dropdown(label="From", choices=[])
662
  rel3 = gr.Dropdown(label="Type", choices=RELATIONSHIP_TYPES, value="related_to")
663
  tgt3 = gr.Dropdown(label="To", choices=[])
664
  relationship_inputs.extend([src3, rel3, tgt3])
665
+
666
+ with gr.Column(scale=1, min_width=180):
 
667
  gr.Markdown("**Relationship 4**")
668
  src4 = gr.Dropdown(label="From", choices=[])
669
  rel4 = gr.Dropdown(label="Type", choices=RELATIONSHIP_TYPES, value="related_to")
670
  tgt4 = gr.Dropdown(label="To", choices=[])
671
  relationship_inputs.extend([src4, rel4, tgt4])
672
 
673
+ with gr.Column(scale=1, min_width=180):
674
  gr.Markdown("**Relationship 5**")
675
  src5 = gr.Dropdown(label="From", choices=[])
676
  rel5 = gr.Dropdown(label="Type", choices=RELATIONSHIP_TYPES, value="related_to")
677
  tgt5 = gr.Dropdown(label="To", choices=[])
678
  relationship_inputs.extend([src5, rel5, tgt5])
 
 
 
 
 
 
 
 
679
 
680
  gr.HTML("<hr style='margin: 20px 0;'>")
681
 
 
684
 
685
  generate_btn = gr.Button("🎨 Generate Network Graph", variant="primary", size="lg")
686
 
687
+ # Full-width network graph with stats sidebar
688
  with gr.Row():
689
  with gr.Column(scale=3):
690
  network_plot = gr.HTML(label="Interactive Network Graph")
 
764
  gr.HTML("""
765
  <hr style="margin: 40px 0 20px 0;">
766
  <div style="text-align: center; color: #666; font-size: 14px; padding: 20px;">
767
+ <p>This <strong>Basic Network Explorer</strong> tool was developed as part of a Bodleian Libraries (Oxford) Sassoon Research Fellowship.</p>
768
+ <p style="color: #888; font-size: 12px; margin-top: 10px;">Built with the aid of Claude Opus 4.5.</p>
769
  </div>
770
  """)
771