@deneb-ui/cli 2.0.40 → 2.0.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/index.js +11 -2
- package/package.json +4 -2
- package/src/arc/ai-agent.cjs +328 -0
- package/src/arc/ai-prompts.cjs +214 -0
- package/src/arc/component-registry.cjs +158 -0
- package/src/arc/index.cjs +133 -1
- package/src/arc/pr-agent.cjs +295 -0
- package/src/arc/printer.cjs +46 -0
- package/src/arc/version.cjs +2 -2
- package/src/common/fivora-site-data.ts +15 -0
- package/src/common/template-editor-schema.ts +258 -0
package/bin/index.js
CHANGED
|
@@ -1021,6 +1021,8 @@ if (command === 'init') {
|
|
|
1021
1021
|
let explain = false;
|
|
1022
1022
|
let legacy = false;
|
|
1023
1023
|
let telemetry = 'off';
|
|
1024
|
+
let aiEnabled = false;
|
|
1025
|
+
let aiDryRun = false;
|
|
1024
1026
|
for (let i = 0; i < commandArgs.length; i++) {
|
|
1025
1027
|
const arg = commandArgs[i];
|
|
1026
1028
|
if (arg === '--recipe' || arg === '-r') {
|
|
@@ -1033,6 +1035,11 @@ if (command === 'init') {
|
|
|
1033
1035
|
explain = true;
|
|
1034
1036
|
} else if (arg === '--legacy') {
|
|
1035
1037
|
legacy = true;
|
|
1038
|
+
} else if (arg === '--ai') {
|
|
1039
|
+
aiEnabled = true;
|
|
1040
|
+
} else if (arg === '--ai-dry-run') {
|
|
1041
|
+
aiEnabled = true;
|
|
1042
|
+
aiDryRun = true;
|
|
1036
1043
|
} else if (arg === '--telemetry' && commandArgs[i + 1]) {
|
|
1037
1044
|
telemetry = commandArgs[++i];
|
|
1038
1045
|
} else if (arg.startsWith('--telemetry=')) {
|
|
@@ -1041,7 +1048,7 @@ if (command === 'init') {
|
|
|
1041
1048
|
targetInput = arg;
|
|
1042
1049
|
}
|
|
1043
1050
|
}
|
|
1044
|
-
initProject(targetInput, { recipeName, dryRun, explain, legacy, telemetry });
|
|
1051
|
+
initProject(targetInput, { recipeName, dryRun, explain, legacy, telemetry, aiEnabled, aiDryRun });
|
|
1045
1052
|
} else if (command === 'create') {
|
|
1046
1053
|
createTemplate(commandArgs[0]);
|
|
1047
1054
|
} else if (command === 'add') {
|
|
@@ -1108,7 +1115,7 @@ if (command === 'init') {
|
|
|
1108
1115
|
|
|
1109
1116
|
Core Commands:
|
|
1110
1117
|
init Deneb ARC: convert an existing React/Next.js app into a Fivora-editable storefront
|
|
1111
|
-
flags: --dry-run --explain --recipe <name> --legacy --telemetry off|anonymous|enhanced
|
|
1118
|
+
flags: --dry-run --explain --recipe <name> --legacy --ai --ai-dry-run --telemetry off|anonymous|enhanced
|
|
1112
1119
|
doctor Run comprehensive environment, manifest, visual editing AST & asset diagnostic checks (flags: --fix, --json)
|
|
1113
1120
|
save-recipe Learn and save calibrated fixes & schemas into reusable recipe bank (e.g. deneb save-recipe . shoes-store)
|
|
1114
1121
|
learn Alias for save-recipe
|
|
@@ -1136,6 +1143,8 @@ Examples:
|
|
|
1136
1143
|
deneb init --recipe electronics
|
|
1137
1144
|
deneb init --recipe cosmetics
|
|
1138
1145
|
deneb init --legacy
|
|
1146
|
+
deneb init --ai
|
|
1147
|
+
deneb init --ai-dry-run
|
|
1139
1148
|
deneb fonts list
|
|
1140
1149
|
deneb fonts install .
|
|
1141
1150
|
deneb fonts install . --font inter --font playfair-display
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deneb-ui/cli",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.42",
|
|
4
4
|
"description": "Official DENEB CLI — scaffold, convert, validate, and package Fivora-ready storefront templates.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"deneb": "bin/index.js",
|
|
@@ -49,8 +49,10 @@
|
|
|
49
49
|
"license": "MIT",
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@babel/parser": "^7.28.0",
|
|
52
|
-
"@deneb-ui/core": "^2.0.
|
|
52
|
+
"@deneb-ui/core": "^2.0.42",
|
|
53
|
+
"@octokit/rest": "^22.0.1",
|
|
53
54
|
"adm-zip": "^0.6.0",
|
|
55
|
+
"dotenv": "^17.4.2",
|
|
54
56
|
"recast": "^0.23.11"
|
|
55
57
|
},
|
|
56
58
|
"devDependencies": {
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Deneb ARC — AI Agent
|
|
5
|
+
*
|
|
6
|
+
* OpenAI API integration with self-healing validation loop.
|
|
7
|
+
* Generates editable wrapper components for unknown UI components
|
|
8
|
+
* and validates them through ARC's strict pipeline before accepting.
|
|
9
|
+
*
|
|
10
|
+
* Default engine: GPT-4o-mini (configurable via OPENAI_CONTENT_MODEL).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const { parseSource } = require('./ast.cjs');
|
|
16
|
+
const { buildComponentPrompt, buildFixPrompt, buildDocsPrompt } = require('./ai-prompts.cjs');
|
|
17
|
+
|
|
18
|
+
const MAX_RETRIES = 3;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Load AI configuration from environment variables.
|
|
22
|
+
* Call loadEnv() before using this if .env is not loaded yet.
|
|
23
|
+
*/
|
|
24
|
+
function getAiConfig() {
|
|
25
|
+
return {
|
|
26
|
+
apiKey: process.env.OPENAI_API_KEY || '',
|
|
27
|
+
model: process.env.OPENAI_CONTENT_MODEL || 'gpt-4o-mini',
|
|
28
|
+
maxTokens: parseInt(process.env.OPENAI_MAX_OUTPUT_TOKENS || '4000', 10),
|
|
29
|
+
timeoutMs: parseInt(process.env.OPENAI_CONTENT_TIMEOUT_MS || '120000', 10),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Load .env file purely locally.
|
|
35
|
+
* Searches upward from targetDir or process.cwd(), supporting local .env and .env.local files.
|
|
36
|
+
* Eliminates the need for any global system configuration.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} [startDir] - Starting directory (defaults to process.cwd())
|
|
39
|
+
*/
|
|
40
|
+
function loadEnv(startDir) {
|
|
41
|
+
const checked = new Set();
|
|
42
|
+
const candidates = [];
|
|
43
|
+
|
|
44
|
+
let curr = path.resolve(startDir || process.cwd());
|
|
45
|
+
for (let i = 0; i < 5; i++) {
|
|
46
|
+
candidates.push(path.join(curr, '.env'));
|
|
47
|
+
candidates.push(path.join(curr, '.env.local'));
|
|
48
|
+
candidates.push(path.join(curr, 'core', '.env'));
|
|
49
|
+
const parent = path.dirname(curr);
|
|
50
|
+
if (parent === curr) break;
|
|
51
|
+
curr = parent;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
candidates.push(path.resolve(__dirname, '..', '..', '..', '..', '.env'));
|
|
55
|
+
candidates.push(path.resolve(__dirname, '..', '..', '.env'));
|
|
56
|
+
|
|
57
|
+
for (const envPath of candidates) {
|
|
58
|
+
if (!checked.has(envPath) && fs.existsSync(envPath)) {
|
|
59
|
+
try {
|
|
60
|
+
require('dotenv').config({ path: envPath });
|
|
61
|
+
} catch {
|
|
62
|
+
// dotenv not available — env vars must be set manually
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
checked.add(envPath);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Check if the AI agent is properly configured and ready to use.
|
|
72
|
+
* @returns {{ ready: boolean, reason?: string }}
|
|
73
|
+
*/
|
|
74
|
+
function checkAiReady() {
|
|
75
|
+
loadEnv();
|
|
76
|
+
const config = getAiConfig();
|
|
77
|
+
if (!config.apiKey) {
|
|
78
|
+
return { ready: false, reason: 'OPENAI_API_KEY not set. Create a .env file (see .env.example) or set the environment variable.' };
|
|
79
|
+
}
|
|
80
|
+
if (!config.apiKey.startsWith('sk-')) {
|
|
81
|
+
return { ready: false, reason: 'OPENAI_API_KEY does not look valid (should start with sk-).' };
|
|
82
|
+
}
|
|
83
|
+
return { ready: true };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Call the OpenAI Chat Completions API.
|
|
88
|
+
*
|
|
89
|
+
* Uses native fetch (Node 18+) — no external HTTP library needed.
|
|
90
|
+
*
|
|
91
|
+
* @param {string} prompt - The user prompt
|
|
92
|
+
* @param {object} [opts] - Optional overrides
|
|
93
|
+
* @returns {Promise<{ content: string, inputTokens: number, outputTokens: number }>}
|
|
94
|
+
*/
|
|
95
|
+
async function callOpenAI(prompt, opts = {}) {
|
|
96
|
+
const config = getAiConfig();
|
|
97
|
+
const model = opts.model || config.model;
|
|
98
|
+
const maxTokens = opts.maxTokens || config.maxTokens;
|
|
99
|
+
const timeoutMs = opts.timeoutMs || config.timeoutMs;
|
|
100
|
+
|
|
101
|
+
const controller = new AbortController();
|
|
102
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const response = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
106
|
+
method: 'POST',
|
|
107
|
+
headers: {
|
|
108
|
+
'Content-Type': 'application/json',
|
|
109
|
+
'Authorization': `Bearer ${config.apiKey}`,
|
|
110
|
+
},
|
|
111
|
+
body: JSON.stringify({
|
|
112
|
+
model,
|
|
113
|
+
messages: [
|
|
114
|
+
{ role: 'system', content: 'You are a senior React/TypeScript developer specializing in the DENEB UI framework. Output only raw code — no markdown, no explanations.' },
|
|
115
|
+
{ role: 'user', content: prompt },
|
|
116
|
+
],
|
|
117
|
+
max_tokens: maxTokens,
|
|
118
|
+
temperature: 0.2,
|
|
119
|
+
}),
|
|
120
|
+
signal: controller.signal,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (!response.ok) {
|
|
124
|
+
const errorBody = await response.text().catch(() => '');
|
|
125
|
+
throw new Error(`OpenAI API error ${response.status}: ${errorBody.slice(0, 200)}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const data = await response.json();
|
|
129
|
+
const choice = data.choices?.[0];
|
|
130
|
+
if (!choice || !choice.message?.content) {
|
|
131
|
+
throw new Error('OpenAI returned empty response — no content in choices[0].message');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let content = choice.message.content.trim();
|
|
135
|
+
// Strip markdown fences if the model wraps output despite instructions
|
|
136
|
+
content = content.replace(/^```(?:tsx?|jsx?|typescript|javascript)?\s*\n?/i, '').replace(/\n?```\s*$/i, '').trim();
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
content,
|
|
140
|
+
inputTokens: data.usage?.prompt_tokens || 0,
|
|
141
|
+
outputTokens: data.usage?.completion_tokens || 0,
|
|
142
|
+
};
|
|
143
|
+
} finally {
|
|
144
|
+
clearTimeout(timer);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Validate generated component code using ARC's validators.
|
|
150
|
+
*
|
|
151
|
+
* @param {string} code - The generated TypeScript/JSX code
|
|
152
|
+
* @param {string} filename - Virtual filename for error messages
|
|
153
|
+
* @returns {{ passed: boolean, errors: string[] }}
|
|
154
|
+
*/
|
|
155
|
+
function validateGeneratedCode(code, filename) {
|
|
156
|
+
const errors = [];
|
|
157
|
+
|
|
158
|
+
// 1. AST syntax check
|
|
159
|
+
try {
|
|
160
|
+
parseSource(code, filename);
|
|
161
|
+
} catch (err) {
|
|
162
|
+
errors.push(`AST syntax error: ${err.message}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// 2. Must have data-preview-field-path markers
|
|
166
|
+
if (!code.includes('data-preview-field-path')) {
|
|
167
|
+
errors.push('Missing data-preview-field-path markers — the component has no editable fields.');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 3. Must have data-preview-item-path on the root wrapper
|
|
171
|
+
if (!code.includes('data-preview-item-path')) {
|
|
172
|
+
errors.push('Missing data-preview-item-path on root wrapper element.');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 4. Must have proper TypeScript exports
|
|
176
|
+
if (!code.includes('export function') && !code.includes('export const')) {
|
|
177
|
+
errors.push('Missing named export — component must use `export function Editable...()`.');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 5. Must have TypeScript interface
|
|
181
|
+
if (!code.includes('export interface')) {
|
|
182
|
+
errors.push('Missing exported TypeScript interface for component props.');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 6. Must import EditableText or EditableImage (at least one)
|
|
186
|
+
if (!code.includes("from './EditableText'") && !code.includes("from './EditableImage'")) {
|
|
187
|
+
errors.push('Must import at least EditableText or EditableImage from relative path.');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 7. Must NOT have default export (Deneb convention)
|
|
191
|
+
if (/export\s+default\s+/m.test(code)) {
|
|
192
|
+
errors.push('Must NOT use default export — use named export only (Deneb convention).');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 8. Check for forbidden patterns
|
|
196
|
+
if (code.includes('useState') || code.includes('useEffect') || code.includes('useContext')) {
|
|
197
|
+
errors.push('Must NOT import React hooks (useState, useEffect, useContext). Component must be stateless.');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (code.includes('useSiteData')) {
|
|
201
|
+
errors.push('Must NOT import useSiteData directly. The wrapper uses EditableText/EditableImage primitives instead.');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
passed: errors.length === 0,
|
|
206
|
+
errors,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Extract the list of editable field paths from generated component code.
|
|
212
|
+
*
|
|
213
|
+
* @param {string} code - The generated component code
|
|
214
|
+
* @returns {string[]} Array of field path patterns found
|
|
215
|
+
*/
|
|
216
|
+
function extractFieldPaths(code) {
|
|
217
|
+
const matches = code.matchAll(/data-preview-field-path=\{[`"']([^`"']+)[`"']\}/g);
|
|
218
|
+
const paths = new Set();
|
|
219
|
+
for (const m of matches) {
|
|
220
|
+
// Normalize template literal patterns like `${itemPath}.name`
|
|
221
|
+
const path = m[1].replace(/\$\{[^}]+\}/g, '*');
|
|
222
|
+
paths.add(path);
|
|
223
|
+
}
|
|
224
|
+
return [...paths];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Main AI adaptation function with self-healing validation loop.
|
|
229
|
+
*
|
|
230
|
+
* @param {string} sourceCode - The unknown component's source code
|
|
231
|
+
* @param {string} componentName - The component name (e.g. "Carousel")
|
|
232
|
+
* @param {object} profile - ARC project profile
|
|
233
|
+
* @param {object} [callbacks] - Optional progress callbacks
|
|
234
|
+
* @param {function} [callbacks.onAttempt] - Called at start of each attempt: (attemptNum, maxRetries)
|
|
235
|
+
* @param {function} [callbacks.onValidationFail] - Called when validation fails: (errors, attemptNum)
|
|
236
|
+
* @param {function} [callbacks.onSuccess] - Called on success: (attemptNum)
|
|
237
|
+
* @returns {Promise<{ success: boolean, code?: string, fields?: string[], attempts: number, totalInputTokens: number, totalOutputTokens: number, error?: string }>}
|
|
238
|
+
*/
|
|
239
|
+
async function adaptComponent(sourceCode, componentName, profile, callbacks = {}) {
|
|
240
|
+
let totalInputTokens = 0;
|
|
241
|
+
let totalOutputTokens = 0;
|
|
242
|
+
|
|
243
|
+
// Generate initial code
|
|
244
|
+
const prompt = buildComponentPrompt(sourceCode, componentName, profile);
|
|
245
|
+
let lastCode = '';
|
|
246
|
+
let lastErrors = [];
|
|
247
|
+
|
|
248
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
249
|
+
if (callbacks.onAttempt) callbacks.onAttempt(attempt, MAX_RETRIES);
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
let result;
|
|
253
|
+
if (attempt === 1) {
|
|
254
|
+
result = await callOpenAI(prompt);
|
|
255
|
+
} else {
|
|
256
|
+
const fixPrompt = buildFixPrompt(lastCode, lastErrors.join('\n'), attempt);
|
|
257
|
+
result = await callOpenAI(fixPrompt);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
totalInputTokens += result.inputTokens;
|
|
261
|
+
totalOutputTokens += result.outputTokens;
|
|
262
|
+
lastCode = result.content;
|
|
263
|
+
|
|
264
|
+
// Validate the generated code
|
|
265
|
+
const filename = `Editable${componentName}.tsx`;
|
|
266
|
+
const validation = validateGeneratedCode(lastCode, filename);
|
|
267
|
+
|
|
268
|
+
if (validation.passed) {
|
|
269
|
+
if (callbacks.onSuccess) callbacks.onSuccess(attempt);
|
|
270
|
+
return {
|
|
271
|
+
success: true,
|
|
272
|
+
code: lastCode,
|
|
273
|
+
fields: extractFieldPaths(lastCode),
|
|
274
|
+
attempts: attempt,
|
|
275
|
+
totalInputTokens,
|
|
276
|
+
totalOutputTokens,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Validation failed — prepare for retry
|
|
281
|
+
lastErrors = validation.errors;
|
|
282
|
+
if (callbacks.onValidationFail) callbacks.onValidationFail(validation.errors, attempt);
|
|
283
|
+
} catch (err) {
|
|
284
|
+
lastErrors = [err.message];
|
|
285
|
+
if (callbacks.onValidationFail) callbacks.onValidationFail([err.message], attempt);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// All retries exhausted
|
|
290
|
+
return {
|
|
291
|
+
success: false,
|
|
292
|
+
attempts: MAX_RETRIES,
|
|
293
|
+
totalInputTokens,
|
|
294
|
+
totalOutputTokens,
|
|
295
|
+
error: `Failed after ${MAX_RETRIES} attempts. Last errors:\n${lastErrors.join('\n')}`,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Generate documentation page for a successfully adapted component.
|
|
301
|
+
*
|
|
302
|
+
* @param {string} componentName - e.g. "Carousel"
|
|
303
|
+
* @param {string} componentCode - The validated Editable*.tsx source
|
|
304
|
+
* @param {string[]} editableFields - List of field paths
|
|
305
|
+
* @returns {Promise<{ success: boolean, code?: string, error?: string }>}
|
|
306
|
+
*/
|
|
307
|
+
async function generateDocsPage(componentName, componentCode, editableFields) {
|
|
308
|
+
const prompt = buildDocsPrompt(componentName, componentCode, editableFields);
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
const result = await callOpenAI(prompt);
|
|
312
|
+
return { success: true, code: result.content };
|
|
313
|
+
} catch (err) {
|
|
314
|
+
return { success: false, error: err.message };
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
module.exports = {
|
|
319
|
+
loadEnv,
|
|
320
|
+
checkAiReady,
|
|
321
|
+
getAiConfig,
|
|
322
|
+
callOpenAI,
|
|
323
|
+
validateGeneratedCode,
|
|
324
|
+
extractFieldPaths,
|
|
325
|
+
adaptComponent,
|
|
326
|
+
generateDocsPage,
|
|
327
|
+
MAX_RETRIES,
|
|
328
|
+
};
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Deneb ARC — AI Prompt Templates
|
|
5
|
+
*
|
|
6
|
+
* Carefully engineered prompt templates that teach GPT-4o-mini the exact
|
|
7
|
+
* Deneb editable component conventions. Uses a real EditableServiceCard
|
|
8
|
+
* as a reference pattern so the AI learns the correct structure.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// ─── Reference component (shortened for token efficiency) ────────
|
|
12
|
+
const REFERENCE_COMPONENT = `
|
|
13
|
+
import React from 'react';
|
|
14
|
+
import { EditableText } from './EditableText';
|
|
15
|
+
import { EditableImage } from './EditableImage';
|
|
16
|
+
|
|
17
|
+
export interface ServiceItem {
|
|
18
|
+
id?: string | number;
|
|
19
|
+
name?: string;
|
|
20
|
+
title?: string;
|
|
21
|
+
price?: string | number;
|
|
22
|
+
description?: string;
|
|
23
|
+
imageUrl?: string;
|
|
24
|
+
image?: string;
|
|
25
|
+
features?: unknown[];
|
|
26
|
+
[key: string]: unknown;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface EditableServiceCardProps extends React.HTMLAttributes<HTMLElement> {
|
|
30
|
+
itemPath: string;
|
|
31
|
+
service: ServiceItem;
|
|
32
|
+
imageFallback?: string;
|
|
33
|
+
as?: React.ElementType;
|
|
34
|
+
align?: 'left' | 'center' | 'right';
|
|
35
|
+
showPrice?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function EditableServiceCard({
|
|
39
|
+
itemPath,
|
|
40
|
+
service,
|
|
41
|
+
imageFallback = '/placeholder.svg',
|
|
42
|
+
as: Component = 'article',
|
|
43
|
+
align = 'left',
|
|
44
|
+
showPrice = true,
|
|
45
|
+
className = '',
|
|
46
|
+
style,
|
|
47
|
+
children,
|
|
48
|
+
...props
|
|
49
|
+
}: EditableServiceCardProps) {
|
|
50
|
+
const name = String(service?.name || service?.title || '');
|
|
51
|
+
const description = String(service?.description || '');
|
|
52
|
+
const imageUrl = String(service?.imageUrl || service?.image || '');
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<Component
|
|
56
|
+
data-preview-item-path={itemPath}
|
|
57
|
+
className={\`editable-service-card \${className}\`.trim()}
|
|
58
|
+
style={{ textAlign: align, ...style }}
|
|
59
|
+
{...(props as any)}
|
|
60
|
+
>
|
|
61
|
+
<div className="service-card-image-wrap">
|
|
62
|
+
<EditableImage
|
|
63
|
+
id={\`\${itemPath}.imageUrl\`}
|
|
64
|
+
data-preview-field-path={\`\${itemPath}.imageUrl\`}
|
|
65
|
+
src={imageUrl}
|
|
66
|
+
fallbackSrc={imageFallback}
|
|
67
|
+
alt={name}
|
|
68
|
+
className="service-card-image"
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
<div className="service-card-body">
|
|
72
|
+
<EditableText
|
|
73
|
+
as="h2"
|
|
74
|
+
id={\`\${itemPath}.name\`}
|
|
75
|
+
data-preview-field-path={\`\${itemPath}.name\`}
|
|
76
|
+
defaultValue={name}
|
|
77
|
+
className="service-card-title"
|
|
78
|
+
/>
|
|
79
|
+
<EditableText
|
|
80
|
+
as="p"
|
|
81
|
+
id={\`\${itemPath}.description\`}
|
|
82
|
+
data-preview-field-path={\`\${itemPath}.description\`}
|
|
83
|
+
defaultValue={description}
|
|
84
|
+
className="service-card-description"
|
|
85
|
+
/>
|
|
86
|
+
{children}
|
|
87
|
+
</div>
|
|
88
|
+
</Component>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
`.trim();
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Build the main component generation prompt.
|
|
95
|
+
*
|
|
96
|
+
* @param {string} sourceCode - The unknown component's source code
|
|
97
|
+
* @param {string} componentName - The component's name (e.g. "Carousel")
|
|
98
|
+
* @param {object} profile - ARC project profile (framework, libraries, etc.)
|
|
99
|
+
* @returns {string} The complete prompt for OpenAI
|
|
100
|
+
*/
|
|
101
|
+
function buildComponentPrompt(sourceCode, componentName, profile) {
|
|
102
|
+
return `You are an expert React/TypeScript developer working on the DENEB UI framework.
|
|
103
|
+
Your task is to create an editable wrapper component for an existing component.
|
|
104
|
+
|
|
105
|
+
## RULES (MANDATORY — violating any rule means the output is rejected):
|
|
106
|
+
|
|
107
|
+
1. **Import only from relative paths**: Use \`./EditableText\` and \`./EditableImage\` (these are the ONLY two Deneb primitives you may use).
|
|
108
|
+
2. **Every user-visible text** must be wrapped in \`<EditableText>\` with:
|
|
109
|
+
- \`as\` prop matching the original HTML tag (h1, h2, p, span, etc.)
|
|
110
|
+
- \`id={\`\${itemPath}.fieldName\`}\`
|
|
111
|
+
- \`data-preview-field-path={\`\${itemPath}.fieldName\`}\`
|
|
112
|
+
- \`defaultValue={value}\`
|
|
113
|
+
3. **Every user-visible image** must use \`<EditableImage>\` with:
|
|
114
|
+
- \`id={\`\${itemPath}.fieldName\`}\`
|
|
115
|
+
- \`data-preview-field-path={\`\${itemPath}.fieldName\`}\`
|
|
116
|
+
- \`src={value}\`
|
|
117
|
+
- \`fallbackSrc={imageFallback}\`
|
|
118
|
+
4. **Lists/arrays** must have \`data-preview-list-path\` on the container and \`data-preview-item-path\` on each item.
|
|
119
|
+
5. **Root wrapper** must have \`data-preview-item-path={itemPath}\`.
|
|
120
|
+
6. **Export a TypeScript interface** named \`Editable${componentName}Props\` extending \`React.HTMLAttributes<HTMLElement>\`.
|
|
121
|
+
7. **Export a named function** (not default export) named \`Editable${componentName}\`.
|
|
122
|
+
8. **Preserve all original CSS classes and styling** — do NOT remove or change className values.
|
|
123
|
+
9. **Props must include**: \`itemPath: string\` (required), data object, \`as?: React.ElementType\`, \`className?\`, \`style?\`, \`children?\`.
|
|
124
|
+
10. **Use \`'use strict'\` is NOT needed** — this is a .tsx file.
|
|
125
|
+
11. **Do NOT import React hooks** like useState, useEffect, useContext. The component must be stateless.
|
|
126
|
+
12. **Do NOT import useSiteData** — the wrapper component does not need it directly.
|
|
127
|
+
|
|
128
|
+
## REFERENCE PATTERN (follow this structure exactly):
|
|
129
|
+
|
|
130
|
+
\`\`\`tsx
|
|
131
|
+
${REFERENCE_COMPONENT}
|
|
132
|
+
\`\`\`
|
|
133
|
+
|
|
134
|
+
## SOURCE COMPONENT TO ADAPT:
|
|
135
|
+
|
|
136
|
+
Component name: ${componentName}
|
|
137
|
+
Framework: ${profile.framework || 'nextjs'}
|
|
138
|
+
Libraries: ${(profile.componentLibraries || []).join(', ') || 'none detected'}
|
|
139
|
+
|
|
140
|
+
\`\`\`tsx
|
|
141
|
+
${sourceCode}
|
|
142
|
+
\`\`\`
|
|
143
|
+
|
|
144
|
+
## YOUR OUTPUT:
|
|
145
|
+
|
|
146
|
+
Generate ONLY the complete TypeScript (.tsx) file content for \`Editable${componentName}.tsx\`.
|
|
147
|
+
Do NOT include markdown fences, explanations, or comments outside the code.
|
|
148
|
+
Output the raw TypeScript code directly.`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Build the error-fix prompt when the validator rejects AI-generated code.
|
|
153
|
+
*
|
|
154
|
+
* @param {string} previousCode - The code that failed validation
|
|
155
|
+
* @param {string} errorMessage - The exact validator error
|
|
156
|
+
* @param {number} attempt - Current attempt number
|
|
157
|
+
* @returns {string} The fix prompt
|
|
158
|
+
*/
|
|
159
|
+
function buildFixPrompt(previousCode, errorMessage, attempt) {
|
|
160
|
+
return `The DENEB ARC validator REJECTED your generated component (attempt ${attempt}/3).
|
|
161
|
+
|
|
162
|
+
## EXACT ERROR:
|
|
163
|
+
${errorMessage}
|
|
164
|
+
|
|
165
|
+
## YOUR PREVIOUS CODE:
|
|
166
|
+
\`\`\`tsx
|
|
167
|
+
${previousCode}
|
|
168
|
+
\`\`\`
|
|
169
|
+
|
|
170
|
+
## INSTRUCTIONS:
|
|
171
|
+
1. Fix ONLY the specific error described above.
|
|
172
|
+
2. Do NOT change anything else — preserve all existing field paths, markers, and class names.
|
|
173
|
+
3. Output the COMPLETE corrected .tsx file (not a diff, not a snippet — the full file).
|
|
174
|
+
4. Do NOT include markdown fences or explanations. Output raw TypeScript code only.`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Build the docs page generation prompt.
|
|
179
|
+
*
|
|
180
|
+
* @param {string} componentName - e.g. "Carousel"
|
|
181
|
+
* @param {string} componentCode - The validated EditableCarousel.tsx source
|
|
182
|
+
* @param {string[]} editableFields - List of field paths discovered
|
|
183
|
+
* @returns {string} The docs prompt
|
|
184
|
+
*/
|
|
185
|
+
function buildDocsPrompt(componentName, componentCode, editableFields) {
|
|
186
|
+
return `Generate a Next.js documentation page for the DENEB UI component "Editable${componentName}".
|
|
187
|
+
|
|
188
|
+
## COMPONENT CODE:
|
|
189
|
+
\`\`\`tsx
|
|
190
|
+
${componentCode}
|
|
191
|
+
\`\`\`
|
|
192
|
+
|
|
193
|
+
## EDITABLE FIELDS:
|
|
194
|
+
${editableFields.map((f) => `- ${f}`).join('\n')}
|
|
195
|
+
|
|
196
|
+
## OUTPUT FORMAT:
|
|
197
|
+
Generate a React component that exports a default function named \`Editable${componentName}DocsPage\`.
|
|
198
|
+
It should render:
|
|
199
|
+
1. A title section with the component name
|
|
200
|
+
2. A description paragraph explaining what the component does
|
|
201
|
+
3. A props table showing all available props with their types and defaults
|
|
202
|
+
4. A usage example code block
|
|
203
|
+
5. A list of editable field paths
|
|
204
|
+
|
|
205
|
+
Use plain HTML/JSX with className for styling. Do NOT import any external UI libraries.
|
|
206
|
+
Output raw TSX code only — no markdown fences or explanations.`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
module.exports = {
|
|
210
|
+
REFERENCE_COMPONENT,
|
|
211
|
+
buildComponentPrompt,
|
|
212
|
+
buildFixPrompt,
|
|
213
|
+
buildDocsPrompt,
|
|
214
|
+
};
|