File size: 15,608 Bytes
31157e8
1258179
d5f098f
359570c
 
 
 
 
 
31157e8
e517fb4
bbb83bb
 
 
31157e8
 
 
 
359570c
 
 
1258179
359570c
64b3fe7
359570c
 
31157e8
 
 
f7f3e73
1c66a83
d5f098f
 
2ec763a
d5f098f
 
 
 
31157e8
 
 
 
18fb6c3
 
31157e8
 
18fb6c3
2ec763a
0a4e0d7
2ec763a
 
0a4e0d7
2ec763a
 
0a4e0d7
 
2ec763a
 
0a4e0d7
18fb6c3
31157e8
18fb6c3
0a4e0d7
2ec763a
 
 
0a4e0d7
2ec763a
0a4e0d7
 
 
2ec763a
 
0a4e0d7
18fb6c3
31157e8
abfd25a
359570c
 
 
 
abfd25a
359570c
 
abfd25a
359570c
 
abfd25a
359570c
 
31157e8
359570c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31157e8
359570c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0a4e0d7
 
 
 
359570c
 
 
 
 
 
 
 
0a4e0d7
359570c
d5f098f
 
 
 
 
 
 
359570c
0a4e0d7
359570c
 
0a4e0d7
d5f098f
1c66a83
359570c
0a4e0d7
 
 
 
 
359570c
 
0a4e0d7
359570c
 
 
 
 
 
 
 
 
31157e8
359570c
 
 
 
 
 
 
 
 
 
 
 
 
0a4e0d7
359570c
 
 
 
 
0a4e0d7
359570c
 
0a4e0d7
359570c
1c66a83
 
6f9927c
acbda2a
 
 
 
 
 
6f9927c
359570c
acbda2a
6f9927c
359570c
 
 
 
 
 
 
 
 
 
 
 
6f9927c
18fb6c3
 
 
 
 
 
 
 
 
 
 
 
 
 
31157e8
 
 
d5f098f
 
 
 
 
 
 
359570c
18fb6c3
 
359570c
 
 
 
 
 
2ec763a
 
 
 
 
359570c
 
18fb6c3
 
 
 
 
359570c
 
18fb6c3
 
 
 
359570c
 
 
 
 
 
 
 
18fb6c3
31157e8
 
 
 
 
 
 
 
 
18fb6c3
31157e8
 
 
18fb6c3
31157e8
1cb2d7e
 
18fb6c3
1cb2d7e
 
5181d14
1cb2d7e
18fb6c3
 
1cb2d7e
 
359570c
1cb2d7e
 
 
 
359570c
 
1cb2d7e
 
18fb6c3
 
 
359570c
18fb6c3
0a4e0d7
 
 
 
 
 
 
 
 
18fb6c3
 
 
 
 
 
 
 
 
 
 
 
 
 
359570c
 
18fb6c3
 
 
31157e8
 
 
18fb6c3
31157e8
 
 
 
 
 
 
18fb6c3
 
359570c
 
31157e8
 
 
 
 
 
 
 
 
 
 
 
 
18fb6c3
 
359570c
 
31157e8
 
 
359570c
 
0a4e0d7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
359570c
31157e8
 
 
 
359570c
31157e8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
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", "AI Agents Fundamentals")

ds = load_dataset(EXAM_DATASET_ID, split="train")

DATASET_REPO_URL = "https://huggingface.co/datasets/agents-course/certificates"

# Convert dataset to a list of dicts and randomly sort
quiz_data = ds.to_pandas().to_dict("records")
random.shuffle(quiz_data)

