vargha commited on
Commit
71830dc
Β·
1 Parent(s): 63ca1b3

Resume Feature Implementation

Browse files
Files changed (1) hide show
  1. components/review_dashboard_page.py +70 -400
components/review_dashboard_page.py CHANGED
@@ -3,7 +3,7 @@
3
  import gradio as gr
4
  import datetime
5
  import sentry_sdk
6
- from sqlalchemy import orm, func
7
 
8
  from components.header import Header
9
  from utils.logger import Logger
@@ -26,10 +26,6 @@ class ReviewDashboardPage:
26
  # Review info banner
27
  with gr.Row():
28
  self.review_info = gr.Markdown("", elem_classes="review-banner")
29
-
30
- # Position info display
31
- with gr.Row():
32
- self.position_info = gr.Markdown("", elem_classes="position-info")
33
 
34
  with gr.Row():
35
  # Left Column - Review Content
@@ -72,7 +68,6 @@ class ReviewDashboardPage:
72
  min_width=120
73
  )
74
  self.btn_jump = gr.Button("Go to ID", min_width=70)
75
- self.btn_jump_to_unreviewed = gr.Button("🎯 Jump to Unreviewed", min_width=150, variant="secondary")
76
 
77
  # Right Column - Audio
78
  with gr.Column(scale=2):
@@ -105,8 +100,7 @@ class ReviewDashboardPage:
105
  # List of interactive UI elements for enabling/disabling
106
  self.interactive_ui_elements = [
107
  self.btn_prev, self.btn_next, self.btn_approve, self.btn_reject,
108
- self.btn_skip, self.btn_jump, self.jump_data_id_input, self.btn_load_voice,
109
- self.btn_jump_to_unreviewed
110
  ]
111
 
112
  def register_callbacks(self, login_page, session_state: gr.State, root_blocks: gr.Blocks):
@@ -224,19 +218,6 @@ class ReviewDashboardPage:
224
  Validation.validator_id == user_id
225
  ).count()
226
 
227
- # Find the first unreviewed item to show current position
228
- first_unreviewed_query = db.query(Annotation.id).outerjoin(
229
- Validation,
230
- (Annotation.id == Validation.annotation_id) &
231
- (Validation.validator_id == user_id)
232
- ).filter(
233
- Annotation.annotator_id == target_annotator_obj.id,
234
- Validation.id.is_(None) # No validation record exists for this reviewer
235
- ).order_by(Annotation.id).limit(1)
236
-
237
- first_unreviewed_result = first_unreviewed_query.first()
238
- current_position = "Completed" if not first_unreviewed_result else f"ID: {first_unreviewed_result.id}"
239
-
240
  if total_count > 0:
241
  percentage = (reviewed_count / total_count) * 100
242
 
@@ -268,7 +249,7 @@ class ReviewDashboardPage:
268
  # Estimate remaining items
269
  remaining = total_count - reviewed_count
270
 
271
- # Create the beautiful progress display with current position
272
  progress_html = f"""
273
  <div class="progress-container">
274
  <div class="progress-header">
@@ -286,9 +267,6 @@ class ReviewDashboardPage:
286
  πŸ“Š <code>{progress_bar}</code>
287
  <span class="remaining-items">({remaining} remaining)</span>
288
  </div>
289
- <div class="progress-position">
290
- 🎯 <strong>Current Position:</strong> {current_position}
291
- </div>
292
  </div>
293
  """
294
 
@@ -300,82 +278,18 @@ class ReviewDashboardPage:
300
  log.error(f"Error calculating review progress for user {user_id}: {e}")
301
  return f"⚠️ **Error calculating progress**"
302
 
