G. Claude Opus 4.7 commited on
Commit
ca68436
·
1 Parent(s): dea9e25

Restructure layout: all prompt selection left, analysis only right

Browse files

- Left column groups every prompt-selection surface: t-SNE map, category
filter (with Select/Deselect all), dataset dropdown, custom prompt input.
- Right column is purely read-out: verdict, risk level, full prompt, stats.
- Plotly legend now uses itemclick='toggleothers' so a single click isolates
the chosen category and a second click restores all.
- New JS bridge captures plotly_legendclick / plotly_legenddoubleclick,
reads the resulting visible trace set, and pushes it to a hidden Gradio
input. The on_legend_sync callback then updates the checkbox filter and
rebuilds the figure, keeping legend and filter authoritative-in-sync.
- LABEL_TO_KEY mapping resolves display names back to category keys
server-side, so the JS stays agnostic of internal IDs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

Files changed (1) hide show
  1. app.py +121 -63
app.py CHANGED
@@ -98,6 +98,7 @@ CATEGORY_LABELS = {
98
  "prompt_leaking": "Prompt Leaking",
99
  "unknown": "Unknown",
100
  }
 
101
 
102
  # ---------------------------------------------------------------------------
103
  # Lazy-loaded risk classifier (Llama Prompt Guard 2)
@@ -238,6 +239,8 @@ def build_tsne_figure(selected_categories=None):
238
  borderwidth=1,
239
  font=dict(color=AB["ink_800"], size=10),
240
  itemsizing="constant",
 
 
241
  ),
