from __future__ import annotations import json import html from typing import Any import gradio as gr from course_slide_factory.constants import STAGE_IDS, STAGE_LABELS from course_slide_factory.workflow import ( apply_change_set_action, audit_table, build_empty_state, compare_latest_versions_action, create_review_packet_action, current_artifact_diff, deck_health_table, final_render_export, generate_stage, generate_suggested_fix_action, grade_stage_action, improve_with_ai, issue_table, latest_critique_text, latest_semantic_diff_text, objective_matrix_table, prompt_run_detail_text, prompt_runs_table, preflight_table, proposed_change_sets_table, proposed_changes_table, reject_change_set_action, restore_candidate_action, review_mode_summary_text, review_queue_table, review_role_choices, reviewer_metrics_table, run_draft_until_next_required_approval, run_generate_and_grade_for_current_stage, run_preflight_action, save_human_edits, suggested_fixes_table, slide_inventory_table, stage_status_table, state_from_dict, state_to_dict, targeted_slide_improvement_action, triage_issue_action, update_setup_from_inputs, approve_and_continue, ) from course_slide_factory.quality import can_unlock_next_stage, get_current_stage_artifact, get_stage_lock_reasons from course_slide_factory.models import ArtifactStatus, IssueSeverity, IssueStatus, IssueType, RevisionConstraints CSS = """ :root { color-scheme: light; } body, .gradio-container { --body-background-fill: #f7f8fa; --body-text-color: #111827; --body-text-color-subdued: #4b5563; --background-fill-primary: #ffffff; --background-fill-secondary: #f8fafc; --border-color-primary: #d9dee7; --border-color-accent: #cbd5e1; --border-color-accent-subdued: #d9dee7; --block-background-fill: #ffffff; --block-border-color: #d9dee7; --block-label-text-color: #111827; --block-label-background-fill: #f8fafc; --block-title-text-color: #111827; --input-background-fill: #ffffff; --input-background-fill-focus: #ffffff; --input-border-color: #cbd5e1; --input-border-color-focus: #2563eb; --input-placeholder-color: #6b7280; --input-text-color: #111827; --button-primary-background-fill: #f97316; --button-primary-background-fill-hover: #ea580c; --button-primary-text-color: #ffffff; --button-secondary-background-fill: #f8fafc; --button-secondary-background-fill-hover: #f1f5f9; --button-secondary-border-color: #cbd5e1; --button-secondary-border-color-hover: #94a3b8; --button-secondary-text-color: #111827; --button-secondary-text-color-hover: #111827; --checkbox-background-color: #ffffff; --checkbox-background-color-hover: #f8fafc; --checkbox-background-color-focus: #ffffff; --checkbox-background-color-selected: #f97316; --checkbox-border-color: #cbd5e1; --checkbox-border-color-hover: #94a3b8; --checkbox-border-color-focus: #f97316; --checkbox-border-color-selected: #f97316; --table-border-color: #d9dee7; --table-even-background-fill: #f8fafc; --table-odd-background-fill: #ffffff; --table-row-focus: #eff6ff; --ag-background-color: #ffffff; --ag-foreground-color: #111827; --ag-data-color: #111827; --ag-border-color: #d9dee7; --ag-header-background-color: #f8fafc; --ag-header-foreground-color: #374151; --ag-odd-row-background-color: #f8fafc; --ag-row-hover-color: #eff6ff; --ag-selected-row-background-color: #eff6ff; background: #f7f8fa !important; color: #111827 !important; } .gradio-container { max-width: 1540px !important; } .gradio-container .block, .gradio-container .form, .gradio-container .wrap, .gradio-container .contain, .gradio-container .panel, .gradio-container .input-container, .gradio-container .file-preview, .gradio-container [data-testid="block-info"], .gradio-container [data-testid="file"], .gradio-container [data-testid="textbox"] { background: #ffffff !important; color: #111827 !important; border-color: #d9dee7 !important; } .gradio-container label, .gradio-container span, .gradio-container p, .gradio-container h1, .gradio-container h2, .gradio-container h3, .gradio-container h4, .gradio-container h5, .gradio-container h6 { color: #111827 !important; } .gradio-container input, .gradio-container textarea, .gradio-container select, .gradio-container [contenteditable="true"] { background: #ffffff !important; border-color: #cbd5e1 !important; color: #111827 !important; } textarea, .cm-editor { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; } .cm-editor, .cm-scroller, .cm-gutters { background: #ffffff !important; color: #111827 !important; } .compact-panel, .gradio-container [data-testid="dataframe"], .gradio-container .dataframe, .gradio-container .table-wrap, .gradio-container .table-wrapper { background: #ffffff !important; color: #111827 !important; overflow-x: auto; } .compact-panel table, .gradio-container [data-testid="dataframe"] table, .gradio-container .dataframe table, .gradio-container .table-wrap table { font-size: 12px; } .compact-panel table, .compact-panel thead, .compact-panel tbody, .compact-panel tr, .compact-panel th, .compact-panel td, .gradio-container [data-testid="dataframe"] table, .gradio-container [data-testid="dataframe"] thead, .gradio-container [data-testid="dataframe"] tbody, .gradio-container [data-testid="dataframe"] tr, .gradio-container [data-testid="dataframe"] th, .gradio-container [data-testid="dataframe"] td, .gradio-container .dataframe table, .gradio-container .dataframe thead, .gradio-container .dataframe tbody, .gradio-container .dataframe tr, .gradio-container .dataframe th, .gradio-container .dataframe td, .gradio-container .table-wrap table, .gradio-container .table-wrap thead, .gradio-container .table-wrap tbody, .gradio-container .table-wrap tr, .gradio-container .table-wrap th, .gradio-container .table-wrap td, .compact-panel [role="grid"], .compact-panel [role="row"], .compact-panel [role="columnheader"], .compact-panel [role="gridcell"], .compact-panel .table-container, .compact-panel .virtual-table-viewport, .compact-panel .virtual-body, .compact-panel .virtual-row, .compact-panel .body-cell, .compact-panel .cell-wrap, .compact-panel .ag-root-wrapper, .compact-panel .ag-root, .compact-panel .ag-header, .compact-panel .ag-header-row, .compact-panel .ag-center-cols-container, .compact-panel .ag-center-cols-viewport, .compact-panel .ag-body, .compact-panel .ag-body-viewport, .compact-panel .ag-row, .compact-panel .ag-cell, .gradio-container [role="grid"], .gradio-container [role="row"], .gradio-container [role="columnheader"], .gradio-container [role="gridcell"], .gradio-container .table-container, .gradio-container .virtual-table-viewport, .gradio-container .virtual-body, .gradio-container .virtual-row, .gradio-container .body-cell, .gradio-container .cell-wrap, .gradio-container .ag-root-wrapper, .gradio-container .ag-root, .gradio-container .ag-header, .gradio-container .ag-header-row, .gradio-container .ag-center-cols-container, .gradio-container .ag-center-cols-viewport, .gradio-container .ag-body, .gradio-container .ag-body-viewport, .gradio-container .ag-row, .gradio-container .ag-cell { background: #ffffff !important; color: #111827 !important; border-color: #d9dee7 !important; } .compact-panel th, .gradio-container [data-testid="dataframe"] th, .gradio-container .dataframe th, .gradio-container .table-wrap th, .compact-panel .header-table, .compact-panel .header-cell, .compact-panel [role="columnheader"], .compact-panel .ag-header, .compact-panel .ag-header-cell, .gradio-container .header-table, .gradio-container .header-cell, .gradio-container [role="columnheader"], .gradio-container .ag-header, .gradio-container .ag-header-cell { background: #f8fafc !important; color: #374151 !important; font-weight: 700 !important; } .compact-panel tbody tr:nth-child(even), .gradio-container [data-testid="dataframe"] tbody tr:nth-child(even), .gradio-container .dataframe tbody tr:nth-child(even), .gradio-container .table-wrap tbody tr:nth-child(even), .compact-panel .virtual-row.row-odd, .gradio-container .virtual-row.row-odd, .compact-panel .ag-row-even, .gradio-container .ag-row-even { background: #f8fafc !important; } .compact-panel tbody tr:nth-child(even) td, .gradio-container [data-testid="dataframe"] tbody tr:nth-child(even) td, .gradio-container .dataframe tbody tr:nth-child(even) td, .gradio-container .table-wrap tbody tr:nth-child(even) td, .compact-panel .virtual-row.row-odd .body-cell, .compact-panel .virtual-row.row-odd .cell-wrap, .gradio-container .virtual-row.row-odd .body-cell, .gradio-container .virtual-row.row-odd .cell-wrap, .compact-panel .ag-row-even .ag-cell, .gradio-container .ag-row-even .ag-cell { background: #f8fafc !important; } .compact-panel .ag-row-selected, .compact-panel .ag-row-selected .ag-cell, .gradio-container .ag-row-selected, .gradio-container .ag-row-selected .ag-cell, .gradio-container tr[aria-selected="true"], .gradio-container tr[aria-selected="true"] td { background: #eff6ff !important; color: #111827 !important; } .compact-panel button, .compact-panel svg, .gradio-container [data-testid="dataframe"] button, .gradio-container [data-testid="dataframe"] svg { color: #374151 !important; } .workflow-topbar { position: sticky; top: 0; z-index: 20; background: rgba(247, 248, 250, 0.96); border-bottom: 1px solid #d9dee7; padding: 10px 0 12px; backdrop-filter: blur(8px); } .workflow-shell { border: 1px solid #d9dee7; background: #ffffff; border-radius: 8px; padding: 14px 16px; } .workflow-title-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 18px; } .workflow-kicker { color: #667085; font-size: 11px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; } .workflow-shell h2 { color: #111827; font-size: 22px; line-height: 1.2; margin: 4px 0 2px; } .workflow-shell p { color: #4b5563; margin: 0; } .workflow-progress-card { min-width: 170px; border: 1px solid #d9dee7; border-radius: 8px; padding: 10px 12px; text-align: right; background: #f9fafb; } .workflow-progress-card strong { display: block; color: #111827; font-size: 26px; line-height: 1; } .workflow-progress-card span { color: #667085; font-size: 12px; } .workflow-progress-track { height: 8px; background: #e5e7eb; border-radius: 999px; margin-top: 12px; overflow: hidden; } .workflow-progress-fill { height: 100%; background: #2563eb; border-radius: inherit; } .workflow-metrics { display: grid; gap: 8px; grid-template-columns: repeat(4, minmax(0, 1fr)); margin-top: 12px; } .workflow-metric { border: 1px solid #e5e7eb; border-radius: 8px; padding: 8px 10px; background: #ffffff; } .workflow-metric span { color: #667085; display: block; font-size: 11px; } .workflow-metric strong { color: #111827; display: block; font-size: 15px; margin-top: 2px; } .stage-score-grid { display: grid; gap: 6px; grid-template-columns: repeat(auto-fit, minmax(126px, 1fr)); } .stage-score-chip { border: 1px solid #d9dee7; border-left: 4px solid #9ca3af; border-radius: 8px; background: #ffffff; padding: 8px; min-height: 74px; } .stage-score-chip.active { border-color: #2563eb; border-left-color: #2563eb; box-shadow: inset 0 0 0 1px #2563eb; } .stage-score-chip.complete { border-left-color: #059669; } .stage-score-chip.review { border-left-color: #d97706; } .stage-score-chip.blocked { border-left-color: #dc2626; } .stage-score-chip.stale { border-left-color: #7c3aed; } .stage-score-chip.ready { border-left-color: #2563eb; } .stage-chip-top { align-items: center; display: flex; gap: 6px; justify-content: space-between; } .stage-chip-number, .stage-chip-score { color: #111827; font-size: 12px; font-weight: 700; } .stage-chip-label { color: #111827; font-size: 12px; font-weight: 600; line-height: 1.2; margin-top: 6px; } .stage-chip-state { color: #667085; font-size: 11px; margin-top: 4px; text-transform: capitalize; } .stage-nav-row button { min-height: 42px; white-space: normal !important; } .production-action, .production-action button { background: #f97316 !important; border-color: #f97316 !important; color: #ffffff !important; } .approval-action, .approval-action button { background: #059669 !important; border-color: #059669 !important; color: #ffffff !important; } .score-action, .score-action button { background: #ffffff !important; border-color: #cbd5e1 !important; color: #111827 !important; } .secondary-action, .secondary-action button { background: #f8fafc !important; border-color: #cbd5e1 !important; color: #111827 !important; } .screen-card { border: 1px solid #d9dee7; border-radius: 8px; background: #ffffff; padding: 14px; } .screen-card, .screen-card label, .screen-card span, .screen-card p, .screen-card .prose, .screen-card .markdown, .screen-card [data-testid="block-info"] { color: #111827 !important; } .screen-card h3 { color: #111827; font-size: 18px; margin: 0 0 4px; } .screen-card p { color: #4b5563; margin: 0 0 10px; } .screen-card input, .screen-card textarea, .screen-card select { background: #ffffff !important; border-color: #cbd5e1 !important; color: #111827 !important; } .screen-card input::placeholder, .screen-card textarea::placeholder { color: #6b7280 !important; } .action-strip button { min-height: 42px; } .activity-panel { padding: 0; min-height: 0; } .activity-panel p { color: #1f2937; font-weight: 600; margin: 0; } .activity-card { align-items: center; background: #ffffff; border: 1px solid #bfdbfe; border-left: 4px solid #2563eb; border-radius: 8px; display: flex; gap: 12px; min-height: 52px; padding: 12px 14px; } .activity-card::before { border-radius: 999px; content: ""; display: block; flex: 0 0 12px; height: 12px; width: 12px; } .activity-card.pending::before { animation: activityPulse 1s ease-in-out infinite; background: #2563eb; } .activity-card.complete::before { background: #059669; } .activity-card.complete { border-color: #bbf7d0; border-left-color: #059669; background: #f0fdf4; } .activity-card.warning::before { background: #d97706; } .activity-card.warning { border-color: #fde68a; border-left-color: #d97706; background: #fffbeb; } .activity-card.blocked::before { background: #dc2626; } .activity-card.blocked { border-color: #fecaca; border-left-color: #dc2626; background: #fef2f2; } .activity-label { color: #1f2937; font-size: 11px; font-weight: 800; letter-spacing: 0.06em; line-height: 1.2; text-transform: uppercase; } .activity-copy { color: #111827; font-size: 14px; font-weight: 700; line-height: 1.35; margin-top: 2px; } @keyframes activityPulse { 0% { box-shadow: 0 0 0 0 rgba(37, 99, 235, 0.45); opacity: 0.7; } 70% { box-shadow: 0 0 0 8px rgba(37, 99, 235, 0); opacity: 1; } 100% { box-shadow: 0 0 0 0 rgba(37, 99, 235, 0); opacity: 0.7; } } .stage-result-panel { margin-top: 14px; } .score-gate { border: 1px solid #d9dee7; border-radius: 8px; background: #f8fafc; margin-top: 14px; padding: 12px; } .score-gate h4 { color: #111827; font-size: 14px; margin: 0 0 8px; } .score-gate-row { align-items: stretch; } .score-gate .form, .score-gate .wrap, .score-gate .contain, .score-gate .block, .score-gate [data-testid="row"], .score-gate [data-testid="column"], .screen-card .form, .screen-card .wrap, .screen-card .contain { background: #f8fafc !important; color: #111827 !important; border-color: #d9dee7 !important; } .screen-card .block, .artifact-workspace .block { background: #ffffff !important; color: #111827 !important; border-color: #d9dee7 !important; } .stage-result-card { border: 1px solid #d9dee7; border-left: 5px solid #64748b; border-radius: 8px; background: #f8fafc; padding: 12px 14px; } .stage-result-card.scored { border-color: #bbf7d0; border-left-color: #059669; background: #f0fdf4; } .stage-result-card.review { border-color: #fde68a; border-left-color: #d97706; background: #fffbeb; } .stage-result-card h4 { color: #111827; font-size: 14px; margin: 0; } .stage-result-card p { color: #374151 !important; margin: 4px 0 0; } .stage-result-score { color: #111827; font-size: 28px; font-weight: 800; line-height: 1; margin-top: 8px; } .output-panel { border: 1px solid #d9dee7; border-radius: 8px; background: #ffffff; padding: 14px; } .output-panel, .output-panel p, .output-panel li, .output-panel strong { color: #111827 !important; } .output-panel h3 { color: #111827 !important; font-size: 16px; margin: 0 0 8px; } .output-panel code, .output-panel pre { background: #f8fafc !important; color: #111827 !important; } @media (max-width: 900px) { .workflow-title-row, .workflow-metrics { display: block; } .workflow-progress-card { margin-top: 12px; text-align: left; } } """ START_PATH_SUMMARIES = { "instructions_file": ( "**Selected path: Instructions file**\n\n" "Upload a brief, notes document, or source file. The app extracts readable text and seeds the slide outline." ), "input_outline": ( "**Selected path: Input outline**\n\n" "Paste the intended slide flow, then add source context or constraints for the generator and reviewers." ), "import_existing_draft": ( "**Selected path: Draft deck**\n\n" "Upload an existing PPTX, PDF, or image draft. The app imports it as candidate slide work." ), } PASSING_STAGE_SCORE = 80 RESOLVED_ISSUE_STATUSES = {IssueStatus.RESOLVED, IssueStatus.WAIVED, IssueStatus.WONT_FIX} COMPLETED_ARTIFACT_STATUSES = {ArtifactStatus.APPROVED, ArtifactStatus.EXPORTED} REVIEW_STAGE_DEFAULTS = { "technical_review": ("technical_review", "sme"), "pedagogical_review": ("pedagogical_review", "instructional_designer"), "aesthetic_review": ("aesthetic_review", "visual_designer"), } ARTIFACT_WORKSPACE_STAGES = { "setup_inputs", "source_extraction_objective_mapping", "slide_outline_order", "title_generation", "text_generation", "image_visual_asset_generation", "aesthetic_ordering_visual_composition", } REVIEW_WORKSPACE_STAGES = {"technical_review", "pedagogical_review", "aesthetic_review"} FINAL_WORKSPACE_STAGES = {"final_render_export"} AUDIT_WORKSPACE_STAGES = {"audit_log_version_history"} HISTORY_WORKSPACE_STAGES = ARTIFACT_WORKSPACE_STAGES | AUDIT_WORKSPACE_STAGES STAGE_UI_CONFIG = { "setup_inputs": { "short": "Setup", "focus": "Collect source material, choose the start path, and lock the deck operating settings.", "primary": "Save the working setup, create a setup snapshot, then check its score.", }, "source_extraction_objective_mapping": { "short": "Sources", "focus": "Extract usable source context and connect it to measurable learning objectives.", "primary": "Generate mapping", }, "slide_outline_order": { "short": "Outline", "focus": "Shape the instructional sequence, slide roles, and objective coverage.", "primary": "Build outline", }, "title_generation": { "short": "Titles", "focus": "Draft clear slide titles that make the course arc scannable.", "primary": "Generate titles", }, "text_generation": { "short": "Text", "focus": "Draft visible text and speaker notes with density and grounding checks.", "primary": "Generate slide text", }, "image_visual_asset_generation": { "short": "Visuals", "focus": "Create visual briefs, assets, alt text, and licensing metadata.", "primary": "Generate visuals", }, "aesthetic_ordering_visual_composition": { "short": "Layout", "focus": "Assign layouts, check slot compliance, and refine visual hierarchy.", "primary": "Compose layout", }, "technical_review": { "short": "Technical", "focus": "Review technical accuracy, terminology, and unsupported claims.", "primary": "Refresh technical queue", }, "pedagogical_review": { "short": "Pedagogy", "focus": "Review objective coverage, learning progression, examples, and teachability.", "primary": "Refresh pedagogy queue", }, "aesthetic_review": { "short": "Aesthetic", "focus": "Review readability, accessibility, visual consistency, and template fit.", "primary": "Refresh aesthetic queue", }, "final_render_export": { "short": "Export", "focus": "Run export preflight, resolve blockers, and produce the final deck artifact.", "primary": "Final render", }, "audit_log_version_history": { "short": "Audit", "focus": "Inspect audit events, prompt runs, semantic diffs, and candidate versions.", "primary": "Inspect history", }, } def _safe_stage_id(stage_id: str | None) -> str: return stage_id if stage_id in STAGE_IDS else STAGE_IDS[0] def _short_stage_label(stage_id: str) -> str: return STAGE_UI_CONFIG[stage_id]["short"] def _escape(value: object) -> str: return html.escape("" if value is None else str(value), quote=True) def render_activity_message(message: str, status: str = "complete") -> str: labels = { "pending": "Working", "complete": "Done", "warning": "Needs attention", "blocked": "Blocked", } normalized_status = status if status in labels else "complete" message_text = message or "Ready." return ( f'
' "
" f'
{labels[normalized_status]}
' f'
{_escape(message_text)}
' "
" "
" ) def render_activity_updates(message: str, status: str | None = None): if not message: return gr.update(visible=False), "" return gr.update(visible=True), render_activity_message(message, status or _activity_status_for_message(message)) def _activity_status_for_message(message: str) -> str: lower_message = (message or "").lower() if "blocked" in lower_message or "failed" in lower_message: return "blocked" if "missing" in lower_message or "not saved" in lower_message or "before generation" in lower_message: return "warning" return "complete" def pending_activity_message(message: str) -> str: return render_activity_message(message, "pending") def pending_activity_updates(message: str): return render_activity_updates(message, "pending") SCORE_GATE_STAGE_IDS = ( "setup_inputs", "source_extraction_objective_mapping", "slide_outline_order", "title_generation", "text_generation", "image_visual_asset_generation", "aesthetic_ordering_visual_composition", ) def render_stage_score_gate_panel(state, stage_id: str) -> str: stage_id = _safe_stage_id(stage_id) stage = state.stages[stage_id] score = stage.score label = STAGE_LABELS[stage_id] if score is None: return f"""

{_escape(label)} score

Run the stage score check after creating a candidate.

Pending
""" result_class = "scored" if score >= PASSING_STAGE_SCORE else "review" result_text = "Ready to continue" if score >= PASSING_STAGE_SCORE else "Needs review before continuing" return f"""

{_escape(label)} score

{_escape(result_text)}

{score}/100
""" def render_setup_score_panel(state) -> str: return render_stage_score_gate_panel(state, "setup_inputs") def _human_label(value: object) -> str: return str(value).replace("_", " ").strip().capitalize() def _human_scalar(value: object) -> str: if value is None or value == "": return "Not set" if isinstance(value, bool): return "Yes" if value else "No" return str(value) def _artifact_header(state, stage_id: str) -> list[str]: artifact = get_current_stage_artifact(stage_id, state) if artifact is None: return [f"### {STAGE_LABELS[stage_id]} output", "", "No candidate has been created for this stage yet."] score = state.stages[stage_id].score score_text = "Not scored yet" if score is None else f"{score}/100" return [ f"### {STAGE_LABELS[stage_id]} output", "", f"**Artifact:** {artifact.artifact_version_id}", f"**Status:** {_human_label(artifact.status.value)}", f"**Score:** {score_text}", ] def _format_setup_content(content: dict[str, Any]) -> list[str]: summary = content.get("input_summary", {}) lines = [ "Input summary", f"- Start path: {_human_label(content.get('start_mode', 'instructions_file'))}", f"- Source chunks: {_human_scalar(summary.get('source_chunk_count', 0))}", f"- Parsed uploads: {_human_scalar(summary.get('uploaded_material_count', 0))}", f"- Unsupported uploads: {_human_scalar(summary.get('unsupported_material_count', 0))}", f"- Learning objectives: {_human_scalar(summary.get('objective_count', 0))}", "", "Settings", f"- Deck title: {_human_scalar(content.get('deck_title'))}", f"- Template: {_human_scalar(content.get('template_url'))}", f"- Material URL: {_human_scalar(content.get('source_url'))}", f"- Dry run: {_human_scalar(content.get('dry_run'))}", f"- Production export requested: {_human_scalar(content.get('production_export_requested'))}", ] uploads = content.get("uploaded_materials") or [] if uploads: lines.extend(["", "Uploaded materials"]) for upload in uploads: filename = upload.get("filename") or "Unnamed file" extension = upload.get("extension") or "file" parsed = "parsed" if upload.get("parsed") else "not parsed" lines.append(f"- {filename} ({extension}, {parsed})") unsupported = content.get("unsupported_materials") or [] if unsupported: lines.extend(["", "Unsupported materials"]) for upload in unsupported: lines.append(f"- {upload.get('filename', 'Unnamed file')}: {upload.get('reason', 'Unsupported')}") return lines def _format_slide_rows(slides: list[dict[str, Any]]) -> list[str]: if not slides: return ["No slides generated yet."] lines = ["Slides"] for slide in slides: title = slide.get("title") or f"Slide {slide.get('slide_number', '')}".strip() role = slide.get("pedagogical_role") or "role not set" objectives = ", ".join(slide.get("objective_ids") or []) or "no objectives" lines.append(f"- {slide.get('slide_number', '?')}. {title} ({role}; {objectives})") bullets = slide.get("bullet_points") or [] if bullets: lines.append(f" Bullets: {'; '.join(str(item) for item in bullets[:3])}") visible_text = slide.get("visible_text") if visible_text: lines.append(f" Text: {visible_text}") return lines def _format_generic_value(value: Any, indent: int = 0) -> list[str]: prefix = " " * indent if isinstance(value, dict): lines: list[str] = [] for key, item in value.items(): if isinstance(item, (dict, list)): lines.append(f"{prefix}{_human_label(key)}:") lines.extend(_format_generic_value(item, indent + 1)) else: lines.append(f"{prefix}- {_human_label(key)}: {_human_scalar(item)}") return lines or [f"{prefix}No details."] if isinstance(value, list): lines = [] for item in value: if isinstance(item, dict): title = item.get("title") or item.get("slide_id") or item.get("asset_id") or item.get("layout_id") lines.append(f"{prefix}- {_human_scalar(title)}") for key, detail in item.items(): if key in {"title", "slide_id", "asset_id", "layout_id"}: continue if isinstance(detail, (dict, list)): continue lines.append(f"{prefix} {_human_label(key)}: {_human_scalar(detail)}") else: lines.append(f"{prefix}- {_human_scalar(item)}") return lines or [f"{prefix}No items."] return [f"{prefix}{_human_scalar(value)}"] def readable_current_artifact_markdown(state, stage_id: str) -> str: stage_id = _safe_stage_id(stage_id) artifact = get_current_stage_artifact(stage_id, state) lines = _artifact_header(state, stage_id) if artifact is None: return "\n".join(lines) content = artifact.metadata.get("content", {}) if not isinstance(content, dict): return "\n".join([*lines, "", _human_scalar(content)]) if "human_edit" in content: return "\n".join([*lines, "", "Human edit", _human_scalar(content.get("human_edit"))]) if stage_id == "setup_inputs": lines.extend(["", *_format_setup_content(content)]) elif stage_id == "source_extraction_objective_mapping": objectives = content.get("objectives", {}) source_chunks = content.get("source_chunks", {}) lines.extend(["", "Objectives"]) lines.extend([f"- {objective_id}: {text}" for objective_id, text in objectives.items()] or ["- No objectives mapped yet."]) lines.extend(["", "Source context"]) lines.extend( [f"- {chunk_id}: {len(str(text))} characters extracted" for chunk_id, text in source_chunks.items()] or ["- No source context extracted yet."] ) elif stage_id == "slide_outline_order": lines.extend(["", *_format_slide_rows(content.get("slides", []))]) elif stage_id == "title_generation": titles = content.get("titles", {}) lines.extend(["", "Generated titles"]) lines.extend([f"- {slide_id}: {title}" for slide_id, title in titles.items()] or ["- No titles generated yet."]) elif stage_id == "text_generation": lines.extend(["", *_format_slide_rows(content.get("slides", []))]) claims = content.get("claims", []) if claims: lines.extend(["", "Claims"]) for claim in claims: lines.append(f"- {claim.get('claim_id', 'Claim')}: {claim.get('claim_text', '')}") elif stage_id == "image_visual_asset_generation": assets = content.get("visual_assets", []) lines.extend(["", "Visual assets"]) if not assets: lines.append("- No visual assets generated yet.") for asset in assets: lines.append(f"- {asset.get('asset_id', 'Asset')}: {asset.get('description') or asset.get('visual_brief') or 'Visual plan'}") if asset.get("alt_text"): lines.append(f" Alt text: {asset.get('alt_text')}") if asset.get("license"): lines.append(f" License: {asset.get('license')}") elif stage_id == "aesthetic_ordering_visual_composition": layouts = content.get("layout_specs", []) lines.extend(["", "Layout assignments"]) if not layouts: lines.append("- No layouts assigned yet.") for layout in layouts: lines.append(f"- {layout.get('slide_id', 'Slide')}: {layout.get('layout_id', 'layout not set')}") elif stage_id == "final_render_export": lines.extend(["", "Export preflight", *_format_generic_value(content.get("preflight", content))]) else: lines.extend(["", *_format_generic_value(content)]) return "\n".join(lines) def readable_current_artifact_text(state, stage_id: str) -> str: return readable_current_artifact_markdown(state, stage_id) def advanced_human_edit_text(state, stage_id: str) -> str: stage_id = _safe_stage_id(stage_id) artifact = get_current_stage_artifact(stage_id, state) if artifact is None: return "" content = artifact.metadata.get("content", {}) if isinstance(content, dict) and "human_edit" in content: return _human_scalar(content.get("human_edit")) return readable_current_artifact_markdown(state, stage_id) def _is_unresolved_issue(issue) -> bool: return issue.status not in RESOLVED_ISSUE_STATUSES and not issue.resolved def _unresolved_blocker_count(state, stage_id: str | None = None) -> int: return sum( 1 for issue in state.issues.values() if issue.severity == IssueSeverity.BLOCKER and _is_unresolved_issue(issue) and (stage_id is None or issue.stage_id == stage_id) ) def _stage_status_key(stage_id: str, state) -> str: stage = state.stages[stage_id] artifact = get_current_stage_artifact(stage_id, state) if artifact and artifact.status in COMPLETED_ARTIFACT_STATUSES: return "complete" if stage.is_stale: return "stale" if _unresolved_blocker_count(state, stage_id) > 0: return "blocked" if artifact is None: return "pending" if stage.score is None or stage.score < PASSING_STAGE_SCORE: return "review" if can_unlock_next_stage(stage_id, state): return "ready" if get_stage_lock_reasons(stage_id, state): return "review" return "ready" def workflow_progress_summary(state) -> dict[str, Any]: total = len(STAGE_IDS) complete = 0 scored_values: list[int] = [] stale = 0 for stage_id in STAGE_IDS: stage = state.stages[stage_id] artifact = get_current_stage_artifact(stage_id, state) if artifact and artifact.status in COMPLETED_ARTIFACT_STATUSES: complete += 1 if stage.score is not None: scored_values.append(stage.score) if stage.is_stale: stale += 1 percent = round((complete / total) * 100) if total else 0 return { "complete": complete, "total": total, "percent": percent, "stale": stale, "unresolved_blockers": _unresolved_blocker_count(state), "average_stage_score": round(sum(scored_values) / len(scored_values)) if scored_values else None, } def stage_score_rows(state, active_stage_id: str) -> list[dict[str, Any]]: active_stage_id = _safe_stage_id(active_stage_id) rows = [] for index, stage_id in enumerate(STAGE_IDS, start=1): stage = state.stages[stage_id] rows.append( { "index": index, "stage_id": stage_id, "label": STAGE_LABELS[stage_id], "short_label": _short_stage_label(stage_id), "score": stage.score, "status": _stage_status_key(stage_id, state), "active": stage_id == active_stage_id, } ) return rows def render_workflow_header(state, active_stage_id: str) -> str: active_stage_id = _safe_stage_id(active_stage_id) summary = workflow_progress_summary(state) config = STAGE_UI_CONFIG[active_stage_id] average_score = summary["average_stage_score"] score_label = "No scores yet" if average_score is None else f"{average_score}/100" percent = summary["percent"] return f"""
Course Slide Factory

{_escape(STAGE_LABELS[active_stage_id])}

{_escape(config["focus"])}

{summary["percent"]}% {summary["complete"]}/{summary["total"]} stages complete
Average stage score{_escape(score_label)}
Unresolved blockers{summary["unresolved_blockers"]}
Stale stages{summary["stale"]}
Current action{_escape(config["primary"])}
""" def render_stage_score_strip(state, active_stage_id: str) -> str: chips = [] for row in stage_score_rows(state, active_stage_id): score = "No score" if row["score"] is None else f"{row['score']}/100" classes = ["stage-score-chip", row["status"]] if row["active"]: classes.append("active") chips.append( f"""
{row["index"]:02d} {_escape(score)}
{_escape(row["short_label"])}
{_escape(row["status"].replace("_", " "))}
""" ) return f'
{"".join(chips)}
' def stage_visibility_updates(active_stage_id: str): active_stage_id = _safe_stage_id(active_stage_id) return tuple(gr.update(visible=stage_id == active_stage_id) for stage_id in STAGE_IDS) def workspace_visibility_updates(active_stage_id: str): active_stage_id = _safe_stage_id(active_stage_id) return ( gr.update(visible=active_stage_id in ARTIFACT_WORKSPACE_STAGES), gr.update(visible=active_stage_id in REVIEW_WORKSPACE_STAGES), gr.update(visible=active_stage_id in FINAL_WORKSPACE_STAGES), gr.update(visible=active_stage_id in AUDIT_WORKSPACE_STAGES), gr.update(visible=active_stage_id in HISTORY_WORKSPACE_STAGES), ) def stage_step_button_updates(active_stage_id: str): active_stage_id = _safe_stage_id(active_stage_id) return tuple( gr.update( value=f"{index + 1}. {_short_stage_label(stage_id)}", variant="primary" if stage_id == active_stage_id else "secondary", ) for index, stage_id in enumerate(STAGE_IDS) ) def _render_chrome_outputs(state, active_stage_id: str): active_stage_id = _safe_stage_id(active_stage_id) return ( render_workflow_header(state, active_stage_id), render_stage_score_strip(state, active_stage_id), *(render_stage_score_gate_panel(state, stage_id) for stage_id in SCORE_GATE_STAGE_IDS), *stage_visibility_updates(active_stage_id), *workspace_visibility_updates(active_stage_id), *stage_step_button_updates(active_stage_id), ) def _stage_nav_defaults(stage_id: str) -> tuple[str, str]: return REVIEW_STAGE_DEFAULTS.get(stage_id, ("all", "general_reviewer")) def _has_file(file_value) -> bool: if not file_value: return False if isinstance(file_value, (list, tuple)): return any(bool(item) for item in file_value) return True def _has_text(value: str | None) -> bool: return bool(value and value.strip()) def _active_start_path_message(start_mode, source_url, material_files, outline_text, draft_file) -> str | None: if start_mode == "instructions_file" and not _has_file(material_files) and not _has_text(source_url): return "Instructions path selected. Upload an instructions file or provide a material URL before generation." if start_mode == "input_outline" and not _has_text(outline_text): return "Input outline path selected. Paste an outline before generation." if start_mode == "import_existing_draft" and not _has_file(draft_file): return "Draft path selected. Upload a PPTX, PDF, or image draft before generation." return None def _setup_candidate_message(state, fallback: str) -> str: artifact = get_current_stage_artifact("setup_inputs", state) artifact_id = artifact.artifact_version_id if artifact else "setup candidate" material_count = len(state.uploaded_materials) unsupported_count = len(state.unsupported_materials) objective_count = len(state.objectives) source_count = len(state.source_chunks) input_bits = [ f"{material_count} parsed upload(s)", f"{source_count} source chunk(s)", f"{objective_count} objective(s)", ] if state.source_url: input_bits.append("material URL") if unsupported_count: input_bits.append(f"{unsupported_count} unsupported upload(s)") if state.draft_upload_metadata: input_bits.append("draft import metadata") return ( f"Setup candidate created as {artifact_id}. Captured " f"{', '.join(input_bits)}. Next: click Check Setup Score, then open Sources and click Generate Mapping." if artifact else fallback ) def _select_start_path(path: str): return ( path, START_PATH_SUMMARIES[path], gr.update(visible=path == "instructions_file"), gr.update(visible=path == "input_outline"), gr.update(visible=path == "import_existing_draft"), ) def select_instructions_path(): return _select_start_path("instructions_file") def select_outline_path(): return _select_start_path("input_outline") def select_draft_path(): return _select_start_path("import_existing_draft") def _load_or_update_state( state_data: dict[str, Any] | None, deck_title: str, source_url: str, template_url: str, output_folder_id: str, dry_run: bool, objectives_text: str, material_files, source_text: str, outline_text: str, start_mode: str, draft_file, mutation_target_url: str, production_export_requested: bool, ): state = state_from_dict(state_data) if not state_data: state = build_empty_state( deck_title=deck_title, source_url=source_url, template_url=template_url, output_folder_id=output_folder_id, dry_run=dry_run, mutation_target_url=mutation_target_url, production_export_requested=production_export_requested, ) state = update_setup_from_inputs( state, deck_title=deck_title, source_url=source_url, template_url=template_url, output_folder_id=output_folder_id, dry_run=dry_run, objectives_text=objectives_text, material_files=material_files, source_text=source_text, outline_text=outline_text, start_mode=start_mode, draft_file=draft_file, mutation_target_url=mutation_target_url or None, production_export_requested=production_export_requested, ) return state def _render_outputs( state, stage_id: str, message: str, review_role: str = "general_reviewer", queue_stage: str = "all", queue_severity: str = "all", queue_status: str = "all", queue_issue_type: str = "all", assigned_filter: str = "", change_set_id: str = "", prompt_run_id: str = "", ): stage_id = _safe_stage_id(stage_id) lock_reasons = "\n".join(get_stage_lock_reasons(stage_id, state)) or "Unlocked" role_summary = review_mode_summary_text(state, review_role) review_queue = review_queue_table( state, role=review_role, stage_id=queue_stage, severity=queue_severity, status=queue_status, issue_type=queue_issue_type, assigned_to=assigned_filter, ) fixes = suggested_fixes_table(state) change_sets = proposed_change_sets_table(state) changes = proposed_changes_table(state, change_set_id or None) critique = latest_critique_text(state) semantic_diff = latest_semantic_diff_text(state) prompt_runs = prompt_runs_table(state, stage_id) prompt_detail = prompt_run_detail_text(state, prompt_run_id or None) metrics = reviewer_metrics_table(state) state_json = json.dumps(state_to_dict(state), indent=2, sort_keys=True) return ( state_to_dict(state), *render_activity_updates(message), readable_current_artifact_markdown(state, stage_id), advanced_human_edit_text(state, stage_id), current_artifact_diff(state, stage_id), lock_reasons, deck_health_table(state), slide_inventory_table(state), objective_matrix_table(state), preflight_table(state), stage_status_table(state), issue_table(state), audit_table(state), state_json, role_summary, review_queue, fixes, critique, change_sets, changes, semantic_diff, prompt_runs, prompt_detail, metrics, *_render_chrome_outputs(state, stage_id), ) def handle_update_setup( state_data, deck_title, source_url, template_url, output_folder_id, dry_run, objectives_text, material_files, source_text, outline_text, start_mode, draft_file, mutation_target_url, production_export_requested, stage_id, ): state = _load_or_update_state( state_data, deck_title, source_url, template_url, output_folder_id, dry_run, objectives_text, material_files, source_text, outline_text, start_mode, draft_file, mutation_target_url, production_export_requested, ) required_message = _active_start_path_message(start_mode, source_url, material_files, outline_text, draft_file) return _render_outputs(state, stage_id, required_message or "Setup saved in working state.") def handle_generate( state_data, deck_title, source_url, template_url, output_folder_id, dry_run, objectives_text, material_files, source_text, outline_text, start_mode, draft_file, mutation_target_url, production_export_requested, stage_id, ): state = _load_or_update_state( state_data, deck_title, source_url, template_url, output_folder_id, dry_run, objectives_text, material_files, source_text, outline_text, start_mode, draft_file, mutation_target_url, production_export_requested, ) required_message = _active_start_path_message(start_mode, source_url, material_files, outline_text, draft_file) if required_message: return _render_outputs(state, stage_id, required_message) state, message = generate_stage(state, stage_id) if stage_id == "setup_inputs": message = _setup_candidate_message(state, message) return _render_outputs(state, stage_id, message) def handle_grade(state_data, stage_id): state = state_from_dict(state_data) state, message = grade_stage_action(state, stage_id) return _render_outputs(state, stage_id, message) def handle_save_edits( state_data, stage_id, edited_content, reviewer_name, reviewer_summary, requested_changes_json, ): state = state_from_dict(state_data) try: state, message = save_human_edits( state, stage_id, edited_content, reviewer_name, reviewer_summary, requested_changes_json or "[]", ) except (json.JSONDecodeError, ValueError) as exc: message = f"Reviewer notes were not saved: {exc}" return _render_outputs(state, stage_id, message) def handle_improve(state_data, stage_id): state = state_from_dict(state_data) state, message = improve_with_ai(state, stage_id) return _render_outputs(state, stage_id, message) def handle_approve(state_data, stage_id, reviewer_name, review_role): state = state_from_dict(state_data) state, message = approve_and_continue(state, stage_id, reviewer_name, review_role) return _render_outputs(state, stage_id, message) def handle_preflight(state_data, stage_id): state = state_from_dict(state_data) state, message = run_preflight_action(state) return _render_outputs(state, stage_id, message) def handle_export(state_data, stage_id): state = state_from_dict(state_data) state, message = final_render_export(state) return _render_outputs(state, stage_id, message) def handle_stage_nav(state_data, target_stage_id): target_stage_id = _safe_stage_id(target_stage_id) state = state_from_dict(state_data) queue_stage, review_role = _stage_nav_defaults(target_stage_id) return ( target_stage_id, queue_stage, review_role, *_render_outputs( state, target_stage_id, "", review_role=review_role, queue_stage=queue_stage, ), ) def _constraints_from_inputs( preserve_slide_order, preserve_slide_titles, preserve_technical_claims, preserve_objective_mappings, preserve_visual_assets, preserve_layout_ids, selected_slide_ids, allowed_fields, blocked_fields, reviewer_instruction, ): return RevisionConstraints( preserve_slide_order=preserve_slide_order, preserve_slide_titles=preserve_slide_titles, preserve_technical_claims=preserve_technical_claims, preserve_objective_mappings=preserve_objective_mappings, preserve_visual_assets=preserve_visual_assets, preserve_layout_ids=preserve_layout_ids, selected_slide_ids=[ item.strip() for item in (selected_slide_ids or "").split(",") if item.strip() ], allowed_fields=[ item.strip() for item in (allowed_fields or "").split(",") if item.strip() ], blocked_fields=[ item.strip() for item in (blocked_fields or "").split(",") if item.strip() ], reviewer_instruction=reviewer_instruction or None, ) def handle_review_refresh( state_data, stage_id, review_role, queue_stage, queue_severity, queue_status, queue_issue_type, assigned_filter, change_set_id, prompt_run_id, ): state = state_from_dict(state_data) return _render_outputs( state, stage_id, "Review workbench refreshed.", review_role, queue_stage, queue_severity, queue_status, queue_issue_type, assigned_filter, change_set_id, prompt_run_id, ) def handle_triage_issue( state_data, stage_id, issue_id, action, reviewer_name, note, assigned_role, assigned_to, review_role, queue_stage, queue_severity, queue_status, queue_issue_type, assigned_filter, change_set_id, prompt_run_id, ): state = state_from_dict(state_data) state, message = triage_issue_action( state, issue_id, action, reviewer_name, note, assigned_role, assigned_to, ) return _render_outputs( state, stage_id, message, review_role, queue_stage, queue_severity, queue_status, queue_issue_type, assigned_filter, change_set_id, prompt_run_id, ) def handle_suggested_fix( state_data, stage_id, issue_id, review_role, queue_stage, queue_severity, queue_status, queue_issue_type, assigned_filter, change_set_id, prompt_run_id, ): state = state_from_dict(state_data) state, message = generate_suggested_fix_action(state, issue_id) return _render_outputs( state, stage_id, message, review_role, queue_stage, queue_severity, queue_status, queue_issue_type, assigned_filter, change_set_id, prompt_run_id, ) def handle_propose_with_constraints( state_data, stage_id, preserve_slide_order, preserve_slide_titles, preserve_technical_claims, preserve_objective_mappings, preserve_visual_assets, preserve_layout_ids, selected_slide_ids, allowed_fields, blocked_fields, reviewer_instruction, ): state = state_from_dict(state_data) constraints = _constraints_from_inputs( preserve_slide_order, preserve_slide_titles, preserve_technical_claims, preserve_objective_mappings, preserve_visual_assets, preserve_layout_ids, selected_slide_ids, allowed_fields, blocked_fields, reviewer_instruction, ) state, message = improve_with_ai(state, stage_id, constraints) return _render_outputs(state, stage_id, message) def handle_targeted_improvement( state_data, stage_id, slide_ids, targeted_action, preserve_slide_order, preserve_slide_titles, preserve_technical_claims, preserve_objective_mappings, preserve_visual_assets, preserve_layout_ids, selected_slide_ids, allowed_fields, blocked_fields, reviewer_instruction, ): state = state_from_dict(state_data) constraints = _constraints_from_inputs( preserve_slide_order, preserve_slide_titles, preserve_technical_claims, preserve_objective_mappings, preserve_visual_assets, preserve_layout_ids, selected_slide_ids or slide_ids, allowed_fields, blocked_fields, reviewer_instruction, ) state, message = targeted_slide_improvement_action( state, stage_id, slide_ids, targeted_action, constraints, ) return _render_outputs(state, stage_id, message) def handle_apply_change_set(state_data, stage_id, change_set_id, selected_change_ids, reviewer_name): state = state_from_dict(state_data) state, message = apply_change_set_action( state, change_set_id, selected_change_ids, reviewer_name, ) return _render_outputs(state, stage_id, message, change_set_id=change_set_id) def handle_reject_change_set(state_data, stage_id, change_set_id, reviewer_name, reason): state = state_from_dict(state_data) state, message = reject_change_set_action(state, change_set_id, reviewer_name, reason) return _render_outputs(state, stage_id, message, change_set_id=change_set_id) def handle_fast_generate_grade(state_data, stage_id): state = state_from_dict(state_data) run_generate_and_grade_for_current_stage(stage_id, state) return _render_outputs(state, stage_id, "Generated and graded current stage.") def handle_fast_draft_ahead(state_data, stage_id): state = state_from_dict(state_data) run_draft_until_next_required_approval(stage_id, state) return _render_outputs(state, stage_id, "Drafted ahead until review was required.") def handle_semantic_compare(state_data, stage_id): state = state_from_dict(state_data) state, message = compare_latest_versions_action(state, stage_id) return _render_outputs(state, stage_id, message) def handle_review_packet( state_data, stage_id, review_role, packet_stage_ids, packet_slide_ids, packet_format, ): state = state_from_dict(state_data) state, message = create_review_packet_action( state, review_role, packet_stage_ids, packet_slide_ids, packet_format, ) return _render_outputs(state, stage_id, message, review_role=review_role) def handle_restore_candidate(state_data, stage_id, artifact_version_id, reviewer_name): state = state_from_dict(state_data) state, message = restore_candidate_action(state, artifact_version_id, reviewer_name) return _render_outputs(state, stage_id, message) def build_app() -> gr.Blocks: stage_choices = [(f"{index + 1}. {STAGE_LABELS[stage_id]}", stage_id) for index, stage_id in enumerate(STAGE_IDS)] queue_stage_choices = [("All stages", "all"), *stage_choices] review_role_options = review_role_choices() severity_choices = [("All severities", "all"), *[(item.value, item.value) for item in IssueSeverity]] status_choices = [("All statuses", "all"), *[(item.value, item.value) for item in IssueStatus]] issue_type_choices = [("All issue types", "all"), *[(item.value, item.value) for item in IssueType]] targeted_action_choices = [ ("Improve title", "rewrite_title"), ("Improve visible text", "rewrite_visible_text"), ("Improve speaker notes", "rewrite_speaker_notes"), ("Improve visual brief", "improve_visual_brief"), ("Suggest layout switch", "suggest_layout_switch"), ("Add alt text", "add_alt_text"), ] initial_state = build_empty_state(deck_title="Course Slide Deck", source_url="", template_url="mock://template/course") with gr.Blocks(title="Course Slide Factory") as demo: state_store = gr.State() stage_id = gr.Textbox(value=STAGE_IDS[0], visible=False) with gr.Group(elem_classes=["workflow-topbar"]): workflow_header = gr.Markdown(render_workflow_header(initial_state, STAGE_IDS[0])) with gr.Row(elem_classes=["stage-nav-row"]): stage_nav_buttons = [ gr.Button( f"{index + 1}. {_short_stage_label(nav_stage_id)}", variant="primary" if nav_stage_id == STAGE_IDS[0] else "secondary", size="sm", ) for index, nav_stage_id in enumerate(STAGE_IDS) ] stage_score_strip = gr.Markdown(render_stage_score_strip(initial_state, STAGE_IDS[0])) with gr.Row(): with gr.Column(scale=2): with gr.Group(visible=False, elem_classes=["status-region"]) as status_region: status = gr.Markdown(elem_classes=["activity-panel"]) with gr.Column(scale=1, min_width=240): reviewer_name = gr.Textbox(label="Reviewer", value="human_reviewer") stage_screens: list[Any] = [] with gr.Group(visible=True, elem_classes=["screen-card"]) as setup_screen: gr.Markdown( "### Setup & Inputs\n" "Choose how the deck starts, collect the source material, and save the operating settings." ) with gr.Row(): with gr.Column(scale=1, min_width=320): deck_title = gr.Textbox(label="Deck title", value="Course Slide Deck") gr.Markdown("#### Start path") with gr.Row(): instructions_path = gr.Button("Instructions file") outline_path = gr.Button("Input outline") draft_path = gr.Button("Draft deck") start_mode = gr.Textbox(value="instructions_file", visible=False) selected_start_path = gr.Markdown(START_PATH_SUMMARIES["instructions_file"]) with gr.Group(visible=True) as instructions_panel: material_files = gr.File( label="Instructions file", file_count="multiple", file_types=[".txt", ".md", ".csv", ".json", ".pdf", ".docx", ".pptx"], type="filepath", ) source_url = gr.Textbox(label="Material URL", value="") with gr.Group(visible=False) as outline_panel: outline_text = gr.Textbox( label="Outline", lines=8, placeholder="Opening hook\nCore concept\nWorked example\nPractice prompt\nWrap-up", ) source_text = gr.Textbox( label="Input information", lines=6, placeholder="Audience, prerequisites, source notes, constraints, examples, or must-cover points.", ) with gr.Group(visible=False) as draft_panel: draft_file = gr.File( label="Draft input required (.pptx, .pdf, or image)", file_count="single", file_types=[".pptx", ".pdf", ".png", ".jpg", ".jpeg", ".webp"], type="filepath", ) with gr.Column(scale=1, min_width=320): template_url = gr.Textbox(label="Template URL", value="mock://template/course") output_folder_id = gr.Textbox(label="Output folder ID", value="") mutation_target_url = gr.Textbox(label="Mutation target URL", value="") with gr.Row(): dry_run = gr.Checkbox(label="Dry run", value=True) production_export_requested = gr.Checkbox(label="Production export", value=False) objectives_text = gr.Textbox( label="Learning objectives", lines=8, placeholder="obj_1: Explain the core course concept.\nobj_2: Apply the concept to a worked example.", value="", ) with gr.Row(elem_classes=["action-strip"]): update_setup = gr.Button("Save Setup", elem_classes=["secondary-action"]) setup_generate = gr.Button("Create Setup Candidate", variant="primary", elem_classes=["production-action"]) with gr.Group(elem_classes=["score-gate"]): gr.Markdown("#### Setup Score & Gate") with gr.Row(elem_classes=["score-gate-row"]): with gr.Column(scale=1, min_width=220): setup_grade = gr.Button("Check Setup Score", elem_classes=["score-action"]) with gr.Column(scale=3, min_width=320): setup_score_panel = gr.Markdown( render_setup_score_panel(initial_state), elem_classes=["stage-result-panel"], ) stage_screens.append(setup_screen) with gr.Group(visible=False, elem_classes=["screen-card"]) as source_screen: gr.Markdown("### Source Extraction / Objective Mapping\nExtract source chunks and map them to learning objectives.") with gr.Row(elem_classes=["action-strip"]): source_generate = gr.Button("Generate Mapping", variant="primary", elem_classes=["production-action"]) source_approve = gr.Button("Approve Mapping", variant="primary", elem_classes=["approval-action"]) objective_matrix = gr.Dataframe( headers=[ "Objective ID", "Objective", "Mapped Slides", "Coverage Score", "Coverage Status", "Evidence Count", "Issues", ], interactive=False, elem_classes=["compact-panel"], ) with gr.Group(elem_classes=["score-gate"]): gr.Markdown("#### Mapping Score & Gate") with gr.Row(elem_classes=["score-gate-row"]): with gr.Column(scale=1, min_width=220): source_grade = gr.Button("Grade Mapping", elem_classes=["score-action"]) with gr.Column(scale=3, min_width=320): source_score_panel = gr.Markdown( render_stage_score_gate_panel(initial_state, "source_extraction_objective_mapping"), elem_classes=["stage-result-panel"], ) stage_screens.append(source_screen) with gr.Group(visible=False, elem_classes=["screen-card"]) as outline_screen: gr.Markdown("### Slide Outline & Order\nCreate the slide sequence, inspect slide roles, and check coverage.") with gr.Row(elem_classes=["action-strip"]): outline_generate = gr.Button("Build Outline", variant="primary", elem_classes=["production-action"]) outline_approve = gr.Button("Approve Outline", variant="primary", elem_classes=["approval-action"]) slide_inventory = gr.Dataframe( headers=[ "Slide #", "Title", "Role", "Objectives", "Claims", "Visuals", "Aggregate", "Technical", "Pedagogical", "Aesthetic", "Status", "Issues", "Stale?", ], interactive=False, elem_classes=["compact-panel"], ) with gr.Group(elem_classes=["score-gate"]): gr.Markdown("#### Outline Score & Gate") with gr.Row(elem_classes=["score-gate-row"]): with gr.Column(scale=1, min_width=220): outline_grade = gr.Button("Grade Outline", elem_classes=["score-action"]) with gr.Column(scale=3, min_width=320): outline_score_panel = gr.Markdown( render_stage_score_gate_panel(initial_state, "slide_outline_order"), elem_classes=["stage-result-panel"], ) stage_screens.append(outline_screen) with gr.Group(visible=False, elem_classes=["screen-card"]) as title_screen: gr.Markdown("### Title Generation\nDraft, review, edit, and approve slide titles.") with gr.Row(elem_classes=["action-strip"]): title_generate = gr.Button("Generate Titles", variant="primary", elem_classes=["production-action"]) title_save_edits = gr.Button("Save Title Edits", elem_classes=["secondary-action"]) title_approve = gr.Button("Approve Titles", variant="primary", elem_classes=["approval-action"]) with gr.Group(elem_classes=["score-gate"]): gr.Markdown("#### Titles Score & Gate") with gr.Row(elem_classes=["score-gate-row"]): with gr.Column(scale=1, min_width=220): title_grade = gr.Button("Grade Titles", elem_classes=["score-action"]) with gr.Column(scale=3, min_width=320): title_score_panel = gr.Markdown( render_stage_score_gate_panel(initial_state, "title_generation"), elem_classes=["stage-result-panel"], ) stage_screens.append(title_screen) with gr.Group(visible=False, elem_classes=["screen-card"]) as text_screen: gr.Markdown("### Text Generation\nDraft visible slide text and speaker notes, then improve or approve the result.") with gr.Row(elem_classes=["action-strip"]): text_generate = gr.Button("Generate Slide Text", variant="primary", elem_classes=["production-action"]) text_improve = gr.Button("Improve Text with AI", elem_classes=["secondary-action"]) text_save_edits = gr.Button("Save Text Edits", elem_classes=["secondary-action"]) text_approve = gr.Button("Approve Text", variant="primary", elem_classes=["approval-action"]) with gr.Group(elem_classes=["score-gate"]): gr.Markdown("#### Text Score & Gate") with gr.Row(elem_classes=["score-gate-row"]): with gr.Column(scale=1, min_width=220): text_grade = gr.Button("Grade Text", elem_classes=["score-action"]) with gr.Column(scale=3, min_width=320): text_score_panel = gr.Markdown( render_stage_score_gate_panel(initial_state, "text_generation"), elem_classes=["stage-result-panel"], ) stage_screens.append(text_screen) with gr.Group(visible=False, elem_classes=["screen-card"]) as image_screen: gr.Markdown("### Image / Visual Asset Generation\nGenerate visual asset plans, alt text, and asset readiness metadata.") with gr.Row(elem_classes=["action-strip"]): image_generate = gr.Button("Generate Visuals", variant="primary", elem_classes=["production-action"]) image_improve = gr.Button("Improve Visual Plan", elem_classes=["secondary-action"]) image_approve = gr.Button("Approve Visuals", variant="primary", elem_classes=["approval-action"]) with gr.Group(elem_classes=["score-gate"]): gr.Markdown("#### Visuals Score & Gate") with gr.Row(elem_classes=["score-gate-row"]): with gr.Column(scale=1, min_width=220): image_grade = gr.Button("Grade Visuals", elem_classes=["score-action"]) with gr.Column(scale=3, min_width=320): image_score_panel = gr.Markdown( render_stage_score_gate_panel(initial_state, "image_visual_asset_generation"), elem_classes=["stage-result-panel"], ) stage_screens.append(image_screen) with gr.Group(visible=False, elem_classes=["screen-card"]) as layout_screen: gr.Markdown( "### Aesthetic Ordering / Visual Composition\n" "Assign layouts, inspect composition, and compare the latest candidate versions." ) with gr.Row(elem_classes=["action-strip"]): layout_generate = gr.Button("Compose Layout", variant="primary", elem_classes=["production-action"]) layout_improve = gr.Button("Improve Composition", elem_classes=["secondary-action"]) layout_compare = gr.Button("Compare Layout Versions", elem_classes=["secondary-action"]) layout_approve = gr.Button("Approve Layout", variant="primary", elem_classes=["approval-action"]) with gr.Group(elem_classes=["score-gate"]): gr.Markdown("#### Layout Score & Gate") with gr.Row(elem_classes=["score-gate-row"]): with gr.Column(scale=1, min_width=220): layout_grade = gr.Button("Grade Layout", elem_classes=["score-action"]) with gr.Column(scale=3, min_width=320): layout_score_panel = gr.Markdown( render_stage_score_gate_panel(initial_state, "aesthetic_ordering_visual_composition"), elem_classes=["stage-result-panel"], ) stage_screens.append(layout_screen) with gr.Group(visible=False, elem_classes=["screen-card"]) as technical_screen: gr.Markdown("### Technical Review\nFocus on technical accuracy, terminology, unsupported claims, and blocking issues.") with gr.Row(elem_classes=["action-strip"]): technical_refresh = gr.Button("Refresh Technical Queue", elem_classes=["secondary-action"]) technical_fix = gr.Button("Generate Technical Fix", variant="primary", elem_classes=["production-action"]) technical_approve = gr.Button("Approve Technical Review", variant="primary", elem_classes=["approval-action"]) stage_screens.append(technical_screen) with gr.Group(visible=False, elem_classes=["screen-card"]) as pedagogical_screen: gr.Markdown("### Pedagogical Review\nFocus on objective coverage, teaching flow, examples, and speaker notes.") with gr.Row(elem_classes=["action-strip"]): pedagogical_refresh = gr.Button("Refresh Pedagogy Queue", elem_classes=["secondary-action"]) pedagogical_fix = gr.Button("Generate Pedagogy Fix", variant="primary", elem_classes=["production-action"]) pedagogical_approve = gr.Button("Approve Pedagogical Review", variant="primary", elem_classes=["approval-action"]) stage_screens.append(pedagogical_screen) with gr.Group(visible=False, elem_classes=["screen-card"]) as aesthetic_screen: gr.Markdown("### Aesthetic Review\nFocus on readability, accessibility, brand/template compliance, and layout consistency.") with gr.Row(elem_classes=["action-strip"]): aesthetic_refresh = gr.Button("Refresh Aesthetic Queue", elem_classes=["secondary-action"]) aesthetic_fix = gr.Button("Generate Aesthetic Fix", variant="primary", elem_classes=["production-action"]) aesthetic_approve = gr.Button("Approve Aesthetic Review", variant="primary", elem_classes=["approval-action"]) stage_screens.append(aesthetic_screen) with gr.Group(visible=False, elem_classes=["screen-card"]) as final_screen: gr.Markdown("### Final Render & Export\nRun the final readiness checks and export the completed deck.") with gr.Row(elem_classes=["action-strip"]): final_preflight = gr.Button("Run Export Preflight", elem_classes=["secondary-action"]) export = gr.Button("Final Render & Export", variant="primary", elem_classes=["production-action"]) stage_screens.append(final_screen) with gr.Group(visible=False, elem_classes=["screen-card"]) as audit_screen: gr.Markdown("### Audit Log / Version History\nInspect prompt runs, state, semantic diffs, and candidate versions.") with gr.Row(elem_classes=["action-strip"]): audit_compare = gr.Button("Compare Latest Versions", elem_classes=["secondary-action"]) restore_candidate = gr.Button("Restore Candidate", elem_classes=["secondary-action"]) stage_screens.append(audit_screen) with gr.Group(visible=False, elem_classes=["screen-card"]) as artifact_workspace: gr.Markdown("### Current Stage Output") with gr.Row(): with gr.Column(scale=2): artifact_output = gr.Markdown( readable_current_artifact_markdown(initial_state, STAGE_IDS[0]), elem_classes=["output-panel"], ) artifact_diff = gr.Textbox(label="Latest version changes", lines=10, interactive=False) with gr.Column(scale=1, min_width=320): lock_reasons = gr.Textbox(label="Lock reasons", lines=5, interactive=False) with gr.Accordion("Advanced human edit", open=False): human_edit_text = gr.Textbox( label="Human edit text", value=advanced_human_edit_text(initial_state, STAGE_IDS[0]), lines=10, interactive=True, ) reviewer_summary = gr.Textbox(label="Reviewer summary") requested_changes_json = gr.Textbox(label="Requested changes JSON", lines=6, value="[]") with gr.Accordion("AI revision controls", open=False): with gr.Row(): preserve_slide_order = gr.Checkbox(label="Preserve slide order", value=True) preserve_slide_titles = gr.Checkbox(label="Preserve slide titles", value=False) preserve_technical_claims = gr.Checkbox(label="Preserve technical claims", value=True) with gr.Row(): preserve_objective_mappings = gr.Checkbox(label="Preserve objective mappings", value=True) preserve_visual_assets = gr.Checkbox(label="Preserve visual assets", value=False) preserve_layout_ids = gr.Checkbox(label="Preserve layout IDs", value=False) with gr.Row(): constraint_slide_ids = gr.Textbox(label="Selected slide IDs") allowed_fields = gr.Textbox(label="Allowed fields") blocked_fields = gr.Textbox(label="Blocked fields") reviewer_instruction = gr.Textbox(label="Reviewer instruction", lines=2) with gr.Row(): propose_with_constraints = gr.Button("Critique + Propose Changes") targeted_slide_ids = gr.Textbox(label="Target slide IDs") targeted_action = gr.Dropdown( label="Selected slide action", choices=targeted_action_choices, value="rewrite_visible_text", ) targeted_improvement = gr.Button("Create Targeted Changes") critique_output = gr.Textbox(label="Latest critique", lines=7, interactive=False) proposed_change_sets = gr.Dataframe( headers=["Change Set ID", "Stage", "Status", "Artifact", "Changes", "Summary", "Created At"], interactive=False, elem_classes=["compact-panel"], ) with gr.Row(): selected_change_set_id = gr.Textbox(label="Change set ID") selected_change_ids = gr.Textbox(label="Selected change IDs") apply_changes = gr.Button("Apply Selected Proposed Changes", variant="primary") reject_changes = gr.Button("Reject Change Set") proposed_changes = gr.Dataframe( headers=[ "Change Set", "Change ID", "Target Type", "Target ID", "Field", "Before", "After", "Rationale", "Issues", "Risk", ], interactive=False, elem_classes=["compact-panel"], ) with gr.Group(visible=False, elem_classes=["screen-card"]) as review_workspace: gr.Markdown("### Review Workbench") with gr.Row(): review_role = gr.Dropdown(label="Review role", choices=review_role_options, value="general_reviewer", interactive=True) queue_stage = gr.Dropdown(label="Stage filter", choices=queue_stage_choices, value="all", interactive=True) queue_severity = gr.Dropdown(label="Severity filter", choices=severity_choices, value="all", interactive=True) with gr.Row(): queue_status = gr.Dropdown(label="Status filter", choices=status_choices, value="all", interactive=True) queue_issue_type = gr.Dropdown(label="Issue type filter", choices=issue_type_choices, value="all", interactive=True) assigned_filter = gr.Textbox(label="Assigned reviewer filter") role_summary = gr.Textbox(label="Review mode", lines=3, interactive=False) refresh_review = gr.Button("Refresh Review Workbench") review_queue = gr.Dataframe( headers=[ "Severity", "Priority", "Status", "Stage", "Slide", "Role", "Issue Type", "Message", "Suggested Fix", "Assigned To", "Created At", "Issue ID", ], interactive=False, elem_classes=["compact-panel"], ) with gr.Row(): selected_issue_id = gr.Textbox(label="Issue ID") issue_action = gr.Dropdown( label="Triage action", choices=[ ("Acknowledge", "acknowledge"), ("Mark in progress", "in_progress"), ("Resolve", "resolve"), ("Waive", "waive"), ("Won't fix", "wont_fix"), ("Reopen", "reopen"), ("Assign", "assign"), ], value="acknowledge", ) triage_note = gr.Textbox(label="Reason or note") with gr.Row(): assigned_role = gr.Dropdown(label="Assign role", choices=[("No role", "all"), *review_role_options], value="all") assigned_to = gr.Textbox(label="Assign reviewer") triage_issue = gr.Button("Apply Triage") generate_issue_fix = gr.Button("Generate Suggested Fix") suggested_fixes = gr.Dataframe( headers=["Fix ID", "Type", "Stage", "Slides", "Issues", "Risk", "Description"], interactive=False, elem_classes=["compact-panel"], ) with gr.Row(): fast_generate_grade = gr.Button("Generate + Grade Current Stage") fast_draft_ahead = gr.Button("Draft Ahead Until Review Needed") with gr.Row(): packet_stage_ids = gr.Textbox(label="Packet stage IDs") packet_slide_ids = gr.Textbox(label="Packet slide IDs") packet_format = gr.Radio(label="Packet format", choices=[("Markdown", "markdown"), ("JSON", "json")], value="markdown") create_packet = gr.Button("Create Review Packet") reviewer_metrics = gr.Dataframe(headers=["Metric", "Value"], interactive=False, elem_classes=["compact-panel"]) issues = gr.Dataframe( headers=["Issue ID", "Type", "Severity", "Stage", "Slide", "Message", "Resolved?"], interactive=False, elem_classes=["compact-panel"], ) with gr.Group(visible=False, elem_classes=["screen-card"]) as final_workspace: gr.Markdown("### Workflow Readiness") deck_health = gr.Dataframe(headers=["Metric", "Value"], interactive=False, elem_classes=["compact-panel"]) preflight_results = gr.Dataframe(headers=["Field", "Value"], interactive=False, elem_classes=["compact-panel"]) stage_status = gr.Dataframe( headers=["#", "Stage", "Score", "Artifact", "Artifact Status", "Stale?", "Can Unlock Next?", "Lock Reasons"], interactive=False, elem_classes=["compact-panel"], ) with gr.Group(visible=False, elem_classes=["screen-card"]) as audit_workspace: gr.Markdown("### Audit & Version Details") with gr.Row(): restore_artifact_id = gr.Textbox(label="Artifact version ID") audit = gr.Dataframe( headers=["Event ID", "Type", "Timestamp", "Stage", "Slide", "Issue", "Reason"], interactive=False, elem_classes=["compact-panel"], ) state_json = gr.Textbox(label="State", lines=18, interactive=False) with gr.Group(visible=False, elem_classes=["screen-card"]) as history_workspace: gr.Markdown("### Version History") semantic_compare = gr.Button("Compare Latest Versions") semantic_diff = gr.Textbox(label="Semantic diff", lines=8, interactive=False) prompt_runs = gr.Dataframe( headers=[ "Prompt Run ID", "Created At", "Stage", "Provider", "Model", "Template", "Inputs", "Output", "Settings", ], interactive=False, elem_classes=["compact-panel"], ) prompt_run_id = gr.Textbox(label="Prompt run ID") prompt_detail = gr.Textbox(label="Prompt run detail", lines=8, interactive=False) stage_score_gate_panels = [ setup_score_panel, source_score_panel, outline_score_panel, title_score_panel, text_score_panel, image_score_panel, layout_score_panel, ] outputs = [ state_store, status_region, status, artifact_output, human_edit_text, artifact_diff, lock_reasons, deck_health, slide_inventory, objective_matrix, preflight_results, stage_status, issues, audit, state_json, role_summary, review_queue, suggested_fixes, critique_output, proposed_change_sets, proposed_changes, semantic_diff, prompt_runs, prompt_detail, reviewer_metrics, workflow_header, stage_score_strip, *stage_score_gate_panels, *stage_screens, artifact_workspace, review_workspace, final_workspace, audit_workspace, history_workspace, *stage_nav_buttons, ] setup_inputs = [ state_store, deck_title, source_url, template_url, output_folder_id, dry_run, objectives_text, material_files, source_text, outline_text, start_mode, draft_file, mutation_target_url, production_export_requested, stage_id, ] review_filter_inputs = [ state_store, stage_id, review_role, queue_stage, queue_severity, queue_status, queue_issue_type, assigned_filter, selected_change_set_id, prompt_run_id, ] triage_inputs = [ state_store, stage_id, selected_issue_id, issue_action, reviewer_name, triage_note, assigned_role, assigned_to, review_role, queue_stage, queue_severity, queue_status, queue_issue_type, assigned_filter, selected_change_set_id, prompt_run_id, ] suggested_fix_inputs = [ state_store, stage_id, selected_issue_id, review_role, queue_stage, queue_severity, queue_status, queue_issue_type, assigned_filter, selected_change_set_id, prompt_run_id, ] constraint_inputs = [ preserve_slide_order, preserve_slide_titles, preserve_technical_claims, preserve_objective_mappings, preserve_visual_assets, preserve_layout_ids, constraint_slide_ids, allowed_fields, blocked_fields, reviewer_instruction, ] start_path_outputs = [start_mode, selected_start_path, instructions_panel, outline_panel, draft_panel] nav_outputs = [stage_id, queue_stage, review_role, *outputs] def _nav_handler(target_stage_id): def _handle(state_data): return handle_stage_nav(state_data, target_stage_id) return _handle def _pending_handler(message): def _handle(): return pending_activity_updates(message) return _handle def _with_pending(button, pending_message, handler, handler_inputs, handler_outputs): button.click( _pending_handler(pending_message), inputs=[], outputs=[status_region, status], show_progress="hidden", queue=False, ).then( handler, inputs=handler_inputs, outputs=handler_outputs, show_progress="minimal", show_progress_on=status, scroll_to_output=True, ) demo.load(handle_update_setup, inputs=setup_inputs, outputs=outputs, show_progress="hidden") instructions_path.click(select_instructions_path, inputs=[], outputs=start_path_outputs) outline_path.click(select_outline_path, inputs=[], outputs=start_path_outputs) draft_path.click(select_draft_path, inputs=[], outputs=start_path_outputs) for button, target_stage_id in zip(stage_nav_buttons, STAGE_IDS, strict=True): button.click(_nav_handler(target_stage_id), inputs=[state_store], outputs=nav_outputs, show_progress="hidden") _with_pending(update_setup, "Saving setup settings...", handle_update_setup, setup_inputs, outputs) _with_pending(setup_generate, "Creating setup candidate...", handle_generate, setup_inputs, outputs) _with_pending(setup_grade, "Checking setup score...", handle_grade, [state_store, stage_id], outputs) for button in [ source_generate, outline_generate, title_generate, text_generate, image_generate, layout_generate, ]: button.click(handle_generate, inputs=setup_inputs, outputs=outputs) for button in [source_grade, outline_grade, title_grade, text_grade, image_grade, layout_grade]: button.click(handle_grade, inputs=[state_store, stage_id], outputs=outputs) for button in [text_improve, image_improve, layout_improve]: button.click(handle_improve, inputs=[state_store, stage_id], outputs=outputs) for button in [ source_approve, outline_approve, title_approve, text_approve, image_approve, layout_approve, technical_approve, pedagogical_approve, aesthetic_approve, ]: button.click(handle_approve, inputs=[state_store, stage_id, reviewer_name, review_role], outputs=outputs) for button in [title_save_edits, text_save_edits]: button.click( handle_save_edits, inputs=[state_store, stage_id, human_edit_text, reviewer_name, reviewer_summary, requested_changes_json], outputs=outputs, ) final_preflight.click(handle_preflight, inputs=[state_store, stage_id], outputs=outputs) export.click(handle_export, inputs=[state_store, stage_id], outputs=outputs) for button in [refresh_review, technical_refresh, pedagogical_refresh, aesthetic_refresh]: button.click(handle_review_refresh, inputs=review_filter_inputs, outputs=outputs) for filter_component in [ review_role, queue_stage, queue_severity, queue_status, queue_issue_type, assigned_filter, selected_change_set_id, prompt_run_id, ]: filter_component.change(handle_review_refresh, inputs=review_filter_inputs, outputs=outputs, show_progress="hidden") triage_issue.click(handle_triage_issue, inputs=triage_inputs, outputs=outputs) for button in [generate_issue_fix, technical_fix, pedagogical_fix, aesthetic_fix]: button.click(handle_suggested_fix, inputs=suggested_fix_inputs, outputs=outputs) propose_with_constraints.click(handle_propose_with_constraints, inputs=[state_store, stage_id, *constraint_inputs], outputs=outputs) targeted_improvement.click( handle_targeted_improvement, inputs=[state_store, stage_id, targeted_slide_ids, targeted_action, *constraint_inputs], outputs=outputs, ) apply_changes.click( handle_apply_change_set, inputs=[state_store, stage_id, selected_change_set_id, selected_change_ids, reviewer_name], outputs=outputs, ) reject_changes.click( handle_reject_change_set, inputs=[state_store, stage_id, selected_change_set_id, reviewer_name, triage_note], outputs=outputs, ) for button in [semantic_compare, layout_compare, audit_compare]: button.click(handle_semantic_compare, inputs=[state_store, stage_id], outputs=outputs) fast_generate_grade.click(handle_fast_generate_grade, inputs=[state_store, stage_id], outputs=outputs) fast_draft_ahead.click(handle_fast_draft_ahead, inputs=[state_store, stage_id], outputs=outputs) create_packet.click( handle_review_packet, inputs=[state_store, stage_id, review_role, packet_stage_ids, packet_slide_ids, packet_format], outputs=outputs, ) restore_candidate.click( handle_restore_candidate, inputs=[state_store, stage_id, restore_artifact_id, reviewer_name], outputs=outputs, ) return demo demo = build_app() if __name__ == "__main__": demo.launch(css=CSS)