303
- def get_current_item_position_info(items, current_idx, session):
304
- """Get information about the current item's position in the review queue"""
305
- if not items or current_idx >= len(items):
306
- return ""
307
-
308
- user_id = session.get("user_id")
309
- username = session.get("username")
310
- if not user_id or not username:
311
- return ""
312
-
313
- # Find target annotator
314
- target_annotator = None
315
- for annotator_name, reviewer_name in conf.REVIEW_MAPPING.items():
316
- if reviewer_name == username:
317
- target_annotator = annotator_name
318
- break
319
-
320
- if not target_annotator:
321
- return ""
322
-
323
- with get_db() as db:
324
- try:
325
- # Get target annotator's ID
326
- target_annotator_obj = db.query(Annotator).filter_by(name=target_annotator).first()
327
- if not target_annotator_obj:
328
- return ""
329
-
330
- # Get total count
331
- total_count = db.query(Annotation).filter(
332
- Annotation.annotator_id == target_annotator_obj.id
333
- ).count()
334
-
335
- # Get count of reviewed items for this reviewer
336
- reviewed_count = db.query(Validation).join(
337
- Annotation, Validation.annotation_id == Annotation.id
338
- ).filter(
339
- Validation.validator_id == user_id,
340
- Annotation.annotator_id == target_annotator_obj.id
341
- ).count()
342
-
343
- # Get current item's position in the overall sequence
344
- current_item_id = items[current_idx]["annotation_id"]
345
-
346
- # Calculate position based on the actual review sequence
347
- # First, get all annotations for this target annotator ordered by ID
348
- all_annotations = db.query(Annotation.id).filter(
349
- Annotation.annotator_id == target_annotator_obj.id
350
- ).order_by(Annotation.id).all()
351
-
352
- # Create a list of annotation IDs
353
- all_annotation_ids = [ann.id for ann in all_annotations]
354
-
355
- # Find the position of current item in the overall sequence
356
- try:
357
- current_position = all_annotation_ids.index(current_item_id) + 1
358
- except ValueError:
359
- current_position = 0
360
-
361
- return f"πŸ“ **Position:** {current_position} of {total_count} items"
362
-
363
- except Exception as e:
364
- log.error(f"Error getting current item position: {e}")
365
- return ""
366
-
367
  def load_review_items_fn(session):
368
  user_id = session.get("user_id")
369
  username = session.get("username")
370
 
371
  if not user_id or not username:
372
  log.warning("load_review_items_fn: user not found in session")
373
- return [], 0, "", "", "", "", "", "", "", "", gr.update(value=None, autoplay=False), gr.update(visible=False, value=""), False, gr.update(value="❌ Reject"), ""
374
 
375
  # Check if user is in Phase 2 (should be a reviewer)
376
  if username not in conf.REVIEW_MAPPING.values():
377
  log.warning(f"User {username} is not assigned as a reviewer")
378
- return [], 0, "", "", "", "", "", "", "", "", gr.update(value=None, autoplay=False), gr.update(visible=False, value=""), False, gr.update(value="❌ Reject"), ""
379
 
380
  # Find which annotator this user should review
381
  target_annotator = None
@@ -386,135 +300,82 @@ class ReviewDashboardPage:
386
 
387
  if not target_annotator:
388
  log.warning(f"No target annotator found for reviewer {username}")
389
- return [], 0, "", "", "", "", "", "", "", "", gr.update(value=None, autoplay=False), gr.update(visible=False, value=""), False, gr.update(value="❌ Reject"), ""
390
 
391
- # Load annotations from target annotator with FAST INITIAL LOADING
392
  with get_db() as db:
393
  # Get target annotator's ID
394
  target_annotator_obj = db.query(Annotator).filter_by(name=target_annotator).first()
395
  if not target_annotator_obj:
396
  log.error(f"Target annotator {target_annotator} not found in database")
397
- return [], 0, f"Review Target Error: Annotator '{target_annotator}' not found.", "", "", "", "", "", "", "", gr.update(value=None, autoplay=False), gr.update(visible=False, value=""), False, gr.update(value="❌ Reject"), ""
398
 
399
  log.info(f"Found target annotator with ID: {target_annotator_obj.id}")
400
 
401
- # Get total count for progress info (this is fast)
402
- total_count = db.query(Annotation).filter(
 
 
 
 
 
 
 
403
  Annotation.annotator_id == target_annotator_obj.id
404
- ).count()
405
 
406
- # RESUME LOGIC: Find the first unreviewed item by this reviewer
407
- # Query to find the first unreviewed annotation for this reviewer
408
- first_unreviewed_query = db.query(
409
- Annotation.id,
410
- Annotation.tts_data_id,
411
- Annotation.annotated_sentence,
412
- Annotation.annotated_at
413
- ).outerjoin(
414
- Validation,
415
- (Annotation.id == Validation.annotation_id) &
416
- (Validation.validator_id == user_id)
417
- ).filter(
418
- Annotation.annotator_id == target_annotator_obj.id,
419
- Validation.id.is_(None) # No validation record exists for this reviewer
420
- ).order_by(Annotation.id).limit(1)
421
 