242
  xaxis=dict(
243
  title=dict(text="t-SNE 1", font=dict(color=AB["ink_500"], size=11)),
@@ -281,6 +284,23 @@ def deselect_all_categories():
281
  return gr.update(value=[]), build_tsne_figure([])
282
 
283
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
284
  def _dataset_meta_block(category, severity, ground_truth):
285
  return (
286
  f"\n\n<span class='ab-eyebrow'>Dataset metadata</span>\n"
@@ -495,45 +515,68 @@ def build_stats_html():
495
  # ---------------------------------------------------------------------------
496
  PLOTLY_CLICK_JS = """
497
  () => {
498
- function setupClickHandler() {
499
- const plotEl = document.querySelector('#tsne-chart .js-plotly-plot');
500
- if (!plotEl) {
501
- setTimeout(setupClickHandler, 500);
502
- return;
503
- }
504
- function handleClick(data) {
 
 
 
 
 
 
 
 
 
 
 
 
505
  if (data && data.points && data.points.length > 0) {
506
  const idx = data.points[0].customdata;
507
  if (idx !== undefined && idx !== null) {
508
- const inputEl = document.querySelector('#click-index-input textarea')
509
- || document.querySelector('#click-index-input input');
510
- if (inputEl) {
511
- const proto = inputEl.tagName === 'TEXTAREA'
512
- ? window.HTMLTextAreaElement.prototype
513
- : window.HTMLInputElement.prototype;
514
- const nativeSetter = Object.getOwnPropertyDescriptor(proto, 'value').set;
515
- nativeSetter.call(inputEl, String(idx));
516
- inputEl.dispatchEvent(new Event('input', { bubbles: true }));
517
- setTimeout(() => {
518
- inputEl.dispatchEvent(new Event('change', { bubbles: true }));
519
- }, 50);
520
- }
521
  }
522
  }
523
- }
524
- plotEl.on('plotly_click', handleClick);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
525
  const observer = new MutationObserver(() => {
526
  const newPlot = document.querySelector('#tsne-chart .js-plotly-plot');
527
- if (newPlot && !newPlot._hasClickHandler) {
528
- newPlot._hasClickHandler = true;
529
- newPlot.on('plotly_click', handleClick);
530
- }
531
- });
532
- observer.observe(document.querySelector('#tsne-chart') || document.body, {
533
- childList: true, subtree: true
534
  });
 
535
  }
536
- setTimeout(setupClickHandler, 1000);
537
  }
538
  """
539
 
@@ -932,8 +975,8 @@ ALEPH_BETH_CSS = """
932
  margin: 18px 0 !important;
933
  }
934
 
935
- /* Hidden index input (kept invisible) */
936
- #click-index-input {
937
  position: absolute !important;
938
  width: 1px !important;
939
  height: 1px !important;
@@ -1085,64 +1128,77 @@ with gr.Blocks(
1085
  gr.HTML(HEADER_HTML)
1086
  gr.HTML(HOW_TO_HTML)
1087
 
 
1088
  click_index = gr.Textbox(value="", visible=True, elem_id="click-index-input")
 
1089
 
1090
  with gr.Row():
1091
- # ---- Left — t-SNE chart + filters ----
 
 
1092
  with gr.Column(scale=3):
1093
- with gr.Row():
1094
- select_all_btn = gr.Button("Select all", size="sm", scale=1)
1095
- deselect_all_btn = gr.Button("Deselect all", size="sm", scale=1)
1096
-
1097
- category_filter = gr.CheckboxGroup(
1098
- choices=UNIQUE_CATEGORIES,
1099
- value=UNIQUE_CATEGORIES,
1100
- label="Filter by category",
1101
- interactive=True,
1102
- )
1103
  tsne_plot = gr.Plot(
1104
  value=build_tsne_figure(),
1105
  label="t-SNE space",
1106
  elem_id="tsne-chart",
 
1107
  )
1108
  gr.Markdown(
1109
- "<span class='ab-caption'>Click a point to inspect it. "
1110
- "Hover to preview. Scroll to zoom, drag to pan.</span>"
 
1111
  )
1112
 
1113
- # ---- Right — Analysis + controls + stats ----
1114
- with gr.Column(scale=2):
1115
- gr.HTML("<div class='ab-eyebrow'>Analysis</div>"
1116
- "<h3 class='ab-h3'>Verdict & confidence</h3>")
1117
- result_html = gr.HTML(value=empty_analysis_html())
1118
- risk_md = gr.Markdown(value="")
1119
- full_prompt = gr.Textbox(
1120
- label="Full prompt",
1121
- lines=3,
1122
- interactive=False,
1123
- visible=True,
1124
  )
1125
 
1126
- gr.Markdown("---")
1127
-
1128
- gr.HTML("<div class='ab-eyebrow'>Library</div>"
1129
- "<h3 class='ab-h3'>Pick a prompt</h3>")
1130
  prompt_dropdown = gr.Dropdown(
1131
  choices=DROPDOWN_CHOICES,
1132
  label="Search the dataset",
 
1133
  filterable=True,
1134
  interactive=True,
1135
  )
1136
 
1137
- gr.HTML("<div class='ab-eyebrow' style='margin-top:14px;'>Custom</div>"
1138
- "<h3 class='ab-h3'>Analyze your own</h3>")
1139
  manual_input = gr.Textbox(
1140
  label="Prompt",
 
1141
  placeholder="Type or paste a request to evaluate…",
1142
- lines=2,
1143
  )
1144
  analyze_btn = gr.Button("Inspect", variant="primary")
1145
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1146
  gr.Markdown("---")
1147
 
1148
  gr.HTML(build_stats_html())
@@ -1151,6 +1207,8 @@ with gr.Blocks(
1151
  category_filter.change(fn=on_filter_change, inputs=[category_filter], outputs=[tsne_plot])
1152
  select_all_btn.click(fn=select_all_categories, inputs=[], outputs=[category_filter, tsne_plot])
1153
  deselect_all_btn.click(fn=deselect_all_categories, inputs=[], outputs=[category_filter, tsne_plot])
 
 
1154
  click_index.change(fn=on_index_input, inputs=[click_index],
1155
  outputs=[result_html, risk_md, full_prompt])
1156
  prompt_dropdown.change(fn=on_dropdown_select, inputs=[prompt_dropdown],
 
98
  "prompt_leaking": "Prompt Leaking",
99
  "unknown": "Unknown",
100
  }
101
+ LABEL_TO_KEY = {v: k for k, v in CATEGORY_LABELS.items()}
102
 
103
  # ---------------------------------------------------------------------------
104
  # Lazy-loaded risk classifier (Llama Prompt Guard 2)
 
239
  borderwidth=1,
240
  font=dict(color=AB["ink_800"], size=10),
241
  itemsizing="constant",
242
+ itemclick="toggleothers",
243
+ itemdoubleclick="toggle",
244
  ),
245
  xaxis=dict(
246
  title=dict(text="t-SNE 1", font=dict(color=AB["ink_500"], size=11)),
 
284
  return gr.update(value=[]), build_tsne_figure([])
285
 
286
 
287
+ def on_legend_sync(payload):
288
+ """Plotly legend click → sync the checkbox filter + rebuild the chart."""
289
+ if not payload or not payload.strip():
290
+ return gr.update(), gr.update()
291
+ try:
292
+ data = json.loads(payload)
293
+ visible_labels = data.get("visible", [])
294
+ visible_keys = [LABEL_TO_KEY.get(lbl, lbl) for lbl in visible_labels]
295
+ visible_keys = [k for k in visible_keys if k in UNIQUE_CATEGORIES]
296
+ if not visible_keys:
297
+ return gr.update(value=[]), build_tsne_figure([])
298
+ return gr.update(value=visible_keys), build_tsne_figure(visible_keys)
299
+ except Exception as e:
300
+ logger.error("legend sync error: %s", e)
301
+ return gr.update(), gr.update()
302
+
303
+
304
  def _dataset_meta_block(category, severity, ground_truth):
305
  return (
306
  f"\n\n<span class='ab-eyebrow'>Dataset metadata</span>\n"
 
515
  # ---------------------------------------------------------------------------
516
  PLOTLY_CLICK_JS = """
517
  () => {
518
+ function pushToHidden(selector, value) {
519
+ const el = document.querySelector(selector + ' textarea')
520
+ || document.querySelector(selector + ' input');
521
+ if (!el) return;
522
+ const proto = el.tagName === 'TEXTAREA'
523
+ ? window.HTMLTextAreaElement.prototype
524
+ : window.HTMLInputElement.prototype;
525
+ const setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
526
+ setter.call(el, String(value));
527
+ el.dispatchEvent(new Event('input', { bubbles: true }));
528
+ setTimeout(() => el.dispatchEvent(new Event('change', { bubbles: true })), 40);
529
+ }
530
+
531
+ function attachHandlers(plotEl) {
532
+ if (!plotEl || plotEl._abHandlersAttached) return;
533
+ plotEl._abHandlersAttached = true;
534
+
535
+ // Point click → push index to #click-index-input
536
+ plotEl.on('plotly_click', function (data) {
537
  if (data && data.points && data.points.length > 0) {
538
  const idx = data.points[0].customdata;
539
  if (idx !== undefined && idx !== null) {
540
+ pushToHidden('#click-index-input', idx);
 
 
 
 
 
 
 
 
 
 
 
 
541
  }
542
  }
543
+ });
544
+
545
+ // Legend click → after toggleothers settles, read visible trace names
546
+ // and push them to #legend-sync-input as JSON {visible: [...]}.
547
+ plotEl.on('plotly_legendclick', function (ed) {
548
+ setTimeout(() => {
549
+ const visible = (plotEl.data || [])
550
+ .filter(t => t.visible === undefined || t.visible === true)
551
+ .map(t => t.name);
552
+ pushToHidden('#legend-sync-input', JSON.stringify({visible: visible}));
553
+ }, 60);
554
+ return true; // allow Plotly to process its default toggleothers
555
+ });
556
+
557
+ plotEl.on('plotly_legenddoubleclick', function (ed) {
558
+ setTimeout(() => {
559
+ const visible = (plotEl.data || [])
560
+ .filter(t => t.visible === undefined || t.visible === true)
561
+ .map(t => t.name);
562
+ pushToHidden('#legend-sync-input', JSON.stringify({visible: visible}));
563
+ }, 60);
564
+ return true;
565
+ });
566
+ }
567
+
568
+ function setup() {
569
+ const plotEl = document.querySelector('#tsne-chart .js-plotly-plot');
570
+ if (!plotEl) { setTimeout(setup, 500); return; }
571
+ attachHandlers(plotEl);
572
+ const root = document.querySelector('#tsne-chart') || document.body;
573
  const observer = new MutationObserver(() => {
574
  const newPlot = document.querySelector('#tsne-chart .js-plotly-plot');
575
+ if (newPlot) attachHandlers(newPlot);
 
 
 
 
 
 
576
  });
577
+ observer.observe(root, { childList: true, subtree: true });
578
  }
579
+ setTimeout(setup, 1000);
580
  }
581
  """
582
 
 
975
  margin: 18px 0 !important;
976
  }
977
 
978
+ /* Hidden bridges from Plotly DOM → Gradio state */
979
+ #click-index-input, #legend-sync-input {
980
  position: absolute !important;
981
  width: 1px !important;
982
  height: 1px !important;
 
1128
  gr.HTML(HEADER_HTML)
1129
  gr.HTML(HOW_TO_HTML)
1130
 
1131
+ # Hidden bridges from Plotly DOM → Gradio state
1132
  click_index = gr.Textbox(value="", visible=True, elem_id="click-index-input")
1133
+ legend_sync = gr.Textbox(value="", visible=True, elem_id="legend-sync-input")
1134
 
1135
  with gr.Row():
1136
+ # ============================================================
1137
+ # LEFT — every way to pick a prompt
1138
+ # ============================================================
1139
  with gr.Column(scale=3):
1140
+ gr.HTML("<div class='ab-eyebrow'>Map</div>"
1141
+ "<h3 class='ab-h3'>t-SNE Prompt landscape</h3>")
 
 
 
 
 
 
 
 
1142
  tsne_plot = gr.Plot(
1143
  value=build_tsne_figure(),
1144
  label="t-SNE space",
1145
  elem_id="tsne-chart",
1146
+ show_label=False,
1147
  )
1148
  gr.Markdown(
1149
+ "<span class='ab-caption'>Click a point to inspect. "
1150
+ "Click a legend entry to isolate that category — click again to restore. "
1151
+ "Double-click a legend entry to toggle just that trace.</span>"
1152
  )
1153
 
1154
+ gr.HTML("<div class='ab-eyebrow' style='margin-top:18px;'>Filter</div>"
1155
+ "<h3 class='ab-h3'>By category</h3>")
1156
+ with gr.Row():
1157
+ select_all_btn = gr.Button("Select all", size="sm", scale=1)
1158
+ deselect_all_btn = gr.Button("Deselect all", size="sm", scale=1)
1159
+ category_filter = gr.CheckboxGroup(
1160
+ choices=UNIQUE_CATEGORIES,
1161
+ value=UNIQUE_CATEGORIES,
1162
+ label="Categories",
1163
+ show_label=False,
1164
+ interactive=True,
1165
  )
1166
 
1167
+ gr.HTML("<div class='ab-eyebrow' style='margin-top:18px;'>Library</div>"
1168
+ "<h3 class='ab-h3'>Pick a prompt from the dataset</h3>")
 
 
1169
  prompt_dropdown = gr.Dropdown(
1170
  choices=DROPDOWN_CHOICES,
1171
  label="Search the dataset",
1172
+ show_label=False,
1173
  filterable=True,
1174
  interactive=True,
1175
  )
1176
 
1177
+ gr.HTML("<div class='ab-eyebrow' style='margin-top:18px;'>Custom</div>"
1178
+ "<h3 class='ab-h3'>Analyze your own prompt</h3>")
1179
  manual_input = gr.Textbox(
1180
  label="Prompt",
1181
+ show_label=False,
1182
  placeholder="Type or paste a request to evaluate…",
1183
+ lines=3,
1184
  )
1185
  analyze_btn = gr.Button("Inspect", variant="primary")
1186
 
1187
+ # ============================================================
1188
+ # RIGHT — the analysis only
1189
+ # ============================================================
1190
+ with gr.Column(scale=2):
1191
+ gr.HTML("<div class='ab-eyebrow'>Analysis</div>"
1192
+ "<h3 class='ab-h3'>Verdict & confidence</h3>")
1193
+ result_html = gr.HTML(value=empty_analysis_html())
1194
+ risk_md = gr.Markdown(value="")
1195
+ full_prompt = gr.Textbox(
1196
+ label="Full prompt",
1197
+ lines=4,
1198
+ interactive=False,
1199
+ visible=True,
1200
+ )
1201
+
1202
  gr.Markdown("---")
1203
 
1204
  gr.HTML(build_stats_html())
 
1207
  category_filter.change(fn=on_filter_change, inputs=[category_filter], outputs=[tsne_plot])
1208
  select_all_btn.click(fn=select_all_categories, inputs=[], outputs=[category_filter, tsne_plot])
1209
  deselect_all_btn.click(fn=deselect_all_categories, inputs=[], outputs=[category_filter, tsne_plot])
1210
+ legend_sync.change(fn=on_legend_sync, inputs=[legend_sync],
1211
+ outputs=[category_filter, tsne_plot])
1212
  click_index.change(fn=on_index_input, inputs=[click_index],
1213
  outputs=[result_html, risk_md, full_prompt])
1214
  prompt_dropdown.change(fn=on_dropdown_select, inputs=[prompt_dropdown],