From b0af254f829c1a9455c5d7b8d2bfe3d4f6ee06cc Mon Sep 17 00:00:00 2001 From: Yi-Jyun Pan Date: Fri, 15 Nov 2024 07:52:34 +0800 Subject: [PATCH 1/6] fix(challenge): DiffPresenter should be deferred It takes some time to generate the diff. --- templates/components/Challenge/Tabs.html.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/components/Challenge/Tabs.html.twig b/templates/components/Challenge/Tabs.html.twig index b069252..5b4ce8f 100644 --- a/templates/components/Challenge/Tabs.html.twig +++ b/templates/components/Challenge/Tabs.html.twig @@ -18,7 +18,7 @@ {% elseif currentTab == 'events' %} {% elseif currentTab == 'diff' %} - + {% else %} {% endif %} From 221cf0fd8fd35ecb8ca7ede2b6161380dc110ca7 Mon Sep 17 00:00:00 2001 From: Yi-Jyun Pan Date: Fri, 15 Nov 2024 07:53:03 +0800 Subject: [PATCH 2/6] fix(challenge): Update diff and header when a new query is submitted --- src/Twig/Components/Challenge/Header.php | 19 ++++++++++++++++--- .../Challenge/Tabs/DiffPresenter.php | 8 ++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) 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/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; + } } From 322a8a131a1738f4916c2be90a85728de53747d3 Mon Sep 17 00:00:00 2001 From: Yi-Jyun Pan Date: Fri, 15 Nov 2024 07:56:37 +0800 Subject: [PATCH 3/6] fix(challenge): Remove unused "limit" --- src/Controller/ChallengeController.php | 3 --- src/Twig/Components/Challenge/Ui.php | 3 --- templates/challenge/index.html.twig | 2 +- templates/components/Challenge/Ui.html.twig | 2 +- 4 files changed, 2 insertions(+), 8 deletions(-) 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/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 %} - + {% endblock %} diff --git a/templates/components/Challenge/Ui.html.twig b/templates/components/Challenge/Ui.html.twig index d3cb57a..39ade25 100644 --- a/templates/components/Challenge/Ui.html.twig +++ b/templates/components/Challenge/Ui.html.twig @@ -8,7 +8,7 @@ {% endif %}
- +
From 32cc177d9bed191dbf690e724332555a358cc328 Mon Sep 17 00:00:00 2001 From: Yi-Jyun Pan Date: Fri, 15 Nov 2024 10:22:19 +0800 Subject: [PATCH 4/6] refactor(challenge/instruction): Improve hint prompts and code --- .../Challenge/Instruction/Content.php | 12 ++- .../Challenge/Instruction/HintPayload.php | 51 ---------- .../Challenge/Instruction/Modal.php | 92 +++++++++++++------ .../Challenge/Instruction/Content.html.twig | 25 +++-- translations/messages.en_US.yaml | 8 ++ translations/messages.zh_TW.yaml | 4 +- 6 files changed, 97 insertions(+), 95 deletions(-) delete mode 100644 src/Twig/Components/Challenge/Instruction/HintPayload.php create mode 100644 translations/messages.en_US.yaml 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/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 @@ - {% if hint and hint.error %} - - {% elseif hint and hint.hint %} - + {% if hint %} + {% if type == 'informative' %} + + {% elseif type == 'hint' %} + + {% else %} + + {% endif %} {% endif %}
diff --git a/translations/messages.en_US.yaml b/translations/messages.en_US.yaml new file mode 100644 index 0000000..465b4da --- /dev/null +++ b/translations/messages.en_US.yaml @@ -0,0 +1,8 @@ +challenge: + compare-result: + same: The answers are completely identical. + empty-answer: The correct answer has no fields, which usually indicates that the query statement written by the questioner is incorrect. Please report this to us. + empty-result: Your answer has no fields, which usually indicates that the query statement is incorrect. + column-different: The column names differ; please compare and modify according to the correct answer. + row-different: The %row% row you answered is different from the correct answer. + row-unmatched: The number of returned rows does not match the correct answer (the correct answer has %expected% rows, while you answered %actual% rows). diff --git a/translations/messages.zh_TW.yaml b/translations/messages.zh_TW.yaml index b22bc18..19ca4cc 100644 --- a/translations/messages.zh_TW.yaml +++ b/translations/messages.zh_TW.yaml @@ -98,7 +98,9 @@ charts: instruction: hint: - no_hint: 沒有提示。 + not_submitted: 提交答案之後才能請 GPT 提示。 + solved: 正確答案不需要提示。 + error: 無法取得正確答案:%error% feedback: type: From cd6b1a36c8e85c75d70f93842afba9529829f1b5 Mon Sep 17 00:00:00 2001 From: Yi-Jyun Pan Date: Fri, 15 Nov 2024 10:36:38 +0800 Subject: [PATCH 5/6] refactor(challenge): Add loading to deferred result --- templates/components/Challenge/ColumnsOfAnswer.html.twig | 8 +++++++- .../components/Challenge/Tabs/DiffPresenter.html.twig | 7 +++++++ .../components/Challenge/Tabs/UserQueryResult.html.twig | 9 ++++++++- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/templates/components/Challenge/ColumnsOfAnswer.html.twig b/templates/components/Challenge/ColumnsOfAnswer.html.twig index 9e2122f..403a133 100644 --- a/templates/components/Challenge/ColumnsOfAnswer.html.twig +++ b/templates/components/Challenge/ColumnsOfAnswer.html.twig @@ -1,5 +1,11 @@

