@intellectif/lk-react 2.1.0 → 3.0.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,87 @@
1
+ import { XAPIStatement, WrittenResponseData, LearnerResponse, ItemOutcome, InteractionEvent, ThemeTokens } from '@intellectif/lk-core';
2
+ import { a as Renderable, R as RenderMode, H as HtmlSanitizer } from './types-CNmjqD6M.cjs';
3
+
4
+ /**
5
+ * Payload delivered when the learner submits a written response. Grading is
6
+ * DEFERRED (asynchronous AI/human grading), so there is no score here — and
7
+ * deliberately no fake one: an ungraded submission must never be shown or
8
+ * stored as a 0. The consumer persists `text` and grades it out-of-band.
9
+ */
10
+ interface WrittenResponseSubmission {
11
+ /** The submitted text, verbatim. */
12
+ text: string;
13
+ /** Word count of `text`, recomputed with the canonical `countWords()`. */
14
+ wordCount: number;
15
+ /** Whether `wordCount` falls within the activity's `[minWords, maxWords]`. */
16
+ withinWordBounds: boolean;
17
+ /** Time in milliseconds from first interaction to submission. */
18
+ timeSpent: number;
19
+ /** The SUBMITTED-verb xAPI statement built for this submission (no score fields). */
20
+ xapiStatement: XAPIStatement;
21
+ }
22
+ /**
23
+ * Props for {@link WrittenResponse}. Mirrors `ActivityProps` except for the
24
+ * completion callback: a deferred-grading activity completes with a
25
+ * {@link WrittenResponseSubmission} (`onSubmitted`), not a scored
26
+ * `ActivityResult` (`onComplete`) — deviation from Req 22.8 recorded in the
27
+ * roadmap (§3.1): fabricating a score of 0 for ungraded work is the exact
28
+ * bug this type exists to fix. Everything else — controlled value, render
29
+ * mode, server outcome, rich text — follows the shared `ActivityProps`
30
+ * contract verbatim.
31
+ */
32
+ interface WrittenResponseProps {
33
+ /** Activity content. Accepts a `redact()` projection in `exam` mode. */
34
+ data: Renderable<WrittenResponseData>;
35
+ /**
36
+ * Called on submit with the ungraded submission (text, recomputed word
37
+ * count, bounds flag, timing, xAPI statement). Fires in `practice` and
38
+ * `exam` mode, after `onSubmit`; never in `review`.
39
+ *
40
+ * Optional since v0.4 so a `review`-mode render (which has no submit
41
+ * control) and an exam runner that only wants the raw response through
42
+ * `onSubmit` do not have to pass a no-op. Supply it in `practice`/`exam`
43
+ * unless `onSubmit` already captures everything you persist.
44
+ */
45
+ onSubmitted?: (submission: WrittenResponseSubmission) => void;
46
+ /**
47
+ * Called on submit with the raw learner response and no grade, BEFORE
48
+ * `onSubmitted`. This is the only submit callback an exam runner needs:
49
+ * the server grades the text and returns an {@link ItemOutcome} later.
50
+ */
51
+ onSubmit?: (response: LearnerResponse) => void;
52
+ /**
53
+ * Controlled value: the learner's current response. When present the
54
+ * textarea renders from it (never from internal state) and every keystroke
55
+ * goes out through `onChange` — restore an in-progress attempt, autosave a
56
+ * delta, or drive a review render with it.
57
+ */
58
+ value?: LearnerResponse;
59
+ /** Initial response for an uncontrolled component (ignored when `value` is set). */
60
+ defaultValue?: LearnerResponse;
61
+ /** Fires on every change to the learner's response. Required for a controlled component. */
62
+ onChange?: (response: LearnerResponse) => void;
63
+ /**
64
+ * Presentation mode, default `practice`. `practice` and `exam` are nearly
65
+ * identical here — a written response is never graded client-side, so
66
+ * there is no local score to withhold; `exam` additionally never calls
67
+ * `evaluate()` and renders happily from a `redact()` projection.
68
+ * `review` is read-only and shows the server's `outcome`.
69
+ */
70
+ renderMode?: RenderMode;
71
+ /**
72
+ * Server-computed outcome, rendered by `review` mode: `deferred` shows a
73
+ * "not graded yet" affordance, `scored` shows the returned grade and
74
+ * feedback. Ignored in other modes — this component never manufactures an
75
+ * outcome of its own.
76
+ */
77
+ outcome?: ItemOutcome;
78
+ /** Renders `data.promptHtml` when provided. See `HtmlSanitizer`. */
79
+ sanitizeHtml?: HtmlSanitizer;
80
+ onInteraction?: (event: InteractionEvent) => void;
81
+ /** Per-instance token overrides, applied as inline CSS vars on the root. */
82
+ theme?: Partial<ThemeTokens>;
83
+ locale?: string;
84
+ disabled?: boolean;
85
+ }
86
+
87
+ export type { WrittenResponseProps as W, WrittenResponseSubmission as a };
@@ -0,0 +1,87 @@
1
+ import { XAPIStatement, WrittenResponseData, LearnerResponse, ItemOutcome, InteractionEvent, ThemeTokens } from '@intellectif/lk-core';
2
+ import { a as Renderable, R as RenderMode, H as HtmlSanitizer } from './types-CNmjqD6M.js';
3
+
4
+ /**
5
+ * Payload delivered when the learner submits a written response. Grading is
6
+ * DEFERRED (asynchronous AI/human grading), so there is no score here — and
7
+ * deliberately no fake one: an ungraded submission must never be shown or
8
+ * stored as a 0. The consumer persists `text` and grades it out-of-band.
9
+ */
10
+ interface WrittenResponseSubmission {
11
+ /** The submitted text, verbatim. */
12
+ text: string;
13
+ /** Word count of `text`, recomputed with the canonical `countWords()`. */
14
+ wordCount: number;
15
+ /** Whether `wordCount` falls within the activity's `[minWords, maxWords]`. */
16
+ withinWordBounds: boolean;
17
+ /** Time in milliseconds from first interaction to submission. */
18
+ timeSpent: number;
19
+ /** The SUBMITTED-verb xAPI statement built for this submission (no score fields). */
20
+ xapiStatement: XAPIStatement;
21
+ }
22
+ /**
23
+ * Props for {@link WrittenResponse}. Mirrors `ActivityProps` except for the
24
+ * completion callback: a deferred-grading activity completes with a
25
+ * {@link WrittenResponseSubmission} (`onSubmitted`), not a scored
26
+ * `ActivityResult` (`onComplete`) — deviation from Req 22.8 recorded in the
27
+ * roadmap (§3.1): fabricating a score of 0 for ungraded work is the exact
28
+ * bug this type exists to fix. Everything else — controlled value, render
29
+ * mode, server outcome, rich text — follows the shared `ActivityProps`
30
+ * contract verbatim.
31
+ */
32
+ interface WrittenResponseProps {
33
+ /** Activity content. Accepts a `redact()` projection in `exam` mode. */
34
+ data: Renderable<WrittenResponseData>;
35
+ /**
36
+ * Called on submit with the ungraded submission (text, recomputed word
37
+ * count, bounds flag, timing, xAPI statement). Fires in `practice` and
38
+ * `exam` mode, after `onSubmit`; never in `review`.
39
+ *
40
+ * Optional since v0.4 so a `review`-mode render (which has no submit
41
+ * control) and an exam runner that only wants the raw response through
42
+ * `onSubmit` do not have to pass a no-op. Supply it in `practice`/`exam`
43
+ * unless `onSubmit` already captures everything you persist.
44
+ */
45
+ onSubmitted?: (submission: WrittenResponseSubmission) => void;
46
+ /**
47
+ * Called on submit with the raw learner response and no grade, BEFORE
48
+ * `onSubmitted`. This is the only submit callback an exam runner needs:
49
+ * the server grades the text and returns an {@link ItemOutcome} later.
50
+ */
51
+ onSubmit?: (response: LearnerResponse) => void;
52
+ /**
53
+ * Controlled value: the learner's current response. When present the
54
+ * textarea renders from it (never from internal state) and every keystroke
55
+ * goes out through `onChange` — restore an in-progress attempt, autosave a
56
+ * delta, or drive a review render with it.
57
+ */
58
+ value?: LearnerResponse;
59
+ /** Initial response for an uncontrolled component (ignored when `value` is set). */
60
+ defaultValue?: LearnerResponse;
61
+ /** Fires on every change to the learner's response. Required for a controlled component. */
62
+ onChange?: (response: LearnerResponse) => void;
63
+ /**
64
+ * Presentation mode, default `practice`. `practice` and `exam` are nearly
65
+ * identical here — a written response is never graded client-side, so
66
+ * there is no local score to withhold; `exam` additionally never calls
67
+ * `evaluate()` and renders happily from a `redact()` projection.
68
+ * `review` is read-only and shows the server's `outcome`.
69
+ */
70
+ renderMode?: RenderMode;
71
+ /**
72
+ * Server-computed outcome, rendered by `review` mode: `deferred` shows a
73
+ * "not graded yet" affordance, `scored` shows the returned grade and
74
+ * feedback. Ignored in other modes — this component never manufactures an
75
+ * outcome of its own.
76
+ */
77
+ outcome?: ItemOutcome;
78
+ /** Renders `data.promptHtml` when provided. See `HtmlSanitizer`. */
79
+ sanitizeHtml?: HtmlSanitizer;
80
+ onInteraction?: (event: InteractionEvent) => void;
81
+ /** Per-instance token overrides, applied as inline CSS vars on the root. */
82
+ theme?: Partial<ThemeTokens>;
83
+ locale?: string;
84
+ disabled?: boolean;
85
+ }
86
+
87
+ export type { WrittenResponseProps as W, WrittenResponseSubmission as a };
@@ -9,18 +9,51 @@ var jsxRuntime = require('react/jsx-runtime');
9
9
  function textOf(response) {
10
10
  return response !== void 0 && response.type === "written-response" ? response.text : "";
11
11
  }
