@khorsheed/dsh-library 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.en.md +49 -0
  3. package/README.i18n.yaml +6 -0
  4. package/README.md +48 -2
  5. package/cordis.patch.yml +17 -0
  6. package/lib/client.js +66334 -0
  7. package/lib/index.js +3318 -0
  8. package/lib/profiles-CFrPQdfO.js +1692 -0
  9. package/lib/tool.js +1597 -0
  10. package/lib/typert.host.d.ts +3 -0
  11. package/lib/typert.host.js +3811 -0
  12. package/lib/typert.remote-client.d.ts +137 -0
  13. package/lib/typert.remote-client.js +3077 -0
  14. package/lib/types/cards.d.ts +41 -0
  15. package/lib/types/cards.js +111 -0
  16. package/lib/types/client/LibraryPane.d.ts +26 -0
  17. package/lib/types/client/LibraryPane.js +129 -0
  18. package/lib/types/client/bind/BindChip.d.ts +33 -0
  19. package/lib/types/client/bind/BindChip.js +102 -0
  20. package/lib/types/client/contract.d.ts +167 -0
  21. package/lib/types/client/contract.js +1 -0
  22. package/lib/types/client/covers.d.ts +36 -0
  23. package/lib/types/client/covers.js +66 -0
  24. package/lib/types/client/definition.d.ts +27 -0
  25. package/lib/types/client/definition.js +32 -0
  26. package/lib/types/client/icons-local.d.ts +41 -0
  27. package/lib/types/client/icons-local.js +25 -0
  28. package/lib/types/client/icons.d.ts +27 -0
  29. package/lib/types/client/icons.js +11 -0
  30. package/lib/types/client/index.d.ts +35 -0
  31. package/lib/types/client/index.js +231 -0
  32. package/lib/types/client/judge.d.ts +70 -0
  33. package/lib/types/client/judge.js +262 -0
  34. package/lib/types/client/locales.d.ts +553 -0
  35. package/lib/types/client/locales.js +1076 -0
  36. package/lib/types/client/look.d.ts +63 -0
  37. package/lib/types/client/look.js +64 -0
  38. package/lib/types/client/math.d.ts +24 -0
  39. package/lib/types/client/math.js +47 -0
  40. package/lib/types/client/mistakes.d.ts +21 -0
  41. package/lib/types/client/mistakes.js +28 -0
  42. package/lib/types/client/preset-visibility.d.ts +109 -0
  43. package/lib/types/client/preset-visibility.js +123 -0
  44. package/lib/types/client/quiz-drafts.d.ts +29 -0
  45. package/lib/types/client/quiz-drafts.js +50 -0
  46. package/lib/types/client/reveal.d.ts +21 -0
  47. package/lib/types/client/reveal.js +31 -0
  48. package/lib/types/client/store.d.ts +111 -0
  49. package/lib/types/client/store.js +48 -0
  50. package/lib/types/client/titles.d.ts +17 -0
  51. package/lib/types/client/titles.js +23 -0
  52. package/lib/types/client/ui.d.ts +96 -0
  53. package/lib/types/client/ui.js +124 -0
  54. package/lib/types/client/views/book.d.ts +57 -0
  55. package/lib/types/client/views/book.js +164 -0
  56. package/lib/types/client/views/cards.d.ts +17 -0
  57. package/lib/types/client/views/cards.js +53 -0
  58. package/lib/types/client/views/home.d.ts +26 -0
  59. package/lib/types/client/views/home.js +263 -0
  60. package/lib/types/client/views/lesson.d.ts +42 -0
  61. package/lib/types/client/views/lesson.js +146 -0
  62. package/lib/types/client/views/path.d.ts +32 -0
  63. package/lib/types/client/views/path.js +64 -0
  64. package/lib/types/client/views/quiz.d.ts +42 -0
  65. package/lib/types/client/views/quiz.js +366 -0
  66. package/lib/types/client/views/station-quiz.d.ts +43 -0
  67. package/lib/types/client/views/station-quiz.js +203 -0
  68. package/lib/types/client/views/station.d.ts +18 -0
  69. package/lib/types/client/views/station.js +142 -0
  70. package/lib/types/image-refs.d.ts +35 -0
  71. package/lib/types/image-refs.js +71 -0
  72. package/lib/types/index.d.ts +51 -0
  73. package/lib/types/index.js +31 -0
  74. package/lib/types/mastery.d.ts +81 -0
  75. package/lib/types/mastery.js +110 -0
  76. package/lib/types/profiles.d.ts +31 -0
  77. package/lib/types/profiles.js +25 -0
  78. package/lib/types/remote.d.ts +355 -0
  79. package/lib/types/remote.js +332 -0
  80. package/lib/types/service/assess.d.ts +144 -0
  81. package/lib/types/service/assess.js +773 -0
  82. package/lib/types/service/bindings.d.ts +92 -0
  83. package/lib/types/service/bindings.js +198 -0
  84. package/lib/types/service/cards.d.ts +85 -0
  85. package/lib/types/service/cards.js +320 -0
  86. package/lib/types/service/core.d.ts +124 -0
  87. package/lib/types/service/core.js +169 -0
  88. package/lib/types/service/pages.d.ts +60 -0
  89. package/lib/types/service/pages.js +125 -0
  90. package/lib/types/service/roadmap.d.ts +99 -0
  91. package/lib/types/service/roadmap.js +284 -0
  92. package/lib/types/service/rules.d.ts +63 -0
  93. package/lib/types/service/rules.js +245 -0
  94. package/lib/types/service/shelf.d.ts +136 -0
  95. package/lib/types/service/shelf.js +422 -0
  96. package/lib/types/service.d.ts +42 -0
  97. package/lib/types/service.js +41 -0
  98. package/lib/types/store.d.ts +257 -0
  99. package/lib/types/store.js +1376 -0
  100. package/lib/types/tool-definition.d.ts +49 -0
  101. package/lib/types/tool-definition.js +49 -0
  102. package/lib/types/tool.d.ts +71 -0
  103. package/lib/types/tool.js +174 -0
  104. package/lib/types/tools/assess.d.ts +11 -0
  105. package/lib/types/tools/assess.js +82 -0
  106. package/lib/types/tools/binding.d.ts +11 -0
  107. package/lib/types/tools/binding.js +46 -0
  108. package/lib/types/tools/cards.d.ts +11 -0
  109. package/lib/types/tools/cards.js +100 -0
  110. package/lib/types/tools/kit.d.ts +215 -0
  111. package/lib/types/tools/kit.js +281 -0
  112. package/lib/types/tools/notes.d.ts +11 -0
  113. package/lib/types/tools/notes.js +40 -0
  114. package/lib/types/tools/page.d.ts +11 -0
  115. package/lib/types/tools/page.js +59 -0
  116. package/lib/types/tools/project.d.ts +11 -0
  117. package/lib/types/tools/project.js +67 -0
  118. package/lib/types/tools/quiz.d.ts +11 -0
  119. package/lib/types/tools/quiz.js +74 -0
  120. package/lib/types/tools/read.d.ts +11 -0
  121. package/lib/types/tools/read.js +127 -0
  122. package/lib/types/tools/resource.d.ts +11 -0
  123. package/lib/types/tools/resource.js +147 -0
  124. package/lib/types/tools/roadmap.d.ts +11 -0
  125. package/lib/types/tools/roadmap.js +102 -0
  126. package/lib/types/types.d.ts +752 -0
  127. package/lib/types/types.js +159 -0
  128. package/locale/en.json +6 -0
  129. package/locale/zh.json +6 -0
  130. package/package.json +135 -3
