import gradio as gr import networkx as nx from pyvis.network import Network import base64 # Entity type colours (matching your NER tool) # Updated for better distinction between Person, Location, and Event ENTITY_COLOURS = { 'PERSON': '#00B894', # Green 'LOCATION': '#9B59B6', # Purple (more distinct from green) 'EVENT': '#F39C12', # Orange/Gold (very distinct) 'ORGANIZATION': '#55A3FF', # Light Blue 'DATE': '#FF6B6B' # Red } # Relationship types for dropdown RELATIONSHIP_TYPES = [ 'related_to', 'knows', 'works_with', 'located_in', 'participated_in', 'member_of', 'occurred_at', 'employed_by', 'founded', 'attended', 'collaborates_with', 'married_to', 'sibling_of', 'parent_of', 'lives_at', 'wrote', 'visited', 'born_in', 'died_in', 'other' ] class NetworkGraphBuilder: def __init__(self): self.entities = [] self.relationships = [] def add_entity(self, name, entity_type, record_id): """Add an entity to the collection""" if name and name.strip(): # Avoid duplicates existing = [e for e in self.entities if e['name'].lower() == name.strip().lower()] if not existing: self.entities.append({ 'name': name.strip(), 'type': entity_type, 'record_id': record_id }) def add_relationship(self, source, target, rel_type): """Add a relationship between entities""" if source and target and source.strip() and target.strip() and source.strip() != target.strip(): self.relationships.append({ 'source': source.strip(), 'target': target.strip(), 'type': rel_type if rel_type else 'related_to' }) def build_graph(self): """Build NetworkX graph from entities and relationships""" G = nx.Graph() # Add nodes with attributes for entity in self.entities: G.add_node( entity['name'], entity_type=entity['type'], record_id=entity['record_id'] ) # Add edges for rel in self.relationships: if rel['source'] in G.nodes and rel['target'] in G.nodes: G.add_edge( rel['source'], rel['target'], relationship=rel['type'] ) return G def create_pyvis_graph(self, G): """Create interactive PyVis visualisation""" if len(G.nodes) == 0: return None # Create PyVis network with dark theme net = Network( height="600px", width="100%", bgcolor="#1a1a2e", font_color="white", directed=False ) # Configure physics and styling net.set_options(""" { "nodes": { "borderWidth": 2, "borderWidthSelected": 4, "font": { "size": 18, "face": "arial", "color": "white", "strokeWidth": 3, "strokeColor": "#1a1a2e" }, "shadow": { "enabled": true, "color": "rgba(0,0,0,0.5)", "size": 10 } }, "edges": { "color": { "color": "#888888", "highlight": "#ffffff" }, "font": { "size": 14, "face": "arial", "color": "#ffffff", "strokeWidth": 0, "background": "rgba(26, 26, 46, 0.8)", "align": "middle" }, "smooth": { "enabled": true, "type": "continuous" }, "width": 2 }, "physics": { "enabled": true, "barnesHut": { "gravitationalConstant": -8000, "centralGravity": 0.3, "springLength": 150, "springConstant": 0.04, "damping": 0.09 }, "stabilisation": { "enabled": true, "iterations": 100 } }, "interaction": { "hover": true, "tooltipDelay": 200, "dragNodes": true, "dragView": true, "zoomView": true } } """) # Add nodes with styling based on entity type and degree for node in G.nodes(): data = G.nodes[node] entity_type = data.get('entity_type', 'UNKNOWN') colour = ENTITY_COLOURS.get(entity_type, '#CCCCCC') # Size based on connections (degree) degree = G.degree(node) size = 25 + (degree * 8) # Create plain text tooltip (no HTML) connections = list(G.neighbors(node)) title_lines = [ node, f"Type: {entity_type}", f"Connections: {len(connections)}" ] if connections: connected_to = ', '.join(connections[:5]) if len(connections) > 5: connected_to += f"... +{len(connections) - 5} more" title_lines.append(f"Connected to: {connected_to}") title = '\n'.join(title_lines) net.add_node( node, label=node, color=colour, size=size, title=title, font={'size': 18, 'color': 'white', 'strokeWidth': 3, 'strokeColor': '#1a1a2e'} ) # Add edges with relationship labels for edge in G.edges(data=True): rel_type = edge[2].get('relationship', '') net.add_edge( edge[0], edge[1], title=rel_type, label=rel_type, color='#888888', width=2, font={'size': 14, 'color': '#ffffff', 'strokeWidth': 0, 'background': 'rgba(26,26,46,0.8)'} ) # Generate HTML html = net.generate_html() # Encode as base64 data URI for iframe src html_bytes = html.encode('utf-8') b64_html = base64.b64encode(html_bytes).decode('utf-8') iframe_html = f''' ''' return iframe_html def collect_entities_from_records( p1, l1, e1, o1, d1, p2, l2, e2, o2, d2, p3, l3, e3, o3, d3, p4, l4, e4, o4, d4, p5, l5, e5, o5, d5, p6, l6, e6, o6, d6, p7, l7, e7, o7, d7, p8, l8, e8, o8, d8 ): """Collect all entities from the input fields""" builder = NetworkGraphBuilder() # Process each record records = [ (p1, l1, e1, o1, d1), (p2, l2, e2, o2, d2), (p3, l3, e3, o3, d3), (p4, l4, e4, o4, d4), (p5, l5, e5, o5, d5), (p6, l6, e6, o6, d6), (p7, l7, e7, o7, d7), (p8, l8, e8, o8, d8), ] for record_id, (person, location, event, org, date) in enumerate(records, 1): if person: builder.add_entity(person, 'PERSON', record_id) if location: builder.add_entity(location, 'LOCATION', record_id) if event: builder.add_entity(event, 'EVENT', record_id) if org: builder.add_entity(org, 'ORGANIZATION', record_id) if date: builder.add_entity(date, 'DATE', record_id) # Create list of all entity names for relationship dropdowns entity_names = sorted([e['name'] for e in builder.entities]) # Count by type counts = { 'PERSON': sum(1 for e in builder.entities if e['type'] == 'PERSON'), 'LOCATION': sum(1 for e in builder.entities if e['type'] == 'LOCATION'), 'EVENT': sum(1 for e in builder.entities if e['type'] == 'EVENT'), 'ORGANIZATION': sum(1 for e in builder.entities if e['type'] == 'ORGANIZATION'), 'DATE': sum(1 for e in builder.entities if e['type'] == 'DATE'), } # Create HTML summary that spans full width summary_html = f'''

