dsh-think-translate 1.0.3 → 1.0.5

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.
Files changed (3) hide show
  1. package/lib/client.js +117 -13
  2. package/lib/index.js +21 -8
  3. package/package.json +4 -1
package/lib/client.js CHANGED
@@ -6,6 +6,14 @@ window.__ModuleLoader__.load({
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  var react = require("react");
8
8
  var React = react.default !== undefined ? react.default : react;
9
+ // Official Markdown renderer (loaded via the client module graph, declared
10
+ // in dsh.client.external). Using it keeps official formatting — code-block
11
+ // copy, syntax highlighting, tables, footnotes, file mentions — while this
12
+ // plugin only supplies translated text. Falls back to the local Markdownish
13
+ // if the dependency is unavailable.
14
+ var primitives = null;
15
+ try { primitives = require("@deepseek-ai/dsh-client-ui-primitives"); } catch (e) { primitives = null; }
16
+ var MarkdownText = primitives && primitives.MarkdownText ? primitives.MarkdownText : null;
9
17
  var createElement = function (type, props) {
10
18
  var children = [];
11
19
  for (var i = 2; i < arguments.length; i++) children.push(arguments[i]);
@@ -72,7 +80,12 @@ window.__ModuleLoader__.load({
72
80
  ".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
81
  ".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
82
  ".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}"
83
+ ".xl-progress-bar{height:100%;background:var(--dsw-alias-brand-primary,#4c9aff);border-radius:3px;transition:width .4s}",
84
+ ".xl-table-wrap{overflow-x:auto;margin:6px 0}",
85
+ ".xl-table{border-collapse:collapse;min-width:100%;font-size:13px;line-height:1.5}",
86
+ ".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}",
87
+ ".xl-table th{background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.1));font-weight:600}",
88
+ ".xl-table code{font-family:var(--dsw-font-mono,monospace);font-size:12px}"
76
89
  ].join("\n");
77
90
  if (typeof document !== "undefined" && document.querySelector('style[data-plugin-css="dsh-think-translate/styles.css"]') === null) {
78
91
  var tag = document.createElement("style");
@@ -130,6 +143,7 @@ window.__ModuleLoader__.load({
130
143
  providerUnavailable: "翻译服务未加载:插件的 host 半边未生效(常见于刚改过配置或更新后)。请重启 Web 再试——重启后翻译与设置将恢复正常。",
131
144
  providerDesc: "Google / Bing / 本地模型(自动选择)",
132
145
  labels: { google: "google gtx(免费)", bing: "bing(免费)", openai: "本地部署模型(Ollama)" },
146
+ copy: "复制", copied: "已复制",
133
147
  note: "界面翻译:思考链、任务卡片、回答正文会显示为你选的语言,原文保留在会话记录中可随时对照。优先使用本地模型,其次 Google/Bing;文件路径、代码等自动跳过。",
134
148
  },
135
149
  "en": {
@@ -144,6 +158,7 @@ window.__ModuleLoader__.load({
144
158
  providerUnavailable: "Translation service not loaded: the plugin host half is inactive (common after config changes or updates). Restart the web app to restore translation and settings.",
145
159
  providerDesc: "Google / Bing / local model (auto)",
146
160
  labels: { google: "google gtx (free)", bing: "bing (free)", openai: "Local model (Ollama)" },
161
+ copy: "Copy", copied: "Copied",
147
162
  note: "UI translation: the thinking chain, task cards and answer text display in your chosen language; originals stay in the transcript for comparison. Local model first, then Google/Bing; file paths and code are skipped.",
148
163
  },
149
164
  "ja": {
@@ -237,6 +252,29 @@ window.__ModuleLoader__.load({
237
252
  // system proxy automatically; endpoint returns CORS *)
238
253
  // ------------------------------------------------------------------
239
254
  var mem = new Map();
255
+ // Translation cache persisted to localStorage so switching sessions or
256
+ // refreshing a page reuses prior results instead of re-translating every
257
+ // historical message (which wedged the UI when the local model was slow,
258
+ // 2026-08-26). In-memory map is the fast path; localStorage is the
259
+ // durable cross-session floor.
260
+ var MEM_KEY = "dsh-xlate-cache:";
261
+ function cacheMemGet(key) {
262
+ var v = mem.get(key);
263
+ if (v !== undefined) return v;
264
+ try {
265
+ var lv = localStorage.getItem(MEM_KEY + key);
266
+ if (lv !== null) {
267
+ var parsed = JSON.parse(lv);
268
+ if (parsed && parsed.ok) { mem.set(key, parsed); return parsed; }
269
+ }
270
+ } catch (e) {}
271
+ return undefined;
272
+ }
273
+ function cacheMemSet(key, entry) {
274
+ mem.set(key, entry);
275
+ if (mem.size > 800) mem.delete(mem.keys().next().value);
276
+ try { localStorage.setItem(MEM_KEY + key, JSON.stringify(entry)); } catch (e) {}
277
+ }
240
278
  var pend = new Map();
241
279
  var stats = { ok: 0, fail: 0, lastError: null, lastProvider: null, lastModel: null };
242
280
  // Diagnostic ledger surfaced in the settings panel (2026-08-24): bundle
@@ -365,7 +403,7 @@ window.__ModuleLoader__.load({
365
403
 
366
404
  function requestTranslate(text, target) {
367
405
  var key = target + "|" + fnv(text);
368
- var hit = mem.get(key);
406
+ var hit = cacheMemGet(key);
369
407
  if (hit !== undefined) return Promise.resolve(hit);
370
408
  var busy = pend.get(key);
371
409
  if (busy !== undefined) return busy;
@@ -411,21 +449,20 @@ window.__ModuleLoader__.load({
411
449
  return p;
412
450
  }
413
451
  function memSet(key, entry) {
414
- mem.set(key, entry);
415
- if (mem.size > 800) mem.delete(mem.keys().next().value);
452
+ cacheMemSet(key, entry);
416
453
  }
417
454
 
418
455
  function useXlate(text, enabled) {
419
456
  var s = useStore();
420
457
  var active = !!(enabled && text && text.trim());
421
458
  var key = active ? s.lang + "|" + fnv(text) : null;
422
- var pair = useState(function () { return key !== null ? mem.get(key) : undefined; });
459
+ var pair = useState(function () { return key !== null ? cacheMemGet(key) : undefined; });
423
460
  var entry = pair[0];
424
461
  var setEntry = pair[1];
425
462
  useEffect(function () {
426
463
  if (!active || key === null) { setEntry(undefined); return undefined; }
427
464
  var alive = true;
428
- var hit = mem.get(key);
465
+ var hit = cacheMemGet(key);
429
466
  if (hit !== undefined) { setEntry(hit); return undefined; }
430
467
  requestTranslate(text, s.lang).then(function (r) {
431
468
  if (!alive) return;
@@ -607,6 +644,21 @@ window.__ModuleLoader__.load({
607
644
  if (last < s.length) out.push(s.slice(last));
608
645
  return out;
609
646
  }
647
+ function tableCells(line) {
648
+ var s = line.trim();
649
+ if (s.charAt(0) === "|") s = s.slice(1);
650
+ if (s.charAt(s.length - 1) === "|") s = s.slice(0, -1);
651
+ return s.split("|").map(function (c) { return c.trim(); });
652
+ }
653
+ function isDelimRow(line) {
654
+ var s = line.trim();
655
+ if (s.charAt(0) === "|") s = s.slice(1);
656
+ if (s.charAt(s.length - 1) === "|") s = s.slice(0, -1);
657
+ var parts = s.split("|");
658
+ if (!parts.length) return false;
659
+ for (var p = 0; p < parts.length; p++) { if (!/^[\s:-]*$/.test(parts[p])) return false; }
660
+ return /-/.test(s);
661
+ }
610
662
  function Markdownish(props) {
611
663
  var lines = props.text.split("\n");
612
664
  var blocks = [];
@@ -652,6 +704,23 @@ window.__ModuleLoader__.load({
652
704
  })));
653
705
  continue;
654
706
  }
707
+ if (/^\s*\|/.test(line) && i + 1 < lines.length && isDelimRow(lines[i + 1])) {
708
+ var headCells = tableCells(line);
709
+ i += 2;
710
+ var bodyRows = [];
711
+ while (i < lines.length && /^\s*\|/.test(lines[i])) { bodyRows.push(tableCells(lines[i])); i++; }
712
+ var thead = createElement("thead", null, createElement("tr", null, headCells.map(function (c, n) {
713
+ return createElement("th", { key: n }, inlineNodes(c, nk() + "h" + n));
714
+ })));
715
+ var tbody = bodyRows.map(function (r, n) {
716
+ return createElement("tr", { key: n }, r.map(function (c, j) {
717
+ return createElement("td", { key: j }, inlineNodes(c, nk() + "d" + n + "j" + j));
718
+ }));
719
+ });
720
+ blocks.push(createElement("div", { key: nk(), className: "xl-table-wrap" },
721
+ createElement("table", { className: "xl-table" }, thead, createElement("tbody", null, tbody))));
722
+ continue;
723
+ }
655
724
  var para = [line];
656
725
  i++;
657
726
  while (i < lines.length && lines[i].trim() !== "" && !/^\s*(?:```|#{1,4}\s|>|[-*+]\s|\d+[.)]\s)/.test(lines[i])) { para.push(lines[i]); i++; }
@@ -661,11 +730,25 @@ window.__ModuleLoader__.load({
661
730
  }
662
731
  function TextBlock(props) {
663
732
  var s = useStore();
733
+ var tt = UI_TEXT[s.lang] || UI_TEXT.en;
664
734
  var tr = useXlate(props.text, s.ans && !props.streaming);
665
735
  var hasTr = !!(tr && tr.ok && !tr.skipped && tr.text !== props.text);
736
+ // Official MarkdownText when available: keeps the full official
737
+ // formatting (code copy, highlighting, tables...). Translated text
738
+ // replaces the body when answer translation is on; original stays in
739
+ // a collapsible details block.
740
+ var codeLabels = MarkdownText ? { copyLabel: tt.copy, copiedLabel: tt.copied } : undefined;
741
+ var MDT = MarkdownText || Markdownish;
742
+ var body = hasTr ? tr.text : props.text;
666
743
  return createElement("div", null,
667
- createElement(Markdownish, { text: props.text, streaming: props.streaming }),
668
- hasTr ? createElement("div", { className: "xl-trans-after" }, tr.text) : null);
744
+ createElement(MDT, MarkdownText
745
+ ? { text: body, streaming: props.streaming, codeLabels: codeLabels }
746
+ : { text: body, streaming: props.streaming }),
747
+ hasTr ? createElement("details", { className: "xl-orig" },
748
+ createElement("summary", null, tt.original),
749
+ createElement("div", null, createElement(MDT, MarkdownText
750
+ ? { text: props.text, codeLabels: codeLabels }
751
+ : { text: props.text }))) : null);
669
752
  }
670
753
 
671
754
  function AssistantView(props) {
@@ -797,7 +880,17 @@ window.__ModuleLoader__.load({
797
880
  stats.lastError ? createElement("div", { className: "xl-err" }, t.lastError + ": " + stats.lastError) : null,
798
881
  createElement("button", {
799
882
  className: "xl-btn", type: "button",
800
- onClick: function () { mem.clear(); }
883
+ onClick: function () {
884
+ mem.clear();
885
+ try {
886
+ var keys = [];
887
+ for (var i = 0; i < localStorage.length; i++) {
888
+ var k = localStorage.key(i);
889
+ if (k && k.indexOf(MEM_KEY) === 0) keys.push(k);
890
+ }
891
+ for (var j = 0; j < keys.length; j++) localStorage.removeItem(keys[j]);
892
+ } catch (e) {}
893
+ }
801
894
  }, t.clearCache));
802
895
  }
803
896
 
@@ -812,10 +905,21 @@ window.__ModuleLoader__.load({
812
905
  var models = modelsPair[0], setModels = modelsPair[1];
813
906
  useEffect(function () {
814
907
  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); });
908
+ var tries = 0;
909
+ function attempt() {
910
+ if (!alive) return;
911
+ fetch("/_xlate/config").then(function (r) {
912
+ if (!r.ok) throw new Error("HTTP " + r.status);
913
+ return r.json();
914
+ }).then(function (c) { if (alive) setCfg(c); })
915
+ .catch(function () {
916
+ if (!alive) return;
917
+ tries++;
918
+ if (tries < 15) { setTimeout(attempt, 1500); }
919
+ else { setCfg(null); }
920
+ });
921
+ }
922
+ attempt();
819
923
  return function () { alive = false; };
820
924
  }, []);
821
925
  useEffect(function () {
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 {
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.3",
4
+ "version": "1.0.5",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {
@@ -27,6 +27,9 @@
27
27
  "immediately": true,
28
28
  "inject": [
29
29
  "@deepseek-ai/dsh-client-runtime"
30
+ ],
31
+ "external": [
32
+ "@deepseek-ai/dsh-client-ui-primitives"
30
33
  ]
31
34
  },
32
35
  "bundle": {