nuocuhz Claude Opus 4.6 commited on
Commit
24112e5
·
1 Parent(s): 79e816f

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>

Files changed (3) hide show
  1. app/db.py +30 -9
  2. app/tab_analytics.py +5 -1
  3. 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, bool], notes: str = "",
75
  annotator_name: str = "") -> int:
76
- score = sum(1 for v in checklist.values() if v)
 
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
- int(checklist.get("A1", False)), int(checklist.get("A2", False)),
94
- int(checklist.get("B1", False)), int(checklist.get("B2", False)),
95
- int(checklist.get("C1", False)), int(checklist.get("C2", False)),
96
- int(checklist.get("D1", False)), int(checklist.get("D2", False)),
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
- total = conn.execute(f"SELECT SUM({item_id}) FROM annotations").fetchone()[0]
144
- stats[f"rate_{item_id}"] = total or 0
 
 
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
- checkboxes = {}
114
 
115
  gr.Markdown("**A. Evidence & Counterfactual**")
116
  with gr.Row():
117
- checkboxes["A1"] = gr.Checkbox(
118
- label=f"A1: {CHECKLIST_ITEMS['A1']}", value=False, interactive=True,
 
119
  )
120
- checkboxes["A2"] = gr.Checkbox(
121
- label=f"A2: {CHECKLIST_ITEMS['A2']}", value=False, interactive=True,
 
122
  )
123
 
124
  gr.Markdown("**B. Causal Reasoning**")
125
  with gr.Row():
126
- checkboxes["B1"] = gr.Checkbox(
127
- label=f"B1: {CHECKLIST_ITEMS['B1']}", value=False, interactive=True,
 
128
  )
129
- checkboxes["B2"] = gr.Checkbox(
130
- label=f"B2: {CHECKLIST_ITEMS['B2']}", value=False, interactive=True,
 
131
  )
132
 
133
  gr.Markdown("**C. Effort Investment**")
134
  with gr.Row():
135
- checkboxes["C1"] = gr.Checkbox(
136
- label=f"C1: {CHECKLIST_ITEMS['C1']}", value=False, interactive=True,
 
137
  )
138
- checkboxes["C2"] = gr.Checkbox(
139
- label=f"C2: {CHECKLIST_ITEMS['C2']}", value=False, interactive=True,
 
140
  )
141
 
142
  gr.Markdown("**D. Belief Update**")
143
  with gr.Row():
144
- checkboxes["D1"] = gr.Checkbox(
145
- label=f"D1: {CHECKLIST_ITEMS['D1']}", value=False, interactive=True,
 
146
  )
147
- checkboxes["D2"] = gr.Checkbox(
148
- label=f"D2: {CHECKLIST_ITEMS['D2']}", value=False, interactive=True,
 
149
  )
150
 
151
  # --- Score + Notes + Status ---
@@ -159,14 +169,15 @@ def build_annotation_tab(store: DataStore):
159
 
160
  # ========== Callbacks ==========
161
 
162
- all_cb = [checkboxes[k] for k in ["A1", "A2", "B1", "B2", "C1", "C2", "D1", "D2"]]
 
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
- *all_cb,
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
- False, False, False, False, False, False, False, False,
 
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
- cb_vals = [bool(ann.get(k, 0)) for k in ["A1", "A2", "B1", "B2", "C1", "C2", "D1", "D2"]]
241
  notes = ann.get("notes", "")
242
  score = ann.get("score", 0)
243
  status = f"Loaded existing annotation (score: {score}/8)"
244
  else:
245
- cb_vals = [False] * 8
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
- *cb_vals,
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
- False, False, False, False, False, False, False, False,
 
337
  "", _score_bar(0), "Paper not found",
338
  "**Review 0** / 0", _progress_bar(0, 0),
339
  0,
340
  )
341
 
342
- def live_score(*cb_values):
343
- score = sum(1 for v in cb_values if v)
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, *all_cb, notes_box],
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 checkbox change
374
- for cb in all_cb:
375
- cb.change(fn=live_score, inputs=all_cb, outputs=[score_display])
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():