AdnanElAssadi commited on
Commit
a3a2c22
·
verified ·
1 Parent(s): 91ebc76

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +98 -360
app.py CHANGED
@@ -12,17 +12,21 @@ def create_reranking_interface(task_data):
12
  def save_ranking(rankings, sample_id):
13
  """Save the current set of rankings."""
14
  try:
 
 
 
 
 
 
 
 
 
 
 
15
  # Check if all documents have rankings
16
- all_ranked = all(r is not None and r != "" for r in rankings)
17
- if not all_ranked:
18
  return "⚠️ Please assign a rank to all documents before submitting", f"Progress: {sum(completed_samples.values())}/{len(samples)}"
19
 
20
- # Convert rankings to integers with better error handling
21
- try:
22
- processed_rankings = [int(r) for r in rankings]
23
- except ValueError:
24
- return "⚠️ Invalid ranking value. Please use only numbers.", f"Progress: {sum(completed_samples.values())}/{len(samples)}"
25
-
26
  # Check for duplicate rankings
27
  if len(set(processed_rankings)) != len(processed_rankings):
28
  return "⚠️ Each document must have a unique rank. Please review your rankings.", f"Progress: {sum(completed_samples.values())}/{len(samples)}"
@@ -86,342 +90,40 @@ def create_reranking_interface(task_data):
86
 
87
  gr.Markdown("## Documents to Rank:")
88
 
89
- # Create document displays and ranking dropdowns in synchronized pairs
90
  doc_containers = []
91
- ranking_dropdowns = []
92
 
93
  with gr.Column():
94
  for i, doc in enumerate(samples[0]["candidates"]):
95
- with gr.Row():
 
96
  doc_box = gr.Textbox(
97
  value=doc,
98
- label=f"Document {i+1}",
99
  interactive=False,
100
- elem_classes="doc-box"
101
  )
102
- dropdown = gr.Dropdown(
103
- choices=[str(j) for j in range(1, len(samples[0]["candidates"])+1)],
104
- label=f"Rank",
105
  value=None,
106
- elem_classes="ranking-dropdown"
 
 
 
107
  )
108
- # Add Quick Rank buttons for fast selection
109
- with gr.Column(min_width=120):
110
- gr.Markdown(f"Quick Rank", elem_classes="quick-rank-label")
111
- with gr.Row():
112
- # Add first 5 rank buttons (or fewer if there are fewer candidates)
113
- num_buttons = min(5, len(samples[0]["candidates"]))
114
- for r in range(1, num_buttons + 1):
115
- button = gr.Button(f"{r}", size="sm", elem_classes=f"quick-rank-btn quick-rank-btn-{i}-{r}")
116
- # Use JavaScript to set the dropdown value when clicked
117
- button.click(
118
- None,
119
- [],
120
- [],
121
- _js=f"() => {{ document.querySelectorAll('.ranking-dropdown')[{i}].value = '{r}'; return []; }}"
122
- )
123
-
124
- doc_containers.append(doc_box)
125
- ranking_dropdowns.append(dropdown)
126
 
