@kevin5251984/guild 0.2.18 → 0.2.20
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/package.json +2 -2
- package/src/catalog/subagents.ts +2 -1
- package/src/chat-parts.ts +6 -1
- package/src/cli.ts +2 -5
- package/src/compact.ts +27 -0
- package/src/db.ts +236 -68
- package/src/generate.ts +59 -28
- package/src/handlers.ts +355 -84
- package/src/harness.ts +39 -10
- package/src/host-browse.ts +149 -4
- package/src/image-gen.ts +2 -1
- package/src/llm.ts +435 -71
- package/src/mcp.ts +32 -5
- package/src/memory.ts +69 -0
- package/src/mention.ts +189 -14
- package/src/oauth.ts +204 -27
- package/src/opencode-free.ts +248 -0
- package/src/public/buddy.js +432 -0
- package/src/public/chat.css +174 -72
- package/src/public/chat.html +354 -69
- package/src/public/i18n.js +44 -6
- package/src/public/index.html +1 -46
- package/src/public/mobile.css +597 -0
- package/src/public/mobile.html +954 -0
- package/src/public/settings.html +239 -35
- package/src/public/skills-add.html +8 -0
- package/src/public/studio.html +171 -117
- package/src/public/style.css +220 -26
- package/src/public/subagents-add.html +8 -0
- package/src/reasoning-catalog.ts +346 -0
- package/src/router.ts +184 -13
- package/src/store.ts +170 -5
- package/src/subagent.ts +239 -7
- package/src/tools.ts +132 -21
- package/src/trajectory.ts +72 -9
- package/src/usage.ts +10 -0
- package/src/version.ts +25 -0
- package/vendor/protocol/src/index.ts +27 -2
package/src/public/settings.html
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<link rel="icon" href="/favicon-32.png" type="image/png" sizes="32x32" />
|
|
9
9
|
<link rel="icon" href="/favicon-16.png" type="image/png" sizes="16x16" />
|
|
10
10
|
<title>Guild — 模型</title>
|
|
11
|
-
<link rel="stylesheet" href="/style.css" />
|
|
11
|
+
<link rel="stylesheet" href="/style.css?v=aux-align" />
|
|
12
12
|
<script src="/i18n.js"></script>
|
|
13
13
|
</head>
|
|
14
14
|
<body class="app settings-page" data-i18n-page="settings" data-section="main">
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
</header>
|
|
47
47
|
<div class="page-body">
|
|
48
48
|
<p id="flash" class="muted"></p>
|
|
49
|
+
<p class="sub" data-i18n="settings.lede">第一次:先連訂閱或填 API key,再套用主模型。沒有模型就不能用。訂閱走 OAuth;API key 寫入 ~/.guild/models.json。</p>
|
|
49
50
|
|
|
50
51
|
<section class="panel" data-sec-panel="main">
|
|
51
52
|
<h2 data-i18n="settings.main">主模型</h2>
|
|
@@ -63,20 +64,17 @@
|
|
|
63
64
|
</div>
|
|
64
65
|
<p class="main-now" id="main-now"></p>
|
|
65
66
|
<div class="toggles">
|
|
66
|
-
<span>
|
|
67
|
-
|
|
68
|
-
<
|
|
69
|
-
|
|
70
|
-
<option value="medium">Medium</option>
|
|
71
|
-
<option value="high">High</option>
|
|
72
|
-
</select>
|
|
67
|
+
<span id="reasoning-wrap">
|
|
68
|
+
<span data-i18n="reasoning">推理強度</span>
|
|
69
|
+
<select id="reasoning"></select>
|
|
70
|
+
</span>
|
|
73
71
|
<label><input type="checkbox" id="fast" /> Fast</label>
|
|
74
72
|
</div>
|
|
75
73
|
<div class="panel-head" style="margin-top:1rem">
|
|
76
74
|
<h2 data-i18n="settings.aux">輔助模型</h2>
|
|
77
75
|
<button type="button" class="linkish" id="reset-aux" data-i18n="settings.auxReset">全部改回主模型</button>
|
|
78
76
|
</div>
|
|
79
|
-
<p class="sub" data-i18n="settings.auxHint"
|
|
77
|
+
<p class="sub" data-i18n="settings.auxHint">只有 Vision、Web extract、SubAgent 可以另選模型。其餘用途一律跟主模型。</p>
|
|
80
78
|
<div id="aux"></div>
|
|
81
79
|
</section>
|
|
82
80
|
|
|
@@ -144,6 +142,18 @@
|
|
|
144
142
|
let auxRole = null;
|
|
145
143
|
let keyTab = 0;
|
|
146
144
|
const PRESETS = {
|
|
145
|
+
"opencode-free": {
|
|
146
|
+
id: "opencode-free",
|
|
147
|
+
name: "OpenCode Free",
|
|
148
|
+
baseUrl: "https://opencode.ai/zen/v1",
|
|
149
|
+
api: "openai-completions",
|
|
150
|
+
apiKey: "",
|
|
151
|
+
models: [
|
|
152
|
+
{ id: "muse-spark-1.2-contributor-free", name: "Muse Spark 1.2 Contributor Free" },
|
|
153
|
+
{ id: "laguna-s-2.1-free", name: "Laguna S 2.1 Free" },
|
|
154
|
+
{ id: "mimo-v2.5-free", name: "Mimo V2.5 Free" },
|
|
155
|
+
],
|
|
156
|
+
},
|
|
147
157
|
openai: {
|
|
148
158
|
id: "openai",
|
|
149
159
|
name: "OpenAI",
|
|
@@ -260,11 +270,56 @@
|
|
|
260
270
|
'<option value="' +
|
|
261
271
|
escapeHtml(m.id) +
|
|
262
272
|
'">' +
|
|
263
|
-
escapeHtml(m.
|
|
273
|
+
escapeHtml(prettyModelName(m.id, m.name)) +
|
|
264
274
|
"</option>",
|
|
265
275
|
)
|
|
266
276
|
.join("");
|
|
267
277
|
if (preferred && preferred.provider === psel.value) msel.value = preferred.model;
|
|
278
|
+
if (psel.id === "provider") fillReasoning();
|
|
279
|
+
}
|
|
280
|
+
function reasonLabel(value) {
|
|
281
|
+
if (value === "none") return t("reason.none");
|
|
282
|
+
if (value === "minimal") return t("reason.min");
|
|
283
|
+
if (value === "low") return t("reason.low");
|
|
284
|
+
if (value === "medium") return t("reason.mid");
|
|
285
|
+
if (value === "high") return t("reason.high");
|
|
286
|
+
if (value === "xhigh") return t("reason.xhigh");
|
|
287
|
+
if (value === "max") return t("reason.max");
|
|
288
|
+
return value || "";
|
|
289
|
+
}
|
|
290
|
+
function fillReasoning() {
|
|
291
|
+
const sel = document.getElementById("reasoning");
|
|
292
|
+
const wrap = document.getElementById("reasoning-wrap");
|
|
293
|
+
const ref = currentRef();
|
|
294
|
+
const group = pickerGroups().find((p) => p.id === (ref && ref.provider));
|
|
295
|
+
const model = ((group && group.models) || []).find(
|
|
296
|
+
(m) => m.id === (ref && ref.model),
|
|
297
|
+
);
|
|
298
|
+
const spec = (model && model.reasoning) || null;
|
|
299
|
+
const raw = spec && spec.supportedEfforts;
|
|
300
|
+
const choices = raw && raw.length
|
|
301
|
+
? spec.mandatory
|
|
302
|
+
? raw.filter((key) => key !== "none")
|
|
303
|
+
: raw.slice()
|
|
304
|
+
: [];
|
|
305
|
+
sel.innerHTML = choices
|
|
306
|
+
.map(
|
|
307
|
+
(value) =>
|
|
308
|
+
'<option value="' +
|
|
309
|
+
escapeHtml(value) +
|
|
310
|
+
'">' +
|
|
311
|
+
escapeHtml(reasonLabel(value)) +
|
|
312
|
+
"</option>",
|
|
313
|
+
)
|
|
314
|
+
.join("");
|
|
315
|
+
const want = data.reasoning;
|
|
316
|
+
if (want && choices.indexOf(want) >= 0) sel.value = want;
|
|
317
|
+
else if (spec && spec.defaultEffort && choices.indexOf(spec.defaultEffort) >= 0) {
|
|
318
|
+
sel.value = spec.defaultEffort;
|
|
319
|
+
} else if (choices.length) {
|
|
320
|
+
sel.value = choices[0];
|
|
321
|
+
}
|
|
322
|
+
if (wrap) wrap.hidden = !choices.length;
|
|
268
323
|
}
|
|
269
324
|
function currentRef() {
|
|
270
325
|
const provider = document.getElementById("provider").value;
|
|
@@ -275,11 +330,27 @@
|
|
|
275
330
|
function groupReady(id) {
|
|
276
331
|
return Boolean((data.picker || []).find((p) => p.id === id && p.ready));
|
|
277
332
|
}
|
|
333
|
+
function prettyModelName(id, given) {
|
|
334
|
+
const raw = String(id || "");
|
|
335
|
+
const label = String(given || "").trim();
|
|
336
|
+
if (label && label !== raw) return label;
|
|
337
|
+
const core = (raw.split("/").pop() || raw).trim();
|
|
338
|
+
const spaced = core
|
|
339
|
+
.replace(/[:_]+/g, "-")
|
|
340
|
+
.replace(/([A-Za-z])(\d)/g, "$1 $2")
|
|
341
|
+
.replace(/-/g, " ")
|
|
342
|
+
.replace(/\s+/g, " ")
|
|
343
|
+
.trim();
|
|
344
|
+
if (!spaced) return raw;
|
|
345
|
+
return spaced.replace(/\b([a-z])/g, function (ch) {
|
|
346
|
+
return ch.toUpperCase();
|
|
347
|
+
});
|
|
348
|
+
}
|
|
278
349
|
function modelLabel(ref) {
|
|
279
350
|
if (!ref || !ref.provider || !ref.model) return t("settings.noDefault");
|
|
280
351
|
const group = pickerGroups().find((p) => p.id === ref.provider);
|
|
281
352
|
const hit = ((group && group.models) || []).find((m) => m.id === ref.model);
|
|
282
|
-
const name = (hit && hit.name)
|
|
353
|
+
const name = prettyModelName(ref.model, hit && hit.name);
|
|
283
354
|
const vendor = (group && group.name) || ref.provider;
|
|
284
355
|
return name + " · " + vendor;
|
|
285
356
|
}
|
|
@@ -330,7 +401,7 @@
|
|
|
330
401
|
}
|
|
331
402
|
function renderPicker() {
|
|
332
403
|
fillSelect(document.getElementById("provider"), pickerGroups(), data.default);
|
|
333
|
-
|
|
404
|
+
fillReasoning();
|
|
334
405
|
document.getElementById("fast").checked = Boolean(data.fast);
|
|
335
406
|
const mainNow = document.getElementById("main-now");
|
|
336
407
|
if (mainNow) {
|
|
@@ -388,12 +459,46 @@
|
|
|
388
459
|
if (key.length <= 10) return key;
|
|
389
460
|
return key.slice(0, 5) + "…" + key.slice(-5);
|
|
390
461
|
}
|
|
462
|
+
function isKeylessProvider(id) {
|
|
463
|
+
return id === "opencode-free" || id === "free" || id === "opencode_free";
|
|
464
|
+
}
|
|
391
465
|
function storedBadge(p) {
|
|
466
|
+
if (isKeylessProvider(p.id)) {
|
|
467
|
+
return '<span class="badge">' + t("settings.keyless") + "</span>";
|
|
468
|
+
}
|
|
392
469
|
if (p.stored === "env") return '<span class="badge">env</span>';
|
|
393
470
|
if (p.stored === "literal") return '<span class="badge">' + t("settings.filled") + "</span>";
|
|
394
471
|
return '<span class="badge empty">' + t("settings.emptyKey") + "</span>";
|
|
395
472
|
}
|
|
473
|
+
function probeFor(id) {
|
|
474
|
+
return ((data.probe || []).find((row) => row.id === id) || null);
|
|
475
|
+
}
|
|
476
|
+
function probeBadge(id) {
|
|
477
|
+
const row = probeFor(id);
|
|
478
|
+
if (!row) return "";
|
|
479
|
+
if (row.ok && row.status === 429) {
|
|
480
|
+
return '<span class="badge wait probe-flag">' + t("settings.probeBusy") + "</span>";
|
|
481
|
+
}
|
|
482
|
+
if (row.ok) return '<span class="badge probe-flag">' + t("settings.probeOk") + "</span>";
|
|
483
|
+
return '<span class="badge empty probe-flag">' + t("settings.probeFail") + "</span>";
|
|
484
|
+
}
|
|
485
|
+
function probeHint() {
|
|
486
|
+
const probe = data.probe || [];
|
|
487
|
+
if (!probe.length) return "";
|
|
488
|
+
const skip = probe.filter((row) => !row.ok);
|
|
489
|
+
if (!skip.length) return "";
|
|
490
|
+
return (
|
|
491
|
+
'<p class="hint">' +
|
|
492
|
+
t("settings.syncedSkip", {
|
|
493
|
+
ok: probe.filter((row) => row.ok).length,
|
|
494
|
+
names: skip.map((row) => row.id).join("、"),
|
|
495
|
+
}) +
|
|
496
|
+
"</p>"
|
|
497
|
+
);
|
|
498
|
+
}
|
|
396
499
|
function cardHtml(p, index) {
|
|
500
|
+
const lock = isKeylessProvider(p.id);
|
|
501
|
+
const ro = lock ? " readonly" : "";
|
|
397
502
|
const models = (p.models || [])
|
|
398
503
|
.map((m, i) => {
|
|
399
504
|
return (
|
|
@@ -401,7 +506,9 @@
|
|
|
401
506
|
'" data-f="id" value="' + escapeHtml(m.id || "") +
|
|
402
507
|
'" placeholder="model id" /><input data-p="' + index + '" data-m="' + i +
|
|
403
508
|
'" data-f="name" value="' + escapeHtml(m.name || "") +
|
|
404
|
-
'" placeholder="' + t("settings.displayName") + '"
|
|
509
|
+
'" placeholder="' + t("settings.displayName") + '" />' +
|
|
510
|
+
probeBadge(m.id) +
|
|
511
|
+
'<button type="button" class="secondary" data-del-model="' +
|
|
405
512
|
index + ":" + i + '">×</button></div>'
|
|
406
513
|
);
|
|
407
514
|
})
|
|
@@ -409,19 +516,38 @@
|
|
|
409
516
|
return (
|
|
410
517
|
'<div class="provider-card" data-index="' + index + '">' +
|
|
411
518
|
'<div class="card-head"><h3>' + escapeHtml(p.name || p.id) + storedBadge(p) +
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
519
|
+
"</h3>" +
|
|
520
|
+
(lock
|
|
521
|
+
? ""
|
|
522
|
+
: '<button type="button" class="secondary" data-del-provider="' + index + '">' + t("settings.delete") + "</button>") +
|
|
523
|
+
"</div>" +
|
|
524
|
+
"<label>ID</label><input data-p=\"" + index + '" data-f="id" value="' + escapeHtml(p.id) + '"' + ro + " />" +
|
|
525
|
+
"<label>" + t("settings.name") + "</label><input data-p=\"" + index + '" data-f="name" value="' + escapeHtml(p.name || "") + '"' + ro + " />" +
|
|
526
|
+
"<label>baseUrl</label><input data-p=\"" + index + '" data-f="baseUrl" value="' + escapeHtml(p.baseUrl || "") + '"' + ro + " />" +
|
|
527
|
+
'<div class="row-2"><div><label>API</label><select data-p="' + index + '" data-f="api"' +
|
|
528
|
+
(lock ? " disabled" : "") + ">" +
|
|
417
529
|
'<option value="openai-completions"' + (p.api !== "anthropic-messages" && p.api !== "openai-responses" ? " selected" : "") + ">openai-completions</option>" +
|
|
418
530
|
'<option value="anthropic-messages"' + (p.api === "anthropic-messages" ? " selected" : "") + ">anthropic-messages</option>" +
|
|
419
531
|
'<option value="openai-responses"' + (p.api === "openai-responses" ? " selected" : "") + ">openai-responses</option></select></div>" +
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
532
|
+
(isKeylessProvider(p.id)
|
|
533
|
+
? '<div><label>API key</label><p class="hint">' + t("settings.keyless") + "</p></div>"
|
|
534
|
+
: '<div><label>API key</label><input type="text" data-p="' + index + '" data-f="apiKey" spellcheck="false" autocomplete="off" autocapitalize="off" value="' +
|
|
535
|
+
escapeHtml(p.apiKeyPreview || "") + '" placeholder="$ENV_VAR 或 sk-…" /></div>') +
|
|
536
|
+
"</div>" +
|
|
537
|
+
(isKeylessProvider(p.id)
|
|
538
|
+
? '<p class="hint">' + t("settings.opencodeFreeHint") + "</p>"
|
|
539
|
+
: "") +
|
|
540
|
+
'<div class="model-head"><label>' + t("model") + "</label>" +
|
|
541
|
+
(isKeylessProvider(p.id)
|
|
542
|
+
? '<button type="button" class="sync-free" data-sync-free="1"><span class="sync-ico" aria-hidden="true">↻</span>' + t("settings.sync") + "</button>"
|
|
543
|
+
: "") +
|
|
544
|
+
"</div>" +
|
|
545
|
+
models +
|
|
546
|
+
(lock ? probeHint() : "") +
|
|
423
547
|
'<button type="button" class="secondary" data-add-model="' + index + '" style="margin-top:0.55rem">' + t("settings.addModel") + "</button>" +
|
|
424
|
-
'<div class="card-foot"><p class="hint">' +
|
|
548
|
+
'<div class="card-foot"><p class="hint">' +
|
|
549
|
+
t(isKeylessProvider(p.id) ? "settings.saveKeylessHint" : "settings.saveKeyHint") +
|
|
550
|
+
"</p>" +
|
|
425
551
|
'<button type="button" data-save-keys="' + index + '">' +
|
|
426
552
|
t("settings.saveKey") +
|
|
427
553
|
"</button></div></div>"
|
|
@@ -434,11 +560,19 @@
|
|
|
434
560
|
const p = (data.providers || [])[index];
|
|
435
561
|
if (!p) return;
|
|
436
562
|
const id = card.querySelector('[data-f="id"]').value.trim();
|
|
437
|
-
if (
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
563
|
+
if (isKeylessProvider(p.id) || isKeylessProvider(id)) {
|
|
564
|
+
p.id = "opencode-free";
|
|
565
|
+
p.name = "OpenCode Free";
|
|
566
|
+
p.baseUrl = "https://opencode.ai/zen/v1";
|
|
567
|
+
p.api = "openai-completions";
|
|
568
|
+
} else {
|
|
569
|
+
if (id) p.id = id;
|
|
570
|
+
p.name = card.querySelector('[data-f="name"]').value.trim();
|
|
571
|
+
p.baseUrl = card.querySelector('[data-f="baseUrl"]').value.trim();
|
|
572
|
+
p.api = card.querySelector('[data-f="api"]').value;
|
|
573
|
+
}
|
|
574
|
+
const keyEl = card.querySelector('[data-f="apiKey"]');
|
|
575
|
+
const key = keyEl ? keyEl.value.trim() : "";
|
|
442
576
|
if (key && key !== (p.apiKeyPreview || "")) {
|
|
443
577
|
p.apiKey = key;
|
|
444
578
|
p.apiKeyPreview = maskApiKey(key);
|
|
@@ -454,8 +588,7 @@
|
|
|
454
588
|
});
|
|
455
589
|
p.models = models;
|
|
456
590
|
}
|
|
457
|
-
function
|
|
458
|
-
flushVisibleCard();
|
|
591
|
+
function snapshotProviders() {
|
|
459
592
|
const providers = {};
|
|
460
593
|
(data.providers || []).forEach((p) => {
|
|
461
594
|
if (!p.id) return;
|
|
@@ -469,6 +602,10 @@
|
|
|
469
602
|
});
|
|
470
603
|
return providers;
|
|
471
604
|
}
|
|
605
|
+
function collectKeys() {
|
|
606
|
+
flushVisibleCard();
|
|
607
|
+
return snapshotProviders();
|
|
608
|
+
}
|
|
472
609
|
function presetLabel(key, preset) {
|
|
473
610
|
return key === "custom" ? t("settings.custom") : (preset && preset.name) || key;
|
|
474
611
|
}
|
|
@@ -601,6 +738,7 @@
|
|
|
601
738
|
data.default,
|
|
602
739
|
);
|
|
603
740
|
});
|
|
741
|
+
document.getElementById("model").addEventListener("change", fillReasoning);
|
|
604
742
|
document.getElementById("apply").addEventListener("click", () => applyPicker());
|
|
605
743
|
document.getElementById("reset-aux").addEventListener("click", async () => {
|
|
606
744
|
await applyPicker({ aux: {} });
|
|
@@ -730,6 +868,68 @@
|
|
|
730
868
|
document.getElementById("add-provider-cancel").addEventListener("click", () => {
|
|
731
869
|
document.getElementById("add-provider-dialog").close();
|
|
732
870
|
});
|
|
871
|
+
async function syncOpenCodeFree() {
|
|
872
|
+
const btn = document.querySelector("[data-sync-free]");
|
|
873
|
+
if (btn) btn.disabled = true;
|
|
874
|
+
flash("", t("settings.syncing"));
|
|
875
|
+
try {
|
|
876
|
+
const res = await fetch("/settings/models/opencode-free/sync", {
|
|
877
|
+
method: "POST",
|
|
878
|
+
});
|
|
879
|
+
const body = await res.json().catch(() => ({}));
|
|
880
|
+
if (!res.ok) {
|
|
881
|
+
flash("error", body.error || t("settings.syncFailed"));
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
data = body;
|
|
885
|
+
const probe = Array.isArray(body.probe) ? body.probe : [];
|
|
886
|
+
const ok = probe.filter((row) => row.ok).length;
|
|
887
|
+
const skip = probe.filter((row) => !row.ok);
|
|
888
|
+
render();
|
|
889
|
+
if (probe.length && !ok) {
|
|
890
|
+
flash("error", t("settings.syncedNone"));
|
|
891
|
+
} else if (skip.length) {
|
|
892
|
+
flash(
|
|
893
|
+
"ok",
|
|
894
|
+
t("settings.syncedSkip", {
|
|
895
|
+
ok: ok,
|
|
896
|
+
names: skip.map((row) => row.id).join("、"),
|
|
897
|
+
}),
|
|
898
|
+
);
|
|
899
|
+
} else {
|
|
900
|
+
const n =
|
|
901
|
+
ok ||
|
|
902
|
+
(((data.picker || []).find((p) => p.id === "opencode-free") || {}).models || [])
|
|
903
|
+
.length;
|
|
904
|
+
flash("ok", t("settings.synced", { n: n }));
|
|
905
|
+
}
|
|
906
|
+
} catch (err) {
|
|
907
|
+
flash("error", err.message || t("settings.syncFailed"));
|
|
908
|
+
} finally {
|
|
909
|
+
if (btn) btn.disabled = false;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
async function deleteProvider(index) {
|
|
913
|
+
const doomed = (data.providers || [])[index];
|
|
914
|
+
if (!doomed || isKeylessProvider(doomed.id)) return;
|
|
915
|
+
const name = doomed.name || doomed.id;
|
|
916
|
+
data.providers.splice(index, 1);
|
|
917
|
+
if (keyTab >= data.providers.length) keyTab = Math.max(0, data.providers.length - 1);
|
|
918
|
+
const res = await fetch("/settings/models", {
|
|
919
|
+
method: "PUT",
|
|
920
|
+
headers: { "content-type": "application/json" },
|
|
921
|
+
body: JSON.stringify({ providers: snapshotProviders() }),
|
|
922
|
+
});
|
|
923
|
+
const body = await res.json().catch(() => ({}));
|
|
924
|
+
if (!res.ok) {
|
|
925
|
+
flash("error", body.error || t("deleteFailed"));
|
|
926
|
+
await load();
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
data = body;
|
|
930
|
+
render();
|
|
931
|
+
flash("health", t("settings.deletedProvider", { name: name }));
|
|
932
|
+
}
|
|
733
933
|
async function saveProviderCard(index) {
|
|
734
934
|
const card = document.querySelector('.provider-card[data-index="' + index + '"]');
|
|
735
935
|
const label = card
|
|
@@ -762,17 +962,21 @@
|
|
|
762
962
|
if (p) input.value = p.apiKeyPreview || "";
|
|
763
963
|
});
|
|
764
964
|
document.getElementById("providers").addEventListener("click", (event) => {
|
|
765
|
-
const el = event.target;
|
|
965
|
+
const el = event.target.closest ? event.target : event.target.parentElement;
|
|
966
|
+
if (!el || !el.closest) return;
|
|
766
967
|
if (el.closest("[data-save-keys]")) {
|
|
767
968
|
const i = Number(el.closest("[data-save-keys]").getAttribute("data-save-keys"));
|
|
768
969
|
saveProviderCard(i);
|
|
769
970
|
return;
|
|
770
971
|
}
|
|
771
|
-
if (el.closest("[data-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
972
|
+
if (el.closest("[data-sync-free]")) {
|
|
973
|
+
syncOpenCodeFree();
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
if (el.closest("[data-del-provider]")) {
|
|
977
|
+
const i = Number(el.closest("[data-del-provider]").getAttribute("data-del-provider"));
|
|
978
|
+
deleteProvider(i);
|
|
979
|
+
return;
|
|
776
980
|
}
|
|
777
981
|
if (el.closest("[data-add-model]")) {
|
|
778
982
|
flushVisibleCard();
|
|
@@ -95,7 +95,12 @@
|
|
|
95
95
|
return;
|
|
96
96
|
}
|
|
97
97
|
const button = document.getElementById("ai-generate");
|
|
98
|
+
if (button.disabled || button.getAttribute("aria-busy") === "true") return;
|
|
98
99
|
button.disabled = true;
|
|
100
|
+
button.setAttribute("aria-busy", "true");
|
|
101
|
+
button.classList.add("is-busy");
|
|
102
|
+
const idle = button.textContent;
|
|
103
|
+
button.textContent = t("studio.generating");
|
|
99
104
|
try {
|
|
100
105
|
const response = await fetch("/generate", {
|
|
101
106
|
method: "POST",
|
|
@@ -115,6 +120,9 @@
|
|
|
115
120
|
);
|
|
116
121
|
} finally {
|
|
117
122
|
button.disabled = false;
|
|
123
|
+
button.removeAttribute("aria-busy");
|
|
124
|
+
button.classList.remove("is-busy");
|
|
125
|
+
button.textContent = idle;
|
|
118
126
|
}
|
|
119
127
|
});
|
|
120
128
|
|