This commit is contained in:
parent
516219513a
commit
2f26dbb1e7
@ -189,7 +189,9 @@ async def clippy_proxy(request: Request):
|
|||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"X-App-Reference": "devplace-devii-v-1-0-0",
|
"X-App-Reference": "devplace-devii-v-1-0-0",
|
||||||
}
|
}
|
||||||
if cfg.get("devii_ai_key"):
|
if user.get("api_key"):
|
||||||
|
headers["Authorization"] = f"Bearer {user['api_key']}"
|
||||||
|
elif cfg.get("devii_ai_key"):
|
||||||
headers["Authorization"] = f"Bearer {cfg['devii_ai_key']}"
|
headers["Authorization"] = f"Bearer {cfg['devii_ai_key']}"
|
||||||
async with stealth.stealth_async_client(timeout=45.0) as client:
|
async with stealth.stealth_async_client(timeout=45.0) as client:
|
||||||
upstream = await client.post(cfg["devii_ai_url"], content=body, headers=headers)
|
upstream = await client.post(cfg["devii_ai_url"], content=body, headers=headers)
|
||||||
|
|||||||
@ -681,6 +681,20 @@
|
|||||||
margin-bottom: var(--space-xl);
|
margin-bottom: var(--space-xl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.quiz-slide-nav {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-md);
|
||||||
|
margin-bottom: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quiz-slide-counter {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.86rem;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
.quiz-question-answered .quiz-answer-form {
|
.quiz-question-answered .quiz-answer-form {
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,7 +16,9 @@ export class AppQuizPlayer extends Component {
|
|||||||
this._progress = this.querySelector("[data-quiz-progress]");
|
this._progress = this.querySelector("[data-quiz-progress]");
|
||||||
this._score = this.querySelector("[data-quiz-score]");
|
this._score = this.querySelector("[data-quiz-score]");
|
||||||
this._timer = this.querySelector("[data-quiz-timer]");
|
this._timer = this.querySelector("[data-quiz-timer]");
|
||||||
|
this._slides = Array.from(this.querySelectorAll("[data-quiz-slide]"));
|
||||||
this._bindForms();
|
this._bindForms();
|
||||||
|
if (this._slides.length) this._initSlides();
|
||||||
if (this._hasLimit && this._status === "in_progress") this._startTimer();
|
if (this._hasLimit && this._status === "in_progress") this._startTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,6 +26,46 @@ export class AppQuizPlayer extends Component {
|
|||||||
if (this._interval) clearInterval(this._interval);
|
if (this._interval) clearInterval(this._interval);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_initSlides() {
|
||||||
|
const startAt = this._slides.findIndex((slide) => !slide.classList.contains("quiz-question-answered"));
|
||||||
|
this._buildSlideNav();
|
||||||
|
this._showSlide(startAt === -1 ? this._slides.length - 1 : startAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
_buildSlideNav() {
|
||||||
|
const host = this.querySelector("[data-quiz-slides]");
|
||||||
|
if (!host) return;
|
||||||
|
const nav = document.createElement("div");
|
||||||
|
nav.className = "quiz-slide-nav";
|
||||||
|
this._prevBtn = document.createElement("button");
|
||||||
|
this._prevBtn.type = "button";
|
||||||
|
this._prevBtn.className = "btn btn-secondary";
|
||||||
|
this._prevBtn.textContent = "Previous";
|
||||||
|
this._prevBtn.addEventListener("click", () => this._showSlide(this._current - 1));
|
||||||
|
this._counter = document.createElement("span");
|
||||||
|
this._counter.className = "quiz-slide-counter";
|
||||||
|
this._nextBtn = document.createElement("button");
|
||||||
|
this._nextBtn.type = "button";
|
||||||
|
this._nextBtn.className = "btn btn-secondary";
|
||||||
|
this._nextBtn.textContent = "Next";
|
||||||
|
this._nextBtn.addEventListener("click", () => this._showSlide(this._current + 1));
|
||||||
|
nav.appendChild(this._prevBtn);
|
||||||
|
nav.appendChild(this._counter);
|
||||||
|
nav.appendChild(this._nextBtn);
|
||||||
|
host.before(nav);
|
||||||
|
}
|
||||||
|
|
||||||
|
_showSlide(index) {
|
||||||
|
if (!this._slides.length) return;
|
||||||
|
this._current = Math.max(0, Math.min(this._slides.length - 1, index));
|
||||||
|
this._slides.forEach((slide, i) => {
|
||||||
|
slide.hidden = i !== this._current;
|
||||||
|
});
|
||||||
|
if (this._prevBtn) this._prevBtn.disabled = this._current === 0;
|
||||||
|
if (this._nextBtn) this._nextBtn.disabled = this._current === this._slides.length - 1;
|
||||||
|
if (this._counter) this._counter.textContent = `Question ${this._current + 1} of ${this._slides.length}`;
|
||||||
|
}
|
||||||
|
|
||||||
_bindForms() {
|
_bindForms() {
|
||||||
this.querySelectorAll("[data-quiz-answer-form]").forEach((form) => {
|
this.querySelectorAll("[data-quiz-answer-form]").forEach((form) => {
|
||||||
form.addEventListener("submit", (event) => {
|
form.addEventListener("submit", (event) => {
|
||||||
@ -133,9 +175,9 @@ export class AppQuizPlayer extends Component {
|
|||||||
|
|
||||||
_advance(form) {
|
_advance(form) {
|
||||||
const question = form.closest("[data-quiz-question]");
|
const question = form.closest("[data-quiz-question]");
|
||||||
if (!question) return;
|
if (!question || !this._slides.length) return;
|
||||||
const next = question.nextElementSibling;
|
const index = this._slides.indexOf(question);
|
||||||
if (next && next.scrollIntoView) next.scrollIntoView({ block: "start", behavior: "smooth" });
|
if (index !== -1 && index < this._slides.length - 1) this._showSlide(index + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -69,7 +69,7 @@
|
|||||||
</article>
|
</article>
|
||||||
|
|
||||||
{% if leaderboard %}
|
{% if leaderboard %}
|
||||||
<section class="quiz-leaderboard" aria-label="Quiz leaderboard">
|
<section id="leaderboard" class="quiz-leaderboard" aria-label="Quiz leaderboard">
|
||||||
<h2>Leaderboard</h2>
|
<h2>Leaderboard</h2>
|
||||||
<ol class="quiz-leaderboard-list">
|
<ol class="quiz-leaderboard-list">
|
||||||
{% for entry in leaderboard %}
|
{% for entry in leaderboard %}
|
||||||
|
|||||||
@ -46,7 +46,7 @@
|
|||||||
<form method="POST" action="{{ quiz.url }}/attempts" class="inline-form">
|
<form method="POST" action="{{ quiz.url }}/attempts" class="inline-form">
|
||||||
<button type="submit" class="btn btn-primary">Play again</button>
|
<button type="submit" class="btn btn-primary">Play again</button>
|
||||||
</form>
|
</form>
|
||||||
<a href="{{ quiz.url }}/leaderboard" class="btn btn-secondary">Leaderboard</a>
|
<a href="{{ quiz.url }}#leaderboard" class="btn btn-secondary">Leaderboard</a>
|
||||||
<a href="/quizzes" class="btn btn-secondary">All quizzes</a>
|
<a href="/quizzes" class="btn btn-secondary">All quizzes</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -68,6 +68,33 @@ def test_starting_opens_the_player(alice):
|
|||||||
page.locator(".quiz-hud").wait_for(state="visible")
|
page.locator(".quiz-hud").wait_for(state="visible")
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_player_shows_one_question_at_a_time(alice):
|
||||||
|
page, _ = alice
|
||||||
|
slug = _author_quiz("Player one at a time quiz")
|
||||||
|
_start(page, slug)
|
||||||
|
questions = page.locator(".quiz-question")
|
||||||
|
assert questions.nth(0).is_visible()
|
||||||
|
assert not questions.nth(1).is_visible()
|
||||||
|
page.locator(".quiz-slide-nav button:has-text('Next')").click()
|
||||||
|
assert not questions.nth(0).is_visible()
|
||||||
|
assert questions.nth(1).is_visible()
|
||||||
|
page.locator(".quiz-slide-nav button:has-text('Previous')").click()
|
||||||
|
assert questions.nth(0).is_visible()
|
||||||
|
assert not questions.nth(1).is_visible()
|
||||||
|
|
||||||
|
|
||||||
|
def test_answering_advances_to_the_next_question(alice):
|
||||||
|
page, _ = alice
|
||||||
|
slug = _author_quiz("Player advance quiz")
|
||||||
|
_start(page, slug)
|
||||||
|
questions = page.locator(".quiz-question")
|
||||||
|
questions.nth(0).locator("input[type='radio']").last.check()
|
||||||
|
questions.nth(0).locator("button:has-text('Submit answer')").click()
|
||||||
|
questions.nth(0).locator(".quiz-grade").wait_for(state="visible")
|
||||||
|
questions.nth(1).wait_for(state="visible")
|
||||||
|
assert not questions.nth(0).is_visible()
|
||||||
|
|
||||||
|
|
||||||
def test_the_player_renders_a_real_form_per_question(alice):
|
def test_the_player_renders_a_real_form_per_question(alice):
|
||||||
page, _ = alice
|
page, _ = alice
|
||||||
slug = _author_quiz("Player forms quiz")
|
slug = _author_quiz("Player forms quiz")
|
||||||
@ -129,7 +156,7 @@ def test_an_answered_question_cannot_be_resubmitted(alice):
|
|||||||
|
|
||||||
def test_a_numeric_question_takes_a_typed_answer(alice):
|
def test_a_numeric_question_takes_a_typed_answer(alice):
|
||||||
page, _ = alice
|
page, _ = alice
|
||||||
slug = _author_quiz("Player numeric quiz")
|
slug = _author_quiz("Player numeric quiz", questions=(NUMERIC,))
|
||||||
_start(page, slug)
|
_start(page, slug)
|
||||||
question = page.locator(".quiz-question-numeric").first
|
question = page.locator(".quiz-question-numeric").first
|
||||||
question.locator("input[name='answer_text']").fill("1024")
|
question.locator("input[name='answer_text']").fill("1024")
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user