@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,41 @@
1
+ /**
2
+ * Knowledge-card identity: when two inputs are the SAME card, and when they
3
+ * only look alike. The write path decides here, so the coach never has to read
4
+ * every card before writing one:
5
+ *
6
+ * - an exact match on the normalized key (case, width, whitespace, surrounding
7
+ * punctuation, a light English plural fold) — against the term AND every
8
+ * alias — merges automatically;
9
+ * - a near match (one key contains the other, or the two share most of their
10
+ * words / characters) is held back with up to three candidates, and the
11
+ * coach resends with `mergeInto` or `asNew`;
12
+ * - anything else is a new card.
13
+ *
14
+ * Pure functions only — the service owns the lock and the ledger.
15
+ *
16
+ * @module @khorsheed/dsh-library/cards
17
+ */
18
+ import type { LibraryCard } from './types.ts';
19
+ /**
20
+ * A term's comparison key: NFKC (full-width → half-width), lower case, inner
21
+ * whitespace collapsed, surrounding punctuation and brackets dropped, each
22
+ * English word's plural folded.
23
+ */
24
+ export declare function cardKey(term: string): string;
25
+ /** Every key a card answers to: its term and its aliases. */
26
+ export declare function cardKeys(card: Pick<LibraryCard, 'term' | 'aliases'>): string[];
27
+ /** How alike two keys look, 0..1 (1 = one contains the other). */
28
+ export declare function keySimilarity(a: string, b: string): number;
29
+ /** The threshold above which two keys are a near-duplicate worth asking about. */
30
+ export declare const NEAR_DUPLICATE = 0.6;
31
+ /** The live card an input key matches exactly, when one does. */
32
+ export declare function exactMatch(cards: readonly LibraryCard[], keys: readonly string[]): LibraryCard | undefined;
33
+ /** Up to `limit` live cards that look like the input, most alike first. */
34
+ export declare function nearMatches(cards: readonly LibraryCard[], keys: readonly string[], limit?: number): LibraryCard[];
35
+ /**
36
+ * Whether a card answers a free-text query: any key or the gloss / user gloss
37
+ * / a context sentence contains the query's key (the sidebar's ⌕ and the
38
+ * coach's `library_read · cards` share this).
39
+ */
40
+ export declare function cardMatchesQuery(card: LibraryCard, query: string): boolean;
41
+ //# sourceMappingURL=cards.d.ts.map
@@ -0,0 +1,111 @@
1
+ /** Fold one English word's regular plural (instalments → instalment, theories → theory). */
2
+ function foldPlural(word) {
3
+ if (!/^[a-z]+$/.test(word) || word.length <= 3)
4
+ return word;
5
+ if (word.endsWith('ies') && word.length > 4)
6
+ return `${word.slice(0, -3)}y`;
7
+ if (/(?:s|x|z|ch|sh)es$/.test(word))
8
+ return word.slice(0, -2);
9
+ if (word.endsWith('s') && !word.endsWith('ss') && !word.endsWith('us') && !word.endsWith('is'))
10
+ return word.slice(0, -1);
11
+ return word;
12
+ }
13
+ /**
14
+ * A term's comparison key: NFKC (full-width → half-width), lower case, inner
15
+ * whitespace collapsed, surrounding punctuation and brackets dropped, each
16
+ * English word's plural folded.
17
+ */
18
+ export function cardKey(term) {
19
+ const base = term
20
+ .normalize('NFKC')
21
+ .toLowerCase()
22
+ .replace(/\s+/g, ' ')
23
+ .trim()
24
+ .replace(/^[\p{P}\p{S}\s]+|[\p{P}\p{S}\s]+$/gu, '');
25
+ return base.split(' ').map(foldPlural).join(' ');
26
+ }
27
+ /** Every key a card answers to: its term and its aliases. */
28
+ export function cardKeys(card) {
29
+ return [...new Set([card.term, ...card.aliases].map(cardKey).filter(key => key !== ''))];
30
+ }
31
+ /** The comparison units of a key: English words, or single CJK characters. */
32
+ function units(key) {
33
+ const out = new Set();
34
+ for (const token of key.split(/[\s//、,,·・-]+/)) {
35
+ if (token === '')
36
+ continue;
37
+ if (/[\p{Script=Han}]/u.test(token)) {
38
+ for (const char of token)
39
+ if (/[\p{Script=Han}\p{L}\p{N}]/u.test(char))
40
+ out.add(char);
41
+ }
42
+ else {
43
+ out.add(token);
44
+ }
45
+ }
46
+ return out;
47
+ }
48
+ /** How alike two keys look, 0..1 (1 = one contains the other). */
49
+ export function keySimilarity(a, b) {
50
+ if (a === '' || b === '')
51
+ return 0;
52
+ if (a === b)
53
+ return 1;
54
+ const shorter = a.length <= b.length ? a : b;
55
+ const longer = shorter === a ? b : a;
56
+ // Containment only counts when the shorter key is substantial: 「期」 inside
57
+ // 「不应期」 is noise, 「不应期」 inside 「绝对/相对不应期」 is the same idea.
58
+ const minimum = /[\p{Script=Han}]/u.test(shorter) ? 2 : 4;
59
+ if (shorter.length >= minimum && longer.includes(shorter))
60
+ return 1;
61
+ const left = units(a);
62
+ const right = units(b);
63
+ if (left.size === 0 || right.size === 0)
64
+ return 0;
65
+ let shared = 0;
66
+ for (const unit of left)
67
+ if (right.has(unit))
68
+ shared += 1;
69
+ return shared / Math.max(left.size, right.size);
70
+ }
71
+ /** The threshold above which two keys are a near-duplicate worth asking about. */
72
+ export const NEAR_DUPLICATE = 0.6;
73
+ /** The live card an input key matches exactly, when one does. */
74
+ export function exactMatch(cards, keys) {
75
+ const wanted = new Set(keys);
76
+ return cards.find(card => card.status !== 'deleted' && cardKeys(card).some(key => wanted.has(key)));
77
+ }
78
+ /** Up to `limit` live cards that look like the input, most alike first. */
79
+ export function nearMatches(cards, keys, limit = 3) {
80
+ const scored = [];
81
+ for (const card of cards) {
82
+ if (card.status === 'deleted')
83
+ continue;
84
+ let best = 0;
85
+ for (const own of cardKeys(card)) {
86
+ for (const key of keys)
87
+ best = Math.max(best, keySimilarity(own, key));
88
+ }
89
+ if (best >= NEAR_DUPLICATE)
90
+ scored.push({ card, score: best });
91
+ }
92
+ scored.sort((x, y) => y.score - x.score);
93
+ return scored.slice(0, limit).map(entry => entry.card);
94
+ }
95
+ /**
96
+ * Whether a card answers a free-text query: any key or the gloss / user gloss
97
+ * / a context sentence contains the query's key (the sidebar's ⌕ and the
98
+ * coach's `library_read · cards` share this).
99
+ */
100
+ export function cardMatchesQuery(card, query) {
101
+ const needle = cardKey(query);
102
+ if (needle === '')
103
+ return true;
104
+ if (cardKeys(card).some(key => key.includes(needle)))
105
+ return true;
106
+ const haystack = [card.gloss, card.userGloss ?? '', ...card.contexts.map(context => context.sentence)]
107
+ .join('\n')
108
+ .normalize('NFKC')
109
+ .toLowerCase();
110
+ return haystack.includes(query.normalize('NFKC').toLowerCase().trim());
111
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The library tab body: the right-Sidebar page, one route stack (see
3
+ * `client/store.ts`) over the v2 screens —
4
+ *
5
+ * - **今日** — reviews due, where each book stands, what the coach just noted,
6
+ * the projects;
7
+ * - **项目** — its textbooks in study order (each with its own path — the
8
+ * project carries none), drafts waiting, its cards, its reviews;
9
+ * - **书** — the roadmap draft waiting for 采用, or the adopted S-shaped path;
10
+ * - **这一节** — guide / the pages photographed for it / its chapter quizzes;
11
+ * - **知识卡片** — the learner's own cards, filtered by where they came from;
12
+ * - **章节考 → 结算** — answered here, choice and fill judged on the spot;
13
+ * - **复习** — flashcards over the due cards.
14
+ *
15
+ * The conversation does what needs the model (adding a book, drafting a path,
16
+ * composing a quiz, judging open answers): the pane binds the session and
17
+ * drafts the ask. One layout serves every width (docs/panel-design.md):
18
+ * `PanelRoot` declares the container, compact rules live in the stylesheet.
19
+ *
20
+ * @module @khorsheed/dsh-library/client/LibraryPane
21
+ */
22
+ import { type ReactNode } from 'react';
23
+ import type { LibraryPaneProps } from './contract.ts';
24
+ /** The tab body. */
25
+ export declare function LibraryPane(props: LibraryPaneProps): ReactNode;
26
+ //# sourceMappingURL=LibraryPane.d.ts.map
@@ -0,0 +1,129 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * The library tab body: the right-Sidebar page, one route stack (see
4
+ * `client/store.ts`) over the v2 screens —
5
+ *
6
+ * - **今日** — reviews due, where each book stands, what the coach just noted,
7
+ * the projects;
8
+ * - **项目** — its textbooks in study order (each with its own path — the
9
+ * project carries none), drafts waiting, its cards, its reviews;
10
+ * - **书** — the roadmap draft waiting for 采用, or the adopted S-shaped path;
11
+ * - **这一节** — guide / the pages photographed for it / its chapter quizzes;
12
+ * - **知识卡片** — the learner's own cards, filtered by where they came from;
13
+ * - **章节考 → 结算** — answered here, choice and fill judged on the spot;
14
+ * - **复习** — flashcards over the due cards.
15
+ *
16
+ * The conversation does what needs the model (adding a book, drafting a path,
17
+ * composing a quiz, judging open answers): the pane binds the session and
18
+ * drafts the ask. One layout serves every width (docs/panel-design.md):
19
+ * `PanelRoot` declares the container, compact rules live in the stylesheet.
20
+ *
21
+ * @module @khorsheed/dsh-library/client/LibraryPane
22
+ */
23
+ import { useEffect, useRef, useState } from 'react';
24
+ import { PanelRoot } from '@khorsheed/dsh-client-ui-panel-kit/src/client/index.ts';
25
+ import { BookView } from "./views/book.js";
26
+ import { NodeView } from "./views/station.js";
27
+ import { CardsView } from "./views/cards.js";
28
+ import { NewProjectView, ProjectView, TodayView } from "./views/home.js";
29
+ import { QuizView, ResultView, ReviewView } from "./views/quiz.js";
30
+ import { LessonView } from "./views/lesson.js";
31
+ import css from './LibraryPane.module.css';
32
+ /** The tab body. */
33
+ export function LibraryPane(props) {
34
+ const route = props.useStore(s => s.stack[s.stack.length - 1]);
35
+ const { revealed, actions } = props;
36
+ // A reveal (the session's first library write) lands on its project: taken on
37
+ // mount when the reveal opened this tab, at once when the tab was already open.
38
+ useEffect(() => {
39
+ const land = () => {
40
+ const projectId = revealed.take();
41
+ if (projectId !== undefined)
42
+ actions.openProject(projectId);
43
+ };
44
+ land();
45
+ return revealed.subscribe(land);
46
+ }, [revealed, actions]);
47
+ return (_jsxs(PanelRoot, { className: css.root, children: [(() => {
48
+ switch (route.view) {
49
+ case 'today': return _jsx(TodayView, { p: props });
50
+ case 'newProject': return _jsx(NewProjectView, { p: props });
51
+ case 'project': return _jsx(ProjectView, { p: props, projectId: route.projectId });
52
+ case 'resource': return _jsx(BookView, { p: props, projectId: route.projectId, resourceId: route.resourceId });
53
+ case 'node': return _jsx(NodeView, { p: props, projectId: route.projectId, resourceId: route.resourceId, nodeId: route.nodeId, tab: route.tab });
54
+ case 'lesson': return _jsx(LessonView, { p: props, projectId: route.projectId, resourceId: route.resourceId, nodeId: route.nodeId });
55
+ case 'cards': return _jsx(CardsView, { p: props, projectId: route.projectId, filter: route.filter });
56
+ case 'quiz': return _jsx(QuizView, { p: props, projectId: route.projectId, resourceId: route.resourceId, quizId: route.quizId });
57
+ case 'result': return _jsx(ResultView, { p: props, projectId: route.projectId, resourceId: route.resourceId, result: route.result });
58
+ case 'review': return _jsx(ReviewView, { p: props, ...(route.projectId === undefined ? {} : { projectId: route.projectId }) });
59
+ }
60
+ })(), _jsx(RecordToast, { p: props })] }));
61
+ }
62
+ /** How often the toast looks for new notes from this session. */
63
+ const TOAST_POLL_MS = 5000;
64
+ /** How long a toast stays up. */
65
+ const TOAST_MS = 10_000;
66
+ /**
67
+ * 「已记下」: when the coach notes a weak point from THIS session, say so in
68
+ * the sidebar with an undo — the chat line says it too, this is where the
69
+ * undo lives. Only notes made after the pane opened are toasted.
70
+ *
71
+ * The same timer is the pane's change poll: the host's writes (a landed quiz,
72
+ * a recorded page) move `latestActivity`, and a move refreshes the open view —
73
+ * the per-tab watch windows alone lapsed after two minutes and left the tab
74
+ * stale until a remount.
75
+ */
76
+ function RecordToast({ p }) {
77
+ const { t, actions } = p;
78
+ const since = useRef(new Date().toISOString());
79
+ const seen = useRef(new Set());
80
+ const activityAt = useRef();
81
+ const [record, setRecord] = useState();
82
+ const [undone, setUndone] = useState(false);
83
+ useEffect(() => {
84
+ const poll = () => {
85
+ if (document.visibilityState !== 'visible')
86
+ return;
87
+ void p.latestActivity().then(result => {
88
+ if (!result.ok)
89
+ return;
90
+ const at = result.value.at;
91
+ if (at === undefined)
92
+ return;
93
+ // The first read only sets the baseline — the views just loaded fresh.
94
+ if (activityAt.current !== undefined && at > activityAt.current)
95
+ actions.refresh();
96
+ activityAt.current = at;
97
+ }, () => { });
98
+ void p.recentRecords().then(result => {
99
+ if (!result.ok)
100
+ return;
101
+ const fresh = result.value.records.find(entry => entry.session === String(p.sessionId) && entry.at > since.current && !seen.current.has(entry.at));
102
+ if (fresh === undefined)
103
+ return;
104
+ seen.current.add(fresh.at);
105
+ setUndone(false);
106
+ setRecord(fresh);
107
+ actions.refresh();
108
+ }, () => { });
109
+ };
110
+ const timer = setInterval(poll, TOAST_POLL_MS);
111
+ return () => { clearInterval(timer); };
112
+ }, [p.sessionId]);
113
+ useEffect(() => {
114
+ if (record === undefined)
115
+ return;
116
+ const timer = setTimeout(() => { setRecord(undefined); }, undone ? 2000 : TOAST_MS);
117
+ return () => { clearTimeout(timer); };
118
+ }, [record, undone]);
119
+ if (record === undefined)
120
+ return null;
121
+ return (_jsxs("div", { className: css.toast, role: "status", children: [_jsx("span", { className: css.toastText, children: undone ? t('toast.undone') : t('toast.recorded', { terms: record.title ?? '' }) }), !undone && (_jsx("button", { type: "button", className: css.toastAction, onClick: () => {
122
+ void p.undoCards(record.projectId, record.ids ?? [], record.at).then(result => {
123
+ if (result.ok && result.value.ok) {
124
+ setUndone(true);
125
+ actions.refresh();
126
+ }
127
+ });
128
+ }, children: t('toast.undo') }))] }));
129
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The session header's library chip: which project — or which of its books —
3
+ * this learning session works on. The tools default `project` / `book` to it,
4
+ * the per-turn context names it, and the sidebar's gestures bind it too, so
5
+ * the chip mostly SHOWS the binding; its menu changes it.
6
+ *
7
+ * Shown only in sessions that hold the library tools (the strict half of the
8
+ * visibility criterion), and it re-reads the binding while visible: the coach
9
+ * binds through `library_binding` and nothing announces that here.
10
+ *
11
+ * @module @khorsheed/dsh-library/client/bind
12
+ */
13
+ import { type ReactNode } from 'react';
14
+ import type { TranslateNS } from '@deepseek-ai/dsh-client-locale/client';
15
+ import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol';
16
+ import type { LibraryBinding, LibraryOutcome, LibraryProjectSummary } from '../../types.ts';
17
+ /** One session's chip face (the header slot injects it per session). */
18
+ export interface LibraryBindInjected {
19
+ offered: () => boolean;
20
+ subscribe: (listener: () => void) => () => void;
21
+ boundTarget: () => Promise<RemoteResult<LibraryBinding | null>>;
22
+ bindTarget: (binding: LibraryBinding | null) => Promise<RemoteResult<LibraryOutcome<{
23
+ binding: LibraryBinding | null;
24
+ }>>>;
25
+ listProjects: () => Promise<RemoteResult<{
26
+ projects: LibraryProjectSummary[];
27
+ }>>;
28
+ }
29
+ export interface LibraryBindChipProps extends LibraryBindInjected {
30
+ readonly t: TranslateNS<'library'>;
31
+ }
32
+ export declare function LibraryBindChip(props: LibraryBindChipProps): ReactNode;
33
+ //# sourceMappingURL=BindChip.d.ts.map
@@ -0,0 +1,102 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * The session header's library chip: which project — or which of its books —
4
+ * this learning session works on. The tools default `project` / `book` to it,
5
+ * the per-turn context names it, and the sidebar's gestures bind it too, so
6
+ * the chip mostly SHOWS the binding; its menu changes it.
7
+ *
8
+ * Shown only in sessions that hold the library tools (the strict half of the
9
+ * visibility criterion), and it re-reads the binding while visible: the coach
10
+ * binds through `library_binding` and nothing announces that here.
11
+ *
12
+ * @module @khorsheed/dsh-library/client/bind
13
+ */
14
+ import { useCallback, useEffect, useState, useSyncExternalStore } from 'react';
15
+ import { Menu } from '@deepseek-ai/dsh-client-ui-primitives';
16
+ import { IconChevronDownOutlineMedium } from "../icons.js";
17
+ import css from './BindChip.module.css';
18
+ /** How often the chip re-reads a binding the coach may have moved. */
19
+ const BIND_POLL_MS = 4000;
20
+ /** A binding's menu id. */
21
+ function idOf(binding) {
22
+ return binding.resourceId === undefined ? `project:${binding.projectId}` : `resource:${binding.projectId}:${binding.resourceId}`;
23
+ }
24
+ export function LibraryBindChip(props) {
25
+ const shown = useSyncExternalStore(props.subscribe, props.offered);
26
+ return shown ? _jsx(BindMenu, { ...props }) : null;
27
+ }
28
+ function BindMenu({ t, boundTarget, bindTarget, listProjects }) {
29
+ const [open, setOpen] = useState(false);
30
+ const [bound, setBound] = useState(null);
31
+ const [projects, setProjects] = useState([]);
32
+ const [failed, setFailed] = useState(false);
33
+ const reload = useCallback(async () => {
34
+ const [current, list] = await Promise.all([boundTarget(), listProjects()]);
35
+ if (current.ok)
36
+ setBound(current.value);
37
+ if (list.ok)
38
+ setProjects(list.value.projects);
39
+ }, [boundTarget, listProjects]);
40
+ useEffect(() => { void reload().catch(() => { }); }, [reload]);
41
+ useEffect(() => {
42
+ const timer = setInterval(() => {
43
+ if (document.visibilityState !== 'visible')
44
+ return;
45
+ void boundTarget().then(current => { if (current.ok)
46
+ setBound(current.value); }).catch(() => { });
47
+ }, BIND_POLL_MS);
48
+ return () => { clearInterval(timer); };
49
+ }, [boundTarget]);
50
+ // A binding to something the chip has not listed yet (the coach just made it): list again.
51
+ const unseen = bound !== null && !projects.some(project => project.id === bound.projectId
52
+ && (bound.resourceId === undefined || project.resources.some(resource => resource.id === bound.resourceId)));
53
+ useEffect(() => {
54
+ if (!unseen)
55
+ return;
56
+ void listProjects().then(list => { if (list.ok)
57
+ setProjects(list.value.projects); }).catch(() => { });
58
+ }, [unseen, listProjects]);
59
+ const pick = async (binding) => {
60
+ const result = await bindTarget(binding);
61
+ if (result.ok && result.value.ok) {
62
+ setBound(result.value.binding);
63
+ setFailed(false);
64
+ }
65
+ else {
66
+ setFailed(true);
67
+ }
68
+ };
69
+ const project = bound === null ? undefined : projects.find(candidate => candidate.id === bound.projectId);
70
+ const resource = bound?.resourceId === undefined ? undefined : project?.resources.find(candidate => candidate.id === bound.resourceId);
71
+ const title = resource?.title ?? project?.name;
72
+ const items = [
73
+ { type: 'label', id: 'heading', text: t('bind.heading') },
74
+ ...projects.length === 0
75
+ ? [{ id: 'empty', label: t('bind.empty'), disabled: true }]
76
+ : projects.flatMap((entry) => [
77
+ { id: `project:${entry.id}`, label: t('bind.wholeProject', { project: entry.name }) },
78
+ // Only a thick entry (a book, a course) is bindable — a thin link carries no path.
79
+ ...entry.resources.filter(entryResource => entryResource.kind === 'book' || entryResource.kind === 'course').map(entryResource => ({ id: `resource:${entry.id}:${entryResource.id}`, label: ` ${entryResource.title}` })),
80
+ ]),
81
+ ...bound === null ? [] : [
82
+ { type: 'separator', id: 'sep' },
83
+ { id: 'unbind', label: t('bind.unbind') },
84
+ ],
85
+ ];
86
+ return (_jsx(Menu, { open: open, portal: true, dense: true, items: items, ...bound === null ? {} : { selectedId: idOf(bound) }, onSelect: id => {
87
+ setOpen(false);
88
+ if (id === 'unbind')
89
+ void pick(null);
90
+ else if (id.startsWith('project:'))
91
+ void pick({ projectId: id.slice('project:'.length) });
92
+ else if (id.startsWith('resource:')) {
93
+ const [, projectId, resourceId] = id.split(':');
94
+ if (projectId !== undefined && resourceId !== undefined)
95
+ void pick({ projectId, resourceId });
96
+ }
97
+ }, onClose: () => { setOpen(false); }, anchor: (_jsxs("button", { type: "button", className: css.trigger, "data-bound": bound !== null, "aria-label": t('bind.aria'), "aria-haspopup": "menu", "aria-expanded": open, title: failed ? t('bind.failed') : undefined, onClick: () => {
98
+ if (!open)
99
+ void reload().catch(() => { });
100
+ setOpen(value => !value);
101
+ }, children: [_jsx("span", { className: css.label, children: failed ? t('bind.failed') : title === undefined ? t('bind.unbound') : t('bind.bound', { title }) }), _jsx(IconChevronDownOutlineMedium, { size: 12 })] })) }));
102
+ }
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Compile-time contract between the library's browser half and its host half.
3
+ *
4
+ * Nothing here executes: this module holds the shapes only, so a change in the
5
+ * Remote surface or in the injected business face breaks compilation at the
6
+ * components that consume them instead of at runtime.
7
+ *
8
+ * @module @khorsheed/dsh-library/client/contract
9
+ */
10
+ import type { SessionId } from '@deepseek-ai/dsh-session/types';
11
+ import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol';
12
+ import type { GlobalStandardProps, InjectFace, PropsLocale, PropsStore } from '@deepseek-ai/dsh-client-ui-slots';
13
+ import type { LibraryAssessResult, LibraryAssessment, LibraryAttempt, LibraryBinding, LibraryCard, LibraryCardQuery, LibraryProjectPatch, LibraryEvent, LibraryOutcome, LibraryPage, LibraryProject, LibraryProjectSummary, LibraryResource, LibraryReviewRating, LibraryRoadmap, LibraryTemplate, LibraryVerdict } from '../types.ts';
14
+ import type { createLibraryStore } from './store.ts';
15
+ import type { RevealedProject } from './reveal.ts';
16
+ export type { RemoteResult };
17
+ /** A Remote call's answer: the protocol envelope around a domain outcome. */
18
+ type Answer<T> = Promise<RemoteResult<LibraryOutcome<T>>>;
19
+ /**
20
+ * The business face every library surface receives from its registration:
21
+ * the wire verbs straight off the Remote, plus the two session gestures the
22
+ * pane cannot perform itself (priming the conversation, binding the session).
23
+ */
24
+ export interface LibraryPaneInjected {
25
+ listProjects: () => Promise<RemoteResult<{
26
+ projects: LibraryProjectSummary[];
27
+ }>>;
28
+ getProject: (id: string) => Answer<{
29
+ project: LibraryProjectSummary;
30
+ resources: LibraryResource[];
31
+ }>;
32
+ createProject: (request: {
33
+ name: string;
34
+ template: LibraryTemplate;
35
+ goal?: string;
36
+ }) => Answer<{
37
+ project: LibraryProject;
38
+ }>;
39
+ updateProject: (projectId: string, patch: LibraryProjectPatch) => Answer<{
40
+ project: LibraryProject;
41
+ }>;
42
+ /** Delete a project and everything it holds (shelf, paths, pages, cards) — irreversible. */
43
+ deleteProject: (projectId: string) => Answer<{
44
+ id: string;
45
+ }>;
46
+ /** Remove a shelf entry: its registration, roadmaps and pages go; its cards stay, unhung. */
47
+ removeResource: (projectId: string, resourceId: string) => Answer<{
48
+ id: string;
49
+ }>;
50
+ /** Switch a textbook between 知识型 and 材料型. */
51
+ setBookKind: (projectId: string, resourceId: string, bookKind: 'knowledge' | 'material') => Answer<{
52
+ resource: LibraryResource;
53
+ }>;
54
+ /** Move a textbook to another place in the study order (the rest shift). */
55
+ moveResource: (projectId: string, resourceId: string, seq: number) => Answer<{
56
+ resources: LibraryResource[];
57
+ }>;
58
+ getRoadmap: (projectId: string, resourceId: string) => Promise<RemoteResult<{
59
+ roadmap?: LibraryRoadmap;
60
+ draft?: LibraryRoadmap;
61
+ }>>;
62
+ adoptRoadmap: (projectId: string, resourceId: string) => Answer<{
63
+ roadmap: LibraryRoadmap;
64
+ }>;
65
+ discardDraft: (projectId: string, resourceId: string) => Answer<{
66
+ resourceId: string;
67
+ }>;
68
+ markNodeRead: (projectId: string, resourceId: string | undefined, nodeId: string) => Answer<{
69
+ roadmap: LibraryRoadmap;
70
+ unlocked: string[];
71
+ }>;
72
+ listPages: (projectId: string, resourceId: string, nodeId?: string) => Promise<RemoteResult<{
73
+ pages: LibraryPage[];
74
+ }>>;
75
+ readPagePhoto: (projectId: string, pageId: string) => Answer<{
76
+ data: string;
77
+ mediaType: string;
78
+ }>;
79
+ /** A project's shelf, thick and thin. */
80
+ listResources: (projectId: string) => Promise<RemoteResult<{
81
+ resources: LibraryResource[];
82
+ }>>;
83
+ /** A station's acceptance history, newest first. */
84
+ listAttempts: (projectId: string, nodeId?: string, assessmentId?: string) => Promise<RemoteResult<{
85
+ attempts: LibraryAttempt[];
86
+ }>>;
87
+ listCards: (query: LibraryCardQuery) => Promise<RemoteResult<{
88
+ cards: LibraryCard[];
89
+ total: number;
90
+ }>>;
91
+ dueCards: (projectId?: string) => Promise<RemoteResult<{
92
+ cards: (LibraryCard & {
93
+ projectId: string;
94
+ })[];
95
+ }>>;
96
+ editCard: (projectId: string, cardId: string, patch: {
97
+ status?: LibraryCard['status'];
98
+ gloss?: string;
99
+ }) => Answer<{
100
+ card: LibraryCard;
101
+ }>;
102
+ undoCards: (projectId: string, ids: readonly string[], at: string) => Answer<{
103
+ removed: number;
104
+ reverted: number;
105
+ }>;
106
+ reviewCard: (projectId: string, cardId: string, rating: LibraryReviewRating) => Answer<{
107
+ card: LibraryCard;
108
+ }>;
109
+ listAssessments: (projectId: string, resourceId?: string, nodeId?: string, type?: 'quiz' | 'practice') => Promise<RemoteResult<{
110
+ assessments: LibraryAssessment[];
111
+ }>>;
112
+ getAssessment: (projectId: string, assessmentId: string) => Answer<{
113
+ assessment: LibraryAssessment;
114
+ }>;
115
+ putAttempt: (projectId: string, assessmentId: string, verdicts: readonly LibraryVerdict[]) => Answer<LibraryAssessResult>;
116
+ /** The newest knowledge-point state changes (今日 「最近的变化」). */
117
+ recentChanges: () => Promise<RemoteResult<{
118
+ changes: (LibraryEvent & {
119
+ projectId: string;
120
+ projectName: string;
121
+ })[];
122
+ }>>;
123
+ /** 订正: a retake paper over a station's open misses (all, or the keys named). */
124
+ /** No station: the whole project's open misses (今日复习). */
125
+ createRetake: (projectId: string, resourceId?: string, nodeId?: string, keys?: readonly string[]) => Answer<{
126
+ assessment: LibraryAssessment;
127
+ }>;
128
+ recentRecords: () => Promise<RemoteResult<{
129
+ records: (LibraryEvent & {
130
+ projectId: string;
131
+ projectName: string;
132
+ })[];
133
+ }>>;
134
+ /** The newest event's timestamp anywhere — the pane's change poll (host-side writes otherwise surface only on remount). */
135
+ latestActivity: () => Promise<RemoteResult<{
136
+ at?: string;
137
+ session?: {
138
+ id: string;
139
+ at: string;
140
+ };
141
+ }>>;
142
+ /** The session's binding, when it has one. */
143
+ boundTarget: () => Promise<RemoteResult<LibraryBinding | null>>;
144
+ /** The project a reveal asks the pane to land on, taken once (see reveal.ts). */
145
+ revealed: Pick<RevealedProject, 'take' | 'subscribe'>;
146
+ /** Bind the session (null unbinds). */
147
+ bindTarget: (binding: LibraryBinding | null) => Answer<{
148
+ binding: LibraryBinding | null;
149
+ }>;
150
+ /**
151
+ * Prime the session's conversation with a request (the coach does the
152
+ * work; the pane drafts the ask). False when the session has no input.
153
+ */
154
+ primeConversation: (text: string) => boolean;
155
+ /**
156
+ * Send a message in the session for the learner (it shows in the chat as
157
+ * theirs). False when the session has no input or is busy admitting.
158
+ */
159
+ sendMessage: (text: string) => boolean;
160
+ /** Send a photo with a message in the session for the learner (the 拍照 gesture). */
161
+ sendPhoto: (file: File, text: string) => boolean;
162
+ }
163
+ /** Full props of the library pane: the seat's session + store + locale + face. */
164
+ export type LibraryPaneProps = {
165
+ sessionId: SessionId;
166
+ } & GlobalStandardProps & PropsStore<ReturnType<typeof createLibraryStore>> & InjectFace<LibraryPaneInjected> & PropsLocale<'library'>;
167
+ //# sourceMappingURL=contract.d.ts.map
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Covers — what a station draws on — as chips with their provenance badges,
3
+ * plus the client-side mirror of the host's evidence gate (display only; the
4
+ * host enforces, `service.ts`'s coversVerbatimBlocker).
5
+ *
6
+ * v5: a cover is one shape — `{ ref, locator? }` — and the ref describes
7
+ * itself: a shelf resource's id, a bare url, or `topic:…` for coached
8
+ * teaching.
9
+ *
10
+ * @module @khorsheed/dsh-library/client/covers
11
+ */
12
+ import type { ReactNode } from 'react';
13
+ import { type LibraryNodeCover, type LibraryResource } from '../types.ts';
14
+ import type { LibraryPaneProps } from './contract.ts';
15
+ /** What labeling a cover needs: the project's shelf (books included). */
16
+ export interface CoverContext {
17
+ readonly resources: readonly LibraryResource[];
18
+ }
19
+ type T = LibraryPaneProps['t'];
20
+ /** A cover's one-line label. */
21
+ export declare function coverLabel(cover: LibraryNodeCover, context: CoverContext, t: T): string;
22
+ /** A cover's provenance badge (absent on a shelf resource — its tocSource already records it). */
23
+ export declare function ProvenanceBadge({ cover, t }: {
24
+ cover: LibraryNodeCover;
25
+ t: T;
26
+ }): ReactNode;
27
+ /** One station's covers as a chip row: label + provenance badge each. */
28
+ export declare function CoverChips({ covers, context, t }: {
29
+ covers: readonly LibraryNodeCover[];
30
+ context: CoverContext;
31
+ t: T;
32
+ }): ReactNode;
33
+ /** One line for a path row: 「《书》 第 3-5 章 · 链接 ×2 · 讲授 ×1」. */
34
+ export declare function summarizeCovers(covers: readonly LibraryNodeCover[], context: CoverContext, t: T): string;
35
+ export {};
36
+ //# sourceMappingURL=covers.d.ts.map