12
+ function GradeBody({ grade }) {
13
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
14
+ grade.feedback ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "lk-wr-grade-feedback", children: grade.feedback }) : null,
15
+ grade.criteria && grade.criteria.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "lk-wr-criteria", children: grade.criteria.map((criterion) => /* @__PURE__ */ jsxRuntime.jsxs(
16
+ "li",
17
+ {
18
+ className: "lk-wr-criterion",
19
+ "data-na": String(criterion.notApplicable === true),
20
+ children: [
21
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "lk-wr-criterion-name", children: criterion.name }),
22
+ criterion.notApplicable === true ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "lk-wr-criterion-score", children: "Not applicable" }) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "lk-wr-criterion-score", children: criterion.band ?? (typeof criterion.score === "number" ? `${Math.round(criterion.score * 100)}%` : "") }),
23
+ criterion.comment ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "lk-wr-criterion-comment", children: criterion.comment }) : null
24
+ ]
25
+ },
26
+ criterion.name
27
+ )) }) : null,
28
+ grade.corrections && grade.corrections.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "lk-wr-corrections", children: grade.corrections.map((correction) => /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "lk-wr-correction", children: [
29
+ /* @__PURE__ */ jsxRuntime.jsx("del", { className: "lk-wr-correction-original", children: correction.original }),
30
+ " ",
31
+ /* @__PURE__ */ jsxRuntime.jsx("ins", { className: "lk-wr-correction-corrected", children: correction.corrected }),
32
+ correction.explanation ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "lk-wr-correction-explanation", children: correction.explanation }) : null
33
+ ] }, `${correction.original}:${correction.corrected}`)) }) : null,
34
+ grade.requiresHumanReview === true ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "lk-wr-review-flag", children: "This grade is awaiting review by a teacher." }) : null
35
+ ] });
36
+ }
12
37
  function OutcomeSummary({ outcome }) {
13
- if (outcome.status === "scored") {
38
+ if (outcome.status === "scored" || outcome.status === "graded") {
14
39
  const percent = outcome.maxScore > 0 ? Math.round(outcome.score / outcome.maxScore * 100) : Math.round(outcome.score * 100);
15
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lk-wr-outcome", "data-status": "scored", "data-passed": String(outcome.passed), children: [
16
- /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "lk-wr-grade", children: [
17
- "Score ",
18
- percent,
19
- "%. ",
20
- outcome.passed ? "Passed." : "Not passed."
21
- ] }),
22
- outcome.feedback ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "lk-wr-grade-feedback", children: outcome.feedback }) : null
23
- ] });
40
+ return /* @__PURE__ */ jsxRuntime.jsxs(
41
+ "div",
42
+ {
43
+ className: "lk-wr-outcome",
44
+ "data-status": outcome.status,
45
+ "data-passed": String(outcome.passed),
46
+ children: [
47
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "lk-wr-grade", children: [
48
+ "Score ",
49
+ percent,
50
+ "%. ",
51
+ outcome.passed ? "Passed." : "Not passed."
52
+ ] }),
53
+ outcome.status === "graded" ? /* @__PURE__ */ jsxRuntime.jsx(GradeBody, { grade: outcome.grade }) : outcome.feedback ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "lk-wr-grade-feedback", children: outcome.feedback }) : null
54
+ ]
55
+ }
56
+ );
24
57
  }
25
58
  if (outcome.status === "deferred") {
26
59
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "lk-wr-outcome", "data-status": "deferred", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "lk-wr-grade", children: "Not graded yet. This response is waiting for its grade." }) });
@@ -221,5 +254,5 @@ function WrittenResponse2(props) {
221
254
  }
222
255
 
223
256
  exports.WrittenResponse = WrittenResponse2;