πŸ“Š Identified Entities ({len(builder.entities)} total)

{counts['PERSON']}
πŸ‘€ People
{counts['LOCATION']}
πŸ“ Locations
{counts['EVENT']}
πŸ“… Events
{counts['ORGANIZATION']}
🏒 Organisations
{counts['DATE']}
πŸ—“οΈ Dates
Entities: {', '.join(entity_names) if entity_names else 'None found'}
''' # Return summary and update all 10 dropdowns (5 source + 5 target) return ( summary_html, gr.update(choices=entity_names, value=None), # source 1 gr.update(choices=entity_names, value=None), # target 1 gr.update(choices=entity_names, value=None), # source 2 gr.update(choices=entity_names, value=None), # target 2 gr.update(choices=entity_names, value=None), # source 3 gr.update(choices=entity_names, value=None), # target 3 gr.update(choices=entity_names, value=None), # source 4 gr.update(choices=entity_names, value=None), # target 4 gr.update(choices=entity_names, value=None), # source 5 gr.update(choices=entity_names, value=None), # target 5 ) def generate_network_graph( p1, l1, e1, o1, d1, p2, l2, e2, o2, d2, p3, l3, e3, o3, d3, p4, l4, e4, o4, d4, p5, l5, e5, o5, d5, p6, l6, e6, o6, d6, p7, l7, e7, o7, d7, p8, l8, e8, o8, d8, src1, rel1, tgt1, src2, rel2, tgt2, src3, rel3, tgt3, src4, rel4, tgt4, src5, rel5, tgt5 ): """Generate the network graph from all inputs""" try: builder = NetworkGraphBuilder() # Process each record records = [ (p1, l1, e1, o1, d1), (p2, l2, e2, o2, d2), (p3, l3, e3, o3, d3), (p4, l4, e4, o4, d4), (p5, l5, e5, o5, d5), (p6, l6, e6, o6, d6), (p7, l7, e7, o7, d7), (p8, l8, e8, o8, d8), ] for record_id, (person, location, event, org, date) in enumerate(records, 1): if person: builder.add_entity(person, 'PERSON', record_id) if location: builder.add_entity(location, 'LOCATION', record_id) if event: builder.add_entity(event, 'EVENT', record_id) if org: builder.add_entity(org, 'ORGANIZATION', record_id) if date: builder.add_entity(date, 'DATE', record_id) # Process relationships relationships = [ (src1, rel1, tgt1), (src2, rel2, tgt2), (src3, rel3, tgt3), (src4, rel4, tgt4), (src5, rel5, tgt5), ] for source, rel_type, target in relationships: if source and target: builder.add_relationship(source, target, rel_type) # Build graph G = builder.build_graph() if len(G.nodes) == 0: empty_html = '''
πŸ“Š

