@tinyweb_dev/oe-exam-sdk 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +56 -11
  2. package/dist/api/result-review.d.ts +2 -1
  3. package/dist/api/result-review.js +1 -1
  4. package/dist/api/result-review.types.d.ts +69 -3
  5. package/dist/api/result-review.types.js +1 -1
  6. package/dist/components/exams/take/ExamTakingPageContainer.js +1 -1
  7. package/dist/components/exams/take/components/speaking/renderers/SpeakingDescribeImageRenderer.js +1 -1
  8. package/dist/components/exams/take/components/speaking/renderers/SpeakingReadDisplayedContentRenderer.d.ts +0 -17
  9. package/dist/components/exams/take/components/speaking/renderers/SpeakingReadDisplayedContentRenderer.js +1 -1
  10. package/dist/components/exams/take/hooks/useSpeakingRecorder.js +1 -1
  11. package/dist/components/results/ResultReviewPageContainer.d.ts +15 -3
  12. package/dist/components/results/ResultReviewPageContainer.js +1 -1
  13. package/dist/components/results/ReviewQuestionRenderer.d.ts +11 -1
  14. package/dist/components/results/ReviewQuestionRenderer.js +1 -1
  15. package/dist/components/results/hooks/useResultReviewPolling.d.ts +12 -2
  16. package/dist/components/results/hooks/useResultReviewPolling.js +1 -1
  17. package/dist/components/results/index.d.ts +34 -2
  18. package/dist/components/results/index.js +1 -1
  19. package/dist/components/results/renderers/QuestionExplanations.d.ts +22 -0
  20. package/dist/components/results/renderers/QuestionExplanations.js +1 -0
  21. package/dist/components/results/renderers/ReviewChooseAdjectiveRenderer.d.ts +13 -0
  22. package/dist/components/results/renderers/ReviewChooseAdjectiveRenderer.js +1 -0
  23. package/dist/components/results/renderers/ReviewChooseBestAnswerRenderer.d.ts +22 -0
  24. package/dist/components/results/renderers/ReviewChooseBestAnswerRenderer.js +1 -0
  25. package/dist/components/results/renderers/ReviewColoringRenderer.d.ts +16 -0
  26. package/dist/components/results/renderers/ReviewColoringRenderer.js +1 -0
  27. package/dist/components/results/renderers/ReviewListenAndWriteRenderer.d.ts +18 -0
  28. package/dist/components/results/renderers/ReviewListenAndWriteRenderer.js +1 -0
  29. package/dist/components/results/renderers/ReviewListenChooseAnswerGroupRenderer.d.ts +25 -0
  30. package/dist/components/results/renderers/ReviewListenChooseAnswerGroupRenderer.js +1 -0
  31. package/dist/components/results/renderers/ReviewListenChooseObjectsRenderer.d.ts +25 -0
  32. package/dist/components/results/renderers/ReviewListenChooseObjectsRenderer.js +1 -0
  33. package/dist/components/results/renderers/ReviewListenDragObjectsRenderer.d.ts +25 -0
  34. package/dist/components/results/renderers/ReviewListenDragObjectsRenderer.js +1 -0
  35. package/dist/components/results/renderers/ReviewListeningHeader.d.ts +44 -0
  36. package/dist/components/results/renderers/ReviewListeningHeader.js +1 -0
  37. package/dist/components/results/renderers/ReviewSpeakingAIPanel.d.ts +17 -0
  38. package/dist/components/results/renderers/ReviewSpeakingAIPanel.js +1 -0
  39. package/dist/components/results/renderers/ReviewSpeakingMedia.d.ts +39 -0
  40. package/dist/components/results/renderers/ReviewSpeakingMedia.js +1 -0
  41. package/dist/components/results/renderers/ReviewSpeakingQuestionItem.d.ts +5 -0
  42. package/dist/components/results/renderers/ReviewSpeakingQuestionItem.js +1 -0
  43. package/dist/components/results/renderers/ReviewSpeakingRenderer.d.ts +3 -0
  44. package/dist/components/results/renderers/ReviewSpeakingRenderer.js +1 -0
  45. package/dist/components/results/renderers/ReviewSpeakingRenderer.types.d.ts +80 -0
  46. package/dist/components/results/renderers/ReviewSpeakingRenderer.types.js +1 -0
  47. package/dist/components/results/renderers/review-question-body-dedicated.d.ts +15 -0
  48. package/dist/components/results/renderers/review-question-body-dedicated.js +1 -0
  49. package/dist/components/results/renderers/review-question-body-movers.d.ts +17 -0
  50. package/dist/components/results/renderers/review-question-body-movers.js +1 -0
  51. package/dist/components/results/renderers/review-speaking-map.d.ts +7 -0
  52. package/dist/components/results/renderers/review-speaking-map.js +1 -0
  53. package/dist/components/results/report/BadgeTitle.d.ts +5 -0
  54. package/dist/components/results/report/BadgeTitle.js +1 -0
  55. package/dist/components/results/report/ExamSkillRow.d.ts +14 -0
  56. package/dist/components/results/report/ExamSkillRow.js +1 -0
  57. package/dist/components/results/report/OceanEduExamReport.d.ts +31 -0
  58. package/dist/components/results/report/OceanEduExamReport.js +1 -0
  59. package/dist/components/results/report/OceanEduSpeakingReport.d.ts +26 -0
  60. package/dist/components/results/report/OceanEduSpeakingReport.js +1 -0
  61. package/dist/components/results/report/OverallAssessmentSection.d.ts +24 -0
  62. package/dist/components/results/report/OverallAssessmentSection.js +1 -0
  63. package/dist/components/results/report/ResultErrorState.d.ts +14 -0
  64. package/dist/components/results/report/ResultErrorState.js +1 -0
  65. package/dist/components/results/report/ResultLoadingState.d.ts +11 -0
  66. package/dist/components/results/report/ResultLoadingState.js +1 -0
  67. package/dist/components/results/report/ResultNotPublicState.d.ts +16 -0
  68. package/dist/components/results/report/ResultNotPublicState.js +1 -0
  69. package/dist/components/results/report/SpeakingReportParts.d.ts +4 -0
  70. package/dist/components/results/report/SpeakingReportParts.js +1 -0
  71. package/dist/components/results/report/SpeakingReportShared.d.ts +36 -0
  72. package/dist/components/results/report/SpeakingReportShared.js +1 -0
  73. package/dist/components/results/report/SpeakingRubricSection.d.ts +21 -0
  74. package/dist/components/results/report/SpeakingRubricSection.js +1 -0
  75. package/dist/components/results/report/assets/dofu2.png +0 -0
  76. package/dist/components/results/report/assets/dofu4.png +0 -0
  77. package/dist/components/results/report/assets/logo-ocean.png +0 -0
  78. package/dist/components/results/report/assets/penguin-main.png +0 -0
  79. package/dist/components/results/report/assets/penguin-small.png +0 -0
  80. package/dist/components/results/report/speaking-report.utils.d.ts +37 -0
  81. package/dist/components/results/report/speaking-report.utils.js +1 -0
  82. package/dist/components/results/review-data.d.ts +1 -0
  83. package/dist/components/results/review-data.js +1 -1
  84. package/dist/components/results/review-types.d.ts +14 -0
  85. package/dist/components/results/speaking/SpeakingResultAudioPlayer.d.ts +17 -0
  86. package/dist/components/results/speaking/SpeakingResultAudioPlayer.js +1 -0
  87. package/dist/components/results/speaking/SpeakingReviewResult.d.ts +30 -0
  88. package/dist/components/results/speaking/SpeakingReviewResult.js +1 -0
  89. package/dist/components/results/utils/review-score.utils.d.ts +21 -0
  90. package/dist/components/results/utils/review-score.utils.js +1 -0
  91. package/dist/components/results/utils/speaking-summary.d.ts +41 -0
  92. package/dist/components/results/utils/speaking-summary.js +1 -0
  93. package/dist/components/results/utils/transform-speaking-review.d.ts +62 -0
  94. package/dist/components/results/utils/transform-speaking-review.js +1 -0
  95. package/dist/shared/lib/hooks/usePresignedFileUrl.d.ts +9 -1
  96. package/dist/shared/lib/hooks/usePresignedFileUrl.js +1 -1
  97. package/package.json +4 -2
package/README.md CHANGED
@@ -175,21 +175,66 @@ Renderer câu hỏi trong màn làm bài hiện là `GenericQuestionRenderer`.
175
175
 
176
176
  ## Màn xem kết quả
177
177
 
178
+ `ResultReviewPageContainer` sở hữu toàn bộ màn review và tự chọn giao diện theo template:
179
+
180
+ - `SPEAKING`: dùng report nói chuyên biệt, chi tiết từng phần/câu, audio, AI assessment và speaking rubric.
181
+ - Template còn lại: dùng `OceanEduExamReport`, phân tích tổng quan, rubric (nếu có) và `ReviewQuestionRenderer` đầy đủ cho câu hỏi dedicated lẫn Movers fallback.
182
+ - Template `PLACEMENT_TEST_KINDY_VER3` và `PLACEMENT_TEST_PRESCHOOL` dùng điểm rubric theo Part và ẩn chấm điểm từng câu.
183
+
178
184
  ```tsx
179
- <ResultReviewPageContainer
180
- attemptId={attemptId}
181
- token={token}
182
- api={createResultReviewApi({ baseUrl })}
183
- isPrintMode={isPrintMode}
184
- topActionsSlot={null}
185
- beforeReportSlot={null}
186
- afterReportSlot={null}
187
- />
185
+ 'use client';
186
+
187
+ import {
188
+ ResultReviewPageContainer,
189
+ createResultReviewApi,
190
+ } from '@tinyweb_dev/oe-exam-sdk';
191
+
192
+ export function ResultReviewPage({ attemptId, token, isPrintMode, router }) {
193
+ const reviewUrl = typeof window !== 'undefined' ? window.location.href : undefined;
194
+
195
+ return (
196
+ <ResultReviewPageContainer
197
+ attemptId={attemptId}
198
+ token={token}
199
+ api={createResultReviewApi({ baseUrl })}
200
+ isPrintMode={isPrintMode}
201
+ canExportPdf
202
+ reviewUrl={reviewUrl}
203
+ assetBaseUrl="/speaking/ocean-edu"
204
+ loginHref={`/auth/login?redirect=${encodeURIComponent(reviewUrl ?? '')}`}
205
+ onBack={() => router.back()}
206
+ onNavigate={(href) => router.push(href)}
207
+ topActionsSlot={<SummaryVideoAction attemptId={attemptId} />}
208
+ beforeReportSlot={<GiftClaimDialog attemptId={attemptId} />}
209
+ afterReportSlot={<ResultExtraContent attemptId={attemptId} />}
210
+ />
211
+ );
212
+ }
188
213
  ```
189
214
 
190
- Container tự poll lại kết quả khi còn câu đang chấm hoặc LLM assessment chưa sẵn sàng, dừng poll ở `isPrintMode` phát tín hiệu `body[data-pdf-ready="true"]` cho headless PDF renderer. Lỗi 403 "kết quả chưa công khai" được nhận diện qua helper `isResultNotPublic` và hiển thị màn thông báo riêng thay vì toast lỗi.
215
+ ### Propshành vi
216
+
217
+ | Prop | Hành vi |
218
+ |---|---|
219
+ | `attemptId`, `token`, `api` | Tải detail/attempt; `token` hỗ trợ link kết quả công khai. |
220
+ | `isPrintMode` | Dừng polling và phát `body[data-pdf-ready="true"]` sau khi load để headless PDF renderer tiếp tục. |
221
+ | `canExportPdf` | Hiện nút xuất PDF khi adapter có `exportPdf`; SDK tải blob bằng tên file từ API. |
222
+ | `reviewUrl` | URL canonical đưa vào QR; SDK bổ sung verification token từ prop hoặc attempt. Mặc định dùng URL hiện tại trên browser. |
223
+ | `assetBaseUrl` | Prefix tùy chọn cho `logo-ocean.png`, `penguin-main.png`, `penguin-small.png`, `dofu2.png`; bỏ prop để dùng asset bundle trong package. |
224
+ | `loginHref` | Đích đăng nhập ở trạng thái 403/private; mặc định là `/auth/login?redirect=<current-url>`. |
225
+ | `onBack`, `onNavigate` | Tích hợp router của host. Nếu có contest, back qua `onNavigate` về rooms của contest; nếu không có callback thì dùng browser history. |
226
+
227
+ Container poll mỗi 5 giây khi còn grading status đang xử lý hoặc LLM assessment chưa sẵn sàng. Các trạng thái loading, lỗi chung và 403 chưa công khai có UI riêng. `showDetailedResults=false` chỉ ẩn phần câu hỏi chi tiết, không ẩn report tổng quan.
228
+
229
+ ### Slots và phạm vi host
230
+
231
+ - `topActionsSlot`: action bổ sung cạnh nút xuất PDF, ví dụ summary video.
232
+ - `beforeReportSlot`: nội dung trước report, ví dụ gift claim hoặc thông báo nâng cấp.
233
+ - `afterReportSlot`: nội dung sau report.
234
+
235
+ Gift claim, summary video/remotion, account upgrade, auth store và router không được nhúng trong SDK. Host truyền các phần này qua slot/callback; không cần nối lại page của `oe-exam-fe` để dùng container.
191
236
 
192
- Các phần phụ thuộc app chính như gift dialog, upgrade modal, summary video/remotion nên truyền qua slot từ host app.
237
+ Các report shell, renderer, transform helper, polling/status helper result-review types cũng được export từ `@tinyweb_dev/oe-exam-sdk/components/results` khi host cần tái sử dụng riêng lẻ.
193
238
 
194
239
  ## MCP server
195
240
 
@@ -4,4 +4,5 @@ export declare function createMockResultReviewApi(seed: {
4
4
  detail: ResultReviewDetail;
5
5
  attempt?: ResultReviewAttempt;
6
6
  }): ResultReviewSdkApi;
7
- export type { ResultReviewAnswer, ResultReviewAttempt, ResultReviewDetail, ResultReviewFetchApiOptions, ResultReviewSdkApi } from './result-review.types';
7
+ export { ResultTemplateType, ReviewGradingStatus } from './result-review.types';
8
+ export type { ResultReviewAnswer, ResultReviewAttempt, ResultReviewDetail, ResultReviewFetchApiOptions, ResultReviewSdkApi, ResultReviewTemplatePart, ReviewAnswerValue, SpeakingRubric, SpeakingRubricCriterion, SpeakingRubricQuestion, SpeakingRubricSection, } from './result-review.types';
@@ -1 +1 @@
1
- import{API_BASE_URL as e,API_ENDPOINTS as t}from"../shared/constants/ApiConstant";export function createResultReviewApi(i={}){const r=function({baseUrl:t=e??"",fetcher:n=fetch,headers:a}){return async function(e,i){const r=await n(`${t}${e}`,{credentials:"include",...i,headers:{"Content-Type":"application/json",...a,...i?.headers}});if(!r.ok)throw new Error(await o(r));return r.json()}}(i);return{getDetailedResult:(e,a)=>r(n(t.STUDENT.RESULT_DETAIL(e),a)),getAttempt:(e,a)=>r(n(t.STUDENT.ATTEMPT_DETAIL(e),a)),exportPdf:async(n,r)=>{const s=await(i.fetcher??fetch)(`${i.baseUrl??e??""}${t.STUDENT.EXPORT_NATIVE_PDF}`,{method:"POST",credentials:"include",headers:{"Content-Type":"application/json",...i.headers},body:JSON.stringify({attemptId:n,examType:r})});if(!s.ok)throw new Error(await o(s));return{blob:await s.blob(),filename:a(s.headers,n)}}}}export function createMockResultReviewApi(e){return{getDetailedResult:async()=>e.detail,getAttempt:async()=>e.attempt??{id:e.detail.id},exportPdf:async()=>({blob:new Blob(["mock pdf"],{type:"application/pdf"}),filename:`result-${e.detail.id}.pdf`})}}function n(e,t){if(!t)return e;const n=e.includes("?")?"&":"?";return`${e}${n}token=${encodeURIComponent(t)}`}function a(e,t){const n=e.get("content-disposition")??"";return n.match(/filename="?([^\"]+)"?/)?.[1]??`result-${t.slice(0,8)}.pdf`}async function o(e){const t=await e.json().catch(()=>{});return"object"==typeof t&&null!==t&&"message"in t&&"string"==typeof t.message?t.message:e.statusText}
1
+ import{API_BASE_URL as e,API_ENDPOINTS as t}from"../shared/constants/ApiConstant";export function createResultReviewApi(r={}){const i=function({baseUrl:t=e??"",fetcher:n=fetch,headers:a}){return async function(e,r){const i=await n(`${t}${e}`,{credentials:"include",...r,headers:{"Content-Type":"application/json",...a,...r?.headers}});if(!i.ok)throw new Error(await o(i));return i.json()}}(r);return{getDetailedResult:(e,a)=>i(n(t.STUDENT.RESULT_DETAIL(e),a)),getAttempt:(e,a)=>i(n(t.STUDENT.ATTEMPT_DETAIL(e),a)),exportPdf:async(n,i)=>{const s=await(r.fetcher??fetch)(`${r.baseUrl??e??""}${t.STUDENT.EXPORT_NATIVE_PDF}`,{method:"POST",credentials:"include",headers:{"Content-Type":"application/json",...r.headers},body:JSON.stringify({attemptId:n,examType:i})});if(!s.ok)throw new Error(await o(s));return{blob:await s.blob(),filename:a(s.headers,n)}}}}export function createMockResultReviewApi(e){return{getDetailedResult:async()=>e.detail,getAttempt:async()=>e.attempt??{id:e.detail.id},exportPdf:async()=>({blob:new Blob(["mock pdf"],{type:"application/pdf"}),filename:`result-${e.detail.id}.pdf`})}}function n(e,t){if(!t)return e;const n=e.includes("?")?"&":"?";return`${e}${n}token=${encodeURIComponent(t)}`}function a(e,t){const n=e.get("content-disposition")??"";return n.match(/filename="?([^"]+)"?/)?.[1]??`result-${t.slice(0,8)}.pdf`}async function o(e){const t=await e.json().catch(()=>{});return"object"==typeof t&&null!==t&&"message"in t&&"string"==typeof t.message?t.message:e.statusText}export{ResultTemplateType,ReviewGradingStatus}from"./result-review.types";
@@ -1,4 +1,63 @@
1
1
  export type ReviewAnswerValue = string | number | boolean | string[] | Record<string, unknown> | null;