127
- # Add keyboard shortcuts explanation
128
- with gr.Accordion("Keyboard Shortcuts", open=False):
129
  gr.Markdown("""
130
- ### Keyboard Shortcuts
131
- - When a document text box is focused:
132
- - Press number keys (1-9) to assign rankings quickly
133
- - Navigation:
134
- - Press 'n' to go to the next query
135
- - Press 'p' to go to the previous query
136
- - Press 's' to submit the current rankings
137
- """)
138
-
139
- # Add JavaScript for keyboard shortcuts
140
- gr.HTML("""
141
- <script>
142
- document.addEventListener('DOMContentLoaded', function() {
143
- // Wait for Gradio elements to be fully loaded
144
- setTimeout(() => {
145
- // Get all document textboxes
146
- const docBoxes = document.querySelectorAll('.doc-box');
147
- const dropdowns = document.querySelectorAll('.ranking-dropdown');
148
-
149
- // Add event listeners to document boxes
150
- docBoxes.forEach((box, index) => {
151
- box.addEventListener('click', function() {
152
- // Mark this box as active for keyboard shortcuts
153
- docBoxes.forEach(b => b.classList.remove('active-doc'));
154
- box.classList.add('active-doc');
155
- });
156
- });
157
-
158
- // Add event listeners to dropdowns for color coding
159
- dropdowns.forEach((dropdown, index) => {
160
- dropdown.addEventListener('change', function() {
161
- updateDropdownColor(dropdown, docBoxes[index]);
162
- });
163
- });
164
-
165
- // Function to update color based on rank
166
- function updateDropdownColor(dropdown, docBox) {
167
- const value = dropdown.value;
168
- if (!value) return;
169
-
170
- // Remove existing color classes
171
- dropdown.classList.remove('rank-1', 'rank-2', 'rank-3', 'rank-4', 'rank-5', 'rank-high');
172
-
173
- // Add appropriate color class
174
- if (value === '1') dropdown.classList.add('rank-1');
175
- else if (value === '2') dropdown.classList.add('rank-2');
176
- else if (value === '3') dropdown.classList.add('rank-3');
177
- else if (value === '4') dropdown.classList.add('rank-4');
178
- else if (value === '5') dropdown.classList.add('rank-5');
179
- else dropdown.classList.add('rank-high');
180
-
181
- // Add highlighting to document box
182
- docBox.classList.add('ranked-doc');
183
- }
184
-
185
- // Add global keyboard listener
186
- document.addEventListener('keydown', function(e) {
187
- // Number keys 1-9 for ranking
188
- if (e.key >= '1' && e.key <= '9') {
189
- const activeDoc = document.querySelector('.active-doc');
190
- if (activeDoc) {
191
- const index = Array.from(docBoxes).indexOf(activeDoc);
192
- const dropdown = document.querySelectorAll('.ranking-dropdown')[index];
193
- if (dropdown) {
194
- dropdown.value = e.key;
195
- dropdown.dispatchEvent(new Event('change'));
196
- updateDropdownColor(dropdown, activeDoc);
197
- }
198
- }
199
- }
200
-
201
- // Navigation shortcuts
202
- if (e.key === 'n') {
203
- // Next query
204
- document.querySelector('#next-btn').click();
205
- } else if (e.key === 'p') {
206
- // Previous query
207
- document.querySelector('#prev-btn').click();
208
- } else if (e.key === 's') {
209
- // Submit rankings
210
- document.querySelector('#submit-btn').click();
211
- }
212
- });
213
-
214
- // Add some CSS for active document
215
- const style = document.createElement('style');
216
- style.textContent = `
217
- .active-doc {
218
- border-left: 3px solid #3B82F6 !important;
219
- background-color: rgba(59, 130, 246, 0.05) !important;
220
- }
221
- .ranked-doc {
222
- border-bottom: 2px solid #4ADE80 !important;
223
- }
224
- .rank-1 {
225
- background-color: rgba(74, 222, 128, 0.2) !important;
226
- font-weight: bold !important;
227
- }
228
- .rank-2 {
229
- background-color: rgba(74, 222, 128, 0.15) !important;
230
- }
231
- .rank-3 {
232
- background-color: rgba(251, 191, 36, 0.15) !important;
233
- }
234
- .rank-4 {
235
- background-color: rgba(251, 191, 36, 0.1) !important;
236
- }
237
- .rank-5 {
238
- background-color: rgba(239, 68, 68, 0.1) !important;
239
- }
240
- .rank-high {
241
- background-color: rgba(239, 68, 68, 0.05) !important;
242
- }
243
- .quick-rank-label {
244
- margin-bottom: 0 !important;
245
- font-size: 0.8rem !important;
246
- opacity: 0.8;
247
- }
248
- .quick-rank-btn {
249
- min-width: 20px !important;
250
- height: 24px !important;
251
- line-height: 1 !important;
252
- padding: 2px 6px !important;
253
- }
254
- `;
255
- document.head.appendChild(style);
256
- }, 1000);
257
- });
258
- </script>
259
- """)
260
-
261
- # Add visual ranking mode option
262
- with gr.Row():
263
- visual_mode_btn = gr.Button("Toggle Visual Ranking Mode", size="sm")
264
- reset_rankings_btn = gr.Button("Reset Rankings", size="sm", variant="secondary")
265
-
266
- # Visual ranking display
267
- with gr.Column(visible=False) as visual_ranking_container:
268
- gr.Markdown("## Current Rankings (Most to Least Relevant)")
269
- ranked_display = gr.HTML("No rankings yet")
270
-
271
- # Function to toggle visual ranking mode
272
- def toggle_visual_mode(visible):
273
- return not visible
274
-
275
- # Function to update visual ranking display
276
- def update_visual_ranking(*rankings):
277
- # Convert to integers with error handling
278
- clean_rankings = []
279
- for r in rankings:
280
- try:
281
- if r and r.strip():
282
- clean_rankings.append(int(r))
283
- else:
284
- clean_rankings.append(None)
285
- except ValueError:
286
- clean_rankings.append(None)
287
-
288
- # Check if any rankings exist
289
- if not any(r is not None for r in clean_rankings):
290
- return "<p>No rankings assigned yet.</p>"
291
-
292
- # Create sorted order
293
- ranked_indices = []
294
- for rank in range(1, len(clean_rankings) + 1):
295
- try:
296
- idx = clean_rankings.index(rank)
297
- ranked_indices.append(idx)
298
- except ValueError:
299
- pass
300
-
301
- # Build HTML
302
- html = "<div class='visual-ranking'>"
303
- for i, idx in enumerate(ranked_indices):
304
- rank = i + 1
305
- doc_text = doc_containers[idx].value
306
-
307
- # Apply color classes based on rank
308
- rank_class = ""
309
- if rank == 1:
310
- rank_class = "visual-rank-1"
311
- elif rank == 2:
312
- rank_class = "visual-rank-2"
313
- elif rank == 3:
314
- rank_class = "visual-rank-3"
315
- elif rank <= 5:
316
- rank_class = "visual-rank-45"
317
- else:
318
- rank_class = "visual-rank-high"
319
-
320
- html += f"""
321
- <div class='visual-rank-item {rank_class}'>
322
- <div class='visual-rank-number'>{rank}</div>
323
- <div class='visual-rank-content'>{doc_text}</div>
324
- </div>
325
- """
326
-
327
- # Add unranked items if any
328
- unranked_indices = [i for i, r in enumerate(clean_rankings) if r is None]
329
- if unranked_indices:
330
- html += "<h3>Unranked Documents</h3>"
331
- for idx in unranked_indices:
332
- doc_text = doc_containers[idx].value
333
- html += f"""
334
- <div class='visual-rank-item visual-rank-unranked'>
335
- <div class='visual-rank-number'>?</div>
336
- <div class='visual-rank-content'>{doc_text}</div>
337
- </div>
338
- """
339
-
340
- html += "</div>"
341
-
342
- # Add CSS
343
- html += """
344
- <style>
345
- .visual-ranking {
346
- margin-top: 15px;
347
- }
348
- .visual-rank-item {
349
- display: flex;
350
- margin-bottom: 15px;
351
- padding: 10px;
352
- border-radius: 8px;
353
- }
354
- .visual-rank-number {
355
- font-size: 18px;
356
- font-weight: bold;
357
- margin-right: 10px;
358
- min-width: 30px;
359
- height: 30px;
360
- border-radius: 15px;
361
- background-color: #e5e7eb;
362
- display: flex;
363
- align-items: center;
364
- justify-content: center;
365
- }
366
- .visual-rank-content {
367
- flex: 1;
368
- }
369
- .visual-rank-1 {
370
- background-color: rgba(74, 222, 128, 0.2);
371
- border-left: 4px solid #4ADE80;
372
- }
373
- .visual-rank-2 {
374
- background-color: rgba(74, 222, 128, 0.15);
375
- border-left: 3px solid #4ADE80;
376
- }
377
- .visual-rank-3 {
378
- background-color: rgba(251, 191, 36, 0.15);
379
- border-left: 3px solid #FBBF24;
380
- }
381
- .visual-rank-45 {
382
- background-color: rgba(251, 191, 36, 0.1);
383
- border-left: 2px solid #FBBF24;
384
- }
385
- .visual-rank-high {
386
- background-color: rgba(239, 68, 68, 0.05);
387
- border-left: 2px solid #EF4444;
388
- }
389
- .visual-rank-unranked {
390
- background-color: #f9fafb;
391
- border: 1px dashed #d1d5db;
392
- }
393
- .visual-rank-unranked .visual-rank-number {
394
- background-color: #d1d5db;
395
- }
396
- </style>
397
- """
398
-
399
- return html
400
-
401
- # Function to reset all rankings
402
- def reset_rankings():
403
- return ["" for _ in ranking_dropdowns]
404
-
405
- # Connect events
406
- visual_mode_btn.click(
407
- toggle_visual_mode,
408
- inputs=[visual_ranking_container],
409
- outputs=[visual_ranking_container]
410
- )
411
-
412
- # Update visual ranking when any dropdown changes
413
- for dropdown in ranking_dropdowns:
414
- dropdown.change(
415
- update_visual_ranking,
416
- inputs=ranking_dropdowns,
417
- outputs=[ranked_display]
418
- )
419
-
420
- # Reset rankings button
421
- reset_rankings_btn.click(
422
- reset_rankings,
423
- outputs=ranking_dropdowns
424
- )
425
 
