next-ai-editor 0.1.0 → 0.1.2
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/dist/{AIEditorProvider-D-w9-GZb.js → AIEditorProvider-CFFnEtEB.js} +849 -402
- package/dist/AIEditorProvider-CFFnEtEB.js.map +1 -0
- package/dist/{AIEditorProvider-Bs9zUVrL.cjs → AIEditorProvider-CmiACRfw.cjs} +825 -378
- package/dist/AIEditorProvider-CmiACRfw.cjs.map +1 -0
- package/dist/client/AIEditorProvider.d.ts +8 -16
- package/dist/client/AIEditorProvider.d.ts.map +1 -1
- package/dist/client/fiber-utils.d.ts +35 -0
- package/dist/client/fiber-utils.d.ts.map +1 -0
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/query-params.d.ts +9 -0
- package/dist/client/query-params.d.ts.map +1 -0
- package/dist/client.cjs +1 -1
- package/dist/client.js +1 -1
- package/dist/{index-BFa7H-uO.js → index-3OMXRwpD.js} +601 -224
- package/dist/index-3OMXRwpD.js.map +1 -0
- package/dist/{index-DnoYi4f8.cjs → index-9QODCOgD.cjs} +595 -218
- package/dist/index-9QODCOgD.cjs.map +1 -0
- package/dist/index.cjs +6 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +14 -10
- package/dist/path-utils-Bai2xKx9.js +36 -0
- package/dist/path-utils-Bai2xKx9.js.map +1 -0
- package/dist/path-utils-DYzEWUGy.cjs +35 -0
- package/dist/path-utils-DYzEWUGy.cjs.map +1 -0
- package/dist/server/handlers/edit.d.ts.map +1 -1
- package/dist/server/handlers/index.d.ts +1 -0
- package/dist/server/handlers/index.d.ts.map +1 -1
- package/dist/server/handlers/read.d.ts.map +1 -1
- package/dist/server/handlers/resolve.d.ts.map +1 -1
- package/dist/server/handlers/suggestions.d.ts +3 -0
- package/dist/server/handlers/suggestions.d.ts.map +1 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/utils/ast.d.ts +10 -0
- package/dist/server/utils/ast.d.ts.map +1 -1
- package/dist/server/utils/source-map.d.ts +5 -0
- package/dist/server/utils/source-map.d.ts.map +1 -1
- package/dist/server.cjs +5 -1
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +13 -9
- package/dist/shared/path-utils.d.ts +24 -0
- package/dist/shared/path-utils.d.ts.map +1 -0
- package/dist/shared/types.d.ts +30 -0
- package/dist/shared/types.d.ts.map +1 -1
- package/package.json +3 -3
- package/dist/AIEditorProvider-Bs9zUVrL.cjs.map +0 -1
- package/dist/AIEditorProvider-D-w9-GZb.js.map +0 -1
- package/dist/index-BFa7H-uO.js.map +0 -1
- package/dist/index-DnoYi4f8.cjs.map +0 -1
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
const server = require("next/server");
|
|
3
3
|
const fs = require("fs/promises");
|
|
4
|
-
const parser = require("@babel/parser");
|
|
5
4
|
const anthropic = require("@langchain/anthropic");
|
|
6
5
|
const messages = require("@langchain/core/messages");
|
|
7
6
|
const path = require("path");
|
|
7
|
+
const parser = require("@babel/parser");
|
|
8
8
|
const traverse = require("@babel/traverse");
|
|
9
9
|
const t = require("@babel/types");
|
|
10
10
|
const sourceMap = require("@jridgewell/source-map");
|
|
11
|
+
const pathUtils = require("./path-utils-DYzEWUGy.cjs");
|
|
11
12
|
const crypto = require("crypto");
|
|
12
13
|
function _interopNamespaceDefault(e) {
|
|
13
14
|
const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
|
|
@@ -84,9 +85,74 @@ function parseFile(content) {
|
|
|
84
85
|
return null;
|
|
85
86
|
}
|
|
86
87
|
}
|
|
88
|
+
function extractComponentName(ast) {
|
|
89
|
+
let componentName = null;
|
|
90
|
+
traverse(ast, {
|
|
91
|
+
ExportDefaultDeclaration(path2) {
|
|
92
|
+
var _a;
|
|
93
|
+
if (t__namespace.isFunctionDeclaration(path2.node.declaration)) {
|
|
94
|
+
componentName = ((_a = path2.node.declaration.id) == null ? void 0 : _a.name) || null;
|
|
95
|
+
} else if (t__namespace.isArrowFunctionExpression(path2.node.declaration)) {
|
|
96
|
+
componentName = "default";
|
|
97
|
+
} else if (t__namespace.isIdentifier(path2.node.declaration)) {
|
|
98
|
+
componentName = path2.node.declaration.name;
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
ExportNamedDeclaration(path2) {
|
|
102
|
+
var _a;
|
|
103
|
+
if (t__namespace.isFunctionDeclaration(path2.node.declaration)) {
|
|
104
|
+
componentName = ((_a = path2.node.declaration.id) == null ? void 0 : _a.name) || null;
|
|
105
|
+
} else if (t__namespace.isVariableDeclaration(path2.node.declaration)) {
|
|
106
|
+
const declarator = path2.node.declaration.declarations[0];
|
|
107
|
+
if (t__namespace.isIdentifier(declarator.id)) {
|
|
108
|
+
componentName = declarator.id.name;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
return componentName;
|
|
114
|
+
}
|
|
115
|
+
function validateGeneratedCode(newCode, originalCode, fileContent) {
|
|
116
|
+
try {
|
|
117
|
+
const isFullComponent = /^(export\s+)?(default\s+)?function\s+\w+/.test(newCode.trim()) || /^(export\s+)?(default\s+)?const\s+\w+\s*=/.test(newCode.trim());
|
|
118
|
+
if (isFullComponent) {
|
|
119
|
+
let codeToValidate = newCode;
|
|
120
|
+
if (fileContent) {
|
|
121
|
+
const interfaceMatches = fileContent.match(
|
|
122
|
+
/^(interface|type)\s+\w+[^}]*\}/gm
|
|
123
|
+
);
|
|
124
|
+
if (interfaceMatches) {
|
|
125
|
+
codeToValidate = interfaceMatches.join("\n\n") + "\n\n" + newCode;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
parser__namespace.parse(codeToValidate, {
|
|
129
|
+
sourceType: "module",
|
|
130
|
+
plugins: ["jsx", "typescript"]
|
|
131
|
+
});
|
|
132
|
+
} else {
|
|
133
|
+
const wrapped = `function _() { return (${newCode}); }`;
|
|
134
|
+
parser__namespace.parse(wrapped, {
|
|
135
|
+
sourceType: "module",
|
|
136
|
+
plugins: ["jsx", "typescript"]
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
} catch (e) {
|
|
140
|
+
console.error("Generated code parse error:", e);
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
const origBraces = (originalCode.match(/[{}]/g) || []).length;
|
|
144
|
+
const newBraces = (newCode.match(/[{}]/g) || []).length;
|
|
145
|
+
const origTags = (originalCode.match(/[<>]/g) || []).length;
|
|
146
|
+
const newTags = (newCode.match(/[<>]/g) || []).length;
|
|
147
|
+
if (Math.abs(origBraces - newBraces) > 4 || Math.abs(origTags - newTags) > 4) {
|
|
148
|
+
console.warn(
|
|
149
|
+
`Structure changed significantly: braces ${origBraces}->${newBraces}, tags ${origTags}->${newTags}`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
87
154
|
function findTargetElement(ast, fileContent, options) {
|
|
88
155
|
const { componentName, lineNumber, elementContext } = options;
|
|
89
|
-
const matches = [];
|
|
90
156
|
let componentNode = null;
|
|
91
157
|
let componentStart = 0;
|
|
92
158
|
let componentEnd = Infinity;
|
|
@@ -137,6 +203,7 @@ function findTargetElement(ast, fileContent, options) {
|
|
|
137
203
|
componentEnd = fallback.end;
|
|
138
204
|
}
|
|
139
205
|
const allElementsByTag = /* @__PURE__ */ new Map();
|
|
206
|
+
const elementsAtLine = [];
|
|
140
207
|
traverse(ast, {
|
|
141
208
|
JSXElement(path2) {
|
|
142
209
|
const loc = path2.node.loc;
|
|
@@ -152,59 +219,89 @@ function findTargetElement(ast, fileContent, options) {
|
|
|
152
219
|
}
|
|
153
220
|
allElementsByTag.get(tagName).push({ node: path2.node, startLine, endLine, score: 0 });
|
|
154
221
|
}
|
|
155
|
-
if (
|
|
156
|
-
|
|
157
|
-
if (score > 0) {
|
|
158
|
-
matches.push({ node: path2.node, startLine, endLine, score });
|
|
159
|
-
}
|
|
160
|
-
} else if (Math.abs(startLine - lineNumber) <= 5) {
|
|
161
|
-
matches.push({
|
|
162
|
-
node: path2.node,
|
|
163
|
-
startLine,
|
|
164
|
-
endLine,
|
|
165
|
-
score: 100 - Math.abs(startLine - lineNumber)
|
|
166
|
-
});
|
|
222
|
+
if (startLine === lineNumber) {
|
|
223
|
+
elementsAtLine.push({ node: path2.node, startLine, endLine, score: 0 });
|
|
167
224
|
}
|
|
168
225
|
}
|
|
169
226
|
});
|
|
170
|
-
if (
|
|
171
|
-
if (
|
|
227
|
+
if (elementsAtLine.length > 0) {
|
|
228
|
+
if (elementsAtLine.length === 1) {
|
|
229
|
+
const target = elementsAtLine[0];
|
|
172
230
|
return {
|
|
173
|
-
startLine:
|
|
174
|
-
endLine:
|
|
231
|
+
startLine: target.startLine,
|
|
232
|
+
endLine: target.endLine,
|
|
175
233
|
componentStart,
|
|
176
234
|
componentEnd
|
|
177
235
|
};
|
|
178
236
|
}
|
|
179
|
-
|
|
237
|
+
if (elementContext) {
|
|
238
|
+
for (const elem of elementsAtLine) {
|
|
239
|
+
if (t__namespace.isJSXElement(elem.node)) {
|
|
240
|
+
const score = scoreElementMatch(elem.node, elementContext, fileContent);
|
|
241
|
+
elem.score = score;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
elementsAtLine.sort((a, b) => b.score - a.score);
|
|
245
|
+
if (elementsAtLine[0].score > 0) {
|
|
246
|
+
return {
|
|
247
|
+
startLine: elementsAtLine[0].startLine,
|
|
248
|
+
endLine: elementsAtLine[0].endLine,
|
|
249
|
+
componentStart,
|
|
250
|
+
componentEnd
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
startLine: elementsAtLine[0].startLine,
|
|
256
|
+
endLine: elementsAtLine[0].endLine,
|
|
257
|
+
componentStart,
|
|
258
|
+
componentEnd
|
|
259
|
+
};
|
|
180
260
|
}
|
|
181
261
|
if ((elementContext == null ? void 0 : elementContext.nthOfType) && elementContext.tagName) {
|
|
182
262
|
const allOfTag = allElementsByTag.get(elementContext.tagName);
|
|
183
263
|
if (allOfTag && allOfTag.length >= elementContext.nthOfType) {
|
|
184
264
|
const target = allOfTag[elementContext.nthOfType - 1];
|
|
185
|
-
console.log(
|
|
186
|
-
` Using nthOfType=${elementContext.nthOfType}: found ${allOfTag.length} <${elementContext.tagName}> elements`
|
|
187
|
-
);
|
|
188
265
|
return {
|
|
189
266
|
startLine: target.startLine,
|
|
190
267
|
endLine: target.endLine,
|
|
191
268
|
componentStart,
|
|
192
269
|
componentEnd
|
|
193
270
|
};
|
|
194
|
-
} else {
|
|
195
|
-
console.log(
|
|
196
|
-
` nthOfType=${elementContext.nthOfType} but only found ${(allOfTag == null ? void 0 : allOfTag.length) || 0} <${elementContext.tagName}> elements`
|
|
197
|
-
);
|
|
198
271
|
}
|
|
199
272
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
273
|
+
const nearbyElements = [];
|
|
274
|
+
traverse(ast, {
|
|
275
|
+
JSXElement(path2) {
|
|
276
|
+
const loc = path2.node.loc;
|
|
277
|
+
if (!loc) return;
|
|
278
|
+
const startLine = loc.start.line;
|
|
279
|
+
const endLine = loc.end.line;
|
|
280
|
+
if (startLine < componentStart || endLine > componentEnd) return;
|
|
281
|
+
if (Math.abs(startLine - lineNumber) <= 5) {
|
|
282
|
+
const score = elementContext ? scoreElementMatch(path2.node, elementContext, fileContent) : 100 - Math.abs(startLine - lineNumber);
|
|
283
|
+
nearbyElements.push({ node: path2.node, startLine, endLine, score });
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
if (nearbyElements.length > 0) {
|
|
288
|
+
nearbyElements.sort((a, b) => b.score - a.score);
|
|
289
|
+
return {
|
|
290
|
+
startLine: nearbyElements[0].startLine,
|
|
291
|
+
endLine: nearbyElements[0].endLine,
|
|
292
|
+
componentStart,
|
|
293
|
+
componentEnd
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
if (componentNode && componentStart > 0) {
|
|
297
|
+
return {
|
|
298
|
+
startLine: componentStart,
|
|
299
|
+
endLine: componentEnd,
|
|
300
|
+
componentStart,
|
|
301
|
+
componentEnd
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
return null;
|
|
208
305
|
}
|
|
209
306
|
function scoreElementMatch(node, context, fileContent) {
|
|
210
307
|
let score = 0;
|
|
@@ -288,7 +385,8 @@ async function handleEdit(req) {
|
|
|
288
385
|
componentName,
|
|
289
386
|
suggestion,
|
|
290
387
|
elementContext,
|
|
291
|
-
editHistory
|
|
388
|
+
editHistory,
|
|
389
|
+
parentInstance
|
|
292
390
|
} = body;
|
|
293
391
|
const projectRoot = process.cwd();
|
|
294
392
|
const normalizedPath = normalizePath(filePath);
|
|
@@ -300,7 +398,6 @@ async function handleEdit(req) {
|
|
|
300
398
|
);
|
|
301
399
|
}
|
|
302
400
|
const fileContent = await fs.readFile(absolutePath, "utf-8");
|
|
303
|
-
console.log(`[/edit] componentName="${componentName}", filePath="${filePath}"`);
|
|
304
401
|
const ast = parseFile(fileContent);
|
|
305
402
|
if (!ast) {
|
|
306
403
|
return server.NextResponse.json(
|
|
@@ -319,20 +416,10 @@ async function handleEdit(req) {
|
|
|
319
416
|
{ status: 400 }
|
|
320
417
|
);
|
|
321
418
|
}
|
|
322
|
-
console.log(
|
|
323
|
-
`📍 Found element <${(elementContext == null ? void 0 : elementContext.tagName) || "component"}> at lines ${target.startLine}-${target.endLine} (component: ${target.componentStart}-${target.componentEnd})`
|
|
324
|
-
);
|
|
325
|
-
console.log(
|
|
326
|
-
` Element context: tagName=${elementContext == null ? void 0 : elementContext.tagName}, nthOfType=${elementContext == null ? void 0 : elementContext.nthOfType}, textContent="${elementContext == null ? void 0 : elementContext.textContent}"`
|
|
327
|
-
);
|
|
328
419
|
const lines = fileContent.split("\n");
|
|
329
|
-
const foundElementCode = lines.slice(target.startLine - 1, Math.min(target.startLine + 2, target.endLine)).join("\n");
|
|
330
|
-
console.log(` Found element preview:
|
|
331
|
-
${foundElementCode}`);
|
|
332
420
|
const targetCode = lines.slice(target.startLine - 1, target.endLine).join("\n");
|
|
333
421
|
const baseIndentation = ((_a = lines[target.startLine - 1].match(/^(\s*)/)) == null ? void 0 : _a[1]) || "";
|
|
334
422
|
if (target.componentStart <= 0 || target.componentEnd === Infinity) {
|
|
335
|
-
console.error("❌ Invalid component bounds detected");
|
|
336
423
|
return server.NextResponse.json({
|
|
337
424
|
success: false,
|
|
338
425
|
error: `Could not determine component bounds. Component: ${target.componentStart}-${target.componentEnd}`
|
|
@@ -370,7 +457,8 @@ ${foundElementCode}`);
|
|
|
370
457
|
targetEndLine: target.endLine,
|
|
371
458
|
componentStart: target.componentStart,
|
|
372
459
|
componentEnd: target.componentEnd,
|
|
373
|
-
editHistory: editHistory || []
|
|
460
|
+
editHistory: editHistory || [],
|
|
461
|
+
parentInstance
|
|
374
462
|
});
|
|
375
463
|
if (!newCode) {
|
|
376
464
|
return server.NextResponse.json({
|
|
@@ -378,38 +466,56 @@ ${foundElementCode}`);
|
|
|
378
466
|
error: "AI failed to generate valid edit"
|
|
379
467
|
});
|
|
380
468
|
}
|
|
381
|
-
|
|
382
|
-
|
|
469
|
+
const parentInstanceMatch = newCode.match(/\/\/ EDIT_PARENT_INSTANCE\s*\n([\s\S]+)/);
|
|
470
|
+
if (parentInstanceMatch && parentInstance) {
|
|
471
|
+
const parentCode = parentInstanceMatch[1].trim();
|
|
472
|
+
const parentNormalizedPath = normalizePath(parentInstance.filePath);
|
|
473
|
+
const parentAbsolutePath = await resolveFilePath(projectRoot, parentNormalizedPath);
|
|
474
|
+
if (!parentAbsolutePath) {
|
|
475
|
+
return server.NextResponse.json({
|
|
476
|
+
success: false,
|
|
477
|
+
error: `Parent file not found: ${parentNormalizedPath}`
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
const parentFileContent = await fs.readFile(parentAbsolutePath, "utf-8");
|
|
481
|
+
const parentLines = parentFileContent.split("\n");
|
|
482
|
+
const newParentLines = [...parentLines];
|
|
483
|
+
newParentLines.splice(
|
|
484
|
+
parentInstance.lineStart - 1,
|
|
485
|
+
parentInstance.lineEnd - parentInstance.lineStart + 1,
|
|
486
|
+
...parentCode.split("\n")
|
|
487
|
+
);
|
|
488
|
+
await fs.writeFile(parentAbsolutePath, newParentLines.join("\n"), "utf-8");
|
|
489
|
+
return server.NextResponse.json({
|
|
490
|
+
success: true,
|
|
491
|
+
fileSnapshot: parentFileContent,
|
|
492
|
+
generatedCode: parentCode,
|
|
493
|
+
modifiedLines: {
|
|
494
|
+
start: parentInstance.lineStart,
|
|
495
|
+
end: parentInstance.lineEnd
|
|
496
|
+
},
|
|
497
|
+
editedFile: parentInstance.filePath
|
|
498
|
+
// Indicate which file was edited
|
|
499
|
+
});
|
|
500
|
+
}
|
|
383
501
|
const fullComponentMatch = newCode.match(/\/\/ FULL_COMPONENT\s*\n([\s\S]+)/);
|
|
384
502
|
let codeToApply = newCode;
|
|
385
503
|
let startLineToReplace = target.startLine;
|
|
386
504
|
let endLineToReplace = target.endLine;
|
|
505
|
+
const isFullComponentDeclaration = /^(export\s+)?(default\s+)?function\s+\w+/.test(newCode.trim()) || /^(export\s+)?const\s+\w+\s*=/.test(newCode.trim());
|
|
387
506
|
if (fullComponentMatch) {
|
|
388
|
-
console.log("Found // FULL_COMPONENT marker, extracting full component code");
|
|
389
507
|
codeToApply = fullComponentMatch[1].trim();
|
|
390
508
|
startLineToReplace = target.componentStart;
|
|
391
509
|
endLineToReplace = target.componentEnd;
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
console.log("Extracted component code (first 300 chars):", codeToApply.substring(0, 300));
|
|
397
|
-
} else {
|
|
398
|
-
console.log("No // FULL_COMPONENT marker found, treating as target element modification");
|
|
399
|
-
console.log(
|
|
400
|
-
`🔄 AI returned target element modification (lines ${startLineToReplace}-${endLineToReplace})`
|
|
401
|
-
);
|
|
510
|
+
} else if (isFullComponentDeclaration && target.startLine !== target.componentStart) {
|
|
511
|
+
codeToApply = newCode;
|
|
512
|
+
startLineToReplace = target.componentStart;
|
|
513
|
+
endLineToReplace = target.componentEnd;
|
|
402
514
|
}
|
|
403
|
-
console.log("Code to apply (first 200 chars):", codeToApply.substring(0, 200));
|
|
404
515
|
if (!validateGeneratedCode(codeToApply, targetCode, fileContent)) {
|
|
405
|
-
console.error("❌ Generated code failed validation");
|
|
406
|
-
console.error("=== Generated code START ===");
|
|
407
|
-
console.error(codeToApply);
|
|
408
|
-
console.error("=== Generated code END ===");
|
|
409
|
-
console.error(`Length: ${codeToApply.length} chars, ${codeToApply.split("\n").length} lines`);
|
|
410
516
|
return server.NextResponse.json({
|
|
411
517
|
success: false,
|
|
412
|
-
error: "Generated code is invalid
|
|
518
|
+
error: "Generated code is invalid"
|
|
413
519
|
});
|
|
414
520
|
}
|
|
415
521
|
const newLines = [...lines];
|
|
@@ -419,7 +525,6 @@ ${foundElementCode}`);
|
|
|
419
525
|
...codeToApply.split("\n")
|
|
420
526
|
);
|
|
421
527
|
await fs.writeFile(absolutePath, newLines.join("\n"), "utf-8");
|
|
422
|
-
console.log(`✅ Updated ${normalizedPath}`);
|
|
423
528
|
return server.NextResponse.json({
|
|
424
529
|
success: true,
|
|
425
530
|
fileSnapshot: fileContent,
|
|
@@ -429,10 +534,11 @@ ${foundElementCode}`);
|
|
|
429
534
|
modifiedLines: {
|
|
430
535
|
start: startLineToReplace,
|
|
431
536
|
end: endLineToReplace
|
|
432
|
-
}
|
|
537
|
+
},
|
|
538
|
+
editedFile: filePath
|
|
539
|
+
// Indicate which file was edited
|
|
433
540
|
});
|
|
434
541
|
} catch (error) {
|
|
435
|
-
console.error("AI Editor error:", error);
|
|
436
542
|
return server.NextResponse.json(
|
|
437
543
|
{ success: false, error: String(error) },
|
|
438
544
|
{ status: 500 }
|
|
@@ -444,7 +550,8 @@ async function generateEdit(options) {
|
|
|
444
550
|
fullComponentCode,
|
|
445
551
|
suggestion,
|
|
446
552
|
baseIndentation,
|
|
447
|
-
editHistory
|
|
553
|
+
editHistory,
|
|
554
|
+
parentInstance
|
|
448
555
|
} = options;
|
|
449
556
|
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
450
557
|
if (!apiKey) throw new Error("ANTHROPIC_API_KEY not set");
|
|
@@ -457,25 +564,29 @@ async function generateEdit(options) {
|
|
|
457
564
|
const systemPrompt = `You are a precise code editor for React/JSX components.
|
|
458
565
|
|
|
459
566
|
WHAT YOU'LL SEE:
|
|
460
|
-
- Full component
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
567
|
+
- Component Definition: Full code of the clicked component with line annotations
|
|
568
|
+
* The clicked element is marked with "// ← CLICKED ELEMENT STARTS" and "// ← CLICKED ELEMENT ENDS"
|
|
569
|
+
* These are just annotations - NOT part of the actual code
|
|
570
|
+
${parentInstance ? `- Parent Instance: Where this component is used, with "// ← COMPONENT USAGE" marking the usage line` : ""}
|
|
464
571
|
|
|
465
|
-
YOUR
|
|
466
|
-
Modify ONLY the marked element (unless the request requires changing its parent/wrapper).
|
|
572
|
+
YOUR DECISION - Choose ONE approach based on the user's request:
|
|
467
573
|
|
|
468
|
-
|
|
469
|
-
-
|
|
470
|
-
-
|
|
574
|
+
1. EDIT COMPONENT DEFINITION (most common):
|
|
575
|
+
- Modify styling, layout, or behavior that should apply to ALL instances
|
|
576
|
+
- Example requests: "make the button blue", "add padding", "change font size"
|
|
577
|
+
- Output: Just the modified element OR "// FULL_COMPONENT\\n" + complete modified component
|
|
578
|
+
|
|
579
|
+
${parentInstance ? `2. EDIT PARENT INSTANCE (when user wants to modify THIS specific usage):
|
|
580
|
+
- Change props, text, or configuration for THIS specific instance only
|
|
581
|
+
- Example requests: "change this title to...", "remove this card", "add another button here"
|
|
582
|
+
- Output: "// EDIT_PARENT_INSTANCE\\n" + complete modified parent component` : ""}
|
|
471
583
|
|
|
472
584
|
RULES:
|
|
473
585
|
- Output ONLY code, no explanations
|
|
474
586
|
- No markdown fences
|
|
475
|
-
- Do NOT include
|
|
587
|
+
- Do NOT include annotation comments in your output
|
|
476
588
|
- Preserve indentation
|
|
477
|
-
- Make minimal changes
|
|
478
|
-
- Do NOT modify unrelated elements`;
|
|
589
|
+
- Make minimal changes`;
|
|
479
590
|
let userPrompt = "";
|
|
480
591
|
if (editHistory.length > 0) {
|
|
481
592
|
userPrompt += "Previous edits made to this element:\n";
|
|
@@ -485,16 +596,36 @@ RULES:
|
|
|
485
596
|
});
|
|
486
597
|
userPrompt += "\n";
|
|
487
598
|
}
|
|
488
|
-
userPrompt += `
|
|
599
|
+
userPrompt += `COMPONENT DEFINITION (clicked element is annotated):
|
|
489
600
|
|
|
490
601
|
\`\`\`jsx
|
|
491
602
|
${fullComponentCode}
|
|
492
603
|
\`\`\`
|
|
604
|
+
`;
|
|
605
|
+
if (parentInstance) {
|
|
606
|
+
const parentLines = parentInstance.content.split("\n");
|
|
607
|
+
const annotatedParentLines = parentLines.map((line, idx) => {
|
|
608
|
+
const lineNum = parentInstance.lineStart + idx;
|
|
609
|
+
const isUsageLine = lineNum >= parentInstance.usageLineStart && lineNum <= parentInstance.usageLineEnd;
|
|
610
|
+
return line + (isUsageLine ? " // ← COMPONENT USAGE" : "");
|
|
611
|
+
});
|
|
612
|
+
userPrompt += `
|
|
613
|
+
PARENT INSTANCE (where component is used - in ${parentInstance.filePath}):
|
|
493
614
|
|
|
615
|
+
\`\`\`jsx
|
|
616
|
+
${annotatedParentLines.join("\n")}
|
|
617
|
+
\`\`\`
|
|
618
|
+
`;
|
|
619
|
+
}
|
|
620
|
+
userPrompt += `
|
|
494
621
|
User request: "${suggestion}"
|
|
495
|
-
${editHistory.length > 0 ? "
|
|
622
|
+
${editHistory.length > 0 ? "(Build upon previous changes)" : ""}
|
|
623
|
+
|
|
624
|
+
${parentInstance ? `Decide whether to:
|
|
625
|
+
1. Edit the component definition (for changes that affect ALL instances)
|
|
626
|
+
2. Edit the parent instance (for changes specific to THIS usage)
|
|
496
627
|
|
|
497
|
-
Modify the annotated element to fulfill this request. Remember: do NOT include the annotation comments in your output
|
|
628
|
+
Then output the appropriate code with the correct marker.` : `Modify the annotated element to fulfill this request. Remember: do NOT include the annotation comments in your output.`}`;
|
|
498
629
|
try {
|
|
499
630
|
const response = await model.invoke([
|
|
500
631
|
new messages.SystemMessage(systemPrompt),
|
|
@@ -504,12 +635,18 @@ Modify the annotated element to fulfill this request. Remember: do NOT include t
|
|
|
504
635
|
code = cleanGeneratedCode(code, baseIndentation);
|
|
505
636
|
return code || null;
|
|
506
637
|
} catch (e) {
|
|
507
|
-
console.error("AI generation error:", e);
|
|
508
638
|
return null;
|
|
509
639
|
}
|
|
510
640
|
}
|
|
511
641
|
function cleanGeneratedCode(code, baseIndentation) {
|
|
512
642
|
var _a, _b, _c;
|
|
643
|
+
const isParentEdit = code.trim().startsWith("// EDIT_PARENT_INSTANCE");
|
|
644
|
+
if (isParentEdit) {
|
|
645
|
+
const marker2 = "// EDIT_PARENT_INSTANCE\n";
|
|
646
|
+
code = code.replace(/^\/\/ EDIT_PARENT_INSTANCE\n?/, "");
|
|
647
|
+
code = code.replace(/^```[\w]*\n?/gm, "").replace(/\n?```$/gm, "").replace(/\s*\/\/ ← COMPONENT USAGE/g, "").trim();
|
|
648
|
+
return marker2 + code;
|
|
649
|
+
}
|
|
513
650
|
const isFullComponent = code.trim().startsWith("// FULL_COMPONENT");
|
|
514
651
|
let marker = "";
|
|
515
652
|
if (isFullComponent) {
|
|
@@ -540,43 +677,6 @@ function cleanGeneratedCode(code, baseIndentation) {
|
|
|
540
677
|
}
|
|
541
678
|
return marker + code;
|
|
542
679
|
}
|
|
543
|
-
function validateGeneratedCode(newCode, originalCode, fileContent) {
|
|
544
|
-
try {
|
|
545
|
-
const isFullComponent = /^(export\s+)?(default\s+)?function\s+\w+/.test(newCode.trim()) || /^(export\s+)?(default\s+)?const\s+\w+\s*=/.test(newCode.trim());
|
|
546
|
-
if (isFullComponent) {
|
|
547
|
-
let codeToValidate = newCode;
|
|
548
|
-
if (fileContent) {
|
|
549
|
-
const interfaceMatches = fileContent.match(/^(interface|type)\s+\w+[^}]*\}/gm);
|
|
550
|
-
if (interfaceMatches) {
|
|
551
|
-
codeToValidate = interfaceMatches.join("\n\n") + "\n\n" + newCode;
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
parser__namespace.parse(codeToValidate, {
|
|
555
|
-
sourceType: "module",
|
|
556
|
-
plugins: ["jsx", "typescript"]
|
|
557
|
-
});
|
|
558
|
-
} else {
|
|
559
|
-
const wrapped = `function _() { return (${newCode}); }`;
|
|
560
|
-
parser__namespace.parse(wrapped, {
|
|
561
|
-
sourceType: "module",
|
|
562
|
-
plugins: ["jsx", "typescript"]
|
|
563
|
-
});
|
|
564
|
-
}
|
|
565
|
-
} catch (e) {
|
|
566
|
-
console.error("Generated code parse error:", e);
|
|
567
|
-
return false;
|
|
568
|
-
}
|
|
569
|
-
const origBraces = (originalCode.match(/[{}]/g) || []).length;
|
|
570
|
-
const newBraces = (newCode.match(/[{}]/g) || []).length;
|
|
571
|
-
const origTags = (originalCode.match(/[<>]/g) || []).length;
|
|
572
|
-
const newTags = (newCode.match(/[<>]/g) || []).length;
|
|
573
|
-
if (Math.abs(origBraces - newBraces) > 4 || Math.abs(origTags - newTags) > 4) {
|
|
574
|
-
console.warn(
|
|
575
|
-
`Structure changed significantly: braces ${origBraces}->${newBraces}, tags ${origTags}->${newTags}`
|
|
576
|
-
);
|
|
577
|
-
}
|
|
578
|
-
return true;
|
|
579
|
-
}
|
|
580
680
|
function parseDebugStack(stack) {
|
|
581
681
|
if (!stack) return null;
|
|
582
682
|
const stackStr = typeof stack === "string" ? stack : stack.stack || String(stack);
|
|
@@ -601,8 +701,8 @@ function parseDebugStack(stack) {
|
|
|
601
701
|
chunkId = chunkMatch[1];
|
|
602
702
|
filePath = filePath.replace(/\?[^:]*$/, "");
|
|
603
703
|
}
|
|
604
|
-
filePath =
|
|
605
|
-
if (!
|
|
704
|
+
filePath = pathUtils.cleanPath(filePath);
|
|
705
|
+
if (!pathUtils.shouldSkipPath(filePath)) {
|
|
606
706
|
console.log("parseDebugStack extracted:", { filePath, line, column });
|
|
607
707
|
return { filePath, line, column, chunkId };
|
|
608
708
|
}
|
|
@@ -614,46 +714,49 @@ function parseDebugStack(stack) {
|
|
|
614
714
|
);
|
|
615
715
|
return null;
|
|
616
716
|
}
|
|
617
|
-
function
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
717
|
+
function parseDebugStackFrames(stack) {
|
|
718
|
+
if (!stack) return [];
|
|
719
|
+
const stackStr = typeof stack === "string" ? stack : stack.stack || String(stack);
|
|
720
|
+
const frames = stackStr.split("\n");
|
|
721
|
+
const skipPatterns = [
|
|
722
|
+
"node_modules",
|
|
723
|
+
"SegmentViewNode",
|
|
724
|
+
"LayoutRouter",
|
|
725
|
+
"ErrorBoundary",
|
|
726
|
+
"fakeJSXCallSite",
|
|
727
|
+
"react_stack_bottom_frame"
|
|
728
|
+
];
|
|
729
|
+
const positions = [];
|
|
730
|
+
for (const frame of frames) {
|
|
731
|
+
if (skipPatterns.some((p) => frame.includes(p))) continue;
|
|
732
|
+
const match = frame.match(/at\s+(\w+)\s+\((.+?):(\d+):(\d+)\)?$/);
|
|
733
|
+
if (match) {
|
|
734
|
+
match[1];
|
|
735
|
+
let filePath = match[2];
|
|
736
|
+
const line = parseInt(match[3], 10);
|
|
737
|
+
const column = parseInt(match[4], 10);
|
|
738
|
+
let chunkId;
|
|
739
|
+
const chunkMatch = filePath.match(/\?([^:]+)$/);
|
|
740
|
+
if (chunkMatch) {
|
|
741
|
+
chunkId = chunkMatch[1];
|
|
742
|
+
filePath = filePath.replace(/\?[^:]*$/, "");
|
|
743
|
+
}
|
|
744
|
+
filePath = pathUtils.cleanPath(filePath);
|
|
745
|
+
if (!pathUtils.shouldSkipPath(filePath)) {
|
|
746
|
+
positions.push({ filePath, line, column, chunkId });
|
|
747
|
+
if (positions.length >= 2) break;
|
|
748
|
+
}
|
|
626
749
|
}
|
|
627
|
-
} catch (e) {
|
|
628
|
-
console.warn("Failed to decode URL-encoded path:", cleaned, e);
|
|
629
|
-
}
|
|
630
|
-
cleaned = cleaned.replace(/^webpack-internal:\/\/\/\([^)]+\)\/\.\//, "").replace(/^webpack-internal:\/\/\/\([^)]+\)\//, "").replace(/^webpack-internal:\/\//, "").replace(/^webpack:\/\/[^/]*\//, "").replace(/^\([^)]+\)\//, "").replace(/\?.*$/, "");
|
|
631
|
-
if (cleaned.startsWith(".next/") || path.isAbsolute(cleaned)) {
|
|
632
|
-
return cleaned;
|
|
633
750
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
function cleanPath(p) {
|
|
637
|
-
return cleanPathTurbopack(p);
|
|
638
|
-
}
|
|
639
|
-
function normalizeSourcePath(source, projectRoot) {
|
|
640
|
-
let cleaned = cleanPath(source);
|
|
641
|
-
cleaned = cleaned.replace(/\\/g, "/");
|
|
642
|
-
if (cleaned.startsWith(projectRoot)) {
|
|
643
|
-
cleaned = cleaned.substring(projectRoot.length + 1);
|
|
644
|
-
}
|
|
645
|
-
return cleaned.replace(/^\/+/, "");
|
|
646
|
-
}
|
|
647
|
-
function shouldSkip(p) {
|
|
648
|
-
if (!p) return true;
|
|
649
|
-
return ["node_modules", "next/dist", "react-dom"].some((s) => p.includes(s));
|
|
751
|
+
console.log(`parseDebugStackFrames extracted ${positions.length} frames:`, positions);
|
|
752
|
+
return positions;
|
|
650
753
|
}
|
|
651
754
|
async function resolveOriginalPosition(compiledPos, projectRoot) {
|
|
652
755
|
try {
|
|
653
756
|
console.log("resolveOriginalPosition called with:", compiledPos);
|
|
654
757
|
let compiledFilePath = compiledPos.filePath;
|
|
655
|
-
compiledFilePath =
|
|
656
|
-
console.log("After
|
|
758
|
+
compiledFilePath = pathUtils.cleanPath(compiledFilePath);
|
|
759
|
+
console.log("After cleanPath:", compiledFilePath);
|
|
657
760
|
if (compiledFilePath.startsWith("http://") || compiledFilePath.startsWith("https://")) {
|
|
658
761
|
const url = new URL(compiledFilePath);
|
|
659
762
|
const pathname = url.pathname;
|
|
@@ -770,7 +873,7 @@ async function resolveFromSourceMap(sourceMapPath, compiledPos, projectRoot) {
|
|
|
770
873
|
column: adjustedColumn
|
|
771
874
|
});
|
|
772
875
|
if (originalPos.source && originalPos.line !== null) {
|
|
773
|
-
const source = normalizeSourcePath(
|
|
876
|
+
const source = pathUtils.normalizeSourcePath(
|
|
774
877
|
originalPos.source || "",
|
|
775
878
|
projectRoot
|
|
776
879
|
);
|
|
@@ -793,7 +896,7 @@ async function resolveFromSourceMap(sourceMapPath, compiledPos, projectRoot) {
|
|
|
793
896
|
column: compiledPos.column
|
|
794
897
|
});
|
|
795
898
|
if (originalPos.source && originalPos.line !== null) {
|
|
796
|
-
const source = normalizeSourcePath(
|
|
899
|
+
const source = pathUtils.normalizeSourcePath(
|
|
797
900
|
originalPos.source || "",
|
|
798
901
|
projectRoot
|
|
799
902
|
);
|
|
@@ -826,7 +929,6 @@ async function getOriginalPositionFromDebugStack(debugStack, projectRoot) {
|
|
|
826
929
|
return await resolveOriginalPosition(compiledPos, projectRoot);
|
|
827
930
|
}
|
|
828
931
|
async function handleRead(req) {
|
|
829
|
-
var _a;
|
|
830
932
|
const devModeError = validateDevMode();
|
|
831
933
|
if (devModeError) return devModeError;
|
|
832
934
|
const { searchParams } = new URL(req.url);
|
|
@@ -843,6 +945,11 @@ async function handleRead(req) {
|
|
|
843
945
|
textContent: textContent || void 0,
|
|
844
946
|
className: className || void 0
|
|
845
947
|
} : void 0;
|
|
948
|
+
const parentFilePath = searchParams.get("parentFilePath") || "";
|
|
949
|
+
const parentLine = parseInt(searchParams.get("parentLine") || "0");
|
|
950
|
+
const parentComponentName = searchParams.get("parentComponentName") || "";
|
|
951
|
+
const parentDebugStack = searchParams.get("parentDebugStack") || "";
|
|
952
|
+
const childKey = searchParams.get("childKey") || "";
|
|
846
953
|
const projectRoot = process.cwd();
|
|
847
954
|
if (debugStack) {
|
|
848
955
|
const compiledPos = parseDebugStack(debugStack);
|
|
@@ -873,21 +980,7 @@ async function handleRead(req) {
|
|
|
873
980
|
{ status: 400 }
|
|
874
981
|
);
|
|
875
982
|
}
|
|
876
|
-
|
|
877
|
-
traverse(ast, {
|
|
878
|
-
ExportDefaultDeclaration(path2) {
|
|
879
|
-
var _a2;
|
|
880
|
-
if (t__namespace.isFunctionDeclaration(path2.node.declaration)) {
|
|
881
|
-
componentName = ((_a2 = path2.node.declaration.id) == null ? void 0 : _a2.name) || "";
|
|
882
|
-
}
|
|
883
|
-
},
|
|
884
|
-
ExportNamedDeclaration(path2) {
|
|
885
|
-
var _a2;
|
|
886
|
-
if (t__namespace.isFunctionDeclaration(path2.node.declaration)) {
|
|
887
|
-
componentName = ((_a2 = path2.node.declaration.id) == null ? void 0 : _a2.name) || "";
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
});
|
|
983
|
+
const componentName = extractComponentName(ast);
|
|
891
984
|
if (!componentName) {
|
|
892
985
|
return server.NextResponse.json({
|
|
893
986
|
success: true,
|
|
@@ -909,28 +1002,79 @@ async function handleRead(req) {
|
|
|
909
1002
|
lineEnd: content.split("\n").length
|
|
910
1003
|
});
|
|
911
1004
|
}
|
|
912
|
-
console.log(`[/read] Found element:`);
|
|
913
|
-
console.log(
|
|
914
|
-
` Component: ${componentName} (lines ${target.componentStart}-${target.componentEnd})`
|
|
915
|
-
);
|
|
916
|
-
console.log(` Target element: lines ${target.startLine}-${target.endLine}`);
|
|
917
|
-
console.log(
|
|
918
|
-
` Element context: tagName=${elementContext == null ? void 0 : elementContext.tagName}, nthOfType=${elementContext == null ? void 0 : elementContext.nthOfType}`
|
|
919
|
-
);
|
|
920
|
-
const foundLines = content.split("\n").slice(
|
|
921
|
-
target.startLine - 1,
|
|
922
|
-
Math.min(target.startLine + 2, target.endLine)
|
|
923
|
-
);
|
|
924
|
-
console.log(` Preview: ${(_a = foundLines[0]) == null ? void 0 : _a.trim()}`);
|
|
925
|
-
console.log(
|
|
926
|
-
` textContent="${elementContext == null ? void 0 : elementContext.textContent}", className="${elementContext == null ? void 0 : elementContext.className}"`
|
|
927
|
-
);
|
|
928
1005
|
const lines = content.split("\n");
|
|
929
1006
|
const componentLines = lines.slice(
|
|
930
1007
|
target.componentStart - 1,
|
|
931
1008
|
target.componentEnd
|
|
932
1009
|
);
|
|
933
1010
|
const preview = componentLines.join("\n");
|
|
1011
|
+
let parentInstance = null;
|
|
1012
|
+
if (parentDebugStack) {
|
|
1013
|
+
try {
|
|
1014
|
+
let resolvedParentPath = parentFilePath;
|
|
1015
|
+
let resolvedParentLine = parentLine;
|
|
1016
|
+
let resolvedParentComponentName = parentComponentName;
|
|
1017
|
+
if (resolvedParentComponentName && parentDebugStack) {
|
|
1018
|
+
const compiledPos = parseDebugStack(parentDebugStack);
|
|
1019
|
+
if (compiledPos) {
|
|
1020
|
+
const originalPos = await resolveOriginalPosition(
|
|
1021
|
+
compiledPos,
|
|
1022
|
+
projectRoot
|
|
1023
|
+
);
|
|
1024
|
+
if (originalPos) {
|
|
1025
|
+
resolvedParentPath = originalPos.source;
|
|
1026
|
+
resolvedParentLine = originalPos.line;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
const normalizedParentPath = normalizePath(resolvedParentPath);
|
|
1031
|
+
const absoluteParentPath = await resolveFilePath(
|
|
1032
|
+
projectRoot,
|
|
1033
|
+
normalizedParentPath
|
|
1034
|
+
);
|
|
1035
|
+
if (absoluteParentPath && resolvedParentComponentName) {
|
|
1036
|
+
const parentContent = await fs.readFile(absoluteParentPath, "utf-8");
|
|
1037
|
+
const parentAst = parseFile(parentContent);
|
|
1038
|
+
if (parentAst) {
|
|
1039
|
+
let nthOfType2 = void 0;
|
|
1040
|
+
if (childKey) {
|
|
1041
|
+
const keyAsNumber = parseInt(childKey, 10);
|
|
1042
|
+
if (!isNaN(keyAsNumber)) {
|
|
1043
|
+
nthOfType2 = keyAsNumber + 1;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
const parentTarget = findTargetElement(parentAst, parentContent, {
|
|
1047
|
+
componentName: resolvedParentComponentName,
|
|
1048
|
+
lineNumber: 0,
|
|
1049
|
+
// Don't use line number - rely on nthOfType to find correct instance
|
|
1050
|
+
elementContext: {
|
|
1051
|
+
tagName: componentName,
|
|
1052
|
+
// Search for child component usage
|
|
1053
|
+
nthOfType: nthOfType2
|
|
1054
|
+
// Find specific instance if key is numeric
|
|
1055
|
+
}
|
|
1056
|
+
});
|
|
1057
|
+
if (parentTarget) {
|
|
1058
|
+
const parentLines = parentContent.split("\n");
|
|
1059
|
+
const parentComponentLines = parentLines.slice(
|
|
1060
|
+
parentTarget.componentStart - 1,
|
|
1061
|
+
parentTarget.componentEnd
|
|
1062
|
+
);
|
|
1063
|
+
parentInstance = {
|
|
1064
|
+
filePath: resolvedParentPath,
|
|
1065
|
+
content: parentComponentLines.join("\n"),
|
|
1066
|
+
lineStart: parentTarget.componentStart,
|
|
1067
|
+
lineEnd: parentTarget.componentEnd,
|
|
1068
|
+
usageLineStart: parentTarget.startLine,
|
|
1069
|
+
usageLineEnd: parentTarget.endLine,
|
|
1070
|
+
componentName: resolvedParentComponentName
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
} catch (error) {
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
934
1078
|
return server.NextResponse.json({
|
|
935
1079
|
success: true,
|
|
936
1080
|
content: preview,
|
|
@@ -938,8 +1082,10 @@ async function handleRead(req) {
|
|
|
938
1082
|
lineEnd: target.componentEnd,
|
|
939
1083
|
targetStartLine: target.startLine,
|
|
940
1084
|
targetEndLine: target.endLine,
|
|
941
|
-
componentName
|
|
1085
|
+
componentName,
|
|
942
1086
|
// Return the actual component name parsed from code
|
|
1087
|
+
parentInstance
|
|
1088
|
+
// Optional: where this component is used
|
|
943
1089
|
});
|
|
944
1090
|
}
|
|
945
1091
|
async function handleUndo(req) {
|
|
@@ -995,37 +1141,58 @@ async function handleResolve(req) {
|
|
|
995
1141
|
{ status: 400 }
|
|
996
1142
|
);
|
|
997
1143
|
}
|
|
998
|
-
const
|
|
999
|
-
if (
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1144
|
+
const compiledFrames = parseDebugStackFrames(debugStack);
|
|
1145
|
+
if (compiledFrames.length === 0) {
|
|
1146
|
+
const compiledPos = parseDebugStack(debugStack);
|
|
1147
|
+
if (!compiledPos) {
|
|
1148
|
+
console.error("Could not parse debug stack:", debugStack);
|
|
1149
|
+
return server.NextResponse.json(
|
|
1150
|
+
{ success: false, error: "Could not parse stack" },
|
|
1151
|
+
{ status: 422 }
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
const originalPos = await resolveOriginalPosition(
|
|
1155
|
+
compiledPos,
|
|
1156
|
+
process.cwd()
|
|
1004
1157
|
);
|
|
1158
|
+
if (!originalPos) {
|
|
1159
|
+
return server.NextResponse.json(
|
|
1160
|
+
{ success: false, error: "Source map lookup failed" },
|
|
1161
|
+
{ status: 404 }
|
|
1162
|
+
);
|
|
1163
|
+
}
|
|
1164
|
+
return server.NextResponse.json({
|
|
1165
|
+
success: true,
|
|
1166
|
+
filePath: originalPos.source,
|
|
1167
|
+
lineNumber: originalPos.line,
|
|
1168
|
+
columnNumber: originalPos.column ?? 0
|
|
1169
|
+
});
|
|
1005
1170
|
}
|
|
1006
|
-
|
|
1007
|
-
const
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1171
|
+
const resolvedFrames = [];
|
|
1172
|
+
for (const frame of compiledFrames) {
|
|
1173
|
+
const originalPos = await resolveOriginalPosition(frame, process.cwd());
|
|
1174
|
+
if (originalPos) {
|
|
1175
|
+
resolvedFrames.push({
|
|
1176
|
+
filePath: originalPos.source,
|
|
1177
|
+
lineNumber: originalPos.line,
|
|
1178
|
+
columnNumber: originalPos.column ?? 0
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
if (resolvedFrames.length === 0) {
|
|
1018
1183
|
return server.NextResponse.json(
|
|
1019
|
-
{ success: false, error: "Source map lookup failed" },
|
|
1184
|
+
{ success: false, error: "Source map lookup failed for all frames" },
|
|
1020
1185
|
{ status: 404 }
|
|
1021
1186
|
);
|
|
1022
1187
|
}
|
|
1023
|
-
console.log("Resolved
|
|
1188
|
+
console.log("Resolved frames:", resolvedFrames);
|
|
1024
1189
|
return server.NextResponse.json({
|
|
1025
1190
|
success: true,
|
|
1026
|
-
filePath:
|
|
1027
|
-
lineNumber:
|
|
1028
|
-
columnNumber:
|
|
1191
|
+
filePath: resolvedFrames[0].filePath,
|
|
1192
|
+
lineNumber: resolvedFrames[0].lineNumber,
|
|
1193
|
+
columnNumber: resolvedFrames[0].columnNumber,
|
|
1194
|
+
frames: resolvedFrames
|
|
1195
|
+
// [componentDefinition, parentInstance]
|
|
1029
1196
|
});
|
|
1030
1197
|
} catch (error) {
|
|
1031
1198
|
console.error("Source resolve error:", error);
|
|
@@ -1110,6 +1277,209 @@ async function handleValidateSession(req) {
|
|
|
1110
1277
|
);
|
|
1111
1278
|
}
|
|
1112
1279
|
}
|
|
1280
|
+
const suggestionCache = /* @__PURE__ */ new Map();
|
|
1281
|
+
const CACHE_TTL = 3e4;
|
|
1282
|
+
function getCacheKey(params) {
|
|
1283
|
+
return `${params.componentName}:${params.elementTag || "div"}:${params.lastSuggestion || ""}`;
|
|
1284
|
+
}
|
|
1285
|
+
function getCachedSuggestions(key) {
|
|
1286
|
+
const cached = suggestionCache.get(key);
|
|
1287
|
+
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
|
|
1288
|
+
return cached.suggestions;
|
|
1289
|
+
}
|
|
1290
|
+
suggestionCache.delete(key);
|
|
1291
|
+
return null;
|
|
1292
|
+
}
|
|
1293
|
+
function cacheSuggestions(key, suggestions) {
|
|
1294
|
+
suggestionCache.set(key, { suggestions, timestamp: Date.now() });
|
|
1295
|
+
if (suggestionCache.size > 100) {
|
|
1296
|
+
const oldestKeys = Array.from(suggestionCache.entries()).sort((a, b) => a[1].timestamp - b[1].timestamp).slice(0, 20).map(([key2]) => key2);
|
|
1297
|
+
oldestKeys.forEach((key2) => suggestionCache.delete(key2));
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
const DEFAULT_SUGGESTIONS = [
|
|
1301
|
+
"Add padding",
|
|
1302
|
+
"Change color",
|
|
1303
|
+
"Make larger",
|
|
1304
|
+
"Add shadow",
|
|
1305
|
+
"Round corners",
|
|
1306
|
+
"Center content",
|
|
1307
|
+
"Add hover effect",
|
|
1308
|
+
"Adjust spacing"
|
|
1309
|
+
];
|
|
1310
|
+
async function handleSuggestions(req) {
|
|
1311
|
+
const devModeError = validateDevMode();
|
|
1312
|
+
if (devModeError) return devModeError;
|
|
1313
|
+
try {
|
|
1314
|
+
const { searchParams } = new URL(req.url);
|
|
1315
|
+
const componentName = searchParams.get("componentName") || "Component";
|
|
1316
|
+
const elementTag = searchParams.get("elementTag") || void 0;
|
|
1317
|
+
const className = searchParams.get("className") || void 0;
|
|
1318
|
+
const textContent = searchParams.get("textContent") || void 0;
|
|
1319
|
+
const lastSuggestion = searchParams.get("lastSuggestion") || void 0;
|
|
1320
|
+
const editHistoryStr = searchParams.get("editHistory") || void 0;
|
|
1321
|
+
const excludedSuggestionsStr = searchParams.get("excludedSuggestions") || void 0;
|
|
1322
|
+
let editHistory = [];
|
|
1323
|
+
if (editHistoryStr) {
|
|
1324
|
+
try {
|
|
1325
|
+
editHistory = JSON.parse(editHistoryStr);
|
|
1326
|
+
} catch (e) {
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
let excludedSuggestions = [];
|
|
1330
|
+
if (excludedSuggestionsStr) {
|
|
1331
|
+
try {
|
|
1332
|
+
excludedSuggestions = JSON.parse(excludedSuggestionsStr);
|
|
1333
|
+
} catch (e) {
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
const cacheKey = getCacheKey({ componentName, elementTag, lastSuggestion });
|
|
1337
|
+
const cached = excludedSuggestions.length === 0 ? getCachedSuggestions(cacheKey) : null;
|
|
1338
|
+
if (cached) {
|
|
1339
|
+
return server.NextResponse.json({
|
|
1340
|
+
success: true,
|
|
1341
|
+
suggestions: cached
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
const suggestions = await generateSuggestions({
|
|
1345
|
+
componentName,
|
|
1346
|
+
elementTag,
|
|
1347
|
+
className,
|
|
1348
|
+
textContent,
|
|
1349
|
+
editHistory,
|
|
1350
|
+
lastSuggestion,
|
|
1351
|
+
excludedSuggestions
|
|
1352
|
+
});
|
|
1353
|
+
if (suggestions && suggestions.length > 0) {
|
|
1354
|
+
cacheSuggestions(cacheKey, suggestions);
|
|
1355
|
+
return server.NextResponse.json({
|
|
1356
|
+
success: true,
|
|
1357
|
+
suggestions
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
return server.NextResponse.json({
|
|
1361
|
+
success: true,
|
|
1362
|
+
suggestions: DEFAULT_SUGGESTIONS
|
|
1363
|
+
});
|
|
1364
|
+
} catch (error) {
|
|
1365
|
+
return server.NextResponse.json(
|
|
1366
|
+
{
|
|
1367
|
+
success: false,
|
|
1368
|
+
error: String(error),
|
|
1369
|
+
suggestions: DEFAULT_SUGGESTIONS
|
|
1370
|
+
// Fallback
|
|
1371
|
+
},
|
|
1372
|
+
{ status: 500 }
|
|
1373
|
+
);
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
async function generateSuggestions(options) {
|
|
1377
|
+
const {
|
|
1378
|
+
componentName,
|
|
1379
|
+
elementTag,
|
|
1380
|
+
className,
|
|
1381
|
+
textContent,
|
|
1382
|
+
editHistory,
|
|
1383
|
+
lastSuggestion,
|
|
1384
|
+
excludedSuggestions = []
|
|
1385
|
+
} = options;
|
|
1386
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
1387
|
+
if (!apiKey) {
|
|
1388
|
+
return null;
|
|
1389
|
+
}
|
|
1390
|
+
const model = new anthropic.ChatAnthropic({
|
|
1391
|
+
apiKey,
|
|
1392
|
+
modelName: "claude-haiku-4-5-20251001",
|
|
1393
|
+
maxTokens: 1024,
|
|
1394
|
+
temperature: 0.3
|
|
1395
|
+
// Slightly creative for variety
|
|
1396
|
+
});
|
|
1397
|
+
const systemPrompt = `You are a UI/UX expert suggesting quick edits for React components.
|
|
1398
|
+
|
|
1399
|
+
Generate 6-8 concise, actionable suggestions that a developer might want to make next.
|
|
1400
|
+
|
|
1401
|
+
RULES:
|
|
1402
|
+
- Each suggestion must be 2-6 words (e.g., "Add padding", "Make text larger")
|
|
1403
|
+
- Focus on common UI improvements: spacing, colors, sizing, layout, shadows, borders, typography
|
|
1404
|
+
- Consider the element type (${elementTag || "element"})
|
|
1405
|
+
- Output ONLY a JSON array of strings, no explanations, no markdown fences
|
|
1406
|
+
${excludedSuggestions.length > 0 ? `- DO NOT suggest any of these (user wants different options): ${excludedSuggestions.join(
|
|
1407
|
+
", "
|
|
1408
|
+
)}` : ""}
|
|
1409
|
+
|
|
1410
|
+
${lastSuggestion ? `IMPORTANT - LAST EDIT CONTEXT:
|
|
1411
|
+
The user just applied: "${lastSuggestion}"
|
|
1412
|
+
Your suggestions MUST be direct follow-ups/refinements of this last change:
|
|
1413
|
+
- If it was "Add padding" → suggest "More padding", "Less padding", "Add vertical padding only"
|
|
1414
|
+
- If it was "Make it blue" → suggest "Darker blue", "Lighter blue", "Change to navy blue"
|
|
1415
|
+
- If it was "Increase font size" → suggest "Decrease font size", "Make even larger", "Adjust line height"
|
|
1416
|
+
- 4-6 suggestions should be variations/refinements of the last edit
|
|
1417
|
+
- 2-4 suggestions can be other related improvements
|
|
1418
|
+
|
|
1419
|
+
Generate follow-up suggestions that let the user iteratively refine what they just did.` : `Generate varied initial suggestions covering different aspects: layout, colors, spacing, shadows, typography, etc.`}
|
|
1420
|
+
|
|
1421
|
+
Example output format:
|
|
1422
|
+
["Add hover effect", "Increase padding", "Make corners rounder", "Change to flex row", "Add drop shadow", "Adjust font size"]`;
|
|
1423
|
+
let userPrompt = `Element: <${elementTag || "div"}>`;
|
|
1424
|
+
if (className) {
|
|
1425
|
+
userPrompt += `
|
|
1426
|
+
Classes: ${className}`;
|
|
1427
|
+
}
|
|
1428
|
+
if (textContent) {
|
|
1429
|
+
userPrompt += `
|
|
1430
|
+
Text: "${textContent.slice(0, 50)}"`;
|
|
1431
|
+
}
|
|
1432
|
+
userPrompt += `
|
|
1433
|
+
Component: ${componentName}`;
|
|
1434
|
+
if (editHistory && editHistory.length > 0) {
|
|
1435
|
+
userPrompt += `
|
|
1436
|
+
|
|
1437
|
+
Recent edits:
|
|
1438
|
+
`;
|
|
1439
|
+
editHistory.slice(-3).forEach((item, idx) => {
|
|
1440
|
+
userPrompt += `${idx + 1}. ${item.suggestion} ${item.success ? "✓" : "✗"}
|
|
1441
|
+
`;
|
|
1442
|
+
});
|
|
1443
|
+
} else {
|
|
1444
|
+
userPrompt += `
|
|
1445
|
+
|
|
1446
|
+
No previous edits.`;
|
|
1447
|
+
}
|
|
1448
|
+
if (lastSuggestion) {
|
|
1449
|
+
userPrompt += `
|
|
1450
|
+
|
|
1451
|
+
**LAST EDIT APPLIED:** "${lastSuggestion}"`;
|
|
1452
|
+
userPrompt += `
|
|
1453
|
+
|
|
1454
|
+
Generate 6-8 follow-up suggestions (mostly variations of the last edit):`;
|
|
1455
|
+
} else {
|
|
1456
|
+
userPrompt += `
|
|
1457
|
+
|
|
1458
|
+
Generate 6-8 initial suggestions:`;
|
|
1459
|
+
}
|
|
1460
|
+
try {
|
|
1461
|
+
const response = await model.invoke([
|
|
1462
|
+
new messages.SystemMessage(systemPrompt),
|
|
1463
|
+
new messages.HumanMessage(userPrompt)
|
|
1464
|
+
]);
|
|
1465
|
+
let content = typeof response.content === "string" ? response.content : String(response.content);
|
|
1466
|
+
content = content.trim();
|
|
1467
|
+
content = content.replace(/^```json?\s*/gm, "").replace(/\s*```$/gm, "");
|
|
1468
|
+
const suggestions = JSON.parse(content);
|
|
1469
|
+
if (Array.isArray(suggestions)) {
|
|
1470
|
+
const validSuggestions = suggestions.filter((s) => typeof s === "string").map((s) => s.trim()).filter((s) => {
|
|
1471
|
+
const words = s.split(/\s+/).length;
|
|
1472
|
+
return words >= 1 && words <= 15;
|
|
1473
|
+
}).slice(0, 8);
|
|
1474
|
+
if (validSuggestions.length >= 4) {
|
|
1475
|
+
return validSuggestions;
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
return null;
|
|
1479
|
+
} catch (e) {
|
|
1480
|
+
return null;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1113
1483
|
async function handleAIEditorRequest(req, context) {
|
|
1114
1484
|
const { path: path2 } = await context.params;
|
|
1115
1485
|
const endpoint = path2[0];
|
|
@@ -1133,12 +1503,16 @@ async function handleAIEditorRequest(req, context) {
|
|
|
1133
1503
|
case "validate-session":
|
|
1134
1504
|
if (method === "POST") return handleValidateSession(req);
|
|
1135
1505
|
break;
|
|
1506
|
+
case "suggestions":
|
|
1507
|
+
if (method === "GET") return handleSuggestions(req);
|
|
1508
|
+
break;
|
|
1136
1509
|
}
|
|
1137
1510
|
return server.NextResponse.json(
|
|
1138
1511
|
{ error: `Unknown endpoint: ${endpoint}` },
|
|
1139
1512
|
{ status: 404 }
|
|
1140
1513
|
);
|
|
1141
1514
|
}
|
|
1515
|
+
exports.extractComponentName = extractComponentName;
|
|
1142
1516
|
exports.fileExists = fileExists$1;
|
|
1143
1517
|
exports.findTargetElement = findTargetElement;
|
|
1144
1518
|
exports.getAttributeValue = getAttributeValue;
|
|
@@ -1149,14 +1523,17 @@ exports.handleAbsolutePath = handleAbsolutePath;
|
|
|
1149
1523
|
exports.handleEdit = handleEdit;
|
|
1150
1524
|
exports.handleRead = handleRead;
|
|
1151
1525
|
exports.handleResolve = handleResolve;
|
|
1526
|
+
exports.handleSuggestions = handleSuggestions;
|
|
1152
1527
|
exports.handleUndo = handleUndo;
|
|
1153
1528
|
exports.handleValidateSession = handleValidateSession;
|
|
1154
1529
|
exports.isPathSecure = isPathSecure;
|
|
1155
1530
|
exports.normalizePath = normalizePath;
|
|
1156
1531
|
exports.parseDebugStack = parseDebugStack;
|
|
1532
|
+
exports.parseDebugStackFrames = parseDebugStackFrames;
|
|
1157
1533
|
exports.parseFile = parseFile;
|
|
1158
1534
|
exports.resolveFilePath = resolveFilePath;
|
|
1159
1535
|
exports.resolveOriginalPosition = resolveOriginalPosition;
|
|
1160
1536
|
exports.scoreElementMatch = scoreElementMatch;
|
|
1161
1537
|
exports.validateDevMode = validateDevMode;
|
|
1162
|
-
|
|
1538
|
+
exports.validateGeneratedCode = validateGeneratedCode;
|
|
1539
|
+
//# sourceMappingURL=index-9QODCOgD.cjs.map
|