@edpire/sdk 0.5.0

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.
@@ -0,0 +1,430 @@
1
+ import { QuestionRuntimeProps, RuntimeAnswer } from '@youssefalmia/edpire-runtime';
2
+ export { BlankChoiceAnswer, ChoiceSetAnswer, MatchingConnection, MatchingSetAnswer, OpenResponseAnswer, RuntimeAnswer, TypedBlankAnswer } from '@youssefalmia/edpire-runtime';
3
+ import { RichContent } from '@youssefalmia/edpire-schema/authoring';
4
+
5
+ /**
6
+ * Public types for the polished Assessment Shell.
7
+ *
8
+ * The shell renders the same UI used at edpire.com's hosted player.
9
+ * Consumers using the EdpireAssessment.mount() API get these types
10
+ * indirectly via MountOptions; consumers rendering <AssessmentShell>
11
+ * directly import them from "@edpire/sdk/react".
12
+ */
13
+
14
+ /** Branding payload accepted by AssessmentShell. Mirrors what edpire.com sends. */
15
+ interface TakeBranding {
16
+ /** Org display name — shown as the alt text on the logo. */
17
+ name: string;
18
+ /** Logo URL (any https URL). When absent, the Edpire logo is shown. */
19
+ logoUrl: string | null;
20
+ branding: {
21
+ /** CSS color for the primary action buttons (Submit, View Report, etc.). */
22
+ primaryColor?: string;
23
+ /** CSS color used as the background of the top bar and per-exercise headers. */
24
+ accentColor?: string;
25
+ /** font-family applied to the shell. Pair with `fontUrl` for non-system fonts. */
26
+ fontFamily?: string;
27
+ /** A Google Fonts stylesheet URL; the shell injects a <link> tag at mount. */
28
+ fontUrl?: string;
29
+ /** When false, the "Powered by Edpire" watermark is hidden. Default true. */
30
+ showPoweredBy?: boolean;
31
+ } | null;
32
+ }
33
+ type NodeDetail = {
34
+ type: "choiceSet";
35
+ correctChoiceIds: string[];
36
+ } | {
37
+ type: "matchingSet";
38
+ pairFeedback: Array<{
39
+ leftId: string;
40
+ rightId: string;
41
+ correct: boolean;
42
+ }>;
43
+ correctPairings: Array<{
44
+ leftId: string;
45
+ rightId: string;
46
+ }>;
47
+ };
48
+ interface NodeFeedback {
49
+ nodeId: string;
50
+ status: "correct" | "partial" | "incorrect" | "awaiting_review";
51
+ score: number;
52
+ maxScore: number;
53
+ feedback?: string;
54
+ displayAnswer?: string;
55
+ detail?: NodeDetail;
56
+ teacherFeedback?: RichContent[];
57
+ }
58
+ interface QuestionFeedback {
59
+ questionId: string;
60
+ nodeResults: NodeFeedback[];
61
+ }
62
+ interface ExerciseFeedbackData {
63
+ exerciseId: string;
64
+ questions: QuestionFeedback[];
65
+ }
66
+ interface SubmitResult {
67
+ submissionId: string;
68
+ totalScore: number;
69
+ maxScore: number;
70
+ percentage: number;
71
+ passed: boolean;
72
+ /** If null/undefined, the shell hides the "View Full Report" button. */
73
+ returnUrl?: string | null;
74
+ exerciseFeedback: ExerciseFeedbackData[];
75
+ /** When true, score banners hide the score (still being reviewed). */
76
+ awaitingManualGrading?: boolean;
77
+ }
78
+
79
+ type SpinnerTheme = "default" | "dots" | "pulse" | "bars";
80
+ interface GradingOverlayProps {
81
+ visible: boolean;
82
+ bgColor?: string;
83
+ accentColor?: string;
84
+ text?: string;
85
+ aiText?: string;
86
+ spinnerTheme?: SpinnerTheme;
87
+ animationUrl?: string;
88
+ hasAiNodes?: boolean;
89
+ contained?: boolean;
90
+ }
91
+ type OverlayConfig = Pick<GradingOverlayProps, "bgColor" | "accentColor" | "text" | "spinnerTheme" | "animationUrl">;
92
+
93
+ /**
94
+ * Standalone, dependency-free i18n for the SDK shell.
95
+ *
96
+ * Replaces the web app's `useTranslations("learner.taking")` so the shell can
97
+ * be rendered in any React environment (Vite, CRA, Astro, UMD CDN, mobile
98
+ * WebView). Locale is passed as a prop — there is no React context to set up.
99
+ *
100
+ * Strings mirror `packages/i18n/messages/{en,fr,ar}.json` under `learner.taking.*`.
101
+ * Keep in sync if those copies are edited.
102
+ */
103
+ type ShellLocale = "en" | "fr" | "ar";
104
+
105
+ /**
106
+ * Public types for the Edpire Embeddable JS SDK.
107
+ */
108
+
109
+ type NodeFeedbackStatus = "correct" | "partial" | "incorrect" | "awaiting_review";
110
+ interface EmbedResult {
111
+ submission_id: string;
112
+ score: number;
113
+ max_score: number;
114
+ percentage: number;
115
+ passed: boolean;
116
+ passing_score_percent: number;
117
+ attempt_number: number;
118
+ /** True when the submission contains open-response nodes still pending teacher review. */
119
+ awaiting_manual_grading?: boolean;
120
+ exercise_results: Array<{
121
+ exercise_id: string;
122
+ total_score: number;
123
+ max_score: number;
124
+ question_results: Array<{
125
+ question_id: string;
126
+ score: number;
127
+ correct: boolean;
128
+ points: number;
129
+ }>;
130
+ }>;
131
+ exercise_feedback: Array<{
132
+ exercise_id: string;
133
+ questions: Array<{
134
+ question_id: string;
135
+ node_results: Array<{
136
+ node_id: string;
137
+ status: NodeFeedbackStatus;
138
+ score: number;
139
+ max_score: number;
140
+ feedback?: string;
141
+ display_answer?: string;
142
+ detail?: {
143
+ type: "choiceSet";
144
+ correctChoiceIds: string[];
145
+ } | {
146
+ type: "matchingSet";
147
+ pairFeedback: Array<{
148
+ leftId: string;
149
+ rightId: string;
150
+ correct: boolean;
151
+ }>;
152
+ correctPairings: Array<{
153
+ leftId: string;
154
+ rightId: string;
155
+ }>;
156
+ };
157
+ }>;
158
+ }>;
159
+ }>;
160
+ }
161
+ interface EmbedError {
162
+ code: "TOKEN_INVALID" | "TOKEN_EXPIRED" | "TOKEN_USED" | "ORIGIN_NOT_ALLOWED" | "ASSESSMENT_NOT_FOUND" | "MAX_ATTEMPTS_REACHED" | "NETWORK_ERROR" | "UNKNOWN";
163
+ message: string;
164
+ }
165
+ interface MountOptions {
166
+ /**
167
+ * The embed token received from POST /api/v1/embed/token.
168
+ * Pass this from your server to the browser — do not hardcode.
169
+ */
170
+ token: string;
171
+ /**
172
+ * Where to mount the assessment player.
173
+ * CSS selector string or DOM element.
174
+ */
175
+ container: string | HTMLElement;
176
+ /**
177
+ * Base URL of the Edpire instance. Defaults to "https://edpire.com".
178
+ * Override for self-hosted or staging environments.
179
+ */
180
+ baseUrl?: string;
181
+ /**
182
+ * Locale for the assessment player UI. Defaults to "en".
183
+ * Right-to-left layout is applied automatically for "ar".
184
+ */
185
+ locale?: ShellLocale;
186
+ /**
187
+ * Branding overrides. By default the SDK loads the assessment's org branding
188
+ * (logo, colors, fonts) automatically. Pass this to override per-deployment
189
+ * — useful for white-labelling.
190
+ */
191
+ branding?: TakeBranding;
192
+ /**
193
+ * When set, the post-submit "View Full Report" button redirects the browser
194
+ * to this URL with `?submission_id=...&score=...&max_score=...` appended.
195
+ * Omit to hide the button (consumers can still react via onComplete).
196
+ */
197
+ returnUrl?: string;
198
+ /** Custom label for the post-submit redirect button. */
199
+ reportLabel?: string;
200
+ /** Called when the learner clicks the back button. Shown only BEFORE submission. */
201
+ onBack?: () => void;
202
+ /** Custom label for the back button. */
203
+ backLabel?: string;
204
+ /** Customise the grading overlay (branded spinner, animation URL, etc.). */
205
+ overlayConfig?: OverlayConfig;
206
+ /**
207
+ * Media upload handler — required if the assessment contains OpenResponse
208
+ * questions with file-upload or recording modes.
209
+ */
210
+ mediaHandler?: QuestionRuntimeProps["mediaHandler"];
211
+ /**
212
+ * Called when the learner successfully submits the assessment.
213
+ */
214
+ onComplete?: (result: EmbedResult) => void;
215
+ /**
216
+ * Called when a non-recoverable error occurs.
217
+ */
218
+ onError?: (error: EmbedError) => void;
219
+ }
220
+ interface EmbedInstance {
221
+ /** Unmounts the assessment player and cleans up event listeners. */
222
+ unmount: () => void;
223
+ }
224
+ interface RenderQuestionOptions {
225
+ /**
226
+ * DOM element or CSS selector string to mount into.
227
+ */
228
+ container: string | HTMLElement;
229
+ /**
230
+ * ContentAST from GET /api/v1/assessments/{id} → exercises[n].questions[n].content_ast
231
+ */
232
+ content: unknown;
233
+ /**
234
+ * Called on every answer change.
235
+ */
236
+ onAnswersChange?: (answers: RuntimeAnswer[]) => void;
237
+ /**
238
+ * Feedback from POST /check. Pass the `feedback` field directly.
239
+ */
240
+ feedback?: Record<string, unknown> | null;
241
+ /**
242
+ * Pre-fill answers.
243
+ */
244
+ initialAnswers?: RuntimeAnswer[];
245
+ /**
246
+ * Text direction for the question container.
247
+ */
248
+ dir?: "ltr" | "rtl";
249
+ }
250
+ interface QuestionInstance {
251
+ /**
252
+ * Replace the current question's content.
253
+ * Also clears feedback — ready for the next question.
254
+ */
255
+ setContent(content: unknown): void;
256
+ /**
257
+ * Update the feedback state (e.g. after POST /check).
258
+ * Pass null to clear.
259
+ */
260
+ setFeedback(feedback: Record<string, unknown> | null): void;
261
+ /**
262
+ * Pre-fill answers (e.g. when navigating back to a previous question).
263
+ */
264
+ setInitialAnswers(answers: RuntimeAnswer[]): void;
265
+ /**
266
+ * Unmount the React root and clean up.
267
+ */
268
+ unmount(): void;
269
+ }
270
+
271
+ /** Flat per-question answer collected during a custom player session. */
272
+ interface StoredAnswer {
273
+ /** Exercise ID — from the `exerciseId` field on each `FlatStep`. */
274
+ exerciseId: string;
275
+ /** Question ID — from the `questionId` field on each `FlatStep`. */
276
+ questionId: string;
277
+ /** Answers collected from `EdpireQuestion`'s `onAnswersChange` callback. */
278
+ answers: RuntimeAnswer[];
279
+ }
280
+ /**
281
+ * Convert a flat array of per-question answers into the nested structure
282
+ * expected by `EdpireClient.submit()` and `POST /api/v1/assessments/{id}/submit`.
283
+ *
284
+ * This is the bridge between the simple flat collection pattern used in custom
285
+ * players and the grouped format the API requires.
286
+ *
287
+ * @example
288
+ * ```typescript
289
+ * import { flattenAssessment, buildSubmitPayload } from "@edpire/sdk"
290
+ * import type { StoredAnswer } from "@edpire/sdk"
291
+ *
292
+ * const stored: StoredAnswer[] = []
293
+ *
294
+ * // During the session, after each question:
295
+ * stored.push({ exerciseId: step.exerciseId, questionId: step.questionId, answers })
296
+ *
297
+ * // At the end, submit:
298
+ * const payload = buildSubmitPayload(stored)
299
+ * await client.submit(assessmentId, { learner_ref: userId, answers: payload })
300
+ *
301
+ * // Or pass the flat array directly — client.submit() accepts both formats:
302
+ * await client.submit(assessmentId, { learner_ref: userId, answers: stored })
303
+ * ```
304
+ */
305
+ declare function buildSubmitPayload(stored: StoredAnswer[]): {
306
+ exerciseAnswers: Array<{
307
+ exerciseId: string;
308
+ questionAnswers: Array<{
309
+ questionId: string;
310
+ answers: RuntimeAnswer[];
311
+ }>;
312
+ }>;
313
+ };
314
+
315
+ /**
316
+ * @edpire/sdk/client — Server-side API client for Edpire.
317
+ *
318
+ * Node.js only. Zero browser dependencies. Uses native fetch (Node 18+).
319
+ *
320
+ * @example
321
+ * ```typescript
322
+ * import { EdpireClient } from "@edpire/sdk/client"
323
+ *
324
+ * const client = new EdpireClient({
325
+ * apiKey: process.env.EDPIRE_API_KEY!,
326
+ * baseUrl: "https://edpire.com",
327
+ * })
328
+ *
329
+ * const assessments = await client.getAssessments({ status: "published" })
330
+ * const result = await client.submit("assessment-id", {
331
+ * learner_ref: "user-123",
332
+ * answers: { exerciseAnswers: [...] },
333
+ * })
334
+ * ```
335
+ */
336
+
337
+ /** Assessment summary returned by getAssessments() (list). Does not include exercises. */
338
+ interface AssessmentSummary {
339
+ id: string;
340
+ title: string;
341
+ description: string | null;
342
+ type: string;
343
+ status: string;
344
+ share_code: string | null;
345
+ settings: Record<string, unknown>;
346
+ exercise_count?: number;
347
+ created_at: string;
348
+ updated_at: string;
349
+ }
350
+ /** Full assessment returned by getAssessment() (single fetch). Always includes exercises. */
351
+ interface Assessment extends AssessmentSummary {
352
+ exercises: AssessmentExercise[];
353
+ }
354
+ interface AssessmentExercise {
355
+ id: string;
356
+ shared_context: unknown | null;
357
+ questions: AssessmentQuestion[];
358
+ }
359
+ interface AssessmentQuestion {
360
+ id: string;
361
+ content_ast: unknown;
362
+ points: number;
363
+ sequence_number: number;
364
+ }
365
+
366
+ /**
367
+ * flattenAssessment — convert an assessment API response into a linear
368
+ * sequence of steps, one per question.
369
+ *
370
+ * Useful for Duolingo-style flows where you present questions one at a time
371
+ * and need to know the exercise context of each step (for POST /check).
372
+ *
373
+ * @example
374
+ * const res = await fetch(`/api/v1/assessments/${id}`, { headers: { Authorization: `Bearer ${key}` } })
375
+ * const { data: assessment } = await res.json()
376
+ *
377
+ * const steps = flattenAssessment(assessment)
378
+ * // steps[0] = { exerciseId, questionId, content, points, sequenceNumber, index: 0 }
379
+ *
380
+ * // Render step:
381
+ * <EdpireQuestion content={steps[currentIndex].content} ... />
382
+ *
383
+ * // Check step:
384
+ * POST /api/v1/assessments/{id}/check { exercise_id: steps[i].exerciseId, question_id: steps[i].questionId, ... }
385
+ */
386
+
387
+ interface FlatStep {
388
+ /** The exercise this question belongs to — required for POST /check */
389
+ exerciseId: string;
390
+ /** The question ID — required for POST /check */
391
+ questionId: string;
392
+ /** ContentAST — pass directly to EdpireQuestion's `content` prop */
393
+ content: unknown;
394
+ /** Point value of this question */
395
+ points: number;
396
+ /** Original sequence number within its exercise */
397
+ sequenceNumber: number;
398
+ /** 0-based index in the flattened sequence — use for progress bar */
399
+ index: number;
400
+ }
401
+ /**
402
+ * Flatten an assessment from getAssessment() / GET /api/v1/assessments/{id}
403
+ * into an ordered array of steps, preserving exercise and position information.
404
+ *
405
+ * The order follows the assessment's exercise order, then question order
406
+ * within each exercise — the same order a learner would see them.
407
+ */
408
+ declare function flattenAssessment(assessment: Assessment): FlatStep[];
409
+
410
+ declare function renderQuestion(options: RenderQuestionOptions): QuestionInstance;
411
+
412
+ declare const EdpireAssessment: {
413
+ /**
414
+ * Mount an Edpire assessment into the given container.
415
+ *
416
+ * @param options - Configuration including the embed token and container element.
417
+ * @returns An EmbedInstance with an `unmount()` method.
418
+ *
419
+ * @example
420
+ * const embed = EdpireAssessment.mount({
421
+ * token: serverSuppliedToken,
422
+ * container: document.getElementById("assessment-root"),
423
+ * onComplete: (result) => console.log("Score:", result.percentage),
424
+ * onError: (err) => console.error(err.message),
425
+ * })
426
+ */
427
+ mount(options: MountOptions): EmbedInstance;
428
+ };
429
+
430
+ export { EdpireAssessment, type EmbedError, type EmbedInstance, type EmbedResult, type ExerciseFeedbackData, type FlatStep, type MountOptions, type OverlayConfig, type QuestionInstance, type RenderQuestionOptions, type ShellLocale, type SpinnerTheme, type StoredAnswer, type SubmitResult, type TakeBranding, buildSubmitPayload, flattenAssessment, renderQuestion };