224
- //# sourceMappingURL=chunk-7BT6OXAR.cjs.map
225
- //# sourceMappingURL=chunk-7BT6OXAR.cjs.map
257
+ //# sourceMappingURL=chunk-7MW7HGOA.cjs.map
258
+ //# sourceMappingURL=chunk-7MW7HGOA.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/WrittenResponse/WrittenResponse.tsx","../src/components/WrittenResponse/index.tsx"],"names":["jsxs","Fragment","jsx","useMemo","isDevelopment","validateActivity","ActivitySchemaError","useActivityState","useState","useRef","useEffect","countWords","evaluate","xAPIBuilder","ANONYMOUS_ACTOR","objectIdFor","ActivityMedia","FeedbackRegion","WrittenResponse","ActivityErrorBoundary"],"mappings":"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","file":"chunk-7MW7HGOA.cjs","sourcesContent":["'use client';\n\nimport {\n ActivitySchemaError,\n countWords,\n evaluate,\n type GradeRecord,\n type InteractionEvent,\n type ItemOutcome,\n type LearnerResponse,\n type ThemeTokens,\n validateActivity,\n type WrittenResponseData,\n type XAPIStatement,\n xAPIBuilder,\n} from '@intellectif/lk-core';\nimport { type CSSProperties, useEffect, useMemo, useRef, useState } from 'react';\nimport { useActivityState } from '../../hooks/useActivityState.js';\nimport { ANONYMOUS_ACTOR, isDevelopment, objectIdFor } from '../_internal.js';\nimport { ActivityMedia } from '../shared/ActivityMedia.js';\nimport { FeedbackRegion } from '../shared/FeedbackRegion.js';\nimport type { HtmlSanitizer, Renderable, RenderMode } from '../types.js';\n\n/**\n * Payload delivered when the learner submits a written response. Grading is\n * DEFERRED (asynchronous AI/human grading), so there is no score here — and\n * deliberately no fake one: an ungraded submission must never be shown or\n * stored as a 0. The consumer persists `text` and grades it out-of-band.\n */\nexport interface WrittenResponseSubmission {\n /** The submitted text, verbatim. */\n text: string;\n /** Word count of `text`, recomputed with the canonical `countWords()`. */\n wordCount: number;\n /** Whether `wordCount` falls within the activity's `[minWords, maxWords]`. */\n withinWordBounds: boolean;\n /** Time in milliseconds from first interaction to submission. */\n timeSpent: number;\n /** The SUBMITTED-verb xAPI statement built for this submission (no score fields). */\n xapiStatement: XAPIStatement;\n}\n\n/**\n * Props for {@link WrittenResponse}. Mirrors `ActivityProps` except for the\n * completion callback: a deferred-grading activity completes with a\n * {@link WrittenResponseSubmission} (`onSubmitted`), not a scored\n * `ActivityResult` (`onComplete`) — deviation from Req 22.8 recorded in the\n * roadmap (§3.1): fabricating a score of 0 for ungraded work is the exact\n * bug this type exists to fix. Everything else — controlled value, render\n * mode, server outcome, rich text — follows the shared `ActivityProps`\n * contract verbatim.\n */\nexport interface WrittenResponseProps {\n /** Activity content. Accepts a `redact()` projection in `exam` mode. */\n data: Renderable<WrittenResponseData>;\n /**\n * Called on submit with the ungraded submission (text, recomputed word\n * count, bounds flag, timing, xAPI statement). Fires in `practice` and\n * `exam` mode, after `onSubmit`; never in `review`.\n *\n * Optional since v0.4 so a `review`-mode render (which has no submit\n * control) and an exam runner that only wants the raw response through\n * `onSubmit` do not have to pass a no-op. Supply it in `practice`/`exam`\n * unless `onSubmit` already captures everything you persist.\n */\n onSubmitted?: (submission: WrittenResponseSubmission) => void;\n /**\n * Called on submit with the raw learner response and no grade, BEFORE\n * `onSubmitted`. This is the only submit callback an exam runner needs:\n * the server grades the text and returns an {@link ItemOutcome} later.\n */\n onSubmit?: (response: LearnerResponse) => void;\n /**\n * Controlled value: the learner's current response. When present the\n * textarea renders from it (never from internal state) and every keystroke\n * goes out through `onChange` — restore an in-progress attempt, autosave a\n * delta, or drive a review render with it.\n */\n value?: LearnerResponse;\n /** Initial response for an uncontrolled component (ignored when `value` is set). */\n defaultValue?: LearnerResponse;\n /** Fires on every change to the learner's response. Required for a controlled component. */\n onChange?: (response: LearnerResponse) => void;\n /**\n * Presentation mode, default `practice`. `practice` and `exam` are nearly\n * identical here — a written response is never graded client-side, so\n * there is no local score to withhold; `exam` additionally never calls\n * `evaluate()` and renders happily from a `redact()` projection.\n * `review` is read-only and shows the server's `outcome`.\n */\n renderMode?: RenderMode;\n /**\n * Server-computed outcome, rendered by `review` mode: `deferred` shows a\n * \"not graded yet\" affordance, `scored` shows the returned grade and\n * feedback. Ignored in other modes — this component never manufactures an\n * outcome of its own.\n */\n outcome?: ItemOutcome;\n /** Renders `data.promptHtml` when provided. See `HtmlSanitizer`. */\n sanitizeHtml?: HtmlSanitizer;\n onInteraction?: (event: InteractionEvent) => void;\n /** Per-instance token overrides, applied as inline CSS vars on the root. */\n theme?: Partial<ThemeTokens>;\n locale?: string;\n disabled?: boolean;\n}\n\n/**\n * Reads the text out of a learner response. Another activity type's response\n * shape (the `value`/`defaultValue` props take the `LearnerResponse` union,\n * so a generic runner can hand any of them over) reads as empty rather than\n * throwing mid-exam.\n */\nfunction textOf(response: LearnerResponse | undefined): string {\n return response !== undefined && response.type === 'written-response' ? response.text : '';\n}\n\n/**\n * Renders the server-computed outcome in `review` mode. The grade shown here\n * always came back from the asynchronous grader — nothing on this path scores,\n * infers, or defaults a grade. A `written-response` outcome carries no\n * per-item `details` (there are no options or blanks to mark), so the render\n * is the scaled score, the pass state, and the grader's feedback.\n */\nfunction GradeBody({ grade }: { grade: GradeRecord }) {\n return (\n <>\n {grade.feedback ? <p className=\"lk-wr-grade-feedback\">{grade.feedback}</p> : null}\n {grade.criteria && grade.criteria.length > 0 ? (\n <ul className=\"lk-wr-criteria\">\n {grade.criteria.map((criterion) => (\n <li\n className=\"lk-wr-criterion\"\n key={criterion.name}\n data-na={String(criterion.notApplicable === true)}\n >\n <span className=\"lk-wr-criterion-name\">{criterion.name}</span>\n {criterion.notApplicable === true ? (\n <span className=\"lk-wr-criterion-score\">Not applicable</span>\n ) : (\n <span className=\"lk-wr-criterion-score\">\n {criterion.band ??\n (typeof criterion.score === 'number'\n ? `${Math.round(criterion.score * 100)}%`\n : '')}\n </span>\n )}\n {criterion.comment ? (\n <span className=\"lk-wr-criterion-comment\">{criterion.comment}</span>\n ) : null}\n </li>\n ))}\n </ul>\n ) : null}\n {grade.corrections && grade.corrections.length > 0 ? (\n <ul className=\"lk-wr-corrections\">\n {grade.corrections.map((correction) => (\n <li className=\"lk-wr-correction\" key={`${correction.original}:${correction.corrected}`}>\n <del className=\"lk-wr-correction-original\">{correction.original}</del>{' '}\n <ins className=\"lk-wr-correction-corrected\">{correction.corrected}</ins>\n {correction.explanation ? (\n <span className=\"lk-wr-correction-explanation\">{correction.explanation}</span>\n ) : null}\n </li>\n ))}\n </ul>\n ) : null}\n {grade.requiresHumanReview === true ? (\n <p className=\"lk-wr-review-flag\">This grade is awaiting review by a teacher.</p>\n ) : null}\n </>\n );\n}\n\n/**\n * Renders the server-computed outcome in `review` mode. The grade shown here\n * always came back from the grader — nothing on this path scores, infers, or\n * defaults a grade.\n */\nfunction OutcomeSummary({ outcome }: { outcome: ItemOutcome }) {\n if (outcome.status === 'scored' || outcome.status === 'graded') {\n // `score` is scaled [0–1] against `maxScore`; normalising by `maxScore`\n // is a no-op for the canonical maxScore of 1 and keeps an unscaled\n // grader payload (8.5 / 10) from rendering as 850%.\n const percent =\n outcome.maxScore > 0\n ? Math.round((outcome.score / outcome.maxScore) * 100)\n : Math.round(outcome.score * 100);\n return (\n <div\n className=\"lk-wr-outcome\"\n data-status={outcome.status}\n data-passed={String(outcome.passed)}\n >\n <p className=\"lk-wr-grade\">\n Score {percent}%. {outcome.passed ? 'Passed.' : 'Not passed.'}\n </p>\n {outcome.status === 'graded' ? (\n <GradeBody grade={outcome.grade} />\n ) : outcome.feedback ? (\n <p className=\"lk-wr-grade-feedback\">{outcome.feedback}</p>\n ) : null}\n </div>\n );\n }\n if (outcome.status === 'deferred') {\n return (\n <div className=\"lk-wr-outcome\" data-status=\"deferred\">\n <p className=\"lk-wr-grade\">Not graded yet. This response is waiting for its grade.</p>\n </div>\n );\n }\n // `unscorable` reasons are developer-facing strings; surface the state, not\n // the reason, and leave it on the element for diagnostics.\n return (\n <div className=\"lk-wr-outcome\" data-status=\"unscorable\" data-reason={outcome.reason}>\n <p className=\"lk-wr-grade\">This response could not be graded.</p>\n </div>\n );\n}\n\nexport function WrittenResponse({\n data,\n onSubmitted,\n onSubmit,\n value,\n defaultValue,\n onChange,\n renderMode = 'practice',\n outcome,\n sanitizeHtml,\n onInteraction,\n theme,\n locale,\n disabled,\n}: WrittenResponseProps) {\n // Dev-only boundary validation (Req 2.3), same convention as MC/FIB. The\n // content schema is loose, so a `redact()` projection validates too.\n const devError = useMemo(() => {\n if (!isDevelopment()) {\n return null;\n }\n const result = validateActivity('written-response', data);\n return result.success ? null : new ActivitySchemaError('written-response', result.errors);\n }, [data]);\n\n const { state, start, complete, getTimeSpent, reset } = useActivityState();\n const isControlled = value !== undefined;\n const [innerText, setInnerText] = useState<string>(() => textOf(defaultValue));\n const [summary, setSummary] = useState<string | null>(null);\n\n /**\n * Rich text is rendered ONLY through a caller-supplied sanitiser, and only\n * when the author actually shipped the HTML sidecar. Without both, the\n * plain-text prompt is rendered as an escaped React child.\n */\n const promptHtml = useMemo(() => {\n if (sanitizeHtml === undefined || typeof data.promptHtml !== 'string') {\n return null;\n }\n return sanitizeHtml(data.promptHtml);\n }, [data.promptHtml, sanitizeHtml]);\n\n // Latest seed text, kept in a ref so `defaultValue` is NOT a reset trigger:\n // callers pass an object literal, whose identity changes every render.\n const defaultTextRef = useRef<string>(textOf(defaultValue));\n useEffect(() => {\n defaultTextRef.current = textOf(defaultValue);\n }, [defaultValue]);\n\n // Reset on data-prop change (Req 3.7): back to the seed (empty when there\n // is none — the v1 behaviour), never the previous activity's draft.\n // biome-ignore lint/correctness/useExhaustiveDependencies: data is the reset trigger (Req 3.7)\n useEffect(() => {\n setInnerText(defaultTextRef.current);\n setSummary(null);\n reset();\n }, [data, reset]);\n\n if (devError) {\n throw devError;\n }\n\n const isExam = renderMode === 'exam';\n const isReview = renderMode === 'review';\n\n const text = isControlled ? textOf(value) : innerText;\n const submitted = state === 'completed';\n const inactive = disabled === true || submitted || isReview;\n const wordCount = countWords(text);\n const withinBounds = wordCount >= data.minWords && wordCount <= data.maxWords;\n\n const handleChange = (next: string): void => {\n if (inactive) {\n return;\n }\n if (state === 'idle') {\n start();\n }\n // A controlled component renders from `props.value` only; the caller's\n // state is the single source of truth for the text.\n if (!isControlled) {\n setInnerText(next);\n }\n const nextWordCount = countWords(next);\n onChange?.({ type: 'written-response', text: next, wordCount: nextWordCount });\n onInteraction?.({\n type: 'text-changed',\n activityId: data.id,\n timestamp: Date.now(),\n payload: { wordCount: nextWordCount },\n });\n };\n\n const handleSubmit = (event: React.FormEvent): void => {\n event.preventDefault();\n if (inactive || text.trim().length === 0) {\n return;\n }\n let finalWordCount = wordCount;\n let finalWithinBounds = withinBounds;\n // `exam` mode must not call evaluate(): the component does not grade, and\n // a redacted projection carries no scoring input anyway. The two facts\n // evaluate() returns for a deferred type (recomputed word count, bounds)\n // are exactly what is computed above, so the payload is identical either\n // way; practice keeps going through core so the numbers stay canonical.\n if (!isExam) {\n const itemOutcome = evaluate(data, { type: 'written-response', text, wordCount });\n const partial = itemOutcome.status === 'deferred' ? itemOutcome.partial : undefined;\n if (typeof partial?.wordCount === 'number') {\n finalWordCount = partial.wordCount;\n }\n if (typeof partial?.withinWordBounds === 'boolean') {\n finalWithinBounds = partial.withinWordBounds;\n }\n }\n complete();\n const timeSpent = getTimeSpent();\n const xapiStatement = xAPIBuilder.buildSubmittedStatement({\n actor: ANONYMOUS_ACTOR,\n object: {\n id: objectIdFor(data.id),\n name: { [data.locale ?? 'en-US']: data.title },\n type: 'http://adlnet.gov/expapi/activities/cmi.interaction',\n interactionType: 'long-fill-in',\n },\n timeSpentMs: timeSpent,\n response: text,\n resultExtensions: {\n 'urn:learning-kit:extension:word-count': finalWordCount,\n 'urn:learning-kit:extension:within-word-bounds': finalWithinBounds,\n },\n });\n // Raw response first (an exam runner persists this), then the fuller\n // ungraded submission — mirroring `onSubmit` before `onComplete`.\n onSubmit?.({ type: 'written-response', text, wordCount: finalWordCount });\n onSubmitted?.({\n text,\n wordCount: finalWordCount,\n withinWordBounds: finalWithinBounds,\n timeSpent,\n xapiStatement,\n });\n setSummary('Response submitted. It will be graded and your result will appear here later.');\n onInteraction?.({\n type: 'submitted',\n activityId: data.id,\n timestamp: Date.now(),\n payload: { wordCount: finalWordCount, withinWordBounds: finalWithinBounds },\n });\n };\n\n const promptId = `${data.id}-prompt`;\n const counterId = `${data.id}-counter`;\n const boundsLabel =\n data.minWords > 0 ? `${data.minWords}–${data.maxWords} words` : `up to ${data.maxWords} words`;\n\n return (\n <form\n className=\"lk-wr\"\n aria-label={data.title}\n lang={locale}\n data-render-mode={renderMode}\n style={theme as CSSProperties | undefined}\n onSubmit={handleSubmit}\n >\n {data.media ? <ActivityMedia media={data.media} /> : null}\n {promptHtml === null ? (\n <p className=\"lk-wr-prompt\" id={promptId}>\n {data.prompt}\n </p>\n ) : (\n // A div, not a p: sanitised author HTML may contain block content.\n <div\n className=\"lk-wr-prompt\"\n id={promptId}\n // biome-ignore lint/security/noDangerouslySetInnerHtml: rendered only through the caller-supplied HtmlSanitizer — the SDK never injects unsanitised markup\n dangerouslySetInnerHTML={{ __html: promptHtml }}\n />\n )}\n <textarea\n className=\"lk-wr-textarea\"\n aria-labelledby={promptId}\n aria-describedby={counterId}\n value={text}\n onChange={(event) => handleChange(event.target.value)}\n // `review` uses readOnly rather than disabled: the submitted essay\n // must stay focusable, selectable and scrollable for a screen reader\n // and a keyboard, while remaining uneditable.\n disabled={!isReview && inactive}\n readOnly={isReview}\n rows={8}\n />\n <div\n className=\"lk-wr-counter\"\n id={counterId}\n aria-live=\"polite\"\n data-within-bounds={withinBounds}\n >\n {wordCount} {wordCount === 1 ? 'word' : 'words'} ({boundsLabel})\n </div>\n {isReview ? null : (\n <button\n type=\"submit\"\n className=\"lk-wr-submit\"\n disabled={inactive || text.trim().length === 0}\n >\n Submit\n </button>\n )}\n <FeedbackRegion id={`${data.id}-feedback`}>\n {isReview ? outcome ? <OutcomeSummary outcome={outcome} /> : null : summary}\n </FeedbackRegion>\n </form>\n );\n}\n","'use client';\n\nimport { ActivityErrorBoundary } from '../ActivityErrorBoundary.js';\nimport {\n WrittenResponse as WrittenResponseCore,\n type WrittenResponseProps,\n} from './WrittenResponse.js';\n\nexport type { HtmlSanitizer, Renderable, RenderMode } from '../types.js';\nexport type { WrittenResponseProps, WrittenResponseSubmission } from './WrittenResponse.js';\n\n/**\n * Public Written Response activity: a labelled textarea with a live word\n * counter for free-text writing graded asynchronously (AI or human). The core\n * is wrapped in `ActivityErrorBoundary` so a render failure (incl. the dev\n * schema-validation throw) degrades to an accessible fallback.\n */\nexport function WrittenResponse(props: WrittenResponseProps) {\n return (\n <ActivityErrorBoundary activityTitle={props.data?.title}>\n <WrittenResponseCore {...props} />\n </ActivityErrorBoundary>\n );\n}\n"]}
@@ -2,23 +2,56 @@ import { ActivityErrorBoundary, isDevelopment, ActivityMedia, FeedbackRegion, AN
2
2
  import { useActivityState } from './chunk-5HVUKXF3.js';
3
3
  import { validateActivity, ActivitySchemaError, countWords, evaluate, xAPIBuilder } from '@intellectif/lk-core';
4
4
  import { useMemo, useState, useRef, useEffect } from 'react';
5
- import { jsx, jsxs } from 'react/jsx-runtime';
5
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
6
6
 
7
7
  function textOf(response) {
8
8
  return response !== void 0 && response.type === "written-response" ? response.text : "";
9
9
  }
10
+ function GradeBody({ grade }) {
11
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
12
+ grade.feedback ? /* @__PURE__ */ jsx("p", { className: "lk-wr-grade-feedback", children: grade.feedback }) : null,
13
+ grade.criteria && grade.criteria.length > 0 ? /* @__PURE__ */ jsx("ul", { className: "lk-wr-criteria", children: grade.criteria.map((criterion) => /* @__PURE__ */ jsxs(
14
+ "li",
15
+ {
16
+ className: "lk-wr-criterion",
17
+ "data-na": String(criterion.notApplicable === true),
18
+ children: [
19
+ /* @__PURE__ */ jsx("span", { className: "lk-wr-criterion-name", children: criterion.name }),
20
+ criterion.notApplicable === true ? /* @__PURE__ */ jsx("span", { className: "lk-wr-criterion-score", children: "Not applicable" }) : /* @__PURE__ */ jsx("span", { className: "lk-wr-criterion-score", children: criterion.band ?? (typeof criterion.score === "number" ? `${Math.round(criterion.score * 100)}%` : "") }),
21
+ criterion.comment ? /* @__PURE__ */ jsx("span", { className: "lk-wr-criterion-comment", children: criterion.comment }) : null
22
+ ]
23
+ },
24
+ criterion.name
25
+ )) }) : null,
26
+ grade.corrections && grade.corrections.length > 0 ? /* @__PURE__ */ jsx("ul", { className: "lk-wr-corrections", children: grade.corrections.map((correction) => /* @__PURE__ */ jsxs("li", { className: "lk-wr-correction", children: [
27
+ /* @__PURE__ */ jsx("del", { className: "lk-wr-correction-original", children: correction.original }),
28
+ " ",
29
+ /* @__PURE__ */ jsx("ins", { className: "lk-wr-correction-corrected", children: correction.corrected }),
30
+ correction.explanation ? /* @__PURE__ */ jsx("span", { className: "lk-wr-correction-explanation", children: correction.explanation }) : null
31
+ ] }, `${correction.original}:${correction.corrected}`)) }) : null,
32
+ grade.requiresHumanReview === true ? /* @__PURE__ */ jsx("p", { className: "lk-wr-review-flag", children: "This grade is awaiting review by a teacher." }) : null
33
+ ] });
34
+ }
10
35
  function OutcomeSummary({ outcome }) {
11
- if (outcome.status === "scored") {
36
+ if (outcome.status === "scored" || outcome.status === "graded") {
12
37
  const percent = outcome.maxScore > 0 ? Math.round(outcome.score / outcome.maxScore * 100) : Math.round(outcome.score * 100);
13
- return /* @__PURE__ */ jsxs("div", { className: "lk-wr-outcome", "data-status": "scored", "data-passed": String(outcome.passed), children: [
14
- /* @__PURE__ */ jsxs("p", { className: "lk-wr-grade", children: [
15
- "Score ",
16
- percent,
17
- "%. ",
18
- outcome.passed ? "Passed." : "Not passed."
19
- ] }),
20
- outcome.feedback ? /* @__PURE__ */ jsx("p", { className: "lk-wr-grade-feedback", children: outcome.feedback }) : null
21
- ] });
38
+ return /* @__PURE__ */ jsxs(
39
+ "div",
40
+ {
41
+ className: "lk-wr-outcome",
42
+ "data-status": outcome.status,
43
+ "data-passed": String(outcome.passed),
44
+ children: [
45
+ /* @__PURE__ */ jsxs("p", { className: "lk-wr-grade", children: [
46
+ "Score ",
47
+ percent,
48
+ "%. ",
49
+ outcome.passed ? "Passed." : "Not passed."
50
+ ] }),
51
+ outcome.status === "graded" ? /* @__PURE__ */ jsx(GradeBody, { grade: outcome.grade }) : outcome.feedback ? /* @__PURE__ */ jsx("p", { className: "lk-wr-grade-feedback", children: outcome.feedback }) : null
52
+ ]
53
+ }
54
+ );
22
55
  }
23
56
  if (outcome.status === "deferred") {
24
57
  return /* @__PURE__ */ jsx("div", { className: "lk-wr-outcome", "data-status": "deferred", children: /* @__PURE__ */ jsx("p", { className: "lk-wr-grade", children: "Not graded yet. This response is waiting for its grade." }) });
@@ -219,5 +252,5 @@ function WrittenResponse2(props) {
219
252
  }
220
253
 
221
254
  export { WrittenResponse2 as WrittenResponse };
222
- //# sourceMappingURL=chunk-XTJ22JAP.js.map
223
- //# sourceMappingURL=chunk-XTJ22JAP.js.map
255
+ //# sourceMappingURL=chunk-AEQQ7VPF.js.map
256
+ //# sourceMappingURL=chunk-AEQQ7VPF.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/WrittenResponse/WrittenResponse.tsx","../src/components/WrittenResponse/index.tsx"],"names":["WrittenResponse","jsx"],"mappings":"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","file":"chunk-AEQQ7VPF.js","sourcesContent":["'use client';\n\nimport {\n ActivitySchemaError,\n countWords,\n evaluate,\n type GradeRecord,\n type InteractionEvent,\n type ItemOutcome,\n type LearnerResponse,\n type ThemeTokens,\n validateActivity,\n type WrittenResponseData,\n type XAPIStatement,\n xAPIBuilder,\n} from '@intellectif/lk-core';\nimport { type CSSProperties, useEffect, useMemo, useRef, useState } from 'react';\nimport { useActivityState } from '../../hooks/useActivityState.js';\nimport { ANONYMOUS_ACTOR, isDevelopment, objectIdFor } from '../_internal.js';\nimport { ActivityMedia } from '../shared/ActivityMedia.js';\nimport { FeedbackRegion } from '../shared/FeedbackRegion.js';\nimport type { HtmlSanitizer, Renderable, RenderMode } from '../types.js';\n\n/**\n * Payload delivered when the learner submits a written response. Grading is\n * DEFERRED (asynchronous AI/human grading), so there is no score here — and\n * deliberately no fake one: an ungraded submission must never be shown or\n * stored as a 0. The consumer persists `text` and grades it out-of-band.\n */\nexport interface WrittenResponseSubmission {\n /** The submitted text, verbatim. */\n text: string;\n /** Word count of `text`, recomputed with the canonical `countWords()`. */\n wordCount: number;\n /** Whether `wordCount` falls within the activity's `[minWords, maxWords]`. */\n withinWordBounds: boolean;\n /** Time in milliseconds from first interaction to submission. */\n timeSpent: number;\n /** The SUBMITTED-verb xAPI statement built for this submission (no score fields). */\n xapiStatement: XAPIStatement;\n}\n\n/**\n * Props for {@link WrittenResponse}. Mirrors `ActivityProps` except for the\n * completion callback: a deferred-grading activity completes with a\n * {@link WrittenResponseSubmission} (`onSubmitted`), not a scored\n * `ActivityResult` (`onComplete`) — deviation from Req 22.8 recorded in the\n * roadmap (§3.1): fabricating a score of 0 for ungraded work is the exact\n * bug this type exists to fix. Everything else — controlled value, render\n * mode, server outcome, rich text — follows the shared `ActivityProps`\n * contract verbatim.\n */\nexport interface WrittenResponseProps {\n /** Activity content. Accepts a `redact()` projection in `exam` mode. */\n data: Renderable<WrittenResponseData>;\n /**\n * Called on submit with the ungraded submission (text, recomputed word\n * count, bounds flag, timing, xAPI statement). Fires in `practice` and\n * `exam` mode, after `onSubmit`; never in `review`.\n *\n * Optional since v0.4 so a `review`-mode render (which has no submit\n * control) and an exam runner that only wants the raw response through\n * `onSubmit` do not have to pass a no-op. Supply it in `practice`/`exam`\n * unless `onSubmit` already captures everything you persist.\n */\n onSubmitted?: (submission: WrittenResponseSubmission) => void;\n /**\n * Called on submit with the raw learner response and no grade, BEFORE\n * `onSubmitted`. This is the only submit callback an exam runner needs:\n * the server grades the text and returns an {@link ItemOutcome} later.\n */\n onSubmit?: (response: LearnerResponse) => void;\n /**\n * Controlled value: the learner's current response. When present the\n * textarea renders from it (never from internal state) and every keystroke\n * goes out through `onChange` — restore an in-progress attempt, autosave a\n * delta, or drive a review render with it.\n */\n value?: LearnerResponse;\n /** Initial response for an uncontrolled component (ignored when `value` is set). */\n defaultValue?: LearnerResponse;\n /** Fires on every change to the learner's response. Required for a controlled component. */\n onChange?: (response: LearnerResponse) => void;\n /**\n * Presentation mode, default `practice`. `practice` and `exam` are nearly\n * identical here — a written response is never graded client-side, so\n * there is no local score to withhold; `exam` additionally never calls\n * `evaluate()` and renders happily from a `redact()` projection.\n * `review` is read-only and shows the server's `outcome`.\n */\n renderMode?: RenderMode;\n /**\n * Server-computed outcome, rendered by `review` mode: `deferred` shows a\n * \"not graded yet\" affordance, `scored` shows the returned grade and\n * feedback. Ignored in other modes — this component never manufactures an\n * outcome of its own.\n */\n outcome?: ItemOutcome;\n /** Renders `data.promptHtml` when provided. See `HtmlSanitizer`. */\n sanitizeHtml?: HtmlSanitizer;\n onInteraction?: (event: InteractionEvent) => void;\n /** Per-instance token overrides, applied as inline CSS vars on the root. */\n theme?: Partial<ThemeTokens>;\n locale?: string;\n disabled?: boolean;\n}\n\n/**\n * Reads the text out of a learner response. Another activity type's response\n * shape (the `value`/`defaultValue` props take the `LearnerResponse` union,\n * so a generic runner can hand any of them over) reads as empty rather than\n * throwing mid-exam.\n */\nfunction textOf(response: LearnerResponse | undefined): string {\n return response !== undefined && response.type === 'written-response' ? response.text : '';\n}\n\n/**\n * Renders the server-computed outcome in `review` mode. The grade shown here\n * always came back from the asynchronous grader — nothing on this path scores,\n * infers, or defaults a grade. A `written-response` outcome carries no\n * per-item `details` (there are no options or blanks to mark), so the render\n * is the scaled score, the pass state, and the grader's feedback.\n */\nfunction GradeBody({ grade }: { grade: GradeRecord }) {\n return (\n <>\n {grade.feedback ? <p className=\"lk-wr-grade-feedback\">{grade.feedback}</p> : null}\n {grade.criteria && grade.criteria.length > 0 ? (\n <ul className=\"lk-wr-criteria\">\n {grade.criteria.map((criterion) => (\n <li\n className=\"lk-wr-criterion\"\n key={criterion.name}\n data-na={String(criterion.notApplicable === true)}\n >\n <span className=\"lk-wr-criterion-name\">{criterion.name}</span>\n {criterion.notApplicable === true ? (\n <span className=\"lk-wr-criterion-score\">Not applicable</span>\n ) : (\n <span className=\"lk-wr-criterion-score\">\n {criterion.band ??\n (typeof criterion.score === 'number'\n ? `${Math.round(criterion.score * 100)}%`\n : '')}\n </span>\n )}\n {criterion.comment ? (\n <span className=\"lk-wr-criterion-comment\">{criterion.comment}</span>\n ) : null}\n </li>\n ))}\n </ul>\n ) : null}\n {grade.corrections && grade.corrections.length > 0 ? (\n <ul className=\"lk-wr-corrections\">\n {grade.corrections.map((correction) => (\n <li className=\"lk-wr-correction\" key={`${correction.original}:${correction.corrected}`}>\n <del className=\"lk-wr-correction-original\">{correction.original}</del>{' '}\n <ins className=\"lk-wr-correction-corrected\">{correction.corrected}</ins>\n {correction.explanation ? (\n <span className=\"lk-wr-correction-explanation\">{correction.explanation}</span>\n ) : null}\n </li>\n ))}\n </ul>\n ) : null}\n {grade.requiresHumanReview === true ? (\n <p className=\"lk-wr-review-flag\">This grade is awaiting review by a teacher.</p>\n ) : null}\n </>\n );\n}\n\n/**\n * Renders the server-computed outcome in `review` mode. The grade shown here\n * always came back from the grader — nothing on this path scores, infers, or\n * defaults a grade.\n */\nfunction OutcomeSummary({ outcome }: { outcome: ItemOutcome }) {\n if (outcome.status === 'scored' || outcome.status === 'graded') {\n // `score` is scaled [0–1] against `maxScore`; normalising by `maxScore`\n // is a no-op for the canonical maxScore of 1 and keeps an unscaled\n // grader payload (8.5 / 10) from rendering as 850%.\n const percent =\n outcome.maxScore > 0\n ? Math.round((outcome.score / outcome.maxScore) * 100)\n : Math.round(outcome.score * 100);\n return (\n <div\n className=\"lk-wr-outcome\"\n data-status={outcome.status}\n data-passed={String(outcome.passed)}\n >\n <p className=\"lk-wr-grade\">\n Score {percent}%. {outcome.passed ? 'Passed.' : 'Not passed.'}\n </p>\n {outcome.status === 'graded' ? (\n <GradeBody grade={outcome.grade} />\n ) : outcome.feedback ? (\n <p className=\"lk-wr-grade-feedback\">{outcome.feedback}</p>\n ) : null}\n </div>\n );\n }\n if (outcome.status === 'deferred') {\n return (\n <div className=\"lk-wr-outcome\" data-status=\"deferred\">\n <p className=\"lk-wr-grade\">Not graded yet. This response is waiting for its grade.</p>\n </div>\n );\n }\n // `unscorable` reasons are developer-facing strings; surface the state, not\n // the reason, and leave it on the element for diagnostics.\n return (\n <div className=\"lk-wr-outcome\" data-status=\"unscorable\" data-reason={outcome.reason}>\n <p className=\"lk-wr-grade\">This response could not be graded.</p>\n </div>\n );\n}\n\nexport function WrittenResponse({\n data,\n onSubmitted,\n onSubmit,\n value,\n defaultValue,\n onChange,\n renderMode = 'practice',\n outcome,\n sanitizeHtml,\n onInteraction,\n theme,\n locale,\n disabled,\n}: WrittenResponseProps) {\n // Dev-only boundary validation (Req 2.3), same convention as MC/FIB. The\n // content schema is loose, so a `redact()` projection validates too.\n const devError = useMemo(() => {\n if (!isDevelopment()) {\n return null;\n }\n const result = validateActivity('written-response', data);\n return result.success ? null : new ActivitySchemaError('written-response', result.errors);\n }, [data]);\n\n const { state, start, complete, getTimeSpent, reset } = useActivityState();\n const isControlled = value !== undefined;\n const [innerText, setInnerText] = useState<string>(() => textOf(defaultValue));\n const [summary, setSummary] = useState<string | null>(null);\n\n /**\n * Rich text is rendered ONLY through a caller-supplied sanitiser, and only\n * when the author actually shipped the HTML sidecar. Without both, the\n * plain-text prompt is rendered as an escaped React child.\n */\n const promptHtml = useMemo(() => {\n if (sanitizeHtml === undefined || typeof data.promptHtml !== 'string') {\n return null;\n }\n return sanitizeHtml(data.promptHtml);\n }, [data.promptHtml, sanitizeHtml]);\n\n // Latest seed text, kept in a ref so `defaultValue` is NOT a reset trigger:\n // callers pass an object literal, whose identity changes every render.\n const defaultTextRef = useRef<string>(textOf(defaultValue));\n useEffect(() => {\n defaultTextRef.current = textOf(defaultValue);\n }, [defaultValue]);\n\n // Reset on data-prop change (Req 3.7): back to the seed (empty when there\n // is none — the v1 behaviour), never the previous activity's draft.\n // biome-ignore lint/correctness/useExhaustiveDependencies: data is the reset trigger (Req 3.7)\n useEffect(() => {\n setInnerText(defaultTextRef.current);\n setSummary(null);\n reset();\n }, [data, reset]);\n\n if (devError) {\n throw devError;\n }\n\n const isExam = renderMode === 'exam';\n const isReview = renderMode === 'review';\n\n const text = isControlled ? textOf(value) : innerText;\n const submitted = state === 'completed';\n const inactive = disabled === true || submitted || isReview;\n const wordCount = countWords(text);\n const withinBounds = wordCount >= data.minWords && wordCount <= data.maxWords;\n\n const handleChange = (next: string): void => {\n if (inactive) {\n return;\n }\n if (state === 'idle') {\n start();\n }\n // A controlled component renders from `props.value` only; the caller's\n // state is the single source of truth for the text.\n if (!isControlled) {\n setInnerText(next);\n }\n const nextWordCount = countWords(next);\n onChange?.({ type: 'written-response', text: next, wordCount: nextWordCount });\n onInteraction?.({\n type: 'text-changed',\n activityId: data.id,\n timestamp: Date.now(),\n payload: { wordCount: nextWordCount },\n });\n };\n\n const handleSubmit = (event: React.FormEvent): void => {\n event.preventDefault();\n if (inactive || text.trim().length === 0) {\n return;\n }\n let finalWordCount = wordCount;\n let finalWithinBounds = withinBounds;\n // `exam` mode must not call evaluate(): the component does not grade, and\n // a redacted projection carries no scoring input anyway. The two facts\n // evaluate() returns for a deferred type (recomputed word count, bounds)\n // are exactly what is computed above, so the payload is identical either\n // way; practice keeps going through core so the numbers stay canonical.\n if (!isExam) {\n const itemOutcome = evaluate(data, { type: 'written-response', text, wordCount });\n const partial = itemOutcome.status === 'deferred' ? itemOutcome.partial : undefined;\n if (typeof partial?.wordCount === 'number') {\n finalWordCount = partial.wordCount;\n }\n if (typeof partial?.withinWordBounds === 'boolean') {\n finalWithinBounds = partial.withinWordBounds;\n }\n }\n complete();\n const timeSpent = getTimeSpent();\n const xapiStatement = xAPIBuilder.buildSubmittedStatement({\n actor: ANONYMOUS_ACTOR,\n object: {\n id: objectIdFor(data.id),\n name: { [data.locale ?? 'en-US']: data.title },\n type: 'http://adlnet.gov/expapi/activities/cmi.interaction',\n interactionType: 'long-fill-in',\n },\n timeSpentMs: timeSpent,\n response: text,\n resultExtensions: {\n 'urn:learning-kit:extension:word-count': finalWordCount,\n 'urn:learning-kit:extension:within-word-bounds': finalWithinBounds,\n },\n });\n // Raw response first (an exam runner persists this), then the fuller\n // ungraded submission — mirroring `onSubmit` before `onComplete`.\n onSubmit?.({ type: 'written-response', text, wordCount: finalWordCount });\n onSubmitted?.({\n text,\n wordCount: finalWordCount,\n withinWordBounds: finalWithinBounds,\n timeSpent,\n xapiStatement,\n });\n setSummary('Response submitted. It will be graded and your result will appear here later.');\n onInteraction?.({\n type: 'submitted',\n activityId: data.id,\n timestamp: Date.now(),\n payload: { wordCount: finalWordCount, withinWordBounds: finalWithinBounds },\n });\n };\n\n const promptId = `${data.id}-prompt`;\n const counterId = `${data.id}-counter`;\n const boundsLabel =\n data.minWords > 0 ? `${data.minWords}–${data.maxWords} words` : `up to ${data.maxWords} words`;\n\n return (\n <form\n className=\"lk-wr\"\n aria-label={data.title}\n lang={locale}\n data-render-mode={renderMode}\n style={theme as CSSProperties | undefined}\n onSubmit={handleSubmit}\n >\n {data.media ? <ActivityMedia media={data.media} /> : null}\n {promptHtml === null ? (\n <p className=\"lk-wr-prompt\" id={promptId}>\n {data.prompt}\n </p>\n ) : (\n // A div, not a p: sanitised author HTML may contain block content.\n <div\n className=\"lk-wr-prompt\"\n id={promptId}\n // biome-ignore lint/security/noDangerouslySetInnerHtml: rendered only through the caller-supplied HtmlSanitizer — the SDK never injects unsanitised markup\n dangerouslySetInnerHTML={{ __html: promptHtml }}\n />\n )}\n <textarea\n className=\"lk-wr-textarea\"\n aria-labelledby={promptId}\n aria-describedby={counterId}\n value={text}\n onChange={(event) => handleChange(event.target.value)}\n // `review` uses readOnly rather than disabled: the submitted essay\n // must stay focusable, selectable and scrollable for a screen reader\n // and a keyboard, while remaining uneditable.\n disabled={!isReview && inactive}\n readOnly={isReview}\n rows={8}\n />\n <div\n className=\"lk-wr-counter\"\n id={counterId}\n aria-live=\"polite\"\n data-within-bounds={withinBounds}\n >\n {wordCount} {wordCount === 1 ? 'word' : 'words'} ({boundsLabel})\n </div>\n {isReview ? null : (\n <button\n type=\"submit\"\n className=\"lk-wr-submit\"\n disabled={inactive || text.trim().length === 0}\n >\n Submit\n </button>\n )}\n <FeedbackRegion id={`${data.id}-feedback`}>\n {isReview ? outcome ? <OutcomeSummary outcome={outcome} /> : null : summary}\n </FeedbackRegion>\n </form>\n );\n}\n","'use client';\n\nimport { ActivityErrorBoundary } from '../ActivityErrorBoundary.js';\nimport {\n WrittenResponse as WrittenResponseCore,\n type WrittenResponseProps,\n} from './WrittenResponse.js';\n\nexport type { HtmlSanitizer, Renderable, RenderMode } from '../types.js';\nexport type { WrittenResponseProps, WrittenResponseSubmission } from './WrittenResponse.js';\n\n/**\n * Public Written Response activity: a labelled textarea with a live word\n * counter for free-text writing graded asynchronously (AI or human). The core\n * is wrapped in `ActivityErrorBoundary` so a render failure (incl. the dev\n * schema-validation throw) degrades to an accessible fallback.\n */\nexport function WrittenResponse(props: WrittenResponseProps) {\n return (\n <ActivityErrorBoundary activityTitle={props.data?.title}>\n <WrittenResponseCore {...props} />\n </ActivityErrorBoundary>\n );\n}\n"]}
@@ -0,0 +1,152 @@
1
+ import { MultipleChoice } from './chunk-ZVFWJTT4.js';
2
+ import { FillInTheBlanks } from './chunk-3X7MYAFT.js';
3
+ import { WrittenResponse } from './chunk-AEQQ7VPF.js';
4
+ import { useState, useRef, useEffect } from 'react';
5
+ import { jsx, jsxs } from 'react/jsx-runtime';
6
+
7
+ function ActivitySequence({
8
+ activities,
9
+ renderers,
10
+ onActivityComplete,
11
+ onComplete,
12
+ onFinished,
13
+ onInteraction,
14
+ renderMode,
15
+ theme,
16
+ locale,
17
+ disabled
18
+ }) {
19
+ const [index, setIndex] = useState(0);
20
+ const [outcomes, setOutcomes] = useState(
21
+ () => activities.map(() => null)
22
+ );
23
+ const regionRef = useRef(null);
24
+ const mountedRef = useRef(false);
25
+ const setKey = JSON.stringify(activities.map((activity) => activity.id));
26
+ const [prevSetKey, setPrevSetKey] = useState(setKey);
27
+ if (prevSetKey !== setKey) {
28
+ setPrevSetKey(setKey);
29
+ setIndex(0);
30
+ setOutcomes(activities.map(() => null));
31
+ }
32
+ useEffect(() => {
33
+ if (mountedRef.current) {
34
+ regionRef.current?.focus();
35
+ } else {
36
+ mountedRef.current = true;
37
+ }
38
+ }, [index]);
39
+ const total = activities.length;
40
+ const current = activities[index];
41
+ const go = (next) => {
42
+ setIndex(next);
43
+ };
44
+ const record = (outcome) => {
45
+ if (outcomes[outcome.index] != null) {
46
+ return;
47
+ }
48
+ const next = outcomes.slice();
49
+ next[outcome.index] = outcome;
50
+ setOutcomes(next);
51
+ if (outcome.kind === "scored") {
52
+ onActivityComplete?.(outcome.result, outcome.index);
53
+ }
54
+ if (!next.every((item) => item !== null)) {
55
+ return;
56
+ }
57
+ onFinished?.(next);
58
+ const scored = [];
59
+ for (const item of next) {
60
+ if (item.kind !== "scored") {
61
+ return;
62
+ }
63
+ scored.push(item.result);
64
+ }
65
+ onComplete?.(scored);
66
+ };
67
+ if (!current) {
68
+ return /* @__PURE__ */ jsx("div", { className: "lk-seq" });
69
+ }
70
+ const forwarded = {
71
+ ...onInteraction ? { onInteraction } : {},
72
+ ...renderMode ? { renderMode } : {},
73
+ ...theme ? { theme } : {},
74
+ ...locale ? { locale } : {},
75
+ ...disabled ? { disabled } : {}
76
+ };
77
+ const renderSlot = (activity, slotIndex) => {
78
+ const activityId = activity.id;
79
+ const childProps = {
80
+ onComplete: (result) => record({ kind: "scored", index: slotIndex, activityId, result }),
81
+ ...forwarded
82
+ };
83
+ const CustomRenderer = renderers?.[activity.type];
84
+ if (CustomRenderer) {
85
+ return /* @__PURE__ */ jsx(CustomRenderer, { data: activity, ...childProps });
86
+ }
87
+ if (activity.type === "multiple-choice") {
88
+ return /* @__PURE__ */ jsx(MultipleChoice, { data: activity, ...childProps });
89
+ }
90
+ if (activity.type === "fill-in-the-blanks") {
91
+ return /* @__PURE__ */ jsx(FillInTheBlanks, { data: activity, ...childProps });
92
+ }
93
+ if (activity.type === "written-response") {
94
+ return /* @__PURE__ */ jsx(
95
+ WrittenResponse,
96
+ {
97
+ data: activity,
98
+ onSubmitted: (submission) => record({ kind: "submitted", index: slotIndex, activityId, submission }),
99
+ ...forwarded
100
+ }
101
+ );
102
+ }
103
+ return /* @__PURE__ */ jsx("div", { className: "lk-seq-unsupported", role: "note", children: "This activity type has no renderer. Supply one through the renderers prop." });
104
+ };
105
+ return /* @__PURE__ */ jsxs("div", { className: "lk-seq", lang: locale, children: [
106
+ /* @__PURE__ */ jsxs("div", { className: "lk-seq-progress", "aria-live": "polite", role: "status", children: [
107
+ "Question ",
108
+ index + 1,
109
+ " of ",
110
+ total
111
+ ] }),
112
+ /* @__PURE__ */ jsx(
113
+ "section",
114
+ {
115
+ className: "lk-seq-question",
116
+ ref: regionRef,
117
+ tabIndex: -1,
118
+ "aria-label": `Question ${index + 1} of ${total}`,
119
+ children: activities.map((activity, slotIndex) => {
120
+ const slotKey = `slot-${slotIndex}-${activity.id}`;
121
+ return /* @__PURE__ */ jsx("div", { className: "lk-seq-slot", hidden: slotIndex !== index, children: renderSlot(activity, slotIndex) }, slotKey);
122
+ })
123
+ }
124
+ ),
125
+ /* @__PURE__ */ jsxs("div", { className: "lk-seq-nav", children: [
126
+ /* @__PURE__ */ jsx(
127
+ "button",
128
+ {
129
+ type: "button",
130
+ className: "lk-seq-prev",
131
+ onClick: () => go(index - 1),
132
+ disabled: index === 0,
133
+ children: "Previous"
134
+ }
135
+ ),
136
+ /* @__PURE__ */ jsx(
137
+ "button",
138
+ {
139
+ type: "button",
140
+ className: "lk-seq-next",
141
+ onClick: () => go(index + 1),
142
+ disabled: index === total - 1,
143
+ children: "Next"
144
+ }
145
+ )
146
+ ] })
147
+ ] });
148
+ }
149
+
150
+ export { ActivitySequence };
151
+ //# sourceMappingURL=chunk-BGWXN44S.js.map
152
+ //# sourceMappingURL=chunk-BGWXN44S.js.map