dsh-think-translate 1.0.2 → 1.0.4

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
@@ -72,7 +72,12 @@ window.__ModuleLoader__.load({
72
72
  ".xl-cfg{width:100%;box-sizing:border-box;font-family:monospace;font-size:11px;line-height:1.5;padding:8px;border-radius:8px;border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.35));background:var(--dsw-alias-bg-base,rgba(127,127,127,.08));color:var(--dsw-alias-label-primary,inherit);outline:none}",
73
73
  ".xl-cfg:focus{border-color:var(--dsw-alias-border-l2,rgba(127,127,127,.6));box-shadow:0 0 0 2px rgba(76,154,255,.2)}",
74
74
  ".xl-progress{height:6px;border-radius:3px;background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.15));overflow:hidden;margin:4px 0}",
75
- ".xl-progress-bar{height:100%;background:var(--dsw-alias-brand-primary,#4c9aff);border-radius:3px;transition:width .4s}"
75
+ ".xl-progress-bar{height:100%;background:var(--dsw-alias-brand-primary,#4c9aff);border-radius:3px;transition:width .4s}",
76
+ ".xl-table-wrap{overflow-x:auto;margin:6px 0}",
77
+ ".xl-table{border-collapse:collapse;min-width:100%;font-size:13px;line-height:1.5}",
78
+ ".xl-table th,.xl-table td{border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.35));padding:6px 10px;text-align:left;vertical-align:top}",
79
+ ".xl-table th{background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.1));font-weight:600}",
80
+ ".xl-table code{font-family:var(--dsw-font-mono,monospace);font-size:12px}"
76
81
  ].join("\n");
77
82
  if (typeof document !== "undefined" && document.querySelector('style[data-plugin-css="dsh-think-translate/styles.css"]') === null) {
78
83
  var tag = document.createElement("style");
@@ -237,8 +242,31 @@ window.__ModuleLoader__.load({
237
242
  // system proxy automatically; endpoint returns CORS *)
238
243
  // ------------------------------------------------------------------
239
244
  var mem = new Map();
245
+ // Translation cache persisted to localStorage so switching sessions or
246
+ // refreshing a page reuses prior results instead of re-translating every
247
+ // historical message (which wedged the UI when the local model was slow,
248
+ // 2026-08-26). In-memory map is the fast path; localStorage is the
249
+ // durable cross-session floor.
250
+ var MEM_KEY = "dsh-xlate-cache:";
251
+ function cacheMemGet(key) {
252
+ var v = mem.get(key);
253
+ if (v !== undefined) return v;
254
+ try {
255
+ var lv = localStorage.getItem(MEM_KEY + key);
256
+ if (lv !== null) {
257
+ var parsed = JSON.parse(lv);
258
+ if (parsed && parsed.ok) { mem.set(key, parsed); return parsed; }
259
+ }
260
+ } catch (e) {}
261
+ return undefined;
262
+ }
263
+ function cacheMemSet(key, entry) {
264
+ mem.set(key, entry);
265
+ if (mem.size > 800) mem.delete(mem.keys().next().value);
266
+ try { localStorage.setItem(MEM_KEY + key, JSON.stringify(entry)); } catch (e) {}
267
+ }
240
268
  var pend = new Map();
241
- var stats = { ok: 0, fail: 0, lastError: null, lastProvider: null };
269
+ var stats = { ok: 0, fail: 0, lastError: null, lastProvider: null, lastModel: null };
242
270
  // Diagnostic ledger surfaced in the settings panel (2026-08-24): bundle
243
271
  // version, apply execution, per-slot registration outcome, current lang.
244
272
  var diag = { version: "v3-priority-1", applied: false, lang: "?", slots: {}, lastLangChange: 0 };
@@ -365,7 +393,7 @@ window.__ModuleLoader__.load({
365
393
 
366
394
  function requestTranslate(text, target) {
367
395
  var key = target + "|" + fnv(text);
368
- var hit = mem.get(key);
396
+ var hit = cacheMemGet(key);
369
397
  if (hit !== undefined) return Promise.resolve(hit);
370
398
  var busy = pend.get(key);
371
399
  if (busy !== undefined) return busy;
@@ -395,7 +423,7 @@ window.__ModuleLoader__.load({
395
423
  } catch (e2) { r = null; if (e2 && e2.message) lastErr = e2; }
396
424
  }
397
425
  if (r && r.ok) {
398
- stats.ok++; stats.lastError = null; stats.lastProvider = r.provider;
426
+ stats.ok++; stats.lastError = null; stats.lastProvider = r.provider; stats.lastModel = r.model || null;
399
427
  memSet(key, r);
400
428
  return r;
401
429
  }
@@ -411,21 +439,20 @@ window.__ModuleLoader__.load({
411
439
  return p;
412
440
  }
413
441
  function memSet(key, entry) {
414
- mem.set(key, entry);
415
- if (mem.size > 800) mem.delete(mem.keys().next().value);
442
+ cacheMemSet(key, entry);
416
443
  }
417
444
 
418
445
  function useXlate(text, enabled) {
419
446
  var s = useStore();
420
447
  var active = !!(enabled && text && text.trim());
421
448
  var key = active ? s.lang + "|" + fnv(text) : null;
422
- var pair = useState(function () { return key !== null ? mem.get(key) : undefined; });
449
+ var pair = useState(function () { return key !== null ? cacheMemGet(key) : undefined; });
423
450
  var entry = pair[0];
424
451
  var setEntry = pair[1];
425
452
  useEffect(function () {
426
453
  if (!active || key === null) { setEntry(undefined); return undefined; }
427
454
  var alive = true;
428
- var hit = mem.get(key);
455
+ var hit = cacheMemGet(key);
429
456
  if (hit !== undefined) { setEntry(hit); return undefined; }
430
457
  requestTranslate(text, s.lang).then(function (r) {
431
458
  if (!alive) return;
@@ -607,6 +634,21 @@ window.__ModuleLoader__.load({
607
634
  if (last < s.length) out.push(s.slice(last));
608
635
  return out;
609
636
  }
637
+ function tableCells(line) {
638
+ var s = line.trim();
639
+ if (s.charAt(0) === "|") s = s.slice(1);
640
+ if (s.charAt(s.length - 1) === "|") s = s.slice(0, -1);
641
+ return s.split("|").map(function (c) { return c.trim(); });
642
+ }
643
+ function isDelimRow(line) {
644
+ var s = line.trim();
645
+ if (s.charAt(0) === "|") s = s.slice(1);
646
+ if (s.charAt(s.length - 1) === "|") s = s.slice(0, -1);
647
+ var parts = s.split("|");
648
+ if (!parts.length) return false;
649
+ for (var p = 0; p < parts.length; p++) { if (!/^[\s:-]*$/.test(parts[p])) return false; }
650
+ return /-/.test(s);
651
+ }
610
652
  function Markdownish(props) {
611
653
  var lines = props.text.split("\n");
612
654
  var blocks = [];
@@ -652,6 +694,23 @@ window.__ModuleLoader__.load({
652
694
  })));
653
695
  continue;
654
696
  }
697
+ if (/^\s*\|/.test(line) && i + 1 < lines.length && isDelimRow(lines[i + 1])) {
698
+ var headCells = tableCells(line);
699
+ i += 2;
700
+ var bodyRows = [];
701
+ while (i < lines.length && /^\s*\|/.test(lines[i])) { bodyRows.push(tableCells(lines[i])); i++; }
702
+ var thead = createElement("thead", null, createElement("tr", null, headCells.map(function (c, n) {
703
+ return createElement("th", { key: n }, inlineNodes(c, nk() + "h" + n));
704
+ })));
705
+ var tbody = bodyRows.map(function (r, n) {
706
+ return createElement("tr", { key: n }, r.map(function (c, j) {
707
+ return createElement("td", { key: j }, inlineNodes(c, nk() + "d" + n + "j" + j));
708
+ }));
709
+ });
710
+ blocks.push(createElement("div", { key: nk(), className: "xl-table-wrap" },
711
+ createElement("table", { className: "xl-table" }, thead, createElement("tbody", null, tbody))));
712
+ continue;
713
+ }
655
714
  var para = [line];
656
715
  i++;
657
716
  while (i < lines.length && lines[i].trim() !== "" && !/^\s*(?:```|#{1,4}\s|>|[-*+]\s|\d+[.)]\s)/.test(lines[i])) { para.push(lines[i]); i++; }
@@ -792,12 +851,22 @@ window.__ModuleLoader__.load({
792
851
  var t = UI_TEXT[s.lang] || UI_TEXT.en;
793
852
  return createElement("div", null,
794
853
  createElement("div", { className: "xl-kv" }, createElement("b", null, t.status), createElement("span", null, stats.ok + " / " + stats.fail)),
795
- createElement("div", { className: "xl-kv" }, createElement("b", null, t.providerOf), createElement("span", null, stats.lastProvider ? (t.labels[stats.lastProvider] || stats.lastProvider) : t.providerDesc)),
854
+ createElement("div", { className: "xl-kv" }, createElement("b", null, t.providerOf), createElement("span", null, stats.lastProvider ? ((t.labels[stats.lastProvider] || stats.lastProvider) + (stats.lastModel ? " · " + stats.lastModel : "")) : t.providerDesc)),
796
855
  createElement("div", { className: "xl-kv" }, createElement("b", null, t.cacheEntries), createElement("span", null, String(mem.size))),
797
856
  stats.lastError ? createElement("div", { className: "xl-err" }, t.lastError + ": " + stats.lastError) : null,
798
857
  createElement("button", {
799
858
  className: "xl-btn", type: "button",
800
- onClick: function () { mem.clear(); }
859
+ onClick: function () {
860
+ mem.clear();
861
+ try {
862
+ var keys = [];
863
+ for (var i = 0; i < localStorage.length; i++) {
864
+ var k = localStorage.key(i);
865
+ if (k && k.indexOf(MEM_KEY) === 0) keys.push(k);
866
+ }
867
+ for (var j = 0; j < keys.length; j++) localStorage.removeItem(keys[j]);
868
+ } catch (e) {}
869
+ }
801
870
  }, t.clearCache));
802
871
  }
803
872
 
@@ -812,10 +881,21 @@ window.__ModuleLoader__.load({
812
881
  var models = modelsPair[0], setModels = modelsPair[1];
813
882
  useEffect(function () {
814
883
  var alive = true;
815
- fetch("/_xlate/config").then(function (r) {
816
- if (!r.ok) throw new Error("HTTP " + r.status);
817
- return r.json();
818
- }).then(function (c) { if (alive) setCfg(c); }).catch(function () { if (alive) setCfg(null); });
884
+ var tries = 0;
885
+ function attempt() {
886
+ if (!alive) return;
887
+ fetch("/_xlate/config").then(function (r) {
888
+ if (!r.ok) throw new Error("HTTP " + r.status);
889
+ return r.json();
890
+ }).then(function (c) { if (alive) setCfg(c); })
891
+ .catch(function () {
892
+ if (!alive) return;
893
+ tries++;
894
+ if (tries < 15) { setTimeout(attempt, 1500); }
895
+ else { setCfg(null); }
896
+ });
897
+ }
898
+ attempt();
819
899
  return function () { alive = false; };
820
900
  }, []);
821
901
  useEffect(function () {
@@ -889,7 +969,7 @@ window.__ModuleLoader__.load({
889
969
  fetch("/_xlate/translate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text: "Hello world, this is a connection test. [" + Date.now().toString(36) + "]", target: store.lang }) })
890
970
  .then(function (r) { return r.json(); })
891
971
  .then(function (j) {
892
- setMsg(j && j.ok ? t.connOk + " ✓ (" + (j.provider || "?") + ")" : t.connFail + ": " + (j.error || "unknown"));
972
+ setMsg(j && j.ok ? t.connOk + " ✓ (" + (t.labels[j.provider] || j.provider) + (j.model ? ": " + j.model : "") + ")" : t.connFail + ": " + (j.error || "unknown"));
893
973
  })
894
974
  .catch(function (e) { setMsg(t.connFail + ": " + (e && e.message ? e.message : e)); });
895
975
  };
package/lib/index.js CHANGED
@@ -271,14 +271,27 @@ async function viaOpenAI(text, to, cfg) {
271
271
  // (2026-08-24: zh->ja via host chain returned garbage/off-topic output
272
272
  // while direct fetch worked). Loopback bypasses the system proxy anyway,
273
273
  // so fetch is both correct and proxy-safe here.
274
- const res = await fetch(url, {
275
- method: 'POST',
276
- headers: {
277
- 'Authorization': 'Bearer ' + cfg.apiKey,
278
- 'Content-Type': 'application/json',
279
- },
280
- body,
281
- })
274
+ //
275
+ // A local model with insufficient VRAM can hang the request indefinitely
276
+ // (fetch has no default timeout), which stalls the whole host chain and
277
+ // every pending request. Bound it so a slow local model fails fast and the
278
+ // chain falls through to google/bing instead of wedging the UI (2026-08-26).
279
+ const controller = new AbortController()
280
+ const timer = setTimeout(function () { controller.abort() }, 15000)
281
+ let res
282
+ try {
283
+ res = await fetch(url, {
284
+ method: 'POST',
285
+ headers: {
286
+ 'Authorization': 'Bearer ' + cfg.apiKey,
287
+ 'Content-Type': 'application/json',
288
+ },
289
+ body,
290
+ signal: controller.signal,
291
+ })
292
+ } finally {
293
+ clearTimeout(timer)
294
+ }
282
295
  if (!res.ok) throw new Error('openai HTTP ' + res.status)
283
296
  raw = await res.text()
284
297
  } else {
@@ -437,7 +450,9 @@ async function translateViaChain(text, target, cfg) {
437
450
  state.ok++
438
451
  state.lastProvider = id
439
452
  state.lastError = null
440
- return { ok: true, text: outs.join(''), provider: id }
453
+ // Carry the concrete model name for openai (local Ollama) so the UI can
454
+ // show e.g. "local model: qwen2.5:14b" instead of the bare provider id.
455
+ return { ok: true, text: outs.join(''), provider: id, ...(id === 'openai' ? { model: cfg.providers.openai.model } : {}) }
441
456
  } catch (e) {
442
457
  lastErr = e
443
458
  if (id === 'bing') bingCache = null
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-think-translate",
3
3
  "description": "Display-layer translation for the DSH web app: thinking chain, task cards, and optional answer text, in 8 target languages (local Ollama / Google / Bing, fail-open)",
4
- "version": "1.0.2",
4
+ "version": "1.0.4",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {