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,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,74 @@ 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
|
+
}
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
return componentName;
|
|
95
|
+
}
|
|
96
|
+
function validateGeneratedCode(newCode, originalCode, fileContent) {
|
|
97
|
+
try {
|
|
98
|
+
const isFullComponent = /^(export\s+)?(default\s+)?function\s+\w+/.test(newCode.trim()) || /^(export\s+)?(default\s+)?const\s+\w+\s*=/.test(newCode.trim());
|
|
99
|
+
if (isFullComponent) {
|
|
100
|
+
let codeToValidate = newCode;
|
|
101
|
+
if (fileContent) {
|
|
102
|
+
const interfaceMatches = fileContent.match(
|
|
103
|
+
/^(interface|type)\s+\w+[^}]*\}/gm
|
|
104
|
+
);
|
|
105
|
+
if (interfaceMatches) {
|
|
106
|
+
codeToValidate = interfaceMatches.join("\n\n") + "\n\n" + newCode;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
parser.parse(codeToValidate, {
|
|
110
|
+
sourceType: "module",
|
|
111
|
+
plugins: ["jsx", "typescript"]
|
|
112
|
+
});
|
|
113
|
+
} else {
|
|
114
|
+
const wrapped = `function _() { return (${newCode}); }`;
|
|
115
|
+
parser.parse(wrapped, {
|
|
116
|
+
sourceType: "module",
|
|
117
|
+
plugins: ["jsx", "typescript"]
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
} catch (e) {
|
|
121
|
+
console.error("Generated code parse error:", e);
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
const origBraces = (originalCode.match(/[{}]/g) || []).length;
|
|
125
|
+
const newBraces = (newCode.match(/[{}]/g) || []).length;
|
|
126
|
+
const origTags = (originalCode.match(/[<>]/g) || []).length;
|
|
127
|
+
const newTags = (newCode.match(/[<>]/g) || []).length;
|
|
128
|
+
if (Math.abs(origBraces - newBraces) > 4 || Math.abs(origTags - newTags) > 4) {
|
|
129
|
+
console.warn(
|
|
130
|
+
`Structure changed significantly: braces ${origBraces}->${newBraces}, tags ${origTags}->${newTags}`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
68
135
|
function findTargetElement(ast, fileContent, options) {
|
|
69
136
|
const { componentName, lineNumber, elementContext } = options;
|
|
70
|
-
const matches = [];
|
|
71
137
|
let componentNode = null;
|
|
72
138
|
let componentStart = 0;
|
|
73
139
|
let componentEnd = Infinity;
|
|
@@ -118,6 +184,7 @@ function findTargetElement(ast, fileContent, options) {
|
|
|
118
184
|
componentEnd = fallback.end;
|
|
119
185
|
}
|
|
120
186
|
const allElementsByTag = /* @__PURE__ */ new Map();
|
|
187
|
+
const elementsAtLine = [];
|
|
121
188
|
traverse(ast, {
|
|
122
189
|
JSXElement(path2) {
|
|
123
190
|
const loc = path2.node.loc;
|
|
@@ -133,59 +200,89 @@ function findTargetElement(ast, fileContent, options) {
|
|
|
133
200
|
}
|
|
134
201
|
allElementsByTag.get(tagName).push({ node: path2.node, startLine, endLine, score: 0 });
|
|
135
202
|
}
|
|
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
|
-
});
|
|
203
|
+
if (startLine === lineNumber) {
|
|
204
|
+
elementsAtLine.push({ node: path2.node, startLine, endLine, score: 0 });
|
|
148
205
|
}
|
|
149
206
|
}
|
|
150
207
|
});
|
|
151
|
-
if (
|
|
152
|
-
if (
|
|
208
|
+
if (elementsAtLine.length > 0) {
|
|
209
|
+
if (elementsAtLine.length === 1) {
|
|
210
|
+
const target = elementsAtLine[0];
|
|
153
211
|
return {
|
|
154
|
-
startLine:
|
|
155
|
-
endLine:
|
|
212
|
+
startLine: target.startLine,
|
|
213
|
+
endLine: target.endLine,
|
|
156
214
|
componentStart,
|
|
157
215
|
componentEnd
|
|
158
216
|
};
|
|
159
217
|
}
|
|
160
|
-
|
|
218
|
+
if (elementContext) {
|
|
219
|
+
for (const elem of elementsAtLine) {
|
|
220
|
+
if (t.isJSXElement(elem.node)) {
|
|
221
|
+
const score = scoreElementMatch(elem.node, elementContext, fileContent);
|
|
222
|
+
elem.score = score;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
elementsAtLine.sort((a, b) => b.score - a.score);
|
|
226
|
+
if (elementsAtLine[0].score > 0) {
|
|
227
|
+
return {
|
|
228
|
+
startLine: elementsAtLine[0].startLine,
|
|
229
|
+
endLine: elementsAtLine[0].endLine,
|
|
230
|
+
componentStart,
|
|
231
|
+
componentEnd
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
startLine: elementsAtLine[0].startLine,
|
|
237
|
+
endLine: elementsAtLine[0].endLine,
|
|
238
|
+
componentStart,
|
|
239
|
+
componentEnd
|
|
240
|
+
};
|
|
161
241
|
}
|
|
162
242
|
if ((elementContext == null ? void 0 : elementContext.nthOfType) && elementContext.tagName) {
|
|
163
243
|
const allOfTag = allElementsByTag.get(elementContext.tagName);
|
|
164
244
|
if (allOfTag && allOfTag.length >= elementContext.nthOfType) {
|
|
165
245
|
const target = allOfTag[elementContext.nthOfType - 1];
|
|
166
|
-
console.log(
|
|
167
|
-
` Using nthOfType=${elementContext.nthOfType}: found ${allOfTag.length} <${elementContext.tagName}> elements`
|
|
168
|
-
);
|
|
169
246
|
return {
|
|
170
247
|
startLine: target.startLine,
|
|
171
248
|
endLine: target.endLine,
|
|
172
249
|
componentStart,
|
|
173
250
|
componentEnd
|
|
174
251
|
};
|
|
175
|
-
} else {
|
|
176
|
-
console.log(
|
|
177
|
-
` nthOfType=${elementContext.nthOfType} but only found ${(allOfTag == null ? void 0 : allOfTag.length) || 0} <${elementContext.tagName}> elements`
|
|
178
|
-
);
|
|
179
252
|
}
|
|
180
253
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
254
|
+
const nearbyElements = [];
|
|
255
|
+
traverse(ast, {
|
|
256
|
+
JSXElement(path2) {
|
|
257
|
+
const loc = path2.node.loc;
|
|
258
|
+
if (!loc) return;
|
|
259
|
+
const startLine = loc.start.line;
|
|
260
|
+
const endLine = loc.end.line;
|
|
261
|
+
if (startLine < componentStart || endLine > componentEnd) return;
|
|
262
|
+
if (Math.abs(startLine - lineNumber) <= 5) {
|
|
263
|
+
const score = elementContext ? scoreElementMatch(path2.node, elementContext, fileContent) : 100 - Math.abs(startLine - lineNumber);
|
|
264
|
+
nearbyElements.push({ node: path2.node, startLine, endLine, score });
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
if (nearbyElements.length > 0) {
|
|
269
|
+
nearbyElements.sort((a, b) => b.score - a.score);
|
|
270
|
+
return {
|
|
271
|
+
startLine: nearbyElements[0].startLine,
|
|
272
|
+
endLine: nearbyElements[0].endLine,
|
|
273
|
+
componentStart,
|
|
274
|
+
componentEnd
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
if (componentNode && componentStart > 0) {
|
|
278
|
+
return {
|
|
279
|
+
startLine: componentStart,
|
|
280
|
+
endLine: componentEnd,
|
|
281
|
+
componentStart,
|
|
282
|
+
componentEnd
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
return null;
|
|
189
286
|
}
|
|
190
287
|
function scoreElementMatch(node, context, fileContent) {
|
|
191
288
|
let score = 0;
|
|
@@ -269,7 +366,8 @@ async function handleEdit(req) {
|
|
|
269
366
|
componentName,
|
|
270
367
|
suggestion,
|
|
271
368
|
elementContext,
|
|
272
|
-
editHistory
|
|
369
|
+
editHistory,
|
|
370
|
+
parentInstance
|
|
273
371
|
} = body;
|
|
274
372
|
const projectRoot = process.cwd();
|
|
275
373
|
const normalizedPath = normalizePath(filePath);
|
|
@@ -281,7 +379,6 @@ async function handleEdit(req) {
|
|
|
281
379
|
);
|
|
282
380
|
}
|
|
283
381
|
const fileContent = await fs.readFile(absolutePath, "utf-8");
|
|
284
|
-
console.log(`[/edit] componentName="${componentName}", filePath="${filePath}"`);
|
|
285
382
|
const ast = parseFile(fileContent);
|
|
286
383
|
if (!ast) {
|
|
287
384
|
return NextResponse.json(
|
|
@@ -300,20 +397,10 @@ async function handleEdit(req) {
|
|
|
300
397
|
{ status: 400 }
|
|
301
398
|
);
|
|
302
399
|
}
|
|
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
400
|
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
401
|
const targetCode = lines.slice(target.startLine - 1, target.endLine).join("\n");
|
|
314
402
|
const baseIndentation = ((_a = lines[target.startLine - 1].match(/^(\s*)/)) == null ? void 0 : _a[1]) || "";
|
|
315
403
|
if (target.componentStart <= 0 || target.componentEnd === Infinity) {
|
|
316
|
-
console.error("❌ Invalid component bounds detected");
|
|
317
404
|
return NextResponse.json({
|
|
318
405
|
success: false,
|
|
319
406
|
error: `Could not determine component bounds. Component: ${target.componentStart}-${target.componentEnd}`
|
|
@@ -351,7 +438,8 @@ ${foundElementCode}`);
|
|
|
351
438
|
targetEndLine: target.endLine,
|
|
352
439
|
componentStart: target.componentStart,
|
|
353
440
|
componentEnd: target.componentEnd,
|
|
354
|
-
editHistory: editHistory || []
|
|
441
|
+
editHistory: editHistory || [],
|
|
442
|
+
parentInstance
|
|
355
443
|
});
|
|
356
444
|
if (!newCode) {
|
|
357
445
|
return NextResponse.json({
|
|
@@ -359,38 +447,56 @@ ${foundElementCode}`);
|
|
|
359
447
|
error: "AI failed to generate valid edit"
|
|
360
448
|
});
|
|
361
449
|
}
|
|
362
|
-
|
|
363
|
-
|
|
450
|
+
const parentInstanceMatch = newCode.match(/\/\/ EDIT_PARENT_INSTANCE\s*\n([\s\S]+)/);
|
|
451
|
+
if (parentInstanceMatch && parentInstance) {
|
|
452
|
+
const parentCode = parentInstanceMatch[1].trim();
|
|
453
|
+
const parentNormalizedPath = normalizePath(parentInstance.filePath);
|
|
454
|
+
const parentAbsolutePath = await resolveFilePath(projectRoot, parentNormalizedPath);
|
|
455
|
+
if (!parentAbsolutePath) {
|
|
456
|
+
return NextResponse.json({
|
|
457
|
+
success: false,
|
|
458
|
+
error: `Parent file not found: ${parentNormalizedPath}`
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
const parentFileContent = await fs.readFile(parentAbsolutePath, "utf-8");
|
|
462
|
+
const parentLines = parentFileContent.split("\n");
|
|
463
|
+
const newParentLines = [...parentLines];
|
|
464
|
+
newParentLines.splice(
|
|
465
|
+
parentInstance.lineStart - 1,
|
|
466
|
+
parentInstance.lineEnd - parentInstance.lineStart + 1,
|
|
467
|
+
...parentCode.split("\n")
|
|
468
|
+
);
|
|
469
|
+
await fs.writeFile(parentAbsolutePath, newParentLines.join("\n"), "utf-8");
|
|
470
|
+
return NextResponse.json({
|
|
471
|
+
success: true,
|
|
472
|
+
fileSnapshot: parentFileContent,
|
|
473
|
+
generatedCode: parentCode,
|
|
474
|
+
modifiedLines: {
|
|
475
|
+
start: parentInstance.lineStart,
|
|
476
|
+
end: parentInstance.lineEnd
|
|
477
|
+
},
|
|
478
|
+
editedFile: parentInstance.filePath
|
|
479
|
+
// Indicate which file was edited
|
|
480
|
+
});
|
|
481
|
+
}
|
|
364
482
|
const fullComponentMatch = newCode.match(/\/\/ FULL_COMPONENT\s*\n([\s\S]+)/);
|
|
365
483
|
let codeToApply = newCode;
|
|
366
484
|
let startLineToReplace = target.startLine;
|
|
367
485
|
let endLineToReplace = target.endLine;
|
|
486
|
+
const isFullComponentDeclaration = /^(export\s+)?(default\s+)?function\s+\w+/.test(newCode.trim()) || /^(export\s+)?const\s+\w+\s*=/.test(newCode.trim());
|
|
368
487
|
if (fullComponentMatch) {
|
|
369
|
-
console.log("Found // FULL_COMPONENT marker, extracting full component code");
|
|
370
488
|
codeToApply = fullComponentMatch[1].trim();
|
|
371
489
|
startLineToReplace = target.componentStart;
|
|
372
490
|
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
|
-
);
|
|
491
|
+
} else if (isFullComponentDeclaration && target.startLine !== target.componentStart) {
|
|
492
|
+
codeToApply = newCode;
|
|
493
|
+
startLineToReplace = target.componentStart;
|
|
494
|
+
endLineToReplace = target.componentEnd;
|
|
383
495
|
}
|
|
384
|
-
console.log("Code to apply (first 200 chars):", codeToApply.substring(0, 200));
|
|
385
496
|
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
497
|
return NextResponse.json({
|
|
392
498
|
success: false,
|
|
393
|
-
error: "Generated code is invalid
|
|
499
|
+
error: "Generated code is invalid"
|
|
394
500
|
});
|
|
395
501
|
}
|
|
396
502
|
const newLines = [...lines];
|
|
@@ -400,7 +506,6 @@ ${foundElementCode}`);
|
|
|
400
506
|
...codeToApply.split("\n")
|
|
401
507
|
);
|
|
402
508
|
await fs.writeFile(absolutePath, newLines.join("\n"), "utf-8");
|
|
403
|
-
console.log(`✅ Updated ${normalizedPath}`);
|
|
404
509
|
return NextResponse.json({
|
|
405
510
|
success: true,
|
|
406
511
|
fileSnapshot: fileContent,
|
|
@@ -410,10 +515,11 @@ ${foundElementCode}`);
|
|
|
410
515
|
modifiedLines: {
|
|
411
516
|
start: startLineToReplace,
|
|
412
517
|
end: endLineToReplace
|
|
413
|
-
}
|
|
518
|
+
},
|
|
519
|
+
editedFile: filePath
|
|
520
|
+
// Indicate which file was edited
|
|
414
521
|
});
|
|
415
522
|
} catch (error) {
|
|
416
|
-
console.error("AI Editor error:", error);
|
|
417
523
|
return NextResponse.json(
|
|
418
524
|
{ success: false, error: String(error) },
|
|
419
525
|
{ status: 500 }
|
|
@@ -425,7 +531,8 @@ async function generateEdit(options) {
|
|
|
425
531
|
fullComponentCode,
|
|
426
532
|
suggestion,
|
|
427
533
|
baseIndentation,
|
|
428
|
-
editHistory
|
|
534
|
+
editHistory,
|
|
535
|
+
parentInstance
|
|
429
536
|
} = options;
|
|
430
537
|
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
431
538
|
if (!apiKey) throw new Error("ANTHROPIC_API_KEY not set");
|
|
@@ -438,25 +545,29 @@ async function generateEdit(options) {
|
|
|
438
545
|
const systemPrompt = `You are a precise code editor for React/JSX components.
|
|
439
546
|
|
|
440
547
|
WHAT YOU'LL SEE:
|
|
441
|
-
- Full component
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
548
|
+
- Component Definition: Full code of the clicked component with line annotations
|
|
549
|
+
* The clicked element is marked with "// ← CLICKED ELEMENT STARTS" and "// ← CLICKED ELEMENT ENDS"
|
|
550
|
+
* These are just annotations - NOT part of the actual code
|
|
551
|
+
${parentInstance ? `- Parent Instance: Where this component is used, with "// ← COMPONENT USAGE" marking the usage line` : ""}
|
|
445
552
|
|
|
446
|
-
YOUR
|
|
447
|
-
Modify ONLY the marked element (unless the request requires changing its parent/wrapper).
|
|
553
|
+
YOUR DECISION - Choose ONE approach based on the user's request:
|
|
448
554
|
|
|
449
|
-
|
|
450
|
-
-
|
|
451
|
-
-
|
|
555
|
+
1. EDIT COMPONENT DEFINITION (most common):
|
|
556
|
+
- Modify styling, layout, or behavior that should apply to ALL instances
|
|
557
|
+
- Example requests: "make the button blue", "add padding", "change font size"
|
|
558
|
+
- Output: Just the modified element OR "// FULL_COMPONENT\\n" + complete modified component
|
|
559
|
+
|
|
560
|
+
${parentInstance ? `2. EDIT PARENT INSTANCE (when user wants to modify THIS specific usage):
|
|
561
|
+
- Change props, text, or configuration for THIS specific instance only
|
|
562
|
+
- Example requests: "change this title to...", "remove this card", "add another button here"
|
|
563
|
+
- Output: "// EDIT_PARENT_INSTANCE\\n" + complete modified parent component` : ""}
|
|
452
564
|
|
|
453
565
|
RULES:
|
|
454
566
|
- Output ONLY code, no explanations
|
|
455
567
|
- No markdown fences
|
|
456
|
-
- Do NOT include
|
|
568
|
+
- Do NOT include annotation comments in your output
|
|
457
569
|
- Preserve indentation
|
|
458
|
-
- Make minimal changes
|
|
459
|
-
- Do NOT modify unrelated elements`;
|
|
570
|
+
- Make minimal changes`;
|
|
460
571
|
let userPrompt = "";
|
|
461
572
|
if (editHistory.length > 0) {
|
|
462
573
|
userPrompt += "Previous edits made to this element:\n";
|
|
@@ -466,16 +577,36 @@ RULES:
|
|
|
466
577
|
});
|
|
467
578
|
userPrompt += "\n";
|
|
468
579
|
}
|
|
469
|
-
userPrompt += `
|
|
580
|
+
userPrompt += `COMPONENT DEFINITION (clicked element is annotated):
|
|
470
581
|
|
|
471
582
|
\`\`\`jsx
|
|
472
583
|
${fullComponentCode}
|
|
473
584
|
\`\`\`
|
|
585
|
+
`;
|
|
586
|
+
if (parentInstance) {
|
|
587
|
+
const parentLines = parentInstance.content.split("\n");
|
|
588
|
+
const annotatedParentLines = parentLines.map((line, idx) => {
|
|
589
|
+
const lineNum = parentInstance.lineStart + idx;
|
|
590
|
+
const isUsageLine = lineNum >= parentInstance.usageLineStart && lineNum <= parentInstance.usageLineEnd;
|
|
591
|
+
return line + (isUsageLine ? " // ← COMPONENT USAGE" : "");
|
|
592
|
+
});
|
|
593
|
+
userPrompt += `
|
|
594
|
+
PARENT INSTANCE (where component is used - in ${parentInstance.filePath}):
|
|
474
595
|
|
|
596
|
+
\`\`\`jsx
|
|
597
|
+
${annotatedParentLines.join("\n")}
|
|
598
|
+
\`\`\`
|
|
599
|
+
`;
|
|
600
|
+
}
|
|
601
|
+
userPrompt += `
|
|
475
602
|
User request: "${suggestion}"
|
|
476
|
-
${editHistory.length > 0 ? "
|
|
603
|
+
${editHistory.length > 0 ? "(Build upon previous changes)" : ""}
|
|
604
|
+
|
|
605
|
+
${parentInstance ? `Decide whether to:
|
|
606
|
+
1. Edit the component definition (for changes that affect ALL instances)
|
|
607
|
+
2. Edit the parent instance (for changes specific to THIS usage)
|
|
477
608
|
|
|
478
|
-
Modify the annotated element to fulfill this request. Remember: do NOT include the annotation comments in your output
|
|
609
|
+
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
610
|
try {
|
|
480
611
|
const response = await model.invoke([
|
|
481
612
|
new SystemMessage(systemPrompt),
|
|
@@ -485,12 +616,18 @@ Modify the annotated element to fulfill this request. Remember: do NOT include t
|
|
|
485
616
|
code = cleanGeneratedCode(code, baseIndentation);
|
|
486
617
|
return code || null;
|
|
487
618
|
} catch (e) {
|
|
488
|
-
console.error("AI generation error:", e);
|
|
489
619
|
return null;
|
|
490
620
|
}
|
|
491
621
|
}
|
|
492
622
|
function cleanGeneratedCode(code, baseIndentation) {
|
|
493
623
|
var _a, _b, _c;
|
|
624
|
+
const isParentEdit = code.trim().startsWith("// EDIT_PARENT_INSTANCE");
|
|
625
|
+
if (isParentEdit) {
|
|
626
|
+
const marker2 = "// EDIT_PARENT_INSTANCE\n";
|
|
627
|
+
code = code.replace(/^\/\/ EDIT_PARENT_INSTANCE\n?/, "");
|
|
628
|
+
code = code.replace(/^```[\w]*\n?/gm, "").replace(/\n?```$/gm, "").replace(/\s*\/\/ ← COMPONENT USAGE/g, "").trim();
|
|
629
|
+
return marker2 + code;
|
|
630
|
+
}
|
|
494
631
|
const isFullComponent = code.trim().startsWith("// FULL_COMPONENT");
|
|
495
632
|
let marker = "";
|
|
496
633
|
if (isFullComponent) {
|
|
@@ -521,43 +658,6 @@ function cleanGeneratedCode(code, baseIndentation) {
|
|
|
521
658
|
}
|
|
522
659
|
return marker + code;
|
|
523
660
|
}
|
|
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
661
|
function parseDebugStack(stack) {
|
|
562
662
|
if (!stack) return null;
|
|
563
663
|
const stackStr = typeof stack === "string" ? stack : stack.stack || String(stack);
|
|
@@ -582,8 +682,8 @@ function parseDebugStack(stack) {
|
|
|
582
682
|
chunkId = chunkMatch[1];
|
|
583
683
|
filePath = filePath.replace(/\?[^:]*$/, "");
|
|
584
684
|
}
|
|
585
|
-
filePath =
|
|
586
|
-
if (!
|
|
685
|
+
filePath = cleanPath(filePath);
|
|
686
|
+
if (!shouldSkipPath(filePath)) {
|
|
587
687
|
console.log("parseDebugStack extracted:", { filePath, line, column });
|
|
588
688
|
return { filePath, line, column, chunkId };
|
|
589
689
|
}
|
|
@@ -595,46 +695,49 @@ function parseDebugStack(stack) {
|
|
|
595
695
|
);
|
|
596
696
|
return null;
|
|
597
697
|
}
|
|
598
|
-
function
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
698
|
+
function parseDebugStackFrames(stack) {
|
|
699
|
+
if (!stack) return [];
|
|
700
|
+
const stackStr = typeof stack === "string" ? stack : stack.stack || String(stack);
|
|
701
|
+
const frames = stackStr.split("\n");
|
|
702
|
+
const skipPatterns = [
|
|
703
|
+
"node_modules",
|
|
704
|
+
"SegmentViewNode",
|
|
705
|
+
"LayoutRouter",
|
|
706
|
+
"ErrorBoundary",
|
|
707
|
+
"fakeJSXCallSite",
|
|
708
|
+
"react_stack_bottom_frame"
|
|
709
|
+
];
|
|
710
|
+
const positions = [];
|
|
711
|
+
for (const frame of frames) {
|
|
712
|
+
if (skipPatterns.some((p) => frame.includes(p))) continue;
|
|
713
|
+
const match = frame.match(/at\s+(\w+)\s+\((.+?):(\d+):(\d+)\)?$/);
|
|
714
|
+
if (match) {
|
|
715
|
+
match[1];
|
|
716
|
+
let filePath = match[2];
|
|
717
|
+
const line = parseInt(match[3], 10);
|
|
718
|
+
const column = parseInt(match[4], 10);
|
|
719
|
+
let chunkId;
|
|
720
|
+
const chunkMatch = filePath.match(/\?([^:]+)$/);
|
|
721
|
+
if (chunkMatch) {
|
|
722
|
+
chunkId = chunkMatch[1];
|
|
723
|
+
filePath = filePath.replace(/\?[^:]*$/, "");
|
|
724
|
+
}
|
|
725
|
+
filePath = cleanPath(filePath);
|
|
726
|
+
if (!shouldSkipPath(filePath)) {
|
|
727
|
+
positions.push({ filePath, line, column, chunkId });
|
|
728
|
+
if (positions.length >= 2) break;
|
|
729
|
+
}
|
|
607
730
|
}
|
|
608
|
-
} catch (e) {
|
|
609
|
-
console.warn("Failed to decode URL-encoded path:", cleaned, e);
|
|
610
|
-
}
|
|
611
|
-
cleaned = cleaned.replace(/^webpack-internal:\/\/\/\([^)]+\)\/\.\//, "").replace(/^webpack-internal:\/\/\/\([^)]+\)\//, "").replace(/^webpack-internal:\/\//, "").replace(/^webpack:\/\/[^/]*\//, "").replace(/^\([^)]+\)\//, "").replace(/\?.*$/, "");
|
|
612
|
-
if (cleaned.startsWith(".next/") || path.isAbsolute(cleaned)) {
|
|
613
|
-
return cleaned;
|
|
614
731
|
}
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
function cleanPath(p) {
|
|
618
|
-
return cleanPathTurbopack(p);
|
|
619
|
-
}
|
|
620
|
-
function normalizeSourcePath(source, projectRoot) {
|
|
621
|
-
let cleaned = cleanPath(source);
|
|
622
|
-
cleaned = cleaned.replace(/\\/g, "/");
|
|
623
|
-
if (cleaned.startsWith(projectRoot)) {
|
|
624
|
-
cleaned = cleaned.substring(projectRoot.length + 1);
|
|
625
|
-
}
|
|
626
|
-
return cleaned.replace(/^\/+/, "");
|
|
627
|
-
}
|
|
628
|
-
function shouldSkip(p) {
|
|
629
|
-
if (!p) return true;
|
|
630
|
-
return ["node_modules", "next/dist", "react-dom"].some((s) => p.includes(s));
|
|
732
|
+
console.log(`parseDebugStackFrames extracted ${positions.length} frames:`, positions);
|
|
733
|
+
return positions;
|
|
631
734
|
}
|
|
632
735
|
async function resolveOriginalPosition(compiledPos, projectRoot) {
|
|
633
736
|
try {
|
|
634
737
|
console.log("resolveOriginalPosition called with:", compiledPos);
|
|
635
738
|
let compiledFilePath = compiledPos.filePath;
|
|
636
|
-
compiledFilePath =
|
|
637
|
-
console.log("After
|
|
739
|
+
compiledFilePath = cleanPath(compiledFilePath);
|
|
740
|
+
console.log("After cleanPath:", compiledFilePath);
|
|
638
741
|
if (compiledFilePath.startsWith("http://") || compiledFilePath.startsWith("https://")) {
|
|
639
742
|
const url = new URL(compiledFilePath);
|
|
640
743
|
const pathname = url.pathname;
|
|
@@ -807,7 +910,6 @@ async function getOriginalPositionFromDebugStack(debugStack, projectRoot) {
|
|
|
807
910
|
return await resolveOriginalPosition(compiledPos, projectRoot);
|
|
808
911
|
}
|
|
809
912
|
async function handleRead(req) {
|
|
810
|
-
var _a;
|
|
811
913
|
const devModeError = validateDevMode();
|
|
812
914
|
if (devModeError) return devModeError;
|
|
813
915
|
const { searchParams } = new URL(req.url);
|
|
@@ -824,6 +926,11 @@ async function handleRead(req) {
|
|
|
824
926
|
textContent: textContent || void 0,
|
|
825
927
|
className: className || void 0
|
|
826
928
|
} : void 0;
|
|
929
|
+
const parentFilePath = searchParams.get("parentFilePath") || "";
|
|
930
|
+
const parentLine = parseInt(searchParams.get("parentLine") || "0");
|
|
931
|
+
const parentComponentName = searchParams.get("parentComponentName") || "";
|
|
932
|
+
const parentDebugStack = searchParams.get("parentDebugStack") || "";
|
|
933
|
+
const childKey = searchParams.get("childKey") || "";
|
|
827
934
|
const projectRoot = process.cwd();
|
|
828
935
|
if (debugStack) {
|
|
829
936
|
const compiledPos = parseDebugStack(debugStack);
|
|
@@ -854,21 +961,7 @@ async function handleRead(req) {
|
|
|
854
961
|
{ status: 400 }
|
|
855
962
|
);
|
|
856
963
|
}
|
|
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
|
-
});
|
|
964
|
+
const componentName = extractComponentName(ast);
|
|
872
965
|
if (!componentName) {
|
|
873
966
|
return NextResponse.json({
|
|
874
967
|
success: true,
|
|
@@ -890,28 +983,79 @@ async function handleRead(req) {
|
|
|
890
983
|
lineEnd: content.split("\n").length
|
|
891
984
|
});
|
|
892
985
|
}
|
|
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
986
|
const lines = content.split("\n");
|
|
910
987
|
const componentLines = lines.slice(
|
|
911
988
|
target.componentStart - 1,
|
|
912
989
|
target.componentEnd
|
|
913
990
|
);
|
|
914
991
|
const preview = componentLines.join("\n");
|
|
992
|
+
let parentInstance = null;
|
|
993
|
+
if (parentDebugStack) {
|
|
994
|
+
try {
|
|
995
|
+
let resolvedParentPath = parentFilePath;
|
|
996
|
+
let resolvedParentLine = parentLine;
|
|
997
|
+
let resolvedParentComponentName = parentComponentName;
|
|
998
|
+
if (resolvedParentComponentName && parentDebugStack) {
|
|
999
|
+
const compiledPos = parseDebugStack(parentDebugStack);
|
|
1000
|
+
if (compiledPos) {
|
|
1001
|
+
const originalPos = await resolveOriginalPosition(
|
|
1002
|
+
compiledPos,
|
|
1003
|
+
projectRoot
|
|
1004
|
+
);
|
|
1005
|
+
if (originalPos) {
|
|
1006
|
+
resolvedParentPath = originalPos.source;
|
|
1007
|
+
resolvedParentLine = originalPos.line;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
const normalizedParentPath = normalizePath(resolvedParentPath);
|
|
1012
|
+
const absoluteParentPath = await resolveFilePath(
|
|
1013
|
+
projectRoot,
|
|
1014
|
+
normalizedParentPath
|
|
1015
|
+
);
|
|
1016
|
+
if (absoluteParentPath && resolvedParentComponentName) {
|
|
1017
|
+
const parentContent = await fs.readFile(absoluteParentPath, "utf-8");
|
|
1018
|
+
const parentAst = parseFile(parentContent);
|
|
1019
|
+
if (parentAst) {
|
|
1020
|
+
let nthOfType2 = void 0;
|
|
1021
|
+
if (childKey) {
|
|
1022
|
+
const keyAsNumber = parseInt(childKey, 10);
|
|
1023
|
+
if (!isNaN(keyAsNumber)) {
|
|
1024
|
+
nthOfType2 = keyAsNumber + 1;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
const parentTarget = findTargetElement(parentAst, parentContent, {
|
|
1028
|
+
componentName: resolvedParentComponentName,
|
|
1029
|
+
lineNumber: 0,
|
|
1030
|
+
// Don't use line number - rely on nthOfType to find correct instance
|
|
1031
|
+
elementContext: {
|
|
1032
|
+
tagName: componentName,
|
|
1033
|
+
// Search for child component usage
|
|
1034
|
+
nthOfType: nthOfType2
|
|
1035
|
+
// Find specific instance if key is numeric
|
|
1036
|
+
}
|
|
1037
|
+
});
|
|
1038
|
+
if (parentTarget) {
|
|
1039
|
+
const parentLines = parentContent.split("\n");
|
|
1040
|
+
const parentComponentLines = parentLines.slice(
|
|
1041
|
+
parentTarget.componentStart - 1,
|
|
1042
|
+
parentTarget.componentEnd
|
|
1043
|
+
);
|
|
1044
|
+
parentInstance = {
|
|
1045
|
+
filePath: resolvedParentPath,
|
|
1046
|
+
content: parentComponentLines.join("\n"),
|
|
1047
|
+
lineStart: parentTarget.componentStart,
|
|
1048
|
+
lineEnd: parentTarget.componentEnd,
|
|
1049
|
+
usageLineStart: parentTarget.startLine,
|
|
1050
|
+
usageLineEnd: parentTarget.endLine,
|
|
1051
|
+
componentName: resolvedParentComponentName
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
} catch (error) {
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
915
1059
|
return NextResponse.json({
|
|
916
1060
|
success: true,
|
|
917
1061
|
content: preview,
|
|
@@ -919,8 +1063,10 @@ async function handleRead(req) {
|
|
|
919
1063
|
lineEnd: target.componentEnd,
|
|
920
1064
|
targetStartLine: target.startLine,
|
|
921
1065
|
targetEndLine: target.endLine,
|
|
922
|
-
componentName
|
|
1066
|
+
componentName,
|
|
923
1067
|
// Return the actual component name parsed from code
|
|
1068
|
+
parentInstance
|
|
1069
|
+
// Optional: where this component is used
|
|
924
1070
|
});
|
|
925
1071
|
}
|
|
926
1072
|
async function handleUndo(req) {
|
|
@@ -976,37 +1122,58 @@ async function handleResolve(req) {
|
|
|
976
1122
|
{ status: 400 }
|
|
977
1123
|
);
|
|
978
1124
|
}
|
|
979
|
-
const
|
|
980
|
-
if (
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1125
|
+
const compiledFrames = parseDebugStackFrames(debugStack);
|
|
1126
|
+
if (compiledFrames.length === 0) {
|
|
1127
|
+
const compiledPos = parseDebugStack(debugStack);
|
|
1128
|
+
if (!compiledPos) {
|
|
1129
|
+
console.error("Could not parse debug stack:", debugStack);
|
|
1130
|
+
return NextResponse.json(
|
|
1131
|
+
{ success: false, error: "Could not parse stack" },
|
|
1132
|
+
{ status: 422 }
|
|
1133
|
+
);
|
|
1134
|
+
}
|
|
1135
|
+
const originalPos = await resolveOriginalPosition(
|
|
1136
|
+
compiledPos,
|
|
1137
|
+
process.cwd()
|
|
985
1138
|
);
|
|
1139
|
+
if (!originalPos) {
|
|
1140
|
+
return NextResponse.json(
|
|
1141
|
+
{ success: false, error: "Source map lookup failed" },
|
|
1142
|
+
{ status: 404 }
|
|
1143
|
+
);
|
|
1144
|
+
}
|
|
1145
|
+
return NextResponse.json({
|
|
1146
|
+
success: true,
|
|
1147
|
+
filePath: originalPos.source,
|
|
1148
|
+
lineNumber: originalPos.line,
|
|
1149
|
+
columnNumber: originalPos.column ?? 0
|
|
1150
|
+
});
|
|
986
1151
|
}
|
|
987
|
-
|
|
988
|
-
const
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1152
|
+
const resolvedFrames = [];
|
|
1153
|
+
for (const frame of compiledFrames) {
|
|
1154
|
+
const originalPos = await resolveOriginalPosition(frame, process.cwd());
|
|
1155
|
+
if (originalPos) {
|
|
1156
|
+
resolvedFrames.push({
|
|
1157
|
+
filePath: originalPos.source,
|
|
1158
|
+
lineNumber: originalPos.line,
|
|
1159
|
+
columnNumber: originalPos.column ?? 0
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
if (resolvedFrames.length === 0) {
|
|
999
1164
|
return NextResponse.json(
|
|
1000
|
-
{ success: false, error: "Source map lookup failed" },
|
|
1165
|
+
{ success: false, error: "Source map lookup failed for all frames" },
|
|
1001
1166
|
{ status: 404 }
|
|
1002
1167
|
);
|
|
1003
1168
|
}
|
|
1004
|
-
console.log("Resolved
|
|
1169
|
+
console.log("Resolved frames:", resolvedFrames);
|
|
1005
1170
|
return NextResponse.json({
|
|
1006
1171
|
success: true,
|
|
1007
|
-
filePath:
|
|
1008
|
-
lineNumber:
|
|
1009
|
-
columnNumber:
|
|
1172
|
+
filePath: resolvedFrames[0].filePath,
|
|
1173
|
+
lineNumber: resolvedFrames[0].lineNumber,
|
|
1174
|
+
columnNumber: resolvedFrames[0].columnNumber,
|
|
1175
|
+
frames: resolvedFrames
|
|
1176
|
+
// [componentDefinition, parentInstance]
|
|
1010
1177
|
});
|
|
1011
1178
|
} catch (error) {
|
|
1012
1179
|
console.error("Source resolve error:", error);
|
|
@@ -1091,6 +1258,209 @@ async function handleValidateSession(req) {
|
|
|
1091
1258
|
);
|
|
1092
1259
|
}
|
|
1093
1260
|
}
|
|
1261
|
+
const suggestionCache = /* @__PURE__ */ new Map();
|
|
1262
|
+
const CACHE_TTL = 3e4;
|
|
1263
|
+
function getCacheKey(params) {
|
|
1264
|
+
return `${params.componentName}:${params.elementTag || "div"}:${params.lastSuggestion || ""}`;
|
|
1265
|
+
}
|
|
1266
|
+
function getCachedSuggestions(key) {
|
|
1267
|
+
const cached = suggestionCache.get(key);
|
|
1268
|
+
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
|
|
1269
|
+
return cached.suggestions;
|
|
1270
|
+
}
|
|
1271
|
+
suggestionCache.delete(key);
|
|
1272
|
+
return null;
|
|
1273
|
+
}
|
|
1274
|
+
function cacheSuggestions(key, suggestions) {
|
|
1275
|
+
suggestionCache.set(key, { suggestions, timestamp: Date.now() });
|
|
1276
|
+
if (suggestionCache.size > 100) {
|
|
1277
|
+
const oldestKeys = Array.from(suggestionCache.entries()).sort((a, b) => a[1].timestamp - b[1].timestamp).slice(0, 20).map(([key2]) => key2);
|
|
1278
|
+
oldestKeys.forEach((key2) => suggestionCache.delete(key2));
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
const DEFAULT_SUGGESTIONS = [
|
|
1282
|
+
"Add padding",
|
|
1283
|
+
"Change color",
|
|
1284
|
+
"Make larger",
|
|
1285
|
+
"Add shadow",
|
|
1286
|
+
"Round corners",
|
|
1287
|
+
"Center content",
|
|
1288
|
+
"Add hover effect",
|
|
1289
|
+
"Adjust spacing"
|
|
1290
|
+
];
|
|
1291
|
+
async function handleSuggestions(req) {
|
|
1292
|
+
const devModeError = validateDevMode();
|
|
1293
|
+
if (devModeError) return devModeError;
|
|
1294
|
+
try {
|
|
1295
|
+
const { searchParams } = new URL(req.url);
|
|
1296
|
+
const componentName = searchParams.get("componentName") || "Component";
|
|
1297
|
+
const elementTag = searchParams.get("elementTag") || void 0;
|
|
1298
|
+
const className = searchParams.get("className") || void 0;
|
|
1299
|
+
const textContent = searchParams.get("textContent") || void 0;
|
|
1300
|
+
const lastSuggestion = searchParams.get("lastSuggestion") || void 0;
|
|
1301
|
+
const editHistoryStr = searchParams.get("editHistory") || void 0;
|
|
1302
|
+
const excludedSuggestionsStr = searchParams.get("excludedSuggestions") || void 0;
|
|
1303
|
+
let editHistory = [];
|
|
1304
|
+
if (editHistoryStr) {
|
|
1305
|
+
try {
|
|
1306
|
+
editHistory = JSON.parse(editHistoryStr);
|
|
1307
|
+
} catch (e) {
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
let excludedSuggestions = [];
|
|
1311
|
+
if (excludedSuggestionsStr) {
|
|
1312
|
+
try {
|
|
1313
|
+
excludedSuggestions = JSON.parse(excludedSuggestionsStr);
|
|
1314
|
+
} catch (e) {
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
const cacheKey = getCacheKey({ componentName, elementTag, lastSuggestion });
|
|
1318
|
+
const cached = excludedSuggestions.length === 0 ? getCachedSuggestions(cacheKey) : null;
|
|
1319
|
+
if (cached) {
|
|
1320
|
+
return NextResponse.json({
|
|
1321
|
+
success: true,
|
|
1322
|
+
suggestions: cached
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
const suggestions = await generateSuggestions({
|
|
1326
|
+
componentName,
|
|
1327
|
+
elementTag,
|
|
1328
|
+
className,
|
|
1329
|
+
textContent,
|
|
1330
|
+
editHistory,
|
|
1331
|
+
lastSuggestion,
|
|
1332
|
+
excludedSuggestions
|
|
1333
|
+
});
|
|
1334
|
+
if (suggestions && suggestions.length > 0) {
|
|
1335
|
+
cacheSuggestions(cacheKey, suggestions);
|
|
1336
|
+
return NextResponse.json({
|
|
1337
|
+
success: true,
|
|
1338
|
+
suggestions
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
return NextResponse.json({
|
|
1342
|
+
success: true,
|
|
1343
|
+
suggestions: DEFAULT_SUGGESTIONS
|
|
1344
|
+
});
|
|
1345
|
+
} catch (error) {
|
|
1346
|
+
return NextResponse.json(
|
|
1347
|
+
{
|
|
1348
|
+
success: false,
|
|
1349
|
+
error: String(error),
|
|
1350
|
+
suggestions: DEFAULT_SUGGESTIONS
|
|
1351
|
+
// Fallback
|
|
1352
|
+
},
|
|
1353
|
+
{ status: 500 }
|
|
1354
|
+
);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
async function generateSuggestions(options) {
|
|
1358
|
+
const {
|
|
1359
|
+
componentName,
|
|
1360
|
+
elementTag,
|
|
1361
|
+
className,
|
|
1362
|
+
textContent,
|
|
1363
|
+
editHistory,
|
|
1364
|
+
lastSuggestion,
|
|
1365
|
+
excludedSuggestions = []
|
|
1366
|
+
} = options;
|
|
1367
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
1368
|
+
if (!apiKey) {
|
|
1369
|
+
return null;
|
|
1370
|
+
}
|
|
1371
|
+
const model = new ChatAnthropic({
|
|
1372
|
+
apiKey,
|
|
1373
|
+
modelName: "claude-haiku-4-5-20251001",
|
|
1374
|
+
maxTokens: 1024,
|
|
1375
|
+
temperature: 0.3
|
|
1376
|
+
// Slightly creative for variety
|
|
1377
|
+
});
|
|
1378
|
+
const systemPrompt = `You are a UI/UX expert suggesting quick edits for React components.
|
|
1379
|
+
|
|
1380
|
+
Generate 6-8 concise, actionable suggestions that a developer might want to make next.
|
|
1381
|
+
|
|
1382
|
+
RULES:
|
|
1383
|
+
- Each suggestion must be 2-6 words (e.g., "Add padding", "Make text larger")
|
|
1384
|
+
- Focus on common UI improvements: spacing, colors, sizing, layout, shadows, borders, typography
|
|
1385
|
+
- Consider the element type (${elementTag || "element"})
|
|
1386
|
+
- Output ONLY a JSON array of strings, no explanations, no markdown fences
|
|
1387
|
+
${excludedSuggestions.length > 0 ? `- DO NOT suggest any of these (user wants different options): ${excludedSuggestions.join(
|
|
1388
|
+
", "
|
|
1389
|
+
)}` : ""}
|
|
1390
|
+
|
|
1391
|
+
${lastSuggestion ? `IMPORTANT - LAST EDIT CONTEXT:
|
|
1392
|
+
The user just applied: "${lastSuggestion}"
|
|
1393
|
+
Your suggestions MUST be direct follow-ups/refinements of this last change:
|
|
1394
|
+
- If it was "Add padding" → suggest "More padding", "Less padding", "Add vertical padding only"
|
|
1395
|
+
- If it was "Make it blue" → suggest "Darker blue", "Lighter blue", "Change to navy blue"
|
|
1396
|
+
- If it was "Increase font size" → suggest "Decrease font size", "Make even larger", "Adjust line height"
|
|
1397
|
+
- 4-6 suggestions should be variations/refinements of the last edit
|
|
1398
|
+
- 2-4 suggestions can be other related improvements
|
|
1399
|
+
|
|
1400
|
+
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.`}
|
|
1401
|
+
|
|
1402
|
+
Example output format:
|
|
1403
|
+
["Add hover effect", "Increase padding", "Make corners rounder", "Change to flex row", "Add drop shadow", "Adjust font size"]`;
|
|
1404
|
+
let userPrompt = `Element: <${elementTag || "div"}>`;
|
|
1405
|
+
if (className) {
|
|
1406
|
+
userPrompt += `
|
|
1407
|
+
Classes: ${className}`;
|
|
1408
|
+
}
|
|
1409
|
+
if (textContent) {
|
|
1410
|
+
userPrompt += `
|
|
1411
|
+
Text: "${textContent.slice(0, 50)}"`;
|
|
1412
|
+
}
|
|
1413
|
+
userPrompt += `
|
|
1414
|
+
Component: ${componentName}`;
|
|
1415
|
+
if (editHistory && editHistory.length > 0) {
|
|
1416
|
+
userPrompt += `
|
|
1417
|
+
|
|
1418
|
+
Recent edits:
|
|
1419
|
+
`;
|
|
1420
|
+
editHistory.slice(-3).forEach((item, idx) => {
|
|
1421
|
+
userPrompt += `${idx + 1}. ${item.suggestion} ${item.success ? "✓" : "✗"}
|
|
1422
|
+
`;
|
|
1423
|
+
});
|
|
1424
|
+
} else {
|
|
1425
|
+
userPrompt += `
|
|
1426
|
+
|
|
1427
|
+
No previous edits.`;
|
|
1428
|
+
}
|
|
1429
|
+
if (lastSuggestion) {
|
|
1430
|
+
userPrompt += `
|
|
1431
|
+
|
|
1432
|
+
**LAST EDIT APPLIED:** "${lastSuggestion}"`;
|
|
1433
|
+
userPrompt += `
|
|
1434
|
+
|
|
1435
|
+
Generate 6-8 follow-up suggestions (mostly variations of the last edit):`;
|
|
1436
|
+
} else {
|
|
1437
|
+
userPrompt += `
|
|
1438
|
+
|
|
1439
|
+
Generate 6-8 initial suggestions:`;
|
|
1440
|
+
}
|
|
1441
|
+
try {
|
|
1442
|
+
const response = await model.invoke([
|
|
1443
|
+
new SystemMessage(systemPrompt),
|
|
1444
|
+
new HumanMessage(userPrompt)
|
|
1445
|
+
]);
|
|
1446
|
+
let content = typeof response.content === "string" ? response.content : String(response.content);
|
|
1447
|
+
content = content.trim();
|
|
1448
|
+
content = content.replace(/^```json?\s*/gm, "").replace(/\s*```$/gm, "");
|
|
1449
|
+
const suggestions = JSON.parse(content);
|
|
1450
|
+
if (Array.isArray(suggestions)) {
|
|
1451
|
+
const validSuggestions = suggestions.filter((s) => typeof s === "string").map((s) => s.trim()).filter((s) => {
|
|
1452
|
+
const words = s.split(/\s+/).length;
|
|
1453
|
+
return words >= 1 && words <= 15;
|
|
1454
|
+
}).slice(0, 8);
|
|
1455
|
+
if (validSuggestions.length >= 4) {
|
|
1456
|
+
return validSuggestions;
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
return null;
|
|
1460
|
+
} catch (e) {
|
|
1461
|
+
return null;
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1094
1464
|
async function handleAIEditorRequest(req, context) {
|
|
1095
1465
|
const { path: path2 } = await context.params;
|
|
1096
1466
|
const endpoint = path2[0];
|
|
@@ -1114,6 +1484,9 @@ async function handleAIEditorRequest(req, context) {
|
|
|
1114
1484
|
case "validate-session":
|
|
1115
1485
|
if (method === "POST") return handleValidateSession(req);
|
|
1116
1486
|
break;
|
|
1487
|
+
case "suggestions":
|
|
1488
|
+
if (method === "GET") return handleSuggestions(req);
|
|
1489
|
+
break;
|
|
1117
1490
|
}
|
|
1118
1491
|
return NextResponse.json(
|
|
1119
1492
|
{ error: `Unknown endpoint: ${endpoint}` },
|
|
@@ -1127,19 +1500,23 @@ export {
|
|
|
1127
1500
|
handleResolve as d,
|
|
1128
1501
|
handleAbsolutePath as e,
|
|
1129
1502
|
handleValidateSession as f,
|
|
1130
|
-
|
|
1503
|
+
handleSuggestions as g,
|
|
1131
1504
|
handleAIEditorRequest as h,
|
|
1132
1505
|
isPathSecure as i,
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1506
|
+
fileExists$1 as j,
|
|
1507
|
+
extractComponentName as k,
|
|
1508
|
+
validateGeneratedCode as l,
|
|
1509
|
+
findTargetElement as m,
|
|
1137
1510
|
normalizePath as n,
|
|
1138
|
-
|
|
1511
|
+
getJSXMemberName as o,
|
|
1139
1512
|
parseFile as p,
|
|
1140
|
-
|
|
1513
|
+
getAttributeValue as q,
|
|
1141
1514
|
resolveFilePath as r,
|
|
1142
1515
|
scoreElementMatch as s,
|
|
1143
|
-
|
|
1516
|
+
parseDebugStack as t,
|
|
1517
|
+
parseDebugStackFrames as u,
|
|
1518
|
+
validateDevMode as v,
|
|
1519
|
+
resolveOriginalPosition as w,
|
|
1520
|
+
getOriginalPositionFromDebugStack as x
|
|
1144
1521
|
};
|
|
1145
|
-
//# sourceMappingURL=index-
|
|
1522
|
+
//# sourceMappingURL=index-3OMXRwpD.js.map
|