@@ -0,0 +1,42 @@
1
+ /**
2
+ * A station's 讲义 as its own page (proposal library-lesson-page): pushed from
3
+ * the station's guide (打开讲义) or opened by itself once the coach has written
4
+ * them after 开始学这一站. The markdown splits at its `##` headings — one
5
+ * section per knowledge point — so the contents can jump between them and
6
+ * show each point's mastery; wide panes keep the contents beside the text.
7
+ * Also the station guide's lesson card (LessonCard).
8
+ *
9
+ * @module @khorsheed/dsh-library/client/views/lesson
10
+ */
11
+ import { type ReactNode } from 'react';
12
+ import type { LibraryResource, RoadmapNode } from '../../types.ts';
13
+ import { type ViewProps } from '../ui.tsx';
14
+ /** One `##` section of the 讲义; the text before the first heading is the opening. */
15
+ interface LessonSection {
16
+ readonly id: string;
17
+ readonly title: string | undefined;
18
+ readonly body: string;
19
+ }
20
+ /** Split the markdown at its `##` headings (deeper headings stay inside their section). */
21
+ export declare function lessonSections(text: string): LessonSection[];
22
+ /** The 讲义 page. */
23
+ export declare function LessonView({ p, projectId, resourceId, nodeId }: ViewProps & {
24
+ projectId: string;
25
+ resourceId: string;
26
+ nodeId: string;
27
+ }): ReactNode;
28
+ /**
29
+ * The station guide's lesson card: no 讲义 yet → 开始学这一站 (a draft in the
30
+ * conversation input; the coach writes the 讲义 when it is sent); asked and
31
+ * waiting → 正在备课 (the 讲义 page opens by itself when they land); written →
32
+ * the points with their mastery and 打开讲义.
33
+ */
34
+ export declare function LessonCard({ p, projectId, resourceId, node, resource }: {
35
+ p: ViewProps['p'];
36
+ projectId: string;
37
+ resourceId: string;
38
+ node: RoadmapNode;
39
+ resource: LibraryResource | undefined;
40
+ }): ReactNode;
41
+ export {};
42
+ //# sourceMappingURL=lesson.d.ts.map
@@ -0,0 +1,146 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * A station's 讲义 as its own page (proposal library-lesson-page): pushed from
4
+ * the station's guide (打开讲义) or opened by itself once the coach has written
5
+ * them after 开始学这一站. The markdown splits at its `##` headings — one
6
+ * section per knowledge point — so the contents can jump between them and
7
+ * show each point's mastery; wide panes keep the contents beside the text.
8
+ * Also the station guide's lesson card (LessonCard).
9
+ *
10
+ * @module @khorsheed/dsh-library/client/views/lesson
11
+ */
12
+ import { useEffect, useMemo, useRef, useState } from 'react';
13
+ import { MarkdownText } from '@deepseek-ai/dsh-client-ui-primitives';
14
+ import { PanelHeader } from '@khorsheed/dsh-client-ui-panel-kit/src/client/index.ts';
15
+ import { stationMastery } from "../../mastery.js";
16
+ import { outcome, StateLine, useLoad, value, when } from "../ui.js";
17
+ import css from '../LibraryPane.module.css';
18
+ /** How long 正在备课 waits for the 讲义 before offering to ask again. */
19
+ const PREPARE_PATIENCE_MS = 10 * 60_000;
20
+ /** Split the markdown at its `##` headings (deeper headings stay inside their section). */
21
+ export function lessonSections(text) {
22
+ const sections = [];
23
+ let title;
24
+ let lines = [];
25
+ const flush = () => {
26
+ const body = lines.join('\n').trim();
27
+ if (title !== undefined || body !== '')
28
+ sections.push({ id: `s${sections.length}`, title, body });
29
+ };
30
+ for (const line of text.replace(/^#\s+.*\n/, '').split('\n')) {
31
+ const heading = /^##\s+(.+?)\s*$/.exec(line);
32
+ if (heading === null) {
33
+ lines.push(line);
34
+ continue;
35
+ }
36
+ flush();
37
+ title = heading[1].replace(/[*_`]/g, '');
38
+ lines = [];
39
+ }
40
+ flush();
41
+ return sections;
42
+ }
43
+ /** The point a section heading names, by its title. */
44
+ function pointOf(title, points) {
45
+ if (title === undefined)
46
+ return undefined;
47
+ const squash = (value) => value.replace(/\s/g, '').toLowerCase();
48
+ return points.find(point => squash(title).includes(squash(point.title)));
49
+ }
50
+ /** The station's points with their mastery, for the dots (empty while loading). */
51
+ function useStationPoints(p, projectId, node) {
52
+ const rev = p.useStore(s => s.rev);
53
+ const assessments = useLoad(() => value(p.listAssessments(projectId)), [projectId, rev]);
54
+ const attempts = useLoad(() => value(p.listAttempts(projectId)), [projectId, rev]);
55
+ if (node === undefined)
56
+ return [];
57
+ return stationMastery(node, assessments.data?.assessments ?? [], attempts.data?.attempts ?? []).points;
58
+ }
59
+ /** The 讲义 page. */
60
+ export function LessonView({ p, projectId, resourceId, nodeId }) {
61
+ const { t, actions, useStore } = p;
62
+ const rev = useStore(s => s.rev);
63
+ const loaded = useLoad(async () => {
64
+ const [detail, roadmaps] = await Promise.all([outcome(p.getProject(projectId)), value(p.getRoadmap(projectId, resourceId))]);
65
+ return { resource: detail.resources.find(candidate => candidate.id === resourceId), node: roadmaps.roadmap?.nodes.find(candidate => candidate.id === nodeId) };
66
+ }, [projectId, resourceId, nodeId, rev]);
67
+ const node = loaded.data?.node;
68
+ const points = useStationPoints(p, projectId, node);
69
+ const sections = useMemo(() => lessonSections(node?.notes?.text ?? ''), [node?.notes?.text]);
70
+ const [active, setActive] = useState();
71
+ const [notice, setNotice] = useState();
72
+ const refs = useRef(new Map());
73
+ const markdownLabels = useMemo(() => ({
74
+ code: { copyLabel: t('markdown.copy'), copiedLabel: t('markdown.copied') },
75
+ footnotes: t('markdown.footnotes'),
76
+ }), [t]);
77
+ const jump = (id) => {
78
+ setActive(id);
79
+ refs.current.get(id)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
80
+ };
81
+ const ask = () => {
82
+ if (node === undefined)
83
+ return;
84
+ void p.bindTarget({ projectId, resourceId });
85
+ setNotice(p.primeConversation(t('prime.lessonAsk', { node: node.title })) ? t('lesson.asked') : t('notice.noConversation'));
86
+ };
87
+ const titled = sections.filter(section => section.title !== undefined);
88
+ return (_jsxs(_Fragment, { children: [_jsx(PanelHeader, { title: node === undefined ? t('lesson.title') : t('lesson.titleOf', { node: node.title }), meta: loaded.data?.resource?.title, onBack: () => { actions.pop(); }, backLabel: t('action.back'), moreLabel: t('action.more') }), _jsxs("div", { className: css.body, children: [_jsx(StateLine, { loaded: loaded, t: t }), node !== undefined && node.notes === undefined && _jsx("div", { className: css.muted, children: t('lesson.none') }), node?.notes !== undefined && (_jsxs("div", { className: css.lesson, children: [_jsxs("nav", { className: css.lessonToc, "aria-label": t('lesson.contents'), children: [_jsx("span", { className: css.lessonTocHead, children: t('lesson.contentsMeta', { count: String(points.length) }) }), sections.map((section, index) => {
89
+ const point = pointOf(section.title, points);
90
+ return (_jsxs("button", { type: "button", className: css.lessonTocItem, "aria-current": active === section.id ? 'true' : undefined, onClick: () => { jump(section.id); }, children: [_jsx("i", { className: css.lessonDot, "data-state": point?.state ?? 'none', "aria-hidden": "true" }), _jsx("span", { children: section.title ?? (index === 0 ? t('lesson.opening') : '') })] }, section.id));
91
+ }), _jsxs("div", { className: css.lessonTocActions, children: [_jsx("button", { type: "button", className: css.primary, onClick: () => { actions.replace({ view: 'node', projectId, resourceId, nodeId, tab: 'quiz' }); }, children: t('lesson.toQuiz') }), _jsx("button", { type: "button", className: css.ghost, onClick: ask, children: t('lesson.ask') })] })] }), _jsxs("article", { className: css.lessonBody, children: [_jsxs("div", { className: css.muted, children: [t('lesson.meta', { at: when(node.notes.updatedAt, t), count: String(titled.length) }), node.notes.revisions > 1 ? ` · ${t('lesson.revised', { n: String(node.notes.revisions - 1) })}` : ''] }), sections.map(section => (_jsxs("section", { ref: element => { if (element === null)
92
+ refs.current.delete(section.id);
93
+ else
94
+ refs.current.set(section.id, element); }, className: css.lessonSection, children: [section.title !== undefined && _jsx("h2", { className: css.lessonHeading, children: section.title }), _jsx(MarkdownText, { text: section.body, labels: markdownLabels })] }, section.id))), _jsxs("section", { className: css.stepBox, children: [_jsxs("span", { className: css.stepText, children: [_jsx("strong", { children: t('lesson.doneTitle') }), _jsx("span", { className: css.muted, children: t('lesson.doneHint', { count: String(points.length) }) })] }), _jsxs("div", { className: css.askActions, children: [_jsx("button", { type: "button", className: css.ghost, onClick: ask, children: t('lesson.ask') }), _jsx("button", { type: "button", className: css.primary, onClick: () => { actions.replace({ view: 'node', projectId, resourceId, nodeId, tab: 'quiz' }); }, children: t('lesson.toQuiz') })] })] }), notice !== undefined && _jsx("div", { className: css.notice, children: notice })] })] }))] })] }));
95
+ }
96
+ /**
97
+ * The station guide's lesson card: no 讲义 yet → 开始学这一站 (a draft in the
98
+ * conversation input; the coach writes the 讲义 when it is sent); asked and
99
+ * waiting → 正在备课 (the 讲义 page opens by itself when they land); written →
100
+ * the points with their mastery and 打开讲义.
101
+ */
102
+ export function LessonCard({ p, projectId, resourceId, node, resource }) {
103
+ const { t, actions, useStore } = p;
104
+ const key = `${projectId}:${resourceId}:${node.id}`;
105
+ const since = useStore(s => s.preparing[key]);
106
+ const points = useStationPoints(p, projectId, node);
107
+ const [failed, setFailed] = useState(false);
108
+ const [now, setNow] = useState(Date.now());
109
+ const notes = node.notes;
110
+ // Waiting: re-read now and then; the page opens itself once the 讲义 land.
111
+ useEffect(() => {
112
+ if (since === undefined)
113
+ return;
114
+ if (notes !== undefined && Date.parse(notes.updatedAt) >= since) {
115
+ actions.setPreparing(key, undefined);
116
+ actions.push({ view: 'lesson', projectId, resourceId, nodeId: node.id });
117
+ return;
118
+ }
119
+ const timer = setInterval(() => { setNow(Date.now()); actions.refresh(); }, 4000);
120
+ return () => { clearInterval(timer); };
121
+ }, [since, notes?.updatedAt]);
122
+ const start = () => {
123
+ void p.bindTarget({ projectId, resourceId });
124
+ const text = t(resource?.origin === 'authored' ? 'prime.lessonStart' : 'prime.lessonStartBook', {
125
+ book: resource?.title ?? '',
126
+ node: node.title,
127
+ points: (node.points ?? []).map(point => point.title).join('、'),
128
+ });
129
+ if (!p.primeConversation(text)) {
130
+ setFailed(true);
131
+ return;
132
+ }
133
+ setFailed(false);
134
+ actions.setPreparing(key, Date.now());
135
+ };
136
+ const ask = () => {
137
+ void p.bindTarget({ projectId, resourceId });
138
+ if (!p.primeConversation(t('prime.lessonAsk', { node: node.title })))
139
+ setFailed(true);
140
+ };
141
+ const waiting = since !== undefined && notes === undefined;
142
+ const stale = waiting && now - since > PREPARE_PATIENCE_MS;
143
+ return (_jsxs("section", { className: css.stepBox, "data-lesson": "true", children: [notes === undefined ? (_jsxs(_Fragment, { children: [_jsxs("span", { className: css.stepText, children: [_jsx("strong", { children: waiting ? (stale ? t('lesson.prepareStale') : t('lesson.preparing')) : t('lesson.notReady') }), _jsx("span", { className: css.muted, children: waiting ? t('lesson.preparingHint') : t('lesson.notReadyHint') })] }), _jsxs("div", { className: css.askActions, children: [waiting && !stale
144
+ ? _jsx("button", { type: "button", className: css.textButton, onClick: () => { actions.setPreparing(key, undefined); }, children: t('lesson.cancelWait') })
145
+ : _jsx("button", { type: "button", className: css.ghost, onClick: ask, children: t('lesson.ask') }), _jsx("button", { type: "button", className: css.primary, disabled: waiting && !stale, onClick: start, children: t('lesson.start') })] })] })) : (_jsxs(_Fragment, { children: [_jsxs("span", { className: css.stepText, children: [_jsx("strong", { children: t('lesson.ready', { count: String((node.points ?? []).length) }) }), _jsx("span", { className: css.muted, children: t('lesson.meta', { at: when(notes.updatedAt, t), count: String(lessonSections(notes.text).filter(section => section.title !== undefined).length) }) }), points.length > 0 && (_jsx("span", { className: css.lessonPoints, children: points.map(point => _jsxs("span", { children: [_jsx("i", { className: css.lessonDot, "data-state": point.state, "aria-hidden": "true" }), point.title] }, point.id)) }))] }), _jsxs("div", { className: css.askActions, children: [_jsx("button", { type: "button", className: css.ghost, onClick: ask, children: t('lesson.ask') }), _jsx("button", { type: "button", className: css.primary, onClick: () => { actions.push({ view: 'lesson', projectId, resourceId, nodeId: node.id }); }, children: t('lesson.open') })] })] })), failed && _jsx("span", { className: css.muted, children: t('notice.noConversation') })] }));
146
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * A textbook's adopted path as a vertical lesson rail (proposal
3
+ * library-slimming §5, option A): one row per station with its full title,
4
+ * a disc that says where the learner stands, and its knowledge points as
5
+ * mastery dots. Rows group under the contents' parts. Nothing is locked
6
+ * (D6): the next station — the first not yet passed — is only highlighted
7
+ * and opens its card (开始学 / 接着学 / 去订正).
8
+ *
9
+ * @module @khorsheed/dsh-library/client/views/path
10
+ */
11
+ import type { ReactNode } from 'react';
12
+ import { type LibraryStationMastery } from '../../mastery.ts';
13
+ import { type LibraryResource, type LibraryRoadmap, type RoadmapNode } from '../../types.ts';
14
+ import { type ViewProps } from '../ui.tsx';
15
+ /** Where the learner stands on one station. */
16
+ export type StationStatus = 'done' | 'studying' | 'ready' | 'todo';
17
+ /**
18
+ * Passed (done) → 已过关; 讲义 written and the learner has been quizzed or has
19
+ * a weak point → 学习中; 讲义 written → 已备课; nothing yet → 未开始.
20
+ */
21
+ export declare function stationStatus(node: RoadmapNode, mastery: LibraryStationMastery): StationStatus;
22
+ /** The rail. */
23
+ export declare function Path({ p, projectId, resource, roadmap, onOpen, onLesson, onRetake }: {
24
+ p: ViewProps['p'];
25
+ projectId: string;
26
+ resource: LibraryResource;
27
+ roadmap: LibraryRoadmap;
28
+ onOpen: (node: RoadmapNode) => void;
29
+ onLesson: (node: RoadmapNode) => void;
30
+ onRetake: (node: RoadmapNode) => void;
31
+ }): ReactNode;
32
+ //# sourceMappingURL=path.d.ts.map
@@ -0,0 +1,64 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { stationMastery } from "../../mastery.js";
3
+ import { coverKind, parseLocator } from "../../types.js";
4
+ import { IconCheckOutlineMedium } from "../icons.js";
5
+ import { Segments, useLoad, value } from "../ui.js";
6
+ import { stationTitle } from "../titles.js";
7
+ import css from '../LibraryPane.module.css';
8
+ /**
9
+ * Passed (done) → 已过关; 讲义 written and the learner has been quizzed or has
10
+ * a weak point → 学习中; 讲义 written → 已备课; nothing yet → 未开始.
11
+ */
12
+ export function stationStatus(node, mastery) {
13
+ if (node.state === 'done')
14
+ return 'done';
15
+ const tried = (node.quizStats?.attempts ?? 0) > 0 || mastery.open.length > 0 || mastery.points.some(point => point.state !== 'untested');
16
+ if (tried)
17
+ return 'studying';
18
+ return node.notes === undefined ? 'todo' : 'ready';
19
+ }
20
+ /** The contents part a station's first row sits under, when the contents has parts. */
21
+ function partOf(node, resource) {
22
+ const cover = node.covers.find(candidate => coverKind(candidate) === 'resource' && candidate.ref === resource.id && candidate.locator !== undefined);
23
+ const first = cover?.locator === undefined ? undefined : parseLocator(cover.locator)[0]?.from;
24
+ return first === undefined ? undefined : resource.toc?.find(row => row.n === first)?.part;
25
+ }
26
+ /** The station's rows as a short label (第 3 章 / 第 3–4 章). */
27
+ function rowsLabel(node, resource, t) {
28
+ const cover = node.covers.find(candidate => coverKind(candidate) === 'resource' && candidate.ref === resource.id && candidate.locator !== undefined);
29
+ return cover?.locator === undefined ? undefined : t('path.rows', { rows: cover.locator.replace(/-/g, '–') });
30
+ }
31
+ /** The rail. */
32
+ export function Path({ p, projectId, resource, roadmap, onOpen, onLesson, onRetake }) {
33
+ const { t, useStore } = p;
34
+ const rev = useStore(s => s.rev);
35
+ const assessments = useLoad(() => value(p.listAssessments(projectId)), [projectId, rev]);
36
+ const attempts = useLoad(() => value(p.listAttempts(projectId)), [projectId, rev]);
37
+ const rows = roadmap.nodes.map(node => {
38
+ const mastery = stationMastery(node, assessments.data?.assessments ?? [], attempts.data?.attempts ?? []);
39
+ return { node, mastery, status: stationStatus(node, mastery) };
40
+ });
41
+ const levels = rows.filter(row => row.node.gate !== 'read');
42
+ const passed = levels.filter(row => row.status === 'done').length;
43
+ const allPoints = rows.flatMap(row => row.mastery.points);
44
+ const mastered = allPoints.filter(point => point.state === 'mastered').length;
45
+ // The recommended next station: the first level not yet passed.
46
+ const next = levels.find(row => row.status !== 'done')?.node.id;
47
+ let lastPart;
48
+ let number = 0;
49
+ return (_jsxs("section", { className: css.section, children: [_jsxs("div", { className: css.railHead, children: [_jsxs("span", { className: css.railCounts, children: [_jsxs("span", { children: [_jsx("b", { children: passed }), " / ", levels.length, " ", t('path.stationsPassed')] }), allPoints.length > 0 && _jsxs("span", { children: [_jsx("b", { children: mastered }), " / ", allPoints.length, " ", t('path.pointsMastered')] })] }), _jsx(Segments, { done: passed, total: levels.length, label: t('path.progress', { done: String(passed), total: String(levels.length) }) })] }), _jsx("ol", { className: css.rail, children: rows.map(({ node, mastery, status }) => {
50
+ const part = partOf(node, resource);
51
+ const showPart = part !== undefined && part !== lastPart;
52
+ lastPart = part ?? lastPart;
53
+ const side = node.gate === 'read';
54
+ if (!side)
55
+ number += 1;
56
+ const isNext = node.id === next;
57
+ const best = node.quizStats?.bestRatio;
58
+ const open = mastery.open.length;
59
+ return (_jsxs("li", { className: css.railItem, "data-status": status, "data-side": side ? 'true' : undefined, "data-next": isNext ? 'true' : undefined, children: [showPart && _jsx("span", { className: css.railPart, children: part }), _jsxs("div", { className: css.railRow, children: [_jsx("button", { type: "button", className: css.railDisc, "data-status": status, "data-next": isNext ? 'true' : undefined, "aria-label": node.title, onClick: () => { onOpen(node); }, children: status === 'done' ? _jsx(IconCheckOutlineMedium, { size: 18 }) : side ? '·' : number }), _jsxs("div", { className: css.railBody, children: [_jsx("button", { type: "button", className: css.railTitle, onClick: () => { onOpen(node); }, children: stationTitle(node.title) }), _jsxs("span", { className: css.railMeta, children: [status === 'done' && _jsx("span", { className: css.railState, "data-status": "done", children: best === undefined ? t('path.done') : t('path.doneBest', { percent: String(Math.round(best * 100)) }) }), status === 'studying' && _jsx("span", { className: css.railState, "data-status": "studying", children: open > 0 ? t('path.studyingOpen', { count: String(open) }) : t('path.studying') }), status === 'ready' && _jsx("span", { className: css.railState, "data-status": "ready", children: t('path.ready') }), rowsLabel(node, resource, t) !== undefined && _jsx("span", { children: rowsLabel(node, resource, t) }), mastery.points.length > 0 && (_jsx("span", { className: css.railDots, "aria-label": t('path.dotsLabel'), children: mastery.points.map(point => _jsx("i", { className: css.lessonDot, "data-state": point.state, title: `${point.title} · ${t(`point.${point.state}`)}` }, point.id)) }))] }), isNext && (_jsxs("span", { className: css.railNext, children: [_jsx("span", { className: css.muted, children: t(status === 'todo' ? 'path.nextTodo' : 'path.nextHint') }), _jsxs("span", { className: css.btnRow, children: [open > 0 && _jsx("button", { type: "button", className: css.ghost, onClick: () => { onRetake(node); }, children: t('path.retake', { count: String(open) }) }), _jsx("button", { type: "button", className: css.primary, onClick: () => { if (node.notes === undefined)
60
+ onOpen(node);
61
+ else
62
+ onLesson(node); }, children: t(node.notes === undefined ? 'lesson.start' : 'path.continue') })] })] }))] })] })] }, node.id));
63
+ }) })] }));
64
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The chapter quiz in the sidebar, its result, and the daily review.
3
+ *
4
+ * A quiz is answered here: choice and fill questions are judged on the spot
5
+ * against the answer key; open questions are collected and handed to the
6
+ * coach, who judges them and ledgers the whole sitting (one attempt per
7
+ * sitting). A sitting with no open question is ledgered from here directly.
8
+ * 「这题有问题」 voids a question — it counts for nothing.
9
+ *
10
+ * Review is flashcards over the due cards: front, flip, self-rate. It moves
11
+ * only the schedule — never a node, never weakness.
12
+ *
13
+ * @module @khorsheed/dsh-library/client/views/quiz
14
+ */
15
+ import { type ReactNode } from 'react';
16
+ import { type LibraryAssessResult } from '../../types.ts';
17
+ import { type ViewProps } from '../ui.tsx';
18
+ /** The quiz screen: the whole paper on one scroll — every question numbered, judged on the spot, 交卷 once all are answered. The in-progress sitting persists locally (quiz-drafts) — leaving the paper never loses an answer. */
19
+ export declare function QuizView({ p, projectId, resourceId, quizId }: ViewProps & {
20
+ projectId: string;
21
+ resourceId: string | undefined;
22
+ quizId: string;
23
+ }): ReactNode;
24
+ /** The sitting's result: the score, what unlocked, how the cards moved — and the loop's next step, which rides the conversation from here (a retake of the misses or a fresh set), so the flow never dead-ends on the score. */
25
+ export declare function ResultView({ p, projectId, resourceId, result }: ViewProps & {
26
+ projectId: string;
27
+ resourceId: string | undefined;
28
+ result: LibraryAssessResult;
29
+ }): ReactNode;
30
+ /**
31
+ * Daily review as answering, not looking (M4): each due card is asked the
32
+ * way that fits it — a word as a cloze over its own sentence or a choice of
33
+ * its meaning against the learner's other glosses, a well-known word as a
34
+ * spelling, a concept as a one-line explanation checked against the answer.
35
+ * The pane judges what it can and maps it to the schedule (first try →
36
+ * recalled, after a hint → fuzzy, missed → forgot); concepts are self-rated.
37
+ * 「翻卡」 in the header falls back to the plain flashcard.
38
+ */
39
+ export declare function ReviewView({ p, projectId }: ViewProps & {
40
+ projectId?: string;
41
+ }): ReactNode;
42
+ //# sourceMappingURL=quiz.d.ts.map