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 CHANGED
@@ -362,8 +362,8 @@ window.__ModuleLoader__.load({
362
362
  .lks-ui .lks14-railbody{position:absolute;inset:0;overflow:hidden;z-index:10}
363
363
  .lks-ui .lks14-railtrack{display:flex;height:100%;width:200%;transition:transform .3s var(--ease-out-expo)}
364
364
  .lks-ui .lks14-railpane{width:50%;height:100%;position:relative}
365
- .lks-ui .lks14-railscroll{height:100%;overflow-y:auto;overflow-x:hidden;padding:112px 8px 16px}
366
- .lks-ui .lks14-railpane-import{height:100%;overflow-y:auto;padding:64px 12px 12px}
365
+ .lks-ui .lks14-railscroll{height:100%;overflow-y:auto;overflow-x:hidden;padding:112px 8px 16px;box-sizing:border-box}
366
+ .lks-ui .lks14-railpane-import{height:100%;overflow-y:auto;padding:64px 12px 12px;box-sizing:border-box}
367
367
  .lks-ui .lks14-raillist{display:flex;flex-direction:column;gap:8px;margin-bottom:10px}
368
368
  .lks-ui .lks14-railcourse{text-align:left;padding:10px 12px;border-radius:12px;background:rgb(255 255 255/0.05);display:flex;flex-direction:column;gap:2px}
369
369
  .lks-ui .lks14-railcourse:hover{background:rgb(255 255 255/0.1)}
@@ -2258,6 +2258,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
2258
2258
  "toast.close": "关闭",
2259
2259
  "composer.busy": "导师正在回复…",
2260
2260
  "rail.empty.demo": "导入示例课程",
2261
+ "import.session.title": "课程导入",
2261
2262
  "import.cta": "导入新课程",
2262
2263
  "import.tab.url": "URL",
2263
2264
  "import.tab.md": "MD",
@@ -2360,7 +2361,6 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
2360
2361
  "chat.reasoning": "思考过程({n} 字)",
2361
2362
  "chat.think": "思考",
2362
2363
  "langview.original": "原文",
2363
- "langview.bilingual": "对照",
2364
2364
  "langview.translation": "译文",
2365
2365
  "msg.speak": "朗读",
2366
2366
  "note.pin": "置顶笔记",
@@ -2629,6 +2629,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
2629
2629
  "toast.close": "Close",
2630
2630
  "composer.busy": "The tutor is replying…",
2631
2631
  "rail.empty.demo": "Import the demo course",
2632
+ "import.session.title": "Course import",
2632
2633
  "import.cta": "Import a new course",
2633
2634
  "import.tab.url": "URL",
2634
2635
  "import.tab.md": "MD",
@@ -2731,7 +2732,6 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
2731
2732
  "chat.reasoning": "Thinking ({n} chars)",
2732
2733
  "chat.think": "Think",
2733
2734
  "langview.original": "Original",
2734
- "langview.bilingual": "Both",
2735
2735
  "langview.translation": "Translation",
2736
2736
  "msg.speak": "Speak",
2737
2737
  "note.pin": "Pin note",
@@ -3747,6 +3747,17 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
3747
3747
  })()).replaceAll("**", "");
3748
3748
  }
3749
3749
  /**
3750
+ * The dedicated import session's host-list title (0.24.1): imports mint a
3751
+ * course-less one-shot session that never rides a lesson thread — a uniform
3752
+ * label keeps the host session list legible instead of auto-title noise.
3753
+ * Label slugified to one line, whole title capped. Pure.
3754
+ */
3755
+ function importSessionTitle(label, now, t = tr) {
3756
+ const flat = label.replace(/[\u0000-\u001f\u007f]+/g, " ").replace(/\s+/g, " ").trim().slice(0, 24);
3757
+ const date = `${String(now.getFullYear()).slice(2)}.${String(now.getMonth() + 1).padStart(2, "0")}.${String(now.getDate()).padStart(2, "0")}`;
3758
+ return `${flat === "" ? t("import.session.title") : flat} · ${date}`;
3759
+ }
3760
+ /**
3750
3761
  * Tooltip text for one course-tree status glyph. Pure (the translator is an
3751
3762
  * optional parameter defaulting to the active one, evaluated per call).
3752
3763
  */
