dsh-plugin-lookatstudy 0.24.0 → 0.25.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.
- package/lib/client.js +85 -20
- package/lib/client.js.map +1 -1
- package/lib/index.mjs +68 -26
- package/package.json +1 -1
package/lib/index.mjs
CHANGED
|
@@ -163,6 +163,7 @@ const ZH = {
|
|
|
163
163
|
"toast.close": "关闭",
|
|
164
164
|
"composer.busy": "导师正在回复…",
|
|
165
165
|
"rail.empty.demo": "导入示例课程",
|
|
166
|
+
"import.session.title": "课程导入",
|
|
166
167
|
"import.cta": "导入新课程",
|
|
167
168
|
"import.tab.url": "URL",
|
|
168
169
|
"import.tab.md": "MD",
|
|
@@ -265,7 +266,6 @@ const ZH = {
|
|
|
265
266
|
"chat.reasoning": "思考过程({n} 字)",
|
|
266
267
|
"chat.think": "思考",
|
|
267
268
|
"langview.original": "原文",
|
|
268
|
-
"langview.bilingual": "对照",
|
|
269
269
|
"langview.translation": "译文",
|
|
270
270
|
"msg.speak": "朗读",
|
|
271
271
|
"note.pin": "置顶笔记",
|
|
@@ -534,6 +534,7 @@ const EN = {
|
|
|
534
534
|
"toast.close": "Close",
|
|
535
535
|
"composer.busy": "The tutor is replying…",
|
|
536
536
|
"rail.empty.demo": "Import the demo course",
|
|
537
|
+
"import.session.title": "Course import",
|
|
537
538
|
"import.cta": "Import a new course",
|
|
538
539
|
"import.tab.url": "URL",
|
|
539
540
|
"import.tab.md": "MD",
|
|
@@ -636,7 +637,6 @@ const EN = {
|
|
|
636
637
|
"chat.reasoning": "Thinking ({n} chars)",
|
|
637
638
|
"chat.think": "Think",
|
|
638
639
|
"langview.original": "Original",
|
|
639
|
-
"langview.bilingual": "Both",
|
|
640
640
|
"langview.translation": "Translation",
|
|
641
641
|
"msg.speak": "Speak",
|
|
642
642
|
"note.pin": "Pin note",
|
|
@@ -836,21 +836,6 @@ function escapeHtml(text) {
|
|
|
836
836
|
function inline(escaped) {
|
|
837
837
|
return escaped.replace(/`([^`]+)`/g, "<code>$1</code>").replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/\*([^*]+)\*/g, "<em>$1</em>").replace(/!\[([^\]]*)\]\((https?:\/\/[^)\s]+|data:image\/[^)\s]+|\/lookatstudy\/api\/attachment\/[^)\s]+)\)/g, (_m, alt, src) => `<img src="${src}" alt="${alt}" loading="lazy">`).replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g, "<a href=\"$2\" target=\"_blank\" rel=\"noreferrer\">$1</a>");
|
|
838
838
|
}
|
|
839
|
-
/** Interleave one body with its translation, paragraph by paragraph (upstream
|
|
840
|
-
* translation-layout "microsoft" semantics: original paragraph then its
|
|
841
|
-
* translation as a quote, tight pairing for bilingual reading; length
|
|
842
|
-
* mismatches append the remainder in order). Pure. */
|
|
843
|
-
function renderBilingual(bodyMd, translationMd) {
|
|
844
|
-
const par = (md) => md.split(/\n\s*\n/).map((p) => p.trim()).filter((p) => p !== "");
|
|
845
|
-
const a = par(bodyMd);
|
|
846
|
-
const b = par(translationMd);
|
|
847
|
-
const out = [];
|
|
848
|
-
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
849
|
-
if (a[i] !== void 0) out.push(a[i]);
|
|
850
|
-
if (b[i] !== void 0) out.push(`> ${b[i].split("\n").join("\n> ")}`);
|
|
851
|
-
}
|
|
852
|
-
return out.join("\n\n");
|
|
853
|
-
}
|
|
854
839
|
/** True when the line opens a GFM table row (pipes with a delimiter row next). */
|
|
855
840
|
function isTableRow(line) {
|
|
856
841
|
return line.trim().startsWith("|") && line.trim().endsWith("|") && line.includes("|", 1);
|
|
@@ -2353,7 +2338,18 @@ function recordAnswer(state, lessonId, correct, concept, now) {
|
|
|
2353
2338
|
function setCourseThreadScope(state, courseId, scope) {
|
|
2354
2339
|
const course = state.courses.find((c) => c.id === courseId);
|
|
2355
2340
|
if (course === void 0) throw new Error(`lookatstudy-plugin: unknown course id ${JSON.stringify(courseId)}`);
|
|
2341
|
+
if ((course.threadScope === "course" ? "course" : "lesson") === scope) return;
|
|
2356
2342
|
course.threadScope = scope;
|
|
2343
|
+
const courseKey = `course:${courseId}`;
|
|
2344
|
+
const focusLessonId = state.focus?.lessonId ?? null;
|
|
2345
|
+
if (focusLessonId === null || !course.sections.some((s) => s.lessons.some((l) => l.id === focusLessonId))) return;
|
|
2346
|
+
const lessonKey = focusLessonId;
|
|
2347
|
+
const src = scope === "course" ? state.lessonThreads[lessonKey] : state.lessonThreads[courseKey];
|
|
2348
|
+
const dst = scope === "course" ? state.lessonThreads[courseKey] : state.lessonThreads[lessonKey];
|
|
2349
|
+
if (src === void 0 || src.threads.length === 0) return;
|
|
2350
|
+
if (dst !== void 0 && (dst.active !== null || dst.threads.length > 0)) return;
|
|
2351
|
+
state.lessonThreads[scope === "course" ? courseKey : lessonKey] = src;
|
|
2352
|
+
delete state.lessonThreads[scope === "course" ? lessonKey : courseKey];
|
|
2357
2353
|
}
|
|
2358
2354
|
/**
|
|
2359
2355
|
* Resolve the thread-group map key a send from `lessonId` belongs to — the
|
|
@@ -3125,7 +3121,6 @@ function workbenchState(state, now) {
|
|
|
3125
3121
|
markdown: ref.lesson.body,
|
|
3126
3122
|
speechText: normalizeSpeechText(normalizeMathNotation(ref.lesson.body)),
|
|
3127
3123
|
...ref.lesson.translation !== void 0 ? {
|
|
3128
|
-
bilingualHtml: renderMarkdown(normalizeMathNotation(renderBilingual(ref.lesson.body, ref.lesson.translation))),
|
|
3129
3124
|
translationHtml: renderMarkdown(normalizeMathNotation(ref.lesson.translation)),
|
|
3130
3125
|
translationLang: ref.lesson.translationLang ?? ""
|
|
3131
3126
|
} : {}
|
|
@@ -6586,6 +6581,50 @@ function validateDesign(design, validFiles) {
|
|
|
6586
6581
|
droppedLessons
|
|
6587
6582
|
};
|
|
6588
6583
|
}
|
|
6584
|
+
/**
|
|
6585
|
+
* Heading similarity (word-level overlap) — the translation misalignment
|
|
6586
|
+
* guard. Ported verbatim from upstream import-pipeline.ts (their audit F16,
|
|
6587
|
+
* 2026-09-13): ordinal alignment assumes both heading lists share structure;
|
|
6588
|
+
* a machine-translated file with a dropped header shifts every ordinal, and
|
|
6589
|
+
* A's translation would silently land on B's node — overlap too low skips the
|
|
6590
|
+
* segment (宁缺毋错). Empty titles cannot be checked and pass conservatively.
|
|
6591
|
+
*/
|
|
6592
|
+
function headingsSimilar(a, b) {
|
|
6593
|
+
const norm = (s) => s.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, " ").trim().split(/\s+/).filter(Boolean);
|
|
6594
|
+
const A = norm(a);
|
|
6595
|
+
const B = norm(b);
|
|
6596
|
+
if (A.length === 0 || B.length === 0) return true;
|
|
6597
|
+
if (A.join(" ") === B.join(" ")) return true;
|
|
6598
|
+
const setB = new Set(B);
|
|
6599
|
+
return A.filter((w) => setB.has(w)).length / A.length >= .3;
|
|
6600
|
+
}
|
|
6601
|
+
/**
|
|
6602
|
+
* Replace a translation body's image references BY POSITION with the
|
|
6603
|
+
* original lesson's images (upstream method, import-pipeline.ts): translated
|
|
6604
|
+
* figure i ← original figure i (the original's refs are already absolute
|
|
6605
|
+
* CDN URLs or inlined data URLs); extras beyond the original count drop
|
|
6606
|
+
* (machine translation adding figures is rare — dropping is cleanest);
|
|
6607
|
+
* HTML imgs convert to markdown. One unified regex pass so HTML→markdown
|
|
6608
|
+
* conversion is never re-scanned with a shared index.
|
|
6609
|
+
*/
|
|
6610
|
+
function replaceImagesByPosition(content, originalImgs) {
|
|
6611
|
+
let imgIdx = 0;
|
|
6612
|
+
return content.replace(/!\[([^\]]*)\]\(([^)]+)\)|<img[^>]+src=["']([^"']+)["'][^>]*>/gi, (match, mdAlt, _mdSrc, htmlSrc) => {
|
|
6613
|
+
if (imgIdx >= originalImgs.length) return "";
|
|
6614
|
+
const replacement = originalImgs[imgIdx++];
|
|
6615
|
+
if (htmlSrc !== void 0) return `["']/i.exec(match)?.[1] ?? ""}](${replacement})`;
|
|
6616
|
+
return ``;
|
|
6617
|
+
});
|
|
6618
|
+
}
|
|
6619
|
+
/** The original lesson body's image refs in order (the position-mapping source). */
|
|
6620
|
+
function originalImagesOf(body) {
|
|
6621
|
+
const out = [];
|
|
6622
|
+
for (const m of body.matchAll(/!\[([^\]]*)\]\(([^)]+)\)|<img[^>]+src=["']([^"']+)["'][^>]*>/gi)) {
|
|
6623
|
+
const src = m[2] ?? m[3] ?? "";
|
|
6624
|
+
if (src !== "") out.push(src);
|
|
6625
|
+
}
|
|
6626
|
+
return out;
|
|
6627
|
+
}
|
|
6589
6628
|
/** GitHub-style anchor slug for ParsedLesson/ParsedSection anchors. */
|
|
6590
6629
|
function slugAnchor(title) {
|
|
6591
6630
|
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
@@ -6622,13 +6661,6 @@ function buildCourseFromDesign(courseTitle, validated, contents, translations) {
|
|
|
6622
6661
|
const titleIndex = lesson.anchor === null ? -1 : findTitleIndex(headings, lesson.anchor);
|
|
6623
6662
|
const isFirst = !firstLessonSeen.has(lesson.file);
|
|
6624
6663
|
firstLessonSeen.add(lesson.file);
|
|
6625
|
-
const pair = translations?.get(lesson.file);
|
|
6626
|
-
let translation;
|
|
6627
|
-
if (pair !== void 0) {
|
|
6628
|
-
const tHeadings = extractHeadings(pair.content);
|
|
6629
|
-
const tIndex = lesson.anchor === null ? -1 : findTitleIndex(tHeadings, lesson.anchor);
|
|
6630
|
-
translation = sliceLessonBody(pair.content, tHeadings, tIndex, isFirst);
|
|
6631
|
-
}
|
|
6632
6664
|
const body = sliceLessonBody(content, headings, titleIndex, isFirst);
|
|
6633
6665
|
if (body.length > 8e3 && splittableHeadingCount(headings, titleIndex) > 0) pacingViolations.push({
|
|
6634
6666
|
title: lesson.title,
|
|
@@ -6636,6 +6668,16 @@ function buildCourseFromDesign(courseTitle, validated, contents, translations) {
|
|
|
6636
6668
|
chars: body.length,
|
|
6637
6669
|
splittable: splittableHeadingCount(headings, titleIndex)
|
|
6638
6670
|
});
|
|
6671
|
+
const pair = translations?.get(lesson.file);
|
|
6672
|
+
let translation;
|
|
6673
|
+
if (pair !== void 0) {
|
|
6674
|
+
const tHeadings = extractHeadings(pair.content);
|
|
6675
|
+
let tIndex = titleIndex;
|
|
6676
|
+
let keep = true;
|
|
6677
|
+
if (lesson.anchor === null || titleIndex === -1) tIndex = -1;
|
|
6678
|
+
else if (titleIndex >= tHeadings.length || !headingsSimilar(lesson.title, tHeadings[titleIndex].title)) keep = false;
|
|
6679
|
+
if (keep) translation = replaceImagesByPosition(sliceLessonBody(pair.content, tHeadings, tIndex, isFirst), originalImagesOf(body));
|
|
6680
|
+
}
|
|
6639
6681
|
return {
|
|
6640
6682
|
title: lesson.title,
|
|
6641
6683
|
anchor: slugAnchor(lesson.title),
|
|
@@ -7366,7 +7408,7 @@ function studyTools(store, deps = {}) {
|
|
|
7366
7408
|
};
|
|
7367
7409
|
const designOrImportedRender = (_args, value) => [{
|
|
7368
7410
|
type: "text",
|
|
7369
|
-
text: value.status === "design_required" ? pendingDesign === null ? "Course design required — the brief is no longer pending; call the import tool again to re-fetch it." : typeof value.designSeq === "number" && value.designSeq !== designSeq ? "This design brief is STALE — a newer import replaced the pending design. Call the import tool again for the fresh brief; do not design against this one." : renderDesignBrief(pendingDesign, pendingPart) : `Imported course “${value.title}” (${value.sections} sections, ${value.lessons} lessons). First lesson: “${value.firstLessonTitle}” (id ${value.firstLessonId}).`
|
|
7411
|
+
text: value.status === "design_required" ? pendingDesign === null ? "Course design required — the brief is no longer pending; call the import tool again to re-fetch it." : typeof value.designSeq === "number" && value.designSeq !== designSeq ? "This design brief is STALE — a newer import replaced the pending design. Call the import tool again for the fresh brief; do not design against this one." : renderDesignBrief(pendingDesign, pendingPart) + (store.get().interfaceLang === void 0 && (pendingDesign.availableLangs?.length ?? 0) > 0 ? "\nNote: the learner's interface language is not recorded — when applying, pass translationLang explicitly as the language the learner writes to you in (e.g. \"zh-CN\" for Chinese), so the translation mirrors pair." : "") : `Imported course “${value.title}” (${value.sections} sections, ${value.lessons} lessons). First lesson: “${value.firstLessonTitle}” (id ${value.firstLessonId}).`
|
|
7370
7412
|
}];
|
|
7371
7413
|
const designOrImportedPresentationMeta = (_args, value) => value.status === "design_required" ? designBriefLines(value) : importLines(value);
|
|
7372
7414
|
const designOrImportedPresent = { presentResult: (_args, result) => ({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-lookatstudy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.1",
|
|
4
4
|
"packageManager": "pnpm@11.7.0",
|
|
5
5
|
"description": "Turn any markdown, local folder, or GitHub learning repo into a guided course inside DeepSeek Harness: gated skill-tree progression, BKT mastery tracking, SM-2 spaced repetition.",
|
|
6
6
|
"type": "module",
|