Spaces:
Runtime error
Runtime error
Switch checklist from Checkbox to Radio (Yes/No/Unsure)
Browse files- Each checklist item now has 3 options instead of binary check
- DB stores 1 (Yes), -1 (No), 0 (Unsure); backward compatible with old 0/1 data
- Analytics: count only Yes (=1) for check rates, show labels in table
- Score still counts only Yes answers out of 8
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- app/db.py +30 -9
- app/tab_analytics.py +5 -1
- app/tab_annotation.py +45 -32
app/db.py
CHANGED
|
@@ -70,10 +70,29 @@ def init_db():
|
|
| 70 |
conn.close()
|
| 71 |
|
| 72 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
def save_annotation(paper_id: str, reviewer_id: str, conference: str,
|
| 74 |
-
checklist: Dict[str,
|
| 75 |
annotator_name: str = "") -> int:
|
| 76 |
-
|
|
|
|
| 77 |
now = datetime.now().isoformat()
|
| 78 |
|
| 79 |
conn = sqlite3.connect(DB_PATH)
|
|
@@ -90,10 +109,10 @@ def save_annotation(paper_id: str, reviewer_id: str, conference: str,
|
|
| 90 |
score=excluded.score, notes=excluded.notes,
|
| 91 |
updated_at=excluded.updated_at
|
| 92 |
""", (paper_id, reviewer_id, conference, annotator_name,
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
score, notes, now, now))
|
| 98 |
conn.commit()
|
| 99 |
conn.close()
|
|
@@ -138,10 +157,12 @@ def get_stats() -> dict:
|
|
| 138 |
).fetchall()
|
| 139 |
stats["score_dist"] = {r[0]: r[1] for r in rows}
|
| 140 |
|
| 141 |
-
# Per-item check rates
|
| 142 |
for item_id in CHECKLIST_ITEMS:
|
| 143 |
-
|
| 144 |
-
|
|
|
|
|
|
|
| 145 |
|
| 146 |
# Per-conference
|
| 147 |
rows = conn.execute(
|
|
|
|
| 70 |
conn.close()
|
| 71 |
|
| 72 |
|
| 73 |
+
def _radio_to_int(val) -> int:
|
| 74 |
+
"""Convert Radio value to int: 'Yes'->1, 'No'->-1, 'Unsure'/other->0."""
|
| 75 |
+
if val == "Yes":
|
| 76 |
+
return 1
|
| 77 |
+
elif val == "No":
|
| 78 |
+
return -1
|
| 79 |
+
return 0
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def _int_to_radio(val) -> str:
|
| 83 |
+
"""Convert stored int to Radio value."""
|
| 84 |
+
if val == 1:
|
| 85 |
+
return "Yes"
|
| 86 |
+
elif val == -1:
|
| 87 |
+
return "No"
|
| 88 |
+
return "Unsure"
|
| 89 |
+
|
| 90 |
+
|
| 91 |
def save_annotation(paper_id: str, reviewer_id: str, conference: str,
|
| 92 |
+
checklist: Dict[str, str], notes: str = "",
|
| 93 |
annotator_name: str = "") -> int:
|
| 94 |
+
vals = {k: _radio_to_int(v) for k, v in checklist.items()}
|
| 95 |
+
score = sum(1 for v in vals.values() if v == 1)
|
| 96 |
now = datetime.now().isoformat()
|
| 97 |
|
| 98 |
conn = sqlite3.connect(DB_PATH)
|
|
|
|
| 109 |
score=excluded.score, notes=excluded.notes,
|
| 110 |
updated_at=excluded.updated_at
|
| 111 |
""", (paper_id, reviewer_id, conference, annotator_name,
|
| 112 |
+
vals.get("A1", 0), vals.get("A2", 0),
|
| 113 |
+
vals.get("B1", 0), vals.get("B2", 0),
|
| 114 |
+
vals.get("C1", 0), vals.get("C2", 0),
|
| 115 |
+
vals.get("D1", 0), vals.get("D2", 0),
|
| 116 |
score, notes, now, now))
|
| 117 |
conn.commit()
|
| 118 |
conn.close()
|
|
|
|
| 157 |
).fetchall()
|
| 158 |
stats["score_dist"] = {r[0]: r[1] for r in rows}
|
| 159 |
|
| 160 |
+
# Per-item check rates (count rows where item == 1, i.e. "Yes")
|
| 161 |
for item_id in CHECKLIST_ITEMS:
|
| 162 |
+
cnt = conn.execute(
|
| 163 |
+
f"SELECT COUNT(*) FROM annotations WHERE {item_id} = 1"
|
| 164 |
+
).fetchone()[0]
|
| 165 |
+
stats[f"rate_{item_id}"] = cnt or 0
|
| 166 |
|
| 167 |
# Per-conference
|
| 168 |
rows = conn.execute(
|
app/tab_analytics.py
CHANGED
|
@@ -8,7 +8,7 @@ import tempfile
|
|
| 8 |
import os
|
| 9 |
from data_loader import DataStore
|
| 10 |
from db import (CHECKLIST_ITEMS, DIMENSIONS, get_all_annotations_df,
|
| 11 |
-
get_stats, export_csv)
|
| 12 |
|
| 13 |
|
| 14 |
def _empty_fig(msg="No annotation data yet"):
|
|
@@ -175,6 +175,10 @@ def build_analytics_tab(store: DataStore):
|
|
| 175 |
"A1", "A2", "B1", "B2", "C1", "C2", "D1", "D2",
|
| 176 |
"score", "notes", "updated_at"]
|
| 177 |
table_df = df[display_cols] if not df.empty else pd.DataFrame()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
|
| 179 |
return (summary, fig_hist, fig_items, fig_conf_count, fig_conf_score,
|
| 180 |
fig_corr, fig_dim, table_df)
|
|
|
|
| 8 |
import os
|
| 9 |
from data_loader import DataStore
|
| 10 |
from db import (CHECKLIST_ITEMS, DIMENSIONS, get_all_annotations_df,
|
| 11 |
+
get_stats, export_csv, _int_to_radio)
|
| 12 |
|
| 13 |
|
| 14 |
def _empty_fig(msg="No annotation data yet"):
|
|
|
|
| 175 |
"A1", "A2", "B1", "B2", "C1", "C2", "D1", "D2",
|
| 176 |
"score", "notes", "updated_at"]
|
| 177 |
table_df = df[display_cols] if not df.empty else pd.DataFrame()
|
| 178 |
+
# Convert integer codes to readable labels in table
|
| 179 |
+
if not table_df.empty:
|
| 180 |
+
for col in ["A1", "A2", "B1", "B2", "C1", "C2", "D1", "D2"]:
|
| 181 |
+
table_df[col] = table_df[col].apply(_int_to_radio)
|
| 182 |
|
| 183 |
return (summary, fig_hist, fig_items, fig_conf_count, fig_conf_score,
|
| 184 |
fig_corr, fig_dim, table_df)
|
app/tab_annotation.py
CHANGED
|
@@ -3,8 +3,10 @@
|
|
| 3 |
import gradio as gr
|
| 4 |
from data_loader import DataStore
|
| 5 |
from db import (CHECKLIST_ITEMS, DIMENSIONS, save_annotation,
|
| 6 |
-
get_annotation, get_annotated_set, get_global_annotated_count
|
|
|
|
| 7 |
|
|
|
|
| 8 |
|
| 9 |
# Server-side queue storage (avoids sending 70K tuples to browser)
|
| 10 |
_queues = {} # queue_key -> list of (paper_id, reviewer_id, conference)
|
|
@@ -108,44 +110,52 @@ def build_annotation_tab(store: DataStore):
|
|
| 108 |
|
| 109 |
gr.Markdown("---")
|
| 110 |
|
| 111 |
-
# --- Checklist ---
|
| 112 |
gr.Markdown("### Checklist")
|
| 113 |
-
|
| 114 |
|
| 115 |
gr.Markdown("**A. Evidence & Counterfactual**")
|
| 116 |
with gr.Row():
|
| 117 |
-
|
| 118 |
-
|
|
|
|
| 119 |
)
|
| 120 |
-
|
| 121 |
-
|
|
|
|
| 122 |
)
|
| 123 |
|
| 124 |
gr.Markdown("**B. Causal Reasoning**")
|
| 125 |
with gr.Row():
|
| 126 |
-
|
| 127 |
-
|
|
|
|
| 128 |
)
|
| 129 |
-
|
| 130 |
-
|
|
|
|
| 131 |
)
|
| 132 |
|
| 133 |
gr.Markdown("**C. Effort Investment**")
|
| 134 |
with gr.Row():
|
| 135 |
-
|
| 136 |
-
|
|
|
|
| 137 |
)
|
| 138 |
-
|
| 139 |
-
|
|
|
|
| 140 |
)
|
| 141 |
|
| 142 |
gr.Markdown("**D. Belief Update**")
|
| 143 |
with gr.Row():
|
| 144 |
-
|
| 145 |
-
|
|
|
|
| 146 |
)
|
| 147 |
-
|
| 148 |
-
|
|
|
|
| 149 |
)
|
| 150 |
|
| 151 |
# --- Score + Notes + Status ---
|
|
@@ -159,14 +169,15 @@ def build_annotation_tab(store: DataStore):
|
|
| 159 |
|
| 160 |
# ========== Callbacks ==========
|
| 161 |
|
| 162 |
-
|
|
|
|
| 163 |
|
| 164 |
# All outputs that get updated when navigating to a review
|
| 165 |
review_outputs = [
|
| 166 |
paper_dd, reviewer_dd,
|
| 167 |
conf_display, init_score_display, final_score_display, change_display,
|
| 168 |
review_display, rebuttal_display,
|
| 169 |
-
*
|
| 170 |
notes_box, score_display, status_msg,
|
| 171 |
idx_display, progress_md,
|
| 172 |
idx_state,
|
|
@@ -184,7 +195,8 @@ def build_annotation_tab(store: DataStore):
|
|
| 184 |
gr.update(), gr.update(),
|
| 185 |
"", "", "", "",
|
| 186 |
"", "",
|
| 187 |
-
|
|
|
|
| 188 |
"", _score_bar(0), "Queue is empty",
|
| 189 |
"**Review 0** / 0", _progress_bar(0, 0),
|
| 190 |
idx,
|
|
@@ -237,12 +249,12 @@ def build_annotation_tab(store: DataStore):
|
|
| 237 |
# Load existing annotation
|
| 238 |
ann = get_annotation(paper_id, reviewer_id, annotator_name or "")
|
| 239 |
if ann:
|
| 240 |
-
|
| 241 |
notes = ann.get("notes", "")
|
| 242 |
score = ann.get("score", 0)
|
| 243 |
status = f"Loaded existing annotation (score: {score}/8)"
|
| 244 |
else:
|
| 245 |
-
|
| 246 |
notes = ""
|
| 247 |
score = 0
|
| 248 |
status = "No existing annotation — start checking items below"
|
|
@@ -257,7 +269,7 @@ def build_annotation_tab(store: DataStore):
|
|
| 257 |
conference,
|
| 258 |
str(init_rating), str(final_rating), change_str,
|
| 259 |
review_text, rebuttal_text,
|
| 260 |
-
*
|
| 261 |
notes, _score_bar(score), status,
|
| 262 |
idx_text, _progress_bar(done, total),
|
| 263 |
idx,
|
|
@@ -333,14 +345,15 @@ def build_annotation_tab(store: DataStore):
|
|
| 333 |
gr.update(), gr.update(),
|
| 334 |
"", "", "", "",
|
| 335 |
"", "",
|
| 336 |
-
|
|
|
|
| 337 |
"", _score_bar(0), "Paper not found",
|
| 338 |
"**Review 0** / 0", _progress_bar(0, 0),
|
| 339 |
0,
|
| 340 |
)
|
| 341 |
|
| 342 |
-
def live_score(*
|
| 343 |
-
score = sum(1 for v in
|
| 344 |
return _score_bar(score)
|
| 345 |
|
| 346 |
# ========== Wire Events ==========
|
|
@@ -352,7 +365,7 @@ def build_annotation_tab(store: DataStore):
|
|
| 352 |
save_next_btn.click(
|
| 353 |
fn=save_and_next,
|
| 354 |
inputs=[idx_state, queue_key_state, annotator_input,
|
| 355 |
-
paper_dd, reviewer_dd, *
|
| 356 |
outputs=review_outputs,
|
| 357 |
)
|
| 358 |
|
|
@@ -370,9 +383,9 @@ def build_annotation_tab(store: DataStore):
|
|
| 370 |
outputs=review_outputs,
|
| 371 |
)
|
| 372 |
|
| 373 |
-
# Live score update on any
|
| 374 |
-
for
|
| 375 |
-
|
| 376 |
|
| 377 |
# Return progress component and init function for demo.load()
|
| 378 |
def init_progress():
|
|
|
|
| 3 |
import gradio as gr
|
| 4 |
from data_loader import DataStore
|
| 5 |
from db import (CHECKLIST_ITEMS, DIMENSIONS, save_annotation,
|
| 6 |
+
get_annotation, get_annotated_set, get_global_annotated_count,
|
| 7 |
+
_int_to_radio)
|
| 8 |
|
| 9 |
+
RADIO_CHOICES = ["Yes", "No", "Unsure"]
|
| 10 |
|
| 11 |
# Server-side queue storage (avoids sending 70K tuples to browser)
|
| 12 |
_queues = {} # queue_key -> list of (paper_id, reviewer_id, conference)
|
|
|
|
| 110 |
|
| 111 |
gr.Markdown("---")
|
| 112 |
|
| 113 |
+
# --- Checklist (Radio buttons: Yes / No / Unsure) ---
|
| 114 |
gr.Markdown("### Checklist")
|
| 115 |
+
radios = {}
|
| 116 |
|
| 117 |
gr.Markdown("**A. Evidence & Counterfactual**")
|
| 118 |
with gr.Row():
|
| 119 |
+
radios["A1"] = gr.Radio(
|
| 120 |
+
choices=RADIO_CHOICES, value="Unsure", interactive=True,
|
| 121 |
+
label=f"A1: {CHECKLIST_ITEMS['A1']}",
|
| 122 |
)
|
| 123 |
+
radios["A2"] = gr.Radio(
|
| 124 |
+
choices=RADIO_CHOICES, value="Unsure", interactive=True,
|
| 125 |
+
label=f"A2: {CHECKLIST_ITEMS['A2']}",
|
| 126 |
)
|
| 127 |
|
| 128 |
gr.Markdown("**B. Causal Reasoning**")
|
| 129 |
with gr.Row():
|
| 130 |
+
radios["B1"] = gr.Radio(
|
| 131 |
+
choices=RADIO_CHOICES, value="Unsure", interactive=True,
|
| 132 |
+
label=f"B1: {CHECKLIST_ITEMS['B1']}",
|
| 133 |
)
|
| 134 |
+
radios["B2"] = gr.Radio(
|
| 135 |
+
choices=RADIO_CHOICES, value="Unsure", interactive=True,
|
| 136 |
+
label=f"B2: {CHECKLIST_ITEMS['B2']}",
|
| 137 |
)
|
| 138 |
|
| 139 |
gr.Markdown("**C. Effort Investment**")
|
| 140 |
with gr.Row():
|
| 141 |
+
radios["C1"] = gr.Radio(
|
| 142 |
+
choices=RADIO_CHOICES, value="Unsure", interactive=True,
|
| 143 |
+
label=f"C1: {CHECKLIST_ITEMS['C1']}",
|
| 144 |
)
|
| 145 |
+
radios["C2"] = gr.Radio(
|
| 146 |
+
choices=RADIO_CHOICES, value="Unsure", interactive=True,
|
| 147 |
+
label=f"C2: {CHECKLIST_ITEMS['C2']}",
|
| 148 |
)
|
| 149 |
|
| 150 |
gr.Markdown("**D. Belief Update**")
|
| 151 |
with gr.Row():
|
| 152 |
+
radios["D1"] = gr.Radio(
|
| 153 |
+
choices=RADIO_CHOICES, value="Unsure", interactive=True,
|
| 154 |
+
label=f"D1: {CHECKLIST_ITEMS['D1']}",
|
| 155 |
)
|
| 156 |
+
radios["D2"] = gr.Radio(
|
| 157 |
+
choices=RADIO_CHOICES, value="Unsure", interactive=True,
|
| 158 |
+
label=f"D2: {CHECKLIST_ITEMS['D2']}",
|
| 159 |
)
|
| 160 |
|
| 161 |
# --- Score + Notes + Status ---
|
|
|
|
| 169 |
|
| 170 |
# ========== Callbacks ==========
|
| 171 |
|
| 172 |
+
ITEM_KEYS = ["A1", "A2", "B1", "B2", "C1", "C2", "D1", "D2"]
|
| 173 |
+
all_radios = [radios[k] for k in ITEM_KEYS]
|
| 174 |
|
| 175 |
# All outputs that get updated when navigating to a review
|
| 176 |
review_outputs = [
|
| 177 |
paper_dd, reviewer_dd,
|
| 178 |
conf_display, init_score_display, final_score_display, change_display,
|
| 179 |
review_display, rebuttal_display,
|
| 180 |
+
*all_radios,
|
| 181 |
notes_box, score_display, status_msg,
|
| 182 |
idx_display, progress_md,
|
| 183 |
idx_state,
|
|
|
|
| 195 |
gr.update(), gr.update(),
|
| 196 |
"", "", "", "",
|
| 197 |
"", "",
|
| 198 |
+
"Unsure", "Unsure", "Unsure", "Unsure",
|
| 199 |
+
"Unsure", "Unsure", "Unsure", "Unsure",
|
| 200 |
"", _score_bar(0), "Queue is empty",
|
| 201 |
"**Review 0** / 0", _progress_bar(0, 0),
|
| 202 |
idx,
|
|
|
|
| 249 |
# Load existing annotation
|
| 250 |
ann = get_annotation(paper_id, reviewer_id, annotator_name or "")
|
| 251 |
if ann:
|
| 252 |
+
radio_vals = [_int_to_radio(ann.get(k, 0)) for k in ITEM_KEYS]
|
| 253 |
notes = ann.get("notes", "")
|
| 254 |
score = ann.get("score", 0)
|
| 255 |
status = f"Loaded existing annotation (score: {score}/8)"
|
| 256 |
else:
|
| 257 |
+
radio_vals = ["Unsure"] * 8
|
| 258 |
notes = ""
|
| 259 |
score = 0
|
| 260 |
status = "No existing annotation — start checking items below"
|
|
|
|
| 269 |
conference,
|
| 270 |
str(init_rating), str(final_rating), change_str,
|
| 271 |
review_text, rebuttal_text,
|
| 272 |
+
*radio_vals,
|
| 273 |
notes, _score_bar(score), status,
|
| 274 |
idx_text, _progress_bar(done, total),
|
| 275 |
idx,
|
|
|
|
| 345 |
gr.update(), gr.update(),
|
| 346 |
"", "", "", "",
|
| 347 |
"", "",
|
| 348 |
+
"Unsure", "Unsure", "Unsure", "Unsure",
|
| 349 |
+
"Unsure", "Unsure", "Unsure", "Unsure",
|
| 350 |
"", _score_bar(0), "Paper not found",
|
| 351 |
"**Review 0** / 0", _progress_bar(0, 0),
|
| 352 |
0,
|
| 353 |
)
|
| 354 |
|
| 355 |
+
def live_score(*radio_values):
|
| 356 |
+
score = sum(1 for v in radio_values if v == "Yes")
|
| 357 |
return _score_bar(score)
|
| 358 |
|
| 359 |
# ========== Wire Events ==========
|
|
|
|
| 365 |
save_next_btn.click(
|
| 366 |
fn=save_and_next,
|
| 367 |
inputs=[idx_state, queue_key_state, annotator_input,
|
| 368 |
+
paper_dd, reviewer_dd, *all_radios, notes_box],
|
| 369 |
outputs=review_outputs,
|
| 370 |
)
|
| 371 |
|
|
|
|
| 383 |
outputs=review_outputs,
|
| 384 |
)
|
| 385 |
|
| 386 |
+
# Live score update on any radio change
|
| 387 |
+
for r in all_radios:
|
| 388 |
+
r.change(fn=live_score, inputs=all_radios, outputs=[score_display])
|
| 389 |
|
| 390 |
# Return progress component and init function for demo.load()
|
| 391 |
def init_progress():
|