Spaces:
Sleeping
Sleeping
| 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'<div class="activity-card {normalized_status}">' | |
| "<div>" | |
| f'<div class="activity-label">{labels[normalized_status]}</div>' | |
| f'<div class="activity-copy">{_escape(message_text)}</div>' | |
| "</div>" | |
| "</div>" | |
| ) | |
| 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""" | |
| <div class="stage-result-card"> | |
| <h4>{_escape(label)} score</h4> | |
| <p>Run the stage score check after creating a candidate.</p> | |
| <div class="stage-result-score">Pending</div> | |
| </div> | |
| """ | |
| 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""" | |
| <div class="stage-result-card {result_class}"> | |
| <h4>{_escape(label)} score</h4> | |
| <p>{_escape(result_text)}</p> | |
| <div class="stage-result-score">{score}/100</div> | |
| </div> | |
| """ | |
| 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""" | |
| <div class="workflow-shell"> | |
| <div class="workflow-title-row"> | |
| <div> | |
| <div class="workflow-kicker">Course Slide Factory</div> | |
| <h2>{_escape(STAGE_LABELS[active_stage_id])}</h2> | |
| <p>{_escape(config["focus"])}</p> | |
| </div> | |
| <div class="workflow-progress-card"> | |
| <strong>{summary["percent"]}%</strong> | |
| <span>{summary["complete"]}/{summary["total"]} stages complete</span> | |
| </div> | |
| </div> | |
| <div class="workflow-progress-track"> | |
| <div class="workflow-progress-fill" style="width: {percent}%"></div> | |
| </div> | |
| <div class="workflow-metrics"> | |
| <div class="workflow-metric"><span>Average stage score</span><strong>{_escape(score_label)}</strong></div> | |
| <div class="workflow-metric"><span>Unresolved blockers</span><strong>{summary["unresolved_blockers"]}</strong></div> | |
| <div class="workflow-metric"><span>Stale stages</span><strong>{summary["stale"]}</strong></div> | |
| <div class="workflow-metric"><span>Current action</span><strong>{_escape(config["primary"])}</strong></div> | |
| </div> | |
| </div> | |
| """ | |
| 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""" | |
| <div class="{_escape(" ".join(classes))}"> | |
| <div class="stage-chip-top"> | |
| <span class="stage-chip-number">{row["index"]:02d}</span> | |
| <span class="stage-chip-score">{_escape(score)}</span> | |
| </div> | |
| <div class="stage-chip-label">{_escape(row["short_label"])}</div> | |
| <div class="stage-chip-state">{_escape(row["status"].replace("_", " "))}</div> | |
| </div> | |
| """ | |
| ) | |
| return f'<div class="stage-score-grid">{"".join(chips)}</div>' | |
| 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) | |