dsh-think-translate 1.2.7 → 1.2.8
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 +183 -175
- package/lib/index.js +241 -288
- package/lib/transport.js +135 -0
- package/package.json +4 -3
package/lib/client.js
CHANGED
|
@@ -23,7 +23,7 @@ window.__ModuleLoader__.load({
|
|
|
23
23
|
// there yet (an older process still serving the port), and the contract test
|
|
24
24
|
// pins PLUGIN_VERSION to package.json so the two can never drift.
|
|
25
25
|
var PLUGIN_NAME = "dsh-think-translate";
|
|
26
|
-
var PLUGIN_VERSION = "1.2.
|
|
26
|
+
var PLUGIN_VERSION = "1.2.8";
|
|
27
27
|
var PLUGIN_REPO = "https://github.com/UncleK/dsh-think-translate";
|
|
28
28
|
// The official MarkdownText contract changed in 0.1.2-alpha: it now takes a
|
|
29
29
|
// nested `labels` object and dereferences it without defaults inside its code
|
|
@@ -665,7 +665,14 @@ window.__ModuleLoader__.load({
|
|
|
665
665
|
var MEM_KEY = "dsh-xlate-cache:";
|
|
666
666
|
function cacheMemGet(key) {
|
|
667
667
|
var v = mem.get(key);
|
|
668
|
-
if (v !== undefined)
|
|
668
|
+
if (v !== undefined) {
|
|
669
|
+
if (v.ok === false) {
|
|
670
|
+
if (Date.now() < v.retryAt) return v;
|
|
671
|
+
mem.delete(key);
|
|
672
|
+
return undefined;
|
|
673
|
+
}
|
|
674
|
+
return v;
|
|
675
|
+
}
|
|
669
676
|
try {
|
|
670
677
|
var lv = localStorage.getItem(MEM_KEY + key);
|
|
671
678
|
if (lv !== null) {
|
|
@@ -705,6 +712,7 @@ window.__ModuleLoader__.load({
|
|
|
705
712
|
function cacheMemSet(key, entry) {
|
|
706
713
|
mem.set(key, entry);
|
|
707
714
|
if (mem.size > 800) mem.delete(mem.keys().next().value);
|
|
715
|
+
if (entry && !entry.ok) return;
|
|
708
716
|
try {
|
|
709
717
|
var stamped = entry && typeof entry === "object" ? Object.assign({ at: Date.now() }, entry) : entry;
|
|
710
718
|
localStorage.setItem(MEM_KEY + key, JSON.stringify(stamped));
|
|
@@ -713,18 +721,18 @@ window.__ModuleLoader__.load({
|
|
|
713
721
|
} catch (e) {}
|
|
714
722
|
}
|
|
715
723
|
var pend = new Map();
|
|
716
|
-
var stats = { ok: 0, fail: 0, lastError: null, lastProvider: null, lastModel: null };
|
|
717
|
-
var providerChecks = {};
|
|
718
|
-
var providerTestSequence = {};
|
|
719
|
-
var statusListeners = new Set();
|
|
720
|
-
function providerFingerprint(p) {
|
|
721
|
-
return fnv(JSON.stringify({ type: p.type, baseURL: p.baseURL, model: p.model, apiKeyEnv: p.apiKeyEnv,
|
|
722
|
-
apiKey: p.source === "dsh" || p.apiKeyEnv ? undefined : p.apiKey }));
|
|
723
|
-
}
|
|
724
|
-
function recordCheck(id, ok, model, signature) {
|
|
725
|
-
providerChecks[id] = { ok: ok, model: model || null, signature: signature };
|
|
726
|
-
statusListeners.forEach(function (notify) { notify(); });
|
|
727
|
-
}
|
|
724
|
+
var stats = { ok: 0, fail: 0, lastError: null, lastProvider: null, lastModel: null };
|
|
725
|
+
var providerChecks = {};
|
|
726
|
+
var providerTestSequence = {};
|
|
727
|
+
var statusListeners = new Set();
|
|
728
|
+
function providerFingerprint(p) {
|
|
729
|
+
return fnv(JSON.stringify({ type: p.type, baseURL: p.baseURL, model: p.model, apiKeyEnv: p.apiKeyEnv,
|
|
730
|
+
apiKey: p.source === "dsh" || p.apiKeyEnv ? undefined : p.apiKey }));
|
|
731
|
+
}
|
|
732
|
+
function recordCheck(id, ok, model, signature) {
|
|
733
|
+
providerChecks[id] = { ok: ok, model: model || null, signature: signature };
|
|
734
|
+
statusListeners.forEach(function (notify) { notify(); });
|
|
735
|
+
}
|
|
728
736
|
// Diagnostic ledger surfaced in the settings panel (2026-08-24): bundle
|
|
729
737
|
// version, apply execution, per-slot registration outcome, current lang.
|
|
730
738
|
var diag = { version: "v5-mode", applied: false, lang: "?", slots: {}, lastLangChange: 0 };
|
|
@@ -849,15 +857,21 @@ window.__ModuleLoader__.load({
|
|
|
849
857
|
}
|
|
850
858
|
|
|
851
859
|
async function hostAttempt(text, target) {
|
|
860
|
+
// The host may first load a local model (up to 120s), then infer or
|
|
861
|
+
// walk its fallback providers. Bound the full route, not each hop.
|
|
852
862
|
var res = await fetchWithTimeout("/_xlate/translate", {
|
|
853
863
|
method: "POST",
|
|
854
864
|
headers: { "Content-Type": "application/json" },
|
|
855
865
|
body: JSON.stringify({ text: text, target: target })
|
|
856
|
-
},
|
|
866
|
+
}, 180000);
|
|
857
867
|
if (!res.ok) throw new Error("route HTTP " + res.status);
|
|
858
|
-
var j = await res.json();
|
|
859
|
-
if (j && Array.isArray(j.attempts)) j.attempts.forEach(function (attempt) { recordCheck(attempt.provider, attempt.ok, attempt.model, attempt.signature); });
|
|
860
|
-
if (!(j && j.ok))
|
|
868
|
+
var j = await res.json();
|
|
869
|
+
if (j && Array.isArray(j.attempts)) j.attempts.forEach(function (attempt) { recordCheck(attempt.provider, attempt.ok, attempt.model, attempt.signature); });
|
|
870
|
+
if (!(j && j.ok)) {
|
|
871
|
+
var error = new Error(j && j.error || "route ok=false");
|
|
872
|
+
if (j && typeof j.retryAt === "number") error.retryAt = j.retryAt;
|
|
873
|
+
throw error;
|
|
874
|
+
}
|
|
861
875
|
return j;
|
|
862
876
|
}
|
|
863
877
|
|
|
@@ -875,14 +889,13 @@ window.__ModuleLoader__.load({
|
|
|
875
889
|
return skipped;
|
|
876
890
|
}
|
|
877
891
|
// Host chain with bounded retries: a single transient google/proxy
|
|
878
|
-
// blip must not flip the row into its permanent failed state
|
|
879
|
-
// (2026-08-22: one blip used to kill translation for the whole block).
|
|
892
|
+
// blip must not flip the row into its permanent failed state.
|
|
880
893
|
var r = null, lastErr = null;
|
|
881
|
-
var backoff = [0, 400
|
|
894
|
+
var backoff = [0, 400];
|
|
882
895
|
for (var i = 0; i < backoff.length; i++) {
|
|
883
896
|
if (backoff[i] > 0) await sleepMs(backoff[i]);
|
|
884
897
|
try { r = await hostAttempt(text, target); break; }
|
|
885
|
-
catch (e) { r = null; lastErr = e; }
|
|
898
|
+
catch (e) { r = null; lastErr = e; if (e && e.retryAt > Date.now()) break; }
|
|
886
899
|
}
|
|
887
900
|
if (!(r && r.ok)) {
|
|
888
901
|
try {
|
|
@@ -892,8 +905,8 @@ window.__ModuleLoader__.load({
|
|
|
892
905
|
r = { ok: true, text: outs.join(""), provider: "google-direct" };
|
|
893
906
|
} catch (e2) { r = null; if (e2 && e2.message) lastErr = e2; }
|
|
894
907
|
}
|
|
895
|
-
if (r && r.ok) {
|
|
896
|
-
if (r.providerSignature) recordCheck(r.provider, true, r.model, r.providerSignature);
|
|
908
|
+
if (r && r.ok) {
|
|
909
|
+
if (r.providerSignature) recordCheck(r.provider, true, r.model, r.providerSignature);
|
|
897
910
|
stats.ok++; stats.lastError = null; stats.lastProvider = r.provider; stats.lastModel = r.model || null;
|
|
898
911
|
memSet(key, r);
|
|
899
912
|
return r;
|
|
@@ -902,7 +915,9 @@ window.__ModuleLoader__.load({
|
|
|
902
915
|
} catch (e) {
|
|
903
916
|
stats.fail++;
|
|
904
917
|
stats.lastError = e instanceof Error ? e.message : String(e);
|
|
905
|
-
|
|
918
|
+
var failedResult = { ok: false, text: text, skipped: false, retryAt: Math.max(Date.now() + 30000, lastErr && lastErr.retryAt || 0) };
|
|
919
|
+
memSet(key, failedResult);
|
|
920
|
+
return failedResult;
|
|
906
921
|
}
|
|
907
922
|
})();
|
|
908
923
|
pend.set(key, p);
|
|
@@ -923,21 +938,18 @@ window.__ModuleLoader__.load({
|
|
|
923
938
|
useEffect(function () {
|
|
924
939
|
if (!active || key === null) { setEntry(undefined); return undefined; }
|
|
925
940
|
var alive = true;
|
|
926
|
-
var
|
|
927
|
-
|
|
928
|
-
requestTranslate(text, s.lang).then(function (r) {
|
|
941
|
+
var retryTimer = null;
|
|
942
|
+
function attempt() {
|
|
929
943
|
if (!alive) return;
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
setTimeout(
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
});
|
|
940
|
-
return function () { alive = false; };
|
|
944
|
+
retryTimer = null;
|
|
945
|
+
requestTranslate(text, s.lang).then(function (r) {
|
|
946
|
+
if (!alive) return;
|
|
947
|
+
setEntry(r);
|
|
948
|
+
if (!(r && r.ok)) retryTimer = setTimeout(attempt, Math.max(1, (r && r.retryAt || Date.now() + 30000) - Date.now()));
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
attempt();
|
|
952
|
+
return function () { alive = false; if (retryTimer !== null) clearTimeout(retryTimer); };
|
|
941
953
|
}, [key, active]);
|
|
942
954
|
return entry;
|
|
943
955
|
}
|
|
@@ -1026,7 +1038,7 @@ window.__ModuleLoader__.load({
|
|
|
1026
1038
|
(async function () {
|
|
1027
1039
|
while (alive) {
|
|
1028
1040
|
var cur = tRef.current;
|
|
1029
|
-
if (cur.
|
|
1041
|
+
if (cur.done) return;
|
|
1030
1042
|
if (cur.busy) { await sleepMs(150); continue; }
|
|
1031
1043
|
if (cur.retryAt && Date.now() < cur.retryAt) { await sleepMs(300); continue; }
|
|
1032
1044
|
var latest = stateRef.current.text;
|
|
@@ -1060,19 +1072,15 @@ window.__ModuleLoader__.load({
|
|
|
1060
1072
|
mutate(function (p) {
|
|
1061
1073
|
var next = Object.assign({}, p, { busy: false });
|
|
1062
1074
|
if (r && r.ok) {
|
|
1075
|
+
next.failed = false;
|
|
1076
|
+
next.retryAt = 0;
|
|
1063
1077
|
if (!r.skipped) next.transText = p.transText + r.text;
|
|
1064
1078
|
next.segIdx = Math.max(p.segIdx, nextIdx);
|
|
1065
1079
|
} else {
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
next.failCount = fc;
|
|
1071
|
-
next.retryAt = Date.now() + 2500;
|
|
1072
|
-
setTimeout(function () {
|
|
1073
|
-
mutate(function (q) { return Object.assign({}, q, { retryAt: 0 }); });
|
|
1074
|
-
}, 2600);
|
|
1075
|
-
}
|
|
1080
|
+
// A negative-cache hit is a cooldown, not another failed
|
|
1081
|
+
// network attempt. Resume automatically after it expires.
|
|
1082
|
+
next.failed = true;
|
|
1083
|
+
next.retryAt = Math.max(Date.now() + 2500, r && r.retryAt || Date.now() + 30000);
|
|
1076
1084
|
}
|
|
1077
1085
|
return next;
|
|
1078
1086
|
});
|
|
@@ -1238,9 +1246,9 @@ window.__ModuleLoader__.load({
|
|
|
1238
1246
|
}
|
|
1239
1247
|
|
|
1240
1248
|
function AssistantView(props) {
|
|
1241
|
-
var node = props.node;
|
|
1242
|
-
var data = node.data;
|
|
1243
|
-
var groupPart = props.groupPart;
|
|
1249
|
+
var node = props.node;
|
|
1250
|
+
var data = node.data;
|
|
1251
|
+
var groupPart = props.groupPart;
|
|
1244
1252
|
var useTurnData = props.useTurnData;
|
|
1245
1253
|
var tt = UI_TEXT[useStore().lang] || UI_TEXT.en;
|
|
1246
1254
|
var tail = useTurnData("turn-tail");
|
|
@@ -1259,12 +1267,12 @@ window.__ModuleLoader__.load({
|
|
|
1259
1267
|
var rendered = [];
|
|
1260
1268
|
var last = blocks.length - 1;
|
|
1261
1269
|
for (var i = 0; i < blocks.length; i++) {
|
|
1262
|
-
var block = blocks[i];
|
|
1263
|
-
if (!block) continue;
|
|
1264
|
-
// DSH may mount this step separately for reasoning and response.
|
|
1265
|
-
// Keep original block indexes so streaming-tail detection is unchanged.
|
|
1266
|
-
if (groupPart === "reasoning" && block.kind !== "reasoning") continue;
|
|
1267
|
-
if (groupPart === "response" && block.kind === "reasoning") continue;
|
|
1270
|
+
var block = blocks[i];
|
|
1271
|
+
if (!block) continue;
|
|
1272
|
+
// DSH may mount this step separately for reasoning and response.
|
|
1273
|
+
// Keep original block indexes so streaming-tail detection is unchanged.
|
|
1274
|
+
if (groupPart === "reasoning" && block.kind !== "reasoning") continue;
|
|
1275
|
+
if (groupPart === "response" && block.kind === "reasoning") continue;
|
|
1268
1276
|
if (block.kind === "text") {
|
|
1269
1277
|
rendered.push(createElement(TextBlockSafe, { key: "t" + i, text: block.text, streaming: streaming }));
|
|
1270
1278
|
} else if (block.kind === "reasoning") {
|
|
@@ -1280,20 +1288,20 @@ window.__ModuleLoader__.load({
|
|
|
1280
1288
|
} else {
|
|
1281
1289
|
rendered.push(createElement("pre", { key: "u" + i, className: "xl-pre" }, JSON.stringify(block, null, 2)));
|
|
1282
1290
|
}
|
|
1283
|
-
}
|
|
1284
|
-
var showStopped = interrupted && (groupPart === undefined || groupPart === "response" ||
|
|
1285
|
-
!blocks.some(function (b) { return b && b.kind !== "reasoning" && b.kind !== "tool-call"; }));
|
|
1286
|
-
return createElement("div", null, rendered,
|
|
1287
|
-
showStopped ? createElement("span", { className: "xl-warn" }, (props.t && props.t("message.stopped")) || tt.stopped) : null);
|
|
1291
|
+
}
|
|
1292
|
+
var showStopped = interrupted && (groupPart === undefined || groupPart === "response" ||
|
|
1293
|
+
!blocks.some(function (b) { return b && b.kind !== "reasoning" && b.kind !== "tool-call"; }));
|
|
1294
|
+
return createElement("div", null, rendered,
|
|
1295
|
+
showStopped ? createElement("span", { className: "xl-warn" }, (props.t && props.t("message.stopped")) || tt.stopped) : null);
|
|
1288
1296
|
}
|
|
1289
1297
|
|
|
1290
|
-
function originalBlocksText(blocks, groupPart) {
|
|
1298
|
+
function originalBlocksText(blocks, groupPart) {
|
|
1291
1299
|
var parts = [];
|
|
1292
1300
|
for (var i = 0; i < blocks.length; i++) {
|
|
1293
|
-
var b = blocks[i];
|
|
1294
|
-
if (!b) continue;
|
|
1295
|
-
if (groupPart === "reasoning" && b.kind !== "reasoning") continue;
|
|
1296
|
-
if (groupPart === "response" && b.kind === "reasoning") continue;
|
|
1301
|
+
var b = blocks[i];
|
|
1302
|
+
if (!b) continue;
|
|
1303
|
+
if (groupPart === "reasoning" && b.kind !== "reasoning") continue;
|
|
1304
|
+
if (groupPart === "response" && b.kind === "reasoning") continue;
|
|
1297
1305
|
if ((b.kind === "text" || b.kind === "reasoning") && typeof b.text === "string" && b.text.trim()) parts.push(b.text);
|
|
1298
1306
|
}
|
|
1299
1307
|
return parts;
|
|
@@ -1316,7 +1324,7 @@ window.__ModuleLoader__.load({
|
|
|
1316
1324
|
try {
|
|
1317
1325
|
var node = props && props.node;
|
|
1318
1326
|
var blocks = node && node.data && Array.isArray(node.data.blocks) ? node.data.blocks : [];
|
|
1319
|
-
var parts = originalBlocksText(blocks, props.groupPart);
|
|
1327
|
+
var parts = originalBlocksText(blocks, props.groupPart);
|
|
1320
1328
|
if (parts.length) fb = createElement("div", null, createElement(Markdownish, { text: parts.join("\n\n") }));
|
|
1321
1329
|
} catch (e) { fb = null; }
|
|
1322
1330
|
return createElement(XBoundary, { fallback: fb }, createElement(AssistantView, props));
|
|
@@ -1500,17 +1508,17 @@ window.__ModuleLoader__.load({
|
|
|
1500
1508
|
// `mem` is a plain Map, so clearing it changes nothing React can see: the
|
|
1501
1509
|
// 缓存条目 counter kept the old number until some other state moved.
|
|
1502
1510
|
var tickPair = useState(0);
|
|
1503
|
-
var bumpTick = tickPair[1];
|
|
1504
|
-
useEffect(function () { var notify = function () { bumpTick(function (n) { return n + 1; }); }; statusListeners.add(notify); return function () { statusListeners.delete(notify); }; }, []);
|
|
1505
|
-
// A lower-ranked provider test cannot displace a verified higher-ranked one.
|
|
1506
|
-
var currentCfg = draft.cfg || {};
|
|
1507
|
-
var ids = (currentCfg.chain || []).concat(currentCfg.fallback && currentCfg.fallback.enabled ? currentCfg.fallback.chain || [] : []);
|
|
1508
|
-
var current = null;
|
|
1509
|
-
for (var i = 0; i < ids.length; i++) {
|
|
1510
|
-
var p = currentCfg.providers && currentCfg.providers[ids[i]], check = providerChecks[ids[i]];
|
|
1511
|
-
if (p && (p.enabled || p.source === "dsh") && check && check.ok && check.signature === providerFingerprint(p)) { current = { id: ids[i], model: check.model }; break; }
|
|
1512
|
-
}
|
|
1513
|
-
var inUse = current ? ((t.labels[current.id] || current.id) + (current.model ? " · " + current.model : "")) : (LOCAL_EMPTY[s.lang] || LOCAL_EMPTY.en);
|
|
1511
|
+
var bumpTick = tickPair[1];
|
|
1512
|
+
useEffect(function () { var notify = function () { bumpTick(function (n) { return n + 1; }); }; statusListeners.add(notify); return function () { statusListeners.delete(notify); }; }, []);
|
|
1513
|
+
// A lower-ranked provider test cannot displace a verified higher-ranked one.
|
|
1514
|
+
var currentCfg = draft.cfg || {};
|
|
1515
|
+
var ids = (currentCfg.chain || []).concat(currentCfg.fallback && currentCfg.fallback.enabled ? currentCfg.fallback.chain || [] : []);
|
|
1516
|
+
var current = null;
|
|
1517
|
+
for (var i = 0; i < ids.length; i++) {
|
|
1518
|
+
var p = currentCfg.providers && currentCfg.providers[ids[i]], check = providerChecks[ids[i]];
|
|
1519
|
+
if (p && (p.enabled || p.source === "dsh") && check && check.ok && check.signature === providerFingerprint(p)) { current = { id: ids[i], model: check.model }; break; }
|
|
1520
|
+
}
|
|
1521
|
+
var inUse = current ? ((t.labels[current.id] || current.id) + (current.model ? " · " + current.model : "")) : (LOCAL_EMPTY[s.lang] || LOCAL_EMPTY.en);
|
|
1514
1522
|
return createElement("div", null,
|
|
1515
1523
|
// The clear button rides the "当前使用" row, pushed to that row's end:
|
|
1516
1524
|
// one row per idea (what is in use, then the counters), no stray rows.
|
|
@@ -1617,25 +1625,25 @@ window.__ModuleLoader__.load({
|
|
|
1617
1625
|
// lives here instead, outside React. Refs (cfgRef/seqRef) deliberately do NOT:
|
|
1618
1626
|
// they belong to the instance that owns the in-flight requests.
|
|
1619
1627
|
// ------------------------------------------------------------------
|
|
1620
|
-
var draft = {};
|
|
1621
|
-
var LOCAL_EMPTY = { "zh-CN": "尚未验证可用提供方", en: "No provider verified yet", ja: "提供元は未確認です", ko: "검증된 제공자가 없습니다", es: "Sin proveedor verificado", fr: "Aucun fournisseur vérifié", de: "Noch kein Anbieter geprüft", ru: "Провайдер пока не проверен" };
|
|
1622
|
-
var LOCAL_PHASES = ["checking", "needs_install", "downloading_ollama", "verifying", "installing", "starting", "needs_model", "downloading_model", "loading_model", "ready", "error", "setup", "retry", "manual"];
|
|
1623
|
-
var LOCAL_LABELS = {
|
|
1624
|
-
"zh-CN": ["检查已有 Ollama", "未找到 Ollama", "下载 Ollama", "验证安装包", "安装 Ollama", "启动 Ollama", "所选模型尚未下载", "下载模型", "加载模型", "本地模型已就绪", "准备失败", "一键安装并准备(含模型下载)", "重试准备", "打开官方安装指南"],
|
|
1625
|
-
en: ["Checking existing Ollama", "Ollama not found", "Downloading Ollama", "Verifying installer", "Installing Ollama", "Starting Ollama", "Selected model is not downloaded", "Downloading model", "Loading model", "Local model is ready", "Preparation failed", "Install and prepare (includes model download)", "Retry preparation", "Official installation guide"],
|
|
1626
|
-
ja: ["既存の Ollama を確認", "Ollama が見つかりません", "Ollama をダウンロード", "インストーラーを検証", "Ollama をインストール", "Ollama を起動", "モデルは未ダウンロード", "モデルをダウンロード", "モデルをロード", "ローカルモデル準備完了", "準備に失敗", "インストールして準備(モデルを含む)", "準備を再試行", "公式インストールガイド"],
|
|
1627
|
-
ko: ["기존 Ollama 확인", "Ollama를 찾을 수 없음", "Ollama 다운로드", "설치 파일 검증", "Ollama 설치", "Ollama 시작", "모델이 다운로드되지 않음", "모델 다운로드", "모델 로드", "로컬 모델 준비 완료", "준비 실패", "설치 및 준비 (모델 다운로드 포함)", "준비 다시 시도", "공식 설치 안내"],
|
|
1628
|
-
es: ["Comprobando Ollama", "Ollama no encontrado", "Descargando Ollama", "Verificando instalador", "Instalando Ollama", "Iniciando Ollama", "Modelo sin descargar", "Descargando modelo", "Cargando modelo", "Modelo local listo", "Preparación fallida", "Instalar y preparar (incluye el modelo)", "Reintentar preparación", "Guía oficial de instalación"],
|
|
1629
|
-
fr: ["Vérification de Ollama", "Ollama introuvable", "Téléchargement de Ollama", "Vérification du programme", "Installation de Ollama", "Démarrage de Ollama", "Modèle non téléchargé", "Téléchargement du modèle", "Chargement du modèle", "Modèle local prêt", "Échec de préparation", "Installer et préparer (modèle inclus)", "Réessayer", "Guide officiel"],
|
|
1630
|
-
de: ["Ollama prüfen", "Ollama nicht gefunden", "Ollama herunterladen", "Installationsdatei prüfen", "Ollama installieren", "Ollama starten", "Modell nicht heruntergeladen", "Modell herunterladen", "Modell laden", "Lokales Modell bereit", "Vorbereitung fehlgeschlagen", "Installieren und vorbereiten (mit Modell)", "Erneut vorbereiten", "Offizielle Installationsanleitung"],
|
|
1631
|
-
ru: ["Проверка Ollama", "Ollama не найден", "Загрузка Ollama", "Проверка установщика", "Установка Ollama", "Запуск Ollama", "Модель не скачана", "Скачивание модели", "Загрузка модели", "Локальная модель готова", "Ошибка подготовки", "Установить и подготовить (с моделью)", "Повторить подготовку", "Официальная инструкция"],
|
|
1632
|
-
};
|
|
1633
|
-
function localLabel(phase, lang) { var index = LOCAL_PHASES.indexOf(phase); return (LOCAL_LABELS[lang] || LOCAL_LABELS.en)[index < 0 ? 0 : index]; }
|
|
1634
|
-
function localOrigin(p) {
|
|
1635
|
-
try { var u = new URL(p && p.baseURL); return p.type === "openai" && u.protocol === "http:" &&
|
|
1636
|
-
["localhost", "127.0.0.1", "[::1]"].indexOf(u.hostname) >= 0 && u.port === "11434" && /^\/v1\/?$/.test(u.pathname) && !u.username && !u.password && !u.search && !u.hash ? u.origin : null; }
|
|
1637
|
-
catch (e) { return null; }
|
|
1638
|
-
}
|
|
1628
|
+
var draft = {};
|
|
1629
|
+
var LOCAL_EMPTY = { "zh-CN": "尚未验证可用提供方", en: "No provider verified yet", ja: "提供元は未確認です", ko: "검증된 제공자가 없습니다", es: "Sin proveedor verificado", fr: "Aucun fournisseur vérifié", de: "Noch kein Anbieter geprüft", ru: "Провайдер пока не проверен" };
|
|
1630
|
+
var LOCAL_PHASES = ["checking", "needs_install", "downloading_ollama", "verifying", "installing", "starting", "needs_model", "downloading_model", "loading_model", "ready", "error", "setup", "retry", "manual"];
|
|
1631
|
+
var LOCAL_LABELS = {
|
|
1632
|
+
"zh-CN": ["检查已有 Ollama", "未找到 Ollama", "下载 Ollama", "验证安装包", "安装 Ollama", "启动 Ollama", "所选模型尚未下载", "下载模型", "加载模型", "本地模型已就绪", "准备失败", "一键安装并准备(含模型下载)", "重试准备", "打开官方安装指南"],
|
|
1633
|
+
en: ["Checking existing Ollama", "Ollama not found", "Downloading Ollama", "Verifying installer", "Installing Ollama", "Starting Ollama", "Selected model is not downloaded", "Downloading model", "Loading model", "Local model is ready", "Preparation failed", "Install and prepare (includes model download)", "Retry preparation", "Official installation guide"],
|
|
1634
|
+
ja: ["既存の Ollama を確認", "Ollama が見つかりません", "Ollama をダウンロード", "インストーラーを検証", "Ollama をインストール", "Ollama を起動", "モデルは未ダウンロード", "モデルをダウンロード", "モデルをロード", "ローカルモデル準備完了", "準備に失敗", "インストールして準備(モデルを含む)", "準備を再試行", "公式インストールガイド"],
|
|
1635
|
+
ko: ["기존 Ollama 확인", "Ollama를 찾을 수 없음", "Ollama 다운로드", "설치 파일 검증", "Ollama 설치", "Ollama 시작", "모델이 다운로드되지 않음", "모델 다운로드", "모델 로드", "로컬 모델 준비 완료", "준비 실패", "설치 및 준비 (모델 다운로드 포함)", "준비 다시 시도", "공식 설치 안내"],
|
|
1636
|
+
es: ["Comprobando Ollama", "Ollama no encontrado", "Descargando Ollama", "Verificando instalador", "Instalando Ollama", "Iniciando Ollama", "Modelo sin descargar", "Descargando modelo", "Cargando modelo", "Modelo local listo", "Preparación fallida", "Instalar y preparar (incluye el modelo)", "Reintentar preparación", "Guía oficial de instalación"],
|
|
1637
|
+
fr: ["Vérification de Ollama", "Ollama introuvable", "Téléchargement de Ollama", "Vérification du programme", "Installation de Ollama", "Démarrage de Ollama", "Modèle non téléchargé", "Téléchargement du modèle", "Chargement du modèle", "Modèle local prêt", "Échec de préparation", "Installer et préparer (modèle inclus)", "Réessayer", "Guide officiel"],
|
|
1638
|
+
de: ["Ollama prüfen", "Ollama nicht gefunden", "Ollama herunterladen", "Installationsdatei prüfen", "Ollama installieren", "Ollama starten", "Modell nicht heruntergeladen", "Modell herunterladen", "Modell laden", "Lokales Modell bereit", "Vorbereitung fehlgeschlagen", "Installieren und vorbereiten (mit Modell)", "Erneut vorbereiten", "Offizielle Installationsanleitung"],
|
|
1639
|
+
ru: ["Проверка Ollama", "Ollama не найден", "Загрузка Ollama", "Проверка установщика", "Установка Ollama", "Запуск Ollama", "Модель не скачана", "Скачивание модели", "Загрузка модели", "Локальная модель готова", "Ошибка подготовки", "Установить и подготовить (с моделью)", "Повторить подготовку", "Официальная инструкция"],
|
|
1640
|
+
};
|
|
1641
|
+
function localLabel(phase, lang) { var index = LOCAL_PHASES.indexOf(phase); return (LOCAL_LABELS[lang] || LOCAL_LABELS.en)[index < 0 ? 0 : index]; }
|
|
1642
|
+
function localOrigin(p) {
|
|
1643
|
+
try { var u = new URL(p && p.baseURL); return p.type === "openai" && u.protocol === "http:" &&
|
|
1644
|
+
["localhost", "127.0.0.1", "[::1]"].indexOf(u.hostname) >= 0 && u.port === "11434" && /^\/v1\/?$/.test(u.pathname) && !u.username && !u.password && !u.search && !u.hash ? u.origin : null; }
|
|
1645
|
+
catch (e) { return null; }
|
|
1646
|
+
}
|
|
1639
1647
|
function useDraft(key, initial) {
|
|
1640
1648
|
var pair = useState(key in draft ? draft[key] : initial);
|
|
1641
1649
|
if (!(key in draft)) draft[key] = pair[0];
|
|
@@ -1658,11 +1666,11 @@ window.__ModuleLoader__.load({
|
|
|
1658
1666
|
var pair = useDraft("cfg", null);
|
|
1659
1667
|
var cfg = pair[0], setCfg = pair[1];
|
|
1660
1668
|
var msgPair = useDraft("msg", "");
|
|
1661
|
-
var msg = msgPair[0], setMsg = msgPair[1];
|
|
1662
|
-
var localPair = useDraft("localJob", null);
|
|
1663
|
-
var localJob = localPair[0];
|
|
1664
|
-
var localJobRef = useState(function () { return { current: localJob && localJob.id }; })[0];
|
|
1665
|
-
function setLocalJob(job) { localJobRef.current = job && job.id; localPair[1](job); }
|
|
1669
|
+
var msg = msgPair[0], setMsg = msgPair[1];
|
|
1670
|
+
var localPair = useDraft("localJob", null);
|
|
1671
|
+
var localJob = localPair[0];
|
|
1672
|
+
var localJobRef = useState(function () { return { current: localJob && localJob.id }; })[0];
|
|
1673
|
+
function setLocalJob(job) { localJobRef.current = job && job.id; localPair[1](job); }
|
|
1666
1674
|
var modelsPair = useDraft("models", null);
|
|
1667
1675
|
var models = modelsPair[0], setModels = modelsPair[1];
|
|
1668
1676
|
var modelMenuPair = useDraft("modelMenu", null); var modelMenu = modelMenuPair[0], setModelMenu = modelMenuPair[1];
|
|
@@ -1710,53 +1718,53 @@ window.__ModuleLoader__.load({
|
|
|
1710
1718
|
// Monotonic save sequence: a stale /_xlate/config POST response must
|
|
1711
1719
|
// not overwrite a newer optimistic state (Important-3/4).
|
|
1712
1720
|
var seqRefPair = useState(function () { return { current: 0 }; });
|
|
1713
|
-
var seqRef = seqRefPair[0];
|
|
1714
|
-
var localSeq = useState(function () { return { current: 0 }; })[0];
|
|
1715
|
-
function testProvider(id, sequence) {
|
|
1716
|
-
var testSequence = (providerTestSequence[id] || 0) + 1;
|
|
1717
|
-
providerTestSequence[id] = testSequence;
|
|
1718
|
-
var testedProvider = cfgRef.current.providers[id] || {};
|
|
1719
|
-
var testedModel = testedProvider.model;
|
|
1720
|
-
var testedSignature = providerFingerprint(testedProvider);
|
|
1721
|
-
setMsg(t.testing);
|
|
1722
|
-
return fetch("/_xlate/translate", { method: "POST", headers: { "Content-Type": "application/json" },
|
|
1723
|
-
body: JSON.stringify({ text: "Hello world, connection test. [" + Date.now().toString(36) + "]", target: s.lang, provider: id }) })
|
|
1724
|
-
.then(function (r) { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); })
|
|
1725
|
-
.then(function (j) { if (sequence !== undefined && sequence !== localSeq.current) return;
|
|
1726
|
-
if (providerTestSequence[id] !== testSequence) return;
|
|
1727
|
-
var currentProvider = cfgRef.current.providers[id] || {};
|
|
1728
|
-
if (providerFingerprint(currentProvider) !== testedSignature || (j.model && testedModel && j.model !== testedModel)) return;
|
|
1729
|
-
recordCheck(id, !!(j && j.ok), j.model || testedModel, j.providerSignature || testedSignature);
|
|
1730
|
-
if (j && j.ok) stats.lastError = null;
|
|
1731
|
-
setMsg(j && j.ok ? t.connOk + " ✓ (" + providerLabel(id, (cfgRef.current.providers || {})[id] || {}, t) + (j.model ? " · " + j.model : "") + ")" : (t.connFail + ": " + (j.error || "unknown"))); })
|
|
1732
|
-
.catch(function (e) { if (sequence !== undefined && sequence !== localSeq.current) return;
|
|
1733
|
-
if (providerTestSequence[id] !== testSequence || providerFingerprint(cfgRef.current.providers[id] || {}) !== testedSignature) return;
|
|
1734
|
-
recordCheck(id, false, testedModel, testedSignature);
|
|
1735
|
-
setMsg(t.connFail + ": " + (e && e.message ? e.message : e)); });
|
|
1736
|
-
}
|
|
1737
|
-
function prepareLocal(allowSetup, testAfter, downloadOnly) {
|
|
1738
|
-
var sequence = ++localSeq.current;
|
|
1739
|
-
return fetch("/_xlate/local/prepare", { method: "POST", headers: { "Content-Type": "application/json" },
|
|
1740
|
-
body: JSON.stringify({ allowInstall: allowSetup === true, allowDownload: allowSetup === true || downloadOnly === true }) })
|
|
1741
|
-
.then(function (r) { return r.json().then(function (j) { if (!r.ok || !j.ok) throw new Error(j.error || "HTTP " + r.status); return j; }); })
|
|
1742
|
-
.then(function (j) { if (sequence !== localSeq.current) return; setLocalJob(j.job); if (testAfter) return testProvider("openai", sequence); })
|
|
1743
|
-
.catch(function (e) { if (sequence === localSeq.current) setMsg(localLabel("error", s.lang) + ": " + (e.message || e)); });
|
|
1744
|
-
}
|
|
1745
|
-
var localProvider = cfg && cfg.providers && cfg.providers.openai;
|
|
1746
|
-
var firstEnabled = cfg && (cfg.chain || []).filter(function (id) { var p = cfg.providers[id]; return p && (p.enabled || p.source === "dsh"); })[0];
|
|
1747
|
-
var localKey = firstEnabled === "openai" && localOrigin(localProvider) && localProvider.model ? localProvider.baseURL + "|" + localProvider.model : "";
|
|
1748
|
-
useEffect(function () { if (localKey) prepareLocal(false, false); }, [localKey]);
|
|
1749
|
-
useEffect(function () {
|
|
1750
|
-
if (!localJob || !localJob.running) return;
|
|
1751
|
-
var alive = true;
|
|
1752
|
-
var timer = setInterval(function () {
|
|
1753
|
-
fetch("/_xlate/local/status?id=" + encodeURIComponent(localJob.id)).then(function (r) { return r.json(); }).then(function (j) {
|
|
1754
|
-
if (alive && j.job && localJobRef.current === localJob.id) setLocalJob(j.job);
|
|
1755
|
-
}).catch(function () {});
|
|
1756
|
-
}, 750);
|
|
1757
|
-
return function () { alive = false; clearInterval(timer); };
|
|
1758
|
-
}, [localJob ? localJob.id + "|" + localJob.running : ""]);
|
|
1759
|
-
useEffect(function () { return function () { localSeq.current++; }; }, []);
|
|
1721
|
+
var seqRef = seqRefPair[0];
|
|
1722
|
+
var localSeq = useState(function () { return { current: 0 }; })[0];
|
|
1723
|
+
function testProvider(id, sequence) {
|
|
1724
|
+
var testSequence = (providerTestSequence[id] || 0) + 1;
|
|
1725
|
+
providerTestSequence[id] = testSequence;
|
|
1726
|
+
var testedProvider = cfgRef.current.providers[id] || {};
|
|
1727
|
+
var testedModel = testedProvider.model;
|
|
1728
|
+
var testedSignature = providerFingerprint(testedProvider);
|
|
1729
|
+
setMsg(t.testing);
|
|
1730
|
+
return fetch("/_xlate/translate", { method: "POST", headers: { "Content-Type": "application/json" },
|
|
1731
|
+
body: JSON.stringify({ text: "Hello world, connection test. [" + Date.now().toString(36) + "]", target: s.lang, provider: id }) })
|
|
1732
|
+
.then(function (r) { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); })
|
|
1733
|
+
.then(function (j) { if (sequence !== undefined && sequence !== localSeq.current) return;
|
|
1734
|
+
if (providerTestSequence[id] !== testSequence) return;
|
|
1735
|
+
var currentProvider = cfgRef.current.providers[id] || {};
|
|
1736
|
+
if (providerFingerprint(currentProvider) !== testedSignature || (j.model && testedModel && j.model !== testedModel)) return;
|
|
1737
|
+
recordCheck(id, !!(j && j.ok), j.model || testedModel, j.providerSignature || testedSignature);
|
|
1738
|
+
if (j && j.ok) stats.lastError = null;
|
|
1739
|
+
setMsg(j && j.ok ? t.connOk + " ✓ (" + providerLabel(id, (cfgRef.current.providers || {})[id] || {}, t) + (j.model ? " · " + j.model : "") + ")" : (t.connFail + ": " + (j.error || "unknown"))); })
|
|
1740
|
+
.catch(function (e) { if (sequence !== undefined && sequence !== localSeq.current) return;
|
|
1741
|
+
if (providerTestSequence[id] !== testSequence || providerFingerprint(cfgRef.current.providers[id] || {}) !== testedSignature) return;
|
|
1742
|
+
recordCheck(id, false, testedModel, testedSignature);
|
|
1743
|
+
setMsg(t.connFail + ": " + (e && e.message ? e.message : e)); });
|
|
1744
|
+
}
|
|
1745
|
+
function prepareLocal(allowSetup, testAfter, downloadOnly) {
|
|
1746
|
+
var sequence = ++localSeq.current;
|
|
1747
|
+
return fetch("/_xlate/local/prepare", { method: "POST", headers: { "Content-Type": "application/json" },
|
|
1748
|
+
body: JSON.stringify({ allowInstall: allowSetup === true, allowDownload: allowSetup === true || downloadOnly === true }) })
|
|
1749
|
+
.then(function (r) { return r.json().then(function (j) { if (!r.ok || !j.ok) throw new Error(j.error || "HTTP " + r.status); return j; }); })
|
|
1750
|
+
.then(function (j) { if (sequence !== localSeq.current) return; setLocalJob(j.job); if (testAfter) return testProvider("openai", sequence); })
|
|
1751
|
+
.catch(function (e) { if (sequence === localSeq.current) setMsg(localLabel("error", s.lang) + ": " + (e.message || e)); });
|
|
1752
|
+
}
|
|
1753
|
+
var localProvider = cfg && cfg.providers && cfg.providers.openai;
|
|
1754
|
+
var firstEnabled = cfg && (cfg.chain || []).filter(function (id) { var p = cfg.providers[id]; return p && (p.enabled || p.source === "dsh"); })[0];
|
|
1755
|
+
var localKey = firstEnabled === "openai" && localOrigin(localProvider) && localProvider.model ? localProvider.baseURL + "|" + localProvider.model : "";
|
|
1756
|
+
useEffect(function () { if (localKey) prepareLocal(false, false); }, [localKey]);
|
|
1757
|
+
useEffect(function () {
|
|
1758
|
+
if (!localJob || !localJob.running) return;
|
|
1759
|
+
var alive = true;
|
|
1760
|
+
var timer = setInterval(function () {
|
|
1761
|
+
fetch("/_xlate/local/status?id=" + encodeURIComponent(localJob.id)).then(function (r) { return r.json(); }).then(function (j) {
|
|
1762
|
+
if (alive && j.job && localJobRef.current === localJob.id) setLocalJob(j.job);
|
|
1763
|
+
}).catch(function () {});
|
|
1764
|
+
}, 750);
|
|
1765
|
+
return function () { alive = false; clearInterval(timer); };
|
|
1766
|
+
}, [localJob ? localJob.id + "|" + localJob.running : ""]);
|
|
1767
|
+
useEffect(function () { return function () { localSeq.current++; }; }, []);
|
|
1760
1768
|
// Add/edit inline form state. Lives here (not in a nested component,
|
|
1761
1769
|
// and before the cfg guard) so re-renders — including the cfg=null →
|
|
1762
1770
|
// cfg=value transition — never reset or reorder what the user types;
|
|
@@ -1871,12 +1879,12 @@ window.__ModuleLoader__.load({
|
|
|
1871
1879
|
.catch(function () {});
|
|
1872
1880
|
});
|
|
1873
1881
|
};
|
|
1874
|
-
var startPull = function (m) {
|
|
1875
|
-
if (!m || !m.trim()) return;
|
|
1876
|
-
var name = m.trim();
|
|
1877
|
-
// Download intent prepares an existing service first. Software install
|
|
1878
|
-
// still needs the separately labelled setup button if Ollama is absent.
|
|
1879
|
-
pickModel(name).then(function (saved) { if (saved) prepareLocal(false, false, true); });
|
|
1882
|
+
var startPull = function (m) {
|
|
1883
|
+
if (!m || !m.trim()) return;
|
|
1884
|
+
var name = m.trim();
|
|
1885
|
+
// Download intent prepares an existing service first. Software install
|
|
1886
|
+
// still needs the separately labelled setup button if Ollama is absent.
|
|
1887
|
+
pickModel(name).then(function (saved) { if (saved) prepareLocal(false, false, true); });
|
|
1880
1888
|
};
|
|
1881
1889
|
var pickModel = function (m) {
|
|
1882
1890
|
if (!m) return;
|
|
@@ -1884,9 +1892,9 @@ window.__ModuleLoader__.load({
|
|
|
1884
1892
|
var openai = (cur.providers && cur.providers.openai) || {};
|
|
1885
1893
|
applyLocal({ providers: { openai: { model: m } } });
|
|
1886
1894
|
setModelMenu(null);
|
|
1887
|
-
return fetch("/_xlate/config", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ providers: { openai: { model: m } } }) })
|
|
1895
|
+
return fetch("/_xlate/config", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ providers: { openai: { model: m } } }) })
|
|
1888
1896
|
.then(function (r) { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); })
|
|
1889
|
-
.then(function (c) { cfgRef.current = c; setCfg(c); return c; })
|
|
1897
|
+
.then(function (c) { cfgRef.current = c; setCfg(c); return c; })
|
|
1890
1898
|
.catch(function (e) {
|
|
1891
1899
|
// Do not leave the chip showing a model the host never stored.
|
|
1892
1900
|
setMsg((t.providerSaveError || t.connFail) + ": " + (e && e.message ? e.message : e));
|
|
@@ -1953,9 +1961,9 @@ window.__ModuleLoader__.load({
|
|
|
1953
1961
|
applyLocal({ chain: chain });
|
|
1954
1962
|
saveCfg({ chain: chain });
|
|
1955
1963
|
},
|
|
1956
|
-
test: function (id) {
|
|
1957
|
-
if (id === "openai" && localOrigin(cfgRef.current.providers.openai)) prepareLocal(false, true);
|
|
1958
|
-
else testProvider(id);
|
|
1964
|
+
test: function (id) {
|
|
1965
|
+
if (id === "openai" && localOrigin(cfgRef.current.providers.openai)) prepareLocal(false, true);
|
|
1966
|
+
else testProvider(id);
|
|
1959
1967
|
},
|
|
1960
1968
|
edit: function (id) {
|
|
1961
1969
|
var p = cfgRef.current.providers && cfgRef.current.providers[id];
|
|
@@ -2308,18 +2316,18 @@ window.__ModuleLoader__.load({
|
|
|
2308
2316
|
adding || editingId ? null
|
|
2309
2317
|
: createElement("button", { className: "xl-btn", type: "button", onClick: rescanDsh, style: { marginTop: "4px", marginLeft: "6px" } },
|
|
2310
2318
|
"↻ " + t.dshImport),
|
|
2311
|
-
unusedArea,
|
|
2312
|
-
localJob && localProvider && localJob.model === localProvider.model && localJob.origin === localOrigin(localProvider)
|
|
2313
|
-
? createElement("div", { className: "xl-local-status", role: "status", style: { marginTop: "8px" } },
|
|
2314
|
-
createElement("div", { className: "xl-note" }, localLabel(localJob.phase, s.lang) + " · " + localJob.model +
|
|
2315
|
-
(localJob.percent !== null && localJob.phase.indexOf("downloading") === 0 ? " · " + localJob.percent + "%" : "") +
|
|
2316
|
-
(localJob.completed ? " · " + (localJob.completed / 1048576).toFixed(1) + " MB" + (localJob.total ? " / " + (localJob.total / 1048576).toFixed(1) + " MB" : "") : "")),
|
|
2317
|
-
localJob.running ? createElement("div", { className: "xl-progress" }, createElement("div", { className: "xl-progress-bar", style: { width: (localJob.percent === null ? 15 : localJob.percent) + "%" } })) : null,
|
|
2318
|
-
localJob.error ? createElement("div", { className: "xl-warn" }, localJob.error) : null,
|
|
2319
|
-
!localJob.running && (localJob.phase === "needs_model" || localJob.phase === "error" || (localJob.phase === "needs_install" && localJob.canInstall))
|
|
2320
|
-
? createElement("button", { className: "xl-btn", onClick: function () { prepareLocal(true, true); } }, localLabel(localJob.phase === "error" ? "retry" : "setup", s.lang)) : null,
|
|
2321
|
-
localJob.phase === "needs_install" && !localJob.canInstall ? createElement("a", { href: "https://ollama.com/download", target: "_blank", rel: "noreferrer" }, localLabel("manual", s.lang)) : null)
|
|
2322
|
-
: null,
|
|
2319
|
+
unusedArea,
|
|
2320
|
+
localJob && localProvider && localJob.model === localProvider.model && localJob.origin === localOrigin(localProvider)
|
|
2321
|
+
? createElement("div", { className: "xl-local-status", role: "status", style: { marginTop: "8px" } },
|
|
2322
|
+
createElement("div", { className: "xl-note" }, localLabel(localJob.phase, s.lang) + " · " + localJob.model +
|
|
2323
|
+
(localJob.percent !== null && localJob.phase.indexOf("downloading") === 0 ? " · " + localJob.percent + "%" : "") +
|
|
2324
|
+
(localJob.completed ? " · " + (localJob.completed / 1048576).toFixed(1) + " MB" + (localJob.total ? " / " + (localJob.total / 1048576).toFixed(1) + " MB" : "") : "")),
|
|
2325
|
+
localJob.running ? createElement("div", { className: "xl-progress" }, createElement("div", { className: "xl-progress-bar", style: { width: (localJob.percent === null ? 15 : localJob.percent) + "%" } })) : null,
|
|
2326
|
+
localJob.error ? createElement("div", { className: "xl-warn" }, localJob.error) : null,
|
|
2327
|
+
!localJob.running && (localJob.phase === "needs_model" || localJob.phase === "error" || (localJob.phase === "needs_install" && localJob.canInstall))
|
|
2328
|
+
? createElement("button", { className: "xl-btn", onClick: function () { prepareLocal(true, true); } }, localLabel(localJob.phase === "error" ? "retry" : "setup", s.lang)) : null,
|
|
2329
|
+
localJob.phase === "needs_install" && !localJob.canInstall ? createElement("a", { href: "https://ollama.com/download", target: "_blank", rel: "noreferrer" }, localLabel("manual", s.lang)) : null)
|
|
2330
|
+
: null,
|
|
2323
2331
|
msg ? createElement("div", { className: "xl-note", style: { marginTop: "6px" } }, msg) : null);
|
|
2324
2332
|
}
|
|
2325
2333
|
|