No entities to display

Enter entities above and click "Identify Entities" first

''' return empty_html, "❌ **No entities to display.** Please enter entities in Step 1 first." # Create visualisation graph_html = builder.create_pyvis_graph(G) # Create statistics stats_html = f'''

πŸ“ˆ Network Statistics

''' if len(G.edges) > 0: density = nx.density(G) avg_degree = sum(dict(G.degree()).values()) / G.number_of_nodes() stats_html += f'''
Nodes {G.number_of_nodes()}
Edges {G.number_of_edges()}
Density {density:.3f}
Avg. Connections {avg_degree:.2f}
''' # Most connected degrees = dict(G.degree()) top_nodes = sorted(degrees.items(), key=lambda x: x[1], reverse=True)[:3] stats_html += '''
Most Connected:
' else: stats_html += '''
⚠️ No relationships defined - nodes are isolated
''' stats_html += '
' return graph_html, stats_html except Exception as e: import traceback error_trace = traceback.format_exc() error_html = f'''
⚠️

Error generating graph

{str(e)}

''' return error_html, f"❌ Error: {str(e)}" def load_austen_example(): """Load the Jane Austen Pride and Prejudice example""" return ( # Record 1 "Elizabeth Bennet", "Longbourn", "Meryton Ball", "Bennet Family", "1811", # Record 2 "Mr. Darcy", "Pemberley", "Netherfield Ball", "Darcy Estate", "", # Record 3 "Jane Bennet", "Netherfield", "", "", "", # Record 4 "Mr. Bingley", "London", "", "", "", # Record 5 "Mr. Wickham", "Meryton", "", "Militia", "", # Record 6 "Charlotte Lucas", "Hunsford", "", "", "", # Record 7 "", "", "", "", "", # Record 8 "", "", "", "", "", ) def load_wwii_example(): """Load a WWII history example""" return ( # Record 1 "Winston Churchill", "London", "Battle of Britain", "War Cabinet", "1940", # Record 2 "Franklin D. Roosevelt", "Washington D.C.", "D-Day", "Allied Forces", "1944", # Record 3 "Field Marshal Montgomery", "North Africa", "Battle of El Alamein", "Eighth Army", "1942", # Record 4 "Clement Attlee", "Potsdam", "Potsdam Conference", "Labour Party", "1945", # Record 5 "", "", "", "", "", # Record 6 "", "", "", "", "", # Record 7 "", "", "", "", "", # Record 8 "", "", "", "", "", ) def create_coloured_label(text, colour, emoji): """Create a coloured pill-style label HTML""" return f''' {emoji} {text} ''' def create_interface(): with gr.Blocks(title="Network Explorer", theme=gr.themes.Soft()) as demo: gr.Markdown(""" # πŸ•ΈοΈ Basic Network Explorer Build interactive network graphs by entering entities extracted through Named Entity Recognition (NER). ### How to use this tool: 1. **πŸ“ Enter entities** in the records below (or load an example to get started) 2. **πŸ” Click "Identify Entities"** to collect and list all entities 3. **🀝 Define relationships** between entities using the dropdowns 4. **🎨 Click "Generate Network Graph"** to visualise 5. **πŸ‘οΈ Explore** - drag nodes to rearrange, scroll to zoom, hover for details """) gr.HTML("""
πŸ’‘ Top tip: Start with just a few entities and relationships to see how it works!
""") # Quick start buttons gr.Markdown("### πŸ’‘ Quick Start - Load an Example:") with gr.Row(): austen_btn = gr.Button("πŸ“š Jane Austen (Pride & Prejudice)", variant="secondary", size="sm") wwii_btn = gr.Button("βš”οΈ WWII History", variant="secondary", size="sm") gr.HTML("
") # ==================== STEP 1: ENTITY INPUT (4 per row) ==================== gr.Markdown("## πŸ“ Step 1: Enter Entities") entity_inputs = [] # First row: Records 1-4 (all on one line) with gr.Row(): with gr.Column(scale=1, min_width=200): gr.Markdown("**Record 1**") gr.HTML(create_coloured_label("Person", ENTITY_COLOURS['PERSON'], "πŸ‘€")) p1 = gr.Textbox(label="", placeholder="e.g., Elizabeth Bennet", show_label=False) gr.HTML(create_coloured_label("Location", ENTITY_COLOURS['LOCATION'], "πŸ“")) l1 = gr.Textbox(label="", placeholder="e.g., Longbourn", show_label=False) gr.HTML(create_coloured_label("Event", ENTITY_COLOURS['EVENT'], "πŸ“…")) e1 = gr.Textbox(label="", placeholder="e.g., Meryton Ball", show_label=False) gr.HTML(create_coloured_label("Organisation", ENTITY_COLOURS['ORGANIZATION'], "🏒")) o1 = gr.Textbox(label="", placeholder="e.g., Bennet Family", show_label=False) gr.HTML(create_coloured_label("Date", ENTITY_COLOURS['DATE'], "πŸ—“οΈ")) d1 = gr.Textbox(label="", placeholder="e.g., 1811", show_label=False) entity_inputs.extend([p1, l1, e1, o1, d1]) with gr.Column(scale=1, min_width=200): gr.Markdown("**Record 2**") gr.HTML(create_coloured_label("Person", ENTITY_COLOURS['PERSON'], "πŸ‘€")) p2 = gr.Textbox(label="", placeholder="e.g., Mr. Darcy", show_label=False) gr.HTML(create_coloured_label("Location", ENTITY_COLOURS['LOCATION'], "πŸ“")) l2 = gr.Textbox(label="", placeholder="e.g., Pemberley", show_label=False) gr.HTML(create_coloured_label("Event", ENTITY_COLOURS['EVENT'], "πŸ“…")) e2 = gr.Textbox(label="", placeholder="e.g., Netherfield Ball", show_label=False) gr.HTML(create_coloured_label("Organisation", ENTITY_COLOURS['ORGANIZATION'], "🏒")) o2 = gr.Textbox(label="", placeholder="e.g., Darcy Estate", show_label=False) gr.HTML(create_coloured_label("Date", ENTITY_COLOURS['DATE'], "πŸ—“οΈ")) d2 = gr.Textbox(label="", placeholder="", show_label=False) entity_inputs.extend([p2, l2, e2, o2, d2]) with gr.Column(scale=1, min_width=200): gr.Markdown("**Record 3**") gr.HTML(create_coloured_label("Person", ENTITY_COLOURS['PERSON'], "πŸ‘€")) p3 = gr.Textbox(label="", placeholder="e.g., Jane Bennet", show_label=False) gr.HTML(create_coloured_label("Location", ENTITY_COLOURS['LOCATION'], "πŸ“")) l3 = gr.Textbox(label="", placeholder="e.g., Netherfield", show_label=False) gr.HTML(create_coloured_label("Event", ENTITY_COLOURS['EVENT'], "πŸ“…")) e3 = gr.Textbox(label="", placeholder="", show_label=False) gr.HTML(create_coloured_label("Organisation", ENTITY_COLOURS['ORGANIZATION'], "🏒")) o3 = gr.Textbox(label="", placeholder="", show_label=False) gr.HTML(create_coloured_label("Date", ENTITY_COLOURS['DATE'], "πŸ—“οΈ")) d3 = gr.Textbox(label="", placeholder="", show_label=False) entity_inputs.extend([p3, l3, e3, o3, d3]) with gr.Column(scale=1, min_width=200): gr.Markdown("**Record 4**") gr.HTML(create_coloured_label("Person", ENTITY_COLOURS['PERSON'], "πŸ‘€")) p4 = gr.Textbox(label="", placeholder="e.g., Mr. Bingley", show_label=False) gr.HTML(create_coloured_label("Location", ENTITY_COLOURS['LOCATION'], "πŸ“")) l4 = gr.Textbox(label="", placeholder="e.g., London", show_label=False) gr.HTML(create_coloured_label("Event", ENTITY_COLOURS['EVENT'], "πŸ“…")) e4 = gr.Textbox(label="", placeholder="", show_label=False) gr.HTML(create_coloured_label("Organisation", ENTITY_COLOURS['ORGANIZATION'], "🏒")) o4 = gr.Textbox(label="", placeholder="", show_label=False) gr.HTML(create_coloured_label("Date", ENTITY_COLOURS['DATE'], "πŸ—“οΈ")) d4 = gr.Textbox(label="", placeholder="", show_label=False) entity_inputs.extend([p4, l4, e4, o4, d4]) # Additional records 5-8 with gr.Accordion("βž• Additional Records (5-8)", open=False): with gr.Row(): with gr.Column(scale=1, min_width=200): gr.Markdown("**Record 5**") gr.HTML(create_coloured_label("Person", ENTITY_COLOURS['PERSON'], "πŸ‘€")) p5 = gr.Textbox(label="", show_label=False) gr.HTML(create_coloured_label("Location", ENTITY_COLOURS['LOCATION'], "πŸ“")) l5 = gr.Textbox(label="", show_label=False) gr.HTML(create_coloured_label("Event", ENTITY_COLOURS['EVENT'], "πŸ“…")) e5 = gr.Textbox(label="", show_label=False) gr.HTML(create_coloured_label("Organisation", ENTITY_COLOURS['ORGANIZATION'], "🏒")) o5 = gr.Textbox(label="", show_label=False) gr.HTML(create_coloured_label("Date", ENTITY_COLOURS['DATE'], "πŸ—“οΈ")) d5 = gr.Textbox(label="", show_label=False) entity_inputs.extend([p5, l5, e5, o5, d5]) with gr.Column(scale=1, min_width=200): gr.Markdown("**Record 6**") gr.HTML(create_coloured_label("Person", ENTITY_COLOURS['PERSON'], "πŸ‘€")) p6 = gr.Textbox(label="", show_label=False) gr.HTML(create_coloured_label("Location", ENTITY_COLOURS['LOCATION'], "πŸ“")) l6 = gr.Textbox(label="", show_label=False) gr.HTML(create_coloured_label("Event", ENTITY_COLOURS['EVENT'], "πŸ“…")) e6 = gr.Textbox(label="", show_label=False) gr.HTML(create_coloured_label("Organisation", ENTITY_COLOURS['ORGANIZATION'], "🏒")) o6 = gr.Textbox(label="", show_label=False) gr.HTML(create_coloured_label("Date", ENTITY_COLOURS['DATE'], "πŸ—“οΈ")) d6 = gr.Textbox(label="", show_label=False) entity_inputs.extend([p6, l6, e6, o6, d6]) with gr.Column(scale=1, min_width=200): gr.Markdown("**Record 7**") gr.HTML(create_coloured_label("Person", ENTITY_COLOURS['PERSON'], "πŸ‘€")) p7 = gr.Textbox(label="", show_label=False) gr.HTML(create_coloured_label("Location", ENTITY_COLOURS['LOCATION'], "πŸ“")) l7 = gr.Textbox(label="", show_label=False) gr.HTML(create_coloured_label("Event", ENTITY_COLOURS['EVENT'], "πŸ“…")) e7 = gr.Textbox(label="", show_label=False) gr.HTML(create_coloured_label("Organisation", ENTITY_COLOURS['ORGANIZATION'], "🏒")) o7 = gr.Textbox(label="", show_label=False) gr.HTML(create_coloured_label("Date", ENTITY_COLOURS['DATE'], "πŸ—“οΈ")) d7 = gr.Textbox(label="", show_label=False) entity_inputs.extend([p7, l7, e7, o7, d7]) with gr.Column(scale=1, min_width=200): gr.Markdown("**Record 8**") gr.HTML(create_coloured_label("Person", ENTITY_COLOURS['PERSON'], "πŸ‘€")) p8 = gr.Textbox(label="", show_label=False) gr.HTML(create_coloured_label("Location", ENTITY_COLOURS['LOCATION'], "πŸ“")) l8 = gr.Textbox(label="", show_label=False) gr.HTML(create_coloured_label("Event", ENTITY_COLOURS['EVENT'], "πŸ“…")) e8 = gr.Textbox(label="", show_label=False) gr.HTML(create_coloured_label("Organisation", ENTITY_COLOURS['ORGANIZATION'], "🏒")) o8 = gr.Textbox(label="", show_label=False) gr.HTML(create_coloured_label("Date", ENTITY_COLOURS['DATE'], "πŸ—“οΈ")) d8 = gr.Textbox(label="", show_label=False) entity_inputs.extend([p8, l8, e8, o8, d8]) # Identify button collect_btn = gr.Button("πŸ” Identify Entities", variant="primary", size="lg") # Full-width entity summary entity_summary = gr.HTML() gr.HTML("
") # ==================== STEP 2: RELATIONSHIPS (Grid) ==================== gr.Markdown("## 🀝 Step 2: Define Relationships") gr.Markdown("*Select entities from the dropdowns to create connections (click 'Identify Entities' first)*") # Relationship inputs in a grid (5 columns) relationship_inputs = [] with gr.Row(): with gr.Column(scale=1, min_width=180): gr.Markdown("**Relationship 1**") src1 = gr.Dropdown(label="From", choices=[]) rel1 = gr.Dropdown(label="Type", choices=RELATIONSHIP_TYPES, value="related_to") tgt1 = gr.Dropdown(label="To", choices=[]) relationship_inputs.extend([src1, rel1, tgt1]) with gr.Column(scale=1, min_width=180): gr.Markdown("**Relationship 2**") src2 = gr.Dropdown(label="From", choices=[]) rel2 = gr.Dropdown(label="Type", choices=RELATIONSHIP_TYPES, value="related_to") tgt2 = gr.Dropdown(label="To", choices=[]) relationship_inputs.extend([src2, rel2, tgt2]) with gr.Column(scale=1, min_width=180): gr.Markdown("**Relationship 3**") src3 = gr.Dropdown(label="From", choices=[]) rel3 = gr.Dropdown(label="Type", choices=RELATIONSHIP_TYPES, value="related_to") tgt3 = gr.Dropdown(label="To", choices=[]) relationship_inputs.extend([src3, rel3, tgt3]) with gr.Column(scale=1, min_width=180): gr.Markdown("**Relationship 4**") src4 = gr.Dropdown(label="From", choices=[]) rel4 = gr.Dropdown(label="Type", choices=RELATIONSHIP_TYPES, value="related_to") tgt4 = gr.Dropdown(label="To", choices=[]) relationship_inputs.extend([src4, rel4, tgt4]) with gr.Column(scale=1, min_width=180): gr.Markdown("**Relationship 5**") src5 = gr.Dropdown(label="From", choices=[]) rel5 = gr.Dropdown(label="Type", choices=RELATIONSHIP_TYPES, value="related_to") tgt5 = gr.Dropdown(label="To", choices=[]) relationship_inputs.extend([src5, rel5, tgt5]) gr.HTML("
") # ==================== STEP 3: GENERATE & VIEW ==================== gr.Markdown("## 🎨 Step 3: Generate Network Graph") generate_btn = gr.Button("🎨 Generate Network Graph", variant="primary", size="lg") # Full-width network graph with stats sidebar with gr.Row(): with gr.Column(scale=3): network_plot = gr.HTML(label="Interactive Network Graph") with gr.Column(scale=1): network_stats = gr.HTML() # Colour legend gr.HTML(f"""

🎨 Entity Colour Legend

Person Location Event Organisation Date

πŸ–±οΈ Interaction: Drag nodes to rearrange β€’ Scroll to zoom β€’ Hover for details

""") # ==================== WIRE UP EVENTS ==================== # Example buttons austen_btn.click( fn=load_austen_example, inputs=[], outputs=entity_inputs ) wwii_btn.click( fn=load_wwii_example, inputs=[], outputs=entity_inputs ) # Collect entities collect_btn.click( fn=collect_entities_from_records, inputs=entity_inputs, outputs=[ entity_summary, src1, tgt1, src2, tgt2, src3, tgt3, src4, tgt4, src5, tgt5 ] ) # Generate graph all_inputs = entity_inputs + relationship_inputs generate_btn.click( fn=generate_network_graph, inputs=all_inputs, outputs=[network_plot, network_stats] ) # Model Information & Documentation section gr.HTML("""

πŸ“š Library Information & Documentation

Learn more about the libraries used in this tool:

""") # Footer gr.HTML("""

This Basic Network Explorer tool was developed as part of a Bodleian Libraries (Oxford) Sassoon Research Fellowship.

Built with the aid of Claude Opus 4.5.

""") return demo if __name__ == "__main__": demo = create_interface() demo.launch()