- 輸出格式:欄位順序分別為:{{ + 輸出格式:欄位順序分別為 {{ this.columnsOfAnswer|joinToQuoted('、') }}

+ +{% macro placeholder(props) %} +

+ 輸出格式:欄位順序分別為 +

+{% endmacro %} diff --git a/templates/components/Challenge/Tabs/DiffPresenter.html.twig b/templates/components/Challenge/Tabs/DiffPresenter.html.twig index effd043..649f2c4 100644 --- a/templates/components/Challenge/Tabs/DiffPresenter.html.twig +++ b/templates/components/Challenge/Tabs/DiffPresenter.html.twig @@ -171,3 +171,10 @@ {% endif %} + +{% macro placeholder(props) %} +
+ +
正在進行比較⋯⋯
+
+{% endmacro %} diff --git a/templates/components/Challenge/Tabs/UserQueryResult.html.twig b/templates/components/Challenge/Tabs/UserQueryResult.html.twig index 56bf83a..30ed4b1 100644 --- a/templates/components/Challenge/Tabs/UserQueryResult.html.twig +++ b/templates/components/Challenge/Tabs/UserQueryResult.html.twig @@ -16,6 +16,13 @@ {% endif %} {% if result is not null and result.result %} - + {% endif %} + +{% macro placeholder(props) %} +
+ +
正在取回答案⋯⋯
+
+{% endmacro %} From 70f8e2fbe2674352526f55fa8a79cc3cf6d286a0 Mon Sep 17 00:00:00 2001 From: Yi-Jyun Pan Date: Fri, 15 Nov 2024 10:59:07 +0800 Subject: [PATCH 6/6] refactor(challenge): Improve UX of placeholder --- assets/styles/app.scss | 18 ++++++++++++++++++ .../Challenge/Tabs/DiffPresenter.html.twig | 2 +- .../Challenge/Tabs/UserQueryResult.html.twig | 2 +- 3 files changed, 20 insertions(+), 2 deletions(-) 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/templates/components/Challenge/Tabs/DiffPresenter.html.twig b/templates/components/Challenge/Tabs/DiffPresenter.html.twig index 649f2c4..17f446a 100644 --- a/templates/components/Challenge/Tabs/DiffPresenter.html.twig +++ b/templates/components/Challenge/Tabs/DiffPresenter.html.twig @@ -173,7 +173,7 @@ {% macro placeholder(props) %} -
+
正在進行比較⋯⋯
diff --git a/templates/components/Challenge/Tabs/UserQueryResult.html.twig b/templates/components/Challenge/Tabs/UserQueryResult.html.twig index 30ed4b1..676466a 100644 --- a/templates/components/Challenge/Tabs/UserQueryResult.html.twig +++ b/templates/components/Challenge/Tabs/UserQueryResult.html.twig @@ -21,7 +21,7 @@
{% macro placeholder(props) %} -
+
正在取回答案⋯⋯