Skip to content
This repository was archived by the owner on Oct 15, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions assets/styles/app.scss
Original file line number Diff line number Diff line change
Expand Up @@ -413,3 +413,21 @@ ul.credit {
grid-area: leaderboard;
}
}

// The placeholder that shows only after 100ms
// to prevent the flickering.
.app-placeholder {
animation: fade-in 200ms;

@keyframes fade-in {
0% {
opacity: 0;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}
}
3 changes: 0 additions & 3 deletions src/Controller/ChallengeController.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
use App\Entity\Question;
use App\Entity\SolutionVideoEvent;
use App\Entity\User;
use App\Repository\QuestionRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
Expand All @@ -21,12 +20,10 @@ class ChallengeController extends AbstractController
public function index(
#[CurrentUser] User $user,
Question $question,
QuestionRepository $questionRepository,
): Response {
return $this->render('challenge/index.html.twig', [
'user' => $user,
'question' => $question,
'limit' => $questionRepository->count(),
]);
}

Expand Down
19 changes: 16 additions & 3 deletions src/Twig/Components/Challenge/Header.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,21 @@
use App\Repository\SolutionEventRepository;
use App\Service\PassRateService;
use App\Service\Types\PassRate;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveListener;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
use Symfony\UX\LiveComponent\DefaultActionTrait;

#[AsTwigComponent]
#[AsLiveComponent]
final class Header
{
use DefaultActionTrait;

#[LiveProp]
public User $user;

#[LiveProp]
public Question $question;
public int $limit;

public function __construct(
private readonly SolutionEventRepository $solutionEventRepository,
Expand Down Expand Up @@ -50,4 +57,10 @@ public function getPassRate(): PassRate
{
return $this->passRateService->getPassRate($this->question, $this->user->getGroup());
}

#[LiveListener('app:challenge-executor:query-created')]
public function onQueryUpdated(): void
{
// Update "Solve State" and "Pass Rate" after a new query is created.
}
}
12 changes: 7 additions & 5 deletions src/Twig/Components/Challenge/Instruction/Content.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

namespace App\Twig\Components\Challenge\Instruction;

use Symfony\Component\Serializer\SerializerInterface;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveArg;
use Symfony\UX\LiveComponent\Attribute\LiveListener;
Expand All @@ -17,12 +16,15 @@ final class Content
use DefaultActionTrait;

#[LiveProp(writable: true)]
public ?HintPayload $hint = null;
public ?string $type = null;

#[LiveProp(writable: true)]
public ?string $hint = null;

#[LiveListener('app:challenge-hint')]
public function onHintReceived(SerializerInterface $serializer, #[LiveArg] string $hint): void
public function onHintReceived(#[LiveArg] string $type, #[LiveArg] string $hint): void
{
$deserializedHint = $serializer->deserialize($hint, HintPayload::class, 'json');
$this->hint = $deserializedHint;
$this->type = $type;
$this->hint = $hint;
}
}
51 changes: 0 additions & 51 deletions src/Twig/Components/Challenge/Instruction/HintPayload.php

This file was deleted.

92 changes: 64 additions & 28 deletions src/Twig/Components/Challenge/Instruction/Modal.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,37 @@

use App\Entity\HintOpenEvent;
use App\Entity\Question;
use App\Entity\SolutionEventStatus;
use App\Entity\User;
use App\Repository\SolutionEventRepository;
use App\Service\DbRunnerComparer;
use App\Service\DbRunnerService;
use App\Service\PointCalculationService;
use App\Service\PromptService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Translation\TranslatableMessage;
use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveAction;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
use Symfony\UX\LiveComponent\ComponentToolsTrait;
use Symfony\UX\LiveComponent\DefaultActionTrait;

use function Symfony\Component\Translation\t;

#[AsLiveComponent]
final class Modal
{
use ComponentToolsTrait;
use DefaultActionTrait;

public function __construct(
private readonly TranslatorInterface $translator,
) {
}

#[LiveProp]
public User $currentUser;

Expand All @@ -50,7 +59,6 @@ public function instruct(
DbRunnerService $dbRunnerService,
PromptService $promptService,
TranslatorInterface $translator,
SerializerInterface $serializer,
EntityManagerInterface $entityManager,
ParameterBagInterface $parameterBag,
): void {
Expand All @@ -63,49 +71,77 @@ public function instruct(

$query = $solutionEventRepository->getLatestQuery($this->question, $this->currentUser);
if (null === $query) {
$this->flushHint('informative', t('instruction.hint.not_submitted'));

return;
}
if (SolutionEventStatus::Passed === $query->getStatus()) {
$this->flushHint('informative', t('instruction.hint.solved'));

$schema = $this->question->getSchema()->getSchema();
$answer = $this->question->getAnswer();
return;
}

$hintOpenEvent = (new HintOpenEvent())
->setOpener($this->currentUser)
->setQuestion($this->question)
->setQuery($query->getQuery());
$schema = $query->getQuestion()->getSchema();

// run answer. if it failed, we should consider it an error
try {
$answerResult = $dbRunnerService->runQuery($schema, $answer);
$answer = $query->getQuestion()->getAnswer();
$answerResult = $dbRunnerService->runQuery($schema->getSchema(), $answer);
} catch (\Throwable $e) {
$this->emit('app:challenge-hint', [
'hint' => $serializer->serialize(HintPayload::fromError($e->getMessage()), 'json'),
]);
$this->flushHint('informative', t('instruction.hint.error', [
'%error%' => $e->getMessage(),
]));

return;
}

$hintOpenEvent = (new HintOpenEvent())
->setOpener($this->currentUser)
->setQuestion($this->question)
->setQuery($query->getQuery());

try {
// run query to get the error message (or compare the result)
$result = $dbRunnerService->runQuery($schema, $query->getQuery());
} catch (\Throwable $e) {
$hint = $promptService->hint($query->getQuery(), $e->getMessage(), $answer);
}
try {
$userResult = $dbRunnerService->runQuery($schema->getSchema(), $query->getQuery());
} catch (\Throwable $e) {
$hint = $promptService->hint($query->getQuery(), $e->getMessage(), $answer);
$hintOpenEvent->setResponse($hint);

if (isset($result) && $result !== $answerResult) {
$hint = $promptService->hint($query->getQuery(), 'Different output', $answer);
}
$this->flushHint('hint', $hint);

return;
}

$compareResult = DbRunnerComparer::compare($answerResult, $userResult);
if ($compareResult->correct()) {
$this->flushHint('informative', t('instruction.hint.solved'));

return;
}

if (!isset($hint)) {
$hint = $translator->trans('instruction.hint.no_hint');
$compareReason = $compareResult->reason()->trans($translator, 'en_US');
$hint = $promptService->hint($query->getQuery(), "Different result: {$compareReason}", $answer);
$hintOpenEvent->setResponse($hint);

$this->flushHint('hint', $hint);
} finally {
$entityManager->persist($hintOpenEvent);
$entityManager->flush();
}
}

/**
* Flush the hint to the client.
*
* @param string $type the type of the hint (informative or hint)
* @param string|TranslatableMessage $hint the hint to flush
*/
private function flushHint(string $type, string|TranslatableMessage $hint): void
{
$this->emit('app:challenge-hint', [
'hint' => $serializer->serialize(HintPayload::fromHint($hint), 'json'),
'type' => $type,
'hint' => $hint instanceof TranslatableMessage
? $hint->trans($this->translator)
: $hint,
]);

$hintOpenEvent = $hintOpenEvent->setResponse($hint);
$entityManager->persist($hintOpenEvent);
$entityManager->flush();
}
}
8 changes: 8 additions & 0 deletions src/Twig/Components/Challenge/Tabs/DiffPresenter.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveArg;
use Symfony\UX\LiveComponent\Attribute\LiveListener;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
use Symfony\UX\LiveComponent\DefaultActionTrait;
use Symfony\UX\TwigComponent\Attribute\PostMount;
Expand Down Expand Up @@ -116,4 +118,10 @@ public function getDiff(): ?string

return $result;
}

#[LiveListener('app:challenge-executor:query-created')]
public function onQueryUpdated(#[LiveArg] string $query): void
{
$this->query = $query;
}
}
3 changes: 0 additions & 3 deletions src/Twig/Components/Challenge/Ui.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,4 @@ final class Ui

#[LiveProp]
public Question $question;

#[LiveProp]
public int $limit;
}
2 changes: 1 addition & 1 deletion templates/challenge/index.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
{% block title %}練習題目 – {{ question.title }}{% endblock %}

{% block app %}
<twig:Challenge:Ui :user="user" :question="question" :limit="limit" />
<twig:Challenge:Ui :user="user" :question="question" />
{% endblock %}
8 changes: 7 additions & 1 deletion templates/components/Challenge/ColumnsOfAnswer.html.twig
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
<p {{ attributes }}>
<strong>輸出格式:</strong>欄位順序分別為<code>{{
<strong>輸出格式:</strong>欄位順序分別為 <code>{{
this.columnsOfAnswer|joinToQuoted('、')
}}</code>。
</p>

{% macro placeholder(props) %}
<p>
<strong>輸出格式:</strong>欄位順序分別為
</p>
{% endmacro %}
25 changes: 15 additions & 10 deletions templates/components/Challenge/Instruction/Content.html.twig
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
<div{{ attributes }}>
{% if hint and hint.error %}
<div class="alert alert-danger" role="alert">
<h4 class="alert-heading">無法取得提示</h4>
<p>{{ hint.error }}</p>
</div>
{% elseif hint and hint.hint %}
<div class="alert alert-info" role="alert">
<h4 class="alert-heading">提示</h4>
<p>{{ hint.hint|markdown_to_html }}</p>
</div>
{% if hint %}
{% if type == 'informative' %}
<div class="alert alert-warning" role="alert">
{{ hint }}
</div>
{% elseif type == 'hint' %}
<div class="alert alert-info" role="alert">
<h4 class="alert-heading">提示</h4>
<p>{{ hint|markdown_to_html }}</p>
</div>
{% else %}
<div class="alert alert-danger" role="alert">
<p>發現一個未知類型的提示({{ type }}:{{ hint }})</p>
</div>
{% endif %}
{% endif %}
</div>
2 changes: 1 addition & 1 deletion templates/components/Challenge/Tabs.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
{% elseif currentTab == 'events' %}
<twig:Challenge:Tabs:Events :question="question" :user="user" />
{% elseif currentTab == 'diff' %}
<twig:Challenge:Tabs:DiffPresenter :question="question" :user="user" />
<twig:Challenge:Tabs:DiffPresenter :question="question" :user="user" loading="defer" />
{% else %}
<twig:Challenge:Tabs:UserQueryResult :question="question" :user="user" loading="defer" />
{% endif %}
Expand Down
Loading