openvibers/openvibe.exchange Commits
No text arrows: drawn glyphs (Design rule 169) (#255)
Changes
nnew file mode 120000@@@ -0,0 +1 @@++/home/workstation/.cache/openvibe-deps/OpenVibe.Exchange/bc6bf3c8e9366ab2/node_modules\\ No newline at end of file@@@ -96,6 +96,9 @@.ex-more { margin: 10px 0 0; font-size: .9rem; }.ex-more a { text-decoration: none; }++.ex-more a { gap: 5px; }++.cd-open svg { margin-left: 5px; vertical-align: -2px; }++.ex-back svg { margin-right: 5px; }.ex-more a:hover { text-decoration: underline; }.ex-prose { max-width: 74ch; line-height: 1.65; }/* OVMetrics blocks (openvibe-shared/ov-metrics.js): the browser draws tiles, strips, spark and bars; the plain-text@@@ -10,12 +10,14 @@ document.querySelectorAll('[data-task-default]').forEach((form) => {const byValue = new Map(models.map((m) => [JSON.stringify({ model: m.model, source: m.source }), m.model]));let dragged = null;++ const UP = '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M12 20V4M6 10l6-6 6 6"/></svg>';++ const DOWN = '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M12 4v16M6 14l6 6 6-6"/></svg>';const buttonsFor = (li) => {-- for (const [move, text, label] of [['-1', '↑', 'Move fallback up'], ['1', '↓', 'Move fallback down']]) {++ for (const [move, svg, label] of [['-1', UP, 'Move fallback up'], ['1', DOWN, 'Move fallback down']]) {const button = document.createElement('button');button.type = 'button';button.className = 'ovk-btn ovk-btn--quiet';-- button.textContent = text;++ button.innerHTML = svg;button.setAttribute('aria-label', label);button.dataset.move = move;li.append(button);@@@ -793,11 +793,11 @@ function createCompanionRoutes(ctx) {if (at < 0) at = pairs.length - 1;const url = (p) => branchUrl(chatId, query, turn.id, p);const prev = at > 0-- ? '<a class="ex-branch-arrow" href="' + esc(url(pairs[at - 1])) + '" rel="nofollow" aria-label="The previous branch">\u2039</a>'-- : '<span class="ex-branch-arrow is-off" aria-hidden="true">\u2039</span>';++ ? '<a class="ex-branch-arrow" href="' + esc(url(pairs[at - 1])) + '" rel="nofollow" aria-label="The previous branch">' + kit.icon('back') + '</a>'++ : '<span class="ex-branch-arrow is-off" aria-hidden="true">' + kit.icon('back') + '</span>';const next = at < pairs.length - 1-- ? '<a class="ex-branch-arrow" href="' + esc(url(pairs[at + 1])) + '" rel="nofollow" aria-label="The next branch">\u203a</a>'-- : '<span class="ex-branch-arrow is-off" aria-hidden="true">\u203a</span>';++ ? '<a class="ex-branch-arrow" href="' + esc(url(pairs[at + 1])) + '" rel="nofollow" aria-label="The next branch">' + kit.icon('forward') + '</a>'++ : '<span class="ex-branch-arrow is-off" aria-hidden="true">' + kit.icon('forward') + '</span>';return '<span class="ex-branch-nav" title="Branch ' + (at + 1) + ' of ' + pairs.length + '">' + prev+ '<span class="ex-branch-count">' + (at + 1) + '/' + pairs.length + '</span>' + next + '</span>';}@@@ -34,7 +34,7 @@ async function start() {// queues requests in the kernel instead of answering 502. Without the socket unit, the usual listen.const socketActivated = process.env.LISTEN_FDS === '1' && Number(process.env.LISTEN_PID) === process.pid;const server = app.listen(...(socketActivated ? [{ fd: 3 }] : [config.port, config.host]), () => {-- console.log(`[OpenVibe.Exchange] ${config.nodeEnv} on http://${config.host}:${config.port} → ${config.baseUrl} (db ${ctx.s.db.store})`);++ console.log(`[OpenVibe.Exchange] ${config.nodeEnv} on http://${config.host}:${config.port}, public ${config.baseUrl} (db ${ctx.s.db.store})`);// Warm the Network signing key immediately: a service nobody has signed in to yet must not read degraded// only because the first JWKS fetch had not happened.ctx.keys.ensure().catch(() => {});@@@ -56,7 +56,7 @@ function card(repo, now) {<p class="cd-desc">${repo.description ? esc(repo.description) : '<span class="cd-muted">No description.</span>'}</p><p class="cd-stats"><span class="cd-stat" title="Stars">${icon('fa-star')}<b>${count(repo.stars)}</b>${gain}</span><span class="cd-stat" title="Forks">${icon('fa-code-branch') || ''}<b>${count(repo.forks)}</b> forks</span>${lang ? `<span class="cd-lang">${dot}${esc(lang)}</span>` : ''}</p>${topics.length ? `<ul class="cd-topics" aria-label="Topics">${topics.map((t) => `<li>${esc(t)}</li>`).join('')}</ul>` : ''}--<footer class="cd-foot"><span>Updated ${esc(when(repo.pushed, now))}</span><a class="cd-open" href="${esc(repo.url)}" rel="noopener noreferrer">GitHub →</a></footer>++<footer class="cd-foot"><span>Updated ${esc(when(repo.pushed, now))}</span><a class="cd-open" href="${esc(repo.url)}" rel="noopener noreferrer">GitHub ${glyphs.svg('arrow-right', { size: 16 })}</a></footer></li>`;}@@@ -224,7 +224,7 @@ function memoryPage({ card = {}, notes = [], locked = false, lock = null, proble+ '</div></li>';};return '<div class="ex-wrap ex-narrow ex-memory-page">'-- + '<p><a class="ex-back" href="' + esc(pathOf(card)) + '">← Back to ' + esc(card.name) + '</a></p>'++ + '<p><a class="ex-back" href="' + esc(pathOf(card)) + '">' + kit.icon('back') + ' Back to ' + esc(card.name) + '</a></p>'+ '<h1>Memory</h1>'+ '<p class="ex-lede">What <b>' + esc(card.name) + '</b> remembers about you. The notes are yours: edit them, pin the ones that matter, delete the rest, or add your own. They live in your OpenVibe memory, not in a plan.</p>'+ (problem ? kit.notice({ kind: 'error', text: problem }) : '')@@@ -297,7 +297,7 @@ function lorebookPage({ card = {}, books = [], chat = null, problem = null, noti+ '</section>';};return '<div class="ex-wrap ex-narrow ex-lorebook-page">'-- + '<p><a class="ex-back" href="' + esc(pathOf(card)) + '">← Back to ' + esc(card.name) + '</a></p>'++ + '<p><a class="ex-back" href="' + esc(pathOf(card)) + '">' + kit.icon('back') + ' Back to ' + esc(card.name) + '</a></p>'+ '<h1>Lorebooks</h1>'+ '<p class="ex-lede">Entries that join the context when one of their keys appears in the recent turns, the highest priority first, within a token budget. Attach a book to your world, to <b>' + esc(card.name) + '</b>, or to this chat. The chat’s “What the model saw” panel lists the entries used for each answer.</p>'+ (problem ? kit.notice({ kind: 'error', text: problem }) : '')@@@ -9,6 +9,7 @@*/const { esc } = require('../render/html');const marks = require('openvibe-shared/ai-marks');++const glyphs = require('openvibe-shared/glyphs');const ACCESS = Object.freeze({OWN_DEPLOYMENT: ['Our own deployment', 'Runs on machines OpenVibe controls.'],@@@ -265,7 +266,7 @@ function pricebookSection(data = {}) {const dir = c.old_price == null ? 'new' : Number(c.new_price) > Number(c.old_price) ? 'up' : 'down';return `<li class="exc-change"><div class="exc-model-head">${mark(c.provider_key, 24)}<b>${esc(c.model_key || '')}</b><small>${esc(c.provider_key || '')} · ${esc(FIELD[c.field] || c.field || '')} price per 1M${c.checked_at ? ` · read ${esc(day(c.checked_at))}` : ''}${c.source ? ` · <a href="${esc(c.source)}" rel="noopener noreferrer" target="_blank">their page</a>` : ''}</small></div>-- <p class="exc-delta is-${dir}"><span>${esc(money(c.old_price) || 'No price')}</span><span aria-hidden="true">→</span><b>${esc(money(c.new_price) || '–')}</b>${pct != null ? `<em>${pct > 0 ? '+' : ''}${esc(pct.toFixed(Math.abs(pct) < 10 ? 1 : 0))}%</em>` : ''}</p>++ <p class="exc-delta is-${dir}"><span>${esc(money(c.old_price) || 'No price')}</span><span aria-hidden="true">${glyphs.svg('arrow-right', { size: 16 })}</span><b>${esc(money(c.new_price) || '–')}</b>${pct != null ? `<em>${pct > 0 ? '+' : ''}${esc(pct.toFixed(Math.abs(pct) < 10 ? 1 : 0))}%</em>` : ''}</p>${open ? `<form method="post" action="/console/pricebook/changes/${encodeURIComponent(c.id)}" class="exc-inline exc-decide"><button class="ovk-btn ovk-btn--primary" name="decision" value="approve">Accept</button><button class="ovk-btn ovk-btn--quiet" name="decision" value="reject">Keep the current price</button></form>` : `<p class="exc-note">${esc(c.status === 'applied' ? 'Applied' : c.status || '')}${c.decided_at ? ` ${esc(day(c.decided_at))}` : ''}</p>`}</li>`;};@@@ -7,6 +7,8 @@* read is unavailable the page says so and shows no invented figure.*/const kit = require('openvibe-shared/kit');++const glyphs = require('openvibe-shared/glyphs');++const GO = glyphs.svg('arrow-right', { size: 16 });const { esc } = require('../render/html');const { mark, product, productMark } = require('./marks');const metrics = require('./metrics');@@@ -220,7 +222,7 @@ ${stats.length ? kit.stats(stats) : ''}${panel({title: 'Popular now', note: 'By real requests in the last seven days.', id: 'popular',body: `${modelList(popular, { empty: 'No requests yet. The first one will show up here.', withTry: true })}--${models.length ? `<p class="ex-more"><a href="/models">All ${models.length} models →</a></p>` : ''}`,++${models.length ? `<p class="ex-more"><a href="/models">All ${models.length} models ${GO}</a></p>` : ''}`,})}<div class="ex-lines">${lineCard({ href: '/arena', name: 'The Arena', text: 'One prompt, two unnamed models, one vote: then both names and their receipts.', icon: 'swap', tone: 'accent' })}@@@ -237,7 +239,7 @@ ${panel({title: 'Use it from code', note: 'One endpoint, compatible with OpenAI and with Anthropic.', id: 'code', body: `<p class="ex-prose">Point an OpenAI client at <code>https://api.openvibe.exchange/v1</code>, or Claude Code at its <code>/v1/messages</code>. Model ids are <code>deepseek/deepseek-chat</code>, or <code>openvibe/auto</code> to let the router pick by your floor, ceiling and policy.</p>${kit.snippet({ code: `curl ${ENDPOINT} \\\n -H "Authorization: Bearer $OPENVIBE_KEY" \\\n -H "Content-Type: application/json" \\\n -d '{"model":"deepseek/deepseek-chat","messages":[{"role":"user","content":"Hello"}]}'`, id: 'home-curl' })}--<p class="ex-more"><a href="/start">Start building in four steps →</a> · <a href="/api">Keys, usage and the full reference →</a></p>`,++<p class="ex-more"><a href="/start">Start building in four steps ${GO}</a> · <a href="/api">Keys, usage and the full reference ${GO}</a></p>`,})}${panel({title: 'AI in OpenVibe', note: 'Which products run which models, and what each costs a day.', id: 'products',@@@ -266,7 +268,7 @@ function previewCard(preview) {<p class="ex-preview-tag">A side-by-side answer <span>example words · live prices</span></p><p class="ex-preview-q">“Summarize this contract in three bullets.”</p><div class="ex-preview-grid">${cells}</div>--<p class="ex-more"><a href="https://openvibe.chat/vibe">Open the real chat →</a></p>++<p class="ex-more"><a href="https://openvibe.chat/vibe">Open the real chat ${GO}</a></p></aside>`;}@@@ -283,7 +285,7 @@ const EXAMPLE_ANSWER = {// A feature's own icon, from the kit's set, on a tint of a theme token. Model family marks name models only: on these// cards a family's mark read as a stray logo (owner, 11-09: "what is it even supposed to be?").function lineCard({ href, name, text, icon, tone = 'accent' }) {-- return `<a class="ex-line" href="${href}"><span class="ex-line-ic ex-line-ic--${esc(tone)}" aria-hidden="true">${kit.icon(icon)}</span><span><b>${esc(name)}</b><small>${esc(text)}</small></span><span class="ex-line-go" aria-hidden="true">→</span></a>`;++ return `<a class="ex-line" href="${href}"><span class="ex-line-ic ex-line-ic--${esc(tone)}" aria-hidden="true">${kit.icon(icon)}</span><span><b>${esc(name)}</b><small>${esc(text)}</small></span><span class="ex-line-go" aria-hidden="true">${GO}</span></a>`;}function productsSummary(usage) {@@@ -322,8 +324,8 @@ ${families.length ? `<div class="ex-chips" role="group" aria-label="Family">${fa${unavailable(catalog)}${panel({ title: '', body: modelList(models, { empty: 'No model matches that search. Try a family or a provider.' }) })}${communityPools(community)}--<p class="ex-more"><a href="https://openvibe.compare/">Compare models side by side on OpenVibe.Compare →</a></p>--<p class="ex-more"><a href="/status">Uptime and incidents →</a></p>++<p class="ex-more"><a href="https://openvibe.compare/">Compare models side by side on OpenVibe.Compare ${GO}</a></p>++<p class="ex-more"><a href="/status">Uptime and incidents ${GO}</a></p></div>`;}@@@ -422,7 +424,7 @@ ${stats.length ? kit.stats(stats) : ''}${model.description ? `<p class="ex-prose">${esc(model.description)}</p>` : ''}${measuredPanel(measured)}${panel({ title: 'Every offer that serves it', note: 'Price in, out and cached per million tokens; speed and uptime from the last seven days of real requests; the privacy label and the provider’s own data policy.', id: 'offers', body: kit.table({ columns, rows, label: `Offers for ${model.name}`, empty: 'No live offer serves this model right now.' }) })}--<p class="ex-more"><a href="https://openvibe.compare/things/${encodeURIComponent(model.slug)}">Compare ${esc(model.name)} with another model →</a></p>++<p class="ex-more"><a href="https://openvibe.compare/things/${encodeURIComponent(model.slug)}">Compare ${esc(model.name)} with another model ${GO}</a></p>${activityPanel(model.activity)}${require('./analysis').modelFacts(analysis)}${panel({ title: 'Presets', note: 'A preset fixes the router’s floor, ceiling and policy, and you name it as the model.', id: 'presets', body: presetsBox() })}@@@ -946,7 +948,7 @@ function changelogBody(changes) {const block = (title, items, render) => (items.length? `<h3 class="ex-h3">${esc(title)} <span class="ex-muted">${items.length}</span></h3><ul class="ex-changes">${items.map(render).join('')}</ul>`: '');-- const priceChange = (x) => `${x.was_in == null && x.was_out == null ? 'no price' : `${money(x.was_in)} in / ${money(x.was_out)} out`} → ${x.price_in == null && x.price_out == null ? 'free capacity' : `${money(x.price_in)} in / ${money(x.price_out)} out`}`;++ const priceChange = (x) => `${x.was_in == null && x.was_out == null ? 'no price' : `${money(x.was_in)} in / ${money(x.was_out)} out`} to ${x.price_in == null && x.price_out == null ? 'free capacity' : `${money(x.price_in)} in / ${money(x.price_out)} out`}`;const where = (x) => (x.community ? 'a community machine' : (x.provider ? providerLabel({ provider: x.provider, provider_name: x.provider }) : ''));const added = block('New', changes.added, (x) => `<li><b>${esc(x.name)}</b>${x.family ? ` · <span class="ex-muted">${esc(x.family)}</span>` : ''}${where(x) ? ` on ${esc(where(x))}` : ''}${x.price_out == null ? '' : ` · ${money(x.price_out)} out per M`}</li>`);const repriced = block('Price changed', changes.repriced, (x) => `<li><b>${esc(x.name)}</b> · ${esc(priceChange(x))}</li>`);@@@ -1150,7 +1152,7 @@ function startPage({ catalog = null } = {}) {'curl -s -X POST https://ai.openvibe.services/api/v1/chat \\',' -H "Authorization: Bearer $TOKEN" -H \'Content-Type: application/json\' \\',' -d \'{ "messages": [ { "role": "user", "content": "Name three uses for a paperclip." } ] }\'',-- '# → { "run": { "id": "run_…", "status": "succeeded", "output": …, "provenance": …, "explain": … } }',++ '# Response: { "run": { "id": "run_…", "status": "succeeded", "output": …, "provenance": …, "explain": … } }',].join('\n');return `<div class="ex-wrap ex-page">${kit.head({ title: 'Start building', sub: 'One endpoint, a key, and every answer says which model ran and why.', actions: [kit.button({ href: '/api', label: 'Keys and the API', kind: 'quiet' })] })}@@@ -1,5 +1,6 @@'use strict';const kit = require('openvibe-shared/kit');++const glyphs = require('openvibe-shared/glyphs');const thread = require('openvibe-shared/thread');const legacy = require('./exchange');const listed = (catalog) => legacy.chatModels(catalog);@@@ -241,6 +242,6 @@ function modelsPage({ catalog, query = {}, searchResults = null }) {// Filters that match nothing go last (still there, still saying 0), so the row starts with what narrows.const ordered = [...chips.filter((c) => !c.includes('is-empty')), ...chips.filter((c) => c.includes('is-empty'))];const sub = rows.length ? `${rows.length} to call now · live prices, measured speed` : 'None to call now match · the registry is below';-- return `<div class="ex-wrap xp xm-page">${kit.head({ title: 'Models', sub })}${unavailable(catalog)}<div class="xm-find">${filters}<nav class="ex-chips" aria-label="Model filters">${ordered.join('')}</nav></div>${table(rows, searchMeta)}<p class="ex-more"><a href="https://openvibe.compare/">Compare models side by side on OpenVibe.Compare →</a></p></div>`;++ return `<div class="ex-wrap xp xm-page">${kit.head({ title: 'Models', sub })}${unavailable(catalog)}<div class="xm-find">${filters}<nav class="ex-chips" aria-label="Model filters">${ordered.join('')}</nav></div>${table(rows, searchMeta)}<p class="ex-more"><a href="https://openvibe.compare/">Compare models side by side on OpenVibe.Compare ${glyphs.svg('arrow-right', { size: 16 })}</a></p></div>`;}module.exports = { computePage, home, modelsPage, pricing, pricingPage, table };@@@ -6,6 +6,7 @@* Design and layout come from the Kit; Opus restyles on review.*/const { esc } = require('../render/html');++const glyphs = require('openvibe-shared/glyphs');const AUTO = { model: 'openvibe/auto', source: 'openvibe' };@@@ -26,7 +27,7 @@ function picker(name, label, choice, models) {function fallbackRow(choice, models) {return `<li draggable="true"><span data-fallback-model>${esc(choice.model)}</span><input type="hidden" name="fallback" value="${esc(JSON.stringify(choice))}"><select data-fallback-edit aria-label="Change this fallback">${models.map((m) => option({ model: m.model, source: m.source }, m.name || m.model, m.model === choice.model)).join('')}-- </select><button type="button" class="ovk-btn ovk-btn--quiet" data-move="-1" aria-label="Move fallback up">↑</button><button type="button" class="ovk-btn ovk-btn--quiet" data-move="1" aria-label="Move fallback down">↓</button><button type="button" class="ovk-btn ovk-btn--quiet" data-remove>Remove</button></li>`;++ </select><button type="button" class="ovk-btn ovk-btn--quiet" data-move="-1" aria-label="Move fallback up">${glyphs.svg('arrow-up', { size: 16 })}</button><button type="button" class="ovk-btn ovk-btn--quiet" data-move="1" aria-label="Move fallback down">${glyphs.svg('arrow-down', { size: 16 })}</button><button type="button" class="ovk-btn ovk-btn--quiet" data-remove>Remove</button></li>`;}/**
new file mode 120000
new file mode 120000
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+/home/workstation/.cache/openvibe-deps/OpenVibe.Exchange/bc6bf3c8e9366ab2/node_modules
\ No newline at end of file
\ No newline at end of file
@@ -96,6 +96,9 @@
@@ -96,6 +96,9 @@
.ex-more { margin: 10px 0 0; font-size: .9rem; }
.ex-more { margin: 10px 0 0; font-size: .9rem; }
.ex-more a { text-decoration: none; }
.ex-more a { text-decoration: none; }
+.ex-more a { gap: 5px; }
+.cd-open svg { margin-left: 5px; vertical-align: -2px; }
+.ex-back svg { margin-right: 5px; }
.ex-more a:hover { text-decoration: underline; }
.ex-more a:hover { text-decoration: underline; }
.ex-prose { max-width: 74ch; line-height: 1.65; }
.ex-prose { max-width: 74ch; line-height: 1.65; }
/* OVMetrics blocks (openvibe-shared/ov-metrics.js): the browser draws tiles, strips, spark and bars; the plain-text
/* OVMetrics blocks (openvibe-shared/ov-metrics.js): the browser draws tiles, strips, spark and bars; the plain-text
@@ -10,12 +10,14 @@ document.querySelectorAll('[data-task-default]').forEach((form) => {
@@ -10,12 +10,14 @@ document.querySelectorAll('[data-task-default]').forEach((form) => {
const byValue = new Map(models.map((m) => [JSON.stringify({ model: m.model, source: m.source }), m.model]));
const byValue = new Map(models.map((m) => [JSON.stringify({ model: m.model, source: m.source }), m.model]));
let dragged = null;
let dragged = null;
+ const UP = '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M12 20V4M6 10l6-6 6 6"/></svg>';
+ const DOWN = '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M12 4v16M6 14l6 6 6-6"/></svg>';
const buttonsFor = (li) => {
const buttonsFor = (li) => {
- for (const [move, text, label] of [['-1', '↑', 'Move fallback up'], ['1', '↓', 'Move fallback down']]) {
+ for (const [move, svg, label] of [['-1', UP, 'Move fallback up'], ['1', DOWN, 'Move fallback down']]) {
const button = document.createElement('button');
const button = document.createElement('button');
button.type = 'button';
button.type = 'button';
button.className = 'ovk-btn ovk-btn--quiet';
button.className = 'ovk-btn ovk-btn--quiet';
- button.textContent = text;
+ button.innerHTML = svg;
button.setAttribute('aria-label', label);
button.setAttribute('aria-label', label);
button.dataset.move = move;
button.dataset.move = move;
li.append(button);
li.append(button);
@@ -793,11 +793,11 @@ function createCompanionRoutes(ctx) {
@@ -793,11 +793,11 @@ function createCompanionRoutes(ctx) {
if (at < 0) at = pairs.length - 1;
if (at < 0) at = pairs.length - 1;
const url = (p) => branchUrl(chatId, query, turn.id, p);
const url = (p) => branchUrl(chatId, query, turn.id, p);
const prev = at > 0
const prev = at > 0
- ? '<a class="ex-branch-arrow" href="' + esc(url(pairs[at - 1])) + '" rel="nofollow" aria-label="The previous branch">\u2039</a>'
- : '<span class="ex-branch-arrow is-off" aria-hidden="true">\u2039</span>';
+ ? '<a class="ex-branch-arrow" href="' + esc(url(pairs[at - 1])) + '" rel="nofollow" aria-label="The previous branch">' + kit.icon('back') + '</a>'
+ : '<span class="ex-branch-arrow is-off" aria-hidden="true">' + kit.icon('back') + '</span>';
const next = at < pairs.length - 1
const next = at < pairs.length - 1
- ? '<a class="ex-branch-arrow" href="' + esc(url(pairs[at + 1])) + '" rel="nofollow" aria-label="The next branch">\u203a</a>'
- : '<span class="ex-branch-arrow is-off" aria-hidden="true">\u203a</span>';
+ ? '<a class="ex-branch-arrow" href="' + esc(url(pairs[at + 1])) + '" rel="nofollow" aria-label="The next branch">' + kit.icon('forward') + '</a>'
+ : '<span class="ex-branch-arrow is-off" aria-hidden="true">' + kit.icon('forward') + '</span>';
return '<span class="ex-branch-nav" title="Branch ' + (at + 1) + ' of ' + pairs.length + '">' + prev
return '<span class="ex-branch-nav" title="Branch ' + (at + 1) + ' of ' + pairs.length + '">' + prev
+ '<span class="ex-branch-count">' + (at + 1) + '/' + pairs.length + '</span>' + next + '</span>';
+ '<span class="ex-branch-count">' + (at + 1) + '/' + pairs.length + '</span>' + next + '</span>';
}
}
@@ -34,7 +34,7 @@ async function start() {
@@ -34,7 +34,7 @@ async function start() {
// queues requests in the kernel instead of answering 502. Without the socket unit, the usual listen.
// queues requests in the kernel instead of answering 502. Without the socket unit, the usual listen.
const socketActivated = process.env.LISTEN_FDS === '1' && Number(process.env.LISTEN_PID) === process.pid;
const socketActivated = process.env.LISTEN_FDS === '1' && Number(process.env.LISTEN_PID) === process.pid;
const server = app.listen(...(socketActivated ? [{ fd: 3 }] : [config.port, config.host]), () => {
const server = app.listen(...(socketActivated ? [{ fd: 3 }] : [config.port, config.host]), () => {
- console.log(`[OpenVibe.Exchange] ${config.nodeEnv} on http://${config.host}:${config.port} → ${config.baseUrl} (db ${ctx.s.db.store})`);
+ console.log(`[OpenVibe.Exchange] ${config.nodeEnv} on http://${config.host}:${config.port}, public ${config.baseUrl} (db ${ctx.s.db.store})`);
// Warm the Network signing key immediately: a service nobody has signed in to yet must not read degraded
// Warm the Network signing key immediately: a service nobody has signed in to yet must not read degraded
// only because the first JWKS fetch had not happened.
// only because the first JWKS fetch had not happened.
ctx.keys.ensure().catch(() => {});
ctx.keys.ensure().catch(() => {});
@@ -56,7 +56,7 @@ function card(repo, now) {
@@ -56,7 +56,7 @@ function card(repo, now) {
<p class="cd-desc">${repo.description ? esc(repo.description) : '<span class="cd-muted">No description.</span>'}</p>
<p class="cd-desc">${repo.description ? esc(repo.description) : '<span class="cd-muted">No description.</span>'}</p>
<p class="cd-stats"><span class="cd-stat" title="Stars">${icon('fa-star')}<b>${count(repo.stars)}</b>${gain}</span><span class="cd-stat" title="Forks">${icon('fa-code-branch') || ''}<b>${count(repo.forks)}</b> forks</span>${lang ? `<span class="cd-lang">${dot}${esc(lang)}</span>` : ''}</p>
<p class="cd-stats"><span class="cd-stat" title="Stars">${icon('fa-star')}<b>${count(repo.stars)}</b>${gain}</span><span class="cd-stat" title="Forks">${icon('fa-code-branch') || ''}<b>${count(repo.forks)}</b> forks</span>${lang ? `<span class="cd-lang">${dot}${esc(lang)}</span>` : ''}</p>
${topics.length ? `<ul class="cd-topics" aria-label="Topics">${topics.map((t) => `<li>${esc(t)}</li>`).join('')}</ul>` : ''}
${topics.length ? `<ul class="cd-topics" aria-label="Topics">${topics.map((t) => `<li>${esc(t)}</li>`).join('')}</ul>` : ''}
-<footer class="cd-foot"><span>Updated ${esc(when(repo.pushed, now))}</span><a class="cd-open" href="${esc(repo.url)}" rel="noopener noreferrer">GitHub →</a></footer>
+<footer class="cd-foot"><span>Updated ${esc(when(repo.pushed, now))}</span><a class="cd-open" href="${esc(repo.url)}" rel="noopener noreferrer">GitHub ${glyphs.svg('arrow-right', { size: 16 })}</a></footer>
</li>`;
</li>`;
}
}
@@ -224,7 +224,7 @@ function memoryPage({ card = {}, notes = [], locked = false, lock = null, proble
@@ -224,7 +224,7 @@ function memoryPage({ card = {}, notes = [], locked = false, lock = null, proble
+ '</div></li>';
+ '</div></li>';
};
};
return '<div class="ex-wrap ex-narrow ex-memory-page">'
return '<div class="ex-wrap ex-narrow ex-memory-page">'
- + '<p><a class="ex-back" href="' + esc(pathOf(card)) + '">← Back to ' + esc(card.name) + '</a></p>'
+ + '<p><a class="ex-back" href="' + esc(pathOf(card)) + '">' + kit.icon('back') + ' Back to ' + esc(card.name) + '</a></p>'
+ '<h1>Memory</h1>'
+ '<h1>Memory</h1>'
+ '<p class="ex-lede">What <b>' + esc(card.name) + '</b> remembers about you. The notes are yours: edit them, pin the ones that matter, delete the rest, or add your own. They live in your OpenVibe memory, not in a plan.</p>'
+ '<p class="ex-lede">What <b>' + esc(card.name) + '</b> remembers about you. The notes are yours: edit them, pin the ones that matter, delete the rest, or add your own. They live in your OpenVibe memory, not in a plan.</p>'
+ (problem ? kit.notice({ kind: 'error', text: problem }) : '')
+ (problem ? kit.notice({ kind: 'error', text: problem }) : '')
@@ -297,7 +297,7 @@ function lorebookPage({ card = {}, books = [], chat = null, problem = null, noti
@@ -297,7 +297,7 @@ function lorebookPage({ card = {}, books = [], chat = null, problem = null, noti
+ '</section>';
+ '</section>';
};
};
return '<div class="ex-wrap ex-narrow ex-lorebook-page">'
return '<div class="ex-wrap ex-narrow ex-lorebook-page">'
- + '<p><a class="ex-back" href="' + esc(pathOf(card)) + '">← Back to ' + esc(card.name) + '</a></p>'
+ + '<p><a class="ex-back" href="' + esc(pathOf(card)) + '">' + kit.icon('back') + ' Back to ' + esc(card.name) + '</a></p>'
+ '<h1>Lorebooks</h1>'
+ '<h1>Lorebooks</h1>'
+ '<p class="ex-lede">Entries that join the context when one of their keys appears in the recent turns, the highest priority first, within a token budget. Attach a book to your world, to <b>' + esc(card.name) + '</b>, or to this chat. The chat’s “What the model saw” panel lists the entries used for each answer.</p>'
+ '<p class="ex-lede">Entries that join the context when one of their keys appears in the recent turns, the highest priority first, within a token budget. Attach a book to your world, to <b>' + esc(card.name) + '</b>, or to this chat. The chat’s “What the model saw” panel lists the entries used for each answer.</p>'
+ (problem ? kit.notice({ kind: 'error', text: problem }) : '')
+ (problem ? kit.notice({ kind: 'error', text: problem }) : '')
@@ -9,6 +9,7 @@
@@ -9,6 +9,7 @@
*/
*/
const { esc } = require('../render/html');
const { esc } = require('../render/html');
const marks = require('openvibe-shared/ai-marks');
const marks = require('openvibe-shared/ai-marks');
+const glyphs = require('openvibe-shared/glyphs');
const ACCESS = Object.freeze({
const ACCESS = Object.freeze({
OWN_DEPLOYMENT: ['Our own deployment', 'Runs on machines OpenVibe controls.'],
OWN_DEPLOYMENT: ['Our own deployment', 'Runs on machines OpenVibe controls.'],
@@ -265,7 +266,7 @@ function pricebookSection(data = {}) {
@@ -265,7 +266,7 @@ function pricebookSection(data = {}) {
const dir = c.old_price == null ? 'new' : Number(c.new_price) > Number(c.old_price) ? 'up' : 'down';
const dir = c.old_price == null ? 'new' : Number(c.new_price) > Number(c.old_price) ? 'up' : 'down';
return `<li class="exc-change">
return `<li class="exc-change">
<div class="exc-model-head">${mark(c.provider_key, 24)}<b>${esc(c.model_key || '')}</b><small>${esc(c.provider_key || '')} · ${esc(FIELD[c.field] || c.field || '')} price per 1M${c.checked_at ? ` · read ${esc(day(c.checked_at))}` : ''}${c.source ? ` · <a href="${esc(c.source)}" rel="noopener noreferrer" target="_blank">their page</a>` : ''}</small></div>
<div class="exc-model-head">${mark(c.provider_key, 24)}<b>${esc(c.model_key || '')}</b><small>${esc(c.provider_key || '')} · ${esc(FIELD[c.field] || c.field || '')} price per 1M${c.checked_at ? ` · read ${esc(day(c.checked_at))}` : ''}${c.source ? ` · <a href="${esc(c.source)}" rel="noopener noreferrer" target="_blank">their page</a>` : ''}</small></div>
- <p class="exc-delta is-${dir}"><span>${esc(money(c.old_price) || 'No price')}</span><span aria-hidden="true">→</span><b>${esc(money(c.new_price) || '–')}</b>${pct != null ? `<em>${pct > 0 ? '+' : ''}${esc(pct.toFixed(Math.abs(pct) < 10 ? 1 : 0))}%</em>` : ''}</p>
+ <p class="exc-delta is-${dir}"><span>${esc(money(c.old_price) || 'No price')}</span><span aria-hidden="true">${glyphs.svg('arrow-right', { size: 16 })}</span><b>${esc(money(c.new_price) || '–')}</b>${pct != null ? `<em>${pct > 0 ? '+' : ''}${esc(pct.toFixed(Math.abs(pct) < 10 ? 1 : 0))}%</em>` : ''}</p>
${open ? `<form method="post" action="/console/pricebook/changes/${encodeURIComponent(c.id)}" class="exc-inline exc-decide"><button class="ovk-btn ovk-btn--primary" name="decision" value="approve">Accept</button><button class="ovk-btn ovk-btn--quiet" name="decision" value="reject">Keep the current price</button></form>` : `<p class="exc-note">${esc(c.status === 'applied' ? 'Applied' : c.status || '')}${c.decided_at ? ` ${esc(day(c.decided_at))}` : ''}</p>`}
${open ? `<form method="post" action="/console/pricebook/changes/${encodeURIComponent(c.id)}" class="exc-inline exc-decide"><button class="ovk-btn ovk-btn--primary" name="decision" value="approve">Accept</button><button class="ovk-btn ovk-btn--quiet" name="decision" value="reject">Keep the current price</button></form>` : `<p class="exc-note">${esc(c.status === 'applied' ? 'Applied' : c.status || '')}${c.decided_at ? ` ${esc(day(c.decided_at))}` : ''}</p>`}
</li>`;
</li>`;
};
};
@@ -7,6 +7,8 @@
@@ -7,6 +7,8 @@
* read is unavailable the page says so and shows no invented figure.
* read is unavailable the page says so and shows no invented figure.
*/
*/
const kit = require('openvibe-shared/kit');
const kit = require('openvibe-shared/kit');
+const glyphs = require('openvibe-shared/glyphs');
+const GO = glyphs.svg('arrow-right', { size: 16 });
const { esc } = require('../render/html');
const { esc } = require('../render/html');
const { mark, product, productMark } = require('./marks');
const { mark, product, productMark } = require('./marks');
const metrics = require('./metrics');
const metrics = require('./metrics');
@@ -220,7 +222,7 @@ ${stats.length ? kit.stats(stats) : ''}
@@ -220,7 +222,7 @@ ${stats.length ? kit.stats(stats) : ''}
${panel({
${panel({
title: 'Popular now', note: 'By real requests in the last seven days.', id: 'popular',
title: 'Popular now', note: 'By real requests in the last seven days.', id: 'popular',
body: `${modelList(popular, { empty: 'No requests yet. The first one will show up here.', withTry: true })}
body: `${modelList(popular, { empty: 'No requests yet. The first one will show up here.', withTry: true })}
-${models.length ? `<p class="ex-more"><a href="/models">All ${models.length} models →</a></p>` : ''}`,
+${models.length ? `<p class="ex-more"><a href="/models">All ${models.length} models ${GO}</a></p>` : ''}`,
})}
})}
<div class="ex-lines">
<div class="ex-lines">
${lineCard({ href: '/arena', name: 'The Arena', text: 'One prompt, two unnamed models, one vote: then both names and their receipts.', icon: 'swap', tone: 'accent' })}
${lineCard({ href: '/arena', name: 'The Arena', text: 'One prompt, two unnamed models, one vote: then both names and their receipts.', icon: 'swap', tone: 'accent' })}
@@ -237,7 +239,7 @@ ${panel({
@@ -237,7 +239,7 @@ ${panel({
title: 'Use it from code', note: 'One endpoint, compatible with OpenAI and with Anthropic.', id: 'code', body: `
title: 'Use it from code', note: 'One endpoint, compatible with OpenAI and with Anthropic.', id: 'code', body: `
<p class="ex-prose">Point an OpenAI client at <code>https://api.openvibe.exchange/v1</code>, or Claude Code at its <code>/v1/messages</code>. Model ids are <code>deepseek/deepseek-chat</code>, or <code>openvibe/auto</code> to let the router pick by your floor, ceiling and policy.</p>
<p class="ex-prose">Point an OpenAI client at <code>https://api.openvibe.exchange/v1</code>, or Claude Code at its <code>/v1/messages</code>. Model ids are <code>deepseek/deepseek-chat</code>, or <code>openvibe/auto</code> to let the router pick by your floor, ceiling and policy.</p>
${kit.snippet({ code: `curl ${ENDPOINT} \\\n -H "Authorization: Bearer $OPENVIBE_KEY" \\\n -H "Content-Type: application/json" \\\n -d '{"model":"deepseek/deepseek-chat","messages":[{"role":"user","content":"Hello"}]}'`, id: 'home-curl' })}
${kit.snippet({ code: `curl ${ENDPOINT} \\\n -H "Authorization: Bearer $OPENVIBE_KEY" \\\n -H "Content-Type: application/json" \\\n -d '{"model":"deepseek/deepseek-chat","messages":[{"role":"user","content":"Hello"}]}'`, id: 'home-curl' })}
-<p class="ex-more"><a href="/start">Start building in four steps →</a> · <a href="/api">Keys, usage and the full reference →</a></p>`,
+<p class="ex-more"><a href="/start">Start building in four steps ${GO}</a> · <a href="/api">Keys, usage and the full reference ${GO}</a></p>`,
})}
})}
${panel({
${panel({
title: 'AI in OpenVibe', note: 'Which products run which models, and what each costs a day.', id: 'products',
title: 'AI in OpenVibe', note: 'Which products run which models, and what each costs a day.', id: 'products',
@@ -266,7 +268,7 @@ function previewCard(preview) {
@@ -266,7 +268,7 @@ function previewCard(preview) {
<p class="ex-preview-tag">A side-by-side answer <span>example words · live prices</span></p>
<p class="ex-preview-tag">A side-by-side answer <span>example words · live prices</span></p>
<p class="ex-preview-q">“Summarize this contract in three bullets.”</p>
<p class="ex-preview-q">“Summarize this contract in three bullets.”</p>
<div class="ex-preview-grid">${cells}</div>
<div class="ex-preview-grid">${cells}</div>
-<p class="ex-more"><a href="https://openvibe.chat/vibe">Open the real chat →</a></p>
+<p class="ex-more"><a href="https://openvibe.chat/vibe">Open the real chat ${GO}</a></p>
</aside>`;
</aside>`;
}
}
@@ -283,7 +285,7 @@ const EXAMPLE_ANSWER = {
@@ -283,7 +285,7 @@ const EXAMPLE_ANSWER = {
// A feature's own icon, from the kit's set, on a tint of a theme token. Model family marks name models only: on these
// A feature's own icon, from the kit's set, on a tint of a theme token. Model family marks name models only: on these
// cards a family's mark read as a stray logo (owner, 11-09: "what is it even supposed to be?").
// cards a family's mark read as a stray logo (owner, 11-09: "what is it even supposed to be?").
function lineCard({ href, name, text, icon, tone = 'accent' }) {
function lineCard({ href, name, text, icon, tone = 'accent' }) {
- return `<a class="ex-line" href="${href}"><span class="ex-line-ic ex-line-ic--${esc(tone)}" aria-hidden="true">${kit.icon(icon)}</span><span><b>${esc(name)}</b><small>${esc(text)}</small></span><span class="ex-line-go" aria-hidden="true">→</span></a>`;
+ return `<a class="ex-line" href="${href}"><span class="ex-line-ic ex-line-ic--${esc(tone)}" aria-hidden="true">${kit.icon(icon)}</span><span><b>${esc(name)}</b><small>${esc(text)}</small></span><span class="ex-line-go" aria-hidden="true">${GO}</span></a>`;
}
}
function productsSummary(usage) {
function productsSummary(usage) {
@@ -322,8 +324,8 @@ ${families.length ? `<div class="ex-chips" role="group" aria-label="Family">${fa
@@ -322,8 +324,8 @@ ${families.length ? `<div class="ex-chips" role="group" aria-label="Family">${fa
${unavailable(catalog)}
${unavailable(catalog)}
${panel({ title: '', body: modelList(models, { empty: 'No model matches that search. Try a family or a provider.' }) })}
${panel({ title: '', body: modelList(models, { empty: 'No model matches that search. Try a family or a provider.' }) })}
${communityPools(community)}
${communityPools(community)}
-<p class="ex-more"><a href="https://openvibe.compare/">Compare models side by side on OpenVibe.Compare →</a></p>
-<p class="ex-more"><a href="/status">Uptime and incidents →</a></p>
+<p class="ex-more"><a href="https://openvibe.compare/">Compare models side by side on OpenVibe.Compare ${GO}</a></p>
+<p class="ex-more"><a href="/status">Uptime and incidents ${GO}</a></p>
</div>`;
</div>`;
}
}
@@ -422,7 +424,7 @@ ${stats.length ? kit.stats(stats) : ''}
@@ -422,7 +424,7 @@ ${stats.length ? kit.stats(stats) : ''}
${model.description ? `<p class="ex-prose">${esc(model.description)}</p>` : ''}
${model.description ? `<p class="ex-prose">${esc(model.description)}</p>` : ''}
${measuredPanel(measured)}
${measuredPanel(measured)}
${panel({ title: 'Every offer that serves it', note: 'Price in, out and cached per million tokens; speed and uptime from the last seven days of real requests; the privacy label and the provider’s own data policy.', id: 'offers', body: kit.table({ columns, rows, label: `Offers for ${model.name}`, empty: 'No live offer serves this model right now.' }) })}
${panel({ title: 'Every offer that serves it', note: 'Price in, out and cached per million tokens; speed and uptime from the last seven days of real requests; the privacy label and the provider’s own data policy.', id: 'offers', body: kit.table({ columns, rows, label: `Offers for ${model.name}`, empty: 'No live offer serves this model right now.' }) })}
-<p class="ex-more"><a href="https://openvibe.compare/things/${encodeURIComponent(model.slug)}">Compare ${esc(model.name)} with another model →</a></p>
+<p class="ex-more"><a href="https://openvibe.compare/things/${encodeURIComponent(model.slug)}">Compare ${esc(model.name)} with another model ${GO}</a></p>
${activityPanel(model.activity)}
${activityPanel(model.activity)}
${require('./analysis').modelFacts(analysis)}
${require('./analysis').modelFacts(analysis)}
${panel({ title: 'Presets', note: 'A preset fixes the router’s floor, ceiling and policy, and you name it as the model.', id: 'presets', body: presetsBox() })}
${panel({ title: 'Presets', note: 'A preset fixes the router’s floor, ceiling and policy, and you name it as the model.', id: 'presets', body: presetsBox() })}
@@ -946,7 +948,7 @@ function changelogBody(changes) {
@@ -946,7 +948,7 @@ function changelogBody(changes) {
const block = (title, items, render) => (items.length
const block = (title, items, render) => (items.length
? `<h3 class="ex-h3">${esc(title)} <span class="ex-muted">${items.length}</span></h3><ul class="ex-changes">${items.map(render).join('')}</ul>`
? `<h3 class="ex-h3">${esc(title)} <span class="ex-muted">${items.length}</span></h3><ul class="ex-changes">${items.map(render).join('')}</ul>`
: '');
: '');
- const priceChange = (x) => `${x.was_in == null && x.was_out == null ? 'no price' : `${money(x.was_in)} in / ${money(x.was_out)} out`} → ${x.price_in == null && x.price_out == null ? 'free capacity' : `${money(x.price_in)} in / ${money(x.price_out)} out`}`;
+ const priceChange = (x) => `${x.was_in == null && x.was_out == null ? 'no price' : `${money(x.was_in)} in / ${money(x.was_out)} out`} to ${x.price_in == null && x.price_out == null ? 'free capacity' : `${money(x.price_in)} in / ${money(x.price_out)} out`}`;
const where = (x) => (x.community ? 'a community machine' : (x.provider ? providerLabel({ provider: x.provider, provider_name: x.provider }) : ''));
const where = (x) => (x.community ? 'a community machine' : (x.provider ? providerLabel({ provider: x.provider, provider_name: x.provider }) : ''));
const added = block('New', changes.added, (x) => `<li><b>${esc(x.name)}</b>${x.family ? ` · <span class="ex-muted">${esc(x.family)}</span>` : ''}${where(x) ? ` on ${esc(where(x))}` : ''}${x.price_out == null ? '' : ` · ${money(x.price_out)} out per M`}</li>`);
const added = block('New', changes.added, (x) => `<li><b>${esc(x.name)}</b>${x.family ? ` · <span class="ex-muted">${esc(x.family)}</span>` : ''}${where(x) ? ` on ${esc(where(x))}` : ''}${x.price_out == null ? '' : ` · ${money(x.price_out)} out per M`}</li>`);
const repriced = block('Price changed', changes.repriced, (x) => `<li><b>${esc(x.name)}</b> · ${esc(priceChange(x))}</li>`);
const repriced = block('Price changed', changes.repriced, (x) => `<li><b>${esc(x.name)}</b> · ${esc(priceChange(x))}</li>`);
@@ -1150,7 +1152,7 @@ function startPage({ catalog = null } = {}) {
@@ -1150,7 +1152,7 @@ function startPage({ catalog = null } = {}) {
'curl -s -X POST https://ai.openvibe.services/api/v1/chat \\',
'curl -s -X POST https://ai.openvibe.services/api/v1/chat \\',
' -H "Authorization: Bearer $TOKEN" -H \'Content-Type: application/json\' \\',
' -H "Authorization: Bearer $TOKEN" -H \'Content-Type: application/json\' \\',
' -d \'{ "messages": [ { "role": "user", "content": "Name three uses for a paperclip." } ] }\'',
' -d \'{ "messages": [ { "role": "user", "content": "Name three uses for a paperclip." } ] }\'',
- '# → { "run": { "id": "run_…", "status": "succeeded", "output": …, "provenance": …, "explain": … } }',
+ '# Response: { "run": { "id": "run_…", "status": "succeeded", "output": …, "provenance": …, "explain": … } }',
].join('\n');
].join('\n');
return `<div class="ex-wrap ex-page">
return `<div class="ex-wrap ex-page">
${kit.head({ title: 'Start building', sub: 'One endpoint, a key, and every answer says which model ran and why.', actions: [kit.button({ href: '/api', label: 'Keys and the API', kind: 'quiet' })] })}
${kit.head({ title: 'Start building', sub: 'One endpoint, a key, and every answer says which model ran and why.', actions: [kit.button({ href: '/api', label: 'Keys and the API', kind: 'quiet' })] })}
@@ -1,5 +1,6 @@
@@ -1,5 +1,6 @@
'use strict';
'use strict';
const kit = require('openvibe-shared/kit');
const kit = require('openvibe-shared/kit');
+const glyphs = require('openvibe-shared/glyphs');
const thread = require('openvibe-shared/thread');
const thread = require('openvibe-shared/thread');
const legacy = require('./exchange');
const legacy = require('./exchange');
const listed = (catalog) => legacy.chatModels(catalog);
const listed = (catalog) => legacy.chatModels(catalog);
@@ -241,6 +242,6 @@ function modelsPage({ catalog, query = {}, searchResults = null }) {
@@ -241,6 +242,6 @@ function modelsPage({ catalog, query = {}, searchResults = null }) {
// Filters that match nothing go last (still there, still saying 0), so the row starts with what narrows.
// Filters that match nothing go last (still there, still saying 0), so the row starts with what narrows.
const ordered = [...chips.filter((c) => !c.includes('is-empty')), ...chips.filter((c) => c.includes('is-empty'))];
const ordered = [...chips.filter((c) => !c.includes('is-empty')), ...chips.filter((c) => c.includes('is-empty'))];
const sub = rows.length ? `${rows.length} to call now · live prices, measured speed` : 'None to call now match · the registry is below';
const sub = rows.length ? `${rows.length} to call now · live prices, measured speed` : 'None to call now match · the registry is below';
- return `<div class="ex-wrap xp xm-page">${kit.head({ title: 'Models', sub })}${unavailable(catalog)}<div class="xm-find">${filters}<nav class="ex-chips" aria-label="Model filters">${ordered.join('')}</nav></div>${table(rows, searchMeta)}<p class="ex-more"><a href="https://openvibe.compare/">Compare models side by side on OpenVibe.Compare →</a></p></div>`;
+ return `<div class="ex-wrap xp xm-page">${kit.head({ title: 'Models', sub })}${unavailable(catalog)}<div class="xm-find">${filters}<nav class="ex-chips" aria-label="Model filters">${ordered.join('')}</nav></div>${table(rows, searchMeta)}<p class="ex-more"><a href="https://openvibe.compare/">Compare models side by side on OpenVibe.Compare ${glyphs.svg('arrow-right', { size: 16 })}</a></p></div>`;
}
}
module.exports = { computePage, home, modelsPage, pricing, pricingPage, table };
module.exports = { computePage, home, modelsPage, pricing, pricingPage, table };
@@ -6,6 +6,7 @@
@@ -6,6 +6,7 @@
* Design and layout come from the Kit; Opus restyles on review.
* Design and layout come from the Kit; Opus restyles on review.
*/
*/
const { esc } = require('../render/html');
const { esc } = require('../render/html');
+const glyphs = require('openvibe-shared/glyphs');
const AUTO = { model: 'openvibe/auto', source: 'openvibe' };
const AUTO = { model: 'openvibe/auto', source: 'openvibe' };
@@ -26,7 +27,7 @@ function picker(name, label, choice, models) {
@@ -26,7 +27,7 @@ function picker(name, label, choice, models) {
function fallbackRow(choice, models) {
function fallbackRow(choice, models) {
return `<li draggable="true"><span data-fallback-model>${esc(choice.model)}</span><input type="hidden" name="fallback" value="${esc(JSON.stringify(choice))}"><select data-fallback-edit aria-label="Change this fallback">
return `<li draggable="true"><span data-fallback-model>${esc(choice.model)}</span><input type="hidden" name="fallback" value="${esc(JSON.stringify(choice))}"><select data-fallback-edit aria-label="Change this fallback">
${models.map((m) => option({ model: m.model, source: m.source }, m.name || m.model, m.model === choice.model)).join('')}
${models.map((m) => option({ model: m.model, source: m.source }, m.name || m.model, m.model === choice.model)).join('')}
- </select><button type="button" class="ovk-btn ovk-btn--quiet" data-move="-1" aria-label="Move fallback up">↑</button><button type="button" class="ovk-btn ovk-btn--quiet" data-move="1" aria-label="Move fallback down">↓</button><button type="button" class="ovk-btn ovk-btn--quiet" data-remove>Remove</button></li>`;
+ </select><button type="button" class="ovk-btn ovk-btn--quiet" data-move="-1" aria-label="Move fallback up">${glyphs.svg('arrow-up', { size: 16 })}</button><button type="button" class="ovk-btn ovk-btn--quiet" data-move="1" aria-label="Move fallback down">${glyphs.svg('arrow-down', { size: 16 })}</button><button type="button" class="ovk-btn ovk-btn--quiet" data-remove>Remove</button></li>`;
}
}
/**
/**