@@ -7719,11 +7730,19 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
7719
7730
  const { data, activate, setMode, setFocus, searchLessons, deleteCourse, deleteNote, bindLessonSession, lessonThreadOp, setCourseScope, uploadAttachment, setInterfaceLang } = useStudy();
7720
7731
  (0, react.useEffect)(() => {
7721
7732
  const read = () => {
7722
- const lang = document.documentElement.lang;
7733
+ let lang = document.documentElement.lang;
7734
+ if (!/^[a-z]{2,3}(-[A-Za-z0-9]+)*$/.test(lang)) lang = navigator.language;
7723
7735
  if (/^[a-z]{2,3}(-[A-Za-z0-9]+)*$/.test(lang)) setInterfaceLang(lang).catch(() => {});
7724
7736
  };
7725
7737
  read();
7726
- return ctx.locale?.subscribe(read);
7738
+ const t1 = setTimeout(read, 600);
7739
+ const t2 = setTimeout(read, 2400);
7740
+ const off = ctx.locale?.subscribe(read);
7741
+ return () => {
7742
+ clearTimeout(t1);
7743
+ clearTimeout(t2);
7744
+ off?.();
7745
+ };
7727
7746
  }, []);
7728
7747
  (0, react.useEffect)(() => {
7729
7748
  wireCodeBlockCopy();
@@ -7925,7 +7944,43 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
7925
7944
  } else celebrate(ev.kind);
7926
7945
  prevCelebrationRef.current = snapshot;
7927
7946
  }, [data]);
7928
- const boundId = boundSessionOf(lesson !== null ? lesson.lessonId : null) ?? threadGroupOf(lesson !== null ? lesson.lessonId : null)?.active ?? (lesson !== null ? data?.lessonSessions[lesson.lessonId] ?? null : null);
7947
+ const [importSessionId, setImportSessionId] = (0, react.useState)(null);
7948
+ const sendImport = async (prompt, label) => {
7949
+ if (data?.active !== true) await activate(true);
7950
+ let sessionId = importSessionId;
7951
+ if (sessionId === null || !sessionKnown(ctx, sessionId)) {
7952
+ const area = await fetch("/lookatstudy/api/study-workspace");
7953
+ if (!area.ok) throw new Error(`study area unavailable (HTTP ${String(area.status)})`);
7954
+ const { path } = await area.json();
7955
+ const workspace = await ctx.workspaces.create({ path });
7956
+ sessionId = await ctx.sessions.create({ workspaceId: workspace.workspaceId });
7957
+ setImportSessionId(sessionId);
7958
+ }
7959
+ const shell = PANEL_SHELL.current;
7960
+ if (shell !== null) shell.suppressHandBack(() => {
7961
+ ctx.sessions.open(sessionId);
7962
+ });
7963
+ else ctx.sessions.open(sessionId);
7964
+ const actx = ctx.sessions.scope(sessionId);
7965
+ const face = actx === void 0 ? void 0 : ctx.sessions.sessionOf(actx);
7966
+ if (face === void 0) throw new Error("import session is not addressable yet");
7967
+ activeFace.current = face;
7968
+ try {
7969
+ await face.rename?.(importSessionTitle(label, /* @__PURE__ */ new Date()));
7970
+ } catch {}
7971
+ const result = await face.prompt([{
7972
+ type: "text",
7973
+ text: prompt
7974
+ }], "queue");
7975
+ if (!result.ok) throw new Error(`prompt rejected: ${result.error.code}: ${result.error.message}`);
7976
+ };
7977
+ /** Forget the import session — called from every funnel exit. Unpinning
7978
+ * re-runs the binding effect, which restages the focused lesson's thread. */
7979
+ const endImportSession = (0, react.useCallback)(() => {
7980
+ setImportSessionId(null);
7981
+ activeFace.current = null;
7982
+ }, []);
7983
+ const boundId = importSessionId ?? boundSessionOf(lesson !== null ? lesson.lessonId : null) ?? threadGroupOf(lesson !== null ? lesson.lessonId : null)?.active ?? (lesson !== null ? data?.lessonSessions[lesson.lessonId] ?? null : null);
7929
7984
  const nudged = (0, react.useRef)(false);
