diff --git a/assets/styles/app.scss b/assets/styles/app.scss
index 2d9b40a..dc51f0b 100644
--- a/assets/styles/app.scss
+++ b/assets/styles/app.scss
@@ -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;
+ }
+ }
+}
diff --git a/src/Controller/ChallengeController.php b/src/Controller/ChallengeController.php
index f61a514..6e3c470 100644
--- a/src/Controller/ChallengeController.php
+++ b/src/Controller/ChallengeController.php
@@ -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;
@@ -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(),
]);
}
diff --git a/src/Twig/Components/Challenge/Header.php b/src/Twig/Components/Challenge/Header.php
index 18750b4..acd71c7 100644
--- a/src/Twig/Components/Challenge/Header.php
+++ b/src/Twig/Components/Challenge/Header.php
@@ -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,
@@ -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.
+ }
}
diff --git a/src/Twig/Components/Challenge/Instruction/Content.php b/src/Twig/Components/Challenge/Instruction/Content.php
index c1c0ede..9d7212c 100644
--- a/src/Twig/Components/Challenge/Instruction/Content.php
+++ b/src/Twig/Components/Challenge/Instruction/Content.php
@@ -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;
@@ -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;
}
}
diff --git a/src/Twig/Components/Challenge/Instruction/HintPayload.php b/src/Twig/Components/Challenge/Instruction/HintPayload.php
deleted file mode 100644
index a107121..0000000
--- a/src/Twig/Components/Challenge/Instruction/HintPayload.php
+++ /dev/null
@@ -1,51 +0,0 @@
-hint;
- }
-
- public function getError(): ?string
- {
- return $this->error;
- }
-
- public function setHint(?string $hint): self
- {
- $this->hint = $hint;
-
- return $this;
- }
-
- public function setError(?string $error): self
- {
- $this->error = $error;
-
- return $this;
- }
-
- public static function fromHint(string $hint): self
- {
- $payload = new self();
- $payload->setHint($hint);
-
- return $payload;
- }
-
- public static function fromError(string $error): self
- {
- $payload = new self();
- $payload->setError($error);
-
- return $payload;
- }
-}
diff --git a/src/Twig/Components/Challenge/Instruction/Modal.php b/src/Twig/Components/Challenge/Instruction/Modal.php
index ba32bab..b749303 100644
--- a/src/Twig/Components/Challenge/Instruction/Modal.php
+++ b/src/Twig/Components/Challenge/Instruction/Modal.php
@@ -6,15 +6,17 @@
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;
@@ -22,12 +24,19 @@
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;
@@ -50,7 +59,6 @@ public function instruct(
DbRunnerService $dbRunnerService,
PromptService $promptService,
TranslatorInterface $translator,
- SerializerInterface $serializer,
EntityManagerInterface $entityManager,
ParameterBagInterface $parameterBag,
): void {
@@ -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();
}
}
diff --git a/src/Twig/Components/Challenge/Tabs/DiffPresenter.php b/src/Twig/Components/Challenge/Tabs/DiffPresenter.php
index 808375d..5d59d1d 100644
--- a/src/Twig/Components/Challenge/Tabs/DiffPresenter.php
+++ b/src/Twig/Components/Challenge/Tabs/DiffPresenter.php
@@ -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;
@@ -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;
+ }
}
diff --git a/src/Twig/Components/Challenge/Ui.php b/src/Twig/Components/Challenge/Ui.php
index 1a5f908..7ddaffe 100644
--- a/src/Twig/Components/Challenge/Ui.php
+++ b/src/Twig/Components/Challenge/Ui.php
@@ -20,7 +20,4 @@ final class Ui
#[LiveProp]
public Question $question;
-
- #[LiveProp]
- public int $limit;
}
diff --git a/templates/challenge/index.html.twig b/templates/challenge/index.html.twig
index ef6a02c..79ae043 100644
--- a/templates/challenge/index.html.twig
+++ b/templates/challenge/index.html.twig
@@ -4,5 +4,5 @@
{% block title %}練習題目 – {{ question.title }}{% endblock %}
{% block app %}
-
- 輸出格式:欄位順序分別為:{{
+ 輸出格式:欄位順序分別為
{{
this.columnsOfAnswer|joinToQuoted('、')
}}
。
+ 輸出格式:欄位順序分別為 +
+{% endmacro %} diff --git a/templates/components/Challenge/Instruction/Content.html.twig b/templates/components/Challenge/Instruction/Content.html.twig index f48b3cc..f28c875 100644 --- a/templates/components/Challenge/Instruction/Content.html.twig +++ b/templates/components/Challenge/Instruction/Content.html.twig @@ -1,13 +1,18 @@{{ hint.error }}
-{{ hint.hint|markdown_to_html }}
-{{ hint|markdown_to_html }}
+發現一個未知類型的提示({{ type }}:{{ hint }})
+