Spaces:
Sleeping
Sleeping
Redesign workflow UI by stage
Browse files- app.py +910 -436
- tests/test_app_ui.py +56 -0
app.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
import json
|
|
|
|
| 4 |
from typing import Any
|
| 5 |
|
| 6 |
import gradio as gr
|
|
@@ -49,14 +50,185 @@ from course_slide_factory.workflow import (
|
|
| 49 |
update_setup_from_inputs,
|
| 50 |
approve_and_continue,
|
| 51 |
)
|
| 52 |
-
from course_slide_factory.quality import get_stage_lock_reasons
|
| 53 |
-
from course_slide_factory.models import IssueSeverity, IssueStatus, IssueType, RevisionConstraints
|
| 54 |
|
| 55 |
|
| 56 |
CSS = """
|
| 57 |
-
.gradio-container { max-width:
|
| 58 |
textarea, .cm-editor { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; }
|
| 59 |
.compact-panel table { font-size: 12px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
"""
|
| 61 |
|
| 62 |
START_PATH_SUMMARIES = {
|
|
@@ -74,6 +246,277 @@ START_PATH_SUMMARIES = {
|
|
| 74 |
),
|
| 75 |
}
|
| 76 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
|
| 78 |
def _has_file(file_value) -> bool:
|
| 79 |
if not file_value:
|
|
@@ -178,6 +621,7 @@ def _render_outputs(
|
|
| 178 |
change_set_id: str = "",
|
| 179 |
prompt_run_id: str = "",
|
| 180 |
):
|
|
|
|
| 181 |
lock_reasons = "\n".join(get_stage_lock_reasons(stage_id, state)) or "Unlocked"
|
| 182 |
role_summary = review_mode_summary_text(state, review_role)
|
| 183 |
review_queue = review_queue_table(
|
|
@@ -222,6 +666,7 @@ def _render_outputs(
|
|
| 222 |
prompt_runs,
|
| 223 |
prompt_detail,
|
| 224 |
metrics,
|
|
|
|
| 225 |
)
|
| 226 |
|
| 227 |
|
|
@@ -355,9 +800,22 @@ def handle_export(state_data, stage_id):
|
|
| 355 |
return _render_outputs(state, stage_id, message)
|
| 356 |
|
| 357 |
|
| 358 |
-
def
|
|
|
|
| 359 |
state = state_from_dict(state_data)
|
| 360 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 361 |
|
| 362 |
|
| 363 |
def _constraints_from_inputs(
|
|
@@ -644,377 +1102,393 @@ def build_app() -> gr.Blocks:
|
|
| 644 |
("Suggest layout switch", "suggest_layout_switch"),
|
| 645 |
("Add alt text", "add_alt_text"),
|
| 646 |
]
|
|
|
|
|
|
|
| 647 |
with gr.Blocks(title="Course Slide Factory") as demo:
|
| 648 |
state_store = gr.State()
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
with gr.
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
start_mode = gr.Textbox(value="instructions_file", visible=False)
|
| 660 |
-
selected_start_path = gr.Markdown(START_PATH_SUMMARIES["instructions_file"])
|
| 661 |
-
with gr.Group(visible=True) as instructions_panel:
|
| 662 |
-
material_files = gr.File(
|
| 663 |
-
label="Instructions file",
|
| 664 |
-
file_count="multiple",
|
| 665 |
-
file_types=[".txt", ".md", ".csv", ".json", ".pdf", ".docx", ".pptx"],
|
| 666 |
-
type="filepath",
|
| 667 |
-
)
|
| 668 |
-
source_url = gr.Textbox(label="Material URL", value="")
|
| 669 |
-
with gr.Group(visible=False) as outline_panel:
|
| 670 |
-
outline_text = gr.Textbox(
|
| 671 |
-
label="Outline",
|
| 672 |
-
lines=8,
|
| 673 |
-
placeholder="Opening hook\nCore concept\nWorked example\nPractice prompt\nWrap-up",
|
| 674 |
-
)
|
| 675 |
-
source_text = gr.Textbox(
|
| 676 |
-
label="Input information",
|
| 677 |
-
lines=6,
|
| 678 |
-
placeholder="Audience, prerequisites, source notes, constraints, examples, or must-cover points.",
|
| 679 |
)
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
file_count="single",
|
| 684 |
-
file_types=[".pptx", ".pdf", ".png", ".jpg", ".jpeg", ".webp"],
|
| 685 |
-
type="filepath",
|
| 686 |
-
)
|
| 687 |
-
template_url = gr.Textbox(label="Template URL", value="mock://template/course")
|
| 688 |
-
output_folder_id = gr.Textbox(label="Output folder ID", value="")
|
| 689 |
-
mutation_target_url = gr.Textbox(label="Mutation target URL", value="")
|
| 690 |
-
with gr.Row():
|
| 691 |
-
dry_run = gr.Checkbox(label="Dry run", value=True)
|
| 692 |
-
production_export_requested = gr.Checkbox(label="Production export", value=False)
|
| 693 |
-
objectives_text = gr.Textbox(
|
| 694 |
-
label="Learning objectives",
|
| 695 |
-
lines=5,
|
| 696 |
-
placeholder="obj_1: Explain the core course concept.\nobj_2: Apply the concept to a worked example.",
|
| 697 |
-
value="",
|
| 698 |
-
)
|
| 699 |
-
update_setup = gr.Button("Save Setup", variant="secondary")
|
| 700 |
|
|
|
|
| 701 |
with gr.Column(scale=2):
|
| 702 |
-
stage_id = gr.Dropdown(
|
| 703 |
-
label="Workflow stage",
|
| 704 |
-
choices=stage_choices,
|
| 705 |
-
value=STAGE_IDS[0],
|
| 706 |
-
interactive=True,
|
| 707 |
-
)
|
| 708 |
-
with gr.Row():
|
| 709 |
-
generate = gr.Button("Generate", variant="primary")
|
| 710 |
-
grade = gr.Button("Grade")
|
| 711 |
-
improve = gr.Button("Improve with AI")
|
| 712 |
-
approve = gr.Button("Approve & Continue", variant="primary")
|
| 713 |
-
with gr.Row():
|
| 714 |
-
preflight = gr.Button("Run Preflight")
|
| 715 |
-
export = gr.Button("Final Render & Export", variant="stop")
|
| 716 |
status = gr.Markdown()
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
)
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
slide_inventory = gr.Dataframe(
|
| 744 |
-
headers=[
|
| 745 |
-
"Slide #",
|
| 746 |
-
"Title",
|
| 747 |
-
"Role",
|
| 748 |
-
"Objectives",
|
| 749 |
-
"Claims",
|
| 750 |
-
"Visuals",
|
| 751 |
-
"Aggregate",
|
| 752 |
-
"Technical",
|
| 753 |
-
"Pedagogical",
|
| 754 |
-
"Aesthetic",
|
| 755 |
-
"Status",
|
| 756 |
-
"Issues",
|
| 757 |
-
"Stale?",
|
| 758 |
-
],
|
| 759 |
-
interactive=False,
|
| 760 |
-
elem_classes=["compact-panel"],
|
| 761 |
-
)
|
| 762 |
-
with gr.Tab("Objectives"):
|
| 763 |
-
objective_matrix = gr.Dataframe(
|
| 764 |
-
headers=[
|
| 765 |
-
"Objective ID",
|
| 766 |
-
"Objective",
|
| 767 |
-
"Mapped Slides",
|
| 768 |
-
"Coverage Score",
|
| 769 |
-
"Coverage Status",
|
| 770 |
-
"Evidence Count",
|
| 771 |
-
"Issues",
|
| 772 |
-
],
|
| 773 |
-
interactive=False,
|
| 774 |
-
elem_classes=["compact-panel"],
|
| 775 |
-
)
|
| 776 |
-
with gr.Tab("Preflight"):
|
| 777 |
-
preflight_results = gr.Dataframe(
|
| 778 |
-
headers=["Field", "Value"],
|
| 779 |
-
interactive=False,
|
| 780 |
-
elem_classes=["compact-panel"],
|
| 781 |
-
)
|
| 782 |
-
with gr.Tab("Stages"):
|
| 783 |
-
stage_status = gr.Dataframe(
|
| 784 |
-
headers=[
|
| 785 |
-
"#",
|
| 786 |
-
"Stage",
|
| 787 |
-
"Score",
|
| 788 |
-
"Artifact",
|
| 789 |
-
"Artifact Status",
|
| 790 |
-
"Stale?",
|
| 791 |
-
"Can Unlock Next?",
|
| 792 |
-
"Lock Reasons",
|
| 793 |
-
],
|
| 794 |
-
interactive=False,
|
| 795 |
-
elem_classes=["compact-panel"],
|
| 796 |
-
)
|
| 797 |
-
with gr.Tab("Issues"):
|
| 798 |
-
issues = gr.Dataframe(
|
| 799 |
-
headers=[
|
| 800 |
-
"Issue ID",
|
| 801 |
-
"Type",
|
| 802 |
-
"Severity",
|
| 803 |
-
"Stage",
|
| 804 |
-
"Slide",
|
| 805 |
-
"Message",
|
| 806 |
-
"Resolved?",
|
| 807 |
-
],
|
| 808 |
-
interactive=False,
|
| 809 |
-
elem_classes=["compact-panel"],
|
| 810 |
-
)
|
| 811 |
-
with gr.Tab("Review Workbench"):
|
| 812 |
-
with gr.Row():
|
| 813 |
-
review_role = gr.Dropdown(
|
| 814 |
-
label="Review role",
|
| 815 |
-
choices=review_role_options,
|
| 816 |
-
value="general_reviewer",
|
| 817 |
-
interactive=True,
|
| 818 |
-
)
|
| 819 |
-
queue_stage = gr.Dropdown(
|
| 820 |
-
label="Stage filter",
|
| 821 |
-
choices=queue_stage_choices,
|
| 822 |
-
value="all",
|
| 823 |
-
interactive=True,
|
| 824 |
-
)
|
| 825 |
-
queue_severity = gr.Dropdown(
|
| 826 |
-
label="Severity filter",
|
| 827 |
-
choices=severity_choices,
|
| 828 |
-
value="all",
|
| 829 |
-
interactive=True,
|
| 830 |
-
)
|
| 831 |
-
with gr.Row():
|
| 832 |
-
queue_status = gr.Dropdown(
|
| 833 |
-
label="Status filter",
|
| 834 |
-
choices=status_choices,
|
| 835 |
-
value="all",
|
| 836 |
-
interactive=True,
|
| 837 |
-
)
|
| 838 |
-
queue_issue_type = gr.Dropdown(
|
| 839 |
-
label="Issue type filter",
|
| 840 |
-
choices=issue_type_choices,
|
| 841 |
-
value="all",
|
| 842 |
-
interactive=True,
|
| 843 |
-
)
|
| 844 |
-
assigned_filter = gr.Textbox(label="Assigned reviewer filter")
|
| 845 |
-
role_summary = gr.Textbox(label="Review mode", lines=3, interactive=False)
|
| 846 |
-
refresh_review = gr.Button("Refresh Review Workbench")
|
| 847 |
-
review_queue = gr.Dataframe(
|
| 848 |
-
headers=[
|
| 849 |
-
"Severity",
|
| 850 |
-
"Priority",
|
| 851 |
-
"Status",
|
| 852 |
-
"Stage",
|
| 853 |
-
"Slide",
|
| 854 |
-
"Role",
|
| 855 |
-
"Issue Type",
|
| 856 |
-
"Message",
|
| 857 |
-
"Suggested Fix",
|
| 858 |
-
"Assigned To",
|
| 859 |
-
"Created At",
|
| 860 |
-
"Issue ID",
|
| 861 |
-
],
|
| 862 |
-
interactive=False,
|
| 863 |
-
elem_classes=["compact-panel"],
|
| 864 |
-
)
|
| 865 |
-
with gr.Row():
|
| 866 |
-
selected_issue_id = gr.Textbox(label="Issue ID")
|
| 867 |
-
issue_action = gr.Dropdown(
|
| 868 |
-
label="Triage action",
|
| 869 |
-
choices=[
|
| 870 |
-
("Acknowledge", "acknowledge"),
|
| 871 |
-
("Mark in progress", "in_progress"),
|
| 872 |
-
("Resolve", "resolve"),
|
| 873 |
-
("Waive", "waive"),
|
| 874 |
-
("Won't fix", "wont_fix"),
|
| 875 |
-
("Reopen", "reopen"),
|
| 876 |
-
("Assign", "assign"),
|
| 877 |
-
],
|
| 878 |
-
value="acknowledge",
|
| 879 |
-
)
|
| 880 |
-
triage_note = gr.Textbox(label="Reason or note")
|
| 881 |
-
with gr.Row():
|
| 882 |
-
assigned_role = gr.Dropdown(
|
| 883 |
-
label="Assign role",
|
| 884 |
-
choices=[("No role", "all"), *review_role_options],
|
| 885 |
-
value="all",
|
| 886 |
-
)
|
| 887 |
-
assigned_to = gr.Textbox(label="Assign reviewer")
|
| 888 |
-
triage_issue = gr.Button("Apply Triage")
|
| 889 |
-
generate_issue_fix = gr.Button("Generate Suggested Fix")
|
| 890 |
-
suggested_fixes = gr.Dataframe(
|
| 891 |
-
headers=[
|
| 892 |
-
"Fix ID",
|
| 893 |
-
"Type",
|
| 894 |
-
"Stage",
|
| 895 |
-
"Slides",
|
| 896 |
-
"Issues",
|
| 897 |
-
"Risk",
|
| 898 |
-
"Description",
|
| 899 |
-
],
|
| 900 |
-
interactive=False,
|
| 901 |
-
elem_classes=["compact-panel"],
|
| 902 |
-
)
|
| 903 |
-
with gr.Row():
|
| 904 |
-
fast_generate_grade = gr.Button("Generate + Grade Current Stage")
|
| 905 |
-
fast_draft_ahead = gr.Button("Draft Ahead Until Review Needed")
|
| 906 |
-
with gr.Row():
|
| 907 |
-
preserve_slide_order = gr.Checkbox(label="Preserve slide order", value=True)
|
| 908 |
-
preserve_slide_titles = gr.Checkbox(label="Preserve slide titles", value=False)
|
| 909 |
-
preserve_technical_claims = gr.Checkbox(label="Preserve technical claims", value=True)
|
| 910 |
-
with gr.Row():
|
| 911 |
-
preserve_objective_mappings = gr.Checkbox(
|
| 912 |
-
label="Preserve objective mappings",
|
| 913 |
-
value=True,
|
| 914 |
-
)
|
| 915 |
-
preserve_visual_assets = gr.Checkbox(label="Preserve visual assets", value=False)
|
| 916 |
-
preserve_layout_ids = gr.Checkbox(label="Preserve layout IDs", value=False)
|
| 917 |
-
with gr.Row():
|
| 918 |
-
constraint_slide_ids = gr.Textbox(label="Selected slide IDs")
|
| 919 |
-
allowed_fields = gr.Textbox(label="Allowed fields")
|
| 920 |
-
blocked_fields = gr.Textbox(label="Blocked fields")
|
| 921 |
-
reviewer_instruction = gr.Textbox(label="Reviewer instruction", lines=2)
|
| 922 |
-
with gr.Row():
|
| 923 |
-
propose_with_constraints = gr.Button("Critique + Propose Changes")
|
| 924 |
-
targeted_slide_ids = gr.Textbox(label="Target slide IDs")
|
| 925 |
-
targeted_action = gr.Dropdown(
|
| 926 |
-
label="Selected slide action",
|
| 927 |
-
choices=targeted_action_choices,
|
| 928 |
-
value="rewrite_visible_text",
|
| 929 |
-
)
|
| 930 |
-
targeted_improvement = gr.Button("Create Targeted Changes")
|
| 931 |
-
critique_output = gr.Textbox(label="Latest critique", lines=8, interactive=False)
|
| 932 |
-
proposed_change_sets = gr.Dataframe(
|
| 933 |
-
headers=[
|
| 934 |
-
"Change Set ID",
|
| 935 |
-
"Stage",
|
| 936 |
-
"Status",
|
| 937 |
-
"Artifact",
|
| 938 |
-
"Changes",
|
| 939 |
-
"Summary",
|
| 940 |
-
"Created At",
|
| 941 |
-
],
|
| 942 |
-
interactive=False,
|
| 943 |
-
elem_classes=["compact-panel"],
|
| 944 |
-
)
|
| 945 |
-
with gr.Row():
|
| 946 |
-
selected_change_set_id = gr.Textbox(label="Change set ID")
|
| 947 |
-
selected_change_ids = gr.Textbox(label="Selected change IDs")
|
| 948 |
-
apply_changes = gr.Button("Apply Selected Proposed Changes", variant="primary")
|
| 949 |
-
reject_changes = gr.Button("Reject Change Set")
|
| 950 |
-
proposed_changes = gr.Dataframe(
|
| 951 |
-
headers=[
|
| 952 |
-
"Change Set",
|
| 953 |
-
"Change ID",
|
| 954 |
-
"Target Type",
|
| 955 |
-
"Target ID",
|
| 956 |
-
"Field",
|
| 957 |
-
"Before",
|
| 958 |
-
"After",
|
| 959 |
-
"Rationale",
|
| 960 |
-
"Issues",
|
| 961 |
-
"Risk",
|
| 962 |
-
],
|
| 963 |
-
interactive=False,
|
| 964 |
-
elem_classes=["compact-panel"],
|
| 965 |
)
|
| 966 |
-
|
| 967 |
-
|
| 968 |
-
|
| 969 |
-
|
| 970 |
-
|
| 971 |
-
|
| 972 |
-
label="Packet format",
|
| 973 |
-
choices=[("Markdown", "markdown"), ("JSON", "json")],
|
| 974 |
-
value="markdown",
|
| 975 |
-
)
|
| 976 |
-
create_packet = gr.Button("Create Review Packet")
|
| 977 |
-
with gr.Row():
|
| 978 |
-
restore_artifact_id = gr.Textbox(label="Artifact version ID")
|
| 979 |
-
restore_candidate = gr.Button("Restore as Candidate")
|
| 980 |
-
prompt_runs = gr.Dataframe(
|
| 981 |
-
headers=[
|
| 982 |
-
"Prompt Run ID",
|
| 983 |
-
"Created At",
|
| 984 |
-
"Stage",
|
| 985 |
-
"Provider",
|
| 986 |
-
"Model",
|
| 987 |
-
"Template",
|
| 988 |
-
"Inputs",
|
| 989 |
-
"Output",
|
| 990 |
-
"Settings",
|
| 991 |
-
],
|
| 992 |
-
interactive=False,
|
| 993 |
-
elem_classes=["compact-panel"],
|
| 994 |
)
|
| 995 |
-
|
| 996 |
-
|
| 997 |
-
|
| 998 |
-
|
| 999 |
-
interactive=False,
|
| 1000 |
-
elem_classes=["compact-panel"],
|
| 1001 |
)
|
| 1002 |
-
with gr.
|
| 1003 |
-
|
| 1004 |
-
|
| 1005 |
-
|
| 1006 |
-
|
| 1007 |
-
|
| 1008 |
-
"Stage",
|
| 1009 |
-
"Slide",
|
| 1010 |
-
"Issue",
|
| 1011 |
-
"Reason",
|
| 1012 |
-
],
|
| 1013 |
-
interactive=False,
|
| 1014 |
-
elem_classes=["compact-panel"],
|
| 1015 |
)
|
| 1016 |
-
|
| 1017 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1018 |
|
| 1019 |
outputs = [
|
| 1020 |
state_store,
|
|
@@ -1040,6 +1514,15 @@ def build_app() -> gr.Blocks:
|
|
| 1040 |
prompt_runs,
|
| 1041 |
prompt_detail,
|
| 1042 |
reviewer_metrics,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1043 |
]
|
| 1044 |
setup_inputs = [
|
| 1045 |
state_store,
|
|
@@ -1113,44 +1596,62 @@ def build_app() -> gr.Blocks:
|
|
| 1113 |
blocked_fields,
|
| 1114 |
reviewer_instruction,
|
| 1115 |
]
|
| 1116 |
-
start_path_outputs = [
|
| 1117 |
-
|
| 1118 |
-
selected_start_path,
|
| 1119 |
-
instructions_panel,
|
| 1120 |
-
outline_panel,
|
| 1121 |
-
draft_panel,
|
| 1122 |
-
]
|
| 1123 |
|
| 1124 |
-
|
| 1125 |
-
|
| 1126 |
-
|
| 1127 |
-
|
| 1128 |
-
|
| 1129 |
-
|
|
|
|
| 1130 |
instructions_path.click(select_instructions_path, inputs=[], outputs=start_path_outputs)
|
| 1131 |
outline_path.click(select_outline_path, inputs=[], outputs=start_path_outputs)
|
| 1132 |
draft_path.click(select_draft_path, inputs=[], outputs=start_path_outputs)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1133 |
update_setup.click(handle_update_setup, inputs=setup_inputs, outputs=outputs)
|
| 1134 |
-
|
| 1135 |
-
|
| 1136 |
-
|
| 1137 |
-
|
| 1138 |
-
|
| 1139 |
-
|
| 1140 |
-
|
| 1141 |
-
|
| 1142 |
-
|
| 1143 |
-
|
| 1144 |
-
|
| 1145 |
-
],
|
| 1146 |
-
|
| 1147 |
-
|
| 1148 |
-
|
| 1149 |
-
|
| 1150 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1151 |
export.click(handle_export, inputs=[state_store, stage_id], outputs=outputs)
|
| 1152 |
-
|
| 1153 |
-
refresh_review
|
|
|
|
| 1154 |
for filter_component in [
|
| 1155 |
review_role,
|
| 1156 |
queue_stage,
|
|
@@ -1161,28 +1662,15 @@ def build_app() -> gr.Blocks:
|
|
| 1161 |
selected_change_set_id,
|
| 1162 |
prompt_run_id,
|
| 1163 |
]:
|
| 1164 |
-
filter_component.change(
|
| 1165 |
-
handle_review_refresh,
|
| 1166 |
-
inputs=review_filter_inputs,
|
| 1167 |
-
outputs=outputs,
|
| 1168 |
-
show_progress="hidden",
|
| 1169 |
-
)
|
| 1170 |
triage_issue.click(handle_triage_issue, inputs=triage_inputs, outputs=outputs)
|
| 1171 |
-
generate_issue_fix
|
| 1172 |
-
|
| 1173 |
-
|
| 1174 |
-
|
| 1175 |
-
outputs=outputs,
|
| 1176 |
-
)
|
| 1177 |
targeted_improvement.click(
|
| 1178 |
handle_targeted_improvement,
|
| 1179 |
-
inputs=[
|
| 1180 |
-
state_store,
|
| 1181 |
-
stage_id,
|
| 1182 |
-
targeted_slide_ids,
|
| 1183 |
-
targeted_action,
|
| 1184 |
-
*constraint_inputs,
|
| 1185 |
-
],
|
| 1186 |
outputs=outputs,
|
| 1187 |
)
|
| 1188 |
apply_changes.click(
|
|
@@ -1195,27 +1683,13 @@ def build_app() -> gr.Blocks:
|
|
| 1195 |
inputs=[state_store, stage_id, selected_change_set_id, reviewer_name, triage_note],
|
| 1196 |
outputs=outputs,
|
| 1197 |
)
|
| 1198 |
-
|
| 1199 |
-
|
| 1200 |
-
|
| 1201 |
-
|
| 1202 |
-
)
|
| 1203 |
-
fast_draft_ahead.click(
|
| 1204 |
-
handle_fast_draft_ahead,
|
| 1205 |
-
inputs=[state_store, stage_id],
|
| 1206 |
-
outputs=outputs,
|
| 1207 |
-
)
|
| 1208 |
-
semantic_compare.click(handle_semantic_compare, inputs=[state_store, stage_id], outputs=outputs)
|
| 1209 |
create_packet.click(
|
| 1210 |
handle_review_packet,
|
| 1211 |
-
inputs=[
|
| 1212 |
-
state_store,
|
| 1213 |
-
stage_id,
|
| 1214 |
-
review_role,
|
| 1215 |
-
packet_stage_ids,
|
| 1216 |
-
packet_slide_ids,
|
| 1217 |
-
packet_format,
|
| 1218 |
-
],
|
| 1219 |
outputs=outputs,
|
| 1220 |
)
|
| 1221 |
restore_candidate.click(
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
import json
|
| 4 |
+
import html
|
| 5 |
from typing import Any
|
| 6 |
|
| 7 |
import gradio as gr
|
|
|
|
| 50 |
update_setup_from_inputs,
|
| 51 |
approve_and_continue,
|
| 52 |
)
|
| 53 |
+
from course_slide_factory.quality import can_unlock_next_stage, get_current_stage_artifact, get_stage_lock_reasons
|
| 54 |
+
from course_slide_factory.models import ArtifactStatus, IssueSeverity, IssueStatus, IssueType, RevisionConstraints
|
| 55 |
|
| 56 |
|
| 57 |
CSS = """
|
| 58 |
+
.gradio-container { max-width: 1540px !important; background: #f7f8fa; }
|
| 59 |
textarea, .cm-editor { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; }
|
| 60 |
.compact-panel table { font-size: 12px; }
|
| 61 |
+
.workflow-topbar {
|
| 62 |
+
position: sticky;
|
| 63 |
+
top: 0;
|
| 64 |
+
z-index: 20;
|
| 65 |
+
background: rgba(247, 248, 250, 0.96);
|
| 66 |
+
border-bottom: 1px solid #d9dee7;
|
| 67 |
+
padding: 10px 0 12px;
|
| 68 |
+
backdrop-filter: blur(8px);
|
| 69 |
+
}
|
| 70 |
+
.workflow-shell {
|
| 71 |
+
border: 1px solid #d9dee7;
|
| 72 |
+
background: #ffffff;
|
| 73 |
+
border-radius: 8px;
|
| 74 |
+
padding: 14px 16px;
|
| 75 |
+
}
|
| 76 |
+
.workflow-title-row {
|
| 77 |
+
display: flex;
|
| 78 |
+
align-items: flex-start;
|
| 79 |
+
justify-content: space-between;
|
| 80 |
+
gap: 18px;
|
| 81 |
+
}
|
| 82 |
+
.workflow-kicker {
|
| 83 |
+
color: #667085;
|
| 84 |
+
font-size: 11px;
|
| 85 |
+
font-weight: 700;
|
| 86 |
+
letter-spacing: 0.06em;
|
| 87 |
+
text-transform: uppercase;
|
| 88 |
+
}
|
| 89 |
+
.workflow-shell h2 {
|
| 90 |
+
color: #111827;
|
| 91 |
+
font-size: 22px;
|
| 92 |
+
line-height: 1.2;
|
| 93 |
+
margin: 4px 0 2px;
|
| 94 |
+
}
|
| 95 |
+
.workflow-shell p {
|
| 96 |
+
color: #4b5563;
|
| 97 |
+
margin: 0;
|
| 98 |
+
}
|
| 99 |
+
.workflow-progress-card {
|
| 100 |
+
min-width: 170px;
|
| 101 |
+
border: 1px solid #d9dee7;
|
| 102 |
+
border-radius: 8px;
|
| 103 |
+
padding: 10px 12px;
|
| 104 |
+
text-align: right;
|
| 105 |
+
background: #f9fafb;
|
| 106 |
+
}
|
| 107 |
+
.workflow-progress-card strong {
|
| 108 |
+
display: block;
|
| 109 |
+
color: #111827;
|
| 110 |
+
font-size: 26px;
|
| 111 |
+
line-height: 1;
|
| 112 |
+
}
|
| 113 |
+
.workflow-progress-card span {
|
| 114 |
+
color: #667085;
|
| 115 |
+
font-size: 12px;
|
| 116 |
+
}
|
| 117 |
+
.workflow-progress-track {
|
| 118 |
+
height: 8px;
|
| 119 |
+
background: #e5e7eb;
|
| 120 |
+
border-radius: 999px;
|
| 121 |
+
margin-top: 12px;
|
| 122 |
+
overflow: hidden;
|
| 123 |
+
}
|
| 124 |
+
.workflow-progress-fill {
|
| 125 |
+
height: 100%;
|
| 126 |
+
background: #2563eb;
|
| 127 |
+
border-radius: inherit;
|
| 128 |
+
}
|
| 129 |
+
.workflow-metrics {
|
| 130 |
+
display: grid;
|
| 131 |
+
gap: 8px;
|
| 132 |
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
| 133 |
+
margin-top: 12px;
|
| 134 |
+
}
|
| 135 |
+
.workflow-metric {
|
| 136 |
+
border: 1px solid #e5e7eb;
|
| 137 |
+
border-radius: 8px;
|
| 138 |
+
padding: 8px 10px;
|
| 139 |
+
background: #ffffff;
|
| 140 |
+
}
|
| 141 |
+
.workflow-metric span {
|
| 142 |
+
color: #667085;
|
| 143 |
+
display: block;
|
| 144 |
+
font-size: 11px;
|
| 145 |
+
}
|
| 146 |
+
.workflow-metric strong {
|
| 147 |
+
color: #111827;
|
| 148 |
+
display: block;
|
| 149 |
+
font-size: 15px;
|
| 150 |
+
margin-top: 2px;
|
| 151 |
+
}
|
| 152 |
+
.stage-score-grid {
|
| 153 |
+
display: grid;
|
| 154 |
+
gap: 6px;
|
| 155 |
+
grid-template-columns: repeat(auto-fit, minmax(126px, 1fr));
|
| 156 |
+
}
|
| 157 |
+
.stage-score-chip {
|
| 158 |
+
border: 1px solid #d9dee7;
|
| 159 |
+
border-left: 4px solid #9ca3af;
|
| 160 |
+
border-radius: 8px;
|
| 161 |
+
background: #ffffff;
|
| 162 |
+
padding: 8px;
|
| 163 |
+
min-height: 74px;
|
| 164 |
+
}
|
| 165 |
+
.stage-score-chip.active {
|
| 166 |
+
border-color: #2563eb;
|
| 167 |
+
border-left-color: #2563eb;
|
| 168 |
+
box-shadow: inset 0 0 0 1px #2563eb;
|
| 169 |
+
}
|
| 170 |
+
.stage-score-chip.complete { border-left-color: #059669; }
|
| 171 |
+
.stage-score-chip.review { border-left-color: #d97706; }
|
| 172 |
+
.stage-score-chip.blocked { border-left-color: #dc2626; }
|
| 173 |
+
.stage-score-chip.stale { border-left-color: #7c3aed; }
|
| 174 |
+
.stage-score-chip.ready { border-left-color: #2563eb; }
|
| 175 |
+
.stage-chip-top {
|
| 176 |
+
align-items: center;
|
| 177 |
+
display: flex;
|
| 178 |
+
gap: 6px;
|
| 179 |
+
justify-content: space-between;
|
| 180 |
+
}
|
| 181 |
+
.stage-chip-number,
|
| 182 |
+
.stage-chip-score {
|
| 183 |
+
color: #111827;
|
| 184 |
+
font-size: 12px;
|
| 185 |
+
font-weight: 700;
|
| 186 |
+
}
|
| 187 |
+
.stage-chip-label {
|
| 188 |
+
color: #111827;
|
| 189 |
+
font-size: 12px;
|
| 190 |
+
font-weight: 600;
|
| 191 |
+
line-height: 1.2;
|
| 192 |
+
margin-top: 6px;
|
| 193 |
+
}
|
| 194 |
+
.stage-chip-state {
|
| 195 |
+
color: #667085;
|
| 196 |
+
font-size: 11px;
|
| 197 |
+
margin-top: 4px;
|
| 198 |
+
text-transform: capitalize;
|
| 199 |
+
}
|
| 200 |
+
.stage-nav-row button {
|
| 201 |
+
min-height: 42px;
|
| 202 |
+
white-space: normal !important;
|
| 203 |
+
}
|
| 204 |
+
.screen-card {
|
| 205 |
+
border: 1px solid #d9dee7;
|
| 206 |
+
border-radius: 8px;
|
| 207 |
+
background: #ffffff;
|
| 208 |
+
padding: 14px;
|
| 209 |
+
}
|
| 210 |
+
.screen-card h3 {
|
| 211 |
+
color: #111827;
|
| 212 |
+
font-size: 18px;
|
| 213 |
+
margin: 0 0 4px;
|
| 214 |
+
}
|
| 215 |
+
.screen-card p {
|
| 216 |
+
color: #4b5563;
|
| 217 |
+
margin: 0 0 10px;
|
| 218 |
+
}
|
| 219 |
+
.action-strip button {
|
| 220 |
+
min-height: 42px;
|
| 221 |
+
}
|
| 222 |
+
@media (max-width: 900px) {
|
| 223 |
+
.workflow-title-row,
|
| 224 |
+
.workflow-metrics {
|
| 225 |
+
display: block;
|
| 226 |
+
}
|
| 227 |
+
.workflow-progress-card {
|
| 228 |
+
margin-top: 12px;
|
| 229 |
+
text-align: left;
|
| 230 |
+
}
|
| 231 |
+
}
|
| 232 |
"""
|
| 233 |
|
| 234 |
START_PATH_SUMMARIES = {
|
|
|
|
| 246 |
),
|
| 247 |
}
|
| 248 |
|
| 249 |
+
PASSING_STAGE_SCORE = 80
|
| 250 |
+
RESOLVED_ISSUE_STATUSES = {IssueStatus.RESOLVED, IssueStatus.WAIVED, IssueStatus.WONT_FIX}
|
| 251 |
+
COMPLETED_ARTIFACT_STATUSES = {ArtifactStatus.APPROVED, ArtifactStatus.EXPORTED}
|
| 252 |
+
REVIEW_STAGE_DEFAULTS = {
|
| 253 |
+
"technical_review": ("technical_review", "sme"),
|
| 254 |
+
"pedagogical_review": ("pedagogical_review", "instructional_designer"),
|
| 255 |
+
"aesthetic_review": ("aesthetic_review", "visual_designer"),
|
| 256 |
+
}
|
| 257 |
+
ARTIFACT_WORKSPACE_STAGES = {
|
| 258 |
+
"source_extraction_objective_mapping",
|
| 259 |
+
"slide_outline_order",
|
| 260 |
+
"title_generation",
|
| 261 |
+
"text_generation",
|
| 262 |
+
"image_visual_asset_generation",
|
| 263 |
+
"aesthetic_ordering_visual_composition",
|
| 264 |
+
}
|
| 265 |
+
REVIEW_WORKSPACE_STAGES = {"technical_review", "pedagogical_review", "aesthetic_review"}
|
| 266 |
+
FINAL_WORKSPACE_STAGES = {"final_render_export"}
|
| 267 |
+
AUDIT_WORKSPACE_STAGES = {"audit_log_version_history"}
|
| 268 |
+
HISTORY_WORKSPACE_STAGES = ARTIFACT_WORKSPACE_STAGES | AUDIT_WORKSPACE_STAGES
|
| 269 |
+
|
| 270 |
+
STAGE_UI_CONFIG = {
|
| 271 |
+
"setup_inputs": {
|
| 272 |
+
"short": "Setup",
|
| 273 |
+
"focus": "Collect source material, choose the start path, and lock the deck operating settings.",
|
| 274 |
+
"primary": "Save the working setup, then run preflight before drafting.",
|
| 275 |
+
},
|
| 276 |
+
"source_extraction_objective_mapping": {
|
| 277 |
+
"short": "Sources",
|
| 278 |
+
"focus": "Extract usable source context and connect it to measurable learning objectives.",
|
| 279 |
+
"primary": "Generate mapping",
|
| 280 |
+
},
|
| 281 |
+
"slide_outline_order": {
|
| 282 |
+
"short": "Outline",
|
| 283 |
+
"focus": "Shape the instructional sequence, slide roles, and objective coverage.",
|
| 284 |
+
"primary": "Build outline",
|
| 285 |
+
},
|
| 286 |
+
"title_generation": {
|
| 287 |
+
"short": "Titles",
|
| 288 |
+
"focus": "Draft clear slide titles that make the course arc scannable.",
|
| 289 |
+
"primary": "Generate titles",
|
| 290 |
+
},
|
| 291 |
+
"text_generation": {
|
| 292 |
+
"short": "Text",
|
| 293 |
+
"focus": "Draft visible text and speaker notes with density and grounding checks.",
|
| 294 |
+
"primary": "Generate slide text",
|
| 295 |
+
},
|
| 296 |
+
"image_visual_asset_generation": {
|
| 297 |
+
"short": "Visuals",
|
| 298 |
+
"focus": "Create visual briefs, assets, alt text, and licensing metadata.",
|
| 299 |
+
"primary": "Generate visuals",
|
| 300 |
+
},
|
| 301 |
+
"aesthetic_ordering_visual_composition": {
|
| 302 |
+
"short": "Layout",
|
| 303 |
+
"focus": "Assign layouts, check slot compliance, and refine visual hierarchy.",
|
| 304 |
+
"primary": "Compose layout",
|
| 305 |
+
},
|
| 306 |
+
"technical_review": {
|
| 307 |
+
"short": "Technical",
|
| 308 |
+
"focus": "Review technical accuracy, terminology, and unsupported claims.",
|
| 309 |
+
"primary": "Refresh technical queue",
|
| 310 |
+
},
|
| 311 |
+
"pedagogical_review": {
|
| 312 |
+
"short": "Pedagogy",
|
| 313 |
+
"focus": "Review objective coverage, learning progression, examples, and teachability.",
|
| 314 |
+
"primary": "Refresh pedagogy queue",
|
| 315 |
+
},
|
| 316 |
+
"aesthetic_review": {
|
| 317 |
+
"short": "Aesthetic",
|
| 318 |
+
"focus": "Review readability, accessibility, visual consistency, and template fit.",
|
| 319 |
+
"primary": "Refresh aesthetic queue",
|
| 320 |
+
},
|
| 321 |
+
"final_render_export": {
|
| 322 |
+
"short": "Export",
|
| 323 |
+
"focus": "Run export preflight, resolve blockers, and produce the final deck artifact.",
|
| 324 |
+
"primary": "Final render",
|
| 325 |
+
},
|
| 326 |
+
"audit_log_version_history": {
|
| 327 |
+
"short": "Audit",
|
| 328 |
+
"focus": "Inspect audit events, prompt runs, semantic diffs, and candidate versions.",
|
| 329 |
+
"primary": "Inspect history",
|
| 330 |
+
},
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
|
| 334 |
+
def _safe_stage_id(stage_id: str | None) -> str:
|
| 335 |
+
return stage_id if stage_id in STAGE_IDS else STAGE_IDS[0]
|
| 336 |
+
|
| 337 |
+
|
| 338 |
+
def _short_stage_label(stage_id: str) -> str:
|
| 339 |
+
return STAGE_UI_CONFIG[stage_id]["short"]
|
| 340 |
+
|
| 341 |
+
|
| 342 |
+
def _escape(value: object) -> str:
|
| 343 |
+
return html.escape("" if value is None else str(value), quote=True)
|
| 344 |
+
|
| 345 |
+
|
| 346 |
+
def _is_unresolved_issue(issue) -> bool:
|
| 347 |
+
return issue.status not in RESOLVED_ISSUE_STATUSES and not issue.resolved
|
| 348 |
+
|
| 349 |
+
|
| 350 |
+
def _unresolved_blocker_count(state, stage_id: str | None = None) -> int:
|
| 351 |
+
return sum(
|
| 352 |
+
1
|
| 353 |
+
for issue in state.issues.values()
|
| 354 |
+
if issue.severity == IssueSeverity.BLOCKER
|
| 355 |
+
and _is_unresolved_issue(issue)
|
| 356 |
+
and (stage_id is None or issue.stage_id == stage_id)
|
| 357 |
+
)
|
| 358 |
+
|
| 359 |
+
|
| 360 |
+
def _stage_status_key(stage_id: str, state) -> str:
|
| 361 |
+
stage = state.stages[stage_id]
|
| 362 |
+
artifact = get_current_stage_artifact(stage_id, state)
|
| 363 |
+
if artifact and artifact.status in COMPLETED_ARTIFACT_STATUSES:
|
| 364 |
+
return "complete"
|
| 365 |
+
if stage.is_stale:
|
| 366 |
+
return "stale"
|
| 367 |
+
if _unresolved_blocker_count(state, stage_id) > 0:
|
| 368 |
+
return "blocked"
|
| 369 |
+
if artifact is None:
|
| 370 |
+
return "pending"
|
| 371 |
+
if stage.score is None or stage.score < PASSING_STAGE_SCORE:
|
| 372 |
+
return "review"
|
| 373 |
+
if can_unlock_next_stage(stage_id, state):
|
| 374 |
+
return "ready"
|
| 375 |
+
if get_stage_lock_reasons(stage_id, state):
|
| 376 |
+
return "review"
|
| 377 |
+
return "ready"
|
| 378 |
+
|
| 379 |
+
|
| 380 |
+
def workflow_progress_summary(state) -> dict[str, Any]:
|
| 381 |
+
total = len(STAGE_IDS)
|
| 382 |
+
complete = 0
|
| 383 |
+
scored_values: list[int] = []
|
| 384 |
+
stale = 0
|
| 385 |
+
for stage_id in STAGE_IDS:
|
| 386 |
+
stage = state.stages[stage_id]
|
| 387 |
+
artifact = get_current_stage_artifact(stage_id, state)
|
| 388 |
+
if artifact and artifact.status in COMPLETED_ARTIFACT_STATUSES:
|
| 389 |
+
complete += 1
|
| 390 |
+
if stage.score is not None:
|
| 391 |
+
scored_values.append(stage.score)
|
| 392 |
+
if stage.is_stale:
|
| 393 |
+
stale += 1
|
| 394 |
+
percent = round((complete / total) * 100) if total else 0
|
| 395 |
+
return {
|
| 396 |
+
"complete": complete,
|
| 397 |
+
"total": total,
|
| 398 |
+
"percent": percent,
|
| 399 |
+
"stale": stale,
|
| 400 |
+
"unresolved_blockers": _unresolved_blocker_count(state),
|
| 401 |
+
"average_stage_score": round(sum(scored_values) / len(scored_values)) if scored_values else None,
|
| 402 |
+
}
|
| 403 |
+
|
| 404 |
+
|
| 405 |
+
def stage_score_rows(state, active_stage_id: str) -> list[dict[str, Any]]:
|
| 406 |
+
active_stage_id = _safe_stage_id(active_stage_id)
|
| 407 |
+
rows = []
|
| 408 |
+
for index, stage_id in enumerate(STAGE_IDS, start=1):
|
| 409 |
+
stage = state.stages[stage_id]
|
| 410 |
+
rows.append(
|
| 411 |
+
{
|
| 412 |
+
"index": index,
|
| 413 |
+
"stage_id": stage_id,
|
| 414 |
+
"label": STAGE_LABELS[stage_id],
|
| 415 |
+
"short_label": _short_stage_label(stage_id),
|
| 416 |
+
"score": stage.score,
|
| 417 |
+
"status": _stage_status_key(stage_id, state),
|
| 418 |
+
"active": stage_id == active_stage_id,
|
| 419 |
+
}
|
| 420 |
+
)
|
| 421 |
+
return rows
|
| 422 |
+
|
| 423 |
+
|
| 424 |
+
def render_workflow_header(state, active_stage_id: str) -> str:
|
| 425 |
+
active_stage_id = _safe_stage_id(active_stage_id)
|
| 426 |
+
summary = workflow_progress_summary(state)
|
| 427 |
+
config = STAGE_UI_CONFIG[active_stage_id]
|
| 428 |
+
average_score = summary["average_stage_score"]
|
| 429 |
+
score_label = "No scores yet" if average_score is None else f"{average_score}/100"
|
| 430 |
+
percent = summary["percent"]
|
| 431 |
+
return f"""
|
| 432 |
+
<div class="workflow-shell">
|
| 433 |
+
<div class="workflow-title-row">
|
| 434 |
+
<div>
|
| 435 |
+
<div class="workflow-kicker">Course Slide Factory</div>
|
| 436 |
+
<h2>{_escape(STAGE_LABELS[active_stage_id])}</h2>
|
| 437 |
+
<p>{_escape(config["focus"])}</p>
|
| 438 |
+
</div>
|
| 439 |
+
<div class="workflow-progress-card">
|
| 440 |
+
<strong>{summary["percent"]}%</strong>
|
| 441 |
+
<span>{summary["complete"]}/{summary["total"]} stages complete</span>
|
| 442 |
+
</div>
|
| 443 |
+
</div>
|
| 444 |
+
<div class="workflow-progress-track">
|
| 445 |
+
<div class="workflow-progress-fill" style="width: {percent}%"></div>
|
| 446 |
+
</div>
|
| 447 |
+
<div class="workflow-metrics">
|
| 448 |
+
<div class="workflow-metric"><span>Average stage score</span><strong>{_escape(score_label)}</strong></div>
|
| 449 |
+
<div class="workflow-metric"><span>Unresolved blockers</span><strong>{summary["unresolved_blockers"]}</strong></div>
|
| 450 |
+
<div class="workflow-metric"><span>Stale stages</span><strong>{summary["stale"]}</strong></div>
|
| 451 |
+
<div class="workflow-metric"><span>Current action</span><strong>{_escape(config["primary"])}</strong></div>
|
| 452 |
+
</div>
|
| 453 |
+
</div>
|
| 454 |
+
"""
|
| 455 |
+
|
| 456 |
+
|
| 457 |
+
def render_stage_score_strip(state, active_stage_id: str) -> str:
|
| 458 |
+
chips = []
|
| 459 |
+
for row in stage_score_rows(state, active_stage_id):
|
| 460 |
+
score = "No score" if row["score"] is None else f"{row['score']}/100"
|
| 461 |
+
classes = ["stage-score-chip", row["status"]]
|
| 462 |
+
if row["active"]:
|
| 463 |
+
classes.append("active")
|
| 464 |
+
chips.append(
|
| 465 |
+
f"""
|
| 466 |
+
<div class="{_escape(" ".join(classes))}">
|
| 467 |
+
<div class="stage-chip-top">
|
| 468 |
+
<span class="stage-chip-number">{row["index"]:02d}</span>
|
| 469 |
+
<span class="stage-chip-score">{_escape(score)}</span>
|
| 470 |
+
</div>
|
| 471 |
+
<div class="stage-chip-label">{_escape(row["short_label"])}</div>
|
| 472 |
+
<div class="stage-chip-state">{_escape(row["status"].replace("_", " "))}</div>
|
| 473 |
+
</div>
|
| 474 |
+
"""
|
| 475 |
+
)
|
| 476 |
+
return f'<div class="stage-score-grid">{"".join(chips)}</div>'
|
| 477 |
+
|
| 478 |
+
|
| 479 |
+
def stage_visibility_updates(active_stage_id: str):
|
| 480 |
+
active_stage_id = _safe_stage_id(active_stage_id)
|
| 481 |
+
return tuple(gr.update(visible=stage_id == active_stage_id) for stage_id in STAGE_IDS)
|
| 482 |
+
|
| 483 |
+
|
| 484 |
+
def workspace_visibility_updates(active_stage_id: str):
|
| 485 |
+
active_stage_id = _safe_stage_id(active_stage_id)
|
| 486 |
+
return (
|
| 487 |
+
gr.update(visible=active_stage_id in ARTIFACT_WORKSPACE_STAGES),
|
| 488 |
+
gr.update(visible=active_stage_id in REVIEW_WORKSPACE_STAGES),
|
| 489 |
+
gr.update(visible=active_stage_id in FINAL_WORKSPACE_STAGES),
|
| 490 |
+
gr.update(visible=active_stage_id in AUDIT_WORKSPACE_STAGES),
|
| 491 |
+
gr.update(visible=active_stage_id in HISTORY_WORKSPACE_STAGES),
|
| 492 |
+
)
|
| 493 |
+
|
| 494 |
+
|
| 495 |
+
def stage_step_button_updates(active_stage_id: str):
|
| 496 |
+
active_stage_id = _safe_stage_id(active_stage_id)
|
| 497 |
+
return tuple(
|
| 498 |
+
gr.update(
|
| 499 |
+
value=f"{index + 1}. {_short_stage_label(stage_id)}",
|
| 500 |
+
variant="primary" if stage_id == active_stage_id else "secondary",
|
| 501 |
+
)
|
| 502 |
+
for index, stage_id in enumerate(STAGE_IDS)
|
| 503 |
+
)
|
| 504 |
+
|
| 505 |
+
|
| 506 |
+
def _render_chrome_outputs(state, active_stage_id: str):
|
| 507 |
+
active_stage_id = _safe_stage_id(active_stage_id)
|
| 508 |
+
return (
|
| 509 |
+
render_workflow_header(state, active_stage_id),
|
| 510 |
+
render_stage_score_strip(state, active_stage_id),
|
| 511 |
+
*stage_visibility_updates(active_stage_id),
|
| 512 |
+
*workspace_visibility_updates(active_stage_id),
|
| 513 |
+
*stage_step_button_updates(active_stage_id),
|
| 514 |
+
)
|
| 515 |
+
|
| 516 |
+
|
| 517 |
+
def _stage_nav_defaults(stage_id: str) -> tuple[str, str]:
|
| 518 |
+
return REVIEW_STAGE_DEFAULTS.get(stage_id, ("all", "general_reviewer"))
|
| 519 |
+
|
| 520 |
|
| 521 |
def _has_file(file_value) -> bool:
|
| 522 |
if not file_value:
|
|
|
|
| 621 |
change_set_id: str = "",
|
| 622 |
prompt_run_id: str = "",
|
| 623 |
):
|
| 624 |
+
stage_id = _safe_stage_id(stage_id)
|
| 625 |
lock_reasons = "\n".join(get_stage_lock_reasons(stage_id, state)) or "Unlocked"
|
| 626 |
role_summary = review_mode_summary_text(state, review_role)
|
| 627 |
review_queue = review_queue_table(
|
|
|
|
| 666 |
prompt_runs,
|
| 667 |
prompt_detail,
|
| 668 |
metrics,
|
| 669 |
+
*_render_chrome_outputs(state, stage_id),
|
| 670 |
)
|
| 671 |
|
| 672 |
|
|
|
|
| 800 |
return _render_outputs(state, stage_id, message)
|
| 801 |
|
| 802 |
|
| 803 |
+
def handle_stage_nav(state_data, target_stage_id):
|
| 804 |
+
target_stage_id = _safe_stage_id(target_stage_id)
|
| 805 |
state = state_from_dict(state_data)
|
| 806 |
+
queue_stage, review_role = _stage_nav_defaults(target_stage_id)
|
| 807 |
+
return (
|
| 808 |
+
target_stage_id,
|
| 809 |
+
queue_stage,
|
| 810 |
+
review_role,
|
| 811 |
+
*_render_outputs(
|
| 812 |
+
state,
|
| 813 |
+
target_stage_id,
|
| 814 |
+
f"Selected {STAGE_LABELS[target_stage_id]}.",
|
| 815 |
+
review_role=review_role,
|
| 816 |
+
queue_stage=queue_stage,
|
| 817 |
+
),
|
| 818 |
+
)
|
| 819 |
|
| 820 |
|
| 821 |
def _constraints_from_inputs(
|
|
|
|
| 1102 |
("Suggest layout switch", "suggest_layout_switch"),
|
| 1103 |
("Add alt text", "add_alt_text"),
|
| 1104 |
]
|
| 1105 |
+
initial_state = build_empty_state(deck_title="Course Slide Deck", source_url="", template_url="mock://template/course")
|
| 1106 |
+
|
| 1107 |
with gr.Blocks(title="Course Slide Factory") as demo:
|
| 1108 |
state_store = gr.State()
|
| 1109 |
+
stage_id = gr.Textbox(value=STAGE_IDS[0], visible=False)
|
| 1110 |
+
|
| 1111 |
+
with gr.Group(elem_classes=["workflow-topbar"]):
|
| 1112 |
+
workflow_header = gr.Markdown(render_workflow_header(initial_state, STAGE_IDS[0]))
|
| 1113 |
+
with gr.Row(elem_classes=["stage-nav-row"]):
|
| 1114 |
+
stage_nav_buttons = [
|
| 1115 |
+
gr.Button(
|
| 1116 |
+
f"{index + 1}. {_short_stage_label(nav_stage_id)}",
|
| 1117 |
+
variant="primary" if nav_stage_id == STAGE_IDS[0] else "secondary",
|
| 1118 |
+
size="sm",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1119 |
)
|
| 1120 |
+
for index, nav_stage_id in enumerate(STAGE_IDS)
|
| 1121 |
+
]
|
| 1122 |
+
stage_score_strip = gr.Markdown(render_stage_score_strip(initial_state, STAGE_IDS[0]))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1123 |
|
| 1124 |
+
with gr.Row():
|
| 1125 |
with gr.Column(scale=2):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1126 |
status = gr.Markdown()
|
| 1127 |
+
with gr.Column(scale=1, min_width=240):
|
| 1128 |
+
reviewer_name = gr.Textbox(label="Reviewer", value="human_reviewer")
|
| 1129 |
+
|
| 1130 |
+
stage_screens: list[Any] = []
|
| 1131 |
+
|
| 1132 |
+
with gr.Group(visible=True, elem_classes=["screen-card"]) as setup_screen:
|
| 1133 |
+
gr.Markdown(
|
| 1134 |
+
"### Setup & Inputs\n"
|
| 1135 |
+
"Choose how the deck starts, collect the source material, and save the operating settings."
|
| 1136 |
+
)
|
| 1137 |
+
with gr.Row():
|
| 1138 |
+
with gr.Column(scale=1, min_width=320):
|
| 1139 |
+
deck_title = gr.Textbox(label="Deck title", value="Course Slide Deck")
|
| 1140 |
+
gr.Markdown("#### Start path")
|
| 1141 |
+
with gr.Row():
|
| 1142 |
+
instructions_path = gr.Button("Instructions file")
|
| 1143 |
+
outline_path = gr.Button("Input outline")
|
| 1144 |
+
draft_path = gr.Button("Draft deck")
|
| 1145 |
+
start_mode = gr.Textbox(value="instructions_file", visible=False)
|
| 1146 |
+
selected_start_path = gr.Markdown(START_PATH_SUMMARIES["instructions_file"])
|
| 1147 |
+
with gr.Group(visible=True) as instructions_panel:
|
| 1148 |
+
material_files = gr.File(
|
| 1149 |
+
label="Instructions file",
|
| 1150 |
+
file_count="multiple",
|
| 1151 |
+
file_types=[".txt", ".md", ".csv", ".json", ".pdf", ".docx", ".pptx"],
|
| 1152 |
+
type="filepath",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1153 |
)
|
| 1154 |
+
source_url = gr.Textbox(label="Material URL", value="")
|
| 1155 |
+
with gr.Group(visible=False) as outline_panel:
|
| 1156 |
+
outline_text = gr.Textbox(
|
| 1157 |
+
label="Outline",
|
| 1158 |
+
lines=8,
|
| 1159 |
+
placeholder="Opening hook\nCore concept\nWorked example\nPractice prompt\nWrap-up",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1160 |
)
|
| 1161 |
+
source_text = gr.Textbox(
|
| 1162 |
+
label="Input information",
|
| 1163 |
+
lines=6,
|
| 1164 |
+
placeholder="Audience, prerequisites, source notes, constraints, examples, or must-cover points.",
|
|
|
|
|
|
|
| 1165 |
)
|
| 1166 |
+
with gr.Group(visible=False) as draft_panel:
|
| 1167 |
+
draft_file = gr.File(
|
| 1168 |
+
label="Draft input required (.pptx, .pdf, or image)",
|
| 1169 |
+
file_count="single",
|
| 1170 |
+
file_types=[".pptx", ".pdf", ".png", ".jpg", ".jpeg", ".webp"],
|
| 1171 |
+
type="filepath",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1172 |
)
|
| 1173 |
+
with gr.Column(scale=1, min_width=320):
|
| 1174 |
+
template_url = gr.Textbox(label="Template URL", value="mock://template/course")
|
| 1175 |
+
output_folder_id = gr.Textbox(label="Output folder ID", value="")
|
| 1176 |
+
mutation_target_url = gr.Textbox(label="Mutation target URL", value="")
|
| 1177 |
+
with gr.Row():
|
| 1178 |
+
dry_run = gr.Checkbox(label="Dry run", value=True)
|
| 1179 |
+
production_export_requested = gr.Checkbox(label="Production export", value=False)
|
| 1180 |
+
objectives_text = gr.Textbox(
|
| 1181 |
+
label="Learning objectives",
|
| 1182 |
+
lines=8,
|
| 1183 |
+
placeholder="obj_1: Explain the core course concept.\nobj_2: Apply the concept to a worked example.",
|
| 1184 |
+
value="",
|
| 1185 |
+
)
|
| 1186 |
+
with gr.Row(elem_classes=["action-strip"]):
|
| 1187 |
+
update_setup = gr.Button("Save Setup", variant="primary")
|
| 1188 |
+
setup_preflight = gr.Button("Run Preflight")
|
| 1189 |
+
setup_generate = gr.Button("Create Setup Candidate")
|
| 1190 |
+
stage_screens.append(setup_screen)
|
| 1191 |
+
|
| 1192 |
+
with gr.Group(visible=False, elem_classes=["screen-card"]) as source_screen:
|
| 1193 |
+
gr.Markdown("### Source Extraction / Objective Mapping\nExtract source chunks and map them to learning objectives.")
|
| 1194 |
+
with gr.Row(elem_classes=["action-strip"]):
|
| 1195 |
+
source_generate = gr.Button("Generate Mapping", variant="primary")
|
| 1196 |
+
source_grade = gr.Button("Grade Mapping")
|
| 1197 |
+
source_approve = gr.Button("Approve Mapping", variant="primary")
|
| 1198 |
+
objective_matrix = gr.Dataframe(
|
| 1199 |
+
headers=[
|
| 1200 |
+
"Objective ID",
|
| 1201 |
+
"Objective",
|
| 1202 |
+
"Mapped Slides",
|
| 1203 |
+
"Coverage Score",
|
| 1204 |
+
"Coverage Status",
|
| 1205 |
+
"Evidence Count",
|
| 1206 |
+
"Issues",
|
| 1207 |
+
],
|
| 1208 |
+
interactive=False,
|
| 1209 |
+
elem_classes=["compact-panel"],
|
| 1210 |
+
)
|
| 1211 |
+
stage_screens.append(source_screen)
|
| 1212 |
+
|
| 1213 |
+
with gr.Group(visible=False, elem_classes=["screen-card"]) as outline_screen:
|
| 1214 |
+
gr.Markdown("### Slide Outline & Order\nCreate the slide sequence, inspect slide roles, and check coverage.")
|
| 1215 |
+
with gr.Row(elem_classes=["action-strip"]):
|
| 1216 |
+
outline_generate = gr.Button("Build Outline", variant="primary")
|
| 1217 |
+
outline_grade = gr.Button("Grade Outline")
|
| 1218 |
+
outline_approve = gr.Button("Approve Outline", variant="primary")
|
| 1219 |
+
slide_inventory = gr.Dataframe(
|
| 1220 |
+
headers=[
|
| 1221 |
+
"Slide #",
|
| 1222 |
+
"Title",
|
| 1223 |
+
"Role",
|
| 1224 |
+
"Objectives",
|
| 1225 |
+
"Claims",
|
| 1226 |
+
"Visuals",
|
| 1227 |
+
"Aggregate",
|
| 1228 |
+
"Technical",
|
| 1229 |
+
"Pedagogical",
|
| 1230 |
+
"Aesthetic",
|
| 1231 |
+
"Status",
|
| 1232 |
+
"Issues",
|
| 1233 |
+
"Stale?",
|
| 1234 |
+
],
|
| 1235 |
+
interactive=False,
|
| 1236 |
+
elem_classes=["compact-panel"],
|
| 1237 |
+
)
|
| 1238 |
+
stage_screens.append(outline_screen)
|
| 1239 |
+
|
| 1240 |
+
with gr.Group(visible=False, elem_classes=["screen-card"]) as title_screen:
|
| 1241 |
+
gr.Markdown("### Title Generation\nDraft, review, edit, and approve slide titles.")
|
| 1242 |
+
with gr.Row(elem_classes=["action-strip"]):
|
| 1243 |
+
title_generate = gr.Button("Generate Titles", variant="primary")
|
| 1244 |
+
title_grade = gr.Button("Grade Titles")
|
| 1245 |
+
title_save_edits = gr.Button("Save Title Edits")
|
| 1246 |
+
title_approve = gr.Button("Approve Titles", variant="primary")
|
| 1247 |
+
stage_screens.append(title_screen)
|
| 1248 |
+
|
| 1249 |
+
with gr.Group(visible=False, elem_classes=["screen-card"]) as text_screen:
|
| 1250 |
+
gr.Markdown("### Text Generation\nDraft visible slide text and speaker notes, then improve or approve the result.")
|
| 1251 |
+
with gr.Row(elem_classes=["action-strip"]):
|
| 1252 |
+
text_generate = gr.Button("Generate Slide Text", variant="primary")
|
| 1253 |
+
text_improve = gr.Button("Improve Text with AI")
|
| 1254 |
+
text_save_edits = gr.Button("Save Text Edits")
|
| 1255 |
+
text_grade = gr.Button("Grade Text")
|
| 1256 |
+
text_approve = gr.Button("Approve Text", variant="primary")
|
| 1257 |
+
stage_screens.append(text_screen)
|
| 1258 |
+
|
| 1259 |
+
with gr.Group(visible=False, elem_classes=["screen-card"]) as image_screen:
|
| 1260 |
+
gr.Markdown("### Image / Visual Asset Generation\nGenerate visual asset plans, alt text, and asset readiness metadata.")
|
| 1261 |
+
with gr.Row(elem_classes=["action-strip"]):
|
| 1262 |
+
image_generate = gr.Button("Generate Visuals", variant="primary")
|
| 1263 |
+
image_improve = gr.Button("Improve Visual Plan")
|
| 1264 |
+
image_grade = gr.Button("Grade Visuals")
|
| 1265 |
+
image_approve = gr.Button("Approve Visuals", variant="primary")
|
| 1266 |
+
stage_screens.append(image_screen)
|
| 1267 |
+
|
| 1268 |
+
with gr.Group(visible=False, elem_classes=["screen-card"]) as layout_screen:
|
| 1269 |
+
gr.Markdown(
|
| 1270 |
+
"### Aesthetic Ordering / Visual Composition\n"
|
| 1271 |
+
"Assign layouts, inspect composition, and compare the latest candidate versions."
|
| 1272 |
+
)
|
| 1273 |
+
with gr.Row(elem_classes=["action-strip"]):
|
| 1274 |
+
layout_generate = gr.Button("Compose Layout", variant="primary")
|
| 1275 |
+
layout_improve = gr.Button("Improve Composition")
|
| 1276 |
+
layout_compare = gr.Button("Compare Layout Versions")
|
| 1277 |
+
layout_grade = gr.Button("Grade Layout")
|
| 1278 |
+
layout_approve = gr.Button("Approve Layout", variant="primary")
|
| 1279 |
+
stage_screens.append(layout_screen)
|
| 1280 |
+
|
| 1281 |
+
with gr.Group(visible=False, elem_classes=["screen-card"]) as technical_screen:
|
| 1282 |
+
gr.Markdown("### Technical Review\nFocus on technical accuracy, terminology, unsupported claims, and blocking issues.")
|
| 1283 |
+
with gr.Row(elem_classes=["action-strip"]):
|
| 1284 |
+
technical_refresh = gr.Button("Refresh Technical Queue", variant="primary")
|
| 1285 |
+
technical_fix = gr.Button("Generate Technical Fix")
|
| 1286 |
+
technical_approve = gr.Button("Approve Technical Review", variant="primary")
|
| 1287 |
+
stage_screens.append(technical_screen)
|
| 1288 |
+
|
| 1289 |
+
with gr.Group(visible=False, elem_classes=["screen-card"]) as pedagogical_screen:
|
| 1290 |
+
gr.Markdown("### Pedagogical Review\nFocus on objective coverage, teaching flow, examples, and speaker notes.")
|
| 1291 |
+
with gr.Row(elem_classes=["action-strip"]):
|
| 1292 |
+
pedagogical_refresh = gr.Button("Refresh Pedagogy Queue", variant="primary")
|
| 1293 |
+
pedagogical_fix = gr.Button("Generate Pedagogy Fix")
|
| 1294 |
+
pedagogical_approve = gr.Button("Approve Pedagogical Review", variant="primary")
|
| 1295 |
+
stage_screens.append(pedagogical_screen)
|
| 1296 |
+
|
| 1297 |
+
with gr.Group(visible=False, elem_classes=["screen-card"]) as aesthetic_screen:
|
| 1298 |
+
gr.Markdown("### Aesthetic Review\nFocus on readability, accessibility, brand/template compliance, and layout consistency.")
|
| 1299 |
+
with gr.Row(elem_classes=["action-strip"]):
|
| 1300 |
+
aesthetic_refresh = gr.Button("Refresh Aesthetic Queue", variant="primary")
|
| 1301 |
+
aesthetic_fix = gr.Button("Generate Aesthetic Fix")
|
| 1302 |
+
aesthetic_approve = gr.Button("Approve Aesthetic Review", variant="primary")
|
| 1303 |
+
stage_screens.append(aesthetic_screen)
|
| 1304 |
+
|
| 1305 |
+
with gr.Group(visible=False, elem_classes=["screen-card"]) as final_screen:
|
| 1306 |
+
gr.Markdown("### Final Render & Export\nRun the final readiness checks and export the completed deck.")
|
| 1307 |
+
with gr.Row(elem_classes=["action-strip"]):
|
| 1308 |
+
final_preflight = gr.Button("Run Export Preflight", variant="primary")
|
| 1309 |
+
export = gr.Button("Final Render & Export", variant="stop")
|
| 1310 |
+
stage_screens.append(final_screen)
|
| 1311 |
+
|
| 1312 |
+
with gr.Group(visible=False, elem_classes=["screen-card"]) as audit_screen:
|
| 1313 |
+
gr.Markdown("### Audit Log / Version History\nInspect prompt runs, state, semantic diffs, and candidate versions.")
|
| 1314 |
+
with gr.Row(elem_classes=["action-strip"]):
|
| 1315 |
+
audit_compare = gr.Button("Compare Latest Versions", variant="primary")
|
| 1316 |
+
restore_candidate = gr.Button("Restore Candidate")
|
| 1317 |
+
stage_screens.append(audit_screen)
|
| 1318 |
+
|
| 1319 |
+
with gr.Group(visible=False, elem_classes=["screen-card"]) as artifact_workspace:
|
| 1320 |
+
gr.Markdown("### Current Stage Artifact")
|
| 1321 |
+
with gr.Row():
|
| 1322 |
+
with gr.Column(scale=2):
|
| 1323 |
+
artifact_editor = gr.Textbox(label="Current artifact / human edit", lines=16, interactive=True)
|
| 1324 |
+
artifact_diff = gr.Textbox(label="Artifact diff", lines=10, interactive=False)
|
| 1325 |
+
with gr.Column(scale=1, min_width=320):
|
| 1326 |
+
lock_reasons = gr.Textbox(label="Lock reasons", lines=5, interactive=False)
|
| 1327 |
+
reviewer_summary = gr.Textbox(label="Reviewer summary")
|
| 1328 |
+
requested_changes_json = gr.Textbox(label="Requested changes JSON", lines=6, value="[]")
|
| 1329 |
+
with gr.Accordion("AI revision controls", open=False):
|
| 1330 |
+
with gr.Row():
|
| 1331 |
+
preserve_slide_order = gr.Checkbox(label="Preserve slide order", value=True)
|
| 1332 |
+
preserve_slide_titles = gr.Checkbox(label="Preserve slide titles", value=False)
|
| 1333 |
+
preserve_technical_claims = gr.Checkbox(label="Preserve technical claims", value=True)
|
| 1334 |
+
with gr.Row():
|
| 1335 |
+
preserve_objective_mappings = gr.Checkbox(label="Preserve objective mappings", value=True)
|
| 1336 |
+
preserve_visual_assets = gr.Checkbox(label="Preserve visual assets", value=False)
|
| 1337 |
+
preserve_layout_ids = gr.Checkbox(label="Preserve layout IDs", value=False)
|
| 1338 |
+
with gr.Row():
|
| 1339 |
+
constraint_slide_ids = gr.Textbox(label="Selected slide IDs")
|
| 1340 |
+
allowed_fields = gr.Textbox(label="Allowed fields")
|
| 1341 |
+
blocked_fields = gr.Textbox(label="Blocked fields")
|
| 1342 |
+
reviewer_instruction = gr.Textbox(label="Reviewer instruction", lines=2)
|
| 1343 |
+
with gr.Row():
|
| 1344 |
+
propose_with_constraints = gr.Button("Critique + Propose Changes")
|
| 1345 |
+
targeted_slide_ids = gr.Textbox(label="Target slide IDs")
|
| 1346 |
+
targeted_action = gr.Dropdown(
|
| 1347 |
+
label="Selected slide action",
|
| 1348 |
+
choices=targeted_action_choices,
|
| 1349 |
+
value="rewrite_visible_text",
|
| 1350 |
+
)
|
| 1351 |
+
targeted_improvement = gr.Button("Create Targeted Changes")
|
| 1352 |
+
critique_output = gr.Textbox(label="Latest critique", lines=7, interactive=False)
|
| 1353 |
+
proposed_change_sets = gr.Dataframe(
|
| 1354 |
+
headers=["Change Set ID", "Stage", "Status", "Artifact", "Changes", "Summary", "Created At"],
|
| 1355 |
+
interactive=False,
|
| 1356 |
+
elem_classes=["compact-panel"],
|
| 1357 |
+
)
|
| 1358 |
+
with gr.Row():
|
| 1359 |
+
selected_change_set_id = gr.Textbox(label="Change set ID")
|
| 1360 |
+
selected_change_ids = gr.Textbox(label="Selected change IDs")
|
| 1361 |
+
apply_changes = gr.Button("Apply Selected Proposed Changes", variant="primary")
|
| 1362 |
+
reject_changes = gr.Button("Reject Change Set")
|
| 1363 |
+
proposed_changes = gr.Dataframe(
|
| 1364 |
+
headers=[
|
| 1365 |
+
"Change Set",
|
| 1366 |
+
"Change ID",
|
| 1367 |
+
"Target Type",
|
| 1368 |
+
"Target ID",
|
| 1369 |
+
"Field",
|
| 1370 |
+
"Before",
|
| 1371 |
+
"After",
|
| 1372 |
+
"Rationale",
|
| 1373 |
+
"Issues",
|
| 1374 |
+
"Risk",
|
| 1375 |
+
],
|
| 1376 |
+
interactive=False,
|
| 1377 |
+
elem_classes=["compact-panel"],
|
| 1378 |
+
)
|
| 1379 |
+
with gr.Group(visible=False, elem_classes=["screen-card"]) as review_workspace:
|
| 1380 |
+
gr.Markdown("### Review Workbench")
|
| 1381 |
+
with gr.Row():
|
| 1382 |
+
review_role = gr.Dropdown(label="Review role", choices=review_role_options, value="general_reviewer", interactive=True)
|
| 1383 |
+
queue_stage = gr.Dropdown(label="Stage filter", choices=queue_stage_choices, value="all", interactive=True)
|
| 1384 |
+
queue_severity = gr.Dropdown(label="Severity filter", choices=severity_choices, value="all", interactive=True)
|
| 1385 |
+
with gr.Row():
|
| 1386 |
+
queue_status = gr.Dropdown(label="Status filter", choices=status_choices, value="all", interactive=True)
|
| 1387 |
+
queue_issue_type = gr.Dropdown(label="Issue type filter", choices=issue_type_choices, value="all", interactive=True)
|
| 1388 |
+
assigned_filter = gr.Textbox(label="Assigned reviewer filter")
|
| 1389 |
+
role_summary = gr.Textbox(label="Review mode", lines=3, interactive=False)
|
| 1390 |
+
refresh_review = gr.Button("Refresh Review Workbench")
|
| 1391 |
+
review_queue = gr.Dataframe(
|
| 1392 |
+
headers=[
|
| 1393 |
+
"Severity",
|
| 1394 |
+
"Priority",
|
| 1395 |
+
"Status",
|
| 1396 |
+
"Stage",
|
| 1397 |
+
"Slide",
|
| 1398 |
+
"Role",
|
| 1399 |
+
"Issue Type",
|
| 1400 |
+
"Message",
|
| 1401 |
+
"Suggested Fix",
|
| 1402 |
+
"Assigned To",
|
| 1403 |
+
"Created At",
|
| 1404 |
+
"Issue ID",
|
| 1405 |
+
],
|
| 1406 |
+
interactive=False,
|
| 1407 |
+
elem_classes=["compact-panel"],
|
| 1408 |
+
)
|
| 1409 |
+
with gr.Row():
|
| 1410 |
+
selected_issue_id = gr.Textbox(label="Issue ID")
|
| 1411 |
+
issue_action = gr.Dropdown(
|
| 1412 |
+
label="Triage action",
|
| 1413 |
+
choices=[
|
| 1414 |
+
("Acknowledge", "acknowledge"),
|
| 1415 |
+
("Mark in progress", "in_progress"),
|
| 1416 |
+
("Resolve", "resolve"),
|
| 1417 |
+
("Waive", "waive"),
|
| 1418 |
+
("Won't fix", "wont_fix"),
|
| 1419 |
+
("Reopen", "reopen"),
|
| 1420 |
+
("Assign", "assign"),
|
| 1421 |
+
],
|
| 1422 |
+
value="acknowledge",
|
| 1423 |
+
)
|
| 1424 |
+
triage_note = gr.Textbox(label="Reason or note")
|
| 1425 |
+
with gr.Row():
|
| 1426 |
+
assigned_role = gr.Dropdown(label="Assign role", choices=[("No role", "all"), *review_role_options], value="all")
|
| 1427 |
+
assigned_to = gr.Textbox(label="Assign reviewer")
|
| 1428 |
+
triage_issue = gr.Button("Apply Triage")
|
| 1429 |
+
generate_issue_fix = gr.Button("Generate Suggested Fix")
|
| 1430 |
+
suggested_fixes = gr.Dataframe(
|
| 1431 |
+
headers=["Fix ID", "Type", "Stage", "Slides", "Issues", "Risk", "Description"],
|
| 1432 |
+
interactive=False,
|
| 1433 |
+
elem_classes=["compact-panel"],
|
| 1434 |
+
)
|
| 1435 |
+
with gr.Row():
|
| 1436 |
+
fast_generate_grade = gr.Button("Generate + Grade Current Stage")
|
| 1437 |
+
fast_draft_ahead = gr.Button("Draft Ahead Until Review Needed")
|
| 1438 |
+
with gr.Row():
|
| 1439 |
+
packet_stage_ids = gr.Textbox(label="Packet stage IDs")
|
| 1440 |
+
packet_slide_ids = gr.Textbox(label="Packet slide IDs")
|
| 1441 |
+
packet_format = gr.Radio(label="Packet format", choices=[("Markdown", "markdown"), ("JSON", "json")], value="markdown")
|
| 1442 |
+
create_packet = gr.Button("Create Review Packet")
|
| 1443 |
+
reviewer_metrics = gr.Dataframe(headers=["Metric", "Value"], interactive=False, elem_classes=["compact-panel"])
|
| 1444 |
+
issues = gr.Dataframe(
|
| 1445 |
+
headers=["Issue ID", "Type", "Severity", "Stage", "Slide", "Message", "Resolved?"],
|
| 1446 |
+
interactive=False,
|
| 1447 |
+
elem_classes=["compact-panel"],
|
| 1448 |
+
)
|
| 1449 |
+
|
| 1450 |
+
with gr.Group(visible=False, elem_classes=["screen-card"]) as final_workspace:
|
| 1451 |
+
gr.Markdown("### Export Readiness")
|
| 1452 |
+
deck_health = gr.Dataframe(headers=["Metric", "Value"], interactive=False, elem_classes=["compact-panel"])
|
| 1453 |
+
preflight_results = gr.Dataframe(headers=["Field", "Value"], interactive=False, elem_classes=["compact-panel"])
|
| 1454 |
+
stage_status = gr.Dataframe(
|
| 1455 |
+
headers=["#", "Stage", "Score", "Artifact", "Artifact Status", "Stale?", "Can Unlock Next?", "Lock Reasons"],
|
| 1456 |
+
interactive=False,
|
| 1457 |
+
elem_classes=["compact-panel"],
|
| 1458 |
+
)
|
| 1459 |
+
|
| 1460 |
+
with gr.Group(visible=False, elem_classes=["screen-card"]) as audit_workspace:
|
| 1461 |
+
gr.Markdown("### Audit & Version Details")
|
| 1462 |
+
with gr.Row():
|
| 1463 |
+
restore_artifact_id = gr.Textbox(label="Artifact version ID")
|
| 1464 |
+
audit = gr.Dataframe(
|
| 1465 |
+
headers=["Event ID", "Type", "Timestamp", "Stage", "Slide", "Issue", "Reason"],
|
| 1466 |
+
interactive=False,
|
| 1467 |
+
elem_classes=["compact-panel"],
|
| 1468 |
+
)
|
| 1469 |
+
state_json = gr.Textbox(label="State", lines=18, interactive=False)
|
| 1470 |
+
|
| 1471 |
+
with gr.Group(visible=False, elem_classes=["screen-card"]) as history_workspace:
|
| 1472 |
+
gr.Markdown("### Version History")
|
| 1473 |
+
semantic_compare = gr.Button("Compare Latest Versions")
|
| 1474 |
+
semantic_diff = gr.Textbox(label="Semantic diff", lines=8, interactive=False)
|
| 1475 |
+
prompt_runs = gr.Dataframe(
|
| 1476 |
+
headers=[
|
| 1477 |
+
"Prompt Run ID",
|
| 1478 |
+
"Created At",
|
| 1479 |
+
"Stage",
|
| 1480 |
+
"Provider",
|
| 1481 |
+
"Model",
|
| 1482 |
+
"Template",
|
| 1483 |
+
"Inputs",
|
| 1484 |
+
"Output",
|
| 1485 |
+
"Settings",
|
| 1486 |
+
],
|
| 1487 |
+
interactive=False,
|
| 1488 |
+
elem_classes=["compact-panel"],
|
| 1489 |
+
)
|
| 1490 |
+
prompt_run_id = gr.Textbox(label="Prompt run ID")
|
| 1491 |
+
prompt_detail = gr.Textbox(label="Prompt run detail", lines=8, interactive=False)
|
| 1492 |
|
| 1493 |
outputs = [
|
| 1494 |
state_store,
|
|
|
|
| 1514 |
prompt_runs,
|
| 1515 |
prompt_detail,
|
| 1516 |
reviewer_metrics,
|
| 1517 |
+
workflow_header,
|
| 1518 |
+
stage_score_strip,
|
| 1519 |
+
*stage_screens,
|
| 1520 |
+
artifact_workspace,
|
| 1521 |
+
review_workspace,
|
| 1522 |
+
final_workspace,
|
| 1523 |
+
audit_workspace,
|
| 1524 |
+
history_workspace,
|
| 1525 |
+
*stage_nav_buttons,
|
| 1526 |
]
|
| 1527 |
setup_inputs = [
|
| 1528 |
state_store,
|
|
|
|
| 1596 |
blocked_fields,
|
| 1597 |
reviewer_instruction,
|
| 1598 |
]
|
| 1599 |
+
start_path_outputs = [start_mode, selected_start_path, instructions_panel, outline_panel, draft_panel]
|
| 1600 |
+
nav_outputs = [stage_id, queue_stage, review_role, *outputs]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1601 |
|
| 1602 |
+
def _nav_handler(target_stage_id):
|
| 1603 |
+
def _handle(state_data):
|
| 1604 |
+
return handle_stage_nav(state_data, target_stage_id)
|
| 1605 |
+
|
| 1606 |
+
return _handle
|
| 1607 |
+
|
| 1608 |
+
demo.load(handle_update_setup, inputs=setup_inputs, outputs=outputs, show_progress="hidden")
|
| 1609 |
instructions_path.click(select_instructions_path, inputs=[], outputs=start_path_outputs)
|
| 1610 |
outline_path.click(select_outline_path, inputs=[], outputs=start_path_outputs)
|
| 1611 |
draft_path.click(select_draft_path, inputs=[], outputs=start_path_outputs)
|
| 1612 |
+
|
| 1613 |
+
for button, target_stage_id in zip(stage_nav_buttons, STAGE_IDS, strict=True):
|
| 1614 |
+
button.click(_nav_handler(target_stage_id), inputs=[state_store], outputs=nav_outputs, show_progress="hidden")
|
| 1615 |
+
|
| 1616 |
update_setup.click(handle_update_setup, inputs=setup_inputs, outputs=outputs)
|
| 1617 |
+
for button in [
|
| 1618 |
+
setup_generate,
|
| 1619 |
+
source_generate,
|
| 1620 |
+
outline_generate,
|
| 1621 |
+
title_generate,
|
| 1622 |
+
text_generate,
|
| 1623 |
+
image_generate,
|
| 1624 |
+
layout_generate,
|
| 1625 |
+
]:
|
| 1626 |
+
button.click(handle_generate, inputs=setup_inputs, outputs=outputs)
|
| 1627 |
+
for button in [source_grade, outline_grade, title_grade, text_grade, image_grade, layout_grade]:
|
| 1628 |
+
button.click(handle_grade, inputs=[state_store, stage_id], outputs=outputs)
|
| 1629 |
+
for button in [text_improve, image_improve, layout_improve]:
|
| 1630 |
+
button.click(handle_improve, inputs=[state_store, stage_id], outputs=outputs)
|
| 1631 |
+
for button in [
|
| 1632 |
+
source_approve,
|
| 1633 |
+
outline_approve,
|
| 1634 |
+
title_approve,
|
| 1635 |
+
text_approve,
|
| 1636 |
+
image_approve,
|
| 1637 |
+
layout_approve,
|
| 1638 |
+
technical_approve,
|
| 1639 |
+
pedagogical_approve,
|
| 1640 |
+
aesthetic_approve,
|
| 1641 |
+
]:
|
| 1642 |
+
button.click(handle_approve, inputs=[state_store, stage_id, reviewer_name, review_role], outputs=outputs)
|
| 1643 |
+
for button in [title_save_edits, text_save_edits]:
|
| 1644 |
+
button.click(
|
| 1645 |
+
handle_save_edits,
|
| 1646 |
+
inputs=[state_store, stage_id, artifact_editor, reviewer_name, reviewer_summary, requested_changes_json],
|
| 1647 |
+
outputs=outputs,
|
| 1648 |
+
)
|
| 1649 |
+
for button in [setup_preflight, final_preflight]:
|
| 1650 |
+
button.click(handle_preflight, inputs=[state_store, stage_id], outputs=outputs)
|
| 1651 |
export.click(handle_export, inputs=[state_store, stage_id], outputs=outputs)
|
| 1652 |
+
|
| 1653 |
+
for button in [refresh_review, technical_refresh, pedagogical_refresh, aesthetic_refresh]:
|
| 1654 |
+
button.click(handle_review_refresh, inputs=review_filter_inputs, outputs=outputs)
|
| 1655 |
for filter_component in [
|
| 1656 |
review_role,
|
| 1657 |
queue_stage,
|
|
|
|
| 1662 |
selected_change_set_id,
|
| 1663 |
prompt_run_id,
|
| 1664 |
]:
|
| 1665 |
+
filter_component.change(handle_review_refresh, inputs=review_filter_inputs, outputs=outputs, show_progress="hidden")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1666 |
triage_issue.click(handle_triage_issue, inputs=triage_inputs, outputs=outputs)
|
| 1667 |
+
for button in [generate_issue_fix, technical_fix, pedagogical_fix, aesthetic_fix]:
|
| 1668 |
+
button.click(handle_suggested_fix, inputs=suggested_fix_inputs, outputs=outputs)
|
| 1669 |
+
|
| 1670 |
+
propose_with_constraints.click(handle_propose_with_constraints, inputs=[state_store, stage_id, *constraint_inputs], outputs=outputs)
|
|
|
|
|
|
|
| 1671 |
targeted_improvement.click(
|
| 1672 |
handle_targeted_improvement,
|
| 1673 |
+
inputs=[state_store, stage_id, targeted_slide_ids, targeted_action, *constraint_inputs],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1674 |
outputs=outputs,
|
| 1675 |
)
|
| 1676 |
apply_changes.click(
|
|
|
|
| 1683 |
inputs=[state_store, stage_id, selected_change_set_id, reviewer_name, triage_note],
|
| 1684 |
outputs=outputs,
|
| 1685 |
)
|
| 1686 |
+
for button in [semantic_compare, layout_compare, audit_compare]:
|
| 1687 |
+
button.click(handle_semantic_compare, inputs=[state_store, stage_id], outputs=outputs)
|
| 1688 |
+
fast_generate_grade.click(handle_fast_generate_grade, inputs=[state_store, stage_id], outputs=outputs)
|
| 1689 |
+
fast_draft_ahead.click(handle_fast_draft_ahead, inputs=[state_store, stage_id], outputs=outputs)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1690 |
create_packet.click(
|
| 1691 |
handle_review_packet,
|
| 1692 |
+
inputs=[state_store, stage_id, review_role, packet_stage_ids, packet_slide_ids, packet_format],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1693 |
outputs=outputs,
|
| 1694 |
)
|
| 1695 |
restore_candidate.click(
|
tests/test_app_ui.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from app import render_stage_score_strip, stage_score_rows, workflow_progress_summary
|
| 4 |
+
from course_slide_factory.constants import STAGE_IDS
|
| 5 |
+
from course_slide_factory.models import IssueSeverity, IssueType
|
| 6 |
+
from course_slide_factory.quality import approve_current_artifact, upsert_issue
|
| 7 |
+
from course_slide_factory.workflow import build_empty_state, generate_stage
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def test_workflow_progress_summary_starts_empty():
|
| 11 |
+
state = build_empty_state(deck_title="UI Test", source_url="mock://source")
|
| 12 |
+
|
| 13 |
+
summary = workflow_progress_summary(state)
|
| 14 |
+
rows = stage_score_rows(state, "setup_inputs")
|
| 15 |
+
|
| 16 |
+
assert summary["complete"] == 0
|
| 17 |
+
assert summary["total"] == len(STAGE_IDS)
|
| 18 |
+
assert summary["percent"] == 0
|
| 19 |
+
assert rows[0]["stage_id"] == "setup_inputs"
|
| 20 |
+
assert rows[0]["active"]
|
| 21 |
+
assert rows[0]["score"] is None
|
| 22 |
+
assert rows[0]["status"] == "pending"
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def test_stage_score_rows_track_complete_review_stale_and_blockers():
|
| 26 |
+
state = build_empty_state(deck_title="UI Test", source_url="mock://source")
|
| 27 |
+
state, _message = generate_stage(state, "setup_inputs")
|
| 28 |
+
state.stages["setup_inputs"].score = 92
|
| 29 |
+
approve_current_artifact(state, "setup_inputs")
|
| 30 |
+
|
| 31 |
+
state, _message = generate_stage(state, "text_generation")
|
| 32 |
+
state.stages["text_generation"].score = 72
|
| 33 |
+
state.stages["title_generation"].is_stale = True
|
| 34 |
+
upsert_issue(
|
| 35 |
+
state,
|
| 36 |
+
IssueType.TECHNICAL_INACCURACY,
|
| 37 |
+
IssueSeverity.BLOCKER,
|
| 38 |
+
"Blocking issue",
|
| 39 |
+
stage_id="technical_review",
|
| 40 |
+
)
|
| 41 |
+
state.stages["technical_review"].is_stale = False
|
| 42 |
+
|
| 43 |
+
summary = workflow_progress_summary(state)
|
| 44 |
+
rows = {row["stage_id"]: row for row in stage_score_rows(state, "text_generation")}
|
| 45 |
+
score_strip = render_stage_score_strip(state, "text_generation")
|
| 46 |
+
|
| 47 |
+
assert summary["complete"] == 1
|
| 48 |
+
assert summary["percent"] == round(1 / len(STAGE_IDS) * 100)
|
| 49 |
+
assert summary["unresolved_blockers"] >= 1
|
| 50 |
+
assert rows["setup_inputs"]["status"] == "complete"
|
| 51 |
+
assert rows["text_generation"]["status"] == "review"
|
| 52 |
+
assert rows["text_generation"]["active"]
|
| 53 |
+
assert rows["title_generation"]["status"] == "stale"
|
| 54 |
+
assert rows["technical_review"]["status"] == "blocked"
|
| 55 |
+
assert "Text" in score_strip
|
| 56 |
+
assert "72/100" in score_strip
|