7930
7985
  (0, react.useEffect)(() => {
7931
7986
  if (nudged.current || (data?.dueCount ?? 0) === 0) return;
@@ -8167,6 +8222,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
8167
8222
  }, [send]);
8168
8223
  const switchLessonThread = (sessionId) => {
8169
8224
  if (lesson === null) return;
8225
+ if (importSessionId !== null) return;
8170
8226
  if (localBound.current?.lessonId === lesson.lessonId && localBound.current.sessionId === sessionId) return;
8171
8227
  localBound.current = {
8172
8228
  lessonId: lesson.lessonId,
@@ -8181,6 +8237,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
8181
8237
  };
8182
8238
  const startNewThread = () => {
8183
8239
  if (lesson === null) return;
8240
+ if (importSessionId !== null) return;
8184
8241
  localBound.current = null;
8185
8242
  bindLessonSession(lesson.lessonId, null);
8186
8243
  setRows([]);
@@ -8243,6 +8300,8 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
8243
8300
  turnActive: feedGen,
8244
8301
  importProgress,
8245
8302
  stop,
8303
+ sendImport,
8304
+ endImportSession,
8246
8305
  paletteBus
8247
8306
  }), (0, react.createElement)(PaneResizeHandle, {
8248
8307
  side: "rail",
@@ -8654,7 +8713,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
8654
8713
  PANEL_SHELL.current = shell;
8655
8714
  }
8656
8715
  /** 左栏:course picker, tree, review box, import. */
8657
- function CourseRail({ data, activate, setFocus, searchLessons, deleteCourse, send, onJumped, streamingLessonId, turnActive, importProgress, stop, paletteBus }) {
8716
+ function CourseRail({ data, activate, setFocus, searchLessons, deleteCourse, send, onJumped, streamingLessonId, turnActive, importProgress, stop, paletteBus, sendImport, endImportSession }) {
8658
8717
  const [selectedCourse, setSelectedCourse] = (0, react.useState)("");
8659
8718
  const [query, setQuery] = (0, react.useState)("");
8660
8719
  const [searchRows, setSearchRows] = (0, react.useState)([]);
@@ -8694,13 +8753,21 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
8694
8753
  startedAt: Date.now(),
8695
8754
  baselineIds: new Set((data?.courses ?? []).map((c) => c.courseId))
8696
8755
  });
8697
- send(prompt);
8756
+ sendImport(prompt, label).catch((err) => {
8757
+ setImportJob(null);
8758
+ endImportSession();
8759
+ setImportResult({
8760
+ ok: false,
8761
+ msg: err instanceof Error ? err.message : String(err)
8762
+ });
8763
+ });
8698
8764
  };
