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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +144 -61
app.py CHANGED
@@ -3,11 +3,12 @@ import networkx as nx
3
  from pyvis.network import Network
4
  import base64
5
 
6
- # Entity type colors (matching your NER tool)
7
- ENTITY_COLORS = {
 
8
  'PERSON': '#00B894', # Green
9
- 'LOCATION': '#A0E7E5', # Light Cyan
10
- 'EVENT': '#4ECDC4', # Teal
11
  'ORGANIZATION': '#55A3FF', # Light Blue
12
  'DATE': '#FF6B6B' # Red
13
  }
@@ -87,7 +88,7 @@ class NetworkGraphBuilder:
87
  return G
88
 
89
  def create_pyvis_graph(self, G):
90
- """Create interactive PyVis visualization"""
91
  if len(G.nodes) == 0:
92
  return None
93
 
@@ -147,7 +148,7 @@ class NetworkGraphBuilder:
147
  "springConstant": 0.04,
148
  "damping": 0.09
149
  },
150
- "stabilization": {
151
  "enabled": true,
152
  "iterations": 100
153
  }
@@ -166,7 +167,7 @@ class NetworkGraphBuilder:
166
  for node in G.nodes():
167
  data = G.nodes[node]
168
  entity_type = data.get('entity_type', 'UNKNOWN')
169
- color = ENTITY_COLORS.get(entity_type, '#CCCCCC')
170
 
171
  # Size based on connections (degree)
172
  degree = G.degree(node)