426
  with gr.Row():
427
  prev_btn = gr.Button("← Previous Query", size="sm", elem_id="prev-btn")
@@ -434,7 +136,7 @@ def create_reranking_interface(task_data):
434
  """Load a specific sample into the interface."""
435
  sample = next((s for s in samples if s["id"] == sample_id), None)
436
  if not sample:
437
- return [query_text.value] + [d.value for d in doc_containers] + [""] * len(ranking_dropdowns) + [current_sample_id.value, progress_text.value, status_box.value]
438
 
439
  # Update query
440
  new_query = sample["query"]
@@ -446,7 +148,7 @@ def create_reranking_interface(task_data):
446
  new_docs.append(doc)
447
 
448
  # Initialize rankings
449
- new_rankings = [""] * len(ranking_dropdowns)
450
 
451
  # Check if this sample has already been annotated
452
  existing_annotation = next((a for a in results["annotations"] if a["sample_id"] == sample_id), None)
@@ -454,7 +156,7 @@ def create_reranking_interface(task_data):
454
  # Restore previous rankings
455
  for i, rank in enumerate(existing_annotation["rankings"]):
456
  if i < len(new_rankings) and rank is not None:
457
- new_rankings[i] = str(rank)
458
 
459
  # Update progress
460
  current_idx = samples.index(sample)
