@khorsheed/dsh-library 0.0.0-stage → 0.1.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.
Files changed (136) hide show
  1. package/CHANGELOG.md +14 -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 +66276 -0
  7. package/lib/index.js +3318 -0
  8. package/lib/profiles-BXM8xNUf.js +1608 -0
  9. package/lib/profiles-BrbeM-bi.js +1635 -0
  10. package/lib/profiles-CFrPQdfO.js +1692 -0
  11. package/lib/profiles-DxtzIUvd.js +1632 -0
  12. package/lib/profiles-O0ddEit3.js +1631 -0
  13. package/lib/profiles-Oj-zBAY9.js +1692 -0
  14. package/lib/profiles-gIEviEuO.js +1610 -0
  15. package/lib/store-CIcZgTsg.js +1456 -0
  16. package/lib/store-DfYOmS44.js +1418 -0
  17. package/lib/tool.js +1597 -0
  18. package/lib/typert.host.d.ts +3 -0
  19. package/lib/typert.host.js +3811 -0
  20. package/lib/typert.remote-client.d.ts +137 -0
  21. package/lib/typert.remote-client.js +3077 -0
  22. package/lib/types/cards.d.ts +41 -0
  23. package/lib/types/cards.js +111 -0
  24. package/lib/types/client/LibraryPane.d.ts +26 -0
  25. package/lib/types/client/LibraryPane.js +117 -0
  26. package/lib/types/client/bind/BindChip.d.ts +33 -0
  27. package/lib/types/client/bind/BindChip.js +102 -0
  28. package/lib/types/client/contract.d.ts +164 -0
  29. package/lib/types/client/contract.js +1 -0
  30. package/lib/types/client/covers.d.ts +36 -0
  31. package/lib/types/client/covers.js +66 -0
  32. package/lib/types/client/definition.d.ts +27 -0
  33. package/lib/types/client/definition.js +32 -0
  34. package/lib/types/client/icons-local.d.ts +41 -0
  35. package/lib/types/client/icons-local.js +25 -0
  36. package/lib/types/client/icons.d.ts +27 -0
  37. package/lib/types/client/icons.js +11 -0
  38. package/lib/types/client/index.d.ts +35 -0
  39. package/lib/types/client/index.js +219 -0
  40. package/lib/types/client/judge.d.ts +70 -0
  41. package/lib/types/client/judge.js +262 -0
  42. package/lib/types/client/locales.d.ts +553 -0
  43. package/lib/types/client/locales.js +1076 -0
  44. package/lib/types/client/look.d.ts +63 -0
  45. package/lib/types/client/look.js +64 -0
  46. package/lib/types/client/math.d.ts +24 -0
  47. package/lib/types/client/math.js +47 -0
  48. package/lib/types/client/mistakes.d.ts +21 -0
  49. package/lib/types/client/mistakes.js +28 -0
  50. package/lib/types/client/preset-visibility.d.ts +109 -0
  51. package/lib/types/client/preset-visibility.js +123 -0
  52. package/lib/types/client/quiz-drafts.d.ts +29 -0
  53. package/lib/types/client/quiz-drafts.js +50 -0
  54. package/lib/types/client/store.d.ts +109 -0
  55. package/lib/types/client/store.js +44 -0
  56. package/lib/types/client/titles.d.ts +17 -0
  57. package/lib/types/client/titles.js +23 -0
  58. package/lib/types/client/ui.d.ts +96 -0
  59. package/lib/types/client/ui.js +124 -0
  60. package/lib/types/client/views/book.d.ts +57 -0
  61. package/lib/types/client/views/book.js +164 -0
  62. package/lib/types/client/views/cards.d.ts +17 -0
  63. package/lib/types/client/views/cards.js +53 -0
  64. package/lib/types/client/views/home.d.ts +26 -0
  65. package/lib/types/client/views/home.js +263 -0
  66. package/lib/types/client/views/lesson.d.ts +42 -0
  67. package/lib/types/client/views/lesson.js +146 -0
  68. package/lib/types/client/views/path.d.ts +32 -0
  69. package/lib/types/client/views/path.js +64 -0
  70. package/lib/types/client/views/quiz.d.ts +42 -0
  71. package/lib/types/client/views/quiz.js +366 -0
  72. package/lib/types/client/views/station-quiz.d.ts +43 -0
  73. package/lib/types/client/views/station-quiz.js +203 -0
  74. package/lib/types/client/views/station.d.ts +18 -0
  75. package/lib/types/client/views/station.js +142 -0
  76. package/lib/types/image-refs.d.ts +35 -0
  77. package/lib/types/image-refs.js +71 -0
  78. package/lib/types/index.d.ts +51 -0
  79. package/lib/types/index.js +31 -0
  80. package/lib/types/mastery.d.ts +81 -0
  81. package/lib/types/mastery.js +110 -0
  82. package/lib/types/profiles.d.ts +31 -0
  83. package/lib/types/profiles.js +25 -0
  84. package/lib/types/remote.d.ts +355 -0
  85. package/lib/types/remote.js +332 -0
  86. package/lib/types/service/assess.d.ts +144 -0
  87. package/lib/types/service/assess.js +773 -0
  88. package/lib/types/service/bindings.d.ts +92 -0
  89. package/lib/types/service/bindings.js +198 -0
  90. package/lib/types/service/cards.d.ts +85 -0
  91. package/lib/types/service/cards.js +320 -0
  92. package/lib/types/service/core.d.ts +124 -0
  93. package/lib/types/service/core.js +169 -0
  94. package/lib/types/service/pages.d.ts +60 -0
  95. package/lib/types/service/pages.js +125 -0
  96. package/lib/types/service/roadmap.d.ts +99 -0
  97. package/lib/types/service/roadmap.js +284 -0
  98. package/lib/types/service/rules.d.ts +63 -0
  99. package/lib/types/service/rules.js +245 -0
  100. package/lib/types/service/shelf.d.ts +136 -0
  101. package/lib/types/service/shelf.js +422 -0
  102. package/lib/types/service.d.ts +42 -0
  103. package/lib/types/service.js +41 -0
  104. package/lib/types/store.d.ts +257 -0
  105. package/lib/types/store.js +1376 -0
  106. package/lib/types/tool-definition.d.ts +49 -0
  107. package/lib/types/tool-definition.js +49 -0
  108. package/lib/types/tool.d.ts +71 -0
  109. package/lib/types/tool.js +174 -0
  110. package/lib/types/tools/assess.d.ts +11 -0
  111. package/lib/types/tools/assess.js +82 -0
  112. package/lib/types/tools/binding.d.ts +11 -0
  113. package/lib/types/tools/binding.js +46 -0
  114. package/lib/types/tools/cards.d.ts +11 -0
  115. package/lib/types/tools/cards.js +100 -0
  116. package/lib/types/tools/kit.d.ts +215 -0
  117. package/lib/types/tools/kit.js +281 -0
  118. package/lib/types/tools/notes.d.ts +11 -0
  119. package/lib/types/tools/notes.js +40 -0
  120. package/lib/types/tools/page.d.ts +11 -0
  121. package/lib/types/tools/page.js +59 -0
  122. package/lib/types/tools/project.d.ts +11 -0
  123. package/lib/types/tools/project.js +67 -0
  124. package/lib/types/tools/quiz.d.ts +11 -0
  125. package/lib/types/tools/quiz.js +74 -0
  126. package/lib/types/tools/read.d.ts +11 -0
  127. package/lib/types/tools/read.js +127 -0
  128. package/lib/types/tools/resource.d.ts +11 -0
  129. package/lib/types/tools/resource.js +147 -0
  130. package/lib/types/tools/roadmap.d.ts +11 -0
  131. package/lib/types/tools/roadmap.js +102 -0
  132. package/lib/types/types.d.ts +752 -0
  133. package/lib/types/types.js +159 -0
  134. package/locale/en.json +6 -0
  135. package/locale/zh.json +6 -0
  136. package/package.json +135 -3
