Spaces:
Sleeping
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>
|
@@ -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
|
| 499 |
-
const
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 505 |
if (data && data.points && data.points.length > 0) {
|
| 506 |
const idx = data.points[0].customdata;
|
| 507 |
if (idx !== undefined && idx !== null) {
|
| 508 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 525 |
const observer = new MutationObserver(() => {
|
| 526 |
const newPlot = document.querySelector('#tsne-chart .js-plotly-plot');
|
| 527 |
-
if (newPlot
|
| 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(
|
| 537 |
}
|
| 538 |
"""
|
| 539 |
|
|
@@ -932,8 +975,8 @@ ALEPH_BETH_CSS = """
|
|
| 932 |
margin: 18px 0 !important;
|
| 933 |
}
|
| 934 |
|
| 935 |
-
/* Hidden
|
| 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 |
-
#
|
|
|
|
|
|
|
| 1092 |
with gr.Column(scale=3):
|
| 1093 |
-
|
| 1094 |
-
|
| 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
|
| 1110 |
-
"
|
|
|
|
| 1111 |
)
|
| 1112 |
|
| 1113 |
-
|
| 1114 |
-
|
| 1115 |
-
gr.
|
| 1116 |
-
|
| 1117 |
-
|
| 1118 |
-
|
| 1119 |
-
|
| 1120 |
-
|
| 1121 |
-
|
| 1122 |
-
|
| 1123 |
-
|
| 1124 |
)
|
| 1125 |
|
| 1126 |
-
gr.
|
| 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:
|
| 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=
|
| 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],
|