@@ -497,22 +199,64 @@ def create_reranking_interface(task_data):
497
  json.dump(results, f, indent=2)
498
  return f"✅ Results saved to {output_path} ({len(results['annotations'])} annotations)"
499
 
500
- # Define a wrapper function that collects all the dropdown values into a list
501
- def save_ranking_wrapper(*args):
502
- # The last argument is the sample_id, all others are rankings
503
- rankings = args[:-1]
504
- sample_id = args[-1]
505
- return save_ranking(rankings, sample_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
506
 
507
  # Connect events
508
  submit_btn.click(
509
- save_ranking_wrapper,
510
- inputs=ranking_dropdowns + [current_sample_id],
511
  outputs=[status_box, progress_text]
512
- ).then(
513
- update_visual_ranking,
514
- inputs=ranking_dropdowns,
515
- outputs=[ranked_display]
516
  )
517
 
518
  next_btn.click(
@@ -522,11 +266,7 @@ def create_reranking_interface(task_data):
522
  ).then(
523
  load_sample,
524
  inputs=[current_sample_id],
525
- outputs=[query_text] + doc_containers + ranking_dropdowns + [current_sample_id, progress_text, status_box]
526
- ).then(
527
- update_visual_ranking,
528
- inputs=ranking_dropdowns,
529
- outputs=[ranked_display]
530
  )
531
 
532
  prev_btn.click(
@@ -536,11 +276,7 @@ def create_reranking_interface(task_data):
536
  ).then(
537
  load_sample,
538
  inputs=[current_sample_id],
539
- outputs=[query_text] + doc_containers + ranking_dropdowns + [current_sample_id, progress_text, status_box]
540
- ).then(
541
- update_visual_ranking,
542
- inputs=ranking_dropdowns,
543
- outputs=[ranked_display]
544
  )
545
 
546
  save_btn.click(save_results, outputs=[status_box])
@@ -754,9 +490,11 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
754
 
755
  # Add download options
756
  with gr.Row():
757
- download_all_btn = gr.Button("Download All Results (ZIP)")
758
- result_select = gr.Dropdown(choices=[f for f in os.listdir(".") if f.endswith("_human_results.json")], label="Select Result to Download")
759
- download_selected_btn = gr.Button("Download Selected")
 
 
760
 
761
  # Add results visualization placeholder
762
  gr.Markdown("### Results Visualization")
 
12
  def save_ranking(rankings, sample_id):
13
  """Save the current set of rankings."""
14
  try:
15
+ # Convert to integers with error handling
16
+ processed_rankings = []
17
+ for r in rankings:
18
+ if r is None or r == "":
19
+ processed_rankings.append(None)
20
+ else:
21
+ try:
22
+ processed_rankings.append(int(r))
23
+ except ValueError:
24
+ return "⚠️ Invalid ranking value. Please use only numbers.", f"Progress: {sum(completed_samples.values())}/{len(samples)}"
25
+
26
  # Check if all documents have rankings
27
+ if None in processed_rankings:
 
28
  return "⚠️ Please assign a rank to all documents before submitting", f"Progress: {sum(completed_samples.values())}/{len(samples)}"
29
 
 
 
 
 
 
 
30
  # Check for duplicate rankings
31
  if len(set(processed_rankings)) != len(processed_rankings):
32
  return "⚠️ Each document must have a unique rank. Please review your rankings.", f"Progress: {sum(completed_samples.values())}/{len(samples)}"
 
90
 
91
  gr.Markdown("## Documents to Rank:")
92
 
93
+ # Create document displays and ranking inputs in synchronized pairs
94
  doc_containers = []
95
+ ranking_inputs = []
96
 
97
  with gr.Column():
98
  for i, doc in enumerate(samples[0]["candidates"]):
99
+ with gr.Box():
100
+ gr.Markdown(f"### Document {i+1}")
101
  doc_box = gr.Textbox(
102
  value=doc,
103
+ label=None,
104
  interactive=False,
105
+ lines=4
106
  )
107
+ doc_containers.append(doc_box)
108
+
109
+ rank_input = gr.Number(
110
  value=None,
111
+ label=f"Rank (1 = highest, {len(samples[0]['candidates'])} = lowest)",
112
+ minimum=1,
113
+ maximum=len(samples[0]['candidates']),
114
+ step=1
115
  )
116
+ ranking_inputs.append(rank_input)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
 
118
+ # Add simple instructions for ranking
119
+ with gr.Accordion("Ranking Instructions", open=True):
120
  gr.Markdown("""
121
+ ### Ranking Documents:
122
+ - Enter a number from 1 to {max_rank} for each document
123
+ - 1 = most relevant document
124
+ - Higher numbers = less relevant documents
125
+ - Each document must have a unique rank
126
+ """.format(max_rank=len(samples[0]["candidates"])))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
 
128
  with gr.Row():
129
  prev_btn = gr.Button("← Previous Query", size="sm", elem_id="prev-btn")
 
136
  """Load a specific sample into the interface."""
137
  sample = next((s for s in samples if s["id"] == sample_id), None)
138
  if not sample:
139
+ return [query_text.value] + [d.value for d in doc_containers] + [None] * len(ranking_inputs) + [current_sample_id.value, progress_text.value, status_box.value]
140
 
141
  # Update query
142
  new_query = sample["query"]
 
148
  new_docs.append(doc)
149
 
150
  # Initialize rankings
151
+ new_rankings = [None] * len(ranking_inputs)
152
 
153
  # Check if this sample has already been annotated
154
  existing_annotation = next((a for a in results["annotations"] if a["sample_id"] == sample_id), None)
 
156
  # Restore previous rankings
157
  for i, rank in enumerate(existing_annotation["rankings"]):
158
  if i < len(new_rankings) and rank is not None:
159
+ new_rankings[i] = rank
160
 
161
  # Update progress
162
  current_idx = samples.index(sample)
 
199
  json.dump(results, f, indent=2)
200
  return f"✅ Results saved to {output_path} ({len(results['annotations'])} annotations)"
201
 
202
+ # Resolve rank conflicts automatically
203
+ def resolve_rank_conflicts(*ranks):
204
+ ranks = list(ranks)
205
+ # Convert to integers with validation
206
+ int_ranks = []
207
+ for r in ranks:
208
+ try:
209
+ r_int = int(r) if r is not None else None
210
+ if r_int is not None and (r_int < 1 or r_int > len(ranks)):
211
+ r_int = None
212
+ int_ranks.append(r_int)
213
+ except:
214
+ int_ranks.append(None)
215
+
216
+ # Find duplicates
217
+ seen = set()
218
+ duplicates = set()
219
+ for i, r in enumerate(int_ranks):
220
+ if r is None:
221
+ continue
222
+ if r in seen:
223
+ duplicates.add(r)
224
+ seen.add(r)
225
+
226
+ # Resolve duplicates by incrementing/shifting
227
+ for dup in sorted(duplicates):
228
+ indices = [i for i, r in enumerate(int_ranks) if r == dup]
229
+ # Keep the first occurrence, shift others
230
+ for idx in indices[1:]:
231
+ # Find the next available rank
232
+ next_rank = dup + 1
233
+ while next_rank in int_ranks and next_rank <= len(ranks):
234
+ next_rank += 1
235
+ if next_rank <= len(ranks):
236
+ int_ranks[idx] = next_rank
237
+ else:
238
+ # If no ranks available, find the first empty spot
239
+ for j in range(1, len(ranks) + 1):
240
+ if j not in int_ranks:
241
+ int_ranks[idx] = j
242
+ break
243
+
244
+ # Convert back to original type
245
+ return int_ranks
246
+
247
+ # Connect events
248
+ for i, rank_input in enumerate(ranking_inputs):
249
+ rank_input.change(
250
+ resolve_rank_conflicts,
251
+ inputs=ranking_inputs,
252
+ outputs=ranking_inputs
253
+ )
254
 
255
  # Connect events
256
  submit_btn.click(
257
+ save_ranking,
258
+ inputs=ranking_inputs + [current_sample_id],
259
  outputs=[status_box, progress_text]
 
 
 
 
260
  )
261
 
262
  next_btn.click(
 
266
  ).then(
267
  load_sample,
268
  inputs=[current_sample_id],
269
+ outputs=[query_text] + doc_containers + ranking_inputs + [current_sample_id, progress_text, status_box]
 
 
 
 
270
  )
271
 
272
  prev_btn.click(
 
276
  ).then(
277
  load_sample,
278
  inputs=[current_sample_id],
279
+ outputs=[query_text] + doc_containers + ranking_inputs + [current_sample_id, progress_text, status_box]
 
 
 
 
280
  )
281
 
282
  save_btn.click(save_results, outputs=[status_box])
 
490
 
491
  # Add download options
492
  with gr.Row():
493
+ with gr.Column():
494
+ download_all_btn = gr.Button("Download All Results (ZIP)")
495
+ with gr.Column():
496
+ result_select = gr.Dropdown(choices=[f for f in os.listdir(".") if f.endswith("_human_results.json")], label="Select Result to Download", value=None)
497
+ download_selected_btn = gr.Button("Download Selected")
498
 
499
  # Add results visualization placeholder
500
  gr.Markdown("### Results Visualization")