Skip to content

Commit fbcdf62

Browse files
committed
feat(prompt): make Demo Data and Instructional Text optional via catalog slots; add CRUD onboarding guidance item; add decision tool; support RAG-only resources (Issue #184)
1 parent efea8ec commit fbcdf62

File tree

9 files changed

+576
-12
lines changed

9 files changed

+576
-12
lines changed

app/llms/crud-onboarding.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "crud-onboarding",
3+
"label": "CRUD Onboarding",
4+
"version": "1",
5+
"type": "guidance",
6+
"description": "Optional instructional and demo-data guidance for plain CRUD apps.",
7+
"slots": {
8+
"instructionalText": "instructional-guidance",
9+
"demoDataGuidance": "demo-data-guidance"
10+
},
11+
"ragResources": [
12+
{
13+
"id": "demo-examples",
14+
"type": "examples",
15+
"uri": "https://use-fireproof.com/crud-demo-examples.json",
16+
"format": "json",
17+
"embeddingProfile": "default",
18+
"includeInPrompt": false,
19+
"tags": ["crud", "demo", "onboarding"]
20+
}
21+
]
22+
}

app/llms/crud-onboarding.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<instructional-guidance>
2+
In the UI, include a short, vivid description of the app's purpose and clear, step‑by‑step instructions for how to use it. Render the instructions in italic text. Keep them concise and focused on the core CRUD actions the user can perform. Prefer a short “Getting started” paragraph rather than long documentation.
3+
</instructional-guidance>
4+
5+
<demo-data-guidance>
6+
If your app has a function that uses callAI with a schema to save data, include a "Demo Data" button that calls that same function with an example prompt. Do not create a separate demo-only code path. The demo data should exercise the real save logic so the UI shows exactly what users will get when they perform the same action.
7+
Never include a callAI instance that is only used to generate demo data. Always route demo actions through the same user-facing code paths that persist data.
8+
</demo-data-guidance>

app/prompts.ts

