bareloop 0.40.0 → 0.41.0
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/CHANGELOG.md +33 -0
- package/bareloop.context.md +97 -124
- package/package.json +2 -2
- package/src/authorflow.js +9 -9
- package/src/authorjob.js +4 -4
- package/src/authorrun.js +25 -25
- package/src/authorscout.js +4 -4
- package/src/bundle.js +7 -4
- package/src/bundlerun.js +4 -1
- package/src/cli.js +1 -1
- package/src/config.js +129 -29
- package/src/draftspend.js +4 -4
- package/src/index.js +2 -4
- package/src/interviewrun.js +17 -25
- package/src/job.js +13 -7
- package/src/judged.js +11 -43
- package/src/keysfile.js +15 -7
- package/src/kinds.js +2 -2
- package/src/modellist.js +84 -0
- package/src/monthly.js +23 -43
- package/src/panel/authorroutes.js +19 -14
- package/src/panel/authorsession.js +62 -58
- package/src/panel/index.html +520 -145
- package/src/panel/runroutes.js +3 -0
- package/src/panel/server.js +35 -15
- package/src/panel/settingsroutes.js +232 -173
- package/src/planrun.js +2 -2
- package/src/providerrows.js +226 -141
- package/src/providers.js +58 -332
- package/src/providershapes.js +18 -0
- package/src/replay.js +1 -1
- package/src/thinking.js +130 -0
- package/src/userrun.js +50 -73
- package/types/authorflow.d.ts +5 -5
- package/types/authorjob.d.ts +2 -2
- package/types/authorscout.d.ts +2 -2
- package/types/config.d.ts +23 -6
- package/types/draftspend.d.ts +8 -8
- package/types/index.d.ts +1 -1
- package/types/job.d.ts +5 -5
- package/types/judged.d.ts +5 -36
- package/types/keysfile.d.ts +15 -4
- package/types/modellist.d.ts +50 -0
- package/types/monthly.d.ts +4 -3
- package/types/panel/authorsession.d.ts +2 -9
- package/types/panel/server.d.ts +14 -4
- package/types/providerrows.d.ts +192 -99
- package/types/providers.d.ts +34 -159
- package/types/providershapes.d.ts +10 -0
- package/types/thinking.d.ts +90 -0
- package/types/userrun.d.ts +6 -1
package/src/panel/index.html
CHANGED
|
@@ -530,19 +530,92 @@
|
|
|
530
530
|
.money-item input{width:100%;height:44px;box-sizing:border-box;}
|
|
531
531
|
.keyfile-strip{position:relative;border:1px solid var(--border-strong);padding:12px 12px 8px;background:var(--panel2);margin:14px 0;font-size:12px;display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;}
|
|
532
532
|
.keyfile-strip::before{content:"┤ KEYS ├";position:absolute;top:-9px;left:10px;background:var(--panel2);padding:0 4px;font-size:11px;color:var(--text-faint);letter-spacing:0.08em;}
|
|
533
|
-
.keyfile-strip-main{display:flex;flex-direction:column;gap:3px;flex:1;min-width:
|
|
534
|
-
.keyfile-strip-actions{display:flex;gap:6px;flex:none;}
|
|
533
|
+
.keyfile-strip-main{display:flex;flex-direction:column;gap:3px;flex:1;min-width:0;overflow-wrap:anywhere;}
|
|
534
|
+
.keyfile-strip-actions{display:flex;flex-wrap:wrap;gap:6px;flex:none;max-width:100%;}
|
|
535
535
|
.keyfile-strip code{background:var(--bg);border:1px solid var(--border-strong);padding:1px 5px;font-size:11px;color:var(--amber);overflow-wrap:anywhere;}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
.pv-
|
|
542
|
-
.pv-
|
|
536
|
+
/* M4d Providers layout F (hamr approved 2026-10-11): ONE shared grid, so a key row, the model sub-head and
|
|
537
|
+
every model row sit on the same column edges. Key band: KEY | API SHAPE | BASE URL (two columns) | OLD MAX_TOKENS | TOKENS | refresh.
|
|
538
|
+
Model row: [USE 44px][MODEL] under KEY, IN + OUT side by side under BASE URL, TOKENS on the key band's TOKENS edge. A key row has a
|
|
539
|
+
solid 1px line, a model row a dotted 1px hairline. Below 1280px every row collapses to stacked label : value lines. */
|
|
540
|
+
.pv-grid{--pv-cols:minmax(0,2.8fr) minmax(0,1.5fr) minmax(0,.95fr) minmax(0,.95fr) minmax(0,.9fr) minmax(0,.6fr) 272px;font-size:13px;}
|
|
541
|
+
.pv-head,.pv-krow,.pv-mhead,.pv-mrow{display:grid;grid-template-columns:var(--pv-cols);column-gap:8px;align-items:center;padding:0 8px;min-width:0;}
|
|
542
|
+
.pv-head > *,.pv-krow > *,.pv-mhead > *,.pv-mrow > *{min-width:0;}
|
|
543
|
+
.pv-head,.pv-mhead{font-size:11px;font-weight:700;color:var(--text-dim);letter-spacing:0.04em;min-height:26px;}
|
|
544
|
+
.pv-head{background:var(--panel2);border-bottom:1px solid var(--border-strong);}
|
|
545
|
+
.pv-mhead{background:var(--bg);border-bottom:1px dotted var(--border-strong);}
|
|
546
|
+
.pv-key{border-top:1px solid var(--border-strong);}
|
|
547
|
+
.pv-krow{min-height:40px;padding-top:4px;padding-bottom:4px;}
|
|
548
|
+
.pv-c-url{grid-column:3 / span 2;}
|
|
549
|
+
.pv-lbl{display:none;}
|
|
550
|
+
.pv-name{display:flex;align-items:center;flex-wrap:wrap;gap:4px 6px;min-width:0;overflow-wrap:anywhere;}
|
|
551
|
+
.pv-name code{font-weight:700;}
|
|
552
|
+
.pv-ok{color:var(--green);font-weight:700;}
|
|
553
|
+
.pv-fold{border:0;background:none;color:var(--text);font:inherit;padding:0 4px 0 0;min-width:22px;min-height:24px;cursor:pointer;text-align:left;}
|
|
554
|
+
.pv-krow select,.pv-krow input[type=text]{width:100%;max-width:100%;min-height:28px;box-sizing:border-box;}
|
|
555
|
+
.pv-grid select,.pv-grid input[type=text]{font-size:12px;height:28px;padding:2px 6px;}
|
|
556
|
+
.pv-legacy{display:inline-flex;align-items:center;gap:6px;cursor:pointer;min-height:28px;text-transform:none;letter-spacing:0;font-weight:400;font-size:12px;}
|
|
557
|
+
.pv-legacy input,.pv-m-use input{width:16px;height:16px;margin:0;accent-color:var(--accent);cursor:pointer;}
|
|
558
|
+
.pv-tok{text-align:right;font-variant-numeric:tabular-nums;}
|
|
559
|
+
.pv-refcell{display:flex;justify-content:flex-end;}
|
|
560
|
+
.pv-refbox{display:flex;flex-direction:column;align-items:flex-start;max-width:100%;}
|
|
561
|
+
.pv-refbtns{display:flex;gap:6px;}
|
|
562
|
+
.pv-refbox .pv-msg{width:0;min-width:100%;overflow-wrap:anywhere;margin:2px 0 0;}
|
|
563
|
+
.pv-refbox .pv-msg:empty{display:none;}
|
|
564
|
+
.pv-refbtn{white-space:nowrap;flex:0 0 auto;}
|
|
565
|
+
.pv-refbtn:disabled{opacity:.5;cursor:not-allowed;}
|
|
566
|
+
.pv-sum{color:var(--text-faint);font-size:11px;font-weight:400;}
|
|
567
|
+
.pv-key[data-open="1"] .pv-sum{display:none;}
|
|
568
|
+
.pv-knote{padding:0 8px 4px 34px;margin:0;}
|
|
569
|
+
.pv-knote:empty{display:none;}
|
|
570
|
+
.pv-knote > .hint:empty{display:none;}
|
|
571
|
+
.pv-key[data-open="0"] .pv-models{display:none;}
|
|
572
|
+
.pv-mrow{border-top:1px dotted var(--border-strong);min-height:32px;}
|
|
573
|
+
.pv-mrow:hover,.pv-mrow:focus-within{background:var(--panel2);}
|
|
574
|
+
.pv-um{grid-column:1;display:flex;align-items:center;gap:0;min-width:0;}
|
|
575
|
+
.pv-uh,.pv-m-use{flex:0 0 44px;width:44px;display:flex;align-items:center;justify-content:center;}
|
|
576
|
+
.pv-m-use{min-height:32px;}
|
|
577
|
+
.pv-m-name{min-width:0;overflow-wrap:anywhere;font-size:12px;}
|
|
578
|
+
.pv-pair{grid-column:3 / span 2;display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);column-gap:8px;align-items:center;min-width:0;}
|
|
579
|
+
.pv-pair > *{min-width:0;}
|
|
580
|
+
.pv-pair input[type=text]{width:100%;text-align:right;}
|
|
581
|
+
.pv-mhead .pv-tok{text-align:right;}
|
|
582
|
+
.pv-mhead .pv-tok,.pv-m-tok{grid-column:6;}
|
|
583
|
+
.pv-m-msg{grid-column:1 / -1;padding:2px 8px 2px 52px;margin:0;}
|
|
584
|
+
.pv-m-msg:empty{display:none;}
|
|
585
|
+
.pv-more{padding:3px 8px 3px 34px;border-top:1px dotted var(--border-strong);}
|
|
586
|
+
.pv-more:empty{display:none;}
|
|
587
|
+
.pv-more .pv-add{border:0;background:none;color:var(--accent);font:inherit;font-size:12px;padding:2px 0;min-height:24px;cursor:pointer;text-decoration:underline;}
|
|
588
|
+
.pv-more.pv-failed{color:var(--amber);background:var(--amber-bg);font-size:12px;}
|
|
589
|
+
.pv-typed{display:flex;flex-wrap:wrap;align-items:center;gap:6px 10px;margin-top:4px;}
|
|
590
|
+
.pv-typed input{flex:1 1 160px;min-width:0;max-width:260px;min-height:28px;box-sizing:border-box;}
|
|
591
|
+
.pv-typed .hint{flex:1 1 100%;margin:0;}
|
|
592
|
+
.hint.bad{color:var(--red);}
|
|
593
|
+
.field label.jf-thinking{display:flex;flex-direction:row;align-items:center;justify-content:flex-start;gap:6px;margin-top:6px;font-size:12px;text-transform:none;letter-spacing:0;}
|
|
594
|
+
.field label.jf-thinking input{width:auto;min-height:0;margin:0;}
|
|
543
595
|
.table-wrap{overflow-x:auto;border:1px solid var(--border-strong);background:var(--panel2);max-width:100%;}
|
|
544
596
|
@media (max-width: 899px){
|
|
545
597
|
.money-strip{grid-template-columns:1fr 1fr;}
|
|
598
|
+
.keyfile-strip-main{flex:1 1 100%;}
|
|
599
|
+
}
|
|
600
|
+
@media (max-width: 1279px){
|
|
601
|
+
.pv-head,.pv-mhead{display:none;}
|
|
602
|
+
.pv-krow,.pv-mrow{display:block;padding:2px 8px;}
|
|
603
|
+
.pv-cell{display:grid;grid-template-columns:104px minmax(0,1fr);gap:8px;align-items:center;min-height:44px;border-top:1px dotted var(--border);}
|
|
604
|
+
.pv-cell:first-child{border-top:none;}
|
|
605
|
+
.pv-lbl{display:block;font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:var(--text-dim);font-weight:700;}
|
|
606
|
+
.pv-cell.nolbl{display:flex;}
|
|
607
|
+
.pv-tok{text-align:left;}
|
|
608
|
+
.pv-grid select,.pv-grid input[type=text]{height:44px;font-size:14px;}
|
|
609
|
+
.pv-krow select,.pv-krow input[type=text]{min-height:44px;}
|
|
610
|
+
.pv-legacy{min-height:44px;gap:10px;}
|
|
611
|
+
.pv-legacy input,.pv-m-use input{width:24px;height:24px;}
|
|
612
|
+
.pv-um{min-height:44px;}
|
|
613
|
+
.pv-uh,.pv-m-use{min-height:44px;}
|
|
614
|
+
.pv-cell.pv-pair{display:grid;grid-template-columns:104px minmax(0,1fr) minmax(0,1fr);}
|
|
615
|
+
.pv-cell.pv-pair::before{content:"In / Out $/1M";font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:var(--text-dim);font-weight:700;}
|
|
616
|
+
.pv-fold,.pv-refbtn,.pv-more .pv-add{min-height:44px;min-width:44px;}
|
|
617
|
+
.pv-knote,.pv-more{padding-left:8px;}
|
|
618
|
+
.pv-m-msg{padding-left:8px;}
|
|
546
619
|
}
|
|
547
620
|
@media (max-width: 899px){
|
|
548
621
|
.main{flex-direction:column;}
|
|
@@ -615,6 +688,9 @@
|
|
|
615
688
|
<!-- build item 5 (2026-09-28) — ONE line under Model, not two:
|
|
616
689
|
key + reachability collapsed into a single readiness read. -->
|
|
617
690
|
<div class="jf-readiness unknown" id="jf-model-status" data-testid="jf-model-status"></div>
|
|
691
|
+
<!-- P8: the job's "Thinking on" tick (drafting + the rubric judge; never the worker). The SERVER's per-line `thinking.canChange` decides. -->
|
|
692
|
+
<label class="jf-thinking" id="jf-thinking-label"><input type="checkbox" id="jf-thinking" data-testid="jf-thinking"> Thinking on</label>
|
|
693
|
+
<div class="hint" id="jf-thinking-note" data-testid="jf-thinking-note"></div>
|
|
618
694
|
</div>
|
|
619
695
|
<div class="field">
|
|
620
696
|
<label for="jf-name">Job name</label>
|
|
@@ -815,30 +891,26 @@
|
|
|
815
891
|
</div>
|
|
816
892
|
|
|
817
893
|
<div class="pane-body">
|
|
818
|
-
<!-- PROVIDERS TAB (
|
|
894
|
+
<!-- PROVIDERS TAB (P8: one row per key in the keys file that has a value; layout rewritten from fwdloop src/panel/index.html@f9449e8) -->
|
|
819
895
|
<section id="panel-providers" class="tabpanel active" role="tabpanel" aria-labelledby="tab-providers">
|
|
820
|
-
<div class="keyfile-strip" data-testid="
|
|
896
|
+
<div class="keyfile-strip" data-testid="keys-strip">
|
|
821
897
|
<div class="keyfile-strip-main">
|
|
822
|
-
<span>Put <code>NAME=key</code> lines in
|
|
823
|
-
<span class="hint" id="
|
|
824
|
-
<span class="hint" id="
|
|
898
|
+
<span>Put <code>NAME=key</code> lines in <code id="keys-path" data-testid="keys-path">~/.config/bareloop/.env</code>, then Reload keys — each row below says whether its key is set. Only names are listed; keys never reach this page.</span>
|
|
899
|
+
<span class="hint bad" id="keys-refusal" data-testid="keys-refusal"></span>
|
|
900
|
+
<span class="hint" id="keys-open-note" data-testid="keys-open-note"></span>
|
|
825
901
|
</div>
|
|
826
902
|
<div class="keyfile-strip-actions">
|
|
827
|
-
<button class="btn small"
|
|
828
|
-
<button class="btn small"
|
|
903
|
+
<button class="btn small" id="open-keys-folder" type="button" data-testid="open-keys-folder" aria-label="Open keys folder">Open keys folder</button>
|
|
904
|
+
<button class="btn small" id="reload-keys" type="button" data-testid="reload-keys">Reload keys</button>
|
|
829
905
|
</div>
|
|
830
906
|
</div>
|
|
831
907
|
<div class="table-wrap" data-testid="provider-table-wrap">
|
|
832
|
-
<
|
|
833
|
-
<
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
<tbody id="pv-rows"></tbody>
|
|
837
|
-
</table>
|
|
908
|
+
<div class="pv-grid" data-testid="provider-table">
|
|
909
|
+
<div class="pv-head" aria-hidden="true"><span>KEY</span><span>API SHAPE</span><span class="pv-c-url">BASE URL</span><span>OLD MAX_TOKENS</span><span class="pv-tok">TOKENS</span><span></span></div>
|
|
910
|
+
<div id="pv-rows"></div>
|
|
911
|
+
</div>
|
|
838
912
|
</div>
|
|
839
|
-
<div class="hint" id="pv-
|
|
840
|
-
<div id="pv-shell" data-testid="pv-shell"></div>
|
|
841
|
-
<div class="hint" id="pv-note" data-testid="pv-note" style="margin-top:6px;">Name is the model id Chat uses. Base URL blank = the API shape's own host. Test asks the provider for its model list — it costs nothing.</div>
|
|
913
|
+
<div class="hint" id="pv-note" data-testid="pv-note" style="margin-top:6px;">⟳ refresh models (free) · old max_tokens: tick if the server wants the old field name</div>
|
|
842
914
|
</section>
|
|
843
915
|
|
|
844
916
|
<!-- MONEY & LIMITS TAB -->
|
|
@@ -2327,7 +2399,7 @@
|
|
|
2327
2399
|
// hand it to the Chat tab's New job card. The page decides nothing: the server owns what is locked and what is open.
|
|
2328
2400
|
// `model` (optional) = the Chat card's Model box, when the picker asks: the estimate line then counts only that worker's runs.
|
|
2329
2401
|
function reuseWorkflow(runid, model){
|
|
2330
|
-
authorGet("/api/author/start-from?runid=" + encodeURIComponent(runid) + (model ? "&
|
|
2402
|
+
authorGet("/api/author/start-from?runid=" + encodeURIComponent(runid) + (model ? "&" + model : "")).then(function(r){
|
|
2331
2403
|
if(!r || !r.ok){ window.alert((r && r.error) || "nothing to start from"); return; }
|
|
2332
2404
|
document.getElementById("tab-chat").click();
|
|
2333
2405
|
document.dispatchEvent(new CustomEvent("bareloop-reuse", {detail: {runid: runid, prefill: r}}));
|
|
@@ -3478,7 +3550,7 @@
|
|
|
3478
3550
|
document.getElementById("btn-settings").addEventListener("click", function(){
|
|
3479
3551
|
mainView.hidden = true;
|
|
3480
3552
|
settingsView.hidden = false;
|
|
3481
|
-
loadProviders();
|
|
3553
|
+
loadProviders(true);
|
|
3482
3554
|
loadMoney();
|
|
3483
3555
|
});
|
|
3484
3556
|
document.getElementById("settings-back").addEventListener("click", function(){
|
|
@@ -3487,7 +3559,7 @@
|
|
|
3487
3559
|
});
|
|
3488
3560
|
document.getElementById("tab-money").addEventListener("click", loadMoney);
|
|
3489
3561
|
|
|
3490
|
-
// ---- Providers tab (
|
|
3562
|
+
// ---- Providers tab (P8): one block per key in the keys file that has a value; rewritten from fwdloop src/panel/index.html@f9449e8 ----
|
|
3491
3563
|
var pvRows = document.getElementById("pv-rows");
|
|
3492
3564
|
var pvNote = document.getElementById("pv-note");
|
|
3493
3565
|
var PV_NOTE_DEFAULT = pvNote.textContent;
|
|
@@ -3497,104 +3569,316 @@
|
|
|
3497
3569
|
if(n >= 1e3) return (n / 1e3).toFixed(1) + "k";
|
|
3498
3570
|
return String(n);
|
|
3499
3571
|
}
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
+ '<td><input type="text" class="pv-name" data-testid="pv-name-' + e + '" placeholder="model id" value="' + escapeXml(row.name) + '"></td>'
|
|
3518
|
-
+ '<td><select class="pv-shape" data-testid="pv-shape-' + e + '">' + shapeOpts + "</select></td>"
|
|
3519
|
-
+ '<td><input type="text" class="pv-url" data-testid="pv-url-' + e + '" placeholder="' + escapeXml(row.placeholder || "the shape\'s own host") + '" value="' + escapeXml(row.baseUrl) + '"></td>'
|
|
3520
|
-
+ '<td class="pv-test-cell">' + (row.canTest
|
|
3521
|
-
? '<button class="btn small pv-test" type="button" data-testid="test-connection-' + e + '">Test</button><div class="hint pv-test-result" data-testid="test-result-' + e + '"></div>'
|
|
3522
|
-
: '<span class="hint">not available</span>') + "</td>"
|
|
3523
|
-
+ '<td data-testid="pv-tokens-' + e + '">' + escapeXml(tokensText(row.tokens)) + "</td>"
|
|
3524
|
-
+ "<td>" + bal + "</td>"
|
|
3525
|
-
+ '<td class="pv-price-cell"><input type="text" inputmode="decimal" class="pv-price-in" data-testid="pv-price-in-' + e + '" placeholder="not set" value="' + (row.priceInPerM === null ? "" : escapeXml(String(row.priceInPerM))) + '"></td>'
|
|
3526
|
-
+ '<td class="pv-price-cell"><input type="text" inputmode="decimal" class="pv-price-out" data-testid="pv-price-out-' + e + '" placeholder="not set" value="' + (row.priceOutPerM === null ? "" : escapeXml(String(row.priceOutPerM))) + '"></td></tr>';
|
|
3527
|
-
}).join("") : '<tr><td colspan="9" class="hint" data-testid="pv-empty">no keys with a value yet — add NAME=key to the keys file, then Reload keys</td></tr>';
|
|
3528
|
-
pvRows.querySelectorAll(".pv-balance-fetch").forEach(function(fetchEl){
|
|
3529
|
-
authorGet("/api/settings/providers/balance?env=" + encodeURIComponent(fetchEl.getAttribute("data-env"))).then(function(b){
|
|
3530
|
-
fetchEl.textContent = b && b.text ? b.text : ("— " + ((b && b.note) || "unavailable"));
|
|
3531
|
-
}).catch(function(){ fetchEl.textContent = "— unavailable"; });
|
|
3532
|
-
});
|
|
3533
|
-
// a key exported only in the shell has no row: say so, by NAME, once per name
|
|
3534
|
-
document.getElementById("pv-shell").innerHTML = (r.shellOnly || []).map(function(n){
|
|
3535
|
-
return '<div class="hint" data-testid="pv-shell-' + escapeXml(n) + '">' + escapeXml(n) + " is set in your shell but not in ~/.config/bareloop/.env — add it there to use it here and in Chat.</div>";
|
|
3536
|
-
}).join("");
|
|
3537
|
-
pvNote.textContent = r.configProblem ? r.configProblem : PV_NOTE_DEFAULT;
|
|
3572
|
+
// A hint line: plain text, red when it is a refusal.
|
|
3573
|
+
function setNote(el, bad, text){
|
|
3574
|
+
el.className = "hint" + (bad ? " bad" : "");
|
|
3575
|
+
el.textContent = text || "";
|
|
3576
|
+
}
|
|
3577
|
+
// The server's own plain sentence (`say`); the page never shows a code or an HTTP status.
|
|
3578
|
+
function settingsRefusal(r){
|
|
3579
|
+
var b = r && r.body;
|
|
3580
|
+
if(b && typeof b.say === "string" && b.say) return b.say;
|
|
3581
|
+
return "Settings cannot do that. Keys are typed into your keys file by hand.";
|
|
3582
|
+
}
|
|
3583
|
+
function networkText(){ return "Could not reach the panel, so nothing is confirmed. Check it is still running."; }
|
|
3584
|
+
// `list` true = also ask each provider for its model list (free): on page open and on Reload keys. A save repaints without asking again.
|
|
3585
|
+
function loadProviders(list){
|
|
3586
|
+
return authorGet("/api/settings/providers").then(function(d){
|
|
3587
|
+
if(!d || !d.ok){ setNote(document.getElementById("keys-refusal"), true, "Could not read your providers. Reload the page."); return; }
|
|
3588
|
+
paintProviders(d, list === true);
|
|
3538
3589
|
document.dispatchEvent(new CustomEvent("bareloop-providers-changed"));
|
|
3539
|
-
}
|
|
3590
|
+
}, function(){ setNote(document.getElementById("keys-refusal"), true, networkText()); });
|
|
3591
|
+
}
|
|
3592
|
+
// Open keys folder: the server opens the FOLDER in the file manager; if it cannot, the path shows as text
|
|
3593
|
+
document.getElementById("open-keys-folder").addEventListener("click", function(){
|
|
3594
|
+
var out = document.getElementById("keys-open-note");
|
|
3595
|
+
out.textContent = "";
|
|
3596
|
+
authorPost("/api/settings/open-keys-folder", {}).then(function(r){
|
|
3597
|
+
var b = r.body || {};
|
|
3598
|
+
if(b.ok === true) out.textContent = b.opened ? "" : "Could not open it here. Your keys folder: " + (b.path || "(unknown)");
|
|
3599
|
+
else out.textContent = settingsRefusal(r);
|
|
3600
|
+
}, function(){ out.textContent = networkText(); });
|
|
3601
|
+
});
|
|
3602
|
+
document.getElementById("reload-keys").addEventListener("click", function(){ loadProviders(true); });
|
|
3603
|
+
|
|
3604
|
+
// M4d Providers layout F. One parent block per key (.pv-key): its key band (name, API shape select, Base URL box, the legacy tick, tokens,
|
|
3605
|
+
// the Refresh button), a folded summary, and its model lines as child rows (.pv-mrow). The key row owns shape, Base URL and the legacy
|
|
3606
|
+
// tick, so a model line can never drift from them. Shape and Base URL are always editable; a change of either saves both (judged together).
|
|
3607
|
+
var folds = {}; // slot -> true (unfolded) / false (folded): set once, on first paint (a key with a ticked model starts unfolded), then only the person changes it
|
|
3608
|
+
var typedOpen = {}; // slot -> true while "+ type a model name" is open
|
|
3609
|
+
function buildProviderRow(row){
|
|
3610
|
+
var key = document.createElement("div");
|
|
3611
|
+
key.className = "pv-key";
|
|
3612
|
+
key.setAttribute("data-slot", row.slot);
|
|
3613
|
+
key.setAttribute("data-testid", "provider-row-" + row.slot);
|
|
3614
|
+
key.innerHTML =
|
|
3615
|
+
'<div class="pv-krow">' +
|
|
3616
|
+
'<div class="pv-cell pv-name nolbl"><button class="pv-fold" type="button" data-act="fold" data-testid="pv-fold"></button><code></code><span class="pv-status" data-f="status" data-testid="pv-status-' + escapeXml(row.slot) + '"></span><span class="pv-sum" data-f="sum" data-testid="pv-sum"></span></div>' +
|
|
3617
|
+
'<div class="pv-cell"><span class="pv-lbl">API shape</span><select data-prov="shape" data-testid="prov-shape" aria-label="' + escapeXml(row.label + " API shape") + '"></select></div>' +
|
|
3618
|
+
'<div class="pv-cell pv-c-url"><span class="pv-lbl">Base URL</span><input type="text" data-prov="baseUrl" data-testid="prov-baseurl" aria-label="' + escapeXml(row.label + " Base URL") + '" autocomplete="off" spellcheck="false" placeholder="type the Base URL"></div>' +
|
|
3619
|
+
'<div class="pv-cell"><span class="pv-lbl">Old max_tokens</span><label class="pv-legacy"><input type="checkbox" data-prov="legacyMaxTokens" data-testid="prov-legacy" aria-label="' + escapeXml(row.label + " legacy max_tokens") + '"><span data-f="legacy-word">off</span></label></div>' +
|
|
3620
|
+
'<div class="pv-cell pv-tok pv-ktok"><span class="pv-lbl">Tokens</span><span data-f="tokens" data-testid="pv-tokens-' + escapeXml(row.slot) + '"></span></div>' +
|
|
3621
|
+
'<div class="pv-cell pv-refcell nolbl"><div class="pv-refbox"><div class="pv-refbtns"><button class="btn small pv-refbtn" type="button" data-act="test-key" data-testid="prov-test" aria-label="' + escapeXml("Test " + row.envVar) + '">Test</button><button class="btn small pv-refbtn" type="button" data-act="refresh-models" data-testid="prov-refresh" aria-label="' + escapeXml("Refresh models for " + row.label) + '">⟳ Refresh</button></div><div class="hint pv-msg" data-f="msg-test" data-testid="prov-msg-test" role="status"></div></div></div>' +
|
|
3622
|
+
'</div>' +
|
|
3623
|
+
'<div class="pv-knote"><div class="hint bad pv-msg" data-f="msg-edit" data-testid="prov-msg-edit"></div><div class="hint pv-msg" data-f="msg-legacyMaxTokens" data-testid="prov-msg-legacyMaxTokens"></div></div>' +
|
|
3624
|
+
'<div class="pv-models" data-models-for="' + escapeXml(row.slot) + '" data-testid="models-row-' + escapeXml(row.slot) + '">' +
|
|
3625
|
+
'<div class="pv-mhead" aria-hidden="true"><span class="pv-um"><span class="pv-uh">USE</span><span>MODEL</span></span><span class="pv-pair"><span>IN $/1M</span><span>OUT $/1M</span></span><span class="pv-tok">TOKENS</span></div>' +
|
|
3626
|
+
'<div data-f="models"></div>' +
|
|
3627
|
+
'<div class="pv-more"><button class="pv-add" type="button" data-act="typed-open" data-testid="model-typed-open">+ type a model name</button>' +
|
|
3628
|
+
'<div class="pv-typed" data-f="typed" hidden><input type="text" data-typed="1" data-testid="model-typed" autocomplete="off" spellcheck="false" aria-label="' + escapeXml(row.label + " model name") + '" placeholder="type a model name"> ' +
|
|
3629
|
+
'<button class="btn small" type="button" data-act="add-model" data-testid="model-add">Add</button><div class="hint pv-msg" data-f="typed-msg" data-testid="model-typed-msg"></div></div></div>' +
|
|
3630
|
+
'</div>';
|
|
3631
|
+
key.querySelector("code").textContent = row.envVar;
|
|
3632
|
+
key.querySelectorAll("input").forEach(function(inp){
|
|
3633
|
+
inp.addEventListener("input", function(){ inp.setAttribute("data-dirty", "1"); });
|
|
3634
|
+
});
|
|
3635
|
+
key.querySelector("select").addEventListener("change", function(ev){ ev.target.setAttribute("data-dirty", "1"); });
|
|
3636
|
+
return key;
|
|
3540
3637
|
}
|
|
3541
|
-
//
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3638
|
+
// One child row per model: [Use][name], In + Out side by side, Tokens. The price pair is both-or-neither; the server's one rule says why not.
|
|
3639
|
+
function buildModelLine(slot, m){
|
|
3640
|
+
var d = document.createElement("div");
|
|
3641
|
+
d.className = "pv-mrow";
|
|
3642
|
+
d.setAttribute("data-model", m.model);
|
|
3643
|
+
d.setAttribute("data-testid", "model-line");
|
|
3644
|
+
d.innerHTML =
|
|
3645
|
+
'<div class="pv-cell pv-um nolbl"><span class="pv-m-use"><input type="checkbox" data-mf="use" data-testid="model-use"></span><code class="pv-m-name"></code></div>' +
|
|
3646
|
+
'<div class="pv-cell pv-pair"><label><input type="text" inputmode="decimal" data-mf="inPerM" data-testid="model-in" autocomplete="off"></label>' +
|
|
3647
|
+
'<label><input type="text" inputmode="decimal" data-mf="outPerM" data-testid="model-out" autocomplete="off"></label></div>' +
|
|
3648
|
+
'<div class="pv-cell pv-tok pv-m-tok"><span class="pv-lbl">Tokens</span><span data-f="model-tokens" data-testid="model-tokens"></span></div>' +
|
|
3649
|
+
'<div class="hint pv-msg pv-m-msg" data-f="msg-model" data-testid="model-msg"></div>';
|
|
3650
|
+
d.querySelector(".pv-m-name").textContent = m.model;
|
|
3651
|
+
d.querySelector("[data-mf=use]").setAttribute("aria-label", slot + " " + m.model + " use");
|
|
3652
|
+
d.querySelector("[data-mf=inPerM]").setAttribute("aria-label", m.model + " price in per 1M tokens");
|
|
3653
|
+
d.querySelector("[data-mf=outPerM]").setAttribute("aria-label", m.model + " price out per 1M tokens");
|
|
3654
|
+
d.querySelectorAll("input[type=text]").forEach(function(inp){
|
|
3655
|
+
inp.addEventListener("input", function(){ inp.setAttribute("data-dirty", "1"); });
|
|
3656
|
+
});
|
|
3657
|
+
return d;
|
|
3658
|
+
}
|
|
3659
|
+
// The provider's own model list per key (asked on page open, Reload keys, Refresh models): { models: [ids] } or { why }. Not saved: only ticked or typed lines are.
|
|
3660
|
+
var listed = {};
|
|
3661
|
+
var lastData = null;
|
|
3662
|
+
function linesFor(row){
|
|
3663
|
+
var l = listed[row.slot];
|
|
3664
|
+
var have = {};
|
|
3665
|
+
row.models.forEach(function(m){ have[m.model] = true; });
|
|
3666
|
+
var extra = (l && l.models ? l.models : []).filter(function(id){ return !have[id]; }).map(function(id){ return { model: id, use: false }; });
|
|
3667
|
+
return row.models.concat(extra);
|
|
3668
|
+
}
|
|
3669
|
+
// "3 models · none ticked", "2 models · 1 ticked"; a list that failed with nothing saved says so.
|
|
3670
|
+
function sumText(row){
|
|
3671
|
+
var l = listed[row.slot];
|
|
3672
|
+
var lines = linesFor(row);
|
|
3673
|
+
if(l && l.why && lines.length === 0) return "failed — type a name";
|
|
3674
|
+
var ticked = lines.filter(function(m){ return m.use === true; }).length;
|
|
3675
|
+
return lines.length + (lines.length === 1 ? " model" : " models") + " · " + (ticked === 0 ? "none ticked" : ticked + " ticked");
|
|
3676
|
+
}
|
|
3677
|
+
function paintModels(row, mr){
|
|
3678
|
+
var box = mr.querySelector("[data-f=models]");
|
|
3679
|
+
var lines = linesFor(row);
|
|
3680
|
+
var l = listed[row.slot];
|
|
3681
|
+
var typed = mr.querySelector("[data-f=typed]");
|
|
3682
|
+
typed.hidden = typedOpen[row.slot] !== true;
|
|
3683
|
+
var wanted = lines.map(function(m){ return m.model; });
|
|
3684
|
+
Array.prototype.slice.call(box.querySelectorAll(".pv-mrow")).forEach(function(el){
|
|
3685
|
+
if(wanted.indexOf(el.getAttribute("data-model")) < 0) box.removeChild(el);
|
|
3686
|
+
});
|
|
3687
|
+
var note = box.querySelector("[data-f=models-note]");
|
|
3688
|
+
lines.forEach(function(m){
|
|
3689
|
+
var line = Array.prototype.find.call(box.querySelectorAll(".pv-mrow"), function(el){ return el.getAttribute("data-model") === m.model; });
|
|
3690
|
+
if(!line){ line = buildModelLine(row.slot, m); box.insertBefore(line, note); }
|
|
3691
|
+
var use = line.querySelector("[data-mf=use]");
|
|
3692
|
+
use.checked = m.use === true;
|
|
3693
|
+
["inPerM", "outPerM"].forEach(function(f){
|
|
3694
|
+
var inp = line.querySelector('[data-mf="' + f + '"]');
|
|
3695
|
+
inp.placeholder = "—";
|
|
3696
|
+
if(inp.getAttribute("data-dirty") !== "1") inp.value = typeof m[f] === "number" ? String(m[f]) : "";
|
|
3697
|
+
});
|
|
3698
|
+
line.querySelector("[data-f=model-tokens]").textContent = typeof m.tokens === "number" ? tokensText(m.tokens) : "—";
|
|
3699
|
+
});
|
|
3700
|
+
note = box.querySelector("[data-f=models-note]");
|
|
3701
|
+
if(!note){ note = document.createElement("div"); note.className = "hint pv-more"; note.setAttribute("data-f", "models-note"); note.setAttribute("data-testid", "models-note"); box.appendChild(note); }
|
|
3702
|
+
setNote(note, false, l && l.why ? l.why + " Type the model name below." : (lines.length === 0 ? "No models yet. Press ⟳ to refresh models, or type a name." : ""));
|
|
3703
|
+
note.className += " pv-more" + (l && l.why ? " pv-failed" : "");
|
|
3704
|
+
box.parentNode.querySelector(".pv-mhead").hidden = lines.length === 0;
|
|
3705
|
+
}
|
|
3706
|
+
var STATUS_WORD = { "set in file": "set in file", "found in shell": "found in shell", "not set": "not set", "refused": "refused — see above" };
|
|
3707
|
+
// The Refresh button: "[ ⟳ Refresh ]", disabled "[ ⟳ refreshing… ]" while the free list call is out.
|
|
3708
|
+
function setRefreshBtn(key, pending){
|
|
3709
|
+
var b = key.querySelector("[data-act=refresh-models]");
|
|
3710
|
+
b.disabled = pending === true;
|
|
3711
|
+
b.innerHTML = pending === true ? "⟳ refreshing…" : "⟳ Refresh";
|
|
3550
3712
|
}
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
var
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
}).catch(function(){ pvNote.textContent = "could not save"; });
|
|
3713
|
+
// The Test button: "[ Test ]", disabled "[ testing… ]" while the free call is out. The route says ok + ms or a plain-words why; written on the key row's own note.
|
|
3714
|
+
function testKey(slot, key){
|
|
3715
|
+
var b = key.querySelector("[data-act=test-key]");
|
|
3716
|
+
var msg = key.querySelector("[data-f=msg-test]");
|
|
3717
|
+
var row = lastData && lastData.rows.filter(function(r){ return r.slot === slot; })[0];
|
|
3718
|
+
if(row && row.baseUrl === ""){
|
|
3719
|
+
setNote(msg, true, "Type a Base URL for " + row.envVar + " in Settings.");
|
|
3559
3720
|
return;
|
|
3560
3721
|
}
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3722
|
+
b.disabled = true;
|
|
3723
|
+
b.innerHTML = "testing…";
|
|
3724
|
+
setNote(msg, false, "");
|
|
3725
|
+
authorPost("/api/settings/test", { slot: slot }).then(function(r){
|
|
3726
|
+
var d = r.body;
|
|
3727
|
+
if(d && d.ok === true) setNote(msg, false, "OK \u00b7 " + d.ms + " ms");
|
|
3728
|
+
else setNote(msg, true, d && d.why ? d.why : settingsRefusal(r));
|
|
3729
|
+
}, function(){ setNote(msg, true, networkText()); }).then(function(){
|
|
3730
|
+
b.disabled = false;
|
|
3731
|
+
b.innerHTML = "Test";
|
|
3569
3732
|
});
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3733
|
+
}
|
|
3734
|
+
function refreshModels(slot, tr){
|
|
3735
|
+
listed[slot] = { pending: true };
|
|
3736
|
+
setRefreshBtn(tr, true);
|
|
3737
|
+
return authorPost("/api/settings/models", { slot: slot }).then(function(r){
|
|
3738
|
+
var b = r.body;
|
|
3739
|
+
listed[slot] = b && b.ok ? { models: b.models } : { why: (b && b.why) ? b.why : settingsRefusal(r) };
|
|
3740
|
+
}, function(){ listed[slot] = { why: networkText() }; }).then(function(){
|
|
3741
|
+
if(lastData) paintProviders(lastData, false);
|
|
3742
|
+
});
|
|
3743
|
+
}
|
|
3744
|
+
function paintProviders(data, list){
|
|
3745
|
+
lastData = data;
|
|
3746
|
+
var path = data.keysFile && data.keysFile.path ? data.keysFile.path : "~/.config/bareloop/.env";
|
|
3747
|
+
document.getElementById("keys-path").textContent = path;
|
|
3748
|
+
setNote(document.getElementById("keys-refusal"), true, data.keysFile && data.keysFile.refusal);
|
|
3749
|
+
setNote(pvNote, !!data.configProblem, data.configProblem || PV_NOTE_DEFAULT);
|
|
3750
|
+
data.rows.forEach(function(row){
|
|
3751
|
+
var key = pvRows.querySelector('.pv-key[data-slot="' + row.slot + '"]');
|
|
3752
|
+
if(!key){ key = buildProviderRow(row); pvRows.appendChild(key); }
|
|
3753
|
+
if(folds[row.slot] === undefined) folds[row.slot] = row.models.some(function(m){ return m.use === true; });
|
|
3754
|
+
key.setAttribute("data-open", folds[row.slot] ? "1" : "0");
|
|
3755
|
+
var fold = key.querySelector("[data-act=fold]");
|
|
3756
|
+
fold.textContent = folds[row.slot] ? "▾" : "▸";
|
|
3757
|
+
fold.setAttribute("aria-expanded", folds[row.slot] ? "true" : "false");
|
|
3758
|
+
fold.setAttribute("aria-label", (folds[row.slot] ? "Fold " : "Unfold ") + row.label + " models");
|
|
3759
|
+
paintModels(row, key.querySelector(".pv-models"));
|
|
3760
|
+
setRefreshBtn(key, !!(listed[row.slot] && listed[row.slot].pending));
|
|
3761
|
+
key.querySelector("[data-f=sum]").textContent = sumText(row);
|
|
3762
|
+
var st = key.querySelector("[data-f=status]");
|
|
3763
|
+
var word = STATUS_WORD[row.keyStatus] || row.keyStatus;
|
|
3764
|
+
st.className = row.keyStatus === "set in file" || row.keyStatus === "found in shell" ? "pv-status pv-ok" : "pv-status hint";
|
|
3765
|
+
st.textContent = st.className.indexOf("pv-ok") >= 0 ? "✓" : word;
|
|
3766
|
+
st.setAttribute("title", word);
|
|
3767
|
+
if(st.className.indexOf("pv-ok") >= 0) st.setAttribute("aria-label", word); else st.removeAttribute("aria-label");
|
|
3768
|
+
var legacyIn = key.querySelector('[data-prov=legacyMaxTokens]');
|
|
3769
|
+
if(document.activeElement !== legacyIn) legacyIn.checked = row.legacyMaxTokens === true;
|
|
3770
|
+
key.querySelector("[data-f=legacy-word]").textContent = legacyIn.checked ? "on" : "off";
|
|
3771
|
+
// shape + Base URL: the key row's own boxes, written from the server's row unless the person has typed in them (a refused value stays typed, with its sentence)
|
|
3772
|
+
var sel = key.querySelector("[data-prov=shape]");
|
|
3773
|
+
if(sel.options.length !== data.shapes.length || data.shapes.some(function(sh, i){ return sel.options[i].value !== sh.id; })){
|
|
3774
|
+
sel.textContent = "";
|
|
3775
|
+
data.shapes.forEach(function(sh){ var o = document.createElement("option"); o.value = sh.id; o.textContent = sh.label; sel.appendChild(o); });
|
|
3776
|
+
}
|
|
3777
|
+
if(sel.getAttribute("data-dirty") !== "1") sel.value = row.shape;
|
|
3778
|
+
var urlIn = key.querySelector("[data-prov=baseUrl]");
|
|
3779
|
+
if(urlIn.getAttribute("data-dirty") !== "1" && document.activeElement !== urlIn) urlIn.value = row.baseUrl;
|
|
3780
|
+
key.querySelector("[data-f=tokens]").textContent = tokensText(row.tokens);
|
|
3781
|
+
});
|
|
3782
|
+
// rows that left the keys file leave the page
|
|
3783
|
+
Array.prototype.slice.call(pvRows.querySelectorAll(".pv-key")).forEach(function(key){
|
|
3784
|
+
if(!data.rows.some(function(r){ return r.slot === key.getAttribute("data-slot"); })) pvRows.removeChild(key);
|
|
3785
|
+
});
|
|
3786
|
+
if(list) data.rows.forEach(function(row){
|
|
3787
|
+
var key = pvRows.querySelector('.pv-key[data-slot="' + row.slot + '"]');
|
|
3788
|
+
if(row.baseUrl === ""){ listed[row.slot] = { why: "Type a Base URL for " + row.slot + " in Settings." }; paintProviders(lastData, false); }
|
|
3789
|
+
else refreshModels(row.slot, key);
|
|
3790
|
+
});
|
|
3791
|
+
}
|
|
3792
|
+
// Shape and Base URL save together, on change of either (blur / Enter / pick); the server judges them as a pair and says which sentence on
|
|
3793
|
+
// a refusal (nothing saved, the typed value stays so the person can fix it).
|
|
3794
|
+
function saveEndpoint(key){
|
|
3795
|
+
var msg = key.querySelector("[data-f=msg-edit]");
|
|
3796
|
+
var payload = { slot: key.getAttribute("data-slot"), shape: key.querySelector("[data-prov=shape]").value, baseUrl: key.querySelector("[data-prov=baseUrl]").value.trim() };
|
|
3797
|
+
authorPost("/api/settings/provider", payload).then(function(r){
|
|
3798
|
+
if(r.body && r.body.ok){
|
|
3799
|
+
key.querySelector("[data-prov=shape]").removeAttribute("data-dirty");
|
|
3800
|
+
key.querySelector("[data-prov=baseUrl]").removeAttribute("data-dirty");
|
|
3801
|
+
setNote(msg, true, "");
|
|
3802
|
+
loadProviders();
|
|
3803
|
+
} else setNote(msg, true, settingsRefusal(r));
|
|
3804
|
+
}, function(){ setNote(msg, true, networkText()); });
|
|
3805
|
+
}
|
|
3806
|
+
pvRows.addEventListener("click", function(ev){
|
|
3807
|
+
var b = ev.target.closest && ev.target.closest("button[data-act]");
|
|
3808
|
+
if(!b) return;
|
|
3809
|
+
var key = b.closest(".pv-key");
|
|
3810
|
+
var slot = key.getAttribute("data-slot");
|
|
3811
|
+
var act = b.getAttribute("data-act");
|
|
3812
|
+
if(act === "refresh-models") refreshModels(slot, key);
|
|
3813
|
+
else if(act === "test-key") testKey(slot, key);
|
|
3814
|
+
else if(act === "fold"){ folds[slot] = !folds[slot]; paintProviders(lastData, false); }
|
|
3815
|
+
else if(act === "typed-open"){ typedOpen[slot] = !typedOpen[slot]; paintProviders(lastData, false); }
|
|
3816
|
+
else if(act === "add-model") addTypedModel(slot, b.closest("[data-f=typed]"));
|
|
3586
3817
|
});
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3818
|
+
// Saved like bareloop saves a row field: on change (blur or Enter), no Save button. The legacy tick saves alone; shape and Base URL save as a pair.
|
|
3819
|
+
pvRows.addEventListener("change", function(ev){
|
|
3820
|
+
var inp = ev.target;
|
|
3821
|
+
if(!inp.getAttribute) return;
|
|
3822
|
+
if(inp.getAttribute("data-prov") === "legacyMaxTokens"){ saveProvider(inp.closest(".pv-key"), inp); return; }
|
|
3823
|
+
if(inp.getAttribute("data-prov") === "shape" || inp.getAttribute("data-prov") === "baseUrl"){ saveEndpoint(inp.closest(".pv-key")); return; }
|
|
3824
|
+
if(!inp.getAttribute("data-mf")) return;
|
|
3825
|
+
saveModel(inp.closest(".pv-models") ? inp.closest(".pv-models").getAttribute("data-models-for") : "", inp.closest(".pv-mrow"));
|
|
3595
3826
|
});
|
|
3827
|
+
// A box the person typed: empty = back to the default (null); a number is sent as a number; anything else is
|
|
3828
|
+
// sent as the text typed, so the server refuses it by naming the box.
|
|
3829
|
+
function priceValue(text){
|
|
3830
|
+
var t = String(text).trim();
|
|
3831
|
+
if(t === "") return null;
|
|
3832
|
+
var n = Number(t);
|
|
3833
|
+
return isFinite(n) ? n : t;
|
|
3834
|
+
}
|
|
3835
|
+
// One model line per save: {slot, model, use, inPerM, outPerM}. A refusal is the line's hint (the server's one rule),
|
|
3836
|
+
// and nothing was saved: the repaint puts the tick back.
|
|
3837
|
+
function saveModel(slot, line){
|
|
3838
|
+
var msg = line.querySelector("[data-f=msg-model]");
|
|
3839
|
+
var payload = {
|
|
3840
|
+
slot: slot,
|
|
3841
|
+
model: line.getAttribute("data-model"),
|
|
3842
|
+
use: line.querySelector("[data-mf=use]").checked,
|
|
3843
|
+
inPerM: priceValue(line.querySelector("[data-mf=inPerM]").value),
|
|
3844
|
+
outPerM: priceValue(line.querySelector("[data-mf=outPerM]").value)
|
|
3845
|
+
};
|
|
3846
|
+
authorPost("/api/settings/model", payload).then(function(r){
|
|
3847
|
+
if(r.body && r.body.ok){
|
|
3848
|
+
line.querySelectorAll("input[type=text]").forEach(function(inp){ inp.removeAttribute("data-dirty"); });
|
|
3849
|
+
loadProviders();
|
|
3850
|
+
setNote(msg, false, "saved — applies from the next model call");
|
|
3851
|
+
} else { setNote(msg, true, settingsRefusal(r)); loadProviders(); }
|
|
3852
|
+
}, function(){ setNote(msg, true, networkText()); });
|
|
3853
|
+
}
|
|
3854
|
+
|
|
3855
|
+
// A model name typed because the list failed: saved as an unticked, empty line (typed lines are saved); the person then fills the prices and ticks it.
|
|
3856
|
+
function addTypedModel(slot, box){
|
|
3857
|
+
var inp = box.querySelector("input");
|
|
3858
|
+
var msg = box.querySelector("[data-f=typed-msg]");
|
|
3859
|
+
authorPost("/api/settings/model", { slot: slot, model: inp.value.trim(), use: false }).then(function(r){
|
|
3860
|
+
if(r.body && r.body.ok){ inp.value = ""; setNote(msg, false, "added"); loadProviders(); }
|
|
3861
|
+
else setNote(msg, true, settingsRefusal(r));
|
|
3862
|
+
}, function(){ setNote(msg, true, networkText()); });
|
|
3863
|
+
}
|
|
3864
|
+
|
|
3865
|
+
// The legacy tick saves alone: {slot, legacyMaxTokens}. A refusal is the cell's hint line.
|
|
3866
|
+
function saveProvider(key, inp){
|
|
3867
|
+
var field = inp.getAttribute("data-prov");
|
|
3868
|
+
var msg = key.querySelector("[data-f=msg-" + field + "]");
|
|
3869
|
+
var payload = { slot: key.getAttribute("data-slot") };
|
|
3870
|
+
payload[field] = inp.checked;
|
|
3871
|
+
authorPost("/api/settings/provider", payload).then(function(r){
|
|
3872
|
+
if(r.body && r.body.ok){
|
|
3873
|
+
inp.removeAttribute("data-dirty");
|
|
3874
|
+
loadProviders();
|
|
3875
|
+
setNote(msg, false, "saved — applies from the next model call");
|
|
3876
|
+
} else setNote(msg, true, settingsRefusal(r));
|
|
3877
|
+
}, function(){ setNote(msg, true, networkText()); });
|
|
3878
|
+
}
|
|
3879
|
+
|
|
3880
|
+
document.getElementById("tab-providers").addEventListener("click", function(){ loadProviders(); });
|
|
3596
3881
|
wireTabs([document.getElementById("tab-providers"), document.getElementById("tab-money")]);
|
|
3597
|
-
document.getElementById("tab-providers").addEventListener("click", loadProviders);
|
|
3598
3882
|
// Auto-save on a human edit (change = blur or Enter): POST carries the per-start token; the chat has no route here
|
|
3599
3883
|
limitInput.addEventListener("change", function(){
|
|
3600
3884
|
var text = limitInput.value.trim();
|
|
@@ -3628,6 +3912,9 @@
|
|
|
3628
3912
|
var actionErrEl = document.getElementById("chat-action-error");
|
|
3629
3913
|
var moneyCapInput = document.getElementById("jf-cap-money");
|
|
3630
3914
|
var modelSelect = document.getElementById("jf-model");
|
|
3915
|
+
var thinkingBox = document.getElementById("jf-thinking");
|
|
3916
|
+
var thinkingNote = document.getElementById("jf-thinking-note");
|
|
3917
|
+
var lineThinking = {}; // option value -> the server's {support, canChange, say} for that Model line
|
|
3631
3918
|
var modelStatusEl = document.getElementById("jf-model-status");
|
|
3632
3919
|
var progressRow = document.getElementById("chat-progress-row");
|
|
3633
3920
|
|
|
@@ -3672,11 +3959,14 @@
|
|
|
3672
3959
|
var resuming = resumeRun !== null;
|
|
3673
3960
|
var setLock = function(id, on){ var el = document.getElementById(id); el.readOnly = on; el.classList.toggle("locked", on); };
|
|
3674
3961
|
LOCKED_IDS.forEach(function(id){ setLock(id, reuseOn || live || resuming); });
|
|
3675
|
-
|
|
3676
|
-
|
|
3962
|
+
// the one exception on a resume card: an older spec that names no model leaves the Model box open (picked at resume)
|
|
3963
|
+
var modelOpen = resuming && resumeRun.pickModel === true;
|
|
3964
|
+
OPEN_IDS.forEach(function(id){ setLock(id, live || (resuming && CAP_IDS.indexOf(id) === -1 && !(modelOpen && id === "jf-model"))); });
|
|
3965
|
+
document.getElementById("jf-model").disabled = live || (resuming && !modelOpen);
|
|
3677
3966
|
// the Check type radios stay clickable on a reuse card (Deterministic / Rubric = leave reuse); a live session or a resume freezes them
|
|
3678
3967
|
document.querySelectorAll('input[name="jf-verdict"]').forEach(function(r){ r.disabled = live || resuming; });
|
|
3679
3968
|
document.getElementById("job-card").classList.toggle("reuse", reuseOn);
|
|
3969
|
+
syncThinking();
|
|
3680
3970
|
}
|
|
3681
3971
|
function setReuseLocked(on){ reuseOn = on; syncCardLock(); }
|
|
3682
3972
|
function clearStartFrom(keepBoxes){
|
|
@@ -3754,7 +4044,7 @@
|
|
|
3754
4044
|
}
|
|
3755
4045
|
}
|
|
3756
4046
|
function loadReuseJobs(){
|
|
3757
|
-
authorGet("/api/author/reuse-jobs" + (modelSelect.value ? "?
|
|
4047
|
+
authorGet("/api/author/reuse-jobs" + (modelSelect.value ? "?" + pickQuery(modelSelect.value) : "")).then(function(r){
|
|
3758
4048
|
rwFailed = !(r && r.ok);
|
|
3759
4049
|
rwJobs = rwFailed ? null : r.jobs;
|
|
3760
4050
|
renderReuseList();
|
|
@@ -3771,7 +4061,7 @@
|
|
|
3771
4061
|
refreshStartEnabled();
|
|
3772
4062
|
rwQ.focus();
|
|
3773
4063
|
}
|
|
3774
|
-
function pickReuseJob(j){ if(j) reuseWorkflow(j.runid, modelSelect.value); }
|
|
4064
|
+
function pickReuseJob(j){ if(j) reuseWorkflow(j.runid, pickQuery(modelSelect.value)); }
|
|
3775
4065
|
rwQ.addEventListener("input", function(){ rwHl = 0; renderReuseList(); });
|
|
3776
4066
|
rwQ.addEventListener("keydown", function(e){
|
|
3777
4067
|
if(e.key === "ArrowDown"){ rwHl = Math.min(rwHl + 1, rwShown.length - 1); renderReuseList(); e.preventDefault(); }
|
|
@@ -3791,7 +4081,9 @@
|
|
|
3791
4081
|
document.getElementById("rw-change").addEventListener("click", unpickReuse);
|
|
3792
4082
|
// leaving resume mode: the title, the hidden drafting chrome, the time box and the resume box all come back
|
|
3793
4083
|
function exitResumeMode(){
|
|
4084
|
+
var filtered = resumeRun !== null && !!resumeRun.worker;
|
|
3794
4085
|
resumeRun = null;
|
|
4086
|
+
if(filtered){ renderModelOptions(); }
|
|
3795
4087
|
originLine.classList.remove("res");
|
|
3796
4088
|
document.getElementById("job-card-title").textContent = "Job card \u2014 draft";
|
|
3797
4089
|
document.getElementById("chat-resume-box").hidden = true;
|
|
@@ -3836,6 +4128,7 @@
|
|
|
3836
4128
|
clearCardBoxes();
|
|
3837
4129
|
setVerdict("deterministic");
|
|
3838
4130
|
modelSelect.selectedIndex = 0;
|
|
4131
|
+
setThinkingTick(false);
|
|
3839
4132
|
msgInput.value = "";
|
|
3840
4133
|
rwQ.value = "";
|
|
3841
4134
|
lastPhase = null;
|
|
@@ -3861,11 +4154,14 @@
|
|
|
3861
4154
|
return {
|
|
3862
4155
|
// on "Reuse workflow" the type is the picked job's own, never the radio's
|
|
3863
4156
|
checkType: (verdictValue() === "reuse" ? reuseCheckType : verdictValue()) === "rubric" ? "rubric" : "deterministic",
|
|
3864
|
-
|
|
4157
|
+
provider: pickParts(document.getElementById("jf-model").value).provider,
|
|
4158
|
+
model: pickParts(document.getElementById("jf-model").value).model,
|
|
3865
4159
|
jobName: document.getElementById("jf-name").value.trim(),
|
|
3866
4160
|
jobText: document.getElementById("jf-job").value.trim(),
|
|
3867
4161
|
inputs: document.getElementById("jf-inputs").value.trim(),
|
|
3868
4162
|
destination: document.getElementById("jf-dest").value.trim(),
|
|
4163
|
+
// the "Thinking on" tick: sent as a boolean; the server judges it per line and signs it only when ticked
|
|
4164
|
+
thinking: thinkingBox.checked === true,
|
|
3869
4165
|
capUsd: moneyCap,
|
|
3870
4166
|
maxWallMs: timeCapMin === "" ? undefined : Math.round(Number(timeCapMin) * 60000),
|
|
3871
4167
|
};
|
|
@@ -3883,6 +4179,7 @@
|
|
|
3883
4179
|
var capOk = moneyCapInput.value.trim() !== "" && Number.isFinite(v) && v > 0;
|
|
3884
4180
|
// on Reuse workflow nothing starts before a job is picked
|
|
3885
4181
|
startOk = capOk && !sessionLive && keyOk && (verdictValue() !== "reuse" || startFrom !== null);
|
|
4182
|
+
if(typeof syncResumeSign === "function"){ syncResumeSign(); }
|
|
3886
4183
|
syncCardLock();
|
|
3887
4184
|
renderMain();
|
|
3888
4185
|
refreshClearBtn();
|
|
@@ -3963,21 +4260,30 @@
|
|
|
3963
4260
|
return { keyOk: true, ok: false, text: reachStatusText(reach.status) };
|
|
3964
4261
|
}
|
|
3965
4262
|
function refreshModelStatus(){
|
|
3966
|
-
var modelId = modelSelect.value;
|
|
3967
|
-
if(
|
|
4263
|
+
var modelId = pickParts(modelSelect.value).model;
|
|
4264
|
+
if(modelSelect.value === ""){
|
|
3968
4265
|
keyOk = false;
|
|
3969
4266
|
modelStatusEl.className = "jf-readiness bad";
|
|
3970
|
-
modelStatusEl.textContent = "[✗] no model —
|
|
4267
|
+
modelStatusEl.textContent = "[✗] no model — Tick a model in Settings > Providers.";
|
|
3971
4268
|
refreshStartEnabled();
|
|
3972
4269
|
return;
|
|
3973
4270
|
}
|
|
3974
|
-
|
|
4271
|
+
// the key state of the NEW pick is unknown until its check returns: blocked at once, and a late answer for a line
|
|
4272
|
+
// no longer selected is ignored (it would otherwise unblock Start / Sign & resume for the wrong key)
|
|
4273
|
+
var asked = modelSelect.value;
|
|
4274
|
+
keyOk = false;
|
|
4275
|
+
modelStatusEl.className = "jf-readiness unknown";
|
|
4276
|
+
modelStatusEl.textContent = "checking the key\u2026";
|
|
4277
|
+
refreshStartEnabled();
|
|
4278
|
+
authorGet("/api/author/model-check?" + pickQuery(modelSelect.value)).then(function(r){
|
|
4279
|
+
if(modelSelect.value !== asked) return;
|
|
3975
4280
|
var line = modelStatusLine(r, modelId);
|
|
3976
4281
|
keyOk = line.keyOk;
|
|
3977
4282
|
modelStatusEl.className = "jf-readiness " + (line.ok ? "ok" : "bad");
|
|
3978
4283
|
modelStatusEl.textContent = line.text;
|
|
3979
4284
|
refreshStartEnabled();
|
|
3980
4285
|
}).catch(function(){
|
|
4286
|
+
if(modelSelect.value !== asked) return;
|
|
3981
4287
|
keyOk = false;
|
|
3982
4288
|
modelStatusEl.className = "jf-readiness bad";
|
|
3983
4289
|
modelStatusEl.textContent = "[✗] could not check the key";
|
|
@@ -3989,25 +4295,89 @@
|
|
|
3989
4295
|
function refreshReuseLine(){
|
|
3990
4296
|
if(!startFrom) return;
|
|
3991
4297
|
var q = (startFrom.importId ? "import=" + encodeURIComponent(startFrom.importId) : "runid=" + encodeURIComponent(startFrom.runid)) +
|
|
3992
|
-
"&
|
|
4298
|
+
"&" + pickQuery(modelSelect.value);
|
|
3993
4299
|
authorGet("/api/author/start-from?" + q).then(function(r){
|
|
3994
4300
|
if(startFrom && r && r.ok){ sfLine.textContent = r.line ? rwMeta({checkType: reuseCheckType, line: r.line}) : ""; }
|
|
3995
4301
|
}).catch(function(){ /* the line keeps what it had */ });
|
|
3996
4302
|
}
|
|
3997
4303
|
modelSelect.addEventListener("change", refreshReuseLine);
|
|
3998
4304
|
modelSelect.addEventListener("change", function(){ if(!rwBox.hidden && startFrom === null && !reuseSession) loadReuseJobs(); });
|
|
3999
|
-
//
|
|
4000
|
-
//
|
|
4305
|
+
// P8 — the Model menu IS the Settings rows: every TICKED, PRICED line, labelled "<KEY> · <model>", in keys-file order. An option's
|
|
4306
|
+
// value carries the key AND the model ("KEY|model"): two keys can tick the same model id, and the card names which one.
|
|
4307
|
+
// Re-read whenever Settings loads or saves, keeping the pick if it survives.
|
|
4308
|
+
function pickValue(slot, model){ return (slot || "") + "|" + model; }
|
|
4309
|
+
function pickParts(v){
|
|
4310
|
+
var i = String(v).indexOf("|");
|
|
4311
|
+
return i < 0 ? {provider: "", model: String(v)} : {provider: v.slice(0, i), model: v.slice(i + 1)};
|
|
4312
|
+
}
|
|
4313
|
+
function pickQuery(v){ var p = pickParts(v); return "model=" + encodeURIComponent(p.model) + "&provider=" + encodeURIComponent(p.provider); }
|
|
4314
|
+
// restore a saved card's pick: its key and model; an older card names only the model (the first option holding it)
|
|
4315
|
+
function setPick(c){
|
|
4316
|
+
if(!c || !c.model) return;
|
|
4317
|
+
var want = c.provider ? pickValue(c.provider, c.model) : null;
|
|
4318
|
+
var opts = Array.prototype.slice.call(modelSelect.options);
|
|
4319
|
+
var hit = opts.filter(function(o){ return want ? o.value === want : pickParts(o.value).model === c.model; })[0];
|
|
4320
|
+
if(!hit){
|
|
4321
|
+
hit = document.createElement("option");
|
|
4322
|
+
hit.value = want || pickValue("", c.model);
|
|
4323
|
+
hit.textContent = (c.provider ? c.provider + " \u00b7 " : "") + c.model;
|
|
4324
|
+
modelSelect.appendChild(hit);
|
|
4325
|
+
}
|
|
4326
|
+
modelSelect.value = hit.value;
|
|
4327
|
+
syncThinking();
|
|
4328
|
+
}
|
|
4329
|
+
// The "Thinking on" box follows the SERVER's answer for the picked line (`thinking.canChange`): false greys AND unticks it with the
|
|
4330
|
+
// server's words; 'unverified' leaves it enabled with no note. The tick is the person's; the page decides nothing.
|
|
4331
|
+
function syncThinking(){
|
|
4332
|
+
var t = lineThinking[modelSelect.value];
|
|
4333
|
+
var blocked = !!(t && t.canChange === false);
|
|
4334
|
+
if(blocked) thinkingBox.checked = false;
|
|
4335
|
+
thinkingBox.disabled = blocked || sessionLive || resumeRun !== null || reuseOn;
|
|
4336
|
+
thinkingNote.textContent = blocked ? (t.say || "") : "";
|
|
4337
|
+
document.getElementById("jf-thinking-label").hidden = reuseOn;
|
|
4338
|
+
}
|
|
4339
|
+
function setThinkingTick(on){ thinkingBox.checked = on === true; syncThinking(); }
|
|
4340
|
+
modelSelect.addEventListener("change", syncThinking);
|
|
4341
|
+
var modelReads = 0;
|
|
4342
|
+
var modelLines = [];
|
|
4343
|
+
var modelSay = "";
|
|
4344
|
+
// A Resume card that must PICK a model (old spec naming none) offers only the lines resolveWorker (src/providerrows.js) accepts
|
|
4345
|
+
// for that spec: the same shape, and the same endpoint when the spec names one (`resumeRun.worker`, from start-from, endpoint
|
|
4346
|
+
// already normalised by the server). With none, the sentence names the shape to tick (label from providershapes).
|
|
4347
|
+
function renderModelOptions(){
|
|
4348
|
+
var w = resumeRun !== null && resumeRun.pickModel === true ? resumeRun.worker : null;
|
|
4349
|
+
var lines = modelLines.filter(function(m){ return !w || (m.shape === w.provider && (w.endpoint === "" || m.endpoint === w.endpoint)); });
|
|
4350
|
+
var was = modelSelect.value;
|
|
4351
|
+
lineThinking = {};
|
|
4352
|
+
lines.forEach(function(m){ lineThinking[pickValue(m.slot, m.model)] = m.thinking || null; });
|
|
4353
|
+
var none = (w && modelLines.length) ? "Tick a " + w.label + " model in Settings > Providers." : (modelSay || "Tick a model in Settings > Providers.");
|
|
4354
|
+
modelSelect.innerHTML = lines.length ? lines.map(function(m){
|
|
4355
|
+
return '<option value="' + escapeXml(pickValue(m.slot, m.model)) + '">' + escapeXml(m.label) + "</option>";
|
|
4356
|
+
}).join("") : '<option value="">' + escapeXml(none) + "</option>";
|
|
4357
|
+
if(lines.some(function(m){ return pickValue(m.slot, m.model) === was; })) modelSelect.value = was;
|
|
4358
|
+
syncThinking();
|
|
4359
|
+
}
|
|
4001
4360
|
function loadModels(){
|
|
4361
|
+
if(modelSelect.options.length === 0){
|
|
4362
|
+
modelSelect.innerHTML = '<option value="">Reading your models\u2026</option>';
|
|
4363
|
+
}
|
|
4002
4364
|
authorGet("/api/author/models").then(function(r){
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
if(models.some(function(m){ return m.id === was; })) modelSelect.value = was;
|
|
4365
|
+
if(!r || !r.ok) throw new Error("read");
|
|
4366
|
+
modelReads = 0;
|
|
4367
|
+
modelLines = r.lines || [];
|
|
4368
|
+
modelSay = r.say || "";
|
|
4369
|
+
renderModelOptions();
|
|
4009
4370
|
refreshModelStatus();
|
|
4010
|
-
}).catch(function(){
|
|
4371
|
+
}).catch(function(){
|
|
4372
|
+
modelReads += 1;
|
|
4373
|
+
if(modelReads <= 5){
|
|
4374
|
+
modelSelect.innerHTML = '<option value="">Could not read your models. Trying again\u2026</option>';
|
|
4375
|
+
setTimeout(loadModels, 1500);
|
|
4376
|
+
} else {
|
|
4377
|
+
modelSelect.innerHTML = '<option value="">Could not read your models. Reload the page.</option>';
|
|
4378
|
+
}
|
|
4379
|
+
refreshModelStatus();
|
|
4380
|
+
});
|
|
4011
4381
|
}
|
|
4012
4382
|
document.addEventListener("bareloop-providers-changed", loadModels);
|
|
4013
4383
|
loadModels();
|
|
@@ -4289,10 +4659,7 @@
|
|
|
4289
4659
|
reusePickedName = c.jobName || "job";
|
|
4290
4660
|
setVerdict(reuseSession ? "reuse" : reuseCheckType);
|
|
4291
4661
|
if(c.model){
|
|
4292
|
-
|
|
4293
|
-
modelSelect.insertAdjacentHTML("beforeend", '<option value="' + escapeXml(c.model) + '">' + escapeXml(c.model) + "</option>");
|
|
4294
|
-
}
|
|
4295
|
-
modelSelect.value = c.model;
|
|
4662
|
+
setPick(c);
|
|
4296
4663
|
}
|
|
4297
4664
|
fillCard(c);
|
|
4298
4665
|
|
|
@@ -4323,6 +4690,7 @@
|
|
|
4323
4690
|
setVal("jf-name", c.jobName); setVal("jf-job", c.jobText); setVal("jf-inputs", c.inputs);
|
|
4324
4691
|
setVal("jf-dest", c.destination); setVal("jf-cap-money", c.capUsd);
|
|
4325
4692
|
setVal("jf-cap-time", typeof c.maxWallMs === "number" ? Math.round(c.maxWallMs / 60000) : "");
|
|
4693
|
+
setThinkingTick(c.thinking === true);
|
|
4326
4694
|
fitCardBoxes();
|
|
4327
4695
|
}
|
|
4328
4696
|
// ONE owner of the reuse fill: the Run tab's / Import view's button and the picker both arrive here (the
|
|
@@ -4336,7 +4704,7 @@
|
|
|
4336
4704
|
reuseCheckType = c.checkType === "rubric" ? "rubric" : "deterministic";
|
|
4337
4705
|
reusePickedName = c.jobName || (pre.origin && pre.origin.job) || "job";
|
|
4338
4706
|
setVerdict("reuse");
|
|
4339
|
-
if(c.model){
|
|
4707
|
+
if(c.model){ setPick(c); }
|
|
4340
4708
|
fillCard(c);
|
|
4341
4709
|
|
|
4342
4710
|
setReuseLocked(true);
|
|
@@ -4362,7 +4730,7 @@
|
|
|
4362
4730
|
clearStartFrom();
|
|
4363
4731
|
openNewCard();
|
|
4364
4732
|
setVerdict(c.checkType === "rubric" ? "rubric" : "deterministic");
|
|
4365
|
-
if(c.model){
|
|
4733
|
+
if(c.model){ setPick(c); }
|
|
4366
4734
|
fillCard(c);
|
|
4367
4735
|
|
|
4368
4736
|
originLine.textContent = d.origin || "";
|
|
@@ -4383,10 +4751,12 @@
|
|
|
4383
4751
|
var r = d.resume || {};
|
|
4384
4752
|
clearStartFrom();
|
|
4385
4753
|
openNewCard();
|
|
4754
|
+
// hamr 2026-10-11 Q2(b): a spec that names no model (the server says so) opens the Model box; the pick rides the POST
|
|
4386
4755
|
resumeRun = {runid: d.runid, noTime: typeof r.maxWallMin !== "number"};
|
|
4756
|
+
if((d.prefill || {}).specNamesModel === false){ resumeRun.pickModel = true; if(d.prefill.specWorker){ resumeRun.worker = d.prefill.specWorker; } renderModelOptions(); }
|
|
4387
4757
|
document.getElementById("job-card-title").textContent = "RESUME " + (d.title || d.runid);
|
|
4388
4758
|
setVerdict(c.checkType === "rubric" ? "rubric" : "deterministic");
|
|
4389
|
-
if(c.model){
|
|
4759
|
+
if(c.model && !resumeRun.pickModel){ setPick(c); }
|
|
4390
4760
|
fillCard(c);
|
|
4391
4761
|
setVal("jf-cap-money", r.budgetUsd);
|
|
4392
4762
|
setVal("jf-cap-time", resumeRun.noTime ? "" : r.maxWallMin);
|
|
@@ -4408,16 +4778,21 @@
|
|
|
4408
4778
|
document.addEventListener("bareloop-resume", function(e){ applyResume(e.detail || {}); });
|
|
4409
4779
|
document.getElementById("chat-resume-cancel").addEventListener("click", function(){ resetCard(); });
|
|
4410
4780
|
var resumeSignBtn = document.getElementById("chat-resume-sign");
|
|
4781
|
+
// a Model pick that is needed (older spec) blocks Sign & resume until a ticked line is picked and its key is usable
|
|
4782
|
+
function resumeSignBlocked(){ return resumeRun !== null && resumeRun.pickModel === true && (modelSelect.value === "" || !keyOk); }
|
|
4783
|
+
function syncResumeSign(){ resumeSignBtn.disabled = resumeSignBlocked(); }
|
|
4411
4784
|
function signResume(){
|
|
4412
4785
|
var run = resumeRun;
|
|
4413
4786
|
if(run === null) return;
|
|
4787
|
+
if(resumeSignBlocked()) return;
|
|
4414
4788
|
var err = document.getElementById("chat-resume-err");
|
|
4415
4789
|
err.hidden = true;
|
|
4416
4790
|
resumeSignBtn.disabled = true;
|
|
4417
4791
|
var body = {budgetUsd: document.getElementById("jf-cap-money").value};
|
|
4418
4792
|
if(!run.noTime){ body.maxWallMin = document.getElementById("jf-cap-time").value; }
|
|
4793
|
+
if(run.pickModel){ var pk = pickParts(modelSelect.value); body.model = pk.model; if(pk.provider !== ""){ body.provider = pk.provider; } }
|
|
4419
4794
|
return authorPost("/api/runs/" + encodeURIComponent(run.runid) + "/resume", body).then(function(res){
|
|
4420
|
-
resumeSignBtn.disabled =
|
|
4795
|
+
resumeSignBtn.disabled = resumeSignBlocked();
|
|
4421
4796
|
if(res.body && res.body.ok){
|
|
4422
4797
|
// the SAME run goes on: the card goes back to empty, the right side to its Run tab, which follows it until it is live again
|
|
4423
4798
|
resetCard();
|
|
@@ -4427,7 +4802,7 @@
|
|
|
4427
4802
|
err.textContent = (res.body && res.body.error) || ("refused (" + res.status + ")");
|
|
4428
4803
|
err.hidden = false;
|
|
4429
4804
|
}
|
|
4430
|
-
}).catch(function(x){ resumeSignBtn.disabled =
|
|
4805
|
+
}).catch(function(x){ resumeSignBtn.disabled = resumeSignBlocked(); err.textContent = "could not reach the panel: " + x.message; err.hidden = false; });
|
|
4431
4806
|
}
|
|
4432
4807
|
resumeSignBtn.addEventListener("click", signResume);
|
|
4433
4808
|
|