openvibers/openvibe.exchange Commits
The bar: six places, the rest under More; an even grid of doors on the home (#109)
Changes
@@@ -77,7 +77,9 @@.ex-sec-body > .ovk-list > .ovk-row:first-child, .ex-sec-body > .ex-mlist > .ex-mrow:first-child { border-top: 0; }.ex-lines { display: grid; grid-template-columns: minmax(0, 1fr); gap: 10px; margin: 0 0 var(--ex-gap); }--@media (min-width: 860px) { .ex-lines { grid-template-columns: repeat(3, minmax(0, 1fr)); } }++/* Eight doors: two columns, then four from 1120 px, so no card is left alone on a row (11-07). */++@media (min-width: 720px) { .ex-lines { grid-template-columns: repeat(2, minmax(0, 1fr)); } }++@media (min-width: 1120px) { .ex-lines { grid-template-columns: repeat(4, minmax(0, 1fr)); } }.ex-line {display: flex; align-items: center; gap: 12px;padding: 14px; border: 1px solid var(--ovk-line); border-radius: 16px; background: var(--ovk-panel);@@@ -18,21 +18,22 @@ const NETWORK_URL = 'https://openvibe.network';const SITE_NAME = 'OpenVibe.Exchange';const TAGLINE = 'Every AI model, one account.';const PUBLIC_DIR = path.join(__dirname, '..', '..', 'public');++// Six places in the bar, the rest under More (11-07: fourteen links wrapped into two lines and cut the name to++// "OpenVibe..."). Icons are the navbar's own glyphs; the brand is the way home.const NAV = [-- { label: 'Home', href: '/' },-- { label: 'Models', href: '/models' },-- { label: 'Chat', href: '/chat' },-- { label: 'Arena', href: '/arena' },-- { label: 'Rankings', href: '/rankings' },-- { label: 'API', href: '/api' },-- { label: 'Start', href: '/start' },-- { label: 'Why this model', href: '/why' },-- { label: 'Sell compute', href: '/sell' },-- { label: 'Your endpoint', href: '/own' },-- { label: 'Uncensored', href: '/uncensored' },-- { label: 'AI in OpenVibe', href: '/ai' },-- { label: 'Questions', href: '/faq' },-- { label: 'Status', href: '/status' },++ { label: 'Models', href: '/models', icon: 'fa-microchip' },++ { label: 'Chat', href: '/chat', icon: 'fa-comments' },++ { label: 'Arena', href: '/arena', icon: 'fa-hand-fist' },++ { label: 'Rankings', href: '/rankings', icon: 'fa-chart-line' },++ { label: 'Start building', href: '/start', icon: 'fa-bolt' },++ { label: 'API', href: '/api', icon: 'fa-code' },++ { label: 'Why this model', href: '/why', icon: 'fa-diagram-project', more: true },++ { label: 'Your own endpoint', href: '/own', icon: 'fa-server', more: true },++ { label: 'Sell compute', href: '/sell', icon: 'fa-microchip', more: true },++ { label: 'Uncensored (18+)', href: '/uncensored', icon: 'fa-lock', more: true },++ { label: 'AI in OpenVibe', href: '/ai', icon: 'fa-circle-nodes', more: true },++ { label: 'Questions', href: '/faq', icon: 'fa-circle-info', more: true },++ { label: 'Status', href: '/status', icon: 'fa-gauge', more: true },];const hashes = new Map();@@@ -62,7 +63,7 @@ function renderPage(o) {const nav = {service: 'exchange',apiBase: NETWORK_URL,-- links: NAV.map((l) => ({ label: l.label, href: l.href, active: active(pathNow, l.href) })),++ links: NAV.map((l) => ({ label: l.label, href: l.href, icon: l.icon, ...(l.more ? { more: true } : {}), active: active(pathNow, l.href) })),history: { type: 'page', title: o.title || SITE_NAME },silentLogin: `${o.config.baseUrl}/auth/login?silent=1&next={url}`,sessionUrl: '/auth/me',@@@ -204,6 +204,7 @@ ${lineCard({ href: '/why', name: 'Why this model', text: 'Every answer carries i${lineCard({ href: '/start', name: 'Start building', text: 'One key, one endpoint and the SDK you already use; four steps from a key to a receipt.', mark: mark('Qwen') })}${lineCard({ href: '/sell', name: 'Run on people’s machines', text: 'Community GPUs through OpenVibe Node, labelled on every answer.', mark: mark('Qwen') })}${lineCard({ href: '/api', name: 'Bring your own key, 0% fee', text: 'Your provider key pays the provider directly; the router never adds a fee on it.', mark: mark('GLM') })}++${lineCard({ href: '/own', name: 'Your own endpoint', text: 'A private GPU for one model or your own weights: only you reach it, and it scales to zero when idle.', mark: mark('Llama') })}</div>${panel({title: 'Use it from code', note: 'One endpoint, compatible with OpenAI and with Anthropic.', id: 'code', body: `nnew file mode 100644@@@ -0,0 +1,17 @@++'use strict';++// The bar (11-07): fourteen links wrapped into two lines and cut the site's name to "OpenVibe…". Now at most six++// places sit in the bar, each with an icon; everything else is under More, and every link is a page this site serves.++const assert = require('assert');++const { NAV } = require('../server/render/layout');++const { PAGES } = (() => { try { return require('../server/http/discovery'); } catch { return {}; } })();++++const inBar = NAV.filter((l) => !l.more);++assert.ok(inBar.length <= 6, `at most six places in the bar, not ${inBar.length}`);++assert.ok(NAV.every((l) => /^fa-[a-z0-9-]+$/.test(l.icon || '')), 'every place has an icon');++assert.ok(!NAV.some((l) => l.href === '/'), 'the brand is the way home');++assert.deepStrictEqual(new Set(NAV.map((l) => l.href)).size, NAV.length, 'no place twice');++for (const want of ['/models', '/chat', '/arena', '/start', '/api', '/why', '/own', '/sell', '/uncensored', '/ai', '/faq', '/status']) {++ assert.ok(NAV.some((l) => l.href === want), `${want} is reachable from the bar or More`);++}++if (Array.isArray(PAGES)) for (const l of NAV) assert.ok(PAGES.some((p) => p.path === l.href) || ['/chat', '/status', '/arena', '/rankings'].includes(l.href), `${l.href} is a page`);++console.log('nav: six places in the bar with icons, the rest under More, every page reachable');
@@ -77,7 +77,9 @@
@@ -77,7 +77,9 @@
.ex-sec-body > .ovk-list > .ovk-row:first-child, .ex-sec-body > .ex-mlist > .ex-mrow:first-child { border-top: 0; }
.ex-sec-body > .ovk-list > .ovk-row:first-child, .ex-sec-body > .ex-mlist > .ex-mrow:first-child { border-top: 0; }
.ex-lines { display: grid; grid-template-columns: minmax(0, 1fr); gap: 10px; margin: 0 0 var(--ex-gap); }
.ex-lines { display: grid; grid-template-columns: minmax(0, 1fr); gap: 10px; margin: 0 0 var(--ex-gap); }
-@media (min-width: 860px) { .ex-lines { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
+/* Eight doors: two columns, then four from 1120 px, so no card is left alone on a row (11-07). */
+@media (min-width: 720px) { .ex-lines { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
+@media (min-width: 1120px) { .ex-lines { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
.ex-line {
.ex-line {
display: flex; align-items: center; gap: 12px;
display: flex; align-items: center; gap: 12px;
padding: 14px; border: 1px solid var(--ovk-line); border-radius: 16px; background: var(--ovk-panel);
padding: 14px; border: 1px solid var(--ovk-line); border-radius: 16px; background: var(--ovk-panel);
@@ -18,21 +18,22 @@ const NETWORK_URL = 'https://openvibe.network';
@@ -18,21 +18,22 @@ const NETWORK_URL = 'https://openvibe.network';
const SITE_NAME = 'OpenVibe.Exchange';
const SITE_NAME = 'OpenVibe.Exchange';
const TAGLINE = 'Every AI model, one account.';
const TAGLINE = 'Every AI model, one account.';
const PUBLIC_DIR = path.join(__dirname, '..', '..', 'public');
const PUBLIC_DIR = path.join(__dirname, '..', '..', 'public');
+// Six places in the bar, the rest under More (11-07: fourteen links wrapped into two lines and cut the name to
+// "OpenVibe..."). Icons are the navbar's own glyphs; the brand is the way home.
const NAV = [
const NAV = [
- { label: 'Home', href: '/' },
- { label: 'Models', href: '/models' },
- { label: 'Chat', href: '/chat' },
- { label: 'Arena', href: '/arena' },
- { label: 'Rankings', href: '/rankings' },
- { label: 'API', href: '/api' },
- { label: 'Start', href: '/start' },
- { label: 'Why this model', href: '/why' },
- { label: 'Sell compute', href: '/sell' },
- { label: 'Your endpoint', href: '/own' },
- { label: 'Uncensored', href: '/uncensored' },
- { label: 'AI in OpenVibe', href: '/ai' },
- { label: 'Questions', href: '/faq' },
- { label: 'Status', href: '/status' },
+ { label: 'Models', href: '/models', icon: 'fa-microchip' },
+ { label: 'Chat', href: '/chat', icon: 'fa-comments' },
+ { label: 'Arena', href: '/arena', icon: 'fa-hand-fist' },
+ { label: 'Rankings', href: '/rankings', icon: 'fa-chart-line' },
+ { label: 'Start building', href: '/start', icon: 'fa-bolt' },
+ { label: 'API', href: '/api', icon: 'fa-code' },
+ { label: 'Why this model', href: '/why', icon: 'fa-diagram-project', more: true },
+ { label: 'Your own endpoint', href: '/own', icon: 'fa-server', more: true },
+ { label: 'Sell compute', href: '/sell', icon: 'fa-microchip', more: true },
+ { label: 'Uncensored (18+)', href: '/uncensored', icon: 'fa-lock', more: true },
+ { label: 'AI in OpenVibe', href: '/ai', icon: 'fa-circle-nodes', more: true },
+ { label: 'Questions', href: '/faq', icon: 'fa-circle-info', more: true },
+ { label: 'Status', href: '/status', icon: 'fa-gauge', more: true },
];
];
const hashes = new Map();
const hashes = new Map();
@@ -62,7 +63,7 @@ function renderPage(o) {
@@ -62,7 +63,7 @@ function renderPage(o) {
const nav = {
const nav = {
service: 'exchange',
service: 'exchange',
apiBase: NETWORK_URL,
apiBase: NETWORK_URL,
- links: NAV.map((l) => ({ label: l.label, href: l.href, active: active(pathNow, l.href) })),
+ links: NAV.map((l) => ({ label: l.label, href: l.href, icon: l.icon, ...(l.more ? { more: true } : {}), active: active(pathNow, l.href) })),
history: { type: 'page', title: o.title || SITE_NAME },
history: { type: 'page', title: o.title || SITE_NAME },
silentLogin: `${o.config.baseUrl}/auth/login?silent=1&next={url}`,
silentLogin: `${o.config.baseUrl}/auth/login?silent=1&next={url}`,
sessionUrl: '/auth/me',
sessionUrl: '/auth/me',
@@ -204,6 +204,7 @@ ${lineCard({ href: '/why', name: 'Why this model', text: 'Every answer carries i
@@ -204,6 +204,7 @@ ${lineCard({ href: '/why', name: 'Why this model', text: 'Every answer carries i
${lineCard({ href: '/start', name: 'Start building', text: 'One key, one endpoint and the SDK you already use; four steps from a key to a receipt.', mark: mark('Qwen') })}
${lineCard({ href: '/start', name: 'Start building', text: 'One key, one endpoint and the SDK you already use; four steps from a key to a receipt.', mark: mark('Qwen') })}
${lineCard({ href: '/sell', name: 'Run on people’s machines', text: 'Community GPUs through OpenVibe Node, labelled on every answer.', mark: mark('Qwen') })}
${lineCard({ href: '/sell', name: 'Run on people’s machines', text: 'Community GPUs through OpenVibe Node, labelled on every answer.', mark: mark('Qwen') })}
${lineCard({ href: '/api', name: 'Bring your own key, 0% fee', text: 'Your provider key pays the provider directly; the router never adds a fee on it.', mark: mark('GLM') })}
${lineCard({ href: '/api', name: 'Bring your own key, 0% fee', text: 'Your provider key pays the provider directly; the router never adds a fee on it.', mark: mark('GLM') })}
+${lineCard({ href: '/own', name: 'Your own endpoint', text: 'A private GPU for one model or your own weights: only you reach it, and it scales to zero when idle.', mark: mark('Llama') })}
</div>
</div>
${panel({
${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: `
new file mode 100644
new file mode 100644
@@ -0,0 +1,17 @@
@@ -0,0 +1,17 @@
+'use strict';
+// The bar (11-07): fourteen links wrapped into two lines and cut the site's name to "OpenVibe…". Now at most six
+// places sit in the bar, each with an icon; everything else is under More, and every link is a page this site serves.
+const assert = require('assert');
+const { NAV } = require('../server/render/layout');
+const { PAGES } = (() => { try { return require('../server/http/discovery'); } catch { return {}; } })();
+
+const inBar = NAV.filter((l) => !l.more);
+assert.ok(inBar.length <= 6, `at most six places in the bar, not ${inBar.length}`);
+assert.ok(NAV.every((l) => /^fa-[a-z0-9-]+$/.test(l.icon || '')), 'every place has an icon');
+assert.ok(!NAV.some((l) => l.href === '/'), 'the brand is the way home');
+assert.deepStrictEqual(new Set(NAV.map((l) => l.href)).size, NAV.length, 'no place twice');
+for (const want of ['/models', '/chat', '/arena', '/start', '/api', '/why', '/own', '/sell', '/uncensored', '/ai', '/faq', '/status']) {
+ assert.ok(NAV.some((l) => l.href === want), `${want} is reachable from the bar or More`);
+}
+if (Array.isArray(PAGES)) for (const l of NAV) assert.ok(PAGES.some((p) => p.path === l.href) || ['/chat', '/status', '/arena', '/rankings'].includes(l.href), `${l.href} is a page`);
+console.log('nav: six places in the bar with icons, the rest under More, every page reachable');