Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -22,9 +22,6 @@ MODEL = "meta/llama-4-maverick-17b-128e-instruct"
|
|
| 22 |
AFSCTP_FOLDER = "./frameworks/standards"
|
| 23 |
ATQF_FOLDER = "./frameworks/qualifications"
|
| 24 |
|
| 25 |
-
# Knowledge base storage
|
| 26 |
-
KNOWLEDGE_DB_FILE = "./frameworks/knowledge_base.json"
|
| 27 |
-
|
| 28 |
# ---------- AFSCTP SCORECARD (Standards and Competencies) ----------
|
| 29 |
AFSCTP_CRITERIA = {
|
| 30 |
"S1": {
|
|
@@ -177,248 +174,9 @@ ATQF_CRITERIA = {
|
|
| 177 |
}
|
| 178 |
}
|
| 179 |
|
| 180 |
-
# ---------- KNOWLEDGE BASE MANAGEMENT ----------
|
| 181 |
-
|
| 182 |
-
def load_knowledge_base():
|
| 183 |
-
"""Load existing knowledge base or return empty structure"""
|
| 184 |
-
if os.path.exists(KNOWLEDGE_DB_FILE):
|
| 185 |
-
try:
|
| 186 |
-
with open(KNOWLEDGE_DB_FILE, 'r', encoding='utf-8') as f:
|
| 187 |
-
return json.load(f)
|
| 188 |
-
except:
|
| 189 |
-
return {"countries": {}, "last_updated": None}
|
| 190 |
-
return {"countries": {}, "last_updated": None}
|
| 191 |
-
|
| 192 |
-
def save_knowledge_base(kb):
|
| 193 |
-
"""Save knowledge base to file"""
|
| 194 |
-
os.makedirs(os.path.dirname(KNOWLEDGE_DB_FILE), exist_ok=True)
|
| 195 |
-
kb["last_updated"] = datetime.now().isoformat()
|
| 196 |
-
with open(KNOWLEDGE_DB_FILE, 'w', encoding='utf-8') as f:
|
| 197 |
-
json.dump(kb, f, indent=2)
|
| 198 |
-
|
| 199 |
-
def scan_folders_for_documents():
|
| 200 |
-
"""Scan both folders and return list of all documents with their framework types"""
|
| 201 |
-
documents = []
|
| 202 |
-
|
| 203 |
-
# Scan AFSCTP folder
|
| 204 |
-
if os.path.exists(AFSCTP_FOLDER):
|
| 205 |
-
for f in os.listdir(AFSCTP_FOLDER):
|
| 206 |
-
if f.lower().endswith('.pdf'):
|
| 207 |
-
documents.append({
|
| 208 |
-
"filename": f,
|
| 209 |
-
"framework_type": "Standards and Competencies (AFSCTP)",
|
| 210 |
-
"framework_code": "AFSCTP",
|
| 211 |
-
"path": os.path.join(AFSCTP_FOLDER, f)
|
| 212 |
-
})
|
| 213 |
-
|
| 214 |
-
# Scan ATQF folder
|
| 215 |
-
if os.path.exists(ATQF_FOLDER):
|
| 216 |
-
for f in os.listdir(ATQF_FOLDER):
|
| 217 |
-
if f.lower().endswith('.pdf'):
|
| 218 |
-
documents.append({
|
| 219 |
-
"filename": f,
|
| 220 |
-
"framework_type": "Qualifications Framework (ATQF/CTQF)",
|
| 221 |
-
"framework_code": "ATQF",
|
| 222 |
-
"path": os.path.join(ATQF_FOLDER, f)
|
| 223 |
-
})
|
| 224 |
-
|
| 225 |
-
return documents
|
| 226 |
-
|
| 227 |
-
def build_knowledge_base_from_documents():
|
| 228 |
-
"""Scan all documents and build/update knowledge base"""
|
| 229 |
-
kb = load_knowledge_base()
|
| 230 |
-
documents = scan_folders_for_documents()
|
| 231 |
-
|
| 232 |
-
if not NVIDIA_API_KEY:
|
| 233 |
-
print("Warning: NVIDIA_API_KEY not set. Cannot build knowledge base.")
|
| 234 |
-
return kb
|
| 235 |
-
|
| 236 |
-
processed_count = 0
|
| 237 |
-
for doc in documents:
|
| 238 |
-
# Check if already processed and up to date
|
| 239 |
-
doc_id = f"{doc['framework_code']}_{doc['filename']}"
|
| 240 |
-
if doc_id in kb.get("processed_docs", {}):
|
| 241 |
-
# Check if file has been modified
|
| 242 |
-
stored_mtime = kb["processed_docs"][doc_id].get("mtime", 0)
|
| 243 |
-
current_mtime = os.path.getmtime(doc["path"])
|
| 244 |
-
if stored_mtime == current_mtime:
|
| 245 |
-
continue # Skip already processed files
|
| 246 |
-
|
| 247 |
-
try:
|
| 248 |
-
print(f"Processing: {doc['filename']}...")
|
| 249 |
-
text = extract_text_from_file(doc["path"])
|
| 250 |
-
country = detect_country(text)
|
| 251 |
-
|
| 252 |
-
# Determine criteria dict
|
| 253 |
-
if doc["framework_code"] == "AFSCTP":
|
| 254 |
-
criteria_dict = AFSCTP_CRITERIA
|
| 255 |
-
else:
|
| 256 |
-
criteria_dict = ATQF_CRITERIA
|
| 257 |
-
|
| 258 |
-
# Evaluate
|
| 259 |
-
prompt = build_evaluation_prompt(text, criteria_dict, doc["framework_code"])
|
| 260 |
-
results = call_nvidia_llm(prompt)
|
| 261 |
-
|
| 262 |
-
# Calculate overall score
|
| 263 |
-
scores = []
|
| 264 |
-
for cid in criteria_dict.keys():
|
| 265 |
-
score = results.get(cid, 0)
|
| 266 |
-
try:
|
| 267 |
-
scores.append(int(score))
|
| 268 |
-
except:
|
| 269 |
-
scores.append(0)
|
| 270 |
-
|
| 271 |
-
total_score = sum(scores)
|
| 272 |
-
max_score = len(criteria_dict) * 5
|
| 273 |
-
percent = (total_score / max_score) * 100 if max_score > 0 else 0
|
| 274 |
-
|
| 275 |
-
# Store in knowledge base
|
| 276 |
-
if country not in kb["countries"]:
|
| 277 |
-
kb["countries"][country] = {}
|
| 278 |
-
|
| 279 |
-
kb["countries"][country][doc["framework_code"]] = {
|
| 280 |
-
"filename": doc["filename"],
|
| 281 |
-
"overall_score": round(percent, 1),
|
| 282 |
-
"criterion_scores": results,
|
| 283 |
-
"individual_scores": scores,
|
| 284 |
-
"processed_date": datetime.now().isoformat(),
|
| 285 |
-
"document_path": doc["path"]
|
| 286 |
-
}
|
| 287 |
-
|
| 288 |
-
# Track processed document
|
| 289 |
-
if "processed_docs" not in kb:
|
| 290 |
-
kb["processed_docs"] = {}
|
| 291 |
-
kb["processed_docs"][doc_id] = {
|
| 292 |
-
"mtime": os.path.getmtime(doc["path"]),
|
| 293 |
-
"processed_date": datetime.now().isoformat()
|
| 294 |
-
}
|
| 295 |
-
|
| 296 |
-
processed_count += 1
|
| 297 |
-
|
| 298 |
-
except Exception as e:
|
| 299 |
-
print(f"Error processing {doc['filename']}: {e}")
|
| 300 |
-
|
| 301 |
-
save_knowledge_base(kb)
|
| 302 |
-
print(f"Knowledge base updated. Processed {processed_count} new/updated documents.")
|
| 303 |
-
return kb
|
| 304 |
-
|
| 305 |
-
def get_knowledge_glossary():
|
| 306 |
-
"""Generate glossary display from knowledge base"""
|
| 307 |
-
kb = load_knowledge_base()
|
| 308 |
-
|
| 309 |
-
if not kb["countries"]:
|
| 310 |
-
return "### 📚 Knowledge Base\\n\\nNo documents have been processed yet. Upload documents and click 'Refresh Knowledge Base' to build the glossary."
|
| 311 |
-
|
| 312 |
-
md = "### 📚 African Union Teacher Frameworks Knowledge Base\\n\\n"
|
| 313 |
-
md += f"*Last Updated: {kb.get('last_updated', 'Unknown')[:10] if kb.get('last_updated') else 'Unknown'}*\\n\\n"
|
| 314 |
-
|
| 315 |
-
# Summary statistics
|
| 316 |
-
total_countries = len(kb["countries"])
|
| 317 |
-
afsctp_count = sum(1 for c in kb["countries"].values() if "AFSCTP" in c)
|
| 318 |
-
atqf_count = sum(1 for c in kb["countries"].values() if "ATQF" in c)
|
| 319 |
-
|
| 320 |
-
md += f"**📊 Overview:** {total_countries} countries | {afsctp_count} AFSCTP evaluations | {atqf_count} ATQF/CTQF evaluations\\n\\n"
|
| 321 |
-
md += "---\\n\\n"
|
| 322 |
-
|
| 323 |
-
# Sort countries alphabetically
|
| 324 |
-
for country in sorted(kb["countries"].keys()):
|
| 325 |
-
data = kb["countries"][country]
|
| 326 |
-
md += f"#### 🌍 {country}\\n\\n"
|
| 327 |
-
|
| 328 |
-
for framework_code, eval_data in data.items():
|
| 329 |
-
framework_name = "AFSCTP (Standards)" if framework_code == "AFSCTP" else "ATQF/CTQF (Qualifications)"
|
| 330 |
-
score = eval_data["overall_score"]
|
| 331 |
-
|
| 332 |
-
# Color code the score
|
| 333 |
-
if score >= 75:
|
| 334 |
-
emoji = "🟢"
|
| 335 |
-
color = "green"
|
| 336 |
-
elif score >= 50:
|
| 337 |
-
emoji = "🟡"
|
| 338 |
-
color = "orange"
|
| 339 |
-
else:
|
| 340 |
-
emoji = "🔴"
|
| 341 |
-
color = "red"
|
| 342 |
-
|
| 343 |
-
md += f"**{framework_name}:** {emoji} **{score:.1f}%** alignment\\n"
|
| 344 |
-
|
| 345 |
-
# Show individual criterion scores
|
| 346 |
-
scores = eval_data.get("individual_scores", [])
|
| 347 |
-
criteria_dict = AFSCTP_CRITERIA if framework_code == "AFSCTP" else ATQF_CRITERIA
|
| 348 |
-
|
| 349 |
-
if scores and len(scores) == len(criteria_dict):
|
| 350 |
-
score_details = []
|
| 351 |
-
for (cid, data_criteria), score_val in zip(criteria_dict.items(), scores):
|
| 352 |
-
score_emoji = "🟢" if score_val >= 4 else "🟡" if score_val == 3 else "🔴"
|
| 353 |
-
score_details.append(f"{cid}:{score_emoji}{score_val}")
|
| 354 |
-
md += f" *Scores:* {' | '.join(score_details)}\\n"
|
| 355 |
-
|
| 356 |
-
md += "\\n"
|
| 357 |
-
|
| 358 |
-
md += "---\\n\\n"
|
| 359 |
-
|
| 360 |
-
return md
|
| 361 |
-
|
| 362 |
-
def get_country_comparison_data():
|
| 363 |
-
"""Get data for country comparison charts"""
|
| 364 |
-
kb = load_knowledge_base()
|
| 365 |
-
|
| 366 |
-
countries = []
|
| 367 |
-
afsctp_scores = []
|
| 368 |
-
atqf_scores = []
|
| 369 |
-
|
| 370 |
-
for country in sorted(kb["countries"].keys()):
|
| 371 |
-
countries.append(country)
|
| 372 |
-
data = kb["countries"][country]
|
| 373 |
-
afsctp_scores.append(data.get("AFSCTP", {}).get("overall_score", 0))
|
| 374 |
-
atqf_scores.append(data.get("ATQF", {}).get("overall_score", 0))
|
| 375 |
-
|
| 376 |
-
return countries, afsctp_scores, atqf_scores
|
| 377 |
-
|
| 378 |
-
def generate_comparison_charts():
|
| 379 |
-
"""Generate comparison charts for knowledge base"""
|
| 380 |
-
countries, afsctp_scores, atqf_scores = get_country_comparison_data()
|
| 381 |
-
|
| 382 |
-
if not countries:
|
| 383 |
-
return None, None
|
| 384 |
-
|
| 385 |
-
# Bar chart comparing countries
|
| 386 |
-
fig, ax = plt.subplots(figsize=(14, max(6, len(countries) * 0.5)))
|
| 387 |
-
|
| 388 |
-
y_pos = np.arange(len(countries))
|
| 389 |
-
height = 0.35
|
| 390 |
-
|
| 391 |
-
# Only show frameworks that have data
|
| 392 |
-
has_afsctp = any(s > 0 for s in afsctp_scores)
|
| 393 |
-
has_atqf = any(s > 0 for s in atqf_scores)
|
| 394 |
-
|
| 395 |
-
if has_afsctp:
|
| 396 |
-
bars1 = ax.barh(y_pos - height/2 if has_atqf else y_pos, afsctp_scores, height,
|
| 397 |
-
label='AFSCTP (Standards)', color='#1565c0', alpha=0.8)
|
| 398 |
-
if has_atqf:
|
| 399 |
-
bars2 = ax.barh(y_pos + height/2 if has_afsctp else y_pos, atqf_scores, height,
|
| 400 |
-
label='ATQF/CTQF (Qualifications)', color='#2e7d32', alpha=0.8)
|
| 401 |
-
|
| 402 |
-
ax.set_yticks(y_pos)
|
| 403 |
-
ax.set_yticklabels(countries)
|
| 404 |
-
ax.set_xlabel('Alignment Score (%)', fontsize=12, fontweight='bold')
|
| 405 |
-
ax.set_title('Country Comparison: Framework Alignment Scores', fontsize=14, fontweight='bold')
|
| 406 |
-
ax.set_xlim(0, 100)
|
| 407 |
-
ax.axvline(x=50, color='gray', linestyle='--', alpha=0.5, label='50% threshold')
|
| 408 |
-
ax.legend(loc='lower right')
|
| 409 |
-
|
| 410 |
-
plt.tight_layout()
|
| 411 |
-
buf = BytesIO()
|
| 412 |
-
plt.savefig(buf, format='png', dpi=150, bbox_inches='tight')
|
| 413 |
-
buf.seek(0)
|
| 414 |
-
chart_img = base64.b64encode(buf.read()).decode()
|
| 415 |
-
plt.close()
|
| 416 |
-
|
| 417 |
-
return chart_img
|
| 418 |
-
|
| 419 |
def get_criteria_display(criteria_dict, framework_name):
|
| 420 |
"""Generate markdown display of criteria for a specific framework"""
|
| 421 |
-
md = f"### 📋 {framework_name} Evaluation Criteria\
|
| 422 |
|
| 423 |
sections = {}
|
| 424 |
for cid, data in criteria_dict.items():
|
|
@@ -428,14 +186,14 @@ def get_criteria_display(criteria_dict, framework_name):
|
|
| 428 |
sections[section].append((cid, data))
|
| 429 |
|
| 430 |
for section, items in sections.items():
|
| 431 |
-
md += f"\
|
| 432 |
for cid, data in items:
|
| 433 |
-
md += f"**{cid} - {data['short_label']}**\
|
| 434 |
-
md += "<details><summary>View Scoring Descriptors (0-5)</summary>\
|
| 435 |
for level, desc in data['levels'].items():
|
| 436 |
emoji = "🔴" if level <= 1 else "🟡" if level <= 3 else "🟢"
|
| 437 |
-
md += f"{emoji} **{level}:** {desc}\
|
| 438 |
-
md += "</details>\
|
| 439 |
|
| 440 |
return md
|
| 441 |
|
|
@@ -467,33 +225,15 @@ def get_folder_for_framework(framework_type):
|
|
| 467 |
else:
|
| 468 |
return ATQF_FOLDER
|
| 469 |
|
| 470 |
-
def get_available_framework_categories():
|
| 471 |
-
"""Get list of available framework categories for upload selection"""
|
| 472 |
-
categories = []
|
| 473 |
-
|
| 474 |
-
# Check AFSCTP
|
| 475 |
-
if os.path.exists(AFSCTP_FOLDER) and any(f.endswith('.pdf') for f in os.listdir(AFSCTP_FOLDER)):
|
| 476 |
-
categories.append("Standards and Competencies (AFSCTP)")
|
| 477 |
-
else:
|
| 478 |
-
categories.append("Standards and Competencies (AFSCTP) [No documents yet]")
|
| 479 |
-
|
| 480 |
-
# Check ATQF
|
| 481 |
-
if os.path.exists(ATQF_FOLDER) and any(f.endswith('.pdf') for f in os.listdir(ATQF_FOLDER)):
|
| 482 |
-
categories.append("Qualifications Framework (ATQF/CTQF)")
|
| 483 |
-
else:
|
| 484 |
-
categories.append("Qualifications Framework (ATQF/CTQF) [No documents yet]")
|
| 485 |
-
|
| 486 |
-
return categories
|
| 487 |
-
|
| 488 |
def extract_text_from_file(file_path):
|
| 489 |
if file_path.endswith('.pdf'):
|
| 490 |
with open(file_path, 'rb') as f:
|
| 491 |
reader = PyPDF2.PdfReader(f)
|
| 492 |
text = "".join(page.extract_text() for page in reader.pages)
|
| 493 |
-
return re.sub(r'\
|
| 494 |
elif file_path.endswith('.docx'):
|
| 495 |
doc = Document(file_path)
|
| 496 |
-
return "\
|
| 497 |
else:
|
| 498 |
with open(file_path, 'r', encoding='utf-8') as f:
|
| 499 |
return f.read()
|
|
@@ -523,7 +263,7 @@ def detect_country(document_text):
|
|
| 523 |
response = requests.post(NVIDIA_URL, headers=headers, json=payload)
|
| 524 |
if response.status_code == 200:
|
| 525 |
country = response.json()["choices"][0]["message"]["content"].strip()
|
| 526 |
-
country = country.replace("The ", "").replace("the ", "").replace(".", "").replace("\
|
| 527 |
return country if country else "Unknown Country"
|
| 528 |
except:
|
| 529 |
pass
|
|
@@ -534,17 +274,17 @@ def build_evaluation_prompt(document_text, criteria_dict, framework_name):
|
|
| 534 |
prompt_parts = []
|
| 535 |
prompt_parts.append(f"You are evaluating a national teacher framework against the {framework_name}. ")
|
| 536 |
prompt_parts.append("For each criterion below, select the SINGLE best description (0-5) that matches the document content. ")
|
| 537 |
-
prompt_parts.append("Return ONLY a JSON object where keys are criterion IDs (e.g., 'S1' or 'Q1') and values are integers 0-5. No explanations.\
|
| 538 |
|
| 539 |
for cid, data in criteria_dict.items():
|
| 540 |
-
prompt_parts.append(f"\
|
| 541 |
for level, desc in data['levels'].items():
|
| 542 |
prompt_parts.append(f" [{level}] {desc}")
|
| 543 |
|
| 544 |
-
prompt_parts.append(f"\
|
| 545 |
-
prompt_parts.append("\
|
| 546 |
|
| 547 |
-
return "\
|
| 548 |
|
| 549 |
def call_nvidia_llm(prompt):
|
| 550 |
headers = {"Authorization": f"Bearer {NVIDIA_API_KEY}", "Content-Type": "application/json"}
|
|
@@ -569,7 +309,7 @@ def call_nvidia_llm(prompt):
|
|
| 569 |
try:
|
| 570 |
return json.loads(content)
|
| 571 |
except json.JSONDecodeError:
|
| 572 |
-
match = re.search(r'\
|
| 573 |
if match:
|
| 574 |
return json.loads(match.group())
|
| 575 |
raise Exception(f"Could not parse JSON from: {content[:200]}")
|
|
@@ -586,7 +326,7 @@ def generate_charts(df, country, framework_name):
|
|
| 586 |
bars = plt.barh(labels, values, color=colors, edgecolor='black', linewidth=0.5)
|
| 587 |
plt.xlim(0, 5)
|
| 588 |
plt.xlabel("Score (0-5)", fontsize=12, fontweight='bold')
|
| 589 |
-
plt.title(f"{country}\
|
| 590 |
plt.axvline(x=3, color='gray', linestyle='--', alpha=0.5)
|
| 591 |
|
| 592 |
for i, (bar, score) in enumerate(zip(bars, values)):
|
|
@@ -613,7 +353,7 @@ def generate_charts(df, country, framework_name):
|
|
| 613 |
ax.set_ylim(0, 5)
|
| 614 |
ax.set_yticks([1, 2, 3, 4, 5])
|
| 615 |
ax.set_yticklabels(['1', '2', '3', '4', '5'], color="grey", size=8)
|
| 616 |
-
ax.set_title(f"{country}\
|
| 617 |
radar_buf = BytesIO()
|
| 618 |
plt.savefig(radar_buf, format='png', dpi=150, bbox_inches='tight')
|
| 619 |
radar_buf.seek(0)
|
|
@@ -645,7 +385,7 @@ def generate_charts(df, country, framework_name):
|
|
| 645 |
ax.set_xticklabels(['0%', '25%', '50%', '75%', '100%'], fontsize=11)
|
| 646 |
ax.set_yticks([])
|
| 647 |
ax.set_xlabel(f"Overall Alignment Score", fontsize=14, fontweight='bold')
|
| 648 |
-
ax.set_title(f"{country}\
|
| 649 |
ax.text(50, 0, f"{percent:.1f}%", ha='center', va='center', fontsize=20, fontweight='bold', color='white' if percent > 50 else 'black')
|
| 650 |
|
| 651 |
gauge_buf = BytesIO()
|
|
@@ -677,17 +417,17 @@ def generate_pdf_report(df, country, framework_name, percent, bar_img_data, rada
|
|
| 677 |
weak = df[df['Score'] <= 2]['Label'].tolist()
|
| 678 |
moderate = df[df['Score'] == 3]['Label'].tolist()
|
| 679 |
|
| 680 |
-
summary_text.append(f"\
|
| 681 |
if strong:
|
| 682 |
for s in strong:
|
| 683 |
summary_text.append(f" ✓ {s}")
|
| 684 |
|
| 685 |
-
summary_text.append(f"\
|
| 686 |
if moderate:
|
| 687 |
for m in moderate:
|
| 688 |
summary_text.append(f" ~ {m}")
|
| 689 |
|
| 690 |
-
summary_text.append(f"\
|
| 691 |
if weak:
|
| 692 |
for w in weak:
|
| 693 |
summary_text.append(f" ✗ {w}")
|
|
@@ -872,136 +612,66 @@ ATQF_DISPLAY = get_criteria_display(ATQF_CRITERIA, "ATQF/CTQF (Qualifications)")
|
|
| 872 |
# Initialize folders and get initial choices
|
| 873 |
os.makedirs(AFSCTP_FOLDER, exist_ok=True)
|
| 874 |
os.makedirs(ATQF_FOLDER, exist_ok=True)
|
| 875 |
-
|
| 876 |
-
# Build knowledge base on startup (if API key available)
|
| 877 |
-
if NVIDIA_API_KEY:
|
| 878 |
-
print("Building knowledge base from existing documents...")
|
| 879 |
-
build_knowledge_base_from_documents()
|
| 880 |
-
|
| 881 |
INITIAL_DROPDOWN_CHOICES = get_frameworks_for_type("Standards and Competencies (AFSCTP)")
|
| 882 |
-
INITIAL_GLOSSARY = get_knowledge_glossary()
|
| 883 |
-
INITIAL_COMPARISON_CHART = generate_comparison_charts()
|
| 884 |
|
| 885 |
# Gradio UI
|
| 886 |
-
with gr.Blocks(title="African Union Teacher Frameworks
|
| 887 |
-
gr.Markdown("#
|
| 888 |
-
gr.Markdown("
|
| 889 |
-
|
| 890 |
-
|
| 891 |
-
|
| 892 |
-
|
| 893 |
-
|
| 894 |
-
|
| 895 |
-
with gr.Row():
|
| 896 |
-
with gr.Column(scale=1):
|
| 897 |
-
gr.Markdown("### Knowledge Base Controls")
|
| 898 |
-
refresh_kb_btn = gr.Button("🔄 Refresh Knowledge Base", variant="primary")
|
| 899 |
-
kb_status = gr.Textbox(label="Status", value="Ready", interactive=False)
|
| 900 |
-
|
| 901 |
-
gr.Markdown("---")
|
| 902 |
-
gr.Markdown("### 📊 Statistics")
|
| 903 |
-
kb_stats = gr.Markdown("Click 'Refresh' to see statistics")
|
| 904 |
-
|
| 905 |
-
with gr.Column(scale=3):
|
| 906 |
-
gr.Markdown("### Country Framework Scores Glossary")
|
| 907 |
-
glossary_output = gr.Markdown(INITIAL_GLOSSARY)
|
| 908 |
|
| 909 |
-
|
| 910 |
-
with gr.Column():
|
| 911 |
-
gr.Markdown("### Country Comparison")
|
| 912 |
-
if INITIAL_COMPARISON_CHART:
|
| 913 |
-
comparison_html = gr.HTML(f'<img src="data:image/png;base64,{INITIAL_COMPARISON_CHART}" style="width:100%; max-width:1200px;">')
|
| 914 |
-
else:
|
| 915 |
-
comparison_html = gr.HTML("No data available for comparison. Add documents and refresh.")
|
| 916 |
-
|
| 917 |
-
# ========== EVALUATION TAB ==========
|
| 918 |
-
with gr.TabItem("🔍 Evaluate Document", id="evaluate"):
|
| 919 |
-
with gr.Row():
|
| 920 |
-
with gr.Column(scale=1):
|
| 921 |
-
# Dynamic criteria display based on selection
|
| 922 |
-
with gr.Accordion("View Evaluation Criteria & Scoring Descriptors", open=False):
|
| 923 |
-
criteria_markdown = gr.Markdown(AFSCTP_DISPLAY)
|
| 924 |
-
|
| 925 |
-
gr.Markdown("### Framework Selection")
|
| 926 |
-
|
| 927 |
-
# Framework type selector (default to Standards)
|
| 928 |
-
framework_selector = gr.Dropdown(
|
| 929 |
-
label="Select Evaluation Framework",
|
| 930 |
-
choices=["Standards and Competencies (AFSCTP)", "Qualifications Framework (ATQF/CTQF)"],
|
| 931 |
-
value="Standards and Competencies (AFSCTP)",
|
| 932 |
-
interactive=True
|
| 933 |
-
)
|
| 934 |
-
|
| 935 |
-
gr.Markdown("### Input Selection")
|
| 936 |
-
|
| 937 |
-
# Dropdown for pre-loaded frameworks - now dynamic based on type
|
| 938 |
-
framework_dropdown = gr.Dropdown(
|
| 939 |
-
label="Select Pre-loaded Framework",
|
| 940 |
-
choices=INITIAL_DROPDOWN_CHOICES,
|
| 941 |
-
value=INITIAL_DROPDOWN_CHOICES[0] if INITIAL_DROPDOWN_CHOICES else None,
|
| 942 |
-
interactive=True
|
| 943 |
-
)
|
| 944 |
-
|
| 945 |
-
gr.Markdown("**OR Upload New Document**")
|
| 946 |
-
|
| 947 |
-
# File upload with framework category selection
|
| 948 |
-
with gr.Group():
|
| 949 |
-
file_input = gr.File(label="Upload Document", file_types=[".pdf", ".docx", ".txt"])
|
| 950 |
-
upload_framework_selector = gr.Dropdown(
|
| 951 |
-
label="Select Framework Category for Upload",
|
| 952 |
-
choices=get_available_framework_categories(),
|
| 953 |
-
value=get_available_framework_categories()[0] if get_available_framework_categories() else None,
|
| 954 |
-
interactive=True
|
| 955 |
-
)
|
| 956 |
-
|
| 957 |
-
evaluate_btn = gr.Button("Evaluate Framework", variant="primary")
|
| 958 |
-
status_text = gr.Textbox(label="Status", value="Waiting", interactive=False)
|
| 959 |
-
country_text = gr.Textbox(label="Detected Country", value="Unknown", interactive=False)
|
| 960 |
-
|
| 961 |
-
pdf_output = gr.File(label="Download PDF Report", visible=False)
|
| 962 |
-
|
| 963 |
-
with gr.Column(scale=2):
|
| 964 |
-
summary_output = gr.Markdown("")
|
| 965 |
-
gauge_output = gr.HTML("")
|
| 966 |
-
|
| 967 |
-
with gr.Row():
|
| 968 |
-
with gr.Column():
|
| 969 |
-
gr.Markdown("### Detailed Scores by Criterion")
|
| 970 |
-
bar_output = gr.HTML()
|
| 971 |
-
with gr.Column():
|
| 972 |
-
gr.Markdown("### Alignment Profile")
|
| 973 |
-
radar_output = gr.HTML()
|
| 974 |
-
|
| 975 |
-
with gr.Row():
|
| 976 |
-
table_output = gr.Dataframe(label="Detailed Evaluation Results")
|
| 977 |
-
|
| 978 |
-
# ========== EVENT HANDLERS ==========
|
| 979 |
-
|
| 980 |
-
# Refresh knowledge base
|
| 981 |
-
def refresh_knowledge_base():
|
| 982 |
-
if not NVIDIA_API_KEY:
|
| 983 |
-
return "❌ NVIDIA_API_KEY not set. Cannot refresh.", "No data available", "Please configure NVIDIA_API_KEY"
|
| 984 |
-
try:
|
| 985 |
-
kb = build_knowledge_base_from_documents()
|
| 986 |
-
glossary = get_knowledge_glossary()
|
| 987 |
-
chart = generate_comparison_charts()
|
| 988 |
|
| 989 |
-
#
|
| 990 |
-
|
| 991 |
-
|
| 992 |
-
|
|
|
|
|
|
|
|
|
|
| 993 |
|
| 994 |
-
|
| 995 |
|
| 996 |
-
|
| 997 |
-
|
| 998 |
-
|
| 999 |
-
|
| 1000 |
-
|
| 1001 |
-
|
| 1002 |
-
|
| 1003 |
-
|
| 1004 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1005 |
# Update dropdown and criteria when framework type changes
|
| 1006 |
def on_framework_change(framework_type):
|
| 1007 |
choices = get_frameworks_for_type(framework_type)
|
|
@@ -1018,20 +688,9 @@ with gr.Blocks(title="African Union Teacher Frameworks Knowledge & Assessment Pl
|
|
| 1018 |
)
|
| 1019 |
|
| 1020 |
# Main evaluation handler
|
| 1021 |
-
def handle_evaluation(file, framework_type, dropdown_selection, upload_framework):
|
| 1022 |
-
"""Handle evaluation with proper framework type selection"""
|
| 1023 |
-
# If file is uploaded, use the upload framework selector
|
| 1024 |
-
if file is not None:
|
| 1025 |
-
# Clean up the framework type (remove [No documents yet] suffix if present)
|
| 1026 |
-
clean_framework = upload_framework.split(" [")[0] if upload_framework else framework_type
|
| 1027 |
-
return process_inputs(file, clean_framework, None)
|
| 1028 |
-
else:
|
| 1029 |
-
# Use the main framework selector for dropdown selection
|
| 1030 |
-
return process_inputs(None, framework_type, dropdown_selection)
|
| 1031 |
-
|
| 1032 |
evaluate_btn.click(
|
| 1033 |
-
fn=
|
| 1034 |
-
inputs=[file_input, framework_selector, framework_dropdown
|
| 1035 |
outputs=[table_output, summary_output, bar_output, radar_output, gauge_output,
|
| 1036 |
gr.Number(visible=False), status_text, country_text, pdf_output]
|
| 1037 |
).then(
|
|
|
|
| 22 |
AFSCTP_FOLDER = "./frameworks/standards"
|
| 23 |
ATQF_FOLDER = "./frameworks/qualifications"
|
| 24 |
|
|
|
|
|
|
|
|
|
|
| 25 |
# ---------- AFSCTP SCORECARD (Standards and Competencies) ----------
|
| 26 |
AFSCTP_CRITERIA = {
|
| 27 |
"S1": {
|
|
|
|
| 174 |
}
|
| 175 |
}
|
| 176 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
def get_criteria_display(criteria_dict, framework_name):
|
| 178 |
"""Generate markdown display of criteria for a specific framework"""
|
| 179 |
+
md = f"### 📋 {framework_name} Evaluation Criteria\n\n"
|
| 180 |
|
| 181 |
sections = {}
|
| 182 |
for cid, data in criteria_dict.items():
|
|
|
|
| 186 |
sections[section].append((cid, data))
|
| 187 |
|
| 188 |
for section, items in sections.items():
|
| 189 |
+
md += f"\n#### {section}\n\n"
|
| 190 |
for cid, data in items:
|
| 191 |
+
md += f"**{cid} - {data['short_label']}**\n"
|
| 192 |
+
md += "<details><summary>View Scoring Descriptors (0-5)</summary>\n\n"
|
| 193 |
for level, desc in data['levels'].items():
|
| 194 |
emoji = "🔴" if level <= 1 else "🟡" if level <= 3 else "🟢"
|
| 195 |
+
md += f"{emoji} **{level}:** {desc}\n\n"
|
| 196 |
+
md += "</details>\n\n"
|
| 197 |
|
| 198 |
return md
|
| 199 |
|
|
|
|
| 225 |
else:
|
| 226 |
return ATQF_FOLDER
|
| 227 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
def extract_text_from_file(file_path):
|
| 229 |
if file_path.endswith('.pdf'):
|
| 230 |
with open(file_path, 'rb') as f:
|
| 231 |
reader = PyPDF2.PdfReader(f)
|
| 232 |
text = "".join(page.extract_text() for page in reader.pages)
|
| 233 |
+
return re.sub(r'\s+', ' ', text)
|
| 234 |
elif file_path.endswith('.docx'):
|
| 235 |
doc = Document(file_path)
|
| 236 |
+
return "\n".join([para.text for para in doc.paragraphs])
|
| 237 |
else:
|
| 238 |
with open(file_path, 'r', encoding='utf-8') as f:
|
| 239 |
return f.read()
|
|
|
|
| 263 |
response = requests.post(NVIDIA_URL, headers=headers, json=payload)
|
| 264 |
if response.status_code == 200:
|
| 265 |
country = response.json()["choices"][0]["message"]["content"].strip()
|
| 266 |
+
country = country.replace("The ", "").replace("the ", "").replace(".", "").replace("\n", " ").strip()
|
| 267 |
return country if country else "Unknown Country"
|
| 268 |
except:
|
| 269 |
pass
|
|
|
|
| 274 |
prompt_parts = []
|
| 275 |
prompt_parts.append(f"You are evaluating a national teacher framework against the {framework_name}. ")
|
| 276 |
prompt_parts.append("For each criterion below, select the SINGLE best description (0-5) that matches the document content. ")
|
| 277 |
+
prompt_parts.append("Return ONLY a JSON object where keys are criterion IDs (e.g., 'S1' or 'Q1') and values are integers 0-5. No explanations.\n")
|
| 278 |
|
| 279 |
for cid, data in criteria_dict.items():
|
| 280 |
+
prompt_parts.append(f"\n{cid} - {data['short_label']}:")
|
| 281 |
for level, desc in data['levels'].items():
|
| 282 |
prompt_parts.append(f" [{level}] {desc}")
|
| 283 |
|
| 284 |
+
prompt_parts.append(f"\n\nDOCUMENT TEXT:\n{document_text[:20000]}\n")
|
| 285 |
+
prompt_parts.append("\nRespond with valid JSON only: {\"S1\": 4, \"S2\": 3, ...} or {\"Q1\": 5, \"Q2\": 4, ...}")
|
| 286 |
|
| 287 |
+
return "\n".join(prompt_parts)
|
| 288 |
|
| 289 |
def call_nvidia_llm(prompt):
|
| 290 |
headers = {"Authorization": f"Bearer {NVIDIA_API_KEY}", "Content-Type": "application/json"}
|
|
|
|
| 309 |
try:
|
| 310 |
return json.loads(content)
|
| 311 |
except json.JSONDecodeError:
|
| 312 |
+
match = re.search(r'\{.*\}', content, re.DOTALL)
|
| 313 |
if match:
|
| 314 |
return json.loads(match.group())
|
| 315 |
raise Exception(f"Could not parse JSON from: {content[:200]}")
|
|
|
|
| 326 |
bars = plt.barh(labels, values, color=colors, edgecolor='black', linewidth=0.5)
|
| 327 |
plt.xlim(0, 5)
|
| 328 |
plt.xlabel("Score (0-5)", fontsize=12, fontweight='bold')
|
| 329 |
+
plt.title(f"{country}\n{framework_name} Alignment Evaluation", fontsize=16, fontweight='bold', pad=20)
|
| 330 |
plt.axvline(x=3, color='gray', linestyle='--', alpha=0.5)
|
| 331 |
|
| 332 |
for i, (bar, score) in enumerate(zip(bars, values)):
|
|
|
|
| 353 |
ax.set_ylim(0, 5)
|
| 354 |
ax.set_yticks([1, 2, 3, 4, 5])
|
| 355 |
ax.set_yticklabels(['1', '2', '3', '4', '5'], color="grey", size=8)
|
| 356 |
+
ax.set_title(f"{country}\nAlignment Profile", size=16, fontweight='bold', pad=20)
|
| 357 |
radar_buf = BytesIO()
|
| 358 |
plt.savefig(radar_buf, format='png', dpi=150, bbox_inches='tight')
|
| 359 |
radar_buf.seek(0)
|
|
|
|
| 385 |
ax.set_xticklabels(['0%', '25%', '50%', '75%', '100%'], fontsize=11)
|
| 386 |
ax.set_yticks([])
|
| 387 |
ax.set_xlabel(f"Overall Alignment Score", fontsize=14, fontweight='bold')
|
| 388 |
+
ax.set_title(f"{country}\n{percent:.1f}% Aligned with {framework_name}", fontsize=18, fontweight='bold', pad=15)
|
| 389 |
ax.text(50, 0, f"{percent:.1f}%", ha='center', va='center', fontsize=20, fontweight='bold', color='white' if percent > 50 else 'black')
|
| 390 |
|
| 391 |
gauge_buf = BytesIO()
|
|
|
|
| 417 |
weak = df[df['Score'] <= 2]['Label'].tolist()
|
| 418 |
moderate = df[df['Score'] == 3]['Label'].tolist()
|
| 419 |
|
| 420 |
+
summary_text.append(f"\nStrengths (Score 4-5): {len(strong)} criteria")
|
| 421 |
if strong:
|
| 422 |
for s in strong:
|
| 423 |
summary_text.append(f" ✓ {s}")
|
| 424 |
|
| 425 |
+
summary_text.append(f"\nModerate Alignment (Score 3): {len(moderate)} criteria")
|
| 426 |
if moderate:
|
| 427 |
for m in moderate:
|
| 428 |
summary_text.append(f" ~ {m}")
|
| 429 |
|
| 430 |
+
summary_text.append(f"\nPriority Areas (Score 0-2): {len(weak)} criteria")
|
| 431 |
if weak:
|
| 432 |
for w in weak:
|
| 433 |
summary_text.append(f" ✗ {w}")
|
|
|
|
| 612 |
# Initialize folders and get initial choices
|
| 613 |
os.makedirs(AFSCTP_FOLDER, exist_ok=True)
|
| 614 |
os.makedirs(ATQF_FOLDER, exist_ok=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 615 |
INITIAL_DROPDOWN_CHOICES = get_frameworks_for_type("Standards and Competencies (AFSCTP)")
|
|
|
|
|
|
|
| 616 |
|
| 617 |
# Gradio UI
|
| 618 |
+
with gr.Blocks(title="African Union Teacher Frameworks Self Assessment Tool") as demo:
|
| 619 |
+
gr.Markdown("# African Union Teacher Frameworks Self Assessment Tool")
|
| 620 |
+
gr.Markdown("Evaluate national teacher frameworks against the **African Framework of Standards and Competences for Teachers (AFSCTP)** or the **Continental Teacher Qualification Framework (ATQF/CTQF)**.")
|
| 621 |
+
|
| 622 |
+
with gr.Row():
|
| 623 |
+
with gr.Column(scale=1):
|
| 624 |
+
# Dynamic criteria display based on selection
|
| 625 |
+
with gr.Accordion("View Evaluation Criteria & Scoring Descriptors", open=False):
|
| 626 |
+
criteria_markdown = gr.Markdown(AFSCTP_DISPLAY)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 627 |
|
| 628 |
+
gr.Markdown("### Framework Selection")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 629 |
|
| 630 |
+
# Framework type selector (default to Standards)
|
| 631 |
+
framework_selector = gr.Dropdown(
|
| 632 |
+
label="Select Evaluation Framework",
|
| 633 |
+
choices=["Standards and Competencies (AFSCTP)", "Qualifications Framework (ATQF/CTQF)"],
|
| 634 |
+
value="Standards and Competencies (AFSCTP)",
|
| 635 |
+
interactive=True
|
| 636 |
+
)
|
| 637 |
|
| 638 |
+
gr.Markdown("### Input Selection")
|
| 639 |
|
| 640 |
+
# Dropdown for pre-loaded frameworks - now dynamic based on type
|
| 641 |
+
framework_dropdown = gr.Dropdown(
|
| 642 |
+
label="Select Pre-loaded Framework",
|
| 643 |
+
choices=INITIAL_DROPDOWN_CHOICES,
|
| 644 |
+
value=INITIAL_DROPDOWN_CHOICES[0] if INITIAL_DROPDOWN_CHOICES else None,
|
| 645 |
+
interactive=True
|
| 646 |
+
)
|
| 647 |
+
|
| 648 |
+
gr.Markdown("**OR**")
|
| 649 |
+
|
| 650 |
+
# File upload
|
| 651 |
+
file_input = gr.File(label="Upload Custom Document", file_types=[".pdf", ".docx", ".txt"])
|
| 652 |
+
|
| 653 |
+
evaluate_btn = gr.Button("Evaluate Framework", variant="primary")
|
| 654 |
+
status_text = gr.Textbox(label="Status", value="Waiting", interactive=False)
|
| 655 |
+
country_text = gr.Textbox(label="Detected Country", value="Unknown", interactive=False)
|
| 656 |
+
|
| 657 |
+
|
| 658 |
+
pdf_output = gr.File(label="Download PDF Report", visible=False)
|
| 659 |
+
|
| 660 |
+
with gr.Column(scale=2):
|
| 661 |
+
summary_output = gr.Markdown("")
|
| 662 |
+
gauge_output = gr.HTML("")
|
| 663 |
+
|
| 664 |
+
with gr.Row():
|
| 665 |
+
with gr.Column():
|
| 666 |
+
gr.Markdown("### Detailed Scores by Criterion")
|
| 667 |
+
bar_output = gr.HTML()
|
| 668 |
+
with gr.Column():
|
| 669 |
+
gr.Markdown("### Alignment Profile")
|
| 670 |
+
radar_output = gr.HTML()
|
| 671 |
+
|
| 672 |
+
with gr.Row():
|
| 673 |
+
table_output = gr.Dataframe(label="Detailed Evaluation Results")
|
| 674 |
+
|
| 675 |
# Update dropdown and criteria when framework type changes
|
| 676 |
def on_framework_change(framework_type):
|
| 677 |
choices = get_frameworks_for_type(framework_type)
|
|
|
|
| 688 |
)
|
| 689 |
|
| 690 |
# Main evaluation handler
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 691 |
evaluate_btn.click(
|
| 692 |
+
fn=process_inputs,
|
| 693 |
+
inputs=[file_input, framework_selector, framework_dropdown],
|
| 694 |
outputs=[table_output, summary_output, bar_output, radar_output, gauge_output,
|
| 695 |
gr.Number(visible=False), status_text, country_text, pdf_output]
|
| 696 |
).then(
|