@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.
- package/CHANGELOG.md +14 -0
- package/README.en.md +49 -0
- package/README.i18n.yaml +6 -0
- package/README.md +48 -2
- package/cordis.patch.yml +17 -0
- package/lib/client.js +66276 -0
- package/lib/index.js +3318 -0
- package/lib/profiles-BXM8xNUf.js +1608 -0
- package/lib/profiles-BrbeM-bi.js +1635 -0
- package/lib/profiles-CFrPQdfO.js +1692 -0
- package/lib/profiles-DxtzIUvd.js +1632 -0
- package/lib/profiles-O0ddEit3.js +1631 -0
- package/lib/profiles-Oj-zBAY9.js +1692 -0
- package/lib/profiles-gIEviEuO.js +1610 -0
- package/lib/store-CIcZgTsg.js +1456 -0
- package/lib/store-DfYOmS44.js +1418 -0
- package/lib/tool.js +1597 -0
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +3811 -0
- package/lib/typert.remote-client.d.ts +137 -0
- package/lib/typert.remote-client.js +3077 -0
- package/lib/types/cards.d.ts +41 -0
- package/lib/types/cards.js +111 -0
- package/lib/types/client/LibraryPane.d.ts +26 -0
- package/lib/types/client/LibraryPane.js +117 -0
- package/lib/types/client/bind/BindChip.d.ts +33 -0
- package/lib/types/client/bind/BindChip.js +102 -0
- package/lib/types/client/contract.d.ts +164 -0
- package/lib/types/client/contract.js +1 -0
- package/lib/types/client/covers.d.ts +36 -0
- package/lib/types/client/covers.js +66 -0
- package/lib/types/client/definition.d.ts +27 -0
- package/lib/types/client/definition.js +32 -0
- package/lib/types/client/icons-local.d.ts +41 -0
- package/lib/types/client/icons-local.js +25 -0
- package/lib/types/client/icons.d.ts +27 -0
- package/lib/types/client/icons.js +11 -0
- package/lib/types/client/index.d.ts +35 -0
- package/lib/types/client/index.js +219 -0
- package/lib/types/client/judge.d.ts +70 -0
- package/lib/types/client/judge.js +262 -0
- package/lib/types/client/locales.d.ts +553 -0
- package/lib/types/client/locales.js +1076 -0
- package/lib/types/client/look.d.ts +63 -0
- package/lib/types/client/look.js +64 -0
- package/lib/types/client/math.d.ts +24 -0
- package/lib/types/client/math.js +47 -0
- package/lib/types/client/mistakes.d.ts +21 -0
- package/lib/types/client/mistakes.js +28 -0
- package/lib/types/client/preset-visibility.d.ts +109 -0
- package/lib/types/client/preset-visibility.js +123 -0
- package/lib/types/client/quiz-drafts.d.ts +29 -0
- package/lib/types/client/quiz-drafts.js +50 -0
- package/lib/types/client/store.d.ts +109 -0
- package/lib/types/client/store.js +44 -0
- package/lib/types/client/titles.d.ts +17 -0
- package/lib/types/client/titles.js +23 -0
- package/lib/types/client/ui.d.ts +96 -0
- package/lib/types/client/ui.js +124 -0
- package/lib/types/client/views/book.d.ts +57 -0
- package/lib/types/client/views/book.js +164 -0
- package/lib/types/client/views/cards.d.ts +17 -0
- package/lib/types/client/views/cards.js +53 -0
- package/lib/types/client/views/home.d.ts +26 -0
- package/lib/types/client/views/home.js +263 -0
- package/lib/types/client/views/lesson.d.ts +42 -0
- package/lib/types/client/views/lesson.js +146 -0
- package/lib/types/client/views/path.d.ts +32 -0
- package/lib/types/client/views/path.js +64 -0
- package/lib/types/client/views/quiz.d.ts +42 -0
- package/lib/types/client/views/quiz.js +366 -0
- package/lib/types/client/views/station-quiz.d.ts +43 -0
- package/lib/types/client/views/station-quiz.js +203 -0
- package/lib/types/client/views/station.d.ts +18 -0
- package/lib/types/client/views/station.js +142 -0
- package/lib/types/image-refs.d.ts +35 -0
- package/lib/types/image-refs.js +71 -0
- package/lib/types/index.d.ts +51 -0
- package/lib/types/index.js +31 -0
- package/lib/types/mastery.d.ts +81 -0
- package/lib/types/mastery.js +110 -0
- package/lib/types/profiles.d.ts +31 -0
- package/lib/types/profiles.js +25 -0
- package/lib/types/remote.d.ts +355 -0
- package/lib/types/remote.js +332 -0
- package/lib/types/service/assess.d.ts +144 -0
- package/lib/types/service/assess.js +773 -0
- package/lib/types/service/bindings.d.ts +92 -0
- package/lib/types/service/bindings.js +198 -0
- package/lib/types/service/cards.d.ts +85 -0
- package/lib/types/service/cards.js +320 -0
- package/lib/types/service/core.d.ts +124 -0
- package/lib/types/service/core.js +169 -0
- package/lib/types/service/pages.d.ts +60 -0
- package/lib/types/service/pages.js +125 -0
- package/lib/types/service/roadmap.d.ts +99 -0
- package/lib/types/service/roadmap.js +284 -0
- package/lib/types/service/rules.d.ts +63 -0
- package/lib/types/service/rules.js +245 -0
- package/lib/types/service/shelf.d.ts +136 -0
- package/lib/types/service/shelf.js +422 -0
- package/lib/types/service.d.ts +42 -0
- package/lib/types/service.js +41 -0
- package/lib/types/store.d.ts +257 -0
- package/lib/types/store.js +1376 -0
- package/lib/types/tool-definition.d.ts +49 -0
- package/lib/types/tool-definition.js +49 -0
- package/lib/types/tool.d.ts +71 -0
- package/lib/types/tool.js +174 -0
- package/lib/types/tools/assess.d.ts +11 -0
- package/lib/types/tools/assess.js +82 -0
- package/lib/types/tools/binding.d.ts +11 -0
- package/lib/types/tools/binding.js +46 -0
- package/lib/types/tools/cards.d.ts +11 -0
- package/lib/types/tools/cards.js +100 -0
- package/lib/types/tools/kit.d.ts +215 -0
- package/lib/types/tools/kit.js +281 -0
- package/lib/types/tools/notes.d.ts +11 -0
- package/lib/types/tools/notes.js +40 -0
- package/lib/types/tools/page.d.ts +11 -0
- package/lib/types/tools/page.js +59 -0
- package/lib/types/tools/project.d.ts +11 -0
- package/lib/types/tools/project.js +67 -0
- package/lib/types/tools/quiz.d.ts +11 -0
- package/lib/types/tools/quiz.js +74 -0
- package/lib/types/tools/read.d.ts +11 -0
- package/lib/types/tools/read.js +127 -0
- package/lib/types/tools/resource.d.ts +11 -0
- package/lib/types/tools/resource.js +147 -0
- package/lib/types/tools/roadmap.d.ts +11 -0
- package/lib/types/tools/roadmap.js +102 -0
- package/lib/types/types.d.ts +752 -0
- package/lib/types/types.js +159 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +135 -3
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The library's visual identity pieces (M4): a project's color and glyph,
|
|
3
|
+
* and a book's generated cover. A project's color rides the `--pc` custom
|
|
4
|
+
* property (set by `data-color` in the stylesheet), so every surface under it
|
|
5
|
+
* — its card, its header band, its books' covers and its path — inherits one
|
|
6
|
+
* hue without passing it around.
|
|
7
|
+
*
|
|
8
|
+
* @module @khorsheed/dsh-library/client/look
|
|
9
|
+
*/
|
|
10
|
+
import type { ReactNode } from 'react';
|
|
11
|
+
import type { LibraryColor, LibraryIcon, LibraryProject } from '../types.ts';
|
|
12
|
+
/** A project's color, with the template default for projects made before M4. */
|
|
13
|
+
export declare function colorOf(project: Pick<LibraryProject, 'color' | 'template'>): LibraryColor;
|
|
14
|
+
/** A project's icon, with the template default. */
|
|
15
|
+
export declare function iconOf(project: Pick<LibraryProject, 'icon' | 'template'>): LibraryIcon;
|
|
16
|
+
/** A project's glyph tile (white strokes on its color). */
|
|
17
|
+
export declare function ProjectGlyph({ icon, size }: {
|
|
18
|
+
icon: LibraryIcon;
|
|
19
|
+
size?: number;
|
|
20
|
+
}): ReactNode;
|
|
21
|
+
/** Just the glyph's strokes, for pickers. */
|
|
22
|
+
export declare function GlyphStrokes({ icon }: {
|
|
23
|
+
icon: LibraryIcon;
|
|
24
|
+
}): ReactNode;
|
|
25
|
+
/**
|
|
26
|
+
* A cover's abbreviation: the coach's, else derived — CJK titles keep their
|
|
27
|
+
* first two characters plus any number (剑桥雅思 5 → 剑5), Latin titles their
|
|
28
|
+
* capitalized initials plus any number (Principles of Neural Science → PNS).
|
|
29
|
+
*/
|
|
30
|
+
export declare function shortOf(book: {
|
|
31
|
+
readonly title: string;
|
|
32
|
+
readonly short?: string | undefined;
|
|
33
|
+
}): string;
|
|
34
|
+
/** A generated cover: the project's hue (shaded per shelf position), the abbreviation, the kind. */
|
|
35
|
+
export declare function Cover({ book, shade, size, kindLabel }: {
|
|
36
|
+
book: {
|
|
37
|
+
readonly title: string;
|
|
38
|
+
readonly short?: string | undefined;
|
|
39
|
+
readonly edition?: string | undefined;
|
|
40
|
+
};
|
|
41
|
+
shade?: number;
|
|
42
|
+
size?: 'shelf' | 'mini' | 'badge' | 'card' | 'hero';
|
|
43
|
+
kindLabel?: string | undefined;
|
|
44
|
+
}): ReactNode;
|
|
45
|
+
/** The display shape a cover tile needs, lifted off a shelf resource's meta. */
|
|
46
|
+
export declare function coverOf(resource: {
|
|
47
|
+
readonly title: string;
|
|
48
|
+
readonly meta?: {
|
|
49
|
+
readonly short?: string;
|
|
50
|
+
readonly edition?: string;
|
|
51
|
+
} | undefined;
|
|
52
|
+
}): {
|
|
53
|
+
title: string;
|
|
54
|
+
short?: string;
|
|
55
|
+
edition?: string;
|
|
56
|
+
};
|
|
57
|
+
/** A circular progress ring in the project's color. */
|
|
58
|
+
export declare function Ring({ percent, label, title }: {
|
|
59
|
+
percent: number;
|
|
60
|
+
label?: string | undefined;
|
|
61
|
+
title?: string | undefined;
|
|
62
|
+
}): ReactNode;
|
|
63
|
+
//# sourceMappingURL=look.d.ts.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { TEMPLATE_LOOK } from "../types.js";
|
|
3
|
+
import css from './LibraryPane.module.css';
|
|
4
|
+
/** A project's color, with the template default for projects made before M4. */
|
|
5
|
+
export function colorOf(project) {
|
|
6
|
+
return project.color ?? TEMPLATE_LOOK[project.template].color;
|
|
7
|
+
}
|
|
8
|
+
/** A project's icon, with the template default. */
|
|
9
|
+
export function iconOf(project) {
|
|
10
|
+
return project.icon ?? TEMPLATE_LOOK[project.template].icon;
|
|
11
|
+
}
|
|
12
|
+
/** The 24×24 strokes of each preset icon. */
|
|
13
|
+
const GLYPHS = {
|
|
14
|
+
globe: _jsxs(_Fragment, { children: [_jsx("circle", { cx: "12", cy: "12", r: "9" }), _jsx("path", { d: "M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" })] }),
|
|
15
|
+
brain: _jsxs(_Fragment, { children: [_jsx("path", { d: "M9 4a3 3 0 0 0-3 3v1a3 3 0 0 0-2 5 3 3 0 0 0 2 5v1a3 3 0 0 0 6 0V5a3 3 0 0 0-3-1z" }), _jsx("path", { d: "M15 4a3 3 0 0 1 3 3v1a3 3 0 0 1 2 5 3 3 0 0 1-2 5v1a3 3 0 0 1-6 0" })] }),
|
|
16
|
+
book: _jsxs(_Fragment, { children: [_jsx("path", { d: "M4 5a2 2 0 0 1 2-2h13v16H6a2 2 0 0 0-2 2z" }), _jsx("path", { d: "M4 19V5" })] }),
|
|
17
|
+
flask: _jsx(_Fragment, { children: _jsx("path", { d: "M9 3h6M10 3v6l-5 9a2 2 0 0 0 2 3h10a2 2 0 0 0 2-3l-5-9V3" }) }),
|
|
18
|
+
atom: _jsxs(_Fragment, { children: [_jsx("circle", { cx: "12", cy: "12", r: "1.5" }), _jsx("ellipse", { cx: "12", cy: "12", rx: "9", ry: "4" }), _jsx("ellipse", { cx: "12", cy: "12", rx: "9", ry: "4", transform: "rotate(60 12 12)" }), _jsx("ellipse", { cx: "12", cy: "12", rx: "9", ry: "4", transform: "rotate(120 12 12)" })] }),
|
|
19
|
+
leaf: _jsxs(_Fragment, { children: [_jsx("path", { d: "M5 19c0-8 6-14 15-14 0 9-6 15-14 15" }), _jsx("path", { d: "M5 19 13 11" })] }),
|
|
20
|
+
pen: _jsxs(_Fragment, { children: [_jsx("path", { d: "M4 20l4-1 11-11-3-3L5 16z" }), _jsx("path", { d: "M14 6l3 3" })] }),
|
|
21
|
+
music: _jsxs(_Fragment, { children: [_jsx("path", { d: "M9 18V5l11-2v13" }), _jsx("circle", { cx: "6", cy: "18", r: "3" }), _jsx("circle", { cx: "17", cy: "16", r: "3" })] }),
|
|
22
|
+
scale: _jsx(_Fragment, { children: _jsx("path", { d: "M12 3v18M5 7h14M7 7l-3 7a3 3 0 0 0 6 0zM17 7l-3 7a3 3 0 0 0 6 0z" }) }),
|
|
23
|
+
chart: _jsxs(_Fragment, { children: [_jsx("path", { d: "M4 20V4M4 20h16" }), _jsx("path", { d: "M8 16v-4M12 16V8M16 16v-6" })] }),
|
|
24
|
+
code: _jsx(_Fragment, { children: _jsx("path", { d: "M8 7l-5 5 5 5M16 7l5 5-5 5" }) }),
|
|
25
|
+
compass: _jsxs(_Fragment, { children: [_jsx("circle", { cx: "12", cy: "12", r: "9" }), _jsx("path", { d: "M15.5 8.5l-2 5-5 2 2-5z" })] }),
|
|
26
|
+
};
|
|
27
|
+
/** A project's glyph tile (white strokes on its color). */
|
|
28
|
+
export function ProjectGlyph({ icon, size = 34 }) {
|
|
29
|
+
return (_jsx("span", { className: css.glyph, style: { width: size, height: size }, "aria-hidden": "true", children: _jsx("svg", { viewBox: "0 0 24 24", children: GLYPHS[icon] }) }));
|
|
30
|
+
}
|
|
31
|
+
/** Just the glyph's strokes, for pickers. */
|
|
32
|
+
export function GlyphStrokes({ icon }) {
|
|
33
|
+
return _jsx("svg", { viewBox: "0 0 24 24", className: css.glyphStrokes, "aria-hidden": "true", children: GLYPHS[icon] });
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* A cover's abbreviation: the coach's, else derived — CJK titles keep their
|
|
37
|
+
* first two characters plus any number (剑桥雅思 5 → 剑5), Latin titles their
|
|
38
|
+
* capitalized initials plus any number (Principles of Neural Science → PNS).
|
|
39
|
+
*/
|
|
40
|
+
export function shortOf(book) {
|
|
41
|
+
if (book.short !== undefined && book.short !== '')
|
|
42
|
+
return book.short;
|
|
43
|
+
const number = /\d+/.exec(book.title)?.[0] ?? '';
|
|
44
|
+
if (/[\p{Script=Han}]/u.test(book.title))
|
|
45
|
+
return `${book.title.replace(/[\s\d((].*$/, '').slice(0, 1)}${number || book.title.slice(1, 2)}`;
|
|
46
|
+
const initials = book.title.split(/\s+/).filter(word => /^[A-Z]/.test(word)).map(word => word[0]).join('');
|
|
47
|
+
return `${initials.slice(0, 4)}${number}`.slice(0, 5) || book.title.slice(0, 3);
|
|
48
|
+
}
|
|
49
|
+
/** A generated cover: the project's hue (shaded per shelf position), the abbreviation, the kind. */
|
|
50
|
+
export function Cover({ book, shade = 0, size = 'shelf', kindLabel }) {
|
|
51
|
+
return (_jsxs("span", { className: css.cover, "data-size": size, "data-shade": String(shade % 3), "aria-hidden": size !== 'shelf', children: [size !== 'mini' && (_jsxs("span", { className: css.coverHead, children: [_jsx("span", { className: css.coverShort, children: shortOf(book) }), size === 'shelf' && kindLabel !== undefined && _jsx("span", { className: css.coverKind, children: kindLabel })] })), size === 'shelf' && _jsx("span", { className: css.coverTitle, children: book.edition === undefined ? book.title : `${book.title} · ${book.edition}` })] }));
|
|
52
|
+
}
|
|
53
|
+
/** The display shape a cover tile needs, lifted off a shelf resource's meta. */
|
|
54
|
+
export function coverOf(resource) {
|
|
55
|
+
return {
|
|
56
|
+
title: resource.title,
|
|
57
|
+
...(resource.meta?.short === undefined ? {} : { short: resource.meta.short }),
|
|
58
|
+
...(resource.meta?.edition === undefined ? {} : { edition: resource.meta.edition }),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/** A circular progress ring in the project's color. */
|
|
62
|
+
export function Ring({ percent, label, title }) {
|
|
63
|
+
return (_jsx("span", { className: css.ring, "data-labelled": label === undefined ? undefined : 'true', style: { ['--pct']: `${Math.max(0, Math.min(100, percent))}%` }, role: "img", "aria-label": title ?? `${percent}%`, title: title, children: _jsxs("span", { children: [percent, "%", label !== undefined && _jsx("small", { children: label })] }) }));
|
|
64
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TeX rendering for the pane: $…$ / $$…$$ segments in coach-authored text
|
|
3
|
+
* (quiz prompts, options, explanations, station notes) render as MathML —
|
|
4
|
+
* browser-native, so no KaTeX fonts or CSS need bundling.
|
|
5
|
+
*
|
|
6
|
+
* @module @khorsheed/dsh-library/client/math
|
|
7
|
+
*/
|
|
8
|
+
import { type ReactNode } from 'react';
|
|
9
|
+
/** One TeX segment, rendered to MathML; falls back to the source text. */
|
|
10
|
+
export declare function Tex({ tex, block }: {
|
|
11
|
+
tex: string;
|
|
12
|
+
block?: boolean;
|
|
13
|
+
}): ReactNode;
|
|
14
|
+
/** Text with its $…$ / $$…$$ segments rendered; everything else plain. */
|
|
15
|
+
export declare function MathText({ text }: {
|
|
16
|
+
text: string;
|
|
17
|
+
}): ReactNode;
|
|
18
|
+
/** Split text into plain runs and $…$ (inline) / $$…$$ (block) TeX segments. */
|
|
19
|
+
export declare function splitMath(text: string): readonly {
|
|
20
|
+
text?: string;
|
|
21
|
+
tex?: string;
|
|
22
|
+
block?: boolean;
|
|
23
|
+
}[];
|
|
24
|
+
//# sourceMappingURL=math.d.ts.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* TeX rendering for the pane: $…$ / $$…$$ segments in coach-authored text
|
|
4
|
+
* (quiz prompts, options, explanations, station notes) render as MathML —
|
|
5
|
+
* browser-native, so no KaTeX fonts or CSS need bundling.
|
|
6
|
+
*
|
|
7
|
+
* @module @khorsheed/dsh-library/client/math
|
|
8
|
+
*/
|
|
9
|
+
import { Fragment, useMemo } from 'react';
|
|
10
|
+
import katex from 'katex';
|
|
11
|
+
import css from './LibraryPane.module.css';
|
|
12
|
+
/** One TeX segment, rendered to MathML; falls back to the source text. */
|
|
13
|
+
export function Tex({ tex, block }) {
|
|
14
|
+
const html = useMemo(() => {
|
|
15
|
+
try {
|
|
16
|
+
return katex.renderToString(tex, { output: 'mathml', throwOnError: false, displayMode: block === true });
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
}, [tex, block]);
|
|
22
|
+
if (html === undefined)
|
|
23
|
+
return _jsx("code", { children: tex });
|
|
24
|
+
return _jsx("span", { className: block === true ? css.mathBlock : undefined, dangerouslySetInnerHTML: { __html: html } });
|
|
25
|
+
}
|
|
26
|
+
/** Text with its $…$ / $$…$$ segments rendered; everything else plain. */
|
|
27
|
+
export function MathText({ text }) {
|
|
28
|
+
const parts = useMemo(() => splitMath(text), [text]);
|
|
29
|
+
return (_jsx(_Fragment, { children: parts.map((part, at) => part.tex === undefined
|
|
30
|
+
? _jsx(Fragment, { children: part.text }, at)
|
|
31
|
+
: _jsx(Tex, { tex: part.tex, ...(part.block === undefined ? {} : { block: part.block }) }, at)) }));
|
|
32
|
+
}
|
|
33
|
+
/** Split text into plain runs and $…$ (inline) / $$…$$ (block) TeX segments. */
|
|
34
|
+
export function splitMath(text) {
|
|
35
|
+
const parts = [];
|
|
36
|
+
const pattern = /\$\$([\s\S]+?)\$\$|\$([^$\n]+)\$/g;
|
|
37
|
+
let at = 0;
|
|
38
|
+
for (const match of text.matchAll(pattern)) {
|
|
39
|
+
if (match.index > at)
|
|
40
|
+
parts.push({ text: text.slice(at, match.index) });
|
|
41
|
+
parts.push({ tex: match[1] ?? match[2] ?? '', block: match[1] !== undefined });
|
|
42
|
+
at = match.index + match[0].length;
|
|
43
|
+
}
|
|
44
|
+
if (at < text.length)
|
|
45
|
+
parts.push({ text: text.slice(at) });
|
|
46
|
+
return parts;
|
|
47
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The mistake ledger's shared read: every quiz's latest sitting (attempts
|
|
3
|
+
* arrive newest first), and the failed or flagged questions off it. One
|
|
4
|
+
* collection, three filters — the station's 错题 list, its 其他站 shelf and
|
|
5
|
+
* the project page's entry count are the same rows scoped differently, so
|
|
6
|
+
* the two views can never disagree about what counts as missed.
|
|
7
|
+
*
|
|
8
|
+
* @module @khorsheed/dsh-library/client/mistakes
|
|
9
|
+
*/
|
|
10
|
+
import type { LibraryAssessment, LibraryAttempt, LibraryQuizQuestion } from '../types.ts';
|
|
11
|
+
/** One failed (or flagged) question off a paper's latest sitting. */
|
|
12
|
+
export interface MissedRow {
|
|
13
|
+
readonly quiz: LibraryAssessment;
|
|
14
|
+
readonly question: LibraryQuizQuestion;
|
|
15
|
+
readonly at: string;
|
|
16
|
+
}
|
|
17
|
+
/** Every failed, non-voided question — pass the project's quizzes for the cross-station shelf, one station's for its own list. */
|
|
18
|
+
export declare function collectMissed(attempts: readonly LibraryAttempt[], quizzes: readonly LibraryAssessment[]): MissedRow[];
|
|
19
|
+
/** Every question the learner flagged 「这题有问题」 (void — counted for nothing). */
|
|
20
|
+
export declare function collectVoided(attempts: readonly LibraryAttempt[], quizzes: readonly LibraryAssessment[]): MissedRow[];
|
|
21
|
+
//# sourceMappingURL=mistakes.d.ts.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** The rows one predicate collects: each quiz contributes its newest sitting only. */
|
|
2
|
+
function latestSittingRows(attempts, quizzes, keep) {
|
|
3
|
+
const byId = new Map(quizzes.map(quiz => [quiz.id, quiz]));
|
|
4
|
+
const seen = new Set();
|
|
5
|
+
const rows = [];
|
|
6
|
+
for (const attempt of attempts) {
|
|
7
|
+
const quiz = byId.get(attempt.assessmentId ?? '');
|
|
8
|
+
if (quiz === undefined || seen.has(quiz.id))
|
|
9
|
+
continue;
|
|
10
|
+
seen.add(quiz.id);
|
|
11
|
+
for (const verdict of attempt.verdicts) {
|
|
12
|
+
if (!keep(verdict))
|
|
13
|
+
continue;
|
|
14
|
+
const question = quiz.questions?.find(candidate => candidate.id === verdict.task);
|
|
15
|
+
if (question !== undefined)
|
|
16
|
+
rows.push({ quiz, question, at: attempt.at });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return rows;
|
|
20
|
+
}
|
|
21
|
+
/** Every failed, non-voided question — pass the project's quizzes for the cross-station shelf, one station's for its own list. */
|
|
22
|
+
export function collectMissed(attempts, quizzes) {
|
|
23
|
+
return latestSittingRows(attempts, quizzes, verdict => !verdict.pass && verdict.void !== true);
|
|
24
|
+
}
|
|
25
|
+
/** Every question the learner flagged 「这题有问题」 (void — counted for nothing). */
|
|
26
|
+
export function collectVoided(attempts, quizzes) {
|
|
27
|
+
return latestSittingRows(attempts, quizzes, verdict => verdict.void === true);
|
|
28
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The preset-composition visibility of the library right-Sidebar tab type
|
|
3
|
+
* (proposal 2026-10-06-library-learning-mode; the canvas double check,
|
|
4
|
+
* `packages/canvas/src/client/preset-visibility.ts`, inlined per the
|
|
5
|
+
* visibility doc's keep-copies-inline rule). The tab is the entry to a
|
|
6
|
+
* cross-session workspace, so the criterion is NOT "did this session's preset
|
|
7
|
+
* grant the row" alone — it answers "can THIS session reach the library
|
|
8
|
+
* tools", which has two grant paths because the `./tool` entry mounts in two
|
|
9
|
+
* legal shapes:
|
|
10
|
+
*
|
|
11
|
+
* 1. **Profile-root mount** (a deployment that grants every session the
|
|
12
|
+
* library tools): the inventory's `entries` carry an ENABLED
|
|
13
|
+
* `@khorsheed/dsh-library/tool` row → visible for every session.
|
|
14
|
+
* 2. **Preset mount** (the learning-mode recipe: the row named in the target
|
|
15
|
+
* preset's plugins): visible exactly when the CURRENT session's preset
|
|
16
|
+
* composition names that row.
|
|
17
|
+
*
|
|
18
|
+
* Checking only path 2 re-runs the 2026-09-16 canvas incident: the
|
|
19
|
+
* composition data is perfectly readable, the row simply is not IN any
|
|
20
|
+
* preset, so the verdict would be a permanent hide. Every unreadable path
|
|
21
|
+
* therefore fails OPEN (visible): no pluginInventory namespace, a
|
|
22
|
+
* pending/failed RPC, a missing or `broken` preset group, a session with no
|
|
23
|
+
* preset, and the no-session home state (a frame-level surface has no preset
|
|
24
|
+
* input until a session exists — showing there is the neutral state, not a
|
|
25
|
+
* leak).
|
|
26
|
+
*
|
|
27
|
+
* Hidden means NOT registered: the guide enumerates the tab-type registry,
|
|
28
|
+
* opened tabs are stored per session (an ungranted session's layout never
|
|
29
|
+
* held one), and a kind with no registrant renders the host's designed
|
|
30
|
+
* `tab.unavailable` fallback ("a real state, not a defect").
|
|
31
|
+
* @module @khorsheed/dsh-library/client/preset-visibility
|
|
32
|
+
*/
|
|
33
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
34
|
+
import type { SessionId } from '@deepseek-ai/dsh-session/types';
|
|
35
|
+
import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol';
|
|
36
|
+
/** The criterion's row: the library tools/prompt/skill composition entry. */
|
|
37
|
+
export declare const LIBRARY_TOOL_ROW_MODULE = "@khorsheed/dsh-library/tool";
|
|
38
|
+
/**
|
|
39
|
+
* Minimal structural mirror of the official pluginInventory snapshot — the
|
|
40
|
+
* entries slice (root grants) plus the agentPresets slice (preset grants),
|
|
41
|
+
* duck-typed and host-version tolerant.
|
|
42
|
+
*/
|
|
43
|
+
export interface LibraryPluginInventorySnapshot {
|
|
44
|
+
readonly entries?: readonly {
|
|
45
|
+
readonly moduleName: string;
|
|
46
|
+
readonly enabled?: boolean;
|
|
47
|
+
}[];
|
|
48
|
+
readonly agentPresets?: readonly {
|
|
49
|
+
readonly id: string;
|
|
50
|
+
readonly broken?: string;
|
|
51
|
+
readonly rows: readonly {
|
|
52
|
+
readonly moduleName: string;
|
|
53
|
+
readonly enabled?: boolean;
|
|
54
|
+
}[];
|
|
55
|
+
}[];
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The tab's visibility controller: one inventory fetch per page (composition
|
|
59
|
+
* data changes only with preset files, which a reload re-reads anyway),
|
|
60
|
+
* re-evaluated by the toggle on every session switch.
|
|
61
|
+
*/
|
|
62
|
+
export declare class LibraryTabVisibility {
|
|
63
|
+
private readonly ctx;
|
|
64
|
+
/** The inventory answer; null while pending/failed and on namespace-less hosts. */
|
|
65
|
+
private composition;
|
|
66
|
+
private readonly listeners;
|
|
67
|
+
/**
|
|
68
|
+
* @param ctx - client root context.
|
|
69
|
+
* @param pluginInventory - the OFFICIAL namespace, probed through ctx.get
|
|
70
|
+
* (never injected — declaring it would pend the whole client on a host
|
|
71
|
+
* without it, and the criterion must fail open there).
|
|
72
|
+
*/
|
|
73
|
+
constructor(ctx: Context, pluginInventory: {
|
|
74
|
+
list: () => Promise<RemoteResult<LibraryPluginInventorySnapshot>>;
|
|
75
|
+
} | undefined);
|
|
76
|
+
show(sessionId: SessionId | undefined): boolean;
|
|
77
|
+
/**
|
|
78
|
+
* The binding chip's stricter question: does THIS session hold the library
|
|
79
|
+
* tools? Unlike the tab (fail-open), an unanswered inventory or an unknown
|
|
80
|
+
* preset answers no — a chip in every session's header is noise, and the
|
|
81
|
+
* tab stays reachable either way.
|
|
82
|
+
*/
|
|
83
|
+
grants(sessionId: SessionId | undefined): boolean;
|
|
84
|
+
/** Subscribe to criterion-input changes (session switches, the inventory answer). */
|
|
85
|
+
subscribe(listener: () => void): () => void;
|
|
86
|
+
private notify;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The tab type's registration toggle (the room template): registers exactly
|
|
90
|
+
* while the criterion passes for the current session and disposes otherwise.
|
|
91
|
+
* Unlike the keyed-slot consumers, the sidebarRightTabs registry is a direct
|
|
92
|
+
* inject — no slot-declaration wait, so the toggle is ready immediately.
|
|
93
|
+
*/
|
|
94
|
+
export declare class RegistrationToggle {
|
|
95
|
+
private readonly register;
|
|
96
|
+
private readonly show;
|
|
97
|
+
private dispose;
|
|
98
|
+
private ready;
|
|
99
|
+
/**
|
|
100
|
+
* @param register - mounts the entry and returns its disposer.
|
|
101
|
+
* @param show - the current decision (re-read on every sync).
|
|
102
|
+
*/
|
|
103
|
+
constructor(register: () => () => void, show: () => boolean);
|
|
104
|
+
/** The registry is reachable — decisions may register now. */
|
|
105
|
+
setReady(ready: boolean): void;
|
|
106
|
+
/** Re-evaluate: register while shown and ready, dispose otherwise. */
|
|
107
|
+
sync(): void;
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=preset-visibility.d.ts.map
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/** The criterion's row: the library tools/prompt/skill composition entry. */
|
|
2
|
+
export const LIBRARY_TOOL_ROW_MODULE = '@khorsheed/dsh-library/tool';
|
|
3
|
+
/** The current session's agent preset, the dual read per host line. */
|
|
4
|
+
function presetOf(row) {
|
|
5
|
+
const value = row
|
|
6
|
+
?.projectionValues?.agentPreset
|
|
7
|
+
?? row?.agentPreset;
|
|
8
|
+
return typeof value === 'string' ? value : undefined;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The tab's visibility controller: one inventory fetch per page (composition
|
|
12
|
+
* data changes only with preset files, which a reload re-reads anyway),
|
|
13
|
+
* re-evaluated by the toggle on every session switch.
|
|
14
|
+
*/
|
|
15
|
+
export class LibraryTabVisibility {
|
|
16
|
+
ctx;
|
|
17
|
+
/** The inventory answer; null while pending/failed and on namespace-less hosts. */
|
|
18
|
+
composition = null;
|
|
19
|
+
listeners = new Set();
|
|
20
|
+
/**
|
|
21
|
+
* @param ctx - client root context.
|
|
22
|
+
* @param pluginInventory - the OFFICIAL namespace, probed through ctx.get
|
|
23
|
+
* (never injected — declaring it would pend the whole client on a host
|
|
24
|
+
* without it, and the criterion must fail open there).
|
|
25
|
+
*/
|
|
26
|
+
constructor(ctx, pluginInventory) {
|
|
27
|
+
this.ctx = ctx;
|
|
28
|
+
if (pluginInventory !== undefined) {
|
|
29
|
+
void pluginInventory.list().then(result => {
|
|
30
|
+
if (!result.ok)
|
|
31
|
+
return;
|
|
32
|
+
this.composition = result.value ?? null;
|
|
33
|
+
this.notify();
|
|
34
|
+
}).catch(() => { });
|
|
35
|
+
}
|
|
36
|
+
// Session switches re-decide the tab type's registration (the guide
|
|
37
|
+
// enumerates registrations, not components).
|
|
38
|
+
ctx.effect(() => ctx.sessions.list.subscribe(() => { this.notify(); }), 'library: tab visibility session follower');
|
|
39
|
+
}
|
|
40
|
+
show(sessionId) {
|
|
41
|
+
if (this.composition === null)
|
|
42
|
+
return true;
|
|
43
|
+
// Path 1: a root-mounted tool row grants the tools to every session —
|
|
44
|
+
// a deployment-level constant, checked before any per-session question.
|
|
45
|
+
if (this.composition.entries?.some(row => row.moduleName === LIBRARY_TOOL_ROW_MODULE && row.enabled === true) === true)
|
|
46
|
+
return true;
|
|
47
|
+
if (sessionId === undefined)
|
|
48
|
+
return true;
|
|
49
|
+
const preset = presetOf(this.ctx.sessions.list.getSnapshot().byId[sessionId]);
|
|
50
|
+
if (preset === undefined)
|
|
51
|
+
return true;
|
|
52
|
+
const group = this.composition.agentPresets?.find(candidate => candidate.id === preset);
|
|
53
|
+
if (group === undefined || group.broken !== undefined)
|
|
54
|
+
return true;
|
|
55
|
+
// Path 2: the preset-mounted row grants this session.
|
|
56
|
+
return group.rows.some(row => row.moduleName === LIBRARY_TOOL_ROW_MODULE && row.enabled !== false);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The binding chip's stricter question: does THIS session hold the library
|
|
60
|
+
* tools? Unlike the tab (fail-open), an unanswered inventory or an unknown
|
|
61
|
+
* preset answers no — a chip in every session's header is noise, and the
|
|
62
|
+
* tab stays reachable either way.
|
|
63
|
+
*/
|
|
64
|
+
grants(sessionId) {
|
|
65
|
+
if (this.composition === null || sessionId === undefined)
|
|
66
|
+
return false;
|
|
67
|
+
if (this.composition.entries?.some(row => row.moduleName === LIBRARY_TOOL_ROW_MODULE && row.enabled === true) === true)
|
|
68
|
+
return true;
|
|
69
|
+
const preset = presetOf(this.ctx.sessions.list.getSnapshot().byId[sessionId]);
|
|
70
|
+
const group = preset === undefined ? undefined : this.composition.agentPresets?.find(candidate => candidate.id === preset);
|
|
71
|
+
return group?.rows.some(row => row.moduleName === LIBRARY_TOOL_ROW_MODULE && row.enabled !== false) === true;
|
|
72
|
+
}
|
|
73
|
+
/** Subscribe to criterion-input changes (session switches, the inventory answer). */
|
|
74
|
+
subscribe(listener) {
|
|
75
|
+
this.listeners.add(listener);
|
|
76
|
+
return () => { this.listeners.delete(listener); };
|
|
77
|
+
}
|
|
78
|
+
notify() {
|
|
79
|
+
for (const listener of this.listeners)
|
|
80
|
+
listener();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The tab type's registration toggle (the room template): registers exactly
|
|
85
|
+
* while the criterion passes for the current session and disposes otherwise.
|
|
86
|
+
* Unlike the keyed-slot consumers, the sidebarRightTabs registry is a direct
|
|
87
|
+
* inject — no slot-declaration wait, so the toggle is ready immediately.
|
|
88
|
+
*/
|
|
89
|
+
export class RegistrationToggle {
|
|
90
|
+
register;
|
|
91
|
+
show;
|
|
92
|
+
dispose;
|
|
93
|
+
ready = false;
|
|
94
|
+
/**
|
|
95
|
+
* @param register - mounts the entry and returns its disposer.
|
|
96
|
+
* @param show - the current decision (re-read on every sync).
|
|
97
|
+
*/
|
|
98
|
+
constructor(register, show) {
|
|
99
|
+
this.register = register;
|
|
100
|
+
this.show = show;
|
|
101
|
+
}
|
|
102
|
+
/** The registry is reachable — decisions may register now. */
|
|
103
|
+
setReady(ready) {
|
|
104
|
+
this.ready = ready;
|
|
105
|
+
if (!ready && this.dispose !== undefined) {
|
|
106
|
+
this.dispose();
|
|
107
|
+
this.dispose = undefined;
|
|
108
|
+
}
|
|
109
|
+
this.sync();
|
|
110
|
+
}
|
|
111
|
+
/** Re-evaluate: register while shown and ready, dispose otherwise. */
|
|
112
|
+
sync() {
|
|
113
|
+
if (!this.ready)
|
|
114
|
+
return;
|
|
115
|
+
if (this.show()) {
|
|
116
|
+
this.dispose ??= this.register();
|
|
117
|
+
}
|
|
118
|
+
else if (this.dispose !== undefined) {
|
|
119
|
+
this.dispose();
|
|
120
|
+
this.dispose = undefined;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quiz drafts: the learner's in-progress answers survive leaving the paper
|
|
3
|
+
* (back to the station, a closed sidebar, a reload). Stored in localStorage
|
|
4
|
+
* per quiz id — a draft is local by definition (never ledgered until 交卷),
|
|
5
|
+
* so no host write is involved; the entry clears when the sitting lands.
|
|
6
|
+
*
|
|
7
|
+
* @module @khorsheed/dsh-library/client/quiz-drafts
|
|
8
|
+
*/
|
|
9
|
+
/** One question's saved state in the draft. */
|
|
10
|
+
export interface QuizDraftAnswer {
|
|
11
|
+
readonly given: string;
|
|
12
|
+
/** Judged on the spot (choice / fill); open questions stay undefined. */
|
|
13
|
+
readonly pass?: boolean;
|
|
14
|
+
readonly void?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/** One paper's in-progress sitting. */
|
|
17
|
+
export interface QuizDraft {
|
|
18
|
+
readonly answers: Readonly<Record<string, QuizDraftAnswer>>;
|
|
19
|
+
/** Open-answer text not yet submitted. */
|
|
20
|
+
readonly drafts: Readonly<Record<string, string>>;
|
|
21
|
+
readonly at: string;
|
|
22
|
+
}
|
|
23
|
+
/** The saved sitting of one paper, when one parses. */
|
|
24
|
+
export declare function loadQuizDraft(quizId: string): QuizDraft | undefined;
|
|
25
|
+
/** Save the sitting (every answer / keystroke; the write is a full replace). */
|
|
26
|
+
export declare function saveQuizDraft(quizId: string, draft: QuizDraft): void;
|
|
27
|
+
/** Drop the draft (the sitting landed, or the paper was superseded under it). */
|
|
28
|
+
export declare function clearQuizDraft(quizId: string): void;
|
|
29
|
+
//# sourceMappingURL=quiz-drafts.d.ts.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quiz drafts: the learner's in-progress answers survive leaving the paper
|
|
3
|
+
* (back to the station, a closed sidebar, a reload). Stored in localStorage
|
|
4
|
+
* per quiz id — a draft is local by definition (never ledgered until 交卷),
|
|
5
|
+
* so no host write is involved; the entry clears when the sitting lands.
|
|
6
|
+
*
|
|
7
|
+
* @module @khorsheed/dsh-library/client/quiz-drafts
|
|
8
|
+
*/
|
|
9
|
+
function keyOf(quizId) {
|
|
10
|
+
return `dsh-library:quiz-draft:${quizId}`;
|
|
11
|
+
}
|
|
12
|
+
/** The saved sitting of one paper, when one parses. */
|
|
13
|
+
export function loadQuizDraft(quizId) {
|
|
14
|
+
let raw;
|
|
15
|
+
try {
|
|
16
|
+
raw = localStorage.getItem(keyOf(quizId));
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return undefined; // storage blocked (private mode) — a draft is a nicety, never a failure
|
|
20
|
+
}
|
|
21
|
+
if (raw === null)
|
|
22
|
+
return undefined;
|
|
23
|
+
try {
|
|
24
|
+
const parsed = JSON.parse(raw);
|
|
25
|
+
if (typeof parsed !== 'object' || parsed === null || typeof parsed.answers !== 'object' || parsed.answers === null)
|
|
26
|
+
return undefined;
|
|
27
|
+
return parsed;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** Save the sitting (every answer / keystroke; the write is a full replace). */
|
|
34
|
+
export function saveQuizDraft(quizId, draft) {
|
|
35
|
+
try {
|
|
36
|
+
localStorage.setItem(keyOf(quizId), JSON.stringify(draft));
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// Quota or privacy mode — the sitting stays in memory for this mount.
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** Drop the draft (the sitting landed, or the paper was superseded under it). */
|
|
43
|
+
export function clearQuizDraft(quizId) {
|
|
44
|
+
try {
|
|
45
|
+
localStorage.removeItem(keyOf(quizId));
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// As above.
|
|
49
|
+
}
|
|
50
|
+
}
|