Spaces:
Running
Running
Resume Feature Implementation
Browse files- components/review_dashboard_page.py +166 -36
components/review_dashboard_page.py
CHANGED
@@ -171,8 +171,13 @@ class ReviewDashboardPage:
|
|
171 |
validation_status += f" ({validation.description})"
|
172 |
|
173 |
# For deleted annotations, show special status
|
174 |
-
if is_deleted
|
175 |
-
validation_status
|
|
|
|
|
|
|
|
|
|
|
176 |
|
177 |
return validation_status, is_deleted
|
178 |
|
@@ -355,48 +360,93 @@ class ReviewDashboardPage:
|
|
355 |
"validation_loaded": False # Track if validation status has been loaded
|
356 |
})
|
357 |
|
358 |
-
#
|
359 |
initial_idx = 0
|
360 |
if items:
|
|
|
|
|
361 |
found_unreviewed = False
|
362 |
-
|
363 |
-
# Check database for validation status to find first unreviewed item
|
364 |
for i, item_data in enumerate(items):
|
365 |
-
|
366 |
-
|
367 |
-
|
368 |
-
validator_id=user_id
|
369 |
-
).first()
|
370 |
-
|
371 |
-
# If no validation exists, this is unreviewed
|
372 |
-
if not existing_validation:
|
373 |
-
# Prioritize non-deleted annotations
|
374 |
-
if not item_data.get("is_deleted", False):
|
375 |
-
initial_idx = i
|
376 |
-
found_unreviewed = True
|
377 |
-
log.info(f"Found first unreviewed non-deleted item at index {i} (annotation_id: {item_data['annotation_id']})")
|
378 |
-
break
|
379 |
-
|
380 |
-
# If no unreviewed non-deleted items found, look for any unreviewed items (including deleted)
|
381 |
-
if not found_unreviewed:
|
382 |
-
for i, item_data in enumerate(items):
|
383 |
-
existing_validation = db.query(Validation).filter_by(
|
384 |
annotation_id=item_data["annotation_id"],
|
385 |
validator_id=user_id
|
386 |
).first()
|
387 |
|
388 |
-
if not
|
|
|
389 |
initial_idx = i
|
390 |
found_unreviewed = True
|
391 |
-
log.info(f"
|
392 |
break
|
|
|
|
|
|
|
|
|
393 |
|
394 |
-
# If
|
|
|
395 |
if not found_unreviewed:
|
396 |
-
|
397 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
398 |
|
399 |
-
log.info(f"
|
400 |
|
401 |
# Set initial display
|
402 |
if items:
|
@@ -476,6 +526,7 @@ class ReviewDashboardPage:
|
|
476 |
# Check if this is a deleted annotation
|
477 |
is_deleted = current_item.get("is_deleted", False)
|
478 |
|
|
|
479 |
if current_item["validation_status"].startswith("Rejected"):
|
480 |
# Extract reason from status like "Rejected (reason)" or just use empty if no parenthesis
|
481 |
start_paren = current_item["validation_status"].find("(")
|
@@ -483,6 +534,14 @@ class ReviewDashboardPage:
|
|
483 |
if start_paren != -1 and end_paren != -1:
|
484 |
rejection_reason = current_item["validation_status"][start_paren+1:end_paren]
|
485 |
rejection_visible = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
486 |
|
487 |
return (
|
488 |
str(current_item["tts_id"]),
|
@@ -505,6 +564,68 @@ class ReviewDashboardPage:
|
|
505 |
else:
|
506 |
return f"���� **Phase 2 Review Mode** - No annotations found for review."
|
507 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
508 |
def navigate_and_load_fn(items, current_idx, direction, session):
|
509 |
"""Combined navigation and loading function"""
|
510 |
if not items:
|
@@ -703,6 +824,15 @@ class ReviewDashboardPage:
|
|
703 |
# Combine with existing items
|
704 |
all_items = items + new_items
|
705 |
log.info(f"Loaded {len(new_items)} more items, total now: {len(all_items)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
706 |
return all_items, total_count
|
707 |
|
708 |
# Output definitions
|
@@ -782,9 +912,9 @@ class ReviewDashboardPage:
|
|
782 |
fn=lambda: gr.update(value="❌ Reject"), # Reset reject button
|
783 |
outputs=[self.btn_reject]
|
784 |
).then(
|
785 |
-
fn=lambda items, idx, session:
|
786 |
inputs=[self.items_state, self.idx_state, session_state],
|
787 |
-
outputs=[self.
|
788 |
).then(
|
789 |
fn=show_current_review_item_fn,
|
790 |
inputs=[self.items_state, self.idx_state, session_state],
|
@@ -805,9 +935,9 @@ class ReviewDashboardPage:
|
|
805 |
inputs=[self.items_state, self.idx_state, session_state, self.rejection_mode_active],
|
806 |
outputs=[self.header.progress_display]
|
807 |
).then(
|
808 |
-
fn=lambda items, idx, session, rejection_mode:
|
809 |
inputs=[self.items_state, self.idx_state, session_state, self.rejection_mode_active],
|
810 |
-
outputs=[self.
|
811 |
).then(
|
812 |
fn=lambda items, idx, session, rejection_mode: show_current_review_item_fn(items, idx, session) if not rejection_mode else (
|
813 |
str(items[idx]["tts_id"]) if items and idx < len(items) else "",
|
@@ -833,9 +963,9 @@ class ReviewDashboardPage:
|
|
833 |
|
834 |
# Skip button (just navigate to next)
|
835 |
self.btn_skip.click(
|
836 |
-
fn=lambda items, idx, session:
|
837 |
inputs=[self.items_state, self.idx_state, session_state],
|
838 |
-
outputs=[self.
|
839 |
).then(
|
840 |
fn=show_current_review_item_fn,
|
841 |
inputs=[self.items_state, self.idx_state, session_state],
|
|
|
171 |
validation_status += f" ({validation.description})"
|
172 |
|
173 |
# For deleted annotations, show special status
|
174 |
+
if is_deleted:
|
175 |
+
if validation_status == "Not Reviewed":
|
176 |
+
validation_status = "Not Reviewed (Deleted)"
|
177 |
+
elif validation_status == "Approved":
|
178 |
+
validation_status = "Approved (Deleted)"
|
179 |
+
elif validation_status.startswith("Rejected"):
|
180 |
+
validation_status = f"Rejected (Deleted{validation_status[8:] if validation_status[8:] else ''})"
|
181 |
|
182 |
return validation_status, is_deleted
|
183 |
|
|
|
360 |
"validation_loaded": False # Track if validation status has been loaded
|
361 |
})
|
362 |
|
363 |
+
# --- RESUME LOGIC: Find first unreviewed item ---
|
364 |
initial_idx = 0
|
365 |
if items:
|
366 |
+
log.info(f"Starting resume logic search with {len(items)} items")
|
367 |
+
# First, try to find the first unreviewed item that is not deleted
|
368 |
found_unreviewed = False
|
|
|
|
|
369 |
for i, item_data in enumerate(items):
|
370 |
+
if not item_data.get("is_deleted", False):
|
371 |
+
# Check if this item has been reviewed by the current validator
|
372 |
+
validation = db.query(Validation).filter_by(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
373 |
annotation_id=item_data["annotation_id"],
|
374 |
validator_id=user_id
|
375 |
).first()
|
376 |
|
377 |
+
if not validation:
|
378 |
+
# This item hasn't been reviewed yet
|
379 |
initial_idx = i
|
380 |
found_unreviewed = True
|
381 |
+
log.info(f"✅ Resume point found in initial batch: index {initial_idx}, TTS ID {item_data['tts_id']}, filename: {item_data['filename']}")
|
382 |
break
|
383 |
+
else:
|
384 |
+
log.info(f"Item {i} (TTS ID {item_data['tts_id']}) already reviewed by validator {user_id}")
|
385 |
+
else:
|
386 |
+
log.info(f"Item {i} (TTS ID {item_data['tts_id']}) is deleted, skipping")
|
387 |
|
388 |
+
# If no unreviewed non-deleted items found in initial batch,
|
389 |
+
# we need to load more items to find the resume point
|
390 |
if not found_unreviewed:
|
391 |
+
log.info("No unreviewed items found in initial batch, loading more items to find resume point")
|
392 |
+
|
393 |
+
# Load more items to find the resume point
|
394 |
+
offset = len(items)
|
395 |
+
additional_query = db.query(
|
396 |
+
Annotation,
|
397 |
+
TTSData.filename,
|
398 |
+
TTSData.sentence
|
399 |
+
).join(
|
400 |
+
TTSData, Annotation.tts_data_id == TTSData.id
|
401 |
+
).filter(
|
402 |
+
Annotation.annotator_id == target_annotator_obj.id
|
403 |
+
).order_by(Annotation.id).offset(offset).limit(20) # Load more to find resume point
|
404 |
+
|
405 |
+
additional_results = additional_query.all()
|
406 |
+
log.info(f"Loaded {len(additional_results)} additional items to search for resume point")
|
407 |
+
|
408 |
+
# Process additional items
|
409 |
+
for annotation, filename, sentence in additional_results:
|
410 |
+
is_deleted = not annotation.annotated_sentence or annotation.annotated_sentence.strip() == ""
|
411 |
+
annotated_sentence_display = "[DELETED ANNOTATION]" if is_deleted else annotation.annotated_sentence
|
412 |
+
|
413 |
+
items.append({
|
414 |
+
"annotation_id": annotation.id,
|
415 |
+
"tts_id": annotation.tts_data_id,
|
416 |
+
"filename": filename,
|
417 |
+
"sentence": sentence,
|
418 |
+
"annotated_sentence": annotated_sentence_display,
|
419 |
+
"is_deleted": is_deleted,
|
420 |
+
"annotated_at": annotation.annotated_at.isoformat() if annotation.annotated_at else "",
|
421 |
+
"validation_status": "Loading...",
|
422 |
+
"validation_loaded": False
|
423 |
+
})
|
424 |
+
|
425 |
+
# Now search for the first unreviewed item in the expanded list
|
426 |
+
log.info(f"Searching expanded list of {len(items)} items for resume point")
|
427 |
+
for i, item_data in enumerate(items):
|
428 |
+
if not item_data.get("is_deleted", False):
|
429 |
+
validation = db.query(Validation).filter_by(
|
430 |
+
annotation_id=item_data["annotation_id"],
|
431 |
+
validator_id=user_id
|
432 |
+
).first()
|
433 |
+
|
434 |
+
if not validation:
|
435 |
+
initial_idx = i
|
436 |
+
found_unreviewed = True
|
437 |
+
log.info(f"✅ Resume point found in expanded list: index {initial_idx}, TTS ID {item_data['tts_id']}, filename: {item_data['filename']}")
|
438 |
+
break
|
439 |
+
else:
|
440 |
+
log.info(f"Item {i} (TTS ID {item_data['tts_id']}) already reviewed by validator {user_id}")
|
441 |
+
else:
|
442 |
+
log.info(f"Item {i} (TTS ID {item_data['tts_id']}) is deleted, skipping")
|
443 |
+
|
444 |
+
# If still no unreviewed items found, start from the last item
|
445 |
+
if not found_unreviewed:
|
446 |
+
initial_idx = len(items) - 1 if items else 0
|
447 |
+
log.info(f"⚠️ All items reviewed, starting at last item, index: {initial_idx}")
|
448 |
|
449 |
+
log.info(f"Final resume decision: starting at index {initial_idx} with {len(items)} total items loaded")
|
450 |
|
451 |
# Set initial display
|
452 |
if items:
|
|
|
526 |
# Check if this is a deleted annotation
|
527 |
is_deleted = current_item.get("is_deleted", False)
|
528 |
|
529 |
+
# Handle validation status display
|
530 |
if current_item["validation_status"].startswith("Rejected"):
|
531 |
# Extract reason from status like "Rejected (reason)" or just use empty if no parenthesis
|
532 |
start_paren = current_item["validation_status"].find("(")
|
|
|
534 |
if start_paren != -1 and end_paren != -1:
|
535 |
rejection_reason = current_item["validation_status"][start_paren+1:end_paren]
|
536 |
rejection_visible = True
|
537 |
+
elif current_item["validation_status"] == "Not Reviewed (Deleted)":
|
538 |
+
# Special handling for deleted annotations that haven't been reviewed
|
539 |
+
rejection_visible = False
|
540 |
+
rejection_reason = ""
|
541 |
+
elif current_item["validation_status"] == "Not Reviewed":
|
542 |
+
# Regular unreviewed items
|
543 |
+
rejection_visible = False
|
544 |
+
rejection_reason = ""
|
545 |
|
546 |
return (
|
547 |
str(current_item["tts_id"]),
|
|
|
564 |
else:
|
565 |
return f"���� **Phase 2 Review Mode** - No annotations found for review."
|
566 |
|
567 |
+
def find_next_unreviewed_item(items, current_idx, session):
|
568 |
+
"""Find the next unreviewed item starting from current_idx"""
|
569 |
+
if not items or current_idx >= len(items):
|
570 |
+
log.warning(f"find_next_unreviewed_item: Invalid items or current_idx. items: {len(items) if items else 0}, current_idx: {current_idx}")
|
571 |
+
return current_idx
|
572 |
+
|
573 |
+
user_id = session.get("user_id")
|
574 |
+
if not user_id:
|
575 |
+
log.warning("find_next_unreviewed_item: No user_id in session")
|
576 |
+
return current_idx
|
577 |
+
|
578 |
+
log.info(f"Searching for next unreviewed item starting from index {current_idx} (TTS ID: {items[current_idx]['tts_id'] if current_idx < len(items) else 'N/A'})")
|
579 |
+
|
580 |
+
# Start searching from the next item
|
581 |
+
start_idx = current_idx + 1
|
582 |
+
if start_idx >= len(items):
|
583 |
+
start_idx = 0 # Wrap around to beginning
|
584 |
+
log.info("Wrapping around to beginning of items list")
|
585 |
+
|
586 |
+
# Search forward from start_idx
|
587 |
+
for i in range(len(items)):
|
588 |
+
check_idx = (start_idx + i) % len(items)
|
589 |
+
item_data = items[check_idx]
|
590 |
+
|
591 |
+
log.info(f"Checking item {check_idx} (TTS ID: {item_data['tts_id']}, filename: {item_data['filename']})")
|
592 |
+
|
593 |
+
# Skip deleted annotations
|
594 |
+
if item_data.get("is_deleted", False):
|
595 |
+
log.info(f"Item {check_idx} is deleted, skipping")
|
596 |
+
continue
|
597 |
+
|
598 |
+
# Check if this item has been reviewed
|
599 |
+
if not item_data.get("validation_loaded", False):
|
600 |
+
# Load validation status on-demand
|
601 |
+
with get_db() as db:
|
602 |
+
try:
|
603 |
+
validation = db.query(Validation).filter_by(
|
604 |
+
annotation_id=item_data["annotation_id"],
|
605 |
+
validator_id=user_id
|
606 |
+
).first()
|
607 |
+
item_data["validation_loaded"] = True
|
608 |
+
if validation:
|
609 |
+
item_data["validation_status"] = "Approved" if validation.validated else f"Rejected ({validation.description})" if validation.description else "Rejected"
|
610 |
+
log.info(f"Item {check_idx} validation status loaded: {item_data['validation_status']}")
|
611 |
+
else:
|
612 |
+
item_data["validation_status"] = "Not Reviewed"
|
613 |
+
log.info(f"Item {check_idx} has no validation record - unreviewed")
|
614 |
+
except Exception as e:
|
615 |
+
log.error(f"Error loading validation status for item {check_idx}: {e}")
|
616 |
+
item_data["validation_status"] = "Error loading status"
|
617 |
+
|
618 |
+
# If not reviewed, this is our target
|
619 |
+
if item_data["validation_status"] == "Not Reviewed":
|
620 |
+
log.info(f"✅ Found next unreviewed item at index {check_idx} (TTS ID: {item_data['tts_id']}, filename: {item_data['filename']})")
|
621 |
+
return check_idx
|
622 |
+
else:
|
623 |
+
log.info(f"Item {check_idx} already reviewed with status: {item_data['validation_status']}")
|
624 |
+
|
625 |
+
# If no unreviewed items found, return current index
|
626 |
+
log.info("⚠️ No unreviewed items found, staying at current position")
|
627 |
+
return current_idx
|
628 |
+
|
629 |
def navigate_and_load_fn(items, current_idx, direction, session):
|
630 |
"""Combined navigation and loading function"""
|
631 |
if not items:
|
|
|
824 |
# Combine with existing items
|
825 |
all_items = items + new_items
|
826 |
log.info(f"Loaded {len(new_items)} more items, total now: {len(all_items)}")
|
827 |
+
|
828 |
+
# If this is the first time loading more items, we might need to find the resume point
|
829 |
+
# This ensures that even if the initial batch didn't find unreviewed items,
|
830 |
+
# we can still resume from the correct position
|
831 |
+
if len(items) == 5 and len(all_items) > 5: # Initial batch was 5 items
|
832 |
+
log.info("First load_more call, checking if we need to find resume point")
|
833 |
+
# The resume logic in load_review_items_fn should have already handled this,
|
834 |
+
# but we can add additional safety here if needed
|
835 |
+
|
836 |
return all_items, total_count
|
837 |
|
838 |
# Output definitions
|
|
|
912 |
fn=lambda: gr.update(value="❌ Reject"), # Reset reject button
|
913 |
outputs=[self.btn_reject]
|
914 |
).then(
|
915 |
+
fn=lambda items, idx, session: find_next_unreviewed_item(items, idx, session),
|
916 |
inputs=[self.items_state, self.idx_state, session_state],
|
917 |
+
outputs=[self.idx_state]
|
918 |
).then(
|
919 |
fn=show_current_review_item_fn,
|
920 |
inputs=[self.items_state, self.idx_state, session_state],
|
|
|
935 |
inputs=[self.items_state, self.idx_state, session_state, self.rejection_mode_active],
|
936 |
outputs=[self.header.progress_display]
|
937 |
).then(
|
938 |
+
fn=lambda items, idx, session, rejection_mode: find_next_unreviewed_item(items, idx, session) if not rejection_mode else idx,
|
939 |
inputs=[self.items_state, self.idx_state, session_state, self.rejection_mode_active],
|
940 |
+
outputs=[self.idx_state]
|
941 |
).then(
|
942 |
fn=lambda items, idx, session, rejection_mode: show_current_review_item_fn(items, idx, session) if not rejection_mode else (
|
943 |
str(items[idx]["tts_id"]) if items and idx < len(items) else "",
|
|
|
963 |
|
964 |
# Skip button (just navigate to next)
|
965 |
self.btn_skip.click(
|
966 |
+
fn=lambda items, idx, session: find_next_unreviewed_item(items, idx, session),
|
967 |
inputs=[self.items_state, self.idx_state, session_state],
|
968 |
+
outputs=[self.idx_state]
|
969 |
).then(
|
970 |
fn=show_current_review_item_fn,
|
971 |
inputs=[self.items_state, self.idx_state, session_state],
|