2
+ export declare enum ResultTemplateType {
3
+ SPEAKING = "SPEAKING"
4
+ }
5
+ export declare enum ReviewGradingStatus {
6
+ PENDING = "PENDING",
7
+ QUEUED = "QUEUED",
8
+ PROCESSING = "PROCESSING",
9
+ AUTO_GRADED = "AUTO_GRADED",
10
+ GRADED = "GRADED",
11
+ COMPLETED = "COMPLETED",
12
+ FAILED = "FAILED",
13
+ MANUAL_REVIEW = "MANUAL_REVIEW",
14
+ NOT_ANSWERED = "NOT_ANSWERED"
15
+ }
16
+ export interface SpeakingRubricCriterion {
17
+ code: string;
18
+ score: number;
19
+ weightedScore?: number;
20
+ maxScore?: number;
21
+ evidence: string;
22
+ }
23
+ export interface SpeakingRubricQuestion {
24
+ questionNo: number;
25
+ score: number;
26
+ maxScore: number;
27
+ evidence: string;
28
+ }
29
+ export interface SpeakingRubricSection {
30
+ code: string;
31
+ score: number;
32
+ maxScore: number;
33
+ questions: SpeakingRubricQuestion[];
34
+ }
35
+ export interface SpeakingRubric {
36
+ scoringVersion: string;
37
+ criteria: SpeakingRubricCriterion[];
38
+ sections?: SpeakingRubricSection[] | null;
39
+ total: number;
40
+ maxScore: number;
41
+ feedback: {
42
+ en: string;
43
+ vi: string;
44
+ } | null;
45
+ assessedAt: string | null;
46
+ }
47
+ export interface ResultReviewTemplatePart {
48
+ id: string | number;
49
+ name?: string;
50
+ instructions?: string;
51
+ audioUrl?: string;
52
+ sections?: Array<{
53
+ sectionId?: number;
54
+ name?: string;
55
+ instructions?: string;
56
+ startIndex?: number;
57
+ endIndex?: number;
58
+ questionType?: string;
59
+ }>;
60
+ }
2
61
  export interface ResultReviewAnswer {
3
62
  id?: string;
4
63
  questionId: string;
@@ -14,7 +73,11 @@ export interface ResultReviewAnswer {
14
73
  partNo: number | null;
15
74
  sectionId?: number | null;
16
75
  isExample?: boolean;
17
- gradingStatus?: string | null;
76
+ gradingStatus?: ReviewGradingStatus | string | null;
77
+ questionGroup?: {
78
+ id?: string | number;
79
+ payload?: unknown;
80
+ } | null;
18
81
  explanation?: string;
19
82
  teacherScore?: number | null;
20
83
  teacherFeedback?: string | null;
@@ -42,6 +105,7 @@ export interface ResultReviewDetail {
42
105
  rank?: number;
43
106
  totalStudents?: number;
44
107
  status?: string;
108
+ templateCode?: string | null;
45
109
  isPendingGrading?: boolean;
46
110
  showDetailedResults?: boolean;
47
111
  answers: ResultReviewAnswer[];
@@ -51,6 +115,7 @@ export interface ResultReviewDetail {
51
115
  percentage: number;
52
116
  feedback: string;
53
117
  } | undefined> | null;
118
+ speakingRubric?: SpeakingRubric | null;
54
119
  }
55
120
  export interface ResultReviewAttempt {
56
121
  id: string;
@@ -62,8 +127,9 @@ export interface ResultReviewAttempt {
62
127
  exam?: {
63
128
  template?: {
64
129
  title?: string;
65
- type?: string;
66
- parts?: unknown[];
130
+ type?: ResultTemplateType | string;
131
+ code?: string;
132
+ parts?: ResultReviewTemplatePart[];
67
133
  };
68
134
  };
69
135
  }
@@ -1 +1 @@
1
- export{};
1
+ export var ResultTemplateType;!function(E){E.SPEAKING="SPEAKING"}(ResultTemplateType||(ResultTemplateType={}));export var ReviewGradingStatus;!function(E){E.PENDING="PENDING",E.QUEUED="QUEUED",E.PROCESSING="PROCESSING",E.AUTO_GRADED="AUTO_GRADED",E.GRADED="GRADED",E.COMPLETED="COMPLETED",E.FAILED="FAILED",E.MANUAL_REVIEW="MANUAL_REVIEW",E.NOT_ANSWERED="NOT_ANSWERED"}(ReviewGradingStatus||(ReviewGradingStatus={}));
@@ -1 +1 @@
1
- "use client";import{jsx as e,jsxs as t,Fragment as n}from"react/jsx-runtime";import{useEffect as i,useRef as s,useState as r,useMemo as a,useCallback as o}from"react";import d from"next/image";import l from"../../../components/themes/summer-sky/assets/images/clock.png";import m from"../../../components/themes/summer-sky/assets/images/submit.png";import c from"../../../components/themes/summer-sky/assets/images/has_answered.png";import{toast as p}from"sonner";import{Loader2 as u}from"lucide-react";import{t as g}from"../../../shared/lib/i18n";import{transformAnswersForSubmission as h}from"./utils/answer-transformers";import{getThemeForLevel as x,normalizeExamTheme as b}from"../../../shared/lib/themes";import{useExamTakingStore as I}from"../../../shared/lib/stores/examTakingStore";import{useGradingTransientStore as f}from"../../../shared/lib/stores/gradingTransientStore";import{AlertDialog as w,AlertDialogAction as S,AlertDialogCancel as E,AlertDialogContent as y,AlertDialogDescription as _,AlertDialogFooter as v,AlertDialogHeader as N,AlertDialogTitle as A}from"../../../components/ui/alert-dialog";import{useExamTimer as C}from"./hooks/useExamTimer";import{useAutoSave as T}from"./hooks/useAutoSave";import{useProctoringCamera as k}from"./hooks/useProctoringCamera";import{getStandardExamTheme as P}from"../../../components/themes";import{QuestionRenderer as q}from"./components/QuestionRenderer";import{FullscreenLoading as M}from"../entry/fullscreen-loading";import{SummerSkySubmitModal as R}from"../../../components/themes/summer-sky/SummerSkySubmitModal";import{EnglishCertificationExamLayout as L,EnglishCertificationQuestionRenderer as G,englishCertQuestionDomId as D}from"../../../components/themes/english-certification";import{groupQuestionsByPart as Q}from"./utils/question-transformers";import{unwrap as B,unwrapAttempt as O}from"./exam-taking.utils";import{ExamTakingApiProvider as F}from"./ExamTakingApiContext";function Y({roomId:F,api:Y,onNavigate:K,resultPath:U=e=>`/student/results/${e}`,fallbackPath:V="/student/contests/discover",proctoringEnabled:$=!1,achieversExamTitle:H,onSubmitted:j}){const z=I(),W=s(!1),J=o(e=>{K?K(e):"undefined"!=typeof window&&(window.location.href=e)},[K]),{isCameraActive:X}=k({api:Y,attemptId:z.attemptId,intervalMs:1e4,enabled:$}),[Z,ee]=r(!0),te=z.currentPartIndex,ne=z.currentQuestionGlobalIndex,ie=o(e=>{const t=I.getState().currentPartIndex,n="function"==typeof e?e(t):e;I.getState().setCurrentPartIndex(n)},[]),se=o(e=>{const t=I.getState().currentQuestionGlobalIndex,n="function"==typeof e?e(t):e;I.getState().setCurrentQuestionGlobalIndex(n)},[]),[re,ae]=r([]),[oe,de]=r("SUMMER_SKY"),[le,me]=r(""),[ce,pe]=r(!1),[ue,ge]=r(null),[he,xe]=r(""),be=o(e=>{I.getState().markIntroPlayed(e)},[]),Ie="TOEIC"===z.examType||"ENGLISH_CERTIFICATION"===le,fe=a(()=>{if(!Ie||0===z.parts.length)return te;const e=z.currentIndex;for(let t=0;t<z.parts.length;t++){const n=z.parts[t],i=n.startIndex??0,s=n.endIndex??i;if(e>=i&&e<=s)return t}return 0},[Ie,z.currentIndex,z.parts,te]);i(()=>{(async()=>{try{ee(!0);let e=B(await Y.getActiveAttempt(F));e||(e=O(B(await Y.startAttempt(F))));let t=null;try{t=Y.getRoom?B(await Y.getRoom(F)):null,t&&ge(t)}catch{}const{examTemplate:n,questions:i}=B(await Y.getQuestions(e.id));n?.title&&xe(n.title);const s=n?.parts??e.exam?.template?.parts,r=Q(i,s),a=n?.subTitle??null,o=(r.every(e=>e.questions.every(e=>"SPEAKING"===e.question_skill_type)),r.some(e=>"SPEAKING"===e.category||e.questions.every(e=>"SPEAKING"===e.question_skill_type)));ae(r);const d=n?.theme||e?.exam?.template?.theme||e?.exam?.theme||t?.exam?.template?.theme||t?.exam?.theme||"",l=b(d);if(o){const e=["SPACESHIP","SUMMER_SKY"].includes(d);de(e?d:"SUMMER_SKY")}const m=new Date(e.startedAt),c=60*e.durationMinutes*1e3,u=m.getTime()+c,h=Date.now();if(0===Math.max(0,u-h)){p.info(g("student.exam.take.timeExpired"));try{await Y.submitAttempt(e.id)}catch{}return I.persist.clearStorage(),void J(U(e.id))}z.hydrateFromServer({attemptId:e.id,examRoomId:F,questions:i.map(e=>({id:e.id,question_type:e.question_type,content:e.content,is_example:e.is_example,total_points:e.total_points,part_id:e.part_id,part_no:e.part_no,question_number:e.question_number})),durationMinutes:e.durationMinutes,startedAt:e.startedAt,examType:a??void 0,examTitle:n?.title??void 0,parts:s}),a&&z.setExamType(a),me(l),s&&z.setParts(s),ee(!1)}catch(e){console.error("Failed to load exam data:",e),p.error(g("student.exam.take.loadError")),J(V)}})()},[F]);const we=a(()=>re.length>0&&re.every(e=>e.questions.every(e=>"SPEAKING"===e.question_skill_type)),[re]),Se=a(()=>{const e=re[te];return!!e&&(("SPEAKING_DESCRIBE_IMAGE"!==e.questionType&&"SPONTANEOUS_QA"!==e.questionType&&"SPEAKING_CUE_CARD"!==e.questionType||"CAMBRIDGE_YLE"!==le)&&("SPEAKING"===e.category||e.questions.every(e=>"SPEAKING"===e.question_skill_type)))},[re,te,le]),{remainingSeconds:Ee,warningLevel:ye}=C({onTimeExpired:async()=>{p.info(g("student.exam.take.autoSubmit")),await Ae()},onWarningThreshold:we||Ie?void 0:()=>{p.warning(g("student.exam.take.timeWarning"))},warningThresholdSeconds:we||Ie?void 0:300,warningThresholds:Ie?[{seconds:900,level:"warning",callback:()=>p.warning("Còn 15 phút!",{duration:3e3})},{seconds:300,level:"critical",callback:()=>p.warning("Còn 5 phút! Hãy nộp bài ngay!",{duration:5e3})}]:we?[{seconds:300,level:"warning",callback:()=>p.warning("Còn 5 phút! Hãy hoàn thành bài thi.",{duration:5e3})},{seconds:120,level:"critical",callback:()=>p.warning("Còn 2 phút! Bài thi sẽ được tự động nộp.",{duration:5e3})}]:void 0}),_e=T({api:Y,attemptId:z.attemptId||"",parts:re,intervalMs:Ie?15e3:3e4,onSaveError:e=>{console.error("Save error:",e)}}),ve=s(_e.saveNow),Ne=s(re);i(()=>{ve.current=_e.saveNow,Ne.current=re}),i(()=>{const e=()=>{const e=I.getState();if(!e.attemptId||!e.isDirty||e.isSubmitting)return;const t=h(e.answers,Ne.current);if(0!==t.length)try{Y.saveAnswers?.({attemptId:e.attemptId,answers:t,keepalive:!0})}catch{}},t=()=>{"hidden"===document.visibilityState&&e()},n=()=>{const e=I.getState();e.isDirty&&!e.isSubmitting&&ve.current()};return document.addEventListener("visibilitychange",t),window.addEventListener("pagehide",e),window.addEventListener("online",n),()=>{document.removeEventListener("visibilitychange",t),window.removeEventListener("pagehide",e),window.removeEventListener("online",n)}},[]),i(()=>{we||ve.current()},[z.currentIndex,te,ne,we]);const Ae=async()=>{if(!W.current){W.current=!0,z.setIsSubmitting(!0),pe(!1);try{z.isDirty&&await _e.saveNow();const e=B(await Y.submitAttempt(z.attemptId)),t=e?.pendingAnswerIds??[];z.attemptId&&(f.getState().setPending(z.attemptId,t),j?.({attemptId:z.attemptId,pendingAnswerIds:t})),0===t.length?p.success(g("student.exam.take.submitSuccess")):p.success(g("student.exam.take.submittedAIGradingInProgress")),I.persist.clearStorage(),J(U(z.attemptId))}catch(e){console.error("Failed to submit exam:",e);const t=e?.response?.data?.errorCode;if("ATTEMPT_ALREADY_SUBMITTED"===t)return I.persist.clearStorage(),void J(U(z.attemptId));p.warning(g("student.exam.take.submitError")),W.current=!1,z.setIsSubmitting(!1)}}},Ce=o(()=>{pe(!0)},[]),Te=s(new Map),ke=s([]),Pe=o((e,t)=>{z.setAnswer(e,t);const n=Te.current.get(e)||e,i=ke.current.findIndex(e=>e.questionId===n);-1!==i&&se(e=>e!==i?i:e)},[]),qe=s("forward"),Me=e=>{if("CAMBRIDGE_YLE"!==le)return!1;const t=re[e];return"SPEAKING_DESCRIBE_IMAGE"===t?.questionType||"SPONTANEOUS_QA"===t?.questionType||"SPEAKING_CUE_CARD"===t?.questionType},Re=()=>{const e=()=>{window.scrollTo({top:0,behavior:"smooth"});const e=document.getElementById("exam-main-scroll-area");if(e){e.scrollTo({top:0,behavior:"smooth"});e.querySelectorAll(".overflow-y-auto").forEach(e=>{e.scrollTo({top:0,behavior:"smooth"})})}};requestAnimationFrame(e),setTimeout(e,100),setTimeout(e,250)},Le=()=>{if("ENGLISH_CERTIFICATION"===le){const e=z.parts[fe+1];e&&z.goToQuestion(e.startIndex??0)}else if(Ie)z.goToQuestion(Math.min(z.currentIndex+1,z.questions.length-1));else if("CAMBRIDGE_YLE"===le&&Me(te)){const e=ne+1,t=Ve[e];t&&(t.partIndex!==te&&(qe.current="forward",ie(t.partIndex)),se(e))}else qe.current="forward",ie(e=>{const t=Math.min(e+1,re.length-1);if("CAMBRIDGE_YLE"===le){const e=Ve.find(e=>e.partIndex===t);e&&se(e.globalIndex)}return t});Re()},Ge=()=>{if("ENGLISH_CERTIFICATION"===le){const e=z.parts[fe-1];e&&z.goToQuestion(e.startIndex??0)}else if(Ie)z.goToQuestion(Math.max(z.currentIndex-1,0));else if("CAMBRIDGE_YLE"===le&&Me(te)){const e=ne-1,t=Ve[e];t&&(t.partIndex!==te&&(qe.current="backward",ie(t.partIndex)),se(e))}else qe.current="backward",ie(e=>{const t=Math.max(e-1,0);if("CAMBRIDGE_YLE"===le){const e=Ve.find(e=>e.partIndex===t);e&&se(e.globalIndex)}return t});Re()},De=()=>{J(V)},Qe=a(()=>re.reduce((e,t)=>e+("CAMBRIDGE_YLE"===le?t.questions.length:t.questions.filter(e=>!e.is_example).length),0),[re,le]),Be=a(()=>{const e=new Map;return re.forEach(t=>{t.questions.forEach(n=>{if(n.is_example)return;const i=n.content;if("FILL_MISSING_WORDS_IN_GRID"===t.questionType&&i?.grid&&i.grid.forEach((t,i)=>{t.forEach((t,s)=>{e.set(`${i}-${s}`,n.id)})}),"LABEL_THE_PICTURE"===t.questionType&&i?.labels&&i.labels.forEach(t=>{t.id&&e.set(t.id,n.id)}),"READ_AND_COLOR_OBJECTS"===t.questionType){const t=i?.regions||i?.regionNodes;t?.forEach(t=>{t.id&&e.set(t.id,n.id)})}if("IMAGE_OBJECT_MATCHING"===t.questionType){const t=i?.regions||i?.regionNodes;t?.forEach(t=>{t.id&&e.set(t.id,n.id)})}if("LOOK_PICTURE_FILL_WORD_HINT"===t.questionType){const t=i?.word;if(t&&"string"==typeof t){const i=/\{(\d+)\}/g;let s;for(;null!==(s=i.exec(t));){const t=s[1];e.set(`${n.id}-blank-${t}`,n.id)}}}})}),e},[re]);i(()=>{Te.current=Be},[Be]);const Oe=a(()=>{if(!z.answers)return new Set;const e=new Set(re.flatMap(e=>e.questions.filter(e=>!e.is_example).map(e=>e.id))),t=new Set;return Object.keys(z.answers).forEach(n=>{const i=z.answers[n];if(void 0===i||""===i)return;if(e.has(n))return void t.add(n);const s=Be.get(n);s&&e.has(s)&&t.add(s)}),t},[z.answers,re,Be]),Fe=Oe.size+("CAMBRIDGE_YLE"===le?re.reduce((e,t)=>e+t.questions.filter(e=>e.is_example).length,0):0),Ye=a(()=>ue?.exam?.name||ue?.name||(Ie?"TOEIC Listening and Reading Test":"Bài thi"),[ue,Ie]),Ke=a(()=>x(ue?.exam?.level).displayName,[ue?.exam?.level]),{sidebarParts:Ue,sidebarQuestions:Ve}=a(()=>{const e=[],t=[];let n=0;return re.forEach((i,s)=>{const r="CAMBRIDGE_YLE"===le?i.questions:i.questions.filter(e=>!e.is_example);let a=0;r.forEach(e=>{const r=!("CAMBRIDGE_YLE"!==le||!e.is_example)||Oe.has(e.id);r&&a++;const o=i.questions.findIndex(t=>t.id===e.id);t.push({globalIndex:n,partIndex:s,indexInPart:o,questionNumber:e.question_number,isAnswered:r,questionId:e.id,isExample:e.is_example}),n++}),e.push({partIndex:s,partNo:i.partNo,name:i.name,category:i.category,totalQuestions:"CAMBRIDGE_YLE"===le?i.questions.length:i.questions.filter(e=>!e.is_example).length,answeredCount:a})}),{sidebarParts:e,sidebarQuestions:t}},[re,Oe,le]);if(i(()=>{ke.current=Ve},[Ve]),Z)return e(M,{});const $e=te,He=re[$e];if(!He)return e("div",{className:"flex min-h-screen items-center justify-center",children:t("div",{className:"text-center",children:[e("p",{className:"text-lg font-semibold text-red-500",children:"Không tìm thấy câu hỏi"}),e("button",{type:"button",onClick:()=>J(V),className:"mt-4 rounded-lg bg-blue-500 px-4 py-2 text-white hover:bg-blue-600",children:"Quay lại"})]})});if(Se)return t(n,{children:[e(q,{part:He,answers:z.answers||{},onAnswerChange:Pe,isReviewMode:!1,onExit:De,onPartComplete:$e<re.length-1?Le:Ce,onPartBack:$e>0?Ge:void 0,speakingTheme:oe,initialQuestionIndex:"backward"===qe.current?He.questions.length-1:0,skipTeacherVideo:z.playedIntroParts.has($e)||he!==H,onTeacherIntroPlayed:()=>be($e)},`${He.partId}-${$e}`),"SUMMER_SKY"===oe?ce&&e(R,{onClose:()=>pe(!1),onSubmit:Ae,isSubmitting:z.isSubmitting}):e(w,{open:ce,onOpenChange:pe,children:t(y,{children:[t(N,{children:[e(A,{children:"Nộp bài thi"}),e(_,{children:"Bạn có chắc chắn muốn nộp bài không? Sau khi nộp, bạn sẽ không thể thay đổi câu trả lời."})]}),t(v,{children:[e(E,{disabled:z.isSubmitting,children:"Quay lại"}),e(S,{onClick:Ae,disabled:z.isSubmitting,children:z.isSubmitting?t(n,{children:[e(u,{className:"mr-2 h-4 w-4 animate-spin"}),"Đang nộp..."]}):"Nộp bài"})]})]})}),t("div",{className:"fixed top-8 right-6 z-[100] flex flex-col items-end gap-4 sm:gap-6 lg:top-10 lg:right-16",children:[t("div",{className:"flex items-center gap-4 sm:gap-6",children:[Ee>0&&t("div",{className:"flex items-center w-[130px] sm:w-[170px] h-[40px] sm:h-[50px] rounded-[33.5px] backdrop-blur-[8px] transition-all duration-300 ease-in-out border-none shadow-[0_4px_20px_rgba(0,0,0,0.25)] "+("critical"===ye?"bg-[rgba(220,38,38,0.85)] shadow-[0_0_24px_rgba(220,38,38,0.5)] animate-pulse":"warning"===ye?"bg-[rgba(180,120,0,0.85)]":"bg-[#001590]"),children:[e(d,{src:l,alt:"clock",width:75,height:75,className:"w-[50px] h-[50px] sm:w-[75px] sm:h-[75px] -ml-[5px] sm:-ml-[10px] -mt-[10px] sm:-mt-[15px] shrink-0 drop-shadow-[0_2px_6px_rgba(0,0,0,0.2)] pointer-events-none z-10"}),t("span",{className:"flex-1 text-center font-bold text-[18px] sm:text-[25px] leading-none tracking-[-0.03em] uppercase text-white pr-[10px] sm:pr-[16px]",style:{fontFamily:"'SVN-Volte Rounded', 'Quicksand', sans-serif",textShadow:"0 2px 4px rgba(0, 0, 0, 0.2)"},children:[Math.floor(Ee/60).toString().padStart(2,"0"),":",(Ee%60).toString().padStart(2,"0")]})]}),t("div",{className:"flex items-center w-[130px] sm:w-[170px] h-[40px] sm:h-[50px] rounded-[33.5px] bg-[#FF9900] shadow-[0_4px_20px_rgba(0,0,0,0.25)] border-none shrink-0",title:"Số câu hỏi đã hoàn thành",children:[e(d,{src:c,alt:"Progress",width:75,height:75,className:"w-[50px] h-[50px] sm:w-[75px] sm:h-[75px] -ml-[5px] sm:-ml-[10px] -mt-[10px] sm:-mt-[15px] shrink-0 drop-shadow-[0_2px_6px_rgba(0,0,0,0.2)] pointer-events-none z-10"}),t("span",{className:"flex-1 text-center font-bold text-[18px] sm:text-[25px] leading-none tracking-[-0.03em] uppercase text-white pr-[10px] sm:pr-[16px]",style:{fontFamily:'"SVN-Volte Rounded", "Quicksand", sans-serif',textShadow:"0 2px 4px rgba(0,0,0,0.2)"},children:[Fe,"/",Qe]})]})]}),t("button",{onClick:Ce,disabled:z.isSubmitting,className:"group cursor-pointer flex items-center h-[40px] sm:h-[50px] w-[130px] sm:w-[170px] rounded-[33.5px] shadow-[0_4px_20px_rgba(0,0,0,0.25)] transition-all duration-300 hover:scale-105 active:scale-95 disabled:pointer-events-none disabled:opacity-60 border-none "+(z.isSubmitting?"bg-slate-400":"bg-[#16CA3B] hover:bg-[#15b736]"),children:[e(d,{src:m,alt:"Submit",width:75,height:75,className:"w-[50px] h-[50px] sm:w-[75px] sm:h-[75px] -ml-[5px] sm:-ml-[10px] -mt-[10px] sm:-mt-[15px] shrink-0 drop-shadow-[0_2px_6px_rgba(0,0,0,0.2)] pointer-events-none transition-transform duration-300 group-hover:scale-110 z-10"}),z.isSubmitting?e("div",{className:"flex-1 flex justify-center pr-[10px] sm:pr-[16px]",children:e(u,{className:"h-6 w-6 animate-spin text-white"})}):e("span",{className:"flex-1 text-center font-bold text-[18px] sm:text-[25px] leading-none tracking-[-0.03em] uppercase text-white pr-[10px] sm:pr-[16px]",style:{fontFamily:'"SVN-Volte Rounded", "Quicksand", sans-serif',textShadow:"0 2px 4px rgba(0,0,0,0.2)"},children:"Nộp bài"})]})]}),z.isSubmitting&&e("div",{className:"fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm",children:t("div",{className:"rounded-xl bg-white p-8 text-center shadow-2xl",children:[e(u,{className:"mx-auto h-12 w-12 animate-spin text-green-500"}),e("p",{className:"mt-4 text-lg font-semibold text-gray-700",children:"Đang nộp bài..."}),e("p",{className:"mt-2 text-sm text-gray-500",children:"Vui lòng đợi trong giây lát"})]})})]});if(Ie){const i=re[fe],s=z.parts[fe],r=s?Math.max(0,z.currentIndex-(s.startIndex??0)):0,a=(s?.startIndex??0)+1,o=(s?.endIndex??a-1)+1,d={examName:Ye,remainingSeconds:z.remainingSeconds,totalQuestions:Qe,answeredCount:Fe,isSubmitting:z.isSubmitting,onSubmit:Ae,onClose:De,lastSavedAt:z.lastSavedAt,isSaving:_e.isSaving,partLabel:(s?.name||i?.name||"").toUpperCase()||void 0,partSubLabel:s?.category||void 0,questionRange:`${a}–${o}`},l=i?.questions?.[r],m="ENGLISH_CERTIFICATION"===le,c={onPrevious:Ge,onClear:m?void 0:()=>{l&&Pe(l.id,{optionId:null})},onToggleMark:m?void 0:()=>{l&&z.toggleFlag(l.id)},onNext:Le,isMarked:!!l&&z.flaggedQuestions.has(l.id),showSecondaryActions:!m,isPreviousDisabled:m?fe<=0:z.currentIndex<=0,isNextDisabled:m?fe>=z.parts.length-1:z.currentIndex>=z.questions.length-1},p={parts:z.parts,questions:z.questions,answers:z.answers,flaggedQuestions:z.flaggedQuestions,currentIndex:z.currentIndex,onQuestionClick:e=>{z.goToQuestion(e);const t=z.questions[e];t&&setTimeout(()=>{document.getElementById(D(t.id))?.scrollIntoView({behavior:"smooth",block:"start"})},80)},onPartClick:e=>{const t=z.parts[e];t&&(z.goToQuestion(t.startIndex??0),z.setCurrentPartIndex(e))}};if(i&&s)return t(n,{children:[e(L,{headerProps:d,navigationProps:p,footerProps:c,children:e(G,{part:i,partConfig:s,currentQuestionIndex:r,answers:z.answers,flaggedQuestions:z.flaggedQuestions,onAnswerChange:Pe,onToggleFlag:e=>z.toggleFlag(e),isReviewMode:!1,isAudioPlayed:z.playedListeningAudioParts.has(i.partId),onAudioFirstPlay:()=>z.markListeningAudioPlayed(i.partId)})}),z.isSubmitting&&e("div",{className:"fixed inset-0 z-50 flex items-center justify-center bg-black/50",children:t("div",{className:"rounded-xl bg-white p-8 text-center shadow-2xl",children:[e(u,{className:"mx-auto h-12 w-12 animate-spin text-green-500"}),e("p",{className:"mt-4 text-lg font-semibold text-gray-700",children:"Đang nộp bài..."}),e("p",{className:"mt-2 text-sm text-gray-500",children:"Vui lòng đợi trong giây lát"})]})})]})}return t(n,{children:[(()=>{const{Layout:n,QuestionCard:i}=P(le),s={headerProps:{examName:Ye,examType:"EXAM",levelLabel:Ke,totalQuestions:Qe,answeredCount:Fe,remainingSeconds:z.remainingSeconds,isSubmitting:z.isSubmitting,isCameraActive:X,onSubmit:Ce},onPrevious:Ge,onNext:Le,onClose:De,isNavDisabled:z.isNavDisabled,partName:He?.name||(He?.partNo?`PART ${He.partNo}`:void 0)};if("CAMBRIDGE_YLE"===le){const e=Me($e);s.isFirstQuestion=e?0===ne:0===$e,s.isLastQuestion=e?ne===Ve.length-1:$e===re.length-1,s.sidebarProps={parts:Ue,questions:Ve,currentQuestionGlobalIndex:ne,isNavDisabled:z.isNavDisabled,onPartClick:e=>{if(z.isNavDisabled)return;qe.current=e>$e?"forward":"backward",ie(e);const t=Ve.find(t=>t.partIndex===e);t&&se(t.globalIndex),Re()},onQuestionClick:e=>{if(z.isNavDisabled)return;const t=Ve[e];t&&t.partIndex!==$e&&(qe.current=t.partIndex>$e?"forward":"backward",ie(t.partIndex)),se(e),t&&setTimeout(()=>{const e=document.getElementById(`question-${t.questionId}`);e&&e.scrollIntoView({behavior:"smooth",block:"center"})},100)}}}else s.isFirstQuestion=0===$e,s.isLastQuestion=$e===re.length-1;const r="CAMBRIDGE_YLE"===le&&("SPEAKING_DESCRIBE_IMAGE"===He?.questionType||"SPONTANEOUS_QA"===He?.questionType||"SPEAKING_CUE_CARD"===He?.questionType),a=Ve[ne],o=r&&a?.partIndex===$e?a.indexInPart:void 0;return t(n,{...s,children:[e(i,{partName:He?.name||(He?.partNo?`PART ${He.partNo}`:void 0),children:e(q,{part:He,answers:z.answers||{},onAnswerChange:Pe,isReviewMode:!1,currentQuestionIndex:o,onExit:De,onPartComplete:$e<re.length-1?Le:Ce,onPartBack:$e>0?Ge:void 0,speakingTheme:"CAMBRIDGE_YLE"===le?"CAMBRIDGE_YLE":oe,initialQuestionIndex:"backward"===qe.current?He?.questions.length-1:0,skipTeacherVideo:z.playedIntroParts.has($e)||he!==H,onTeacherIntroPlayed:()=>be($e)})}),z.isSubmitting&&e("div",{className:"fixed inset-0 z-50 flex items-center justify-center bg-black/50",children:t("div",{className:"rounded-xl bg-white p-8 text-center shadow-2xl",children:[e(u,{className:"mx-auto h-12 w-12 animate-spin text-green-500"}),e("p",{className:"mt-4 text-lg font-semibold text-gray-700",children:"Đang nộp bài..."}),e("p",{className:"mt-2 text-sm text-gray-500",children:"Vui lòng đợi trong giây lát"})]})})]})})(),"CAMBRIDGE_YLE"===le?ce&&e(R,{onClose:()=>pe(!1),onSubmit:Ae,isSubmitting:z.isSubmitting}):e(w,{open:ce,onOpenChange:pe,children:t(y,{children:[t(N,{children:[e(A,{children:"Nộp bài thi"}),e(_,{children:"Bạn có chắc chắn muốn nộp bài không? Sau khi nộp, bạn sẽ không thể thay đổi câu trả lời."})]}),t(v,{children:[e(E,{disabled:z.isSubmitting,children:"Quay lại"}),e(S,{onClick:Ae,disabled:z.isSubmitting,children:z.isSubmitting?t(n,{children:[e(u,{className:"mr-2 h-4 w-4 animate-spin"}),"Đang nộp..."]}):"Nộp bài"})]})]})})]})}export function ExamTakingPageContainer(t){return e(F,{api:t.api,children:e(Y,{...t})})}
1
+ "use client";import{jsx as e,jsxs as t,Fragment as n}from"react/jsx-runtime";import{useEffect as i,useRef as s,useState as r,useMemo as a,useCallback as o}from"react";import d from"next/image";import l from"../../../components/themes/summer-sky/assets/images/clock.png";import m from"../../../components/themes/summer-sky/assets/images/submit.png";import c from"../../../components/themes/summer-sky/assets/images/has_answered.png";import{toast as p}from"sonner";import{Loader2 as u}from"lucide-react";import{t as g}from"../../../shared/lib/i18n";import{transformAnswersForSubmission as h}from"./utils/answer-transformers";import{getThemeForLevel as x,normalizeExamTheme as b}from"../../../shared/lib/themes";import{useExamTakingStore as I}from"../../../shared/lib/stores/examTakingStore";import{useGradingTransientStore as f}from"../../../shared/lib/stores/gradingTransientStore";import{AlertDialog as w,AlertDialogAction as E,AlertDialogCancel as S,AlertDialogContent as y,AlertDialogDescription as _,AlertDialogFooter as v,AlertDialogHeader as N,AlertDialogTitle as A}from"../../../components/ui/alert-dialog";import{useExamTimer as T}from"./hooks/useExamTimer";import{useAutoSave as C}from"./hooks/useAutoSave";import{useProctoringCamera as k}from"./hooks/useProctoringCamera";import{getStandardExamTheme as P}from"../../../components/themes";import{QuestionRenderer as q}from"./components/QuestionRenderer";import{FullscreenLoading as R}from"../entry/fullscreen-loading";import{SummerSkySubmitModal as M}from"../../../components/themes/summer-sky/SummerSkySubmitModal";import{EnglishCertificationExamLayout as D,EnglishCertificationQuestionRenderer as L,englishCertQuestionDomId as G}from"../../../components/themes/english-certification";import{groupQuestionsByPart as Q}from"./utils/question-transformers";import{unwrap as B,unwrapAttempt as O}from"./exam-taking.utils";import{ExamTakingApiProvider as Y}from"./ExamTakingApiContext";function F({roomId:Y,api:F,onNavigate:K,resultPath:U=e=>`/student/results/${e}`,fallbackPath:V="/student/contests/discover",proctoringEnabled:$=!1,achieversExamTitle:H,onSubmitted:j}){const z=I(),W=s(!1),J=o(e=>{K?K(e):"undefined"!=typeof window&&(window.location.href=e)},[K]),{isCameraActive:X}=k({api:F,attemptId:z.attemptId,intervalMs:1e4,enabled:$}),[Z,ee]=r(!0),te=z.currentPartIndex,ne=z.currentQuestionGlobalIndex,ie=o(e=>{const t=I.getState().currentPartIndex,n="function"==typeof e?e(t):e;I.getState().setCurrentPartIndex(n)},[]),se=o(e=>{const t=I.getState().currentQuestionGlobalIndex,n="function"==typeof e?e(t):e;I.getState().setCurrentQuestionGlobalIndex(n)},[]),[re,ae]=r([]),[oe,de]=r("SUMMER_SKY"),[le,me]=r(""),[ce,pe]=r(!1),[ue,ge]=r(null),[he,xe]=r(""),be=o(e=>{I.getState().markIntroPlayed(e)},[]),Ie="TOEIC"===z.examType||"ENGLISH_CERTIFICATION"===le,fe=a(()=>{if(!Ie||0===z.parts.length)return te;const e=z.currentIndex;for(let t=0;t<z.parts.length;t++){const n=z.parts[t],i=n.startIndex??0,s=n.endIndex??i;if(e>=i&&e<=s)return t}return 0},[Ie,z.currentIndex,z.parts,te]);i(()=>{(async()=>{try{ee(!0);let e=B(await F.getActiveAttempt(Y));e||(e=O(B(await F.startAttempt(Y))));let t=null;try{t=F.getRoom?B(await F.getRoom(Y)):null,t&&ge(t)}catch{}const{examTemplate:n,questions:i}=B(await F.getQuestions(e.id));n?.title&&xe(n.title);const s=n?.parts??e.exam?.template?.parts,r=Q(i,s),a=n?.subTitle??null,o=(r.every(e=>e.questions.every(e=>"SPEAKING"===e.question_skill_type)),r.some(e=>"SPEAKING"===e.category||e.questions.every(e=>"SPEAKING"===e.question_skill_type)));ae(r);const d=n?.theme||e?.exam?.template?.theme||e?.exam?.theme||t?.exam?.template?.theme||t?.exam?.theme||"",l=b(d);if(o){const e=["SPACESHIP","SUMMER_SKY"].includes(d);de(e?d:"SUMMER_SKY")}const m=new Date(e.startedAt),c=60*e.durationMinutes*1e3,u=m.getTime()+c,h=Date.now();if(0===Math.max(0,u-h)){p.info(g("student.exam.take.timeExpired"));try{await F.submitAttempt(e.id)}catch{}return I.persist.clearStorage(),void J(U(e.id))}z.hydrateFromServer({attemptId:e.id,examRoomId:Y,questions:i.map(e=>({id:e.id,question_type:e.question_type,content:e.content,is_example:e.is_example,total_points:e.total_points,part_id:e.part_id,part_no:e.part_no,question_number:e.question_number})),durationMinutes:e.durationMinutes,startedAt:e.startedAt,examType:a??void 0,examTitle:n?.title??void 0,parts:s}),a&&z.setExamType(a),me(l),s&&z.setParts(s),ee(!1)}catch(e){console.error("Failed to load exam data:",e),p.error(g("student.exam.take.loadError")),J(V)}})()},[Y]);const we=a(()=>re.length>0&&re.every(e=>e.questions.every(e=>"SPEAKING"===e.question_skill_type)),[re]),Ee=a(()=>{const e=re[te];return!!e&&(("SPEAKING_DESCRIBE_IMAGE"!==e.questionType&&"SPONTANEOUS_QA"!==e.questionType&&"SPEAKING_CUE_CARD"!==e.questionType&&"READ_DISPLAYED_CONTENT"!==e.questionType||"CAMBRIDGE_YLE"!==le)&&("SPEAKING"===e.category||e.questions.every(e=>"SPEAKING"===e.question_skill_type)))},[re,te,le]),{remainingSeconds:Se,warningLevel:ye}=T({onTimeExpired:async()=>{p.info(g("student.exam.take.autoSubmit")),await Ae()},onWarningThreshold:we||Ie?void 0:()=>{p.warning(g("student.exam.take.timeWarning"))},warningThresholdSeconds:we||Ie?void 0:300,warningThresholds:Ie?[{seconds:900,level:"warning",callback:()=>p.warning("Còn 15 phút!",{duration:3e3})},{seconds:300,level:"critical",callback:()=>p.warning("Còn 5 phút! Hãy nộp bài ngay!",{duration:5e3})}]:we?[{seconds:300,level:"warning",callback:()=>p.warning("Còn 5 phút! Hãy hoàn thành bài thi.",{duration:5e3})},{seconds:120,level:"critical",callback:()=>p.warning("Còn 2 phút! Bài thi sẽ được tự động nộp.",{duration:5e3})}]:void 0}),_e=C({api:F,attemptId:z.attemptId||"",parts:re,intervalMs:Ie?15e3:3e4,onSaveError:e=>{console.error("Save error:",e)}}),ve=s(_e.saveNow),Ne=s(re);i(()=>{ve.current=_e.saveNow,Ne.current=re}),i(()=>{const e=()=>{const e=I.getState();if(!e.attemptId||!e.isDirty||e.isSubmitting)return;const t=h(e.answers,Ne.current);if(0!==t.length)try{F.saveAnswers?.({attemptId:e.attemptId,answers:t,keepalive:!0})}catch{}},t=()=>{"hidden"===document.visibilityState&&e()},n=()=>{const e=I.getState();e.isDirty&&!e.isSubmitting&&ve.current()};return document.addEventListener("visibilitychange",t),window.addEventListener("pagehide",e),window.addEventListener("online",n),()=>{document.removeEventListener("visibilitychange",t),window.removeEventListener("pagehide",e),window.removeEventListener("online",n)}},[]),i(()=>{we||ve.current()},[z.currentIndex,te,ne,we]);const Ae=async()=>{if(!W.current){W.current=!0,z.setIsSubmitting(!0),pe(!1);try{z.isDirty&&await _e.saveNow();const e=B(await F.submitAttempt(z.attemptId)),t=e?.pendingAnswerIds??[];z.attemptId&&(f.getState().setPending(z.attemptId,t),j?.({attemptId:z.attemptId,pendingAnswerIds:t})),0===t.length?p.success(g("student.exam.take.submitSuccess")):p.success(g("student.exam.take.submittedAIGradingInProgress")),I.persist.clearStorage(),J(U(z.attemptId))}catch(e){console.error("Failed to submit exam:",e);const t=e?.response?.data?.errorCode;if("ATTEMPT_ALREADY_SUBMITTED"===t)return I.persist.clearStorage(),void J(U(z.attemptId));p.warning(g("student.exam.take.submitError")),W.current=!1,z.setIsSubmitting(!1)}}},Te=o(()=>{pe(!0)},[]),Ce=s(new Map),ke=s([]),Pe=o((e,t)=>{z.setAnswer(e,t);const n=Ce.current.get(e)||e,i=ke.current.findIndex(e=>e.questionId===n);-1!==i&&se(e=>e!==i?i:e)},[]),qe=s("forward"),Re=e=>{if("CAMBRIDGE_YLE"!==le)return!1;const t=re[e];return"SPEAKING_DESCRIBE_IMAGE"===t?.questionType||"SPONTANEOUS_QA"===t?.questionType||"SPEAKING_CUE_CARD"===t?.questionType||"READ_DISPLAYED_CONTENT"===t?.questionType},Me=()=>{const e=()=>{window.scrollTo({top:0,behavior:"smooth"});const e=document.getElementById("exam-main-scroll-area");if(e){e.scrollTo({top:0,behavior:"smooth"});e.querySelectorAll(".overflow-y-auto").forEach(e=>{e.scrollTo({top:0,behavior:"smooth"})})}};requestAnimationFrame(e),setTimeout(e,100),setTimeout(e,250)},De=()=>{if("ENGLISH_CERTIFICATION"===le){const e=z.parts[fe+1];e&&z.goToQuestion(e.startIndex??0)}else if(Ie)z.goToQuestion(Math.min(z.currentIndex+1,z.questions.length-1));else if("CAMBRIDGE_YLE"===le&&Re(te)){const e=ne+1,t=Ve[e];t&&(t.partIndex!==te&&(qe.current="forward",ie(t.partIndex)),se(e))}else qe.current="forward",ie(e=>{const t=Math.min(e+1,re.length-1);if("CAMBRIDGE_YLE"===le){const e=Ve.find(e=>e.partIndex===t);e&&se(e.globalIndex)}return t});Me()},Le=()=>{if("ENGLISH_CERTIFICATION"===le){const e=z.parts[fe-1];e&&z.goToQuestion(e.startIndex??0)}else if(Ie)z.goToQuestion(Math.max(z.currentIndex-1,0));else if("CAMBRIDGE_YLE"===le&&Re(te)){const e=ne-1,t=Ve[e];t&&(t.partIndex!==te&&(qe.current="backward",ie(t.partIndex)),se(e))}else qe.current="backward",ie(e=>{const t=Math.max(e-1,0);if("CAMBRIDGE_YLE"===le){const e=Ve.find(e=>e.partIndex===t);e&&se(e.globalIndex)}return t});Me()},Ge=()=>{J(V)},Qe=a(()=>re.reduce((e,t)=>e+("CAMBRIDGE_YLE"===le?t.questions.length:t.questions.filter(e=>!e.is_example).length),0),[re,le]),Be=a(()=>{const e=new Map;return re.forEach(t=>{t.questions.forEach(n=>{if(n.is_example)return;const i=n.content;if("FILL_MISSING_WORDS_IN_GRID"===t.questionType&&i?.grid&&i.grid.forEach((t,i)=>{t.forEach((t,s)=>{e.set(`${i}-${s}`,n.id)})}),"LABEL_THE_PICTURE"===t.questionType&&i?.labels&&i.labels.forEach(t=>{t.id&&e.set(t.id,n.id)}),"READ_AND_COLOR_OBJECTS"===t.questionType){const t=i?.regions||i?.regionNodes;t?.forEach(t=>{t.id&&e.set(t.id,n.id)})}if("IMAGE_OBJECT_MATCHING"===t.questionType){const t=i?.regions||i?.regionNodes;t?.forEach(t=>{t.id&&e.set(t.id,n.id)})}if("LOOK_PICTURE_FILL_WORD_HINT"===t.questionType){const t=i?.word;if(t&&"string"==typeof t){const i=/\{(\d+)\}/g;let s;for(;null!==(s=i.exec(t));){const t=s[1];e.set(`${n.id}-blank-${t}`,n.id)}}}})}),e},[re]);i(()=>{Ce.current=Be},[Be]);const Oe=a(()=>{if(!z.answers)return new Set;const e=new Set(re.flatMap(e=>e.questions.filter(e=>!e.is_example).map(e=>e.id))),t=new Set;return Object.keys(z.answers).forEach(n=>{const i=z.answers[n];if(void 0===i||""===i)return;if(e.has(n))return void t.add(n);const s=Be.get(n);s&&e.has(s)&&t.add(s)}),t},[z.answers,re,Be]),Ye=Oe.size+("CAMBRIDGE_YLE"===le?re.reduce((e,t)=>e+t.questions.filter(e=>e.is_example).length,0):0),Fe=a(()=>ue?.exam?.name||ue?.name||(Ie?"TOEIC Listening and Reading Test":"Bài thi"),[ue,Ie]),Ke=a(()=>x(ue?.exam?.level).displayName,[ue?.exam?.level]),{sidebarParts:Ue,sidebarQuestions:Ve}=a(()=>{const e=[],t=[];let n=0;return re.forEach((i,s)=>{const r="CAMBRIDGE_YLE"===le?i.questions:i.questions.filter(e=>!e.is_example);let a=0;r.forEach(e=>{const r=!("CAMBRIDGE_YLE"!==le||!e.is_example)||Oe.has(e.id);r&&a++;const o=i.questions.findIndex(t=>t.id===e.id);t.push({globalIndex:n,partIndex:s,indexInPart:o,questionNumber:e.question_number,isAnswered:r,questionId:e.id,isExample:e.is_example}),n++}),e.push({partIndex:s,partNo:i.partNo,name:i.name,category:i.category,totalQuestions:"CAMBRIDGE_YLE"===le?i.questions.length:i.questions.filter(e=>!e.is_example).length,answeredCount:a})}),{sidebarParts:e,sidebarQuestions:t}},[re,Oe,le]);if(i(()=>{ke.current=Ve},[Ve]),Z)return e(R,{});const $e=te,He=re[$e];if(!He)return e("div",{className:"flex min-h-screen items-center justify-center",children:t("div",{className:"text-center",children:[e("p",{className:"text-lg font-semibold text-red-500",children:"Không tìm thấy câu hỏi"}),e("button",{type:"button",onClick:()=>J(V),className:"mt-4 rounded-lg bg-blue-500 px-4 py-2 text-white hover:bg-blue-600",children:"Quay lại"})]})});if(Ee)return t(n,{children:[e(q,{part:He,answers:z.answers||{},onAnswerChange:Pe,isReviewMode:!1,onExit:Ge,onPartComplete:$e<re.length-1?De:Te,onPartBack:$e>0?Le:void 0,speakingTheme:oe,initialQuestionIndex:"backward"===qe.current?He.questions.length-1:0,skipTeacherVideo:z.playedIntroParts.has($e)||he!==H,onTeacherIntroPlayed:()=>be($e)},`${He.partId}-${$e}`),"SUMMER_SKY"===oe?ce&&e(M,{onClose:()=>pe(!1),onSubmit:Ae,isSubmitting:z.isSubmitting}):e(w,{open:ce,onOpenChange:pe,children:t(y,{children:[t(N,{children:[e(A,{children:"Nộp bài thi"}),e(_,{children:"Bạn có chắc chắn muốn nộp bài không? Sau khi nộp, bạn sẽ không thể thay đổi câu trả lời."})]}),t(v,{children:[e(S,{disabled:z.isSubmitting,children:"Quay lại"}),e(E,{onClick:Ae,disabled:z.isSubmitting,children:z.isSubmitting?t(n,{children:[e(u,{className:"mr-2 h-4 w-4 animate-spin"}),"Đang nộp..."]}):"Nộp bài"})]})]})}),t("div",{className:"fixed top-8 right-6 z-[100] flex flex-col items-end gap-4 sm:gap-6 lg:top-10 lg:right-16",children:[t("div",{className:"flex items-center gap-4 sm:gap-6",children:[Se>0&&t("div",{className:"flex items-center w-[130px] sm:w-[170px] h-[40px] sm:h-[50px] rounded-[33.5px] backdrop-blur-[8px] transition-all duration-300 ease-in-out border-none shadow-[0_4px_20px_rgba(0,0,0,0.25)] "+("critical"===ye?"bg-[rgba(220,38,38,0.85)] shadow-[0_0_24px_rgba(220,38,38,0.5)] animate-pulse":"warning"===ye?"bg-[rgba(180,120,0,0.85)]":"bg-[#001590]"),children:[e(d,{src:l,alt:"clock",width:75,height:75,className:"w-[50px] h-[50px] sm:w-[75px] sm:h-[75px] -ml-[5px] sm:-ml-[10px] -mt-[10px] sm:-mt-[15px] shrink-0 drop-shadow-[0_2px_6px_rgba(0,0,0,0.2)] pointer-events-none z-10"}),t("span",{className:"flex-1 text-center font-bold text-[18px] sm:text-[25px] leading-none tracking-[-0.03em] uppercase text-white pr-[10px] sm:pr-[16px]",style:{fontFamily:"'SVN-Volte Rounded', 'Quicksand', sans-serif",textShadow:"0 2px 4px rgba(0, 0, 0, 0.2)"},children:[Math.floor(Se/60).toString().padStart(2,"0"),":",(Se%60).toString().padStart(2,"0")]})]}),t("div",{className:"flex items-center w-[130px] sm:w-[170px] h-[40px] sm:h-[50px] rounded-[33.5px] bg-[#FF9900] shadow-[0_4px_20px_rgba(0,0,0,0.25)] border-none shrink-0",title:"Số câu hỏi đã hoàn thành",children:[e(d,{src:c,alt:"Progress",width:75,height:75,className:"w-[50px] h-[50px] sm:w-[75px] sm:h-[75px] -ml-[5px] sm:-ml-[10px] -mt-[10px] sm:-mt-[15px] shrink-0 drop-shadow-[0_2px_6px_rgba(0,0,0,0.2)] pointer-events-none z-10"}),t("span",{className:"flex-1 text-center font-bold text-[18px] sm:text-[25px] leading-none tracking-[-0.03em] uppercase text-white pr-[10px] sm:pr-[16px]",style:{fontFamily:'"SVN-Volte Rounded", "Quicksand", sans-serif',textShadow:"0 2px 4px rgba(0,0,0,0.2)"},children:[Ye,"/",Qe]})]})]}),t("button",{onClick:Te,disabled:z.isSubmitting,className:"group cursor-pointer flex items-center h-[40px] sm:h-[50px] w-[130px] sm:w-[170px] rounded-[33.5px] shadow-[0_4px_20px_rgba(0,0,0,0.25)] transition-all duration-300 hover:scale-105 active:scale-95 disabled:pointer-events-none disabled:opacity-60 border-none "+(z.isSubmitting?"bg-slate-400":"bg-[#16CA3B] hover:bg-[#15b736]"),children:[e(d,{src:m,alt:"Submit",width:75,height:75,className:"w-[50px] h-[50px] sm:w-[75px] sm:h-[75px] -ml-[5px] sm:-ml-[10px] -mt-[10px] sm:-mt-[15px] shrink-0 drop-shadow-[0_2px_6px_rgba(0,0,0,0.2)] pointer-events-none transition-transform duration-300 group-hover:scale-110 z-10"}),z.isSubmitting?e("div",{className:"flex-1 flex justify-center pr-[10px] sm:pr-[16px]",children:e(u,{className:"h-6 w-6 animate-spin text-white"})}):e("span",{className:"flex-1 text-center font-bold text-[18px] sm:text-[25px] leading-none tracking-[-0.03em] uppercase text-white pr-[10px] sm:pr-[16px]",style:{fontFamily:'"SVN-Volte Rounded", "Quicksand", sans-serif',textShadow:"0 2px 4px rgba(0,0,0,0.2)"},children:"Nộp bài"})]})]}),z.isSubmitting&&e("div",{className:"fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm",children:t("div",{className:"rounded-xl bg-white p-8 text-center shadow-2xl",children:[e(u,{className:"mx-auto h-12 w-12 animate-spin text-green-500"}),e("p",{className:"mt-4 text-lg font-semibold text-gray-700",children:"Đang nộp bài..."}),e("p",{className:"mt-2 text-sm text-gray-500",children:"Vui lòng đợi trong giây lát"})]})})]});if(Ie){const i=re[fe],s=z.parts[fe],r=s?Math.max(0,z.currentIndex-(s.startIndex??0)):0,a=(s?.startIndex??0)+1,o=(s?.endIndex??a-1)+1,d={examName:Fe,remainingSeconds:z.remainingSeconds,totalQuestions:Qe,answeredCount:Ye,isSubmitting:z.isSubmitting,onSubmit:Ae,onClose:Ge,lastSavedAt:z.lastSavedAt,isSaving:_e.isSaving,partLabel:(s?.name||i?.name||"").toUpperCase()||void 0,partSubLabel:s?.category||void 0,questionRange:`${a}–${o}`},l=i?.questions?.[r],m="ENGLISH_CERTIFICATION"===le,c={onPrevious:Le,onClear:m?void 0:()=>{l&&Pe(l.id,{optionId:null})},onToggleMark:m?void 0:()=>{l&&z.toggleFlag(l.id)},onNext:De,isMarked:!!l&&z.flaggedQuestions.has(l.id),showSecondaryActions:!m,isPreviousDisabled:m?fe<=0:z.currentIndex<=0,isNextDisabled:m?fe>=z.parts.length-1:z.currentIndex>=z.questions.length-1},p={parts:z.parts,questions:z.questions,answers:z.answers,flaggedQuestions:z.flaggedQuestions,currentIndex:z.currentIndex,onQuestionClick:e=>{z.goToQuestion(e);const t=z.questions[e];t&&setTimeout(()=>{document.getElementById(G(t.id))?.scrollIntoView({behavior:"smooth",block:"start"})},80)},onPartClick:e=>{const t=z.parts[e];t&&(z.goToQuestion(t.startIndex??0),z.setCurrentPartIndex(e))}};if(i&&s)return t(n,{children:[e(D,{headerProps:d,navigationProps:p,footerProps:c,children:e(L,{part:i,partConfig:s,currentQuestionIndex:r,answers:z.answers,flaggedQuestions:z.flaggedQuestions,onAnswerChange:Pe,onToggleFlag:e=>z.toggleFlag(e),isReviewMode:!1,isAudioPlayed:z.playedListeningAudioParts.has(i.partId),onAudioFirstPlay:()=>z.markListeningAudioPlayed(i.partId)})}),z.isSubmitting&&e("div",{className:"fixed inset-0 z-50 flex items-center justify-center bg-black/50",children:t("div",{className:"rounded-xl bg-white p-8 text-center shadow-2xl",children:[e(u,{className:"mx-auto h-12 w-12 animate-spin text-green-500"}),e("p",{className:"mt-4 text-lg font-semibold text-gray-700",children:"Đang nộp bài..."}),e("p",{className:"mt-2 text-sm text-gray-500",children:"Vui lòng đợi trong giây lát"})]})})]})}return t(n,{children:[(()=>{const{Layout:n,QuestionCard:i}=P(le),s={headerProps:{examName:Fe,examType:"EXAM",levelLabel:Ke,totalQuestions:Qe,answeredCount:Ye,remainingSeconds:z.remainingSeconds,isSubmitting:z.isSubmitting,isCameraActive:X,onSubmit:Te},onPrevious:Le,onNext:De,onClose:Ge,isNavDisabled:z.isNavDisabled,partName:He?.name||(He?.partNo?`PART ${He.partNo}`:void 0)};if("CAMBRIDGE_YLE"===le){const e=Re($e);s.isFirstQuestion=e?0===ne:0===$e,s.isLastQuestion=e?ne===Ve.length-1:$e===re.length-1,s.sidebarProps={parts:Ue,questions:Ve,currentQuestionGlobalIndex:ne,isNavDisabled:z.isNavDisabled,onPartClick:e=>{if(z.isNavDisabled)return;qe.current=e>$e?"forward":"backward",ie(e);const t=Ve.find(t=>t.partIndex===e);t&&se(t.globalIndex),Me()},onQuestionClick:e=>{if(z.isNavDisabled)return;const t=Ve[e];t&&t.partIndex!==$e&&(qe.current=t.partIndex>$e?"forward":"backward",ie(t.partIndex)),se(e),t&&setTimeout(()=>{const e=document.getElementById(`question-${t.questionId}`);e&&e.scrollIntoView({behavior:"smooth",block:"center"})},100)}}}else s.isFirstQuestion=0===$e,s.isLastQuestion=$e===re.length-1;const r="CAMBRIDGE_YLE"===le&&("SPEAKING_DESCRIBE_IMAGE"===He?.questionType||"SPONTANEOUS_QA"===He?.questionType||"SPEAKING_CUE_CARD"===He?.questionType||"READ_DISPLAYED_CONTENT"===He?.questionType),a=Ve[ne],o=r&&a?.partIndex===$e?a.indexInPart:void 0;return t(n,{...s,children:[e(i,{partName:He?.name||(He?.partNo?`PART ${He.partNo}`:void 0),children:e(q,{part:He,answers:z.answers||{},onAnswerChange:Pe,isReviewMode:!1,currentQuestionIndex:o,onExit:Ge,onPartComplete:$e<re.length-1?De:Te,onPartBack:$e>0?Le:void 0,speakingTheme:"CAMBRIDGE_YLE"===le?"CAMBRIDGE_YLE":oe,initialQuestionIndex:"backward"===qe.current?He?.questions.length-1:0,skipTeacherVideo:z.playedIntroParts.has($e)||he!==H,onTeacherIntroPlayed:()=>be($e)})}),z.isSubmitting&&e("div",{className:"fixed inset-0 z-50 flex items-center justify-center bg-black/50",children:t("div",{className:"rounded-xl bg-white p-8 text-center shadow-2xl",children:[e(u,{className:"mx-auto h-12 w-12 animate-spin text-green-500"}),e("p",{className:"mt-4 text-lg font-semibold text-gray-700",children:"Đang nộp bài..."}),e("p",{className:"mt-2 text-sm text-gray-500",children:"Vui lòng đợi trong giây lát"})]})})]})})(),"CAMBRIDGE_YLE"===le?ce&&e(M,{onClose:()=>pe(!1),onSubmit:Ae,isSubmitting:z.isSubmitting}):e(w,{open:ce,onOpenChange:pe,children:t(y,{children:[t(N,{children:[e(A,{children:"Nộp bài thi"}),e(_,{children:"Bạn có chắc chắn muốn nộp bài không? Sau khi nộp, bạn sẽ không thể thay đổi câu trả lời."})]}),t(v,{children:[e(S,{disabled:z.isSubmitting,children:"Quay lại"}),e(E,{onClick:Ae,disabled:z.isSubmitting,children:z.isSubmitting?t(n,{children:[e(u,{className:"mr-2 h-4 w-4 animate-spin"}),"Đang nộp..."]}):"Nộp bài"})]})]})})]})}export function ExamTakingPageContainer(t){return e(Y,{api:t.api,children:e(F,{...t})})}
@@ -1 +1 @@
1
- "use client";import{jsxs as e,jsx as r}from"react/jsx-runtime";import{usePresignedFileUrl as t}from"../../../../../../shared/lib/hooks";import{useSpeakingRendererSetup as a}from"../../../hooks/useSpeakingRendererSetup";import n from"../../../../../../components/themes/cambridge-yle/CambridgeYlePartBanner";import{getSpeakingLayout as i}from"../../../../../../components/themes";import{SpeakingAudioPlayer as o}from"../../../../../../components/shared/audio/SpeakingAudioPlayer";import{useEffect as s,useRef as d,useState as l}from"react";import c from"next/image";import{useExamTakingStore as m}from"../../../../../../shared/lib/stores/examTakingStore";import u from"../../../../../../components/themes/summer-sky/assets/images/mic_ver2_icon.png";import h from"../../../../../../components/themes/summer-sky/assets/images/stop_audio_ver2_icon.png";import p from"../../../../../../components/themes/summer-sky/assets/images/audio_ver2_icon.png";import g from"../../../../../../components/themes/summer-sky/assets/images/pause_audio_ver2_icon.png";import x from"../../../../../../components/themes/summer-sky/assets/images/lock_mic_ver2_icon.png";function f({total:t,currentIndex:a,answeredIds:n,questionIds:i}){return e("div",{className:"flex items-center gap-2 font-bold text-[#78350f] font-['Quicksand'] z-10",children:[e("span",{className:"text-sm",children:["Question ",a+1,"/ ",t]}),r("div",{className:"flex gap-1.5",children:Array.from({length:t},(e,t)=>{const o=t===a,s=n.has(i[t]);return r("div",{className:"w-2.5 h-2.5 rounded-full "+(o?"bg-[#FF9900] scale-125 shadow-sm":s?"bg-[#16CA3B]":"bg-white/60")},t)})})]})}const b=new Set(["/speaking/video/read_displayed_content.webm","/speaking/video/speaking_describe_image.webm","/speaking/video/spontaneous_qa.webm"]);function w(e){const r=Math.floor(e/60),t=Math.floor(e%60);return`${r.toString().padStart(2,"0")}:${t.toString().padStart(2,"0")}`}export function SpeakingDescribeImageRenderer(x){const{currentQuestionIndex:v}=x,y=a({...x,maxDurationMs:6e4,currentQuestionIndex:v}),N=m(e=>e.examTitle),k=N?.toLowerCase().includes("placement test")||!1,_=y.currentQuestion?.content,{previewUrl:C,isLoading:E}=t(_?.imageUrl||""),P=d(null),[S,F]=l(!!x.skipTeacherVideo),I=d(null),[T,U]=l(!1),[j,q]=l(0),[A,z]=l(0);if(s(()=>{const e=P.current;e&&x.teacherVideoUrl&&(e.playbackRate=b.has(x.teacherVideoUrl)?1.25:1)},[x.teacherVideoUrl]),s(()=>{const e=I.current;if(!e)return;const r=()=>{U(!0),y.onAudioPlayingChange?.(!0)},t=()=>{U(!1),y.onAudioPlayingChange?.(!1)},a=()=>{U(!1),q(0),y.onAudioPlayingChange?.(!1)},n=()=>{q(e.currentTime),e.duration&&isFinite(e.duration)&&z(e.duration)},i=()=>{e.duration&&isFinite(e.duration)&&z(e.duration)};return e.addEventListener("play",r),e.addEventListener("pause",t),e.addEventListener("ended",a),e.addEventListener("timeupdate",n),e.addEventListener("loadedmetadata",i),e.addEventListener("durationchange",i),()=>{e.removeEventListener("play",r),e.removeEventListener("pause",t),e.removeEventListener("ended",a),e.removeEventListener("timeupdate",n),e.removeEventListener("loadedmetadata",i),e.removeEventListener("durationchange",i),y.onAudioPlayingChange?.(!1)}},[y.recorder?.previewUrl,y.onAudioPlayingChange]),!y.currentQuestion||!_)return null;const L=new Set(x.part.questions.map(e=>e.id).filter(e=>!!x.answers[e])),V=x.part.questions.map(e=>e.id);if(x.speakingTheme&&"CAMBRIDGE_YLE"!==x.speakingTheme){const t=i(x.speakingTheme),a=r(f,{total:x.part.questions.length,currentIndex:y.nav.currentIndex,answeredIds:L,questionIds:V});return r(t,{onExit:x.onExit,partName:x.part.name||`Part ${x.part.partNo}`,instruction:x.part.instructions||"DESCRIBE THE IMAGE",audioUrl:"",questionNumber:y.nav.questionNumberInPart,onPrev:y.handlePrev,onNext:y.handleNext,showPrev:y.nav.showPrev,showNext:y.nav.showNext,canGoPrev:y.nav.canGoPrev,canGoNext:y.nav.canGoNext,onAudioPlayingChange:y.onAudioPlayingChange,recorder:x.isReviewMode?null:y.recorder,showExistingBadge:y.showExistingBadge,headerRight:a,teacherVideoUrl:x.teacherVideoUrl,waitForTeacherVideo:x.waitForTeacherVideo,onTeacherVideoEnded:x.onTeacherIntroPlayed,skipTeacherVideo:x.skipTeacherVideo,children:r("div",{className:"w-full h-full flex flex-col items-center relative",children:r("div",{className:"w-full flex-1",style:{minHeight:0},children:r("div",{className:"mx-auto",style:{width:"900px",maxWidth:"95%",height:"calc(100% - 40px)"},children:e("div",{className:"w-full h-full flex flex-col items-center justify-center",style:{paddingTop:"10px"},children:[_.isBackup&&r("div",{className:"bg-amber-500 text-white px-3 py-1 rounded-md text-xs font-bold border border-white shadow-md z-10 shrink-0 mb-4",children:"Câu hỏi dự phòng"}),_.question&&r("div",{className:"z-10 mx-auto px-8 py-2.5 bg-white/80 backdrop-blur-md rounded-full shadow-sm border border-slate-100 w-fit shrink-0 mb-4",children:r("h3",{className:"text-[17px] md:text-lg font-bold text-[#343C55] font-['Quicksand'] text-center whitespace-pre-wrap leading-tight",children:_.question})}),_.audioUrl&&r("div",{className:"z-10 mx-auto w-full max-w-[280px] shrink-0 mb-4",children:r(o,{audioUrl:_.audioUrl,onPlayingChange:y.onAudioPlayingChange,autoPlay:!x.waitForTeacherVideo})}),y.showExistingBadge&&e("div",{className:"absolute right-12 top-12 z-10 rounded-full bg-green-500 px-4 py-1.5 flex items-center gap-2 text-sm font-bold text-white shadow-md border-2 border-white",children:[r("span",{className:"h-2.5 w-2.5 rounded-full bg-white animate-pulse"}),"Đã ghi âm"]}),r("div",{className:"flex-1 flex justify-center items-center min-h-0 w-full",children:r("div",E?{className:"flex justify-center items-center w-full max-w-[60%] h-full max-h-[300px] border-4 border-dashed border-gray-300 rounded-xl bg-gray-50/50",children:r("div",{className:"animate-spin rounded-full h-12 w-12 border-4 border-blue-500 border-t-transparent"})}:C?{className:"bg-white rounded-2xl shadow-[0_4px_15px_rgba(0,0,0,0.08)] p-2 h-full max-w-[90%] flex justify-center items-center",children:r("img",{src:C,alt:"Scene to describe",className:"max-h-full max-w-full object-contain rounded-xl",draggable:!1})}:{className:"flex justify-center items-center text-orange-800 font-semibold opacity-70 w-full h-full max-h-[300px] border-4 border-dashed border-orange-900/30 rounded-xl bg-orange-900/10",children:"Chưa có hình ảnh mô tả"})})]})})})})})}const B=x.teacherVideoUrl?r("div",{className:"fixed bottom-6 left-6 z-40 flex flex-col items-start w-[12vw] min-w-[150px] pointer-events-none",children:e("div",{className:"relative w-full",children:[r("video",{ref:P,src:x.teacherVideoUrl,autoPlay:!x.skipTeacherVideo,playsInline:!0,onEnded:()=>{F(!0),x.onTeacherIntroPlayed?.()},className:"w-full h-auto drop-shadow-[0_4px_16px_rgba(0,0,0,0.2)] transition-opacity duration-700",style:{opacity:S?0:1}}),!k&&"CAMBRIDGE_YLE"!==x.speakingTheme&&r("img",{src:"/speaking/image/teacher_ai_smile.png",alt:"AI Teacher",className:"absolute top-0 left-0 w-full h-auto drop-shadow-[0_4px_16px_rgba(0,0,0,0.2)] transition-opacity duration-700",style:{opacity:S?1:0}})]})}):null;return e("div",{className:"flex flex-col w-full h-full relative",style:{background:"linear-gradient(180deg, #E6F3FF 0%, #FFFFFF 100%)"},children:[e("div",{className:"relative w-full flex justify-between items-start pt-6 px-6 z-20 shrink-0",children:[r("div",{className:"flex-1"}),r("div",{className:"flex-none drop-shadow-md",children:r(n,{partNumber:x.part.partNo,questionCount:x.part.questions.length,partName:x.part.name||`Part ${x.part.partNo}`})}),r("div",{className:"flex-1 flex justify-end mt-2 z-20",children:r(f,{total:x.part.questions.length,currentIndex:y.nav.currentIndex,answeredIds:L,questionIds:V})})]}),e("div",{className:"flex-1 w-full flex flex-col items-center min-h-0 px-4 pb-6 pt-2",children:[_.isBackup&&r("div",{className:"bg-amber-500 text-white px-3 py-1 rounded-md text-xs font-bold border border-white shadow-md z-10 shrink-0 mb-4",children:"Câu hỏi dự phòng"}),_.question&&r("div",{className:"z-10 mx-auto px-8 py-2.5 bg-white backdrop-blur-md rounded-full shadow-[0_2px_10px_rgba(0,0,0,0.05)] border border-slate-100 w-fit shrink-0 mb-4",children:r("h3",{className:"text-[17px] md:text-lg font-bold text-[#343C55] font-['Quicksand'] text-center whitespace-pre-wrap leading-tight",children:_.question})}),_.audioUrl&&r("div",{className:"z-10 mx-auto w-full max-w-[280px] shrink-0 mb-4",children:r(o,{audioUrl:_.audioUrl,onPlayingChange:y.onAudioPlayingChange,autoPlay:!x.waitForTeacherVideo})}),e("div",{className:"flex-1 w-full flex justify-center items-center min-h-[250px] relative mb-6",children:[y.showExistingBadge&&e("div",{className:"absolute right-4 top-0 z-10 rounded-full bg-green-500 px-4 py-1.5 flex items-center gap-2 text-sm font-bold text-white shadow-lg border-2 border-white transform -translate-y-1/2",children:[r("span",{className:"h-2.5 w-2.5 rounded-full bg-white animate-pulse"}),"Đã ghi âm"]}),r("div",E?{className:"flex justify-center items-center w-full max-w-[60%] h-full max-h-[300px] border-4 border-dashed border-gray-300 rounded-xl bg-gray-50/50",children:r("div",{className:"animate-spin rounded-full h-12 w-12 border-4 border-blue-500 border-t-transparent"})}:C?{className:"bg-white rounded-2xl shadow-[0_4px_15px_rgba(0,0,0,0.08)] p-2 h-full max-w-[90%] flex justify-center items-center",children:r("img",{src:C,alt:"Scene to describe",className:"max-h-full max-w-full object-contain rounded-xl",draggable:!1})}:{className:"flex justify-center items-center text-orange-800 font-semibold opacity-70 w-full h-full max-h-[300px] border-4 border-dashed border-orange-900/30 rounded-xl bg-orange-900/10",children:"Chưa có hình ảnh mô tả"})]}),y.recorder&&!x.isReviewMode&&e("div",{className:"shrink-0 flex flex-col items-center z-50 gap-3",children:[r("div",{className:"bg-white rounded-[100px] shadow-[0_4px_10px_rgba(0,0,0,0.1)] whitespace-nowrap flex items-center justify-center",style:{padding:"10px 30px",color:"recording"===y.recorder.status?"#FF4D4D":["recorded","uploading","uploaded"].includes(y.recorder.status)?"#16CA3B":"#343C55",fontSize:"18px",fontFamily:'"Quicksand", sans-serif',fontWeight:700,lineHeight:"100%",letterSpacing:"-0.01em"},children:"recording"===y.recorder.status?"Ấn lần nữa để lưu kết quả":["recorded","uploading","uploaded"].includes(y.recorder.status)?"Câu trả lời đã được ghi nhận !":"Ấn biểu tượng Micro để trả lời"}),e("div",{className:"relative flex flex-col items-center",children:["recording"===y.recorder.status?e("div",{className:"relative flex flex-col justify-center items-center cursor-pointer drop-shadow-[0_0_15px_rgba(255,100,100,0.8)] transition-transform hover:scale-[1.05] active:scale-[0.95]",onClick:y.recorder.stopRecording,children:[r(c,{src:h,alt:"stop recording",className:"animate-pulse",style:{width:"120px",height:"120px"}}),e("span",{className:"absolute text-white text-[16px] font-bold font-['Quicksand']",style:{bottom:"30px"},children:[Math.floor(y.recorder.remainingSeconds/60),":",(y.recorder.remainingSeconds%60).toString().padStart(2,"0")]})]}):r(c,{src:["recorded","uploading","uploaded"].includes(y.recorder.status)?T?g:p:u,alt:"mic",className:"transition-transform cursor-pointer hover:scale-[1.05] active:scale-[0.95]",style:{width:"120px",height:"120px"},onClick:()=>{if("recording"===y.recorder?.status)y.recorder.stopRecording();else if("idle"===y.recorder?.status||"error"===y.recorder?.status)y.recorder.startRecording();else if("uploaded"===y.recorder?.status||"recorded"===y.recorder?.status){const e=I.current;e&&(T?e.pause():e.play().catch(console.error))}}}),y.recorder?.previewUrl&&r("audio",{ref:I,src:y.recorder.previewUrl}),"recording"===y.recorder.status&&r("div",{className:"absolute whitespace-nowrap text-center",style:{top:"100%",marginTop:"16px",color:"#FF4D4D",fontSize:"18px",fontFamily:'"Quicksand", sans-serif',fontWeight:700,lineHeight:"100%",letterSpacing:"-0.01em",textShadow:"0 1px 3px rgba(0,0,0,0.15)"},children:"Đang ghi âm câu hỏi . . ."}),["recorded","uploading","uploaded"].includes(y.recorder.status)&&r("div",{className:"absolute whitespace-nowrap text-center flex flex-col items-center",style:{top:"100%",marginTop:"16px",color:"#343C55",fontSize:"18px",fontFamily:'"Quicksand", sans-serif',fontWeight:700,lineHeight:"100%",letterSpacing:"-0.01em"},children:T?`${w(j)}/${A?w(A):"--:--"}`:"Nghe đáp án"})]})]})]}),B]})}
1
+ "use client";import{jsxs as e,jsx as r}from"react/jsx-runtime";import{usePresignedFileUrl as t}from"../../../../../../shared/lib/hooks";import{useSpeakingRendererSetup as a}from"../../../hooks/useSpeakingRendererSetup";import n from"../../../../../../components/themes/cambridge-yle/CambridgeYlePartBanner";import{getSpeakingLayout as i}from"../../../../../../components/themes";import{SpeakingAudioPlayer as o}from"../../../../../../components/shared/audio/SpeakingAudioPlayer";import{useEffect as s,useRef as d,useState as l}from"react";import c from"next/image";import{useExamTakingStore as m}from"../../../../../../shared/lib/stores/examTakingStore";import u from"../../../../../../components/themes/summer-sky/assets/images/mic_ver2_icon.png";import h from"../../../../../../components/themes/summer-sky/assets/images/stop_audio_ver2_icon.png";import p from"../../../../../../components/themes/summer-sky/assets/images/audio_ver2_icon.png";import g from"../../../../../../components/themes/summer-sky/assets/images/pause_audio_ver2_icon.png";import x from"../../../../../../components/themes/summer-sky/assets/images/lock_mic_ver2_icon.png";function f({total:t,currentIndex:a,answeredIds:n,questionIds:i}){return e("div",{className:"flex items-center gap-2 font-bold text-[#78350f] font-['Quicksand'] z-10",children:[e("span",{className:"text-sm",children:["Question ",a+1,"/ ",t]}),r("div",{className:"flex gap-1.5",children:Array.from({length:t},(e,t)=>{const o=t===a,s=n.has(i[t]);return r("div",{className:"w-2.5 h-2.5 rounded-full "+(o?"bg-[#FF9900] scale-125 shadow-sm":s?"bg-[#16CA3B]":"bg-white/60")},t)})})]})}const b=new Set(["/speaking/video/read_displayed_content.webm","/speaking/video/speaking_describe_image.webm","/speaking/video/spontaneous_qa.webm"]);function w(e){const r=Math.floor(e/60),t=Math.floor(e%60);return`${r.toString().padStart(2,"0")}:${t.toString().padStart(2,"0")}`}export function SpeakingDescribeImageRenderer(x){const{currentQuestionIndex:v}=x,y=a({...x,maxDurationMs:6e4,currentQuestionIndex:v}),N=m(e=>e.examTitle),k=N?.toLowerCase().includes("placement test")||!1,_=y.currentQuestion?.content,{previewUrl:C,isLoading:E}=t(_?.imageUrl||""),P=d(null),[S,F]=l(!!x.skipTeacherVideo),I=d(null),[T,U]=l(!1),[j,q]=l(0),[A,z]=l(0);if(s(()=>{const e=P.current;e&&x.teacherVideoUrl&&(e.playbackRate=b.has(x.teacherVideoUrl)?1.25:1)},[x.teacherVideoUrl]),s(()=>{const e=I.current;if(!e)return;const r=()=>{U(!0),y.onAudioPlayingChange?.(!0)},t=()=>{U(!1),y.onAudioPlayingChange?.(!1)},a=()=>{U(!1),q(0),y.onAudioPlayingChange?.(!1)},n=()=>{q(e.currentTime),e.duration&&isFinite(e.duration)&&z(e.duration)},i=()=>{e.duration&&isFinite(e.duration)&&z(e.duration)};return e.addEventListener("play",r),e.addEventListener("pause",t),e.addEventListener("ended",a),e.addEventListener("timeupdate",n),e.addEventListener("loadedmetadata",i),e.addEventListener("durationchange",i),()=>{e.removeEventListener("play",r),e.removeEventListener("pause",t),e.removeEventListener("ended",a),e.removeEventListener("timeupdate",n),e.removeEventListener("loadedmetadata",i),e.removeEventListener("durationchange",i),y.onAudioPlayingChange?.(!1)}},[y.recorder?.previewUrl,y.onAudioPlayingChange]),!y.currentQuestion||!_)return null;const L=new Set(x.part.questions.map(e=>e.id).filter(e=>!!x.answers[e])),V=x.part.questions.map(e=>e.id);if(x.speakingTheme&&"CAMBRIDGE_YLE"!==x.speakingTheme){const t=i(x.speakingTheme),a=r(f,{total:x.part.questions.length,currentIndex:y.nav.currentIndex,answeredIds:L,questionIds:V});return r(t,{onExit:x.onExit,partName:x.part.name||`Part ${x.part.partNo}`,instruction:x.part.instructions||"DESCRIBE THE IMAGE",audioUrl:"",questionNumber:y.nav.questionNumberInPart,onPrev:y.handlePrev,onNext:y.handleNext,showPrev:y.nav.showPrev,showNext:y.nav.showNext,canGoPrev:y.nav.canGoPrev,canGoNext:y.nav.canGoNext,onAudioPlayingChange:y.onAudioPlayingChange,recorder:x.isReviewMode?null:y.recorder,showExistingBadge:y.showExistingBadge,headerRight:a,teacherVideoUrl:x.teacherVideoUrl,waitForTeacherVideo:x.waitForTeacherVideo,onTeacherVideoEnded:x.onTeacherIntroPlayed,skipTeacherVideo:x.skipTeacherVideo,children:r("div",{className:"w-full h-full flex flex-col items-center relative",children:r("div",{className:"w-full flex-1",style:{minHeight:0},children:r("div",{className:"mx-auto",style:{width:"900px",maxWidth:"95%",height:"calc(100% - 40px)"},children:e("div",{className:"w-full h-full flex flex-col items-center justify-center",style:{paddingTop:"10px"},children:[_.isBackup&&r("div",{className:"bg-amber-500 text-white px-3 py-1 rounded-md text-xs font-bold border border-white shadow-md z-10 shrink-0 mb-4",children:"Câu hỏi dự phòng"}),_.question&&r("div",{className:"z-10 mx-auto px-8 py-2.5 bg-white/80 backdrop-blur-md rounded-full shadow-sm border border-slate-100 w-fit shrink-0 mb-4",children:r("h3",{className:"text-[17px] md:text-lg font-bold text-[#343C55] font-['Quicksand'] text-center whitespace-pre-wrap leading-tight",children:_.question})}),_.audioUrl&&r("div",{className:"z-10 mx-auto w-full max-w-[280px] shrink-0 mb-4",children:r(o,{audioUrl:_.audioUrl,onPlayingChange:y.onAudioPlayingChange,autoPlay:!x.waitForTeacherVideo})}),y.showExistingBadge&&e("div",{className:"absolute right-12 top-12 z-10 rounded-full bg-green-500 px-4 py-1.5 flex items-center gap-2 text-sm font-bold text-white shadow-md border-2 border-white",children:[r("span",{className:"h-2.5 w-2.5 rounded-full bg-white animate-pulse"}),"Đã ghi âm"]}),r("div",{className:"flex-1 flex justify-center items-center min-h-0 w-full",children:r("div",E?{className:"flex justify-center items-center w-full max-w-[60%] h-full max-h-[300px] border-4 border-dashed border-gray-300 rounded-xl bg-gray-50/50",children:r("div",{className:"animate-spin rounded-full h-12 w-12 border-4 border-blue-500 border-t-transparent"})}:C?{className:"bg-white rounded-2xl shadow-[0_4px_15px_rgba(0,0,0,0.08)] p-2 h-full max-w-[90%] flex justify-center items-center",children:r("img",{src:C,alt:"Scene to describe",className:"max-h-full max-w-full object-contain rounded-xl",draggable:!1})}:{className:"flex justify-center items-center text-orange-800 font-semibold opacity-70 w-full h-full max-h-[300px] border-4 border-dashed border-orange-900/30 rounded-xl bg-orange-900/10",children:"Chưa có hình ảnh mô tả"})})]})})})})})}const B=x.teacherVideoUrl?r("div",{className:"fixed bottom-6 left-6 z-40 flex flex-col items-start w-[12vw] min-w-[150px] pointer-events-none",children:e("div",{className:"relative w-full",children:[r("video",{ref:P,src:x.teacherVideoUrl,autoPlay:!x.skipTeacherVideo,playsInline:!0,onEnded:()=>{F(!0),x.onTeacherIntroPlayed?.()},className:"w-full h-auto drop-shadow-[0_4px_16px_rgba(0,0,0,0.2)] transition-opacity duration-700",style:{opacity:S?0:1}}),!k&&"CAMBRIDGE_YLE"!==x.speakingTheme&&r("img",{src:"/speaking/image/teacher_ai_smile.png",alt:"AI Teacher",className:"absolute top-0 left-0 w-full h-auto drop-shadow-[0_4px_16px_rgba(0,0,0,0.2)] transition-opacity duration-700",style:{opacity:S?1:0}})]})}):null;return e("div",{className:"flex flex-col w-full h-full relative",style:{background:"linear-gradient(180deg, #E6F3FF 0%, #FFFFFF 100%)"},children:[e("div",{className:"relative w-full flex justify-between items-start pt-6 px-6 z-20 shrink-0",children:[r("div",{className:"flex-1"}),r("div",{className:"flex-none drop-shadow-md",children:r(n,{partNumber:x.part.partNo,questionCount:x.part.questions.length,partName:x.part.name||`Part ${x.part.partNo}`})}),r("div",{className:"flex-1 flex justify-end mt-2 z-20",children:r(f,{total:x.part.questions.length,currentIndex:y.nav.currentIndex,answeredIds:L,questionIds:V})})]}),e("div",{className:"flex-1 w-full flex flex-col items-center min-h-0 px-4 pb-6 pt-2",children:[_.isBackup&&r("div",{className:"bg-amber-500 text-white px-3 py-1 rounded-md text-xs font-bold border border-white shadow-md z-10 shrink-0 mb-4",children:"Câu hỏi dự phòng"}),_.question&&r("div",{className:"z-10 mx-auto px-8 py-2.5 bg-white backdrop-blur-md rounded-full shadow-[0_2px_10px_rgba(0,0,0,0.05)] border border-slate-100 w-fit shrink-0 mb-4",children:r("h3",{className:"text-[17px] md:text-lg font-bold text-[#343C55] font-['Quicksand'] text-center whitespace-pre-wrap leading-tight",children:_.question})}),_.audioUrl&&r("div",{className:"z-10 mx-auto w-full max-w-[280px] shrink-0 mb-4",children:r(o,{audioUrl:_.audioUrl,onPlayingChange:y.onAudioPlayingChange,autoPlay:!x.waitForTeacherVideo})}),e("div",{className:"flex-1 w-full flex justify-center items-center min-h-[320px] relative mb-4",children:[y.showExistingBadge&&e("div",{className:"absolute right-4 top-0 z-10 rounded-full bg-green-500 px-4 py-1.5 flex items-center gap-2 text-sm font-bold text-white shadow-lg border-2 border-white transform -translate-y-1/2",children:[r("span",{className:"h-2.5 w-2.5 rounded-full bg-white animate-pulse"}),"Đã ghi âm"]}),r("div",E?{className:"flex justify-center items-center w-full max-w-[60%] h-full max-h-[300px] border-4 border-dashed border-gray-300 rounded-xl bg-gray-50/50",children:r("div",{className:"animate-spin rounded-full h-12 w-12 border-4 border-blue-500 border-t-transparent"})}:C?{className:"bg-white rounded-2xl shadow-[0_4px_15px_rgba(0,0,0,0.08)] p-2 h-full max-w-[92%] flex justify-center items-center",children:r("img",{src:C,alt:"Scene to describe",className:"h-full w-auto max-w-full object-contain rounded-xl",draggable:!1})}:{className:"flex justify-center items-center text-orange-800 font-semibold opacity-70 w-full h-full max-h-[300px] border-4 border-dashed border-orange-900/30 rounded-xl bg-orange-900/10",children:"Chưa có hình ảnh mô tả"})]}),y.recorder&&!x.isReviewMode&&e("div",{className:"shrink-0 flex flex-col items-center z-50 gap-3",children:[r("div",{className:"bg-white rounded-[100px] shadow-[0_4px_10px_rgba(0,0,0,0.1)] whitespace-nowrap flex items-center justify-center",style:{padding:"10px 30px",color:"recording"===y.recorder.status?"#FF4D4D":["recorded","uploading","uploaded"].includes(y.recorder.status)?"#16CA3B":"#343C55",fontSize:"18px",fontFamily:'"Quicksand", sans-serif',fontWeight:700,lineHeight:"100%",letterSpacing:"-0.01em"},children:"recording"===y.recorder.status?"Ấn lần nữa để lưu kết quả":["recorded","uploading","uploaded"].includes(y.recorder.status)?"Câu trả lời đã được ghi nhận !":"Ấn biểu tượng Micro để trả lời"}),e("div",{className:"relative flex flex-col items-center",children:["recording"===y.recorder.status?e("div",{className:"relative flex flex-col justify-center items-center cursor-pointer drop-shadow-[0_0_15px_rgba(255,100,100,0.8)] transition-transform hover:scale-[1.05] active:scale-[0.95]",onClick:y.recorder.stopRecording,children:[r(c,{src:h,alt:"stop recording",className:"animate-pulse",style:{width:"120px",height:"120px"}}),e("span",{className:"absolute text-white text-[16px] font-bold font-['Quicksand']",style:{bottom:"30px"},children:[Math.floor(y.recorder.remainingSeconds/60),":",(y.recorder.remainingSeconds%60).toString().padStart(2,"0")]})]}):r(c,{src:["recorded","uploading","uploaded"].includes(y.recorder.status)?T?g:p:u,alt:"mic",className:"transition-transform cursor-pointer hover:scale-[1.05] active:scale-[0.95]",style:{width:"120px",height:"120px"},onClick:()=>{if("recording"===y.recorder?.status)y.recorder.stopRecording();else if("idle"===y.recorder?.status||"error"===y.recorder?.status)y.recorder.startRecording();else if("uploaded"===y.recorder?.status||"recorded"===y.recorder?.status){const e=I.current;e&&(T?e.pause():e.play().catch(console.error))}}}),y.recorder?.previewUrl&&r("audio",{ref:I,src:y.recorder.previewUrl}),"recording"===y.recorder.status&&r("div",{className:"absolute whitespace-nowrap text-center",style:{top:"100%",marginTop:"16px",color:"#FF4D4D",fontSize:"18px",fontFamily:'"Quicksand", sans-serif',fontWeight:700,lineHeight:"100%",letterSpacing:"-0.01em",textShadow:"0 1px 3px rgba(0,0,0,0.15)"},children:"Đang ghi âm câu hỏi . . ."}),["recorded","uploading","uploaded"].includes(y.recorder.status)&&r("div",{className:"absolute whitespace-nowrap text-center flex flex-col items-center",style:{top:"100%",marginTop:"16px",color:"#343C55",fontSize:"18px",fontFamily:'"Quicksand", sans-serif',fontWeight:700,lineHeight:"100%",letterSpacing:"-0.01em"},children:T?`${w(j)}/${A?w(A):"--:--"}`:"Nghe đáp án"})]})]})]}),B]})}
@@ -1,20 +1,3 @@
1
- /**
2
- * SpeakingReadDisplayedContentRenderer
3
- *
4
- * Renderer for READ_DISPLAYED_CONTENT (English Primary & Junior Speaking Test — Part 1).
5
- *
6
- * Data shape:
7
- * - content.referenceText: the passage the student must read aloud
8
- * - content.modelAudioUrl: optional model pronunciation audio
9
- * - content.difficultyTag: optional tag (BASIC_PHONICS | DAILY_ROUTINE | DESCRIPTIVE_EMOTIONAL)
10
- *
11
- * Interaction:
12
- * - Student sees the reference text prominently displayed
13
- * - Optionally listens to model pronunciation
14
- * - Records themselves reading the passage aloud
15
- * - Answer = S3 audio key from recorder
16
- * - Backend AI compares recorded audio to referenceText using Phonetic Distance
17
- */
18
1
  import type { QuestionPart } from '../../../utils/question-transformers';
19
2
  interface SpeakingReadDisplayedContentRendererProps {
20
3
  part: QuestionPart;
@@ -1 +1 @@
1
- "use client";import{jsxs as e,jsx as t}from"react/jsx-runtime";import{useSpeakingRendererSetup as i}from"../../../hooks/useSpeakingRendererSetup";import{getSpeakingLayout as n}from"../../../../../../components/themes";import a from"../SpeakingReadDisplayedContent.module.css";import{SpeakingAudioPlayer as r}from"../../../../../../components/shared/audio/SpeakingAudioPlayer";function s(e){switch(e){case"BASIC_PHONICS":return a.difficultyBasicPhonics;case"DAILY_ROUTINE":return a.difficultyDailyRoutine;case"DESCRIPTIVE_EMOTIONAL":return a.difficultyDescriptiveEmotional;default:return""}}function l(e){switch(e){case"BASIC_PHONICS":return"📖 Basic Phonics";case"DAILY_ROUTINE":return"🕐 Daily Routine & Time";case"DESCRIPTIVE_EMOTIONAL":return"🌄 Descriptive & Emotional";default:return""}}function o({total:i,currentIndex:n,answeredIds:r,questionIds:s,isSpaceship:l}){return e("div",{className:a.taskIndicator,style:l?{color:"#78350f",textShadow:"none",fontWeight:800}:void 0,children:[e("span",{children:["Question ",n+1,"/ ",i]}),t("div",{className:"flex gap-2",children:Array.from({length:i},(e,i)=>{const l=i===n,o=r.has(s[i]);return t("div",{className:`${a.taskDot} ${l?a.taskDotActive:""} ${o&&!l?a.taskDotCompleted:""}`},i)})})]})}export function SpeakingReadDisplayedContentRenderer(d){const c=i({...d,maxDurationMs:3e4}),u=n(d.speakingTheme),h="SPACESHIP"===d.speakingTheme||"SUMMER_SKY"===d.speakingTheme,m=c.currentQuestion?.content;if(!c.currentQuestion||!m)return null;const p=m.referenceText??"",f=m.modelAudioUrl??"",x=m.difficultyTag,g=new Set(d.part.questions.map(e=>e.id).filter(e=>!!d.answers[e])),w=d.part.questions.map(e=>e.id),N=t(o,{total:d.part.questions.length,currentIndex:c.nav.currentIndex,answeredIds:g,questionIds:w,isSpaceship:h}),v=f?e("div",{className:a.modelAudioSection,children:[t("span",{className:a.modelAudioLabel,children:"🔊 Model pronunciation:"}),t(r,{audioUrl:f})]}):null;return t(u,{onExit:d.onExit,partName:d.part.name||`Part ${d.part.partNo}`,instruction:d.part.instructions||"READ THE PASSAGE ALOUD",audioUrl:h&&f||"",questionNumber:c.nav.questionNumberInPart,onPrev:c.handlePrev,onNext:c.handleNext,showPrev:c.nav.showPrev,showNext:c.nav.showNext,canGoPrev:c.nav.canGoPrev,canGoNext:c.nav.canGoNext,onAudioPlayingChange:c.onAudioPlayingChange,recorder:d.isReviewMode?null:c.recorder,showExistingBadge:c.showExistingBadge,headerRight:N,audioBarRight:h?null:v,teacherVideoUrl:d.teacherVideoUrl,waitForTeacherVideo:d.waitForTeacherVideo,onTeacherVideoEnded:d.onTeacherIntroPlayed,skipTeacherVideo:d.skipTeacherVideo,children:h?t("div",{className:"w-full h-full flex flex-col items-center relative",children:t("div",{className:"w-full flex-1",style:{minHeight:0},children:t("div",{className:"mx-auto",style:{width:"900px",maxWidth:"95%",height:"calc(100% - 100px)"},children:t("div",{className:"w-full h-full flex flex-col items-center justify-center",style:{paddingTop:"10px"},children:e("div",{className:"relative flex flex-col justify-center items-center",style:{width:"100%",paddingLeft:"60px",paddingRight:"60px",paddingTop:"20px",paddingBottom:"20px"},children:[c.showExistingBadge&&e("div",{className:"absolute right-12 top-12 z-10 rounded-full bg-green-500 px-4 py-1.5 flex items-center gap-2 text-sm font-bold text-white shadow-md border-2 border-white",children:[t("span",{className:"h-2.5 w-2.5 rounded-full bg-white animate-pulse"}),"Đã ghi âm"]}),t("div",{className:"max-w-full",children:t("div",{className:"text-center max-w-full",style:{fontSize:"40px",fontFamily:'"SVN-Volte Rounded", sans-serif',fontWeight:600,lineHeight:"52px",letterSpacing:"-0.01em",color:"#2C38B6"},children:t("p",{className:"m-0 break-words whitespace-pre-wrap",children:`"${p}"`})})})]})})})})}):e("div",{className:"flex flex-col items-center justify-center h-full w-full gap-4 px-4",children:[x&&t("div",{className:`${a.difficultyBadge} ${s(x)}`,children:l(x)}),e("div",{className:a.referenceTextCard,children:[c.showExistingBadge&&e("div",{className:"absolute right-4 top-4 z-10 rounded-full bg-green-500 px-4 py-1 flex items-center gap-2 text-sm font-bold text-white shadow-md border-2 border-white",children:[t("span",{className:"h-2.5 w-2.5 rounded-full bg-white animate-pulse"}),"Đã ghi âm"]}),t("p",{className:a.referenceText,children:`"${p}"`})]})]})})}
1
+ "use client";import{jsxs as e,jsx as r}from"react/jsx-runtime";import{useEffect as t,useRef as i,useState as n}from"react";import{useSpeakingRendererSetup as s}from"../../../hooks/useSpeakingRendererSetup";import{getSpeakingLayout as a}from"../../../../../../components/themes";import l from"../../../../../../components/themes/cambridge-yle/CambridgeYlePartBanner";import o from"next/image";import d from"../../../../../../components/themes/summer-sky/assets/images/mic_ver2_icon.png";import c from"../../../../../../components/themes/summer-sky/assets/images/stop_audio_ver2_icon.png";import m from"../../../../../../components/themes/summer-sky/assets/images/audio_ver2_icon.png";import u from"../../../../../../components/themes/summer-sky/assets/images/pause_audio_ver2_icon.png";import p from"../SpeakingReadDisplayedContent.module.css";import{SpeakingAudioPlayer as h}from"../../../../../../components/shared/audio/SpeakingAudioPlayer";function f(e){switch(e){case"BASIC_PHONICS":return p.difficultyBasicPhonics;case"DAILY_ROUTINE":return p.difficultyDailyRoutine;case"DESCRIPTIVE_EMOTIONAL":return p.difficultyDescriptiveEmotional;default:return""}}function x(e){switch(e){case"BASIC_PHONICS":return"📖 Basic Phonics";case"DAILY_ROUTINE":return"🕐 Daily Routine & Time";case"DESCRIPTIVE_EMOTIONAL":return"🌄 Descriptive & Emotional";default:return""}}function g({total:t,currentIndex:i,answeredIds:n,questionIds:s,isSpaceship:a}){return e("div",{className:p.taskIndicator,style:a?{color:"#78350f",textShadow:"none",fontWeight:800}:void 0,children:[e("span",{children:["Question ",i+1,"/ ",t]}),r("div",{className:"flex gap-2",children:Array.from({length:t},(e,t)=>{const a=t===i,l=n.has(s[t]);return r("div",{className:`${p.taskDot} ${a?p.taskDotActive:""} ${l&&!a?p.taskDotCompleted:""}`},t)})})]})}function v({total:t,currentIndex:i,answeredIds:n,questionIds:s}){return e("div",{className:"flex items-center gap-2 font-bold text-[#78350f] font-['Quicksand'] z-10",children:[e("span",{className:"text-sm",children:["Question ",i+1,"/ ",t]}),r("div",{className:"flex gap-1.5",children:Array.from({length:t},(e,t)=>{const a=t===i,l=n.has(s[t]);return r("div",{className:"w-2.5 h-2.5 rounded-full "+(a?"bg-[#FF9900] scale-125 shadow-sm":l?"bg-[#16CA3B]":"bg-white/60")},t)})})]})}export function SpeakingReadDisplayedContentRenderer(w){const N=s({...w,maxDurationMs:3e4}),y=i(null),[b,k]=n(!1);t(()=>{const e=y.current;if(!e)return;const r=()=>{k(!0),N.onAudioPlayingChange?.(!0)},t=()=>{k(!1),N.onAudioPlayingChange?.(!1)},i=()=>{k(!1),N.onAudioPlayingChange?.(!1)};return e.addEventListener("play",r),e.addEventListener("pause",t),e.addEventListener("ended",i),()=>{e.removeEventListener("play",r),e.removeEventListener("pause",t),e.removeEventListener("ended",i),N.onAudioPlayingChange?.(!1)}},[N.recorder?.previewUrl,N.onAudioPlayingChange]);const S=a(w.speakingTheme),E="SPACESHIP"===w.speakingTheme||"SUMMER_SKY"===w.speakingTheme,I=N.currentQuestion?.content;if(!N.currentQuestion||!I)return null;const C=I.referenceText??"",A=I.modelAudioUrl??"",P=I.difficultyTag,_=C.length>700?"clamp(15px, 1.6vw, 19px)":C.length>400?"clamp(18px, 2vw, 24px)":"clamp(22px, 2.2vw, 30px)",T=new Set(w.part.questions.map(e=>e.id).filter(e=>!!w.answers[e])),F=w.part.questions.map(e=>e.id);if(w.speakingTheme&&"CAMBRIDGE_YLE"!==w.speakingTheme){const t=r(g,{total:w.part.questions.length,currentIndex:N.nav.currentIndex,answeredIds:T,questionIds:F,isSpaceship:E}),i=A?e("div",{className:p.modelAudioSection,children:[r("span",{className:p.modelAudioLabel,children:"🔊 Model pronunciation:"}),r(h,{audioUrl:A})]}):null;return r(S,{onExit:w.onExit,partName:w.part.name||`Part ${w.part.partNo}`,instruction:w.part.instructions||"READ THE PASSAGE ALOUD",audioUrl:E&&A||"",questionNumber:N.nav.questionNumberInPart,onPrev:N.handlePrev,onNext:N.handleNext,showPrev:N.nav.showPrev,showNext:N.nav.showNext,canGoPrev:N.nav.canGoPrev,canGoNext:N.nav.canGoNext,onAudioPlayingChange:N.onAudioPlayingChange,recorder:w.isReviewMode?null:N.recorder,showExistingBadge:N.showExistingBadge,headerRight:t,audioBarRight:E?null:i,teacherVideoUrl:w.teacherVideoUrl,waitForTeacherVideo:w.waitForTeacherVideo,onTeacherVideoEnded:w.onTeacherIntroPlayed,skipTeacherVideo:w.skipTeacherVideo,children:E?r("div",{className:"w-full h-full flex flex-col items-center relative",children:r("div",{className:"w-full flex-1",style:{minHeight:0},children:r("div",{className:"mx-auto",style:{width:"900px",maxWidth:"95%",height:"calc(100% - 100px)"},children:r("div",{className:"w-full h-full flex flex-col items-center justify-center",style:{paddingTop:"10px"},children:e("div",{className:"relative flex flex-col justify-center items-center",style:{width:"100%",paddingLeft:"60px",paddingRight:"60px",paddingTop:"20px",paddingBottom:"20px"},children:[N.showExistingBadge&&e("div",{className:"absolute right-12 top-12 z-10 rounded-full bg-green-500 px-4 py-1.5 flex items-center gap-2 text-sm font-bold text-white shadow-md border-2 border-white",children:[r("span",{className:"h-2.5 w-2.5 rounded-full bg-white animate-pulse"}),"Đã ghi âm"]}),r("div",{className:"max-w-full",children:r("div",{className:"text-center max-w-full",style:{fontSize:"40px",fontFamily:'"SVN-Volte Rounded", sans-serif',fontWeight:600,lineHeight:"52px",letterSpacing:"-0.01em",color:"#2C38B6"},children:r("p",{className:"m-0 break-words whitespace-pre-wrap",children:`"${C}"`})})})]})})})})}):e("div",{className:"flex flex-col items-center justify-center h-full w-full gap-4 px-4",children:[P&&r("div",{className:`${p.difficultyBadge} ${f(P)}`,children:x(P)}),e("div",{className:p.referenceTextCard,children:[N.showExistingBadge&&e("div",{className:"absolute right-4 top-4 z-10 rounded-full bg-green-500 px-4 py-1 flex items-center gap-2 text-sm font-bold text-white shadow-md border-2 border-white",children:[r("span",{className:"h-2.5 w-2.5 rounded-full bg-white animate-pulse"}),"Đã ghi âm"]}),r("p",{className:p.referenceText,children:`"${C}"`})]})]})})}return e("div",{className:"flex flex-col w-full h-full relative",style:{background:"linear-gradient(180deg, #E6F3FF 0%, #FFFFFF 100%)"},children:[e("div",{className:"relative w-full flex justify-between items-start pt-6 px-6 z-20 shrink-0",children:[r("div",{className:"flex-1"}),r("div",{className:"flex-none drop-shadow-md",children:r(l,{partNumber:w.part.partNo,questionCount:w.part.questions.length,partName:w.part.name||`Part ${w.part.partNo}`})}),r("div",{className:"flex-1 flex justify-end mt-2 z-20",children:r(v,{total:w.part.questions.length,currentIndex:N.nav.currentIndex,answeredIds:T,questionIds:F})})]}),e("div",{className:"flex-1 w-full flex flex-col items-center min-h-0 px-4 pb-6 pt-5",children:[w.part.instructions&&e("div",{className:"shrink-0 w-full max-w-[760px] flex items-center gap-3 rounded-2xl px-5 py-3 mb-4",style:{background:"rgba(255, 255, 255, 0.9)",border:"2px solid #FCD34D",boxShadow:"0 3px 12px rgba(180, 130, 20, 0.12)"},children:[r("div",{className:"shrink-0 flex items-center justify-center rounded-full",style:{width:42,height:42,background:"#FEF3C7"},children:r("span",{style:{fontSize:22,lineHeight:1},children:"💡"})}),r("div",{className:"flex flex-col text-left min-w-0",children:r("p",{className:"m-0",style:{color:"#78350F",fontSize:16,fontWeight:600,lineHeight:1.35,fontFamily:'"Quicksand", sans-serif'},children:w.part.instructions})})]}),P&&r("div",{className:"shrink-0 mb-3",children:r("div",{className:`${p.difficultyBadge} ${f(P)}`,children:x(P)})}),r("div",{className:"flex-1 w-full flex flex-col items-center justify-center min-h-0 relative mb-8",children:e("div",{className:"w-full max-w-[850px] max-h-full relative bg-white/60 backdrop-blur-md rounded-2xl shadow-[0_2px_15px_rgba(0,0,0,0.04)] border border-blue-50 p-8 flex flex-col min-h-0",children:[N.showExistingBadge&&e("div",{className:"absolute right-4 top-0 z-10 rounded-full bg-green-500 px-4 py-1.5 flex items-center gap-2 text-sm font-bold text-white shadow-lg border-2 border-white transform -translate-y-1/2",children:[r("span",{className:"h-2.5 w-2.5 rounded-full bg-white animate-pulse"}),"Đã ghi âm"]}),r("div",{className:"w-full min-h-0 overflow-y-auto",children:r("p",{className:"text-center break-words whitespace-pre-wrap m-0",style:{fontSize:_,fontFamily:'"SVN-Volte Rounded", "Quicksand", sans-serif',fontWeight:600,lineHeight:1.4,letterSpacing:"-0.01em",color:"#2C38B6"},children:`"${C}"`})}),A&&e("div",{className:"mt-6 flex items-center justify-center gap-2 shrink-0",children:[r("span",{className:"text-sm font-semibold text-[#283593] font-['Quicksand']",children:"🔊 Model pronunciation:"}),r(h,{audioUrl:A})]})]})}),N.recorder&&!w.isReviewMode&&e("div",{className:"shrink-0 flex flex-col items-center z-50 gap-3",children:[r("div",{className:"bg-white rounded-[100px] shadow-[0_4px_10px_rgba(0,0,0,0.1)] whitespace-nowrap flex items-center justify-center",style:{padding:"10px 30px",color:"recording"===N.recorder.status?"#FF4D4D":["recorded","uploading","uploaded"].includes(N.recorder.status)?"#16CA3B":"#343C55",fontSize:"18px",fontFamily:'"Quicksand", sans-serif',fontWeight:700,lineHeight:"100%",letterSpacing:"-0.01em"},children:"recording"===N.recorder.status?"Ấn lần nữa để lưu kết quả":["recorded","uploading","uploaded"].includes(N.recorder.status)?"Câu trả lời đã được ghi nhận !":"Ấn biểu tượng Micro để trả lời"}),r("div",{className:"relative flex flex-col items-center",children:"recording"===N.recorder.status?e("div",{className:"relative flex flex-col justify-center items-center cursor-pointer drop-shadow-[0_0_15px_rgba(255,100,100,0.8)] transition-transform hover:scale-[1.05] active:scale-[0.95]",onClick:N.recorder.stopRecording,children:[r(o,{src:c,alt:"stop audio",className:"animate-pulse",style:{width:"120px",height:"120px"}}),e("span",{className:"absolute text-white text-[16px] font-bold font-['Quicksand']",style:{bottom:"30px"},children:[Math.floor(N.recorder.remainingSeconds/60),":",(N.recorder.remainingSeconds%60).toString().padStart(2,"0")]})]}):r(o,{src:["recorded","uploading","uploaded"].includes(N.recorder.status)?b?u:m:d,alt:"micro",className:"transition-transform cursor-pointer hover:scale-[1.05] active:scale-[0.95]",style:{width:"120px",height:"120px"},onClick:()=>{if("recording"===N.recorder?.status)N.recorder.stopRecording();else if("idle"===N.recorder?.status||"error"===N.recorder?.status)N.recorder.startRecording();else if("uploaded"===N.recorder?.status||"recorded"===N.recorder?.status){const e=y.current;e&&(b?e.pause():e.play().catch(console.error))}}})}),N.recorder.previewUrl&&r("audio",{ref:y,src:N.recorder.previewUrl})]})]})]})}
@@ -1 +1 @@
1
- import{useState as r,useRef as e,useEffect as n,useCallback as t}from"react";import{usePresignedFileUrl as u}from"../../../../shared/lib/hooks";import{useExamTakingApi as o}from"../ExamTakingApiContext";export function useSpeakingRecorder(i){const c=i.maxDurationMs??3e4,a=Math.floor(c/1e3),d=o(),s=e(d);s.current=d;const l=e(i.onRecordingComplete),p=e(i.onError);l.current=i.onRecordingComplete,p.current=i.onError;const g=e(i.attemptId),m=e(i.questionId);g.current=i.attemptId,m.current=i.questionId;const[h,y]=r("idle"),[w,f]=r(a),[A,v]=r(0),[R,T]=r(null),[E,x]=r(null),[I,b]=r(null),[K,M]=r(!1),k=e(null),P=e(null),D=e([]),S=e(null),U=e(null),O=e(""),_=e(0),q=e(null),L=e(void 0),B=e(0),V=e(!0),C=e(""),G="undefined"!=typeof MediaRecorder,{previewUrl:j}=u(i.existingAnswerKey),N=t(()=>{S.current&&(clearInterval(S.current),S.current=null)},[]),F=t(()=>{U.current&&(U.current.startsWith("blob:")&&URL.revokeObjectURL(U.current),U.current=null)},[]),W=t(()=>{P.current&&(P.current.getTracks().forEach(r=>r.stop()),P.current=null)},[]),X=t(async(r,e,n,t)=>{if(!V.current)return;const u=t||C.current||m.current;m.current===u&&y("uploading");const o=g.current;if("mock-attempt-speaking"===o){if(await new Promise(r=>setTimeout(r,500)),!V.current)return;const r=`mock-audio-key-${u}`;return m.current===u&&(y("uploaded"),x(r)),l.current(r,u),q.current=null,void(L.current=void 0)}try{if(!s.current.uploadAudioAnswer)throw new Error("uploadAudioAnswer is not implemented by the provided API adapter.");const n=(i=await s.current.uploadAudioAnswer({attemptId:o,questionId:u,audioBlob:r,durationMs:e}))&&"object"==typeof i&&"data"in i?i.data:i;if(!V.current)return;const t=n.audioKey??"";m.current===u&&(y("uploaded"),x(t)),l.current(t,u),q.current=null,L.current=void 0,B.current=0,s.current.realtimeGradeSingle?.({attemptId:o,questionId:u,audioKey:t}).catch(r=>{console.debug("[Realtime Grading] Skipped or failed:",r?.response?.status??r?.message)})}catch(u){if(!V.current)return;const o=u,i=o?.response?.data?.code;if("ATTEMPT_EXPIRED"===i||"ATTEMPT_NOT_IN_PROGRESS"===i){const r="ATTEMPT_EXPIRED"===i?"Bài thi đã hết giờ":"Bài thi đã được nộp.";return y("error"),b(r),M(!0),void p.current?.(r)}if("AUDIO_FILE_TOO_LARGE"===i){const r="Bài ghi âm quá dài. Vui lòng ghi âm ngắn hơn.";return y("error"),b(r),void p.current?.(r)}if(n<3){const u=1e3*Math.pow(2,n);if(await new Promise(r=>setTimeout(r,u)),!V.current)return;B.current=n+1,await X(r,e,n+1,t)}else{const r="Tải lên bản ghi âm thất bại. Vui lòng thử lại.";y("error"),b(r),p.current?.(r)}}var i},[]),z=t(async(r,e,n)=>{q.current=r,L.current=e,B.current=0,await X(r,e,0,n)},[X]),$=t(async()=>{if(i.existingAnswerKey)return y("uploaded"),void x(i.existingAnswerKey);if(!G){const r="Trình duyệt này không hỗ trợ ghi âm. Vui lòng dùng Chrome, Firefox hoặc Edge.";return y("error"),b(r),void p.current?.(r)}const r=function(){if("undefined"==typeof MediaRecorder)return null;const r=["audio/webm;codecs=opus","audio/webm","audio/mp4","audio/ogg;codecs=opus","audio/ogg"];for(const e of r)if(MediaRecorder.isTypeSupported(e))return e;return null}();if(!r){const r="Trình duyệt không hỗ trợ định dạng ghi âm.";return y("error"),b(r),void p.current?.(r)}try{const e=await navigator.mediaDevices.getUserMedia({audio:!0});P.current=e,O.current=r;const n=new MediaRecorder(e,{mimeType:r});k.current=n,D.current=[],n.ondataavailable=r=>{r.data.size>0&&D.current.push(r.data)},n.onstop=()=>{const r=Date.now()-_.current,e=new Blob(D.current,{type:O.current}),n=URL.createObjectURL(e),t=C.current;F(),U.current=n,T(n),y("recorded"),z(e,r,t)},n.start(),_.current=Date.now(),C.current=m.current,y("recording"),v(0),f(a),b(null),x(null),T(null),M(!1),S.current=setInterval(()=>{v(r=>r+1),f(r=>{const e=Math.max(0,r-1);return 0===e&&(N(),k.current&&"recording"===k.current.state&&k.current.stop(),W()),e})},1e3)}catch(r){W();const e=r instanceof DOMException&&("NotAllowedError"===r.name||"PermissionDeniedError"===r.name)?"Vui lòng cấp quyền truy cập microphone trong trình duyệt.":"Không thể truy cập microphone. Vui lòng thử lại.";y("error"),b(e),p.current?.(e)}},[G,a,i.existingAnswerKey,N,W,z,F]),H=t(()=>{N(),k.current&&"recording"===k.current.state&&k.current.stop(),W()},[N,W]),J=t(()=>{N(),W(),k.current&&"recording"===k.current.state&&k.current.stop(),k.current=null,D.current=[],q.current=null,L.current=void 0,B.current=0,F(),T(null),x(null),v(0),f(a),b(null),y("idle"),M(!1)},[N,W,F,a]),Q=t(async()=>{if(i.existingAnswerKey)return y("uploaded"),void x(i.existingAnswerKey);J(),await $()},[i.existingAnswerKey,J,$]),Y=t(async()=>{const r=q.current;r&&(B.current=0,await X(r,L.current,0,C.current||void 0))},[X]);n(()=>{J()},[i.questionId]),n(()=>{"idle"===h&&j&&i.existingAnswerKey&&!R&&(T(j),U.current=j,x(i.existingAnswerKey),y("uploaded"))},[h,j,i.existingAnswerKey,R]),n(()=>(V.current=!0,()=>{V.current=!1,N(),k.current&&"recording"===k.current.state&&k.current.stop(),W(),F()}),[]);return{status:h,remainingSeconds:w,isWarning:w<=5&&"recording"===h,durationSeconds:A,previewUrl:R,uploadedKey:E,error:I,isSupported:G,isAttemptExpired:K,liveTranscript:null,startRecording:$,stopRecording:H,resetRecording:J,reRecord:Q,retryUpload:Y}}
1
+ import{useState as r,useRef as e,useEffect as n,useCallback as t}from"react";import{usePresignedFileUrl as u}from"../../../../shared/lib/hooks";import{useExamTakingApiOptional as o}from"../ExamTakingApiContext";export function useSpeakingRecorder(i){const c=i.maxDurationMs??3e4,a=Math.floor(c/1e3),d=o(),s=e(d);s.current=d;const l=e(i.onRecordingComplete),p=e(i.onError);l.current=i.onRecordingComplete,p.current=i.onError;const g=e(i.attemptId),m=e(i.questionId);g.current=i.attemptId,m.current=i.questionId;const[h,y]=r("idle"),[w,f]=r(a),[A,v]=r(0),[R,T]=r(null),[E,x]=r(null),[I,b]=r(null),[K,M]=r(!1),k=e(null),P=e(null),D=e([]),S=e(null),U=e(null),O=e(""),_=e(0),q=e(null),L=e(void 0),B=e(0),V=e(!0),C=e(""),G="undefined"!=typeof MediaRecorder,{previewUrl:j}=u(i.existingAnswerKey),N=t(()=>{S.current&&(clearInterval(S.current),S.current=null)},[]),F=t(()=>{U.current&&(U.current.startsWith("blob:")&&URL.revokeObjectURL(U.current),U.current=null)},[]),W=t(()=>{P.current&&(P.current.getTracks().forEach(r=>r.stop()),P.current=null)},[]),X=t(async(r,e,n,t)=>{if(!V.current)return;const u=t||C.current||m.current;m.current===u&&y("uploading");const o=g.current;if("mock-attempt-speaking"===o){if(await new Promise(r=>setTimeout(r,500)),!V.current)return;const r=`mock-audio-key-${u}`;return m.current===u&&(y("uploaded"),x(r)),l.current(r,u),q.current=null,void(L.current=void 0)}try{if(!s.current?.uploadAudioAnswer)throw new Error("uploadAudioAnswer is not implemented by the provided API adapter.");const n=(i=await s.current.uploadAudioAnswer({attemptId:o,questionId:u,audioBlob:r,durationMs:e}))&&"object"==typeof i&&"data"in i?i.data:i;if(!V.current)return;const t=n.audioKey??"";m.current===u&&(y("uploaded"),x(t)),l.current(t,u),q.current=null,L.current=void 0,B.current=0,s.current.realtimeGradeSingle?.({attemptId:o,questionId:u,audioKey:t}).catch(r=>{console.debug("[Realtime Grading] Skipped or failed:",r?.response?.status??r?.message)})}catch(u){if(!V.current)return;const o=u,i=o?.response?.data?.code;if("ATTEMPT_EXPIRED"===i||"ATTEMPT_NOT_IN_PROGRESS"===i){const r="ATTEMPT_EXPIRED"===i?"Bài thi đã hết giờ":"Bài thi đã được nộp.";return y("error"),b(r),M(!0),void p.current?.(r)}if("AUDIO_FILE_TOO_LARGE"===i){const r="Bài ghi âm quá dài. Vui lòng ghi âm ngắn hơn.";return y("error"),b(r),void p.current?.(r)}if(n<3){const u=1e3*Math.pow(2,n);if(await new Promise(r=>setTimeout(r,u)),!V.current)return;B.current=n+1,await X(r,e,n+1,t)}else{const r="Tải lên bản ghi âm thất bại. Vui lòng thử lại.";y("error"),b(r),p.current?.(r)}}var i},[]),z=t(async(r,e,n)=>{q.current=r,L.current=e,B.current=0,await X(r,e,0,n)},[X]),$=t(async()=>{if(i.existingAnswerKey)return y("uploaded"),void x(i.existingAnswerKey);if(!G){const r="Trình duyệt này không hỗ trợ ghi âm. Vui lòng dùng Chrome, Firefox hoặc Edge.";return y("error"),b(r),void p.current?.(r)}const r=function(){if("undefined"==typeof MediaRecorder)return null;const r=["audio/webm;codecs=opus","audio/webm","audio/mp4","audio/ogg;codecs=opus","audio/ogg"];for(const e of r)if(MediaRecorder.isTypeSupported(e))return e;return null}();if(!r){const r="Trình duyệt không hỗ trợ định dạng ghi âm.";return y("error"),b(r),void p.current?.(r)}try{const e=await navigator.mediaDevices.getUserMedia({audio:!0});P.current=e,O.current=r;const n=new MediaRecorder(e,{mimeType:r});k.current=n,D.current=[],n.ondataavailable=r=>{r.data.size>0&&D.current.push(r.data)},n.onstop=()=>{const r=Date.now()-_.current,e=new Blob(D.current,{type:O.current}),n=URL.createObjectURL(e),t=C.current;F(),U.current=n,T(n),y("recorded"),z(e,r,t)},n.start(),_.current=Date.now(),C.current=m.current,y("recording"),v(0),f(a),b(null),x(null),T(null),M(!1),S.current=setInterval(()=>{v(r=>r+1),f(r=>{const e=Math.max(0,r-1);return 0===e&&(N(),k.current&&"recording"===k.current.state&&k.current.stop(),W()),e})},1e3)}catch(r){W();const e=r instanceof DOMException&&("NotAllowedError"===r.name||"PermissionDeniedError"===r.name)?"Vui lòng cấp quyền truy cập microphone trong trình duyệt.":"Không thể truy cập microphone. Vui lòng thử lại.";y("error"),b(e),p.current?.(e)}},[G,a,i.existingAnswerKey,N,W,z,F]),H=t(()=>{N(),k.current&&"recording"===k.current.state&&k.current.stop(),W()},[N,W]),J=t(()=>{N(),W(),k.current&&"recording"===k.current.state&&k.current.stop(),k.current=null,D.current=[],q.current=null,L.current=void 0,B.current=0,F(),T(null),x(null),v(0),f(a),b(null),y("idle"),M(!1)},[N,W,F,a]),Q=t(async()=>{if(i.existingAnswerKey)return y("uploaded"),void x(i.existingAnswerKey);J(),await $()},[i.existingAnswerKey,J,$]),Y=t(async()=>{const r=q.current;r&&(B.current=0,await X(r,L.current,0,C.current||void 0))},[X]);n(()=>{J()},[i.questionId]),n(()=>{"idle"===h&&j&&i.existingAnswerKey&&!R&&(T(j),U.current=j,x(i.existingAnswerKey),y("uploaded"))},[h,j,i.existingAnswerKey,R]),n(()=>(V.current=!0,()=>{V.current=!1,N(),k.current&&"recording"===k.current.state&&k.current.stop(),W(),F()}),[]);return{status:h,remainingSeconds:w,isWarning:w<=5&&"recording"===h,durationSeconds:A,previewUrl:R,uploadedKey:E,error:I,isSupported:G,isAttemptExpired:K,liveTranscript:null,startRecording:$,stopRecording:H,resetRecording:J,reRecord:Q,retryUpload:Y}}
@@ -1,15 +1,27 @@
1
1
  import { type ReactNode } from 'react';
2
- import type { ResultReviewSdkApi } from '../../api/result-review';
2
+ import { type ResultReviewSdkApi } from '../../api/result-review';
3
+ export declare enum PartRubricTemplateCode {
4
+ KINDY_VER3 = "PLACEMENT_TEST_KINDY_VER3",
5
+ PRESCHOOL = "PLACEMENT_TEST_PRESCHOOL"
6
+ }
3
7
  export interface ResultReviewPageContainerProps {
4
8
  attemptId: string;
5
9
  token?: string | null;
6
10
  api: ResultReviewSdkApi;
7
- /** When true, polling is disabled and PDF readiness is signalled (headless render). */
11
+ /** Disables polling and signals `body[data-pdf-ready="true"]` after loading. */
8
12
  isPrintMode?: boolean;
13
+ /** Controls the native PDF action independently from API availability. */
14
+ canExportPdf?: boolean;
15
+ /** Canonical URL encoded in report QR codes. Defaults to the current URL. */
16
+ reviewUrl?: string;
17
+ /** Optional public asset prefix; bundled report assets are used by default. */
18
+ assetBaseUrl?: string;
19
+ /** Login destination used by the private-result state. */
20
+ loginHref?: string;
9
21
  onBack?: () => void;
10
22
  onNavigate?: (href: string) => void;
11
23
  topActionsSlot?: ReactNode;
12
24
  beforeReportSlot?: ReactNode;
13
25
  afterReportSlot?: ReactNode;
14
26
  }
15
- export declare function ResultReviewPageContainer({ attemptId, token, api, isPrintMode, onBack, onNavigate, topActionsSlot, beforeReportSlot, afterReportSlot }: ResultReviewPageContainerProps): import("react").JSX.Element;
27
+ export declare function ResultReviewPageContainer({ attemptId, token, api, isPrintMode, canExportPdf, reviewUrl, assetBaseUrl, loginHref, onBack, onNavigate, topActionsSlot, beforeReportSlot, afterReportSlot, }: ResultReviewPageContainerProps): import("react").JSX.Element;
@@ -1 +1 @@
1
- "use client";import{jsx as e,jsxs as t}from"react/jsx-runtime";import{useCallback as a,useEffect as n,useMemo as l,useState as s}from"react";import{ChevronLeft as i,Download as r,Loader2 as c,Lock as o,Sparkles as m,XCircle as d}from"lucide-react";import{Button as u}from"../../components/ui/button";import{Card as h,CardContent as p,CardHeader as f,CardTitle as x}from"../../components/ui/card";import{useToast as b}from"../../shared/lib/hooks/useToast";import{transformResultAnswersForReview as g}from"./review-data";import{ReviewQuestionRenderer as N}from"./ReviewQuestionRenderer";import{unwrap as w}from"./result-review.utils";import{useResultReviewPolling as v}from"./hooks/useResultReviewPolling";import{isResultNotPublic as y,setPdfReady as k}from"./review-status.utils";export function ResultReviewPageContainer({attemptId:m,token:N,api:S,isPrintMode:F=!1,onBack:j,onNavigate:$,topActionsSlot:C,beforeReportSlot:D,afterReportSlot:q}){const{error:M,success:O}=b(),[K,L]=s(null),[Q,U]=s(null),[B,T]=s(!0),[X,H]=s(null),[I,G]=s(!1),z=a(async e=>{e&&T(!0);try{const[e,t]=await Promise.all([S.getDetailedResult(m,N),S.getAttempt?.(m,N)]);L(w(e)),U(t?w(t):null),H(null)}catch(e){H(e),y(e,Boolean(N))||M({message:"Không thể tải kết quả",description:e instanceof Error?e.message:void 0})}finally{e&&T(!1)}},[S,m,N,M]);n(()=>{let e=!1;return z(!0).then(()=>{}),()=>{e=!0}},[z]);const J=a(()=>{z(!1)},[z]);v({detail:K,isPrintMode:F,isLoading:B,onRefetch:J}),n(()=>{if(!F||B)return;const e=requestAnimationFrame(()=>k(!0));return()=>{cancelAnimationFrame(e),k(!1)}},[F,B]);const V=l(()=>g(K?.answers??[],Q?.exam?.template?.parts),[K?.answers,Q?.exam?.template?.parts]),W=Q?.studentName??"Học sinh",Y=Q?.studentUsername,Z=Q?.branchName??"Chưa tham gia chi nhánh",_=Q?.exam?.template?.title??K?.examName??"Kết quả bài thi",ee=()=>{j?j():$?$("/student/results"):"undefined"!=typeof window&&window.history.back()},te=async()=>{if(K&&S.exportPdf){G(!0);try{const{blob:e,filename:t}=await S.exportPdf(m,Q?.exam?.template?.type);!function(e,t){const a=URL.createObjectURL(e),n=document.createElement("a");n.href=a,n.download=t,document.body.appendChild(n),n.click(),document.body.removeChild(n),URL.revokeObjectURL(a)}(e,t),O({message:"Xuất PDF thành công"})}catch(e){M({message:"Không xuất được PDF",description:e instanceof Error?e.message:void 0})}finally{G(!1)}}};return B?e(E,{icon:e(c,{className:"h-8 w-8 animate-spin text-[#2874A6]"}),title:"Đang tải kết quả bài thi..."}):y(X,Boolean(N))?e(E,{icon:e(o,{className:"h-8 w-8 text-amber-500"}),title:"Kết quả này chưa được công khai",action:e(u,{onClick:ee,children:"Quay lại"})}):K?e("div",{className:"min-h-screen bg-[#e0e0e0] py-4 md:py-6",children:t("div",{className:"mx-auto flex w-full max-w-[1160px] flex-col gap-4 px-3 sm:px-4",children:[t("div",{className:"flex items-center justify-between gap-3 print:hidden",children:[t(u,{variant:"outline",onClick:ee,children:[e(i,{className:"mr-2 h-4 w-4"}),"Quay lại"]}),t("div",{className:"flex items-center gap-2",children:[S.exportPdf&&t(u,{onClick:()=>{te()},disabled:I,className:"bg-[#00AEEF] hover:bg-[#0099D6]",children:[I?e(c,{className:"mr-2 h-4 w-4 animate-spin"}):e(r,{className:"mr-2 h-4 w-4"}),"Xuất PDF"]}),C]})]}),D,t(h,{className:"overflow-hidden rounded-[28px] border-0 shadow-xl",children:[t(f,{className:"bg-gradient-to-r from-[#0054A6] to-[#00AEEF] text-white",children:[e(x,{className:"text-2xl",children:_}),t("p",{className:"text-sm text-white/80",children:[W,Y?` ${Y}`:""," ",Z]})]}),t(p,{className:"space-y-6 p-5 md:p-8",children:[e(A,{detail:K}),e(R,{detail:K}),!1!==K.showDetailedResults&&e(P,{reviewData:V})]})]}),q]})}):e(E,{icon:e(d,{className:"h-8 w-8 text-red-400"}),title:"Không thể tải kết quả",action:e(u,{onClick:ee,children:"Quay lại"})})}function A({detail:a}){return t("div",{className:"grid gap-3 md:grid-cols-4",children:[e(S,{label:"Tổng điểm",value:`${F(a.totalScore)} / ${F(a.maxScore)}`}),e(S,{label:"Câu đúng",value:`${a.correctAnswers} / ${a.totalQuestions}`}),e(S,{label:"Xếp hạng",value:a.rank?`${a.rank}/${a.totalStudents??"-"}`:"-"}),e(S,{label:"Trạng thái",value:a.isPendingGrading?"Đang chấm":"Hoàn tất"})]})}function R({detail:a}){if(!a.llmOverallAssessment&&!a.llmSkillAssessment)return null;const n=Object.entries(a.llmSkillAssessment??{}).filter(e=>null!=e[1]);return t("section",{className:"rounded-2xl border border-[#00AEEF]/30 bg-[#f0f8ff] p-4",children:[t("div",{className:"mb-2 flex items-center gap-2 font-bold text-[#0054A6]",children:[e(m,{className:"h-5 w-5"}),"Phân tích tổng quan"]}),a.llmOverallAssessment&&e("p",{className:"whitespace-pre-wrap text-sm text-slate-700",children:a.llmOverallAssessment}),n.length>0&&e("div",{className:"mt-3 grid gap-2 md:grid-cols-2",children:n.map(([a,n])=>t("div",{className:"rounded-xl bg-white p-3 text-sm",children:[e("strong",{children:n.label}),e("p",{className:"mt-1 text-slate-600",children:n.feedback})]},a))})]})}function P({reviewData:a}){return e("div",{className:"space-y-6",children:a.parts.map((n,l)=>t("section",{className:"rounded-2xl border border-[#00AEEF] bg-white p-5",children:[t("div",{className:"mb-4 flex items-center justify-between",children:[e("h3",{className:"text-lg font-bold text-[#0054A6]",children:n.name||`Phần ${n.partNo}`}),a.partScoresMap[l]&&t("span",{className:"rounded-full bg-[#0054A6] px-3 py-1 text-sm font-bold text-white",children:[F(a.partScoresMap[l].score)," / ",F(a.partScoresMap[l].maxScore)]})]}),n.instructions&&e("p",{className:"mb-4 font-semibold text-slate-700",children:n.instructions}),e(N,{part:n,reviewData:a})]},`${n.partId}-${l}`))})}function S({label:a,value:n}){return t("div",{className:"rounded-2xl bg-slate-50 p-4",children:[e("p",{className:"text-xs font-semibold uppercase text-slate-500",children:a}),e("p",{className:"mt-1 text-xl font-black text-[#0054A6]",children:n})]})}function E({icon:a,title:n,action:l}){return e("div",{className:"flex min-h-screen items-center justify-center bg-[#e0e0e0]",children:t("div",{className:"rounded-[28px] bg-white p-8 text-center shadow-xl",children:[e("div",{className:"mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-slate-50",children:a}),e("p",{className:"font-semibold text-slate-700",children:n}),l&&e("div",{className:"mt-5",children:l})]})})}function F(e){return null!=e&&Number.isFinite(e)?e<10?e.toFixed(1):String(Math.round(e)):"0"}
1
+ "use client";import{jsx as e,Fragment as t,jsxs as r}from"react/jsx-runtime";import{useCallback as n,useEffect as o,useMemo as i,useState as a}from"react";import{ChevronLeft as s,Download as l,Loader2 as c,Sparkles as d}from"lucide-react";import{ResultTemplateType as m}from"../../api/result-review";import{useToast as u}from"../../shared/lib/hooks/useToast";import{ReviewQuestionRenderer as p}from"./ReviewQuestionRenderer";import{useResultReviewPolling as f}from"./hooks/useResultReviewPolling";import{BadgeTitle as g}from"./report/BadgeTitle";import{OceanEduExamReport as x}from"./report/OceanEduExamReport";import{OverallAssessmentSection as h}from"./report/OverallAssessmentSection";import{ResultErrorState as b}from"./report/ResultErrorState";import{ResultLoadingState as w}from"./report/ResultLoadingState";import{ResultNotPublicState as S}from"./report/ResultNotPublicState";import{SpeakingRubricSection as v}from"./report/SpeakingRubricSection";import{SpeakingReviewResult as N}from"./speaking/SpeakingReviewResult";import{transformResultAnswersForReview as R}from"./review-data";import{isResultNotPublic as P,setPdfReady as k}from"./review-status.utils";import{unwrap as E}from"./result-review.utils";import{formatTotalScore as y}from"./utils/review-score.utils";import{transformSpeakingResultForReview as A}from"./utils/transform-speaking-review";export var PartRubricTemplateCode;!function(e){e.KINDY_VER3="PLACEMENT_TEST_KINDY_VER3",e.PRESCHOOL="PLACEMENT_TEST_PRESCHOOL"}(PartRubricTemplateCode||(PartRubricTemplateCode={}));const T=new Set(Object.values(PartRubricTemplateCode));export function ResultReviewPageContainer({attemptId:s,token:l,api:c,isPrintMode:d=!1,canExportPdf:p=!1,reviewUrl:g,assetBaseUrl:y,loginHref:B,onBack:U,onNavigate:F,topActionsSlot:O,beforeReportSlot:I,afterReportSlot:_}){const{error:j,success:Q}=u(),[$,M]=a(null),[H,G]=a(null),[K,V]=a(!0),[q,W]=a(null),[X,Y]=a(!1),z=n(async e=>{e&&V(!0);try{const[e,t]=await Promise.all([c.getDetailedResult(s,l),c.getAttempt?.(s,l)]);M(E(e)),G(t?E(t):null),W(null)}catch(e){W(e),P(e,Boolean(l))||j({message:"Không thể tải kết quả",description:e instanceof Error?e.message:void 0})}finally{e&&V(!1)}},[c,s,l,j]);o(()=>{z(!0)},[z]);const J=n(()=>{z(!1)},[z]),{hasPendingQuestions:Z,hasPendingLlm:ee,hasFailedLlm:te,retryLlm:re}=f({detail:$,isPrintMode:d,isLoading:K,onRefetch:J});o(()=>{if(!d||K)return;const e=requestAnimationFrame(()=>k(!0));return()=>{cancelAnimationFrame(e),k(!1)}},[d,K]);const ne=H?.exam?.template,oe=ne?.parts,ie=H?.studentName??"Học sinh",ae=H?.studentUsername??null,se=H?.branchName??"Chưa tham gia chi nhánh",le=function(e){const t=e?new Date(e):new Date;return Number.isNaN(t.getTime())?(new Date).toLocaleDateString("vi-VN"):t.toLocaleDateString("vi-VN")}($?.submittedAt),ce=i(()=>function(e,t){const r=e??("undefined"!=typeof window?window.location.href:void 0);if(!r||!t)return r;try{const e=new URL(r,"undefined"!=typeof window?window.location.origin:void 0);return e.searchParams.set("token",t),e.toString()}catch{return r}}(g,l??H?.verificationToken),[g,l,H?.verificationToken]),de=i(()=>R($?.answers??[],oe),[$?.answers,oe]),me=i(()=>$&&ne?.type===m.SPEAKING?A($,ie,oe):null,[$,ie,ne?.type,oe]),ue=!!$?.templateCode&&T.has($.templateCode),pe=i(()=>function(e){const t={};return e?.speakingRubric?.criteria?.forEach(e=>{const r=/^C(\d+)$/.exec(e.code);if(!r)return;const n=null!=e.weightedScore&&null!=e.maxScore;t[Number(r[1])]=n?{score:e.weightedScore,maxScore:e.maxScore,weighted:n}:{score:e.score,maxScore:5,weighted:n}}),t}($),[$?.speakingRubric]),fe=n(()=>{if(U)return U();F?F(H?.contestId?`/student/contests/${H.contestId}/rooms`:"/student/results"):"undefined"!=typeof window&&window.history.back()},[H?.contestId,U,F]),ge=async()=>{if($&&c.exportPdf){Y(!0);try{const{blob:e,filename:t}=await c.exportPdf(s,ne?.type);!function(e,t){const r=URL.createObjectURL(e),n=document.createElement("a");n.href=r,n.download=t,document.body.appendChild(n),n.click(),document.body.removeChild(n),URL.revokeObjectURL(r)}(e,t),Q({message:"Xuất PDF thành công"})}catch(e){j({message:"Không xuất được PDF",description:e instanceof Error?e.message:void 0})}finally{Y(!1)}}};return K?e(w,{}):P(q,Boolean(l))?e(S,{loginHref:B,assetBaseUrl:y,onLogin:F}):$?me?e(N,{reviewData:me,studentName:ie,studentCode:ae,branchName:se,examDateText:le,examTitle:ne?.title,showDetailedResults:$.showDetailedResults,speakingRubric:$.speakingRubric,reviewUrl:ce,assetBaseUrl:y,canExportPdf:p&&Boolean(c.exportPdf),isExporting:X,hasPendingQuestions:Z,onBack:fe,onExportPdf:ge,topActionsSlot:O,beforeReportSlot:I,afterReportSlot:_}):e("div",{className:"min-h-screen bg-[#e0e0e0] py-4 md:py-6",children:r("div",{className:"mx-auto flex w-full max-w-[1160px] flex-col gap-4 px-0 sm:px-4",children:[e(D,{onBack:fe,onExport:ge,canExport:p&&Boolean(c.exportPdf),isExporting:X,topActionsSlot:O}),Z?e(L,{}):null,I,e(x,{studentName:ie,studentCode:ae,branchName:se,examDateText:le,totalScore:$.totalScore,maxScore:$.maxScore,isPendingGrading:Boolean($.isPendingGrading||Z),examSubTitle:$.roomName,examTitle:ne?.title,reviewUrl:ce,assetBaseUrl:y,readingScore:$.readingScore,maxReadingScore:$.maxReadingScore,listeningScore:$.listeningScore,maxListeningScore:$.maxListeningScore,writingScore:$.writingScore,maxWritingScore:$.maxWritingScore,speakingScore:$.speakingScore,maxSpeakingScore:$.maxSpeakingScore,overallAssessmentSlot:r(t,{children:[e(h,{isPending:ee,isFailed:te,onRetry:re,llmOverallAssessment:$.llmOverallAssessment,llmSkillAssessment:$.llmSkillAssessment,correctAnswers:$.correctAnswers,totalQuestions:$.totalQuestions}),$.speakingRubric?.criteria?.length?e(v,{rubric:$.speakingRubric}):null]}),children:!1!==$.showDetailedResults?e(C,{reviewData:de,hidePerQuestionGrading:ue,rubricScores:pe}):null}),_]})}):e(b,{error:q,onBack:fe})}function D({onBack:t,onExport:n,canExport:o,isExporting:i,topActionsSlot:a}){return r("div",{className:"flex items-center justify-between gap-3 px-3 print:hidden sm:px-0",children:[r("button",{type:"button",onClick:t,className:"inline-flex items-center gap-2 rounded-xl border border-slate-200 bg-white px-4 py-2.5 text-sm font-semibold text-slate-700 shadow-sm transition hover:bg-slate-50",children:[e(s,{className:"h-4 w-4"}),"Quay lại"]}),r("div",{className:"flex items-center gap-3",children:[o?r("button",{type:"button",onClick:()=>{n()},disabled:i,className:"inline-flex items-center gap-2 rounded-xl bg-gradient-to-r from-[#00AEEF] to-[#0099D6] px-5 py-2.5 text-sm font-bold text-white shadow-lg disabled:opacity-50",children:[i?e(c,{className:"h-4 w-4 animate-spin"}):e(l,{className:"h-4 w-4"}),i?"Đang xuất...":"Xuất PDF"]}):null,a]})]})}function L(){return r("div",{className:"mx-3 flex items-center gap-3 rounded-2xl border border-[#b3dfff] bg-gradient-to-r from-[#e8f6ff] to-[#f0f8ff] px-5 py-4 shadow-sm sm:mx-0",children:[e("div",{className:"flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-[#d0edff]",children:e(d,{className:"h-5 w-5 animate-pulse text-[#00AEEF]"})}),r("div",{children:[e("p",{className:"text-sm font-bold text-[#0054A6]",children:"AI đang chấm điểm..."}),e("p",{className:"text-xs text-[#0078c8]",children:"Trang sẽ tự động cập nhật cho đến khi hoàn tất."})]})]})}function C({reviewData:t,hidePerQuestionGrading:n,rubricScores:o}){return e("div",{className:"space-y-6",children:t.parts.map((i,a)=>{const s=n?o[i.partNo]:void 0,l=t.partScoresMap[a],c=s?`${s.weighted?Number(s.score.toFixed(1)):Math.round(s.score)} / ${Math.round(s.maxScore)}`:l?`${y(l.score)} / ${y(l.maxScore)}`:null;return r("section",{className:"relative rounded-[20px] border-2 border-[#00AEEF] bg-white px-5 pb-6 pt-11 md:px-8",children:[e(g,{number:String(i.partNo),title:i.name||`PHẦN ${i.partNo}`}),r("div",{className:"mb-6 flex flex-col gap-4",children:[c?e("div",{className:"flex justify-end",children:r("div",{className:"rounded-full bg-[#0054A6] px-4 py-2 text-sm font-bold text-white",children:["Điểm: ",c]})}):null,i.instructions?e(B,{value:i.instructions}):null]}),e(p,{part:i,reviewData:t,hidePerQuestionGrading:n})]},`${i.partId}-${a}`)})})}function B({value:t}){return/<[a-z][\s\S]*>/i.test(t)?e("div",{className:"text-[17px] font-bold leading-relaxed text-[#0054A6] [&_p]:mb-2 [&_p:last-child]:mb-0",dangerouslySetInnerHTML:{__html:t}}):e("h3",{className:"text-[17px] font-bold leading-relaxed text-[#0054A6]",children:t})}
@@ -1,6 +1,16 @@
1
+ /**
2
+ * ReviewQuestionRenderer - full FE parity dispatcher.
3
+ * Source: oe-exam-fe ReviewQuestionRenderer.tsx
4
+ *
5
+ * Public props keep `{ part, reviewData }` and add hidePerQuestionGrading.
6
+ */
1
7
  import type { ReviewQuestionPart, ReviewTransformResult } from './review-types';
2
8
  export interface ReviewQuestionRendererProps {
3
9
  part: ReviewQuestionPart;
4
10
  reviewData: ReviewTransformResult;
11
+ /**
12
+ * Hide per-question grading for speaking questions (KINDY VER3 / PRESCHOOL).
13
+ */
14
+ hidePerQuestionGrading?: boolean;
5
15
  }
6
- export declare function ReviewQuestionRenderer({ part, reviewData }: ReviewQuestionRendererProps): import("react").JSX.Element;
16
+ export declare function ReviewQuestionRenderer(props: ReviewQuestionRendererProps): import("react").JSX.Element;