422
- first_unreviewed_result = first_unreviewed_query.first()
423
-
424
- if first_unreviewed_result:
425
- # Found unreviewed item - start from there
426
- first_unreviewed_id = first_unreviewed_result.id
427
- log.info(f"Found first unreviewed item with ID: {first_unreviewed_id}")
428
-
429
- # Load items starting from the unreviewed item
430
- # Load a batch around the unreviewed item for context
431
- BATCH_SIZE = 10
432
- offset = max(0, first_unreviewed_id - 5) # Start 5 items before the unreviewed item
433
-
434
- # Query to get items around the unreviewed item
435
- items_query = db.query(
436
- Annotation,
437
- TTSData.filename,
438
- TTSData.sentence
439
- ).join(
440
- TTSData, Annotation.tts_data_id == TTSData.id
441
- ).filter(
442
- Annotation.annotator_id == target_annotator_obj.id,
443
- Annotation.id >= offset
444
- ).order_by(Annotation.id).limit(BATCH_SIZE)
445
-
446
- initial_results = items_query.all()
447
-
448
- # Find the index of the first unreviewed item in our loaded batch
449
- initial_idx = 0
450
- for i, (annotation, filename, sentence) in enumerate(initial_results):
451
- if annotation.id == first_unreviewed_id:
452
- initial_idx = i
453
- break
454
-
455
- log.info(f"Resuming at first unreviewed item, index: {initial_idx} (ID: {first_unreviewed_id})")
456
- else:
457
- # No unreviewed items found - all items have been reviewed
458
- log.info(f"All items for {target_annotator} have been reviewed by {username}")
459
-
460
- # Load the last batch of items to show completion
461
- BATCH_SIZE = 10
462
- items_query = db.query(
463
- Annotation,
464
- TTSData.filename,
465
- TTSData.sentence
466
- ).join(
467
- TTSData, Annotation.tts_data_id == TTSData.id
468
- ).filter(
469
- Annotation.annotator_id == target_annotator_obj.id
470
- ).order_by(Annotation.id.desc()).limit(BATCH_SIZE)
471
-
472
- # Reverse the results to maintain chronological order
473
- initial_results = list(reversed(items_query.all()))
474
- initial_idx = len(initial_results) - 1 if initial_results else 0
475
-
476
- log.info(f"All items reviewed, starting at last item, index: {initial_idx}")
477
-
478
- # Set completion message
479
- if initial_results:
480
- review_info_text = f"πŸŽ‰ **Phase 2 Review Mode** - All annotations for {target_annotator} have been reviewed! Showing last {len(initial_results)} items for reference."
481
- else:
482
- review_info_text = f"πŸŽ‰ **Phase 2 Review Mode** - All annotations for {target_annotator} have been reviewed! No items to display."
483
-
484
- return (
485
- [],
486
- 0,
487
- review_info_text,
488
- "", "", "", "", "", "", "", gr.update(value=None, autoplay=False), gr.update(visible=False, value=""), False, gr.update(value="❌ Reject"), ""
489
- )
490
 
491
- # Process items with minimal data - validation status will be loaded on-demand
492
  items = []
493
- for annotation, filename, sentence in initial_results:
494
- # Check if annotation is deleted (minimal processing)
495
  is_deleted = not annotation.annotated_sentence or annotation.annotated_sentence.strip() == ""
496
  annotated_sentence_display = "[DELETED ANNOTATION]" if is_deleted else annotation.annotated_sentence
497
 
 
 
 
498
  items.append({
499
  "annotation_id": annotation.id,
500
  "tts_id": annotation.tts_data_id,
501
  "filename": filename,
502
  "sentence": sentence,
503
  "annotated_sentence": annotated_sentence_display,
504
- "is_deleted": is_deleted,
505
  "annotated_at": annotation.annotated_at.isoformat() if annotation.annotated_at else "",
506
- "validation_status": "Loading...", # Will be loaded on-demand
507
- "validation_loaded": False # Track if validation status has been loaded
508
  })
509
 
510
- log.info(f"Fast initial load: {len(items)} annotations out of {total_count} total for target annotator ID {target_annotator_obj.id}")
511
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
512
  # Set initial display
513
  if items:
514
  initial_item = items[initial_idx]
515
- review_info_text = f"πŸ” **Phase 2 Review Mode** - Reviewing assigned annotations. Loaded {len(items)} of {total_count} total items. Resuming from ID: {initial_item['annotation_id']}"
516
-
517
  # Ensure correct order of return values for 14 outputs
 
518
  rejection_reason_val = ""
519
  rejection_visible_val = False
520
  if initial_item["validation_status"].startswith("Rejected"):
@@ -538,42 +399,27 @@ class ReviewDashboardPage:
538
  gr.update(value=None, autoplay=False), # audio_update
539
  gr.update(visible=rejection_visible_val, value=rejection_reason_val), # rejection_reason_input update
540
  False, # Reset rejection mode
541
- gr.update(value="❌ Reject"), # Reset reject button
542
- get_current_item_position_info(items, initial_idx, session) # Position info
543
  )
544
  else:
