LaelaZ commited on
Commit
ea60560
·
verified ·
1 Parent(s): 61d1a2b

Redesign UI: themed hero, verdict banners, severity chips, toolkit footer

Browse files
Files changed (1) hide show
  1. app.py +93 -40
app.py CHANGED
@@ -15,14 +15,49 @@ On Hugging Face Spaces this file is the entry point (app_file: app.py).
15
  """
16
  from __future__ import annotations
17
 
18
- import io
19
  import zipfile
20
  import tempfile
 
21
 
22
  import gradio as gr
23
 
24
  from scorm_qa.checks import validate_package
25
- from scorm_qa import report
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
  GOOD_MANIFEST = """<?xml version="1.0"?>
28
  <manifest identifier="MANIFEST-1" version="1.0">
@@ -41,11 +76,9 @@ GOOD_MANIFEST = """<?xml version="1.0"?>
41
  </manifest>
42
  """
43
 
44
- # Built-in samples: a clean package and three with the defects QA teams actually hit.
45
  SAMPLES = {
46
  "✅ Clean package (1.2, all files present)": {
47
- "imsmanifest.xml": GOOD_MANIFEST,
48
- "lesson.html": "<html>Hello</html>",
49
  },
50
  "❌ Missing launch file (lesson.html absent)": {
51
  "imsmanifest.xml": GOOD_MANIFEST,
@@ -56,15 +89,13 @@ SAMPLES = {
56
  "lesson.html": "<html/>",
57
  },
58
  "⚠️ Dangling extra file not in manifest": {
59
- "imsmanifest.xml": GOOD_MANIFEST,
60
- "lesson.html": "<html/>",
61
  "extras/notes.txt": "orphaned asset",
62
  },
63
  }
64
 
65
 
66
  def _zip_from_files(files: dict) -> str:
67
- """Write an in-memory SCORM package to a temp .zip and return its path."""
68
  tmp = tempfile.NamedTemporaryFile(suffix=".zip", delete=False)
69
  with zipfile.ZipFile(tmp, "w") as zf:
70
  for name, contents in files.items():
@@ -73,55 +104,77 @@ def _zip_from_files(files: dict) -> str:
73
  return tmp.name
74
 
75
 
76
- def _validate_path(zip_path: str, display_name: str):
77
- defects, _ = validate_package(zip_path)
78
- md = report.build_markdown(display_name, defects)
79
  critical = any(d["severity"] in ("CRITICAL", "HIGH") for d in defects)
80
- verdict = "❌ FAIL — would break on LMS upload" if critical else "✅ PASS"
81
- headline = f"### {verdict}{len(defects)} defect(s) found"
82
- return headline, md
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
 
84
 
85
  def run_sample(name: str):
86
  if not name:
87
- return "Pick a sample above.", ""
88
  path = _zip_from_files(SAMPLES[name])
89
- return _validate_path(path, name)
 
90
 
91
 
92
  def run_upload(file_obj):
93
  if file_obj is None:
94
- return "⚠️ Upload a SCORM .zip, or try a built-in sample.", ""
95
- return _validate_path(file_obj.name, file_obj.name.split("/")[-1])
96
-
97
-
98
- with gr.Blocks(title="SCORM QA Validator") as demo:
99
- gr.Markdown(
100
- "# 📦 SCORM QA Validator\n"
101
- "SCORM packages fail **silently** — a missing launch file or a wrong schema "
102
- "version doesn't error, it just shows the learner a blank screen after upload. "
103
- "This tool opens the package, parses `imsmanifest.xml`, and hands you a defect log "
104
- "(severity, location, recommended fix) *before* it ships to Docebo, Workday, or "
105
- "Cornerstone.\n\n"
106
- "*Runs the real package (`scorm_qa/`), the same code the 6-case pytest suite covers.*"
 
 
 
107
  )
108
 
109
  with gr.Tab("Try a built-in sample"):
110
- sample_dd = gr.Dropdown(choices=list(SAMPLES.keys()),
111
- value=list(SAMPLES.keys())[0],
112
  label="Sample package")
113
- sample_btn = gr.Button("Validate sample", variant="primary")
114
- s_headline = gr.Markdown()
115
- s_report = gr.Markdown()
116
- sample_btn.click(run_sample, inputs=sample_dd, outputs=[s_headline, s_report])
117
- demo.load(run_sample, inputs=sample_dd, outputs=[s_headline, s_report])
118
 
119
  with gr.Tab("Upload your own .zip"):
120
  up = gr.File(label="SCORM package (.zip)", file_types=[".zip"])
121
- up_btn = gr.Button("Validate package", variant="primary")
122
- u_headline = gr.Markdown()
123
- u_report = gr.Markdown()
124
- up_btn.click(run_upload, inputs=up, outputs=[u_headline, u_report])
 
 
125
 
126
 
127
  if __name__ == "__main__":
 
15
  """
16
  from __future__ import annotations
17
 
 
18
  import zipfile
19
  import tempfile
20
+ from html import escape
21
 
22
  import gradio as gr
23
 
24
  from scorm_qa.checks import validate_package
25
+
26
+ ACCENT = "#059669" # green
27
+ SEV_COLORS = {"CRITICAL": "#b91c1c", "HIGH": "#ea580c", "MEDIUM": "#ca8a04", "LOW": "#2563eb"}
28
+
29
+ CSS = """
30
+ :root { --accent: %s; }
31
+ .gradio-container { max-width: 1120px !important; }
32
+ #hero { background: linear-gradient(135deg, var(--accent), #0f172a);
33
+ color:#fff; border-radius:18px; padding:26px 30px; margin-bottom:6px; }
34
+ #hero h1 { margin:0 0 8px 0; font-size:1.75rem; font-weight:800; letter-spacing:-.01em; }
35
+ #hero p { margin:0; opacity:.93; font-size:1.02rem; line-height:1.5; max-width:780px; }
36
+ #hero .pill { display:inline-block; background:rgba(255,255,255,.16); border-radius:999px;
37
+ padding:3px 11px; font-size:.74rem; font-weight:700; margin-bottom:12px; letter-spacing:.04em; }
38
+ .verdict { border-radius:12px; padding:14px 18px; font-size:1.12rem; font-weight:800; margin:2px 0 14px; }
39
+ .verdict.pass { background:#dcfce7; color:#166534; border:1px solid #86efac; }
40
+ .verdict.fail { background:#fee2e2; color:#991b1b; border:1px solid #fca5a5; }
41
+ table.qc { width:100%%; border-collapse:collapse; margin:8px 0; font-size:.92rem; }
42
+ table.qc th { text-align:left; padding:7px 10px; border-bottom:2px solid var(--accent); font-weight:700; }
43
+ table.qc td { padding:8px 10px; border-bottom:1px solid rgba(128,128,128,.2); vertical-align:top; }
44
+ .sev { display:inline-block; padding:2px 10px; border-radius:999px; font-size:.7rem;
45
+ font-weight:800; color:#fff; letter-spacing:.03em; }
46
+ .qc code { background:rgba(128,128,128,.16); padding:1px 6px; border-radius:5px; }
47
+ .footer { margin-top:20px; padding-top:14px; border-top:1px solid rgba(128,128,128,.25);
48
+ font-size:.88rem; text-align:center; opacity:.92; }
49
+ .footer a { text-decoration:none; font-weight:700; color:var(--accent); }
50
+ """ % ACCENT
51
+
52
+ FOOTER = """
53
+ <div class="footer">
54
+ 🧰 Part of an AI evaluation &amp; QC toolkit by <b>Laela Zorana</b> &nbsp;·&nbsp;
55
+ 🔍 <a href="https://huggingface.co/spaces/LaelaZ/ai-agent-scenario-qc">Scenario QC</a> &nbsp;·&nbsp;
56
+ ⚖️ <a href="https://huggingface.co/spaces/LaelaZ/rlhf-pairwise-rater">RLHF Rater</a> &nbsp;·&nbsp;
57
+ 📦 SCORM QA &nbsp;·&nbsp;
58
+ <a href="https://github.com/LaelaZorana/scorm-qa-validator">Source on GitHub</a>
59
+ </div>
60
+ """
61
 
62
  GOOD_MANIFEST = """<?xml version="1.0"?>
63
  <manifest identifier="MANIFEST-1" version="1.0">
 
76
  </manifest>
77
  """
78
 
 
79
  SAMPLES = {
80
  "✅ Clean package (1.2, all files present)": {
81
+ "imsmanifest.xml": GOOD_MANIFEST, "lesson.html": "<html>Hello</html>",
 
82
  },
83
  "❌ Missing launch file (lesson.html absent)": {
84
  "imsmanifest.xml": GOOD_MANIFEST,
 
89
  "lesson.html": "<html/>",
90
  },
91
  "⚠️ Dangling extra file not in manifest": {
92
+ "imsmanifest.xml": GOOD_MANIFEST, "lesson.html": "<html/>",
 
93
  "extras/notes.txt": "orphaned asset",
94
  },
95
  }
96
 
97
 
98
  def _zip_from_files(files: dict) -> str:
 
99
  tmp = tempfile.NamedTemporaryFile(suffix=".zip", delete=False)
100
  with zipfile.ZipFile(tmp, "w") as zf:
101
  for name, contents in files.items():
 
104
  return tmp.name
105
 
106
 
107
+ def _result_html(display_name: str, defects: list) -> str:
 
 
108
  critical = any(d["severity"] in ("CRITICAL", "HIGH") for d in defects)
109
+ if critical:
110
+ banner = ('<div class="verdict fail">❌ FAIL would break on LMS upload &nbsp;·&nbsp; '
111
+ f"{len(defects)} defect(s)</div>")
112
+ elif defects:
113
+ banner = ('<div class="verdict pass">⚠️ PASS with minor issues &nbsp;·&nbsp; '
114
+ f"{len(defects)} defect(s)</div>")
115
+ else:
116
+ banner = '<div class="verdict pass">✅ PASS — no defects found</div>'
117
+
118
+ if not defects:
119
+ return banner + "<p>Package is well-formed and ready to upload.</p>"
120
+
121
+ rows = ['<table class="qc"><tr><th>Severity</th><th>Location</th><th>Issue</th></tr>']
122
+ order = {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3}
123
+ for d in sorted(defects, key=lambda x: order.get(x["severity"], 9)):
124
+ color = SEV_COLORS.get(d["severity"], "#6b7280")
125
+ rows.append(
126
+ f'<tr><td><span class="sev" style="background:{color}">{escape(d["severity"])}</span></td>'
127
+ f'<td><code>{escape(str(d.get("location", "")))}</code></td>'
128
+ f'<td>{escape(str(d.get("message", "")))}</td></tr>'
129
+ )
130
+ rows.append("</table>")
131
+ return banner + "\n".join(rows)
132
 
133
 
134
  def run_sample(name: str):
135
  if not name:
136
+ return '<div class="verdict fail">Pick a sample above.</div>'
137
  path = _zip_from_files(SAMPLES[name])
138
+ defects, _ = validate_package(path)
139
+ return _result_html(name, defects)
140
 
141
 
142
  def run_upload(file_obj):
143
  if file_obj is None:
144
+ return '<div class="verdict fail">⚠️ Upload a SCORM .zip, or try a built-in sample.</div>'
145
+ defects, _ = validate_package(file_obj.name)
146
+ return _result_html(file_obj.name.split("/")[-1], defects)
147
+
148
+
149
+ theme = gr.themes.Soft(primary_hue="emerald", neutral_hue="slate",
150
+ font=[gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"])
151
+
152
+ with gr.Blocks(title="SCORM QA Validator", theme=theme, css=CSS) as demo:
153
+ gr.HTML(
154
+ '<div id="hero"><span class="pill">E-LEARNING QA</span>'
155
+ "<h1>📦 SCORM QA Validator</h1>"
156
+ "<p>SCORM packages fail <b>silently</b> a missing launch file or a wrong schema version "
157
+ "doesn't error, it just shows the learner a blank screen after upload. This tool opens the "
158
+ "package, parses <code>imsmanifest.xml</code>, and hands you a defect log (severity, location, "
159
+ "fix) <i>before</i> it ships to Docebo, Workday, or Cornerstone.</p></div>"
160
  )
161
 
162
  with gr.Tab("Try a built-in sample"):
163
+ sample_dd = gr.Dropdown(choices=list(SAMPLES.keys()), value=list(SAMPLES.keys())[0],
 
164
  label="Sample package")
165
+ sample_btn = gr.Button("Validate sample", variant="primary", size="lg")
166
+ s_out = gr.HTML()
167
+ sample_btn.click(run_sample, inputs=sample_dd, outputs=s_out)
168
+ demo.load(run_sample, inputs=sample_dd, outputs=s_out)
 
169
 
170
  with gr.Tab("Upload your own .zip"):
171
  up = gr.File(label="SCORM package (.zip)", file_types=[".zip"])
172
+ up_btn = gr.Button("Validate package", variant="primary", size="lg")
173
+ u_out = gr.HTML()
174
+ up_btn.click(run_upload, inputs=up, outputs=u_out)
175
+
176
+ gr.HTML(FOOTER)
177
+ gr.Markdown("*Runs the actual package (`scorm_qa/`) — the same code the 6-case pytest suite covers.*")
178
 
179
 
180
  if __name__ == "__main__":