openvibers/openvibe.design Commits
openvibe.design/prompts: every prompt the service sends, word for word (#62)
Changes
nnew file mode 100644@@@ -0,0 +1,21 @@++/* /prompts: every prompt openvibe.design sends, word for word (server/views/prompts.js). */++.pr-wrap{max-width:900px;margin:0 auto;padding:clamp(16px,3vw,36px) clamp(14px,3vw,28px) 80px;color:var(--text-primary,#e8edf3)}++.pr-hero{display:grid;gap:10px;margin-bottom:18px}++.pr-eye{margin:0;font-size:12px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--accent,#4f7cff)}++.pr-hero h1{margin:0;font-family:'OV Brand',Nunito,Inter,sans-serif;font-weight:900;font-size:clamp(26px,4vw,40px);line-height:1.08;text-wrap:balance}++.pr-lede{margin:0;max-width:68ch;line-height:1.55;color:var(--text-secondary,#b4bfcc)}++.pr-lede code,.pr-card code{font-size:.9em;padding:1px 5px;border-radius:5px;background:var(--bg-tertiary,#141c2e)}++.pr-toc{display:flex;flex-wrap:wrap;gap:6px}++.pr-toc a{padding:6px 12px;border-radius:999px;border:1px solid var(--border,#1f2d47);color:inherit;text-decoration:none;font-size:13.5px;font-weight:650}++.pr-toc a:hover{border-color:var(--accent,#4f7cff)}++.pr-card{margin-top:14px;padding:14px 16px;border-radius:16px;border:1px solid var(--border,#1f2d47);background:var(--bg-secondary,#101828);scroll-margin-top:80px}++.pr-card h2{margin:0;font-family:'OV Brand',Nunito,Inter,sans-serif;font-weight:900;font-size:20px}++.pr-used{margin:4px 0 10px;font-size:14px;color:var(--text-secondary,#b4bfcc)}++.pr-used a{color:var(--accent,#4f7cff);font-size:12.5px}++.pr-part{margin-top:10px}++.pr-part-h{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:4px;font-size:12px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--text-muted,#7386a3)}++.pr-copy{padding:4px 12px;min-height:30px;border-radius:999px;border:1px solid var(--border,#1f2d47);background:transparent;color:inherit;font:inherit;font-size:12px;font-weight:700;letter-spacing:0;text-transform:none;cursor:pointer}++.pr-copy:hover{border-color:var(--accent,#4f7cff)}++.pr-part pre{margin:0;max-height:420px;overflow:auto;overscroll-behavior:auto;padding:10px 12px;border-radius:10px;background:var(--bg-tertiary,#0b111c);font:12.5px/1.55 ui-monospace,SFMono-Regular,Menlo,monospace;white-space:pre-wrap;overflow-wrap:anywhere}++.pr-fine{margin-top:22px;font-size:14px;color:var(--text-muted,#7386a3)}++.pr-fine a{color:var(--accent,#4f7cff)}nnew file mode 100644@@@ -0,0 +1,13 @@++/* /prompts: Copy puts a prompt on the clipboard (selected instead where the clipboard is refused). */++(function () {++ 'use strict';++ document.addEventListener('click', function (e) {++ var b = e.target.closest('[data-pr-copy]');++ if (!b) return;++ var pre = document.getElementById(b.getAttribute('data-pr-copy'));++ if (!pre) return;++ var done = function () { var was = b.textContent; b.textContent = 'Copied'; setTimeout(function () { b.textContent = was; }, 1300); };++ var select = function () { var r = document.createRange(); r.selectNodeContents(pre); var s = window.getSelection(); s.removeAllRanges(); s.addRange(r); };++ if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(pre.textContent).then(done, select); else select();++ });++})();@@@ -174,6 +174,7 @@ async function createApp(opts = {}) {app.use('/api', express.json({ limit: '64kb' }));app.use('/api/v1/arena', createArenaApi(ctx));app.use('/api/v1/brand', require('./http/brand-api').createBrandApi(ctx));++ app.get('/api/v1/prompts', (req, res) => { res.set('Cache-Control', 'public, max-age=300'); res.json({ prompts: require('./views/prompts').list() }); });app.use(createIconRoutes(ctx));app.use('/api', createApi(ctx));app.use('/api', (req, res) => contracts.http.sendProblem(res, 404, 'route.not_found', {@@@ -32,6 +32,18 @@ const CATEGORY_BRIEF = {slides: 'A short slide deck: a title slide and two or three content slides with a clear visual hierarchy.',};++/** The system prompt every design generation sends (published at openvibe.design/prompts). */++function systemPrompt(category) {++ return [++ 'You are a senior product designer and front-end engineer competing in the OpenVibe Design Arena.',++ 'Build the page the person describes as one complete, self-contained HTML document.',++ `Category: ${category}. ${CATEGORY_BRIEF[category] || ''}`,++ 'Rules: return only the HTML document, starting with <!doctype html>. Use inline CSS in a <style> tag or style attributes.',++ 'No JavaScript, no event-handler attributes, no external stylesheets, fonts, scripts or images. Images may be inline SVG or data: URIs.',++ 'Make it responsive enough to be judged at 390 px and 1280 px wide. Use real, plausible content, not lorem ipsum. No explanations outside the HTML.',++ ].join('\n');++}++function createArenaAi({ config, fetchImpl = globalThis.fetch, log = console }) {const ai = config.ai;const tokenClient = config.oauth.clientSecret@@@ -216,16 +228,6 @@ function createArenaAi({ config, fetchImpl = globalThis.fetch, log = console })claude: prefer ? Boolean(match) : null, prefer, pin: false };}-- function systemPrompt(category) {-- return [-- 'You are a senior product designer and front-end engineer competing in the OpenVibe Design Arena.',-- 'Build the page the person describes as one complete, self-contained HTML document.',-- `Category: ${category}. ${CATEGORY_BRIEF[category] || ''}`,-- 'Rules: return only the HTML document, starting with <!doctype html>. Use inline CSS in a <style> tag or style attributes.',-- 'No JavaScript, no event-handler attributes, no external stylesheets, fonts, scripts or images. Images may be inline SVG or data: URIs.',-- 'Make it responsive enough to be judged at 390 px and 1280 px wide. Use real, plausible content, not lorem ipsum. No explanations outside the HTML.',-- ].join('\n');-- }function textOf(output) {if (typeof output === 'string') return output;@@@ -397,4 +399,4 @@ function createArenaAi({ config, fetchImpl = globalThis.fetch, log = console })return { enabled, slots, slotPlan, designPlan, offeredModels, rankOffers, generate, systemPrompt, text, CATEGORY_BRIEF };}--module.exports = { createArenaAi, ArenaAiError, CATEGORY_BRIEF };++module.exports = { createArenaAi, ArenaAiError, CATEGORY_BRIEF, systemPrompt };@@@ -75,6 +75,23 @@ function wrap(html, variables) {return `<!doctype html>${clean.replace(/^\s*<!doctype\s+html[^>]*>/i, '')}`;}++++// ── The prompts this service sends (published word for word at openvibe.design/prompts) ──++/** A design drawn in the person's theme: their words, then the theme's colour variables. */++function themePrompt(prompt, variableNames) {++ return `${prompt}\nUse ONLY these theme CSS custom properties for all colors: ${variableNames.join(', ')}. No external requests. Mobile first. Return one complete HTML document.`;++}++/** An edit of a drawn design: the person's instruction, the current document, and the theme's colour variables. */++function editPrompt(instruction, html, names) {++ return [++ 'Edit one existing generated design. Return one complete HTML document and nothing else.',++ `The person's instruction: ${instruction}`,++ 'The current document is between the markers. Keep its theme, its content and its structure unless the instruction asks otherwise.',++ `<<<CURRENT_DESIGN\n${html}\nCURRENT_DESIGN>>>`,++ `Use ONLY these theme CSS custom properties for every colour: ${names || '(the document already uses them)'}. Do not add scripts, event-handler attributes, links, web fonts, remote images or any external request. Mobile first.`,++ ].join('\n\n');++}++function createDesigns({ s, ai, bench = null, catalog, config = {}, log = console, now = () => Date.now() }) {const db = s.db;const chatEditLimit = Math.max(1, Number(config.studio && config.studio.chatEditsPerDesign) || CHAT_EDITS_DEFAULT);@@@ -135,7 +152,7 @@ function createDesigns({ s, ai, bench = null, catalog, config = {}, log = consolawait updateVariant(id, index, running);try {const t = await theme(d.theme_id);-- const prompt = `${d.prompt}\nUse ONLY these theme CSS custom properties for all colors: ${Object.keys(t.variables || {}).join(', ')}. No external requests. Mobile first. Return one complete HTML document.`;++ const prompt = themePrompt(d.prompt, Object.keys(t.variables || {}));const out = await ai.generate({ prompt, category: d.kind === 'component' ? 'logo' : d.kind === 'screen' ? 'mobile_app' : 'landing',slot: { provider: v.provider, model: v.model }, roundId: id, index,prefer: v.prefer || null, person: d.owner || null, pin: v.pin === true });@@@ -351,13 +368,7 @@ function createDesigns({ s, ai, bench = null, catalog, config = {}, log = consolconst current = await versionRow(id, p.current_seq);const variables = selected.variables || {};const names = Object.keys(variables).join(', ');-- const prompt = [-- 'Edit one existing generated design. Return one complete HTML document and nothing else.',-- `The person's instruction: ${instruction}`,-- 'The current document is between the markers. Keep its theme, its content and its structure unless the instruction asks otherwise.',-- `<<<CURRENT_DESIGN\n${String(current.html || variant.html || '').slice(0, 80000)}\nCURRENT_DESIGN>>>`,-- `Use ONLY these theme CSS custom properties for every colour: ${names || '(the document already uses them)'}. Do not add scripts, event-handler attributes, links, web fonts, remote images or any external request. Mobile first.`,-- ].join('\n\n');++ const prompt = editPrompt(instruction, String(current.html || variant.html || '').slice(0, 80000), names);// An edit stays on the model that drew the frame, as a soft preference (the person's own key first), never a pin.const asked = variant.prefer && variant.prefer.provider ? variant.prefer : null;const prefer = asked && asked.provider && !String(asked.provider).startsWith('byo:self:')@@@ -461,4 +472,4 @@ function createDesigns({ s, ai, bench = null, catalog, config = {}, log = consolreturn { create, read, setTheme, recent, resume, theme, pick, chatEdit, clickEdit, versions, undo, redo, chatEditLimit };}--module.exports = { createDesigns, wrap, tokenCss, themeCss };++module.exports = { createDesigns, wrap, tokenCss, themeCss, themePrompt, editPrompt };@@@ -69,6 +69,8 @@ function createPageRoutes(ctx) {// ── The OpenVibe mark, and its files ──r.get('/brand', (req, res) => page(res, 200, brandView.brandPage({ base: layoutBase() }), req.viewer));// A hundred takes on the Loop and the Pair, for feedback (the owner, 2026-11-01). Before /brand/:file.++ // Every prompt the service sends, word for word (the owner, 2026-11-01).++ r.get('/prompts', (req, res) => page(res, 200, require('../views/prompts').promptsPage(), req.viewer));r.get('/brand/explore', async (req, res) => {// What people picked so far, at the top (a database hiccup leaves the page whole, without it).const tally = ctx.picks ? await safe(() => ctx.picks.tally()) : null;@@@ -34,6 +34,7 @@ const NAV = [{ label: 'Patterns', href: '/patterns', icon: 'fa-shapes', more: true },{ label: 'Components', href: '/components', icon: 'fa-puzzle-piece', more: true },{ label: 'Developers', href: '/developers', icon: 'fa-code', more: true },++ { label: 'Prompts', href: '/prompts', icon: 'fa-quote-left', more: true },];let RELEASE = 'dev';nnew file mode 100644@@@ -0,0 +1,82 @@++'use strict';++/**++ * openvibe.design/prompts: every instruction openvibe.design sends to an AI model, word for word (the owner,++ * 2026-11-01: "publishing the prompts… public facing"). Each one is built here by the same function the service calls,++ * with the person's own words shown as {placeholders}, so the page cannot drift from what runs. GET /api/v1/prompts++ * answers the same list as JSON.++ */++const { layout, esc } = require('../render/layout');++const { systemPrompt, CATEGORY_BRIEF } = require('../arena/ai');++const { themePrompt, editPrompt } = require('../designs');++const { SYSTEM: GLYPH_SYSTEM } = require('../glyphs/drafts');++++const SOURCE = 'https://github.com/OpenVibers/OpenVibe.Design/blob/main/';++++function list() {++ return [++ {++ id: 'design-page',++ title: 'Designing a page',++ used: 'Create, the Studio and the Arena: every page, screen, component or logo a model draws.',++ source: 'server/arena/ai.js',++ parts: [++ { label: 'System prompt (the category changes per request)', text: systemPrompt('{category}') },++ { label: 'The request', text: '{your prompt}\n\nCategory: {category}\nReturn one complete HTML document.' },++ { label: 'The categories', text: Object.entries(CATEGORY_BRIEF).map(([k, v]) => `${k}: ${v}`).join('\n') },++ ],++ },++ {++ id: 'design-theme',++ title: 'Drawing it in your theme',++ used: 'Create and the Studio, when a design is drawn in the theme you picked.',++ source: 'server/designs.js',++ parts: [{ label: 'The request', text: themePrompt('{your prompt}', ['{the theme\'s colour variables}']) }],++ },++ {++ id: 'design-edit',++ title: 'Editing a design',++ used: 'The Studio, when you ask for a change in words.',++ source: 'server/designs.js',++ parts: [{ label: 'The request', text: editPrompt('{your instruction}', '{the current page}', '{the theme\'s colour variables}') }],++ },++ {++ id: 'glyph-draft',++ title: 'Drawing an icon',++ used: 'Icons: drafts for the OpenVibe glyph set. A person reviews every draft before it joins the set.',++ source: 'server/glyphs/drafts.js',++ parts: [++ { label: 'System prompt (with drawings from the set as examples)', text: GLYPH_SYSTEM },++ { label: 'The request', text: 'The icon: "{name}". What it stands for: {what it is for}' },++ ],++ },++ ];++}++++function promptsPage() {++ const items = list();++ const body = `<div class="pr-wrap">++<header class="pr-hero">++<p class="pr-eye">Open by default</p>++<h1>The prompts behind openvibe.design</h1>++<p class="pr-lede">Every instruction openvibe.design sends to an AI model, word for word. They come from the same code that sends them, so this page always shows what runs. Your own words go where a prompt says <code>{…}</code>. Copy them, improve them, use them anywhere.</p>++<nav class="pr-toc" aria-label="The prompts">${items.map((p) => `<a href="#${p.id}">${esc(p.title)}</a>`).join('')}</nav>++</header>++${items.map((p) => `<section class="pr-card" id="${p.id}">++<h2>${esc(p.title)}</h2>++<p class="pr-used">${esc(p.used)} <a href="${SOURCE}${p.source}">${esc(p.source)}</a></p>++${p.parts.map((part, i) => `<div class="pr-part"><div class="pr-part-h"><span>${esc(part.label)}</span><button type="button" class="pr-copy" data-pr-copy="${p.id}-${i}">Copy</button></div><pre id="${p.id}-${i}">${esc(part.text)}</pre></div>`).join('')}++</section>`).join('')}++<p class="pr-fine">The same list as JSON: <a href="/api/v1/prompts">/api/v1/prompts</a>. openvibe.design is open source (MIT).</p>++</div>`;++ return layout({++ title: 'The prompts behind openvibe.design',++ description: 'Every instruction openvibe.design sends to an AI model, word for word, from the code that sends it.',++ path: '/prompts',++ active: '/prompts',++ styles: ['css/prompts.css'],++ scripts: ['js/prompts.js'],++ body,++ });++}++++module.exports = { promptsPage, list };nnew file mode 100644@@@ -0,0 +1,35 @@++'use strict';++/**++ * openvibe.design/prompts (the owner, 2026-11-01: "publishing the prompts… public facing"): every prompt the service++ * sends, word for word, built by the same functions that send them, so the page cannot drift; the same list as JSON.++ */++const assert = require('assert');++const { boot, check, done } = require('./helpers/boot');++const { systemPrompt } = require('../server/arena/ai');++const { themePrompt, editPrompt } = require('../server/designs');++const { SYSTEM } = require('../server/glyphs/drafts');++++const unescape = (s) => s.replace(/'/g, "'").replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&');++++(async () => {++ const t = await boot();++ try {++ await check('the page shows each prompt as the code builds it', async () => {++ const r = await t.get('/prompts');++ assert.strictEqual(r.status, 200);++ const html = unescape(r.text);++ assert.ok(html.includes(systemPrompt('{category}')), 'the page-design system prompt');++ assert.ok(html.includes(themePrompt('{your prompt}', ["{the theme's colour variables}"])), 'the theme prompt');++ assert.ok(html.includes(editPrompt('{your instruction}', '{the current page}', "{the theme's colour variables}")), 'the edit prompt');++ assert.ok(html.includes(SYSTEM.slice(0, 200)), 'the icon prompt');++ assert.match(r.text, /href="\/prompts"/, 'linked from the navigation');++ });++ await check('the same list as JSON, cacheable', async () => {++ const r = await t.get('/api/v1/prompts');++ assert.strictEqual(r.status, 200);++ assert.deepStrictEqual(r.json().prompts.map((p) => p.id), ['design-page', 'design-theme', 'design-edit', 'glyph-draft']);++ assert.match(r.headers.get('cache-control') || '', /public/);++ });++ } finally { await t.close(); }++ done();++})().catch((e) => { console.error(e); process.exit(1); });
new file mode 100644
new file mode 100644
@@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
+/* /prompts: every prompt openvibe.design sends, word for word (server/views/prompts.js). */
+.pr-wrap{max-width:900px;margin:0 auto;padding:clamp(16px,3vw,36px) clamp(14px,3vw,28px) 80px;color:var(--text-primary,#e8edf3)}
+.pr-hero{display:grid;gap:10px;margin-bottom:18px}
+.pr-eye{margin:0;font-size:12px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--accent,#4f7cff)}
+.pr-hero h1{margin:0;font-family:'OV Brand',Nunito,Inter,sans-serif;font-weight:900;font-size:clamp(26px,4vw,40px);line-height:1.08;text-wrap:balance}
+.pr-lede{margin:0;max-width:68ch;line-height:1.55;color:var(--text-secondary,#b4bfcc)}
+.pr-lede code,.pr-card code{font-size:.9em;padding:1px 5px;border-radius:5px;background:var(--bg-tertiary,#141c2e)}
+.pr-toc{display:flex;flex-wrap:wrap;gap:6px}
+.pr-toc a{padding:6px 12px;border-radius:999px;border:1px solid var(--border,#1f2d47);color:inherit;text-decoration:none;font-size:13.5px;font-weight:650}
+.pr-toc a:hover{border-color:var(--accent,#4f7cff)}
+.pr-card{margin-top:14px;padding:14px 16px;border-radius:16px;border:1px solid var(--border,#1f2d47);background:var(--bg-secondary,#101828);scroll-margin-top:80px}
+.pr-card h2{margin:0;font-family:'OV Brand',Nunito,Inter,sans-serif;font-weight:900;font-size:20px}
+.pr-used{margin:4px 0 10px;font-size:14px;color:var(--text-secondary,#b4bfcc)}
+.pr-used a{color:var(--accent,#4f7cff);font-size:12.5px}
+.pr-part{margin-top:10px}
+.pr-part-h{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:4px;font-size:12px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--text-muted,#7386a3)}
+.pr-copy{padding:4px 12px;min-height:30px;border-radius:999px;border:1px solid var(--border,#1f2d47);background:transparent;color:inherit;font:inherit;font-size:12px;font-weight:700;letter-spacing:0;text-transform:none;cursor:pointer}
+.pr-copy:hover{border-color:var(--accent,#4f7cff)}
+.pr-part pre{margin:0;max-height:420px;overflow:auto;overscroll-behavior:auto;padding:10px 12px;border-radius:10px;background:var(--bg-tertiary,#0b111c);font:12.5px/1.55 ui-monospace,SFMono-Regular,Menlo,monospace;white-space:pre-wrap;overflow-wrap:anywhere}
+.pr-fine{margin-top:22px;font-size:14px;color:var(--text-muted,#7386a3)}
+.pr-fine a{color:var(--accent,#4f7cff)}
new file mode 100644
new file mode 100644
@@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
+/* /prompts: Copy puts a prompt on the clipboard (selected instead where the clipboard is refused). */
+(function () {
+ 'use strict';
+ document.addEventListener('click', function (e) {
+ var b = e.target.closest('[data-pr-copy]');
+ if (!b) return;
+ var pre = document.getElementById(b.getAttribute('data-pr-copy'));
+ if (!pre) return;
+ var done = function () { var was = b.textContent; b.textContent = 'Copied'; setTimeout(function () { b.textContent = was; }, 1300); };
+ var select = function () { var r = document.createRange(); r.selectNodeContents(pre); var s = window.getSelection(); s.removeAllRanges(); s.addRange(r); };
+ if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(pre.textContent).then(done, select); else select();
+ });
+})();
@@ -174,6 +174,7 @@ async function createApp(opts = {}) {
@@ -174,6 +174,7 @@ async function createApp(opts = {}) {
app.use('/api', express.json({ limit: '64kb' }));
app.use('/api', express.json({ limit: '64kb' }));
app.use('/api/v1/arena', createArenaApi(ctx));
app.use('/api/v1/arena', createArenaApi(ctx));
app.use('/api/v1/brand', require('./http/brand-api').createBrandApi(ctx));
app.use('/api/v1/brand', require('./http/brand-api').createBrandApi(ctx));
+ app.get('/api/v1/prompts', (req, res) => { res.set('Cache-Control', 'public, max-age=300'); res.json({ prompts: require('./views/prompts').list() }); });
app.use(createIconRoutes(ctx));
app.use(createIconRoutes(ctx));
app.use('/api', createApi(ctx));
app.use('/api', createApi(ctx));
app.use('/api', (req, res) => contracts.http.sendProblem(res, 404, 'route.not_found', {
app.use('/api', (req, res) => contracts.http.sendProblem(res, 404, 'route.not_found', {
@@ -32,6 +32,18 @@ const CATEGORY_BRIEF = {
@@ -32,6 +32,18 @@ const CATEGORY_BRIEF = {
slides: 'A short slide deck: a title slide and two or three content slides with a clear visual hierarchy.',
slides: 'A short slide deck: a title slide and two or three content slides with a clear visual hierarchy.',
};
};
+/** The system prompt every design generation sends (published at openvibe.design/prompts). */
+function systemPrompt(category) {
+ return [
+ 'You are a senior product designer and front-end engineer competing in the OpenVibe Design Arena.',
+ 'Build the page the person describes as one complete, self-contained HTML document.',
+ `Category: ${category}. ${CATEGORY_BRIEF[category] || ''}`,
+ 'Rules: return only the HTML document, starting with <!doctype html>. Use inline CSS in a <style> tag or style attributes.',
+ 'No JavaScript, no event-handler attributes, no external stylesheets, fonts, scripts or images. Images may be inline SVG or data: URIs.',
+ 'Make it responsive enough to be judged at 390 px and 1280 px wide. Use real, plausible content, not lorem ipsum. No explanations outside the HTML.',
+ ].join('\n');
+}
+
function createArenaAi({ config, fetchImpl = globalThis.fetch, log = console }) {
function createArenaAi({ config, fetchImpl = globalThis.fetch, log = console }) {
const ai = config.ai;
const ai = config.ai;
const tokenClient = config.oauth.clientSecret
const tokenClient = config.oauth.clientSecret
@@ -216,16 +228,6 @@ function createArenaAi({ config, fetchImpl = globalThis.fetch, log = console })
@@ -216,16 +228,6 @@ function createArenaAi({ config, fetchImpl = globalThis.fetch, log = console })
claude: prefer ? Boolean(match) : null, prefer, pin: false };
claude: prefer ? Boolean(match) : null, prefer, pin: false };
}
}
- function systemPrompt(category) {
- return [
- 'You are a senior product designer and front-end engineer competing in the OpenVibe Design Arena.',
- 'Build the page the person describes as one complete, self-contained HTML document.',
- `Category: ${category}. ${CATEGORY_BRIEF[category] || ''}`,
- 'Rules: return only the HTML document, starting with <!doctype html>. Use inline CSS in a <style> tag or style attributes.',
- 'No JavaScript, no event-handler attributes, no external stylesheets, fonts, scripts or images. Images may be inline SVG or data: URIs.',
- 'Make it responsive enough to be judged at 390 px and 1280 px wide. Use real, plausible content, not lorem ipsum. No explanations outside the HTML.',
- ].join('\n');
- }
function textOf(output) {
function textOf(output) {
if (typeof output === 'string') return output;
if (typeof output === 'string') return output;
@@ -397,4 +399,4 @@ function createArenaAi({ config, fetchImpl = globalThis.fetch, log = console })
@@ -397,4 +399,4 @@ function createArenaAi({ config, fetchImpl = globalThis.fetch, log = console })
return { enabled, slots, slotPlan, designPlan, offeredModels, rankOffers, generate, systemPrompt, text, CATEGORY_BRIEF };
return { enabled, slots, slotPlan, designPlan, offeredModels, rankOffers, generate, systemPrompt, text, CATEGORY_BRIEF };
}
}
-module.exports = { createArenaAi, ArenaAiError, CATEGORY_BRIEF };
+module.exports = { createArenaAi, ArenaAiError, CATEGORY_BRIEF, systemPrompt };
@@ -75,6 +75,23 @@ function wrap(html, variables) {
@@ -75,6 +75,23 @@ function wrap(html, variables) {
return `<!doctype html>${clean.replace(/^\s*<!doctype\s+html[^>]*>/i, '')}`;
return `<!doctype html>${clean.replace(/^\s*<!doctype\s+html[^>]*>/i, '')}`;
}
}
+
+// ── The prompts this service sends (published word for word at openvibe.design/prompts) ──
+/** A design drawn in the person's theme: their words, then the theme's colour variables. */
+function themePrompt(prompt, variableNames) {
+ return `${prompt}\nUse ONLY these theme CSS custom properties for all colors: ${variableNames.join(', ')}. No external requests. Mobile first. Return one complete HTML document.`;
+}
+/** An edit of a drawn design: the person's instruction, the current document, and the theme's colour variables. */
+function editPrompt(instruction, html, names) {
+ return [
+ 'Edit one existing generated design. Return one complete HTML document and nothing else.',
+ `The person's instruction: ${instruction}`,
+ 'The current document is between the markers. Keep its theme, its content and its structure unless the instruction asks otherwise.',
+ `<<<CURRENT_DESIGN\n${html}\nCURRENT_DESIGN>>>`,
+ `Use ONLY these theme CSS custom properties for every colour: ${names || '(the document already uses them)'}. Do not add scripts, event-handler attributes, links, web fonts, remote images or any external request. Mobile first.`,
+ ].join('\n\n');
+}
+
function createDesigns({ s, ai, bench = null, catalog, config = {}, log = console, now = () => Date.now() }) {
function createDesigns({ s, ai, bench = null, catalog, config = {}, log = console, now = () => Date.now() }) {
const db = s.db;
const db = s.db;
const chatEditLimit = Math.max(1, Number(config.studio && config.studio.chatEditsPerDesign) || CHAT_EDITS_DEFAULT);
const chatEditLimit = Math.max(1, Number(config.studio && config.studio.chatEditsPerDesign) || CHAT_EDITS_DEFAULT);
@@ -135,7 +152,7 @@ function createDesigns({ s, ai, bench = null, catalog, config = {}, log = consol
@@ -135,7 +152,7 @@ function createDesigns({ s, ai, bench = null, catalog, config = {}, log = consol
await updateVariant(id, index, running);
await updateVariant(id, index, running);
try {
try {
const t = await theme(d.theme_id);
const t = await theme(d.theme_id);
- const prompt = `${d.prompt}\nUse ONLY these theme CSS custom properties for all colors: ${Object.keys(t.variables || {}).join(', ')}. No external requests. Mobile first. Return one complete HTML document.`;
+ const prompt = themePrompt(d.prompt, Object.keys(t.variables || {}));
const out = await ai.generate({ prompt, category: d.kind === 'component' ? 'logo' : d.kind === 'screen' ? 'mobile_app' : 'landing',
const out = await ai.generate({ prompt, category: d.kind === 'component' ? 'logo' : d.kind === 'screen' ? 'mobile_app' : 'landing',
slot: { provider: v.provider, model: v.model }, roundId: id, index,
slot: { provider: v.provider, model: v.model }, roundId: id, index,
prefer: v.prefer || null, person: d.owner || null, pin: v.pin === true });
prefer: v.prefer || null, person: d.owner || null, pin: v.pin === true });
@@ -351,13 +368,7 @@ function createDesigns({ s, ai, bench = null, catalog, config = {}, log = consol
@@ -351,13 +368,7 @@ function createDesigns({ s, ai, bench = null, catalog, config = {}, log = consol
const current = await versionRow(id, p.current_seq);
const current = await versionRow(id, p.current_seq);
const variables = selected.variables || {};
const variables = selected.variables || {};
const names = Object.keys(variables).join(', ');
const names = Object.keys(variables).join(', ');
- const prompt = [
- 'Edit one existing generated design. Return one complete HTML document and nothing else.',
- `The person's instruction: ${instruction}`,
- 'The current document is between the markers. Keep its theme, its content and its structure unless the instruction asks otherwise.',
- `<<<CURRENT_DESIGN\n${String(current.html || variant.html || '').slice(0, 80000)}\nCURRENT_DESIGN>>>`,
- `Use ONLY these theme CSS custom properties for every colour: ${names || '(the document already uses them)'}. Do not add scripts, event-handler attributes, links, web fonts, remote images or any external request. Mobile first.`,
- ].join('\n\n');
+ const prompt = editPrompt(instruction, String(current.html || variant.html || '').slice(0, 80000), names);
// An edit stays on the model that drew the frame, as a soft preference (the person's own key first), never a pin.
// An edit stays on the model that drew the frame, as a soft preference (the person's own key first), never a pin.
const asked = variant.prefer && variant.prefer.provider ? variant.prefer : null;
const asked = variant.prefer && variant.prefer.provider ? variant.prefer : null;
const prefer = asked && asked.provider && !String(asked.provider).startsWith('byo:self:')
const prefer = asked && asked.provider && !String(asked.provider).startsWith('byo:self:')
@@ -461,4 +472,4 @@ function createDesigns({ s, ai, bench = null, catalog, config = {}, log = consol
@@ -461,4 +472,4 @@ function createDesigns({ s, ai, bench = null, catalog, config = {}, log = consol
return { create, read, setTheme, recent, resume, theme, pick, chatEdit, clickEdit, versions, undo, redo, chatEditLimit };
return { create, read, setTheme, recent, resume, theme, pick, chatEdit, clickEdit, versions, undo, redo, chatEditLimit };
}
}
-module.exports = { createDesigns, wrap, tokenCss, themeCss };
+module.exports = { createDesigns, wrap, tokenCss, themeCss, themePrompt, editPrompt };
@@ -69,6 +69,8 @@ function createPageRoutes(ctx) {
@@ -69,6 +69,8 @@ function createPageRoutes(ctx) {
// ── The OpenVibe mark, and its files ──
// ── The OpenVibe mark, and its files ──
r.get('/brand', (req, res) => page(res, 200, brandView.brandPage({ base: layoutBase() }), req.viewer));
r.get('/brand', (req, res) => page(res, 200, brandView.brandPage({ base: layoutBase() }), req.viewer));
// A hundred takes on the Loop and the Pair, for feedback (the owner, 2026-11-01). Before /brand/:file.
// A hundred takes on the Loop and the Pair, for feedback (the owner, 2026-11-01). Before /brand/:file.
+ // Every prompt the service sends, word for word (the owner, 2026-11-01).
+ r.get('/prompts', (req, res) => page(res, 200, require('../views/prompts').promptsPage(), req.viewer));
r.get('/brand/explore', async (req, res) => {
r.get('/brand/explore', async (req, res) => {
// What people picked so far, at the top (a database hiccup leaves the page whole, without it).
// What people picked so far, at the top (a database hiccup leaves the page whole, without it).
const tally = ctx.picks ? await safe(() => ctx.picks.tally()) : null;
const tally = ctx.picks ? await safe(() => ctx.picks.tally()) : null;
@@ -34,6 +34,7 @@ const NAV = [
@@ -34,6 +34,7 @@ const NAV = [
{ label: 'Patterns', href: '/patterns', icon: 'fa-shapes', more: true },
{ label: 'Patterns', href: '/patterns', icon: 'fa-shapes', more: true },
{ label: 'Components', href: '/components', icon: 'fa-puzzle-piece', more: true },
{ label: 'Components', href: '/components', icon: 'fa-puzzle-piece', more: true },
{ label: 'Developers', href: '/developers', icon: 'fa-code', more: true },
{ label: 'Developers', href: '/developers', icon: 'fa-code', more: true },
+ { label: 'Prompts', href: '/prompts', icon: 'fa-quote-left', more: true },
];
];
let RELEASE = 'dev';
let RELEASE = 'dev';
new file mode 100644
new file mode 100644
@@ -0,0 +1,82 @@
@@ -0,0 +1,82 @@
+'use strict';
+/**
+ * openvibe.design/prompts: every instruction openvibe.design sends to an AI model, word for word (the owner,
+ * 2026-11-01: "publishing the prompts… public facing"). Each one is built here by the same function the service calls,
+ * with the person's own words shown as {placeholders}, so the page cannot drift from what runs. GET /api/v1/prompts
+ * answers the same list as JSON.
+ */
+const { layout, esc } = require('../render/layout');
+const { systemPrompt, CATEGORY_BRIEF } = require('../arena/ai');
+const { themePrompt, editPrompt } = require('../designs');
+const { SYSTEM: GLYPH_SYSTEM } = require('../glyphs/drafts');
+
+const SOURCE = 'https://github.com/OpenVibers/OpenVibe.Design/blob/main/';
+
+function list() {
+ return [
+ {
+ id: 'design-page',
+ title: 'Designing a page',
+ used: 'Create, the Studio and the Arena: every page, screen, component or logo a model draws.',
+ source: 'server/arena/ai.js',
+ parts: [
+ { label: 'System prompt (the category changes per request)', text: systemPrompt('{category}') },
+ { label: 'The request', text: '{your prompt}\n\nCategory: {category}\nReturn one complete HTML document.' },
+ { label: 'The categories', text: Object.entries(CATEGORY_BRIEF).map(([k, v]) => `${k}: ${v}`).join('\n') },
+ ],
+ },
+ {
+ id: 'design-theme',
+ title: 'Drawing it in your theme',
+ used: 'Create and the Studio, when a design is drawn in the theme you picked.',
+ source: 'server/designs.js',
+ parts: [{ label: 'The request', text: themePrompt('{your prompt}', ['{the theme\'s colour variables}']) }],
+ },
+ {
+ id: 'design-edit',
+ title: 'Editing a design',
+ used: 'The Studio, when you ask for a change in words.',
+ source: 'server/designs.js',
+ parts: [{ label: 'The request', text: editPrompt('{your instruction}', '{the current page}', '{the theme\'s colour variables}') }],
+ },
+ {
+ id: 'glyph-draft',
+ title: 'Drawing an icon',
+ used: 'Icons: drafts for the OpenVibe glyph set. A person reviews every draft before it joins the set.',
+ source: 'server/glyphs/drafts.js',
+ parts: [
+ { label: 'System prompt (with drawings from the set as examples)', text: GLYPH_SYSTEM },
+ { label: 'The request', text: 'The icon: "{name}". What it stands for: {what it is for}' },
+ ],
+ },
+ ];
+}
+
+function promptsPage() {
+ const items = list();
+ const body = `<div class="pr-wrap">
+<header class="pr-hero">
+<p class="pr-eye">Open by default</p>
+<h1>The prompts behind openvibe.design</h1>
+<p class="pr-lede">Every instruction openvibe.design sends to an AI model, word for word. They come from the same code that sends them, so this page always shows what runs. Your own words go where a prompt says <code>{…}</code>. Copy them, improve them, use them anywhere.</p>
+<nav class="pr-toc" aria-label="The prompts">${items.map((p) => `<a href="#${p.id}">${esc(p.title)}</a>`).join('')}</nav>
+</header>
+${items.map((p) => `<section class="pr-card" id="${p.id}">
+<h2>${esc(p.title)}</h2>
+<p class="pr-used">${esc(p.used)} <a href="${SOURCE}${p.source}">${esc(p.source)}</a></p>
+${p.parts.map((part, i) => `<div class="pr-part"><div class="pr-part-h"><span>${esc(part.label)}</span><button type="button" class="pr-copy" data-pr-copy="${p.id}-${i}">Copy</button></div><pre id="${p.id}-${i}">${esc(part.text)}</pre></div>`).join('')}
+</section>`).join('')}
+<p class="pr-fine">The same list as JSON: <a href="/api/v1/prompts">/api/v1/prompts</a>. openvibe.design is open source (MIT).</p>
+</div>`;
+ return layout({
+ title: 'The prompts behind openvibe.design',
+ description: 'Every instruction openvibe.design sends to an AI model, word for word, from the code that sends it.',
+ path: '/prompts',
+ active: '/prompts',
+ styles: ['css/prompts.css'],
+ scripts: ['js/prompts.js'],
+ body,
+ });
+}
+
+module.exports = { promptsPage, list };
new file mode 100644
new file mode 100644
@@ -0,0 +1,35 @@
@@ -0,0 +1,35 @@
+'use strict';
+/**
+ * openvibe.design/prompts (the owner, 2026-11-01: "publishing the prompts… public facing"): every prompt the service
+ * sends, word for word, built by the same functions that send them, so the page cannot drift; the same list as JSON.
+ */
+const assert = require('assert');
+const { boot, check, done } = require('./helpers/boot');
+const { systemPrompt } = require('../server/arena/ai');
+const { themePrompt, editPrompt } = require('../server/designs');
+const { SYSTEM } = require('../server/glyphs/drafts');
+
+const unescape = (s) => s.replace(/'/g, "'").replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&');
+
+(async () => {
+ const t = await boot();
+ try {
+ await check('the page shows each prompt as the code builds it', async () => {
+ const r = await t.get('/prompts');
+ assert.strictEqual(r.status, 200);
+ const html = unescape(r.text);
+ assert.ok(html.includes(systemPrompt('{category}')), 'the page-design system prompt');
+ assert.ok(html.includes(themePrompt('{your prompt}', ["{the theme's colour variables}"])), 'the theme prompt');
+ assert.ok(html.includes(editPrompt('{your instruction}', '{the current page}', "{the theme's colour variables}")), 'the edit prompt');
+ assert.ok(html.includes(SYSTEM.slice(0, 200)), 'the icon prompt');
+ assert.match(r.text, /href="\/prompts"/, 'linked from the navigation');
+ });
+ await check('the same list as JSON, cacheable', async () => {
+ const r = await t.get('/api/v1/prompts');
+ assert.strictEqual(r.status, 200);
+ assert.deepStrictEqual(r.json().prompts.map((p) => p.id), ['design-page', 'design-theme', 'design-edit', 'glyph-draft']);
+ assert.match(r.headers.get('cache-control') || '', /public/);
+ });
+ } finally { await t.close(); }
+ done();
+})().catch((e) => { console.error(e); process.exit(1); });