545
- # Ensure correct order and number of return values for empty items (15 outputs)
546
- return [], 0, f"πŸ” **Phase 2 Review Mode** - No annotations found for review.", "", "", "", "", "", "", "", gr.update(value=None, autoplay=False), gr.update(visible=False, value=""), False, gr.update(value="❌ Reject"), ""
 
 
 
 
 
 
 
547
 
548
  def show_current_review_item_fn(items, idx, session):
549
  if not items or idx >= len(items) or idx < 0:
550
- # tts_id, filename, sentence, ann_sentence, annotated_at, validation_status, annotator_name_placeholder, audio_update, rejection_reason_update, rejection_mode_reset, btn_reject_update, position_info
551
- return "", "", "", "", "", "", "", gr.update(value=None, autoplay=False), gr.update(visible=False, value=""), False, gr.update(value="❌ Reject"), ""
552
 
553
  current_item = items[idx]
554
 
555
- # Load validation status on-demand if not already loaded
556
- if not current_item.get("validation_loaded", False):
557
- user_id = session.get("user_id")
558
- if user_id:
559
- with get_db() as db:
560
- try:
561
- # Get the full annotation object for validation processing
562
- annotation_obj = db.query(Annotation).filter_by(id=current_item["annotation_id"]).first()
563
- if annotation_obj:
564
- validation_status, is_deleted = get_validation_status_for_item(db, current_item["annotation_id"], user_id, annotation_obj)
565
- current_item["validation_status"] = validation_status
566
- current_item["is_deleted"] = is_deleted
567
- current_item["validation_loaded"] = True
568
-
569
- # Update displayed annotation if deleted
570
- if is_deleted:
571
- current_item["annotated_sentence"] = "[DELETED ANNOTATION]"
572
-
573
- log.info(f"Loaded validation status for item {idx}: {validation_status}")
574
- except Exception as e:
575
- log.error(f"Error loading validation status for item {idx}: {e}")
576
- current_item["validation_status"] = "Error loading status"
577
 
578
  rejection_reason = ""
579
  rejection_visible = False
@@ -589,9 +435,6 @@ class ReviewDashboardPage:
589
  rejection_reason = current_item["validation_status"][start_paren+1:end_paren]
590
  rejection_visible = True
591
 
592
- # Get position info
593
- position_info = get_current_item_position_info(items, idx, session)
594
-
595
  return (
596
  str(current_item["tts_id"]),
597
  current_item["filename"],
@@ -603,37 +446,25 @@ class ReviewDashboardPage:
603
  gr.update(value=None, autoplay=False),
604
  gr.update(visible=rejection_visible, value=rejection_reason),
605
  False, # Reset rejection mode
606
- gr.update(value="❌ Reject"), # Reset reject button text
607
- position_info # Position info
608
  )
609
 
610
  def update_review_info_fn(items, total_count):
611
- """Update the review info banner with current loaded items count and position info"""
612
  if items:
613
- current_item_id = items[0]["annotation_id"] if items else "N/A"
614
- return f"πŸ” **Phase 2 Review Mode** - Reviewing assigned annotations. Loaded {len(items)} of {total_count} total items. Starting from ID: {current_item_id}"
615
  else:
616
  return f"πŸ” **Phase 2 Review Mode** - No annotations found for review."
617
 
618
  def navigate_and_load_fn(items, current_idx, direction, session):
619
- """Combined navigation and loading function"""
620
  if not items:
621
  return items, 0, ""
622
 
623
  # Navigate
624
  if direction == "next":
625
  new_idx = min(current_idx + 1, len(items) - 1)
626
- # Only load more items when user reaches the LAST item of a batch
627
- should_load_more = (new_idx == len(items) - 1 and len(items) % 5 == 0)
628
- if should_load_more:
629
- log.info(f"User reached end of loaded items ({new_idx}/{len(items)}), will load more items")
630
- # Load more items
631
- updated_items, total_count = load_more_items_fn(items, session, current_batch_size=10)
632
- # Update review info with new count
633
- review_info = update_review_info_fn(updated_items, total_count)
634
- return updated_items, new_idx, review_info
635
- else:
636
- return items, new_idx, "" # No review info update needed
637
  else: # prev
638
  new_idx = max(current_idx - 1, 0)
639
  return items, new_idx, "" # No review info update needed
@@ -747,145 +578,7 @@ class ReviewDashboardPage:
747
  # gr.Warning(f"Invalid Data ID format: {target_data_id}")
748
  return current_idx
749
 
750
- def load_more_items_fn(items, session, current_batch_size=10):
751
- """Load more items when user needs them (pagination support)"""
752
- user_id = session.get("user_id")
753
- username = session.get("username")
754
-
755
- if not user_id or not username:
756
- return items, 0 # Return existing items if no user session
757
-
758
- # Find target annotator
759
- target_annotator = None
760
- for annotator_name, reviewer_name in conf.REVIEW_MAPPING.items():
761
- if reviewer_name == username:
762
- target_annotator = annotator_name
763
- break
764
-
765
- if not target_annotator:
766
- return items, 0
767
-
768
- with get_db() as db:
769
- target_annotator_obj = db.query(Annotator).filter_by(name=target_annotator).first()
770
- if not target_annotator_obj:
771
- return items, 0
772
-
773
- # Get total count for updated review info
774
- total_count = db.query(Annotation).filter(
775
- Annotation.annotator_id == target_annotator_obj.id
776
- ).count()
777
-
778
- # Load next batch starting from where we left off
779
- if items:
780
- # Get the last loaded annotation ID to continue from there
781
- last_loaded_id = items[-1]["annotation_id"]
782
-
783
- # Load next batch starting after the last loaded item
784
- query = db.query(
785
- Annotation,
786
- TTSData.filename,
787
- TTSData.sentence
788
- ).join(
789
- TTSData, Annotation.tts_data_id == TTSData.id
790
- ).filter(
791
- Annotation.annotator_id == target_annotator_obj.id,
792
- Annotation.id > last_loaded_id # Continue from where we left off
793
- ).order_by(Annotation.id).limit(current_batch_size)
794
- else:
795
- # No items loaded yet, start from the beginning
796
- query = db.query(
797
- Annotation,
798
- TTSData.filename,
799
- TTSData.sentence
800
- ).join(
801
- TTSData, Annotation.tts_data_id == TTSData.id
802
- ).filter(
803
- Annotation.annotator_id == target_annotator_obj.id
804
- ).order_by(Annotation.id).limit(current_batch_size)
805
-
806
- results = query.all()
807
-
808
- # Process new items with minimal data - validation status loaded on-demand
809
- new_items = []
810
- for annotation, filename, sentence in results:
811
- # Check if annotation is deleted (minimal processing)
812
- is_deleted = not annotation.annotated_sentence or annotation.annotated_sentence.strip() == ""
813
- annotated_sentence_display = "[DELETED ANNOTATION]" if is_deleted else annotation.annotated_sentence
814
-
815
- new_items.append({
816
- "annotation_id": annotation.id,
817
- "tts_id": annotation.tts_data_id,
818
- "filename": filename,
819
- "sentence": sentence,
820
- "annotated_sentence": annotated_sentence_display,
821
- "is_deleted": is_deleted,
822
- "annotated_at": annotation.annotated_at.isoformat() if annotation.annotated_at else "",
823
- "validation_status": "Loading...", # Will be loaded on-demand
824
- "validation_loaded": False # Track if validation status has been loaded
825
- })
826
-
827
- # Combine with existing items
828
- all_items = items + new_items
829
- log.info(f"Loaded {len(new_items)} more items, total now: {len(all_items)}")
830
- return all_items, total_count
831
-
832
- def _jump_to_next_unreviewed_fn(items, session):
833
- """Helper function to jump to the next unreviewed item in the review queue."""
834
- user_id = session.get("user_id")
835
- username = session.get("username")
836
- if not user_id or not username:
837
- gr.Error("User not logged in")
838
- return items, 0, ""
839
-
840
- # Find target annotator
841
- target_annotator = None
842
- for annotator_name, reviewer_name in conf.REVIEW_MAPPING.items():
843
- if reviewer_name == username:
844
- target_annotator = annotator_name
845
- break
846
-
847
- if not target_annotator:
848
- gr.Error("User is not assigned as a reviewer")
849
- return items, 0, ""
850
-
851
- with get_db() as db:
852
- try:
853
- # Get target annotator's ID
854
- target_annotator_obj = db.query(Annotator).filter_by(name=target_annotator).first()
855
- if not target_annotator_obj:
856
- return items, 0, f"⚠️ **Error:** Annotator '{target_annotator}' not found"
857
 
