dsh-plugin-lookatstudy 0.14.0 → 0.14.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
@@ -2210,6 +2210,7 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
2210
2210
  if (event === void 0) continue;
2211
2211
  if (entry.type === "event") {
2212
2212
  if (event.type === "user/message") {
2213
+ if (event.data?.source?.kind !== "user") continue;
2213
2214
  const text = userText(event);
2214
2215
  if (text !== "") rows.push({
2215
2216
  key: `u${event.seq}`,
@@ -2394,7 +2395,13 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
2394
2395
  run = [];
2395
2396
  };
2396
2397
  for (const line of text.split("\n")) {
2397
- const match = /^([A-D])[.、:)]\s+(.+)$/.exec(line.trim());
2398
+ const plain = /^([A-D])[.、:)]\s*(.+)$/.exec(line.trim());
2399
+ const table = /^\|\s*\*{0,2}([A-D])\*{0,2}\s*\|\s*(.+?)\s*\|$/.exec(line.trim());
2400
+ const match = plain ?? (table !== null ? [
2401
+ table[0],
2402
+ table[1],
2403
+ table[2].replaceAll("**", "").replaceAll("`", "")
2404
+ ] : null);
2398
2405
  if (match === null) {
2399
2406
  flush();
2400
2407
  continue;
@@ -2475,6 +2482,17 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
2475
2482
  function titleMatches(title, query) {
2476
2483
  return query.trim().toLowerCase().split(/\s+/).filter(Boolean).every((k) => title.toLowerCase().includes(k));
2477
2484
  }
2485
+ /**
2486
+ * Whether the host session list actually knows a bound thread id. A restart
2487
+ * can drop young sessions from persistence; an empty byId means the list has
2488
+ * not hydrated yet (assume known — the send's own open surfaces a real error).
2489
+ * Pure over the injected list snapshot.
2490
+ */
2491
+ function sessionKnown(ctx, id) {
2492
+ const byId = (ctx.sessions.list?.getSnapshot())?.byId;
2493
+ if (byId === void 0) return true;
2494
+ return Object.keys(byId).length === 0 ? true : id in byId;
2495
+ }
2478
2496
  /** One rendered chat row (shared shape with the read-aloud era's renderer). */
2479
2497
  function chatRow(row, interactive) {
2480
2498
  if (row.role === "user") return (0, react.createElement)("div", {
@@ -2520,6 +2538,7 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
2520
2538
  const [sendError, setSendError] = (0, react.useState)(null);
2521
2539
  const [busy, setBusy] = (0, react.useState)(false);
2522
2540
  const [rows, setRows] = (0, react.useState)([]);
2541
+ const [feedAttached, setFeedAttached] = (0, react.useState)(false);
2523
2542
  const [draft, setDraft] = (0, react.useState)("");
2524
2543
  const lesson = data?.lesson ?? null;
2525
2544
  const localBound = (0, react.useRef)(null);
@@ -2528,18 +2547,66 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
2528
2547
  (0, react.useEffect)(() => {
2529
2548
  if (boundId === null) {
2530
2549
  setRows([]);
2550
+ setFeedAttached(false);
2531
2551
  return;
2532
2552
  }
2533
- const source = ctx.sessions.binding(boundId)?.eventSource;
2534
- if (source === void 0) {
2535
- setRows([]);
2536
- return;
2553
+ setFeedAttached(false);
2554
+ let disposed = false;
2555
+ let offSource;
2556
+ let offList;
2557
+ const attach = () => {
2558
+ const source = ctx.sessions.binding(boundId)?.eventSource;
2559
+ if (source === void 0) return false;
2560
+ const update = () => {
2561
+ setRows(feedRows(source.getSnapshot()));
2562
+ };
2563
+ update();
2564
+ setFeedAttached(true);
2565
+ offSource = source.subscribe(update);
2566
+ return true;
2567
+ };
2568
+ const stage = () => {
2569
+ if (!sessionKnown(ctx, boundId)) return;
2570
+ try {
2571
+ const shell = PANEL_SHELL.current;
2572
+ if (shell !== null) shell.suppressHandBack(() => {
2573
+ ctx.sessions.open(boundId);
2574
+ });
2575
+ else ctx.sessions.open(boundId);
2576
+ } catch {}
2577
+ };
2578
+ const reconcile = () => {
2579
+ if (disposed || offSource !== void 0) return true;
2580
+ if (ctx.sessions.list?.getSnapshot().current !== boundId) {
2581
+ stage();
2582
+ return false;
2583
+ }
2584
+ return attach();
2585
+ };
2586
+ if (!reconcile()) {
2587
+ let tries = 0;
2588
+ const timer = setInterval(() => {
2589
+ if (disposed || offSource !== void 0 || reconcile() || ++tries >= 40) clearInterval(timer);
2590
+ }, 150);
2591
+ const list = ctx.sessions.list;
2592
+ if (list !== void 0) offList = list.subscribe(() => {
2593
+ if (disposed || reconcile()) {
2594
+ offList?.();
2595
+ offList = void 0;
2596
+ }
2597
+ });
2598
+ return () => {
2599
+ disposed = true;
2600
+ clearInterval(timer);
2601
+ offSource?.();
2602
+ offList?.();
2603
+ };
2537
2604
  }
2538
- const update = () => {
2539
- setRows(feedRows(source.getSnapshot()));
2605
+ return () => {
2606
+ disposed = true;
2607
+ offSource?.();
2608
+ offList?.();
2540
2609
  };
2541
- update();
2542
- return source.subscribe(update);
2543
2610
  }, [boundId, ctx]);
2544
2611
  (0, react.useEffect)(() => {
2545
2612
  const el = streamEl.current;
@@ -2553,8 +2620,9 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
2553
2620
  setSendError(null);
2554
2621
  try {
2555
2622
  if (data?.active !== true) await activate(true);
2556
- let sessionId = localBound.current ?? data?.lessonSessions[lesson.lessonId];
2557
- if (sessionId === void 0 || sessionId === null) {
2623
+ let sessionId = localBound.current ?? data?.lessonSessions[lesson.lessonId] ?? null;
2624
+ if (sessionId !== null && !sessionKnown(ctx, sessionId)) sessionId = null;
2625
+ if (sessionId === null) {
2558
2626
  const area = await fetch("/lookatstudy/api/study-workspace");
2559
2627
  if (!area.ok) throw new Error(`study area unavailable (HTTP ${String(area.status)})`);
2560
2628
  const { path } = await area.json();
@@ -2595,6 +2663,8 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
2595
2663
  data,
2596
2664
  lesson,
2597
2665
  rows,
2666
+ feedAttached,
2667
+ bound: boundId !== null,
2598
2668
  busy,
2599
2669
  sendError,
2600
2670
  draft,
@@ -2780,7 +2850,7 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
2780
2850
  }, tr("rail.empty.button"))), (0, react.createElement)("div", { className: "lks14-hint" }, tr("rail.empty.hint"), (0, react.createElement)("br"), tr("rail.empty.hint2")));
2781
2851
  }
2782
2852
  /** 中栏:the tutor chat stream with its own composer (upstream ChatStream + ChatComposer). */
2783
- function ChatPane({ data, lesson, rows, busy, sendError, draft, setDraft, send, setMode }) {
2853
+ function ChatPane({ data, lesson, rows, feedAttached, bound, busy, sendError, draft, setDraft, send, setMode }) {
2784
2854
  const streamEl = (0, react.useRef)(null);
2785
2855
  (0, react.useEffect)(() => {
2786
2856
  const el = streamEl.current;
@@ -2814,7 +2884,8 @@ html[data-dsh-lookatstudy-active] [class*='centerCol'] > :not([data-dsh-lookatst
2814
2884
  }
2815
2885
  }, tr(mode.labelKey))))), (0, react.createElement)("div", {
2816
2886
  className: "lks14-stream",
2817
- ref: streamEl
2887
+ ref: streamEl,
2888
+ "data-lks-feed": rows.length > 0 ? `rows:${String(rows.length)}` : bound ? feedAttached ? "attached-empty" : "waiting" : "no-thread"
2818
2889
  }, rows.length === 0 ? (0, react.createElement)("div", { className: "lks14-empty" }, dormant ? tr("tutor.dormant") : tr("tutor.empty"), (0, react.createElement)("br"), tr("tutor.empty.hint")) : rows.map((row, i) => chatRow(row, !dormant && i === lastAssistant ? { send } : void 0))), (0, react.createElement)("div", { className: "lks14-starters" }, ...starters.map((s) => (0, react.createElement)("button", {
2819
2890
  key: s.label,
2820
2891
  className: "lks14-starter",