@@ -0,0 +1,17 @@
1
+ /**
2
+ * 知识卡片: the project's cards — what the learner marked, asked about and
3
+ * missed — filtered by where they came from, searchable with the same
4
+ * matcher the coach uses, each card showing its sentence, its sources, how
5
+ * weak it is and where its review stands.
6
+ *
7
+ * @module @khorsheed/dsh-library/client/views/cards
8
+ */
9
+ import { type ReactNode } from 'react';
10
+ import type { LibraryCardFilter } from '../store.ts';
11
+ import { type ViewProps } from '../ui.tsx';
12
+ /** The project's knowledge cards. */
13
+ export declare function CardsView({ p, projectId, filter }: ViewProps & {
14
+ projectId: string;
15
+ filter: LibraryCardFilter;
16
+ }): ReactNode;
17
+ //# sourceMappingURL=cards.d.ts.map
@@ -0,0 +1,53 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * 知识卡片: the project's cards — what the learner marked, asked about and
4
+ * missed — filtered by where they came from, searchable with the same
5
+ * matcher the coach uses, each card showing its sentence, its sources, how
6
+ * weak it is and where its review stands.
7
+ *
8
+ * @module @khorsheed/dsh-library/client/views/cards
9
+ */
10
+ import { useState } from 'react';
11
+ import { ChipRow, PanelHeader } from '@khorsheed/dsh-client-ui-panel-kit/src/client/index.ts';
12
+ import { IconSearchOutlineMedium, IconSparkleMedium } from "../icons.js";
13
+ import { BoxDots, daysUntil, Highlight, outcome, shortGloss, StateLine, useLoad, value, WeaknessBars } from "../ui.js";
14
+ import { MathText } from "../math.js";
15
+ import css from '../LibraryPane.module.css';
16
+ const FILTERS = ['all', 'photo', 'chat', 'quiz', 'due'];
17
+ /** The project's knowledge cards. */
18
+ export function CardsView({ p, projectId, filter }) {
19
+ const { t, actions, useStore } = p;
20
+ const rev = useStore(s => s.rev);
21
+ const [searching, setSearching] = useState(false);
22
+ const [query, setQuery] = useState('');
23
+ const [open, setOpen] = useState();
24
+ const project = useLoad(() => outcome(p.getProject(projectId)), [projectId, rev]);
25
+ const cards = useLoad(() => value(p.listCards({
26
+ projectId,
27
+ ...(filter === 'due' ? { due: true, sort: 'due' } : {}),
28
+ ...(filter === 'photo' || filter === 'chat' || filter === 'quiz' ? { source: filter } : {}),
29
+ ...(filter === 'all' ? { sort: 'recent' } : {}),
30
+ ...(query.trim() === '' ? {} : { query }),
31
+ })), [projectId, filter, query, rev]);
32
+ const resources = new Map((project.data?.resources ?? []).map(resource => [resource.id, resource.title]));
33
+ return (_jsxs(_Fragment, { children: [_jsx(PanelHeader, { title: t('cards.title'), meta: cards.data === undefined ? undefined : String(cards.data.total), onBack: () => { actions.pop(); }, backLabel: t('action.back'), actions: [
34
+ { id: 'search', label: t('cards.search'), icon: _jsx(IconSearchOutlineMedium, {}), pressed: searching, onSelect: () => { setSearching(!searching); setQuery(''); } },
35
+ { id: 'review', label: t('review.start'), icon: _jsx(IconSparkleMedium, {}), onSelect: () => { actions.push({ view: 'review', projectId }); } },
36
+ ], moreLabel: t('action.more') }), searching
37
+ ? (_jsx("div", { className: css.searchRow, children: _jsx("input", { className: css.input, autoFocus: true, value: query, placeholder: t('cards.searchPlaceholder'), onChange: event => { setQuery(event.target.value); } }) }))
38
+ : (_jsx(ChipRow, { label: t('cards.filters'), chips: FILTERS.map(id => ({ id, label: t(`cards.filter.${id}`) })), value: filter, onSelect: next => { actions.replace({ view: 'cards', projectId, filter: next }); } })), _jsxs("div", { className: css.body, children: [_jsx(StateLine, { loaded: cards, t: t }), cards.data !== undefined && cards.data.cards.length === 0 && _jsx("div", { className: css.muted, children: t('cards.empty') }), _jsx("div", { className: css.cardList, children: (cards.data?.cards ?? []).map(card => (_jsx(CardRow, { p: p, projectId: projectId, card: card, bookTitle: card.resourceId === undefined ? undefined : resources.get(card.resourceId), open: open === card.id, onToggle: () => { setOpen(open === card.id ? undefined : card.id); } }, card.id))) })] })] }));
39
+ }
40
+ /** One card; tapping opens its sentences, sources and actions. */
41
+ function CardRow({ p, projectId, card, bookTitle, open, onToggle }) {
42
+ const { t, actions } = p;
43
+ const [notice, setNotice] = useState();
44
+ const due = daysUntil(card.mastery.dueAt);
45
+ const [busy, setBusy] = useState(false);
46
+ const setStatus = (status) => {
47
+ if (busy)
48
+ return;
49
+ setBusy(true);
50
+ void outcome(p.editCard(projectId, card.id, { status })).then(() => { actions.refresh(); }, (failure) => { setNotice(String(failure)); setBusy(false); });
51
+ };
52
+ return (_jsxs("article", { className: css.cardRow, "data-open": open ? 'true' : 'false', children: [_jsxs("button", { type: "button", className: css.cardMain, onClick: onToggle, "aria-expanded": open, children: [_jsxs("span", { className: css.cardTop, children: [_jsx("span", { className: card.kind === 'word' ? css.term : css.concept, children: _jsx(MathText, { text: card.term }) }), _jsx("span", { className: css.cardGloss, children: _jsx(MathText, { text: shortGloss(card.gloss, 24) }) }), _jsx(WeaknessBars, { weakness: card.weakness, label: t('cards.weakness', { level: String(card.weakness) }) })] }), card.contexts[0] !== undefined && (_jsx("span", { className: css.sentence, children: _jsx(Highlight, { sentence: card.contexts[0].sentence, term: card.term }) })), _jsxs("span", { className: css.chips, children: [_jsx("span", { className: css.chip, children: bookTitle ?? t('cards.projectLevel') }), [...new Set(card.sources.map(source => source.type))].map(type => (_jsx("span", { className: css.chip, "data-tone": type === 'photo' ? 'photo' : type === 'chat' || type === 'quiz' ? 'weak' : undefined, children: t(`source.${type}`) }, type))), _jsx("span", { className: css.chipPlain, children: due === 0 ? t('cards.dueNow') : t('cards.dueIn', { days: String(due) }) })] })] }), open && (_jsxs("div", { className: css.cardMore, children: [_jsx("div", { children: _jsx(MathText, { text: card.gloss }) }), card.userGloss !== undefined && _jsx("div", { children: t('cards.userGloss', { gloss: card.userGloss }) }), card.aliases.length > 0 && _jsx("div", { className: css.muted, children: t('cards.aliases', { aliases: card.aliases.join('、') }) }), card.contexts.slice(1).map(context => (_jsx("div", { className: css.sentence, children: _jsx(Highlight, { sentence: context.sentence, term: card.term }) }, context.sentence))), _jsx("ul", { className: css.sources, children: card.sources.slice(-5).map(source => (_jsxs("li", { children: [t(`source.${source.type}`), source.note === undefined ? '' : `:${source.note}`] }, `${source.type}-${source.at}`))) }), _jsx("div", { className: css.row, children: _jsx(BoxDots, { box: card.mastery.box, label: t('cards.box', { box: String(card.mastery.box) }) }) }), _jsxs("div", { className: css.actions, children: [_jsx("button", { type: "button", className: css.secondary, onClick: () => { setNotice(p.primeConversation(t('prime.card', { term: card.term })) ? t('notice.primed') : t('notice.noConversation')); }, children: t('action.askCoach') }), _jsx("button", { type: "button", className: css.ghost, disabled: busy, onClick: () => { setStatus('mastered'); }, children: t('cards.mastered') }), _jsx("button", { type: "button", className: css.danger, disabled: busy, onClick: () => { setStatus('deleted'); }, children: t('cards.delete') })] }), notice !== undefined && _jsx("div", { className: css.notice, children: notice })] }))] }));
53
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The sidebar's top screens: 今日 (what to do now — continue learning,
3
+ * reviews due, the projects, what the coach just noted), the new-project
4
+ * form, and one project's page: its goal, its textbooks in study order (each
5
+ * with its own path — the project carries none), the drafts waiting for
6
+ * 采用, its links. Deleting a project sits in the page's menu and asks
7
+ * through a dialog that spells out what goes.
8
+ *
9
+ * @module @khorsheed/dsh-library/client/views/home
10
+ */
11
+ import { type ReactNode } from 'react';
12
+ import { type ViewProps } from '../ui.tsx';
13
+ /**
14
+ * 今日: the projects first — each card carries its goal, its progress and
15
+ * where to continue (the newest project's 继续 is the screen's one solid
16
+ * button) — then the reviews due as one quiet row, then what the coach just
17
+ * noted (math typeset, one line each).
18
+ */
19
+ export declare function TodayView({ p }: ViewProps): ReactNode;
20
+ /** 新建项目. */
21
+ export declare function NewProjectView({ p }: ViewProps): ReactNode;
22
+ /** One project: its goal band, continue learning, drafts waiting, its textbooks in study order, its links. */
23
+ export declare function ProjectView({ p, projectId }: ViewProps & {
24
+ projectId: string;
25
+ }): ReactNode;
26
+ //# sourceMappingURL=home.d.ts.map
@@ -0,0 +1,263 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * The sidebar's top screens: 今日 (what to do now — continue learning,
4
+ * reviews due, the projects, what the coach just noted), the new-project
5
+ * form, and one project's page: its goal, its textbooks in study order (each
6
+ * with its own path — the project carries none), the drafts waiting for
7
+ * 采用, its links. Deleting a project sits in the page's menu and asks
8
+ * through a dialog that spells out what goes.
9
+ *
10
+ * @module @khorsheed/dsh-library/client/views/home
11
+ */
12
+ import { useState } from 'react';
13
+ import { ChipRow, PanelHeader } from '@khorsheed/dsh-client-ui-panel-kit/src/client/index.ts';
14
+ import { isThickResource, LIBRARY_COLORS, LIBRARY_ICONS } from "../../types.js";
15
+ import { colorOf, Cover, coverOf, GlyphStrokes, iconOf, ProjectGlyph, Ring } from "../look.js";
16
+ import { QuizRows } from "./book.js";
17
+ import { IconLinkOutlineMedium, IconPlayOutlineMedium, IconPlusOutlineMedium, IconRefreshOutlineMedium, IconSparkleMedium, IconTrashOutlineMedium } from "../icons.js";
18
+ import { IconArticleOutlineMedium, IconCardsOutlineMedium, IconCourseOutlineMedium, IconHardwareOutlineMedium, IconPenOutlineMedium, IconRepoOutlineMedium, IconToolOutlineMedium } from "../icons-local.js";
19
+ import { ConfirmDialog, OriginTag, outcome, Segments, StateLine, useLoad, value, when } from "../ui.js";
20
+ import { splitTitle, stationTitle } from "../titles.js";
21
+ import { MathText } from "../math.js";
22
+ import css from '../LibraryPane.module.css';
23
+ /** A project's textbooks (thick rows), in study order. */
24
+ function textbooksOf(project) {
25
+ return project.resources.filter(row => row.seq !== undefined || row.roadmap !== undefined || row.hasDraft || row.kind === 'book' || row.kind === 'course');
26
+ }
27
+ /** Where to continue in a project: the first textbook (study order) with a current station. */
28
+ function continueOf(project) {
29
+ for (const textbook of textbooksOf(project)) {
30
+ if (textbook.current !== undefined)
31
+ return { textbook, station: textbook.current };
32
+ }
33
+ return undefined;
34
+ }
35
+ /** 「继续学习」: one card that jumps straight into the current station. */
36
+ function ContinueCard({ p, projectId, label, target }) {
37
+ const { t, actions } = p;
38
+ return (_jsxs("section", { className: `${css.continue} ${css.projectNext}`, children: [_jsxs("span", { className: css.projectNextText, children: [_jsx("span", { className: css.continueLabel, children: label }), _jsx("span", { className: css.continueTitle, children: stationTitle(target.station.title) }), _jsx("span", { className: css.muted, children: t('continue.where', { seq: String(target.textbook.seq ?? ''), book: splitTitle(target.textbook.title).main }) })] }), _jsx("button", { type: "button", className: css.primary, onClick: () => { actions.push({ view: 'node', projectId, resourceId: target.textbook.id, nodeId: target.station.id, tab: 'guide' }); }, children: t('continue.go') })] }));
39
+ }
40
+ /**
41
+ * 今日: the projects first — each card carries its goal, its progress and
42
+ * where to continue (the newest project's 继续 is the screen's one solid
43
+ * button) — then the reviews due as one quiet row, then what the coach just
44
+ * noted (math typeset, one line each).
45
+ */
46
+ export function TodayView({ p }) {
47
+ const { t, actions, useStore } = p;
48
+ const rev = useStore(s => s.rev);
49
+ const projects = useLoad(() => value(p.listProjects()), [rev]);
50
+ const due = useLoad(() => value(p.dueCards()), [rev]);
51
+ const records = useLoad(() => value(p.recentRecords()), [rev]);
52
+ const changes = useLoad(() => value(p.recentChanges()), [rev]);
53
+ // 「最近的变化」: knowledge points that moved, and (language projects) the words the coach noted — newest first.
54
+ const feed = [
55
+ ...(changes.data?.changes ?? []).map(change => ({ kind: 'point', at: change.at, change })),
56
+ ...(records.data?.records ?? []).filter(record => record.source === 'chat').map(record => ({ kind: 'card', at: record.at, record })),
57
+ ].sort((a, b) => b.at.localeCompare(a.at)).slice(0, 5);
58
+ const list = projects.data?.projects ?? [];
59
+ const dueCards = due.data?.cards ?? [];
60
+ // 今日复习: every project's open misses (one retake paper across its stations), then the due cards.
61
+ const retakeRows = list.filter(project => (project.openMisses ?? 0) > 0);
62
+ const [retaking, setRetaking] = useState();
63
+ const [retakeError, setRetakeError] = useState();
64
+ const startRetake = (projectId) => {
65
+ setRetaking(projectId);
66
+ setRetakeError(undefined);
67
+ void outcome(p.createRetake(projectId)).then(created => {
68
+ actions.push({ view: 'quiz', projectId, quizId: created.assessment.id });
69
+ }, (failure) => { setRetakeError(failure instanceof Error ? failure.message : String(failure)); }).finally(() => { setRetaking(undefined); });
70
+ };
71
+ // Projects come most-recent first: the first one with a current station owns the solid 继续.
72
+ const leadId = list.find(project => continueOf(project) !== undefined)?.id;
73
+ return (_jsxs(_Fragment, { children: [_jsx(PanelHeader, { title: t('today.title'), actions: [
74
+ { id: 'new', label: t('project.new'), icon: _jsx(IconPlusOutlineMedium, {}), onSelect: () => { actions.push({ view: 'newProject' }); } },
75
+ { id: 'refresh', label: t('action.refresh'), icon: _jsx(IconRefreshOutlineMedium, {}), onSelect: () => { actions.refresh(); } },
76
+ ], moreLabel: t('action.more') }), _jsxs("div", { className: css.body, children: [_jsx(StateLine, { loaded: projects, t: t }), projects.data !== undefined && list.length === 0 && (_jsxs("div", { className: css.empty, children: [_jsx("div", { className: css.emptyTitle, children: t('today.emptyTitle') }), _jsx("div", { className: css.muted, children: t('today.emptyBody') }), _jsx("button", { type: "button", className: css.primary, onClick: () => { actions.push({ view: 'newProject' }); }, children: t('project.new') })] })), list.length > 0 && (_jsxs("section", { className: css.section, children: [_jsxs("div", { className: css.sectionHead, children: [_jsx("h3", { className: css.sectionTitle, children: t('today.projects') }), _jsxs("button", { type: "button", className: css.textButton, onClick: () => { actions.push({ view: 'newProject' }); }, children: ["\uFF0B ", t('project.new')] })] }), _jsx("div", { className: css.projectList, children: list.map(project => _jsx(ProjectCard, { p: p, project: project, lead: project.id === leadId }, project.id)) })] })), (retakeRows.length > 0 || dueCards.length > 0) && (_jsxs("section", { className: css.section, children: [_jsx("h3", { className: css.sectionTitle, children: t('today.dueTitle') }), retakeRows.map(project => (_jsxs("div", { className: css.reviewRow, children: [_jsx("span", { className: css.big, children: project.openMisses }), _jsxs("span", { className: css.reviewText, children: [_jsx("span", { children: t('today.retakeLine', { project: project.name }) }), _jsx("span", { className: css.muted, children: t('today.retakeHint') })] }), _jsx("button", { type: "button", className: leadId === undefined && project === retakeRows[0] ? css.primary : css.secondary, disabled: retaking !== undefined, onClick: () => { startRetake(project.id); }, children: t('node.retakeStart') })] }, project.id))), dueCards.length > 0 && (_jsxs("div", { className: css.reviewRow, children: [_jsx("span", { className: css.big, children: dueCards.length }), _jsxs("span", { className: css.reviewText, children: [_jsx("span", { children: t('today.cardsDue') }), _jsx("span", { className: css.muted, children: t('today.dueMinutes', { minutes: String(Math.max(1, Math.round(dueCards.length / 4))) }) })] }), _jsx("button", { type: "button", className: leadId === undefined && retakeRows.length === 0 ? css.primary : css.secondary, onClick: () => { actions.push({ view: 'review' }); }, children: t('today.startReview') })] })), retakeError !== undefined && _jsx("div", { className: css.error, children: retakeError })] })), feed.length > 0 && (_jsxs("section", { className: css.section, children: [_jsx("h3", { className: css.sectionTitle, children: t('today.changes') }), _jsx("div", { className: css.recordList, children: feed.map(entry => entry.kind === 'point' ? (_jsxs("button", { type: "button", className: css.record, onClick: () => {
77
+ const { change } = entry;
78
+ if (change.nodeId !== undefined && change.resourceId !== undefined)
79
+ actions.push({ view: 'node', projectId: change.projectId, resourceId: change.resourceId, nodeId: change.nodeId, tab: 'quiz' });
80
+ else
81
+ actions.push({ view: 'project', projectId: change.projectId });
82
+ }, children: [_jsx("span", { className: css.changeDot, "data-state": entry.change.to, "aria-hidden": "true" }), _jsxs("span", { className: css.recordText, children: [_jsx("span", { className: css.recordTitle, children: t(entry.change.to === 'mastered' ? 'today.changeMastered' : entry.change.to === 'weak' ? 'today.changeWeak' : entry.change.to === 'practicing' ? 'today.changePracticing' : 'today.changeUntested', { title: entry.change.title ?? '' }) }), _jsx("span", { className: css.muted, children: [
83
+ ...((entry.change.count ?? 0) > 0 ? [t('today.changeOpen', { count: String(entry.change.count) })] : []),
84
+ ...(entry.change.detail === undefined ? [] : [entry.change.detail]),
85
+ entry.change.projectName,
86
+ when(entry.change.at, t),
87
+ ].join(' · ') })] })] }, `${entry.change.projectId}-${entry.change.at}-${entry.change.id ?? ''}`)) : (_jsxs("button", { type: "button", className: css.record, onClick: () => { actions.push({ view: 'cards', projectId: entry.record.projectId, filter: 'chat' }); }, children: [_jsx("span", { className: css.recordIcon, "aria-hidden": "true", children: _jsx(IconSparkleMedium, { size: 14 }) }), _jsxs("span", { className: css.recordText, children: [_jsx("span", { className: css.recordTitle, children: _jsx(MathText, { text: entry.record.title ?? '' }) }), _jsxs("span", { className: css.muted, children: [entry.record.projectName, " \u00B7 ", t('source.chat'), " \u00B7 ", when(entry.record.at, t)] })] })] }, `${entry.record.projectId}-${entry.record.at}`))) })] }))] })] }));
88
+ }
89
+ const TEMPLATES = ['language', 'subject', 'custom'];
90
+ /** 新建项目. */
91
+ export function NewProjectView({ p }) {
92
+ const { t, actions } = p;
93
+ const [name, setName] = useState('');
94
+ const [template, setTemplate] = useState('subject');
95
+ const [goal, setGoal] = useState('');
96
+ const [error, setError] = useState();
97
+ const [busy, setBusy] = useState(false);
98
+ const submit = async () => {
99
+ setBusy(true);
100
+ setError(undefined);
101
+ try {
102
+ const created = await outcome(p.createProject({ name, template, ...(goal.trim() === '' ? {} : { goal }) }));
103
+ actions.replace({ view: 'project', projectId: created.project.id });
104
+ }
105
+ catch (failure) {
106
+ setError(failure instanceof Error ? failure.message : String(failure));
107
+ }
108
+ finally {
109
+ setBusy(false);
110
+ }
111
+ };
112
+ return (_jsxs(_Fragment, { children: [_jsx(PanelHeader, { title: t('project.new'), onBack: () => { actions.pop(); }, backLabel: t('action.back'), moreLabel: t('action.more') }), _jsxs("form", { className: css.body, onSubmit: event => { event.preventDefault(); void submit(); }, children: [_jsxs("label", { className: css.field, children: [_jsx("span", { className: css.fieldLabel, children: t('project.name') }), _jsx("input", { className: css.input, value: name, placeholder: t('project.namePlaceholder'), onChange: event => { setName(event.target.value); } })] }), _jsxs("div", { className: css.field, children: [_jsx("span", { className: css.fieldLabel, children: t('project.template') }), _jsx(ChipRow, { label: t('project.template'), chips: TEMPLATES.map(id => ({ id, label: t(`template.${id}`) })), value: template, onSelect: setTemplate }), _jsx("span", { className: css.muted, children: t(`template.${template}.hint`) })] }), _jsxs("label", { className: css.field, children: [_jsx("span", { className: css.fieldLabel, children: t('project.goal') }), _jsx("input", { className: css.input, value: goal, placeholder: t('project.goalPlaceholder'), onChange: event => { setGoal(event.target.value); } })] }), error !== undefined && _jsx("div", { className: css.error, children: error }), _jsx("div", { className: css.actions, children: _jsx("button", { type: "submit", className: css.primary, disabled: busy || name.trim() === '', children: t('project.create') }) })] })] }));
113
+ }
114
+ /** Overall progress of a project: done nodes over all nodes of its adopted paths. */
115
+ /** A project's stations, passed and in all, across its textbooks' paths. */
116
+ function stationsOf(project) {
117
+ return {
118
+ done: project.resources.reduce((sum, resource) => sum + (resource.roadmap?.done ?? 0), 0),
119
+ total: project.resources.reduce((sum, resource) => sum + (resource.roadmap?.total ?? 0), 0),
120
+ };
121
+ }
122
+ function progressOf(project) {
123
+ const total = project.resources.reduce((sum, resource) => sum + (resource.roadmap?.total ?? 0), 0);
124
+ const done = project.resources.reduce((sum, resource) => sum + (resource.roadmap?.done ?? 0), 0);
125
+ return total === 0 ? 0 : Math.round((done / total) * 100);
126
+ }
127
+ /** A project card on 今日: glyph, name, goal, progress — and where to continue, with its own button. */
128
+ function ProjectCard({ p, project, lead }) {
129
+ const { t, actions } = p;
130
+ const target = continueOf(project);
131
+ const textbooks = textbooksOf(project);
132
+ const drafts = textbooks.filter(row => row.hasDraft).length;
133
+ const open = () => { actions.push({ view: 'project', projectId: project.id }); };
134
+ return (_jsxs("article", { className: css.projectCard, "data-color": colorOf(project), children: [_jsxs("button", { type: "button", className: css.projectOpen, onClick: open, children: [_jsxs("span", { className: css.projectTop, children: [_jsx(ProjectGlyph, { icon: iconOf(project) }), _jsxs("span", { className: css.projectText, children: [_jsx("span", { className: css.projectName, children: project.name }), _jsxs("span", { className: css.muted, children: [t(`template.${project.template}`), " \u00B7 ", when(project.lastActiveAt, t)] })] }), _jsx(Ring, { percent: progressOf(project) })] }), project.goal !== undefined && _jsx("span", { className: css.projectGoal, children: project.goal }), _jsxs("span", { className: css.muted, children: [t('project.cardMeta', { books: String(textbooks.length), cards: String(project.cardCount), due: String(project.dueCount) }), drafts > 0 ? ` · ${t('project.draftsWaiting', { count: String(drafts) })}` : ''] })] }), target !== undefined && (_jsxs("div", { className: css.projectNext, children: [_jsxs("span", { className: css.projectNextText, children: [_jsx("span", { className: css.continueLabel, children: t('continue.label') }), _jsx("span", { className: css.continueTitle, children: target.station.title }), _jsx("span", { className: css.muted, children: t('continue.where', { seq: String(target.textbook.seq ?? ''), book: target.textbook.title }) })] }), _jsx("button", { type: "button", className: lead ? css.primary : css.secondary, onClick: () => { actions.push({ view: 'node', projectId: project.id, resourceId: target.textbook.id, nodeId: target.station.id, tab: 'guide' }); }, children: t('continue.go') })] }))] }));
135
+ }
136
+ /** Editing a project in place: name, goal, template, color, icon — saved together (deleting lives in the page menu). */
137
+ function ProjectEditor({ p, project, onDone }) {
138
+ const { t, actions } = p;
139
+ const [name, setName] = useState(project.name);
140
+ const [goal, setGoal] = useState(project.goal ?? '');
141
+ const [template, setTemplate] = useState(project.template);
142
+ const [color, setColor] = useState(colorOf(project));
143
+ const [icon, setIcon] = useState(iconOf(project));
144
+ const [busy, setBusy] = useState(false);
145
+ const [error, setError] = useState();
146
+ const save = async () => {
147
+ setBusy(true);
148
+ setError(undefined);
149
+ try {
150
+ await outcome(p.updateProject(project.id, { name: name.trim(), goal: goal.trim(), template, color, icon }));
151
+ actions.refresh();
152
+ onDone();
153
+ }
154
+ catch (failure) {
155
+ setError(failure instanceof Error ? failure.message : String(failure));
156
+ }
157
+ finally {
158
+ setBusy(false);
159
+ }
160
+ };
161
+ return (_jsxs("form", { className: css.editor, "data-color": color, onSubmit: event => { event.preventDefault(); void save(); }, children: [_jsxs("span", { className: css.projectTop, children: [_jsx(ProjectGlyph, { icon: icon }), _jsx("span", { className: css.projectText, children: _jsx("span", { className: css.projectName, children: t('project.edit') }) })] }), _jsxs("label", { className: css.field, children: [_jsx("span", { className: css.fieldLabel, children: t('project.name') }), _jsx("input", { className: css.input, value: name, onChange: event => { setName(event.target.value); } })] }), _jsxs("label", { className: css.field, children: [_jsx("span", { className: css.fieldLabel, children: t('project.goal') }), _jsx("textarea", { className: css.input, rows: 2, value: goal, onChange: event => { setGoal(event.target.value); } })] }), _jsxs("div", { className: css.field, children: [_jsx("span", { className: css.fieldLabel, children: t('project.template') }), _jsx(ChipRow, { label: t('project.template'), chips: TEMPLATES.map(id => ({ id, label: t(`template.${id}`) })), value: template, onSelect: setTemplate }), template !== project.template && _jsx("span", { className: css.muted, children: t('project.templateChange') })] }), _jsxs("div", { className: css.field, children: [_jsx("span", { className: css.fieldLabel, children: t('project.style') }), _jsx("span", { className: css.swatches, children: LIBRARY_COLORS.map(entry => (_jsx("button", { type: "button", className: css.swatch, "data-color": entry, "aria-label": entry, "aria-pressed": color === entry, onClick: () => { setColor(entry); } }, entry))) }), _jsx("span", { className: css.swatches, children: LIBRARY_ICONS.map(entry => (_jsx("button", { type: "button", className: css.iconPick, "aria-label": entry, "aria-pressed": icon === entry, onClick: () => { setIcon(entry); }, children: _jsx(GlyphStrokes, { icon: entry }) }, entry))) })] }), error !== undefined && _jsx("div", { className: css.error, children: error }), _jsxs("div", { className: css.actions, children: [_jsx("button", { type: "button", className: css.secondary, onClick: onDone, children: t('action.cancel') }), _jsx("button", { type: "submit", className: css.primary, disabled: busy || name.trim() === '', children: t('action.save') })] })] }));
162
+ }
163
+ /** One project: its goal band, continue learning, drafts waiting, its textbooks in study order, its links. */
164
+ export function ProjectView({ p, projectId }) {
165
+ const { t, actions, useStore } = p;
166
+ const rev = useStore(s => s.rev);
167
+ const loaded = useLoad(() => outcome(p.getProject(projectId)), [projectId, rev]);
168
+ // Free quizzes hang on no resource and no station — this section is their only entry.
169
+ const assessments = useLoad(() => value(p.listAssessments(projectId, undefined, undefined, 'quiz')), [projectId, rev]);
170
+ // The 错题 entry's count and target ride the shared collection (mistakes.ts) — the same rows the station's 章节考 tab lists.
171
+ const attempts = useLoad(() => value(p.listAttempts(projectId)), [projectId, rev]);
172
+ const openMisses = loaded.data?.project.openMisses ?? 0;
173
+ const [notice, setNotice] = useState();
174
+ const [editing, setEditing] = useState(false);
175
+ const [goalOpen, setGoalOpen] = useState(false);
176
+ const [ordering, setOrdering] = useState(false);
177
+ const [deleting, setDeleting] = useState(false);
178
+ const [busy, setBusy] = useState(false);
179
+ const [error, setError] = useState();
180
+ const project = loaded.data?.project;
181
+ const resources = loaded.data?.resources ?? [];
182
+ const thick = resources.filter(isThickResource);
183
+ const thin = resources.filter(resource => !isThickResource(resource));
184
+ const rows = thick.map(resource => ({ resource, row: project?.resources.find(candidate => candidate.id === resource.id) }));
185
+ const drafts = rows.filter(entry => entry.row?.hasDraft === true);
186
+ const resume = project === undefined ? undefined : continueOf(project);
187
+ const freeQuizzes = (assessments.data?.assessments ?? []).filter(quiz => quiz.scope.resourceId === undefined && quiz.scope.nodeId === undefined).sort((a, b) => b.createdAt.localeCompare(a.createdAt));
188
+ // The 错题 entry jumps to wherever the newest miss lives: its station's 章节考 tab, else its textbook page.
189
+ // The 错题 stat: every station's open misses, retaken in one paper (订正不计分).
190
+ const openMissed = () => {
191
+ if (busy)
192
+ return;
193
+ setBusy(true);
194
+ void outcome(p.createRetake(projectId)).then(created => {
195
+ actions.push({ view: 'quiz', projectId, quizId: created.assessment.id });
196
+ }, (failure) => { setNotice(failure instanceof Error ? failure.message : String(failure)); }).finally(() => { setBusy(false); });
197
+ };
198
+ const prime = (key) => {
199
+ if (project === undefined)
200
+ return;
201
+ void p.bindTarget({ projectId });
202
+ setNotice(p.primeConversation(t(key, { project: project.name })) ? t('notice.primed') : t('notice.noConversation'));
203
+ };
204
+ const move = async (resourceId, seq) => {
205
+ setBusy(true);
206
+ try {
207
+ await outcome(p.moveResource(projectId, resourceId, seq));
208
+ actions.refresh();
209
+ }
210
+ catch (failure) {
211
+ setNotice(failure instanceof Error ? failure.message : String(failure));
212
+ }
213
+ finally {
214
+ setBusy(false);
215
+ }
216
+ };
217
+ const remove = async () => {
218
+ setBusy(true);
219
+ setError(undefined);
220
+ try {
221
+ await outcome(p.deleteProject(projectId));
222
+ setDeleting(false);
223
+ actions.refresh();
224
+ actions.pop();
225
+ }
226
+ catch (failure) {
227
+ setError(failure instanceof Error ? failure.message : String(failure));
228
+ }
229
+ finally {
230
+ setBusy(false);
231
+ }
232
+ };
233
+ return (_jsxs(_Fragment, { children: [_jsx(PanelHeader, { title: project?.name ?? '', onBack: () => { actions.pop(); }, backLabel: t('action.back'), actions: ordering ? [] : [
234
+ { id: 'add', label: t('book.add'), icon: _jsx(IconPlusOutlineMedium, {}), onSelect: () => { prime('prime.addBook'); } },
235
+ { id: 'addLink', label: t('resource.add'), icon: _jsx(IconLinkOutlineMedium, {}), onSelect: () => { prime('prime.addResource'); } },
236
+ { id: 'cards', label: t('cards.title'), icon: _jsx(IconCardsOutlineMedium, {}), onSelect: () => { actions.push({ view: 'cards', projectId, filter: 'all' }); } },
237
+ { id: 'edit', label: t('project.edit'), icon: _jsx(IconPenOutlineMedium, {}), menuOnly: true, onSelect: () => { setEditing(true); } },
238
+ ...(thick.length > 1 ? [{ id: 'order', label: t('project.reorder'), icon: _jsx("span", { "aria-hidden": "true", children: "\u21C5" }), menuOnly: true, onSelect: () => { setOrdering(true); } }] : []),
239
+ { id: 'bind', label: t('action.bindProject'), icon: _jsx(IconSparkleMedium, {}), menuOnly: true, onSelect: () => { void p.bindTarget({ projectId }).then(() => { setNotice(t('notice.bound')); }); } },
240
+ { id: 'delete', label: t('project.delete'), icon: _jsx(IconTrashOutlineMedium, {}), menuOnly: true, onSelect: () => { setError(undefined); setDeleting(true); } },
241
+ ], moreLabel: t('action.more') }), project !== undefined && editing && (_jsx(ProjectEditor, { p: p, project: project, onDone: () => { setEditing(false); } })), project !== undefined && !editing && !ordering && (_jsxs("section", { className: css.band, "data-color": colorOf(project), children: [_jsxs("span", { className: css.projectTop, children: [_jsx(ProjectGlyph, { icon: iconOf(project) }), _jsxs("span", { className: css.projectText, children: [project.goal !== undefined && _jsx("span", { className: css.bandGoal, "data-open": goalOpen ? 'true' : undefined, children: project.goal }), _jsxs("span", { className: css.bandMeta, children: [_jsxs("span", { children: [t(`template.${project.template}`), " \u00B7 ", when(project.lastActiveAt, t)] }), project.goal !== undefined && project.goal.length > 60 && _jsx("button", { type: "button", className: css.textButton, "aria-expanded": goalOpen, onClick: () => { setGoalOpen(open => !open); }, children: t(goalOpen ? 'project.goalLess' : 'project.goalMore') })] })] }), _jsx(Ring, { percent: progressOf(project), label: t('project.ringLabel'), title: t('project.ringTitle', { done: String(stationsOf(project).done), total: String(stationsOf(project).total) }) })] }), _jsxs("span", { className: css.statTiles, children: [_jsxs("button", { type: "button", className: css.statTile, onClick: () => { actions.push({ view: 'cards', projectId, filter: 'all' }); }, children: [_jsx("b", { children: project.cardCount }), _jsx("span", { children: t('cards.title') })] }), _jsxs("button", { type: "button", className: css.statTile, disabled: project.dueCount === 0, onClick: () => { actions.push({ view: 'review', projectId }); }, children: [_jsx("b", { children: project.dueCount }), _jsx("span", { children: t('review.due') })] }), _jsxs("button", { type: "button", className: css.statTile, "data-tone": openMisses > 0 ? 'bad' : undefined, disabled: openMisses === 0 || busy, onClick: openMissed, children: [_jsx("b", { children: openMisses }), _jsx("span", { children: t('mistakes.statOpen') })] })] })] })), _jsxs("div", { className: css.body, children: [_jsx(StateLine, { loaded: loaded, t: t }), notice !== undefined && _jsx("div", { className: css.notice, children: notice }), ordering && (_jsxs(_Fragment, { children: [_jsx("div", { className: css.notice, children: t('project.reorderHint') }), _jsx("ol", { className: css.textbooks, children: rows.map(({ resource }, index) => (_jsxs("li", { className: css.textbook, children: [_jsx("span", { className: css.seq, children: index + 1 }), _jsx("span", { className: css.textbookMain, children: _jsx("span", { className: css.textbookTitle, children: resource.title }) }), _jsxs("span", { className: css.order, children: [_jsx("button", { type: "button", "aria-label": t('project.moveUp', { book: resource.title }), disabled: busy || index === 0, onClick: () => { void move(resource.id, index); }, children: "\u25B2" }), _jsx("button", { type: "button", "aria-label": t('project.moveDown', { book: resource.title }), disabled: busy || index === rows.length - 1, onClick: () => { void move(resource.id, index + 2); }, children: "\u25BC" })] })] }, resource.id))) }), _jsx("div", { className: css.actions, children: _jsx("button", { type: "button", className: css.primary, onClick: () => { setOrdering(false); }, children: t('action.done') }) })] })), project !== undefined && !ordering && (_jsxs(_Fragment, { children: [resume !== undefined && _jsx(ContinueCard, { p: p, projectId: projectId, label: t('continue.label'), target: resume }), drafts.length > 0 && (_jsxs("div", { className: css.banner, children: [_jsx("span", { children: t('project.draftsBanner', { count: String(drafts.length) }) }), _jsx("button", { type: "button", className: css.ghost, onClick: () => { actions.push({ view: 'resource', projectId, resourceId: drafts[0].resource.id }); }, children: t('project.draftsOpen', { seq: String(drafts[0].resource.seq ?? 1) }) })] })), loaded.data !== undefined && thick.length === 0 && (_jsxs("div", { className: css.empty, children: [_jsx("div", { className: css.emptyTitle, children: t('project.noTextbooks') }), _jsx("div", { className: css.muted, children: t('project.noTextbooksBody') }), _jsxs("div", { className: css.starts, children: [_jsxs("button", { type: "button", className: css.start, onClick: () => { prime('prime.addBook'); }, children: [_jsx("b", { children: t('project.startOwn') }), _jsx("span", { children: t('project.startOwnHint') })] }), _jsxs("button", { type: "button", className: css.start, onClick: () => { prime('prime.pickTextbooks'); }, children: [_jsx("b", { children: t('project.startPick') }), _jsx("span", { children: t('project.startPickHint') })] })] })] })), thick.length > 0 && (_jsxs("section", { className: css.section, children: [_jsxs("div", { className: css.sectionHead, children: [_jsx("h3", { className: css.sectionTitle, children: t('project.textbooks', { count: String(thick.length) }) }), thick.length > 1 && _jsx("button", { type: "button", className: css.textButton, onClick: () => { setOrdering(true); }, children: t('project.reorder') })] }), _jsx("div", { className: css.bookCards, "data-color": colorOf(project), children: rows.map(({ resource, row }, index) => {
242
+ const title = splitTitle(resource.title);
243
+ const path = row?.roadmap;
244
+ return (_jsxs("button", { type: "button", className: css.bookCard, "data-pending": path === undefined ? 'true' : undefined, onClick: () => { actions.push({ view: 'resource', projectId, resourceId: resource.id }); }, children: [_jsx(Cover, { book: coverOf(resource), shade: index, size: "card" }), _jsxs("span", { className: css.bookCardText, children: [_jsxs("span", { className: css.bookCardTitle, children: [title.main, _jsx(OriginTag, { origin: resource.origin, digital: resource.kind === 'course', t: t })] }), path !== undefined ? (_jsxs(_Fragment, { children: [_jsxs("span", { className: css.muted, children: [path.done === path.total && path.total > 0 ? t('book.finished') : t('project.bookProgress', { done: String(path.done), total: String(path.total) }), row?.hasDraft === true ? ` · ${t('book.draftWaiting')}` : ''] }), _jsx(Segments, { done: path.done, total: path.total })] })) : (_jsxs(_Fragment, { children: [title.sub !== undefined && _jsx("span", { className: css.muted, children: title.sub }), _jsx("span", { className: css.muted, children: row?.hasDraft === true ? t('project.draftRow', { count: String(row.draftTotal ?? 0) }) : t('project.noPathYet') })] }))] })] }, resource.id));
245
+ }) })] })), freeQuizzes.length > 0 && (_jsxs("section", { className: css.section, children: [_jsx("h3", { className: css.sectionTitle, children: t('project.quizzes', { count: String(freeQuizzes.length) }) }), _jsx(QuizRows, { p: p, projectId: projectId, quizzes: freeQuizzes })] })), thin.length === 0 && loaded.data !== undefined && (_jsxs("div", { className: css.resEmpty, children: [_jsx("span", { children: t('resource.emptyLine') }), _jsxs("button", { type: "button", className: css.textButton, onClick: () => { prime('prime.addResource'); }, children: ["\uFF0B ", t('resource.add')] })] })), thin.length > 0 && (_jsxs("section", { className: css.section, children: [_jsxs("div", { className: css.sectionHead, children: [_jsx("h3", { className: css.sectionTitle, children: t('resource.title', { count: String(thin.length) }) }), _jsxs("button", { type: "button", className: css.textButton, onClick: () => { prime('prime.addResource'); }, children: ["\uFF0B ", t('resource.add')] })] }), thin.length > 0 && (_jsx("div", { className: css.resList, children: thin.map(resource => (_jsxs("div", { className: css.res, children: [_jsx("span", { className: css.resKind, title: t(`resource.kind.${resource.kind}`), children: resourceKindIcon(resource.kind) }), _jsxs("div", { className: css.resMain, children: [_jsx("button", { type: "button", className: css.resTitle, title: resource.url, onClick: () => { if (resource.url !== undefined)
246
+ window.open(resource.url, '_blank', 'noopener,noreferrer'); }, children: resource.title }), resource.note !== undefined && _jsx("span", { className: css.resNote, children: resource.note })] })] }, resource.id))) }))] }))] }))] }), deleting && project !== undefined && (_jsx(ConfirmDialog, { title: t('project.deleteTitle', { project: project.name }), lines: [
247
+ t('project.deleteTextbooks', { count: String(thick.length) }),
248
+ t('project.deleteCards', { count: String(project.cardCount), due: String(project.dueCount) }),
249
+ t('project.deleteAttempts', { count: String(attempts.data?.attempts.length ?? 0) }),
250
+ ...(thin.length > 0 ? [t('project.deleteLinks', { count: String(thin.length) })] : []),
251
+ ], note: t('project.deleteNote'), confirmLabel: t('project.deleteConfirmButton'), cancelLabel: t('action.cancel'), busy: busy, error: error, onConfirm: () => { void remove(); }, onCancel: () => { setDeleting(false); } }))] }));
252
+ }
253
+ /** A resource row's kind tile glyph (article is the default). */
254
+ function resourceKindIcon(kind) {
255
+ switch (kind) {
256
+ case 'video': return _jsx(IconPlayOutlineMedium, { size: 13 });
257
+ case 'repo': return _jsx(IconRepoOutlineMedium, { size: 13 });
258
+ case 'tool': return _jsx(IconToolOutlineMedium, { size: 13 });
259
+ case 'course': return _jsx(IconCourseOutlineMedium, { size: 13 });
260
+ case 'hardware': return _jsx(IconHardwareOutlineMedium, { size: 13 });
261
+ default: return _jsx(IconArticleOutlineMedium, { size: 13 });
262
+ }
263
+ }
@@ -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
+ }