@tinyweb_dev/oe-exam-sdk
v1.0.5
Published
Reusable OceanEdu question components.
Readme
@tinyweb_dev/oe-exam-sdk
SDK tái sử dụng cho hệ thống OE Exam: màn tạo đề, quản lý câu hỏi, làm bài, xem kết quả và theme giao diện.
Cài đặt
npm install @tinyweb_dev/oe-exam-sdkCách dùng nhanh
'use client';
import {
CreateExamPageContainer,
ExamQuestionsPageContainer,
ExamTakingPageContainer,
ResultReviewPageContainer,
StudentExamEntryPageContainer,
} from '@tinyweb_dev/oe-exam-sdk';
import {
createExamCreateApi,
createExamEntryApi,
createExamQuestionsApi,
createExamTakingApi,
createResultReviewApi,
} from '@tinyweb_dev/oe-exam-sdk/api';
const baseUrl = process.env.NEXT_PUBLIC_API_URL;
export function CreateExamPage({ router }) {
return (
<CreateExamPageContainer
api={createExamCreateApi({ baseUrl })}
onNavigate={(href) => router.push(href)}
/>
);
}
export function ExamQuestionsPage({ examId, router }) {
return (
<ExamQuestionsPageContainer
examId={examId}
api={createExamQuestionsApi({ baseUrl })}
onNavigate={(href) => router.push(href)}
/>
);
}
export function ExamEnterPage({ roomId, inviteToken, router }) {
return (
<StudentExamEntryPageContainer
roomId={roomId}
inviteToken={inviteToken}
api={createExamEntryApi({ baseUrl })}
onNavigate={(href) => router.push(href)}
/>
);
}
export function ExamTakePage({ roomId, router }) {
return (
<ExamTakingPageContainer
roomId={roomId}
api={createExamTakingApi({ baseUrl })}
onNavigate={(href) => router.replace(href)}
resultPath={(attemptId) => `/student/results/${attemptId}/review`}
onSubmitted={({ attemptId }) => router.replace(`/student/results/${attemptId}/review`)}
proctoringEnabled
/>
);
}
export function ResultReviewPage({ attemptId, token, router }) {
return (
<ResultReviewPageContainer
attemptId={attemptId}
token={token}
api={createResultReviewApi({ baseUrl })}
onNavigate={(href) => router.push(href)}
/>
);
}Entry points
@tinyweb_dev/oe-exam-sdk # export tổng hợp browser-safe (UI + API)
@tinyweb_dev/oe-exam-sdk/api # API adapters, constants, types
@tinyweb_dev/oe-exam-sdk/shared # shared constants/types/helpers
@tinyweb_dev/oe-exam-sdk/mcp # MCP server ONLY (Node/stdio) — không re-export từ root
@tinyweb_dev/oe-exam-sdk/components # shared UI components
@tinyweb_dev/oe-exam-sdk/components/exams # màn tạo đề / câu hỏi / làm bài / vào phòng
@tinyweb_dev/oe-exam-sdk/components/exams/entry # màn vào phòng (entry) + state machine
@tinyweb_dev/oe-exam-sdk/components/exams/take # màn làm bài + renderer generic + hooks
@tinyweb_dev/oe-exam-sdk/components/questions # hệ thống tạo câu hỏi
@tinyweb_dev/oe-exam-sdk/components/themes # cambridge-yle, english-certification, summer-sky
@tinyweb_dev/oe-exam-sdk/components/results # màn xem chi tiết kết quảMCP: import từ subpath
@tinyweb_dev/oe-exam-sdk/mcp(CLI/binoe-exam-mcp). Root barrel không export MCP để tránh kéonode:fsvào client Next.js (UnhandledSchemeError).
API adapters
SDK không fix cứng API client. Host app có thể truyền adapter riêng qua prop api, hoặc dùng adapter mặc định:
import {
createExamCreateApi,
createExamQuestionsApi,
createExamTakingApi,
createResultReviewApi,
} from '@tinyweb_dev/oe-exam-sdk/api';Các adapter mặc định nhận:
{
baseUrl?: string;
fetcher?: typeof fetch;
headers?: HeadersInit;
}createExamTakingApi đã bao gồm các API cần cho màn làm bài: start/resume attempt, questions, save answers, submit, upload audio, proctoring snapshot, realtime grading, grading summary/status/detail, room/attempt detail và keepalive save.
createExamEntryApi phục vụ màn vào phòng: room detail, active attempt theo room, start/delete attempt, join qua invite token và đăng ký contest (registration form).
Màn vào phòng (entry)
StudentExamEntryPageContainer port toàn bộ luồng /student/exams/:roomId/enter: join qua invite, gating quyền truy cập / đề thi / form đăng ký, state machine theo cửa sổ thời gian (BEFORE_START / WITHIN_WINDOW / EXPIRED / ALREADY_SUBMITTED), resume attempt đang dở và dialog "làm tiếp / làm lại từ đầu".
<StudentExamEntryPageContainer
roomId={roomId}
inviteToken={inviteToken}
api={createExamEntryApi({ baseUrl })}
onNavigate={(href) => router.push(href)}
takePath={(id) => `/student/exams/${id}/take`}
renderRegistrationForm={({ room, onSubmit, isSubmitting }) => (
<MyRegistrationDialog room={room} onSubmit={onSubmit} isSubmitting={isSubmitting} />
)}
/>Themed waiting room (TOEIC / Summer Sky / Cambridge YLE) và registration form là phần phụ thuộc host app — truyền qua renderRegistrationForm hoặc bọc container.
Hooks màn làm bài
@tinyweb_dev/oe-exam-sdk/components/exams/take export các hook tái sử dụng:
useExamCountdown # đếm ngược theo startedAt + duration, auto-submit khi hết giờ
useAnswerAutosave # lưu debounce theo batch + flush keepalive khi ẩn tab/unload + retry khi online
useProctoringSnapshot # chụp webcam định kỳ và upload snapshot
useAttemptSubmit # nộp bài (flush trước), chống double-submit, trả pending/auto-graded answer idsExamTakingPageContainer đã nối sẵn các hook trên. onSubmitted nhận { attemptId, pendingAnswerIds } để điều hướng sang màn review đúng ngữ cảnh; bật proctoring qua prop proctoringEnabled và truyền achieversExamTitle nếu cần gate video intro của giáo viên.
Theme làm bài
ExamTakingPageContainer tự chọn layout theo examTemplate.theme:
ENGLISH_CERTIFICATION -> EnglishCertificationExamLayout
CAMBRIDGE_YLE -> CambridgeYleExamLayout
SUMMER_SKY -> SummerSkyLayout
khác / rỗng -> default SDK layoutRenderer câu hỏi trong màn làm bài hiện là GenericQuestionRenderer.
Màn xem kết quả
ResultReviewPageContainer sở hữu toàn bộ màn review và tự chọn giao diện theo template:
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.- 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. - Template
PLACEMENT_TEST_KINDY_VER3vàPLACEMENT_TEST_PRESCHOOLdùng điểm rubric theo Part và ẩn chấm điểm từng câu.
'use client';
import {
ResultReviewPageContainer,
createResultReviewApi,
} from '@tinyweb_dev/oe-exam-sdk';
export function ResultReviewPage({ attemptId, token, isPrintMode, router }) {
const reviewUrl = typeof window !== 'undefined' ? window.location.href : undefined;
return (
<ResultReviewPageContainer
attemptId={attemptId}
token={token}
api={createResultReviewApi({ baseUrl })}
isPrintMode={isPrintMode}
canExportPdf
reviewUrl={reviewUrl}
assetBaseUrl="/speaking/ocean-edu"
loginHref={`/auth/login?redirect=${encodeURIComponent(reviewUrl ?? '')}`}
onBack={() => router.back()}
onNavigate={(href) => router.push(href)}
topActionsSlot={<SummaryVideoAction attemptId={attemptId} />}
beforeReportSlot={<GiftClaimDialog attemptId={attemptId} />}
afterReportSlot={<ResultExtraContent attemptId={attemptId} />}
/>
);
}Props và hành vi
| Prop | Hành vi |
|---|---|
| attemptId, token, api | Tải detail/attempt; token hỗ trợ link kết quả công khai. |
| isPrintMode | Dừng polling và phát body[data-pdf-ready="true"] sau khi load để headless PDF renderer tiếp tục. |
| canExportPdf | Hiện nút xuất PDF khi adapter có exportPdf; SDK tải blob bằng tên file từ API. |
| 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. |
| 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. |
| loginHref | Đích đăng nhập ở trạng thái 403/private; mặc định là /auth/login?redirect=<current-url>. |
| 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. |
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.
Slots và phạm vi host
topActionsSlot: action bổ sung cạnh nút xuất PDF, ví dụ summary video.beforeReportSlot: nội dung trước report, ví dụ gift claim hoặc thông báo nâng cấp.afterReportSlot: nội dung sau report.
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.
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ẻ.
MCP server
MCP đã tách sang package riêng:
npm install @tinyweb_dev/oe-exam-mcp-serverChạy bằng PAT/API token. Nếu token có dạng oeexam_..., MCP tự gửi qua X-API-Key; token khác sẽ gửi qua Authorization: Bearer.
OE_API_URL=http://localhost:3000 OE_API_TOKEN=... npx @tinyweb_dev/oe-exam-mcp-serverTools hiện có:
exam_templates_search # tìm template qua filter name:like, lấy id để truyền examTemplateId
exam_template_get # lấy templatePayload để truyền vào template
exam_question_template_get # generate API-format questions từ template.parts
exam_json_template_generate # tạo JSON skeleton từ templateTitle/templateId để user edit trước
exam_create_from_json # tạo exam từ JSON thân thiện: { name, templateTitle/templateId, parts: [{ questions: [...] }] }
exam_create_with_questions # tạo exam, patch template, tạo questions qua /manage/questions/batch
exam_result_get # xem result review theo attemptId, hỗ trợ token public linkLuồng tạo exam nhanh qua MCP:
1. Gọi exam_json_template_generate với templateTitle hoặc templateId để tạo JSON skeleton theo template.parts và ghi ra file `<slug-title>.json`.
2. User/Roo sửa `parts[].questions[]` trong file JSON đó.
3. Gọi exam_create_from_json với nội dung JSON đã sửa. Tool tự search/get template, patch template, map questions theo template.parts rồi gọi /manage/questions/batch.Prompt tạo file JSON skeleton trong Roo:
Gọi MCP tool exam_json_template_generate với input:
{
"templateTitle": "Achievers 2026 Round 1",
"name": "Sample exam",
"examLevel": "A1",
"difficulty": "MEDIUM",
"durationMinutes": 45
}Sau khi sửa file JSON, gọi create:
Đọc file JSON đã sửa rồi gọi MCP tool exam_create_from_json với nội dung file đó.Ví dụ JSON thân thiện:
{
"name": "Sample exam",
"templateTitle": "Achievers 2026 Round 1",
"totalScore": 3,
"examLevel": "A1",
"difficulty": "MEDIUM",
"durationMinutes": 45,
"parts": [
{
"name": "Part 1",
"questions": [
{ "question": "Câu 1", "options": ["A", "B", "C", "D", "E"], "answer": "A" }
]
}
]
}Mock API
Dùng cho demo/storybook/test nhẹ:
import {
createMockCreateExamApi,
createMockExamQuestionsApi,
createMockExamTakingApi,
createMockResultReviewApi,
} from '@tinyweb_dev/oe-exam-sdk/api';Ghi chú tích hợp
- Không cần provider toàn cục.
- Các tích hợp host-system nên đi qua props/API adapter.
- Upload file trong component mặc định dùng
blob:URL cục bộ; API upload thật nên truyền từ host app khi cần.
Dev local với host Next.js (không build tay mỗi lần sửa)
SDK không export thẳng src/ vì source dùng path alias @/ / @oe/exam (được tsc-alias rewrite lúc build). Host app luôn resolve exports → dist/.
Để sửa SDK và host nhận ngay:
1) Link package vào host
Yarn (oe-school/frontend — đã cấu hình):
// frontend/package.json
"@tinyweb_dev/oe-exam-sdk": "portal:../../oe-exam/sdk"cd oe-school/frontend
yarn installnpm (host khác):
cd oe-exam/sdk && npm link
cd /path/to/host-nextjs && npm link @tinyweb_dev/oe-exam-sdk2) Transpile package trong host Next.js
// next.config.ts
const nextConfig = {
transpilePackages: ['@tinyweb_dev/oe-exam-sdk'],
};(oe-school/frontend đã có sẵn trong transpilePackages.)
3) Chạy watch build SDK (bắt buộc — cập nhật dist/)
Terminal 1 — SDK:
cd oe-exam/sdk
npm run dev
# initial build:ts (không minify) + tsc --watch
# → tsc-alias + check-aliases sau mỗi lần emit thành công + copy assetsTerminal 2 — host:
cd oe-school/frontend # hoặc host Next.js khác
yarn dev # / npm run devLuồng:
SDK src/** → npm run dev (watch) → dist/**
↑ portal/npm link symlink
host node_modules/@tinyweb_dev/oe-exam-sdk
→ Next transpilePackages + HMRKhông cần: npm pack / publish / reinstall version mới mỗi lần sửa.
Publish production vẫn dùng
npm run build(có minify).npm run devchỉ cho local.
Kiểm tra local
Trong repo này:
npm --prefix sdk run build
# hoặc watch khi dev host:
npm --prefix sdk run devNếu chạy app dev:
npm run dev
# http://localhost:3000/dev/exam-sdkPublish
CI release uses .github/workflows/release_sdk.yml on tags sdk-v* with
npm Trusted Publishing (OIDC) (no long-lived NPM_TOKEN).
Configure Trusted Publisher on npmjs.com for @tinyweb_dev/oe-exam-sdk:
- Organization or user:
ocean-edu - Repository:
oe-exam - Workflow filename:
release_sdk.yml - Allowed actions:
npm publish
Manual local publish (maintainer + 2FA):
cd sdk
npm version patch
npm publish --access public