@tinyweb_dev/oe-exam-sdk 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/README.md +56 -11
  2. package/dist/api/result-review.d.ts +2 -1
  3. package/dist/api/result-review.js +1 -1
  4. package/dist/api/result-review.types.d.ts +69 -3
  5. package/dist/api/result-review.types.js +1 -1
  6. package/dist/components/exams/take/ExamTakingPageContainer.js +1 -1
  7. package/dist/components/exams/take/components/speaking/renderers/SpeakingDescribeImageRenderer.js +1 -1
  8. package/dist/components/exams/take/components/speaking/renderers/SpeakingReadDisplayedContentRenderer.d.ts +0 -17
  9. package/dist/components/exams/take/components/speaking/renderers/SpeakingReadDisplayedContentRenderer.js +1 -1
  10. package/dist/components/results/ResultReviewPageContainer.d.ts +15 -3
  11. package/dist/components/results/ResultReviewPageContainer.js +1 -1
  12. package/dist/components/results/ReviewQuestionRenderer.d.ts +11 -1
  13. package/dist/components/results/ReviewQuestionRenderer.js +1 -1
  14. package/dist/components/results/hooks/useResultReviewPolling.d.ts +12 -2
  15. package/dist/components/results/hooks/useResultReviewPolling.js +1 -1
  16. package/dist/components/results/index.d.ts +34 -2
  17. package/dist/components/results/index.js +1 -1
  18. package/dist/components/results/renderers/QuestionExplanations.d.ts +22 -0
  19. package/dist/components/results/renderers/QuestionExplanations.js +1 -0
  20. package/dist/components/results/renderers/ReviewChooseAdjectiveRenderer.d.ts +13 -0
  21. package/dist/components/results/renderers/ReviewChooseAdjectiveRenderer.js +1 -0
  22. package/dist/components/results/renderers/ReviewChooseBestAnswerRenderer.d.ts +22 -0
  23. package/dist/components/results/renderers/ReviewChooseBestAnswerRenderer.js +1 -0
  24. package/dist/components/results/renderers/ReviewColoringRenderer.d.ts +16 -0
  25. package/dist/components/results/renderers/ReviewColoringRenderer.js +1 -0
  26. package/dist/components/results/renderers/ReviewListenAndWriteRenderer.d.ts +18 -0
  27. package/dist/components/results/renderers/ReviewListenAndWriteRenderer.js +1 -0
  28. package/dist/components/results/renderers/ReviewListenChooseAnswerGroupRenderer.d.ts +25 -0
  29. package/dist/components/results/renderers/ReviewListenChooseAnswerGroupRenderer.js +1 -0
  30. package/dist/components/results/renderers/ReviewListenChooseObjectsRenderer.d.ts +25 -0
  31. package/dist/components/results/renderers/ReviewListenChooseObjectsRenderer.js +1 -0
  32. package/dist/components/results/renderers/ReviewListenDragObjectsRenderer.d.ts +25 -0
  33. package/dist/components/results/renderers/ReviewListenDragObjectsRenderer.js +1 -0
  34. package/dist/components/results/renderers/ReviewListeningHeader.d.ts +44 -0
  35. package/dist/components/results/renderers/ReviewListeningHeader.js +1 -0
  36. package/dist/components/results/renderers/ReviewSpeakingAIPanel.d.ts +17 -0
  37. package/dist/components/results/renderers/ReviewSpeakingAIPanel.js +1 -0
  38. package/dist/components/results/renderers/ReviewSpeakingMedia.d.ts +39 -0
  39. package/dist/components/results/renderers/ReviewSpeakingMedia.js +1 -0
  40. package/dist/components/results/renderers/ReviewSpeakingQuestionItem.d.ts +5 -0
  41. package/dist/components/results/renderers/ReviewSpeakingQuestionItem.js +1 -0
  42. package/dist/components/results/renderers/ReviewSpeakingRenderer.d.ts +3 -0
  43. package/dist/components/results/renderers/ReviewSpeakingRenderer.js +1 -0
  44. package/dist/components/results/renderers/ReviewSpeakingRenderer.types.d.ts +80 -0
  45. package/dist/components/results/renderers/ReviewSpeakingRenderer.types.js +1 -0
  46. package/dist/components/results/renderers/review-question-body-dedicated.d.ts +15 -0
  47. package/dist/components/results/renderers/review-question-body-dedicated.js +1 -0
  48. package/dist/components/results/renderers/review-question-body-movers.d.ts +17 -0
  49. package/dist/components/results/renderers/review-question-body-movers.js +1 -0
  50. package/dist/components/results/renderers/review-speaking-map.d.ts +7 -0
  51. package/dist/components/results/renderers/review-speaking-map.js +1 -0
  52. package/dist/components/results/report/BadgeTitle.d.ts +5 -0
  53. package/dist/components/results/report/BadgeTitle.js +1 -0
  54. package/dist/components/results/report/ExamSkillRow.d.ts +14 -0
  55. package/dist/components/results/report/ExamSkillRow.js +1 -0
  56. package/dist/components/results/report/OceanEduExamReport.d.ts +31 -0
  57. package/dist/components/results/report/OceanEduExamReport.js +1 -0
  58. package/dist/components/results/report/OceanEduSpeakingReport.d.ts +26 -0
  59. package/dist/components/results/report/OceanEduSpeakingReport.js +1 -0
  60. package/dist/components/results/report/OverallAssessmentSection.d.ts +24 -0
  61. package/dist/components/results/report/OverallAssessmentSection.js +1 -0
  62. package/dist/components/results/report/ResultErrorState.d.ts +14 -0
  63. package/dist/components/results/report/ResultErrorState.js +1 -0
  64. package/dist/components/results/report/ResultLoadingState.d.ts +11 -0
  65. package/dist/components/results/report/ResultLoadingState.js +1 -0
  66. package/dist/components/results/report/ResultNotPublicState.d.ts +16 -0
  67. package/dist/components/results/report/ResultNotPublicState.js +1 -0
  68. package/dist/components/results/report/SpeakingReportParts.d.ts +4 -0
  69. package/dist/components/results/report/SpeakingReportParts.js +1 -0
  70. package/dist/components/results/report/SpeakingReportShared.d.ts +36 -0
  71. package/dist/components/results/report/SpeakingReportShared.js +1 -0
  72. package/dist/components/results/report/SpeakingRubricSection.d.ts +21 -0
  73. package/dist/components/results/report/SpeakingRubricSection.js +1 -0
  74. package/dist/components/results/report/assets/dofu2.png +0 -0
  75. package/dist/components/results/report/assets/dofu4.png +0 -0
  76. package/dist/components/results/report/assets/logo-ocean.png +0 -0
  77. package/dist/components/results/report/assets/penguin-main.png +0 -0
  78. package/dist/components/results/report/assets/penguin-small.png +0 -0
  79. package/dist/components/results/report/speaking-report.utils.d.ts +37 -0
  80. package/dist/components/results/report/speaking-report.utils.js +1 -0
  81. package/dist/components/results/review-data.d.ts +1 -0
  82. package/dist/components/results/review-data.js +1 -1
  83. package/dist/components/results/review-types.d.ts +14 -0
  84. package/dist/components/results/speaking/SpeakingResultAudioPlayer.d.ts +17 -0
  85. package/dist/components/results/speaking/SpeakingResultAudioPlayer.js +1 -0
  86. package/dist/components/results/speaking/SpeakingReviewResult.d.ts +30 -0
  87. package/dist/components/results/speaking/SpeakingReviewResult.js +1 -0
  88. package/dist/components/results/utils/review-score.utils.d.ts +21 -0
  89. package/dist/components/results/utils/review-score.utils.js +1 -0
  90. package/dist/components/results/utils/speaking-summary.d.ts +41 -0
  91. package/dist/components/results/utils/speaking-summary.js +1 -0
  92. package/dist/components/results/utils/transform-speaking-review.d.ts +62 -0
  93. package/dist/components/results/utils/transform-speaking-review.js +1 -0
  94. package/package.json +4 -2
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Shared types for speaking review renderers.
3
+ * Source: oe-exam-fe ReviewSpeakingRenderer.tsx
4
+ */
5
+ export interface SpeakingReviewItem {
6
+ questionId: string;
7
+ questionNumber: number;
8
+ questionType: 'READ_DISPLAYED_CONTENT' | 'SPEAKING_DESCRIBE_IMAGE' | 'SPONTANEOUS_QA' | string;
9
+ promptText: string;
10
+ promptAudioKey: string;
11
+ promptImageKey: string;
12
+ studentAudioKey: string;
13
+ score: number | null;
14
+ maxScore: number;
15
+ isCorrect: boolean | null;
16
+ expectedAnswers: string[];
17
+ recognizedText: string;
18
+ pronunciationScore: number | null;
19
+ accuracyScore: number | null;
20
+ fluencyScore: number | null;
21
+ prosodyScore: number | null;
22
+ completenessScore: number | null;
23
+ wordErrors: Array<{
24
+ word: string;
25
+ errorType?: string;
26
+ accuracyScore?: number;
27
+ }>;
28
+ weightedScore: number | null;
29
+ contentScore: number | null;
30
+ feedbackEn: string;
31
+ classificationTitle: string;
32
+ classificationTag: string;
33
+ compareImages?: {
34
+ candidateImageUrl: string;
35
+ examinerImageUrl: string;
36
+ };
37
+ storyImages?: string[];
38
+ oddOneOutImages?: {
39
+ groupImages: Array<{
40
+ id: string;
41
+ url: string;
42
+ label: string;
43
+ }>;
44
+ contentImages: string[];
45
+ activeId?: string;
46
+ };
47
+ infoExchange?: {
48
+ title: string;
49
+ imageUrl: string;
50
+ candidateFields: Array<{
51
+ key: string;
52
+ value?: string;
53
+ answer?: string;
54
+ }>;
55
+ examinerFields: Array<{
56
+ key: string;
57
+ value?: string;
58
+ answer?: string;
59
+ }>;
60
+ };
61
+ }
62
+ export interface ReviewSpeakingRendererProps {
63
+ partNumber: number;
64
+ partName?: string;
65
+ instruction?: string;
66
+ questions: SpeakingReviewItem[];
67
+ /**
68
+ * Hide per-question grading (score pill, verdict, classification, AI panel),
69
+ * keeping prompt + audio only. Used for KINDY VER3 / PRESCHOOL rubric parts.
70
+ */
71
+ hidePerQuestionGrading?: boolean;
72
+ }
73
+ /** Question types where the question audio uses the compact listening audio bar. */
74
+ export declare const LISTENING_AUDIO_BAR_QUESTION_TYPES: Set<string>;
75
+ /** Question types that receive the redesigned AI review panel. */
76
+ export declare const AI_PANEL_QUESTION_TYPES: Set<string>;
77
+ /** Question types where the student selects an answer (no audio recording). */
78
+ export declare const NO_RECORDING_QUESTION_TYPES: Set<string>;
79
+ export declare function getQuestionTypeLabel(questionType: string): string;
80
+ export declare function getAnswerStatus(question: SpeakingReviewItem): 'correct' | 'incorrect' | 'unanswered';
@@ -0,0 +1 @@
1
+ export const LISTENING_AUDIO_BAR_QUESTION_TYPES=new Set(["LISTEN_AND_SPEAK_QUESTION_LIST","LISTEN_AND_SPEAK_IMAGE_GROUP","LISTEN_AND_SPEAK_ANSWER","LISTEN_AND_SPEAK_COMPARE_IMAGES","LISTEN_AND_SPEAK_WITH_STORY_IMAGES","LISTEN_AND_SPEAK_INFO_EXCHANGE","LISTEN_AND_SPEAK_ODD_ONE_OUT"]);export const AI_PANEL_QUESTION_TYPES=new Set(["READ_DISPLAYED_CONTENT","SPEAKING_DESCRIBE_IMAGE","SPEAKING_CUE_CARD","SPONTANEOUS_QA","LISTEN_AND_CHOOSE_OBJECTS_IN_SCENE","LISTEN_AND_CHOOSE_FROM_ANSWER_GROUP","LISTEN_AND_DRAG_OBJECTS_INTO_SCENE","LISTEN_AND_SPEAK_ANSWER","LISTEN_AND_SPEAK_IMAGE_GROUP","LISTEN_AND_SPEAK_QUESTION_LIST","LISTEN_AND_SPEAK_COMPARE_IMAGES","LISTEN_AND_SPEAK_WITH_STORY_IMAGES","LISTEN_AND_SPEAK_INFO_EXCHANGE","LISTEN_AND_SPEAK_ODD_ONE_OUT"]);export const NO_RECORDING_QUESTION_TYPES=new Set(["LISTEN_AND_CHOOSE_OBJECTS_IN_SCENE","LISTEN_AND_CHOOSE_FROM_ANSWER_GROUP","LISTEN_AND_DRAG_OBJECTS_INTO_SCENE"]);export function getQuestionTypeLabel(_){switch(_){case"READ_DISPLAYED_CONTENT":return"Read aloud";case"SPEAKING_DESCRIBE_IMAGE":return"Describe image";case"SPEAKING_CUE_CARD":return"Cue card";case"SPONTANEOUS_QA":return"Q&A";case"LISTEN_AND_CHOOSE_OBJECTS_IN_SCENE":return"Choose object";case"LISTEN_AND_CHOOSE_FROM_ANSWER_GROUP":return"Choose answer";case"LISTEN_AND_DRAG_OBJECTS_INTO_SCENE":return"Drag object";case"LISTEN_AND_SPEAK_ANSWER":return"Speak answer";case"LISTEN_AND_SPEAK_IMAGE_GROUP":return"Speak image";case"LISTEN_AND_SPEAK_QUESTION_LIST":return"Question list";case"LISTEN_AND_SPEAK_COMPARE_IMAGES":return"Compare images";case"LISTEN_AND_SPEAK_WITH_STORY_IMAGES":return"Story images";case"LISTEN_AND_SPEAK_INFO_EXCHANGE":return"Info exchange";case"LISTEN_AND_SPEAK_ODD_ONE_OUT":return"Odd one out";default:return _.replaceAll("_"," ")}}export function getAnswerStatus(_){return _.studentAudioKey?!0===_.isCorrect?"correct":!1===_.isCorrect?"incorrect":"unanswered":"unanswered"}
@@ -0,0 +1,15 @@
1
+ import type { ReviewQuestionPart } from '../review-types';
2
+ export interface DedicatedBodyContext {
3
+ part: ReviewQuestionPart;
4
+ answers: Record<string, unknown>;
5
+ isCorrectMap: Record<string, boolean | null>;
6
+ correctAnswerTextMap: Record<string, string>;
7
+ correctOptionIdMap: Record<string, string>;
8
+ rawCorrectAnswerMap: Record<string, unknown>;
9
+ questionScoreMap: Record<string, {
10
+ score: number | null;
11
+ maxScore: number;
12
+ }>;
13
+ hidePerQuestionGrading: boolean;
14
+ }
15
+ export declare function renderDedicatedReviewBody(ctx: DedicatedBodyContext): React.ReactNode | null;
@@ -0,0 +1 @@
1
+ "use client";import{jsx as e}from"react/jsx-runtime";import{transformChooseCorrectAdjective as r,transformChooseCorrectAnswer as t,transformColorObjects as s,transformFillInBlank as n,transformWriteCorrectVerbForm as i}from"../../../components/exams/take/utils/question-transformers";import{normalizeColoringAssignments as o}from"../../../components/exams/take/utils/coloring-assignments";import{ReviewChooseAdjectiveRenderer as a}from"./ReviewChooseAdjectiveRenderer";import{ReviewChooseBestAnswerRenderer as u}from"./ReviewChooseBestAnswerRenderer";import{ReviewColoringRenderer as c}from"./ReviewColoringRenderer";import{ReviewListenAndWriteRenderer as m}from"./ReviewListenAndWriteRenderer";import{ReviewListenChooseAnswerGroupRenderer as p}from"./ReviewListenChooseAnswerGroupRenderer";import{ReviewListenChooseObjectsRenderer as _}from"./ReviewListenChooseObjectsRenderer";import{ReviewListenDragObjectsRenderer as N}from"./ReviewListenDragObjectsRenderer";import{ReviewSpeakingRenderer as E}from"./ReviewSpeakingRenderer";import{mapSpeakingReviewItems as A}from"./review-speaking-map";export function renderDedicatedReviewBody(S){const{part:l,answers:R,isCorrectMap:I,correctAnswerTextMap:d,correctOptionIdMap:O,rawCorrectAnswerMap:w,questionScoreMap:T,hidePerQuestionGrading:M}=S,{partNo:g,questionType:q,questions:U}=l,D=U.length,L=U;switch(q){case"FILL_IN_BLANK":{const r=n(L);return e(m,{partNumber:g,questionCount:D,partName:l.name,instruction:l.instructions||r.instruction,title:r.title,imageUrl:r.imageUrl,audioUrl:r.audioUrl,exampleText:r.exampleText,exampleHighlight:r.exampleHighlight,questions:r.questions,answers:R,isCorrectMap:I,correctAnswerTextMap:d})}case"WRITE_CORRECT_VERB_FORM":{const r=i(L);return e(m,{partNumber:g,questionCount:D,partName:l.name,instruction:l.instructions||r.instruction,title:r.title,questions:r.questions,answers:R,isCorrectMap:I,correctAnswerTextMap:d})}case"CHOOSE_THE_CORRECT_ANSWER":{const r=t(L);return e(u,{partNumber:g,questionCount:D,partName:l.name,instruction:l.instructions||r.instruction,imageUrl:r.groupImageUrl,audioUrl:r.audioUrl||l.audioUrl,groupContent:r.groupContent,articles:r.articles,questions:r.questions,answers:R,isCorrectMap:I,correctOptionIdMap:O,example:r.example})}case"CHOOSE_CORRECT_ADJECTIVE":{const t=r(L);return e(a,{partNumber:g,questionCount:D,partName:l.name,instruction:l.instructions||t.instruction,questions:t.questions,answers:R,isCorrectMap:I,correctOptionIdMap:O})}case"READ_AND_COLOR_OBJECTS":{const r=s(L),t=R,n=U[0],i=n?C(w[n.id]).answers:{};return e(c,{partNumber:g,questionCount:D,instructions:r.instructions,instruction:l.instructions||r.instruction,partName:l.name,imageUrl:r.imageUrl,audioUrl:r.audioUrl,hintItems:r.hintItems,regions:r.regions,assignments:t,correctAnswers:i||{}})}case"IMAGE_OBJECT_MATCHING":{const r=s(L),t=o(R,r.hintItems),n=U[0],i=n?C(w[n.id]).answers:{};return e(c,{partNumber:g,questionCount:D,instructions:r.instructions,instruction:l.instructions||r.instruction,partName:l.name,imageUrl:r.imageUrl,audioUrl:r.audioUrl||l.audioUrl,hintItems:r.hintItems,regions:r.regions,assignments:t,correctAnswers:i||{}})}case"LISTEN_AND_CHOOSE_OBJECTS_IN_SCENE":return e(_,{partNumber:g,questionCount:D,partName:l.name,questions:U,studentAnswers:R,rawCorrectAnswerMap:w,isCorrectMap:I,questionScoreMap:T});case"LISTEN_AND_CHOOSE_FROM_ANSWER_GROUP":return e(p,{partNumber:g,questionCount:D,partName:l.name,questions:U,studentAnswers:R,rawCorrectAnswerMap:w,isCorrectMap:I,questionScoreMap:T});case"LISTEN_AND_DRAG_OBJECTS_INTO_SCENE":return e(N,{partNumber:g,questionCount:D,partName:l.name,questions:U,studentAnswers:R,rawCorrectAnswerMap:w,isCorrectMap:I,questionScoreMap:T});case"READ_DISPLAYED_CONTENT":case"SPEAKING_DESCRIBE_IMAGE":case"SPEAKING_CUE_CARD":case"SPONTANEOUS_QA":case"LISTEN_AND_SPEAK_ANSWER":case"LISTEN_AND_SPEAK_IMAGE_GROUP":case"LISTEN_AND_SPEAK_QUESTION_LIST":case"LISTEN_AND_SPEAK_COMPARE_IMAGES":case"LISTEN_AND_SPEAK_WITH_STORY_IMAGES":case"LISTEN_AND_SPEAK_INFO_EXCHANGE":case"LISTEN_AND_SPEAK_ODD_ONE_OUT":{const r=A(U,q,R,I);return e(E,{partNumber:g,partName:l.name,questions:r,hidePerQuestionGrading:M})}default:return null}}function C(e){return e&&"object"==typeof e&&!Array.isArray(e)?e:{}}
@@ -0,0 +1,17 @@
1
+ import type { EssayGradingInfo, ReviewQuestionPart } from '../review-types';
2
+ export interface MoversBodyContext {
3
+ part: ReviewQuestionPart;
4
+ answers: Record<string, unknown>;
5
+ isCorrectMap: Record<string, boolean | null>;
6
+ correctAnswerTextMap: Record<string, string>;
7
+ correctOptionIdMap: Record<string, string>;
8
+ cellAnswersMap: Record<string, {
9
+ cells: Record<string, {
10
+ correctAnswer: string;
11
+ }>;
12
+ caseSensitive: boolean;
13
+ }>;
14
+ essayGradingMap: Record<string, EssayGradingInfo>;
15
+ rawCorrectAnswerMap: Record<string, unknown>;
16
+ }
17
+ export declare function renderMoversReviewBody(ctx: MoversBodyContext): React.ReactNode | null;
@@ -0,0 +1 @@
1
+ "use client";import{jsx as e}from"react/jsx-runtime";import{MoversAnswerTheQuestionRenderer as n,MoversFillWordHintRenderer as t,MoversGridFillRenderer as r,MoversLabelThePictureRenderer as s,MoversMatchByWritingAnswerRenderer as o,MoversPictureChooseRenderer as i,MoversPictureFillBlankChooseRenderer as a,MoversReadingPassageRenderer as u,MoversTrueFalseRenderer as c,MoversWordFillParagraphRenderer as m,MoversWordFillStructuredFormRenderer as p,MoversWordOrderingRenderer as l,MoversWriteSentencesRenderer as w,MoversWritingRenderer as g}from"../../../components/exams/take/components/question-renderers";import{transformAnswerTheQuestion as d,transformFillWordHint as C,transformGridFill as R,transformLabelPicture as A,transformMatchByWritingAnswer as N,transformPictureChoose as _,transformPictureFillBlankChoose as M,transformReadingPassage as q,transformTrueFalse as T,transformWordFillParagraph as I,transformWordFillStructuredForm as U,transformWordOrdering as E,transformWriteSentences as h,transformWriteShortLetter as b}from"../../../components/exams/take/utils/question-transformers";const O=()=>{};export function renderMoversReviewBody(S){const{part:v,answers:L,isCorrectMap:x,correctAnswerTextMap:W,correctOptionIdMap:F,cellAnswersMap:D,essayGradingMap:f,rawCorrectAnswerMap:k}=S,{partNo:B,questionType:H,questions:P}=v,Q=P.length,G=P;switch(H){case"WRITE_A_SHORT_LETTER":{const n=G[0];if(!n)return null;const t=b(n),r=L[n.id]||"",s=f[n.id];return e(g,{partNumber:B,partName:v.name,prompt:t.prompt,requirements:t.requirements,minWords:t.minWords,maxWords:t.maxWords,answer:r,onAnswerChange:O,isReviewMode:!0,teacherScore:s?.score,maxScore:s?.maxScore,teacherFeedback:s?.teacherFeedback})}case"LOOK_PICTURE_CHOOSE_CORRECT_ANSWER":{const n=_(G),t=n.subQuestions.map(e=>({...e,correctOptionId:F[e.id]}));return e(i,{partNumber:B,partName:v.name,instruction:v.instructions||n.instruction,questionCount:Q,subQuestions:t,answers:L,onAnswerChange:O,isReviewMode:!0})}case"LOOK_PICTURE_FILL_BLANK_CHOOSE_ANSWER":{const n=M(G),t=n.subQuestions.map(e=>({...e,correctOptionId:F[e.id]}));return e(a,{partNumber:B,partName:v.name,instruction:v.instructions||n.instruction,questionCount:Q,subQuestions:t,answers:L,onAnswerChange:O,isReviewMode:!0})}case"READ_PASSAGE_AND_ANSWER_QUESTIONS":{const n=q(G);return e(u,{partNumber:B,partName:v.name,instruction:v.instructions||n.instruction,questionCount:n.questions.length,passageTitle:n.passageTitle,passage:n.passage,passageImageUrl:n.passageImageUrl,questions:n.questions,answers:L,onAnswerChange:O,isReviewMode:!0,correctOptionIdMap:F})}case"LABEL_THE_PICTURE":{const n=A(G),t=n.labels.map(e=>({...e,correctAnswer:e.isPreFilled?e.correctAnswer:W[e.id]||e.correctAnswer}));return e(s,{partNumber:B,partName:v.name,instruction:v.instructions||n.instruction,questionCount:n.labels.length,imageUrl:n.imageUrl,wordBank:n.wordBank,labels:t,answers:L,onAnswerChange:O,isReviewMode:!0})}case"LOOK_PICTURE_FILL_WORD_HINT":{const n=C(G),r=n.subQuestions.map(e=>({...e,correctAnswer:W[e.id]}));return e(t,{partNumber:B,partName:v.name,instruction:v.instructions||n.instruction,questionCount:Q,hintLetters:n.hintLetters,subQuestions:r,answers:L,onAnswerChange:O,isReviewMode:!0})}case"FILL_MISSING_WORDS_IN_GRID":{const n=R(G),t=P[0]?.id,s=t?D[t]:null,o=s?.cells||{},i=s?.caseSensitive??!1,a=t?L[t]:null,u=a&&"object"==typeof a&&null!==a&&"answers"in a?a.answers??{}:{};return e(r,{partNumber:B,partName:v.name,instruction:v.instructions||n.instruction,questionCount:Q,grid:n.grid,rows:n.rows,columns:n.columns,cellAnswers:o,answers:u,onAnswerChange:O,isReviewMode:!0,caseSensitive:i,disabledCells:n.disabledCells})}case"ANSWER_THE_QUESTION":{const t=d(G);return e(n,{partNumber:B,partName:v.name,instruction:v.instructions||t.instruction,questionCount:Q,imageUrl:t.imageUrl,audioUrl:v.audioUrl,title:t.title,groupContent:t.groupContent,questions:t.questions,answers:L,onAnswerChange:O,isReviewMode:!0,correctAnswerTextMap:W})}case"TRUE_FALSE":{const n=T(G);return e(c,{partNumber:B,partName:v.name,instruction:v.instructions||n.instruction,questionCount:Q,imageUrl:n.imageUrl,questions:n.questions,answers:L,onAnswerChange:O,isReviewMode:!0,correctAnswerTextMap:W})}case"MATCH_BY_WRITING_ANSWER":{const n=N(G);return e(o,{partNumber:B,partName:v.name,instruction:v.instructions||n.instruction,questionCount:Q,audioUrl:v.audioUrl,options:n.options,questions:n.questions,answers:L,onAnswerChange:O,isReviewMode:!0,viewMode:n.viewMode,groupImageUrl:n.groupImageUrl,correctAnswerTextMap:W,isCorrectMap:x})}case"WORD_ORDERING":{const n=E(G);return e(l,{partNumber:B,partName:v.name,instruction:v.instructions||n.instruction,questionCount:Q,questions:n.questions,answers:L,onAnswerChange:O,isReviewMode:!0,rawCorrectAnswerMap:k,isCorrectMap:x})}case"WORD_FILL_PARAGRAPH":{const n=I(G),t=n.wordBank.filter(e=>e.isExample).length,r=n.paragraph.match(/\{\d+\}/g),s=r?r.length:0,o=Math.max(1,s-t);let i=v.instructions||n.instruction;return i=i.replace(/\{FROM\}/gi,"1").replace(/\{TO\}/gi,String(o)),e(m,{partNumber:B,partName:v.name,questionCount:Q,instruction:i,questionId:n.questionId,paragraph:n.paragraph,title:n.title,imageUrl:n.imageUrl,wordBank:n.wordBank,viewMode:n.viewMode,answers:L,onAnswerChange:O,exampleAnswer:n.exampleAnswer,emailFrom:n.emailFrom,emailTo:n.emailTo,isReviewMode:!0,correctAnswerTextMap:W})}case"WRITE_SENTENCES":{const n=h(G);return e(w,{partNumber:B,partName:v.name,instruction:v.instructions||n.instruction,questionCount:Q,imageUrl:n.imageUrl,title:n.title,groupContent:n.groupContent,questions:n.questions,answers:L,onAnswerChange:O,isReviewMode:!0})}case"WORD_FILL_STRUCTURED_FORM":{const n=U(G);return e(p,{partNumber:B,partName:v.name,instruction:v.instructions||n.instruction,questionCount:Q,audioUrl:n.audioUrl||v.audioUrl,groupTitle:n.groupTitle,groupContent:n.groupContent,groupImageUrl:n.groupImageUrl,questions:n.questions,answers:L,onAnswerChange:O,isReviewMode:!0})}default:return null}}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Map review questions into SpeakingReviewItem for ReviewSpeakingRenderer.
3
+ * Source: oe-exam-fe ReviewQuestionRenderer speaking cases.
4
+ */
5
+ import type { ExamTakingQuestion } from '../../../api/exam-taking';
6
+ import type { SpeakingReviewItem } from './ReviewSpeakingRenderer.types';
7
+ export declare function mapSpeakingReviewItems(questions: ExamTakingQuestion[], questionType: string, answers: Record<string, unknown>, isCorrectMap: Record<string, boolean | null>): SpeakingReviewItem[];
@@ -0,0 +1 @@
1
+ function e(e){return e&&"object"==typeof e&&!Array.isArray(e)?e:{}}function r(e,r){const t=e[r];return"string"==typeof t?t:""}function t(e,r){const t=e[r];return"number"==typeof t&&Number.isFinite(t)?t:null}function i(t,i,n){if("READ_DISPLAYED_CONTENT"===t)return r(i,"referenceText");if("SPONTANEOUS_QA"===t)return r(i,"questionText");if("SPEAKING_CUE_CARD"===t)return r(i,"cueCardHtml");if("LISTEN_AND_SPEAK_QUESTION_LIST"===t){const r=Array.isArray(n.questions)?n.questions:[],t=i.activeQuestionId,o=r.find(r=>e(r).id===t);return o&&"string"==typeof e(o).text?String(e(o).text):r.map(r=>e(r).text).filter(e=>"string"==typeof e&&e.length>0).join("\n")}return"LISTEN_AND_SPEAK_IMAGE_GROUP"===t||"LISTEN_AND_SPEAK_ANSWER"===t||"LISTEN_AND_SPEAK_COMPARE_IMAGES"===t||"LISTEN_AND_SPEAK_WITH_STORY_IMAGES"===t||"LISTEN_AND_SPEAK_INFO_EXCHANGE"===t||"LISTEN_AND_SPEAK_ODD_ONE_OUT"===t?"":r(i,"question")||r(i,"title")}function n(t,i,n){if("LISTEN_AND_SPEAK_IMAGE_GROUP"===t){const r=Array.isArray(n.images)?n.images:[],t=i.activeImageId,o=r.find(r=>e(r).id===t),a=o?e(o).url:void 0;if("string"==typeof a&&a)return a;const s=r[0]?e(r[0]).url:void 0;return"string"==typeof s?s:""}if("LISTEN_AND_SPEAK_COMPARE_IMAGES"===t)return r(n,"candidateImageUrl")||r(n,"examinerImageUrl");if("LISTEN_AND_SPEAK_WITH_STORY_IMAGES"===t){const e=Array.isArray(i.images)?i.images:[];return"string"==typeof e[0]?e[0]:""}if("LISTEN_AND_SPEAK_ODD_ONE_OUT"===t){const r=Array.isArray(n.images)?n.images:[],t=Array.isArray(i.images)?i.images:[],o=i.activeImageId;if(o&&r.length>0){const t=r.find(r=>e(r).id===o),i=t?e(t).url:void 0;if("string"==typeof i&&i)return i}if(r.length>0){const t=e(r[0]).url;return"string"==typeof t?t:""}return t.length>0&&"string"==typeof t[0]?t[0]:""}return r(i,"backgroundImageUrl")||r(i,"imageUrl")}export function mapSpeakingReviewItems(o,a,s,c){return o.map(o=>{const u=o,y=e(o.content),A=e(s[o.id]),d=e(A.aiAssessment),g=e(d.pronunciation),_=e(A.llmAssessment),f=e(A.classification),l=e(u.correct_answer),p=function(r){const t=r;let i=t.questionGroup?.payload;if("string"==typeof i)try{i=JSON.parse(i)}catch{i={}}return e(i)}(o),S=e(_.feedback),m="string"==typeof S.vi&&S.vi||"string"==typeof S.en&&S.en||"",E="LISTEN_AND_SPEAK_COMPARE_IMAGES"===a?{candidateImageUrl:r(p,"candidateImageUrl"),examinerImageUrl:r(p,"examinerImageUrl")}:void 0,I="LISTEN_AND_SPEAK_WITH_STORY_IMAGES"===a?(Array.isArray(y.images)?y.images:[]).filter(e=>"string"==typeof e):void 0,N="LISTEN_AND_SPEAK_ODD_ONE_OUT"===a?function(r,t){return{groupImages:(Array.isArray(t.images)?t.images:[]).map((r,t)=>{const i=e(r);return{id:"string"==typeof i.id?i.id:String(t),url:"string"==typeof i.url?i.url:"",label:"string"==typeof i.label?i.label:"string"==typeof i.id?i.id:String(t+1)}}),contentImages:(Array.isArray(r.images)?r.images:[]).filter(e=>"string"==typeof e),activeId:"string"==typeof r.activeImageId?r.activeImageId:void 0}}(y,p):void 0,T="LISTEN_AND_SPEAK_INFO_EXCHANGE"===a?function(r){const t=Array.isArray(r.candidateFields)?r.candidateFields.map(r=>{const t=e(r);return{key:"string"==typeof t.key?t.key:"",value:"string"==typeof t.value?t.value:void 0,answer:"string"==typeof t.answer?t.answer:void 0}}):[],i=Array.isArray(r.examinerFields)?r.examinerFields.map(r=>{const t=e(r);return{key:"string"==typeof t.key?t.key:"",value:"string"==typeof t.value?t.value:void 0,answer:"string"==typeof t.answer?t.answer:void 0}}):[];return{title:"string"==typeof r.title?r.title:"",imageUrl:"string"==typeof r.imageUrl?r.imageUrl:"",candidateFields:t,examinerFields:i}}(p):void 0,v=Array.isArray(g.wordErrors)?g.wordErrors.map(r=>{const t=e(r);return{word:"string"==typeof t.word?t.word:"",errorType:"string"==typeof t.errorType?t.errorType:void 0,accuracyScore:"number"==typeof t.accuracyScore?t.accuracyScore:void 0}}):[],O=Array.isArray(l.expectedAnswers)?l.expectedAnswers.filter(e=>"string"==typeof e):[];return{questionId:o.id,questionNumber:o.question_number,questionType:a,promptText:i(a,y,p),promptAudioKey:r(y,"audioUrl")||r(y,"audio")||r(y,"questionAudio"),promptImageKey:n(a,y,p),studentAudioKey:r(A,"audioKey")||r(A,"answer"),score:"number"==typeof u.score?u.score:null,maxScore:Number(u.total_points||0),isCorrect:c[o.id]??null,expectedAnswers:O,recognizedText:r(g,"recognizedText"),pronunciationScore:t(g,"pronunciationScore"),accuracyScore:t(g,"accuracyScore"),fluencyScore:t(g,"fluencyScore"),prosodyScore:t(g,"prosodyScore"),completenessScore:t(g,"completenessScore"),wordErrors:v,weightedScore:t(d,"weightedScore"),contentScore:t(_,"contentScore"),feedbackEn:m,classificationTitle:r(f,"title"),classificationTag:r(f,"tag"),compareImages:E,storyImages:I,oddOneOutImages:N,infoExchange:T}})}
@@ -0,0 +1,5 @@
1
+ export interface BadgeTitleProps {
2
+ number: string;
3
+ title: string;
4
+ }
5
+ export declare function BadgeTitle({ number, title }: BadgeTitleProps): import("react").JSX.Element;
@@ -0,0 +1 @@
1
+ "use client";import{jsx as e,jsxs as t}from"react/jsx-runtime";export function BadgeTitle({number:r,title:s}){return t("div",{className:"absolute left-3 top-[-20px] z-10 flex items-center sm:left-5 sm:top-[-22px]",children:[e("div",{className:"relative z-[2] flex h-9 w-9 items-center justify-center rounded-full border-[3px] border-white bg-[radial-gradient(circle,_#2b78c5_0%,_#0054A6_100%)] text-lg font-extrabold text-white shadow-[0_2px_8px_rgba(0,0,0,0.2)] sm:h-11 sm:w-11 sm:text-xl",children:r}),e("div",{className:"ml-[-18px] rounded-r-[20px] bg-[linear-gradient(90deg,#0054A6_0%,#00AEEF_100%)] px-6 py-1.5 pl-7 text-sm font-bold text-white shadow-[2px_2px_5px_rgba(0,0,0,0.1)] [clip-path:polygon(0%_0%,90%_0%,100%_100%,0%_100%)] sm:ml-[-22px] sm:rounded-r-[25px] sm:px-10 sm:py-2 sm:pl-9 sm:text-base",children:s})]})}
@@ -0,0 +1,14 @@
1
+ import type { ComponentType } from 'react';
2
+ export interface ExamSkillRowProps {
3
+ icon: ComponentType<{
4
+ className?: string;
5
+ }>;
6
+ label: string;
7
+ score: number | null | undefined;
8
+ maxScore: number | null | undefined;
9
+ barClass: string;
10
+ }
11
+ /**
12
+ * Single skill score row for the general exam report (Reading / Listening / …).
13
+ */
14
+ export declare function ExamSkillRow({ icon: Icon, label, score, maxScore, barClass, }: ExamSkillRowProps): import("react").JSX.Element;
@@ -0,0 +1 @@
1
+ "use client";import{jsx as e,jsxs as t,Fragment as s}from"react/jsx-runtime";import{cn as x}from"../../../shared/lib/utils";import{formatScore as l}from"../utils/review-score.utils";export function ExamSkillRow({icon:r,label:a,score:m,maxScore:d,barClass:i}){if(null==d)return null;const n=d>0?d:0,p=0===n,c=m??0,o=n>0?Math.max(0,Math.min(100,c/n*100)):0,h=l(c,1),b=l(n,0);return t("div",{className:x("mb-2 flex items-center rounded-full border border-[#eef4fb] bg-white px-3 py-2 shadow-[0_1px_0_rgba(255,255,255,0.9)] last:mb-0 sm:mb-3 sm:px-4 sm:py-3",p&&"opacity-70 bg-slate-50 border-slate-200"),children:[e("div",{className:x("flex h-[48px] w-[48px] shrink-0 items-center justify-center rounded-full bg-white xs:h-[52px] xs:w-[52px] sm:h-[58px] sm:w-[58px]",p?"shadow-none border border-slate-100":"shadow-[0_4px_12px_rgba(15,23,42,0.08)]"),children:e("div",{className:x("flex h-[36px] w-[36px] items-center justify-center rounded-full text-white xs:h-[38px] xs:w-[38px] sm:h-[42px] sm:w-[42px]",p?"bg-slate-300":i),children:e(r,{className:"h-5 w-5 text-white"})})}),t("div",{className:"ml-2 mr-2 min-w-0 flex-1 border-r border-dashed border-[#d9e0ea] pr-2 xs:ml-3 xs:mr-3 xs:pr-3 sm:pr-4",children:[e("h4",{className:x("truncate text-[13px] font-extrabold uppercase leading-5 xs:text-[14px] sm:text-[15px]",p?"text-slate-400":"text-[#0054A6]"),children:a}),e("span",{className:x("mt-0.5 block truncate text-[10px] xs:text-[10.5px] sm:mt-1 sm:text-[11px]",p?"text-slate-400":"text-[#8b97a8]"),children:p?"Không áp dụng":"Kỹ năng"})]}),e("div",{className:x("w-[60px] shrink-0 text-[16px] font-extrabold xs:w-[70px] xs:text-[17px] sm:w-20 sm:text-xl",p?"text-slate-400":"text-[#0054A6]"),children:p?"—":t(s,{children:[h," ",t("small",{className:"text-[11px] font-semibold text-[#c5ced8] xs:text-[11.5px] sm:text-[13px]",children:["/",b]})]})}),e("div",{className:"ml-2 h-[10px] flex-1 overflow-hidden rounded-full bg-[#edf0f4] xs:ml-2.5 sm:ml-3 sm:h-[11px]",children:!p&&e("div",{className:x("h-full rounded-full",i),style:{width:`${o}%`}})})]})}
@@ -0,0 +1,31 @@
1
+ import { type ReactNode } from 'react';
2
+ export interface OceanEduExamReportProps {
3
+ studentName: string;
4
+ studentCode?: string | null;
5
+ branchName?: string | null;
6
+ examDateText: string;
7
+ totalScore: number | null;
8
+ maxScore: number;
9
+ isPendingGrading: boolean;
10
+ examSubTitle?: string | null;
11
+ examTitle?: string | null;
12
+ readingScore?: number | null;
13
+ maxReadingScore?: number | null;
14
+ listeningScore?: number | null;
15
+ maxListeningScore?: number | null;
16
+ writingScore?: number | null;
17
+ maxWritingScore?: number | null;
18
+ speakingScore?: number | null;
19
+ maxSpeakingScore?: number | null;
20
+ overallAssessmentSlot?: ReactNode;
21
+ /** Canonical review URL (with verification token) encoded into the QR code. */
22
+ reviewUrl?: string;
23
+ /** Optional host public path prefix for report assets. Falls back to bundled imports. */
24
+ assetBaseUrl?: string;
25
+ children?: ReactNode;
26
+ }
27
+ /**
28
+ * General (non-speaking) Ocean Edu exam result report shell.
29
+ * Source: oe-exam-fe/.../review/components/OceanEduExamReport.tsx
30
+ */
31
+ export declare function OceanEduExamReport({ studentName, studentCode, branchName, examDateText, totalScore, maxScore, isPendingGrading, examSubTitle, examTitle, readingScore, maxReadingScore, listeningScore, maxListeningScore, writingScore, maxWritingScore, speakingScore, maxSpeakingScore, overallAssessmentSlot, reviewUrl, assetBaseUrl, children, }: OceanEduExamReportProps): import("react").JSX.Element;
@@ -0,0 +1 @@
1
+ "use client";import{jsx as e,Fragment as t,jsxs as s}from"react/jsx-runtime";import{useEffect as a,useState as l}from"react";import{BookOpen as n,CalendarDays as x,Headphones as r,IdCard as i,MapPin as m,Mic as c,PenTool as d,Sparkles as o,User as p}from"lucide-react";import{formatScore as h,formatTotalScore as g,getScoreLabel as f,getScoreMessage as b,getScoreStroke as u}from"../utils/review-score.utils";import w from"./assets/logo-ocean.png";import N from"./assets/penguin-main.png";import v from"./assets/penguin-small.png";import F from"./assets/dofu2.png";import{BadgeTitle as E}from"./BadgeTitle";import{ExamSkillRow as y}from"./ExamSkillRow";import{InfoRow as A}from"./SpeakingReportShared";import{resolveAssetUrl as S}from"./speaking-report.utils";export function OceanEduExamReport({studentName:k,studentCode:j,branchName:_,examDateText:T,totalScore:C,maxScore:R,isPendingGrading:H,examSubTitle:M,examTitle:Q,readingScore:I,maxReadingScore:L,listeningScore:U,maxListeningScore:D,writingScore:P,maxWritingScore:G,speakingScore:O,maxSpeakingScore:B,overallAssessmentSlot:K,reviewUrl:q,assetBaseUrl:W,children:z}){const V=f(C,R,H),X=b(C,R,H),J=u(C,R,H),[Y,Z]=l(null),[$,ee]=l(null),te=Q?.toLowerCase().includes("placement test")||M?.toLowerCase().includes("placement test"),se=q??("undefined"!=typeof window?window.location.href:""),ae=S(w,W,"logo-ocean.png"),le=S(N,W,"penguin-main.png"),ne=S(v,W,"penguin-small.png"),xe=S(F,W,"dofu2.png");a(()=>{"undefined"!=typeof window&&import("qrcode").then(e=>{e.default.toDataURL(se,{margin:1,color:{dark:"#0054A6",light:"#ffffff"}}).then(Z).catch(()=>{}),e.default.toDataURL(window.location.origin,{margin:1,color:{dark:"#0054A6",light:"#ffffff"}}).then(ee).catch(()=>{})}).catch(()=>{})},[se]);const re=null!=L||null!=D||null!=G||null!=B;return e("div",{className:"mx-auto w-full max-w-[1120px] py-4 sm:px-4 sm:py-6 md:px-6",children:s("div",{className:"result-exam-container space-y-6",children:[s("div",{className:"overflow-hidden rounded-[12px] bg-white px-3 py-3 shadow-[0_20px_50px_rgba(0,0,0,0.15)] xs:px-4 xs:py-4 sm:rounded-[25px] sm:px-5 sm:py-5 md:rounded-[30px] md:px-8 md:py-6",children:[e("header",{className:"mb-6 md:mb-7",children:s("div",{className:"flex flex-col items-center gap-3 sm:gap-4 md:grid md:grid-cols-[auto_1fr_auto] md:items-center",children:[e("div",{className:"flex items-center justify-center md:justify-start",children:e("img",{src:ae,alt:"Ocean Edu",width:180,className:"h-auto w-[90px] object-contain xs:w-[120px] sm:w-[150px] md:w-[160px] lg:w-[180px]"})}),s("div",{className:"min-w-0 text-center",children:[e("h2",{className:"m-0 text-[16px] font-bold leading-tight text-[#0054A6] xs:text-[20px] sm:text-[22px] md:text-[24px] lg:text-[28px]",children:"PHIẾU KẾT QUẢ"}),e("h1",{className:"mt-1 text-[32px] font-black leading-[0.95] tracking-[-0.5px] text-[#0054A6] xs:text-[40px] sm:text-[40px] md:text-[48px] md:tracking-[-1px] lg:text-[64px]",children:s(t,te?{children:["PLACEMENT ",e("span",{className:"text-[#00AEEF]",children:"TEST"})]}:{children:["EXAM ",e("span",{className:"text-[#00AEEF]",children:"REPORT"})]})}),e("p",{className:"mx-auto mt-1.5 max-w-[520px] text-[12px] font-semibold leading-snug text-[#0054A6] xs:text-[16px] sm:text-sm md:mt-2 md:text-[16px]",children:"Đánh giá năng lực tiếng Anh toàn diện"})]}),e("div",{className:"flex items-center justify-center md:justify-end",children:Y?s("div",{className:"flex flex-col items-center",children:[e("a",{href:se||"#",target:"_blank",rel:"noopener noreferrer",className:"block transition-transform hover:scale-105",children:e("img",{src:Y,alt:"QR Code",className:"h-[80px] w-[80px] rounded-lg border-2 border-[#00AEEF] bg-white p-1 shadow-sm xs:h-[100px] xs:w-[100px] md:h-[120px] md:w-[120px]"})}),e("span",{className:"mt-1.5 text-[9px] font-bold text-[#00AEEF] xs:text-[11px]",children:"Quét hoặc nhấn QR để xem online"})]}):e("img",{src:le,alt:"Mascot",width:250,height:220,className:"h-auto w-[80px] object-contain xs:w-[120px] sm:w-[120px] md:w-[160px] lg:w-[220px]"})})]})}),s("section",{className:"relative mb-10 rounded-[20px] border-2 border-[#00AEEF] px-4 pb-5 pt-10 sm:px-5 sm:pb-6 sm:pt-11 md:px-8",children:[e(E,{number:"1",title:"THÔNG TIN HỌC VIÊN"}),s("div",{className:"flex flex-col items-center justify-between gap-6 md:flex-row md:items-center",children:[s("div",{className:"space-y-3 self-stretch sm:space-y-4",children:[e(A,{icon:p,label:"Họ tên học viên",value:k}),e(A,{icon:i,label:"Mã học viên",value:j||"Đang cập nhật"}),e(A,{icon:m,label:"Chi nhánh",value:_||M||"Ocean Edu"}),e(A,{icon:x,label:"Ngày đánh giá",value:T})]}),e("div",{className:"flex h-[100px] w-[200px] items-center justify-center overflow-hidden rounded-[80px] border-[4px] border-white bg-[linear-gradient(135deg,#EAF6FF_0%,#CFEFFF_45%,#9DDCFF_100%)] shadow-[0_10px_24px_rgba(0,174,239,0.18)] outline outline-2 outline-[#00AEEF] xs:h-[120px] xs:w-[240px] sm:h-[130px] sm:w-[260px] md:h-[150px] md:w-[280px] md:rounded-[100px] md:border-[6px]",children:e("img",{src:xe,alt:"Avatar",width:120,height:120,className:"h-auto w-[40%] scale-[1.2] object-contain md:scale-[1.4]"})})]})]}),s("section",{className:"relative rounded-[20px] border-2 border-[#00AEEF] px-5 pb-6 pt-11 md:px-8",children:[e(E,{number:"2",title:"KẾT QUẢ"}),s("div",{className:"grid gap-6 sm:gap-8 md:grid-cols-[1fr_1.3fr] md:gap-10",children:[s("div",{className:"flex flex-col items-center justify-center border-b border-dashed border-[#ccc] pb-6 text-center sm:pb-8 md:border-b-0 md:border-r md:pb-0 md:pr-8",children:[e("div",{className:"text-[13px] font-extrabold text-[#0054A6] xs:text-[14px] sm:text-[15px]",children:"ĐIỂM TỔNG"}),s("div",{className:"relative mx-auto my-4 flex h-[160px] w-[160px] items-center justify-center rounded-full xs:h-[180px] xs:w-[180px] sm:my-5 sm:h-[200px] sm:w-[200px]",style:{background:J},children:[e("div",{className:"absolute h-[136px] w-[136px] rounded-full bg-white xs:h-[153px] xs:w-[153px] sm:h-[170px] sm:w-[170px]"}),s("div",{className:"relative z-[2] text-center",children:[e("div",{className:"text-[44px] font-black leading-none text-[#0054A6] xs:text-[50px] sm:text-[54px] md:text-[80px]",children:H?"—":g(C)}),s("div",{className:"text-[18px] font-semibold text-[#aaa] xs:text-[20px] sm:text-[22px]",children:["/ ",h(R,0)]})]})]}),e("div",{className:"mt-2 text-[20px] font-extrabold text-[#2ecc71] xs:text-[22px] sm:text-[24px] md:text-[26px]",children:V}),s("div",{className:"mx-auto mt-4 flex w-full max-w-[600px] items-end gap-3 rounded-[20px] border border-[#e1efff] bg-[#F4F9FF] p-3 text-left sm:mt-6 sm:gap-4 sm:p-4",children:[e("img",{src:ne,alt:"Mascot Mini",width:95,height:95,className:"h-auto w-[60px] shrink-0 object-contain xs:w-[80px] sm:w-[90px] md:w-[95px]"}),s("p",{className:"m-0 text-[12px] leading-[1.5] text-[#666] xs:text-[13px]",children:[X,e(o,{className:"ml-1 inline h-4 w-4 text-[#FFD700]"})]})]})]}),s("div",{className:"flex flex-col justify-center rounded-[20px] border border-[#ddecff] bg-[#F4F9FF] p-4 sm:p-5",children:[e("div",{className:"mb-4 text-center text-[16px] font-extrabold text-[#0054A6] xs:text-[18px] sm:mb-5 sm:text-[20px]",children:"ĐIỂM THÀNH PHẦN"}),s("div",{className:"flex flex-col",children:[null!=L&&e(y,{icon:n,label:"Reading",score:I,maxScore:L,barClass:"bg-[#FF9800]"}),null!=D&&e(y,{icon:r,label:"Listening",score:U,maxScore:D,barClass:"bg-[#2196F3]"}),null!=G&&e(y,{icon:d,label:"Writing",score:P,maxScore:G,barClass:"bg-[#9C27B0]"}),null!=B&&e(y,{icon:c,label:"Speaking",score:O,maxScore:B,barClass:"bg-[#4CAF50]"}),!re&&e("div",{className:"py-4 text-center text-sm italic text-slate-500",children:"Không có điểm thành phần"})]})]})]})]}),K,s("footer",{className:"mt-5 grid gap-3 sm:mt-6 sm:gap-4 md:grid-cols-[1fr_1.5fr] md:gap-6",children:[s("div",{className:"flex items-center gap-3 rounded-[20px] bg-[#F4F9FF] p-4 xs:gap-4 sm:gap-5 sm:p-[18px]",children:[$&&e("a",{href:"/",target:"_blank",rel:"noopener noreferrer",className:"block shrink-0 transition-transform hover:scale-105",children:e("img",{src:$,alt:"Home QR",width:110,height:110,className:"w-[80px] rounded-[12px] bg-white object-contain p-[6px] shadow-[0_2px_8px_rgba(0,0,0,0.08)] xs:w-[95px] sm:w-[110px]"})}),s("div",{children:[e("h5",{className:"m-0 text-[14px] font-extrabold text-[#0054A6] xs:text-[15px] sm:text-base",children:"GỢI Ý PHÁT TRIỂN"}),e("p",{className:"mt-1 text-[11px] font-medium text-[#666] xs:text-[11.5px] sm:text-xs",children:"Quét mã QR để nhận bài luyện tập phù hợp với kết quả của bạn!"})]})]}),s("div",{className:"relative flex items-center justify-center rounded-[20px] border-l-[5px] border-[#00AEEF] bg-[#F4F9FF] p-4 text-[13px] font-bold italic leading-[1.4] text-[#0054A6] xs:text-[14px] sm:p-[18px] sm:text-[15px]",children:[e("span",{className:"absolute left-2 top-1 text-xl text-[#00AEEF]/20 xs:left-3 xs:top-2 xs:text-2xl",children:'"'}),"Hãy biến mỗi lần luyện tập thành một bước tiến tới thành công!",e("span",{className:"absolute bottom-1 right-2 text-xl text-[#00AEEF]/20 xs:bottom-2 xs:right-3 xs:text-2xl",children:'"'})]})]})]}),z?e("section",{className:"space-y-6",children:z}):null]})})}
@@ -0,0 +1,26 @@
1
+ import type { SpeakingRubric } from '../../../api/result-review';
2
+ import type { SpeakingReviewData } from '../utils/transform-speaking-review';
3
+ export interface OceanEduSpeakingReportProps {
4
+ reviewData: SpeakingReviewData;
5
+ studentName: string;
6
+ studentCode?: string | null;
7
+ branchName?: string | null;
8
+ examDateText: string;
9
+ examTitle?: string | null;
10
+ /** When false, hides the detailed per-question breakdown section. Defaults to true. */
11
+ showDetailedResults?: boolean;
12
+ /** Canonical review URL (with verification token) encoded into the QR code. */
13
+ reviewUrl?: string;
14
+ /**
15
+ * Attempt-level speaking rubric breakdown (Preschool C1–C5, KINDY C1–C7,
16
+ * KIDS 18 questions/4 sections, TEENS C1–C4).
17
+ */
18
+ speakingRubric?: SpeakingRubric | null;
19
+ /** Optional host public path prefix for report assets. Falls back to bundled imports. */
20
+ assetBaseUrl?: string;
21
+ }
22
+ /**
23
+ * Speaking result report shell.
24
+ * Source: oe-exam-fe/.../speaking-review/_components/OceanEduSpeakingReport.tsx
25
+ */
26
+ export declare function OceanEduSpeakingReport({ reviewData, studentName, studentCode, branchName, examDateText, examTitle, showDetailedResults, reviewUrl, speakingRubric, assetBaseUrl, }: OceanEduSpeakingReportProps): import("react").JSX.Element;
@@ -0,0 +1 @@
1
+ "use client";import{jsx as e,Fragment as t,jsxs as s}from"react/jsx-runtime";import{useEffect as a,useState as x}from"react";import{CalendarDays as r,GraduationCap as m,IdCard as n,MapPin as i,Sparkles as d,User as l}from"lucide-react";import{cn as p}from"../../../shared/lib/utils";import{buildSkillMetrics as c,deriveFeedback as o,deriveSkillScores as h,deriveSummaryPoints as g,getScoreLabel as f}from"../utils/speaking-summary";import b from"./assets/logo-ocean.png";import u from"./assets/penguin-main.png";import w from"./assets/penguin-small.png";import N from"./assets/dofu2.png";import v from"./assets/dofu4.png";import{BadgeTitle as F}from"./BadgeTitle";import{SpeakingPartSection as A}from"./SpeakingReportParts";import{InfoRow as E,SkillRow as y}from"./SpeakingReportShared";import{SpeakingRubricSection as k}from"./SpeakingRubricSection";import{formatScore as T,formatTotalScore as _,getScoreStroke as j,resolveAssetUrl as S,withUi as R}from"./speaking-report.utils";export function OceanEduSpeakingReport({reviewData:I,studentName:H,studentCode:C,branchName:P,examDateText:Q,examTitle:D,showDetailedResults:G,reviewUrl:U,speakingRubric:M,assetBaseUrl:L}){const[$,K]=x(null),[O,q]=x(null),B=D?.toLowerCase().includes("placement test")||I.examSubTitle?.toLowerCase().includes("placement test"),z=U??("undefined"!=typeof window?window.location.href:"");a(()=>{"undefined"!=typeof window&&import("qrcode").then(e=>{e.default.toDataURL(z,{margin:1,color:{dark:"#0054A6",light:"#ffffff"}}).then(K).catch(()=>{}),e.default.toDataURL(window.location.origin,{margin:1,color:{dark:"#0054A6",light:"#ffffff"}}).then(q).catch(()=>{})}).catch(()=>{})},[z]);const V=c(h(I)).map(R),X=o(I),J=g(I,V),W=f(I.totalScore,I.maxScore,I.isPendingGrading),Y=j(I.totalScore,I.maxScore,I.isPendingGrading),Z=S(b,L,"logo-ocean.png"),ee=S(u,L,"penguin-main.png"),te=S(w,L,"penguin-small.png"),se=S(N,L,"dofu2.png"),ae=S(v,L,"dofu4.png");return e("div",{className:"mx-auto w-full max-w-[1120px] py-4 sm:px-4 sm:py-6 md:px-6",children:s("div",{className:"space-y-6 result-exam-container",children:[s("div",{className:"overflow-hidden rounded-[12px] bg-white px-3 py-3 shadow-[0_20px_50px_rgba(0,0,0,0.15)] xs:px-4 xs:py-4 sm:rounded-[25px] sm:px-5 sm:py-5 md:rounded-[30px] md:px-8 md:py-6",children:[e("header",{className:"mb-6 md:mb-7",children:s("div",{className:"flex flex-col items-center gap-3 sm:gap-4 md:grid md:grid-cols-[auto_1fr_auto] md:items-center",children:[e("div",{className:"flex items-center justify-center md:justify-start",children:e("img",{src:Z,alt:"Ocean Edu",width:180,className:"h-auto w-[90px] object-contain xs:w-[120px] sm:w-[150px] md:w-[160px] lg:w-[180px]"})}),s("div",{className:"min-w-0 text-center",children:[e("h2",{className:"m-0 text-[16px] font-bold leading-tight text-[#0054A6] xs:text-[20px] sm:text-[22px] md:text-[24px] lg:text-[28px]",children:"PHIẾU KẾT QUẢ"}),e("h1",{className:"mt-1 text-[32px] font-black leading-[0.95] tracking-[-0.5px] text-[#0054A6] xs:text-[40px] sm:text-[40px] md:text-[48px] lg:text-[64px] md:tracking-[-1px]",children:s(t,B?{children:["PLACEMENT ",e("span",{className:"text-[#00AEEF]",children:"TEST"})]}:{children:["SPEAKING ",e("span",{className:"text-[#00AEEF]",children:"AI"})]})}),e("p",{className:"mx-auto mt-1.5 max-w-[520px] text-[12px] font-semibold leading-snug text-[#0054A6] xs:text-[16px] sm:text-sm md:text-[16px] md:mt-2",children:"Đánh giá năng lực nói tiếng Anh bằng trí tuệ nhân tạo"})]}),e("div",{className:"flex items-center justify-center md:justify-end",children:$?s("div",{className:"flex flex-col items-center",children:[e("a",{href:z||"#",target:"_blank",rel:"noopener noreferrer",className:"block transition-transform hover:scale-105",children:e("img",{src:$,alt:"QR Code",className:"h-[80px] w-[80px] rounded-lg border-2 border-[#00AEEF] bg-white p-1 shadow-sm xs:h-[100px] xs:w-[100px] md:h-[120px] md:w-[120px]"})}),e("span",{className:"mt-1.5 text-[9px] font-bold text-[#00AEEF] xs:text-[11px]",children:"Quét hoặc nhấn QR để xem online"})]}):e("img",{src:ee,alt:"Mascot",width:250,height:220,className:"h-auto w-[80px] object-contain xs:w-[120px] sm:w-[120px] md:w-[160px] lg:w-[220px]"})})]})}),s("section",{className:"relative mb-10 rounded-[20px] border-2 border-[#00AEEF] px-4 pb-5 pt-10 sm:px-5 sm:pb-6 sm:pt-11 md:px-8",children:[e(F,{number:"1",title:"THÔNG TIN HỌC VIÊN"}),s("div",{className:"flex flex-col items-center justify-between gap-6 md:flex-row md:items-center",children:[s("div",{className:"space-y-3 self-stretch sm:space-y-4",children:[e(E,{icon:l,label:"Họ tên học viên",value:H}),e(E,{icon:n,label:"Mã học viên",value:C||"Đang cập nhật"}),e(E,{icon:i,label:"Chi nhánh",value:P||I.examSubTitle||"Ocean Edu"}),e(E,{icon:r,label:"Ngày đánh giá",value:Q})]}),e("div",{className:"flex h-[100px] w-[200px] items-center justify-center overflow-hidden rounded-[80px] border-[4px] border-white bg-[linear-gradient(135deg,#EAF6FF_0%,#CFEFFF_45%,#9DDCFF_100%)] outline outline-2 outline-[#00AEEF] shadow-[0_10px_24px_rgba(0,174,239,0.18)] xs:h-[120px] xs:w-[240px] sm:h-[130px] sm:w-[260px] md:h-[150px] md:w-[280px] md:border-[6px] md:rounded-[100px]",children:e("img",{src:se,alt:"Avatar",width:120,height:120,className:"h-auto w-[40%] scale-[1.2] object-contain md:scale-[1.4]"})})]})]}),s("section",{className:"relative rounded-[20px] border-2 border-[#00AEEF] px-5 pb-6 pt-11 md:px-8",children:[e(F,{number:"2",title:"KẾT QUẢ"}),s("div",{className:"grid gap-6 sm:gap-8 md:grid-cols-[1fr_1.3fr] md:gap-10",children:[s("div",{className:"border-b border-dashed border-[#ccc] pb-6 text-center sm:pb-8 md:border-b-0 md:border-r md:pb-0 md:pr-8",children:[e("div",{className:"text-[13px] font-extrabold text-[#0054A6] xs:text-[14px] sm:text-[15px]",children:"ĐIỂM TỔNG"}),s("div",{className:"relative mx-auto my-4 flex h-[160px] w-[160px] items-center justify-center rounded-full xs:h-[180px] xs:w-[180px] sm:my-5 sm:h-[200px] sm:w-[200px]",style:{background:Y},children:[e("div",{className:"absolute h-[136px] w-[136px] rounded-full bg-white xs:h-[153px] xs:w-[153px] sm:h-[170px] sm:w-[170px]"}),s("div",{className:"relative z-[2]",children:[e("div",{className:"text-[44px] font-black leading-none text-[#0054A6] xs:text-[50px] sm:text-[54px] md:text-[80px]",children:I.isPendingGrading?"—":_(I.totalScore)}),s("div",{className:"text-center text-[18px] font-semibold text-[#aaa] xs:text-[20px] sm:text-[22px]",children:["/ ",T(I.maxScore,0)]})]})]}),e("div",{className:"mt-2 text-[20px] font-extrabold text-[#2ecc71] xs:text-[22px] sm:text-[24px] md:text-[26px]",children:W}),s("div",{className:"mt-4 flex items-end gap-3 rounded-[20px] border border-[#e1efff] bg-[#F4F9FF] p-3 text-left sm:mt-6 sm:gap-4 sm:p-4",children:[e("img",{src:te,alt:"Mascot Mini",width:95,height:95,className:"h-auto w-[60px] shrink-0 object-contain xs:w-[80px] sm:w-[90px] md:w-[95px]"}),s("p",{className:"m-0 text-[12px] leading-[1.5] text-[#666] xs:text-[13px]",children:[X,e(d,{className:"ml-1 inline h-4 w-4 text-[#FFD700]"})]})]})]}),s("div",{className:"rounded-[20px] border border-[#ddecff] bg-[#F4F9FF] p-4 sm:p-5",children:[e("div",{className:"mb-4 text-center text-[16px] font-extrabold text-[#0054A6] xs:text-[18px] sm:mb-5 sm:text-[20px]",children:"ĐIỂM THÀNH PHẦN"}),V.map(t=>e(y,{metric:t},t.key))]})]}),s("div",{className:"relative mt-6 rounded-[20px] border border-[#e1efff] bg-white p-4 sm:mt-8 sm:p-5 md:p-6",children:[s("div",{className:"mb-4 flex items-center text-[14px] font-extrabold text-[#0054A6] xs:text-[15px] sm:mb-5 sm:text-base",children:[e(m,{className:"mr-2 h-4 w-4 text-[#00AEEF] xs:h-[18px] xs:w-[18px] sm:h-5 sm:w-5"}),"NHẬN XÉT TỔNG QUAN TỪ AI"]}),e("div",{className:"grid gap-2.5 sm:gap-3 md:w-[75%] md:grid-cols-2",children:J.map((t,a)=>s("div",{className:p("flex items-start gap-2 text-[12px] font-medium xs:text-[12.5px] sm:text-[13px]",t.highlight?"font-extrabold text-[#f39c12]":"text-[#333]"),children:[e("span",{className:p("mt-0.5 text-[13px] xs:text-[13.5px] sm:text-sm","star"===t.icon?"text-[#f39c12]":"text-[#2ecc71]"),children:"star"===t.icon?"★":"●"}),e("span",{children:t.text})]},`${t.text}-${a}`))}),e("img",{src:ae,alt:"AI Robot",width:230,height:220,className:"mx-auto mt-5 h-auto w-[150px] object-contain xs:w-[160px] sm:mt-6 sm:w-[180px] md:mx-0 md:absolute md:bottom-[0px] md:right-0 md:mt-0 md:w-[230px]"})]})]}),M?e(k,{rubric:M,badgeNumber:"3"}):null,s("footer",{className:"mt-5 grid gap-3 sm:mt-6 sm:gap-4 md:grid-cols-[1fr_1.5fr] md:gap-6",children:[s("div",{className:"flex items-center gap-3 rounded-[20px] bg-[#F4F9FF] p-4 xs:gap-4 sm:gap-5 sm:p-[18px]",children:[O?e("a",{href:"/",target:"_blank",rel:"noopener noreferrer",className:"block shrink-0 transition-transform hover:scale-105",children:e("img",{src:O,alt:"Home QR",width:110,height:110,className:"w-[80px] rounded-[12px] bg-white p-[6px] object-contain shadow-[0_2px_8px_rgba(0,0,0,0.08)] xs:w-[95px] sm:w-[100px]"})}):null,s("div",{children:[e("h5",{className:"m-0 text-[14px] font-extrabold text-[#0054A6] xs:text-[15px] sm:text-base",children:"GỢI Ý PHÁT TRIỂN"}),e("p",{className:"mt-1 text-[11px] font-medium text-[#666] xs:text-[11.5px] sm:text-xs",children:"Quét mã QR để nhận bài luyện tập phù hợp với kết quả của bạn!"})]})]}),s("div",{className:"relative flex items-center justify-center rounded-[20px] border-l-[5px] border-[#00AEEF] bg-[#F4F9FF] p-4 text-[13px] font-bold italic leading-[1.4] text-[#0054A6] xs:text-[14px] sm:p-[18px] sm:text-[15px]",children:[e("span",{className:"absolute left-2 top-1 text-xl text-[#00AEEF]/20 xs:left-3 xs:top-2 xs:text-2xl",children:'"'}),"Hãy biến mỗi lần luyện tập thành một bước tiến tới thành công!",e("span",{className:"absolute bottom-1 right-2 text-xl text-[#00AEEF]/20 xs:bottom-2 xs:right-3 xs:text-2xl",children:'"'})]})]})]}),!1!==G?e("section",{className:"space-y-6",children:I.parts.map(t=>e(A,{part:t},`${t.partId}-${t.partSectionName}`))}):null]})})}
@@ -0,0 +1,24 @@
1
+ export interface LlmSkillAssessmentItem {
2
+ label: string;
3
+ percentage: number;
4
+ feedback: string;
5
+ }
6
+ export interface OverallAssessmentSectionProps {
7
+ /** True while LLM overall/skill assessment is still generating. */
8
+ isPending?: boolean;
9
+ /** True once the assessment failed to arrive within the allowed wait. */
10
+ isFailed?: boolean;
11
+ llmOverallAssessment?: string | null;
12
+ llmSkillAssessment?: Record<string, LlmSkillAssessmentItem | undefined> | null;
13
+ correctAnswers?: number | null;
14
+ totalQuestions?: number | null;
15
+ /** Section badge number. Defaults to "3". */
16
+ sectionNumber?: string;
17
+ /** Retries the assessment wait. The retry button is hidden when omitted. */
18
+ onRetry?: () => void;
19
+ }
20
+ /**
21
+ * Section 3 — overall LLM assessment table + pending skeleton.
22
+ * Source: oe-exam-fe/.../review/page.tsx overallAssessmentSlot
23
+ */
24
+ export declare function OverallAssessmentSection({ isPending, isFailed, llmOverallAssessment, llmSkillAssessment, correctAnswers, totalQuestions, sectionNumber, onRetry, }: OverallAssessmentSectionProps): import("react").JSX.Element;
@@ -0,0 +1 @@
1
+ "use client";import{jsx as e,jsxs as l}from"react/jsx-runtime";import{AlertCircle as t,RotateCw as a,Sparkles as n}from"lucide-react";import{BadgeTitle as s}from"./BadgeTitle";export function OverallAssessmentSection({isPending:r=!1,isFailed:i=!1,llmOverallAssessment:d,llmSkillAssessment:c,correctAnswers:o,totalQuestions:m,sectionNumber:h="3",onRetry:p}){const b=c?Object.values(c).filter(e=>Boolean(e)):[],x=Boolean(d)||b.length>0;return l("section",{className:"relative mt-8 rounded-[20px] border-2 border-[#00AEEF] px-5 pb-6 pt-11 md:px-8",children:[e(s,{number:h,title:"PHÂN TÍCH TỔNG QUAN"}),r&&!x&&l("div",{className:"flex flex-col items-center justify-center gap-3 py-10 text-center",children:[e("div",{className:"flex h-14 w-14 items-center justify-center rounded-full bg-[#d0edff]",children:e(n,{className:"h-7 w-7 animate-pulse text-[#00AEEF]"})}),e("p",{className:"text-sm font-bold text-[#0054A6]",children:"AI đang phân tích bài làm của bạn..."}),e("p",{className:"text-xs text-slate-500",children:"Kết quả sẽ tự động cập nhật sau vài giây. Vui lòng chờ."}),l("div",{className:"flex gap-1.5 pt-1",children:[e("span",{className:"h-2 w-2 animate-bounce rounded-full bg-[#00AEEF] [animation-delay:0ms]"}),e("span",{className:"h-2 w-2 animate-bounce rounded-full bg-[#00AEEF] [animation-delay:150ms]"}),e("span",{className:"h-2 w-2 animate-bounce rounded-full bg-[#00AEEF] [animation-delay:300ms]"})]})]}),i&&!x&&l("div",{className:"flex flex-col items-center justify-center gap-3 py-10 text-center",children:[e("div",{className:"flex h-14 w-14 items-center justify-center rounded-full bg-amber-50",children:e(t,{className:"h-7 w-7 text-amber-500"})}),e("p",{className:"text-sm font-bold text-slate-700",children:"Không thể tạo phân tích cho bài làm này."}),e("p",{className:"text-xs text-slate-500",children:"Điểm số của bạn vẫn được ghi nhận đầy đủ. Vui lòng thử lại, hoặc liên hệ giáo viên nếu tình trạng này tiếp diễn."}),p?l("button",{type:"button",onClick:p,className:"mt-1 inline-flex items-center gap-2 rounded-xl border border-[#00AEEF] px-4 py-2 text-sm font-semibold text-[#0054A6] transition hover:bg-[#f0f8ff]",children:[e(a,{className:"h-4 w-4"}),"Thử lại"]}):null]}),x&&e("div",{className:"overflow-hidden rounded-xl border border-[#00AEEF]/20 shadow-sm",children:l("table",{className:"w-full text-left text-sm text-slate-700",children:[e("thead",{className:"border-b border-[#00AEEF]/20 bg-[#f0f8ff] text-[#0054A6]",children:l("tr",{children:[e("th",{className:"w-[25%] border-r border-[#00AEEF]/10 px-4 py-3 font-bold"}),e("th",{className:"w-[25%] border-r border-[#00AEEF]/10 px-4 py-3 font-bold",children:"Câu đúng/Sai"}),e("th",{className:"w-[50%] px-4 py-3 font-bold",children:"Chi tiết"})]})}),l("tbody",{className:"divide-y divide-[#00AEEF]/10",children:[d?l("tr",{className:"hover:bg-slate-50",children:[e("td",{className:"border-r border-[#00AEEF]/10 px-4 py-4 align-top font-bold",children:"Đánh giá tổng quan"}),l("td",{className:"border-r border-[#00AEEF]/10 px-4 py-4 align-top font-medium text-slate-600",children:[o??"—"," / ",m??"—"]}),e("td",{className:"whitespace-pre-wrap px-4 py-4 align-top leading-relaxed text-slate-700",children:d})]}):null,b.length>0?l("tr",{className:"hover:bg-slate-50",children:[e("td",{className:"border-r border-[#00AEEF]/10 px-4 py-4 align-top font-bold",children:"Đánh giá kỹ năng"}),e("td",{className:"px-0 py-0 align-top",colSpan:2,children:e("table",{className:"h-full w-full",children:e("tbody",{className:"divide-y divide-[#00AEEF]/10",children:b.map((t,a)=>l("tr",{className:"group",children:[l("td",{className:"w-1/3 border-r border-[#00AEEF]/10 px-4 py-4 align-top group-hover:bg-slate-50",children:[e("div",{className:"font-semibold text-slate-800",children:t.label}),l("div",{className:"mt-1 text-xs text-slate-500",children:["(Tỷ lệ"," ",null!=t.percentage?t.percentage.toFixed(2):"0.00","%)"]})]}),e("td",{className:"w-2/3 px-4 py-4 align-top leading-relaxed text-slate-700 group-hover:bg-slate-50",children:t.feedback})]},`${t.label}-${a}`))})})})]}):null]})]})})]})}
@@ -0,0 +1,14 @@
1
+ export interface ResultErrorStateProps {
2
+ /** Error object or message string. */
3
+ error?: unknown;
4
+ /** Fallback message when error has no message. */
5
+ fallbackMessage?: string;
6
+ /** Back / retry CTA label. */
7
+ backLabel?: string;
8
+ onBack?: () => void;
9
+ }
10
+ /**
11
+ * Generic full-page error state for the result review screen.
12
+ * Source: oe-exam-fe/.../review/page.tsx generic error branch
13
+ */
14
+ export declare function ResultErrorState({ error, fallbackMessage, backLabel, onBack, }: ResultErrorStateProps): import("react").JSX.Element;
@@ -0,0 +1 @@
1
+ "use client";import{jsx as e,jsxs as t}from"react/jsx-runtime";import{ChevronLeft as s,XCircle as a}from"lucide-react";export function ResultErrorState({error:l,fallbackMessage:r="Đã xảy ra lỗi khi tải kết quả bài thi.",backLabel:i="Quay lại danh sách",onBack:n}){const c=function(e,t){return e instanceof Error&&e.message?e.message:"string"==typeof e&&e.trim()?e:t}(l,r);return e("div",{className:"flex min-h-screen flex-col bg-[#e0e0e0]",children:e("div",{className:"flex flex-1 items-center justify-center px-4 py-10",children:t("div",{className:"w-full max-w-md rounded-[28px] bg-white p-8 text-center shadow-xl",children:[e("div",{className:"mx-auto mb-5 flex h-16 w-16 items-center justify-center rounded-full bg-red-50",children:e(a,{className:"h-8 w-8 text-red-400"})}),e("h2",{className:"text-base font-bold text-slate-800",children:"Không thể tải kết quả"}),e("p",{className:"mt-2 text-sm text-slate-500",children:c}),n?t("button",{type:"button",onClick:n,className:"mt-6 inline-flex items-center gap-2 rounded-xl bg-[#2874A6] px-5 py-2.5 text-sm font-semibold text-white shadow-sm transition-all hover:bg-[#1A5276] hover:shadow-md active:scale-[0.98]",children:[e(s,{className:"h-4 w-4"}),i]}):null]})})})}
@@ -0,0 +1,11 @@
1
+ export interface ResultLoadingStateProps {
2
+ /** Primary loading message. */
3
+ message?: string;
4
+ /** Secondary helper text under the message. */
5
+ description?: string;
6
+ }
7
+ /**
8
+ * Full-page loading state for the result review screen.
9
+ * Source: oe-exam-fe/.../review/page.tsx isLoading branch
10
+ */
11
+ export declare function ResultLoadingState({ message, description, }: ResultLoadingStateProps): import("react").JSX.Element;
@@ -0,0 +1 @@
1
+ "use client";import{jsx as e,jsxs as t}from"react/jsx-runtime";import{Loader2 as s}from"lucide-react";export function ResultLoadingState({message:i="Đang tải kết quả bài thi...",description:l="Vui lòng đợi trong giây lát"}){return e("div",{className:"flex min-h-screen flex-col bg-[#e0e0e0]",children:e("div",{className:"flex flex-1 items-center justify-center px-4 py-10",children:t("div",{className:"text-center",children:[t("div",{className:"relative mx-auto h-16 w-16",children:[e("div",{className:"absolute inset-0 animate-ping rounded-full bg-[#2874A6]/20"}),e("div",{className:"relative flex h-16 w-16 items-center justify-center rounded-full bg-white shadow-lg",children:e(s,{className:"h-8 w-8 animate-spin text-[#2874A6]"})})]}),e("p",{className:"mt-5 text-sm font-semibold text-slate-600",children:i}),l?e("p",{className:"mt-1 text-xs text-slate-500",children:l}):null]})})})}
@@ -0,0 +1,16 @@
1
+ export interface ResultNotPublicStateProps {
2
+ /**
3
+ * Login destination. When omitted, builds `/auth/login?redirect=...` from the
4
+ * current URL (host can override via prop or `onLogin` navigation).
5
+ */
6
+ loginHref?: string;
7
+ /** Optional host public path prefix for report assets. Falls back to bundled imports. */
8
+ assetBaseUrl?: string;
9
+ /** Optional custom login handler (e.g. host router). Falls back to anchor navigation. */
10
+ onLogin?: (href: string) => void;
11
+ }
12
+ /**
13
+ * Friendly full-page state when the result is private (403 not-public).
14
+ * Source: oe-exam-fe/.../review/page.tsx isResultNotPublic branch
15
+ */
16
+ export declare function ResultNotPublicState({ loginHref, assetBaseUrl, onLogin, }: ResultNotPublicStateProps): import("react").JSX.Element;
@@ -0,0 +1 @@
1
+ "use client";import{jsx as e,jsxs as t}from"react/jsx-runtime";import{Lock as n}from"lucide-react";import a from"./assets/logo-ocean.png";import i from"./assets/penguin-small.png";import{resolveAssetUrl as r}from"./speaking-report.utils";export function ResultNotPublicState({loginHref:o,assetBaseUrl:s,onLogin:l}){const c=o??("undefined"==typeof window?"/auth/login":`/auth/login?redirect=${encodeURIComponent(window.location.href)}`),d=r(a,s,"logo-ocean.png"),m=r(i,s,"penguin-small.png");return e("div",{className:"flex min-h-screen flex-col items-center justify-center bg-gradient-to-br from-[#eaf4ff] via-[#f0f7ff] to-[#e8f0ff] px-4 py-10",children:e("div",{className:"w-full max-w-md",children:t("div",{className:"relative overflow-hidden rounded-[28px] bg-white px-8 py-10 text-center shadow-[0_20px_60px_rgba(0,84,166,0.12)]",children:[e("div",{className:"absolute inset-x-0 top-0 h-1.5 bg-gradient-to-r from-[#00AEEF] via-[#0054A6] to-[#00AEEF]"}),e("div",{className:"mb-6 flex justify-center",children:e("img",{src:d,alt:"Ocean Edu",className:"h-auto w-[120px] object-contain"})}),e("div",{className:"mx-auto mb-5 flex h-20 w-20 items-center justify-center rounded-full bg-gradient-to-br from-[#eaf4ff] to-[#d0e8ff] ring-4 ring-[#00AEEF]/20",children:e(n,{className:"h-9 w-9 text-[#0054A6]",strokeWidth:1.8})}),e("h2",{className:"text-[20px] font-black text-[#0054A6]",children:"Bạn cần đăng nhập để xem kết quả"}),t("p",{className:"mt-3 text-sm leading-relaxed text-slate-500",children:["Kết quả bài thi này chỉ dành cho học viên đã đăng nhập.",e("br",{}),"Vui lòng đăng nhập vào tài khoản Ocean Edu của bạn để tiếp tục."]}),e("div",{className:"my-6 border-t border-dashed border-slate-200"}),e("a",{href:c,onClick:e=>{l&&(e.preventDefault(),l(c))},className:"inline-flex w-full items-center justify-center gap-2 rounded-2xl bg-gradient-to-r from-[#0054A6] to-[#00AEEF] px-6 py-3 text-sm font-bold text-white shadow-md shadow-[#00AEEF]/30 transition-all hover:opacity-90 hover:shadow-lg active:scale-[0.98]",children:"Đăng nhập ngay"}),e("div",{className:"my-5 border-t border-dashed border-slate-200"}),t("div",{className:"flex items-start gap-3 rounded-2xl bg-[#f4f9ff] px-4 py-3 text-left",children:[e("img",{src:m,alt:"",className:"h-auto w-10 shrink-0 object-contain"}),e("p",{className:"text-[12.5px] leading-relaxed text-[#555]",children:"Sau khi đăng nhập, bạn sẽ có thể xem đầy đủ kết quả và chi tiết bài thi của mình! 💙"})]})]})})})}
@@ -0,0 +1,4 @@
1
+ import type { SpeakingReviewPart } from '../utils/transform-speaking-review';
2
+ export declare function SpeakingPartSection({ part }: {
3
+ part: SpeakingReviewPart;
4
+ }): import("react").JSX.Element;
@@ -0,0 +1 @@
1
+ "use client";import{jsxs as e,jsx as t}from"react/jsx-runtime";import{BadgeTitle as r}from"./BadgeTitle";import{QuestionCard as i}from"./SpeakingReportShared";import{formatScore as n,getPartScore as a}from"./speaking-report.utils";function s({part:t}){const r=a(t);return r.hasPending||null==r.score?null:e("div",{className:"inline-flex items-center rounded-full bg-[#0054A6] px-4 py-2 text-sm font-bold text-white shadow-sm",children:["Điểm: ",Math.round(r.score)," / ",n(r.maxScore,0)]})}export function SpeakingPartSection({part:n}){return e("section",{className:"relative rounded-[20px] border-2 border-[#00AEEF] bg-white px-5 pb-6 pt-11 md:px-8",children:[t(r,{number:String(n.partNo),title:n.partName||`PHẦN ${n.partNo}`}),e("div",{className:"flex flex-col gap-4 md:flex-row md:items-start md:justify-between",children:[e("div",{className:"space-y-2",children:[t("h3",{className:"text-xl font-black text-[#0054A6]",children:n.partSectionName||"Speaking section"}),n.instructions?t("p",{className:"max-w-3xl text-sm leading-6 text-slate-600",children:n.instructions}):null]}),t(s,{part:n})]}),t("div",{className:"mt-6 grid gap-4",children:n.questions.map(e=>t(i,{question:e},e.questionId))})]})}
@@ -0,0 +1,36 @@
1
+ import type { ComponentType } from 'react';
2
+ import type { SpeakingReviewQuestion } from '../utils/transform-speaking-review';
3
+ import { type SkillMetric } from './speaking-report.utils';
4
+ export declare function InfoRow({ icon: Icon, label, value, }: {
5
+ icon: ComponentType<{
6
+ className?: string;
7
+ }>;
8
+ label: string;
9
+ value: string;
10
+ }): import("react").JSX.Element;
11
+ export declare function SkillRow({ metric, compact }: {
12
+ metric: SkillMetric;
13
+ compact?: boolean;
14
+ }): import("react").JSX.Element;
15
+ export declare function PendingGradingCard({ gradingStatus }: {
16
+ gradingStatus: string | null | undefined;
17
+ }): import("react").JSX.Element;
18
+ export declare function NotAnsweredCard(): import("react").JSX.Element;
19
+ export declare function QuestionScorePill({ question }: {
20
+ question: SpeakingReviewQuestion;
21
+ }): import("react").JSX.Element;
22
+ export declare function PromptBlock({ question }: {
23
+ question: SpeakingReviewQuestion;
24
+ }): import("react").JSX.Element;
25
+ export declare function AnswerBlock({ question }: {
26
+ question: SpeakingReviewQuestion;
27
+ }): import("react").JSX.Element;
28
+ export declare function QuestionComponentScores({ question }: {
29
+ question: SpeakingReviewQuestion;
30
+ }): import("react").JSX.Element;
31
+ export declare function FeedbackBlock({ question }: {
32
+ question: SpeakingReviewQuestion;
33
+ }): import("react").JSX.Element;
34
+ export declare function QuestionCard({ question }: {
35
+ question: SpeakingReviewQuestion;
36
+ }): import("react").JSX.Element;
@@ -0,0 +1 @@
1
+ "use client";import{jsx as e,jsxs as t}from"react/jsx-runtime";import{CheckCheck as s,Clock as r,MessageSquare as l,MessageSquareQuote as a,Music2 as n,Volume2 as i,Zap as d}from"lucide-react";import{ReviewGradingStatus as c}from"../../../api/result-review";import{ResolvedImage as x}from"../../../components/common/ResolvedImage";import{cn as o}from"../../../shared/lib/utils";import{SpeakingResultAudioPlayer as m}from"../speaking/SpeakingResultAudioPlayer";import{deriveQuestionSkillMetrics as p,extractPrompt as u,extractQuestionFeedback as h,extractStudentAnswerText as f,formatScore as b,getPendingGradingLabel as g,getQuestionTypeLabel as N,isGradingComplete as w}from"./speaking-report.utils";export function InfoRow({icon:s,label:r,value:l}){return t("p",{className:"flex items-start text-[13px] font-medium text-[#333] xs:text-sm sm:text-base md:text-lg",children:[e(s,{className:"mr-2 mt-0.5 h-[14px] w-[14px] shrink-0 text-[#0054A6] xs:h-[16px] xs:w-[16px] sm:mr-3 sm:h-[18px] sm:w-[18px]"}),t("span",{className:"flex flex-wrap items-baseline gap-x-1.5",children:[t("span",{className:"whitespace-nowrap",children:[r," :"]}),e("strong",{suppressHydrationWarning:!0,className:"font-bold text-[#0054A6]",children:l})]})]})}function v({metric:t}){const r="h-[18px] w-[18px] text-white";switch(t.icon){case"speaker":return e(i,{className:r});case"bolt":return e(d,{className:r});case"message":return e(l,{className:r});case"check-double":return e(s,{className:r});case"music":return e(n,{className:r});default:return null}}export function SkillRow({metric:s,compact:r=!1}){const l=b(r?s.score/10:s.score,1),a=r?"/10":"/100";return t("div",{className:o("flex items-center rounded-full border border-[#eef4fb] bg-white shadow-[0_1px_0_rgba(255,255,255,0.9)] last:mb-0",r?"min-h-[70px] pl-[12px] pr-4":"mb-2 px-3 py-2 sm:mb-3 sm:px-4 sm:py-3"),children:[e("div",{className:"flex h-[48px] w-[48px] shrink-0 items-center justify-center rounded-full bg-white shadow-[0_4px_12px_rgba(15,23,42,0.08)] xs:h-[52px] xs:w-[52px] sm:h-[58px] sm:w-[58px]",children:e("div",{className:o("flex items-center justify-center rounded-full text-white",r?"h-[42px] w-[42px]":"h-[36px] w-[36px] xs:h-[38px] xs:w-[38px] sm:h-[42px] sm:w-[42px]",s.barClass),children:e(v,{metric:s})})}),t("div",{className:o("ml-2 mr-2 flex-1 border-r border-dashed border-[#d9e0ea] pr-2 xs:ml-3 xs:mr-3 xs:pr-3 sm:pr-4",r?"w-[148px]":"min-w-0"),children:[e("h4",{className:o("truncate font-extrabold leading-5 text-[#0054A6]",r?"text-[15px]":"text-[13px] xs:text-[14px] sm:text-[15px]"),children:s.label}),e("span",{className:o("mt-0.5 block truncate text-[#8b97a8] sm:mt-1",r?"text-[11px]":"text-[10px] xs:text-[10.5px] sm:text-[11px]"),children:s.sublabel})]}),t("div",{className:o("shrink-0 font-extrabold text-[#0054A6]",r?"w-[86px] text-[18px]":"w-[60px] text-[16px] xs:w-[70px] xs:text-[17px] sm:w-20 sm:text-xl"),children:[l," ",e("small",{className:o("font-semibold text-[#c5ced8]",r?"text-[12px]":"text-[11px] xs:text-[11.5px] sm:text-[13px]"),children:a})]}),e("div",{className:o("ml-2 flex-1 overflow-hidden rounded-full bg-[#edf0f4] xs:ml-2.5 sm:ml-3",r?"h-[12px]":"h-[10px] sm:h-[11px]"),children:e("div",{className:o("h-full rounded-full",s.barClass),style:{width:`${s.score}%`}})})]})}export function PendingGradingCard({gradingStatus:s}){const l=g(s),a=s===c.FAILED;return t("div",{className:o("flex flex-col items-center justify-center gap-3 rounded-2xl border px-6 py-8 text-center",a?"border-orange-200 bg-orange-50":"border-[#dbeafe] bg-[#F4F9FF]"),children:[e("div",{className:o("flex h-14 w-14 items-center justify-center rounded-full",a?"bg-orange-100":"bg-[#E8F6FF]"),children:e(r,{className:o("h-7 w-7 animate-pulse",a?"text-orange-500":"text-[#00AEEF]")})}),t("div",{children:[e("p",{className:o("text-base font-bold",a?"text-orange-600":"text-[#0054A6]"),children:l}),e("p",{className:"mt-1 text-sm text-slate-500",children:"Kết quả sẽ được cập nhật sớm nhất có thể."})]})]})}export function NotAnsweredCard(){return t("div",{className:"flex flex-col items-center justify-center gap-3 rounded-2xl border border-slate-200 bg-slate-50 px-6 py-8 text-center",children:[e("div",{className:"flex h-14 w-14 items-center justify-center rounded-full bg-slate-100",children:e(i,{className:"h-7 w-7 text-slate-300"})}),t("div",{children:[e("p",{className:"text-base font-bold text-slate-500",children:"Học sinh chưa trả lời câu hỏi"}),e("p",{className:"mt-1 text-sm text-slate-400",children:"Câu hỏi này không có bài ghi âm."})]})]})}export function QuestionScorePill({question:s}){return s.gradingStatus===c.NOT_ANSWERED?t("div",{className:"inline-flex items-center rounded-full bg-slate-100 px-3.5 py-2 text-sm font-bold text-slate-500 shadow-sm ring-1 ring-slate-200",children:["Bỏ qua — 0 / ",b(s.maxScore,0)]}):w(s.gradingStatus)?e("div",{className:"inline-flex items-center rounded-full bg-[#E8F6FF] px-3.5 py-2 text-sm font-extrabold text-[#0054A6] shadow-sm ring-1 ring-[#b9dcff]",children:null==s.score?`Chưa chấm / ${b(s.maxScore,0)}`:`${b(s.score,2)} / ${b(s.maxScore,0)}`}):t("div",{className:"inline-flex items-center gap-1.5 rounded-full bg-[#FFF8E8] px-3.5 py-2 text-sm font-bold text-[#D97706] shadow-sm ring-1 ring-[#fcd34d]",children:[e(r,{className:"h-3.5 w-3.5"}),"Chờ chấm"]})}export function PromptBlock({question:s}){const r=s.questionContent??{},l=r.imageUrl||r.image||r.promptImageUrl,a=s.audioUrl||r.audioUrl||r.audio||r.questionAudio,n=u(s);return t("div",{className:"rounded-xl border border-[#dbeafe] bg-[#F8FBFF] px-4 py-3",children:[e("div",{className:"mb-2 text-xs font-bold uppercase tracking-wide text-[#00AEEF]",children:"Câu hỏi"}),n.text&&"Nội dung câu hỏi đang được cập nhật."!==n.text&&(n.isHtml?e("div",{className:"mb-3 text-sm leading-relaxed text-slate-700 [&_ul]:list-disc [&_ul]:pl-5 [&_ol]:list-decimal [&_ol]:pl-5 [&_li]:my-0.5 [&_p]:my-1",dangerouslySetInnerHTML:{__html:n.text}}):e("p",{className:"mb-3 text-center text-sm leading-relaxed text-slate-700",children:n.text})),a?e("div",{className:"mb-3",children:e(m,{audioKey:a})}):null,l?e("div",{className:"overflow-hidden rounded-lg border border-slate-200",children:e(x,{src:l,alt:"Question image",className:"h-auto w-full"})}):null]})}export function AnswerBlock({question:s}){const r=f(s);return s.studentAnswerKey?t("div",{className:"rounded-xl border border-slate-200 bg-white px-4 py-3",children:[t("div",{className:"mb-2.5 flex items-center gap-2 text-xs font-bold text-[#0054A6]",children:[e(i,{className:"h-3.5 w-3.5 text-[#00AEEF]"}),"Bài nói của học viên"]}),e(m,{audioKey:s.studentAnswerKey})]}):t("div",{className:"rounded-xl border border-slate-200 bg-slate-50 px-4 py-3",children:[e("div",{className:"mb-1.5 text-[10px] font-semibold uppercase tracking-wider text-slate-400",children:"Câu trả lời của học viên"}),e("p",{className:"text-sm leading-relaxed text-slate-600",children:r})]})}export function QuestionComponentScores({question:s}){const r=p(s);return t("div",{className:"rounded-[28px] border border-[#cfe4ff] bg-[#F3F8FF] px-4 py-5 md:px-5 md:py-6",children:[e("div",{className:"mb-5 text-center text-[20px] font-black uppercase tracking-tight text-[#0054A6]",children:"ĐIỂM THÀNH PHẦN"}),e("div",{className:"space-y-3",children:r.map(t=>e(SkillRow,{metric:t,compact:!0},`${s.questionId}-${t.key}`))})]})}export function FeedbackBlock({question:s}){return t("div",{className:"rounded-xl border border-[#ffe6b3] bg-[#FFF8E8] px-4 py-3",children:[t("div",{className:"mb-1.5 flex items-center gap-1.5 text-xs font-bold text-[#D97706]",children:[e(a,{className:"h-3.5 w-3.5"}),"Nhận xét AI"]}),e("p",{className:"text-sm leading-relaxed text-slate-700",children:h(s)})]})}export function QuestionCard({question:s}){const r=w(s.gradingStatus),l=s.gradingStatus===c.NOT_ANSWERED;return t("article",{className:"rounded-[28px] border border-[#cfe4ff] bg-white p-4 shadow-[0_10px_28px_rgba(15,23,42,0.06)] md:p-6",children:[e("div",{className:"mb-5 rounded-[22px] border border-[#dbeafe] bg-[linear-gradient(135deg,#f8fbff_0%,#eef6ff_100%)] px-4 py-4 md:px-5",children:t("div",{className:"flex flex-col gap-4 md:flex-row md:items-start md:justify-between",children:[t("div",{className:"min-w-0",children:[t("div",{className:"text-xl font-black text-[#0054A6]",children:["Câu ",s.questionNumber]}),e("div",{className:"mt-1 text-sm font-semibold text-slate-500",children:N(s.questionType)})]}),e("div",{className:"shrink-0 self-start",children:e(QuestionScorePill,{question:s})})]})}),t("div",r?{className:"grid gap-5 xl:grid-cols-[minmax(0,1fr)_minmax(0,1fr)] xl:items-start",children:[t("div",{className:"space-y-4",children:[e(PromptBlock,{question:s}),e(AnswerBlock,{question:s}),e(FeedbackBlock,{question:s})]}),e("aside",{className:"min-w-0",children:e(QuestionComponentScores,{question:s})})]}:l?{className:"space-y-4",children:[e(PromptBlock,{question:s}),e(NotAnsweredCard,{})]}:{className:"space-y-4",children:[e(PromptBlock,{question:s}),e(PendingGradingCard,{gradingStatus:s.gradingStatus})]})]})}
@@ -0,0 +1,21 @@
1
+ import type { SpeakingRubric } from '../../../api/result-review';
2
+ export declare enum SpeakingRubricScoringVersion {
3
+ TEENS_FOCUS_PEARSON_V2 = "TEENS_FOCUS_PEARSON_V2",
4
+ PRESCHOOL_PLACEMENT_V1 = "PRESCHOOL_PLACEMENT_V1",
5
+ KINDY_PLACEMENT_V1 = "KINDY_PLACEMENT_V1"
6
+ }
7
+ export declare enum KidsRubricSectionCode {
8
+ WARM_UP = "WARM_UP",
9
+ PART_1 = "PART_1",
10
+ PART_2 = "PART_2",
11
+ PART_3 = "PART_3"
12
+ }
13
+ export interface SpeakingRubricSectionProps {
14
+ rubric: SpeakingRubric;
15
+ badgeNumber?: string;
16
+ }
17
+ /**
18
+ * Attempt-level speaking rubric breakdown for TEENS / PRESCHOOL / KINDY / KIDS.
19
+ * Source: oe-exam-fe/.../review/components/SpeakingRubricSection.tsx
20
+ */
21
+ export declare function SpeakingRubricSection({ rubric, badgeNumber, }: SpeakingRubricSectionProps): import("react").JSX.Element;
@@ -0,0 +1 @@
1
+ "use client";import{jsx as e,jsxs as n}from"react/jsx-runtime";import{Star as t}from"lucide-react";import{BadgeTitle as i}from"./BadgeTitle";export var SpeakingRubricScoringVersion;!function(e){e.TEENS_FOCUS_PEARSON_V2="TEENS_FOCUS_PEARSON_V2",e.PRESCHOOL_PLACEMENT_V1="PRESCHOOL_PLACEMENT_V1",e.KINDY_PLACEMENT_V1="KINDY_PLACEMENT_V1"}(SpeakingRubricScoringVersion||(SpeakingRubricScoringVersion={}));export var KidsRubricSectionCode;!function(e){e.WARM_UP="WARM_UP",e.PART_1="PART_1",e.PART_2="PART_2",e.PART_3="PART_3"}(KidsRubricSectionCode||(KidsRubricSectionCode={}));const c={[SpeakingRubricScoringVersion.TEENS_FOCUS_PEARSON_V2]:{C1:{vi:"Ngữ pháp & Từ vựng",en:"Grammar & Vocabulary",hint:"Cấu trúc câu, thì động từ và độ đa dạng từ vựng"},C2:{vi:"Phát âm",en:"Pronunciation",hint:"Độ rõ ràng, trọng âm từ và ngữ điệu câu"},C3:{vi:"Giao tiếp tương tác",en:"Interactive Communication",hint:"Phản hồi đủ ý và tự mở rộng câu trả lời"},C4:{vi:"Quản lý diễn ngôn",en:"Discourse Management",hint:"Mạch lạc, tổ chức và phát triển ý rõ ràng"}},[SpeakingRubricScoringVersion.PRESCHOOL_PLACEMENT_V1]:{C1:{vi:"Nhận biết số đếm (1–10)",en:"Cardinal Numbers (1–10)",hint:"Đếm 1–10 và nhận ra từng số"},C2:{vi:"Nhận biết màu sắc",en:"Basic Colours",hint:"Gọi tên màu sắc và màu yêu thích"},C3:{vi:"Nhận biết hình học",en:"Basic Shapes",hint:"Gọi tên các hình cơ bản"},C4:{vi:"Nhận biết động vật",en:"Animals",hint:"Gọi tên động vật và con vật yêu thích"}},[SpeakingRubricScoringVersion.KINDY_PLACEMENT_V1]:{C1:{vi:"Chào hỏi & Giới thiệu bản thân",en:"Greeting & Self-Introduction",hint:"Giao tiếp xã hội cơ bản"},C2:{vi:"Nhận biết số đếm (1–20)",en:"Cardinal Numbers (1–20)",hint:"Từ vựng số học"},C3:{vi:"Đọc bảng chữ cái",en:"The Alphabet",hint:"Nhận biết chữ cái"},C4:{vi:"Nhận biết hình dạng",en:"Shapes",hint:"Từ vựng hình học"},C5:{vi:"Nhận biết màu sắc",en:"Colours",hint:"Từ vựng màu sắc"},C6:{vi:"Nhận biết động vật",en:"Animals",hint:"Từ vựng chủ đề quen thuộc"},C7:{vi:"Nhận biết thức ăn",en:"Food/Drinks",hint:"Từ vựng chủ đề quen thuộc"}}},r={5:{text:"#166534",bg:"#dcfce7",accent:"#16a34a"},4:{text:"#15803d",bg:"#dcfce7",accent:"#22c55e"},3:{text:"#b45309",bg:"#fef3c7",accent:"#f59e0b"},2:{text:"#c2410c",bg:"#ffedd5",accent:"#f97316"},1:{text:"#b91c1c",bg:"#fee2e2",accent:"#ef4444"}},a={5:"Excellent",4:"Good",3:"Adequate",2:"Weak",1:"Very weak"},s={5:"Vượt kỳ vọng",4:"Đáp ứng kỳ vọng (Cao)",3:"Đáp ứng kỳ vọng (Thấp)",2:"Dưới kỳ vọng",1:"Không thực hiện"},l={[SpeakingRubricScoringVersion.TEENS_FOCUS_PEARSON_V2]:a,[SpeakingRubricScoringVersion.PRESCHOOL_PLACEMENT_V1]:s,[SpeakingRubricScoringVersion.KINDY_PLACEMENT_V1]:s},o={[KidsRubricSectionCode.WARM_UP]:{vi:"Khởi động",en:"Warm-up Questions"},[KidsRubricSectionCode.PART_1]:{vi:"Part 1 — Nhận diện qua tranh",en:"Picture Identification"},[KidsRubricSectionCode.PART_2]:{vi:"Part 2 — Đọc và cung cấp thông tin",en:"Reading and Giving Information"},[KidsRubricSectionCode.PART_3]:{vi:"Part 3 — Mô tả tranh",en:"Picture Description"}};function d({score:n}){const i=Math.max(0,Math.min(5,Math.round(n)));return e("span",{className:"inline-flex items-center gap-0.5","aria-label":`${i}/5`,children:Array.from({length:5}).map((n,c)=>e(t,{"aria-hidden":!0,className:c<i?"h-4 w-4 fill-[#FFC107] text-[#FFC107]":"h-4 w-4 fill-[#e6edf5] text-[#e6edf5]"},c))})}function h({rubric:t}){const i=c[t.scoringVersion]??{},s=l[t.scoringVersion]??a;return e("div",{className:"overflow-hidden rounded-xl border border-[#00AEEF]/20 shadow-sm",children:e("div",{className:"overflow-x-auto",children:n("table",{className:"w-full min-w-[640px] text-left text-sm text-slate-700",children:[e("thead",{className:"border-b border-[#00AEEF]/20 bg-[#f0f8ff] text-[#0054A6]",children:n("tr",{className:"text-center",children:[e("th",{scope:"col",className:"w-[30%] border-r border-[#00AEEF]/10 px-4 py-3 font-bold",children:"Tiêu chí"}),e("th",{scope:"col",className:"border-r border-[#00AEEF]/10 px-4 py-3 font-bold",children:"Nhận xét chi tiết"}),e("th",{scope:"col",className:"w-[22%] px-4 py-3 font-bold",children:"Điểm"})]})}),e("tbody",{className:"divide-y divide-slate-100",children:t.criteria.map(t=>{const c=i[t.code]??{vi:t.code,en:"",hint:""},a=Math.round(t.score),l=Math.max(1,Math.min(5,a)),o=s[l],h=r[l];return n("tr",{className:"align-top transition-colors hover:bg-slate-50/70",children:[n("td",{className:"border-r border-[#00AEEF]/10 px-4 py-4",style:{boxShadow:`inset 4px 0 0 ${h.accent}`},children:[e("div",{className:"font-bold text-slate-800",children:c.vi}),c.en?e("div",{className:"text-xs italic text-slate-500",children:c.en}):null,c.hint?e("div",{className:"mt-1 text-[11px] leading-snug text-slate-400",children:c.hint}):null]}),e("td",{className:"border-r border-[#00AEEF]/10 px-4 py-4 leading-relaxed text-slate-700",children:t.evidence?.trim()?t.evidence:e("span",{className:"italic text-slate-400",children:"Không có nhận xét chi tiết."})}),n("td",{className:"px-4 py-4",children:[e(d,{score:t.score}),n("div",{className:"mt-1.5 flex items-baseline gap-1",children:[e("span",{className:"text-lg font-black leading-none text-[#0054A6]",children:a}),e("span",{className:"text-xs font-semibold text-slate-400",children:"/ 5"})]}),o?e("span",{className:"mt-1.5 inline-block whitespace-nowrap rounded-full px-2.5 py-0.5 text-xs font-bold",style:{backgroundColor:h.bg,color:h.text},children:o}):null]})]},t.code)})})]})})})}function m({section:t}){const i=o[t.code]??{vi:t.code,en:""},c=t.maxScore>0?Math.max(0,Math.min(100,t.score/t.maxScore*100)):0;return n("div",{className:"overflow-hidden rounded-xl border border-[#00AEEF]/20 shadow-sm",children:[n("div",{className:"flex flex-col gap-2 border-b border-[#00AEEF]/20 bg-[#f0f8ff] px-4 py-3 sm:flex-row sm:items-center sm:justify-between",children:[n("div",{children:[e("div",{className:"font-bold text-[#0054A6]",children:i.vi}),i.en?e("div",{className:"text-xs italic text-slate-500",children:i.en}):null]}),n("div",{className:"flex items-center gap-3",children:[e("div",{className:"h-[9px] w-24 overflow-hidden rounded-full bg-[#e6edf5]",children:e("div",{className:"h-full rounded-full bg-[#4CAF50]",style:{width:`${c}%`}})}),n("div",{className:"whitespace-nowrap text-sm font-black text-[#0054A6]",children:[t.score,n("span",{className:"text-xs font-semibold text-slate-400",children:[" / ",t.maxScore]})]})]})]}),e("table",{className:"w-full text-left text-sm text-slate-700",children:e("tbody",{className:"divide-y divide-[#00AEEF]/10",children:t.questions.map(t=>n("tr",{className:"align-top hover:bg-slate-50",children:[e("td",{className:"w-[72px] px-4 py-3",children:n("span",{className:"inline-flex h-7 min-w-7 items-center justify-center rounded-full bg-[#eaf4ff] px-2 text-xs font-black text-[#0054A6]",children:["Câu ",t.questionNo]})}),n("td",{className:"w-[88px] whitespace-nowrap px-2 py-3 font-black text-[#0054A6]",children:[t.score,n("span",{className:"text-xs font-semibold text-slate-400",children:[" / ",t.maxScore]})]}),e("td",{className:"px-4 py-3 leading-relaxed text-slate-700",children:t.evidence?.trim()?t.evidence:e("span",{className:"italic text-slate-400",children:"Không có nhận xét chi tiết."})})]},t.questionNo))})})]})}export function SpeakingRubricSection({rubric:t,badgeNumber:c="4"}){const r=t.criteria?.length>0,a=(t.sections?.length??0)>0;return r||a?n("section",{className:"relative mt-8 rounded-[20px] border-2 border-[#00AEEF] px-5 pb-6 pt-11 md:px-8",children:[e(i,{number:c,title:"ĐÁNH GIÁ KỸ NĂNG NÓI"}),a?e("div",{className:"space-y-4",children:(t.sections??[]).map(n=>e(m,{section:n},n.code))}):e(h,{rubric:t})]}):null}
@@ -0,0 +1,37 @@
1
+ import type { SpeakingReviewPart, SpeakingReviewQuestion } from '../utils/transform-speaking-review';
2
+ import { type SkillMetric as SharedSkillMetric } from '../utils/speaking-summary';
3
+ export type LocalizedText = string | {
4
+ vi?: string;
5
+ en?: string;
6
+ } | null | undefined;
7
+ export type SkillUiIcon = 'speaker' | 'bolt' | 'message' | 'check-double' | 'music';
8
+ export interface SkillMetric extends SharedSkillMetric {
9
+ icon: SkillUiIcon;
10
+ colorVarClass: string;
11
+ barClass: string;
12
+ }
13
+ export declare const SKILL_UI_BY_KEY: Record<SharedSkillMetric['key'], Pick<SkillMetric, 'icon' | 'colorVarClass' | 'barClass'>>;
14
+ export declare function withUi(metric: SharedSkillMetric): SkillMetric;
15
+ export declare function getLocalizedText(value: LocalizedText): string;
16
+ export declare function formatScore(score: number | null | undefined, digits?: number): string;
17
+ /** Format total score for "ĐIỂM TỔNG" display */
18
+ export declare function formatTotalScore(score: number | null | undefined): string;
19
+ export declare function getScoreStroke(totalScore: number | null, maxScore: number, isPendingGrading: boolean): string;
20
+ export declare function getQuestionTypeLabel(questionType: string): string;
21
+ export declare function isGradingComplete(gradingStatus: string | null | undefined): boolean;
22
+ export declare function getPendingGradingLabel(gradingStatus: string | null | undefined): string;
23
+ export declare function deriveQuestionSkillMetrics(question: SpeakingReviewQuestion): SkillMetric[];
24
+ export declare function extractPrompt(question: SpeakingReviewQuestion): {
25
+ text: string;
26
+ isHtml: boolean;
27
+ };
28
+ export declare function extractStudentAnswerText(question: SpeakingReviewQuestion): string;
29
+ export declare function extractQuestionFeedback(question: SpeakingReviewQuestion): string;
30
+ export declare function getPartScore(part: SpeakingReviewPart): {
31
+ score: number | null;
32
+ maxScore: number;
33
+ hasPending: boolean;
34
+ };
35
+ export declare function resolveAssetUrl(asset: {
36
+ src: string;
37
+ } | string, assetBaseUrl?: string, fileName?: string): string;