@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.
- package/README.md +56 -11
- package/dist/api/result-review.d.ts +2 -1
- package/dist/api/result-review.js +1 -1
- package/dist/api/result-review.types.d.ts +69 -3
- package/dist/api/result-review.types.js +1 -1
- package/dist/components/exams/take/ExamTakingPageContainer.js +1 -1
- package/dist/components/exams/take/components/speaking/renderers/SpeakingDescribeImageRenderer.js +1 -1
- package/dist/components/exams/take/components/speaking/renderers/SpeakingReadDisplayedContentRenderer.d.ts +0 -17
- package/dist/components/exams/take/components/speaking/renderers/SpeakingReadDisplayedContentRenderer.js +1 -1
- package/dist/components/exams/take/hooks/useSpeakingRecorder.js +1 -1
- package/dist/components/results/ResultReviewPageContainer.d.ts +15 -3
- package/dist/components/results/ResultReviewPageContainer.js +1 -1
- package/dist/components/results/ReviewQuestionRenderer.d.ts +11 -1
- package/dist/components/results/ReviewQuestionRenderer.js +1 -1
- package/dist/components/results/hooks/useResultReviewPolling.d.ts +12 -2
- package/dist/components/results/hooks/useResultReviewPolling.js +1 -1
- package/dist/components/results/index.d.ts +34 -2
- package/dist/components/results/index.js +1 -1
- package/dist/components/results/renderers/QuestionExplanations.d.ts +22 -0
- package/dist/components/results/renderers/QuestionExplanations.js +1 -0
- package/dist/components/results/renderers/ReviewChooseAdjectiveRenderer.d.ts +13 -0
- package/dist/components/results/renderers/ReviewChooseAdjectiveRenderer.js +1 -0
- package/dist/components/results/renderers/ReviewChooseBestAnswerRenderer.d.ts +22 -0
- package/dist/components/results/renderers/ReviewChooseBestAnswerRenderer.js +1 -0
- package/dist/components/results/renderers/ReviewColoringRenderer.d.ts +16 -0
- package/dist/components/results/renderers/ReviewColoringRenderer.js +1 -0
- package/dist/components/results/renderers/ReviewListenAndWriteRenderer.d.ts +18 -0
- package/dist/components/results/renderers/ReviewListenAndWriteRenderer.js +1 -0
- package/dist/components/results/renderers/ReviewListenChooseAnswerGroupRenderer.d.ts +25 -0
- package/dist/components/results/renderers/ReviewListenChooseAnswerGroupRenderer.js +1 -0
- package/dist/components/results/renderers/ReviewListenChooseObjectsRenderer.d.ts +25 -0
- package/dist/components/results/renderers/ReviewListenChooseObjectsRenderer.js +1 -0
- package/dist/components/results/renderers/ReviewListenDragObjectsRenderer.d.ts +25 -0
- package/dist/components/results/renderers/ReviewListenDragObjectsRenderer.js +1 -0
- package/dist/components/results/renderers/ReviewListeningHeader.d.ts +44 -0
- package/dist/components/results/renderers/ReviewListeningHeader.js +1 -0
- package/dist/components/results/renderers/ReviewSpeakingAIPanel.d.ts +17 -0
- package/dist/components/results/renderers/ReviewSpeakingAIPanel.js +1 -0
- package/dist/components/results/renderers/ReviewSpeakingMedia.d.ts +39 -0
- package/dist/components/results/renderers/ReviewSpeakingMedia.js +1 -0
- package/dist/components/results/renderers/ReviewSpeakingQuestionItem.d.ts +5 -0
- package/dist/components/results/renderers/ReviewSpeakingQuestionItem.js +1 -0
- package/dist/components/results/renderers/ReviewSpeakingRenderer.d.ts +3 -0
- package/dist/components/results/renderers/ReviewSpeakingRenderer.js +1 -0
- package/dist/components/results/renderers/ReviewSpeakingRenderer.types.d.ts +80 -0
- package/dist/components/results/renderers/ReviewSpeakingRenderer.types.js +1 -0
- package/dist/components/results/renderers/review-question-body-dedicated.d.ts +15 -0
- package/dist/components/results/renderers/review-question-body-dedicated.js +1 -0
- package/dist/components/results/renderers/review-question-body-movers.d.ts +17 -0
- package/dist/components/results/renderers/review-question-body-movers.js +1 -0
- package/dist/components/results/renderers/review-speaking-map.d.ts +7 -0
- package/dist/components/results/renderers/review-speaking-map.js +1 -0
- package/dist/components/results/report/BadgeTitle.d.ts +5 -0
- package/dist/components/results/report/BadgeTitle.js +1 -0
- package/dist/components/results/report/ExamSkillRow.d.ts +14 -0
- package/dist/components/results/report/ExamSkillRow.js +1 -0
- package/dist/components/results/report/OceanEduExamReport.d.ts +31 -0
- package/dist/components/results/report/OceanEduExamReport.js +1 -0
- package/dist/components/results/report/OceanEduSpeakingReport.d.ts +26 -0
- package/dist/components/results/report/OceanEduSpeakingReport.js +1 -0
- package/dist/components/results/report/OverallAssessmentSection.d.ts +24 -0
- package/dist/components/results/report/OverallAssessmentSection.js +1 -0
- package/dist/components/results/report/ResultErrorState.d.ts +14 -0
- package/dist/components/results/report/ResultErrorState.js +1 -0
- package/dist/components/results/report/ResultLoadingState.d.ts +11 -0
- package/dist/components/results/report/ResultLoadingState.js +1 -0
- package/dist/components/results/report/ResultNotPublicState.d.ts +16 -0
- package/dist/components/results/report/ResultNotPublicState.js +1 -0
- package/dist/components/results/report/SpeakingReportParts.d.ts +4 -0
- package/dist/components/results/report/SpeakingReportParts.js +1 -0
- package/dist/components/results/report/SpeakingReportShared.d.ts +36 -0
- package/dist/components/results/report/SpeakingReportShared.js +1 -0
- package/dist/components/results/report/SpeakingRubricSection.d.ts +21 -0
- package/dist/components/results/report/SpeakingRubricSection.js +1 -0
- package/dist/components/results/report/assets/dofu2.png +0 -0
- package/dist/components/results/report/assets/dofu4.png +0 -0
- package/dist/components/results/report/assets/logo-ocean.png +0 -0
- package/dist/components/results/report/assets/penguin-main.png +0 -0
- package/dist/components/results/report/assets/penguin-small.png +0 -0
- package/dist/components/results/report/speaking-report.utils.d.ts +37 -0
- package/dist/components/results/report/speaking-report.utils.js +1 -0
- package/dist/components/results/review-data.d.ts +1 -0
- package/dist/components/results/review-data.js +1 -1
- package/dist/components/results/review-types.d.ts +14 -0
- package/dist/components/results/speaking/SpeakingResultAudioPlayer.d.ts +17 -0
- package/dist/components/results/speaking/SpeakingResultAudioPlayer.js +1 -0
- package/dist/components/results/speaking/SpeakingReviewResult.d.ts +30 -0
- package/dist/components/results/speaking/SpeakingReviewResult.js +1 -0
- package/dist/components/results/utils/review-score.utils.d.ts +21 -0
- package/dist/components/results/utils/review-score.utils.js +1 -0
- package/dist/components/results/utils/speaking-summary.d.ts +41 -0
- package/dist/components/results/utils/speaking-summary.js +1 -0
- package/dist/components/results/utils/transform-speaking-review.d.ts +62 -0
- package/dist/components/results/utils/transform-speaking-review.js +1 -0
- package/dist/shared/lib/hooks/usePresignedFileUrl.d.ts +9 -1
- package/dist/shared/lib/hooks/usePresignedFileUrl.js +1 -1
- 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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
215
|
+
### Props và hà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
|
|
237
|
+
Các report shell, renderer, transform helper, polling/status helper và 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
|
|
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(
|
|
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
|
-
|
|
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})})}
|
package/dist/components/exams/take/components/speaking/renderers/SpeakingDescribeImageRenderer.js
CHANGED
|
@@ -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{
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
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(
|
|
16
|
+
export declare function ReviewQuestionRenderer(props: ReviewQuestionRendererProps): import("react").JSX.Element;
|