openvibers/openvibe.ai Commits
Let the Space builder prompt carry the OpenVibe component registry (#387)
Changes
@@@ -844,9 +844,11 @@ through an OpenAI-compatible listener and registers it on the existing endpoint-is a space's own AI (`OV.ai.ask` in the space SDK): the maker's instructions under the platform's rules, on route`space.ask` (an alias of `default.chat`), on behalf of the visitor, so it counts against their daily allowance.-- `overview.source` says which (`local_model`, `paid`, `extractive`), and `gaps` lists what is missing (no local-- model, no speech-to-text, no budget). The output is not cached: callers keep it. Live's grant names-- `media.analyze`.++ The builder's prompt can carry OpenVibe.Design's public `design.component@1` registry in a `components` input++ (Space reads and caches it): the definitions are then the interface vocabulary and the answer marks each major++ part with `data-ov-component`. `overview.source` says which (`local_model`, `paid`, `extractive`), and `gaps`++ lists what is missing (no local model, no speech-to-text, no budget). The output is not cached: callers keep it.++ Live's grant names `media.analyze`.- **Direct operations** — `ai.chat|generate|summarize|classify|extract|enrich|embed|image`.## MegaSource research@@@ -60,6 +60,10 @@ const templates = [SDK_REFERENCE,FUNCTIONS_REFERENCE,'',++ '{{#components}}The OpenVibe component registry (design.component@1) is your interface vocabulary. Build the interface from these parts, keep their anatomy, states and keyboard behaviour, and mark each major part with data-ov-component="<name>":',++ '{{components}}',++ '{{/components}}',++ '','How you build:','- Prefer ONE self-contained index.html with inline <style> and <script>. Use more files only when asked; never more than 6, and index.html is always one of them.','- Make it work completely: real logic, no placeholders, no "TODO", no lorem ipsum. Handle loading, empty and error states, and the signed-out visitor (show what works without signing in and a clear "Sign in with OpenVibe" button for the rest).',@@@ -99,7 +103,7 @@ const workflows = [{key: 'space.app.build', name: 'Space: build or change a space', namespace: 'space',description: 'From what a person asks for (and the current files when changing a space): name, description, AI instructions, the SDK features used, and the files.',-- input_schema: OBJ({ prompt: STR(4000), name: STR(40), current: ARR(FILE, MAX_FILES) }, ['prompt']),++ input_schema: OBJ({ prompt: STR(4000), name: STR(40), current: ARR(FILE, MAX_FILES), components: STR(20000) }, ['prompt']),output_schema: OBJ({ name: STR(40), description: STR(400), instructions: STR(4000), capabilities: ARR({ type: 'string', enum: CAPABILITIES }, 4), files: ARR(FILE, MAX_FILES) }),steps: [{ kind: 'llm', template: 'space.app.build', operation: 'generate', output: 'json', prepare: 'space.app.build', postprocess: 'space.app.build', params: { max_tokens: 16000, timeout_ms: 180000 } }],default_route: 'space.build', cache_mode: 'none', metadata: { seed: true },@@@ -155,7 +159,7 @@ const uncapped = (schema) => JSON.parse(JSON.stringify(schema, (k, v) => (k ===const PREPARE = {'space.app.build'(input) {const current = (input.current || []).map((f) => `--- ${f.path} ---\n${f.content}`).join('\n\n');-- return { vars: { prompt: input.prompt, name: input.name || '', current }, jsonSchema: uncapped(workflows.find((w) => w.key === 'space.app.build').output_schema) };++ return { vars: { prompt: input.prompt, name: input.name || '', current, components: input.components || '' }, jsonSchema: uncapped(workflows.find((w) => w.key === 'space.app.build').output_schema) };},};// A JSON answer that came back as text (a fenced block, or prose around the object).@@@ -23,6 +23,13 @@ t.test('the builder\'s prompt: the SDK reference, and the current files when chaassert.match(sys, /OV\.fn\(name, body\)/);assert.match(sys, /never request\.user or request\.ov/, 'the visitor is on ov, not on the request');assert.ok(!/There is no server code/.test(sys));++ assert.match(sys, /design\.component@1/, 'the registry is named as the interface vocabulary');++ const withComponents = render(sys, { components: 'sheet — A modal task surface. Web: kit.sheet()' });++ assert.match(withComponents, /mark each major part with data-ov-component/);++ assert.match(withComponents, /sheet — A modal task surface/);++ assert.ok(!/\{\{#components\}\}/.test(withComponents), 'the section is rendered away');++ const prepWithRegistry = PREPARE['space.app.build']({ prompt: 'a pizza poll', components: 'sheet — A modal task surface.' });++ assert.strictEqual(prepWithRegistry.vars.components, 'sheet — A modal task surface.');const prep = PREPARE['space.app.build']({ prompt: 'a poll about pizza' });assert.ok(prep.jsonSchema && prep.jsonSchema.properties.files && !/maxLength|maxItems/.test(JSON.stringify(prep.jsonSchema)), 'the model answers in the uncapped, non-strict schema');const fresh = render(tpl('space.app.build').user_prompt, prep.vars);
@@ -844,9 +844,11 @@ through an OpenAI-compatible listener and registers it on the existing endpoint-
@@ -844,9 +844,11 @@ through an OpenAI-compatible listener and registers it on the existing endpoint-
is a space's own AI (`OV.ai.ask` in the space SDK): the maker's instructions under the platform's rules, on route
is a space's own AI (`OV.ai.ask` in the space SDK): the maker's instructions under the platform's rules, on route
`space.ask` (an alias of `default.chat`), on behalf of the visitor, so it counts against their daily allowance.
`space.ask` (an alias of `default.chat`), on behalf of the visitor, so it counts against their daily allowance.
- `overview.source` says which (`local_model`, `paid`, `extractive`), and `gaps` lists what is missing (no local
- model, no speech-to-text, no budget). The output is not cached: callers keep it. Live's grant names
- `media.analyze`.
+ The builder's prompt can carry OpenVibe.Design's public `design.component@1` registry in a `components` input
+ (Space reads and caches it): the definitions are then the interface vocabulary and the answer marks each major
+ part with `data-ov-component`. `overview.source` says which (`local_model`, `paid`, `extractive`), and `gaps`
+ lists what is missing (no local model, no speech-to-text, no budget). The output is not cached: callers keep it.
+ Live's grant names `media.analyze`.
- **Direct operations** — `ai.chat|generate|summarize|classify|extract|enrich|embed|image`.
- **Direct operations** — `ai.chat|generate|summarize|classify|extract|enrich|embed|image`.
## MegaSource research
## MegaSource research
@@ -60,6 +60,10 @@ const templates = [
@@ -60,6 +60,10 @@ const templates = [
SDK_REFERENCE,
SDK_REFERENCE,
FUNCTIONS_REFERENCE,
FUNCTIONS_REFERENCE,
'',
'',
+ '{{#components}}The OpenVibe component registry (design.component@1) is your interface vocabulary. Build the interface from these parts, keep their anatomy, states and keyboard behaviour, and mark each major part with data-ov-component="<name>":',
+ '{{components}}',
+ '{{/components}}',
+ '',
'How you build:',
'How you build:',
'- Prefer ONE self-contained index.html with inline <style> and <script>. Use more files only when asked; never more than 6, and index.html is always one of them.',
'- Prefer ONE self-contained index.html with inline <style> and <script>. Use more files only when asked; never more than 6, and index.html is always one of them.',
'- Make it work completely: real logic, no placeholders, no "TODO", no lorem ipsum. Handle loading, empty and error states, and the signed-out visitor (show what works without signing in and a clear "Sign in with OpenVibe" button for the rest).',
'- Make it work completely: real logic, no placeholders, no "TODO", no lorem ipsum. Handle loading, empty and error states, and the signed-out visitor (show what works without signing in and a clear "Sign in with OpenVibe" button for the rest).',
@@ -99,7 +103,7 @@ const workflows = [
@@ -99,7 +103,7 @@ const workflows = [
{
{
key: 'space.app.build', name: 'Space: build or change a space', namespace: 'space',
key: 'space.app.build', name: 'Space: build or change a space', namespace: 'space',
description: 'From what a person asks for (and the current files when changing a space): name, description, AI instructions, the SDK features used, and the files.',
description: 'From what a person asks for (and the current files when changing a space): name, description, AI instructions, the SDK features used, and the files.',
- input_schema: OBJ({ prompt: STR(4000), name: STR(40), current: ARR(FILE, MAX_FILES) }, ['prompt']),
+ input_schema: OBJ({ prompt: STR(4000), name: STR(40), current: ARR(FILE, MAX_FILES), components: STR(20000) }, ['prompt']),
output_schema: OBJ({ name: STR(40), description: STR(400), instructions: STR(4000), capabilities: ARR({ type: 'string', enum: CAPABILITIES }, 4), files: ARR(FILE, MAX_FILES) }),
output_schema: OBJ({ name: STR(40), description: STR(400), instructions: STR(4000), capabilities: ARR({ type: 'string', enum: CAPABILITIES }, 4), files: ARR(FILE, MAX_FILES) }),
steps: [{ kind: 'llm', template: 'space.app.build', operation: 'generate', output: 'json', prepare: 'space.app.build', postprocess: 'space.app.build', params: { max_tokens: 16000, timeout_ms: 180000 } }],
steps: [{ kind: 'llm', template: 'space.app.build', operation: 'generate', output: 'json', prepare: 'space.app.build', postprocess: 'space.app.build', params: { max_tokens: 16000, timeout_ms: 180000 } }],
default_route: 'space.build', cache_mode: 'none', metadata: { seed: true },
default_route: 'space.build', cache_mode: 'none', metadata: { seed: true },
@@ -155,7 +159,7 @@ const uncapped = (schema) => JSON.parse(JSON.stringify(schema, (k, v) => (k ===
@@ -155,7 +159,7 @@ const uncapped = (schema) => JSON.parse(JSON.stringify(schema, (k, v) => (k ===
const PREPARE = {
const PREPARE = {
'space.app.build'(input) {
'space.app.build'(input) {
const current = (input.current || []).map((f) => `--- ${f.path} ---\n${f.content}`).join('\n\n');
const current = (input.current || []).map((f) => `--- ${f.path} ---\n${f.content}`).join('\n\n');
- return { vars: { prompt: input.prompt, name: input.name || '', current }, jsonSchema: uncapped(workflows.find((w) => w.key === 'space.app.build').output_schema) };
+ return { vars: { prompt: input.prompt, name: input.name || '', current, components: input.components || '' }, jsonSchema: uncapped(workflows.find((w) => w.key === 'space.app.build').output_schema) };
},
},
};
};
// A JSON answer that came back as text (a fenced block, or prose around the object).
// A JSON answer that came back as text (a fenced block, or prose around the object).
@@ -23,6 +23,13 @@ t.test('the builder\'s prompt: the SDK reference, and the current files when cha
@@ -23,6 +23,13 @@ t.test('the builder\'s prompt: the SDK reference, and the current files when cha
assert.match(sys, /OV\.fn\(name, body\)/);
assert.match(sys, /OV\.fn\(name, body\)/);
assert.match(sys, /never request\.user or request\.ov/, 'the visitor is on ov, not on the request');
assert.match(sys, /never request\.user or request\.ov/, 'the visitor is on ov, not on the request');
assert.ok(!/There is no server code/.test(sys));
assert.ok(!/There is no server code/.test(sys));
+ assert.match(sys, /design\.component@1/, 'the registry is named as the interface vocabulary');
+ const withComponents = render(sys, { components: 'sheet — A modal task surface. Web: kit.sheet()' });
+ assert.match(withComponents, /mark each major part with data-ov-component/);
+ assert.match(withComponents, /sheet — A modal task surface/);
+ assert.ok(!/\{\{#components\}\}/.test(withComponents), 'the section is rendered away');
+ const prepWithRegistry = PREPARE['space.app.build']({ prompt: 'a pizza poll', components: 'sheet — A modal task surface.' });
+ assert.strictEqual(prepWithRegistry.vars.components, 'sheet — A modal task surface.');
const prep = PREPARE['space.app.build']({ prompt: 'a poll about pizza' });
const prep = PREPARE['space.app.build']({ prompt: 'a poll about pizza' });
assert.ok(prep.jsonSchema && prep.jsonSchema.properties.files && !/maxLength|maxItems/.test(JSON.stringify(prep.jsonSchema)), 'the model answers in the uncapped, non-strict schema');
assert.ok(prep.jsonSchema && prep.jsonSchema.properties.files && !/maxLength|maxItems/.test(JSON.stringify(prep.jsonSchema)), 'the model answers in the uncapped, non-strict schema');
const fresh = render(tpl('space.app.build').user_prompt, prep.vars);
const fresh = render(tpl('space.app.build').user_prompt, prep.vars);