openvibers/openvibe.space Commits
New space: a description handed over from Codes fills the box (?prompt=) (#152)
Changes
@@@ -18,6 +18,9 @@.sp-actions form { margin: 0; }.sp-flash { padding: 12px 14px; border-radius: 12px; border: 1px solid var(--border, #2c3d5c); background: var(--bg-secondary, #101828); }.sp-flash.is-bad { border-color: #ef4444; color: #fecaca; background: color-mix(in srgb, #ef4444 12%, transparent); }++/* A description handed over from openvibe.codes: one quiet line in the accent, above the form it filled. */++.sp-handover { display: flex; align-items: center; gap: 10px; margin: 0 0 4px; padding: 10px 14px; border-radius: 12px; border: 1px solid color-mix(in srgb, var(--accent, #4f8cff) 40%, transparent); background: color-mix(in srgb, var(--accent, #4f8cff) 10%, transparent); color: var(--text-primary, #e8edf3); font-size: 14px; }++.sp-handover i { color: var(--accent, #4f8cff); }/* The hidden attribute wins over every display rule here (the AI overlay, Unpublish). */.sp-editor [hidden], .sp-wrap [hidden], .sp-section [hidden] { display: none !important; }.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }@@@ -185,11 +185,15 @@ function createSpacesPages(ctx) {html(res, 200, render.galleryPage({ viewer: signedIn(req) ? req.viewer : null, spaces: await spaces.gallery({ q, sort }), templates: templates.list(), q, sort, csrf: csrfOf(req) }));}));++ // ?prompt=<text>: a description written elsewhere on the network (openvibe.codes "Something new") fills the box,++ // one press from a build; nothing is built until the person presses Build. ?from=codes says where it came from.++ const promptOf = (req) => (typeof req.query.prompt === 'string' ? req.query.prompt.replace(/\r\n?/g, '\n').trim().slice(0, 4000) : '');++ const fromOf = (req) => (req.query.from === 'codes' ? 'codes' : null);router.get('/spaces/new', view(async (req, res) => {if (!signedIn(req)) throw new ApiError(401, 'auth.required', 'sign in first');// ?template=<id>: someone picked a template on openvibe.website; it leads the page, one press from a new space.const pick = req.query.template ? templates.get(String(req.query.template)) : null;-- html(res, 200, render.newPage({ viewer: req.viewer, templates: templates.list(), csrf: csrfOf(req), aiOn: ai.enabled(), buildsLeft: await buildsLeft(req), pick }));++ html(res, 200, render.newPage({ viewer: req.viewer, templates: templates.list(), csrf: csrfOf(req), aiOn: ai.enabled(), buildsLeft: await buildsLeft(req), pick, prompt: pick ? '' : promptOf(req), from: fromOf(req) }));}));router.post('/spaces/new', ...action(async (req, res) => {const body = req.body || {};@@@ -220,7 +224,7 @@ function createSpacesPages(ctx) {// ?template=<id>: a website template picked on openvibe.website leads the page, one press from a draft.const picked = req.query.template ? templates.get(String(req.query.template)) : null;const pick = picked && picked.kind === 'site' ? picked : null;-- html(res, 200, render.guestPage({ templates: templates.sites(), error: null, prompt: '', aiOn: ai.enabled(), pick }));++ html(res, 200, render.guestPage({ templates: templates.sites(), error: null, prompt: pick ? '' : promptOf(req), aiOn: ai.enabled(), pick, from: fromOf(req) }));}));router.post('/spaces/guest', ...guestAction(async (req, res) => {const body = req.body || {};@@@ -93,7 +93,12 @@ function galleryPage({ viewer, spaces, templates, q, sort, csrf }) {return page({ title: 'Spaces', description: 'Make a space: a small app on your OpenVibe account, built by OpenVibe AI or from a template, with sign-in, shared lists, private storage and AI.', path: '/spaces', showcase: true, body });}--function newPage({ viewer, templates, csrf, error, prompt, aiOn, buildsLeft, pick = null }) {++// A description handed over from another OpenVibe site: say so, and put the Build button one press away.++const handedOver = (from, prompt) => (prompt && from === 'codes'++ ? '<p class="sp-handover" role="status"><i class="fa-solid fa-code" aria-hidden="true"></i> From OpenVibe Codes: check what you wrote, then press Build it.</p>'++ : '');++++function newPage({ viewer, templates, csrf, error, prompt, aiOn, buildsLeft, pick = null, from = null }) {const picked = pick ? `<section class="sp-pick" style="--tpl:${esc(pick.accent)}" aria-labelledby="sp-pick-h"><span class="sp-template-icon"><i class="fa-solid ${esc(pick.icon)}" aria-hidden="true"></i></span><div class="sp-pick-text"><p class="eyebrow">The template you picked</p><h2 id="sp-pick-h">${esc(pick.name)}</h2><p>${esc(pick.description)}</p></div>@@@ -104,11 +109,12 @@ function newPage({ viewer, templates, csrf, error, prompt, aiOn, buildsLeft, pic<h1 class="sp-title">${pick ? 'Make it yours' : 'What do you want to make?'}</h1>${error ? `<p class="sp-flash is-bad" role="alert">${esc(error)}</p>` : ''}${picked}++ ${handedOver(from, prompt)}<form class="sp-ai-form" method="post" action="/spaces/new"><input type="hidden" name="csrf" value="${esc(csrf)}"><label for="sp-prompt">Describe it and OpenVibe AI builds it</label><textarea id="sp-prompt" name="prompt" rows="4" maxlength="4000" placeholder="A trivia quiz about 90s cartoons with a leaderboard…" ${aiOn ? '' : 'disabled'}>${esc(prompt || '')}</textarea>-- <div class="sp-row"><button class="sp-btn" ${aiOn ? '' : 'disabled'}><i class="fa-solid fa-wand-magic-sparkles" aria-hidden="true"></i> Build it</button>++ <div class="sp-row"><button class="sp-btn" ${aiOn ? '' : 'disabled'}${prompt && aiOn ? ' autofocus' : ''}><i class="fa-solid fa-wand-magic-sparkles" aria-hidden="true"></i> Build it</button><span class="sp-muted">${aiOn ? `${Number(buildsLeft)} AI builds left today. It takes about a minute; you can change everything after.` : 'OpenVibe AI is not available right now; start from a template instead.'}</span></div><div class="sp-ideas" aria-label="Ideas">${['A daily mood check-in with a shared chart', 'A pixel art board everyone can paint on', 'A recipe idea generator that asks what is in my fridge', 'A study flashcard deck I can quiz myself with'].map((i) => `<button type="button" class="sp-chip" data-idea="${esc(i)}">${esc(i)}</button>`).join('')}</div></form>@@@ -233,7 +239,7 @@ function editorPage({ space, files, csrf, canPublish, url, aiOn, buildsLeft, tem* The start page of a guest draft (openvibe.website, plan 5.9): no account, a template or a sentence, and a* private link to edit it. Plain forms in the spaces markup; the design is the orchestrator's.*/--function guestPage({ templates, error, prompt, aiOn, pick = null }) {++function guestPage({ templates, error, prompt, aiOn, pick = null, from = null }) {const picked = pick ? `<section class="sp-pick" style="--tpl:${esc(pick.accent)}" aria-labelledby="sp-pick-h"><span class="sp-template-icon"><i class="fa-solid ${esc(pick.icon)}" aria-hidden="true"></i></span><div class="sp-pick-text"><p class="eyebrow">The template you picked</p><h2 id="sp-pick-h">${esc(pick.name)}</h2><p>${esc(pick.description)}</p></div>@@@ -250,10 +256,11 @@ function guestPage({ templates, error, prompt, aiOn, pick = null }) {<p class="sp-lede">No account needed. Start from a template or describe what you want and OpenVibe AI writes it. You get a private link to edit it; sign in later to keep it.</p>${error ? `<p class="sp-flash is-bad" role="alert">${esc(error)}</p>` : ''}${picked}++ ${handedOver(from, prompt)}<form class="sp-ai-form" method="post" action="/spaces/guest"><label for="sp-prompt">Describe it and OpenVibe AI builds it</label><textarea id="sp-prompt" name="prompt" rows="4" maxlength="4000" placeholder="A bakery site with today’s hours, where to find us and how to order…" ${aiOn ? '' : 'disabled'}>${esc(prompt || '')}</textarea>-- <div class="sp-row"><button class="sp-btn" ${aiOn ? '' : 'disabled'}><i class="fa-solid fa-wand-magic-sparkles" aria-hidden="true"></i> Build it</button>++ <div class="sp-row"><button class="sp-btn" ${aiOn ? '' : 'disabled'}${prompt && aiOn ? ' autofocus' : ''}><i class="fa-solid fa-wand-magic-sparkles" aria-hidden="true"></i> Build it</button><span class="sp-muted">${aiOn ? 'It takes about a minute; you can change everything after.' : 'OpenVibe AI is not available right now; start from a template instead.'}</span></div></form><h2 class="sp-subtitle">Or start from a template</h2>@@@ -96,6 +96,30 @@ assert.match(hostingSrc, /target\.kind === 'dashboard' \|\| target\.kind === 'weconst plain = await t.request({ method: 'GET', host: 'openvibe.space', path: '/spaces/new?template=nope', session: alice });assert.ok(!plain.text.includes('class="sp-pick"'), 'an unknown template picks nothing');});++++ await check('a description handed over from Codes fills the box, one press from a build', async () => {++ const said = 'A snake game <b>with</b> a leaderboard & sound';++ const r = await t.request({ method: 'GET', host: 'openvibe.space', path: `/spaces/new?prompt=${encodeURIComponent(said)}&from=codes`, session: alice });++ assert.strictEqual(r.status, 200, r.text.slice(0, 200));++ assert.ok(r.text.includes('A snake game <b>with</b> a leaderboard & sound</textarea>'), 'the description fills the box, escaped');++ assert.ok(!r.text.includes('<b>with</b>'), 'nothing in it is markup');++ assert.match(r.text, /class="sp-handover"[^>]*>[\s\S]*?From OpenVibe Codes/);++ assert.match(r.text, /<button class="sp-btn"[^>]*autofocus[^>]*>[\s\S]*?Build it/, 'Build it takes the focus');++ const plain = await t.request({ method: 'GET', host: 'openvibe.space', path: `/spaces/new?prompt=${encodeURIComponent(said)}`, session: alice });++ assert.ok(plain.text.includes('a leaderboard & sound</textarea>') && !plain.text.includes('class="sp-handover"'), 'a prompt from anywhere else fills the box without the Codes line');++ const picked = await t.request({ method: 'GET', host: 'openvibe.space', path: `/spaces/new?template=site-portfolio&prompt=${encodeURIComponent(said)}`, session: alice });++ assert.ok(!picked.text.includes('a leaderboard & sound'), 'a picked template leads, and the box stays empty');++ });++++ await check('the guest start page takes a handed-over description too, and a gated page never echoes it', async () => {++ const r = await t.request({ method: 'GET', host: 'openvibe.space', path: `/spaces/guest?prompt=${encodeURIComponent('A bakery site with our hours')}&from=codes` });++ assert.strictEqual(r.status, 200, r.text.slice(0, 200));++ assert.ok(r.text.includes('A bakery site with our hours</textarea>'));++ assert.match(r.text, /class="sp-handover"/);++ const gated = await t.request({ method: 'GET', host: 'openvibe.space', path: `/spaces/new?prompt=${encodeURIComponent('secret plan for a site')}&from=codes` });++ assert.strictEqual(gated.status, 401);++ assert.ok(!gated.text.includes('secret plan'), 'an anonymous answer carries nothing from the query');++ });} finally { await t.close(); }done();})().catch((e) => { console.error(e); process.exit(1); });
@@ -18,6 +18,9 @@
@@ -18,6 +18,9 @@
.sp-actions form { margin: 0; }
.sp-actions form { margin: 0; }
.sp-flash { padding: 12px 14px; border-radius: 12px; border: 1px solid var(--border, #2c3d5c); background: var(--bg-secondary, #101828); }
.sp-flash { padding: 12px 14px; border-radius: 12px; border: 1px solid var(--border, #2c3d5c); background: var(--bg-secondary, #101828); }
.sp-flash.is-bad { border-color: #ef4444; color: #fecaca; background: color-mix(in srgb, #ef4444 12%, transparent); }
.sp-flash.is-bad { border-color: #ef4444; color: #fecaca; background: color-mix(in srgb, #ef4444 12%, transparent); }
+/* A description handed over from openvibe.codes: one quiet line in the accent, above the form it filled. */
+.sp-handover { display: flex; align-items: center; gap: 10px; margin: 0 0 4px; padding: 10px 14px; border-radius: 12px; border: 1px solid color-mix(in srgb, var(--accent, #4f8cff) 40%, transparent); background: color-mix(in srgb, var(--accent, #4f8cff) 10%, transparent); color: var(--text-primary, #e8edf3); font-size: 14px; }
+.sp-handover i { color: var(--accent, #4f8cff); }
/* The hidden attribute wins over every display rule here (the AI overlay, Unpublish). */
/* The hidden attribute wins over every display rule here (the AI overlay, Unpublish). */
.sp-editor [hidden], .sp-wrap [hidden], .sp-section [hidden] { display: none !important; }
.sp-editor [hidden], .sp-wrap [hidden], .sp-section [hidden] { display: none !important; }
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
@@ -185,11 +185,15 @@ function createSpacesPages(ctx) {
@@ -185,11 +185,15 @@ function createSpacesPages(ctx) {
html(res, 200, render.galleryPage({ viewer: signedIn(req) ? req.viewer : null, spaces: await spaces.gallery({ q, sort }), templates: templates.list(), q, sort, csrf: csrfOf(req) }));
html(res, 200, render.galleryPage({ viewer: signedIn(req) ? req.viewer : null, spaces: await spaces.gallery({ q, sort }), templates: templates.list(), q, sort, csrf: csrfOf(req) }));
}));
}));
+ // ?prompt=<text>: a description written elsewhere on the network (openvibe.codes "Something new") fills the box,
+ // one press from a build; nothing is built until the person presses Build. ?from=codes says where it came from.
+ const promptOf = (req) => (typeof req.query.prompt === 'string' ? req.query.prompt.replace(/\r\n?/g, '\n').trim().slice(0, 4000) : '');
+ const fromOf = (req) => (req.query.from === 'codes' ? 'codes' : null);
router.get('/spaces/new', view(async (req, res) => {
router.get('/spaces/new', view(async (req, res) => {
if (!signedIn(req)) throw new ApiError(401, 'auth.required', 'sign in first');
if (!signedIn(req)) throw new ApiError(401, 'auth.required', 'sign in first');
// ?template=<id>: someone picked a template on openvibe.website; it leads the page, one press from a new space.
// ?template=<id>: someone picked a template on openvibe.website; it leads the page, one press from a new space.
const pick = req.query.template ? templates.get(String(req.query.template)) : null;
const pick = req.query.template ? templates.get(String(req.query.template)) : null;
- html(res, 200, render.newPage({ viewer: req.viewer, templates: templates.list(), csrf: csrfOf(req), aiOn: ai.enabled(), buildsLeft: await buildsLeft(req), pick }));
+ html(res, 200, render.newPage({ viewer: req.viewer, templates: templates.list(), csrf: csrfOf(req), aiOn: ai.enabled(), buildsLeft: await buildsLeft(req), pick, prompt: pick ? '' : promptOf(req), from: fromOf(req) }));
}));
}));
router.post('/spaces/new', ...action(async (req, res) => {
router.post('/spaces/new', ...action(async (req, res) => {
const body = req.body || {};
const body = req.body || {};
@@ -220,7 +224,7 @@ function createSpacesPages(ctx) {
@@ -220,7 +224,7 @@ function createSpacesPages(ctx) {
// ?template=<id>: a website template picked on openvibe.website leads the page, one press from a draft.
// ?template=<id>: a website template picked on openvibe.website leads the page, one press from a draft.
const picked = req.query.template ? templates.get(String(req.query.template)) : null;
const picked = req.query.template ? templates.get(String(req.query.template)) : null;
const pick = picked && picked.kind === 'site' ? picked : null;
const pick = picked && picked.kind === 'site' ? picked : null;
- html(res, 200, render.guestPage({ templates: templates.sites(), error: null, prompt: '', aiOn: ai.enabled(), pick }));
+ html(res, 200, render.guestPage({ templates: templates.sites(), error: null, prompt: pick ? '' : promptOf(req), aiOn: ai.enabled(), pick, from: fromOf(req) }));
}));
}));
router.post('/spaces/guest', ...guestAction(async (req, res) => {
router.post('/spaces/guest', ...guestAction(async (req, res) => {
const body = req.body || {};
const body = req.body || {};
@@ -93,7 +93,12 @@ function galleryPage({ viewer, spaces, templates, q, sort, csrf }) {
@@ -93,7 +93,12 @@ function galleryPage({ viewer, spaces, templates, q, sort, csrf }) {
return page({ title: 'Spaces', description: 'Make a space: a small app on your OpenVibe account, built by OpenVibe AI or from a template, with sign-in, shared lists, private storage and AI.', path: '/spaces', showcase: true, body });
return page({ title: 'Spaces', description: 'Make a space: a small app on your OpenVibe account, built by OpenVibe AI or from a template, with sign-in, shared lists, private storage and AI.', path: '/spaces', showcase: true, body });
}
}
-function newPage({ viewer, templates, csrf, error, prompt, aiOn, buildsLeft, pick = null }) {
+// A description handed over from another OpenVibe site: say so, and put the Build button one press away.
+const handedOver = (from, prompt) => (prompt && from === 'codes'
+ ? '<p class="sp-handover" role="status"><i class="fa-solid fa-code" aria-hidden="true"></i> From OpenVibe Codes: check what you wrote, then press Build it.</p>'
+ : '');
+
+function newPage({ viewer, templates, csrf, error, prompt, aiOn, buildsLeft, pick = null, from = null }) {
const picked = pick ? `<section class="sp-pick" style="--tpl:${esc(pick.accent)}" aria-labelledby="sp-pick-h">
const picked = pick ? `<section class="sp-pick" style="--tpl:${esc(pick.accent)}" aria-labelledby="sp-pick-h">
<span class="sp-template-icon"><i class="fa-solid ${esc(pick.icon)}" aria-hidden="true"></i></span>
<span class="sp-template-icon"><i class="fa-solid ${esc(pick.icon)}" aria-hidden="true"></i></span>
<div class="sp-pick-text"><p class="eyebrow">The template you picked</p><h2 id="sp-pick-h">${esc(pick.name)}</h2><p>${esc(pick.description)}</p></div>
<div class="sp-pick-text"><p class="eyebrow">The template you picked</p><h2 id="sp-pick-h">${esc(pick.name)}</h2><p>${esc(pick.description)}</p></div>
@@ -104,11 +109,12 @@ function newPage({ viewer, templates, csrf, error, prompt, aiOn, buildsLeft, pic
@@ -104,11 +109,12 @@ function newPage({ viewer, templates, csrf, error, prompt, aiOn, buildsLeft, pic
<h1 class="sp-title">${pick ? 'Make it yours' : 'What do you want to make?'}</h1>
<h1 class="sp-title">${pick ? 'Make it yours' : 'What do you want to make?'}</h1>
${error ? `<p class="sp-flash is-bad" role="alert">${esc(error)}</p>` : ''}
${error ? `<p class="sp-flash is-bad" role="alert">${esc(error)}</p>` : ''}
${picked}
${picked}
+ ${handedOver(from, prompt)}
<form class="sp-ai-form" method="post" action="/spaces/new">
<form class="sp-ai-form" method="post" action="/spaces/new">
<input type="hidden" name="csrf" value="${esc(csrf)}">
<input type="hidden" name="csrf" value="${esc(csrf)}">
<label for="sp-prompt">Describe it and OpenVibe AI builds it</label>
<label for="sp-prompt">Describe it and OpenVibe AI builds it</label>
<textarea id="sp-prompt" name="prompt" rows="4" maxlength="4000" placeholder="A trivia quiz about 90s cartoons with a leaderboard…" ${aiOn ? '' : 'disabled'}>${esc(prompt || '')}</textarea>
<textarea id="sp-prompt" name="prompt" rows="4" maxlength="4000" placeholder="A trivia quiz about 90s cartoons with a leaderboard…" ${aiOn ? '' : 'disabled'}>${esc(prompt || '')}</textarea>
- <div class="sp-row"><button class="sp-btn" ${aiOn ? '' : 'disabled'}><i class="fa-solid fa-wand-magic-sparkles" aria-hidden="true"></i> Build it</button>
+ <div class="sp-row"><button class="sp-btn" ${aiOn ? '' : 'disabled'}${prompt && aiOn ? ' autofocus' : ''}><i class="fa-solid fa-wand-magic-sparkles" aria-hidden="true"></i> Build it</button>
<span class="sp-muted">${aiOn ? `${Number(buildsLeft)} AI builds left today. It takes about a minute; you can change everything after.` : 'OpenVibe AI is not available right now; start from a template instead.'}</span></div>
<span class="sp-muted">${aiOn ? `${Number(buildsLeft)} AI builds left today. It takes about a minute; you can change everything after.` : 'OpenVibe AI is not available right now; start from a template instead.'}</span></div>
<div class="sp-ideas" aria-label="Ideas">${['A daily mood check-in with a shared chart', 'A pixel art board everyone can paint on', 'A recipe idea generator that asks what is in my fridge', 'A study flashcard deck I can quiz myself with'].map((i) => `<button type="button" class="sp-chip" data-idea="${esc(i)}">${esc(i)}</button>`).join('')}</div>
<div class="sp-ideas" aria-label="Ideas">${['A daily mood check-in with a shared chart', 'A pixel art board everyone can paint on', 'A recipe idea generator that asks what is in my fridge', 'A study flashcard deck I can quiz myself with'].map((i) => `<button type="button" class="sp-chip" data-idea="${esc(i)}">${esc(i)}</button>`).join('')}</div>
</form>
</form>
@@ -233,7 +239,7 @@ function editorPage({ space, files, csrf, canPublish, url, aiOn, buildsLeft, tem
@@ -233,7 +239,7 @@ function editorPage({ space, files, csrf, canPublish, url, aiOn, buildsLeft, tem
* The start page of a guest draft (openvibe.website, plan 5.9): no account, a template or a sentence, and a
* The start page of a guest draft (openvibe.website, plan 5.9): no account, a template or a sentence, and a
* private link to edit it. Plain forms in the spaces markup; the design is the orchestrator's.
* private link to edit it. Plain forms in the spaces markup; the design is the orchestrator's.
*/
*/
-function guestPage({ templates, error, prompt, aiOn, pick = null }) {
+function guestPage({ templates, error, prompt, aiOn, pick = null, from = null }) {
const picked = pick ? `<section class="sp-pick" style="--tpl:${esc(pick.accent)}" aria-labelledby="sp-pick-h">
const picked = pick ? `<section class="sp-pick" style="--tpl:${esc(pick.accent)}" aria-labelledby="sp-pick-h">
<span class="sp-template-icon"><i class="fa-solid ${esc(pick.icon)}" aria-hidden="true"></i></span>
<span class="sp-template-icon"><i class="fa-solid ${esc(pick.icon)}" aria-hidden="true"></i></span>
<div class="sp-pick-text"><p class="eyebrow">The template you picked</p><h2 id="sp-pick-h">${esc(pick.name)}</h2><p>${esc(pick.description)}</p></div>
<div class="sp-pick-text"><p class="eyebrow">The template you picked</p><h2 id="sp-pick-h">${esc(pick.name)}</h2><p>${esc(pick.description)}</p></div>
@@ -250,10 +256,11 @@ function guestPage({ templates, error, prompt, aiOn, pick = null }) {
@@ -250,10 +256,11 @@ function guestPage({ templates, error, prompt, aiOn, pick = null }) {
<p class="sp-lede">No account needed. Start from a template or describe what you want and OpenVibe AI writes it. You get a private link to edit it; sign in later to keep it.</p>
<p class="sp-lede">No account needed. Start from a template or describe what you want and OpenVibe AI writes it. You get a private link to edit it; sign in later to keep it.</p>
${error ? `<p class="sp-flash is-bad" role="alert">${esc(error)}</p>` : ''}
${error ? `<p class="sp-flash is-bad" role="alert">${esc(error)}</p>` : ''}
${picked}
${picked}
+ ${handedOver(from, prompt)}
<form class="sp-ai-form" method="post" action="/spaces/guest">
<form class="sp-ai-form" method="post" action="/spaces/guest">
<label for="sp-prompt">Describe it and OpenVibe AI builds it</label>
<label for="sp-prompt">Describe it and OpenVibe AI builds it</label>
<textarea id="sp-prompt" name="prompt" rows="4" maxlength="4000" placeholder="A bakery site with today’s hours, where to find us and how to order…" ${aiOn ? '' : 'disabled'}>${esc(prompt || '')}</textarea>
<textarea id="sp-prompt" name="prompt" rows="4" maxlength="4000" placeholder="A bakery site with today’s hours, where to find us and how to order…" ${aiOn ? '' : 'disabled'}>${esc(prompt || '')}</textarea>
- <div class="sp-row"><button class="sp-btn" ${aiOn ? '' : 'disabled'}><i class="fa-solid fa-wand-magic-sparkles" aria-hidden="true"></i> Build it</button>
+ <div class="sp-row"><button class="sp-btn" ${aiOn ? '' : 'disabled'}${prompt && aiOn ? ' autofocus' : ''}><i class="fa-solid fa-wand-magic-sparkles" aria-hidden="true"></i> Build it</button>
<span class="sp-muted">${aiOn ? 'It takes about a minute; you can change everything after.' : 'OpenVibe AI is not available right now; start from a template instead.'}</span></div>
<span class="sp-muted">${aiOn ? 'It takes about a minute; you can change everything after.' : 'OpenVibe AI is not available right now; start from a template instead.'}</span></div>
</form>
</form>
<h2 class="sp-subtitle">Or start from a template</h2>
<h2 class="sp-subtitle">Or start from a template</h2>
@@ -96,6 +96,30 @@ assert.match(hostingSrc, /target\.kind === 'dashboard' \|\| target\.kind === 'we
@@ -96,6 +96,30 @@ assert.match(hostingSrc, /target\.kind === 'dashboard' \|\| target\.kind === 'we
const plain = await t.request({ method: 'GET', host: 'openvibe.space', path: '/spaces/new?template=nope', session: alice });
const plain = await t.request({ method: 'GET', host: 'openvibe.space', path: '/spaces/new?template=nope', session: alice });
assert.ok(!plain.text.includes('class="sp-pick"'), 'an unknown template picks nothing');
assert.ok(!plain.text.includes('class="sp-pick"'), 'an unknown template picks nothing');
});
});
+
+ await check('a description handed over from Codes fills the box, one press from a build', async () => {
+ const said = 'A snake game <b>with</b> a leaderboard & sound';
+ const r = await t.request({ method: 'GET', host: 'openvibe.space', path: `/spaces/new?prompt=${encodeURIComponent(said)}&from=codes`, session: alice });
+ assert.strictEqual(r.status, 200, r.text.slice(0, 200));
+ assert.ok(r.text.includes('A snake game <b>with</b> a leaderboard & sound</textarea>'), 'the description fills the box, escaped');
+ assert.ok(!r.text.includes('<b>with</b>'), 'nothing in it is markup');
+ assert.match(r.text, /class="sp-handover"[^>]*>[\s\S]*?From OpenVibe Codes/);
+ assert.match(r.text, /<button class="sp-btn"[^>]*autofocus[^>]*>[\s\S]*?Build it/, 'Build it takes the focus');
+ const plain = await t.request({ method: 'GET', host: 'openvibe.space', path: `/spaces/new?prompt=${encodeURIComponent(said)}`, session: alice });
+ assert.ok(plain.text.includes('a leaderboard & sound</textarea>') && !plain.text.includes('class="sp-handover"'), 'a prompt from anywhere else fills the box without the Codes line');
+ const picked = await t.request({ method: 'GET', host: 'openvibe.space', path: `/spaces/new?template=site-portfolio&prompt=${encodeURIComponent(said)}`, session: alice });
+ assert.ok(!picked.text.includes('a leaderboard & sound'), 'a picked template leads, and the box stays empty');
+ });
+
+ await check('the guest start page takes a handed-over description too, and a gated page never echoes it', async () => {
+ const r = await t.request({ method: 'GET', host: 'openvibe.space', path: `/spaces/guest?prompt=${encodeURIComponent('A bakery site with our hours')}&from=codes` });
+ assert.strictEqual(r.status, 200, r.text.slice(0, 200));
+ assert.ok(r.text.includes('A bakery site with our hours</textarea>'));
+ assert.match(r.text, /class="sp-handover"/);
+ const gated = await t.request({ method: 'GET', host: 'openvibe.space', path: `/spaces/new?prompt=${encodeURIComponent('secret plan for a site')}&from=codes` });
+ assert.strictEqual(gated.status, 401);
+ assert.ok(!gated.text.includes('secret plan'), 'an anonymous answer carries nothing from the query');
+ });
} finally { await t.close(); }
} finally { await t.close(); }
done();
done();
})().catch((e) => { console.error(e); process.exit(1); });
})().catch((e) => { console.error(e); process.exit(1); });