@opennodes/registry 0.1.24 → 0.1.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/web/index.html +88 -29
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opennodes/registry",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.26",
|
|
4
4
|
"description": "OpenNodes (ONP) reference registry: node registration with proof-of-resource qualification (Stage A/B/C), transparent ranking, enforceable estimates, catalog importers (OpenRouter, Hugging Face, models.dev, Ollama), MCP surface, and the web app (catalog, chat, advisor, Node Builder)",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
package/src/web/index.html
CHANGED
|
@@ -47,6 +47,19 @@
|
|
|
47
47
|
.mitem .t .nm { font-weight: 600; font-size: 0.86rem; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
48
48
|
.mitem .b { display: flex; gap: 0.8rem; margin-top: 0.25rem; font-size: 0.7rem; color: var(--muted); font-variant-numeric: tabular-nums; flex-wrap: wrap; }
|
|
49
49
|
.mdetail { min-height: 30rem; }
|
|
50
|
+
.scorebox { margin: 0.4rem 0 0.8rem; border: 1px solid var(--line); border-radius: 6px; background: var(--panel); padding: 0.5rem 0.75rem; font-size: 0.8rem; }
|
|
51
|
+
.scorebox summary { cursor: pointer; }
|
|
52
|
+
.scorebox table { width: 100%; border-collapse: collapse; margin-top: 0.4rem; }
|
|
53
|
+
.scorebox td { padding: 0.2rem 0.5rem 0.2rem 0; border-top: 1px solid var(--line-2); vertical-align: top; }
|
|
54
|
+
.scorebox td.num { text-align: right; white-space: nowrap; }
|
|
55
|
+
.scorebox td.sub { font-size: 0.74rem; }
|
|
56
|
+
.scorebox tr.tot td { border-top: 1px solid var(--line); }
|
|
57
|
+
.pager { display: flex; align-items: center; justify-content: center; gap: 0.3rem; margin-top: 0.5rem; font-size: 0.78rem; color: var(--muted); flex-wrap: wrap; }
|
|
58
|
+
.pager:empty { display: none; }
|
|
59
|
+
.pgbtn { font: inherit; min-width: 2rem; padding: 0.25rem 0.5rem; border-radius: 6px; border: 1px solid var(--line); background: var(--panel); color: var(--accent); cursor: pointer; }
|
|
60
|
+
.pgbtn:disabled { color: var(--faint); cursor: default; }
|
|
61
|
+
.pgnum { padding: 0 0.3rem; }
|
|
62
|
+
.pgnum input { width: 4rem; font: inherit; padding: 0.2rem 0.3rem; border: 1px solid var(--line); border-radius: 6px; background: var(--panel); color: var(--ink); text-align: center; }
|
|
50
63
|
|
|
51
64
|
/* chat */
|
|
52
65
|
.chatgrid { display: grid; grid-template-columns: minmax(0, 1fr) 19rem; gap: 1.2rem; align-items: start; }
|
|
@@ -244,7 +257,7 @@
|
|
|
244
257
|
<div id="adv-out"></div>
|
|
245
258
|
</details>
|
|
246
259
|
<div class="mgrid">
|
|
247
|
-
<div class="mlist" id="m-list"></div>
|
|
260
|
+
<div class="mcol"><div class="mlist" id="m-list"></div><div class="pager" id="m-pager"></div></div>
|
|
248
261
|
<div class="mdetail" id="m-detail"><p class="sub">Select a model from the list.</p></div>
|
|
249
262
|
</div>
|
|
250
263
|
</div>
|
|
@@ -261,6 +274,7 @@
|
|
|
261
274
|
</div>
|
|
262
275
|
<p class="desc" id="md-desc" hidden></p>
|
|
263
276
|
<div class="statstrip" id="md-stats"></div>
|
|
277
|
+
<div id="md-score"></div>
|
|
264
278
|
<div class="secnav" id="md-nav"></div>
|
|
265
279
|
<h2 id="sec-offerings">Offerings <span class="counts" id="md-count"></span></h2>
|
|
266
280
|
<div class="panel tablewrap" id="md-offerings"></div>
|
|
@@ -607,34 +621,47 @@ function provLine(o) {
|
|
|
607
621
|
}
|
|
608
622
|
|
|
609
623
|
const PAGE = 60;
|
|
610
|
-
let listQuery = '', listTotal = 0;
|
|
611
|
-
|
|
612
|
-
// Count line and the
|
|
613
|
-
function
|
|
614
|
-
|
|
615
|
-
const
|
|
616
|
-
$('#m-counts').textContent = fmtInt(listTotal) + ' offerings' + (
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
624
|
+
let listQuery = '', listTotal = 0, listPage = 0;
|
|
625
|
+
|
|
626
|
+
// Count line and page controls under the list: first / previous / page N of M / next / last.
|
|
627
|
+
function updatePager() {
|
|
628
|
+
const pages = Math.max(1, Math.ceil(listTotal / PAGE));
|
|
629
|
+
const from = listTotal ? listPage * PAGE + 1 : 0, to = Math.min(listTotal, (listPage + 1) * PAGE);
|
|
630
|
+
$('#m-counts').textContent = fmtInt(listTotal) + ' offerings' + (pages > 1 ? ` · ${fmtInt(from)}–${fmtInt(to)}` : '');
|
|
631
|
+
const pager = $('#m-pager');
|
|
632
|
+
if (pages <= 1) { pager.innerHTML = ''; return; }
|
|
633
|
+
const btn = (label, page, title) => `<button class="pgbtn" data-page="${page}" title="${title}"${page < 0 || page >= pages || page === listPage ? ' disabled' : ''}>${label}</button>`;
|
|
634
|
+
pager.innerHTML = btn('«', 0, 'first page') + btn('‹', listPage - 1, 'previous page')
|
|
635
|
+
+ `<span class="pgnum">Page <input id="m-page" type="number" min="1" max="${pages}" value="${listPage + 1}" aria-label="page number"> of ${fmtInt(pages)}</span>`
|
|
636
|
+
+ btn('›', listPage + 1, 'next page') + btn('»', pages - 1, 'last page');
|
|
637
|
+
pager.querySelectorAll('.pgbtn').forEach((b) => b.addEventListener('click', () => goToPage(Number(b.dataset.page))));
|
|
638
|
+
$('#m-page').addEventListener('change', (e) => goToPage(Math.min(pages, Math.max(1, Math.round(Number(e.target.value) || 1))) - 1));
|
|
621
639
|
}
|
|
622
640
|
|
|
623
|
-
async function
|
|
624
|
-
const btn = $('#m-more');
|
|
625
|
-
btn.disabled = true;
|
|
641
|
+
async function goToPage(page) {
|
|
626
642
|
const query = listQuery;
|
|
627
643
|
const p = new URLSearchParams(query);
|
|
628
|
-
p.set('offset',
|
|
644
|
+
p.set('offset', page * PAGE);
|
|
645
|
+
$('#m-pager').querySelectorAll('button, input').forEach((el) => { el.disabled = true; });
|
|
629
646
|
try {
|
|
630
|
-
const { offerings } = await api('/v0/offerings?' + p);
|
|
647
|
+
const { offerings, total } = await api('/v0/offerings?' + p);
|
|
631
648
|
if (query !== listQuery) return; // the filters changed meanwhile
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
649
|
+
listPage = page;
|
|
650
|
+
listTotal = total ?? listTotal;
|
|
651
|
+
lastResults = offerings;
|
|
652
|
+
renderList(offerings);
|
|
653
|
+
$('#m-list').scrollTop = 0;
|
|
654
|
+
updatePager();
|
|
655
|
+
} catch (e) { updatePager(); $('#m-pager').insertAdjacentHTML('beforeend', '<span class="err">' + esc(e.message) + '</span>'); }
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Render one page of the list, keeping the highlighted model when it is on this page.
|
|
659
|
+
function renderList(offerings) {
|
|
660
|
+
$('#m-list').innerHTML = offerings.map(renderItem).join('')
|
|
661
|
+
|| '<p class="sub">No offerings match. A fresh registry fills up via catalog imports and node registrations.</p>';
|
|
662
|
+
document.querySelectorAll('.mitem').forEach((el) => el.addEventListener('click', () => selectModel(el)));
|
|
663
|
+
const current = $('#model-card')?.dataset.artifact;
|
|
664
|
+
document.querySelectorAll('.mitem').forEach((el) => el.classList.toggle('sel', el.dataset.artifact === current));
|
|
638
665
|
}
|
|
639
666
|
|
|
640
667
|
async function search() {
|
|
@@ -652,17 +679,16 @@ async function search() {
|
|
|
652
679
|
if (listQuery !== p.toString()) return; // a newer search is under way
|
|
653
680
|
lastResults = offerings;
|
|
654
681
|
listTotal = total ?? offerings.length;
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
document.querySelectorAll('.mitem').forEach((el) => el.addEventListener('click', () => selectModel(el)));
|
|
682
|
+
listPage = 0;
|
|
683
|
+
renderList(offerings);
|
|
684
|
+
updatePager();
|
|
659
685
|
// keep the current selection when still listed, else select the first result
|
|
660
686
|
const current = $('#model-card')?.dataset.artifact;
|
|
661
687
|
const keep = current && [...document.querySelectorAll('.mitem')].find((el) => el.dataset.artifact === current);
|
|
662
688
|
const target = keep ?? document.querySelector('.mitem');
|
|
663
689
|
if (target) selectModel(target);
|
|
664
690
|
else $('#m-detail').innerHTML = '<p class="sub">No offerings match.</p>';
|
|
665
|
-
} catch (e) { $('#m-list').innerHTML = '<p class="err">' + esc(e.message) + '</p>'; }
|
|
691
|
+
} catch (e) { $('#m-list').innerHTML = '<p class="err">' + esc(e.message) + '</p>'; $('#m-pager').innerHTML = ''; }
|
|
666
692
|
}
|
|
667
693
|
|
|
668
694
|
/* ---------- advisor (ONP-3 §5b) ---------- */
|
|
@@ -708,8 +734,10 @@ function renderAdvice(r) {
|
|
|
708
734
|
});
|
|
709
735
|
}
|
|
710
736
|
|
|
737
|
+
let listedOffering = null; // the list entry the detail was opened from: its score is explained there
|
|
711
738
|
function selectModel(el) {
|
|
712
739
|
document.querySelectorAll('.mitem').forEach((x) => x.classList.toggle('sel', x === el));
|
|
740
|
+
listedOffering = lastResults.find((o) => `${o.node_id}/${o.offering_id}` === el.dataset.key) ?? null;
|
|
713
741
|
const host = $('#m-detail');
|
|
714
742
|
if (!host.contains($('#model-card'))) { host.innerHTML = ''; host.appendChild($('#model-card')); }
|
|
715
743
|
loadModel(el.dataset.artifact);
|
|
@@ -718,7 +746,7 @@ function selectModel(el) {
|
|
|
718
746
|
let lastResults = [];
|
|
719
747
|
function renderItem(o) {
|
|
720
748
|
const obs = o.observed || {};
|
|
721
|
-
return `<div class="mitem" data-artifact="${esc(o.model.artifact)}">
|
|
749
|
+
return `<div class="mitem" data-artifact="${esc(o.model.artifact)}" data-key="${esc(o.node_id + '/' + o.offering_id)}">
|
|
722
750
|
<div class="t">
|
|
723
751
|
<span class="nm">${esc(o.model.name)}</span>
|
|
724
752
|
${tierChip(o.tier)}
|
|
@@ -821,6 +849,35 @@ function renderRow(o, idx) {
|
|
|
821
849
|
}
|
|
822
850
|
|
|
823
851
|
/* ---------- model detail ---------- */
|
|
852
|
+
// How the list's ranking score for the selected entry was computed: weight × component per part.
|
|
853
|
+
function renderScore(artifact) {
|
|
854
|
+
const host = $('#md-score');
|
|
855
|
+
const o = listedOffering;
|
|
856
|
+
if (!o?.rank_explanation || o.model.artifact !== artifact) { host.innerHTML = ''; return; }
|
|
857
|
+
const { weights, components } = o.rank_explanation;
|
|
858
|
+
const obs = o.observed || {};
|
|
859
|
+
const why = {
|
|
860
|
+
price: o.pricing?.input_per_mtok == null ? 'no price published'
|
|
861
|
+
: `${o.pricing.input_per_mtok === 0 ? 'free' : '$' + fmt(o.pricing.input_per_mtok) + '/MTok in'} — position among the listed results (cheapest 1.00, dearest 0.00)`,
|
|
862
|
+
tier: {
|
|
863
|
+
verified: 'verified: passed identity, context and capacity checks',
|
|
864
|
+
community: 'community: passed the registration checks',
|
|
865
|
+
attested: 'attested by a third party',
|
|
866
|
+
}[o.tier] ?? (o.source === 'import' ? 'unverified: imported from a public catalog, not checked by this registry' : 'unverified: not checked yet'),
|
|
867
|
+
probe_success: obs.probes > 0 ? `${Math.round((obs.probe_success ?? 0) * obs.probes)} of the last ${obs.probes} checks passed` : 'no checks yet',
|
|
868
|
+
};
|
|
869
|
+
const label = { price: 'Price', tier: 'Trust tier', probe_success: 'Check pass rate' };
|
|
870
|
+
const rows = Object.keys(weights).map((k) => `<tr>
|
|
871
|
+
<td>${label[k] ?? esc(k)}</td>
|
|
872
|
+
<td class="num mono">${fmt(components[k] ?? 0)} × ${fmt(weights[k])}</td>
|
|
873
|
+
<td class="num mono"><b>${fmt((components[k] ?? 0) * weights[k])}</b></td>
|
|
874
|
+
<td class="sub">${esc(why[k] ?? '')}</td></tr>`).join('');
|
|
875
|
+
host.innerHTML = `<details class="scorebox" open>
|
|
876
|
+
<summary>Ranking score <b class="mono">${fmt(o.rank)}</b> <span class="sub">for ${esc(o.node_id)} in the current list</span></summary>
|
|
877
|
+
<table>${rows}<tr class="tot"><td>Total</td><td></td><td class="num mono"><b>${fmt(o.rank)}</b></td><td class="sub">weighted sum; measured values, never paid placement</td></tr></table>
|
|
878
|
+
</details>`;
|
|
879
|
+
}
|
|
880
|
+
|
|
824
881
|
async function loadModel(artifact) {
|
|
825
882
|
$('#model-card').dataset.artifact = artifact;
|
|
826
883
|
$('#md-crumb').innerHTML = '<a href="#/">Models</a> / ' + esc(artifact);
|
|
@@ -874,6 +931,8 @@ async function loadModel(artifact) {
|
|
|
874
931
|
first.modalities ? stat(esc((first.modalities.input || []).join('+')) + ' → ' + esc((first.modalities.output || []).join('+')), 'MODALITIES') : '',
|
|
875
932
|
].filter(Boolean).join('');
|
|
876
933
|
|
|
934
|
+
renderScore(artifact);
|
|
935
|
+
|
|
877
936
|
// section nav (anchors appear only for sections that render)
|
|
878
937
|
const navBits = [['#sec-offerings', 'OFFERINGS'], ['#sec-play', 'PLAYGROUND']];
|
|
879
938
|
|