| | import os |
| | from datetime import datetime |
| | import random |
| | import requests |
| | from io import BytesIO |
| | from datetime import date |
| | import tempfile |
| | from PIL import Image, ImageDraw, ImageFont |
| | from huggingface_hub import upload_file |
| |
|
| | import pandas as pd |
| | from huggingface_hub import HfApi, hf_hub_download, Repository |
| | from huggingface_hub.repocard import metadata_load |
| |
|
| | import gradio as gr |
| | from datasets import load_dataset, Dataset |
| | from huggingface_hub import whoami |
| |
|
| | import asyncio |
| | from functools import partial |
| |
|
| | EXAM_DATASET_ID = os.getenv("EXAM_DATASET_ID") or "agents-course/unit_1_quiz" |
| | EXAM_MAX_QUESTIONS = os.getenv("EXAM_MAX_QUESTIONS") or 1 |
| | EXAM_PASSING_SCORE = os.getenv("EXAM_PASSING_SCORE") or 0.8 |
| | CERTIFYING_ORG_LINKEDIN_ID = os.getenv("CERTIFYING_ORG_LINKEDIN_ID", "000000") |
| | COURSE_TITLE = os.getenv("COURSE_TITLE", "Fundamentals of MCP") |
| |
|
| | ds = load_dataset(EXAM_DATASET_ID, split="train") |
| |
|
| | DATASET_REPO_URL = "https://huggingface.co/datasets/huggingface-course/certificates" |
| |
|
| | |
| | quiz_data = ds.to_pandas().to_dict("records") |
| | random.shuffle(quiz_data) |
| |
|
| | |
| | if EXAM_MAX_QUESTIONS: |
| | quiz_data = quiz_data[: int(EXAM_MAX_QUESTIONS)] |
| |
|
| |
|
| | def on_user_logged_in(token: gr.OAuthToken | None): |
| | """ |
| | If the user has a valid token, show Start button. |
| | Otherwise, keep the login button visible. |
| | """ |
| | if token is not None: |
| | return [ |
| | gr.update(visible=False), |
| | gr.update(visible=True), |
| | gr.update(visible=False), |
| | gr.update(visible=False), |
| | "", |
| | gr.update(choices=[], visible=False), |
| | "Click 'Start' to begin the quiz", |
| | 0, |
| | [], |
| | gr.update(visible=False), |
| | gr.update( |
| | visible=True, |
| | value=""" |
| | <div style="text-align: center; padding: 20px; border: 2px dashed #ccc;"> |
| | <h3>🎯 Complete the Quiz to Unlock</h3> |
| | <p>Pass the quiz to add your certificate to LinkedIn!</p> |
| | <img src="https://huggingface.co/spaces/mcp-course/unit_1_quiz/resolve/main/li_logo.webp" alt="LinkedIn Add to Profile" style="width: 80px; height: 80px; text-align: center;"> |
| | </div> |
| | """, |
| | ), |
| | token, |
| | ] |
| | else: |
| | return [ |
| | gr.update(visible=True), |
| | gr.update(visible=False), |
| | gr.update(visible=False), |
| | gr.update(visible=False), |
| | "", |
| | gr.update(choices=[], visible=False), |
| | "", |
| | 0, |
| | [], |
| | gr.update(visible=False), |
| | gr.update( |
| | visible=True, |
| | value=""" |
| | <div style="text-align: center; padding: 20px; border: 2px dashed #ccc; border-radius: 10px; margin-top: 20px;"> |
| | <h3>🔒 Login Required</h3> |
| | <img src="https://huggingface.co/spaces/mcp-course/unit_1_quiz/resolve/main/li_logo.webp" alt="LinkedIn Add to Profile" style="width: 80px; height: 80px; text-align: center;"> |
| | </div> |
| | """, |
| | ), |
| | None, |
| | ] |
| |
|
| |
|
| | def generate_certificate(name: str, profile_url: str): |
| | """Generate certificate image and PDF.""" |
| | certificate_path = os.path.join( |
| | os.path.dirname(__file__), "templates", "certificate.png" |
| | ) |
| | im = Image.open(certificate_path) |
| | d = ImageDraw.Draw(im) |
| |
|
| | name_font = ImageFont.truetype("Quattrocento-Regular.ttf", 100) |
| | date_font = ImageFont.truetype("Quattrocento-Regular.ttf", 48) |
| |
|
| | name = name.title() |
| | d.text((1000, 740), name, fill="black", anchor="mm", font=name_font) |
| |
|
| | d.text((1480, 1170), str(date.today()), fill="black", anchor="mm", font=date_font) |
| |
|
| | pdf = im.convert("RGB") |
| | pdf.save("certificate.pdf") |
| |
|
| | return im, "certificate.pdf" |
| |
|
| |
|
| | def create_linkedin_button(username: str, cert_url: str | None) -> str: |
| | """Create LinkedIn 'Add to Profile' button HTML.""" |
| | current_year = date.today().year |
| | current_month = date.today().month |
| |
|
| | |
| | certificate_url = cert_url or "https://huggingface.co/mcp-course" |
| |
|
| | linkedin_params = { |
| | "startTask": "CERTIFICATION_NAME", |
| | "name": COURSE_TITLE, |
| | "organizationName": "Hugging Face", |
| | "organizationId": CERTIFYING_ORG_LINKEDIN_ID, |
| | "organizationIdissueYear": str(current_year), |
| | "issueMonth": str(current_month), |
| | "certUrl": certificate_url, |
| | "certId": username, |
| | } |
| |
|
| | |
| | base_url = "https://www.linkedin.com/profile/add?" |
| | params = "&".join( |
| | f"{k}={requests.utils.quote(v)}" for k, v in linkedin_params.items() |
| | ) |
| | button_url = base_url + params |
| |
|
| | message = f""" |
| | <a href="{button_url}" target="_blank" style="display: block; margin-top: 20px; text-align: center;"> |
| | <img src="https://huggingface.co/spaces/mcp-course/unit_1_quiz/resolve/main/li_button.png" |
| | alt="LinkedIn Add to Profile button" style="width: 300px; height: 80px;"> |
| | </a> |
| | """ |
| | return message |
| |
|
| |
|
| | async def upload_certificate_to_hub(username: str, certificate_img) -> str: |
| | """Upload certificate to the dataset hub and return the URL asynchronously.""" |
| | |
| | with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp: |
| | certificate_img.save(tmp.name) |
| |
|
| | try: |
| | |
| | loop = asyncio.get_event_loop() |
| | upload_func = partial( |
| | upload_file, |
| | path_or_fileobj=tmp.name, |
| | path_in_repo=f"certificates/{username}/{date.today()}.png", |
| | repo_id="mcp-course/certificates", |
| | repo_type="dataset", |
| | token=os.getenv("HF_TOKEN"), |
| | ) |
| | await loop.run_in_executor(None, upload_func) |
| |
|
| | |
| | cert_url = ( |
| | f"https://huggingface.co/datasets/mcp-course/certificates/" |
| | f"resolve/main/certificates/{username}/{date.today()}.png" |
| | ) |
| |
|
| | |
| | os.unlink(tmp.name) |
| | return cert_url |
| |
|
| | except Exception as e: |
| | print(f"Error uploading certificate: {e}") |
| | os.unlink(tmp.name) |
| | return None |
| |
|
| |
|
| | async def push_results_to_hub( |
| | user_answers, |
| | custom_name: str | None, |
| | token: gr.OAuthToken | None, |
| | profile: gr.OAuthProfile | None, |
| | ): |
| | """Handle quiz completion and certificate generation.""" |
| | if token is None or profile is None: |
| | gr.Warning("Please log in to Hugging Face before submitting!") |
| | return ( |
| | gr.update(visible=True, value="Please login first"), |
| | gr.update(visible=False), |
| | gr.update(visible=False), |
| | gr.update(visible=False), |
| | ) |
| |
|
| | |
| | correct_count = sum(1 for answer in user_answers if answer["is_correct"]) |
| | total_questions = len(user_answers) |
| | grade = correct_count / total_questions if total_questions > 0 else 0 |
| |
|
| | if grade < float(EXAM_PASSING_SCORE): |
| | return ( |
| | gr.update(visible=True, value=f"You scored {grade:.1%}..."), |
| | gr.update(visible=False), |
| | gr.update(visible=False), |
| | gr.update(visible=False), |
| | ) |
| |
|
| | try: |
| | |
| | name = ( |
| | custom_name.strip() if custom_name and custom_name.strip() else profile.name |
| | ) |
| |
|
| | |
| | certificate_img, _ = generate_certificate( |
| | name=name, profile_url=profile.picture |
| | ) |
| |
|
| | |
| | gr.Info("Uploading your certificate...") |
| | cert_url = await upload_certificate_to_hub(profile.username, certificate_img) |
| |
|
| | if cert_url is None: |
| | gr.Warning("Certificate upload failed, but you still passed!") |
| | cert_url = "https://huggingface.co/mcp-course" |
| |
|
| | |
| | linkedin_button = create_linkedin_button(profile.username, cert_url) |
| |
|
| | result_message = f""" |
| | 🎉 Congratulations! You passed with a score of {grade:.1%}! |
| | |
| | {linkedin_button} |
| | """ |
| |
|
| | return ( |
| | gr.update(visible=True, value=result_message), |
| | gr.update(visible=True, value=certificate_img), |
| | gr.update(visible=True), |
| | gr.update(visible=True), |
| | ) |
| |
|
| | except Exception as e: |
| | print(f"Error generating certificate: {e}") |
| | return ( |
| | gr.update(visible=True, value=f"🎉 You passed with {grade:.1%}!"), |
| | gr.update(visible=False), |
| | gr.update(visible=False), |
| | gr.update(visible=False), |
| | ) |
| |
|
| |
|
| | def handle_quiz( |
| | question_idx, |
| | user_answers, |
| | selected_answer, |
| | is_start, |
| | token: gr.OAuthToken | None, |
| | profile: gr.OAuthProfile | None, |
| | ): |
| | """Handle quiz state transitions and store answers""" |
| | if token is None or profile is None: |
| | gr.Warning("Please log in to Hugging Face before starting the quiz!") |
| | return [ |
| | "", |
| | gr.update(choices=[], visible=False), |
| | "Please login first", |
| | question_idx, |
| | user_answers, |
| | gr.update(visible=True), |
| | gr.update(visible=False), |
| | gr.update(visible=False), |
| | gr.update(visible=False), |
| | gr.update( |
| | visible=True, |
| | value=""" |
| | <div style="text-align: center; padding: 20px; border: 2px dashed #ccc;"> |
| | <h3>🔒 Login Required</h3> |
| | <p>Please log in with your Hugging Face account to access the quiz and earn your LinkedIn certificate!</p> |
| | </div> |
| | """, |
| | ), |
| | ] |
| |
|
| | if not is_start and question_idx < len(quiz_data): |
| | current_q = quiz_data[question_idx] |
| | correct_reference = current_q["correct_answer"] |
| | correct_reference = f"answer_{correct_reference}".lower() |
| | is_correct = selected_answer == current_q[correct_reference] |
| | user_answers.append( |
| | { |
| | "question": current_q["question"], |
| | "selected_answer": selected_answer, |
| | "correct_answer": current_q[correct_reference], |
| | "is_correct": is_correct, |
| | "correct_reference": correct_reference, |
| | } |
| | ) |
| | question_idx += 1 |
| |
|
| | if question_idx >= len(quiz_data): |
| | correct_count = sum(1 for answer in user_answers if answer["is_correct"]) |
| | grade = correct_count / len(user_answers) |
| | has_passed = grade >= float(EXAM_PASSING_SCORE) |
| |
|
| | |
| | linkedin_completion_text = ( |
| | """ |
| | <div style="text-align: center; padding: 20px; border: 2px dashed #4CAF50;"> |
| | <h3>🎉 Ready for LinkedIn!</h3> |
| | <p>Great! Click "Get your certificate" above to unlock the LinkedIn button.</p> |
| | <img src="https://huggingface.co/spaces/mcp-course/unit_1_quiz/resolve/main/li_logo.webp" alt="LinkedIn Add to Profile" style="width: 80px; height: 80px; text-align: center;"> |
| | </div> |
| | """ |
| | if has_passed |
| | else """ |
| | <div style="text-align: center; padding: 20px; border: 2px dashed #ff6b6b;"> |
| | <h3>❌ Try Again</h3> |
| | <p>You need a higher score to earn the LinkedIn certificate. Please retake the quiz!</p> |
| | </div> |
| | """ |
| | ) |
| |
|
| | return [ |
| | "", |
| | gr.update(choices=[], visible=False), |
| | f"{'🎉 Passed! Click now on 🎓 Get your certificate!' if has_passed else '❌ Did not pass'}", |
| | question_idx, |
| | user_answers, |
| | gr.update(visible=False), |
| | gr.update(visible=False), |
| | gr.update( |
| | visible=True, |
| | value="🎓 Get your certificate" if has_passed else "❌ Did not pass", |
| | interactive=has_passed, |
| | ), |
| | gr.update(visible=False), |
| | gr.update(visible=True, value=linkedin_completion_text), |
| | ] |
| |
|
| | |
| | q = quiz_data[question_idx] |
| | return [ |
| | f"## Question {question_idx + 1} \n### {q['question']}", |
| | gr.update( |
| | choices=[q["answer_a"], q["answer_b"], q["answer_c"], q["answer_d"]], |
| | value=None, |
| | visible=True, |
| | ), |
| | "Select an answer and click 'Next' to continue.", |
| | question_idx, |
| | user_answers, |
| | gr.update(visible=False), |
| | gr.update(visible=True), |
| | gr.update(visible=False), |
| | gr.update(visible=False), |
| | gr.update( |
| | visible=True, |
| | value=""" |
| | <div style="text-align: center; padding: 20px; border: 2px dashed #ccc;"> |
| | <h3>🎯 Keep Going!</h3> |
| | <p>Complete the quiz and pass to unlock your LinkedIn certificate!</p> |
| | <img src="https://huggingface.co/spaces/mcp-course/unit_1_quiz/resolve/main/li_logo.webp" alt="LinkedIn Add to Profile" style="width: 80px; height: 80px; text-align: center;"> |
| | </div> |
| | """, |
| | ), |
| | ] |
| |
|
| |
|
| | def success_message(response): |
| | |
| | return f"{response}\n\n**Success!**" |
| |
|
| |
|
| | with gr.Blocks() as demo: |
| | demo.title = f"Dataset Quiz for {EXAM_DATASET_ID}" |
| |
|
| | |
| | question_idx = gr.State(value=0) |
| | user_answers = gr.State(value=[]) |
| | user_token = gr.State(value=None) |
| |
|
| | with gr.Row(variant="compact"): |
| | gr.Markdown(f"## Welcome to the {EXAM_DATASET_ID} Quiz") |
| |
|
| | with gr.Row(variant="compact"): |
| | gr.Markdown( |
| | "- Log in first, then click 'Start' to begin. \n- Answer each question, click 'Next' \n- click 'Submit' to publish your results to the Hugging Face Hub." |
| | ) |
| |
|
| | with gr.Row(variant="panel"): |
| | question_text = gr.Markdown("") |
| | radio_choices = gr.Radio( |
| | choices=[], label="Your Answer", scale=1, visible=False |
| | ) |
| |
|
| | with gr.Row(variant="compact"): |
| | status_text = gr.Markdown("") |
| | certificate_img = gr.Image(type="pil", visible=False) |
| | linkedin_btn = gr.HTML( |
| | visible=True, |
| | value=""" |
| | <div style="text-align: center; padding: 20px; border: 2px dashed #ccc; border-radius: 10px; margin-top: 20px;"> |
| | <h3>🔒 Login Required</h3> |
| | <p>Please log in with your Hugging Face account to access the quiz and earn your LinkedIn certificate!</p> |
| | </div> |
| | """, |
| | ) |
| |
|
| | with gr.Row(variant="compact"): |
| | login_btn = gr.LoginButton(visible=True) |
| | start_btn = gr.Button("Start ⏭️", visible=True) |
| | next_btn = gr.Button("Next ⏭️", visible=False) |
| | submit_btn = gr.Button("🎓 Get your certificate", visible=False) |
| |
|
| | with gr.Row(variant="panel"): |
| | custom_name_input = gr.Textbox( |
| | label="Custom Name for Certificate", |
| | placeholder="Enter name as you want it to appear on the certificate", |
| | info="Leave empty to use your Hugging Face profile name", |
| | visible=False, |
| | value=None, |
| | ) |
| |
|
| | |
| | login_btn.click( |
| | fn=on_user_logged_in, |
| | inputs=None, |
| | outputs=[ |
| | login_btn, |
| | start_btn, |
| | next_btn, |
| | submit_btn, |
| | question_text, |
| | radio_choices, |
| | status_text, |
| | question_idx, |
| | user_answers, |
| | certificate_img, |
| | linkedin_btn, |
| | user_token, |
| | ], |
| | ) |
| |
|
| | start_btn.click( |
| | fn=handle_quiz, |
| | inputs=[question_idx, user_answers, gr.State(""), gr.State(True)], |
| | outputs=[ |
| | question_text, |
| | radio_choices, |
| | status_text, |
| | question_idx, |
| | user_answers, |
| | start_btn, |
| | next_btn, |
| | submit_btn, |
| | certificate_img, |
| | linkedin_btn, |
| | ], |
| | ) |
| |
|
| | next_btn.click( |
| | fn=handle_quiz, |
| | inputs=[question_idx, user_answers, radio_choices, gr.State(False)], |
| | outputs=[ |
| | question_text, |
| | radio_choices, |
| | status_text, |
| | question_idx, |
| | user_answers, |
| | start_btn, |
| | next_btn, |
| | submit_btn, |
| | certificate_img, |
| | linkedin_btn, |
| | ], |
| | ) |
| |
|
| | submit_btn.click( |
| | fn=push_results_to_hub, |
| | inputs=[ |
| | user_answers, |
| | custom_name_input, |
| | ], |
| | outputs=[ |
| | status_text, |
| | certificate_img, |
| | linkedin_btn, |
| | custom_name_input, |
| | ], |
| | ) |
| |
|
| | custom_name_input.submit( |
| | fn=push_results_to_hub, |
| | inputs=[user_answers, custom_name_input], |
| | outputs=[status_text, certificate_img, linkedin_btn, custom_name_input], |
| | ) |
| |
|
| | if __name__ == "__main__": |
| | |
| | |
| | demo.queue() |
| | demo.launch() |
| |
|