8699
8765
  (0, react.useEffect)(() => {
8700
8766
  if (importJob === null || data === null) return;
8701
8767
  const fresh = data.courses.find((c) => !importJob.baselineIds.has(c.courseId));
8702
8768
  if (fresh !== void 0) {
8703
8769
  setImportJob(null);
8770
+ endImportSession();
8704
8771
  setImportResult({
8705
8772
  ok: true,
8706
8773
  msg: tr("import.success")
@@ -8711,6 +8778,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
8711
8778
  }
8712
8779
  if (importJobDead(importJob, turnActive, Date.now())) {
8713
8780
  setImportJob(null);
8781
+ endImportSession();
8714
8782
  setImportResult({
8715
8783
  ok: false,
8716
8784
  msg: tr("import.error.turn")
@@ -8908,7 +8976,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
8908
8976
  }))))) : null, data.courses.length === 0 ? (0, react.createElement)("div", { className: "lks14-empty" }, tr("rail.empty.title")) : null, (0, react.createElement)("button", {
8909
8977
  className: "lks-btn ghost lks14-raildemo",
8910
8978
  onClick: () => {
8911
- send(tr("prompt.import", { url: "https://github.com/microsoft/AI-For-Beginners" }));
8979
+ startImport(tr("import.tab.url"), tr("prompt.import", { url: "https://github.com/microsoft/AI-For-Beginners" }));
8912
8980
  }
8913
8981
  }, (0, react.createElement)(IconDownloadOutline16, null), tr("rail.empty.demo")), (0, react.createElement)(ImportPanel, {
8914
8982
  job: importJob,
@@ -8919,6 +8987,7 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
8919
8987
  onCancel: () => {
8920
8988
  stop();
8921
8989
  setImportJob(null);
8990
+ endImportSession();
8922
8991
  setImportResult({
8923
8992
  ok: false,
8924
8993
  msg: tr("import.cancelled")
@@ -9946,17 +10015,17 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
9946
10015
  const [read, setRead] = (0, react.useState)(null);
9947
10016
  const readCtl = (0, react.useRef)(null);
9948
10017
  const [readError, setReadError] = (0, react.useState)(null);
9949
- const [langView, setLangView] = (0, react.useState)("bilingual");
10018
+ const [langView, setLangView] = (0, react.useState)("original");
9950
10019
  (0, react.useEffect)(() => {
9951
10020
  if (lesson === null) return;
9952
- let next = "bilingual";
10021
+ let next = "original";
9953
10022
  try {
9954
10023
  const stored = localStorage.getItem(`lks-langview:${lesson.lessonId}`);
9955
- if (stored === "original" || stored === "bilingual" || stored === "translation") next = stored;
10024
+ if (stored === "original" || stored === "translation") next = stored;
9956
10025
  } catch {}
9957
10026
  setLangView(next);
9958
10027
  }, [lesson?.lessonId]);
9959
- const teachHtml = lesson === null ? "" : langView === "translation" && lesson.translationHtml !== void 0 ? lesson.translationHtml : langView === "bilingual" && lesson.bilingualHtml !== void 0 ? lesson.bilingualHtml : lesson.html;
10028
+ const teachHtml = lesson === null ? "" : langView === "translation" && lesson.translationHtml !== void 0 ? lesson.translationHtml : lesson.html;
9960
10029
  const { tts, addUserNote, recordReview, editNote, pinNote } = useStudy();
9961
10030
  const proseRef = (0, react.useRef)(null);
9962
10031
  const diagRef = (0, react.useRef)(null);
@@ -10221,14 +10290,10 @@ mark.lks-hl{background:var(--dsw-alias-state-warn-tertiary);color:inherit;border
10221
10290
  setRateBusy(false);
10222
10291
  });
10223
10292
  }
10224
- }, tr(key))))) : null, (0, react.createElement)("div", { className: "lks-readbar" }, lesson.bilingualHtml !== void 0 ? (0, react.createElement)("div", {
10293
+ }, tr(key))))) : null, (0, react.createElement)("div", { className: "lks-readbar" }, lesson.translationHtml !== void 0 ? (0, react.createElement)("div", {
10225
10294
  className: "lks-langview",
10226
10295
  "data-testid": "langview"
10227
- }, [
10228
- "original",
10229
- "bilingual",
10230
- "translation"
10231
- ].map((v) => (0, react.createElement)("button", {
10296
+ }, ["original", "translation"].map((v) => (0, react.createElement)("button", {
10232
10297
  key: v,
10233
10298
  className: langView === v ? "on" : "",
10234
10299
  "data-testid": `langview-${v}`,