Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,9 +1,11 @@
|
|
| 1 |
import gradio as gr
|
| 2 |
import networkx as nx
|
| 3 |
-
|
| 4 |
-
import
|
|
|
|
|
|
|
| 5 |
|
| 6 |
-
# Entity type colors
|
| 7 |
ENTITY_COLORS = {
|
| 8 |
'PERSON': '#00B894', # Green
|
| 9 |
'LOCATION': '#A0E7E5', # Light Cyan
|
|
@@ -14,6 +16,8 @@ ENTITY_COLORS = {
|
|
| 14 |
|
| 15 |
# Relationship types for dropdown
|
| 16 |
RELATIONSHIP_TYPES = [
|
|
|
|
|
|
|
| 17 |
'works_with',
|
| 18 |
'located_in',
|
| 19 |
'participated_in',
|
|
@@ -22,17 +26,19 @@ RELATIONSHIP_TYPES = [
|
|
| 22 |
'employed_by',
|
| 23 |
'founded',
|
| 24 |
'attended',
|
| 25 |
-
'knows',
|
| 26 |
-
'related_to',
|
| 27 |
'collaborates_with',
|
| 28 |
'married_to',
|
| 29 |
'sibling_of',
|
| 30 |
'parent_of',
|
| 31 |
'lives_at',
|
| 32 |
'wrote',
|
|
|
|
|
|
|
|
|
|
| 33 |
'other'
|
| 34 |
]
|
| 35 |
|
|
|
|
| 36 |
class NetworkGraphBuilder:
|
| 37 |
def __init__(self):
|
| 38 |
self.entities = []
|
|
@@ -82,129 +88,133 @@ class NetworkGraphBuilder:
|
|
| 82 |
|
| 83 |
return G
|
| 84 |
|
| 85 |
-
def
|
| 86 |
-
"""Create interactive
|
| 87 |
if len(G.nodes) == 0:
|
| 88 |
return None
|
| 89 |
|
| 90 |
-
#
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
edge_mid_x = []
|
| 98 |
-
edge_mid_y = []
|
| 99 |
-
|
| 100 |
-
for edge in G.edges(data=True):
|
| 101 |
-
x0, y0 = pos[edge[0]]
|
| 102 |
-
x1, y1 = pos[edge[1]]
|
| 103 |
-
edge_x.extend([x0, x1, None])
|
| 104 |
-
edge_y.extend([y0, y1, None])
|
| 105 |
-
|
| 106 |
-
# Midpoint for edge label
|
| 107 |
-
mid_x = (x0 + x1) / 2
|
| 108 |
-
mid_y = (y0 + y1) / 2
|
| 109 |
-
edge_mid_x.append(mid_x)
|
| 110 |
-
edge_mid_y.append(mid_y)
|
| 111 |
-
edge_labels.append(edge[2].get('relationship', ''))
|
| 112 |
-
|
| 113 |
-
edge_trace = go.Scatter(
|
| 114 |
-
x=edge_x, y=edge_y,
|
| 115 |
-
line=dict(width=2, color='#888'),
|
| 116 |
-
hoverinfo='none',
|
| 117 |
-
mode='lines'
|
| 118 |
-
)
|
| 119 |
-
|
| 120 |
-
# Edge labels trace
|
| 121 |
-
edge_label_trace = go.Scatter(
|
| 122 |
-
x=edge_mid_x, y=edge_mid_y,
|
| 123 |
-
mode='text',
|
| 124 |
-
text=edge_labels,
|
| 125 |
-
textposition='middle center',
|
| 126 |
-
textfont=dict(size=10, color='#666'),
|
| 127 |
-
hoverinfo='none'
|
| 128 |
)
|
| 129 |
|
| 130 |
-
#
|
| 131 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
node_x = []
|
| 139 |
-
node_y = []
|
| 140 |
-
node_text = []
|
| 141 |
-
node_hover = []
|
| 142 |
-
node_sizes = []
|
| 143 |
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
node_y.append(y)
|
| 148 |
-
node_text.append(node)
|
| 149 |
-
|
| 150 |
-
# Calculate connections
|
| 151 |
-
connections = list(G.neighbors(node))
|
| 152 |
-
degree = len(connections)
|
| 153 |
-
node_sizes.append(30 + (degree * 10))
|
| 154 |
-
|
| 155 |
-
# Hover text
|
| 156 |
-
hover = f"<b>{node}</b><br>Type: {entity_type}<br>Connections: {degree}"
|
| 157 |
-
if connections:
|
| 158 |
-
hover += f"<br>Connected to: {', '.join(connections[:5])}"
|
| 159 |
-
if len(connections) > 5:
|
| 160 |
-
hover += f"<br>... +{len(connections) - 5} more"
|
| 161 |
-
node_hover.append(hover)
|
| 162 |
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
)
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
yanchor="top",
|
| 192 |
-
y=0.99,
|
| 193 |
-
xanchor="left",
|
| 194 |
-
x=0.01,
|
| 195 |
-
bgcolor="rgba(255,255,255,0.8)"
|
| 196 |
-
),
|
| 197 |
-
hovermode='closest',
|
| 198 |
-
margin=dict(b=20, l=5, r=5, t=50),
|
| 199 |
-
xaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
|
| 200 |
-
yaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
|
| 201 |
-
plot_bgcolor='#fafafa',
|
| 202 |
-
paper_bgcolor='#fafafa',
|
| 203 |
-
height=600
|
| 204 |
)
|
| 205 |
-
)
|
| 206 |
|
| 207 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
|
| 209 |
|
| 210 |
def collect_entities_from_records(
|
|
@@ -252,26 +262,43 @@ def collect_entities_from_records(
|
|
| 252 |
'DATE': sum(1 for e in builder.entities if e['type'] == 'DATE'),
|
| 253 |
}
|
| 254 |
|
| 255 |
-
# Create summary
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 270 |
|
| 271 |
# Return summary and update all 10 dropdowns (5 source + 5 target)
|
| 272 |
-
# Each dropdown gets updated with the entity names
|
| 273 |
return (
|
| 274 |
-
|
| 275 |
gr.update(choices=entity_names, value=None), # source 1
|
| 276 |
gr.update(choices=entity_names, value=None), # target 1
|
| 277 |
gr.update(choices=entity_names, value=None), # source 2
|
|
@@ -341,76 +368,87 @@ def generate_network_graph(
|
|
| 341 |
G = builder.build_graph()
|
| 342 |
|
| 343 |
if len(G.nodes) == 0:
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
paper_bgcolor='#fafafa'
|
| 355 |
-
)
|
| 356 |
-
return empty_fig, "❌ **No entities to display.** Please enter entities in Step 1 first."
|
| 357 |
|
| 358 |
# Create visualization
|
| 359 |
-
|
| 360 |
|
| 361 |
# Create statistics
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
""
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 373 |
density = nx.density(G)
|
| 374 |
avg_degree = sum(dict(G.degree()).values()) / G.number_of_nodes()
|
| 375 |
-
|
| 376 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 377 |
|
| 378 |
-
#
|
| 379 |
degrees = dict(G.degree())
|
| 380 |
top_nodes = sorted(degrees.items(), key=lambda x: x[1], reverse=True)[:3]
|
| 381 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 382 |
for node, degree in top_nodes:
|
| 383 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 384 |
|
| 385 |
-
return
|
| 386 |
|
| 387 |
except Exception as e:
|
| 388 |
import traceback
|
| 389 |
error_trace = traceback.format_exc()
|
| 390 |
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
### ❌ Error Generating Graph
|
| 402 |
-
|
| 403 |
-
{str(e)}
|
| 404 |
-
|
| 405 |
-
<details>
|
| 406 |
-
<summary>Technical details</summary>
|
| 407 |
-
|
| 408 |
-
```
|
| 409 |
-
{error_trace}
|
| 410 |
-
```
|
| 411 |
-
</details>
|
| 412 |
-
"""
|
| 413 |
-
return error_fig, error_msg
|
| 414 |
|
| 415 |
|
| 416 |
def load_austen_example():
|
|
@@ -419,11 +457,11 @@ def load_austen_example():
|
|
| 419 |
# Record 1
|
| 420 |
"Elizabeth Bennet", "Longbourn", "Meryton Ball", "Bennet Family", "1811",
|
| 421 |
# Record 2
|
| 422 |
-
"Mr. Darcy", "Pemberley", "Netherfield Ball", "Darcy Estate", "
|
| 423 |
# Record 3
|
| 424 |
-
"Jane Bennet", "Netherfield", "", "
|
| 425 |
# Record 4
|
| 426 |
-
"Mr. Bingley", "
|
| 427 |
# Record 5
|
| 428 |
"Mr. Wickham", "Meryton", "", "Militia", "",
|
| 429 |
# Record 6
|
|
@@ -437,11 +475,11 @@ def load_wwii_example():
|
|
| 437 |
# Record 1
|
| 438 |
"Winston Churchill", "London", "Battle of Britain", "War Cabinet", "1940",
|
| 439 |
# Record 2
|
| 440 |
-
"
|
| 441 |
# Record 3
|
| 442 |
"Field Marshal Montgomery", "North Africa", "Battle of El Alamein", "Eighth Army", "1942",
|
| 443 |
# Record 4
|
| 444 |
-
"
|
| 445 |
# Record 5
|
| 446 |
"", "", "", "", "",
|
| 447 |
# Record 6
|
|
@@ -450,136 +488,213 @@ def load_wwii_example():
|
|
| 450 |
|
| 451 |
|
| 452 |
def create_interface():
|
| 453 |
-
with gr.Blocks(title="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 454 |
gr.Markdown("""
|
| 455 |
-
# 🕸️
|
| 456 |
-
|
| 457 |
-
Build interactive network graphs by entering entities extracted through Named Entity Recognition (NER).
|
| 458 |
-
Explore relationships between people, places, events, organizations and dates.
|
| 459 |
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
2. **🔍 Click "Identify Entities"** to collect and list all entities
|
| 463 |
-
3. **🤝 Define relationships** between entities using the dropdowns
|
| 464 |
-
4. **🎨 Click "Generate Network Graph"** to visualize
|
| 465 |
-
5. **👁️ Explore** - hover over nodes for details, zoom and pan the graph
|
| 466 |
""")
|
| 467 |
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 473 |
|
| 474 |
-
# Store all entity input components
|
| 475 |
entity_inputs = []
|
| 476 |
|
| 477 |
-
#
|
| 478 |
with gr.Row():
|
| 479 |
-
gr.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 480 |
with gr.Row():
|
| 481 |
-
|
| 482 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 483 |
|
| 484 |
gr.HTML("<hr style='margin: 20px 0;'>")
|
| 485 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 486 |
with gr.Row():
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
gr.
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
with gr.Group():
|
| 494 |
-
gr.Markdown(f"**Record {i+1}**")
|
| 495 |
-
with gr.Row():
|
| 496 |
-
person = gr.Textbox(label="👤 Person", placeholder="e.g., Elizabeth Bennet", scale=1)
|
| 497 |
-
location = gr.Textbox(label="📍 Location", placeholder="e.g., Longbourn", scale=1)
|
| 498 |
-
with gr.Row():
|
| 499 |
-
event = gr.Textbox(label="📅 Event", placeholder="e.g., Meryton Ball", scale=1)
|
| 500 |
-
org = gr.Textbox(label="🏢 Organization", placeholder="e.g., Bennet Family", scale=1)
|
| 501 |
-
date = gr.Textbox(label="🗓️ Date", placeholder="e.g., 1811")
|
| 502 |
-
entity_inputs.extend([person, location, event, org, date])
|
| 503 |
-
|
| 504 |
-
with gr.Accordion("Records 5-6 (Optional)", open=False):
|
| 505 |
-
for i in range(4, 6):
|
| 506 |
-
with gr.Group():
|
| 507 |
-
gr.Markdown(f"**Record {i+1}**")
|
| 508 |
-
with gr.Row():
|
| 509 |
-
person = gr.Textbox(label="👤 Person", scale=1)
|
| 510 |
-
location = gr.Textbox(label="📍 Location", scale=1)
|
| 511 |
-
with gr.Row():
|
| 512 |
-
event = gr.Textbox(label="📅 Event", scale=1)
|
| 513 |
-
org = gr.Textbox(label="🏢 Organization", scale=1)
|
| 514 |
-
date = gr.Textbox(label="🗓️ Date")
|
| 515 |
-
entity_inputs.extend([person, location, event, org, date])
|
| 516 |
-
|
| 517 |
-
collect_btn = gr.Button("🔍 Identify Entities", variant="primary", size="lg")
|
| 518 |
-
entity_summary = gr.Markdown()
|
| 519 |
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
gr.
|
| 523 |
-
gr.
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
gr.
|
| 532 |
-
|
| 533 |
-
source2 = gr.Dropdown(label="From", choices=[], scale=2)
|
| 534 |
-
with gr.Row():
|
| 535 |
-
rel_type2 = gr.Dropdown(label="Relationship Type", choices=RELATIONSHIP_TYPES, value="related_to", scale=2)
|
| 536 |
-
target2 = gr.Dropdown(label="To", choices=[], scale=2)
|
| 537 |
-
|
| 538 |
-
gr.HTML("<hr style='margin: 10px 0; border-top: 1px dashed #ccc;'>")
|
| 539 |
-
|
| 540 |
-
source3 = gr.Dropdown(label="From", choices=[], scale=2)
|
| 541 |
-
with gr.Row():
|
| 542 |
-
rel_type3 = gr.Dropdown(label="Relationship Type", choices=RELATIONSHIP_TYPES, value="related_to", scale=2)
|
| 543 |
-
target3 = gr.Dropdown(label="To", choices=[], scale=2)
|
| 544 |
-
|
| 545 |
-
gr.HTML("<hr style='margin: 10px 0; border-top: 1px dashed #ccc;'>")
|
| 546 |
-
|
| 547 |
-
source4 = gr.Dropdown(label="From", choices=[], scale=2)
|
| 548 |
-
with gr.Row():
|
| 549 |
-
rel_type4 = gr.Dropdown(label="Relationship Type", choices=RELATIONSHIP_TYPES, value="related_to", scale=2)
|
| 550 |
-
target4 = gr.Dropdown(label="To", choices=[], scale=2)
|
| 551 |
-
|
| 552 |
-
gr.HTML("<hr style='margin: 10px 0; border-top: 1px dashed #ccc;'>")
|
| 553 |
-
|
| 554 |
-
source5 = gr.Dropdown(label="From", choices=[], scale=2)
|
| 555 |
-
with gr.Row():
|
| 556 |
-
rel_type5 = gr.Dropdown(label="Relationship Type", choices=RELATIONSHIP_TYPES, value="related_to", scale=2)
|
| 557 |
-
target5 = gr.Dropdown(label="To", choices=[], scale=2)
|
| 558 |
-
|
| 559 |
-
# Collect relationship inputs
|
| 560 |
-
relationship_inputs = [
|
| 561 |
-
source1, rel_type1, target1,
|
| 562 |
-
source2, rel_type2, target2,
|
| 563 |
-
source3, rel_type3, target3,
|
| 564 |
-
source4, rel_type4, target4,
|
| 565 |
-
source5, rel_type5, target5
|
| 566 |
-
]
|
| 567 |
|
| 568 |
-
gr.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 569 |
|
| 570 |
-
|
| 571 |
-
generate_btn = gr.Button("🎨 Generate Network Graph", variant="primary", size="lg")
|
| 572 |
|
| 573 |
-
#
|
| 574 |
-
gr.Markdown("##
|
| 575 |
|
|
|
|
|
|
|
|
|
|
| 576 |
with gr.Row():
|
| 577 |
-
with gr.Column(scale=
|
| 578 |
-
network_plot = gr.
|
| 579 |
with gr.Column(scale=1):
|
| 580 |
-
network_stats = gr.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 581 |
|
| 582 |
-
#
|
|
|
|
|
|
|
| 583 |
austen_btn.click(
|
| 584 |
fn=load_austen_example,
|
| 585 |
inputs=[],
|
|
@@ -592,21 +707,21 @@ def create_interface():
|
|
| 592 |
outputs=entity_inputs
|
| 593 |
)
|
| 594 |
|
| 595 |
-
#
|
| 596 |
collect_btn.click(
|
| 597 |
fn=collect_entities_from_records,
|
| 598 |
inputs=entity_inputs,
|
| 599 |
outputs=[
|
| 600 |
entity_summary,
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
]
|
| 607 |
)
|
| 608 |
|
| 609 |
-
#
|
| 610 |
all_inputs = entity_inputs + relationship_inputs
|
| 611 |
generate_btn.click(
|
| 612 |
fn=generate_network_graph,
|
|
@@ -614,41 +729,12 @@ def create_interface():
|
|
| 614 |
outputs=[network_plot, network_stats]
|
| 615 |
)
|
| 616 |
|
| 617 |
-
# Color legend
|
| 618 |
-
gr.HTML("""
|
| 619 |
-
<div style="background-color: #f8f9fa; padding: 15px; border-radius: 8px; margin-top: 20px;">
|
| 620 |
-
<h4 style="margin-top: 0;">🎨 Entity Color Legend</h4>
|
| 621 |
-
<div style="display: flex; flex-wrap: wrap; gap: 15px; align-items: center;">
|
| 622 |
-
<span style="display: flex; align-items: center; gap: 5px;">
|
| 623 |
-
<span style="width: 20px; height: 20px; border-radius: 50%; background-color: #00B894; display: inline-block;"></span>
|
| 624 |
-
<strong>Person</strong>
|
| 625 |
-
</span>
|
| 626 |
-
<span style="display: flex; align-items: center; gap: 5px;">
|
| 627 |
-
<span style="width: 20px; height: 20px; border-radius: 50%; background-color: #A0E7E5; display: inline-block;"></span>
|
| 628 |
-
<strong>Location</strong>
|
| 629 |
-
</span>
|
| 630 |
-
<span style="display: flex; align-items: center; gap: 5px;">
|
| 631 |
-
<span style="width: 20px; height: 20px; border-radius: 50%; background-color: #4ECDC4; display: inline-block;"></span>
|
| 632 |
-
<strong>Event</strong>
|
| 633 |
-
</span>
|
| 634 |
-
<span style="display: flex; align-items: center; gap: 5px;">
|
| 635 |
-
<span style="width: 20px; height: 20px; border-radius: 50%; background-color: #55A3FF; display: inline-block;"></span>
|
| 636 |
-
<strong>Organization</strong>
|
| 637 |
-
</span>
|
| 638 |
-
<span style="display: flex; align-items: center; gap: 5px;">
|
| 639 |
-
<span style="width: 20px; height: 20px; border-radius: 50%; background-color: #FF6B6B; display: inline-block;"></span>
|
| 640 |
-
<strong>Date</strong>
|
| 641 |
-
</span>
|
| 642 |
-
</div>
|
| 643 |
-
</div>
|
| 644 |
-
""")
|
| 645 |
-
|
| 646 |
# Footer
|
| 647 |
gr.HTML("""
|
| 648 |
<hr style="margin: 40px 0 20px 0;">
|
| 649 |
<div style="text-align: center; color: #666; font-size: 14px; padding: 20px;">
|
| 650 |
-
<p><strong>
|
| 651 |
-
<p>Built with the aid of Claude</p>
|
| 652 |
</div>
|
| 653 |
""")
|
| 654 |
|
|
|
|
| 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)
|
| 9 |
ENTITY_COLORS = {
|
| 10 |
'PERSON': '#00B894', # Green
|
| 11 |
'LOCATION': '#A0E7E5', # Light Cyan
|
|
|
|
| 16 |
|
| 17 |
# Relationship types for dropdown
|
| 18 |
RELATIONSHIP_TYPES = [
|
| 19 |
+
'related_to',
|
| 20 |
+
'knows',
|
| 21 |
'works_with',
|
| 22 |
'located_in',
|
| 23 |
'participated_in',
|
|
|
|
| 26 |
'employed_by',
|
| 27 |
'founded',
|
| 28 |
'attended',
|
|
|
|
|
|
|
| 29 |
'collaborates_with',
|
| 30 |
'married_to',
|
| 31 |
'sibling_of',
|
| 32 |
'parent_of',
|
| 33 |
'lives_at',
|
| 34 |
'wrote',
|
| 35 |
+
'visited',
|
| 36 |
+
'born_in',
|
| 37 |
+
'died_in',
|
| 38 |
'other'
|
| 39 |
]
|
| 40 |
|
| 41 |
+
|
| 42 |
class NetworkGraphBuilder:
|
| 43 |
def __init__(self):
|
| 44 |
self.entities = []
|
|
|
|
| 88 |
|
| 89 |
return G
|
| 90 |
|
| 91 |
+
def create_pyvis_graph(self, G):
|
| 92 |
+
"""Create interactive PyVis visualization"""
|
| 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%",
|
| 100 |
+
bgcolor="#1a1a2e",
|
| 101 |
+
font_color="white",
|
| 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,
|
| 118 |
+
"color": "rgba(0,0,0,0.5)",
|
| 119 |
+
"size": 10
|
| 120 |
+
}
|
| 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,
|
| 137 |
+
"barnesHut": {
|
| 138 |
+
"gravitationalConstant": -8000,
|
| 139 |
+
"centralGravity": 0.3,
|
| 140 |
+
"springLength": 150,
|
| 141 |
+
"springConstant": 0.04,
|
| 142 |
+
"damping": 0.09
|
| 143 |
+
},
|
| 144 |
+
"stabilization": {
|
| 145 |
+
"enabled": true,
|
| 146 |
+
"iterations": 100
|
| 147 |
+
}
|
| 148 |
+
},
|
| 149 |
+
"interaction": {
|
| 150 |
+
"hover": true,
|
| 151 |
+
"tooltipDelay": 100,
|
| 152 |
+
"dragNodes": true,
|
| 153 |
+
"dragView": true,
|
| 154 |
+
"zoomView": true
|
| 155 |
+
}
|
| 156 |
+
}
|
| 157 |
+
""")
|
| 158 |
|
| 159 |
+
# Add nodes with styling based on entity type and degree
|
| 160 |
+
for node in G.nodes():
|
| 161 |
+
data = G.nodes[node]
|
| 162 |
+
entity_type = data.get('entity_type', 'UNKNOWN')
|
| 163 |
+
color = ENTITY_COLORS.get(entity_type, '#CCCCCC')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
|
| 165 |
+
# Size based on connections (degree)
|
| 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,
|
| 179 |
+
label=node,
|
| 180 |
+
color=color,
|
| 181 |
+
size=size,
|
| 182 |
+
title=title,
|
| 183 |
+
font={'size': 12, 'color': 'white'}
|
| 184 |
)
|
| 185 |
+
|
| 186 |
+
# Add edges with relationship labels
|
| 187 |
+
for edge in G.edges(data=True):
|
| 188 |
+
rel_type = edge[2].get('relationship', '')
|
| 189 |
+
net.add_edge(
|
| 190 |
+
edge[0],
|
| 191 |
+
edge[1],
|
| 192 |
+
title=rel_type,
|
| 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')
|
| 206 |
+
|
| 207 |
+
iframe_html = f'''
|
| 208 |
+
<iframe
|
| 209 |
+
src="data:text/html;base64,{b64_html}"
|
| 210 |
+
width="100%"
|
| 211 |
+
height="620px"
|
| 212 |
+
style="border: 2px solid #333; border-radius: 10px; background-color: #1a1a2e;"
|
| 213 |
+
sandbox="allow-scripts allow-same-origin"
|
| 214 |
+
></iframe>
|
| 215 |
+
'''
|
| 216 |
+
|
| 217 |
+
return iframe_html
|
| 218 |
|
| 219 |
|
| 220 |
def collect_entities_from_records(
|
|
|
|
| 262 |
'DATE': sum(1 for e in builder.entities if e['type'] == 'DATE'),
|
| 263 |
}
|
| 264 |
|
| 265 |
+
# Create HTML summary that spans full width
|
| 266 |
+
summary_html = f'''
|
| 267 |
+
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 20px; border-radius: 12px; margin: 10px 0;">
|
| 268 |
+
<h3 style="color: white; margin: 0 0 15px 0; font-size: 18px;">📊 Identified Entities ({len(builder.entities)} total)</h3>
|
| 269 |
+
<div style="display: flex; flex-wrap: wrap; gap: 15px; margin-bottom: 15px;">
|
| 270 |
+
<div style="background: rgba(255,255,255,0.2); padding: 10px 20px; border-radius: 8px; text-align: center;">
|
| 271 |
+
<div style="font-size: 24px; font-weight: bold; color: white;">{counts['PERSON']}</div>
|
| 272 |
+
<div style="color: rgba(255,255,255,0.9); font-size: 12px;">👤 People</div>
|
| 273 |
+
</div>
|
| 274 |
+
<div style="background: rgba(255,255,255,0.2); padding: 10px 20px; border-radius: 8px; text-align: center;">
|
| 275 |
+
<div style="font-size: 24px; font-weight: bold; color: white;">{counts['LOCATION']}</div>
|
| 276 |
+
<div style="color: rgba(255,255,255,0.9); font-size: 12px;">📍 Locations</div>
|
| 277 |
+
</div>
|
| 278 |
+
<div style="background: rgba(255,255,255,0.2); padding: 10px 20px; border-radius: 8px; text-align: center;">
|
| 279 |
+
<div style="font-size: 24px; font-weight: bold; color: white;">{counts['EVENT']}</div>
|
| 280 |
+
<div style="color: rgba(255,255,255,0.9); font-size: 12px;">📅 Events</div>
|
| 281 |
+
</div>
|
| 282 |
+
<div style="background: rgba(255,255,255,0.2); padding: 10px 20px; border-radius: 8px; text-align: center;">
|
| 283 |
+
<div style="font-size: 24px; font-weight: bold; color: white;">{counts['ORGANIZATION']}</div>
|
| 284 |
+
<div style="color: rgba(255,255,255,0.9); font-size: 12px;">🏢 Organizations</div>
|
| 285 |
+
</div>
|
| 286 |
+
<div style="background: rgba(255,255,255,0.2); padding: 10px 20px; border-radius: 8px; text-align: center;">
|
| 287 |
+
<div style="font-size: 24px; font-weight: bold; color: white;">{counts['DATE']}</div>
|
| 288 |
+
<div style="color: rgba(255,255,255,0.9); font-size: 12px;">🗓️ Dates</div>
|
| 289 |
+
</div>
|
| 290 |
+
</div>
|
| 291 |
+
<div style="background: rgba(255,255,255,0.15); padding: 12px; border-radius: 8px;">
|
| 292 |
+
<div style="color: rgba(255,255,255,0.9); font-size: 13px; word-wrap: break-word;">
|
| 293 |
+
<strong>Entities:</strong> {', '.join(entity_names) if entity_names else 'None found'}
|
| 294 |
+
</div>
|
| 295 |
+
</div>
|
| 296 |
+
</div>
|
| 297 |
+
'''
|
| 298 |
|
| 299 |
# Return summary and update all 10 dropdowns (5 source + 5 target)
|
|
|
|
| 300 |
return (
|
| 301 |
+
summary_html,
|
| 302 |
gr.update(choices=entity_names, value=None), # source 1
|
| 303 |
gr.update(choices=entity_names, value=None), # target 1
|
| 304 |
gr.update(choices=entity_names, value=None), # source 2
|
|
|
|
| 368 |
G = builder.build_graph()
|
| 369 |
|
| 370 |
if len(G.nodes) == 0:
|
| 371 |
+
empty_html = '''
|
| 372 |
+
<div style="background-color: #1a1a2e; height: 400px; display: flex; align-items: center; justify-content: center; border-radius: 10px; border: 2px solid #333;">
|
| 373 |
+
<div style="text-align: center; color: white;">
|
| 374 |
+
<div style="font-size: 48px; margin-bottom: 20px;">📊</div>
|
| 375 |
+
<h3>No entities to display</h3>
|
| 376 |
+
<p style="color: #888;">Enter entities above and click "Identify Entities" first</p>
|
| 377 |
+
</div>
|
| 378 |
+
</div>
|
| 379 |
+
'''
|
| 380 |
+
return empty_html, "❌ **No entities to display.** Please enter entities in Step 1 first."
|
|
|
|
|
|
|
|
|
|
| 381 |
|
| 382 |
# Create visualization
|
| 383 |
+
graph_html = builder.create_pyvis_graph(G)
|
| 384 |
|
| 385 |
# Create statistics
|
| 386 |
+
stats_html = f'''
|
| 387 |
+
<div style="background: #f8f9fa; padding: 15px; border-radius: 10px; border: 1px solid #ddd;">
|
| 388 |
+
<h4 style="margin: 0 0 10px 0;">📈 Network Statistics</h4>
|
| 389 |
+
<table style="width: 100%; border-collapse: collapse;">
|
| 390 |
+
<tr>
|
| 391 |
+
<td style="padding: 8px; border-bottom: 1px solid #eee;"><strong>Nodes</strong></td>
|
| 392 |
+
<td style="padding: 8px; border-bottom: 1px solid #eee; text-align: right;">{G.number_of_nodes()}</td>
|
| 393 |
+
</tr>
|
| 394 |
+
<tr>
|
| 395 |
+
<td style="padding: 8px; border-bottom: 1px solid #eee;"><strong>Edges</strong></td>
|
| 396 |
+
<td style="padding: 8px; border-bottom: 1px solid #eee; text-align: right;">{G.number_of_edges()}</td>
|
| 397 |
+
</tr>
|
| 398 |
+
'''
|
| 399 |
+
|
| 400 |
+
if len(G.edges) > 0:
|
| 401 |
density = nx.density(G)
|
| 402 |
avg_degree = sum(dict(G.degree()).values()) / G.number_of_nodes()
|
| 403 |
+
stats_html += f'''
|
| 404 |
+
<tr>
|
| 405 |
+
<td style="padding: 8px; border-bottom: 1px solid #eee;"><strong>Density</strong></td>
|
| 406 |
+
<td style="padding: 8px; border-bottom: 1px solid #eee; text-align: right;">{density:.3f}</td>
|
| 407 |
+
</tr>
|
| 408 |
+
<tr>
|
| 409 |
+
<td style="padding: 8px;"><strong>Avg. Connections</strong></td>
|
| 410 |
+
<td style="padding: 8px; text-align: right;">{avg_degree:.2f}</td>
|
| 411 |
+
</tr>
|
| 412 |
+
</table>
|
| 413 |
+
'''
|
| 414 |
|
| 415 |
+
# Most connected
|
| 416 |
degrees = dict(G.degree())
|
| 417 |
top_nodes = sorted(degrees.items(), key=lambda x: x[1], reverse=True)[:3]
|
| 418 |
+
stats_html += '''
|
| 419 |
+
<div style="margin-top: 15px;">
|
| 420 |
+
<strong>Most Connected:</strong>
|
| 421 |
+
<ul style="margin: 5px 0; padding-left: 20px;">
|
| 422 |
+
'''
|
| 423 |
for node, degree in top_nodes:
|
| 424 |
+
stats_html += f'<li>{node}: {degree}</li>'
|
| 425 |
+
stats_html += '</ul></div>'
|
| 426 |
+
else:
|
| 427 |
+
stats_html += '''
|
| 428 |
+
</table>
|
| 429 |
+
<div style="margin-top: 10px; padding: 10px; background: #fff3cd; border-radius: 5px; color: #856404;">
|
| 430 |
+
⚠️ No relationships defined - nodes are isolated
|
| 431 |
+
</div>
|
| 432 |
+
'''
|
| 433 |
+
|
| 434 |
+
stats_html += '</div>'
|
| 435 |
|
| 436 |
+
return graph_html, stats_html
|
| 437 |
|
| 438 |
except Exception as e:
|
| 439 |
import traceback
|
| 440 |
error_trace = traceback.format_exc()
|
| 441 |
|
| 442 |
+
error_html = f'''
|
| 443 |
+
<div style="background-color: #f8d7da; height: 200px; display: flex; align-items: center; justify-content: center; border-radius: 10px; border: 2px solid #f5c6cb;">
|
| 444 |
+
<div style="text-align: center; color: #721c24; padding: 20px;">
|
| 445 |
+
<div style="font-size: 48px; margin-bottom: 10px;">⚠️</div>
|
| 446 |
+
<h3>Error generating graph</h3>
|
| 447 |
+
<p>{str(e)}</p>
|
| 448 |
+
</div>
|
| 449 |
+
</div>
|
| 450 |
+
'''
|
| 451 |
+
return error_html, f"❌ Error: {str(e)}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 452 |
|
| 453 |
|
| 454 |
def load_austen_example():
|
|
|
|
| 457 |
# Record 1
|
| 458 |
"Elizabeth Bennet", "Longbourn", "Meryton Ball", "Bennet Family", "1811",
|
| 459 |
# Record 2
|
| 460 |
+
"Mr. Darcy", "Pemberley", "Netherfield Ball", "Darcy Estate", "",
|
| 461 |
# Record 3
|
| 462 |
+
"Jane Bennet", "Netherfield", "", "", "",
|
| 463 |
# Record 4
|
| 464 |
+
"Mr. Bingley", "London", "", "", "",
|
| 465 |
# Record 5
|
| 466 |
"Mr. Wickham", "Meryton", "", "Militia", "",
|
| 467 |
# Record 6
|
|
|
|
| 475 |
# Record 1
|
| 476 |
"Winston Churchill", "London", "Battle of Britain", "War Cabinet", "1940",
|
| 477 |
# Record 2
|
| 478 |
+
"Franklin D. Roosevelt", "Washington D.C.", "D-Day", "Allied Forces", "1944",
|
| 479 |
# Record 3
|
| 480 |
"Field Marshal Montgomery", "North Africa", "Battle of El Alamein", "Eighth Army", "1942",
|
| 481 |
# Record 4
|
| 482 |
+
"Clement Attlee", "Potsdam", "Potsdam Conference", "Labour Party", "1945",
|
| 483 |
# Record 5
|
| 484 |
"", "", "", "", "",
|
| 485 |
# Record 6
|
|
|
|
| 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
|
| 510 |
+
gr.Markdown("### 💡 Quick Start - Load an Example:")
|
| 511 |
+
with gr.Row():
|
| 512 |
+
austen_btn = gr.Button("📚 Jane Austen (Pride & Prejudice)", variant="secondary", size="sm")
|
| 513 |
+
wwii_btn = gr.Button("⚔️ WWII History", variant="secondary", size="sm")
|
| 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")
|
| 591 |
+
|
| 592 |
+
# Full-width entity summary
|
| 593 |
+
entity_summary = gr.HTML()
|
| 594 |
|
| 595 |
gr.HTML("<hr style='margin: 20px 0;'>")
|
| 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 |
|
| 651 |
+
# ==================== STEP 3: GENERATE & VIEW ====================
|
| 652 |
+
gr.Markdown("## 🎨 Step 3: Generate Network Graph")
|
| 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")
|
| 660 |
with gr.Column(scale=1):
|
| 661 |
+
network_stats = gr.HTML()
|
| 662 |
+
|
| 663 |
+
# Color legend
|
| 664 |
+
gr.HTML("""
|
| 665 |
+
<div style="background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); padding: 20px; border-radius: 10px; margin-top: 20px;">
|
| 666 |
+
<h4 style="color: white; margin: 0 0 15px 0;">🎨 Entity Color Legend</h4>
|
| 667 |
+
<div style="display: flex; flex-wrap: wrap; gap: 20px;">
|
| 668 |
+
<span style="display: flex; align-items: center; gap: 8px; color: white;">
|
| 669 |
+
<span style="width: 20px; height: 20px; border-radius: 50%; background-color: #00B894; display: inline-block; border: 2px solid white;"></span>
|
| 670 |
+
Person
|
| 671 |
+
</span>
|
| 672 |
+
<span style="display: flex; align-items: center; gap: 8px; color: white;">
|
| 673 |
+
<span style="width: 20px; height: 20px; border-radius: 50%; background-color: #A0E7E5; display: inline-block; border: 2px solid white;"></span>
|
| 674 |
+
Location
|
| 675 |
+
</span>
|
| 676 |
+
<span style="display: flex; align-items: center; gap: 8px; color: white;">
|
| 677 |
+
<span style="width: 20px; height: 20px; border-radius: 50%; background-color: #4ECDC4; display: inline-block; border: 2px solid white;"></span>
|
| 678 |
+
Event
|
| 679 |
+
</span>
|
| 680 |
+
<span style="display: flex; align-items: center; gap: 8px; color: white;">
|
| 681 |
+
<span style="width: 20px; height: 20px; border-radius: 50%; background-color: #55A3FF; display: inline-block; border: 2px solid white;"></span>
|
| 682 |
+
Organization
|
| 683 |
+
</span>
|
| 684 |
+
<span style="display: flex; align-items: center; gap: 8px; color: white;">
|
| 685 |
+
<span style="width: 20px; height: 20px; border-radius: 50%; background-color: #FF6B6B; display: inline-block; border: 2px solid white;"></span>
|
| 686 |
+
Date
|
| 687 |
+
</span>
|
| 688 |
+
</div>
|
| 689 |
+
<p style="color: #888; margin: 15px 0 0 0; font-size: 13px;">
|
| 690 |
+
🖱️ <strong>Interaction:</strong> Drag nodes to rearrange • Scroll to zoom • Hover for details
|
| 691 |
+
</p>
|
| 692 |
+
</div>
|
| 693 |
+
""")
|
| 694 |
|
| 695 |
+
# ==================== WIRE UP EVENTS ====================
|
| 696 |
+
|
| 697 |
+
# Example buttons
|
| 698 |
austen_btn.click(
|
| 699 |
fn=load_austen_example,
|
| 700 |
inputs=[],
|
|
|
|
| 707 |
outputs=entity_inputs
|
| 708 |
)
|
| 709 |
|
| 710 |
+
# Collect entities
|
| 711 |
collect_btn.click(
|
| 712 |
fn=collect_entities_from_records,
|
| 713 |
inputs=entity_inputs,
|
| 714 |
outputs=[
|
| 715 |
entity_summary,
|
| 716 |
+
src1, tgt1,
|
| 717 |
+
src2, tgt2,
|
| 718 |
+
src3, tgt3,
|
| 719 |
+
src4, tgt4,
|
| 720 |
+
src5, tgt5
|
| 721 |
]
|
| 722 |
)
|
| 723 |
|
| 724 |
+
# Generate graph
|
| 725 |
all_inputs = entity_inputs + relationship_inputs
|
| 726 |
generate_btn.click(
|
| 727 |
fn=generate_network_graph,
|
|
|
|
| 729 |
outputs=[network_plot, network_stats]
|
| 730 |
)
|
| 731 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 732 |
# Footer
|
| 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 |
|