@edpire/sdk 0.6.12 → 0.8.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.
- package/CHANGELOG.md +113 -0
- package/README.md +464 -445
- package/bin/create.js +0 -0
- package/dist/client.d.mts +204 -51
- package/dist/client.mjs +18 -40
- package/dist/core.d.mts +286 -1
- package/dist/core.mjs +1 -1
- package/dist/index.d.mts +232 -1
- package/dist/index.mjs +8 -8
- package/dist/react.d.mts +66 -4
- package/dist/react.mjs +10 -10
- package/dist/umd/index.global.js +160 -159
- package/package.json +13 -12
- package/src/styles/shell.css +1 -1
- /package/dist/{dist-WDU5QOZQ.mjs → dist-RXTJ74FF.mjs} +0 -0
package/bin/create.js
CHANGED
|
File without changes
|
package/dist/client.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { RuntimeAnswer } from '@youssefalmia/edpire-runtime';
|
|
2
|
+
import { RichContent } from '@youssefalmia/edpire-schema/authoring';
|
|
2
3
|
|
|
3
4
|
/** Flat per-question answer collected during a custom player session. */
|
|
4
5
|
interface StoredAnswer {
|
|
@@ -10,6 +11,191 @@ interface StoredAnswer {
|
|
|
10
11
|
answers: RuntimeAnswer[];
|
|
11
12
|
}
|
|
12
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Public types for the polished Assessment Shell.
|
|
16
|
+
*
|
|
17
|
+
* The shell renders the same UI used at edpire.com's hosted player.
|
|
18
|
+
* Consumers using the EdpireAssessment.mount() API get these types
|
|
19
|
+
* indirectly via MountOptions; consumers rendering <AssessmentShell>
|
|
20
|
+
* directly import them from "@edpire/sdk/react".
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** Branding payload accepted by AssessmentShell. Mirrors what edpire.com sends. */
|
|
24
|
+
interface TakeBranding {
|
|
25
|
+
/** Org display name — shown as the alt text on the logo. */
|
|
26
|
+
name: string;
|
|
27
|
+
/** Logo URL (any https URL). When absent, the Edpire logo is shown. */
|
|
28
|
+
logoUrl: string | null;
|
|
29
|
+
branding: {
|
|
30
|
+
/** CSS color for the primary action buttons (Submit, View Report, etc.). */
|
|
31
|
+
primaryColor?: string;
|
|
32
|
+
/** CSS color used as the background of the top bar and per-exercise headers. */
|
|
33
|
+
accentColor?: string;
|
|
34
|
+
/** font-family applied to the shell. Pair with `fontUrl` for non-system fonts. */
|
|
35
|
+
fontFamily?: string;
|
|
36
|
+
/** A Google Fonts stylesheet URL; the shell injects a <link> tag at mount. */
|
|
37
|
+
fontUrl?: string;
|
|
38
|
+
/** When false, the "Powered by Edpire" watermark is hidden. Default true. */
|
|
39
|
+
showPoweredBy?: boolean;
|
|
40
|
+
} | null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Public types for the Edpire Embeddable JS SDK.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
type NodeFeedbackStatus = "correct" | "partial" | "incorrect" | "awaiting_review";
|
|
48
|
+
interface EmbedResult {
|
|
49
|
+
submission_id: string;
|
|
50
|
+
score: number;
|
|
51
|
+
max_score: number;
|
|
52
|
+
percentage: number;
|
|
53
|
+
passed: boolean;
|
|
54
|
+
passing_score_percent: number;
|
|
55
|
+
attempt_number: number;
|
|
56
|
+
/** True when the submission contains open-response nodes still pending teacher review. */
|
|
57
|
+
awaiting_manual_grading?: boolean;
|
|
58
|
+
exercise_results: Array<{
|
|
59
|
+
exercise_id: string;
|
|
60
|
+
total_score: number;
|
|
61
|
+
max_score: number;
|
|
62
|
+
question_results: Array<{
|
|
63
|
+
question_id: string;
|
|
64
|
+
score: number;
|
|
65
|
+
correct: boolean;
|
|
66
|
+
points: number;
|
|
67
|
+
}>;
|
|
68
|
+
}>;
|
|
69
|
+
exercise_feedback: Array<{
|
|
70
|
+
exercise_id: string;
|
|
71
|
+
questions: Array<{
|
|
72
|
+
question_id: string;
|
|
73
|
+
node_results: Array<{
|
|
74
|
+
node_id: string;
|
|
75
|
+
status: NodeFeedbackStatus;
|
|
76
|
+
score: number;
|
|
77
|
+
max_score: number;
|
|
78
|
+
feedback?: string;
|
|
79
|
+
display_answer?: string;
|
|
80
|
+
detail?: {
|
|
81
|
+
type: "choiceSet";
|
|
82
|
+
correctChoiceIds: string[];
|
|
83
|
+
} | {
|
|
84
|
+
type: "matchingSet";
|
|
85
|
+
pairFeedback: Array<{
|
|
86
|
+
leftId: string;
|
|
87
|
+
rightId: string;
|
|
88
|
+
correct: boolean;
|
|
89
|
+
}>;
|
|
90
|
+
correctPairings: Array<{
|
|
91
|
+
leftId: string;
|
|
92
|
+
rightId: string;
|
|
93
|
+
}>;
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* The teacher's written feedback on an open response. Only ever present
|
|
97
|
+
* in a review (`GET /submissions/{id}/review`), since at submit time no
|
|
98
|
+
* teacher has graded anything yet.
|
|
99
|
+
*/
|
|
100
|
+
teacher_feedback?: RichContent[];
|
|
101
|
+
}>;
|
|
102
|
+
}>;
|
|
103
|
+
}>;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Response of `GET /api/v1/submissions/{id}/review`: one finished attempt, with
|
|
107
|
+
* everything needed to draw it as the corrected paper. Fetch it on your server
|
|
108
|
+
* (it needs an API key with `read:results`), send it to the browser, and pass it
|
|
109
|
+
* to `EdpireAssessment.review()` unchanged.
|
|
110
|
+
*/
|
|
111
|
+
interface SubmissionReview {
|
|
112
|
+
submission: {
|
|
113
|
+
id: string;
|
|
114
|
+
assessment_id: string;
|
|
115
|
+
learner_ref: string | null;
|
|
116
|
+
status: string;
|
|
117
|
+
attempt_number: number;
|
|
118
|
+
/** How many attempts this learner has made at this assessment, this one included. */
|
|
119
|
+
total_attempts: number;
|
|
120
|
+
score: number;
|
|
121
|
+
max_score: number;
|
|
122
|
+
percentage: number;
|
|
123
|
+
passed: boolean;
|
|
124
|
+
passing_score_percent: number;
|
|
125
|
+
is_fully_graded: boolean;
|
|
126
|
+
/**
|
|
127
|
+
* True while a teacher still has open responses to grade. `score`,
|
|
128
|
+
* `percentage` and `passed` are provisional until it turns false, and the
|
|
129
|
+
* player hides them.
|
|
130
|
+
*/
|
|
131
|
+
awaiting_manual_grading: boolean;
|
|
132
|
+
submitted_at: string | null;
|
|
133
|
+
};
|
|
134
|
+
assessment: AssessmentLoaderResponse["assessment"];
|
|
135
|
+
meta: Omit<AssessmentLoaderResponse["meta"], "type">;
|
|
136
|
+
branding?: TakeBranding | null;
|
|
137
|
+
/** The learner's answers, in the shape the player collected them. */
|
|
138
|
+
answers: {
|
|
139
|
+
exerciseAnswers: Array<{
|
|
140
|
+
exerciseId: string;
|
|
141
|
+
questionAnswers: Array<{
|
|
142
|
+
questionId: string;
|
|
143
|
+
answers: RuntimeAnswer[];
|
|
144
|
+
}>;
|
|
145
|
+
}>;
|
|
146
|
+
};
|
|
147
|
+
exercise_feedback: EmbedResult["exercise_feedback"];
|
|
148
|
+
}
|
|
149
|
+
interface AssessmentLoaderResponse {
|
|
150
|
+
assessment: {
|
|
151
|
+
/**
|
|
152
|
+
* Assessment-level annex — reading texts, figures, formula sheets. The
|
|
153
|
+
* player shows it as a side panel on wide screens and a drawer on narrow
|
|
154
|
+
* ones. Null when the assessment has none.
|
|
155
|
+
*/
|
|
156
|
+
annex?: unknown | null;
|
|
157
|
+
exercises: Array<{
|
|
158
|
+
id: string;
|
|
159
|
+
sharedContext: unknown | null;
|
|
160
|
+
questions: Array<{
|
|
161
|
+
id: string;
|
|
162
|
+
version: "1.0";
|
|
163
|
+
content: unknown;
|
|
164
|
+
/** Sent by the content loader, not by a review. Nothing in the player reads either. */
|
|
165
|
+
points?: number;
|
|
166
|
+
sequenceNumber?: number;
|
|
167
|
+
answerKeys: never[];
|
|
168
|
+
}>;
|
|
169
|
+
}>;
|
|
170
|
+
};
|
|
171
|
+
meta: {
|
|
172
|
+
id: string;
|
|
173
|
+
title: string;
|
|
174
|
+
type: string;
|
|
175
|
+
category?: string | null;
|
|
176
|
+
/**
|
|
177
|
+
* The assessment's learner-facing language, which also decides text
|
|
178
|
+
* direction. The player uses this unless the host passes `locale`
|
|
179
|
+
* explicitly, so a French assessment reads left-to-right even when embedded
|
|
180
|
+
* in a right-to-left product.
|
|
181
|
+
*/
|
|
182
|
+
language?: "en" | "fr" | "ar";
|
|
183
|
+
settings: {
|
|
184
|
+
time_limit_minutes: number | null;
|
|
185
|
+
passing_score_percent: number;
|
|
186
|
+
allowed_attempts: number;
|
|
187
|
+
show_solutions: string;
|
|
188
|
+
};
|
|
189
|
+
};
|
|
190
|
+
attempt_info: {
|
|
191
|
+
attempt_number: number;
|
|
192
|
+
can_retry: boolean;
|
|
193
|
+
attempts_remaining: number | null;
|
|
194
|
+
};
|
|
195
|
+
/** Org branding loaded server-side. May be overridden by MountOptions.branding. */
|
|
196
|
+
branding?: TakeBranding | null;
|
|
197
|
+
}
|
|
198
|
+
|
|
13
199
|
/**
|
|
14
200
|
* @edpire/sdk/client — Server-side API client for Edpire.
|
|
15
201
|
*
|
|
@@ -225,23 +411,6 @@ interface Submission {
|
|
|
225
411
|
result: unknown;
|
|
226
412
|
}>;
|
|
227
413
|
}
|
|
228
|
-
interface Collection {
|
|
229
|
-
id: string;
|
|
230
|
-
name: string;
|
|
231
|
-
slug: string;
|
|
232
|
-
description: string | null;
|
|
233
|
-
status: string;
|
|
234
|
-
item_count: number;
|
|
235
|
-
created_at: string;
|
|
236
|
-
updated_at: string;
|
|
237
|
-
}
|
|
238
|
-
interface CollectionDetail extends Collection {
|
|
239
|
-
items: Array<{
|
|
240
|
-
id: string;
|
|
241
|
-
position: number;
|
|
242
|
-
assessment: Assessment;
|
|
243
|
-
}>;
|
|
244
|
-
}
|
|
245
414
|
interface Webhook {
|
|
246
415
|
id: string;
|
|
247
416
|
url: string;
|
|
@@ -362,52 +531,36 @@ declare class EdpireClient {
|
|
|
362
531
|
* @throws {EdpireError} 404 if not found, 401/403 for auth issues.
|
|
363
532
|
*/
|
|
364
533
|
getSubmission(id: string): Promise<Submission>;
|
|
534
|
+
/**
|
|
535
|
+
* Fetch one finished attempt as the corrected paper: content, the learner's
|
|
536
|
+
* answers and the marking. Send the result to the browser and pass it to
|
|
537
|
+
* `EdpireAssessment.review()` unchanged.
|
|
538
|
+
*
|
|
539
|
+
* @param id - Submission UUID.
|
|
540
|
+
* @param params.learnerRef - The learner you believe owns it. When given, a
|
|
541
|
+
* submission belonging to anyone else is a 404, so a bug in your own
|
|
542
|
+
* ownership check cannot show one learner another's paper. Always pass it.
|
|
543
|
+
* @throws {EdpireError} 404 (not found, or not this learner's), 409 (not
|
|
544
|
+
* submitted yet, or the assessment has been unpublished since).
|
|
545
|
+
*/
|
|
546
|
+
getSubmissionReview(id: string, params?: {
|
|
547
|
+
learnerRef?: string;
|
|
548
|
+
}): Promise<SubmissionReview>;
|
|
365
549
|
/**
|
|
366
550
|
* Fetch all submissions for a learner.
|
|
367
551
|
*
|
|
368
552
|
* @param learnerRef - Your stable user ID (the `learner_ref` you passed during submit).
|
|
553
|
+
* @param params.assessmentId - Only this assessment's attempts.
|
|
369
554
|
* @param params.page - Page number (default 1).
|
|
370
555
|
* @param params.limit - Items per page (default 50, max 100).
|
|
371
556
|
* @returns Paginated list of submissions.
|
|
372
557
|
* @throws {EdpireError} 401/403 for auth issues.
|
|
373
558
|
*/
|
|
374
559
|
getLearnerResults(learnerRef: string, params?: {
|
|
560
|
+
assessmentId?: string;
|
|
375
561
|
page?: number;
|
|
376
562
|
limit?: number;
|
|
377
563
|
}): Promise<PaginatedResponse<Submission>>;
|
|
378
|
-
/**
|
|
379
|
-
* List collections in your org.
|
|
380
|
-
*
|
|
381
|
-
* @param params.page - Page number (default 1).
|
|
382
|
-
* @param params.limit - Items per page (default 20, max 100).
|
|
383
|
-
* @returns Paginated list of collections with item counts.
|
|
384
|
-
*/
|
|
385
|
-
getCollections(params?: {
|
|
386
|
-
page?: number;
|
|
387
|
-
limit?: number;
|
|
388
|
-
}): Promise<PaginatedResponse<Collection>>;
|
|
389
|
-
/**
|
|
390
|
-
* Fetch a collection with its assessment items.
|
|
391
|
-
*
|
|
392
|
-
* @param id - Collection UUID.
|
|
393
|
-
* @returns Collection detail with ordered assessment items.
|
|
394
|
-
* @throws {EdpireError} 404 if not found.
|
|
395
|
-
*/
|
|
396
|
-
getCollection(id: string): Promise<CollectionDetail>;
|
|
397
|
-
/**
|
|
398
|
-
* Fetch aggregated results across all assessments in a collection.
|
|
399
|
-
*
|
|
400
|
-
* @param id - Collection UUID.
|
|
401
|
-
* @param params.page - Page number (default 1).
|
|
402
|
-
* @param params.limit - Items per page (default 50, max 100).
|
|
403
|
-
* @returns Paginated flat list of submissions with assessment titles.
|
|
404
|
-
*/
|
|
405
|
-
getCollectionResults(id: string, params?: {
|
|
406
|
-
page?: number;
|
|
407
|
-
limit?: number;
|
|
408
|
-
}): Promise<PaginatedResponse<Submission & {
|
|
409
|
-
assessment_title: string;
|
|
410
|
-
}>>;
|
|
411
564
|
/**
|
|
412
565
|
* Register a webhook endpoint.
|
|
413
566
|
*
|
|
@@ -578,4 +731,4 @@ declare function createEdpireTokenHandler(opts: EdpireTokenHandlerOptions): (req
|
|
|
578
731
|
*/
|
|
579
732
|
declare function toNodeHandler(handler: (req: Request) => Promise<Response>): (req: ConnectRequest, res: ConnectResponse) => void;
|
|
580
733
|
|
|
581
|
-
export { type Assessment, type AssessmentExercise, type AssessmentQuestion, type AssessmentSummary, type CheckOptions, type CheckResult,
|
|
734
|
+
export { type Assessment, type AssessmentExercise, type AssessmentQuestion, type AssessmentSummary, type CheckOptions, type CheckResult, EdpireClient, type EdpireClientOptions, EdpireError, type EdpireTokenHandlerOptions, type EmbedToken, type GradeResult, type PaginatedResponse, type StoredAnswer, type Submission, type SubmissionReview, type SubmitOptions, type Webhook, type WebhookWithSecret, createEdpireTokenHandler, toNodeHandler };
|
package/dist/client.mjs
CHANGED
|
@@ -182,10 +182,27 @@ var EdpireClient = class {
|
|
|
182
182
|
async getSubmission(id) {
|
|
183
183
|
return this.request("GET", `/submissions/${id}`);
|
|
184
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* Fetch one finished attempt as the corrected paper: content, the learner's
|
|
187
|
+
* answers and the marking. Send the result to the browser and pass it to
|
|
188
|
+
* `EdpireAssessment.review()` unchanged.
|
|
189
|
+
*
|
|
190
|
+
* @param id - Submission UUID.
|
|
191
|
+
* @param params.learnerRef - The learner you believe owns it. When given, a
|
|
192
|
+
* submission belonging to anyone else is a 404, so a bug in your own
|
|
193
|
+
* ownership check cannot show one learner another's paper. Always pass it.
|
|
194
|
+
* @throws {EdpireError} 404 (not found, or not this learner's), 409 (not
|
|
195
|
+
* submitted yet, or the assessment has been unpublished since).
|
|
196
|
+
*/
|
|
197
|
+
async getSubmissionReview(id, params) {
|
|
198
|
+
const query = params?.learnerRef ? `?learner_ref=${encodeURIComponent(params.learnerRef)}` : "";
|
|
199
|
+
return this.request("GET", `/submissions/${id}/review${query}`);
|
|
200
|
+
}
|
|
185
201
|
/**
|
|
186
202
|
* Fetch all submissions for a learner.
|
|
187
203
|
*
|
|
188
204
|
* @param learnerRef - Your stable user ID (the `learner_ref` you passed during submit).
|
|
205
|
+
* @param params.assessmentId - Only this assessment's attempts.
|
|
189
206
|
* @param params.page - Page number (default 1).
|
|
190
207
|
* @param params.limit - Items per page (default 50, max 100).
|
|
191
208
|
* @returns Paginated list of submissions.
|
|
@@ -194,6 +211,7 @@ var EdpireClient = class {
|
|
|
194
211
|
async getLearnerResults(learnerRef, params) {
|
|
195
212
|
const query = new URLSearchParams();
|
|
196
213
|
query.set("learner_ref", learnerRef);
|
|
214
|
+
if (params?.assessmentId) query.set("assessment_id", params.assessmentId);
|
|
197
215
|
if (params?.page) query.set("page", String(params.page));
|
|
198
216
|
if (params?.limit) query.set("limit", String(params.limit));
|
|
199
217
|
return this.requestPaginated(
|
|
@@ -201,46 +219,6 @@ var EdpireClient = class {
|
|
|
201
219
|
`/learners/${encodeURIComponent(learnerRef)}/results?${query.toString()}`
|
|
202
220
|
);
|
|
203
221
|
}
|
|
204
|
-
// ── Collections ────────────────────────────────────────────────────────
|
|
205
|
-
/**
|
|
206
|
-
* List collections in your org.
|
|
207
|
-
*
|
|
208
|
-
* @param params.page - Page number (default 1).
|
|
209
|
-
* @param params.limit - Items per page (default 20, max 100).
|
|
210
|
-
* @returns Paginated list of collections with item counts.
|
|
211
|
-
*/
|
|
212
|
-
async getCollections(params) {
|
|
213
|
-
const query = new URLSearchParams();
|
|
214
|
-
if (params?.page) query.set("page", String(params.page));
|
|
215
|
-
if (params?.limit) query.set("limit", String(params.limit));
|
|
216
|
-
const qs = query.toString();
|
|
217
|
-
return this.requestPaginated("GET", `/collections${qs ? `?${qs}` : ""}`);
|
|
218
|
-
}
|
|
219
|
-
/**
|
|
220
|
-
* Fetch a collection with its assessment items.
|
|
221
|
-
*
|
|
222
|
-
* @param id - Collection UUID.
|
|
223
|
-
* @returns Collection detail with ordered assessment items.
|
|
224
|
-
* @throws {EdpireError} 404 if not found.
|
|
225
|
-
*/
|
|
226
|
-
async getCollection(id) {
|
|
227
|
-
return this.request("GET", `/collections/${id}`);
|
|
228
|
-
}
|
|
229
|
-
/**
|
|
230
|
-
* Fetch aggregated results across all assessments in a collection.
|
|
231
|
-
*
|
|
232
|
-
* @param id - Collection UUID.
|
|
233
|
-
* @param params.page - Page number (default 1).
|
|
234
|
-
* @param params.limit - Items per page (default 50, max 100).
|
|
235
|
-
* @returns Paginated flat list of submissions with assessment titles.
|
|
236
|
-
*/
|
|
237
|
-
async getCollectionResults(id, params) {
|
|
238
|
-
const query = new URLSearchParams();
|
|
239
|
-
if (params?.page) query.set("page", String(params.page));
|
|
240
|
-
if (params?.limit) query.set("limit", String(params.limit));
|
|
241
|
-
const qs = query.toString();
|
|
242
|
-
return this.requestPaginated("GET", `/collections/${id}/results${qs ? `?${qs}` : ""}`);
|
|
243
|
-
}
|
|
244
222
|
// ── Webhooks ───────────────────────────────────────────────────────────
|
|
245
223
|
/**
|
|
246
224
|
* Register a webhook endpoint.
|
package/dist/core.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { RuntimeAnswer } from '@youssefalmia/edpire-runtime';
|
|
2
|
+
import { RichContent } from '@youssefalmia/edpire-schema/authoring';
|
|
2
3
|
|
|
3
4
|
/** Flat per-question answer collected during a custom player session. */
|
|
4
5
|
interface StoredAnswer {
|
|
@@ -44,6 +45,191 @@ declare function buildSubmitPayload(stored: StoredAnswer[]): {
|
|
|
44
45
|
}>;
|
|
45
46
|
};
|
|
46
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Public types for the polished Assessment Shell.
|
|
50
|
+
*
|
|
51
|
+
* The shell renders the same UI used at edpire.com's hosted player.
|
|
52
|
+
* Consumers using the EdpireAssessment.mount() API get these types
|
|
53
|
+
* indirectly via MountOptions; consumers rendering <AssessmentShell>
|
|
54
|
+
* directly import them from "@edpire/sdk/react".
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
/** Branding payload accepted by AssessmentShell. Mirrors what edpire.com sends. */
|
|
58
|
+
interface TakeBranding {
|
|
59
|
+
/** Org display name — shown as the alt text on the logo. */
|
|
60
|
+
name: string;
|
|
61
|
+
/** Logo URL (any https URL). When absent, the Edpire logo is shown. */
|
|
62
|
+
logoUrl: string | null;
|
|
63
|
+
branding: {
|
|
64
|
+
/** CSS color for the primary action buttons (Submit, View Report, etc.). */
|
|
65
|
+
primaryColor?: string;
|
|
66
|
+
/** CSS color used as the background of the top bar and per-exercise headers. */
|
|
67
|
+
accentColor?: string;
|
|
68
|
+
/** font-family applied to the shell. Pair with `fontUrl` for non-system fonts. */
|
|
69
|
+
fontFamily?: string;
|
|
70
|
+
/** A Google Fonts stylesheet URL; the shell injects a <link> tag at mount. */
|
|
71
|
+
fontUrl?: string;
|
|
72
|
+
/** When false, the "Powered by Edpire" watermark is hidden. Default true. */
|
|
73
|
+
showPoweredBy?: boolean;
|
|
74
|
+
} | null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Public types for the Edpire Embeddable JS SDK.
|
|
79
|
+
*/
|
|
80
|
+
|
|
81
|
+
type NodeFeedbackStatus = "correct" | "partial" | "incorrect" | "awaiting_review";
|
|
82
|
+
interface EmbedResult {
|
|
83
|
+
submission_id: string;
|
|
84
|
+
score: number;
|
|
85
|
+
max_score: number;
|
|
86
|
+
percentage: number;
|
|
87
|
+
passed: boolean;
|
|
88
|
+
passing_score_percent: number;
|
|
89
|
+
attempt_number: number;
|
|
90
|
+
/** True when the submission contains open-response nodes still pending teacher review. */
|
|
91
|
+
awaiting_manual_grading?: boolean;
|
|
92
|
+
exercise_results: Array<{
|
|
93
|
+
exercise_id: string;
|
|
94
|
+
total_score: number;
|
|
95
|
+
max_score: number;
|
|
96
|
+
question_results: Array<{
|
|
97
|
+
question_id: string;
|
|
98
|
+
score: number;
|
|
99
|
+
correct: boolean;
|
|
100
|
+
points: number;
|
|
101
|
+
}>;
|
|
102
|
+
}>;
|
|
103
|
+
exercise_feedback: Array<{
|
|
104
|
+
exercise_id: string;
|
|
105
|
+
questions: Array<{
|
|
106
|
+
question_id: string;
|
|
107
|
+
node_results: Array<{
|
|
108
|
+
node_id: string;
|
|
109
|
+
status: NodeFeedbackStatus;
|
|
110
|
+
score: number;
|
|
111
|
+
max_score: number;
|
|
112
|
+
feedback?: string;
|
|
113
|
+
display_answer?: string;
|
|
114
|
+
detail?: {
|
|
115
|
+
type: "choiceSet";
|
|
116
|
+
correctChoiceIds: string[];
|
|
117
|
+
} | {
|
|
118
|
+
type: "matchingSet";
|
|
119
|
+
pairFeedback: Array<{
|
|
120
|
+
leftId: string;
|
|
121
|
+
rightId: string;
|
|
122
|
+
correct: boolean;
|
|
123
|
+
}>;
|
|
124
|
+
correctPairings: Array<{
|
|
125
|
+
leftId: string;
|
|
126
|
+
rightId: string;
|
|
127
|
+
}>;
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* The teacher's written feedback on an open response. Only ever present
|
|
131
|
+
* in a review (`GET /submissions/{id}/review`), since at submit time no
|
|
132
|
+
* teacher has graded anything yet.
|
|
133
|
+
*/
|
|
134
|
+
teacher_feedback?: RichContent[];
|
|
135
|
+
}>;
|
|
136
|
+
}>;
|
|
137
|
+
}>;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Response of `GET /api/v1/submissions/{id}/review`: one finished attempt, with
|
|
141
|
+
* everything needed to draw it as the corrected paper. Fetch it on your server
|
|
142
|
+
* (it needs an API key with `read:results`), send it to the browser, and pass it
|
|
143
|
+
* to `EdpireAssessment.review()` unchanged.
|
|
144
|
+
*/
|
|
145
|
+
interface SubmissionReview {
|
|
146
|
+
submission: {
|
|
147
|
+
id: string;
|
|
148
|
+
assessment_id: string;
|
|
149
|
+
learner_ref: string | null;
|
|
150
|
+
status: string;
|
|
151
|
+
attempt_number: number;
|
|
152
|
+
/** How many attempts this learner has made at this assessment, this one included. */
|
|
153
|
+
total_attempts: number;
|
|
154
|
+
score: number;
|
|
155
|
+
max_score: number;
|
|
156
|
+
percentage: number;
|
|
157
|
+
passed: boolean;
|
|
158
|
+
passing_score_percent: number;
|
|
159
|
+
is_fully_graded: boolean;
|
|
160
|
+
/**
|
|
161
|
+
* True while a teacher still has open responses to grade. `score`,
|
|
162
|
+
* `percentage` and `passed` are provisional until it turns false, and the
|
|
163
|
+
* player hides them.
|
|
164
|
+
*/
|
|
165
|
+
awaiting_manual_grading: boolean;
|
|
166
|
+
submitted_at: string | null;
|
|
167
|
+
};
|
|
168
|
+
assessment: AssessmentLoaderResponse["assessment"];
|
|
169
|
+
meta: Omit<AssessmentLoaderResponse["meta"], "type">;
|
|
170
|
+
branding?: TakeBranding | null;
|
|
171
|
+
/** The learner's answers, in the shape the player collected them. */
|
|
172
|
+
answers: {
|
|
173
|
+
exerciseAnswers: Array<{
|
|
174
|
+
exerciseId: string;
|
|
175
|
+
questionAnswers: Array<{
|
|
176
|
+
questionId: string;
|
|
177
|
+
answers: RuntimeAnswer[];
|
|
178
|
+
}>;
|
|
179
|
+
}>;
|
|
180
|
+
};
|
|
181
|
+
exercise_feedback: EmbedResult["exercise_feedback"];
|
|
182
|
+
}
|
|
183
|
+
interface AssessmentLoaderResponse {
|
|
184
|
+
assessment: {
|
|
185
|
+
/**
|
|
186
|
+
* Assessment-level annex — reading texts, figures, formula sheets. The
|
|
187
|
+
* player shows it as a side panel on wide screens and a drawer on narrow
|
|
188
|
+
* ones. Null when the assessment has none.
|
|
189
|
+
*/
|
|
190
|
+
annex?: unknown | null;
|
|
191
|
+
exercises: Array<{
|
|
192
|
+
id: string;
|
|
193
|
+
sharedContext: unknown | null;
|
|
194
|
+
questions: Array<{
|
|
195
|
+
id: string;
|
|
196
|
+
version: "1.0";
|
|
197
|
+
content: unknown;
|
|
198
|
+
/** Sent by the content loader, not by a review. Nothing in the player reads either. */
|
|
199
|
+
points?: number;
|
|
200
|
+
sequenceNumber?: number;
|
|
201
|
+
answerKeys: never[];
|
|
202
|
+
}>;
|
|
203
|
+
}>;
|
|
204
|
+
};
|
|
205
|
+
meta: {
|
|
206
|
+
id: string;
|
|
207
|
+
title: string;
|
|
208
|
+
type: string;
|
|
209
|
+
category?: string | null;
|
|
210
|
+
/**
|
|
211
|
+
* The assessment's learner-facing language, which also decides text
|
|
212
|
+
* direction. The player uses this unless the host passes `locale`
|
|
213
|
+
* explicitly, so a French assessment reads left-to-right even when embedded
|
|
214
|
+
* in a right-to-left product.
|
|
215
|
+
*/
|
|
216
|
+
language?: "en" | "fr" | "ar";
|
|
217
|
+
settings: {
|
|
218
|
+
time_limit_minutes: number | null;
|
|
219
|
+
passing_score_percent: number;
|
|
220
|
+
allowed_attempts: number;
|
|
221
|
+
show_solutions: string;
|
|
222
|
+
};
|
|
223
|
+
};
|
|
224
|
+
attempt_info: {
|
|
225
|
+
attempt_number: number;
|
|
226
|
+
can_retry: boolean;
|
|
227
|
+
attempts_remaining: number | null;
|
|
228
|
+
};
|
|
229
|
+
/** Org branding loaded server-side. May be overridden by MountOptions.branding. */
|
|
230
|
+
branding?: TakeBranding | null;
|
|
231
|
+
}
|
|
232
|
+
|
|
47
233
|
/**
|
|
48
234
|
* @edpire/sdk/client — Server-side API client for Edpire.
|
|
49
235
|
*
|
|
@@ -146,4 +332,103 @@ interface FlatStep {
|
|
|
146
332
|
*/
|
|
147
333
|
declare function flattenAssessment(assessment: Assessment): FlatStep[];
|
|
148
334
|
|
|
149
|
-
|
|
335
|
+
/**
|
|
336
|
+
* flattenReview: a finished attempt as a linear sequence of steps, one per
|
|
337
|
+
* question, each ready to hand to `renderQuestion()` read-only.
|
|
338
|
+
*
|
|
339
|
+
* The review counterpart of `flattenAssessment()`. Where the full paper
|
|
340
|
+
* (`EdpireAssessment.review()`) draws the whole attempt at once, this is for
|
|
341
|
+
* flows that show one question per screen, Arcade-style, and want the learner
|
|
342
|
+
* to page back through a past attempt the same way they took it.
|
|
343
|
+
*
|
|
344
|
+
* @example
|
|
345
|
+
* const review = await fetch(`/api/attempts/${id}/review`).then((r) => r.json())
|
|
346
|
+
* const steps = flattenReview(review)
|
|
347
|
+
*
|
|
348
|
+
* const q = renderQuestion({
|
|
349
|
+
* container: "#question",
|
|
350
|
+
* content: steps[i].content,
|
|
351
|
+
* initialAnswers: steps[i].answers,
|
|
352
|
+
* feedback: steps[i].feedback,
|
|
353
|
+
* readOnly: true,
|
|
354
|
+
* })
|
|
355
|
+
* // steps[i].status, steps[i].score / steps[i].maxScore for your own chrome
|
|
356
|
+
*
|
|
357
|
+
* Pure: no React, no DOM. Also exported from `@edpire/sdk/core`.
|
|
358
|
+
*/
|
|
359
|
+
|
|
360
|
+
interface ReviewStep {
|
|
361
|
+
exerciseId: string;
|
|
362
|
+
questionId: string;
|
|
363
|
+
/** ContentAST. Pass to renderQuestion's `content`. */
|
|
364
|
+
content: unknown;
|
|
365
|
+
/**
|
|
366
|
+
* The exercise's shared context (reading passage, figure, worked example),
|
|
367
|
+
* or null. Unlike `flattenAssessment()`, carried on every step: a
|
|
368
|
+
* comprehension question reviewed without its passage makes no sense.
|
|
369
|
+
* Render it with renderQuestion too; it is plain content.
|
|
370
|
+
*/
|
|
371
|
+
sharedContext: unknown | null;
|
|
372
|
+
/** What the learner answered. Pass to renderQuestion's `initialAnswers`. */
|
|
373
|
+
answers: RuntimeAnswer[];
|
|
374
|
+
/**
|
|
375
|
+
* The marking, in the same shape POST /check returns, so it goes straight
|
|
376
|
+
* into renderQuestion's `feedback`. Includes the expected answer on anything
|
|
377
|
+
* missed and the teacher's comment on an open response.
|
|
378
|
+
*/
|
|
379
|
+
feedback: Record<string, unknown>;
|
|
380
|
+
score: number;
|
|
381
|
+
maxScore: number;
|
|
382
|
+
/**
|
|
383
|
+
* The question as a whole. `awaiting_review` while a teacher has yet to mark
|
|
384
|
+
* any part of it; its score is then provisional. `unscored` when it is worth
|
|
385
|
+
* nothing (no answerable input, or 0 points): there was nothing to get
|
|
386
|
+
* wrong, so do not show it as wrong.
|
|
387
|
+
*/
|
|
388
|
+
status: "correct" | "partial" | "incorrect" | "awaiting_review" | "unscored";
|
|
389
|
+
/** 0-based position, in the order the assessment presents questions. */
|
|
390
|
+
index: number;
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* The order is the assessment's (exercises, then questions within each), not
|
|
394
|
+
* the order a shuffled Arcade attempt happened to show. Edpire does not record
|
|
395
|
+
* the order an attempt was presented in; store it yourself if you need to
|
|
396
|
+
* replay it exactly.
|
|
397
|
+
*/
|
|
398
|
+
declare function flattenReview(review: SubmissionReview): ReviewStep[];
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* "Did the learner actually answer this?"
|
|
402
|
+
*
|
|
403
|
+
* Used to warn before submit when questions are left blank. The whole module
|
|
404
|
+
* exists because the obvious implementation is wrong: every player emits a
|
|
405
|
+
* RuntimeAnswer on mount, so a freshly-opened assessment already reports one
|
|
406
|
+
* answer per node. A typed blank emits `{ mode: "typed", text: "" }`, a choice
|
|
407
|
+
* set emits `{ selectedIds: [] }`. Counting answers would report every
|
|
408
|
+
* assessment fully answered and the warning would never fire.
|
|
409
|
+
*
|
|
410
|
+
* The expected node count comes from the content AST, never from `answerKeys`.
|
|
411
|
+
* A learner-facing payload ships `answerKeys: []` on purpose (see
|
|
412
|
+
* `getAssessmentForTaking`), so a key-based count works in the builder preview
|
|
413
|
+
* and silently never fires for a real learner, which is the worst of both.
|
|
414
|
+
*/
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* True when this answer carries something the learner actually supplied.
|
|
418
|
+
*
|
|
419
|
+
* The `switch` is exhaustive over RuntimeAnswer so adding a variant upstream
|
|
420
|
+
* fails the build here rather than silently defaulting. The runtime fallback is
|
|
421
|
+
* still `true`: a variant we do not recognise should not make the learner sit
|
|
422
|
+
* through a warning they cannot clear, because a warning that cries wolf gets
|
|
423
|
+
* clicked through and stops protecting anyone.
|
|
424
|
+
*/
|
|
425
|
+
declare function isAnswered(answer: RuntimeAnswer): boolean;
|
|
426
|
+
/**
|
|
427
|
+
* How many inputs this question's content AST presents to the learner.
|
|
428
|
+
*
|
|
429
|
+
* Walks the tree rather than the top level: blanks are inline inside
|
|
430
|
+
* paragraphs, and any node can sit inside a table cell or blockquote.
|
|
431
|
+
*/
|
|
432
|
+
declare function countAnswerableNodes(content: unknown): number;
|
|
433
|
+
|
|
434
|
+
export { type FlatStep, type ReviewStep, type StoredAnswer, type SubmissionReview, buildSubmitPayload, countAnswerableNodes, flattenAssessment, flattenReview, isAnswered };
|