bareloop 0.40.0 → 0.42.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 +96 -1
- package/README.md +13 -21
- package/bareloop.context.md +141 -128
- package/package.json +5 -4
- package/src/authorflow.js +346 -38
- package/src/authoring.js +92 -7
- package/src/authorjob.js +82 -20
- package/src/authorreadout.js +89 -5
- package/src/authorrun.js +39 -30
- package/src/authorscout.js +112 -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/declaredclose.js +23 -12
- package/src/draftspend.js +4 -4
- package/src/facts.js +317 -0
- package/src/factwords.js +129 -0
- package/src/index.js +2 -4
- package/src/inputlook.js +301 -0
- package/src/interviewrun.js +17 -25
- package/src/job.js +15 -8
- package/src/jobblock.js +7 -2
- package/src/judged.js +11 -43
- package/src/keysfile.js +15 -7
- package/src/kinds.js +86 -5
- package/src/labelscan.js +170 -0
- package/src/modellist.js +84 -0
- package/src/monthly.js +23 -43
- package/src/pagefetch.js +125 -0
- package/src/panel/authorroutes.js +34 -16
- package/src/panel/authorsession.js +161 -80
- package/src/panel/index.html +553 -152
- package/src/panel/jobplan.js +34 -11
- package/src/panel/runroutes.js +3 -0
- package/src/panel/server.js +54 -18
- package/src/panel/settingsroutes.js +232 -173
- package/src/planrun.js +5 -4
- 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/rubricstep.js +804 -0
- package/src/source.js +166 -1
- package/src/thinking.js +130 -0
- package/src/u-readout.js +1 -1
- package/src/userrun.js +64 -75
- package/types/authorflow.d.ts +272 -18
- package/types/authoring.d.ts +29 -1
- package/types/authorjob.d.ts +9 -7
- package/types/authorreadout.d.ts +27 -2
- package/types/authorscout.d.ts +72 -2
- package/types/config.d.ts +23 -6
- package/types/declaredclose.d.ts +4 -2
- package/types/draftspend.d.ts +8 -8
- package/types/facts.d.ts +71 -0
- package/types/factwords.d.ts +42 -0
- package/types/index.d.ts +1 -1
- package/types/inputlook.d.ts +238 -0
- package/types/job.d.ts +5 -5
- package/types/jobblock.d.ts +8 -1
- package/types/judged.d.ts +5 -36
- package/types/keysfile.d.ts +15 -4
- package/types/labelscan.d.ts +38 -0
- package/types/modellist.d.ts +50 -0
- package/types/monthly.d.ts +4 -3
- package/types/pagefetch.d.ts +71 -0
- package/types/panel/authorsession.d.ts +20 -11
- package/types/panel/jobplan.d.ts +12 -4
- package/types/panel/server.d.ts +18 -6
- package/types/providerrows.d.ts +192 -99
- package/types/providers.d.ts +34 -159
- package/types/providershapes.d.ts +10 -0
- package/types/rubricstep.d.ts +286 -0
- package/types/source.d.ts +94 -0
- package/types/thinking.d.ts +90 -0
- package/types/userrun.d.ts +17 -1
package/src/panel/index.html
CHANGED
|
@@ -113,8 +113,9 @@
|
|
|
113
113
|
.topbar > div:first-child{display:flex;align-items:baseline;gap:0;white-space:nowrap;overflow:hidden;}
|
|
114
114
|
.topbar .brand{font-weight:700;font-size:17px;letter-spacing:0.06em;flex:none;}
|
|
115
115
|
.topbar .brand::before{content:"■ ";}
|
|
116
|
-
.topbar .tagline{font-size:12px;color:var(--text-dim);margin-left:8px;overflow:hidden;text-overflow:ellipsis;}
|
|
117
|
-
.topbar .tagline::before{content:"· ";color:var(--text-faint);}
|
|
116
|
+
.topbar .tagline,.topbar .version{font-size:12px;color:var(--text-dim);margin-left:8px;overflow:hidden;text-overflow:ellipsis;font-weight:400;}
|
|
117
|
+
.topbar .tagline::before,.topbar .version::before{content:"· ";color:var(--text-faint);}
|
|
118
|
+
.topbar .version{flex:none;}
|
|
118
119
|
@media (max-width: 600px){ .topbar .tagline{display:none;} }
|
|
119
120
|
.topbar-right{display:flex;align-items:center;gap:10px;flex-wrap:wrap;}
|
|
120
121
|
|
|
@@ -530,19 +531,95 @@
|
|
|
530
531
|
.money-item input{width:100%;height:44px;box-sizing:border-box;}
|
|
531
532
|
.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
533
|
.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;}
|
|
534
|
+
.keyfile-strip-main{display:flex;flex-direction:column;gap:3px;flex:1;min-width:0;overflow-wrap:anywhere;}
|
|
535
|
+
.keyfile-strip-actions{display:flex;flex-wrap:wrap;gap:6px;flex:none;max-width:100%;}
|
|
535
536
|
.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-
|
|
537
|
+
/* M4d Providers layout F (hamr approved 2026-10-11): ONE shared grid, so a key row, the model sub-head and
|
|
538
|
+
every model row sit on the same column edges. Key band: KEY | API SHAPE | BASE URL (two columns) | OLD MAX_TOKENS | TOKENS | refresh.
|
|
539
|
+
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
|
|
540
|
+
solid 1px line, a model row a dotted 1px hairline. Below 1280px every row collapses to stacked label : value lines. */
|
|
541
|
+
.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;}
|
|
542
|
+
.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;}
|
|
543
|
+
.pv-head > *,.pv-krow > *,.pv-mhead > *,.pv-mrow > *{min-width:0;}
|
|
544
|
+
.pv-head,.pv-mhead{font-size:11px;font-weight:700;color:var(--text-dim);letter-spacing:0.04em;min-height:26px;}
|
|
545
|
+
.pv-head{background:var(--panel2);border-bottom:1px solid var(--border-strong);}
|
|
546
|
+
.pv-mhead{background:var(--bg);border-bottom:1px dotted var(--border-strong);}
|
|
547
|
+
.pv-key{border-top:1px solid var(--border-strong);}
|
|
548
|
+
.pv-krow{min-height:40px;padding-top:4px;padding-bottom:4px;}
|
|
549
|
+
.pv-c-url{grid-column:3 / span 2;}
|
|
550
|
+
.pv-lbl{display:none;}
|
|
551
|
+
.pv-name{display:flex;align-items:center;flex-wrap:wrap;gap:4px 6px;min-width:0;overflow-wrap:anywhere;}
|
|
552
|
+
.pv-name code{font-weight:700;}
|
|
553
|
+
.pv-ok{color:var(--green);font-weight:700;}
|
|
554
|
+
.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;}
|
|
555
|
+
.pv-krow select,.pv-krow input[type=text]{width:100%;max-width:100%;min-height:28px;box-sizing:border-box;}
|
|
556
|
+
.pv-grid select,.pv-grid input[type=text]{font-size:12px;height:28px;padding:2px 6px;}
|
|
557
|
+
.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;}
|
|
558
|
+
.pv-legacy input,.pv-m-use input{width:16px;height:16px;margin:0;accent-color:var(--accent);cursor:pointer;}
|
|
559
|
+
.pv-tok{text-align:right;font-variant-numeric:tabular-nums;}
|
|
560
|
+
.pv-refcell{display:flex;justify-content:flex-end;}
|
|
561
|
+
.pv-refbox{display:flex;flex-direction:column;align-items:flex-start;max-width:100%;}
|
|
562
|
+
.pv-refbtns{display:flex;gap:6px;}
|
|
563
|
+
.pv-refbox .pv-msg{width:0;min-width:100%;overflow-wrap:anywhere;margin:2px 0 0;}
|
|
564
|
+
.pv-refbox .pv-msg:empty{display:none;}
|
|
565
|
+
.pv-refbtn{white-space:nowrap;flex:0 0 auto;}
|
|
566
|
+
.pv-refbtn:disabled{opacity:.5;cursor:not-allowed;}
|
|
567
|
+
.pv-sum{color:var(--text-faint);font-size:11px;font-weight:400;}
|
|
568
|
+
.pv-key[data-open="1"] .pv-sum{display:none;}
|
|
569
|
+
.pv-knote{padding:0 8px 4px 34px;margin:0;}
|
|
570
|
+
.pv-knote:empty{display:none;}
|
|
571
|
+
.pv-knote > .hint:empty{display:none;}
|
|
572
|
+
.pv-key[data-open="0"] .pv-models{display:none;}
|
|
573
|
+
.pv-mrow{border-top:1px dotted var(--border-strong);min-height:32px;}
|
|
574
|
+
.pv-mrow:hover,.pv-mrow:focus-within{background:var(--panel2);}
|
|
575
|
+
.pv-um{grid-column:1;display:flex;align-items:center;gap:0;min-width:0;}
|
|
576
|
+
.pv-uh,.pv-m-use{flex:0 0 44px;width:44px;display:flex;align-items:center;justify-content:center;}
|
|
577
|
+
.pv-m-use{min-height:32px;}
|
|
578
|
+
.pv-m-name{min-width:0;overflow-wrap:anywhere;font-size:12px;}
|
|
579
|
+
.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;}
|
|
580
|
+
.pv-pair > *{min-width:0;}
|
|
581
|
+
.pv-pair input[type=text]{width:100%;text-align:right;}
|
|
582
|
+
.pv-mhead .pv-tok{text-align:right;}
|
|
583
|
+
.pv-mhead .pv-tok,.pv-m-tok{grid-column:6;}
|
|
584
|
+
.pv-m-msg{grid-column:1 / -1;padding:2px 8px 2px 52px;margin:0;}
|
|
585
|
+
.pv-m-msg:empty{display:none;}
|
|
586
|
+
.pv-more{padding:3px 8px 3px 34px;border-top:1px dotted var(--border-strong);}
|
|
587
|
+
.pv-more:empty{display:none;}
|
|
588
|
+
.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;}
|
|
589
|
+
.pv-more.pv-failed{color:var(--amber);background:var(--amber-bg);font-size:12px;}
|
|
590
|
+
.pv-typed{display:flex;flex-wrap:wrap;align-items:center;gap:6px 10px;margin-top:4px;}
|
|
591
|
+
.pv-typed input{flex:1 1 160px;min-width:0;max-width:260px;min-height:28px;box-sizing:border-box;}
|
|
592
|
+
.pv-typed .hint{flex:1 1 100%;margin:0;}
|
|
593
|
+
.hint.bad{color:var(--red);}
|
|
594
|
+
/* a refused Start (a $0 refusal, e.g. an Inputs line) is read like the red failure line of the progress list, not like a faint hint */
|
|
595
|
+
.card-refusal:not(:empty){font-size:13px;margin-top:8px;line-height:1.4;}
|
|
596
|
+
.card-refusal:empty{display:none;}
|
|
597
|
+
.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;}
|
|
598
|
+
.field label.jf-thinking input{width:auto;min-height:0;margin:0;}
|
|
543
599
|
.table-wrap{overflow-x:auto;border:1px solid var(--border-strong);background:var(--panel2);max-width:100%;}
|
|
544
600
|
@media (max-width: 899px){
|
|
545
601
|
.money-strip{grid-template-columns:1fr 1fr;}
|
|
602
|
+
.keyfile-strip-main{flex:1 1 100%;}
|
|
603
|
+
}
|
|
604
|
+
@media (max-width: 1279px){
|
|
605
|
+
.pv-head,.pv-mhead{display:none;}
|
|
606
|
+
.pv-krow,.pv-mrow{display:block;padding:2px 8px;}
|
|
607
|
+
.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);}
|
|
608
|
+
.pv-cell:first-child{border-top:none;}
|
|
609
|
+
.pv-lbl{display:block;font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:var(--text-dim);font-weight:700;}
|
|
610
|
+
.pv-cell.nolbl{display:flex;}
|
|
611
|
+
.pv-tok{text-align:left;}
|
|
612
|
+
.pv-grid select,.pv-grid input[type=text]{height:44px;font-size:14px;}
|
|
613
|
+
.pv-krow select,.pv-krow input[type=text]{min-height:44px;}
|
|
614
|
+
.pv-legacy{min-height:44px;gap:10px;}
|
|
615
|
+
.pv-legacy input,.pv-m-use input{width:24px;height:24px;}
|
|
616
|
+
.pv-um{min-height:44px;}
|
|
617
|
+
.pv-uh,.pv-m-use{min-height:44px;}
|
|
618
|
+
.pv-cell.pv-pair{display:grid;grid-template-columns:104px minmax(0,1fr) minmax(0,1fr);}
|
|
619
|
+
.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;}
|
|
620
|
+
.pv-fold,.pv-refbtn,.pv-more .pv-add{min-height:44px;min-width:44px;}
|
|
621
|
+
.pv-knote,.pv-more{padding-left:8px;}
|
|
622
|
+
.pv-m-msg{padding-left:8px;}
|
|
546
623
|
}
|
|
547
624
|
@media (max-width: 899px){
|
|
548
625
|
.main{flex-direction:column;}
|
|
@@ -565,6 +642,7 @@
|
|
|
565
642
|
<header class="topbar">
|
|
566
643
|
<div>
|
|
567
644
|
<span class="brand">bareloop</span>
|
|
645
|
+
<span class="version" id="panel-version">v__BARELOOP_PANEL_VERSION__</span>
|
|
568
646
|
<span class="tagline">automate a job, verified</span>
|
|
569
647
|
</div>
|
|
570
648
|
<div class="topbar-right">
|
|
@@ -615,6 +693,9 @@
|
|
|
615
693
|
<!-- build item 5 (2026-09-28) — ONE line under Model, not two:
|
|
616
694
|
key + reachability collapsed into a single readiness read. -->
|
|
617
695
|
<div class="jf-readiness unknown" id="jf-model-status" data-testid="jf-model-status"></div>
|
|
696
|
+
<!-- P8: the job's "Thinking on" tick (drafting + the rubric judge; never the worker). The SERVER's per-line `thinking.canChange` decides. -->
|
|
697
|
+
<label class="jf-thinking" id="jf-thinking-label"><input type="checkbox" id="jf-thinking" data-testid="jf-thinking"> Thinking on</label>
|
|
698
|
+
<div class="hint" id="jf-thinking-note" data-testid="jf-thinking-note"></div>
|
|
618
699
|
</div>
|
|
619
700
|
<div class="field">
|
|
620
701
|
<label for="jf-name">Job name</label>
|
|
@@ -650,7 +731,7 @@
|
|
|
650
731
|
<input id="jf-cap-time" type="text" placeholder="or blank = no wall">
|
|
651
732
|
</div>
|
|
652
733
|
</div>
|
|
653
|
-
<div class="hint" id="chat-card-error" data-testid="chat-card-error"></div>
|
|
734
|
+
<div class="hint bad card-refusal" id="chat-card-error" data-testid="chat-card-error" role="alert"></div>
|
|
654
735
|
<div id="chat-resume-box" class="chat-resume-box" data-testid="resume-box" hidden>
|
|
655
736
|
<div class="hint" id="chat-resume-spent" data-testid="resume-spent"></div>
|
|
656
737
|
<div class="ended-actions"><button class="btn primary" type="button" id="chat-resume-sign" data-testid="btn-sign-resume">Sign & resume</button> <button class="btn" type="button" id="chat-resume-cancel" data-testid="btn-resume-cancel">Cancel</button></div>
|
|
@@ -815,30 +896,26 @@
|
|
|
815
896
|
</div>
|
|
816
897
|
|
|
817
898
|
<div class="pane-body">
|
|
818
|
-
<!-- PROVIDERS TAB (
|
|
899
|
+
<!-- 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
900
|
<section id="panel-providers" class="tabpanel active" role="tabpanel" aria-labelledby="tab-providers">
|
|
820
|
-
<div class="keyfile-strip" data-testid="
|
|
901
|
+
<div class="keyfile-strip" data-testid="keys-strip">
|
|
821
902
|
<div class="keyfile-strip-main">
|
|
822
|
-
<span>Put <code>NAME=key</code> lines in
|
|
823
|
-
<span class="hint" id="
|
|
824
|
-
<span class="hint" id="
|
|
903
|
+
<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>
|
|
904
|
+
<span class="hint bad" id="keys-refusal" data-testid="keys-refusal"></span>
|
|
905
|
+
<span class="hint" id="keys-open-note" data-testid="keys-open-note"></span>
|
|
825
906
|
</div>
|
|
826
907
|
<div class="keyfile-strip-actions">
|
|
827
|
-
<button class="btn small"
|
|
828
|
-
<button class="btn small"
|
|
908
|
+
<button class="btn small" id="open-keys-folder" type="button" data-testid="open-keys-folder" aria-label="Open keys folder">Open keys folder</button>
|
|
909
|
+
<button class="btn small" id="reload-keys" type="button" data-testid="reload-keys">Reload keys</button>
|
|
829
910
|
</div>
|
|
830
911
|
</div>
|
|
831
912
|
<div class="table-wrap" data-testid="provider-table-wrap">
|
|
832
|
-
<
|
|
833
|
-
<
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
<tbody id="pv-rows"></tbody>
|
|
837
|
-
</table>
|
|
913
|
+
<div class="pv-grid" data-testid="provider-table">
|
|
914
|
+
<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>
|
|
915
|
+
<div id="pv-rows"></div>
|
|
916
|
+
</div>
|
|
838
917
|
</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>
|
|
918
|
+
<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
919
|
</section>
|
|
843
920
|
|
|
844
921
|
<!-- MONEY & LIMITS TAB -->
|
|
@@ -2327,7 +2404,7 @@
|
|
|
2327
2404
|
// 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
2405
|
// `model` (optional) = the Chat card's Model box, when the picker asks: the estimate line then counts only that worker's runs.
|
|
2329
2406
|
function reuseWorkflow(runid, model){
|
|
2330
|
-
authorGet("/api/author/start-from?runid=" + encodeURIComponent(runid) + (model ? "&
|
|
2407
|
+
authorGet("/api/author/start-from?runid=" + encodeURIComponent(runid) + (model ? "&" + model : "")).then(function(r){
|
|
2331
2408
|
if(!r || !r.ok){ window.alert((r && r.error) || "nothing to start from"); return; }
|
|
2332
2409
|
document.getElementById("tab-chat").click();
|
|
2333
2410
|
document.dispatchEvent(new CustomEvent("bareloop-reuse", {detail: {runid: runid, prefill: r}}));
|
|
@@ -2416,6 +2493,19 @@
|
|
|
2416
2493
|
}, 1500);
|
|
2417
2494
|
}
|
|
2418
2495
|
|
|
2496
|
+
// M3 (Part 1c full-run fixes): after Sign & run the new run's row is written a few seconds later (the runner starts, then
|
|
2497
|
+
// appends its row), and the Runs list only re-fetches every RUNS_LIST_POLL_MS — so a run just signed showed only after a page
|
|
2498
|
+
// refresh. Like a resume, a sign forces list refreshes until a run that was not listed before the click appears; bounded.
|
|
2499
|
+
function afterSignRefresh(knownIds, tries){
|
|
2500
|
+
refreshRunsList(true).then(function(runs){
|
|
2501
|
+
var fresh = Array.isArray(runs) && runs.some(function(r){ return knownIds.indexOf(r.runid) === -1; });
|
|
2502
|
+
if(!fresh && tries > 0) setTimeout(function(){ afterSignRefresh(knownIds, tries - 1); }, 1500);
|
|
2503
|
+
});
|
|
2504
|
+
}
|
|
2505
|
+
function currentRunIds(){
|
|
2506
|
+
try { return (JSON.parse(lastRunsSig || "[]") || []).map(function(r){ return r.runid; }); } catch(e){ return []; }
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2419
2509
|
function renderRun(detail){
|
|
2420
2510
|
if(detail.fileMissing){
|
|
2421
2511
|
setRunHeader("\u00B7", runName(detail.job, detail.runNo, detail.runid), "file missing");
|
|
@@ -3163,14 +3253,15 @@
|
|
|
3163
3253
|
// (src/panel/jobplan.js): numbered lines with their rule, the checks under the line each serves (folded), the AI's own
|
|
3164
3254
|
// "Not checked", the always-on guards. Only the marker is blue (--plan). Returns HTML (every text is escaped).
|
|
3165
3255
|
function renderJobPlan(plan){
|
|
3166
|
-
var h = '<div class="jp" data-testid="job-plan">' + (plan.older ? '<div class="older" data-testid="plan-older">older job \u2014 numbered lines were not saved</div>' : "") + '<div class="jp-legend"
|
|
3256
|
+
var h = '<div class="jp" data-testid="job-plan">' + (plan.older ? '<div class="older" data-testid="plan-older">older job \u2014 numbered lines were not saved</div>' : "") + '<div class="jp-legend">Done when every <b>\u203A</b> check passes</div>' +
|
|
3167
3257
|
(plan.goal && !plan.older ? '<div class="jp-sum" data-testid="plan-goal"><span class="lbl">AI\'s summary</span>' + escapeXml(plan.goal) + "</div>" : "");
|
|
3168
3258
|
var checkLine = function(c){
|
|
3169
3259
|
return '<div class="fold" data-testid="plan-check"><span class="mk">\u203A</span> check \u00B7 ' + escapeXml(c.name) + (c.cls ? " \u00B7 " + escapeXml(c.cls) : "") + "</div>";
|
|
3170
3260
|
};
|
|
3171
3261
|
(plan.lines || []).forEach(function(l){
|
|
3172
3262
|
h += '<div class="jl" data-testid="plan-line"><span class="num">' + escapeXml(String(l.n)) + "</span>" + escapeXml(l.text);
|
|
3173
|
-
|
|
3263
|
+
// one `~` row per rule (fwdloop's shape); an older plan that carries only the joined `rule` still shows it
|
|
3264
|
+
(l.rules && l.rules.length ? l.rules : (l.rule ? [l.rule] : [])).forEach(function(r){ h += '<div class="sub">~ ' + escapeXml(r) + "</div>"; });
|
|
3174
3265
|
(l.checks || []).forEach(function(c){ h += checkLine(c); });
|
|
3175
3266
|
(l.refused || []).forEach(function(r){ h += '<div class="fold" data-testid="plan-refused"><span class="mk">\u203A</span> not checked \u00B7 ' + escapeXml(r) + "</div>"; });
|
|
3176
3267
|
h += "</div>";
|
|
@@ -3184,7 +3275,7 @@
|
|
|
3184
3275
|
items.map(function(x){ return "<li>" + escapeXml(x) + "</li>"; }).join("") + "</ul></details>";
|
|
3185
3276
|
};
|
|
3186
3277
|
h += fold("Not checked (the AI's own reading)", plan.notChecked, "plan-notchecked");
|
|
3187
|
-
h += fold("Always
|
|
3278
|
+
h += fold("Always checked", plan.alwaysOn, "plan-alwayson");
|
|
3188
3279
|
return h + "</div>";
|
|
3189
3280
|
}
|
|
3190
3281
|
|
|
@@ -3330,7 +3421,7 @@
|
|
|
3330
3421
|
pollFailStreak = 0;
|
|
3331
3422
|
var runs = result.runs || [];
|
|
3332
3423
|
var next = sig(runs);
|
|
3333
|
-
if(next === lastRunsSig) return; // unchanged payload — never rebuild the DOM for nothing
|
|
3424
|
+
if(next === lastRunsSig) return runs; // unchanged payload — never rebuild the DOM for nothing
|
|
3334
3425
|
lastRunsSig = next;
|
|
3335
3426
|
withScrollPreserved(["left-pane-body"], function(){
|
|
3336
3427
|
// setItems re-runs the SAME filter/search state and view mode
|
|
@@ -3338,6 +3429,7 @@
|
|
|
3338
3429
|
// bar's own state) — a poll tick never resets either.
|
|
3339
3430
|
runsFilterBar.setItems(runs);
|
|
3340
3431
|
});
|
|
3432
|
+
return runs;
|
|
3341
3433
|
}).catch(function(){
|
|
3342
3434
|
pollFailStreak++;
|
|
3343
3435
|
// Keep the last good render; a transient failure is invisible. Only a
|
|
@@ -3478,7 +3570,7 @@
|
|
|
3478
3570
|
document.getElementById("btn-settings").addEventListener("click", function(){
|
|
3479
3571
|
mainView.hidden = true;
|
|
3480
3572
|
settingsView.hidden = false;
|
|
3481
|
-
loadProviders();
|
|
3573
|
+
loadProviders(true);
|
|
3482
3574
|
loadMoney();
|
|
3483
3575
|
});
|
|
3484
3576
|
document.getElementById("settings-back").addEventListener("click", function(){
|
|
@@ -3487,7 +3579,7 @@
|
|
|
3487
3579
|
});
|
|
3488
3580
|
document.getElementById("tab-money").addEventListener("click", loadMoney);
|
|
3489
3581
|
|
|
3490
|
-
// ---- Providers tab (
|
|
3582
|
+
// ---- Providers tab (P8): one block per key in the keys file that has a value; rewritten from fwdloop src/panel/index.html@f9449e8 ----
|
|
3491
3583
|
var pvRows = document.getElementById("pv-rows");
|
|
3492
3584
|
var pvNote = document.getElementById("pv-note");
|
|
3493
3585
|
var PV_NOTE_DEFAULT = pvNote.textContent;
|
|
@@ -3497,104 +3589,316 @@
|
|
|
3497
3589
|
if(n >= 1e3) return (n / 1e3).toFixed(1) + "k";
|
|
3498
3590
|
return String(n);
|
|
3499
3591
|
}
|
|
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;
|
|
3592
|
+
// A hint line: plain text, red when it is a refusal.
|
|
3593
|
+
function setNote(el, bad, text){
|
|
3594
|
+
el.className = "hint" + (bad ? " bad" : "");
|
|
3595
|
+
el.textContent = text || "";
|
|
3596
|
+
}
|
|
3597
|
+
// The server's own plain sentence (`say`); the page never shows a code or an HTTP status.
|
|
3598
|
+
function settingsRefusal(r){
|
|
3599
|
+
var b = r && r.body;
|
|
3600
|
+
if(b && typeof b.say === "string" && b.say) return b.say;
|
|
3601
|
+
return "Settings cannot do that. Keys are typed into your keys file by hand.";
|
|
3602
|
+
}
|
|
3603
|
+
function networkText(){ return "Could not reach the panel, so nothing is confirmed. Check it is still running."; }
|
|
3604
|
+
// `list` true = also ask each provider for its model list (free): on page open and on Reload keys. A save repaints without asking again.
|
|
3605
|
+
function loadProviders(list){
|
|
3606
|
+
return authorGet("/api/settings/providers").then(function(d){
|
|
3607
|
+
if(!d || !d.ok){ setNote(document.getElementById("keys-refusal"), true, "Could not read your providers. Reload the page."); return; }
|
|
3608
|
+
paintProviders(d, list === true);
|
|
3538
3609
|
document.dispatchEvent(new CustomEvent("bareloop-providers-changed"));
|
|
3539
|
-
}
|
|
3610
|
+
}, function(){ setNote(document.getElementById("keys-refusal"), true, networkText()); });
|
|
3611
|
+
}
|
|
3612
|
+
// Open keys folder: the server opens the FOLDER in the file manager; if it cannot, the path shows as text
|
|
3613
|
+
document.getElementById("open-keys-folder").addEventListener("click", function(){
|
|
3614
|
+
var out = document.getElementById("keys-open-note");
|
|
3615
|
+
out.textContent = "";
|
|
3616
|
+
authorPost("/api/settings/open-keys-folder", {}).then(function(r){
|
|
3617
|
+
var b = r.body || {};
|
|
3618
|
+
if(b.ok === true) out.textContent = b.opened ? "" : "Could not open it here. Your keys folder: " + (b.path || "(unknown)");
|
|
3619
|
+
else out.textContent = settingsRefusal(r);
|
|
3620
|
+
}, function(){ out.textContent = networkText(); });
|
|
3621
|
+
});
|
|
3622
|
+
document.getElementById("reload-keys").addEventListener("click", function(){ loadProviders(true); });
|
|
3623
|
+
|
|
3624
|
+
// 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,
|
|
3625
|
+
// 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
|
|
3626
|
+
// 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).
|
|
3627
|
+
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
|
|
3628
|
+
var typedOpen = {}; // slot -> true while "+ type a model name" is open
|
|
3629
|
+
function buildProviderRow(row){
|
|
3630
|
+
var key = document.createElement("div");
|
|
3631
|
+
key.className = "pv-key";
|
|
3632
|
+
key.setAttribute("data-slot", row.slot);
|
|
3633
|
+
key.setAttribute("data-testid", "provider-row-" + row.slot);
|
|
3634
|
+
key.innerHTML =
|
|
3635
|
+
'<div class="pv-krow">' +
|
|
3636
|
+
'<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>' +
|
|
3637
|
+
'<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>' +
|
|
3638
|
+
'<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>' +
|
|
3639
|
+
'<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>' +
|
|
3640
|
+
'<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>' +
|
|
3641
|
+
'<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>' +
|
|
3642
|
+
'</div>' +
|
|
3643
|
+
'<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>' +
|
|
3644
|
+
'<div class="pv-models" data-models-for="' + escapeXml(row.slot) + '" data-testid="models-row-' + escapeXml(row.slot) + '">' +
|
|
3645
|
+
'<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>' +
|
|
3646
|
+
'<div data-f="models"></div>' +
|
|
3647
|
+
'<div class="pv-more"><button class="pv-add" type="button" data-act="typed-open" data-testid="model-typed-open">+ type a model name</button>' +
|
|
3648
|
+
'<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"> ' +
|
|
3649
|
+
'<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>' +
|
|
3650
|
+
'</div>';
|
|
3651
|
+
key.querySelector("code").textContent = row.envVar;
|
|
3652
|
+
key.querySelectorAll("input").forEach(function(inp){
|
|
3653
|
+
inp.addEventListener("input", function(){ inp.setAttribute("data-dirty", "1"); });
|
|
3654
|
+
});
|
|
3655
|
+
key.querySelector("select").addEventListener("change", function(ev){ ev.target.setAttribute("data-dirty", "1"); });
|
|
3656
|
+
return key;
|
|
3657
|
+
}
|
|
3658
|
+
// 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.
|
|
3659
|
+
function buildModelLine(slot, m){
|
|
3660
|
+
var d = document.createElement("div");
|
|
3661
|
+
d.className = "pv-mrow";
|
|
3662
|
+
d.setAttribute("data-model", m.model);
|
|
3663
|
+
d.setAttribute("data-testid", "model-line");
|
|
3664
|
+
d.innerHTML =
|
|
3665
|
+
'<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>' +
|
|
3666
|
+
'<div class="pv-cell pv-pair"><label><input type="text" inputmode="decimal" data-mf="inPerM" data-testid="model-in" autocomplete="off"></label>' +
|
|
3667
|
+
'<label><input type="text" inputmode="decimal" data-mf="outPerM" data-testid="model-out" autocomplete="off"></label></div>' +
|
|
3668
|
+
'<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>' +
|
|
3669
|
+
'<div class="hint pv-msg pv-m-msg" data-f="msg-model" data-testid="model-msg"></div>';
|
|
3670
|
+
d.querySelector(".pv-m-name").textContent = m.model;
|
|
3671
|
+
d.querySelector("[data-mf=use]").setAttribute("aria-label", slot + " " + m.model + " use");
|
|
3672
|
+
d.querySelector("[data-mf=inPerM]").setAttribute("aria-label", m.model + " price in per 1M tokens");
|
|
3673
|
+
d.querySelector("[data-mf=outPerM]").setAttribute("aria-label", m.model + " price out per 1M tokens");
|
|
3674
|
+
d.querySelectorAll("input[type=text]").forEach(function(inp){
|
|
3675
|
+
inp.addEventListener("input", function(){ inp.setAttribute("data-dirty", "1"); });
|
|
3676
|
+
});
|
|
3677
|
+
return d;
|
|
3678
|
+
}
|
|
3679
|
+
// 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.
|
|
3680
|
+
var listed = {};
|
|
3681
|
+
var lastData = null;
|
|
3682
|
+
function linesFor(row){
|
|
3683
|
+
var l = listed[row.slot];
|
|
3684
|
+
var have = {};
|
|
3685
|
+
row.models.forEach(function(m){ have[m.model] = true; });
|
|
3686
|
+
var extra = (l && l.models ? l.models : []).filter(function(id){ return !have[id]; }).map(function(id){ return { model: id, use: false }; });
|
|
3687
|
+
return row.models.concat(extra);
|
|
3688
|
+
}
|
|
3689
|
+
// "3 models · none ticked", "2 models · 1 ticked"; a list that failed with nothing saved says so.
|
|
3690
|
+
function sumText(row){
|
|
3691
|
+
var l = listed[row.slot];
|
|
3692
|
+
var lines = linesFor(row);
|
|
3693
|
+
if(l && l.why && lines.length === 0) return "failed — type a name";
|
|
3694
|
+
var ticked = lines.filter(function(m){ return m.use === true; }).length;
|
|
3695
|
+
return lines.length + (lines.length === 1 ? " model" : " models") + " · " + (ticked === 0 ? "none ticked" : ticked + " ticked");
|
|
3696
|
+
}
|
|
3697
|
+
function paintModels(row, mr){
|
|
3698
|
+
var box = mr.querySelector("[data-f=models]");
|
|
3699
|
+
var lines = linesFor(row);
|
|
3700
|
+
var l = listed[row.slot];
|
|
3701
|
+
var typed = mr.querySelector("[data-f=typed]");
|
|
3702
|
+
typed.hidden = typedOpen[row.slot] !== true;
|
|
3703
|
+
var wanted = lines.map(function(m){ return m.model; });
|
|
3704
|
+
Array.prototype.slice.call(box.querySelectorAll(".pv-mrow")).forEach(function(el){
|
|
3705
|
+
if(wanted.indexOf(el.getAttribute("data-model")) < 0) box.removeChild(el);
|
|
3706
|
+
});
|
|
3707
|
+
var note = box.querySelector("[data-f=models-note]");
|
|
3708
|
+
lines.forEach(function(m){
|
|
3709
|
+
var line = Array.prototype.find.call(box.querySelectorAll(".pv-mrow"), function(el){ return el.getAttribute("data-model") === m.model; });
|
|
3710
|
+
if(!line){ line = buildModelLine(row.slot, m); box.insertBefore(line, note); }
|
|
3711
|
+
var use = line.querySelector("[data-mf=use]");
|
|
3712
|
+
use.checked = m.use === true;
|
|
3713
|
+
["inPerM", "outPerM"].forEach(function(f){
|
|
3714
|
+
var inp = line.querySelector('[data-mf="' + f + '"]');
|
|
3715
|
+
inp.placeholder = "—";
|
|
3716
|
+
if(inp.getAttribute("data-dirty") !== "1") inp.value = typeof m[f] === "number" ? String(m[f]) : "";
|
|
3717
|
+
});
|
|
3718
|
+
line.querySelector("[data-f=model-tokens]").textContent = typeof m.tokens === "number" ? tokensText(m.tokens) : "—";
|
|
3719
|
+
});
|
|
3720
|
+
note = box.querySelector("[data-f=models-note]");
|
|
3721
|
+
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); }
|
|
3722
|
+
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." : ""));
|
|
3723
|
+
note.className += " pv-more" + (l && l.why ? " pv-failed" : "");
|
|
3724
|
+
box.parentNode.querySelector(".pv-mhead").hidden = lines.length === 0;
|
|
3540
3725
|
}
|
|
3541
|
-
|
|
3542
|
-
//
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
var pout = String(v.priceOut).trim();
|
|
3548
|
-
if((pin === "") !== (pout === "")) return null;
|
|
3549
|
-
return { envName: env, name: v.name, shape: v.shape, baseUrl: v.baseUrl, priceInPerM: pin, priceOutPerM: pout };
|
|
3726
|
+
var STATUS_WORD = { "set in file": "set in file", "found in shell": "found in shell", "not set": "not set", "refused": "refused — see above" };
|
|
3727
|
+
// The Refresh button: "[ ⟳ Refresh ]", disabled "[ ⟳ refreshing… ]" while the free list call is out.
|
|
3728
|
+
function setRefreshBtn(key, pending){
|
|
3729
|
+
var b = key.querySelector("[data-act=refresh-models]");
|
|
3730
|
+
b.disabled = pending === true;
|
|
3731
|
+
b.innerHTML = pending === true ? "⟳ refreshing…" : "⟳ Refresh";
|
|
3550
3732
|
}
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
var
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
}).catch(function(){ pvNote.textContent = "could not save"; });
|
|
3733
|
+
// 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.
|
|
3734
|
+
function testKey(slot, key){
|
|
3735
|
+
var b = key.querySelector("[data-act=test-key]");
|
|
3736
|
+
var msg = key.querySelector("[data-f=msg-test]");
|
|
3737
|
+
var row = lastData && lastData.rows.filter(function(r){ return r.slot === slot; })[0];
|
|
3738
|
+
if(row && row.baseUrl === ""){
|
|
3739
|
+
setNote(msg, true, "Type a Base URL for " + row.envVar + " in Settings.");
|
|
3559
3740
|
return;
|
|
3560
3741
|
}
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3742
|
+
b.disabled = true;
|
|
3743
|
+
b.innerHTML = "testing…";
|
|
3744
|
+
setNote(msg, false, "");
|
|
3745
|
+
authorPost("/api/settings/test", { slot: slot }).then(function(r){
|
|
3746
|
+
var d = r.body;
|
|
3747
|
+
if(d && d.ok === true) setNote(msg, false, "OK \u00b7 " + d.ms + " ms");
|
|
3748
|
+
else setNote(msg, true, d && d.why ? d.why : settingsRefusal(r));
|
|
3749
|
+
}, function(){ setNote(msg, true, networkText()); }).then(function(){
|
|
3750
|
+
b.disabled = false;
|
|
3751
|
+
b.innerHTML = "Test";
|
|
3569
3752
|
});
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3753
|
+
}
|
|
3754
|
+
function refreshModels(slot, tr){
|
|
3755
|
+
listed[slot] = { pending: true };
|
|
3756
|
+
setRefreshBtn(tr, true);
|
|
3757
|
+
return authorPost("/api/settings/models", { slot: slot }).then(function(r){
|
|
3758
|
+
var b = r.body;
|
|
3759
|
+
listed[slot] = b && b.ok ? { models: b.models } : { why: (b && b.why) ? b.why : settingsRefusal(r) };
|
|
3760
|
+
}, function(){ listed[slot] = { why: networkText() }; }).then(function(){
|
|
3761
|
+
if(lastData) paintProviders(lastData, false);
|
|
3762
|
+
});
|
|
3763
|
+
}
|
|
3764
|
+
function paintProviders(data, list){
|
|
3765
|
+
lastData = data;
|
|
3766
|
+
var path = data.keysFile && data.keysFile.path ? data.keysFile.path : "~/.config/bareloop/.env";
|
|
3767
|
+
document.getElementById("keys-path").textContent = path;
|
|
3768
|
+
setNote(document.getElementById("keys-refusal"), true, data.keysFile && data.keysFile.refusal);
|
|
3769
|
+
setNote(pvNote, !!data.configProblem, data.configProblem || PV_NOTE_DEFAULT);
|
|
3770
|
+
data.rows.forEach(function(row){
|
|
3771
|
+
var key = pvRows.querySelector('.pv-key[data-slot="' + row.slot + '"]');
|
|
3772
|
+
if(!key){ key = buildProviderRow(row); pvRows.appendChild(key); }
|
|
3773
|
+
if(folds[row.slot] === undefined) folds[row.slot] = row.models.some(function(m){ return m.use === true; });
|
|
3774
|
+
key.setAttribute("data-open", folds[row.slot] ? "1" : "0");
|
|
3775
|
+
var fold = key.querySelector("[data-act=fold]");
|
|
3776
|
+
fold.textContent = folds[row.slot] ? "▾" : "▸";
|
|
3777
|
+
fold.setAttribute("aria-expanded", folds[row.slot] ? "true" : "false");
|
|
3778
|
+
fold.setAttribute("aria-label", (folds[row.slot] ? "Fold " : "Unfold ") + row.label + " models");
|
|
3779
|
+
paintModels(row, key.querySelector(".pv-models"));
|
|
3780
|
+
setRefreshBtn(key, !!(listed[row.slot] && listed[row.slot].pending));
|
|
3781
|
+
key.querySelector("[data-f=sum]").textContent = sumText(row);
|
|
3782
|
+
var st = key.querySelector("[data-f=status]");
|
|
3783
|
+
var word = STATUS_WORD[row.keyStatus] || row.keyStatus;
|
|
3784
|
+
st.className = row.keyStatus === "set in file" || row.keyStatus === "found in shell" ? "pv-status pv-ok" : "pv-status hint";
|
|
3785
|
+
st.textContent = st.className.indexOf("pv-ok") >= 0 ? "✓" : word;
|
|
3786
|
+
st.setAttribute("title", word);
|
|
3787
|
+
if(st.className.indexOf("pv-ok") >= 0) st.setAttribute("aria-label", word); else st.removeAttribute("aria-label");
|
|
3788
|
+
var legacyIn = key.querySelector('[data-prov=legacyMaxTokens]');
|
|
3789
|
+
if(document.activeElement !== legacyIn) legacyIn.checked = row.legacyMaxTokens === true;
|
|
3790
|
+
key.querySelector("[data-f=legacy-word]").textContent = legacyIn.checked ? "on" : "off";
|
|
3791
|
+
// 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)
|
|
3792
|
+
var sel = key.querySelector("[data-prov=shape]");
|
|
3793
|
+
if(sel.options.length !== data.shapes.length || data.shapes.some(function(sh, i){ return sel.options[i].value !== sh.id; })){
|
|
3794
|
+
sel.textContent = "";
|
|
3795
|
+
data.shapes.forEach(function(sh){ var o = document.createElement("option"); o.value = sh.id; o.textContent = sh.label; sel.appendChild(o); });
|
|
3796
|
+
}
|
|
3797
|
+
if(sel.getAttribute("data-dirty") !== "1") sel.value = row.shape;
|
|
3798
|
+
var urlIn = key.querySelector("[data-prov=baseUrl]");
|
|
3799
|
+
if(urlIn.getAttribute("data-dirty") !== "1" && document.activeElement !== urlIn) urlIn.value = row.baseUrl;
|
|
3800
|
+
key.querySelector("[data-f=tokens]").textContent = tokensText(row.tokens);
|
|
3801
|
+
});
|
|
3802
|
+
// rows that left the keys file leave the page
|
|
3803
|
+
Array.prototype.slice.call(pvRows.querySelectorAll(".pv-key")).forEach(function(key){
|
|
3804
|
+
if(!data.rows.some(function(r){ return r.slot === key.getAttribute("data-slot"); })) pvRows.removeChild(key);
|
|
3805
|
+
});
|
|
3806
|
+
if(list) data.rows.forEach(function(row){
|
|
3807
|
+
var key = pvRows.querySelector('.pv-key[data-slot="' + row.slot + '"]');
|
|
3808
|
+
if(row.baseUrl === ""){ listed[row.slot] = { why: "Type a Base URL for " + row.slot + " in Settings." }; paintProviders(lastData, false); }
|
|
3809
|
+
else refreshModels(row.slot, key);
|
|
3810
|
+
});
|
|
3811
|
+
}
|
|
3812
|
+
// 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
|
|
3813
|
+
// a refusal (nothing saved, the typed value stays so the person can fix it).
|
|
3814
|
+
function saveEndpoint(key){
|
|
3815
|
+
var msg = key.querySelector("[data-f=msg-edit]");
|
|
3816
|
+
var payload = { slot: key.getAttribute("data-slot"), shape: key.querySelector("[data-prov=shape]").value, baseUrl: key.querySelector("[data-prov=baseUrl]").value.trim() };
|
|
3817
|
+
authorPost("/api/settings/provider", payload).then(function(r){
|
|
3818
|
+
if(r.body && r.body.ok){
|
|
3819
|
+
key.querySelector("[data-prov=shape]").removeAttribute("data-dirty");
|
|
3820
|
+
key.querySelector("[data-prov=baseUrl]").removeAttribute("data-dirty");
|
|
3821
|
+
setNote(msg, true, "");
|
|
3822
|
+
loadProviders();
|
|
3823
|
+
} else setNote(msg, true, settingsRefusal(r));
|
|
3824
|
+
}, function(){ setNote(msg, true, networkText()); });
|
|
3825
|
+
}
|
|
3826
|
+
pvRows.addEventListener("click", function(ev){
|
|
3827
|
+
var b = ev.target.closest && ev.target.closest("button[data-act]");
|
|
3828
|
+
if(!b) return;
|
|
3829
|
+
var key = b.closest(".pv-key");
|
|
3830
|
+
var slot = key.getAttribute("data-slot");
|
|
3831
|
+
var act = b.getAttribute("data-act");
|
|
3832
|
+
if(act === "refresh-models") refreshModels(slot, key);
|
|
3833
|
+
else if(act === "test-key") testKey(slot, key);
|
|
3834
|
+
else if(act === "fold"){ folds[slot] = !folds[slot]; paintProviders(lastData, false); }
|
|
3835
|
+
else if(act === "typed-open"){ typedOpen[slot] = !typedOpen[slot]; paintProviders(lastData, false); }
|
|
3836
|
+
else if(act === "add-model") addTypedModel(slot, b.closest("[data-f=typed]"));
|
|
3586
3837
|
});
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3838
|
+
// 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.
|
|
3839
|
+
pvRows.addEventListener("change", function(ev){
|
|
3840
|
+
var inp = ev.target;
|
|
3841
|
+
if(!inp.getAttribute) return;
|
|
3842
|
+
if(inp.getAttribute("data-prov") === "legacyMaxTokens"){ saveProvider(inp.closest(".pv-key"), inp); return; }
|
|
3843
|
+
if(inp.getAttribute("data-prov") === "shape" || inp.getAttribute("data-prov") === "baseUrl"){ saveEndpoint(inp.closest(".pv-key")); return; }
|
|
3844
|
+
if(!inp.getAttribute("data-mf")) return;
|
|
3845
|
+
saveModel(inp.closest(".pv-models") ? inp.closest(".pv-models").getAttribute("data-models-for") : "", inp.closest(".pv-mrow"));
|
|
3595
3846
|
});
|
|
3847
|
+
// A box the person typed: empty = back to the default (null); a number is sent as a number; anything else is
|
|
3848
|
+
// sent as the text typed, so the server refuses it by naming the box.
|
|
3849
|
+
function priceValue(text){
|
|
3850
|
+
var t = String(text).trim();
|
|
3851
|
+
if(t === "") return null;
|
|
3852
|
+
var n = Number(t);
|
|
3853
|
+
return isFinite(n) ? n : t;
|
|
3854
|
+
}
|
|
3855
|
+
// One model line per save: {slot, model, use, inPerM, outPerM}. A refusal is the line's hint (the server's one rule),
|
|
3856
|
+
// and nothing was saved: the repaint puts the tick back.
|
|
3857
|
+
function saveModel(slot, line){
|
|
3858
|
+
var msg = line.querySelector("[data-f=msg-model]");
|
|
3859
|
+
var payload = {
|
|
3860
|
+
slot: slot,
|
|
3861
|
+
model: line.getAttribute("data-model"),
|
|
3862
|
+
use: line.querySelector("[data-mf=use]").checked,
|
|
3863
|
+
inPerM: priceValue(line.querySelector("[data-mf=inPerM]").value),
|
|
3864
|
+
outPerM: priceValue(line.querySelector("[data-mf=outPerM]").value)
|
|
3865
|
+
};
|
|
3866
|
+
authorPost("/api/settings/model", payload).then(function(r){
|
|
3867
|
+
if(r.body && r.body.ok){
|
|
3868
|
+
line.querySelectorAll("input[type=text]").forEach(function(inp){ inp.removeAttribute("data-dirty"); });
|
|
3869
|
+
loadProviders();
|
|
3870
|
+
setNote(msg, false, "saved — applies from the next model call");
|
|
3871
|
+
} else { setNote(msg, true, settingsRefusal(r)); loadProviders(); }
|
|
3872
|
+
}, function(){ setNote(msg, true, networkText()); });
|
|
3873
|
+
}
|
|
3874
|
+
|
|
3875
|
+
// 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.
|
|
3876
|
+
function addTypedModel(slot, box){
|
|
3877
|
+
var inp = box.querySelector("input");
|
|
3878
|
+
var msg = box.querySelector("[data-f=typed-msg]");
|
|
3879
|
+
authorPost("/api/settings/model", { slot: slot, model: inp.value.trim(), use: false }).then(function(r){
|
|
3880
|
+
if(r.body && r.body.ok){ inp.value = ""; setNote(msg, false, "added"); loadProviders(); }
|
|
3881
|
+
else setNote(msg, true, settingsRefusal(r));
|
|
3882
|
+
}, function(){ setNote(msg, true, networkText()); });
|
|
3883
|
+
}
|
|
3884
|
+
|
|
3885
|
+
// The legacy tick saves alone: {slot, legacyMaxTokens}. A refusal is the cell's hint line.
|
|
3886
|
+
function saveProvider(key, inp){
|
|
3887
|
+
var field = inp.getAttribute("data-prov");
|
|
3888
|
+
var msg = key.querySelector("[data-f=msg-" + field + "]");
|
|
3889
|
+
var payload = { slot: key.getAttribute("data-slot") };
|
|
3890
|
+
payload[field] = inp.checked;
|
|
3891
|
+
authorPost("/api/settings/provider", payload).then(function(r){
|
|
3892
|
+
if(r.body && r.body.ok){
|
|
3893
|
+
inp.removeAttribute("data-dirty");
|
|
3894
|
+
loadProviders();
|
|
3895
|
+
setNote(msg, false, "saved — applies from the next model call");
|
|
3896
|
+
} else setNote(msg, true, settingsRefusal(r));
|
|
3897
|
+
}, function(){ setNote(msg, true, networkText()); });
|
|
3898
|
+
}
|
|
3899
|
+
|
|
3900
|
+
document.getElementById("tab-providers").addEventListener("click", function(){ loadProviders(); });
|
|
3596
3901
|
wireTabs([document.getElementById("tab-providers"), document.getElementById("tab-money")]);
|
|
3597
|
-
document.getElementById("tab-providers").addEventListener("click", loadProviders);
|
|
3598
3902
|
// Auto-save on a human edit (change = blur or Enter): POST carries the per-start token; the chat has no route here
|
|
3599
3903
|
limitInput.addEventListener("change", function(){
|
|
3600
3904
|
var text = limitInput.value.trim();
|
|
@@ -3622,12 +3926,16 @@
|
|
|
3622
3926
|
var mainHint = document.getElementById("chat-main-hint");
|
|
3623
3927
|
var reopenBtn = document.getElementById("chat-reopen-btn");
|
|
3624
3928
|
var mainAction = "none";
|
|
3929
|
+
var knownRunIdsAtSign = null; // the run ids listed when Sign & run was pressed (M3: afterSignRefresh waits for a new one)
|
|
3625
3930
|
var lastState = null;
|
|
3626
3931
|
var startOk = false;
|
|
3627
3932
|
var errEl = document.getElementById("chat-card-error");
|
|
3628
3933
|
var actionErrEl = document.getElementById("chat-action-error");
|
|
3629
3934
|
var moneyCapInput = document.getElementById("jf-cap-money");
|
|
3630
3935
|
var modelSelect = document.getElementById("jf-model");
|
|
3936
|
+
var thinkingBox = document.getElementById("jf-thinking");
|
|
3937
|
+
var thinkingNote = document.getElementById("jf-thinking-note");
|
|
3938
|
+
var lineThinking = {}; // option value -> the server's {support, canChange, say} for that Model line
|
|
3631
3939
|
var modelStatusEl = document.getElementById("jf-model-status");
|
|
3632
3940
|
var progressRow = document.getElementById("chat-progress-row");
|
|
3633
3941
|
|
|
@@ -3672,11 +3980,14 @@
|
|
|
3672
3980
|
var resuming = resumeRun !== null;
|
|
3673
3981
|
var setLock = function(id, on){ var el = document.getElementById(id); el.readOnly = on; el.classList.toggle("locked", on); };
|
|
3674
3982
|
LOCKED_IDS.forEach(function(id){ setLock(id, reuseOn || live || resuming); });
|
|
3675
|
-
|
|
3676
|
-
|
|
3983
|
+
// the one exception on a resume card: an older spec that names no model leaves the Model box open (picked at resume)
|
|
3984
|
+
var modelOpen = resuming && resumeRun.pickModel === true;
|
|
3985
|
+
OPEN_IDS.forEach(function(id){ setLock(id, live || (resuming && CAP_IDS.indexOf(id) === -1 && !(modelOpen && id === "jf-model"))); });
|
|
3986
|
+
document.getElementById("jf-model").disabled = live || (resuming && !modelOpen);
|
|
3677
3987
|
// the Check type radios stay clickable on a reuse card (Deterministic / Rubric = leave reuse); a live session or a resume freezes them
|
|
3678
3988
|
document.querySelectorAll('input[name="jf-verdict"]').forEach(function(r){ r.disabled = live || resuming; });
|
|
3679
3989
|
document.getElementById("job-card").classList.toggle("reuse", reuseOn);
|
|
3990
|
+
syncThinking();
|
|
3680
3991
|
}
|
|
3681
3992
|
function setReuseLocked(on){ reuseOn = on; syncCardLock(); }
|
|
3682
3993
|
function clearStartFrom(keepBoxes){
|
|
@@ -3754,7 +4065,7 @@
|
|
|
3754
4065
|
}
|
|
3755
4066
|
}
|
|
3756
4067
|
function loadReuseJobs(){
|
|
3757
|
-
authorGet("/api/author/reuse-jobs" + (modelSelect.value ? "?
|
|
4068
|
+
authorGet("/api/author/reuse-jobs" + (modelSelect.value ? "?" + pickQuery(modelSelect.value) : "")).then(function(r){
|
|
3758
4069
|
rwFailed = !(r && r.ok);
|
|
3759
4070
|
rwJobs = rwFailed ? null : r.jobs;
|
|
3760
4071
|
renderReuseList();
|
|
@@ -3771,7 +4082,7 @@
|
|
|
3771
4082
|
refreshStartEnabled();
|
|
3772
4083
|
rwQ.focus();
|
|
3773
4084
|
}
|
|
3774
|
-
function pickReuseJob(j){ if(j) reuseWorkflow(j.runid, modelSelect.value); }
|
|
4085
|
+
function pickReuseJob(j){ if(j) reuseWorkflow(j.runid, pickQuery(modelSelect.value)); }
|
|
3775
4086
|
rwQ.addEventListener("input", function(){ rwHl = 0; renderReuseList(); });
|
|
3776
4087
|
rwQ.addEventListener("keydown", function(e){
|
|
3777
4088
|
if(e.key === "ArrowDown"){ rwHl = Math.min(rwHl + 1, rwShown.length - 1); renderReuseList(); e.preventDefault(); }
|
|
@@ -3791,7 +4102,9 @@
|
|
|
3791
4102
|
document.getElementById("rw-change").addEventListener("click", unpickReuse);
|
|
3792
4103
|
// leaving resume mode: the title, the hidden drafting chrome, the time box and the resume box all come back
|
|
3793
4104
|
function exitResumeMode(){
|
|
4105
|
+
var filtered = resumeRun !== null && !!resumeRun.worker;
|
|
3794
4106
|
resumeRun = null;
|
|
4107
|
+
if(filtered){ renderModelOptions(); }
|
|
3795
4108
|
originLine.classList.remove("res");
|
|
3796
4109
|
document.getElementById("job-card-title").textContent = "Job card \u2014 draft";
|
|
3797
4110
|
document.getElementById("chat-resume-box").hidden = true;
|
|
@@ -3836,6 +4149,7 @@
|
|
|
3836
4149
|
clearCardBoxes();
|
|
3837
4150
|
setVerdict("deterministic");
|
|
3838
4151
|
modelSelect.selectedIndex = 0;
|
|
4152
|
+
setThinkingTick(false);
|
|
3839
4153
|
msgInput.value = "";
|
|
3840
4154
|
rwQ.value = "";
|
|
3841
4155
|
lastPhase = null;
|
|
@@ -3861,11 +4175,14 @@
|
|
|
3861
4175
|
return {
|
|
3862
4176
|
// on "Reuse workflow" the type is the picked job's own, never the radio's
|
|
3863
4177
|
checkType: (verdictValue() === "reuse" ? reuseCheckType : verdictValue()) === "rubric" ? "rubric" : "deterministic",
|
|
3864
|
-
|
|
4178
|
+
provider: pickParts(document.getElementById("jf-model").value).provider,
|
|
4179
|
+
model: pickParts(document.getElementById("jf-model").value).model,
|
|
3865
4180
|
jobName: document.getElementById("jf-name").value.trim(),
|
|
3866
4181
|
jobText: document.getElementById("jf-job").value.trim(),
|
|
3867
4182
|
inputs: document.getElementById("jf-inputs").value.trim(),
|
|
3868
4183
|
destination: document.getElementById("jf-dest").value.trim(),
|
|
4184
|
+
// the "Thinking on" tick: sent as a boolean; the server judges it per line and signs it only when ticked
|
|
4185
|
+
thinking: thinkingBox.checked === true,
|
|
3869
4186
|
capUsd: moneyCap,
|
|
3870
4187
|
maxWallMs: timeCapMin === "" ? undefined : Math.round(Number(timeCapMin) * 60000),
|
|
3871
4188
|
};
|
|
@@ -3883,6 +4200,7 @@
|
|
|
3883
4200
|
var capOk = moneyCapInput.value.trim() !== "" && Number.isFinite(v) && v > 0;
|
|
3884
4201
|
// on Reuse workflow nothing starts before a job is picked
|
|
3885
4202
|
startOk = capOk && !sessionLive && keyOk && (verdictValue() !== "reuse" || startFrom !== null);
|
|
4203
|
+
if(typeof syncResumeSign === "function"){ syncResumeSign(); }
|
|
3886
4204
|
syncCardLock();
|
|
3887
4205
|
renderMain();
|
|
3888
4206
|
refreshClearBtn();
|
|
@@ -3963,21 +4281,30 @@
|
|
|
3963
4281
|
return { keyOk: true, ok: false, text: reachStatusText(reach.status) };
|
|
3964
4282
|
}
|
|
3965
4283
|
function refreshModelStatus(){
|
|
3966
|
-
var modelId = modelSelect.value;
|
|
3967
|
-
if(
|
|
4284
|
+
var modelId = pickParts(modelSelect.value).model;
|
|
4285
|
+
if(modelSelect.value === ""){
|
|
3968
4286
|
keyOk = false;
|
|
3969
4287
|
modelStatusEl.className = "jf-readiness bad";
|
|
3970
|
-
modelStatusEl.textContent = "[✗] no model —
|
|
4288
|
+
modelStatusEl.textContent = "[✗] no model — Tick a model in Settings > Providers.";
|
|
3971
4289
|
refreshStartEnabled();
|
|
3972
4290
|
return;
|
|
3973
4291
|
}
|
|
3974
|
-
|
|
4292
|
+
// the key state of the NEW pick is unknown until its check returns: blocked at once, and a late answer for a line
|
|
4293
|
+
// no longer selected is ignored (it would otherwise unblock Start / Sign & resume for the wrong key)
|
|
4294
|
+
var asked = modelSelect.value;
|
|
4295
|
+
keyOk = false;
|
|
4296
|
+
modelStatusEl.className = "jf-readiness unknown";
|
|
4297
|
+
modelStatusEl.textContent = "checking the key\u2026";
|
|
4298
|
+
refreshStartEnabled();
|
|
4299
|
+
authorGet("/api/author/model-check?" + pickQuery(modelSelect.value)).then(function(r){
|
|
4300
|
+
if(modelSelect.value !== asked) return;
|
|
3975
4301
|
var line = modelStatusLine(r, modelId);
|
|
3976
4302
|
keyOk = line.keyOk;
|
|
3977
4303
|
modelStatusEl.className = "jf-readiness " + (line.ok ? "ok" : "bad");
|
|
3978
4304
|
modelStatusEl.textContent = line.text;
|
|
3979
4305
|
refreshStartEnabled();
|
|
3980
4306
|
}).catch(function(){
|
|
4307
|
+
if(modelSelect.value !== asked) return;
|
|
3981
4308
|
keyOk = false;
|
|
3982
4309
|
modelStatusEl.className = "jf-readiness bad";
|
|
3983
4310
|
modelStatusEl.textContent = "[✗] could not check the key";
|
|
@@ -3989,25 +4316,89 @@
|
|
|
3989
4316
|
function refreshReuseLine(){
|
|
3990
4317
|
if(!startFrom) return;
|
|
3991
4318
|
var q = (startFrom.importId ? "import=" + encodeURIComponent(startFrom.importId) : "runid=" + encodeURIComponent(startFrom.runid)) +
|
|
3992
|
-
"&
|
|
4319
|
+
"&" + pickQuery(modelSelect.value);
|
|
3993
4320
|
authorGet("/api/author/start-from?" + q).then(function(r){
|
|
3994
4321
|
if(startFrom && r && r.ok){ sfLine.textContent = r.line ? rwMeta({checkType: reuseCheckType, line: r.line}) : ""; }
|
|
3995
4322
|
}).catch(function(){ /* the line keeps what it had */ });
|
|
3996
4323
|
}
|
|
3997
4324
|
modelSelect.addEventListener("change", refreshReuseLine);
|
|
3998
4325
|
modelSelect.addEventListener("change", function(){ if(!rwBox.hidden && startFrom === null && !reuseSession) loadReuseJobs(); });
|
|
3999
|
-
//
|
|
4000
|
-
//
|
|
4326
|
+
// P8 — the Model menu IS the Settings rows: every TICKED, PRICED line, labelled "<KEY> · <model>", in keys-file order. An option's
|
|
4327
|
+
// value carries the key AND the model ("KEY|model"): two keys can tick the same model id, and the card names which one.
|
|
4328
|
+
// Re-read whenever Settings loads or saves, keeping the pick if it survives.
|
|
4329
|
+
function pickValue(slot, model){ return (slot || "") + "|" + model; }
|
|
4330
|
+
function pickParts(v){
|
|
4331
|
+
var i = String(v).indexOf("|");
|
|
4332
|
+
return i < 0 ? {provider: "", model: String(v)} : {provider: v.slice(0, i), model: v.slice(i + 1)};
|
|
4333
|
+
}
|
|
4334
|
+
function pickQuery(v){ var p = pickParts(v); return "model=" + encodeURIComponent(p.model) + "&provider=" + encodeURIComponent(p.provider); }
|
|
4335
|
+
// restore a saved card's pick: its key and model; an older card names only the model (the first option holding it)
|
|
4336
|
+
function setPick(c){
|
|
4337
|
+
if(!c || !c.model) return;
|
|
4338
|
+
var want = c.provider ? pickValue(c.provider, c.model) : null;
|
|
4339
|
+
var opts = Array.prototype.slice.call(modelSelect.options);
|
|
4340
|
+
var hit = opts.filter(function(o){ return want ? o.value === want : pickParts(o.value).model === c.model; })[0];
|
|
4341
|
+
if(!hit){
|
|
4342
|
+
hit = document.createElement("option");
|
|
4343
|
+
hit.value = want || pickValue("", c.model);
|
|
4344
|
+
hit.textContent = (c.provider ? c.provider + " \u00b7 " : "") + c.model;
|
|
4345
|
+
modelSelect.appendChild(hit);
|
|
4346
|
+
}
|
|
4347
|
+
modelSelect.value = hit.value;
|
|
4348
|
+
syncThinking();
|
|
4349
|
+
}
|
|
4350
|
+
// The "Thinking on" box follows the SERVER's answer for the picked line (`thinking.canChange`): false greys AND unticks it with the
|
|
4351
|
+
// server's words; 'unverified' leaves it enabled with no note. The tick is the person's; the page decides nothing.
|
|
4352
|
+
function syncThinking(){
|
|
4353
|
+
var t = lineThinking[modelSelect.value];
|
|
4354
|
+
var blocked = !!(t && t.canChange === false);
|
|
4355
|
+
if(blocked) thinkingBox.checked = false;
|
|
4356
|
+
thinkingBox.disabled = blocked || sessionLive || resumeRun !== null || reuseOn;
|
|
4357
|
+
thinkingNote.textContent = blocked ? (t.say || "") : "";
|
|
4358
|
+
document.getElementById("jf-thinking-label").hidden = reuseOn;
|
|
4359
|
+
}
|
|
4360
|
+
function setThinkingTick(on){ thinkingBox.checked = on === true; syncThinking(); }
|
|
4361
|
+
modelSelect.addEventListener("change", syncThinking);
|
|
4362
|
+
var modelReads = 0;
|
|
4363
|
+
var modelLines = [];
|
|
4364
|
+
var modelSay = "";
|
|
4365
|
+
// A Resume card that must PICK a model (old spec naming none) offers only the lines resolveWorker (src/providerrows.js) accepts
|
|
4366
|
+
// for that spec: the same shape, and the same endpoint when the spec names one (`resumeRun.worker`, from start-from, endpoint
|
|
4367
|
+
// already normalised by the server). With none, the sentence names the shape to tick (label from providershapes).
|
|
4368
|
+
function renderModelOptions(){
|
|
4369
|
+
var w = resumeRun !== null && resumeRun.pickModel === true ? resumeRun.worker : null;
|
|
4370
|
+
var lines = modelLines.filter(function(m){ return !w || (m.shape === w.provider && (w.endpoint === "" || m.endpoint === w.endpoint)); });
|
|
4371
|
+
var was = modelSelect.value;
|
|
4372
|
+
lineThinking = {};
|
|
4373
|
+
lines.forEach(function(m){ lineThinking[pickValue(m.slot, m.model)] = m.thinking || null; });
|
|
4374
|
+
var none = (w && modelLines.length) ? "Tick a " + w.label + " model in Settings > Providers." : (modelSay || "Tick a model in Settings > Providers.");
|
|
4375
|
+
modelSelect.innerHTML = lines.length ? lines.map(function(m){
|
|
4376
|
+
return '<option value="' + escapeXml(pickValue(m.slot, m.model)) + '">' + escapeXml(m.label) + "</option>";
|
|
4377
|
+
}).join("") : '<option value="">' + escapeXml(none) + "</option>";
|
|
4378
|
+
if(lines.some(function(m){ return pickValue(m.slot, m.model) === was; })) modelSelect.value = was;
|
|
4379
|
+
syncThinking();
|
|
4380
|
+
}
|
|
4001
4381
|
function loadModels(){
|
|
4382
|
+
if(modelSelect.options.length === 0){
|
|
4383
|
+
modelSelect.innerHTML = '<option value="">Reading your models\u2026</option>';
|
|
4384
|
+
}
|
|
4002
4385
|
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;
|
|
4386
|
+
if(!r || !r.ok) throw new Error("read");
|
|
4387
|
+
modelReads = 0;
|
|
4388
|
+
modelLines = r.lines || [];
|
|
4389
|
+
modelSay = r.say || "";
|
|
4390
|
+
renderModelOptions();
|
|
4009
4391
|
refreshModelStatus();
|
|
4010
|
-
}).catch(function(){
|
|
4392
|
+
}).catch(function(){
|
|
4393
|
+
modelReads += 1;
|
|
4394
|
+
if(modelReads <= 5){
|
|
4395
|
+
modelSelect.innerHTML = '<option value="">Could not read your models. Trying again\u2026</option>';
|
|
4396
|
+
setTimeout(loadModels, 1500);
|
|
4397
|
+
} else {
|
|
4398
|
+
modelSelect.innerHTML = '<option value="">Could not read your models. Reload the page.</option>';
|
|
4399
|
+
}
|
|
4400
|
+
refreshModelStatus();
|
|
4401
|
+
});
|
|
4011
4402
|
}
|
|
4012
4403
|
document.addEventListener("bareloop-providers-changed", loadModels);
|
|
4013
4404
|
loadModels();
|
|
@@ -4226,6 +4617,7 @@
|
|
|
4226
4617
|
renderActions(j.state);
|
|
4227
4618
|
if(reuseSession && !autoSigned && j.state.phase === "prepared" && j.state.specHash){
|
|
4228
4619
|
autoSigned = true;
|
|
4620
|
+
knownRunIdsAtSign = currentRunIds();
|
|
4229
4621
|
authorPost("/api/author/" + sessionId + "/sign", {specHash: j.state.specHash}).then(function(r){
|
|
4230
4622
|
var o = chatPostOutcome(r);
|
|
4231
4623
|
if(o.ok){ chatActionOk(); } else { autoSigned = false; chatActionFailed(o.error); }
|
|
@@ -4234,6 +4626,7 @@
|
|
|
4234
4626
|
}
|
|
4235
4627
|
if(j.state.phase === "signed"){
|
|
4236
4628
|
clearInterval(pollTimer);
|
|
4629
|
+
afterSignRefresh(knownRunIdsAtSign || currentRunIds(), 12);
|
|
4237
4630
|
document.getElementById("tab-runs").click();
|
|
4238
4631
|
// hamr's ruling 2026-10-05: the run has started, so the Chat card goes back to the empty card (the one reset
|
|
4239
4632
|
// Clear uses) — it must not keep showing a signed job that invites a rerun. The run itself is untouched.
|
|
@@ -4256,6 +4649,8 @@
|
|
|
4256
4649
|
authorPost("/api/author/start", body).then(function(r){
|
|
4257
4650
|
if(r.status !== 200 || !r.body.ok){
|
|
4258
4651
|
errEl.textContent = (r.body && r.body.error) || "refused";
|
|
4652
|
+
// the refusal is the answer to the click: bring it into view (it sits under the cap fields, above the progress list)
|
|
4653
|
+
try { errEl.scrollIntoView({block: "nearest"}); } catch(e){}
|
|
4259
4654
|
sessionLive = false;
|
|
4260
4655
|
refreshStartEnabled();
|
|
4261
4656
|
return;
|
|
@@ -4289,10 +4684,7 @@
|
|
|
4289
4684
|
reusePickedName = c.jobName || "job";
|
|
4290
4685
|
setVerdict(reuseSession ? "reuse" : reuseCheckType);
|
|
4291
4686
|
if(c.model){
|
|
4292
|
-
|
|
4293
|
-
modelSelect.insertAdjacentHTML("beforeend", '<option value="' + escapeXml(c.model) + '">' + escapeXml(c.model) + "</option>");
|
|
4294
|
-
}
|
|
4295
|
-
modelSelect.value = c.model;
|
|
4687
|
+
setPick(c);
|
|
4296
4688
|
}
|
|
4297
4689
|
fillCard(c);
|
|
4298
4690
|
|
|
@@ -4323,6 +4715,7 @@
|
|
|
4323
4715
|
setVal("jf-name", c.jobName); setVal("jf-job", c.jobText); setVal("jf-inputs", c.inputs);
|
|
4324
4716
|
setVal("jf-dest", c.destination); setVal("jf-cap-money", c.capUsd);
|
|
4325
4717
|
setVal("jf-cap-time", typeof c.maxWallMs === "number" ? Math.round(c.maxWallMs / 60000) : "");
|
|
4718
|
+
setThinkingTick(c.thinking === true);
|
|
4326
4719
|
fitCardBoxes();
|
|
4327
4720
|
}
|
|
4328
4721
|
// ONE owner of the reuse fill: the Run tab's / Import view's button and the picker both arrive here (the
|
|
@@ -4336,7 +4729,7 @@
|
|
|
4336
4729
|
reuseCheckType = c.checkType === "rubric" ? "rubric" : "deterministic";
|
|
4337
4730
|
reusePickedName = c.jobName || (pre.origin && pre.origin.job) || "job";
|
|
4338
4731
|
setVerdict("reuse");
|
|
4339
|
-
if(c.model){
|
|
4732
|
+
if(c.model){ setPick(c); }
|
|
4340
4733
|
fillCard(c);
|
|
4341
4734
|
|
|
4342
4735
|
setReuseLocked(true);
|
|
@@ -4362,7 +4755,7 @@
|
|
|
4362
4755
|
clearStartFrom();
|
|
4363
4756
|
openNewCard();
|
|
4364
4757
|
setVerdict(c.checkType === "rubric" ? "rubric" : "deterministic");
|
|
4365
|
-
if(c.model){
|
|
4758
|
+
if(c.model){ setPick(c); }
|
|
4366
4759
|
fillCard(c);
|
|
4367
4760
|
|
|
4368
4761
|
originLine.textContent = d.origin || "";
|
|
@@ -4383,10 +4776,12 @@
|
|
|
4383
4776
|
var r = d.resume || {};
|
|
4384
4777
|
clearStartFrom();
|
|
4385
4778
|
openNewCard();
|
|
4779
|
+
// 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
4780
|
resumeRun = {runid: d.runid, noTime: typeof r.maxWallMin !== "number"};
|
|
4781
|
+
if((d.prefill || {}).specNamesModel === false){ resumeRun.pickModel = true; if(d.prefill.specWorker){ resumeRun.worker = d.prefill.specWorker; } renderModelOptions(); }
|
|
4387
4782
|
document.getElementById("job-card-title").textContent = "RESUME " + (d.title || d.runid);
|
|
4388
4783
|
setVerdict(c.checkType === "rubric" ? "rubric" : "deterministic");
|
|
4389
|
-
if(c.model){
|
|
4784
|
+
if(c.model && !resumeRun.pickModel){ setPick(c); }
|
|
4390
4785
|
fillCard(c);
|
|
4391
4786
|
setVal("jf-cap-money", r.budgetUsd);
|
|
4392
4787
|
setVal("jf-cap-time", resumeRun.noTime ? "" : r.maxWallMin);
|
|
@@ -4408,16 +4803,21 @@
|
|
|
4408
4803
|
document.addEventListener("bareloop-resume", function(e){ applyResume(e.detail || {}); });
|
|
4409
4804
|
document.getElementById("chat-resume-cancel").addEventListener("click", function(){ resetCard(); });
|
|
4410
4805
|
var resumeSignBtn = document.getElementById("chat-resume-sign");
|
|
4806
|
+
// a Model pick that is needed (older spec) blocks Sign & resume until a ticked line is picked and its key is usable
|
|
4807
|
+
function resumeSignBlocked(){ return resumeRun !== null && resumeRun.pickModel === true && (modelSelect.value === "" || !keyOk); }
|
|
4808
|
+
function syncResumeSign(){ resumeSignBtn.disabled = resumeSignBlocked(); }
|
|
4411
4809
|
function signResume(){
|
|
4412
4810
|
var run = resumeRun;
|
|
4413
4811
|
if(run === null) return;
|
|
4812
|
+
if(resumeSignBlocked()) return;
|
|
4414
4813
|
var err = document.getElementById("chat-resume-err");
|
|
4415
4814
|
err.hidden = true;
|
|
4416
4815
|
resumeSignBtn.disabled = true;
|
|
4417
4816
|
var body = {budgetUsd: document.getElementById("jf-cap-money").value};
|
|
4418
4817
|
if(!run.noTime){ body.maxWallMin = document.getElementById("jf-cap-time").value; }
|
|
4818
|
+
if(run.pickModel){ var pk = pickParts(modelSelect.value); body.model = pk.model; if(pk.provider !== ""){ body.provider = pk.provider; } }
|
|
4419
4819
|
return authorPost("/api/runs/" + encodeURIComponent(run.runid) + "/resume", body).then(function(res){
|
|
4420
|
-
resumeSignBtn.disabled =
|
|
4820
|
+
resumeSignBtn.disabled = resumeSignBlocked();
|
|
4421
4821
|
if(res.body && res.body.ok){
|
|
4422
4822
|
// 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
4823
|
resetCard();
|
|
@@ -4427,7 +4827,7 @@
|
|
|
4427
4827
|
err.textContent = (res.body && res.body.error) || ("refused (" + res.status + ")");
|
|
4428
4828
|
err.hidden = false;
|
|
4429
4829
|
}
|
|
4430
|
-
}).catch(function(x){ resumeSignBtn.disabled =
|
|
4830
|
+
}).catch(function(x){ resumeSignBtn.disabled = resumeSignBlocked(); err.textContent = "could not reach the panel: " + x.message; err.hidden = false; });
|
|
4431
4831
|
}
|
|
4432
4832
|
resumeSignBtn.addEventListener("click", signResume);
|
|
4433
4833
|
|
|
@@ -4531,6 +4931,7 @@
|
|
|
4531
4931
|
authorGet("/api/author/" + sessionId).then(function(j){
|
|
4532
4932
|
var hash = j.state && j.state.specHash;
|
|
4533
4933
|
if(!hash) return;
|
|
4934
|
+
knownRunIdsAtSign = currentRunIds();
|
|
4534
4935
|
authorPost("/api/author/" + sessionId + "/sign", {specHash: hash}).then(function(r){
|
|
4535
4936
|
var o = chatPostOutcome(r);
|
|
4536
4937
|
if(o.ok){ chatActionOk(); } else { chatActionFailed(o.error); return; }
|