858
- # Find the first unreviewed annotation for this reviewer
859
- first_unreviewed_query = db.query(Annotation.id).outerjoin(
860
- Validation,
861
- (Annotation.id == Validation.annotation_id) &
862
- (Validation.validator_id == user_id)
863
- ).filter(
864
- Annotation.annotator_id == target_annotator_obj.id,
865
- Validation.id.is_(None) # No validation record exists for this reviewer
866
- ).order_by(Annotation.id).limit(1)
867
-
868
- first_unreviewed_result = first_unreviewed_query.first()
869
-
870
- if first_unreviewed_result:
871
- target_id = first_unreviewed_result.id
872
- log.info(f"Jumping to next unreviewed item with ID: {target_id}")
873
-
874
- # Find this item in the current loaded items
875
- for i, item in enumerate(items):
876
- if item["annotation_id"] == target_id:
877
- return items, i, f"🎯 **Jumped to unreviewed item ID: {target_id}**"
878
-
879
- # If not found in current items, we need to load more items
880
- # For now, just show a message
881
- return items, 0, f"🎯 **Next unreviewed item ID: {target_id}** - Use navigation to reach it"
882
- else:
883
- log.info(f"No unreviewed items found for user {user_id}. Review queue is empty.")
884
- return items, 0, "πŸŽ‰ **Phase 2 Review Mode** - All annotations for your assigned annotators have been reviewed! Your review queue is empty."
885
- except Exception as e:
886
- log.error(f"Error jumping to next unreviewed item: {e}")
887
- sentry_sdk.capture_exception(e)
888
- return items, 0, f"⚠️ **Error:** Could not find next unreviewed item. Error: {e}"
889
 
890
  # Output definitions
891
  review_display_outputs = [
@@ -896,8 +589,7 @@ class ReviewDashboardPage:
896
  self.audio,
897
  self.rejection_reason_input, # Added rejection reason input to display outputs
898
  self.rejection_mode_active, # Added rejection mode state
899
- self.btn_reject, # Added reject button to display outputs
900
- self.position_info # Added position info to display outputs
901
  ]
902
 
903
  # Trigger data loading when load_trigger changes (after successful login for a reviewer)
@@ -1049,28 +741,6 @@ class ReviewDashboardPage:
1049
  outputs=self.jump_data_id_input
1050
  )
1051
 
1052
- # Jump to Unreviewed button
1053
- self.btn_jump_to_unreviewed.click(
1054
- fn=lambda: update_ui_interactive_state(False),
1055
- outputs=self.interactive_ui_elements
1056
- ).then(
1057
- fn=lambda items, session: _jump_to_next_unreviewed_fn(items, session),
1058
- inputs=[self.items_state, session_state],
1059
- outputs=[self.items_state, self.idx_state, self.review_info]
1060
- ).then(
1061
- fn=show_current_review_item_fn,
1062
- inputs=[self.items_state, self.idx_state, session_state],
1063
- outputs=review_display_outputs
1064
- ).then(
1065
- # Auto-load audio with autoplay after jumping
1066
- fn=download_voice_fn,
1067
- inputs=[self.filename],
1068
- outputs=[self.audio, self.original_audio_state, self.audio]
1069
- ).then(
1070
- fn=lambda: update_ui_interactive_state(True),
1071
- outputs=self.interactive_ui_elements
1072
- )
1073
-
1074
  # Load audio button
1075
  self.btn_load_voice.click(
1076
  fn=lambda: update_ui_interactive_state(False),
 
3
  import gradio as gr
4
  import datetime
5
  import sentry_sdk
6
+ from sqlalchemy import orm
7
 
8
  from components.header import Header
9
  from utils.logger import Logger
 
26
  # Review info banner
27
  with gr.Row():
28
  self.review_info = gr.Markdown("", elem_classes="review-banner")
 
 
 
 
29
 
30
  with gr.Row():
31
  # Left Column - Review Content
 
68
  min_width=120
69
  )
70
  self.btn_jump = gr.Button("Go to ID", min_width=70)
 
71
 
72
  # Right Column - Audio
73
  with gr.Column(scale=2):
 
100
  # List of interactive UI elements for enabling/disabling
101
  self.interactive_ui_elements = [
102
  self.btn_prev, self.btn_next, self.btn_approve, self.btn_reject,
103
+ self.btn_skip, self.btn_jump, self.jump_data_id_input, self.btn_load_voice
 
104
  ]
105
 
106
  def register_callbacks(self, login_page, session_state: gr.State, root_blocks: gr.Blocks):
 
218
  Validation.validator_id == user_id
219
  ).count()
220
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
  if total_count > 0:
222
  percentage = (reviewed_count / total_count) * 100
223
 
 
249
  # Estimate remaining items
250
  remaining = total_count - reviewed_count
251
 
252
+ # Create the beautiful progress display
253
  progress_html = f"""
254
  <div class="progress-container">
255
  <div class="progress-header">
 
267
  πŸ“Š <code>{progress_bar}</code>
268
  <span class="remaining-items">({remaining} remaining)</span>
269
  </div>
 
 
 
270
  </div>
271
  """
272
 
 
278
  log.error(f"Error calculating review progress for user {user_id}: {e}")
