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 +95 -15
- package/lib/index.js +24 -9
- package/package.json +1 -1
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 =
|
|
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
|
-
|
|
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 ?
|
|
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 =
|
|
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 () {
|
|
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
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
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 ||
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
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.
|
|
4
|
+
"version": "1.0.4",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|