Lines changed: 217 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { APP_MODE, CALLAI_ENDPOINT } from './config/env';
44
import callaiTxt from './llms/callai.txt?raw';
55
import fireproofTxt from './llms/fireproof.txt?raw';
66
import imageGenTxt from './llms/image-gen.txt?raw';
7+
import crudOnboardingTxt from './llms/crud-onboarding.txt?raw';
78
const llmsModules = import.meta.glob('./llms/*.json', { eager: true });
89
const llmsList = Object.values(llmsModules).map(
910
(mod) =>
@@ -27,6 +28,7 @@ const llmsTextContent: Record<string, string> = {
2728
callai: callaiTxt,
2829
fireproof: fireproofTxt,
2930
'image-gen': imageGenTxt,
31+
'crud-onboarding': crudOnboardingTxt,
3032
};
3133

3234
// Cache for LLM text documents to prevent redundant fetches/imports
@@ -65,6 +67,34 @@ const llmImportRegexes = llmsList
6567

6668
type HistoryMessage = { role: 'user' | 'assistant' | 'system'; content: string };
6769

70+
// Active retrieval configuration for stream calls (RAG-only)
71+
let activeRetrievalConfig: { use: 'auto' | 'on' | 'off'; members: Array<{ ref: string }> } = {
72+
use: 'off',
73+
members: [],
74+
};
75+
76+
export function getActiveRetrievalConfig() {
77+
return activeRetrievalConfig;
78+
}
79+
80+
function setActiveRetrievalConfig(cfg: {
81+
use?: 'auto' | 'on' | 'off';
82+
members?: Array<{ ref: string }>;
83+
}) {
84+
activeRetrievalConfig = {
85+
use: cfg.use ?? 'off',
86+
members: Array.isArray(cfg.members) ? cfg.members : [],
87+
};
88+
}
89+
90+
// Test-only helper
91+
export function __setActiveRetrievalConfigForTests(cfg: {
92+
use?: 'auto' | 'on' | 'off';
93+
members?: Array<{ ref: string }>;
94+
}) {
95+
setActiveRetrievalConfig(cfg);
96+
}
97+
6898
// Detect modules already referenced in history imports
6999
function detectModulesInHistory(history: HistoryMessage[]): Set<string> {
70100
const detected = new Set<string>();
@@ -87,7 +117,9 @@ async function selectLlmsModules(
87117
): Promise<string[]> {
88118
if (APP_MODE === 'test') return llmsList.map((l) => l.name);
89119

90-
const catalog = llmsList.map((l) => ({ name: l.name, description: l.description || '' }));
120+
// Exclude guidance-only items from the selection catalog
121+
const selectable = llmsList.filter((l: any) => (l as any).type !== 'guidance');
122+
const catalog = selectable.map((l) => ({ name: l.name, description: l.description || '' }));
91123
const payload = { catalog, userPrompt: userPrompt || '', history: history || [] };
92124

93125
const messages: Message[] = [
@@ -138,13 +170,97 @@ export async function preloadLlmsText(): Promise<void> {
138170
});
139171
}
140172

173+
// Decision tool: classify CRUD vs custom look & feel and inclusion booleans
174+
async function decideCrudLookFeel(
175+
model: string,
176+
input: { userPrompt?: string; stylePrompt?: string; history?: HistoryMessage[] }
177+
): Promise<{
178+
appType: 'crud' | 'custom' | 'mixed' | 'unknown';
179+
hasLookAndFeel: boolean;
180+
includeInstructionalText: boolean;
181+
includeDemoData: boolean;
182+
confidence: number;
183+
}> {
184+
if (APP_MODE === 'test') {
185+
const hasStyle = !!(input.stylePrompt && input.stylePrompt.trim());
186+
const crudSignals = /\bcrud\b|create|update|delete|table|list|records?/i.test(
187+
input.userPrompt || ''
188+
);
189+
return {
190+
appType: crudSignals && !hasStyle ? 'crud' : hasStyle ? 'custom' : 'unknown',
191+
hasLookAndFeel: hasStyle,
192+
includeInstructionalText: crudSignals && !hasStyle,
193+
includeDemoData: crudSignals && !hasStyle,
194+
confidence: 0.9,
195+
};
196+
}
197+
198+
const payload = {
199+
userPrompt: input.userPrompt || '',
200+
stylePrompt: input.stylePrompt || '',
201+
history: input.history || [],
202+
};
203+
204+
const messages: Message[] = [
205+
{
206+
role: 'system',
207+
content:
208+
'Classify whether this app is a plain CRUD app without specified look & feel. Return JSON only with keys: appType (crud|custom|mixed|unknown), hasLookAndFeel (boolean), includeInstructionalText (boolean), includeDemoData (boolean), confidence (0..1). Include=true only when clearly CRUD with no specified look & feel.',
209+
},
210+
{ role: 'user', content: JSON.stringify(payload) },
211+
];
212+
213+
const options: CallAIOptions = {
214+
chatUrl: CALLAI_ENDPOINT,
215+
apiKey: 'sk-vibes-proxy-managed',
216+
model,
217+
schema: {
218+
name: 'crud_lookfeel_decision',
219+
properties: {
220+
appType: { type: 'string' },
221+
hasLookAndFeel: { type: 'boolean' },
222+
includeInstructionalText: { type: 'boolean' },
223+
includeDemoData: { type: 'boolean' },
224+
confidence: { type: 'number' },
225+
},
226+
},
227+
max_tokens: 1000,
228+
headers: {
229+
'HTTP-Referer': 'https://vibes.diy',
230+
'X-Title': 'Vibes DIY',
231+
'X-VIBES-Token': localStorage.getItem('auth_token') || '',
232+
},
233+
};
234+
235+
try {
236+
const raw = (await callAI(messages, options)) as string;
237+
const parsed = JSON.parse(raw);
238+
return {
239+
appType: (parsed?.appType as any) || 'unknown',
240+
hasLookAndFeel: Boolean(parsed?.hasLookAndFeel),
241+
includeInstructionalText: Boolean(parsed?.includeInstructionalText),
242+
includeDemoData: Boolean(parsed?.includeDemoData),
243+
confidence: typeof parsed?.confidence === 'number' ? parsed.confidence : 0,
244+
};
245+
} catch (err) {
246+
console.warn('decideCrudLookFeel call failed:', err);
247+
return {
248+
appType: 'unknown',
249+
hasLookAndFeel: false,
250+
includeInstructionalText: false,
251+
includeDemoData: false,
252+
confidence: 0,
253+
};
254+
}
255+
}
256+
141257
// Generate dynamic import statements from LLM configuration
142258
export function generateImportStatements(llms: typeof llmsList) {
143259
const seen = new Set<string>();
144260
return llms
145261
.slice()
146-
.sort((a, b) => a.importModule.localeCompare(b.importModule))
147262
.filter((l) => l.importModule && l.importName)
263+
.sort((a, b) => a.importModule.localeCompare(b.importModule))
148264
.filter((l) => {
149265
const key = `${l.importModule}:${l.importName}`;
150266
if (seen.has(key)) return false;
@@ -160,13 +276,19 @@ export async function makeBaseSystemPrompt(model: string, sessionDoc?: any) {
160276
// Inputs for module selection
161277
const userPrompt = sessionDoc?.userPrompt || '';
162278
const history: HistoryMessage[] = Array.isArray(sessionDoc?.history) ? sessionDoc.history : [];
163-
// 1) Ask AI which modules to include
164-
const aiSelected = await selectLlmsModules(model, userPrompt, history);
279+
const stylePromptInput = sessionDoc?.stylePrompt;
280+
// 1) Ask AI which modules to include and run decision tool in parallel
281+
const [aiSelected, decision] = await Promise.all([
282+
selectLlmsModules(model, userPrompt, history),
283+
decideCrudLookFeel(model, { userPrompt, stylePrompt: stylePromptInput, history }),
284+
]);
165285

166286
// 2) Ensure we retain any modules already used in history
167287
const detected = detectModulesInHistory(history);
168288
const finalNames = new Set<string>([...aiSelected, ...detected]);
169-
const chosenLlms = llmsList.filter((l) => finalNames.has(l.name));
289+
const chosenLlms = llmsList
290+
.filter((l: any) => (l as any).type !== 'guidance')
291+
.filter((l) => finalNames.has(l.name));
170292

171293
// 3) Concatenate docs for chosen modules
172294
let concatenatedLlmsTxt = '';
@@ -191,7 +313,95 @@ ${text || ''}
191313
const defaultStylePrompt = `Create a UI theme inspired by the Memphis Group and Studio Alchimia from the 1980s. Incorporate bold, playful geometric shapes (squiggles, triangles, circles), vibrant primary colors (red, blue, yellow) with contrasting pastels (pink, mint, lavender), and asymmetrical layouts. Use quirky patterns like polka dots, zigzags, and terrazzo textures. Ensure a retro-futuristic vibe with a mix of matte and glossy finishes, evoking a whimsical yet functional design. Secretly name the theme 'Memphis Alchemy' to reflect its roots in Ettore Sotsass’s vision and global 1980s influences. Make sure the app background has some kind of charming patterned background using memphis styled dots or squiggly lines. Use thick "neo-brutalism" style borders for style to enhance legibility. Make sure to retain high contrast in your use of colors. Light background are better than dark ones. Use these colors: #70d6ff #ff70a6 #ff9770 #ffd670 #e9ff70 #242424 #ffffff Never use white text.`;
192314

193315
// Get style prompt from session document if available
194-
const stylePrompt = sessionDoc?.stylePrompt || defaultStylePrompt;
316+
const stylePrompt = stylePromptInput || defaultStylePrompt;
317+
318+
// Resolve optional guidance slots and retrieval-only resources
319+
type SlotConfig =
320+
| { source: 'off' | undefined; inclusion?: 'auto' | 'include' | 'exclude' }
321+
| { source: 'inline'; text?: string; inclusion?: 'auto' | 'include' | 'exclude' }
322+
| {
323+
source: 'catalog';
324+
catalogRef?: string;
325+
key?: string;
326+
inclusion?: 'auto' | 'include' | 'exclude';
327+
};
328+
329+
const slots = sessionDoc?.config?.prompt?.slots || {};
330+
const retrieval = sessionDoc?.config?.prompt?.retrieval || {};
331+
const DEFAULT_CATALOG_REF = 'crud-onboarding@1';
332+
333+
function shouldInclude(slot: SlotConfig | undefined, autoValue: boolean): boolean {
334+
if (!slot || (slot as any).source === undefined || (slot as any).source === 'off') return false;
335+
const incl = (slot as any).inclusion || 'auto';
336+
if (incl === 'exclude') return false;
337+
if (incl === 'include') return true;
338+
return !!autoValue;
339+
}
340+
341+
function extractTaggedSection(txt: string, tag: string): string | undefined {
342+
const re = new RegExp(`<${tag}>\\n([\\s\\S]*?)\\n<\\/${tag}>`);
343+
const m = txt.match(re);
344+
return m ? m[1].trim() : undefined;
345+
}
346+
347+
function resolveCatalogSlot(refOrUndefined: string | undefined, key: string | undefined) {
348+
const ref = refOrUndefined || DEFAULT_CATALOG_REF; // name@version
349+
const [name] = ref.split('@');
350+
const text = llmsTextCache[name] || llmsTextContent[name];
351+
if (!text) return undefined;
352+
let tag = '';
353+
if (name === 'crud-onboarding') {
354+
tag = key === 'demoDataGuidance' ? 'demo-data-guidance' : 'instructional-guidance';
355+
} else {
356+
tag = key || '';
357+
}
358+
if (!tag) return undefined;
359+
return extractTaggedSection(text, tag);
360+
}
361+
362+
const autoInstruction = !!decision?.includeInstructionalText;
363+
const autoDemo = !!decision?.includeDemoData;
364+
const resolvedGuidelines: string[] = [];
365+
366+
const sInstruction: SlotConfig | undefined = (slots as any).instructionalText;
367+
if (shouldInclude(sInstruction, autoInstruction)) {
368+
let block: string | undefined;
369+
if (sInstruction?.source === 'inline') block = (sInstruction as any).text || '';
370+
else if (sInstruction?.source === 'catalog')
371+
block = resolveCatalogSlot(
372+
(sInstruction as any).catalogRef,
373+
(sInstruction as any).key || 'instructionalText'
374+
);
375+
if (block && block.trim()) resolvedGuidelines.push(`- ${block.trim()}`);
376+
}
377+
378+
const sDemo: SlotConfig | undefined = (slots as any).demoDataGuidance;
379+
if (shouldInclude(sDemo, autoDemo)) {
380+
let block: string | undefined;
381+
if (sDemo?.source === 'inline') block = (sDemo as any).text || '';
382+
else if (sDemo?.source === 'catalog')
383+
block = resolveCatalogSlot(
384+
(sDemo as any).catalogRef,
385+
(sDemo as any).key || 'demoDataGuidance'
386+
);
387+
if (block && block.trim()) {
388+
const lines = block
389+
.split(/\n+/)
390+
.map((l) => l.trim())
391+
.filter(Boolean);
392+
for (const line of lines) resolvedGuidelines.push(`- ${line}`);
393+
}
394+
}
395+
396+
// RAG-only retrieval config (not concatenated into text)
397+
const retrievalUse = retrieval?.use || 'off';
398+
const retrievalMembers = Array.isArray(retrieval?.members) ? retrieval.members : [];
399+
setActiveRetrievalConfig({
400+
use: retrievalUse === 'auto' ? (autoDemo ? 'on' : 'off') : retrievalUse,
401+
members: retrievalMembers
402+
.map((m: any) => ({ ref: (m as any).ref }))
403+
.filter((m: { ref?: string }) => !!m.ref),
404+
});
195405

196406
return `
197407
You are an AI assistant tasked with creating React components. You should create components that:
@@ -215,9 +425,7 @@ You are an AI assistant tasked with creating React components. You should create
215425
- If you get missing block errors, change the database name to a new name
216426
- List data items on the main page of your app so users don't have to hunt for them
217427
- If you save data, make sure it is browseable in the app, eg lists should be clickable for more details
218-
- In the UI, include a vivid description of the app's purpose and detailed instructions how to use it, in italic text.
219-
- If your app has a function that uses callAI with a schema to save data, include a Demo Data button that calls that function with an example prompt. Don't write an extra function, use real app code so the data illustrates what it looks like to use the app.
220-
- Never have have an instance of callAI that is only used to generate demo data, always use the same calls that are triggered by user actions in the app.
428+
${resolvedGuidelines.join('\n')}
221429
222430
${concatenatedLlmsTxt}
223431

app/types/settings.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,45 @@ export interface UserSettings {
1313

1414
/** AI model to use for code generation */
1515
model?: string;
16+
17+
/**
18+
* Optional application configuration used by the prompt builder.
19+
* When absent, the prompt uses conservative defaults (excludes optional guidance).
20+
*/
21+
config?: {
22+
prompt?: {
23+
/** Slot configuration for optional guidance blocks */
24+
slots?: {
25+
instructionalText?: PromptSlot;
26+
demoDataGuidance?: PromptSlot;
27+
};
28+
/** Retrieval configuration for RAG-only resources (never concatenated) */
29+
retrieval?: PromptRetrieval;
30+
};
31+
};
32+
}
33+
34+
/** Source and inclusion controls for a prompt slot */
35+
export interface PromptSlot {
36+
/** Where to get the slot content from */
37+
source: 'catalog' | 'inline' | 'off';
38+
/** Optional catalog item reference like "crud-onboarding@1" */
39+
catalogRef?: string;
40+
/** Optional key inside the catalog item, e.g. "instructionalText" */
41+
key?: string;
42+
/** Inline text when source === 'inline' */
43+
text?: string;
44+
/** Inclusion policy: auto (decision tool), include, exclude */
45+
inclusion?: 'auto' | 'include' | 'exclude';
46+
}
47+
48+
/** Retrieval configuration for RAG-only members */
49+
export interface PromptRetrieval {
50+
/** Toggle for retrieval usage */
51+
use?: 'auto' | 'on' | 'off';
52+
/** Members referencing catalog ragResources */
53+
members?: Array<{
54+
ref: string; // e.g. "crud-onboarding@1:demo-examples"
55+
includeInPrompt?: boolean; // must remain false for RAG-only
56+
}>;
1657
}

app/utils/streamHandler.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import { type CallAIOptions, type Message, callAI } from 'call-ai';
66
import { CALLAI_ENDPOINT } from '../config/env';
7+
import { getActiveRetrievalConfig } from '../prompts';
78

89
/**
910
* Stream AI responses with accumulated content callback
@@ -53,6 +54,20 @@ export async function streamAI(
5354
},
5455
};
5556

57+
// Provide retrieval-only members to backend without concatenating to prompt text
58+
try {
59+
const rag = getActiveRetrievalConfig();
60+
if (rag && Array.isArray(rag.members) && rag.members.length > 0) {
61+
(options.headers as any)['X-RAG-Refs'] = encodeURIComponent(JSON.stringify(rag.members));
62+
} else {
63+
const ls =
64+
typeof localStorage !== 'undefined' ? localStorage.getItem('vibes-rag-refs') : null;
65+
if (ls) (options.headers as any)['X-RAG-Refs'] = encodeURIComponent(ls);
66+
}
67+
} catch (_err) {
68+
// Non-fatal: retrieval config not available
69+
}
70+
5671
// Credit checking no longer needed - proxy handles it
5772

5873
try {

0 commit comments

Comments
 (0)