# Limit to max questions if specified
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),  # login_btn
            gr.update(visible=True),  # start_btn
            gr.update(visible=False),  # next_btn
            gr.update(visible=False),  # submit_btn
            "",  # question_text
            gr.update(choices=[], visible=False),  # radio_choices
            "Click 'Start' to begin the quiz",  # status_text
            0,  # question_idx
            [],  # user_answers
            gr.update(visible=False),  # certificate_img
            gr.update(visible=False),  # linkedin_btn
            token,  # user_token
        ]
    else:
        return [
            gr.update(visible=True),  # login_btn
            gr.update(visible=False),  # start_btn
            gr.update(visible=False),  # next_btn
            gr.update(visible=False),  # submit_btn
            "",  # question_text
            gr.update(choices=[], visible=False),  # radio_choices
            "",  # status_text
            0,  # question_idx
            [],  # user_answers
            gr.update(visible=False),  # certificate_img
            gr.update(visible=False),  # linkedin_btn
            None,  # user_token
        ]


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

    # Use the dataset certificate URL if available, otherwise fallback to default
    certificate_url = cert_url or "https://huggingface.co/agents-course-finishers"

    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,  # Using username as cert ID
    }

    # Build the LinkedIn button URL
    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://download.linkedin.com/desktop/add2profile/buttons/en_US.png" 
                 alt="LinkedIn Add to Profile button">
        </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."""
    # Save image to temporary file
    with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
        certificate_img.save(tmp.name)

        try:
            # Run upload in a thread pool since upload_file is blocking
            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="agents-course/certificates",
                repo_type="dataset",
                token=os.getenv("HF_TOKEN"),
            )
            await loop.run_in_executor(None, upload_func)

            # Construct the URL to the image
            cert_url = (
                f"https://huggingface.co/datasets/agents-course/certificates/"
                f"resolve/main/certificates/{username}/{date.today()}.png"
            )

            # Clean up temp file
            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),  # hide custom name input
        )

    # Calculate grade
    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),  # hide custom name input
        )

    try:
        # Use custom name if provided, otherwise use profile name
        name = (
            custom_name.strip() if custom_name and custom_name.strip() else profile.name
        )

        # Generate certificate
        certificate_img, _ = generate_certificate(
            name=name, profile_url=profile.picture
        )

        # Start certificate upload asynchronously
        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/agents-course"

        # Create LinkedIn button
        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),  # show custom name input
        )

    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),  # hide custom name input
        )


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 [
            "",  # question_text
            gr.update(choices=[], visible=False),  # radio choices
            "Please login first",  # status_text
            question_idx,  # question_idx
            user_answers,  # user_answers
            gr.update(visible=True),  # start button
            gr.update(visible=False),  # next button
            gr.update(visible=False),  # submit button
            gr.update(visible=False),  # certificate image
            gr.update(visible=False),  # linkedin button
        ]

    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)
        results_text = (
            f"**Quiz Complete!**\n\n"
            f"Your score: {grade:.1%}\n"
            f"Passing score: {float(EXAM_PASSING_SCORE):.1%}\n\n"
        )
        has_passed = grade >= float(EXAM_PASSING_SCORE)
        return [
            "",  # question_text
            gr.update(choices=[], visible=False),  # radio choices
            f"{'πŸŽ‰ Passed! Click now on πŸŽ“ Get your certificate!' if has_passed else '❌ Did not pass'}",  # status_text
            question_idx,  # question_idx
            user_answers,  # user_answers
            gr.update(visible=False),  # start button
            gr.update(visible=False),  # next button
            gr.update(
                visible=True,
                value=f"πŸŽ“ Get your certificate" if has_passed else "❌ Did not pass",
                interactive=has_passed,
            ),  # submit button
            gr.update(visible=False),  # certificate image
            gr.update(visible=False),  # linkedin button
        ]

    # Show next question
    q = quiz_data[question_idx]
    return [
        f"## Question {question_idx + 1} \n### {q['question']}",  # question_text
        gr.update(  # radio choices
            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.",  # status_text
        question_idx,  # question_idx
        user_answers,  # user_answers
        gr.update(visible=False),  # start button
        gr.update(visible=True),  # next button
        gr.update(visible=False),  # submit button
        gr.update(visible=False),  # certificate image
        gr.update(visible=False),  # linkedin button
    ]


def success_message(response):
    # response is whatever push_results_to_hub returned
    return f"{response}\n\n**Success!**"


with gr.Blocks() as demo:
    demo.title = f"Dataset Quiz for {EXAM_DATASET_ID}"

    # State variables
    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=False)

    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,
        )

    # Wire up the event handlers
    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__":
    # Note: If testing locally, you'll need to run `huggingface-cli login` or set HF_TOKEN
    # environment variable for the login to work locally.
    demo.queue()  # Enable queuing for async operations
    demo.launch()