heartcode 0.1.13 → 0.1.14
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 +8 -0
- package/package.json +1 -1
- package/src/settings.html +149 -157
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
What changed in each version of HeartCode, newest first.
|
|
4
4
|
|
|
5
|
+
## 0.1.14 (2026-10-30)
|
|
6
|
+
|
|
7
|
+
- Settings page, Providers & models: one full-width list. Tick models (or a whole provider, or
|
|
8
|
+
everything shown) and add them to your list or take them off it at once; a click on one still
|
|
9
|
+
adds or removes it. Each provider is one line; its model list switch, the models added by name
|
|
10
|
+
and Delete are in its Edit dialog. Your list is a button to the Your models page, which now has
|
|
11
|
+
what to do when a model fails.
|
|
12
|
+
|
|
5
13
|
## 0.1.13 (2026-10-30)
|
|
6
14
|
|
|
7
15
|
- Settings no longer go back by themselves: a save writes only what changed, so another HeartCode
|
package/package.json
CHANGED
package/src/settings.html
CHANGED
|
@@ -147,29 +147,29 @@
|
|
|
147
147
|
.preset span { color: var(--muted); font-size: 12px; }
|
|
148
148
|
.preset.on { border-color: var(--accent); background: var(--accent-soft); }
|
|
149
149
|
|
|
150
|
-
/* Providers & models: every provider's models
|
|
151
|
-
|
|
152
|
-
.work { display:
|
|
153
|
-
.work > .card { padding: 0; display: flex; flex-direction: column; min-height: 0; overflow: hidden; }
|
|
154
|
-
.cat-top { padding:
|
|
155
|
-
.cat-
|
|
156
|
-
.cat-
|
|
157
|
-
.cat-
|
|
158
|
-
.cat-
|
|
159
|
-
.cat-
|
|
160
|
-
|
|
161
|
-
.
|
|
162
|
-
.
|
|
150
|
+
/* Providers & models: every provider's models in one list, full width; it fills the screen's height and scrolls inside */
|
|
151
|
+
.head-actions { display: flex; gap: 8px; align-items: center; }
|
|
152
|
+
.work { display: flex; flex-direction: column; }
|
|
153
|
+
.work > .card { flex: 1; padding: 0; display: flex; flex-direction: column; min-height: 0; overflow: hidden; }
|
|
154
|
+
.cat-top { display: flex; align-items: center; gap: 10px; padding: 12px 16px; border-bottom: 1px solid var(--line); }
|
|
155
|
+
.cat-top input[type=search] { flex: 1; min-width: 140px; }
|
|
156
|
+
.cat-top select { flex: none; width: auto; padding-right: 30px; }
|
|
157
|
+
.cat-top button { flex: none; }
|
|
158
|
+
.cat-top button.ghost:hover:not(:disabled) { color: var(--text); border-color: transparent; }
|
|
159
|
+
.cat-top input[type=checkbox], .prov-h .pick-all, .mwrap .pick { flex: none; width: 16px; height: 16px; margin: 0; accent-color: var(--accent); cursor: pointer; }
|
|
160
|
+
/* what to do with the ticked models: at the bottom of the list, only while some are ticked */
|
|
161
|
+
.picks { display: flex; align-items: center; gap: 10px; padding: 10px 16px; border-top: 1px solid var(--line); background: var(--accent-soft); font-size: 13px; }
|
|
162
|
+
.picks .n { font-weight: 600; margin-right: auto; }
|
|
163
163
|
.prov-h { cursor: pointer; }
|
|
164
|
-
.prov-h .fold { border: 0; background: none; color: var(--muted); padding: 2px 4px; font-size: 16px; line-height: 1; transition: transform .15s;
|
|
164
|
+
.prov-h .fold { border: 0; background: none; color: var(--muted); padding: 2px 4px; font-size: 16px; line-height: 1; transition: transform .15s; }
|
|
165
165
|
.prov-h .fold:hover { color: var(--text); }
|
|
166
166
|
section.collapsed .prov-h .fold { transform: rotate(-90deg); }
|
|
167
167
|
section.collapsed > :not(.prov-h) { display: none; } /* folded: just its header */
|
|
168
168
|
.cat-list { flex: 1; overflow-y: auto; }
|
|
169
|
-
.prov-h { position: sticky; top: 0; z-index: 1; display: flex; align-items: center; gap: 10px;
|
|
170
|
-
.prov-h .who { flex: 1; min-width:
|
|
169
|
+
.prov-h { position: sticky; top: 0; z-index: 1; display: flex; align-items: center; gap: 10px; padding: 10px 16px; background: var(--panel2); border-bottom: 1px solid var(--line); }
|
|
170
|
+
.prov-h .who { flex: 1; min-width: 0; display: flex; align-items: center; gap: 8px; overflow: hidden; }
|
|
171
171
|
.prov-h .name { font-weight: 700; font-size: 15px; }
|
|
172
|
-
.prov-h .meta { font-size: 12px; color: var(--
|
|
172
|
+
.prov-h .meta { font-size: 12px; color: var(--faint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
|
|
173
173
|
.prov-h .meta .mono { font-size: 12px; }
|
|
174
174
|
.rows { display: flex; flex-direction: column; } /* one model per line, A to Z */
|
|
175
175
|
.mrow { break-inside: avoid; display: flex; align-items: center; gap: 10px; width: 100%; text-align: left; padding: 9px 16px; border: 0; border-radius: 0; border-bottom: 1px solid var(--line); background: none; cursor: pointer; min-width: 0; }
|
|
@@ -185,11 +185,9 @@
|
|
|
185
185
|
.mwrap { display: flex; break-inside: avoid; border-bottom: 1px solid var(--line); }
|
|
186
186
|
.mwrap .mrow { border-bottom: 0; }
|
|
187
187
|
.mwrap .forget { border: 0; background: none; color: var(--faint); padding: 0 12px; }
|
|
188
|
+
.mwrap { padding-left: 16px; align-items: center; }
|
|
189
|
+
.mwrap .pick + .mrow { padding-left: 12px; }
|
|
188
190
|
.mwrap .forget:hover { color: var(--err); }
|
|
189
|
-
.cat-top .listsw { display: flex; align-items: center; gap: 8px; font-size: 12.5px; color: var(--muted); white-space: nowrap; margin-right: 6px; }
|
|
190
|
-
.cat-top .listsw .sw { margin-top: 0; }
|
|
191
|
-
.prov-h .listsw { margin-top: 8px; width: fit-content; display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--muted); cursor: pointer; }
|
|
192
|
-
.prov-h .listsw .sw { margin-top: 0; }
|
|
193
191
|
.log-opts { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 28px; }
|
|
194
192
|
.log-opts .opt { border-bottom: 0; padding-bottom: 0; }
|
|
195
193
|
@media (max-width: 860px) { .log-opts { grid-template-columns: 1fr; } }
|
|
@@ -219,8 +217,6 @@
|
|
|
219
217
|
.req-detail h3 { font-size: 14px; margin: 16px 0 4px; }
|
|
220
218
|
@media (max-width: 860px) { .reqs { grid-template-columns: 1fr; } .req-list { border-right: 0; max-height: 50vh; } }
|
|
221
219
|
.cat-note { padding: 12px 16px; color: var(--muted); font-size: 12.5px; }
|
|
222
|
-
.byname { display: flex; gap: 8px; align-items: center; padding: 10px 16px; border-bottom: 1px solid var(--line); }
|
|
223
|
-
.byname input { height: 32px; font-family: var(--mono); font-size: 12.5px; }
|
|
224
220
|
/* one line: what models.dev says (cut short, all of it on hover), its buttons on the right */
|
|
225
221
|
.olist .test { font-size: 12px; color: var(--faint); display: flex; align-items: center; gap: 6px; min-width: 0; }
|
|
226
222
|
.olist .test .msg { flex: 1; min-width: 0; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; line-height: 1.35; }
|
|
@@ -269,6 +265,11 @@
|
|
|
269
265
|
.mt { min-width: 1116px; }
|
|
270
266
|
.mt-h input[type=checkbox], .mtable input[type=checkbox] { width: 15px; height: 15px; margin: 0; accent-color: var(--brand); cursor: pointer; }
|
|
271
267
|
.mtable li.picked { background: var(--brand-soft); }
|
|
268
|
+
.mt-rules { display: flex; align-items: center; flex-wrap: wrap; gap: 10px 28px; padding: 12px 18px; border-bottom: 1px solid var(--line); font-size: 13px; color: var(--muted); }
|
|
269
|
+
.mt-rules .rule { display: flex; align-items: center; gap: 8px; white-space: nowrap; }
|
|
270
|
+
.mt-rules select { width: auto; height: 32px; padding: 4px 30px 4px 10px; }
|
|
271
|
+
.mt-rules input { width: 64px; height: 32px; }
|
|
272
|
+
.mt-rules .hint { flex-basis: 100%; margin: 0; }
|
|
272
273
|
.mt-outcomes { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; padding: 10px 16px; border-bottom: 1px solid var(--line); font-size: 12.5px; color: var(--muted); }
|
|
273
274
|
.mt-outcomes > span:first-child { margin-right: 4px; }
|
|
274
275
|
/* a test result: what it was and how many (a click selects them), and ↻ to test them again */
|
|
@@ -430,26 +431,23 @@
|
|
|
430
431
|
<!-- Providers & models -->
|
|
431
432
|
<section class="page" id="page-providers">
|
|
432
433
|
<div class="head">
|
|
433
|
-
<div><h1>Providers & models</h1><p class="lead">
|
|
434
|
-
<button class="primary" id="p-open">+ Add a provider</button>
|
|
434
|
+
<div><h1>Providers & models</h1><p class="lead">Click a model to add it to your list, or tick several and add them at once. HeartCode uses your list in order: #1, then the next when one fails.</p></div>
|
|
435
|
+
<div class="head-actions"><button id="p-mine" type="button"></button><button class="primary" id="p-open">+ Add a provider</button></div>
|
|
435
436
|
</div>
|
|
436
437
|
<div class="steps" id="steps"></div>
|
|
437
|
-
<div class="
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
<div class="cat-group end"><button class="small" id="dev-all" type="button" title="ask models.dev what each listed model can do: tool calls, context, thinking, images">Get from models.dev</button><button class="small" id="add-all" type="button" title="add every model shown here (only the matches when searching) to your list">Add all</button></div>
|
|
446
|
-
</div>
|
|
447
|
-
</div>
|
|
448
|
-
<div class="job-slot" data-where="catalog"></div>
|
|
449
|
-
<div class="cat-list" id="p-list"></div>
|
|
450
|
-
</div>
|
|
451
|
-
<div class="card mine" id="inuse"></div>
|
|
438
|
+
<div class="work" id="p-work" hidden>
|
|
439
|
+
<div class="card catalog">
|
|
440
|
+
<div class="cat-top">
|
|
441
|
+
<input type="checkbox" id="pick-shown" aria-label="select every model shown">
|
|
442
|
+
<input type="search" id="m-search" placeholder="Search all models…">
|
|
443
|
+
<select id="m-sort" aria-label="sort the models"><option value="name">Sort by name</option><option value="price">Sort by price</option></select>
|
|
444
|
+
<button class="small ghost" id="fold-all" type="button">Collapse all</button>
|
|
445
|
+
<button class="small" id="dev-all" type="button" title="ask models.dev what each listed model can do: tool calls, context, thinking, images">Get from models.dev</button>
|
|
452
446
|
</div>
|
|
447
|
+
<div class="job-slot" data-where="catalog"></div>
|
|
448
|
+
<div class="cat-list" id="p-list"></div>
|
|
449
|
+
<div class="picks" id="picks" hidden></div>
|
|
450
|
+
</div>
|
|
453
451
|
</div>
|
|
454
452
|
</section>
|
|
455
453
|
|
|
@@ -607,7 +605,11 @@
|
|
|
607
605
|
<label class="f" for="p-key">API key <span class="muted" id="p-keynote" style="font-weight:normal"></span></label>
|
|
608
606
|
<input type="password" id="p-key" autocomplete="off" placeholder="paste it here (none for a server on this machine, like Ollama)">
|
|
609
607
|
<div class="hint">Kept in config.json on this machine. This page only shows its first and last characters.</div>
|
|
610
|
-
<div
|
|
608
|
+
<div id="p-more" hidden>
|
|
609
|
+
<label class="opt" style="margin-top:16px"><div class="t"><b>Load its model list</b><span>On: its models are asked from the provider. Off: never asked; only the models you add by name show (type one's exact name in the search).</span></div><span class="sw"><input type="checkbox" id="p-list-on"><i></i></span></label>
|
|
610
|
+
<div class="actions" id="p-added"></div>
|
|
611
|
+
</div>
|
|
612
|
+
<div class="actions"><button class="danger" id="p-delete" hidden>Delete provider</button><span class="spacer"></span><button id="p-cancel">Cancel</button><button class="primary" id="p-save">Add provider</button></div>
|
|
611
613
|
</dialog>
|
|
612
614
|
<dialog id="c-form" aria-labelledby="c-title">
|
|
613
615
|
<h2 id="c-title">Add a server</h2>
|
|
@@ -654,7 +656,7 @@ function toast(text, err = false) {
|
|
|
654
656
|
}
|
|
655
657
|
// What's running (a test, lists loading, models.dev), shown at the top of the panels it's about
|
|
656
658
|
// while it runs: job(id, { what, where, done, total, detail, stop }) to show or update it (no total:
|
|
657
|
-
// a bar that just moves), job(id) when it's over. where: the panels ('catalog', '
|
|
659
|
+
// a bar that just moves), job(id) when it's over. where: the panels ('catalog', 'table').
|
|
658
660
|
// Updated in place, so nothing jumps while it counts.
|
|
659
661
|
const JOBS = new Map()
|
|
660
662
|
function job(id, state) {
|
|
@@ -901,11 +903,25 @@ function openForm(p = null) {
|
|
|
901
903
|
$('p-key').placeholder = p?.key ? 'empty: keep the key it has' : 'paste it here (none for a server on this machine, like Ollama)'
|
|
902
904
|
$('p-keynote').textContent = p?.key ? `(now ${p.key})` : ''
|
|
903
905
|
$('p-save').textContent = p ? 'Save changes' : 'Add provider'
|
|
906
|
+
// editing: what used to crowd its header
|
|
907
|
+
$('p-more').hidden = $('p-delete').hidden = !p
|
|
908
|
+
$('presets').hidden = Boolean(p)
|
|
909
|
+
$('p-form').querySelector('.sub').hidden = Boolean(p)
|
|
910
|
+
if (p) {
|
|
911
|
+
$('p-list-on').checked = p.list
|
|
912
|
+
put($('p-added'), p.added.length ? [h('span', { className: 'muted', style: 'font-size:13px', textContent: `${p.added.length} model${p.added.length === 1 ? '' : 's'} you added by name` }), h('button', { className: 'small danger', textContent: 'Forget them', onclick: () => ($('p-form').close(), clearAdded(p)) })] : [])
|
|
913
|
+
}
|
|
904
914
|
drawPresets()
|
|
905
915
|
$('p-form').showModal()
|
|
906
916
|
;(p ? $('p-url') : $('p-name')).focus()
|
|
907
917
|
}
|
|
908
918
|
const closeForm = () => $('p-form').close()
|
|
919
|
+
$('p-delete').onclick = () => {
|
|
920
|
+
const p = editing
|
|
921
|
+
if (!p || !confirm(`Delete the provider "${p.name}"? Its models leave your list too.`)) return
|
|
922
|
+
closeForm()
|
|
923
|
+
act('/api/provider/delete', { name: p.name }, `deleted ${p.name}`)
|
|
924
|
+
}
|
|
909
925
|
$('p-form').onclose = () => (editing = null)
|
|
910
926
|
$('p-open').onclick = () => openForm()
|
|
911
927
|
$('p-cancel').onclick = closeForm
|
|
@@ -917,6 +933,7 @@ $('p-save').onclick = async () => {
|
|
|
917
933
|
if (!r) return
|
|
918
934
|
const added = r.providers.find((p) => p.name === name.toLowerCase().replace(/[^a-z0-9_-]+/g, '-').replace(/^-+|-+$/g, ''))
|
|
919
935
|
if (!added) return
|
|
936
|
+
if (was && $('p-list-on').checked !== was.list) await act('/api/provider/list', { name: added.name, on: $('p-list-on').checked }) // its list switched on or off
|
|
920
937
|
if (was) delete MODELS[was.name]
|
|
921
938
|
loadModels(added.name, true) // a new one, or a new URL or key: its models (again)
|
|
922
939
|
toast(was ? `saved ${added.name}` : `added ${added.name}: its models are loading`)
|
|
@@ -945,6 +962,7 @@ const costOf = (d) => (d?.price ? (d.price.input ?? 0) + (d.price.output ?? 0) :
|
|
|
945
962
|
let searchOpen = false
|
|
946
963
|
const FOLDED = new Set()
|
|
947
964
|
function applyFolds() {
|
|
965
|
+
$('fold-all').textContent = allFolded() ? 'Expand all' : 'Collapse all'
|
|
948
966
|
for (const sec of $('p-list').querySelectorAll('section[data-provider]')) {
|
|
949
967
|
const folded = FOLDED.has(sec.dataset.provider) && !(searchOpen && modelQuery.trim())
|
|
950
968
|
sec.classList.toggle('collapsed', folded)
|
|
@@ -959,17 +977,14 @@ function settle() {
|
|
|
959
977
|
}
|
|
960
978
|
const folds = () => (applyFolds(), savePref('folded', [...FOLDED])) // a fold changed: shown and kept
|
|
961
979
|
const fold = (name) => (settle(), FOLDED.has(name) ? FOLDED.delete(name) : FOLDED.add(name), folds())
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
$('
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
if (!confirm(`Add ${add.length} model${add.length === 1 ? '' : 's'} to your list${modelQuery.trim() ? ` (the ones matching "${modelQuery.trim()}")` : ''}?`)) return
|
|
971
|
-
saveOrder([...S.order, ...add], `${add.length} added: your list has ${S.order.length + add.length} models`)
|
|
972
|
-
}
|
|
980
|
+
// one button: collapse them all, or (all collapsed) expand them all
|
|
981
|
+
const allFolded = () => Boolean(S?.providers.length) && S.providers.every((p) => FOLDED.has(p.name))
|
|
982
|
+
$('fold-all').onclick = () => (settle(), allFolded() ? FOLDED.clear() : S?.providers.forEach((p) => FOLDED.add(p.name)), folds())
|
|
983
|
+
$('pick-shown').onchange = (e) => {
|
|
984
|
+
for (const [provider, ids] of SHOWN) for (const id of ids) e.target.checked ? pickedOf(provider).add(id) : PICKED_CAT.get(provider)?.delete(id)
|
|
985
|
+
drawPicks()
|
|
986
|
+
}
|
|
987
|
+
$('p-mine').onclick = () => (location.hash = 'models')
|
|
973
988
|
// models.dev fetched again (your models' profiles too), then every listed provider's models with it
|
|
974
989
|
$('dev-all').onclick = async () => {
|
|
975
990
|
if (!(await lookUp())) return
|
|
@@ -980,21 +995,8 @@ $('m-search').oninput = (e) => ((modelQuery = e.target.value), (searchOpen = tru
|
|
|
980
995
|
// list and its own test), and what a click does. Shown on hover and on keyboard focus.
|
|
981
996
|
const kTok = (n) => (n >= 1e6 ? `${Math.round(n / 1e5) / 10}M` : n >= 1000 ? `${Math.round(n / 1000)}k` : String(n))
|
|
982
997
|
const usd = (x) => (x === 0 ? 'free' : `$${x < 0.01 ? x.toFixed(4) : x < 1 ? x.toFixed(3).replace(/0$/, '') : x.toFixed(2)}`)
|
|
983
|
-
//
|
|
984
|
-
// lines of text, its button on the right): where that comes from, and Edit (or Set it up, for one
|
|
985
|
-
// models.dev doesn't know). Replaced in place when it changes (streamed).
|
|
998
|
+
// where what a model can do comes from (the Your models table)
|
|
986
999
|
const FROM = { 'models.dev': 'from models.dev', you: 'set by you', tested: 'tested' }
|
|
987
|
-
function testLine(t) {
|
|
988
|
-
const x = t.tested
|
|
989
|
-
const edit = (label) => h('button', { className: 'small', textContent: label, onclick: (e) => (e.stopPropagation(), openSetup(t)) })
|
|
990
|
-
const [cls, text, action] =
|
|
991
|
-
!x ? ['none', 'looking it up in models.dev…', null]
|
|
992
|
-
: x.source === 'assumed' ? ['warn', 'not in models.dev', edit('Set up')] // its Retry: on the Your models page, and Refresh from models.dev
|
|
993
|
-
: ['ok', `✓ ${testedText(x)} · ${Object.keys(x.set ?? {}).length ? 'yours' : FROM[x.source]}`, edit('Edit')]
|
|
994
|
-
const about = x?.source === 'assumed' ? "not in models.dev: Retry, or set up what it can do" : text
|
|
995
|
-
return h('div', { className: 'test ' + cls }, h('span', { className: 'msg', textContent: text, title: about }), action)
|
|
996
|
-
}
|
|
997
|
-
|
|
998
1000
|
// Test: one short message to the model, to see that it really answers (the key, access to it, its
|
|
999
1001
|
// name, the server). The result stays under it until the page is reloaded.
|
|
1000
1002
|
const TRIES = {} // "provider/model" → { going } | { ok, ms, reply, at } | { ok: false, status, error, at } (from the server)
|
|
@@ -1077,7 +1079,7 @@ function showRun(run) {
|
|
|
1077
1079
|
if (run?.over) toast(`${run.stopping ? `stopped after ${run.done} of ${run.total}: ` : ''}${run.good} of ${run.done} work${run.good === run.done ? '' : `; ${run.done - run.good} don't (Works? says why, on hover)`}`, run.good < run.done)
|
|
1078
1080
|
} else
|
|
1079
1081
|
job('test-all', {
|
|
1080
|
-
where: ['table'
|
|
1082
|
+
where: ['table'],
|
|
1081
1083
|
what: run.stopping ? 'Stopping the tests…' : `Testing ${run.some ? '' : 'your '}${run.total} model${run.total === 1 ? '' : 's'}${run.each > 1 ? ` · ${run.each} at a time per provider` : ''}`,
|
|
1082
1084
|
done: run.done, total: run.total, stopping: run.stopping,
|
|
1083
1085
|
detail: [h('span', { className: 'ok', textContent: `✓ ${run.good} work` }), ' · ', h('span', { className: 'bad', textContent: `✗ ${run.done - run.good} don't` }), run.busy.length ? ` · now: ${run.busy.join(', ')}` : '', Object.keys(run.paused ?? {}).length ? ` · paused (busy, 429): ${Object.entries(run.paused).map(([p, secs]) => `${p} ${secs}s`).join(', ')}` : ''],
|
|
@@ -1089,7 +1091,7 @@ function showRun(run) {
|
|
|
1089
1091
|
// models.dev looked up again: one model (its Retry), or all of yours (Refresh from models.dev)
|
|
1090
1092
|
const retry = (t) => h('button', { className: 'small', textContent: 'Retry', title: 'look it up in models.dev again', onclick: (e) => (e.stopPropagation(), lookUp(t)) })
|
|
1091
1093
|
async function lookUp(t) {
|
|
1092
|
-
job('models.dev', { where: ['catalog', '
|
|
1094
|
+
job('models.dev', { where: ['catalog', 'table'], what: t ? `Looking up ${t.model} in models.dev` : 'Asking models.dev about your models', detail: 'context, tool calls, thinking, images' })
|
|
1093
1095
|
const r = await act('/api/models-dev', t ? { provider: t.provider, model: t.model } : {})
|
|
1094
1096
|
job('models.dev')
|
|
1095
1097
|
if (r) toast(t ? (r.found ? `${t.model}: found in models.dev` : `${t.model}: still not in models.dev`) : `models.dev: ${r.found} of ${r.of} found`)
|
|
@@ -1179,73 +1181,21 @@ function drawProviders(s) {
|
|
|
1179
1181
|
// the steps: always on the page (nothing jumps when one is done), ticked off as you go
|
|
1180
1182
|
put($('steps'), [
|
|
1181
1183
|
['Add a provider', 'the service with your models', s.providers.length ? 'done' : 'now'],
|
|
1182
|
-
['Choose
|
|
1184
|
+
['Choose models', 'click one, or tick several and add them', order.length ? 'done' : s.providers.length ? 'now' : ''],
|
|
1183
1185
|
['Start HeartCode', 'heart, in your project', order.length ? 'now' : ''],
|
|
1184
1186
|
].map(([t, d, cls], i) => h('div', { className: 'step ' + cls }, h('span', { className: 'num', textContent: cls === 'done' ? '✓' : i + 1 }), h('div', {}, h('b', { textContent: t }), d))))
|
|
1187
|
+
$('steps').hidden = order.length > 0 // the first start only
|
|
1185
1188
|
$('p-work').hidden = none
|
|
1189
|
+
// your list, at the top: how many, and the way to its page (order, tests, what to do when one fails)
|
|
1190
|
+
$('p-mine').hidden = !order.length
|
|
1191
|
+
$('p-mine').textContent = `Your models (${order.length}) →`
|
|
1192
|
+
$('p-mine').title = order.length ? `#1: ${order[0].provider} · ${order[0].model}. Open the Your models page: the order, tests, and what happens when one fails` : ''
|
|
1186
1193
|
fillHeight()
|
|
1187
1194
|
|
|
1188
|
-
//
|
|
1189
|
-
const side = innerWidth > 860 ? 'on the left' : 'below' // where the models are (a phone: under this)
|
|
1190
|
-
const save = (list, done) => saveOrder(list, done)
|
|
1191
|
-
const move = (i, j) => {
|
|
1192
|
-
if (i === j) return
|
|
1193
|
-
const list = [...order]
|
|
1194
|
-
const [t] = list.splice(i, 1)
|
|
1195
|
-
list.splice(j, 0, t)
|
|
1196
|
-
return save(list, list[0] !== order[0] ? `HeartCode now uses ${list[0].provider} · ${list[0].model}` : 'order saved')
|
|
1197
|
-
}
|
|
1198
|
-
// a row of your list (the last: where the next one goes)
|
|
1199
|
-
const mineRow = (i) => {
|
|
1200
|
-
if (i === order.length) return h('li', { className: 'next', textContent: `#${order.length + 1} · click a model ${side} to add it here (a fallback)` })
|
|
1201
|
-
const t = order[i]
|
|
1202
|
-
return h('li', {
|
|
1203
|
-
className: 'in' + (i === 0 ? ' use' : ''), tabIndex: 0, 'data-key': `${t.provider}/${t.model}`, title: order.length > 1 ? 'Drag to move it (or Alt+↑ / Alt+↓)' : '',
|
|
1204
|
-
onkeydown: (e) => e.altKey && (e.key === 'ArrowUp' || e.key === 'ArrowDown') && (e.preventDefault(), move(i, Math.max(0, Math.min(order.length - 1, i + (e.key === 'ArrowUp' ? -1 : 1))))),
|
|
1205
|
-
},
|
|
1206
|
-
h('span', { className: 'grip', textContent: '⋮⋮', title: 'drag to reorder', 'aria-hidden': 'true', style: order.length > 1 ? '' : 'visibility:hidden' }),
|
|
1207
|
-
h('span', { className: 'n', textContent: `#${i + 1}`, title: i === 0 ? 'in use: the one HeartCode uses' : 'a fallback: tried, in order, when the ones above fail' }),
|
|
1208
|
-
h('span', { className: 'm', textContent: t.model, title: t.model }),
|
|
1209
|
-
h('span', { className: 'p' + (i === 0 ? ' use' : ''), textContent: t.provider, title: `${t.provider}${i === 0 ? ': in use' : ''}` }),
|
|
1210
|
-
testLine(t),
|
|
1211
|
-
h('button', { className: 'danger', textContent: '✕', title: 'take it off the list', 'aria-label': `remove ${t.model}`, onclick: () => removeMine(t.provider, t.model) }))
|
|
1212
|
-
}
|
|
1213
|
-
const mine = order.length ? h('ol', { className: 'olist' }) : null
|
|
1214
|
-
const mineV = mine && virtual(mine, { count: order.length + 1, height: MINE_ROW, row: mineRow })
|
|
1215
|
-
if (mineV) mineV.items = order.length
|
|
1216
|
-
put($('inuse'),
|
|
1217
|
-
h('div', { className: 'mine-top' },
|
|
1218
|
-
h('div', { className: 'mine-title' },
|
|
1219
|
-
h('h2', { textContent: 'Your models' }),
|
|
1220
|
-
h('button', { className: 'small', textContent: 'Table', style: order.length ? '' : 'visibility:hidden', title: 'all of them in a table, to compare and sort', onclick: () => (location.hash = 'models') }),
|
|
1221
|
-
h('button', { className: 'small', textContent: 'Refresh from models.dev', title: 'ask models.dev again what each model can do: context, tools, thinking, images, price', style: order.length ? '' : 'visibility:hidden', onclick: () => lookUp() }),
|
|
1222
|
-
h('button', { className: 'small danger', textContent: 'Remove all', title: 'take every model off this list', style: order.length ? '' : 'visibility:hidden', onclick: () => confirm(`Take all ${order.length} model${order.length === 1 ? '' : 's'} off your list? HeartCode then has none to use until you choose one. (They stay under their providers.)`) && saveOrder([], 'all removed: no model chosen now') })),
|
|
1223
|
-
h('p', { className: 'sub', style: 'margin:0', textContent: order.length > 1 ? 'HeartCode uses #1. If it fails, it tries #2, then #3… and round again. Drag to change the order.' : order.length ? `HeartCode uses this one. Click more ${side} to add fallbacks: tried, in order, when one fails.` : `The models HeartCode uses, in the order it tries them. Click one ${side} to start.` })),
|
|
1224
|
-
jobSlot('mine'),
|
|
1225
|
-
order.length ? h('div', { className: 'ohead' }, h('span'), h('span', { textContent: '#' }), h('span', { textContent: 'Model' }), h('span', { textContent: 'Provider' }), h('span', { textContent: 'What it can do' }), h('span')) : null,
|
|
1226
|
-
order.length ? mine
|
|
1227
|
-
: h('div', { className: 'mine-empty' }, h('b', { textContent: 'No model chosen yet' }), `Click a model ${side}: it becomes #1, the one HeartCode uses.`),
|
|
1228
|
-
order.length > 1 ? h('div', { className: 'mine-rules' },
|
|
1229
|
-
h('label', { className: 'f', for: 'r-on', textContent: 'When the one in use fails, go on to the next' }),
|
|
1230
|
-
h('div', { className: 'row', style: 'gap:8px' },
|
|
1231
|
-
h('select', { id: 'r-on', style: 'flex:1', onchange: (e) => save(order, 'saved', { on: e.target.value }) },
|
|
1232
|
-
[['failure', 'at its first failure'], ['retries', 'after some tries of the same model (rate limits, outages)']].map(([v, t]) => h('option', { value: v, textContent: t, selected: s.rotation.on === v }))),
|
|
1233
|
-
s.rotation.on === 'retries' ? h('input', { type: 'number', 'aria-label': 'tries', title: 'tries of the same model first', style: 'width:76px', min: 1, max: 20, value: s.rotation.retries, onchange: (e) => save(order, 'saved', { retries: Number(e.target.value) }) }) : null),
|
|
1234
|
-
h('div', { className: 'hint', textContent: { failure: 'Any error moves on at once, to another provider\'s model first. It stops only when every model fails.', retries: 'A rate limit or outage: the same model is tried again (with a short wait) that many times first. Any other error moves on at once.' }[s.rotation.on] ?? 'Any error moves on at once, to another provider\'s model first.' }),
|
|
1235
|
-
h('label', { className: 'f', for: 'r-next', textContent: 'A new message starts with' }),
|
|
1236
|
-
h('select', { id: 'r-next', onchange: (e) => save(order, 'saved', { next: e.target.value }) },
|
|
1237
|
-
[['first', '#1 again'], ['stay', 'the model that answered last']].map(([v, t]) => h('option', { value: v, textContent: t, selected: s.rotation.next === v })))) : null)
|
|
1238
|
-
|
|
1239
|
-
mineV?.draw()
|
|
1240
|
-
sortable(mineV, move)
|
|
1241
|
-
|
|
1242
|
-
// left: every provider's models, A to Z; one click adds it (the first one becomes #1). Built
|
|
1195
|
+
// every provider's models, A to Z; one click adds it (the first one becomes #1). Built
|
|
1243
1196
|
// again only when what it lists changes (a provider, its models, the search); a change to your
|
|
1244
1197
|
// list only updates the rows it touches (markCatalog), so the list stays where you scrolled it.
|
|
1245
1198
|
const shape = JSON.stringify([s.providers, s.providers.map((p) => (MODELS[p.name]?.list ?? []).length + (MODELS[p.name]?.error ?? '') + ((MODELS[p.name] ?? { loading: true }).loading ? 'loading' : '')), modelQuery.trim(), catSort])
|
|
1246
|
-
// every provider's list at once: on when all are
|
|
1247
|
-
const allOn = s.providers.length > 0 && s.providers.every((p) => p.list)
|
|
1248
|
-
put($('lists-all'), [sw({ checked: allOn, 'aria-label': "load every provider's model list", onchange: (e) => act('/api/provider/list', { on: e.target.checked }, `every provider's model list ${e.target.checked ? 'on' : 'off'}`) }), 'Load all lists'])
|
|
1249
1199
|
if (shape !== catalogShape) (buildCatalog(s), applyFolds())
|
|
1250
1200
|
catalogShape = shape
|
|
1251
1201
|
markCatalog()
|
|
@@ -1258,7 +1208,6 @@ function drawProviders(s) {
|
|
|
1258
1208
|
// Drag a row of a long list (virtual) to move it: it follows the pointer, the others make room, and
|
|
1259
1209
|
// near the list's top or bottom edge it scrolls (faster nearer the edge). onMove(from, to) when
|
|
1260
1210
|
// dropped; the list is drawn again once the order is saved (put back if saving failed).
|
|
1261
|
-
const MINE_ROW = 50 // your models, on the right
|
|
1262
1211
|
const TABLE_ROW = 46 // the Your models page's table
|
|
1263
1212
|
function sortable(v, onMove) {
|
|
1264
1213
|
if (!v || v.items < 2) return
|
|
@@ -1392,12 +1341,56 @@ async function clearAdded(p) {
|
|
|
1392
1341
|
act('/api/provider/added', { name: p.name, all: true }, `${p.name}: the models you added are forgotten`)
|
|
1393
1342
|
}
|
|
1394
1343
|
const CAT_ROW = 56 // a model's row on the left: its name, and what it can do under it
|
|
1395
|
-
|
|
1344
|
+
// The models ticked (provider → Set of ids): kept while you search, so ones ticked for one search
|
|
1345
|
+
// and the next are added (or taken off) together. SHOWN: each provider's models shown now (the
|
|
1346
|
+
// search's matches).
|
|
1347
|
+
const PICKED_CAT = new Map()
|
|
1348
|
+
const SHOWN = new Map()
|
|
1349
|
+
const pickedOf = (name) => PICKED_CAT.get(name) ?? PICKED_CAT.set(name, new Set()).get(name)
|
|
1350
|
+
// The ticked models (every provider's) and what to do with them, on the bar over the list: all the
|
|
1351
|
+
// shown ones ticked (or none), added to your list, taken off it. The tick boxes drawn now (rows and
|
|
1352
|
+
// each provider's) follow.
|
|
1353
|
+
function drawPicks() {
|
|
1354
|
+
if (!S) return
|
|
1355
|
+
const all = [...PICKED_CAT].flatMap(([provider, ids]) => [...ids].map((model) => ({ provider, model })))
|
|
1356
|
+
const shown = [...SHOWN].flatMap(([provider, ids]) => ids.map((model) => ({ provider, model })))
|
|
1357
|
+
const isPicked = (t) => Boolean(PICKED_CAT.get(t.provider)?.has(t.model))
|
|
1358
|
+
const toAdd = all.filter((t) => placeOf(t.provider, t.model) === undefined)
|
|
1359
|
+
const toTake = all.filter((t) => placeOf(t.provider, t.model) !== undefined)
|
|
1360
|
+
for (const box of $('p-list').querySelectorAll('input.pick')) box.checked = Boolean(PICKED_CAT.get(box.closest('section').dataset.provider)?.has(box.dataset.model))
|
|
1361
|
+
for (const box of $('p-list').querySelectorAll('input.pick-all')) {
|
|
1362
|
+
const ids = SHOWN.get(box.dataset.provider) ?? []
|
|
1363
|
+
const n = ids.filter((id) => PICKED_CAT.get(box.dataset.provider)?.has(id)).length
|
|
1364
|
+
box.checked = n > 0 && n === ids.length
|
|
1365
|
+
box.indeterminate = n > 0 && n < ids.length
|
|
1366
|
+
}
|
|
1367
|
+
const top = $('pick-shown')
|
|
1368
|
+
const n = shown.filter(isPicked).length
|
|
1369
|
+
top.checked = n > 0 && n === shown.length
|
|
1370
|
+
top.indeterminate = n > 0 && n < shown.length
|
|
1371
|
+
top.disabled = !shown.length
|
|
1372
|
+
top.title = top.checked ? 'unselect every model shown' : `select every model shown (${shown.length}${modelQuery.trim() ? ': the matches' : ''})`
|
|
1373
|
+
const take = () => {
|
|
1374
|
+
const off = new Set(toTake.map(keyOf))
|
|
1375
|
+
const rest = S.order.filter((t) => !off.has(keyOf(t)))
|
|
1376
|
+
if (!confirm(`Take ${toTake.length} model${toTake.length === 1 ? '' : 's'} off your list?${!rest.length ? ' None is left then: HeartCode has no model to use until you choose one.' : keyOf(rest[0]) !== keyOf(S.order[0]) ? ` HeartCode then uses ${rest[0].provider} · ${rest[0].model}.` : ''}`)) return
|
|
1377
|
+
PICKED_CAT.clear()
|
|
1378
|
+
saveOrder(rest, `${toTake.length} removed`)
|
|
1379
|
+
}
|
|
1380
|
+
const was = $('picks').hidden
|
|
1381
|
+
$('picks').hidden = !all.length
|
|
1382
|
+
put($('picks'),
|
|
1383
|
+
h('span', { className: 'n', textContent: `${all.length} selected` }),
|
|
1384
|
+
h('button', { className: 'small ghost', textContent: 'Clear', onclick: () => (PICKED_CAT.clear(), drawPicks()) }),
|
|
1385
|
+
toTake.length ? h('button', { className: 'small danger', textContent: `Remove ${toTake.length} from your list`, onclick: take }) : null,
|
|
1386
|
+
toAdd.length ? h('button', { className: 'small primary', textContent: `Add ${toAdd.length} to your list`, onclick: () => (PICKED_CAT.clear(), saveOrder([...S.order, ...toAdd], `${toAdd.length} added: your list has ${S.order.length + toAdd.length} models`)) }) : null)
|
|
1387
|
+
if (was !== $('picks').hidden) fillHeight() // the list gives it room
|
|
1388
|
+
}
|
|
1396
1389
|
function buildCatalog(s) {
|
|
1397
|
-
CATALOG = []
|
|
1398
1390
|
const q = modelQuery.trim()
|
|
1399
1391
|
// a name no provider lists (anywhere): offered under each provider, to use it anyway
|
|
1400
1392
|
const anyMatch = !q || s.providers.some((p) => (MODELS[p.name]?.list ?? []).some((id) => id.toLowerCase().includes(q.toLowerCase())))
|
|
1393
|
+
SHOWN.clear()
|
|
1401
1394
|
put($('p-list'), s.providers.map((p) => {
|
|
1402
1395
|
if (!p.list) MODELS[p.name] = { list: [], details: {}, off: true } // its list switched off: never asked for
|
|
1403
1396
|
else loadModels(p.name, MODELS[p.name]?.off)
|
|
@@ -1411,8 +1404,9 @@ function buildCatalog(s) {
|
|
|
1411
1404
|
h('span', { className: 'id', title: id }, id, canCell(m.details?.[id], S.order[placeOf(p.name, id)])),
|
|
1412
1405
|
own ? h('span', { className: 'tag', textContent: 'not in its list' }) : p.added.includes(id) ? h('span', { className: 'tag', textContent: 'added by you' }) : null,
|
|
1413
1406
|
rowTail(p.name, id))
|
|
1414
|
-
// one you added: ✕ forgets it (it leaves your list too)
|
|
1415
|
-
const
|
|
1407
|
+
// its tick box (to add or take off several at once), and one you added: ✕ forgets it (it leaves your list too)
|
|
1408
|
+
const tick = (id) => h('input', { type: 'checkbox', className: 'pick', checked: pickedOf(p.name).has(id), 'data-model': id, 'aria-label': `select ${id}`, onchange: (e) => (e.target.checked ? pickedOf(p.name).add(id) : pickedOf(p.name).delete(id), drawPicks()) })
|
|
1409
|
+
const item = (id) => h('div', { className: 'mwrap' }, tick(id), row(id), !p.added.includes(id) ? null :
|
|
1416
1410
|
h('button', { className: 'forget', textContent: '✕', title: 'forget this model', 'aria-label': `forget ${id}`, onclick: async () => {
|
|
1417
1411
|
if (placeOf(p.name, id) !== undefined && !confirm(`Forget ${id}? It leaves your list too.`)) return
|
|
1418
1412
|
if (placeOf(p.name, id) !== undefined) await saveOrder(S.order.filter((t) => t.provider !== p.name || t.model !== id), `${id} removed`)
|
|
@@ -1421,43 +1415,29 @@ function buildCatalog(s) {
|
|
|
1421
1415
|
const typed = !anyMatch ? [row(q, true)] : []
|
|
1422
1416
|
if (typed[0]) typed[0].dataset.typed = '1' // a name no provider lists: not part of Add all
|
|
1423
1417
|
if (q && anyMatch && !ids.length) return null // searching: only the providers with a match
|
|
1424
|
-
// a model its list leaves out (some servers don't list them all): by its exact name
|
|
1425
|
-
const name = h('input', { type: 'text', placeholder: m.off ? 'List off: a model\'s exact name' : 'Custom model: its exact name', 'aria-label': `add a model to ${p.name} by name` })
|
|
1426
|
-
const go = () => {
|
|
1427
|
-
const id = name.value.trim()
|
|
1428
|
-
if (!id) return name.focus()
|
|
1429
|
-
name.value = ''
|
|
1430
|
-
// only under this provider, with its others: you choose whether to use it (click it, like any)
|
|
1431
|
-
if ([...(MODELS[p.name]?.list ?? []), ...p.added].includes(id)) return showRow(p.name, id, `${id} is in ${p.name}'s models already`)
|
|
1432
|
-
act('/api/provider/added', { name: p.name, model: id }).then((r) => r && showRow(p.name, id, `${id} added to ${p.name}'s models: click it to use it`))
|
|
1433
|
-
}
|
|
1434
|
-
name.addEventListener('keydown', (e) => e.key === 'Enter' && go())
|
|
1435
1418
|
return h('section', { 'data-provider': p.name },
|
|
1436
1419
|
// the header folds it (a click on it, or its arrow; not on its buttons and switch)
|
|
1437
1420
|
h('div', { className: 'prov-h', onclick: (e) => !e.target.closest('button:not(.fold), input, label, a') && fold(p.name) },
|
|
1438
1421
|
h('button', { className: 'fold', type: 'button', textContent: '▾', 'aria-label': `fold ${p.name}`, 'aria-expanded': 'true' }),
|
|
1422
|
+
ids.length ? h('input', { type: 'checkbox', className: 'pick-all', 'data-provider': p.name, 'aria-label': `select ${p.name}'s models`, title: `select all of ${p.name}'s models shown (or none)`, onchange: (e) => (ids.forEach((id) => (e.target.checked ? pickedOf(p.name).add(id) : pickedOf(p.name).delete(id))), drawPicks()) }) : null,
|
|
1439
1423
|
h('div', { className: 'who' },
|
|
1440
|
-
h('span', { className: 'name', textContent: p.name }),
|
|
1441
|
-
m.off ? h('span', { className: 'tag', textContent: 'list off' }) : m.loading ? h('span', { className: 'tag', textContent: 'loading…' }) : m.error ? h('span', { className: 'tag err', textContent: "can't list its models", title: m.error }) : h('span', { className: 'tag ok', textContent: `${m.list.length} model${m.list.length === 1 ? '' : 's'}` }),
|
|
1442
|
-
|
|
1443
|
-
h('
|
|
1444
|
-
|
|
1445
|
-
() => 'On: its models are loaded from the provider. Off: never asked for; only the models you add by name show.')),
|
|
1446
|
-
h('button', { className: 'small', textContent: 'Edit', onclick: () => openForm(p) }),
|
|
1447
|
-
h('button', { className: 'small danger', textContent: 'Delete', onclick: () => confirm(`Delete the provider "${p.name}"? Its models leave your list too.`) && act('/api/provider/delete', { name: p.name }, `deleted ${p.name}`) })),
|
|
1448
|
-
h('div', { className: 'byname' }, name, h('button', { className: 'small byname-go', textContent: 'Add', onclick: go }),
|
|
1449
|
-
p.added.length ? h('button', { className: 'small danger', textContent: `Clear added (${p.added.length})`, title: `forget the ${p.added.length} model${p.added.length === 1 ? '' : 's'} you added to ${p.name} by name`, onclick: () => clearAdded(p) }) : null),
|
|
1424
|
+
h('span', { className: 'name', textContent: p.name }),
|
|
1425
|
+
m.off ? h('span', { className: 'tag', textContent: 'list off', title: "its list isn't loaded: type a model's exact name in the search to add it" }) : m.loading ? h('span', { className: 'tag', textContent: 'loading…' }) : m.error ? h('span', { className: 'tag err', textContent: "can't list its models", title: m.error }) : h('span', { className: 'tag ok', textContent: `${m.list.length} model${m.list.length === 1 ? '' : 's'}` }),
|
|
1426
|
+
h('span', { className: 'tag inlist' }),
|
|
1427
|
+
h('span', { className: 'meta mono', textContent: p.baseUrl, title: `${p.baseUrl} · key ${p.key || 'none'}` })),
|
|
1428
|
+
h('button', { className: 'small', textContent: 'Edit', title: 'its address and key, its model list, delete it', onclick: () => openForm(p) })),
|
|
1450
1429
|
// what's listed first; loading and errors under it (the models already shown stay where they are)
|
|
1451
1430
|
typed.length ? h('div', { className: 'rows' }, typed) : null,
|
|
1452
1431
|
(() => {
|
|
1453
1432
|
if (!ids.length) return null
|
|
1454
|
-
|
|
1433
|
+
SHOWN.set(p.name, ids)
|
|
1455
1434
|
const box = h('div', { className: 'rows' })
|
|
1456
1435
|
box._ids = ids
|
|
1457
1436
|
box._v = virtual(box, { count: ids.length, height: CAT_ROW, row: (i) => item(ids[i]), scroller: $('p-list') })
|
|
1458
1437
|
return box
|
|
1459
1438
|
})(),
|
|
1460
|
-
m.error ? h('div', { className: 'cat-note', style: 'color:var(--err)' }, `can't list its models: ${shortError(m.error)} `, h('button', { className: 'small', textContent: 'Try again', onclick: () => loadModels(p.name, true) }), h('div', { className: 'muted', style: 'margin-top:4px', textContent:
|
|
1439
|
+
m.error ? h('div', { className: 'cat-note', style: 'color:var(--err)' }, `can't list its models: ${shortError(m.error)} `, h('button', { className: 'small', textContent: 'Try again', onclick: () => loadModels(p.name, true) }), h('div', { className: 'muted', style: 'margin-top:4px', textContent: "Or type a model's exact name in the search, and click it." })) : null,
|
|
1440
|
+
m.off && !ids.length && !typed.length ? h('div', { className: 'cat-note', textContent: "Its list is off: type a model's exact name in the search, and click it to add it." }) : null,
|
|
1461
1441
|
m.loading ? h('div', { className: 'cat-note', textContent: 'Loading its models…' }) : null)
|
|
1462
1442
|
}))
|
|
1463
1443
|
}
|
|
@@ -1609,6 +1589,18 @@ function drawMine(s) {
|
|
|
1609
1589
|
order.length > 1 ? h('button', { className: 'small primary', textContent: 'Save order', title: 'save the order shown as your model order (#1 first)', onclick: () => keep(view) }) : null,
|
|
1610
1590
|
order.length ? h('button', { className: 'small', textContent: 'Refresh from models.dev', title: 'ask models.dev again what each model can do: context, tools, thinking, images, price', onclick: () => lookUp() }) : null,
|
|
1611
1591
|
order.length ? h('button', { className: 'small', textContent: 'Test all', disabled: testingAll, title: 'send each a short message to see which really answer: every provider at once, a few from each (one at a time per 100 models)', onclick: () => tryAll() }) : null),
|
|
1592
|
+
// when the one in use fails, and what a new message starts with
|
|
1593
|
+
order.length > 1 ? h('div', { className: 'mt-rules' },
|
|
1594
|
+
h('span', { className: 'rule' },
|
|
1595
|
+
h('label', { for: 'r-on', textContent: 'When a model fails, go on to the next' }),
|
|
1596
|
+
h('select', { id: 'r-on', onchange: (e) => saveOrder(order, 'saved', { on: e.target.value }) },
|
|
1597
|
+
[['failure', 'at its first failure'], ['retries', 'after some tries of the same model']].map(([v, t]) => h('option', { value: v, textContent: t, selected: s.rotation.on === v }))),
|
|
1598
|
+
s.rotation.on === 'retries' ? h('input', { type: 'number', 'aria-label': 'tries', title: 'tries of the same model first (rate limits, outages)', min: 1, max: 20, value: s.rotation.retries, onchange: (e) => saveOrder(order, 'saved', { retries: Number(e.target.value) }) }) : null),
|
|
1599
|
+
h('span', { className: 'rule' },
|
|
1600
|
+
h('label', { for: 'r-next', textContent: 'A new message starts with' }),
|
|
1601
|
+
h('select', { id: 'r-next', onchange: (e) => saveOrder(order, 'saved', { next: e.target.value }) },
|
|
1602
|
+
[['first', '#1 again'], ['stay', 'the model that answered last']].map(([v, t]) => h('option', { value: v, textContent: t, selected: s.rotation.next === v })))),
|
|
1603
|
+
h('span', { className: 'hint', textContent: s.rotation.on === 'retries' ? 'A rate limit or outage: that model is tried again that many times first; any other error moves on at once.' : "Any error moves on at once, to another provider's model first." })) : null,
|
|
1612
1604
|
order.length
|
|
1613
1605
|
? h('div', { className: 'mt-scroll' }, h('div', { className: 'mt' },
|
|
1614
1606
|
jobSlot('table'),
|
|
@@ -1663,6 +1655,7 @@ function markCatalog() {
|
|
|
1663
1655
|
tag.hidden = !n
|
|
1664
1656
|
tag.textContent = `${n} in your list${S.order[0]?.provider === sec.dataset.provider ? ' · #1' : ''}`
|
|
1665
1657
|
}
|
|
1658
|
+
drawPicks()
|
|
1666
1659
|
}
|
|
1667
1660
|
|
|
1668
1661
|
function drawUsing(s) {
|
|
@@ -2038,7 +2031,6 @@ if (token) {
|
|
|
2038
2031
|
const t = S.order[placeOf(...d.key.split(/\/(.*)/s))]
|
|
2039
2032
|
if (!t) continue
|
|
2040
2033
|
Object.assign(t, { tested: d.tested ?? t.tested, known: d.known ?? t.known })
|
|
2041
|
-
document.querySelector(`#inuse li[data-key="${CSS.escape(d.key)}"] .test`)?.replaceWith(testLine(t))
|
|
2042
2034
|
}
|
|
2043
2035
|
news.clear()
|
|
2044
2036
|
drawMine(S)
|