rigelbar's picture
Harden workflow UI contrast
7797512
Raw
History Blame Contribute Delete
94.6 kB
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)