279
  return f"⚠️ **Error calculating progress**"
280
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
281
  def load_review_items_fn(session):
282
  user_id = session.get("user_id")
283
  username = session.get("username")
284
 
285
  if not user_id or not username:
286
  log.warning("load_review_items_fn: user not found in session")
287
+ return [], 0, "", "", "", "", "", "", "", "", gr.update(value=None, autoplay=False), gr.update(visible=False, value=""), False, gr.update(value="❌ Reject")
288
 
289
  # Check if user is in Phase 2 (should be a reviewer)
290
  if username not in conf.REVIEW_MAPPING.values():
291
  log.warning(f"User {username} is not assigned as a reviewer")
292
+ return [], 0, "", "", "", "", "", "", "", "", gr.update(value=None, autoplay=False), gr.update(visible=False, value=""), False, gr.update(value="❌ Reject")
293
 
294
  # Find which annotator this user should review
295
  target_annotator = None
 
300
 
301
  if not target_annotator:
302
  log.warning(f"No target annotator found for reviewer {username}")
303
+ return [], 0, "", "", "", "", "", "", "", "", gr.update(value=None, autoplay=False), gr.update(visible=False, value=""), False, gr.update(value="❌ Reject")
304
 
305
+ # Load ALL annotations from target annotator to properly implement "continue from where left off"
306
  with get_db() as db:
307
  # Get target annotator's ID
308
  target_annotator_obj = db.query(Annotator).filter_by(name=target_annotator).first()
309
  if not target_annotator_obj:
310
  log.error(f"Target annotator {target_annotator} not found in database")
311
+ return [], 0, f"Review Target Error: Annotator '{target_annotator}' not found.", "", "", "", "", "", "", "", gr.update(value=None, autoplay=False), gr.update(visible=False, value=""), False, gr.update(value="❌ Reject")
312
 
313
  log.info(f"Found target annotator with ID: {target_annotator_obj.id}")
314
 
315
+ # Load ALL annotations to properly implement resume functionality
316
+ # This is similar to phase 1 dashboard approach
317
+ all_annotations_query = db.query(
318
+ Annotation,
319
+ TTSData.filename,
320
+ TTSData.sentence
321
+ ).join(
322
+ TTSData, Annotation.tts_data_id == TTSData.id
323
+ ).filter(
324
  Annotation.annotator_id == target_annotator_obj.id
325
+ ).order_by(Annotation.id)
326
 
327
+ all_results = all_annotations_query.all()
328
+ total_count = len(all_results)
 
 
 
 
 
 
 
 
 
 
 
 
 
329
 
330
+ log.info(f"Loaded {total_count} annotations for target annotator ID {target_annotator_obj.id}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
331
 
332
+ # Process all items and determine validation status immediately
333
  items = []
334
+ for annotation, filename, sentence in all_results:
335
+ # Check if annotation is deleted
336
  is_deleted = not annotation.annotated_sentence or annotation.annotated_sentence.strip() == ""
337
  annotated_sentence_display = "[DELETED ANNOTATION]" if is_deleted else annotation.annotated_sentence
338
 
339
+ # Get validation status immediately (not on-demand)
340
+ validation_status, is_deleted_updated = get_validation_status_for_item(db, annotation.id, user_id, annotation)
341
+
342
  items.append({
343
  "annotation_id": annotation.id,
344
  "tts_id": annotation.tts_data_id,
345
  "filename": filename,
346
  "sentence": sentence,
347
  "annotated_sentence": annotated_sentence_display,
348
+ "is_deleted": is_deleted_updated,
349
  "annotated_at": annotation.annotated_at.isoformat() if annotation.annotated_at else "",
350
+ "validation_status": validation_status,
351
+ "validation_loaded": True # Already loaded
352
  })
353
 
354
+ # --- Resume Logic: Find first unreviewed item (similar to phase 1 dashboard) ---
355
+ initial_idx = 0
356
+ if items:
357
+ first_unreviewed_idx = -1
358
+ for i, item_data in enumerate(items):
359
+ if item_data["validation_status"] == "Not Reviewed":
360
+ first_unreviewed_idx = i
361
+ break
362
+
363
+ if first_unreviewed_idx != -1:
364
+ initial_idx = first_unreviewed_idx
365
+ log.info(f"Resuming at first unreviewed item, index: {initial_idx} (ID: {items[initial_idx]['tts_id']})")
366
+ else: # All items are reviewed
367
+ initial_idx = len(items) - 1
368
+ log.info(f"All items reviewed, starting at last item, index: {initial_idx} (ID: {items[initial_idx]['tts_id']})")
369
+ else: # No items assigned
370
+ initial_idx = 0
371
+ log.info("No items assigned to user, starting at index 0.")
372
+
373
  # Set initial display
374
  if items:
375
  initial_item = items[initial_idx]
376
+ review_info_text = f"πŸ” **Phase 2 Review Mode** - Reviewing assigned annotations. Loaded {len(items)} items."
 
377
  # Ensure correct order of return values for 14 outputs
378
+ # items, idx, review_info, tts_id, filename, sentence, ann_sentence, annotated_at, validation_status, annotator_placeholder, audio_update, rejection_reason_update, rejection_mode_reset, btn_reject_update
379
  rejection_reason_val = ""
380
  rejection_visible_val = False
381
  if initial_item["validation_status"].startswith("Rejected"):
 
399
  gr.update(value=None, autoplay=False), # audio_update
400
  gr.update(visible=rejection_visible_val, value=rejection_reason_val), # rejection_reason_input update
401
  False, # Reset rejection mode
402
+ gr.update(value="❌ Reject") # Reset reject button
 
403
  )
