openvibers/openvibe.exchange Commits

No text arrows: drawn glyphs (Design rule 169) (#255)

OP OpenVibers committed 4 h ago · 4ad028665d · 11 files, +33 −22

Changes

ddiff --git a/node_modules b/node_modules
nnew file mode 120000
iindex 0000000..69152e6
---- /dev/null
++++ b/node_modules
@@@ -0,0 +1 @@
++/home/workstation/.cache/openvibe-deps/OpenVibe.Exchange/bc6bf3c8e9366ab2/node_modules
\\ No newline at end of file
ddiff --git a/public/css/exchange.css b/public/css/exchange.css
iindex 0cfe60b..b8ffd0a 100644
---- a/public/css/exchange.css
++++ b/public/css/exchange.css
@@@ -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
ddiff --git a/public/js/task-defaults.js b/public/js/task-defaults.js
iindex 3fd6dea..a64e794 100644
---- a/public/js/task-defaults.js
++++ b/public/js/task-defaults.js
@@@ -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);
ddiff --git a/server/http/companions.js b/server/http/companions.js
iindex 07c505c..99f0425 100644
---- a/server/http/companions.js
++++ b/server/http/companions.js
@@@ -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>';
}
ddiff --git a/server/index.js b/server/index.js
iindex 0030f85..df55281 100644
---- a/server/index.js
++++ b/server/index.js
@@@ -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(() => {});
ddiff --git a/server/views/code.js b/server/views/code.js
iindex d7b4963..da07362 100644
---- a/server/views/code.js
++++ b/server/views/code.js
@@@ -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>`;
}
ddiff --git a/server/views/companions.js b/server/views/companions.js
iindex 0270711..49c5cbe 100644
---- a/server/views/companions.js
++++ b/server/views/companions.js
@@@ -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 }) : '')
ddiff --git a/server/views/console-ai.js b/server/views/console-ai.js
iindex 7610d9e..49d0363 100644
---- a/server/views/console-ai.js
++++ b/server/views/console-ai.js
@@@ -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>`;
};
ddiff --git a/server/views/exchange.js b/server/views/exchange.js
iindex 3a603c0..b26c6fb 100644
---- a/server/views/exchange.js
++++ b/server/views/exchange.js
@@@ -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' })] })}
ddiff --git a/server/views/platform.js b/server/views/platform.js
iindex a2ddac7..b396ada 100644
---- a/server/views/platform.js
++++ b/server/views/platform.js
@@@ -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 };
ddiff --git a/server/views/task-defaults.js b/server/views/task-defaults.js
iindex 33e427e..5967d30 100644
---- a/server/views/task-defaults.js
++++ b/server/views/task-defaults.js
@@@ -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>`;
}
/**