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.
Files changed (50) hide show
  1. package/dist/{AIEditorProvider-Bs9zUVrL.cjs → AIEditorProvider-BGHm2xyU.cjs} +823 -378
  2. package/dist/AIEditorProvider-BGHm2xyU.cjs.map +1 -0
  3. package/dist/{AIEditorProvider-D-w9-GZb.js → AIEditorProvider-CxdGjdLL.js} +847 -402
  4. package/dist/AIEditorProvider-CxdGjdLL.js.map +1 -0
  5. package/dist/client/AIEditorProvider.d.ts +8 -16
  6. package/dist/client/AIEditorProvider.d.ts.map +1 -1
  7. package/dist/client/fiber-utils.d.ts +35 -0
  8. package/dist/client/fiber-utils.d.ts.map +1 -0
  9. package/dist/client/index.d.ts +1 -1
  10. package/dist/client/index.d.ts.map +1 -1
  11. package/dist/client/query-params.d.ts +9 -0
  12. package/dist/client/query-params.d.ts.map +1 -0
  13. package/dist/client.cjs +1 -1
  14. package/dist/client.js +1 -1
  15. package/dist/{index-DnoYi4f8.cjs → index-CNJqd4EQ.cjs} +656 -225
  16. package/dist/index-CNJqd4EQ.cjs.map +1 -0
  17. package/dist/{index-BFa7H-uO.js → index-DrmEf13c.js} +662 -231
  18. package/dist/index-DrmEf13c.js.map +1 -0
  19. package/dist/index.cjs +7 -2
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.js +15 -10
  22. package/dist/path-utils-Bai2xKx9.js +36 -0
  23. package/dist/path-utils-Bai2xKx9.js.map +1 -0
  24. package/dist/path-utils-DYzEWUGy.cjs +35 -0
  25. package/dist/path-utils-DYzEWUGy.cjs.map +1 -0
  26. package/dist/server/handlers/edit.d.ts.map +1 -1
  27. package/dist/server/handlers/index.d.ts +1 -0
  28. package/dist/server/handlers/index.d.ts.map +1 -1
  29. package/dist/server/handlers/read.d.ts.map +1 -1
  30. package/dist/server/handlers/resolve.d.ts.map +1 -1
  31. package/dist/server/handlers/suggestions.d.ts +3 -0
  32. package/dist/server/handlers/suggestions.d.ts.map +1 -0
  33. package/dist/server/index.d.ts +1 -0
  34. package/dist/server/index.d.ts.map +1 -1
  35. package/dist/server/utils/ast.d.ts +10 -0
  36. package/dist/server/utils/ast.d.ts.map +1 -1
  37. package/dist/server/utils/source-map.d.ts +10 -0
  38. package/dist/server/utils/source-map.d.ts.map +1 -1
  39. package/dist/server.cjs +6 -1
  40. package/dist/server.cjs.map +1 -1
  41. package/dist/server.js +14 -9
  42. package/dist/shared/path-utils.d.ts +24 -0
  43. package/dist/shared/path-utils.d.ts.map +1 -0
  44. package/dist/shared/types.d.ts +30 -0
  45. package/dist/shared/types.d.ts.map +1 -1
  46. package/package.json +1 -1
  47. package/dist/AIEditorProvider-Bs9zUVrL.cjs.map +0 -1
  48. package/dist/AIEditorProvider-D-w9-GZb.js.map +0 -1
  49. package/dist/index-BFa7H-uO.js.map +0 -1
  50. 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 (elementContext) {
137
- const score = scoreElementMatch(path2.node, elementContext, fileContent);
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 (matches.length === 0) {
152
- if (componentNode && componentStart > 0) {
213
+ if (elementsAtLine.length > 0) {
214
+ if (elementsAtLine.length === 1) {
215
+ const target = elementsAtLine[0];
153
216
  return {
154
- startLine: componentStart,
155
- endLine: componentEnd,
217
+ startLine: target.startLine,
218
+ endLine: target.endLine,
156
219
  componentStart,
157
220
  componentEnd
158
221
  };
159
222
  }
160
- return null;
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 ((elementContext == null ? void 0 : elementContext.nthOfType) && elementContext.tagName) {
247
+ if (elementContext == null ? void 0 : elementContext.tagName) {
163
248
  const allOfTag = allElementsByTag.get(elementContext.tagName);
164
- if (allOfTag && allOfTag.length >= elementContext.nthOfType) {
165
- const target = allOfTag[elementContext.nthOfType - 1];
166
- console.log(
167
- ` Using nthOfType=${elementContext.nthOfType}: found ${allOfTag.length} <${elementContext.tagName}> elements`
168
- );
169
- return {
170
- startLine: target.startLine,
171
- endLine: target.endLine,
172
- componentStart,
173
- componentEnd
174
- };
175
- } else {
176
- console.log(
177
- ` nthOfType=${elementContext.nthOfType} but only found ${(allOfTag == null ? void 0 : allOfTag.length) || 0} <${elementContext.tagName}> elements`
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
- matches.sort((a, b) => b.score - a.score);
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
- console.log("Raw AI response length:", newCode.length);
363
- console.log("Looking for // FULL_COMPONENT marker...");
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
- console.log(
374
- `🔄 AI returned full component modification (lines ${startLineToReplace}-${endLineToReplace})`
375
- );
376
- console.log("Extracted component code length:", codeToApply.length);
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 - check server logs for details"
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 code with line annotations
442
- - The clicked element is marked with "// ← CLICKED ELEMENT STARTS" and "// ← CLICKED ELEMENT ENDS"
443
- - Lines within the element have "// (clicked element)"
444
- - These are just annotations to help you - they are NOT part of the actual code
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
- YOUR TASK:
447
- Modify ONLY the marked element (unless the request requires changing its parent/wrapper).
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
- OUTPUT:
450
- - Just the modified element code (WITHOUT the annotation comments)
451
- - OR if broader changes needed: "// FULL_COMPONENT\\n" + complete modified component (WITHOUT annotation comments)
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 the "// ← CLICKED ELEMENT" annotation comments in your output
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 += `Component (clicked element is annotated with "// ← CLICKED ELEMENT" comments):
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 ? "\n(Build upon previous changes)" : ""}
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 = cleanPathTurbopack(filePath);
586
- if (!shouldSkip(filePath)) {
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 cleanPathTurbopack(p) {
599
- let cleaned = p;
600
- cleaned = cleaned.replace(/^about:\/\/React\/Server\//, "");
601
- if (cleaned.startsWith("file://")) {
602
- cleaned = cleaned.replace(/^file:\/\//, "");
603
- }
604
- try {
605
- if (cleaned.includes("%")) {
606
- cleaned = decodeURIComponent(cleaned);
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
- 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
- }
615
- return cleaned;
616
- }
617
- function cleanPath(p) {
618
- return cleanPathTurbopack(p);
743
+ return null;
619
744
  }
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);
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
- 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));
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 = cleanPathTurbopack(compiledFilePath);
637
- console.log("After cleanPathTurbopack:", compiledFilePath);
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
- let componentName = "";
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 compiledPos = parseDebugStack(debugStack);
980
- if (!compiledPos) {
981
- console.error("Could not parse debug stack:", debugStack);
982
- return NextResponse.json(
983
- { success: false, error: "Could not parse stack" },
984
- { status: 422 }
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
- console.log("Parsed compiled position:", compiledPos);
988
- const originalPos = await resolveOriginalPosition(
989
- compiledPos,
990
- process.cwd()
991
- );
992
- if (!originalPos) {
993
- console.error(
994
- "Source map lookup failed for:",
995
- compiledPos.filePath,
996
- "at line",
997
- compiledPos.line
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 original position:", originalPos);
1222
+ console.log("Resolved frames:", resolvedFrames);
1005
1223
  return NextResponse.json({
1006
1224
  success: true,
1007
- filePath: originalPos.source,
1008
- lineNumber: originalPos.line,
1009
- columnNumber: originalPos.column ?? 0
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
- fileExists$1 as g,
1556
+ handleSuggestions as g,
1131
1557
  handleAIEditorRequest as h,
1132
1558
  isPathSecure as i,
1133
- findTargetElement as j,
1134
- getJSXMemberName as k,
1135
- getAttributeValue as l,
1136
- parseDebugStack as m,
1559
+ fileExists$1 as j,
1560
+ extractComponentName as k,
1561
+ validateGeneratedCode as l,
1562
+ findTargetElement as m,
1137
1563
  normalizePath as n,
1138
- resolveOriginalPosition as o,
1564
+ getJSXMemberName as o,
1139
1565
  parseFile as p,
1140
- getOriginalPositionFromDebugStack as q,
1566
+ getAttributeValue as q,
1141
1567
  resolveFilePath as r,
1142
1568
  scoreElementMatch as s,
1143
- validateDevMode as v
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-BFa7H-uO.js.map
1576
+ //# sourceMappingURL=index-DrmEf13c.js.map