404
  else:
405
+ # Ensure correct order and number of return values for empty items (14 outputs)
406
+ return [], 0, f"πŸ” **Phase 2 Review Mode** - No annotations found for review.", "", "", "", "", "", "", "", gr.update(value=None, autoplay=False), gr.update(visible=False, value=""), False, gr.update(value="❌ Reject")
407
+
408
+ # except Exception as e:
409
+ # log.error(f"Error loading review items: {e}")
410
+ # sentry_sdk.capture_exception(e)
411
+ # gr.Error(f"Failed to load review data: {e}")
412
+ # # Ensure correct order and number of return values for error case (14 outputs)
413
+ # return [], 0, "", "", "", "", "", "", "", "", gr.update(value=None, autoplay=False), gr.update(visible=False, value=""), False, gr.update(value="❌ Reject")
414
 
415
  def show_current_review_item_fn(items, idx, session):
416
  if not items or idx >= len(items) or idx < 0:
417
+ # tts_id, filename, sentence, ann_sentence, annotated_at, validation_status, annotator_name_placeholder, audio_update, rejection_reason_update, rejection_mode_reset, btn_reject_update
418
+ return "", "", "", "", "", "", "", gr.update(value=None, autoplay=False), gr.update(visible=False, value=""), False, gr.update(value="❌ Reject")
419
 
420
  current_item = items[idx]
421
 
422
+ # Validation status is already loaded, no need for on-demand loading
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
423
 
424
  rejection_reason = ""
425
  rejection_visible = False
 
435
  rejection_reason = current_item["validation_status"][start_paren+1:end_paren]
436
  rejection_visible = True
437
 
 
 
 
438
  return (
439
  str(current_item["tts_id"]),
440
  current_item["filename"],
 
446
  gr.update(value=None, autoplay=False),
447
  gr.update(visible=rejection_visible, value=rejection_reason),
448
  False, # Reset rejection mode
449
+ gr.update(value="❌ Reject") # Reset reject button text
 
450
  )
451
 
452
  def update_review_info_fn(items, total_count):
453
+ """Update the review info banner with current loaded items count"""
454
  if items:
455
+ return f"πŸ” **Phase 2 Review Mode** - Reviewing assigned annotations. Loaded {len(items)} items."
 
456
  else:
457
  return f"πŸ” **Phase 2 Review Mode** - No annotations found for review."
458
 
459
  def navigate_and_load_fn(items, current_idx, direction, session):
460
+ """Simple navigation function - all items are already loaded"""
461
  if not items:
462
  return items, 0, ""
463
 
464
  # Navigate
465
  if direction == "next":
466
  new_idx = min(current_idx + 1, len(items) - 1)
467
+ return items, new_idx, "" # No review info update needed
 
 
 
 
 
 
 
 
 
 
468
  else: # prev
469
  new_idx = max(current_idx - 1, 0)
470
  return items, new_idx, "" # No review info update needed
 
578
  # gr.Warning(f"Invalid Data ID format: {target_data_id}")
579
  return current_idx
580
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
581
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
582
 
583
  # Output definitions
584
  review_display_outputs = [
 
589
  self.audio,
590
  self.rejection_reason_input, # Added rejection reason input to display outputs
591
  self.rejection_mode_active, # Added rejection mode state
592
+ self.btn_reject # Added reject button to display outputs
 
593
  ]
594
 
595
  # Trigger data loading when load_trigger changes (after successful login for a reviewer)
 
741
  outputs=self.jump_data_id_input
742
  )
743
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
744
  # Load audio button
745
  self.btn_load_voice.click(
746
  fn=lambda: update_ui_interactive_state(False),