sonance-brand-mcp 1.3.19 → 1.3.20
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/assets/api/sonance-ai-edit/route.ts +485 -0
- package/dist/assets/api/sonance-component-source/route.ts +78 -0
- package/dist/assets/api/sonance-find-component/route.ts +174 -0
- package/dist/assets/api/sonance-save-colors/route.ts +181 -0
- package/dist/assets/api/sonance-vision-apply/route.ts +652 -0
- package/dist/assets/api/sonance-vision-edit/route.ts +532 -0
- package/dist/index.js +67 -0
- package/package.json +1 -1
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Sonance DevTools API - AI Component Editor
|
|
8
|
+
*
|
|
9
|
+
* Uses Claude to modify component source files based on natural language requests.
|
|
10
|
+
* Supports two actions:
|
|
11
|
+
* - "edit": Generate modified code and diff (preview)
|
|
12
|
+
* - "save": Write the modified code to the source file
|
|
13
|
+
*
|
|
14
|
+
* DEVELOPMENT ONLY.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
interface VariantStyles {
|
|
18
|
+
backgroundColor: string;
|
|
19
|
+
color: string;
|
|
20
|
+
borderColor: string;
|
|
21
|
+
borderRadius: string;
|
|
22
|
+
borderWidth: string;
|
|
23
|
+
padding: string;
|
|
24
|
+
fontSize: string;
|
|
25
|
+
fontWeight: string;
|
|
26
|
+
boxShadow: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface AIEditRequest {
|
|
30
|
+
action: "edit" | "save";
|
|
31
|
+
componentType: string;
|
|
32
|
+
filePath: string;
|
|
33
|
+
currentCode?: string;
|
|
34
|
+
modifiedCode?: string;
|
|
35
|
+
userRequest?: string;
|
|
36
|
+
// Variant-scoped editing
|
|
37
|
+
editScope?: "component" | "variant";
|
|
38
|
+
variantId?: string;
|
|
39
|
+
variantStyles?: VariantStyles;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface AIEditResponse {
|
|
43
|
+
success: boolean;
|
|
44
|
+
modifiedCode?: string;
|
|
45
|
+
diff?: string;
|
|
46
|
+
explanation?: string;
|
|
47
|
+
error?: string;
|
|
48
|
+
// AI-generated CSS for live preview (targets data-sonance-variant selectors)
|
|
49
|
+
previewCSS?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const SYSTEM_PROMPT = `You are a React/Tailwind CSS expert modifying Sonance brand components.
|
|
53
|
+
|
|
54
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
55
|
+
CRITICAL RULES - FOLLOW EXACTLY
|
|
56
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
57
|
+
|
|
58
|
+
**PRESERVATION RULES (NEVER VIOLATE):**
|
|
59
|
+
1. NEVER delete, remove, or modify ANY existing content, children, or JSX elements
|
|
60
|
+
2. NEVER remove existing className values - only ADD or MODIFY specific classes
|
|
61
|
+
3. NEVER change component structure, nesting, or element hierarchy
|
|
62
|
+
4. NEVER modify props interfaces, TypeScript types, imports, or exports
|
|
63
|
+
5. NEVER remove data-sonance-* attributes - these are required for DevTools
|
|
64
|
+
6. NEVER change any text content, children, or slot content
|
|
65
|
+
7. NEVER remove existing functionality, event handlers, or logic
|
|
66
|
+
|
|
67
|
+
**CHANGE RULES:**
|
|
68
|
+
8. ONLY modify the specific styling aspect mentioned in the user's request
|
|
69
|
+
9. When adding border-radius, ONLY add rounded-* classes - don't change background, padding, etc.
|
|
70
|
+
10. When changing colors, ONLY change the relevant color class - preserve all other styles
|
|
71
|
+
11. When adjusting spacing, ONLY modify padding/margin - preserve everything else
|
|
72
|
+
12. Make the MINIMUM possible change to achieve the requested result
|
|
73
|
+
13. If unsure whether to change something, DON'T change it
|
|
74
|
+
|
|
75
|
+
**TECHNICAL RULES:**
|
|
76
|
+
14. Return the COMPLETE modified file, not just changed parts
|
|
77
|
+
15. Use semantic Tailwind classes (bg-primary, text-foreground, etc.) when available
|
|
78
|
+
16. Maintain dark mode compatibility - prefer CSS variables over hardcoded colors
|
|
79
|
+
17. Keep the cn() utility for className merging
|
|
80
|
+
18. For hover effects, use Tailwind's hover: prefix
|
|
81
|
+
19. For transitions, use transition-all duration-200
|
|
82
|
+
|
|
83
|
+
**EXAMPLE - User says "make the borders round":**
|
|
84
|
+
CORRECT: Only add rounded-lg or rounded-xl to the className
|
|
85
|
+
WRONG: Adding rounded-lg AND changing bg color AND removing children
|
|
86
|
+
|
|
87
|
+
**EXAMPLE - User says "change the background to blue":**
|
|
88
|
+
CORRECT: Only change bg-white to bg-blue-500 (or appropriate brand color)
|
|
89
|
+
WRONG: Changing background AND modifying padding AND restructuring content
|
|
90
|
+
|
|
91
|
+
VARIANT-SCOPED EDITING:
|
|
92
|
+
When editScope is "variant", you are targeting ONLY components that match specific computed styles.
|
|
93
|
+
- Add a new variant prop or conditional class that applies styles when the component matches the variant criteria
|
|
94
|
+
- Use data attributes like data-variant="variantId" for targeting
|
|
95
|
+
- Or create a new variant in the component's variant system (if using cva or similar)
|
|
96
|
+
- Do NOT change the default appearance of other component instances
|
|
97
|
+
|
|
98
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
99
|
+
SONANCE BRAND FAMILY - COMPLETE REFERENCE
|
|
100
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
101
|
+
|
|
102
|
+
BRAND QUICK REFERENCE:
|
|
103
|
+
| Brand | Primary Color | Accent Color | Theme Preference |
|
|
104
|
+
|----------|-------------------|---------------------------|------------------|
|
|
105
|
+
| Sonance | Charcoal #333F48 | Cyan "The Beam" #00A3E1 | Light preferred |
|
|
106
|
+
| IPORT | Black #0E1114 | Orange #FC4C02 | Dark preferred |
|
|
107
|
+
| Blaze | Black #1A1A1C | Blue #00A3E1, Red #C02B0A | Dark preferred |
|
|
108
|
+
|
|
109
|
+
───────────────────────────────────────────────────────────────────────────────
|
|
110
|
+
SONANCE COLORS
|
|
111
|
+
───────────────────────────────────────────────────────────────────────────────
|
|
112
|
+
| Color | Hex | CSS Variable | Tailwind Class |
|
|
113
|
+
|-------------|-----------|----------------------|----------------------|
|
|
114
|
+
| Charcoal | #333F48 | --sonance-charcoal | bg-sonance-charcoal |
|
|
115
|
+
| The Beam | #00A3E1 | --sonance-blue | bg-sonance-blue |
|
|
116
|
+
| Light Gray | #D9D9D6 | --sonance-light-gray | bg-sonance-light-gray|
|
|
117
|
+
| White | #FFFFFF | --sonance-white | bg-sonance-white |
|
|
118
|
+
|
|
119
|
+
Sonance Gray Scale:
|
|
120
|
+
- Gray 50: #f8f9fa, Gray 100: #f0f2f3, Gray 200: #D9D9D6
|
|
121
|
+
- Gray 300: #b8bfc4, Gray 400: #8f999f, Gray 500: #6b7780
|
|
122
|
+
- Gray 600: #515c64, Gray 700: #424c54, Gray 800: #3a444c, Gray 900: #333F48
|
|
123
|
+
|
|
124
|
+
───────────────────────────────────────────────────────────────────────────────
|
|
125
|
+
IPORT COLORS (Dark Theme)
|
|
126
|
+
───────────────────────────────────────────────────────────────────────────────
|
|
127
|
+
| Color | Hex | CSS Variable | Tailwind Class |
|
|
128
|
+
|--------------|-----------|------------------------|------------------------|
|
|
129
|
+
| Orange | #FC4C02 | --iport-orange | bg-iport-orange |
|
|
130
|
+
| Black | #0E1114 | --iport-black | bg-iport-black |
|
|
131
|
+
| Dark | #0F161D | --iport-dark | bg-iport-dark |
|
|
132
|
+
| Dark Gray | #1C1E20 | --iport-dark-gray | bg-iport-dark-gray |
|
|
133
|
+
| Medium Gray | #25282A | --iport-medium-gray | bg-iport-medium-gray |
|
|
134
|
+
| Gray | #3A3D3F | --iport-gray | bg-iport-gray |
|
|
135
|
+
| Light Gray | #CED6DB | --iport-light-gray | bg-iport-light-gray |
|
|
136
|
+
| White | #FFFFFF | --iport-white | bg-iport-white |
|
|
137
|
+
|
|
138
|
+
───────────────────────────────────────────────────────────────────────────────
|
|
139
|
+
BLAZE AUDIO COLORS (Dark Theme)
|
|
140
|
+
───────────────────────────────────────────────────────────────────────────────
|
|
141
|
+
| Color | Hex | CSS Variable | Tailwind Class |
|
|
142
|
+
|--------------|-----------|------------------------|------------------------|
|
|
143
|
+
| Blue | #00A3E1 | --blaze-blue | bg-blaze-blue |
|
|
144
|
+
| Red | #C02B0A | --blaze-red | bg-blaze-red |
|
|
145
|
+
| Black | #1A1A1C | --blaze-black | bg-blaze-black |
|
|
146
|
+
| Dark Gray | #28282B | --blaze-dark-gray | bg-blaze-dark-gray |
|
|
147
|
+
| Gray | #313131 | --blaze-gray | bg-blaze-gray |
|
|
148
|
+
| Medium Gray | #575757 | --blaze-medium-gray | bg-blaze-medium-gray |
|
|
149
|
+
| White | #FFFFFF | --blaze-white | bg-blaze-white |
|
|
150
|
+
|
|
151
|
+
───────────────────────────────────────────────────────────────────────────────
|
|
152
|
+
SEMANTIC THEME VARIABLES (use these for theme-aware components)
|
|
153
|
+
───────────────────────────────────────────────────────────────────────────────
|
|
154
|
+
Backgrounds: --background, --background-secondary, --card
|
|
155
|
+
Text: --foreground, --foreground-secondary, --foreground-muted
|
|
156
|
+
Interactive: --primary, --primary-foreground, --border
|
|
157
|
+
Feedback: --success, --error, --warning, --info
|
|
158
|
+
|
|
159
|
+
Tailwind: bg-background, bg-card, text-foreground, text-foreground-secondary,
|
|
160
|
+
bg-primary, text-primary-foreground, border-border
|
|
161
|
+
|
|
162
|
+
───────────────────────────────────────────────────────────────────────────────
|
|
163
|
+
TYPOGRAPHY (All Brands use Montserrat)
|
|
164
|
+
───────────────────────────────────────────────────────────────────────────────
|
|
165
|
+
Font: 'Montserrat', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif
|
|
166
|
+
|
|
167
|
+
Font Weights:
|
|
168
|
+
- 300 Light: Headlines, display text
|
|
169
|
+
- 400 Regular: Body text, paragraphs
|
|
170
|
+
- 500 Medium: Emphasis, subheadings, buttons
|
|
171
|
+
- 600 Semibold: Strong emphasis
|
|
172
|
+
- 700 Bold: CTAs, buttons (SMALL TEXT ONLY - never for large headlines)
|
|
173
|
+
|
|
174
|
+
Headlines:
|
|
175
|
+
- Weight: Light (300) or Medium (500)
|
|
176
|
+
- Letter-spacing: -0.02em (tracking-tight)
|
|
177
|
+
- Line-height: 1.2
|
|
178
|
+
- NEVER use bold (700) for large display text
|
|
179
|
+
|
|
180
|
+
Body Text:
|
|
181
|
+
- Weight: Regular (400)
|
|
182
|
+
- Line-height: 1.6 to 1.75
|
|
183
|
+
- Max-width: 65-75 characters per line
|
|
184
|
+
|
|
185
|
+
All Caps Text:
|
|
186
|
+
- Letter-spacing: 0.08em to 0.1em (tracking-wide or tracking-widest)
|
|
187
|
+
- Use for: headlines, product names, labels, buttons
|
|
188
|
+
|
|
189
|
+
───────────────────────────────────────────────────────────────────────────────
|
|
190
|
+
COMPONENT PATTERNS
|
|
191
|
+
───────────────────────────────────────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
PRIMARY BUTTONS:
|
|
194
|
+
- Sonance: bg-sonance-charcoal text-white hover:bg-sonance-gray-800
|
|
195
|
+
- IPORT: bg-iport-orange text-white hover:opacity-90
|
|
196
|
+
- Blaze: bg-blaze-blue text-white hover:opacity-90
|
|
197
|
+
- All: px-6 py-3 text-sm font-medium tracking-wide uppercase transition-colors
|
|
198
|
+
|
|
199
|
+
SECONDARY BUTTONS:
|
|
200
|
+
- border border-border bg-transparent hover:bg-secondary-hover
|
|
201
|
+
- text-foreground px-6 py-3 text-sm font-medium tracking-wide uppercase
|
|
202
|
+
|
|
203
|
+
CARDS:
|
|
204
|
+
- Light: bg-white border border-sonance-light-gray rounded-sm p-6 shadow-sm
|
|
205
|
+
- Dark: bg-iport-dark-gray border border-iport-gray rounded-sm p-6
|
|
206
|
+
|
|
207
|
+
SECTION HEADERS:
|
|
208
|
+
- Category: text-xs font-medium uppercase tracking-widest text-foreground-muted mb-2
|
|
209
|
+
- Headline: text-4xl font-light text-foreground tracking-tight
|
|
210
|
+
- Description: text-lg text-foreground-secondary mt-4 max-w-2xl
|
|
211
|
+
|
|
212
|
+
───────────────────────────────────────────────────────────────────────────────
|
|
213
|
+
DESIGN PRINCIPLES
|
|
214
|
+
───────────────────────────────────────────────────────────────────────────────
|
|
215
|
+
- Sharp corners: rounded-sm or rounded-none preferred
|
|
216
|
+
- Subtle shadows: shadow-sm, shadow-md (not aggressive drop shadows)
|
|
217
|
+
- Generous whitespace: layouts should feel "breathable"
|
|
218
|
+
- Minimal borders: use subtle dividers, not heavy borders
|
|
219
|
+
- Premium feel: every element should feel high-end and intentional
|
|
220
|
+
- Photo-realistic imagery only (no cartoons or illustrations)
|
|
221
|
+
|
|
222
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
223
|
+
|
|
224
|
+
RESPOND WITH VALID JSON ONLY:
|
|
225
|
+
{
|
|
226
|
+
"modifiedCode": "// Complete file contents here...",
|
|
227
|
+
"explanation": "Brief description of what was changed"
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
Do not include markdown code fences or any text outside the JSON object.`;
|
|
231
|
+
|
|
232
|
+
export async function POST(request: Request) {
|
|
233
|
+
// Security: Only allow in development
|
|
234
|
+
if (process.env.NODE_ENV !== "development") {
|
|
235
|
+
return NextResponse.json(
|
|
236
|
+
{ error: "This endpoint is only available in development mode." },
|
|
237
|
+
{ status: 403 }
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
const body: AIEditRequest = await request.json();
|
|
243
|
+
const { action, componentType, filePath, currentCode, modifiedCode, userRequest, editScope, variantId, variantStyles } = body;
|
|
244
|
+
|
|
245
|
+
const projectRoot = process.cwd();
|
|
246
|
+
|
|
247
|
+
// Handle save action
|
|
248
|
+
if (action === "save") {
|
|
249
|
+
if (!filePath || !modifiedCode) {
|
|
250
|
+
return NextResponse.json(
|
|
251
|
+
{ error: "filePath and modifiedCode are required for save action" },
|
|
252
|
+
{ status: 400 }
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const fullPath = path.join(projectRoot, filePath);
|
|
257
|
+
|
|
258
|
+
// Validate file path is within project
|
|
259
|
+
if (!fullPath.startsWith(projectRoot)) {
|
|
260
|
+
return NextResponse.json(
|
|
261
|
+
{ error: "Invalid file path" },
|
|
262
|
+
{ status: 400 }
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Write the file
|
|
267
|
+
fs.writeFileSync(fullPath, modifiedCode, "utf-8");
|
|
268
|
+
|
|
269
|
+
return NextResponse.json({
|
|
270
|
+
success: true,
|
|
271
|
+
message: `Successfully updated ${filePath}`,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Handle edit action (AI modification)
|
|
276
|
+
if (action === "edit") {
|
|
277
|
+
if (!filePath || !userRequest) {
|
|
278
|
+
return NextResponse.json(
|
|
279
|
+
{ error: "filePath and userRequest are required for edit action" },
|
|
280
|
+
{ status: 400 }
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Read current file if not provided
|
|
285
|
+
let sourceCode = currentCode;
|
|
286
|
+
if (!sourceCode) {
|
|
287
|
+
const fullPath = path.join(projectRoot, filePath);
|
|
288
|
+
if (!fs.existsSync(fullPath)) {
|
|
289
|
+
return NextResponse.json(
|
|
290
|
+
{ error: `File not found: ${filePath}` },
|
|
291
|
+
{ status: 404 }
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
sourceCode = fs.readFileSync(fullPath, "utf-8");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Check for API key
|
|
298
|
+
const apiKey = process.env.NEXT_PUBLIC_CLAUDE_API_KEY;
|
|
299
|
+
if (!apiKey) {
|
|
300
|
+
return NextResponse.json(
|
|
301
|
+
{ error: "NEXT_PUBLIC_CLAUDE_API_KEY not configured. Add it to your .env.local file." },
|
|
302
|
+
{ status: 500 }
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Call Claude API
|
|
307
|
+
const anthropic = new Anthropic({ apiKey });
|
|
308
|
+
|
|
309
|
+
// Build context for variant-scoped editing
|
|
310
|
+
let variantContext = "";
|
|
311
|
+
if (editScope === "variant" && variantId && variantStyles) {
|
|
312
|
+
variantContext = `
|
|
313
|
+
VARIANT-SCOPED EDIT:
|
|
314
|
+
This change should ONLY affect components matching this specific variant (ID: ${variantId}).
|
|
315
|
+
The variant has these computed styles:
|
|
316
|
+
- Background: ${variantStyles.backgroundColor}
|
|
317
|
+
- Text color: ${variantStyles.color}
|
|
318
|
+
- Border: ${variantStyles.borderWidth} ${variantStyles.borderColor}
|
|
319
|
+
- Border radius: ${variantStyles.borderRadius}
|
|
320
|
+
- Padding: ${variantStyles.padding}
|
|
321
|
+
- Font size: ${variantStyles.fontSize}
|
|
322
|
+
- Font weight: ${variantStyles.fontWeight}
|
|
323
|
+
- Box shadow: ${variantStyles.boxShadow}
|
|
324
|
+
|
|
325
|
+
Create a conditional style or variant prop that targets ONLY these components.
|
|
326
|
+
Do NOT change the default appearance of other instances of this component.
|
|
327
|
+
`;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const userMessage = `Component type: ${componentType}
|
|
331
|
+
File: ${filePath}
|
|
332
|
+
Edit scope: ${editScope === "variant" ? "VARIANT ONLY" : "ALL INSTANCES"}
|
|
333
|
+
${variantContext}
|
|
334
|
+
Current code:
|
|
335
|
+
\`\`\`tsx
|
|
336
|
+
${sourceCode}
|
|
337
|
+
\`\`\`
|
|
338
|
+
|
|
339
|
+
User request: "${userRequest}"
|
|
340
|
+
|
|
341
|
+
IMPORTANT REMINDERS:
|
|
342
|
+
- Make ONLY the change requested above - nothing else
|
|
343
|
+
- Do NOT remove or modify any existing content, children, or JSX elements
|
|
344
|
+
- Do NOT change any styles that weren't specifically requested
|
|
345
|
+
- Preserve ALL existing className values - only add/modify what's needed for the request
|
|
346
|
+
- If the user says "round the borders", ONLY add a rounded-* class
|
|
347
|
+
- If the user says "change the color", ONLY change the relevant color class
|
|
348
|
+
|
|
349
|
+
Return the complete modified file as JSON with these keys:
|
|
350
|
+
- "modifiedCode": The complete modified file content
|
|
351
|
+
- "explanation": Brief description of changes made
|
|
352
|
+
- "previewCSS": CSS rules that will visually show the changes. Use selector [data-sonance-variant="${variantId || "selected"}"] to target the specific element(s). Example: "[data-sonance-variant='abc123'] { border-radius: 0.5rem; background-color: white; }"`;
|
|
353
|
+
|
|
354
|
+
const response = await anthropic.messages.create({
|
|
355
|
+
model: "claude-sonnet-4-20250514",
|
|
356
|
+
max_tokens: 8192,
|
|
357
|
+
messages: [
|
|
358
|
+
{
|
|
359
|
+
role: "user",
|
|
360
|
+
content: userMessage,
|
|
361
|
+
},
|
|
362
|
+
],
|
|
363
|
+
system: SYSTEM_PROMPT,
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// Extract text content from response
|
|
367
|
+
const textContent = response.content.find(block => block.type === "text");
|
|
368
|
+
if (!textContent || textContent.type !== "text") {
|
|
369
|
+
return NextResponse.json(
|
|
370
|
+
{ error: "No text response from AI" },
|
|
371
|
+
{ status: 500 }
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Parse AI response
|
|
376
|
+
let aiResponse: { modifiedCode: string; explanation: string; previewCSS?: string };
|
|
377
|
+
try {
|
|
378
|
+
// Clean up response - try to extract JSON from markdown code blocks
|
|
379
|
+
let jsonText = textContent.text.trim();
|
|
380
|
+
|
|
381
|
+
const jsonMatch = jsonText.match(/```json\n([\s\S]*?)\n```/) ||
|
|
382
|
+
jsonText.match(/```\n([\s\S]*?)\n```/);
|
|
383
|
+
|
|
384
|
+
if (jsonMatch) {
|
|
385
|
+
jsonText = jsonMatch[1];
|
|
386
|
+
} else if (jsonText.includes("```json")) {
|
|
387
|
+
// Fallback for cases where regex might miss due to newlines
|
|
388
|
+
const start = jsonText.indexOf("```json") + 7;
|
|
389
|
+
const end = jsonText.lastIndexOf("```");
|
|
390
|
+
if (end > start) {
|
|
391
|
+
jsonText = jsonText.substring(start, end);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Clean up any remaining whitespace
|
|
396
|
+
jsonText = jsonText.trim();
|
|
397
|
+
|
|
398
|
+
aiResponse = JSON.parse(jsonText);
|
|
399
|
+
} catch {
|
|
400
|
+
console.error("Failed to parse AI response:", textContent.text);
|
|
401
|
+
return NextResponse.json(
|
|
402
|
+
{ error: "Failed to parse AI response. Please try again." },
|
|
403
|
+
{ status: 500 }
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Generate simple diff
|
|
408
|
+
const diff = generateSimpleDiff(sourceCode, aiResponse.modifiedCode);
|
|
409
|
+
|
|
410
|
+
return NextResponse.json({
|
|
411
|
+
success: true,
|
|
412
|
+
modifiedCode: aiResponse.modifiedCode,
|
|
413
|
+
diff,
|
|
414
|
+
explanation: aiResponse.explanation,
|
|
415
|
+
previewCSS: aiResponse.previewCSS || "",
|
|
416
|
+
} as AIEditResponse);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return NextResponse.json(
|
|
420
|
+
{ error: "Invalid action. Use 'edit' or 'save'." },
|
|
421
|
+
{ status: 400 }
|
|
422
|
+
);
|
|
423
|
+
} catch (error) {
|
|
424
|
+
console.error("AI edit error:", error);
|
|
425
|
+
return NextResponse.json(
|
|
426
|
+
{ error: "Failed to process request", details: String(error) },
|
|
427
|
+
{ status: 500 }
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Generate a simple unified diff between two strings
|
|
434
|
+
*/
|
|
435
|
+
function generateSimpleDiff(original: string, modified: string): string {
|
|
436
|
+
const originalLines = original.split("\n");
|
|
437
|
+
const modifiedLines = modified.split("\n");
|
|
438
|
+
|
|
439
|
+
const diff: string[] = [];
|
|
440
|
+
const maxLines = Math.max(originalLines.length, modifiedLines.length);
|
|
441
|
+
|
|
442
|
+
let inChange = false;
|
|
443
|
+
let changeStart = -1;
|
|
444
|
+
const changes: { start: number; origLines: string[]; modLines: string[] }[] = [];
|
|
445
|
+
|
|
446
|
+
// Find changed regions
|
|
447
|
+
for (let i = 0; i < maxLines; i++) {
|
|
448
|
+
const origLine = originalLines[i] || "";
|
|
449
|
+
const modLine = modifiedLines[i] || "";
|
|
450
|
+
|
|
451
|
+
if (origLine !== modLine) {
|
|
452
|
+
if (!inChange) {
|
|
453
|
+
inChange = true;
|
|
454
|
+
changeStart = i;
|
|
455
|
+
changes.push({ start: i, origLines: [], modLines: [] });
|
|
456
|
+
}
|
|
457
|
+
if (originalLines[i] !== undefined) {
|
|
458
|
+
changes[changes.length - 1].origLines.push(origLine);
|
|
459
|
+
}
|
|
460
|
+
if (modifiedLines[i] !== undefined) {
|
|
461
|
+
changes[changes.length - 1].modLines.push(modLine);
|
|
462
|
+
}
|
|
463
|
+
} else {
|
|
464
|
+
inChange = false;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Format diff output
|
|
469
|
+
for (const change of changes) {
|
|
470
|
+
diff.push(`@@ Line ${change.start + 1} @@`);
|
|
471
|
+
for (const line of change.origLines) {
|
|
472
|
+
diff.push(`- ${line}`);
|
|
473
|
+
}
|
|
474
|
+
for (const line of change.modLines) {
|
|
475
|
+
diff.push(`+ ${line}`);
|
|
476
|
+
}
|
|
477
|
+
diff.push("");
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (diff.length === 0) {
|
|
481
|
+
return "No changes detected";
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return diff.join("\n");
|
|
485
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Sonance DevTools API - Component Source Reader
|
|
7
|
+
*
|
|
8
|
+
* Returns the source code of a component file for AI editing.
|
|
9
|
+
*
|
|
10
|
+
* DEVELOPMENT ONLY.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export async function GET(request: Request) {
|
|
14
|
+
// Security: Only allow in development
|
|
15
|
+
if (process.env.NODE_ENV !== "development") {
|
|
16
|
+
return NextResponse.json(
|
|
17
|
+
{ error: "This endpoint is only available in development mode." },
|
|
18
|
+
{ status: 403 }
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const { searchParams } = new URL(request.url);
|
|
24
|
+
const filePath = searchParams.get("file");
|
|
25
|
+
|
|
26
|
+
if (!filePath) {
|
|
27
|
+
return NextResponse.json(
|
|
28
|
+
{ error: "file parameter is required" },
|
|
29
|
+
{ status: 400 }
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const projectRoot = process.cwd();
|
|
34
|
+
const fullPath = path.join(projectRoot, filePath);
|
|
35
|
+
|
|
36
|
+
// Validate file path is within project (security)
|
|
37
|
+
const normalizedPath = path.normalize(fullPath);
|
|
38
|
+
if (!normalizedPath.startsWith(projectRoot)) {
|
|
39
|
+
return NextResponse.json(
|
|
40
|
+
{ error: "Invalid file path - must be within project" },
|
|
41
|
+
{ status: 400 }
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Check file exists
|
|
46
|
+
if (!fs.existsSync(fullPath)) {
|
|
47
|
+
return NextResponse.json(
|
|
48
|
+
{ error: `File not found: ${filePath}` },
|
|
49
|
+
{ status: 404 }
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Read file contents
|
|
54
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
55
|
+
const lines = content.split("\n");
|
|
56
|
+
|
|
57
|
+
// Extract component name from file (simple heuristic)
|
|
58
|
+
let componentName = path.basename(filePath, path.extname(filePath));
|
|
59
|
+
// Convert kebab-case to PascalCase
|
|
60
|
+
componentName = componentName
|
|
61
|
+
.split("-")
|
|
62
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
63
|
+
.join("");
|
|
64
|
+
|
|
65
|
+
return NextResponse.json({
|
|
66
|
+
filePath,
|
|
67
|
+
content,
|
|
68
|
+
componentName,
|
|
69
|
+
lineCount: lines.length,
|
|
70
|
+
});
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error("Error reading component source:", error);
|
|
73
|
+
return NextResponse.json(
|
|
74
|
+
{ error: "Failed to read file", details: String(error) },
|
|
75
|
+
{ status: 500 }
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|