cjc0013 commited on
Commit
4a1f0db
·
verified ·
1 Parent(s): 907b56c

Redesign Space UX for guided story-first public flow

Browse files
.gitattributes CHANGED
@@ -36,3 +36,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
36
  dataset_bundle/evidence_audit/claim_supporting_provenance.jsonl filter=lfs diff=lfs merge=lfs -text
37
  dataset_bundle/evidence_audit/scored_event_provenance.jsonl filter=lfs diff=lfs merge=lfs -text
38
  dataset_bundle/evidence_audit/source_artifact_index.csv filter=lfs diff=lfs merge=lfs -text
 
 
36
  dataset_bundle/evidence_audit/claim_supporting_provenance.jsonl filter=lfs diff=lfs merge=lfs -text
37
  dataset_bundle/evidence_audit/scored_event_provenance.jsonl filter=lfs diff=lfs merge=lfs -text
38
  dataset_bundle/evidence_audit/source_artifact_index.csv filter=lfs diff=lfs merge=lfs -text
39
+ __pycache__/public_space_app.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
__pycache__/public_space_app.cpython-311.pyc CHANGED
Binary files a/__pycache__/public_space_app.cpython-311.pyc and b/__pycache__/public_space_app.cpython-311.pyc differ
 
dataset_bundle/evidence_audit/consistency_report.json CHANGED
@@ -1,5 +1,5 @@
1
  {
2
- "generated_at": "2026-04-19T11:09:07-04:00",
3
  "event_provenance": {
4
  "event_count": 3918,
5
  "events_with_artifacts": 3878,
 
1
  {
2
+ "generated_at": "2026-04-19T09:21:59-04:00",
3
  "event_provenance": {
4
  "event_count": 3918,
5
  "events_with_artifacts": 3878,
dataset_bundle/network_graph/graph_config.json CHANGED
@@ -20,10 +20,9 @@
20
  "default_filters": {
21
  "relationship_family": "sector",
22
  "review_status": "stronger",
23
- "max_edges": 30,
24
  "hide_unresolved_only": true,
25
- "overview_member_limit": 5,
26
- "default_member_search": "Josh Gottheimer"
27
  },
28
  "example_member_searches": [
29
  "Josh Gottheimer",
 
20
  "default_filters": {
21
  "relationship_family": "sector",
22
  "review_status": "stronger",
23
+ "max_edges": 60,
24
  "hide_unresolved_only": true,
25
+ "overview_member_limit": 8
 
26
  },
27
  "example_member_searches": [
28
  "Josh Gottheimer",
dataset_bundle/public_release_manifest.json CHANGED
@@ -1,7 +1,7 @@
1
  {
2
  "public_version": "congress-public-records-slice-2026-04-v1",
3
  "title": "Congress Public Records Slice",
4
- "release_date": "2026-04-19T11:10:25-04:00",
5
  "slice_description": "A neutral, review-oriented slice of House public-record linkages across financial disclosures, sector overlap, and community project funding recipient relationships.",
6
  "source_run_name": "house_all_baseline_20260418_v21_recipienthardening",
7
  "dataset_repo_id": "cjc0013/cmp-data",
 
1
  {
2
  "public_version": "congress-public-records-slice-2026-04-v1",
3
  "title": "Congress Public Records Slice",
4
+ "release_date": "2026-04-19T09:22:53-04:00",
5
  "slice_description": "A neutral, review-oriented slice of House public-record linkages across financial disclosures, sector overlap, and community project funding recipient relationships.",
6
  "source_run_name": "house_all_baseline_20260418_v21_recipienthardening",
7
  "dataset_repo_id": "cjc0013/cmp-data",
public_copy.json CHANGED
@@ -1,5 +1,5 @@
1
  {
2
- "public_version": "congress-public-records-slice-2026-04-v1",
3
  "title": "Congress Public Records Slice",
4
  "subtitle": "Neutral Records explorer for a public-record slice of congressional money-and-power linkages.",
5
  "dataset_repo_id": "cjc0013/cmp-data",
 
1
  {
2
+ "public_version": "congress-public-records-slice-2026-04-v1-private-spacefix",
3
  "title": "Congress Public Records Slice",
4
  "subtitle": "Neutral Records explorer for a public-record slice of congressional money-and-power linkages.",
5
  "dataset_repo_id": "cjc0013/cmp-data",
public_space_app.py CHANGED
@@ -239,6 +239,339 @@ def _fictional_example_markdown() -> str:
239
  )
240
 
241
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
  def _plain_status_label(value: str) -> str:
243
  normalized = str(value or "").strip()
244
  mapping = {
@@ -552,12 +885,82 @@ def _overview_summary_markdown(
552
  return "\n".join(lines)
553
 
554
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
555
  def _relationship_options(ranked: pd.DataFrame) -> list[tuple[str, str]]:
556
  if ranked.empty:
557
  return []
558
  options: list[tuple[str, str]] = []
559
  for row in ranked.to_dict("records"):
560
- label = f"{row['member']} -> {row['counterparty / sector']} (score {row['overall score']})"
 
 
 
561
  options.append((label, str(row["relationship_id"])))
562
  return options
563
 
@@ -588,6 +991,7 @@ def _relationship_detail_markdown(edges: pd.DataFrame, relationship_id: str) ->
588
  f"- Supporting relationship rows: `{int(row.get('link_count', 0) or 0)}`",
589
  f"- Stronger-support rows: `{int(row.get('linked_count', 0) or 0) if family == 'recipient' else int(row.get('strong_event_count', 0) or 0)}`",
590
  f"- Caution / weaker rows: `{int(row.get('review_count', 0) or 0) if family == 'recipient' else int(row.get('weak_event_count', 0) or 0)}`",
 
591
  f"- Unresolved source refs still counted: `{int(row.get('unresolved_source_ref_count', 0) or 0)}`",
592
  f"- Evidence signals: `{', '.join(chips) if chips else 'published source support'}`",
593
  f"- Time-window overlap: `{_window_overlap_text(row)}`",
@@ -598,6 +1002,14 @@ def _relationship_detail_markdown(edges: pd.DataFrame, relationship_id: str) ->
598
  if urls:
599
  lines.extend(["", "#### Example published source URLs", ""])
600
  lines.extend(f"- {item}" for item in urls)
 
 
 
 
 
 
 
 
601
  return "\n".join(lines)
602
 
603
 
@@ -742,12 +1154,12 @@ def _consistency_summary_markdown(consistency: Dict[str, Any]) -> str:
742
  "### Audit Summary",
743
  "",
744
  f"- Event rows in the audit index: `{int(event_payload.get('event_count', 0) or 0)}`",
745
- f"- Event rows with attached artifacts: `{int(event_payload.get('events_with_artifacts', 0) or 0)}`",
746
  f"- Stored-versus-lookup provenance mismatches: `{int(event_payload.get('stored_lookup_mismatch_count', 0) or 0)}`",
747
  f"- Claim-supporting rows in the audit index: `{int(claim_payload.get('row_count', 0) or 0)}`",
748
- f"- Claim-supporting rows with attached artifacts: `{int(claim_payload.get('rows_with_artifacts', 0) or 0)}`",
749
  "",
750
- "Use the tables below to inspect the public source URLs and SHA-backed artifacts that support the released rows.",
751
  ]
752
  )
753
 
@@ -877,7 +1289,7 @@ def _render_graph(nodes: pd.DataFrame, edges: pd.DataFrame) -> str:
877
 
878
  def _event_detail(events: pd.DataFrame, provenance: pd.DataFrame, event_id: str) -> Tuple[str, pd.DataFrame]:
879
  if not event_id or event_id not in set(events["event_id"]):
880
- return "Select an event id to inspect source URLs and SHA-backed artifacts.", pd.DataFrame()
881
  event_row = events[events["event_id"] == event_id].head(1).to_dict("records")[0]
882
  prov_rows = provenance[provenance["row_key"] == event_id]
883
  member_name = str(event_row.get("member_name") or event_row.get("member_slug") or "Unknown member")
@@ -895,7 +1307,7 @@ def _event_detail(events: pd.DataFrame, provenance: pd.DataFrame, event_id: str)
895
  "This panel summarizes one released event row from the public slice.",
896
  "",
897
  f"- Event id: `{event_id}`",
898
- f"- Event type: `{event_type}`",
899
  ]
900
  if score_label:
901
  lines.append(f"- Score label: `{score_label}`")
@@ -909,7 +1321,7 @@ def _event_detail(events: pd.DataFrame, provenance: pd.DataFrame, event_id: str)
909
  lines.extend(
910
  [
911
  f"- Attached source URLs in this row: `{int(event_row.get('source_ref_count', 0) or 0)}`",
912
- f"- SHA-backed artifacts attached: `{int(event_row.get('sha_backed_source_artifact_count', 0) or 0)}`",
913
  f"- Unresolved source references still counted: `{int(event_row.get('unresolved_source_ref_count', 0) or 0)}`",
914
  f"- Matching provenance rows shown below: `{len(prov_rows)}`",
915
  ]
@@ -962,140 +1374,259 @@ def build_app(copy_path: str | Path):
962
  overview_member_limit = int(graph_defaults.get("overview_member_limit", 8))
963
  default_member_search = str(graph_defaults.get("default_member_search", "") or "")
964
 
965
- with gr.Blocks(title=copy_payload.get("title", "Congress Public Records Slice")) as app:
966
- gr.Markdown(copy_payload.get("welcome_markdown", copy_payload.get("landing_markdown", "")))
967
- with gr.Tab("Start Here"):
968
- gr.Markdown(_about_release_markdown(manifest, data["recipient_link_quality"], data["source_quality"]))
969
- gr.Markdown(_fictional_example_markdown())
970
- gr.Markdown(_data_used_markdown(manifest))
971
- gr.Markdown(_how_to_use_markdown())
972
- with gr.Tab("Overview"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
973
  gr.Markdown(
974
- "### Start here\n\n"
975
- "Pick one House member, choose whether you want sectors or funding recipients, and read the ranked list first."
 
 
 
 
976
  )
 
977
  with gr.Row():
978
- overview_member = gr.Textbox(label="House member", value=default_member_search)
979
- overview_family = gr.Dropdown(label="Show", choices=[("Sectors", "sector"), ("Funding recipients", "recipient")], value="sector")
980
- overview_only_strong = gr.Checkbox(label="Only strong links", value=True)
981
- overview_top_n = gr.Slider(label="Show top relationships", minimum=5, maximum=40, step=5, value=10)
982
- if example_member_choices:
983
- gr.Examples(examples=example_member_choices, inputs=[overview_member], label="Try one of these example members")
984
- overview_summary_md = gr.Markdown()
985
- overview_df = gr.Dataframe(interactive=False)
986
- relationship_choice = gr.Dropdown(label="Relationship to explain", choices=[], value=None)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
987
  overview_detail_md = gr.Markdown()
988
  overview_timeline_html = gr.HTML()
989
 
990
- def _overview_edges(member_query: str, family: str, only_strong: bool, top_n: int) -> pd.DataFrame:
991
- return _filter_graph(
992
- edges,
993
- family,
994
- member_query,
995
- "",
996
- "all",
997
- "stronger" if only_strong else "all",
998
- True,
999
- top_n,
1000
- overview_member_limit,
1001
- )
1002
-
1003
- def _update_overview(member_query: str, family: str, only_strong: bool, top_n: int):
1004
- filtered_edges = _overview_edges(member_query, family, only_strong, top_n)
1005
- ranked = _rank_relationships(filtered_edges)
1006
- options = _relationship_options(ranked)
1007
- selected = options[0][1] if options else None
1008
- display = ranked.drop(columns=["relationship_id", "source_examples"], errors="ignore")
1009
- return (
1010
- _overview_summary_markdown(
1011
- ranked,
1012
- member_query=member_query,
1013
- family=family,
1014
- only_strong_links=only_strong,
1015
- top_n=top_n,
1016
- ),
1017
- display,
1018
- gr.update(choices=options, value=selected),
1019
- _relationship_detail_markdown(filtered_edges, selected or ""),
1020
- _relationship_timeline_html(filtered_edges, selected or ""),
1021
- )
1022
-
1023
- def _update_overview_detail(member_query: str, family: str, only_strong: bool, top_n: int, relationship_id: str):
1024
- filtered_edges = _overview_edges(member_query, family, only_strong, top_n)
1025
- return _relationship_detail_markdown(filtered_edges, relationship_id), _relationship_timeline_html(filtered_edges, relationship_id)
1026
-
1027
- for control in (overview_member, overview_family, overview_only_strong, overview_top_n):
1028
- control.change(
1029
- _update_overview,
1030
- [overview_member, overview_family, overview_only_strong, overview_top_n],
1031
- [overview_summary_md, overview_df, relationship_choice, overview_detail_md, overview_timeline_html],
1032
- )
1033
- relationship_choice.change(
1034
- _update_overview_detail,
1035
  [overview_member, overview_family, overview_only_strong, overview_top_n, relationship_choice],
1036
- [overview_detail_md, overview_timeline_html],
1037
  )
1038
- app.load(
1039
- _update_overview,
1040
- [overview_member, overview_family, overview_only_strong, overview_top_n],
1041
- [overview_summary_md, overview_df, relationship_choice, overview_detail_md, overview_timeline_html],
 
 
 
 
 
 
1042
  )
1043
- with gr.Tab("Explore Graph (optional)"):
1044
  gr.Markdown(_graph_intro_markdown(data["graph_config"]))
1045
  with gr.Row():
1046
- family = gr.Dropdown(label="Show", choices=graph_family_choices, value=str(graph_defaults.get("relationship_family", "sector")))
1047
- member_graph_query = gr.Textbox(label="House member to focus", value=default_member_search)
1048
- target_query = gr.Textbox(label="Recipient or sector search")
1049
- graph_score = gr.Dropdown(label="Score label", choices=graph_score_choices, value="all")
1050
- review_status = gr.Dropdown(label="Which links to show", choices=graph_status_choices, value=str(graph_defaults.get("review_status", "stronger")))
1051
- if example_member_choices:
1052
- gr.Examples(examples=example_member_choices, inputs=[member_graph_query], label="Try one of these example members")
1053
- with gr.Row():
1054
- hide_unresolved_only = gr.Checkbox(label="Hide unresolved links", value=bool(graph_defaults.get("hide_unresolved_only", True)))
1055
- max_edges = gr.Slider(label="Show top relationships", minimum=25, maximum=300, step=25, value=int(graph_defaults.get("max_edges", 60)))
1056
  graph_summary_md = gr.Markdown()
1057
  graph_html = gr.HTML()
1058
- gr.Markdown("#### Relationship list for this graph view")
1059
- graph_df = gr.Dataframe(interactive=False)
1060
- def _update_graph(family: str, member_graph_query: str, target_query: str, graph_score: str, review_status: str, hide_unresolved_only: bool, max_edges: int):
1061
- filtered_edges = _filter_graph(edges, family, member_graph_query, target_query, graph_score, review_status, hide_unresolved_only, max_edges, overview_member_limit)
1062
- filtered_nodes = nodes[nodes["node_id"].isin(set(filtered_edges["source_node_id"]).union(set(filtered_edges["target_node_id"])))]
1063
- summary = _graph_view_summary_markdown(
1064
- filtered_edges,
1065
- family=family,
1066
- member_query=member_graph_query,
1067
- target_query=target_query,
1068
- review_status=review_status,
1069
- max_edges=max_edges,
1070
  )
1071
- return summary, _render_graph(filtered_nodes, filtered_edges), _graph_table(filtered_edges)
1072
- for control in (family, member_graph_query, target_query, graph_score, review_status, hide_unresolved_only, max_edges):
1073
- control.change(_update_graph, [family, member_graph_query, target_query, graph_score, review_status, hide_unresolved_only, max_edges], [graph_summary_md, graph_html, graph_df])
1074
- app.load(_update_graph, [family, member_graph_query, target_query, graph_score, review_status, hide_unresolved_only, max_edges], [graph_summary_md, graph_html, graph_df])
1075
- with gr.Tab("Search Events"):
1076
- with gr.Row():
1077
- member_query = gr.Textbox(label="Member name or slug")
1078
- event_type = gr.Dropdown(label="Event type", choices=event_type_choices, value="all")
1079
- score_label = gr.Dropdown(label="Score label", choices=score_label_choices, value="all")
1080
- text_query = gr.Textbox(label="Issuer or sector search")
1081
- explore_df = gr.Dataframe(value=events.head(100), interactive=False)
1082
- def _update_events(member_query: str, event_type: str, score_label: str, text_query: str):
1083
- return _filter_events(events, member_query, event_type, score_label, text_query)
1084
- for control in (member_query, event_type, score_label, text_query):
1085
- control.change(_update_events, [member_query, event_type, score_label, text_query], explore_df)
1086
- with gr.Tab("Event Detail"):
1087
- event_id = gr.Dropdown(label="Event id", choices=event_id_choices, value=event_id_choices[0] if event_id_choices else None)
1088
- event_detail_md = gr.Markdown()
1089
- event_detail_df = gr.Dataframe(interactive=False)
1090
- event_id.change(_event_detail, [gr.State(events), gr.State(provenance), event_id], [event_detail_md, event_detail_df])
1091
- app.load(_event_detail, [gr.State(events), gr.State(provenance), event_id], [event_detail_md, event_detail_df])
1092
- with gr.Tab("Audit"):
1093
- gr.Markdown(_consistency_summary_markdown(data["consistency"]))
1094
- gr.Dataframe(value=data["artifact_index"].head(200), interactive=False)
1095
- with gr.Tab("Methodology & Limits"):
1096
- gr.Markdown(copy_payload.get("landing_markdown", ""))
1097
- gr.Markdown(_data_used_markdown(manifest))
1098
- gr.Markdown(copy_payload.get("downloads_markdown", ""))
1099
- with gr.Tab("Downloads"):
1100
- gr.Markdown(copy_payload.get("downloads_markdown", ""))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1101
  return app
 
239
  )
240
 
241
 
242
+ def _space_css() -> str:
243
+ return """
244
+ .gradio-container {
245
+ max-width: 1180px !important;
246
+ margin: 0 auto !important;
247
+ padding-bottom: 48px !important;
248
+ }
249
+ .hero-panel {
250
+ background: linear-gradient(135deg, #fff8ed 0%, #f5eee2 100%);
251
+ border: 1px solid #e6d8bf;
252
+ border-radius: 24px;
253
+ padding: 28px;
254
+ margin: 6px 0 20px 0;
255
+ box-shadow: 0 10px 30px rgba(62, 46, 14, 0.08);
256
+ }
257
+ .hero-eyebrow {
258
+ font-size: 0.82rem;
259
+ font-weight: 700;
260
+ letter-spacing: 0.08em;
261
+ text-transform: uppercase;
262
+ color: #8a6220;
263
+ margin-bottom: 8px;
264
+ }
265
+ .hero-title {
266
+ font-size: 2.2rem;
267
+ line-height: 1.1;
268
+ font-weight: 800;
269
+ color: #1f2b2d;
270
+ margin: 0 0 12px 0;
271
+ }
272
+ .hero-lede {
273
+ font-size: 1.05rem;
274
+ line-height: 1.6;
275
+ color: #334244;
276
+ margin: 0 0 10px 0;
277
+ max-width: 900px;
278
+ }
279
+ .hero-note {
280
+ font-size: 0.98rem;
281
+ line-height: 1.5;
282
+ color: #5f4e2a;
283
+ background: rgba(255,255,255,0.72);
284
+ border: 1px solid #eadcbf;
285
+ border-radius: 14px;
286
+ padding: 12px 14px;
287
+ margin-top: 14px;
288
+ }
289
+ .stat-grid, .story-grid {
290
+ display: grid;
291
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
292
+ gap: 14px;
293
+ margin-top: 18px;
294
+ }
295
+ .story-grid {
296
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
297
+ margin: 10px 0 22px 0;
298
+ }
299
+ .stat-card, .story-card, .source-card, .glossary-card, .result-card {
300
+ background: #fffdf8;
301
+ border: 1px solid #e7dcc8;
302
+ border-radius: 18px;
303
+ padding: 16px 18px;
304
+ box-shadow: 0 6px 18px rgba(40, 31, 9, 0.05);
305
+ }
306
+ .stat-label {
307
+ font-size: 0.82rem;
308
+ font-weight: 700;
309
+ text-transform: uppercase;
310
+ letter-spacing: 0.06em;
311
+ color: #8b6b2e;
312
+ margin-bottom: 8px;
313
+ }
314
+ .stat-value {
315
+ font-size: 1.9rem;
316
+ font-weight: 800;
317
+ color: #1f2b2d;
318
+ line-height: 1;
319
+ margin-bottom: 6px;
320
+ }
321
+ .stat-help {
322
+ font-size: 0.92rem;
323
+ color: #5b5b5b;
324
+ line-height: 1.45;
325
+ }
326
+ .story-title, .source-title, .glossary-title {
327
+ font-size: 1rem;
328
+ font-weight: 800;
329
+ color: #1f2b2d;
330
+ margin-bottom: 6px;
331
+ }
332
+ .story-body, .source-body, .glossary-body {
333
+ font-size: 0.95rem;
334
+ line-height: 1.55;
335
+ color: #435153;
336
+ }
337
+ .source-table {
338
+ width: 100%;
339
+ border-collapse: collapse;
340
+ margin-top: 8px;
341
+ font-size: 0.95rem;
342
+ }
343
+ .source-table th, .source-table td {
344
+ border-top: 1px solid #eadfcf;
345
+ padding: 12px 10px;
346
+ text-align: left;
347
+ vertical-align: top;
348
+ }
349
+ .source-table th {
350
+ color: #7a5b20;
351
+ font-size: 0.82rem;
352
+ text-transform: uppercase;
353
+ letter-spacing: 0.06em;
354
+ width: 32%;
355
+ }
356
+ .glossary-list {
357
+ display: grid;
358
+ gap: 10px;
359
+ margin-top: 8px;
360
+ }
361
+ .glossary-item strong {
362
+ display: block;
363
+ color: #1f2b2d;
364
+ margin-bottom: 2px;
365
+ }
366
+ .section-kicker {
367
+ color: #8a6220;
368
+ font-size: 0.84rem;
369
+ font-weight: 700;
370
+ letter-spacing: 0.06em;
371
+ text-transform: uppercase;
372
+ margin-bottom: 6px;
373
+ }
374
+ .result-list {
375
+ display: flex;
376
+ flex-direction: column;
377
+ gap: 12px;
378
+ margin-top: 10px;
379
+ }
380
+ .result-head {
381
+ display: flex;
382
+ justify-content: space-between;
383
+ align-items: flex-start;
384
+ gap: 12px;
385
+ }
386
+ .result-rank {
387
+ font-size: 0.78rem;
388
+ font-weight: 700;
389
+ color: #8b6b2e;
390
+ text-transform: uppercase;
391
+ letter-spacing: 0.06em;
392
+ margin-bottom: 4px;
393
+ }
394
+ .result-title {
395
+ font-size: 1.12rem;
396
+ font-weight: 800;
397
+ color: #1f2b2d;
398
+ line-height: 1.2;
399
+ margin-bottom: 4px;
400
+ }
401
+ .result-subtitle {
402
+ color: #546365;
403
+ font-size: 0.93rem;
404
+ }
405
+ .metric-stack {
406
+ display: flex;
407
+ gap: 8px;
408
+ flex-wrap: wrap;
409
+ justify-content: flex-end;
410
+ }
411
+ .score-pill, .strength-pill, .chip {
412
+ display: inline-block;
413
+ border-radius: 999px;
414
+ padding: 5px 10px;
415
+ font-size: 0.82rem;
416
+ font-weight: 700;
417
+ white-space: nowrap;
418
+ }
419
+ .score-pill {
420
+ background: #1f5f5b;
421
+ color: white;
422
+ }
423
+ .strength-pill {
424
+ background: #f3e4b9;
425
+ color: #5a470d;
426
+ }
427
+ .chip-row {
428
+ display: flex;
429
+ flex-wrap: wrap;
430
+ gap: 8px;
431
+ margin: 12px 0 10px 0;
432
+ }
433
+ .chip {
434
+ background: #f2ede2;
435
+ color: #4a4a4a;
436
+ }
437
+ .meta-grid {
438
+ display: grid;
439
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
440
+ gap: 10px;
441
+ margin-top: 10px;
442
+ font-size: 0.9rem;
443
+ color: #465558;
444
+ }
445
+ .meta-grid strong {
446
+ display: block;
447
+ color: #1f2b2d;
448
+ margin-bottom: 2px;
449
+ font-size: 0.82rem;
450
+ text-transform: uppercase;
451
+ letter-spacing: 0.04em;
452
+ }
453
+ .result-hint {
454
+ margin-top: 12px;
455
+ font-size: 0.88rem;
456
+ color: #7a5b20;
457
+ }
458
+ .panel-note {
459
+ background: #fffdf8;
460
+ border: 1px solid #e7dcc8;
461
+ border-radius: 18px;
462
+ padding: 14px 16px;
463
+ color: #4b4b4b;
464
+ margin-bottom: 12px;
465
+ }
466
+ """
467
+
468
+
469
+ def _hero_html(manifest: Dict[str, Any]) -> str:
470
+ counts = manifest.get("counts") or {}
471
+ cards = [
472
+ ("House members", int(counts.get("members", 0) or 0), "Members included in this released slice."),
473
+ ("Scored events", int(counts.get("scored_events", 0) or 0), "Row-level public-record overlaps that survived into the release."),
474
+ ("Relationship rows", int(counts.get("graph_links", 0) or 0), "Member-to-sector or member-to-recipient links in the public package."),
475
+ ("Source records", int(counts.get("source_artifacts", 0) or 0), "Published source artifacts in the verification layer."),
476
+ ]
477
+ card_html = "".join(
478
+ f"""
479
+ <div class="stat-card">
480
+ <div class="stat-label">{html.escape(label)}</div>
481
+ <div class="stat-value">{value:,}</div>
482
+ <div class="stat-help">{html.escape(help_text)}</div>
483
+ </div>
484
+ """
485
+ for label, value, help_text in cards
486
+ )
487
+ return f"""
488
+ <section class="hero-panel">
489
+ <div class="hero-eyebrow">Public-record overlap explorer</div>
490
+ <div class="hero-title">{html.escape(str(manifest.get("title") or "Congress Public Records Slice"))}</div>
491
+ <div class="hero-lede">Quickly check whether a House member's disclosed financial or funding relationships line up with public legislative activity in the same area.</div>
492
+ <div class="hero-lede">Built for journalists, researchers, and curious citizens who want a faster path from a vague hunch to inspectable public records.</div>
493
+ <div class="hero-note"><strong>What this does not claim:</strong> this tool does not prove corruption, illegality, intent, or causality. It shows public-record overlap and evidence strength so people can inspect the records themselves.</div>
494
+ <div class="stat-grid">{card_html}</div>
495
+ </section>
496
+ """
497
+
498
+
499
+ def _start_here_cards_html() -> str:
500
+ cards = [
501
+ (
502
+ "What this helps answer",
503
+ "Do a member's disclosed financial or funding relationships line up with public legislative activity in the same area?"
504
+ ),
505
+ (
506
+ "Why someone might care",
507
+ "It helps move from a vague suspicion to a concrete set of records worth checking, without pulling multiple public sources by hand."
508
+ ),
509
+ (
510
+ "What it does not mean",
511
+ "A visible relationship here is not a verdict. It is a signal that enough public records line up to justify closer reporting or review."
512
+ ),
513
+ ]
514
+ return "<div class=\"story-grid\">" + "".join(
515
+ f"""
516
+ <div class="story-card">
517
+ <div class="story-title">{html.escape(title)}</div>
518
+ <div class="story-body">{html.escape(body)}</div>
519
+ </div>
520
+ """
521
+ for title, body in cards
522
+ ) + "</div>"
523
+
524
+
525
+ def _source_table_html(manifest: Dict[str, Any]) -> str:
526
+ summary = manifest.get("methodology_summary") or {}
527
+ present_sources = set(_split_source_group_lines(summary.get("source_groups")))
528
+ source_pairs = [
529
+ ("House Clerk financial disclosures and PTRs", "Show trades or financial holdings disclosed by House members."),
530
+ ("House Clerk member directory and committee list", "Identify members and show committee context."),
531
+ ("GovInfo BILLSTATUS bulk data", "Show bill activity tied to the same policy area."),
532
+ ("House Clerk roll-call vote XML", "Show vote activity tied to the same policy area."),
533
+ ("FEC public bulk downloads", "Add campaign-finance context where it is used in this release."),
534
+ ("LDA public search pages", "Add lobbying visibility around the same issue areas."),
535
+ ("House member community project funding disclosure pages", "Show member-published funding-request disclosures."),
536
+ ("USAspending award pages used for some recipient matching", "Show public award records used to support some funding-recipient links."),
537
+ ]
538
+ rows = "".join(
539
+ f"<tr><th>{html.escape(source)}</th><td>{html.escape(purpose)}</td></tr>"
540
+ for source, purpose in source_pairs
541
+ if source in present_sources
542
+ )
543
+ return f"""
544
+ <div class="source-card">
545
+ <div class="section-kicker">What data is in here</div>
546
+ <div class="source-title">Public source families used in this release</div>
547
+ <table class="source-table">
548
+ <thead><tr><th>Source</th><th>What it adds</th></tr></thead>
549
+ <tbody>{rows}</tbody>
550
+ </table>
551
+ </div>
552
+ """
553
+
554
+
555
+ def _glossary_html() -> str:
556
+ items = [
557
+ ("Stronger support", "The released slice has clearer public support for this relationship."),
558
+ ("Needs review", "There is some support, but it should still be read with caution."),
559
+ ("Integrity-checked record", "The release includes a cryptographic fingerprint to help show the published record has not been altered."),
560
+ ("Evidence window", "A coarse view of when the published records line up; it is not exact chronology."),
561
+ ]
562
+ rows = "".join(
563
+ f"<div class=\"glossary-item\"><strong>{html.escape(term)}</strong><div>{html.escape(body)}</div></div>"
564
+ for term, body in items
565
+ )
566
+ return f"""
567
+ <div class="glossary-card">
568
+ <div class="section-kicker">Sticky terms</div>
569
+ <div class="glossary-title">Plain-English glossary</div>
570
+ <div class="glossary-list">{rows}</div>
571
+ </div>
572
+ """
573
+
574
+
575
  def _plain_status_label(value: str) -> str:
576
  normalized = str(value or "").strip()
577
  mapping = {
 
885
  return "\n".join(lines)
886
 
887
 
888
+ def _overview_cards_html(
889
+ ranked: pd.DataFrame,
890
+ *,
891
+ member_query: str,
892
+ family: str,
893
+ only_strong_links: bool,
894
+ top_n: int,
895
+ ) -> str:
896
+ if ranked.empty:
897
+ return (
898
+ "<div class=\"panel-note\">"
899
+ "<strong>No relationships match the current filters.</strong><br>"
900
+ "Try a different House member, switch from sectors to funding recipients, or turn off the stronger-links-only filter."
901
+ "</div>"
902
+ )
903
+ focus_names = [str(value) for value in ranked["member"].dropna().unique().tolist() if str(value).strip()]
904
+ focus_label = ", ".join(focus_names[:3]) or "this view"
905
+ intro = (
906
+ "<div class=\"panel-note\">"
907
+ f"<strong>Showing the top {min(int(top_n), len(ranked))} {_plain_family_label(family).lower()}</strong> "
908
+ f"for <strong>{html.escape(focus_label)}</strong>. "
909
+ f"Filtered to stronger links only: <strong>{'yes' if bool(only_strong_links) else 'no'}</strong>. "
910
+ "Pick one relationship below to open the plain-English explanation and evidence window."
911
+ "</div>"
912
+ )
913
+ cards: list[str] = []
914
+ for row in ranked.head(int(top_n)).to_dict("records"):
915
+ evidence_chips = [item.strip() for item in str(row.get("evidence", "") or "").split("|") if item.strip()]
916
+ chip_html = "".join(f"<span class=\"chip\">{html.escape(chip)}</span>" for chip in evidence_chips[:6])
917
+ supporting_rows = int(row.get("supporting rows", 0) or 0)
918
+ stronger_support = int(row.get("stronger support", 0) or 0)
919
+ needs_caution = int(row.get("needs caution", 0) or 0)
920
+ unresolved_refs = int(row.get("unresolved refs", 0) or 0)
921
+ cards.append(
922
+ f"""
923
+ <div class="result-card">
924
+ <div class="result-head">
925
+ <div>
926
+ <div class="result-rank">Rank #{int(row.get("rank", 0) or 0)}</div>
927
+ <div class="result-title">{html.escape(str(row.get("counterparty / sector", "") or ""))}</div>
928
+ <div class="result-subtitle">For {html.escape(str(row.get("member", "") or ""))} in the {_plain_family_label(family).lower()} view.</div>
929
+ </div>
930
+ <div class="metric-stack">
931
+ <span class="score-pill">Score {int(row.get("overall score", 0) or 0)}</span>
932
+ <span class="strength-pill">{html.escape(str(row.get("strength", "") or ""))}</span>
933
+ </div>
934
+ </div>
935
+ <div class="chip-row">{chip_html or '<span class="chip">published source support</span>'}</div>
936
+ <div class="meta-grid">
937
+ <div><strong>Evidence window</strong>{html.escape(str(row.get("time-window overlap", "") or ""))}</div>
938
+ <div><strong>Supporting rows</strong>{supporting_rows}</div>
939
+ <div><strong>Stronger support</strong>{stronger_support}</div>
940
+ <div><strong>Needs caution</strong>{needs_caution}</div>
941
+ <div><strong>Unresolved refs</strong>{unresolved_refs}</div>
942
+ </div>
943
+ <div class="result-hint">Use “Explain this link” below to open the detailed breakdown for this relationship.</div>
944
+ </div>
945
+ """
946
+ )
947
+ if not str(member_query or "").strip():
948
+ cards.insert(
949
+ 0,
950
+ "<div class=\"panel-note\"><strong>Tip:</strong> Type one House member name above for the clearest first read.</div>",
951
+ )
952
+ return intro + "<div class=\"result-list\">" + "".join(cards) + "</div>"
953
+
954
+
955
  def _relationship_options(ranked: pd.DataFrame) -> list[tuple[str, str]]:
956
  if ranked.empty:
957
  return []
958
  options: list[tuple[str, str]] = []
959
  for row in ranked.to_dict("records"):
960
+ label = (
961
+ f"#{int(row['rank'])} {row['counterparty / sector']} "
962
+ f"— {row['strength']} (score {row['overall score']})"
963
+ )
964
  options.append((label, str(row["relationship_id"])))
965
  return options
966
 
 
991
  f"- Supporting relationship rows: `{int(row.get('link_count', 0) or 0)}`",
992
  f"- Stronger-support rows: `{int(row.get('linked_count', 0) or 0) if family == 'recipient' else int(row.get('strong_event_count', 0) or 0)}`",
993
  f"- Caution / weaker rows: `{int(row.get('review_count', 0) or 0) if family == 'recipient' else int(row.get('weak_event_count', 0) or 0)}`",
994
+ f"- Integrity-checked source records attached: `{int(row.get('sha_backed_source_artifact_count', 0) or 0)}`",
995
  f"- Unresolved source refs still counted: `{int(row.get('unresolved_source_ref_count', 0) or 0)}`",
996
  f"- Evidence signals: `{', '.join(chips) if chips else 'published source support'}`",
997
  f"- Time-window overlap: `{_window_overlap_text(row)}`",
 
1002
  if urls:
1003
  lines.extend(["", "#### Example published source URLs", ""])
1004
  lines.extend(f"- {item}" for item in urls)
1005
+ lines.extend(
1006
+ [
1007
+ "",
1008
+ "#### Integrity note",
1009
+ "",
1010
+ "- `Integrity-checked` means the release includes a cryptographic fingerprint to help show a published record has not been altered.",
1011
+ ]
1012
+ )
1013
  return "\n".join(lines)
1014
 
1015
 
 
1154
  "### Audit Summary",
1155
  "",
1156
  f"- Event rows in the audit index: `{int(event_payload.get('event_count', 0) or 0)}`",
1157
+ f"- Event rows with integrity-checked source records: `{int(event_payload.get('events_with_artifacts', 0) or 0)}`",
1158
  f"- Stored-versus-lookup provenance mismatches: `{int(event_payload.get('stored_lookup_mismatch_count', 0) or 0)}`",
1159
  f"- Claim-supporting rows in the audit index: `{int(claim_payload.get('row_count', 0) or 0)}`",
1160
+ f"- Claim-supporting rows with integrity-checked source records: `{int(claim_payload.get('rows_with_artifacts', 0) or 0)}`",
1161
  "",
1162
+ "Use the tables below to inspect the public source URLs and integrity-checked source records that support the released rows.",
1163
  ]
1164
  )
1165
 
 
1289
 
1290
  def _event_detail(events: pd.DataFrame, provenance: pd.DataFrame, event_id: str) -> Tuple[str, pd.DataFrame]:
1291
  if not event_id or event_id not in set(events["event_id"]):
1292
+ return "Select an event id to inspect source URLs and integrity-checked source records.", pd.DataFrame()
1293
  event_row = events[events["event_id"] == event_id].head(1).to_dict("records")[0]
1294
  prov_rows = provenance[provenance["row_key"] == event_id]
1295
  member_name = str(event_row.get("member_name") or event_row.get("member_slug") or "Unknown member")
 
1307
  "This panel summarizes one released event row from the public slice.",
1308
  "",
1309
  f"- Event id: `{event_id}`",
1310
+ f"- Event type: `{event_type}`",
1311
  ]
1312
  if score_label:
1313
  lines.append(f"- Score label: `{score_label}`")
 
1321
  lines.extend(
1322
  [
1323
  f"- Attached source URLs in this row: `{int(event_row.get('source_ref_count', 0) or 0)}`",
1324
+ f"- Integrity-checked source records attached: `{int(event_row.get('sha_backed_source_artifact_count', 0) or 0)}`",
1325
  f"- Unresolved source references still counted: `{int(event_row.get('unresolved_source_ref_count', 0) or 0)}`",
1326
  f"- Matching provenance rows shown below: `{len(prov_rows)}`",
1327
  ]
 
1374
  overview_member_limit = int(graph_defaults.get("overview_member_limit", 8))
1375
  default_member_search = str(graph_defaults.get("default_member_search", "") or "")
1376
 
1377
+ def _overview_edges(member_query: str, family: str, only_strong: bool, top_n: int) -> pd.DataFrame:
1378
+ return _filter_graph(
1379
+ edges,
1380
+ family,
1381
+ member_query,
1382
+ "",
1383
+ "all",
1384
+ "stronger" if only_strong else "all",
1385
+ True,
1386
+ int(top_n),
1387
+ overview_member_limit,
1388
+ )
1389
+
1390
+ def _update_overview(member_query: str, family: str, only_strong: bool, top_n: int, relationship_id: str | None = None):
1391
+ filtered_edges = _overview_edges(member_query, family, only_strong, int(top_n))
1392
+ ranked = _rank_relationships(filtered_edges)
1393
+ options = _relationship_options(ranked)
1394
+ valid_ids = {value for _, value in options}
1395
+ selected = relationship_id if relationship_id in valid_ids else (options[0][1] if options else None)
1396
+ return (
1397
+ _overview_summary_markdown(
1398
+ ranked,
1399
+ member_query=member_query,
1400
+ family=family,
1401
+ only_strong_links=only_strong,
1402
+ top_n=int(top_n),
1403
+ ),
1404
+ _overview_cards_html(
1405
+ ranked,
1406
+ member_query=member_query,
1407
+ family=family,
1408
+ only_strong_links=only_strong,
1409
+ top_n=int(top_n),
1410
+ ),
1411
+ gr.update(choices=options, value=selected),
1412
+ _relationship_detail_markdown(filtered_edges, selected or ""),
1413
+ _relationship_timeline_html(filtered_edges, selected or ""),
1414
+ )
1415
+
1416
+ def _update_overview_detail(member_query: str, family: str, only_strong: bool, top_n: int, relationship_id: str):
1417
+ filtered_edges = _overview_edges(member_query, family, only_strong, int(top_n))
1418
+ return _relationship_detail_markdown(filtered_edges, relationship_id), _relationship_timeline_html(filtered_edges, relationship_id)
1419
+
1420
+ def _update_graph(member_query: str, family: str, only_strong: bool, top_n: int):
1421
+ review_status = "stronger" if only_strong else "all"
1422
+ filtered_edges = _filter_graph(
1423
+ edges,
1424
+ family,
1425
+ member_query,
1426
+ "",
1427
+ "all",
1428
+ review_status,
1429
+ True,
1430
+ int(top_n),
1431
+ overview_member_limit,
1432
+ )
1433
+ filtered_nodes = nodes[
1434
+ nodes["node_id"].isin(set(filtered_edges["source_node_id"]).union(set(filtered_edges["target_node_id"])))
1435
+ ]
1436
+ summary = _graph_view_summary_markdown(
1437
+ filtered_edges,
1438
+ family=family,
1439
+ member_query=member_query,
1440
+ target_query="",
1441
+ review_status=review_status,
1442
+ max_edges=int(top_n),
1443
+ )
1444
+ return summary, _render_graph(filtered_nodes, filtered_edges), _graph_table(filtered_edges)
1445
+
1446
+ def _reset_graph(member_query: str):
1447
+ default_family = str(graph_defaults.get("relationship_family", "sector"))
1448
+ default_top_n = min(max(int(graph_defaults.get("max_edges", 20) or 20), 10), 30)
1449
+ filtered_edges = _filter_graph(
1450
+ edges,
1451
+ default_family,
1452
+ member_query,
1453
+ "",
1454
+ "all",
1455
+ "stronger",
1456
+ True,
1457
+ int(default_top_n),
1458
+ overview_member_limit,
1459
+ )
1460
+ filtered_nodes = nodes[
1461
+ nodes["node_id"].isin(set(filtered_edges["source_node_id"]).union(set(filtered_edges["target_node_id"])))
1462
+ ]
1463
+ summary = _graph_view_summary_markdown(
1464
+ filtered_edges,
1465
+ family=default_family,
1466
+ member_query=member_query,
1467
+ target_query="",
1468
+ review_status="stronger",
1469
+ max_edges=int(default_top_n),
1470
+ )
1471
+ return (
1472
+ gr.update(value=default_family),
1473
+ gr.update(value=True),
1474
+ gr.update(value=int(default_top_n)),
1475
+ summary,
1476
+ _render_graph(filtered_nodes, filtered_edges),
1477
+ _graph_table(filtered_edges),
1478
+ )
1479
+
1480
+ def _update_events(member_query: str, event_type: str, score_label: str, text_query: str):
1481
+ return _filter_events(events, member_query, event_type, score_label, text_query)
1482
+
1483
+ with gr.Blocks(title=copy_payload.get("title", "Congress Public Records Slice"), css=_space_css()) as app:
1484
+ gr.HTML(_hero_html(manifest))
1485
+ gr.HTML(_start_here_cards_html())
1486
+
1487
+ with gr.Accordion("Start here: what this is and how to use it", open=True):
1488
  gr.Markdown(
1489
+ "### What you can do in 30 seconds\n\n"
1490
+ "1. Search one House member.\n"
1491
+ "2. Read the ranked sectors or funding recipients.\n"
1492
+ "3. Pick one relationship in **Explain this link**.\n"
1493
+ "4. Open the example source URLs if you want to verify it yourself.\n\n"
1494
+ "Treat this as a lead generator for public-record review, not a conclusion machine."
1495
  )
1496
+ gr.Markdown(_fictional_example_markdown())
1497
  with gr.Row():
1498
+ gr.HTML(_source_table_html(manifest))
1499
+ gr.HTML(_glossary_html())
1500
+
1501
+ gr.Markdown("## Overview")
1502
+ gr.Markdown(
1503
+ "Search one House member, choose sectors or funding recipients, and start with the ranked list. "
1504
+ "This is the main reading path."
1505
+ )
1506
+ with gr.Row():
1507
+ overview_member = gr.Textbox(label="House member", value=default_member_search, scale=3)
1508
+ search_button = gr.Button("Search a House member", variant="primary", scale=1)
1509
+ with gr.Row():
1510
+ overview_family = gr.Radio(
1511
+ label="Show",
1512
+ choices=[("Sectors", "sector"), ("Funding recipients", "recipient")],
1513
+ value="sector",
1514
+ )
1515
+ overview_only_strong = gr.Checkbox(label="Only stronger links", value=True)
1516
+ overview_top_n = gr.Dropdown(label="Show top results", choices=[5, 10, 15, 20], value=10)
1517
+ if example_member_choices:
1518
+ gr.Examples(examples=example_member_choices, inputs=[overview_member], label="Try one of these example members")
1519
+ overview_summary_md = gr.Markdown()
1520
+ overview_cards = gr.HTML()
1521
+
1522
+ gr.Markdown("## Explain Link")
1523
+ relationship_choice = gr.Dropdown(label="Explain this link", choices=[], value=None)
1524
+ with gr.Row():
1525
  overview_detail_md = gr.Markdown()
1526
  overview_timeline_html = gr.HTML()
1527
 
1528
+ search_button.click(
1529
+ _update_overview,
1530
+ [overview_member, overview_family, overview_only_strong, overview_top_n, relationship_choice],
1531
+ [overview_summary_md, overview_cards, relationship_choice, overview_detail_md, overview_timeline_html],
1532
+ )
1533
+ overview_member.submit(
1534
+ _update_overview,
1535
+ [overview_member, overview_family, overview_only_strong, overview_top_n, relationship_choice],
1536
+ [overview_summary_md, overview_cards, relationship_choice, overview_detail_md, overview_timeline_html],
1537
+ )
1538
+ for control in (overview_family, overview_only_strong, overview_top_n):
1539
+ control.change(
1540
+ _update_overview,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1541
  [overview_member, overview_family, overview_only_strong, overview_top_n, relationship_choice],
1542
+ [overview_summary_md, overview_cards, relationship_choice, overview_detail_md, overview_timeline_html],
1543
  )
1544
+ relationship_choice.change(
1545
+ _update_overview_detail,
1546
+ [overview_member, overview_family, overview_only_strong, overview_top_n, relationship_choice],
1547
+ [overview_detail_md, overview_timeline_html],
1548
+ )
1549
+
1550
+ with gr.Accordion("Explore the network map (optional)", open=False):
1551
+ gr.Markdown(
1552
+ "The ranked list above is the clearest way to read this release. "
1553
+ "Use the map below only if you want a visual view of the same relationships."
1554
  )
 
1555
  gr.Markdown(_graph_intro_markdown(data["graph_config"]))
1556
  with gr.Row():
1557
+ graph_family = gr.Radio(
1558
+ label="Show",
1559
+ choices=graph_family_choices,
1560
+ value=str(graph_defaults.get("relationship_family", "sector")),
1561
+ )
1562
+ graph_only_strong = gr.Checkbox(label="Only stronger links", value=True)
1563
+ graph_top_n = gr.Dropdown(label="Show top", choices=[10, 20, 30], value=min(max(int(graph_defaults.get("max_edges", 20) or 20), 10), 30))
1564
+ graph_reset = gr.Button("Reset view")
 
 
1565
  graph_summary_md = gr.Markdown()
1566
  graph_html = gr.HTML()
1567
+ with gr.Accordion("Current relationships in this map", open=False):
1568
+ graph_df = gr.Dataframe(interactive=False)
1569
+
1570
+ for control in (graph_family, graph_only_strong, graph_top_n):
1571
+ control.change(
1572
+ _update_graph,
1573
+ [overview_member, graph_family, graph_only_strong, graph_top_n],
1574
+ [graph_summary_md, graph_html, graph_df],
 
 
 
 
1575
  )
1576
+ graph_reset.click(
1577
+ _reset_graph,
1578
+ [overview_member],
1579
+ [graph_family, graph_only_strong, graph_top_n, graph_summary_md, graph_html, graph_df],
1580
+ )
1581
+ search_button.click(
1582
+ _update_graph,
1583
+ [overview_member, graph_family, graph_only_strong, graph_top_n],
1584
+ [graph_summary_md, graph_html, graph_df],
1585
+ )
1586
+ overview_member.submit(
1587
+ _update_graph,
1588
+ [overview_member, graph_family, graph_only_strong, graph_top_n],
1589
+ [graph_summary_md, graph_html, graph_df],
1590
+ )
1591
+
1592
+ with gr.Accordion("Audit & downloads", open=False):
1593
+ gr.Markdown(
1594
+ "Use these lower sections if you want the raw released event rows, the verification layer, or the download notes. "
1595
+ "Most people can start and stop with the overview above."
1596
+ )
1597
+ with gr.Accordion("Search released event rows", open=False):
1598
+ with gr.Row():
1599
+ member_query = gr.Textbox(label="Member name or slug")
1600
+ event_type = gr.Dropdown(label="Event type", choices=event_type_choices, value="all")
1601
+ score_label = gr.Dropdown(label="Score label", choices=score_label_choices, value="all")
1602
+ text_query = gr.Textbox(label="Issuer or sector search")
1603
+ explore_df = gr.Dataframe(value=events.head(100), interactive=False)
1604
+ for control in (member_query, event_type, score_label, text_query):
1605
+ control.change(_update_events, [member_query, event_type, score_label, text_query], explore_df)
1606
+
1607
+ with gr.Accordion("Inspect one released event row", open=False):
1608
+ event_id = gr.Dropdown(label="Event id", choices=event_id_choices, value=event_id_choices[0] if event_id_choices else None)
1609
+ event_detail_md = gr.Markdown()
1610
+ event_detail_df = gr.Dataframe(interactive=False)
1611
+ event_id.change(_event_detail, [gr.State(events), gr.State(provenance), event_id], [event_detail_md, event_detail_df])
1612
+ app.load(_event_detail, [gr.State(events), gr.State(provenance), event_id], [event_detail_md, event_detail_df])
1613
+
1614
+ with gr.Accordion("Integrity-checked source records and audit summary", open=False):
1615
+ gr.Markdown(_consistency_summary_markdown(data["consistency"]))
1616
+ gr.Dataframe(value=data["artifact_index"].head(200), interactive=False)
1617
+
1618
+ with gr.Accordion("Methodology, limits, and downloads", open=False):
1619
+ gr.Markdown(copy_payload.get("landing_markdown", ""))
1620
+ gr.Markdown(copy_payload.get("downloads_markdown", ""))
1621
+
1622
+ app.load(
1623
+ _update_overview,
1624
+ [overview_member, overview_family, overview_only_strong, overview_top_n, relationship_choice],
1625
+ [overview_summary_md, overview_cards, relationship_choice, overview_detail_md, overview_timeline_html],
1626
+ )
1627
+ app.load(
1628
+ _update_graph,
1629
+ [overview_member, graph_family, graph_only_strong, graph_top_n],
1630
+ [graph_summary_md, graph_html, graph_df],
1631
+ )
1632
  return app