sonance-brand-mcp 1.3.57 → 1.3.59
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-vision-apply/route.ts +221 -297
- package/dist/assets/api/sonance-vision-edit/route.ts +146 -287
- package/dist/assets/dev-tools/components/ApplyFirstPreview.tsx +238 -28
- package/dist/assets/dev-tools/components/ChatInterface.tsx +2 -1
- package/dist/assets/dev-tools/panels/ComponentsPanel.tsx +5 -2
- package/dist/assets/dev-tools/types.ts +7 -4
- package/package.json +1 -1
|
@@ -40,12 +40,14 @@ interface VisionFileModification {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
interface ApplyFirstRequest {
|
|
43
|
-
action: "apply" | "accept" | "revert";
|
|
43
|
+
action: "apply" | "accept" | "revert" | "preview";
|
|
44
44
|
sessionId?: string;
|
|
45
45
|
screenshot?: string;
|
|
46
46
|
pageRoute?: string;
|
|
47
47
|
userPrompt?: string;
|
|
48
48
|
focusedElements?: VisionFocusedElement[];
|
|
49
|
+
// For applying a previously previewed set of modifications
|
|
50
|
+
previewedModifications?: VisionFileModification[];
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
interface BackupManifest {
|
|
@@ -169,6 +171,73 @@ Be smart - use your knowledge of React patterns to make educated guesses about c
|
|
|
169
171
|
}
|
|
170
172
|
}
|
|
171
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Search candidate files for JSX code matching the focused element
|
|
176
|
+
* This helps identify which file actually contains the element the user clicked on
|
|
177
|
+
*/
|
|
178
|
+
function findFilesContainingElement(
|
|
179
|
+
focusedElements: VisionFocusedElement[] | undefined,
|
|
180
|
+
candidateFiles: { path: string; content: string }[]
|
|
181
|
+
): { path: string; score: number; matches: string[] }[] {
|
|
182
|
+
if (!focusedElements || focusedElements.length === 0) {
|
|
183
|
+
return [];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const results: { path: string; score: number; matches: string[] }[] = [];
|
|
187
|
+
|
|
188
|
+
for (const file of candidateFiles) {
|
|
189
|
+
let score = 0;
|
|
190
|
+
const matches: string[] = [];
|
|
191
|
+
const content = file.content.toLowerCase();
|
|
192
|
+
|
|
193
|
+
for (const el of focusedElements) {
|
|
194
|
+
// Extract element type from the name (e.g., "button #123" -> "button")
|
|
195
|
+
const elementType = el.name.split(/[\s#]/)[0].toLowerCase();
|
|
196
|
+
const componentType = el.type.toLowerCase();
|
|
197
|
+
|
|
198
|
+
// Search for JSX patterns
|
|
199
|
+
// Check for HTML tags like <button, <div, etc.
|
|
200
|
+
if (content.includes(`<${elementType}`)) {
|
|
201
|
+
score += 10;
|
|
202
|
+
matches.push(`<${elementType}>`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Check for React component like <Button, <Card, etc.
|
|
206
|
+
const capitalizedType = el.type.charAt(0).toUpperCase() + el.type.slice(1);
|
|
207
|
+
if (file.content.includes(`<${capitalizedType}`)) {
|
|
208
|
+
score += 15;
|
|
209
|
+
matches.push(`<${capitalizedType}>`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Check for component imports
|
|
213
|
+
if (content.includes(`import`) && content.includes(componentType)) {
|
|
214
|
+
score += 5;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Check for event handlers commonly associated with buttons
|
|
218
|
+
if (elementType === "button" || componentType === "button") {
|
|
219
|
+
if (content.includes("onclick") || content.includes("onpress")) {
|
|
220
|
+
score += 3;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Check for the component being defined in this file
|
|
225
|
+
if (file.content.includes(`function ${capitalizedType}`) ||
|
|
226
|
+
file.content.includes(`const ${capitalizedType}`)) {
|
|
227
|
+
score += 20;
|
|
228
|
+
matches.push(`defines ${capitalizedType}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (score > 0) {
|
|
233
|
+
results.push({ path: file.path, score, matches });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Sort by score descending
|
|
238
|
+
return results.sort((a, b) => b.score - a.score);
|
|
239
|
+
}
|
|
240
|
+
|
|
172
241
|
/**
|
|
173
242
|
* Phase 3: Ask LLM to select the best file from actual file list
|
|
174
243
|
* This replaces guessing - the LLM sees the real filenames and picks one
|
|
@@ -177,7 +246,8 @@ async function selectBestFileFromList(
|
|
|
177
246
|
screenshot: string,
|
|
178
247
|
userPrompt: string,
|
|
179
248
|
candidateFiles: string[],
|
|
180
|
-
apiKey: string
|
|
249
|
+
apiKey: string,
|
|
250
|
+
focusedElementHints?: { path: string; score: number; matches: string[] }[]
|
|
181
251
|
): Promise<string | null> {
|
|
182
252
|
if (candidateFiles.length === 0) return null;
|
|
183
253
|
|
|
@@ -201,7 +271,12 @@ async function selectBestFileFromList(
|
|
|
201
271
|
|
|
202
272
|
Here are the actual component files found in this codebase:
|
|
203
273
|
${candidateFiles.map((f, i) => `${i + 1}. ${f}`).join('\n')}
|
|
274
|
+
${focusedElementHints && focusedElementHints.length > 0 ? `
|
|
275
|
+
FOCUSED ELEMENT ANALYSIS - Files that contain the element the user clicked on:
|
|
276
|
+
${focusedElementHints.slice(0, 3).map(h => `- ${h.path} (confidence: ${h.score}, found: ${h.matches.join(", ")})`).join('\n')}
|
|
204
277
|
|
|
278
|
+
IMPORTANT: Prefer files from the FOCUSED ELEMENT ANALYSIS above, as they contain the actual element the user wants to modify.
|
|
279
|
+
` : ''}
|
|
205
280
|
Looking at the screenshot, which file MOST LIKELY contains the UI elements the user wants to modify?
|
|
206
281
|
|
|
207
282
|
IMPORTANT: Return ONLY the exact file path from the list above (e.g., "components/ProcessCatalogue/ProcessDetailPanel.tsx").
|
|
@@ -436,102 +511,48 @@ function searchFilesSmart(
|
|
|
436
511
|
return sortedResults.map(r => ({ path: r.path, content: r.content, score: r.score }));
|
|
437
512
|
}
|
|
438
513
|
|
|
439
|
-
const VISION_SYSTEM_PROMPT = `You are
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
**Patch Rules:**
|
|
468
|
-
- "search" must match the original code EXACTLY (including whitespace/indentation)
|
|
469
|
-
- "replace" contains your modified version
|
|
470
|
-
- Include 2-4 lines of context in "search" to make it unique
|
|
471
|
-
- You may ONLY edit files provided in the PAGE CONTEXT section
|
|
472
|
-
- CRITICAL: NEVER invent or guess code. Your "search" string MUST be copied EXACTLY from the provided file content. If you cannot find the exact code to modify, return an empty modifications array.
|
|
473
|
-
- If the file content appears truncated, only modify code that is visible in the provided content.
|
|
474
|
-
|
|
475
|
-
═══════════════════════════════════════════════════════════════════════════════
|
|
476
|
-
SONANCE BRAND COLOR SYSTEM
|
|
477
|
-
═══════════════════════════════════════════════════════════════════════════════
|
|
478
|
-
|
|
479
|
-
**Core Colors:**
|
|
480
|
-
- Primary (Charcoal): #333F48 - main text, dark backgrounds
|
|
481
|
-
- Accent (Cyan "The Beam"): #00D3C8 - highlights, interactive elements, CTAs
|
|
482
|
-
- Success: Green tones - positive states, confirmations
|
|
483
|
-
- Warning: Amber/Orange tones - caution states, alerts
|
|
484
|
-
- Destructive: Red tones - errors, delete actions
|
|
485
|
-
|
|
486
|
-
**CRITICAL CONTRAST RULES:**
|
|
487
|
-
When using colored backgrounds, ALWAYS use high-contrast text:
|
|
488
|
-
|
|
489
|
-
| Background | Use This Text | NEVER Use |
|
|
490
|
-
|-------------------|------------------------------------|-----------------------|
|
|
491
|
-
| bg-accent | text-white | text-accent-foreground |
|
|
492
|
-
| bg-primary | text-primary-foreground, text-white | text-primary |
|
|
493
|
-
| bg-success | text-white | text-success |
|
|
494
|
-
| bg-warning | text-white | text-warning |
|
|
495
|
-
| bg-destructive | text-white | text-destructive |
|
|
496
|
-
|
|
497
|
-
**SEMANTIC TOKEN PATTERNS:**
|
|
498
|
-
- text-{color} = the color itself (use on NEUTRAL backgrounds like white/gray)
|
|
499
|
-
- text-{color}-foreground = INTENDED for text on {color} backgrounds, but MAY have contrast issues
|
|
500
|
-
- bg-{color} = background in that color
|
|
501
|
-
- WHEN IN DOUBT: Use text-white on any colored background for guaranteed contrast
|
|
502
|
-
|
|
503
|
-
**BUTTON PATTERNS (Sonance Standard):**
|
|
504
|
-
- Primary CTA: bg-primary text-primary-foreground
|
|
505
|
-
- Accent/Highlight: bg-accent text-white (NOT text-accent-foreground)
|
|
506
|
-
- Success: bg-success text-white
|
|
507
|
-
- Warning: bg-warning text-white
|
|
508
|
-
- Destructive: bg-destructive text-white
|
|
509
|
-
- Outlined: border-border bg-transparent text-foreground
|
|
510
|
-
|
|
511
|
-
**COMMON MISTAKES TO AVOID:**
|
|
512
|
-
- WRONG: bg-accent text-accent-foreground (accent-foreground is often dark = invisible text)
|
|
513
|
-
- RIGHT: bg-accent text-white (white text on cyan = visible)
|
|
514
|
-
- WRONG: bg-primary text-primary (same color = invisible)
|
|
515
|
-
- RIGHT: bg-primary text-primary-foreground OR text-white
|
|
516
|
-
|
|
517
|
-
**RESPONSE FORMAT:**
|
|
518
|
-
CRITICAL: Return ONLY the JSON object below. Do NOT include any text, explanation, or thinking before or after the JSON. No preamble. No "Looking at the screenshot..." No markdown code blocks. Just raw JSON:
|
|
514
|
+
const VISION_SYSTEM_PROMPT = `You are a code editor. Make ONLY the change the user requested.
|
|
515
|
+
|
|
516
|
+
RULES:
|
|
517
|
+
1. Make the SMALLEST possible change to accomplish the request
|
|
518
|
+
2. Do NOT refactor, rebrand, or "improve" anything else
|
|
519
|
+
3. Do NOT change import statements unless explicitly required
|
|
520
|
+
4. Do NOT change component libraries (e.g., heroui to shadcn)
|
|
521
|
+
5. If fixing a color/visibility issue, change ONLY that element's classes
|
|
522
|
+
6. Your patch should typically be 1-5 lines, not 50+
|
|
523
|
+
7. NEVER invent or guess code - your "search" string MUST match the file EXACTLY
|
|
524
|
+
8. NEVER change data mappings (icon names, keys, enum values) unless user explicitly provides the new values
|
|
525
|
+
9. If you don't know what values exist in a database or data source, DO NOT guess - ask for clarification
|
|
526
|
+
|
|
527
|
+
CRITICAL - ELEMENT VERIFICATION:
|
|
528
|
+
- When a focused element is mentioned, SEARCH the provided file content for that EXACT element
|
|
529
|
+
- Look for the actual JSX/HTML code: <button, <Button, <div, className, onClick, etc.
|
|
530
|
+
- If you cannot find the element in the TARGET COMPONENT section, it may be in a child component
|
|
531
|
+
- NEVER guess what element code looks like - find it in the file or report "element not found"
|
|
532
|
+
- If the element is not in the provided file, return: {"modifications": [], "explanation": "The focused element appears to be in a child component, not in [filename]"}
|
|
533
|
+
|
|
534
|
+
CRITICAL - DATA INTEGRITY:
|
|
535
|
+
- If code references database values (like icon_name, type, status), DO NOT change the mapping keys
|
|
536
|
+
- You cannot see database content - only change STRUCTURE, not DATA MAPPINGS
|
|
537
|
+
- Example: If you see iconMap["EyeOff"] = SomeIcon, do NOT change "EyeOff" to something else
|
|
538
|
+
- If user wants different icons, they must tell you the EXACT icon names they want
|
|
539
|
+
|
|
540
|
+
PATCH FORMAT:
|
|
541
|
+
Return ONLY raw JSON (no markdown, no preamble):
|
|
519
542
|
{
|
|
520
|
-
"reasoning": "
|
|
521
|
-
"modifications": [
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
"
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
"explanation": "summary of changes made"
|
|
534
|
-
}`;
|
|
543
|
+
"reasoning": "brief explanation",
|
|
544
|
+
"modifications": [{
|
|
545
|
+
"filePath": "path/to/file.tsx",
|
|
546
|
+
"patches": [{
|
|
547
|
+
"search": "exact original code (copy from provided file)",
|
|
548
|
+
"replace": "minimal change",
|
|
549
|
+
"explanation": "what this does"
|
|
550
|
+
}]
|
|
551
|
+
}],
|
|
552
|
+
"explanation": "summary"
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
If you cannot find the exact code to modify, OR if you would need to guess data values, return empty modifications array with explanation.`;
|
|
535
556
|
|
|
536
557
|
export async function POST(request: Request) {
|
|
537
558
|
// Only allow in development
|
|
@@ -544,7 +565,7 @@ export async function POST(request: Request) {
|
|
|
544
565
|
|
|
545
566
|
try {
|
|
546
567
|
const body: ApplyFirstRequest = await request.json();
|
|
547
|
-
const { action, sessionId, screenshot, pageRoute, userPrompt, focusedElements } = body;
|
|
568
|
+
const { action, sessionId, screenshot, pageRoute, userPrompt, focusedElements, previewedModifications } = body;
|
|
548
569
|
const projectRoot = process.cwd();
|
|
549
570
|
|
|
550
571
|
// ========== ACCEPT ACTION ==========
|
|
@@ -571,6 +592,8 @@ export async function POST(request: Request) {
|
|
|
571
592
|
|
|
572
593
|
// ========== REVERT ACTION ==========
|
|
573
594
|
if (action === "revert") {
|
|
595
|
+
console.log(`[Apply-First] Revert requested for session: ${sessionId}`);
|
|
596
|
+
|
|
574
597
|
if (!sessionId) {
|
|
575
598
|
return NextResponse.json(
|
|
576
599
|
{ error: "sessionId is required for revert action" },
|
|
@@ -579,6 +602,7 @@ export async function POST(request: Request) {
|
|
|
579
602
|
}
|
|
580
603
|
|
|
581
604
|
const result = await revertFromBackups(sessionId, projectRoot);
|
|
605
|
+
console.log(`[Apply-First] Revert result:`, result);
|
|
582
606
|
|
|
583
607
|
return NextResponse.json({
|
|
584
608
|
success: result.success,
|
|
@@ -588,11 +612,44 @@ export async function POST(request: Request) {
|
|
|
588
612
|
});
|
|
589
613
|
}
|
|
590
614
|
|
|
591
|
-
// ========== APPLY ACTION ==========
|
|
592
|
-
|
|
615
|
+
// ========== APPLY FROM PREVIEW ACTION ==========
|
|
616
|
+
// When user accepts a preview, apply the previously generated modifications
|
|
617
|
+
if (action === "apply" && previewedModifications && previewedModifications.length > 0) {
|
|
618
|
+
debugLog("Applying previewed modifications", {
|
|
619
|
+
count: previewedModifications.length,
|
|
620
|
+
files: previewedModifications.map(m => m.filePath)
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
const newSessionId = randomUUID().slice(0, 8);
|
|
624
|
+
|
|
625
|
+
const applyResult = await applyChangesWithBackup(
|
|
626
|
+
previewedModifications,
|
|
627
|
+
newSessionId,
|
|
628
|
+
projectRoot
|
|
629
|
+
);
|
|
630
|
+
|
|
631
|
+
if (!applyResult.success) {
|
|
632
|
+
return NextResponse.json(
|
|
633
|
+
{ error: applyResult.error || "Failed to apply previewed changes" },
|
|
634
|
+
{ status: 500 }
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
return NextResponse.json({
|
|
639
|
+
success: true,
|
|
640
|
+
sessionId: newSessionId,
|
|
641
|
+
modifications: previewedModifications,
|
|
642
|
+
backupPaths: applyResult.backupPaths,
|
|
643
|
+
explanation: "Applied previewed changes",
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// ========== APPLY/PREVIEW ACTION (with LLM) ==========
|
|
648
|
+
// Both actions do LLM work; "preview" returns without writing, "apply" writes to files
|
|
649
|
+
if (action === "apply" || action === "preview") {
|
|
593
650
|
if (!userPrompt) {
|
|
594
651
|
return NextResponse.json(
|
|
595
|
-
{ error: "userPrompt is required for apply action" },
|
|
652
|
+
{ error: "userPrompt is required for apply/preview action" },
|
|
596
653
|
{ status: 400 }
|
|
597
654
|
);
|
|
598
655
|
}
|
|
@@ -627,6 +684,22 @@ export async function POST(request: Request) {
|
|
|
627
684
|
const searchResults = searchFilesSmart(analysis, projectRoot, 10);
|
|
628
685
|
smartSearchFiles = searchResults.map(r => ({ path: r.path, content: r.content }));
|
|
629
686
|
|
|
687
|
+
// PHASE 2.5: Find which files contain the focused element
|
|
688
|
+
const focusedElementHints = findFilesContainingElement(
|
|
689
|
+
focusedElements,
|
|
690
|
+
searchResults.map(r => ({ path: r.path, content: r.content }))
|
|
691
|
+
);
|
|
692
|
+
|
|
693
|
+
if (focusedElementHints.length > 0) {
|
|
694
|
+
debugLog("Focused element search results", {
|
|
695
|
+
topMatches: focusedElementHints.slice(0, 3).map(h => ({
|
|
696
|
+
path: h.path,
|
|
697
|
+
score: h.score,
|
|
698
|
+
matches: h.matches
|
|
699
|
+
}))
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
|
|
630
703
|
// PHASE 3: Ask LLM to pick the best file from actual file list
|
|
631
704
|
if (searchResults.length > 0) {
|
|
632
705
|
const candidateFiles = searchResults.slice(0, 10).map(r => r.path);
|
|
@@ -634,7 +707,8 @@ export async function POST(request: Request) {
|
|
|
634
707
|
screenshot,
|
|
635
708
|
userPrompt,
|
|
636
709
|
candidateFiles,
|
|
637
|
-
apiKey
|
|
710
|
+
apiKey,
|
|
711
|
+
focusedElementHints
|
|
638
712
|
);
|
|
639
713
|
|
|
640
714
|
if (selectedFile) {
|
|
@@ -699,141 +773,6 @@ export async function POST(request: Request) {
|
|
|
699
773
|
});
|
|
700
774
|
}
|
|
701
775
|
|
|
702
|
-
// ========== PHASE A: VISUAL PROBLEM ANALYSIS ==========
|
|
703
|
-
// Before generating patches, analyze WHAT is visually wrong
|
|
704
|
-
// This forces the LLM to articulate the problem before trying to fix it
|
|
705
|
-
interface VisualAnalysis {
|
|
706
|
-
element: string;
|
|
707
|
-
elementText: string; // The visible text content of the element (for finding in code)
|
|
708
|
-
problem: string;
|
|
709
|
-
changeType: "styling" | "structural" | "content" | "other";
|
|
710
|
-
currentClasses?: string; // For styling changes: what classes appear to be applied
|
|
711
|
-
classChange?: {
|
|
712
|
-
remove?: string[]; // Classes to remove
|
|
713
|
-
add?: string[]; // Classes to add
|
|
714
|
-
};
|
|
715
|
-
structuralChange?: string; // For structural changes: what to add/remove/wrap
|
|
716
|
-
solution: string;
|
|
717
|
-
confidence: "high" | "medium" | "low";
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
let visualAnalysis: VisualAnalysis | null = null;
|
|
721
|
-
|
|
722
|
-
if (screenshot && userPrompt) {
|
|
723
|
-
debugLog("Phase A: Starting visual problem analysis");
|
|
724
|
-
|
|
725
|
-
const analysisClient = new Anthropic({ apiKey });
|
|
726
|
-
|
|
727
|
-
try {
|
|
728
|
-
const analysisResponse = await analysisClient.messages.create({
|
|
729
|
-
model: "claude-sonnet-4-20250514",
|
|
730
|
-
max_tokens: 1024,
|
|
731
|
-
messages: [
|
|
732
|
-
{
|
|
733
|
-
role: "user",
|
|
734
|
-
content: [
|
|
735
|
-
{
|
|
736
|
-
type: "image",
|
|
737
|
-
source: {
|
|
738
|
-
type: "base64",
|
|
739
|
-
media_type: "image/png",
|
|
740
|
-
data: base64Data,
|
|
741
|
-
},
|
|
742
|
-
},
|
|
743
|
-
{
|
|
744
|
-
type: "text",
|
|
745
|
-
text: `You are analyzing a UI screenshot to understand what the user wants changed.
|
|
746
|
-
|
|
747
|
-
User request: "${userPrompt}"
|
|
748
|
-
|
|
749
|
-
${focusedElements && focusedElements.length > 0 ? `User clicked on these elements:
|
|
750
|
-
${focusedElements.map((el) => `- ${el.name} (${el.type}) at position (${el.coordinates.x}, ${el.coordinates.y})`).join("\n")}
|
|
751
|
-
` : ""}
|
|
752
|
-
|
|
753
|
-
Analyze the request and determine:
|
|
754
|
-
1. ELEMENT: Which specific UI element needs to change? (describe location and appearance)
|
|
755
|
-
2. ELEMENT TEXT: What text is visible inside this element? (EXACT text for finding in code)
|
|
756
|
-
3. PROBLEM: What is wrong or what does the user want changed?
|
|
757
|
-
4. CHANGE TYPE: Is this a "styling" (colors/size/spacing), "structural" (add/remove elements), "content" (text changes), or "other" change?
|
|
758
|
-
5. For STYLING changes: What Tailwind classes should be REMOVED and what should be ADDED?
|
|
759
|
-
6. For STRUCTURAL changes: What element should be added, removed, or wrapped?
|
|
760
|
-
7. SOLUTION: Describe the complete fix
|
|
761
|
-
8. CONFIDENCE: high/medium/low
|
|
762
|
-
|
|
763
|
-
IMPORTANT for styling fixes:
|
|
764
|
-
- If text is invisible on a background, typically REMOVE the problematic text color class and ADD a contrasting one
|
|
765
|
-
- Common fix for invisible text on colored backgrounds: REMOVE "text-accent-foreground" or similar, ADD "text-white" or "text-black"
|
|
766
|
-
- Be SPECIFIC about exact class names
|
|
767
|
-
|
|
768
|
-
Return ONLY valid JSON:
|
|
769
|
-
{
|
|
770
|
-
"element": "description of the UI element",
|
|
771
|
-
"elementText": "exact visible text in the element",
|
|
772
|
-
"problem": "what is wrong",
|
|
773
|
-
"changeType": "styling",
|
|
774
|
-
"currentClasses": "classes that appear to be applied (best guess)",
|
|
775
|
-
"classChange": {
|
|
776
|
-
"remove": ["text-accent-foreground"],
|
|
777
|
-
"add": ["text-white"]
|
|
778
|
-
},
|
|
779
|
-
"solution": "complete description of the fix",
|
|
780
|
-
"confidence": "high"
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
For structural changes:
|
|
784
|
-
{
|
|
785
|
-
"element": "description of the UI element",
|
|
786
|
-
"elementText": "text near the element",
|
|
787
|
-
"problem": "what is wrong",
|
|
788
|
-
"changeType": "structural",
|
|
789
|
-
"structuralChange": "Add a new button after the existing one",
|
|
790
|
-
"solution": "complete description of the fix",
|
|
791
|
-
"confidence": "high"
|
|
792
|
-
}`,
|
|
793
|
-
},
|
|
794
|
-
],
|
|
795
|
-
},
|
|
796
|
-
],
|
|
797
|
-
});
|
|
798
|
-
|
|
799
|
-
const analysisText = analysisResponse.content.find((block) => block.type === "text");
|
|
800
|
-
if (analysisText && analysisText.type === "text") {
|
|
801
|
-
// Parse the JSON response
|
|
802
|
-
let jsonText = analysisText.text.trim();
|
|
803
|
-
// Extract JSON if wrapped in code blocks
|
|
804
|
-
const jsonMatch = jsonText.match(/```(?:json)?\s*([\s\S]*?)\s*```/) || jsonText.match(/(\{[\s\S]*\})/);
|
|
805
|
-
if (jsonMatch) {
|
|
806
|
-
jsonText = jsonMatch[1];
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
try {
|
|
810
|
-
visualAnalysis = JSON.parse(jsonText) as VisualAnalysis;
|
|
811
|
-
debugLog("Phase A: Visual problem analysis complete", visualAnalysis);
|
|
812
|
-
|
|
813
|
-
// If low confidence, return early with clarification request
|
|
814
|
-
if (visualAnalysis.confidence === "low") {
|
|
815
|
-
debugLog("Phase A: Low confidence - requesting clarification");
|
|
816
|
-
return NextResponse.json({
|
|
817
|
-
success: false,
|
|
818
|
-
needsClarification: true,
|
|
819
|
-
analysis: visualAnalysis,
|
|
820
|
-
message: `I can see "${visualAnalysis.element}" but I'm not certain about the problem: "${visualAnalysis.problem}". Can you be more specific about what needs to change?`,
|
|
821
|
-
});
|
|
822
|
-
}
|
|
823
|
-
} catch (parseError) {
|
|
824
|
-
debugLog("Phase A: Failed to parse analysis response", {
|
|
825
|
-
error: String(parseError),
|
|
826
|
-
response: jsonText.substring(0, 500)
|
|
827
|
-
});
|
|
828
|
-
// Continue without analysis if parsing fails
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
} catch (analysisError) {
|
|
832
|
-
debugLog("Phase A: Analysis call failed", { error: String(analysisError) });
|
|
833
|
-
// Continue without analysis - fall back to existing behavior
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
|
|
837
776
|
// ========== SMART CONTEXT BUDGETING ==========
|
|
838
777
|
// Claude can handle 200k tokens (~800k chars), so we can safely include large files
|
|
839
778
|
// Priority: Recommended file (NEVER truncate) > Page file (limited) > Other components (dynamic)
|
|
@@ -870,58 +809,6 @@ ${focusedElements.map((el) => `- ${el.name} (${el.type}) at (${el.coordinates.x}
|
|
|
870
809
|
`;
|
|
871
810
|
}
|
|
872
811
|
|
|
873
|
-
// ========== VISUAL PROBLEM ANALYSIS (from Phase A) ==========
|
|
874
|
-
if (visualAnalysis) {
|
|
875
|
-
let analysisInstructions = `═══════════════════════════════════════════════════════════════════════════════
|
|
876
|
-
🔍 VISUAL PROBLEM ANALYSIS (I analyzed the screenshot first)
|
|
877
|
-
═══════════════════════════════════════════════════════════════════════════════
|
|
878
|
-
|
|
879
|
-
**Element:** ${visualAnalysis.element}
|
|
880
|
-
**Element Text:** "${visualAnalysis.elementText || "unknown"}"
|
|
881
|
-
**Problem:** ${visualAnalysis.problem}
|
|
882
|
-
**Change Type:** ${visualAnalysis.changeType}
|
|
883
|
-
**Solution:** ${visualAnalysis.solution}
|
|
884
|
-
**Confidence:** ${visualAnalysis.confidence}
|
|
885
|
-
|
|
886
|
-
`;
|
|
887
|
-
|
|
888
|
-
// Add specific instructions based on change type
|
|
889
|
-
if (visualAnalysis.changeType === "styling" && visualAnalysis.classChange) {
|
|
890
|
-
analysisInstructions += `🎯 EXACT CLASS CHANGES REQUIRED:
|
|
891
|
-
`;
|
|
892
|
-
if (visualAnalysis.classChange.remove && visualAnalysis.classChange.remove.length > 0) {
|
|
893
|
-
analysisInstructions += ` REMOVE these classes: ${visualAnalysis.classChange.remove.join(", ")}
|
|
894
|
-
`;
|
|
895
|
-
}
|
|
896
|
-
if (visualAnalysis.classChange.add && visualAnalysis.classChange.add.length > 0) {
|
|
897
|
-
analysisInstructions += ` ADD these classes: ${visualAnalysis.classChange.add.join(", ")}
|
|
898
|
-
`;
|
|
899
|
-
}
|
|
900
|
-
analysisInstructions += `
|
|
901
|
-
⚠️ YOUR PATCH MUST:
|
|
902
|
-
1. Find the element containing "${visualAnalysis.elementText || visualAnalysis.element}"
|
|
903
|
-
2. In its className, ${visualAnalysis.classChange.remove?.length ? `REMOVE: ${visualAnalysis.classChange.remove.join(", ")}` : ""}
|
|
904
|
-
3. ${visualAnalysis.classChange.add?.length ? `ADD: ${visualAnalysis.classChange.add.join(", ")}` : ""}
|
|
905
|
-
4. Do NOT add unrelated changes like font-weight or hover states unless specifically requested
|
|
906
|
-
|
|
907
|
-
`;
|
|
908
|
-
} else if (visualAnalysis.changeType === "structural" && visualAnalysis.structuralChange) {
|
|
909
|
-
analysisInstructions += `🔧 STRUCTURAL CHANGE REQUIRED:
|
|
910
|
-
${visualAnalysis.structuralChange}
|
|
911
|
-
|
|
912
|
-
⚠️ YOUR PATCH MUST implement this structural change exactly as described.
|
|
913
|
-
|
|
914
|
-
`;
|
|
915
|
-
} else {
|
|
916
|
-
analysisInstructions += `⚠️ IMPORTANT: Your patches MUST implement the fix described above.
|
|
917
|
-
Find the code that renders "${visualAnalysis.element}" and apply: ${visualAnalysis.solution}
|
|
918
|
-
|
|
919
|
-
`;
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
textContent += analysisInstructions;
|
|
923
|
-
}
|
|
924
|
-
|
|
925
812
|
// ========== TARGET COMPONENT (RECOMMENDED FILE) - SHOWN FIRST ==========
|
|
926
813
|
if (recommendedFileContent) {
|
|
927
814
|
// Never truncate the recommended file - AI needs full context to avoid hallucination
|
|
@@ -986,17 +873,27 @@ ${truncatedContent}${wasTruncated ? "\n// ... (truncated)" : ""}
|
|
|
986
873
|
}
|
|
987
874
|
}
|
|
988
875
|
|
|
989
|
-
// ========== THEME DISCOVERY ==========
|
|
876
|
+
// ========== THEME DISCOVERY (REFERENCE ONLY) ==========
|
|
990
877
|
// Dynamically discover theme tokens from the target codebase
|
|
878
|
+
// This is marked as REFERENCE ONLY so the LLM doesn't use it to justify extra changes
|
|
991
879
|
const discoveredTheme = await discoverTheme(projectRoot);
|
|
992
880
|
const themeContext = formatThemeForPrompt(discoveredTheme);
|
|
993
881
|
|
|
994
882
|
if (discoveredTheme.discoveredFiles.length > 0) {
|
|
995
883
|
textContent += `
|
|
996
884
|
═══════════════════════════════════════════════════════════════════════════════
|
|
997
|
-
|
|
885
|
+
REFERENCE ONLY (do not use this to justify additional changes)
|
|
998
886
|
═══════════════════════════════════════════════════════════════════════════════
|
|
999
887
|
|
|
888
|
+
If you need to pick a color for a VISIBILITY fix, these are safe choices:
|
|
889
|
+
- bg-accent text-white (cyan button with white text)
|
|
890
|
+
- bg-primary text-white (charcoal button with white text)
|
|
891
|
+
- bg-success text-white (green button with white text)
|
|
892
|
+
- bg-destructive text-white (red button with white text)
|
|
893
|
+
|
|
894
|
+
But ONLY use these if the user is asking for a color/visibility change.
|
|
895
|
+
Do NOT rebrand or change other elements to match.
|
|
896
|
+
|
|
1000
897
|
`;
|
|
1001
898
|
debugLog("Theme discovery complete", {
|
|
1002
899
|
filesFound: discoveredTheme.discoveredFiles,
|
|
@@ -1369,7 +1266,23 @@ This is better than generating patches with made-up code.`,
|
|
|
1369
1266
|
break;
|
|
1370
1267
|
} // End of retry loop
|
|
1371
1268
|
|
|
1372
|
-
//
|
|
1269
|
+
// ========== PREVIEW MODE: Return modifications without writing ==========
|
|
1270
|
+
if (action === "preview") {
|
|
1271
|
+
debugLog("Preview mode: returning modifications without applying", {
|
|
1272
|
+
fileCount: modifications.length,
|
|
1273
|
+
files: modifications.map(m => m.filePath)
|
|
1274
|
+
});
|
|
1275
|
+
|
|
1276
|
+
return NextResponse.json({
|
|
1277
|
+
success: true,
|
|
1278
|
+
preview: true,
|
|
1279
|
+
modifications,
|
|
1280
|
+
explanation: finalExplanation,
|
|
1281
|
+
reasoning: finalReasoning,
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
// ========== APPLY MODE: Create backups and apply changes atomically ==========
|
|
1373
1286
|
const applyResult = await applyChangesWithBackup(
|
|
1374
1287
|
modifications,
|
|
1375
1288
|
newSessionId,
|
|
@@ -1394,7 +1307,7 @@ This is better than generating patches with made-up code.`,
|
|
|
1394
1307
|
}
|
|
1395
1308
|
|
|
1396
1309
|
return NextResponse.json(
|
|
1397
|
-
{ error: "Invalid action. Use 'apply', 'accept', or 'revert'." },
|
|
1310
|
+
{ error: "Invalid action. Use 'apply', 'preview', 'accept', or 'revert'." },
|
|
1398
1311
|
{ status: 400 }
|
|
1399
1312
|
);
|
|
1400
1313
|
} catch (error) {
|
|
@@ -1414,6 +1327,12 @@ async function applyChangesWithBackup(
|
|
|
1414
1327
|
sessionId: string,
|
|
1415
1328
|
projectRoot: string
|
|
1416
1329
|
): Promise<{ success: boolean; backupPaths: string[]; error?: string }> {
|
|
1330
|
+
console.log(`[Apply-First] applyChangesWithBackup called`, {
|
|
1331
|
+
sessionId,
|
|
1332
|
+
fileCount: modifications.length,
|
|
1333
|
+
files: modifications.map(m => m.filePath)
|
|
1334
|
+
});
|
|
1335
|
+
|
|
1417
1336
|
const backupDir = path.join(projectRoot, BACKUP_ROOT, sessionId);
|
|
1418
1337
|
const backupPaths: string[] = [];
|
|
1419
1338
|
|
|
@@ -1428,6 +1347,7 @@ async function applyChangesWithBackup(
|
|
|
1428
1347
|
|
|
1429
1348
|
try {
|
|
1430
1349
|
// Step 1: Create backup directory
|
|
1350
|
+
console.log(`[Apply-First] Creating backup directory: ${backupDir}`);
|
|
1431
1351
|
fs.mkdirSync(backupDir, { recursive: true });
|
|
1432
1352
|
|
|
1433
1353
|
// Step 2: Create ALL backups FIRST (before any writes)
|
|
@@ -1441,6 +1361,9 @@ async function applyChangesWithBackup(
|
|
|
1441
1361
|
fs.mkdirSync(backupDirForFile, { recursive: true });
|
|
1442
1362
|
fs.copyFileSync(fullPath, backupPath);
|
|
1443
1363
|
backupPaths.push(backupPath);
|
|
1364
|
+
console.log(`[Apply-First] Backed up: ${mod.filePath}`);
|
|
1365
|
+
} else {
|
|
1366
|
+
console.log(`[Apply-First] New file (no backup needed): ${mod.filePath}`);
|
|
1444
1367
|
}
|
|
1445
1368
|
}
|
|
1446
1369
|
|
|
@@ -1455,10 +1378,9 @@ async function applyChangesWithBackup(
|
|
|
1455
1378
|
})),
|
|
1456
1379
|
};
|
|
1457
1380
|
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
);
|
|
1381
|
+
const manifestPath = path.join(backupDir, "manifest.json");
|
|
1382
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
1383
|
+
console.log(`[Apply-First] Manifest written: ${manifestPath}`);
|
|
1462
1384
|
|
|
1463
1385
|
// Step 4: Apply all changes
|
|
1464
1386
|
for (const mod of modifications) {
|
|
@@ -1476,8 +1398,10 @@ async function applyChangesWithBackup(
|
|
|
1476
1398
|
}
|
|
1477
1399
|
|
|
1478
1400
|
fs.writeFileSync(fullPath, mod.modifiedContent, "utf-8");
|
|
1401
|
+
console.log(`[Apply-First] File written: ${mod.filePath}`);
|
|
1479
1402
|
}
|
|
1480
1403
|
|
|
1404
|
+
console.log(`[Apply-First] All changes applied successfully, session: ${sessionId}`);
|
|
1481
1405
|
return { success: true, backupPaths };
|
|
1482
1406
|
} catch (error) {
|
|
1483
1407
|
// Rollback on error - restore all backed up files
|