next-ai-editor 0.1.1 → 0.1.3
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-Bs9zUVrL.cjs → AIEditorProvider-BGHm2xyU.cjs} +823 -378
- package/dist/AIEditorProvider-BGHm2xyU.cjs.map +1 -0
- package/dist/{AIEditorProvider-D-w9-GZb.js → AIEditorProvider-CxdGjdLL.js} +847 -402
- package/dist/AIEditorProvider-CxdGjdLL.js.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-DnoYi4f8.cjs → index-CNJqd4EQ.cjs} +656 -225
- package/dist/index-CNJqd4EQ.cjs.map +1 -0
- package/dist/{index-BFa7H-uO.js → index-DrmEf13c.js} +662 -231
- package/dist/index-DrmEf13c.js.map +1 -0
- package/dist/index.cjs +7 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +15 -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 +10 -0
- package/dist/server/utils/source-map.d.ts.map +1 -1
- package/dist/server.cjs +6 -1
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +14 -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 +1 -1
- 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,12 +1,13 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
2
|
import fs from "fs/promises";
|
|
3
|
-
import * as parser from "@babel/parser";
|
|
4
3
|
import { ChatAnthropic } from "@langchain/anthropic";
|
|
5
4
|
import { SystemMessage, HumanMessage } from "@langchain/core/messages";
|
|
6
5
|
import path from "path";
|
|
6
|
+
import * as parser from "@babel/parser";
|
|
7
7
|
import traverse from "@babel/traverse";
|
|
8
8
|
import * as t from "@babel/types";
|
|
9
9
|
import { SourceMapConsumer } from "@jridgewell/source-map";
|
|
10
|
+
import { c as cleanPath, s as shouldSkipPath, n as normalizeSourcePath } from "./path-utils-Bai2xKx9.js";
|
|
10
11
|
import crypto from "crypto";
|
|
11
12
|
function validateDevMode() {
|
|
12
13
|
if (process.env.NODE_ENV !== "development") {
|
|
@@ -65,9 +66,79 @@ function parseFile(content) {
|
|
|
65
66
|
return null;
|
|
66
67
|
}
|
|
67
68
|
}
|
|
69
|
+
function extractComponentName(ast) {
|
|
70
|
+
let componentName = null;
|
|
71
|
+
traverse(ast, {
|
|
72
|
+
ExportDefaultDeclaration(path2) {
|
|
73
|
+
var _a;
|
|
74
|
+
if (t.isFunctionDeclaration(path2.node.declaration)) {
|
|
75
|
+
componentName = ((_a = path2.node.declaration.id) == null ? void 0 : _a.name) || null;
|
|
76
|
+
} else if (t.isArrowFunctionExpression(path2.node.declaration)) {
|
|
77
|
+
componentName = "default";
|
|
78
|
+
} else if (t.isIdentifier(path2.node.declaration)) {
|
|
79
|
+
componentName = path2.node.declaration.name;
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
ExportNamedDeclaration(path2) {
|
|
83
|
+
var _a;
|
|
84
|
+
if (t.isFunctionDeclaration(path2.node.declaration)) {
|
|
85
|
+
componentName = ((_a = path2.node.declaration.id) == null ? void 0 : _a.name) || null;
|
|
86
|
+
} else if (t.isVariableDeclaration(path2.node.declaration)) {
|
|
87
|
+
const declarator = path2.node.declaration.declarations[0];
|
|
88
|
+
if (t.isIdentifier(declarator.id)) {
|
|
89
|
+
componentName = declarator.id.name;
|
|
90
|
+
}
|
|
91
|
+
} else if (path2.node.specifiers && path2.node.specifiers.length > 0) {
|
|
92
|
+
const specifier = path2.node.specifiers[0];
|
|
93
|
+
if (t.isExportSpecifier(specifier) && t.isIdentifier(specifier.exported)) {
|
|
94
|
+
componentName = specifier.exported.name;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
return componentName;
|
|
100
|
+
}
|
|
101
|
+
function validateGeneratedCode(newCode, originalCode, fileContent) {
|
|
102
|
+
try {
|
|
103
|
+
const isFullComponent = /^(export\s+)?(default\s+)?function\s+\w+/.test(newCode.trim()) || /^(export\s+)?(default\s+)?const\s+\w+\s*=/.test(newCode.trim());
|
|
104
|
+
if (isFullComponent) {
|
|
105
|
+
let codeToValidate = newCode;
|
|
106
|
+
if (fileContent) {
|
|
107
|
+
const interfaceMatches = fileContent.match(
|
|
108
|
+
/^(interface|type)\s+\w+[^}]*\}/gm
|
|
109
|
+
);
|
|
110
|
+
if (interfaceMatches) {
|
|
111
|
+
codeToValidate = interfaceMatches.join("\n\n") + "\n\n" + newCode;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
parser.parse(codeToValidate, {
|
|
115
|
+
sourceType: "module",
|
|
116
|
+
plugins: ["jsx", "typescript"]
|
|
117
|
+
});
|
|
118
|
+
} else {
|
|
119
|
+
const wrapped = `function _() { return (${newCode}); }`;
|
|
120
|
+
parser.parse(wrapped, {
|
|
121
|
+
sourceType: "module",
|
|
122
|
+
plugins: ["jsx", "typescript"]
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
} catch (e) {
|
|
126
|
+
console.error("Generated code parse error:", e);
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
const origBraces = (originalCode.match(/[{}]/g) || []).length;
|
|
130
|
+
const newBraces = (newCode.match(/[{}]/g) || []).length;
|
|
131
|
+
const origTags = (originalCode.match(/[<>]/g) || []).length;
|
|
132
|
+
const newTags = (newCode.match(/[<>]/g) || []).length;
|
|
133
|
+
if (Math.abs(origBraces - newBraces) > 4 || Math.abs(origTags - newTags) > 4) {
|
|
134
|
+
console.warn(
|
|
135
|
+
`Structure changed significantly: braces ${origBraces}->${newBraces}, tags ${origTags}->${newTags}`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
68
140
|
function findTargetElement(ast, fileContent, options) {
|
|
69
141
|
const { componentName, lineNumber, elementContext } = options;
|
|
70
|
-
const matches = [];
|
|
71
142
|
let componentNode = null;
|
|
72
143
|
let componentStart = 0;
|
|
73
144
|
let componentEnd = Infinity;
|
|
@@ -118,6 +189,7 @@ function findTargetElement(ast, fileContent, options) {
|
|
|
118
189
|
componentEnd = fallback.end;
|
|
119
190
|
}
|
|
120
191
|
const allElementsByTag = /* @__PURE__ */ new Map();
|
|
192
|
+
const elementsAtLine = [];
|
|
121
193
|
traverse(ast, {
|
|
122
194
|
JSXElement(path2) {
|
|
123
195
|
const loc = path2.node.loc;
|
|
@@ -133,59 +205,107 @@ function findTargetElement(ast, fileContent, options) {
|
|
|
133
205
|
}
|
|
134
206
|
allElementsByTag.get(tagName).push({ node: path2.node, startLine, endLine, score: 0 });
|
|
135
207
|
}
|
|
136
|
-
if (
|
|
137
|
-
|
|
138
|
-
if (score > 0) {
|
|
139
|
-
matches.push({ node: path2.node, startLine, endLine, score });
|
|
140
|
-
}
|
|
141
|
-
} else if (Math.abs(startLine - lineNumber) <= 5) {
|
|
142
|
-
matches.push({
|
|
143
|
-
node: path2.node,
|
|
144
|
-
startLine,
|
|
145
|
-
endLine,
|
|
146
|
-
score: 100 - Math.abs(startLine - lineNumber)
|
|
147
|
-
});
|
|
208
|
+
if (startLine === lineNumber) {
|
|
209
|
+
elementsAtLine.push({ node: path2.node, startLine, endLine, score: 0 });
|
|
148
210
|
}
|
|
149
211
|
}
|
|
150
212
|
});
|
|
151
|
-
if (
|
|
152
|
-
if (
|
|
213
|
+
if (elementsAtLine.length > 0) {
|
|
214
|
+
if (elementsAtLine.length === 1) {
|
|
215
|
+
const target = elementsAtLine[0];
|
|
153
216
|
return {
|
|
154
|
-
startLine:
|
|
155
|
-
endLine:
|
|
217
|
+
startLine: target.startLine,
|
|
218
|
+
endLine: target.endLine,
|
|
156
219
|
componentStart,
|
|
157
220
|
componentEnd
|
|
158
221
|
};
|
|
159
222
|
}
|
|
160
|
-
|
|
223
|
+
if (elementContext) {
|
|
224
|
+
for (const elem of elementsAtLine) {
|
|
225
|
+
if (t.isJSXElement(elem.node)) {
|
|
226
|
+
const score = scoreElementMatch(elem.node, elementContext, fileContent);
|
|
227
|
+
elem.score = score;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
elementsAtLine.sort((a, b) => b.score - a.score);
|
|
231
|
+
if (elementsAtLine[0].score > 0) {
|
|
232
|
+
return {
|
|
233
|
+
startLine: elementsAtLine[0].startLine,
|
|
234
|
+
endLine: elementsAtLine[0].endLine,
|
|
235
|
+
componentStart,
|
|
236
|
+
componentEnd
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
startLine: elementsAtLine[0].startLine,
|
|
242
|
+
endLine: elementsAtLine[0].endLine,
|
|
243
|
+
componentStart,
|
|
244
|
+
componentEnd
|
|
245
|
+
};
|
|
161
246
|
}
|
|
162
|
-
if (
|
|
247
|
+
if (elementContext == null ? void 0 : elementContext.tagName) {
|
|
163
248
|
const allOfTag = allElementsByTag.get(elementContext.tagName);
|
|
164
|
-
if (allOfTag && allOfTag.length
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
249
|
+
if (allOfTag && allOfTag.length > 0) {
|
|
250
|
+
if (elementContext.textContent || elementContext.className) {
|
|
251
|
+
for (const elem of allOfTag) {
|
|
252
|
+
if (t.isJSXElement(elem.node)) {
|
|
253
|
+
elem.score = scoreElementMatch(elem.node, elementContext, fileContent);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
allOfTag.sort((a, b) => b.score - a.score);
|
|
257
|
+
if (allOfTag[0].score > 50) {
|
|
258
|
+
return {
|
|
259
|
+
startLine: allOfTag[0].startLine,
|
|
260
|
+
endLine: allOfTag[0].endLine,
|
|
261
|
+
componentStart,
|
|
262
|
+
componentEnd
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (elementContext.nthOfType && allOfTag.length >= elementContext.nthOfType) {
|
|
267
|
+
const target = allOfTag[elementContext.nthOfType - 1];
|
|
268
|
+
return {
|
|
269
|
+
startLine: target.startLine,
|
|
270
|
+
endLine: target.endLine,
|
|
271
|
+
componentStart,
|
|
272
|
+
componentEnd
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const nearbyElements = [];
|
|
278
|
+
traverse(ast, {
|
|
279
|
+
JSXElement(path2) {
|
|
280
|
+
const loc = path2.node.loc;
|
|
281
|
+
if (!loc) return;
|
|
282
|
+
const startLine = loc.start.line;
|
|
283
|
+
const endLine = loc.end.line;
|
|
284
|
+
if (startLine < componentStart || endLine > componentEnd) return;
|
|
285
|
+
if (Math.abs(startLine - lineNumber) <= 5) {
|
|
286
|
+
const score = elementContext ? scoreElementMatch(path2.node, elementContext, fileContent) : 100 - Math.abs(startLine - lineNumber);
|
|
287
|
+
nearbyElements.push({ node: path2.node, startLine, endLine, score });
|
|
288
|
+
}
|
|
179
289
|
}
|
|
290
|
+
});
|
|
291
|
+
if (nearbyElements.length > 0) {
|
|
292
|
+
nearbyElements.sort((a, b) => b.score - a.score);
|
|
293
|
+
return {
|
|
294
|
+
startLine: nearbyElements[0].startLine,
|
|
295
|
+
endLine: nearbyElements[0].endLine,
|
|
296
|
+
componentStart,
|
|
297
|
+
componentEnd
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
if (componentNode && componentStart > 0) {
|
|
301
|
+
return {
|
|
302
|
+
startLine: componentStart,
|
|
303
|
+
endLine: componentEnd,
|
|
304
|
+
componentStart,
|
|
305
|
+
componentEnd
|
|
306
|
+
};
|
|
180
307
|
}
|
|
181
|
-
|
|
182
|
-
const best = matches[0];
|
|
183
|
-
return {
|
|
184
|
-
startLine: best.startLine,
|
|
185
|
-
endLine: best.endLine,
|
|
186
|
-
componentStart,
|
|
187
|
-
componentEnd
|
|
188
|
-
};
|
|
308
|
+
return null;
|
|
189
309
|
}
|
|
190
310
|
function scoreElementMatch(node, context, fileContent) {
|
|
191
311
|
let score = 0;
|
|
@@ -269,7 +389,8 @@ async function handleEdit(req) {
|
|
|
269
389
|
componentName,
|
|
270
390
|
suggestion,
|
|
271
391
|
elementContext,
|
|
272
|
-
editHistory
|
|
392
|
+
editHistory,
|
|
393
|
+
parentInstance
|
|
273
394
|
} = body;
|
|
274
395
|
const projectRoot = process.cwd();
|
|
275
396
|
const normalizedPath = normalizePath(filePath);
|
|
@@ -281,7 +402,6 @@ async function handleEdit(req) {
|
|
|
281
402
|
);
|
|
282
403
|
}
|
|
283
404
|
const fileContent = await fs.readFile(absolutePath, "utf-8");
|
|
284
|
-
console.log(`[/edit] componentName="${componentName}", filePath="${filePath}"`);
|
|
285
405
|
const ast = parseFile(fileContent);
|
|
286
406
|
if (!ast) {
|
|
287
407
|
return NextResponse.json(
|
|
@@ -300,20 +420,10 @@ async function handleEdit(req) {
|
|
|
300
420
|
{ status: 400 }
|
|
301
421
|
);
|
|
302
422
|
}
|
|
303
|
-
console.log(
|
|
304
|
-
`📍 Found element <${(elementContext == null ? void 0 : elementContext.tagName) || "component"}> at lines ${target.startLine}-${target.endLine} (component: ${target.componentStart}-${target.componentEnd})`
|
|
305
|
-
);
|
|
306
|
-
console.log(
|
|
307
|
-
` Element context: tagName=${elementContext == null ? void 0 : elementContext.tagName}, nthOfType=${elementContext == null ? void 0 : elementContext.nthOfType}, textContent="${elementContext == null ? void 0 : elementContext.textContent}"`
|
|
308
|
-
);
|
|
309
423
|
const lines = fileContent.split("\n");
|
|
310
|
-
const foundElementCode = lines.slice(target.startLine - 1, Math.min(target.startLine + 2, target.endLine)).join("\n");
|
|
311
|
-
console.log(` Found element preview:
|
|
312
|
-
${foundElementCode}`);
|
|
313
424
|
const targetCode = lines.slice(target.startLine - 1, target.endLine).join("\n");
|
|
314
425
|
const baseIndentation = ((_a = lines[target.startLine - 1].match(/^(\s*)/)) == null ? void 0 : _a[1]) || "";
|
|
315
426
|
if (target.componentStart <= 0 || target.componentEnd === Infinity) {
|
|
316
|
-
console.error("❌ Invalid component bounds detected");
|
|
317
427
|
return NextResponse.json({
|
|
318
428
|
success: false,
|
|
319
429
|
error: `Could not determine component bounds. Component: ${target.componentStart}-${target.componentEnd}`
|
|
@@ -351,7 +461,8 @@ ${foundElementCode}`);
|
|
|
351
461
|
targetEndLine: target.endLine,
|
|
352
462
|
componentStart: target.componentStart,
|
|
353
463
|
componentEnd: target.componentEnd,
|
|
354
|
-
editHistory: editHistory || []
|
|
464
|
+
editHistory: editHistory || [],
|
|
465
|
+
parentInstance
|
|
355
466
|
});
|
|
356
467
|
if (!newCode) {
|
|
357
468
|
return NextResponse.json({
|
|
@@ -359,38 +470,56 @@ ${foundElementCode}`);
|
|
|
359
470
|
error: "AI failed to generate valid edit"
|
|
360
471
|
});
|
|
361
472
|
}
|
|
362
|
-
|
|
363
|
-
|
|
473
|
+
const parentInstanceMatch = newCode.match(/\/\/ EDIT_PARENT_INSTANCE\s*\n([\s\S]+)/);
|
|
474
|
+
if (parentInstanceMatch && parentInstance) {
|
|
475
|
+
const parentCode = parentInstanceMatch[1].trim();
|
|
476
|
+
const parentNormalizedPath = normalizePath(parentInstance.filePath);
|
|
477
|
+
const parentAbsolutePath = await resolveFilePath(projectRoot, parentNormalizedPath);
|
|
478
|
+
if (!parentAbsolutePath) {
|
|
479
|
+
return NextResponse.json({
|
|
480
|
+
success: false,
|
|
481
|
+
error: `Parent file not found: ${parentNormalizedPath}`
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
const parentFileContent = await fs.readFile(parentAbsolutePath, "utf-8");
|
|
485
|
+
const parentLines = parentFileContent.split("\n");
|
|
486
|
+
const newParentLines = [...parentLines];
|
|
487
|
+
newParentLines.splice(
|
|
488
|
+
parentInstance.lineStart - 1,
|
|
489
|
+
parentInstance.lineEnd - parentInstance.lineStart + 1,
|
|
490
|
+
...parentCode.split("\n")
|
|
491
|
+
);
|
|
492
|
+
await fs.writeFile(parentAbsolutePath, newParentLines.join("\n"), "utf-8");
|
|
493
|
+
return NextResponse.json({
|
|
494
|
+
success: true,
|
|
495
|
+
fileSnapshot: parentFileContent,
|
|
496
|
+
generatedCode: parentCode,
|
|
497
|
+
modifiedLines: {
|
|
498
|
+
start: parentInstance.lineStart,
|
|
499
|
+
end: parentInstance.lineEnd
|
|
500
|
+
},
|
|
501
|
+
editedFile: parentInstance.filePath
|
|
502
|
+
// Indicate which file was edited
|
|
503
|
+
});
|
|
504
|
+
}
|
|
364
505
|
const fullComponentMatch = newCode.match(/\/\/ FULL_COMPONENT\s*\n([\s\S]+)/);
|
|
365
506
|
let codeToApply = newCode;
|
|
366
507
|
let startLineToReplace = target.startLine;
|
|
367
508
|
let endLineToReplace = target.endLine;
|
|
509
|
+
const isFullComponentDeclaration = /^(export\s+)?(default\s+)?function\s+\w+/.test(newCode.trim()) || /^(export\s+)?const\s+\w+\s*=/.test(newCode.trim());
|
|
368
510
|
if (fullComponentMatch) {
|
|
369
|
-
console.log("Found // FULL_COMPONENT marker, extracting full component code");
|
|
370
511
|
codeToApply = fullComponentMatch[1].trim();
|
|
371
512
|
startLineToReplace = target.componentStart;
|
|
372
513
|
endLineToReplace = target.componentEnd;
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
console.log("Extracted component code (first 300 chars):", codeToApply.substring(0, 300));
|
|
378
|
-
} else {
|
|
379
|
-
console.log("No // FULL_COMPONENT marker found, treating as target element modification");
|
|
380
|
-
console.log(
|
|
381
|
-
`🔄 AI returned target element modification (lines ${startLineToReplace}-${endLineToReplace})`
|
|
382
|
-
);
|
|
514
|
+
} else if (isFullComponentDeclaration && target.startLine !== target.componentStart) {
|
|
515
|
+
codeToApply = newCode;
|
|
516
|
+
startLineToReplace = target.componentStart;
|
|
517
|
+
endLineToReplace = target.componentEnd;
|
|
383
518
|
}
|
|
384
|
-
console.log("Code to apply (first 200 chars):", codeToApply.substring(0, 200));
|
|
385
519
|
if (!validateGeneratedCode(codeToApply, targetCode, fileContent)) {
|
|
386
|
-
console.error("❌ Generated code failed validation");
|
|
387
|
-
console.error("=== Generated code START ===");
|
|
388
|
-
console.error(codeToApply);
|
|
389
|
-
console.error("=== Generated code END ===");
|
|
390
|
-
console.error(`Length: ${codeToApply.length} chars, ${codeToApply.split("\n").length} lines`);
|
|
391
520
|
return NextResponse.json({
|
|
392
521
|
success: false,
|
|
393
|
-
error: "Generated code is invalid
|
|
522
|
+
error: "Generated code is invalid"
|
|
394
523
|
});
|
|
395
524
|
}
|
|
396
525
|
const newLines = [...lines];
|
|
@@ -400,7 +529,6 @@ ${foundElementCode}`);
|
|
|
400
529
|
...codeToApply.split("\n")
|
|
401
530
|
);
|
|
402
531
|
await fs.writeFile(absolutePath, newLines.join("\n"), "utf-8");
|
|
403
|
-
console.log(`✅ Updated ${normalizedPath}`);
|
|
404
532
|
return NextResponse.json({
|
|
405
533
|
success: true,
|
|
406
534
|
fileSnapshot: fileContent,
|
|
@@ -410,10 +538,11 @@ ${foundElementCode}`);
|
|
|
410
538
|
modifiedLines: {
|
|
411
539
|
start: startLineToReplace,
|
|
412
540
|
end: endLineToReplace
|
|
413
|
-
}
|
|
541
|
+
},
|
|
542
|
+
editedFile: filePath
|
|
543
|
+
// Indicate which file was edited
|
|
414
544
|
});
|
|
415
545
|
} catch (error) {
|
|
416
|
-
console.error("AI Editor error:", error);
|
|
417
546
|
return NextResponse.json(
|
|
418
547
|
{ success: false, error: String(error) },
|
|
419
548
|
{ status: 500 }
|
|
@@ -425,7 +554,8 @@ async function generateEdit(options) {
|
|
|
425
554
|
fullComponentCode,
|
|
426
555
|
suggestion,
|
|
427
556
|
baseIndentation,
|
|
428
|
-
editHistory
|
|
557
|
+
editHistory,
|
|
558
|
+
parentInstance
|
|
429
559
|
} = options;
|
|
430
560
|
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
431
561
|
if (!apiKey) throw new Error("ANTHROPIC_API_KEY not set");
|
|
@@ -438,25 +568,29 @@ async function generateEdit(options) {
|
|
|
438
568
|
const systemPrompt = `You are a precise code editor for React/JSX components.
|
|
439
569
|
|
|
440
570
|
WHAT YOU'LL SEE:
|
|
441
|
-
- Full component
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
571
|
+
- Component Definition: Full code of the clicked component with line annotations
|
|
572
|
+
* The clicked element is marked with "// ← CLICKED ELEMENT STARTS" and "// ← CLICKED ELEMENT ENDS"
|
|
573
|
+
* These are just annotations - NOT part of the actual code
|
|
574
|
+
${parentInstance ? `- Parent Instance: Where this component is used, with "// ← COMPONENT USAGE" marking the usage line` : ""}
|
|
575
|
+
|
|
576
|
+
YOUR DECISION - Choose ONE approach based on the user's request:
|
|
445
577
|
|
|
446
|
-
|
|
447
|
-
Modify
|
|
578
|
+
1. EDIT COMPONENT DEFINITION (most common):
|
|
579
|
+
- Modify styling, layout, or behavior that should apply to ALL instances
|
|
580
|
+
- Example requests: "make the button blue", "add padding", "change font size"
|
|
581
|
+
- Output: Just the modified element OR "// FULL_COMPONENT\\n" + complete modified component
|
|
448
582
|
|
|
449
|
-
|
|
450
|
-
-
|
|
451
|
-
-
|
|
583
|
+
${parentInstance ? `2. EDIT PARENT INSTANCE (when user wants to modify THIS specific usage):
|
|
584
|
+
- Change props, text, or configuration for THIS specific instance only
|
|
585
|
+
- Example requests: "change this title to...", "remove this card", "add another button here"
|
|
586
|
+
- Output: "// EDIT_PARENT_INSTANCE\\n" + complete modified parent component` : ""}
|
|
452
587
|
|
|
453
588
|
RULES:
|
|
454
589
|
- Output ONLY code, no explanations
|
|
455
590
|
- No markdown fences
|
|
456
|
-
- Do NOT include
|
|
591
|
+
- Do NOT include annotation comments in your output
|
|
457
592
|
- Preserve indentation
|
|
458
|
-
- Make minimal changes
|
|
459
|
-
- Do NOT modify unrelated elements`;
|
|
593
|
+
- Make minimal changes`;
|
|
460
594
|
let userPrompt = "";
|
|
461
595
|
if (editHistory.length > 0) {
|
|
462
596
|
userPrompt += "Previous edits made to this element:\n";
|
|
@@ -466,16 +600,36 @@ RULES:
|
|
|
466
600
|
});
|
|
467
601
|
userPrompt += "\n";
|
|
468
602
|
}
|
|
469
|
-
userPrompt += `
|
|
603
|
+
userPrompt += `COMPONENT DEFINITION (clicked element is annotated):
|
|
470
604
|
|
|
471
605
|
\`\`\`jsx
|
|
472
606
|
${fullComponentCode}
|
|
473
607
|
\`\`\`
|
|
608
|
+
`;
|
|
609
|
+
if (parentInstance) {
|
|
610
|
+
const parentLines = parentInstance.content.split("\n");
|
|
611
|
+
const annotatedParentLines = parentLines.map((line, idx) => {
|
|
612
|
+
const lineNum = parentInstance.lineStart + idx;
|
|
613
|
+
const isUsageLine = lineNum >= parentInstance.usageLineStart && lineNum <= parentInstance.usageLineEnd;
|
|
614
|
+
return line + (isUsageLine ? " // ← COMPONENT USAGE" : "");
|
|
615
|
+
});
|
|
616
|
+
userPrompt += `
|
|
617
|
+
PARENT INSTANCE (where component is used - in ${parentInstance.filePath}):
|
|
474
618
|
|
|
619
|
+
\`\`\`jsx
|
|
620
|
+
${annotatedParentLines.join("\n")}
|
|
621
|
+
\`\`\`
|
|
622
|
+
`;
|
|
623
|
+
}
|
|
624
|
+
userPrompt += `
|
|
475
625
|
User request: "${suggestion}"
|
|
476
|
-
${editHistory.length > 0 ? "
|
|
626
|
+
${editHistory.length > 0 ? "(Build upon previous changes)" : ""}
|
|
627
|
+
|
|
628
|
+
${parentInstance ? `Decide whether to:
|
|
629
|
+
1. Edit the component definition (for changes that affect ALL instances)
|
|
630
|
+
2. Edit the parent instance (for changes specific to THIS usage)
|
|
477
631
|
|
|
478
|
-
Modify the annotated element to fulfill this request. Remember: do NOT include the annotation comments in your output
|
|
632
|
+
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.`}`;
|
|
479
633
|
try {
|
|
480
634
|
const response = await model.invoke([
|
|
481
635
|
new SystemMessage(systemPrompt),
|
|
@@ -485,12 +639,18 @@ Modify the annotated element to fulfill this request. Remember: do NOT include t
|
|
|
485
639
|
code = cleanGeneratedCode(code, baseIndentation);
|
|
486
640
|
return code || null;
|
|
487
641
|
} catch (e) {
|
|
488
|
-
console.error("AI generation error:", e);
|
|
489
642
|
return null;
|
|
490
643
|
}
|
|
491
644
|
}
|
|
492
645
|
function cleanGeneratedCode(code, baseIndentation) {
|
|
493
646
|
var _a, _b, _c;
|
|
647
|
+
const isParentEdit = code.trim().startsWith("// EDIT_PARENT_INSTANCE");
|
|
648
|
+
if (isParentEdit) {
|
|
649
|
+
const marker2 = "// EDIT_PARENT_INSTANCE\n";
|
|
650
|
+
code = code.replace(/^\/\/ EDIT_PARENT_INSTANCE\n?/, "");
|
|
651
|
+
code = code.replace(/^```[\w]*\n?/gm, "").replace(/\n?```$/gm, "").replace(/\s*\/\/ ← COMPONENT USAGE/g, "").trim();
|
|
652
|
+
return marker2 + code;
|
|
653
|
+
}
|
|
494
654
|
const isFullComponent = code.trim().startsWith("// FULL_COMPONENT");
|
|
495
655
|
let marker = "";
|
|
496
656
|
if (isFullComponent) {
|
|
@@ -521,43 +681,6 @@ function cleanGeneratedCode(code, baseIndentation) {
|
|
|
521
681
|
}
|
|
522
682
|
return marker + code;
|
|
523
683
|
}
|
|
524
|
-
function validateGeneratedCode(newCode, originalCode, fileContent) {
|
|
525
|
-
try {
|
|
526
|
-
const isFullComponent = /^(export\s+)?(default\s+)?function\s+\w+/.test(newCode.trim()) || /^(export\s+)?(default\s+)?const\s+\w+\s*=/.test(newCode.trim());
|
|
527
|
-
if (isFullComponent) {
|
|
528
|
-
let codeToValidate = newCode;
|
|
529
|
-
if (fileContent) {
|
|
530
|
-
const interfaceMatches = fileContent.match(/^(interface|type)\s+\w+[^}]*\}/gm);
|
|
531
|
-
if (interfaceMatches) {
|
|
532
|
-
codeToValidate = interfaceMatches.join("\n\n") + "\n\n" + newCode;
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
parser.parse(codeToValidate, {
|
|
536
|
-
sourceType: "module",
|
|
537
|
-
plugins: ["jsx", "typescript"]
|
|
538
|
-
});
|
|
539
|
-
} else {
|
|
540
|
-
const wrapped = `function _() { return (${newCode}); }`;
|
|
541
|
-
parser.parse(wrapped, {
|
|
542
|
-
sourceType: "module",
|
|
543
|
-
plugins: ["jsx", "typescript"]
|
|
544
|
-
});
|
|
545
|
-
}
|
|
546
|
-
} catch (e) {
|
|
547
|
-
console.error("Generated code parse error:", e);
|
|
548
|
-
return false;
|
|
549
|
-
}
|
|
550
|
-
const origBraces = (originalCode.match(/[{}]/g) || []).length;
|
|
551
|
-
const newBraces = (newCode.match(/[{}]/g) || []).length;
|
|
552
|
-
const origTags = (originalCode.match(/[<>]/g) || []).length;
|
|
553
|
-
const newTags = (newCode.match(/[<>]/g) || []).length;
|
|
554
|
-
if (Math.abs(origBraces - newBraces) > 4 || Math.abs(origTags - newTags) > 4) {
|
|
555
|
-
console.warn(
|
|
556
|
-
`Structure changed significantly: braces ${origBraces}->${newBraces}, tags ${origTags}->${newTags}`
|
|
557
|
-
);
|
|
558
|
-
}
|
|
559
|
-
return true;
|
|
560
|
-
}
|
|
561
684
|
function parseDebugStack(stack) {
|
|
562
685
|
if (!stack) return null;
|
|
563
686
|
const stackStr = typeof stack === "string" ? stack : stack.stack || String(stack);
|
|
@@ -582,8 +705,8 @@ function parseDebugStack(stack) {
|
|
|
582
705
|
chunkId = chunkMatch[1];
|
|
583
706
|
filePath = filePath.replace(/\?[^:]*$/, "");
|
|
584
707
|
}
|
|
585
|
-
filePath =
|
|
586
|
-
if (!
|
|
708
|
+
filePath = cleanPath(filePath);
|
|
709
|
+
if (!shouldSkipPath(filePath)) {
|
|
587
710
|
console.log("parseDebugStack extracted:", { filePath, line, column });
|
|
588
711
|
return { filePath, line, column, chunkId };
|
|
589
712
|
}
|
|
@@ -595,46 +718,73 @@ function parseDebugStack(stack) {
|
|
|
595
718
|
);
|
|
596
719
|
return null;
|
|
597
720
|
}
|
|
598
|
-
function
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
721
|
+
function extractComponentNameFromStack(stack) {
|
|
722
|
+
if (!stack) return null;
|
|
723
|
+
const stackStr = typeof stack === "string" ? stack : stack.stack || String(stack);
|
|
724
|
+
const frames = stackStr.split("\n");
|
|
725
|
+
const skipPatterns = [
|
|
726
|
+
"node_modules",
|
|
727
|
+
"SegmentViewNode",
|
|
728
|
+
"LayoutRouter",
|
|
729
|
+
"ErrorBoundary",
|
|
730
|
+
"fakeJSXCallSite",
|
|
731
|
+
"react_stack_bottom_frame"
|
|
732
|
+
];
|
|
733
|
+
for (const frame of frames) {
|
|
734
|
+
if (skipPatterns.some((p) => frame.includes(p))) continue;
|
|
735
|
+
const match = frame.match(/at\s+(\w+)\s+\(/);
|
|
736
|
+
if (match && match[1]) {
|
|
737
|
+
const componentName = match[1];
|
|
738
|
+
if (componentName !== "Object" && componentName !== "anonymous") {
|
|
739
|
+
return componentName;
|
|
740
|
+
}
|
|
607
741
|
}
|
|
608
|
-
} catch (e) {
|
|
609
|
-
console.warn("Failed to decode URL-encoded path:", cleaned, e);
|
|
610
742
|
}
|
|
611
|
-
|
|
612
|
-
if (cleaned.startsWith(".next/") || path.isAbsolute(cleaned)) {
|
|
613
|
-
return cleaned;
|
|
614
|
-
}
|
|
615
|
-
return cleaned;
|
|
616
|
-
}
|
|
617
|
-
function cleanPath(p) {
|
|
618
|
-
return cleanPathTurbopack(p);
|
|
743
|
+
return null;
|
|
619
744
|
}
|
|
620
|
-
function
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
745
|
+
function parseDebugStackFrames(stack) {
|
|
746
|
+
if (!stack) return [];
|
|
747
|
+
const stackStr = typeof stack === "string" ? stack : stack.stack || String(stack);
|
|
748
|
+
const frames = stackStr.split("\n");
|
|
749
|
+
const skipPatterns = [
|
|
750
|
+
"node_modules",
|
|
751
|
+
"SegmentViewNode",
|
|
752
|
+
"LayoutRouter",
|
|
753
|
+
"ErrorBoundary",
|
|
754
|
+
"fakeJSXCallSite",
|
|
755
|
+
"react_stack_bottom_frame"
|
|
756
|
+
];
|
|
757
|
+
const positions = [];
|
|
758
|
+
for (const frame of frames) {
|
|
759
|
+
if (skipPatterns.some((p) => frame.includes(p))) continue;
|
|
760
|
+
const match = frame.match(/at\s+(\w+)\s+\((.+?):(\d+):(\d+)\)?$/);
|
|
761
|
+
if (match) {
|
|
762
|
+
match[1];
|
|
763
|
+
let filePath = match[2];
|
|
764
|
+
const line = parseInt(match[3], 10);
|
|
765
|
+
const column = parseInt(match[4], 10);
|
|
766
|
+
let chunkId;
|
|
767
|
+
const chunkMatch = filePath.match(/\?([^:]+)$/);
|
|
768
|
+
if (chunkMatch) {
|
|
769
|
+
chunkId = chunkMatch[1];
|
|
770
|
+
filePath = filePath.replace(/\?[^:]*$/, "");
|
|
771
|
+
}
|
|
772
|
+
filePath = cleanPath(filePath);
|
|
773
|
+
if (!shouldSkipPath(filePath)) {
|
|
774
|
+
positions.push({ filePath, line, column, chunkId });
|
|
775
|
+
if (positions.length >= 2) break;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
625
778
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
function shouldSkip(p) {
|
|
629
|
-
if (!p) return true;
|
|
630
|
-
return ["node_modules", "next/dist", "react-dom"].some((s) => p.includes(s));
|
|
779
|
+
console.log(`parseDebugStackFrames extracted ${positions.length} frames:`, positions);
|
|
780
|
+
return positions;
|
|
631
781
|
}
|
|
632
782
|
async function resolveOriginalPosition(compiledPos, projectRoot) {
|
|
633
783
|
try {
|
|
634
784
|
console.log("resolveOriginalPosition called with:", compiledPos);
|
|
635
785
|
let compiledFilePath = compiledPos.filePath;
|
|
636
|
-
compiledFilePath =
|
|
637
|
-
console.log("After
|
|
786
|
+
compiledFilePath = cleanPath(compiledFilePath);
|
|
787
|
+
console.log("After cleanPath:", compiledFilePath);
|
|
638
788
|
if (compiledFilePath.startsWith("http://") || compiledFilePath.startsWith("https://")) {
|
|
639
789
|
const url = new URL(compiledFilePath);
|
|
640
790
|
const pathname = url.pathname;
|
|
@@ -807,7 +957,6 @@ async function getOriginalPositionFromDebugStack(debugStack, projectRoot) {
|
|
|
807
957
|
return await resolveOriginalPosition(compiledPos, projectRoot);
|
|
808
958
|
}
|
|
809
959
|
async function handleRead(req) {
|
|
810
|
-
var _a;
|
|
811
960
|
const devModeError = validateDevMode();
|
|
812
961
|
if (devModeError) return devModeError;
|
|
813
962
|
const { searchParams } = new URL(req.url);
|
|
@@ -824,6 +973,11 @@ async function handleRead(req) {
|
|
|
824
973
|
textContent: textContent || void 0,
|
|
825
974
|
className: className || void 0
|
|
826
975
|
} : void 0;
|
|
976
|
+
const parentFilePath = searchParams.get("parentFilePath") || "";
|
|
977
|
+
const parentLine = parseInt(searchParams.get("parentLine") || "0");
|
|
978
|
+
const parentComponentName = searchParams.get("parentComponentName") || "";
|
|
979
|
+
const parentDebugStack = searchParams.get("parentDebugStack") || "";
|
|
980
|
+
const childKey = searchParams.get("childKey") || "";
|
|
827
981
|
const projectRoot = process.cwd();
|
|
828
982
|
if (debugStack) {
|
|
829
983
|
const compiledPos = parseDebugStack(debugStack);
|
|
@@ -854,21 +1008,7 @@ async function handleRead(req) {
|
|
|
854
1008
|
{ status: 400 }
|
|
855
1009
|
);
|
|
856
1010
|
}
|
|
857
|
-
|
|
858
|
-
traverse(ast, {
|
|
859
|
-
ExportDefaultDeclaration(path2) {
|
|
860
|
-
var _a2;
|
|
861
|
-
if (t.isFunctionDeclaration(path2.node.declaration)) {
|
|
862
|
-
componentName = ((_a2 = path2.node.declaration.id) == null ? void 0 : _a2.name) || "";
|
|
863
|
-
}
|
|
864
|
-
},
|
|
865
|
-
ExportNamedDeclaration(path2) {
|
|
866
|
-
var _a2;
|
|
867
|
-
if (t.isFunctionDeclaration(path2.node.declaration)) {
|
|
868
|
-
componentName = ((_a2 = path2.node.declaration.id) == null ? void 0 : _a2.name) || "";
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
});
|
|
1011
|
+
const componentName = extractComponentName(ast);
|
|
872
1012
|
if (!componentName) {
|
|
873
1013
|
return NextResponse.json({
|
|
874
1014
|
success: true,
|
|
@@ -890,28 +1030,85 @@ async function handleRead(req) {
|
|
|
890
1030
|
lineEnd: content.split("\n").length
|
|
891
1031
|
});
|
|
892
1032
|
}
|
|
893
|
-
console.log(`[/read] Found element:`);
|
|
894
|
-
console.log(
|
|
895
|
-
` Component: ${componentName} (lines ${target.componentStart}-${target.componentEnd})`
|
|
896
|
-
);
|
|
897
|
-
console.log(` Target element: lines ${target.startLine}-${target.endLine}`);
|
|
898
|
-
console.log(
|
|
899
|
-
` Element context: tagName=${elementContext == null ? void 0 : elementContext.tagName}, nthOfType=${elementContext == null ? void 0 : elementContext.nthOfType}`
|
|
900
|
-
);
|
|
901
|
-
const foundLines = content.split("\n").slice(
|
|
902
|
-
target.startLine - 1,
|
|
903
|
-
Math.min(target.startLine + 2, target.endLine)
|
|
904
|
-
);
|
|
905
|
-
console.log(` Preview: ${(_a = foundLines[0]) == null ? void 0 : _a.trim()}`);
|
|
906
|
-
console.log(
|
|
907
|
-
` textContent="${elementContext == null ? void 0 : elementContext.textContent}", className="${elementContext == null ? void 0 : elementContext.className}"`
|
|
908
|
-
);
|
|
909
1033
|
const lines = content.split("\n");
|
|
910
1034
|
const componentLines = lines.slice(
|
|
911
1035
|
target.componentStart - 1,
|
|
912
1036
|
target.componentEnd
|
|
913
1037
|
);
|
|
914
1038
|
const preview = componentLines.join("\n");
|
|
1039
|
+
let parentInstance = null;
|
|
1040
|
+
if (parentDebugStack) {
|
|
1041
|
+
try {
|
|
1042
|
+
let resolvedParentPath = parentFilePath;
|
|
1043
|
+
let resolvedParentLine = parentLine;
|
|
1044
|
+
let resolvedParentComponentName = parentComponentName;
|
|
1045
|
+
if (!resolvedParentComponentName && parentDebugStack) {
|
|
1046
|
+
resolvedParentComponentName = extractComponentNameFromStack(parentDebugStack) || "";
|
|
1047
|
+
}
|
|
1048
|
+
if (parentDebugStack) {
|
|
1049
|
+
const compiledPos = parseDebugStack(parentDebugStack);
|
|
1050
|
+
if (compiledPos) {
|
|
1051
|
+
const originalPos = await resolveOriginalPosition(
|
|
1052
|
+
compiledPos,
|
|
1053
|
+
projectRoot
|
|
1054
|
+
);
|
|
1055
|
+
if (originalPos) {
|
|
1056
|
+
resolvedParentPath = originalPos.source;
|
|
1057
|
+
resolvedParentLine = originalPos.line;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
const normalizedParentPath = normalizePath(resolvedParentPath);
|
|
1062
|
+
const absoluteParentPath = await resolveFilePath(
|
|
1063
|
+
projectRoot,
|
|
1064
|
+
normalizedParentPath
|
|
1065
|
+
);
|
|
1066
|
+
if (absoluteParentPath && resolvedParentComponentName) {
|
|
1067
|
+
const parentContent = await fs.readFile(absoluteParentPath, "utf-8");
|
|
1068
|
+
const parentAst = parseFile(parentContent);
|
|
1069
|
+
if (parentAst) {
|
|
1070
|
+
let nthOfType2 = void 0;
|
|
1071
|
+
if (childKey) {
|
|
1072
|
+
const keyAsNumber = parseInt(childKey, 10);
|
|
1073
|
+
if (!isNaN(keyAsNumber)) {
|
|
1074
|
+
nthOfType2 = keyAsNumber + 1;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
const parentTarget = findTargetElement(parentAst, parentContent, {
|
|
1078
|
+
componentName: resolvedParentComponentName,
|
|
1079
|
+
lineNumber: 0,
|
|
1080
|
+
// Don't use line number - rely on element context to find correct instance
|
|
1081
|
+
elementContext: {
|
|
1082
|
+
tagName: componentName,
|
|
1083
|
+
// Search for child component usage
|
|
1084
|
+
nthOfType: nthOfType2,
|
|
1085
|
+
// Find specific instance if key is numeric
|
|
1086
|
+
textContent: textContent || void 0
|
|
1087
|
+
// Use text content to match the specific instance
|
|
1088
|
+
}
|
|
1089
|
+
});
|
|
1090
|
+
if (parentTarget) {
|
|
1091
|
+
const parentLines = parentContent.split("\n");
|
|
1092
|
+
const parentComponentLines = parentLines.slice(
|
|
1093
|
+
parentTarget.componentStart - 1,
|
|
1094
|
+
parentTarget.componentEnd
|
|
1095
|
+
);
|
|
1096
|
+
parentInstance = {
|
|
1097
|
+
filePath: resolvedParentPath,
|
|
1098
|
+
content: parentComponentLines.join("\n"),
|
|
1099
|
+
lineStart: parentTarget.componentStart,
|
|
1100
|
+
lineEnd: parentTarget.componentEnd,
|
|
1101
|
+
usageLineStart: parentTarget.startLine,
|
|
1102
|
+
usageLineEnd: parentTarget.endLine,
|
|
1103
|
+
componentName: resolvedParentComponentName
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
} catch (error) {
|
|
1109
|
+
console.error("Error resolving parent instance:", error);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
915
1112
|
return NextResponse.json({
|
|
916
1113
|
success: true,
|
|
917
1114
|
content: preview,
|
|
@@ -919,8 +1116,10 @@ async function handleRead(req) {
|
|
|
919
1116
|
lineEnd: target.componentEnd,
|
|
920
1117
|
targetStartLine: target.startLine,
|
|
921
1118
|
targetEndLine: target.endLine,
|
|
922
|
-
componentName
|
|
1119
|
+
componentName,
|
|
923
1120
|
// Return the actual component name parsed from code
|
|
1121
|
+
parentInstance
|
|
1122
|
+
// Optional: where this component is used
|
|
924
1123
|
});
|
|
925
1124
|
}
|
|
926
1125
|
async function handleUndo(req) {
|
|
@@ -976,37 +1175,58 @@ async function handleResolve(req) {
|
|
|
976
1175
|
{ status: 400 }
|
|
977
1176
|
);
|
|
978
1177
|
}
|
|
979
|
-
const
|
|
980
|
-
if (
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1178
|
+
const compiledFrames = parseDebugStackFrames(debugStack);
|
|
1179
|
+
if (compiledFrames.length === 0) {
|
|
1180
|
+
const compiledPos = parseDebugStack(debugStack);
|
|
1181
|
+
if (!compiledPos) {
|
|
1182
|
+
console.error("Could not parse debug stack:", debugStack);
|
|
1183
|
+
return NextResponse.json(
|
|
1184
|
+
{ success: false, error: "Could not parse stack" },
|
|
1185
|
+
{ status: 422 }
|
|
1186
|
+
);
|
|
1187
|
+
}
|
|
1188
|
+
const originalPos = await resolveOriginalPosition(
|
|
1189
|
+
compiledPos,
|
|
1190
|
+
process.cwd()
|
|
985
1191
|
);
|
|
1192
|
+
if (!originalPos) {
|
|
1193
|
+
return NextResponse.json(
|
|
1194
|
+
{ success: false, error: "Source map lookup failed" },
|
|
1195
|
+
{ status: 404 }
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
return NextResponse.json({
|
|
1199
|
+
success: true,
|
|
1200
|
+
filePath: originalPos.source,
|
|
1201
|
+
lineNumber: originalPos.line,
|
|
1202
|
+
columnNumber: originalPos.column ?? 0
|
|
1203
|
+
});
|
|
986
1204
|
}
|
|
987
|
-
|
|
988
|
-
const
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1205
|
+
const resolvedFrames = [];
|
|
1206
|
+
for (const frame of compiledFrames) {
|
|
1207
|
+
const originalPos = await resolveOriginalPosition(frame, process.cwd());
|
|
1208
|
+
if (originalPos) {
|
|
1209
|
+
resolvedFrames.push({
|
|
1210
|
+
filePath: originalPos.source,
|
|
1211
|
+
lineNumber: originalPos.line,
|
|
1212
|
+
columnNumber: originalPos.column ?? 0
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
if (resolvedFrames.length === 0) {
|
|
999
1217
|
return NextResponse.json(
|
|
1000
|
-
{ success: false, error: "Source map lookup failed" },
|
|
1218
|
+
{ success: false, error: "Source map lookup failed for all frames" },
|
|
1001
1219
|
{ status: 404 }
|
|
1002
1220
|
);
|
|
1003
1221
|
}
|
|
1004
|
-
console.log("Resolved
|
|
1222
|
+
console.log("Resolved frames:", resolvedFrames);
|
|
1005
1223
|
return NextResponse.json({
|
|
1006
1224
|
success: true,
|
|
1007
|
-
filePath:
|
|
1008
|
-
lineNumber:
|
|
1009
|
-
columnNumber:
|
|
1225
|
+
filePath: resolvedFrames[0].filePath,
|
|
1226
|
+
lineNumber: resolvedFrames[0].lineNumber,
|
|
1227
|
+
columnNumber: resolvedFrames[0].columnNumber,
|
|
1228
|
+
frames: resolvedFrames
|
|
1229
|
+
// [componentDefinition, parentInstance]
|
|
1010
1230
|
});
|
|
1011
1231
|
} catch (error) {
|
|
1012
1232
|
console.error("Source resolve error:", error);
|
|
@@ -1091,6 +1311,209 @@ async function handleValidateSession(req) {
|
|
|
1091
1311
|
);
|
|
1092
1312
|
}
|
|
1093
1313
|
}
|
|
1314
|
+
const suggestionCache = /* @__PURE__ */ new Map();
|
|
1315
|
+
const CACHE_TTL = 3e4;
|
|
1316
|
+
function getCacheKey(params) {
|
|
1317
|
+
return `${params.componentName}:${params.elementTag || "div"}:${params.lastSuggestion || ""}`;
|
|
1318
|
+
}
|
|
1319
|
+
function getCachedSuggestions(key) {
|
|
1320
|
+
const cached = suggestionCache.get(key);
|
|
1321
|
+
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
|
|
1322
|
+
return cached.suggestions;
|
|
1323
|
+
}
|
|
1324
|
+
suggestionCache.delete(key);
|
|
1325
|
+
return null;
|
|
1326
|
+
}
|
|
1327
|
+
function cacheSuggestions(key, suggestions) {
|
|
1328
|
+
suggestionCache.set(key, { suggestions, timestamp: Date.now() });
|
|
1329
|
+
if (suggestionCache.size > 100) {
|
|
1330
|
+
const oldestKeys = Array.from(suggestionCache.entries()).sort((a, b) => a[1].timestamp - b[1].timestamp).slice(0, 20).map(([key2]) => key2);
|
|
1331
|
+
oldestKeys.forEach((key2) => suggestionCache.delete(key2));
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
const DEFAULT_SUGGESTIONS = [
|
|
1335
|
+
"Add padding",
|
|
1336
|
+
"Change color",
|
|
1337
|
+
"Make larger",
|
|
1338
|
+
"Add shadow",
|
|
1339
|
+
"Round corners",
|
|
1340
|
+
"Center content",
|
|
1341
|
+
"Add hover effect",
|
|
1342
|
+
"Adjust spacing"
|
|
1343
|
+
];
|
|
1344
|
+
async function handleSuggestions(req) {
|
|
1345
|
+
const devModeError = validateDevMode();
|
|
1346
|
+
if (devModeError) return devModeError;
|
|
1347
|
+
try {
|
|
1348
|
+
const { searchParams } = new URL(req.url);
|
|
1349
|
+
const componentName = searchParams.get("componentName") || "Component";
|
|
1350
|
+
const elementTag = searchParams.get("elementTag") || void 0;
|
|
1351
|
+
const className = searchParams.get("className") || void 0;
|
|
1352
|
+
const textContent = searchParams.get("textContent") || void 0;
|
|
1353
|
+
const lastSuggestion = searchParams.get("lastSuggestion") || void 0;
|
|
1354
|
+
const editHistoryStr = searchParams.get("editHistory") || void 0;
|
|
1355
|
+
const excludedSuggestionsStr = searchParams.get("excludedSuggestions") || void 0;
|
|
1356
|
+
let editHistory = [];
|
|
1357
|
+
if (editHistoryStr) {
|
|
1358
|
+
try {
|
|
1359
|
+
editHistory = JSON.parse(editHistoryStr);
|
|
1360
|
+
} catch (e) {
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
let excludedSuggestions = [];
|
|
1364
|
+
if (excludedSuggestionsStr) {
|
|
1365
|
+
try {
|
|
1366
|
+
excludedSuggestions = JSON.parse(excludedSuggestionsStr);
|
|
1367
|
+
} catch (e) {
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
const cacheKey = getCacheKey({ componentName, elementTag, lastSuggestion });
|
|
1371
|
+
const cached = excludedSuggestions.length === 0 ? getCachedSuggestions(cacheKey) : null;
|
|
1372
|
+
if (cached) {
|
|
1373
|
+
return NextResponse.json({
|
|
1374
|
+
success: true,
|
|
1375
|
+
suggestions: cached
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
const suggestions = await generateSuggestions({
|
|
1379
|
+
componentName,
|
|
1380
|
+
elementTag,
|
|
1381
|
+
className,
|
|
1382
|
+
textContent,
|
|
1383
|
+
editHistory,
|
|
1384
|
+
lastSuggestion,
|
|
1385
|
+
excludedSuggestions
|
|
1386
|
+
});
|
|
1387
|
+
if (suggestions && suggestions.length > 0) {
|
|
1388
|
+
cacheSuggestions(cacheKey, suggestions);
|
|
1389
|
+
return NextResponse.json({
|
|
1390
|
+
success: true,
|
|
1391
|
+
suggestions
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
return NextResponse.json({
|
|
1395
|
+
success: true,
|
|
1396
|
+
suggestions: DEFAULT_SUGGESTIONS
|
|
1397
|
+
});
|
|
1398
|
+
} catch (error) {
|
|
1399
|
+
return NextResponse.json(
|
|
1400
|
+
{
|
|
1401
|
+
success: false,
|
|
1402
|
+
error: String(error),
|
|
1403
|
+
suggestions: DEFAULT_SUGGESTIONS
|
|
1404
|
+
// Fallback
|
|
1405
|
+
},
|
|
1406
|
+
{ status: 500 }
|
|
1407
|
+
);
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
async function generateSuggestions(options) {
|
|
1411
|
+
const {
|
|
1412
|
+
componentName,
|
|
1413
|
+
elementTag,
|
|
1414
|
+
className,
|
|
1415
|
+
textContent,
|
|
1416
|
+
editHistory,
|
|
1417
|
+
lastSuggestion,
|
|
1418
|
+
excludedSuggestions = []
|
|
1419
|
+
} = options;
|
|
1420
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
1421
|
+
if (!apiKey) {
|
|
1422
|
+
return null;
|
|
1423
|
+
}
|
|
1424
|
+
const model = new ChatAnthropic({
|
|
1425
|
+
apiKey,
|
|
1426
|
+
modelName: "claude-haiku-4-5-20251001",
|
|
1427
|
+
maxTokens: 1024,
|
|
1428
|
+
temperature: 0.3
|
|
1429
|
+
// Slightly creative for variety
|
|
1430
|
+
});
|
|
1431
|
+
const systemPrompt = `You are a UI/UX expert suggesting quick edits for React components.
|
|
1432
|
+
|
|
1433
|
+
Generate 6-8 concise, actionable suggestions that a developer might want to make next.
|
|
1434
|
+
|
|
1435
|
+
RULES:
|
|
1436
|
+
- Each suggestion must be 2-6 words (e.g., "Add padding", "Make text larger")
|
|
1437
|
+
- Focus on common UI improvements: spacing, colors, sizing, layout, shadows, borders, typography
|
|
1438
|
+
- Consider the element type (${elementTag || "element"})
|
|
1439
|
+
- Output ONLY a JSON array of strings, no explanations, no markdown fences
|
|
1440
|
+
${excludedSuggestions.length > 0 ? `- DO NOT suggest any of these (user wants different options): ${excludedSuggestions.join(
|
|
1441
|
+
", "
|
|
1442
|
+
)}` : ""}
|
|
1443
|
+
|
|
1444
|
+
${lastSuggestion ? `IMPORTANT - LAST EDIT CONTEXT:
|
|
1445
|
+
The user just applied: "${lastSuggestion}"
|
|
1446
|
+
Your suggestions MUST be direct follow-ups/refinements of this last change:
|
|
1447
|
+
- If it was "Add padding" → suggest "More padding", "Less padding", "Add vertical padding only"
|
|
1448
|
+
- If it was "Make it blue" → suggest "Darker blue", "Lighter blue", "Change to navy blue"
|
|
1449
|
+
- If it was "Increase font size" → suggest "Decrease font size", "Make even larger", "Adjust line height"
|
|
1450
|
+
- 4-6 suggestions should be variations/refinements of the last edit
|
|
1451
|
+
- 2-4 suggestions can be other related improvements
|
|
1452
|
+
|
|
1453
|
+
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.`}
|
|
1454
|
+
|
|
1455
|
+
Example output format:
|
|
1456
|
+
["Add hover effect", "Increase padding", "Make corners rounder", "Change to flex row", "Add drop shadow", "Adjust font size"]`;
|
|
1457
|
+
let userPrompt = `Element: <${elementTag || "div"}>`;
|
|
1458
|
+
if (className) {
|
|
1459
|
+
userPrompt += `
|
|
1460
|
+
Classes: ${className}`;
|
|
1461
|
+
}
|
|
1462
|
+
if (textContent) {
|
|
1463
|
+
userPrompt += `
|
|
1464
|
+
Text: "${textContent.slice(0, 50)}"`;
|
|
1465
|
+
}
|
|
1466
|
+
userPrompt += `
|
|
1467
|
+
Component: ${componentName}`;
|
|
1468
|
+
if (editHistory && editHistory.length > 0) {
|
|
1469
|
+
userPrompt += `
|
|
1470
|
+
|
|
1471
|
+
Recent edits:
|
|
1472
|
+
`;
|
|
1473
|
+
editHistory.slice(-3).forEach((item, idx) => {
|
|
1474
|
+
userPrompt += `${idx + 1}. ${item.suggestion} ${item.success ? "✓" : "✗"}
|
|
1475
|
+
`;
|
|
1476
|
+
});
|
|
1477
|
+
} else {
|
|
1478
|
+
userPrompt += `
|
|
1479
|
+
|
|
1480
|
+
No previous edits.`;
|
|
1481
|
+
}
|
|
1482
|
+
if (lastSuggestion) {
|
|
1483
|
+
userPrompt += `
|
|
1484
|
+
|
|
1485
|
+
**LAST EDIT APPLIED:** "${lastSuggestion}"`;
|
|
1486
|
+
userPrompt += `
|
|
1487
|
+
|
|
1488
|
+
Generate 6-8 follow-up suggestions (mostly variations of the last edit):`;
|
|
1489
|
+
} else {
|
|
1490
|
+
userPrompt += `
|
|
1491
|
+
|
|
1492
|
+
Generate 6-8 initial suggestions:`;
|
|
1493
|
+
}
|
|
1494
|
+
try {
|
|
1495
|
+
const response = await model.invoke([
|
|
1496
|
+
new SystemMessage(systemPrompt),
|
|
1497
|
+
new HumanMessage(userPrompt)
|
|
1498
|
+
]);
|
|
1499
|
+
let content = typeof response.content === "string" ? response.content : String(response.content);
|
|
1500
|
+
content = content.trim();
|
|
1501
|
+
content = content.replace(/^```json?\s*/gm, "").replace(/\s*```$/gm, "");
|
|
1502
|
+
const suggestions = JSON.parse(content);
|
|
1503
|
+
if (Array.isArray(suggestions)) {
|
|
1504
|
+
const validSuggestions = suggestions.filter((s) => typeof s === "string").map((s) => s.trim()).filter((s) => {
|
|
1505
|
+
const words = s.split(/\s+/).length;
|
|
1506
|
+
return words >= 1 && words <= 15;
|
|
1507
|
+
}).slice(0, 8);
|
|
1508
|
+
if (validSuggestions.length >= 4) {
|
|
1509
|
+
return validSuggestions;
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
return null;
|
|
1513
|
+
} catch (e) {
|
|
1514
|
+
return null;
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1094
1517
|
async function handleAIEditorRequest(req, context) {
|
|
1095
1518
|
const { path: path2 } = await context.params;
|
|
1096
1519
|
const endpoint = path2[0];
|
|
@@ -1114,6 +1537,9 @@ async function handleAIEditorRequest(req, context) {
|
|
|
1114
1537
|
case "validate-session":
|
|
1115
1538
|
if (method === "POST") return handleValidateSession(req);
|
|
1116
1539
|
break;
|
|
1540
|
+
case "suggestions":
|
|
1541
|
+
if (method === "GET") return handleSuggestions(req);
|
|
1542
|
+
break;
|
|
1117
1543
|
}
|
|
1118
1544
|
return NextResponse.json(
|
|
1119
1545
|
{ error: `Unknown endpoint: ${endpoint}` },
|
|
@@ -1127,19 +1553,24 @@ export {
|
|
|
1127
1553
|
handleResolve as d,
|
|
1128
1554
|
handleAbsolutePath as e,
|
|
1129
1555
|
handleValidateSession as f,
|
|
1130
|
-
|
|
1556
|
+
handleSuggestions as g,
|
|
1131
1557
|
handleAIEditorRequest as h,
|
|
1132
1558
|
isPathSecure as i,
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1559
|
+
fileExists$1 as j,
|
|
1560
|
+
extractComponentName as k,
|
|
1561
|
+
validateGeneratedCode as l,
|
|
1562
|
+
findTargetElement as m,
|
|
1137
1563
|
normalizePath as n,
|
|
1138
|
-
|
|
1564
|
+
getJSXMemberName as o,
|
|
1139
1565
|
parseFile as p,
|
|
1140
|
-
|
|
1566
|
+
getAttributeValue as q,
|
|
1141
1567
|
resolveFilePath as r,
|
|
1142
1568
|
scoreElementMatch as s,
|
|
1143
|
-
|
|
1569
|
+
parseDebugStack as t,
|
|
1570
|
+
extractComponentNameFromStack as u,
|
|
1571
|
+
validateDevMode as v,
|
|
1572
|
+
parseDebugStackFrames as w,
|
|
1573
|
+
resolveOriginalPosition as x,
|
|
1574
|
+
getOriginalPositionFromDebugStack as y
|
|
1144
1575
|
};
|
|
1145
|
-
//# sourceMappingURL=index-
|
|
1576
|
+
//# sourceMappingURL=index-DrmEf13c.js.map
|