@@ -190,7 +191,7 @@ class NetworkGraphBuilder:
190
  net.add_node(
191
  node,
192
  label=node,
193
- color=color,
194
  size=size,
195
  title=title,
196
  font={'size': 18, 'color': 'white', 'strokeWidth': 3, 'strokeColor': '#1a1a2e'}
@@ -297,7 +298,7 @@ def collect_entities_from_records(
297
  </div>
298
  <div style="background: rgba(255,255,255,0.2); padding: 10px 20px; border-radius: 8px; text-align: center;">
299
  <div style="font-size: 24px; font-weight: bold; color: white;">{counts['ORGANIZATION']}</div>
300
- <div style="color: rgba(255,255,255,0.9); font-size: 12px;">🏒 Organizations</div>
301
  </div>
302
  <div style="background: rgba(255,255,255,0.2); padding: 10px 20px; border-radius: 8px; text-align: center;">
303
  <div style="font-size: 24px; font-weight: bold; color: white;">{counts['DATE']}</div>
@@ -399,7 +400,7 @@ def generate_network_graph(
399
  '''
400
  return empty_html, "❌ **No entities to display.** Please enter entities in Step 1 first."
401
 
402
- # Create visualization
403
  graph_html = builder.create_pyvis_graph(G)
404
 
405
  # Create statistics
@@ -515,6 +516,22 @@ def load_wwii_example():
515
  )
516
 
517
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
518
  def create_interface():
519
  with gr.Blocks(title="Network Explorer", theme=gr.themes.Soft()) as demo:
520
  gr.Markdown("""
@@ -526,7 +543,7 @@ def create_interface():
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
 
@@ -553,38 +570,58 @@ def create_interface():
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
@@ -592,38 +629,58 @@ def create_interface():
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
@@ -691,29 +748,29 @@ def create_interface():
691
  with gr.Column(scale=1):
692
  network_stats = gr.HTML()
693
 
694
- # Color legend
695
- gr.HTML("""
696
  <div style="background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); padding: 20px; border-radius: 10px; margin-top: 20px;">
697
- <h4 style="color: white; margin: 0 0 15px 0;">🎨 Entity Color Legend</h4>
698
  <div style="display: flex; flex-wrap: wrap; gap: 20px;">
699
  <span style="display: flex; align-items: center; gap: 8px; color: white;">
700
- <span style="width: 20px; height: 20px; border-radius: 50%; background-color: #00B894; display: inline-block; border: 2px solid white;"></span>
701
  Person
702
  </span>
703
  <span style="display: flex; align-items: center; gap: 8px; color: white;">
704
- <span style="width: 20px; height: 20px; border-radius: 50%; background-color: #A0E7E5; display: inline-block; border: 2px solid white;"></span>
705
  Location
706
  </span>
707
  <span style="display: flex; align-items: center; gap: 8px; color: white;">
708
- <span style="width: 20px; height: 20px; border-radius: 50%; background-color: #4ECDC4; display: inline-block; border: 2px solid white;"></span>
709
  Event
710
  </span>
711
  <span style="display: flex; align-items: center; gap: 8px; color: white;">
712
- <span style="width: 20px; height: 20px; border-radius: 50%; background-color: #55A3FF; display: inline-block; border: 2px solid white;"></span>
713
- Organization
714
  </span>
715
  <span style="display: flex; align-items: center; gap: 8px; color: white;">
716
- <span style="width: 20px; height: 20px; border-radius: 50%; background-color: #FF6B6B; display: inline-block; border: 2px solid white;"></span>
717
  Date
718
  </span>
719
  </div>
@@ -760,9 +817,35 @@ def create_interface():
760
  outputs=[network_plot, network_stats]
761
  )
762
 
763
- # Footer
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>
 
3
  from pyvis.network import Network
4
  import base64
5
 
6
+ # Entity type colours (matching your NER tool)
7
+ # Updated for better distinction between Person, Location, and Event
8
+ ENTITY_COLOURS = {
9
  'PERSON': '#00B894', # Green
10
+ 'LOCATION': '#9B59B6', # Purple (more distinct from green)
11
+ 'EVENT': '#F39C12', # Orange/Gold (very distinct)
12
  'ORGANIZATION': '#55A3FF', # Light Blue
13
  'DATE': '#FF6B6B' # Red
14
  }
 
88
  return G
89
 
90
  def create_pyvis_graph(self, G):
91
+ """Create interactive PyVis visualisation"""
92
  if len(G.nodes) == 0:
93
  return None
94
 
 
148
  "springConstant": 0.04,
149
  "damping": 0.09
150
  },
151
+ "stabilisation": {
152
  "enabled": true,
153
  "iterations": 100
154
  }
 
167
  for node in G.nodes():
168
  data = G.nodes[node]
169
  entity_type = data.get('entity_type', 'UNKNOWN')
170
+ colour = ENTITY_COLOURS.get(entity_type, '#CCCCCC')
171
 
172
  # Size based on connections (degree)
173
  degree = G.degree(node)
 
191
  net.add_node(
192
  node,
193
  label=node,
194
+ color=colour,
195
  size=size,
196
  title=title,
197
  font={'size': 18, 'color': 'white', 'strokeWidth': 3, 'strokeColor': '#1a1a2e'}
 
298
  </div>
299
  <div style="background: rgba(255,255,255,0.2); padding: 10px 20px; border-radius: 8px; text-align: center;">
300
  <div style="font-size: 24px; font-weight: bold; color: white;">{counts['ORGANIZATION']}</div>
301
+ <div style="color: rgba(255,255,255,0.9); font-size: 12px;">🏒 Organisations</div>
302
  </div>
303
  <div style="background: rgba(255,255,255,0.2); padding: 10px 20px; border-radius: 8px; text-align: center;">
304
  <div style="font-size: 24px; font-weight: bold; color: white;">{counts['DATE']}</div>
 
400
  '''
401
  return empty_html, "❌ **No entities to display.** Please enter entities in Step 1 first."
402
 
403
+ # Create visualisation
404
  graph_html = builder.create_pyvis_graph(G)
405
 
406
  # Create statistics
 
516
  )
517
 
518
 
519
+ def create_coloured_label(text, colour, emoji):
520
+ """Create a coloured pill-style label HTML"""
521
+ return f'''
522
+ <span style="
523
+ background-color: {colour};
524
+ color: white;
525
+ padding: 4px 12px;
526
+ border-radius: 15px;
527
+ font-size: 13px;
528
+ font-weight: 500;
529
+ display: inline-block;
530
+ margin-bottom: 5px;
531
+ ">{emoji} {text}</span>
532
+ '''
533
+
534
+
535
  def create_interface():
536
  with gr.Blocks(title="Network Explorer", theme=gr.themes.Soft()) as demo:
537
  gr.Markdown("""
 
543
  1. **πŸ“ Enter entities** in the records below (or load an example to get started)
544
  2. **πŸ” Click "Identify Entities"** to collect and list all entities
545
  3. **🀝 Define relationships** between entities using the dropdowns
546
+ 4. **🎨 Click "Generate Network Graph"** to visualise
547
  5. **πŸ‘οΈ Explore** - drag nodes to rearrange, scroll to zoom, hover for details
548
  """)
549
 
 
570
  with gr.Row():
571
  with gr.Column(scale=1, min_width=200):
572
  gr.Markdown("**Record 1**")
573
+ gr.HTML(create_coloured_label("Person", ENTITY_COLOURS['PERSON'], "πŸ‘€"))
574
+ p1 = gr.Textbox(label="", placeholder="e.g., Elizabeth Bennet", show_label=False)
575
+ gr.HTML(create_coloured_label("Location", ENTITY_COLOURS['LOCATION'], "πŸ“"))
576
+ l1 = gr.Textbox(label="", placeholder="e.g., Longbourn", show_label=False)
577
+ gr.HTML(create_coloured_label("Event", ENTITY_COLOURS['EVENT'], "πŸ“…"))
578
+ e1 = gr.Textbox(label="", placeholder="e.g., Meryton Ball", show_label=False)
579
+ gr.HTML(create_coloured_label("Organisation", ENTITY_COLOURS['ORGANIZATION'], "🏒"))
580
+ o1 = gr.Textbox(label="", placeholder="e.g., Bennet Family", show_label=False)
581
+ gr.HTML(create_coloured_label("Date", ENTITY_COLOURS['DATE'], "πŸ—“οΈ"))
582
+ d1 = gr.Textbox(label="", placeholder="e.g., 1811", show_label=False)
583
  entity_inputs.extend([p1, l1, e1, o1, d1])
584
 
585
  with gr.Column(scale=1, min_width=200):
586
  gr.Markdown("**Record 2**")
587
+ gr.HTML(create_coloured_label("Person", ENTITY_COLOURS['PERSON'], "πŸ‘€"))
588
+ p2 = gr.Textbox(label="", placeholder="e.g., Mr. Darcy", show_label=False)
589
+ gr.HTML(create_coloured_label("Location", ENTITY_COLOURS['LOCATION'], "πŸ“"))
590
+ l2 = gr.Textbox(label="", placeholder="e.g., Pemberley", show_label=False)
591
+ gr.HTML(create_coloured_label("Event", ENTITY_COLOURS['EVENT'], "πŸ“…"))
592
+ e2 = gr.Textbox(label="", placeholder="e.g., Netherfield Ball", show_label=False)
593
+ gr.HTML(create_coloured_label("Organisation", ENTITY_COLOURS['ORGANIZATION'], "🏒"))
594
+ o2 = gr.Textbox(label="", placeholder="e.g., Darcy Estate", show_label=False)
595
+ gr.HTML(create_coloured_label("Date", ENTITY_COLOURS['DATE'], "πŸ—“οΈ"))
596
+ d2 = gr.Textbox(label="", placeholder="", show_label=False)
597
  entity_inputs.extend([p2, l2, e2, o2, d2])
598
 
599
  with gr.Column(scale=1, min_width=200):
600
  gr.Markdown("**Record 3**")
601
+ gr.HTML(create_coloured_label("Person", ENTITY_COLOURS['PERSON'], "πŸ‘€"))
602
+ p3 = gr.Textbox(label="", placeholder="e.g., Jane Bennet", show_label=False)
603
+ gr.HTML(create_coloured_label("Location", ENTITY_COLOURS['LOCATION'], "πŸ“"))
604
+ l3 = gr.Textbox(label="", placeholder="e.g., Netherfield", show_label=False)
605
+ gr.HTML(create_coloured_label("Event", ENTITY_COLOURS['EVENT'], "πŸ“…"))
606
+ e3 = gr.Textbox(label="", placeholder="", show_label=False)
607
+ gr.HTML(create_coloured_label("Organisation", ENTITY_COLOURS['ORGANIZATION'], "🏒"))
608
+ o3 = gr.Textbox(label="", placeholder="", show_label=False)
609
+ gr.HTML(create_coloured_label("Date", ENTITY_COLOURS['DATE'], "πŸ—“οΈ"))
610
+ d3 = gr.Textbox(label="", placeholder="", show_label=False)
611
  entity_inputs.extend([p3, l3, e3, o3, d3])
612
 
613
  with gr.Column(scale=1, min_width=200):
614
  gr.Markdown("**Record 4**")
615
+ gr.HTML(create_coloured_label("Person", ENTITY_COLOURS['PERSON'], "πŸ‘€"))
616
+ p4 = gr.Textbox(label="", placeholder="e.g., Mr. Bingley", show_label=False)
617
+ gr.HTML(create_coloured_label("Location", ENTITY_COLOURS['LOCATION'], "πŸ“"))
618
+ l4 = gr.Textbox(label="", placeholder="e.g., London", show_label=False)
619
+ gr.HTML(create_coloured_label("Event", ENTITY_COLOURS['EVENT'], "πŸ“…"))
620
+ e4 = gr.Textbox(label="", placeholder="", show_label=False)
621
+ gr.HTML(create_coloured_label("Organisation", ENTITY_COLOURS['ORGANIZATION'], "🏒"))
622
+ o4 = gr.Textbox(label="", placeholder="", show_label=False)
623
+ gr.HTML(create_coloured_label("Date", ENTITY_COLOURS['DATE'], "πŸ—“οΈ"))
624
+ d4 = gr.Textbox(label="", placeholder="", show_label=False)
625
  entity_inputs.extend([p4, l4, e4, o4, d4])
626
 
627
  # Additional records 5-8
 
629
  with gr.Row():
630
  with gr.Column(scale=1, min_width=200):
631
  gr.Markdown("**Record 5**")
632
+ gr.HTML(create_coloured_label("Person", ENTITY_COLOURS['PERSON'], "πŸ‘€"))
633
+ p5 = gr.Textbox(label="", show_label=False)
634
+ gr.HTML(create_coloured_label("Location", ENTITY_COLOURS['LOCATION'], "πŸ“"))
635
+ l5 = gr.Textbox(label="", show_label=False)
636
+ gr.HTML(create_coloured_label("Event", ENTITY_COLOURS['EVENT'], "πŸ“…"))
637
+ e5 = gr.Textbox(label="", show_label=False)
638
+ gr.HTML(create_coloured_label("Organisation", ENTITY_COLOURS['ORGANIZATION'], "🏒"))
639
+ o5 = gr.Textbox(label="", show_label=False)
640
+ gr.HTML(create_coloured_label("Date", ENTITY_COLOURS['DATE'], "πŸ—“οΈ"))
641
+ d5 = gr.Textbox(label="", show_label=False)
642
  entity_inputs.extend([p5, l5, e5, o5, d5])
643
 
644
  with gr.Column(scale=1, min_width=200):
645
  gr.Markdown("**Record 6**")
646
+ gr.HTML(create_coloured_label("Person", ENTITY_COLOURS['PERSON'], "πŸ‘€"))
647
+ p6 = gr.Textbox(label="", show_label=False)
648
+ gr.HTML(create_coloured_label("Location", ENTITY_COLOURS['LOCATION'], "πŸ“"))
649
+ l6 = gr.Textbox(label="", show_label=False)
650
+ gr.HTML(create_coloured_label("Event", ENTITY_COLOURS['EVENT'], "πŸ“…"))
651
+ e6 = gr.Textbox(label="", show_label=False)
652
+ gr.HTML(create_coloured_label("Organisation", ENTITY_COLOURS['ORGANIZATION'], "🏒"))
653
+ o6 = gr.Textbox(label="", show_label=False)
654
+ gr.HTML(create_coloured_label("Date", ENTITY_COLOURS['DATE'], "πŸ—“οΈ"))
655
+ d6 = gr.Textbox(label="", show_label=False)
656
  entity_inputs.extend([p6, l6, e6, o6, d6])
657
 
658
  with gr.Column(scale=1, min_width=200):
659
  gr.Markdown("**Record 7**")
660
+ gr.HTML(create_coloured_label("Person", ENTITY_COLOURS['PERSON'], "πŸ‘€"))
661
+ p7 = gr.Textbox(label="", show_label=False)
662
+ gr.HTML(create_coloured_label("Location", ENTITY_COLOURS['LOCATION'], "πŸ“"))
663
+ l7 = gr.Textbox(label="", show_label=False)
664
+ gr.HTML(create_coloured_label("Event", ENTITY_COLOURS['EVENT'], "πŸ“…"))
665
+ e7 = gr.Textbox(label="", show_label=False)
666
+ gr.HTML(create_coloured_label("Organisation", ENTITY_COLOURS['ORGANIZATION'], "🏒"))
667
+ o7 = gr.Textbox(label="", show_label=False)
668
+ gr.HTML(create_coloured_label("Date", ENTITY_COLOURS['DATE'], "πŸ—“οΈ"))
669
+ d7 = gr.Textbox(label="", show_label=False)
670
  entity_inputs.extend([p7, l7, e7, o7, d7])
671
 
672
  with gr.Column(scale=1, min_width=200):
673
  gr.Markdown("**Record 8**")
674
+ gr.HTML(create_coloured_label("Person", ENTITY_COLOURS['PERSON'], "πŸ‘€"))
675
+ p8 = gr.Textbox(label="", show_label=False)
676
+ gr.HTML(create_coloured_label("Location", ENTITY_COLOURS['LOCATION'], "πŸ“"))
677
+ l8 = gr.Textbox(label="", show_label=False)
678
+ gr.HTML(create_coloured_label("Event", ENTITY_COLOURS['EVENT'], "πŸ“…"))
679
+ e8 = gr.Textbox(label="", show_label=False)
680
+ gr.HTML(create_coloured_label("Organisation", ENTITY_COLOURS['ORGANIZATION'], "🏒"))
681
+ o8 = gr.Textbox(label="", show_label=False)
682
+ gr.HTML(create_coloured_label("Date", ENTITY_COLOURS['DATE'], "πŸ—“οΈ"))
683
+ d8 = gr.Textbox(label="", show_label=False)
684
  entity_inputs.extend([p8, l8, e8, o8, d8])
685
 
686
  # Identify button
 
748
  with gr.Column(scale=1):
749
  network_stats = gr.HTML()
750
 
751
+ # Colour legend
752
+ gr.HTML(f"""
753
  <div style="background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); padding: 20px; border-radius: 10px; margin-top: 20px;">
754
+ <h4 style="color: white; margin: 0 0 15px 0;">🎨 Entity Colour Legend</h4>
755
  <div style="display: flex; flex-wrap: wrap; gap: 20px;">
756
  <span style="display: flex; align-items: center; gap: 8px; color: white;">
757
+ <span style="width: 20px; height: 20px; border-radius: 50%; background-color: {ENTITY_COLOURS['PERSON']}; display: inline-block; border: 2px solid white;"></span>
758
  Person
759
  </span>
760
  <span style="display: flex; align-items: center; gap: 8px; color: white;">
761
+ <span style="width: 20px; height: 20px; border-radius: 50%; background-color: {ENTITY_COLOURS['LOCATION']}; display: inline-block; border: 2px solid white;"></span>
762
  Location
763
  </span>
764
  <span style="display: flex; align-items: center; gap: 8px; color: white;">
765
+ <span style="width: 20px; height: 20px; border-radius: 50%; background-color: {ENTITY_COLOURS['EVENT']}; display: inline-block; border: 2px solid white;"></span>
766
  Event
767
  </span>
768
  <span style="display: flex; align-items: center; gap: 8px; color: white;">
769
+ <span style="width: 20px; height: 20px; border-radius: 50%; background-color: {ENTITY_COLOURS['ORGANIZATION']}; display: inline-block; border: 2px solid white;"></span>
770
+ Organisation
771
  </span>
772
  <span style="display: flex; align-items: center; gap: 8px; color: white;">
773
+ <span style="width: 20px; height: 20px; border-radius: 50%; background-color: {ENTITY_COLOURS['DATE']}; display: inline-block; border: 2px solid white;"></span>
774
  Date
775
  </span>
776
  </div>
 
817
  outputs=[network_plot, network_stats]
818
  )
819
 
820
+ # Model Information & Documentation section
821
  gr.HTML("""
822
  <hr style="margin: 40px 0 20px 0;">
823
+ <div style="background-color: #f8f9fa; border: 1px solid #e9ecef; border-radius: 8px; padding: 20px; margin: 20px 0;">
824
+ <h3 style="margin: 0 0 10px 0;">πŸ“š Library Information & Documentation</h3>
825
+ <p style="color: #666; margin-bottom: 15px;">Learn more about the libraries used in this tool:</p>
826
+ <ul style="list-style-type: circle; padding-left: 20px; margin: 0;">
827
+ <li style="margin-bottom: 8px;">
828
+ <strong>NetworkX:</strong>
829
+ <a href="https://networkx.org/documentation/stable/" target="_blank" style="color: #667eea;">NetworkX Documentation β†—</a>
830
+ <span style="color: #888; font-size: 13px;"> β€” Python library for creating, manipulating, and studying complex networks</span>
831
+ </li>
832
+ <li style="margin-bottom: 8px;">
833
+ <strong>PyVis:</strong>
834
+ <a href="https://pyvis.readthedocs.io/en/latest/" target="_blank" style="color: #667eea;">PyVis Documentation β†—</a>
835
+ <span style="color: #888; font-size: 13px;"> β€” Interactive network visualisation library built on vis.js</span>
836
+ </li>
837
+ <li style="margin-bottom: 8px;">
838
+ <strong>Gradio:</strong>
839
+ <a href="https://www.gradio.app/docs" target="_blank" style="color: #667eea;">Gradio Documentation β†—</a>
840
+ <span style="color: #888; font-size: 13px;"> β€” Web interface framework for machine learning demos</span>
841
+ </li>
842
+ </ul>
843
+ </div>
844
+ """)
845
+
846
+ # Footer
847
+ gr.HTML("""
848
+ <hr style="margin: 20px 0 20px 0;">
849
  <div style="text-align: center; color: #666; font-size: 14px; padding: 20px;">
850
  <p>This <strong>Basic Network Explorer</strong> tool was developed as part of a Bodleian Libraries (Oxford) Sassoon Research Fellowship.</p>
851
  <p style="color: #888; font-size: 12px; margin-top: 10px;">Built with the aid of Claude Opus 4.5.</p>