sonance-brand-mcp 1.3.56 → 1.3.58
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 +122 -231
- package/dist/assets/api/sonance-vision-edit/route.ts +47 -221
- 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 {
|
|
@@ -436,102 +438,41 @@ function searchFilesSmart(
|
|
|
436
438
|
return sortedResults.map(r => ({ path: r.path, content: r.content, score: r.score }));
|
|
437
439
|
}
|
|
438
440
|
|
|
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
|
-
PATCH FORMAT
|
|
463
|
-
═══════════════════════════════════════════════════════════════════════════════
|
|
464
|
-
|
|
465
|
-
Return search/replace patches (NOT full files). The system applies your patches to the original.
|
|
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:
|
|
441
|
+
const VISION_SYSTEM_PROMPT = `You are a code editor. Make ONLY the change the user requested.
|
|
442
|
+
|
|
443
|
+
RULES:
|
|
444
|
+
1. Make the SMALLEST possible change to accomplish the request
|
|
445
|
+
2. Do NOT refactor, rebrand, or "improve" anything else
|
|
446
|
+
3. Do NOT change import statements unless explicitly required
|
|
447
|
+
4. Do NOT change component libraries (e.g., heroui to shadcn)
|
|
448
|
+
5. If fixing a color/visibility issue, change ONLY that element's classes
|
|
449
|
+
6. Your patch should typically be 1-5 lines, not 50+
|
|
450
|
+
7. NEVER invent or guess code - your "search" string MUST match the file EXACTLY
|
|
451
|
+
8. NEVER change data mappings (icon names, keys, enum values) unless user explicitly provides the new values
|
|
452
|
+
9. If you don't know what values exist in a database or data source, DO NOT guess - ask for clarification
|
|
453
|
+
|
|
454
|
+
CRITICAL - DATA INTEGRITY:
|
|
455
|
+
- If code references database values (like icon_name, type, status), DO NOT change the mapping keys
|
|
456
|
+
- You cannot see database content - only change STRUCTURE, not DATA MAPPINGS
|
|
457
|
+
- Example: If you see iconMap["EyeOff"] = SomeIcon, do NOT change "EyeOff" to something else
|
|
458
|
+
- If user wants different icons, they must tell you the EXACT icon names they want
|
|
459
|
+
|
|
460
|
+
PATCH FORMAT:
|
|
461
|
+
Return ONLY raw JSON (no markdown, no preamble):
|
|
519
462
|
{
|
|
520
|
-
"reasoning": "
|
|
521
|
-
"modifications": [
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
"
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
"explanation": "summary of changes made"
|
|
534
|
-
}`;
|
|
463
|
+
"reasoning": "brief explanation",
|
|
464
|
+
"modifications": [{
|
|
465
|
+
"filePath": "path/to/file.tsx",
|
|
466
|
+
"patches": [{
|
|
467
|
+
"search": "exact original code (copy from provided file)",
|
|
468
|
+
"replace": "minimal change",
|
|
469
|
+
"explanation": "what this does"
|
|
470
|
+
}]
|
|
471
|
+
}],
|
|
472
|
+
"explanation": "summary"
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
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
476
|
|
|
536
477
|
export async function POST(request: Request) {
|
|
537
478
|
// Only allow in development
|
|
@@ -544,7 +485,7 @@ export async function POST(request: Request) {
|
|
|
544
485
|
|
|
545
486
|
try {
|
|
546
487
|
const body: ApplyFirstRequest = await request.json();
|
|
547
|
-
const { action, sessionId, screenshot, pageRoute, userPrompt, focusedElements } = body;
|
|
488
|
+
const { action, sessionId, screenshot, pageRoute, userPrompt, focusedElements, previewedModifications } = body;
|
|
548
489
|
const projectRoot = process.cwd();
|
|
549
490
|
|
|
550
491
|
// ========== ACCEPT ACTION ==========
|
|
@@ -571,6 +512,8 @@ export async function POST(request: Request) {
|
|
|
571
512
|
|
|
572
513
|
// ========== REVERT ACTION ==========
|
|
573
514
|
if (action === "revert") {
|
|
515
|
+
console.log(`[Apply-First] Revert requested for session: ${sessionId}`);
|
|
516
|
+
|
|
574
517
|
if (!sessionId) {
|
|
575
518
|
return NextResponse.json(
|
|
576
519
|
{ error: "sessionId is required for revert action" },
|
|
@@ -579,6 +522,7 @@ export async function POST(request: Request) {
|
|
|
579
522
|
}
|
|
580
523
|
|
|
581
524
|
const result = await revertFromBackups(sessionId, projectRoot);
|
|
525
|
+
console.log(`[Apply-First] Revert result:`, result);
|
|
582
526
|
|
|
583
527
|
return NextResponse.json({
|
|
584
528
|
success: result.success,
|
|
@@ -588,11 +532,44 @@ export async function POST(request: Request) {
|
|
|
588
532
|
});
|
|
589
533
|
}
|
|
590
534
|
|
|
591
|
-
// ========== APPLY ACTION ==========
|
|
592
|
-
|
|
535
|
+
// ========== APPLY FROM PREVIEW ACTION ==========
|
|
536
|
+
// When user accepts a preview, apply the previously generated modifications
|
|
537
|
+
if (action === "apply" && previewedModifications && previewedModifications.length > 0) {
|
|
538
|
+
debugLog("Applying previewed modifications", {
|
|
539
|
+
count: previewedModifications.length,
|
|
540
|
+
files: previewedModifications.map(m => m.filePath)
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
const newSessionId = randomUUID().slice(0, 8);
|
|
544
|
+
|
|
545
|
+
const applyResult = await applyChangesWithBackup(
|
|
546
|
+
previewedModifications,
|
|
547
|
+
newSessionId,
|
|
548
|
+
projectRoot
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
if (!applyResult.success) {
|
|
552
|
+
return NextResponse.json(
|
|
553
|
+
{ error: applyResult.error || "Failed to apply previewed changes" },
|
|
554
|
+
{ status: 500 }
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return NextResponse.json({
|
|
559
|
+
success: true,
|
|
560
|
+
sessionId: newSessionId,
|
|
561
|
+
modifications: previewedModifications,
|
|
562
|
+
backupPaths: applyResult.backupPaths,
|
|
563
|
+
explanation: "Applied previewed changes",
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// ========== APPLY/PREVIEW ACTION (with LLM) ==========
|
|
568
|
+
// Both actions do LLM work; "preview" returns without writing, "apply" writes to files
|
|
569
|
+
if (action === "apply" || action === "preview") {
|
|
593
570
|
if (!userPrompt) {
|
|
594
571
|
return NextResponse.json(
|
|
595
|
-
{ error: "userPrompt is required for apply action" },
|
|
572
|
+
{ error: "userPrompt is required for apply/preview action" },
|
|
596
573
|
{ status: 400 }
|
|
597
574
|
);
|
|
598
575
|
}
|
|
@@ -699,111 +676,6 @@ export async function POST(request: Request) {
|
|
|
699
676
|
});
|
|
700
677
|
}
|
|
701
678
|
|
|
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
|
-
problem: string;
|
|
708
|
-
currentState: string;
|
|
709
|
-
solution: string;
|
|
710
|
-
confidence: "high" | "medium" | "low";
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
let visualAnalysis: VisualAnalysis | null = null;
|
|
714
|
-
|
|
715
|
-
if (screenshot && userPrompt) {
|
|
716
|
-
debugLog("Phase A: Starting visual problem analysis");
|
|
717
|
-
|
|
718
|
-
const analysisClient = new Anthropic({ apiKey });
|
|
719
|
-
|
|
720
|
-
try {
|
|
721
|
-
const analysisResponse = await analysisClient.messages.create({
|
|
722
|
-
model: "claude-sonnet-4-20250514",
|
|
723
|
-
max_tokens: 1024,
|
|
724
|
-
messages: [
|
|
725
|
-
{
|
|
726
|
-
role: "user",
|
|
727
|
-
content: [
|
|
728
|
-
{
|
|
729
|
-
type: "image",
|
|
730
|
-
source: {
|
|
731
|
-
type: "base64",
|
|
732
|
-
media_type: "image/png",
|
|
733
|
-
data: base64Data,
|
|
734
|
-
},
|
|
735
|
-
},
|
|
736
|
-
{
|
|
737
|
-
type: "text",
|
|
738
|
-
text: `You are analyzing a UI screenshot to understand a visual problem.
|
|
739
|
-
|
|
740
|
-
User request: "${userPrompt}"
|
|
741
|
-
|
|
742
|
-
${focusedElements && focusedElements.length > 0 ? `User clicked on these elements:
|
|
743
|
-
${focusedElements.map((el) => `- ${el.name} (${el.type}) at position (${el.coordinates.x}, ${el.coordinates.y})`).join("\n")}
|
|
744
|
-
` : ""}
|
|
745
|
-
|
|
746
|
-
BEFORE any code changes can be made, you must analyze the visual problem.
|
|
747
|
-
|
|
748
|
-
Answer these questions:
|
|
749
|
-
1. ELEMENT: Which specific UI element has the problem? (describe its location, text content, appearance)
|
|
750
|
-
2. PROBLEM: What exactly is visually wrong? (invisible text, wrong color, poor contrast, wrong size, etc.)
|
|
751
|
-
3. CURRENT STATE: What styling/colors appear to be applied to this element?
|
|
752
|
-
4. SOLUTION: What specific CSS/Tailwind change would fix this problem?
|
|
753
|
-
5. CONFIDENCE: How confident are you that you've identified the correct element and problem? (high/medium/low)
|
|
754
|
-
|
|
755
|
-
Return ONLY valid JSON with no other text:
|
|
756
|
-
{
|
|
757
|
-
"element": "specific description of the UI element",
|
|
758
|
-
"problem": "what is visually wrong",
|
|
759
|
-
"currentState": "what styling appears to be applied",
|
|
760
|
-
"solution": "specific CSS/Tailwind fix needed",
|
|
761
|
-
"confidence": "high"
|
|
762
|
-
}`,
|
|
763
|
-
},
|
|
764
|
-
],
|
|
765
|
-
},
|
|
766
|
-
],
|
|
767
|
-
});
|
|
768
|
-
|
|
769
|
-
const analysisText = analysisResponse.content.find((block) => block.type === "text");
|
|
770
|
-
if (analysisText && analysisText.type === "text") {
|
|
771
|
-
// Parse the JSON response
|
|
772
|
-
let jsonText = analysisText.text.trim();
|
|
773
|
-
// Extract JSON if wrapped in code blocks
|
|
774
|
-
const jsonMatch = jsonText.match(/```(?:json)?\s*([\s\S]*?)\s*```/) || jsonText.match(/(\{[\s\S]*\})/);
|
|
775
|
-
if (jsonMatch) {
|
|
776
|
-
jsonText = jsonMatch[1];
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
try {
|
|
780
|
-
visualAnalysis = JSON.parse(jsonText) as VisualAnalysis;
|
|
781
|
-
debugLog("Phase A: Visual problem analysis complete", visualAnalysis);
|
|
782
|
-
|
|
783
|
-
// If low confidence, return early with clarification request
|
|
784
|
-
if (visualAnalysis.confidence === "low") {
|
|
785
|
-
debugLog("Phase A: Low confidence - requesting clarification");
|
|
786
|
-
return NextResponse.json({
|
|
787
|
-
success: false,
|
|
788
|
-
needsClarification: true,
|
|
789
|
-
analysis: visualAnalysis,
|
|
790
|
-
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?`,
|
|
791
|
-
});
|
|
792
|
-
}
|
|
793
|
-
} catch (parseError) {
|
|
794
|
-
debugLog("Phase A: Failed to parse analysis response", {
|
|
795
|
-
error: String(parseError),
|
|
796
|
-
response: jsonText.substring(0, 500)
|
|
797
|
-
});
|
|
798
|
-
// Continue without analysis if parsing fails
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
} catch (analysisError) {
|
|
802
|
-
debugLog("Phase A: Analysis call failed", { error: String(analysisError) });
|
|
803
|
-
// Continue without analysis - fall back to existing behavior
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
|
|
807
679
|
// ========== SMART CONTEXT BUDGETING ==========
|
|
808
680
|
// Claude can handle 200k tokens (~800k chars), so we can safely include large files
|
|
809
681
|
// Priority: Recommended file (NEVER truncate) > Page file (limited) > Other components (dynamic)
|
|
@@ -837,24 +709,6 @@ User Request: "${userPrompt}"
|
|
|
837
709
|
textContent += `FOCUSED ELEMENTS (user clicked on these):
|
|
838
710
|
${focusedElements.map((el) => `- ${el.name} (${el.type}) at (${el.coordinates.x}, ${el.coordinates.y})`).join("\n")}
|
|
839
711
|
|
|
840
|
-
`;
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
// ========== VISUAL PROBLEM ANALYSIS (from Phase A) ==========
|
|
844
|
-
if (visualAnalysis) {
|
|
845
|
-
textContent += `═══════════════════════════════════════════════════════════════════════════════
|
|
846
|
-
🔍 VISUAL PROBLEM ANALYSIS (I analyzed the screenshot first)
|
|
847
|
-
═══════════════════════════════════════════════════════════════════════════════
|
|
848
|
-
|
|
849
|
-
**Element:** ${visualAnalysis.element}
|
|
850
|
-
**Problem:** ${visualAnalysis.problem}
|
|
851
|
-
**Current State:** ${visualAnalysis.currentState}
|
|
852
|
-
**Required Fix:** ${visualAnalysis.solution}
|
|
853
|
-
**Confidence:** ${visualAnalysis.confidence}
|
|
854
|
-
|
|
855
|
-
⚠️ IMPORTANT: Your patches MUST implement the fix described above for the element described above.
|
|
856
|
-
Find the code that renders "${visualAnalysis.element}" and apply "${visualAnalysis.solution}".
|
|
857
|
-
|
|
858
712
|
`;
|
|
859
713
|
}
|
|
860
714
|
|
|
@@ -922,17 +776,27 @@ ${truncatedContent}${wasTruncated ? "\n// ... (truncated)" : ""}
|
|
|
922
776
|
}
|
|
923
777
|
}
|
|
924
778
|
|
|
925
|
-
// ========== THEME DISCOVERY ==========
|
|
779
|
+
// ========== THEME DISCOVERY (REFERENCE ONLY) ==========
|
|
926
780
|
// Dynamically discover theme tokens from the target codebase
|
|
781
|
+
// This is marked as REFERENCE ONLY so the LLM doesn't use it to justify extra changes
|
|
927
782
|
const discoveredTheme = await discoverTheme(projectRoot);
|
|
928
783
|
const themeContext = formatThemeForPrompt(discoveredTheme);
|
|
929
784
|
|
|
930
785
|
if (discoveredTheme.discoveredFiles.length > 0) {
|
|
931
786
|
textContent += `
|
|
932
787
|
═══════════════════════════════════════════════════════════════════════════════
|
|
933
|
-
|
|
788
|
+
REFERENCE ONLY (do not use this to justify additional changes)
|
|
934
789
|
═══════════════════════════════════════════════════════════════════════════════
|
|
935
790
|
|
|
791
|
+
If you need to pick a color for a VISIBILITY fix, these are safe choices:
|
|
792
|
+
- bg-accent text-white (cyan button with white text)
|
|
793
|
+
- bg-primary text-white (charcoal button with white text)
|
|
794
|
+
- bg-success text-white (green button with white text)
|
|
795
|
+
- bg-destructive text-white (red button with white text)
|
|
796
|
+
|
|
797
|
+
But ONLY use these if the user is asking for a color/visibility change.
|
|
798
|
+
Do NOT rebrand or change other elements to match.
|
|
799
|
+
|
|
936
800
|
`;
|
|
937
801
|
debugLog("Theme discovery complete", {
|
|
938
802
|
filesFound: discoveredTheme.discoveredFiles,
|
|
@@ -1305,7 +1169,23 @@ This is better than generating patches with made-up code.`,
|
|
|
1305
1169
|
break;
|
|
1306
1170
|
} // End of retry loop
|
|
1307
1171
|
|
|
1308
|
-
//
|
|
1172
|
+
// ========== PREVIEW MODE: Return modifications without writing ==========
|
|
1173
|
+
if (action === "preview") {
|
|
1174
|
+
debugLog("Preview mode: returning modifications without applying", {
|
|
1175
|
+
fileCount: modifications.length,
|
|
1176
|
+
files: modifications.map(m => m.filePath)
|
|
1177
|
+
});
|
|
1178
|
+
|
|
1179
|
+
return NextResponse.json({
|
|
1180
|
+
success: true,
|
|
1181
|
+
preview: true,
|
|
1182
|
+
modifications,
|
|
1183
|
+
explanation: finalExplanation,
|
|
1184
|
+
reasoning: finalReasoning,
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// ========== APPLY MODE: Create backups and apply changes atomically ==========
|
|
1309
1189
|
const applyResult = await applyChangesWithBackup(
|
|
1310
1190
|
modifications,
|
|
1311
1191
|
newSessionId,
|
|
@@ -1330,7 +1210,7 @@ This is better than generating patches with made-up code.`,
|
|
|
1330
1210
|
}
|
|
1331
1211
|
|
|
1332
1212
|
return NextResponse.json(
|
|
1333
|
-
{ error: "Invalid action. Use 'apply', 'accept', or 'revert'." },
|
|
1213
|
+
{ error: "Invalid action. Use 'apply', 'preview', 'accept', or 'revert'." },
|
|
1334
1214
|
{ status: 400 }
|
|
1335
1215
|
);
|
|
1336
1216
|
} catch (error) {
|
|
@@ -1350,6 +1230,12 @@ async function applyChangesWithBackup(
|
|
|
1350
1230
|
sessionId: string,
|
|
1351
1231
|
projectRoot: string
|
|
1352
1232
|
): Promise<{ success: boolean; backupPaths: string[]; error?: string }> {
|
|
1233
|
+
console.log(`[Apply-First] applyChangesWithBackup called`, {
|
|
1234
|
+
sessionId,
|
|
1235
|
+
fileCount: modifications.length,
|
|
1236
|
+
files: modifications.map(m => m.filePath)
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1353
1239
|
const backupDir = path.join(projectRoot, BACKUP_ROOT, sessionId);
|
|
1354
1240
|
const backupPaths: string[] = [];
|
|
1355
1241
|
|
|
@@ -1364,6 +1250,7 @@ async function applyChangesWithBackup(
|
|
|
1364
1250
|
|
|
1365
1251
|
try {
|
|
1366
1252
|
// Step 1: Create backup directory
|
|
1253
|
+
console.log(`[Apply-First] Creating backup directory: ${backupDir}`);
|
|
1367
1254
|
fs.mkdirSync(backupDir, { recursive: true });
|
|
1368
1255
|
|
|
1369
1256
|
// Step 2: Create ALL backups FIRST (before any writes)
|
|
@@ -1377,6 +1264,9 @@ async function applyChangesWithBackup(
|
|
|
1377
1264
|
fs.mkdirSync(backupDirForFile, { recursive: true });
|
|
1378
1265
|
fs.copyFileSync(fullPath, backupPath);
|
|
1379
1266
|
backupPaths.push(backupPath);
|
|
1267
|
+
console.log(`[Apply-First] Backed up: ${mod.filePath}`);
|
|
1268
|
+
} else {
|
|
1269
|
+
console.log(`[Apply-First] New file (no backup needed): ${mod.filePath}`);
|
|
1380
1270
|
}
|
|
1381
1271
|
}
|
|
1382
1272
|
|
|
@@ -1391,10 +1281,9 @@ async function applyChangesWithBackup(
|
|
|
1391
1281
|
})),
|
|
1392
1282
|
};
|
|
1393
1283
|
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
);
|
|
1284
|
+
const manifestPath = path.join(backupDir, "manifest.json");
|
|
1285
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
1286
|
+
console.log(`[Apply-First] Manifest written: ${manifestPath}`);
|
|
1398
1287
|
|
|
1399
1288
|
// Step 4: Apply all changes
|
|
1400
1289
|
for (const mod of modifications) {
|
|
@@ -1412,8 +1301,10 @@ async function applyChangesWithBackup(
|
|
|
1412
1301
|
}
|
|
1413
1302
|
|
|
1414
1303
|
fs.writeFileSync(fullPath, mod.modifiedContent, "utf-8");
|
|
1304
|
+
console.log(`[Apply-First] File written: ${mod.filePath}`);
|
|
1415
1305
|
}
|
|
1416
1306
|
|
|
1307
|
+
console.log(`[Apply-First] All changes applied successfully, session: ${sessionId}`);
|
|
1417
1308
|
return { success: true, backupPaths };
|
|
1418
1309
|
} catch (error) {
|
|
1419
1310
|
// Rollback on error - restore all backed up files
|
|
@@ -434,104 +434,43 @@ function searchFilesSmart(
|
|
|
434
434
|
return sortedResults.map(r => ({ path: r.path, content: r.content, score: r.score }));
|
|
435
435
|
}
|
|
436
436
|
|
|
437
|
-
const VISION_SYSTEM_PROMPT = `You are
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
═══════════════════════════════════════════════════════════════════════════════
|
|
460
|
-
PATCH FORMAT
|
|
461
|
-
═══════════════════════════════════════════════════════════════════════════════
|
|
462
|
-
|
|
463
|
-
Return search/replace patches (NOT full files). The system applies your patches to the original.
|
|
464
|
-
|
|
465
|
-
**Patch Rules:**
|
|
466
|
-
- "search" must match the original code EXACTLY (including whitespace/indentation)
|
|
467
|
-
- "replace" contains your modified version
|
|
468
|
-
- Include 2-4 lines of context in "search" to make it unique
|
|
469
|
-
- You may ONLY edit files provided in the PAGE CONTEXT section
|
|
470
|
-
- 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.
|
|
471
|
-
- If the file content appears truncated, only modify code that is visible in the provided content.
|
|
472
|
-
|
|
473
|
-
═══════════════════════════════════════════════════════════════════════════════
|
|
474
|
-
SONANCE BRAND COLOR SYSTEM
|
|
475
|
-
═══════════════════════════════════════════════════════════════════════════════
|
|
476
|
-
|
|
477
|
-
**Core Colors:**
|
|
478
|
-
- Primary (Charcoal): #333F48 - main text, dark backgrounds
|
|
479
|
-
- Accent (Cyan "The Beam"): #00D3C8 - highlights, interactive elements, CTAs
|
|
480
|
-
- Success: Green tones - positive states, confirmations
|
|
481
|
-
- Warning: Amber/Orange tones - caution states, alerts
|
|
482
|
-
- Destructive: Red tones - errors, delete actions
|
|
483
|
-
|
|
484
|
-
**CRITICAL CONTRAST RULES:**
|
|
485
|
-
When using colored backgrounds, ALWAYS use high-contrast text:
|
|
486
|
-
|
|
487
|
-
| Background | Use This Text | NEVER Use |
|
|
488
|
-
|-------------------|------------------------------------|-----------------------|
|
|
489
|
-
| bg-accent | text-white | text-accent-foreground |
|
|
490
|
-
| bg-primary | text-primary-foreground, text-white | text-primary |
|
|
491
|
-
| bg-success | text-white | text-success |
|
|
492
|
-
| bg-warning | text-white | text-warning |
|
|
493
|
-
| bg-destructive | text-white | text-destructive |
|
|
494
|
-
|
|
495
|
-
**SEMANTIC TOKEN PATTERNS:**
|
|
496
|
-
- text-{color} = the color itself (use on NEUTRAL backgrounds like white/gray)
|
|
497
|
-
- text-{color}-foreground = INTENDED for text on {color} backgrounds, but MAY have contrast issues
|
|
498
|
-
- bg-{color} = background in that color
|
|
499
|
-
- WHEN IN DOUBT: Use text-white on any colored background for guaranteed contrast
|
|
500
|
-
|
|
501
|
-
**BUTTON PATTERNS (Sonance Standard):**
|
|
502
|
-
- Primary CTA: bg-primary text-primary-foreground
|
|
503
|
-
- Accent/Highlight: bg-accent text-white (NOT text-accent-foreground)
|
|
504
|
-
- Success: bg-success text-white
|
|
505
|
-
- Warning: bg-warning text-white
|
|
506
|
-
- Destructive: bg-destructive text-white
|
|
507
|
-
- Outlined: border-border bg-transparent text-foreground
|
|
508
|
-
|
|
509
|
-
**COMMON MISTAKES TO AVOID:**
|
|
510
|
-
- WRONG: bg-accent text-accent-foreground (accent-foreground is often dark = invisible text)
|
|
511
|
-
- RIGHT: bg-accent text-white (white text on cyan = visible)
|
|
512
|
-
- WRONG: bg-primary text-primary (same color = invisible)
|
|
513
|
-
- RIGHT: bg-primary text-primary-foreground OR text-white
|
|
514
|
-
|
|
515
|
-
**RESPONSE FORMAT:**
|
|
516
|
-
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:
|
|
437
|
+
const VISION_SYSTEM_PROMPT = `You are a code editor. Make ONLY the change the user requested.
|
|
438
|
+
|
|
439
|
+
RULES:
|
|
440
|
+
1. Make the SMALLEST possible change to accomplish the request
|
|
441
|
+
2. Do NOT refactor, rebrand, or "improve" anything else
|
|
442
|
+
3. Do NOT change import statements unless explicitly required
|
|
443
|
+
4. Do NOT change component libraries (e.g., heroui to shadcn)
|
|
444
|
+
5. If fixing a color/visibility issue, change ONLY that element's classes
|
|
445
|
+
6. Your patch should typically be 1-5 lines, not 50+
|
|
446
|
+
7. NEVER invent or guess code - your "search" string MUST match the file EXACTLY
|
|
447
|
+
8. NEVER change data mappings (icon names, keys, enum values) unless user explicitly provides the new values
|
|
448
|
+
9. If you don't know what values exist in a database or data source, DO NOT guess - ask for clarification
|
|
449
|
+
|
|
450
|
+
CRITICAL - DATA INTEGRITY:
|
|
451
|
+
- If code references database values (like icon_name, type, status), DO NOT change the mapping keys
|
|
452
|
+
- You cannot see database content - only change STRUCTURE, not DATA MAPPINGS
|
|
453
|
+
- Example: If you see iconMap["EyeOff"] = SomeIcon, do NOT change "EyeOff" to something else
|
|
454
|
+
- If user wants different icons, they must tell you the EXACT icon names they want
|
|
455
|
+
|
|
456
|
+
PATCH FORMAT:
|
|
457
|
+
Return ONLY raw JSON (no markdown, no preamble):
|
|
517
458
|
{
|
|
518
|
-
"reasoning": "
|
|
519
|
-
"modifications": [
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
"
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
],
|
|
529
|
-
"previewCSS": "optional CSS for live preview"
|
|
530
|
-
}
|
|
531
|
-
],
|
|
459
|
+
"reasoning": "brief explanation",
|
|
460
|
+
"modifications": [{
|
|
461
|
+
"filePath": "path/to/file.tsx",
|
|
462
|
+
"patches": [{
|
|
463
|
+
"search": "exact original code (copy from provided file)",
|
|
464
|
+
"replace": "minimal change",
|
|
465
|
+
"explanation": "what this does"
|
|
466
|
+
}],
|
|
467
|
+
"previewCSS": "optional CSS for live preview"
|
|
468
|
+
}],
|
|
532
469
|
"aggregatedPreviewCSS": "combined CSS for all changes",
|
|
533
|
-
"explanation": "summary
|
|
534
|
-
}
|
|
470
|
+
"explanation": "summary"
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
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
474
|
|
|
536
475
|
export async function POST(request: Request) {
|
|
537
476
|
// Only allow in development
|
|
@@ -708,111 +647,6 @@ export async function POST(request: Request) {
|
|
|
708
647
|
});
|
|
709
648
|
}
|
|
710
649
|
|
|
711
|
-
// ========== PHASE A: VISUAL PROBLEM ANALYSIS ==========
|
|
712
|
-
// Before generating patches, analyze WHAT is visually wrong
|
|
713
|
-
// This forces the LLM to articulate the problem before trying to fix it
|
|
714
|
-
interface VisualAnalysis {
|
|
715
|
-
element: string;
|
|
716
|
-
problem: string;
|
|
717
|
-
currentState: string;
|
|
718
|
-
solution: string;
|
|
719
|
-
confidence: "high" | "medium" | "low";
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
let visualAnalysis: VisualAnalysis | null = null;
|
|
723
|
-
|
|
724
|
-
if (screenshot && userPrompt) {
|
|
725
|
-
debugLog("Phase A: Starting visual problem analysis");
|
|
726
|
-
|
|
727
|
-
const analysisClient = new Anthropic({ apiKey });
|
|
728
|
-
|
|
729
|
-
try {
|
|
730
|
-
const analysisResponse = await analysisClient.messages.create({
|
|
731
|
-
model: "claude-sonnet-4-20250514",
|
|
732
|
-
max_tokens: 1024,
|
|
733
|
-
messages: [
|
|
734
|
-
{
|
|
735
|
-
role: "user",
|
|
736
|
-
content: [
|
|
737
|
-
{
|
|
738
|
-
type: "image",
|
|
739
|
-
source: {
|
|
740
|
-
type: "base64",
|
|
741
|
-
media_type: "image/png",
|
|
742
|
-
data: base64Data,
|
|
743
|
-
},
|
|
744
|
-
},
|
|
745
|
-
{
|
|
746
|
-
type: "text",
|
|
747
|
-
text: `You are analyzing a UI screenshot to understand a visual problem.
|
|
748
|
-
|
|
749
|
-
User request: "${userPrompt}"
|
|
750
|
-
|
|
751
|
-
${focusedElements && focusedElements.length > 0 ? `User clicked on these elements:
|
|
752
|
-
${focusedElements.map((el) => `- ${el.name} (${el.type}) at position (${el.coordinates.x}, ${el.coordinates.y})`).join("\n")}
|
|
753
|
-
` : ""}
|
|
754
|
-
|
|
755
|
-
BEFORE any code changes can be made, you must analyze the visual problem.
|
|
756
|
-
|
|
757
|
-
Answer these questions:
|
|
758
|
-
1. ELEMENT: Which specific UI element has the problem? (describe its location, text content, appearance)
|
|
759
|
-
2. PROBLEM: What exactly is visually wrong? (invisible text, wrong color, poor contrast, wrong size, etc.)
|
|
760
|
-
3. CURRENT STATE: What styling/colors appear to be applied to this element?
|
|
761
|
-
4. SOLUTION: What specific CSS/Tailwind change would fix this problem?
|
|
762
|
-
5. CONFIDENCE: How confident are you that you've identified the correct element and problem? (high/medium/low)
|
|
763
|
-
|
|
764
|
-
Return ONLY valid JSON with no other text:
|
|
765
|
-
{
|
|
766
|
-
"element": "specific description of the UI element",
|
|
767
|
-
"problem": "what is visually wrong",
|
|
768
|
-
"currentState": "what styling appears to be applied",
|
|
769
|
-
"solution": "specific CSS/Tailwind fix needed",
|
|
770
|
-
"confidence": "high"
|
|
771
|
-
}`,
|
|
772
|
-
},
|
|
773
|
-
],
|
|
774
|
-
},
|
|
775
|
-
],
|
|
776
|
-
});
|
|
777
|
-
|
|
778
|
-
const analysisText = analysisResponse.content.find((block) => block.type === "text");
|
|
779
|
-
if (analysisText && analysisText.type === "text") {
|
|
780
|
-
// Parse the JSON response
|
|
781
|
-
let jsonText = analysisText.text.trim();
|
|
782
|
-
// Extract JSON if wrapped in code blocks
|
|
783
|
-
const jsonMatch = jsonText.match(/```(?:json)?\s*([\s\S]*?)\s*```/) || jsonText.match(/(\{[\s\S]*\})/);
|
|
784
|
-
if (jsonMatch) {
|
|
785
|
-
jsonText = jsonMatch[1];
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
try {
|
|
789
|
-
visualAnalysis = JSON.parse(jsonText) as VisualAnalysis;
|
|
790
|
-
debugLog("Phase A: Visual problem analysis complete", visualAnalysis);
|
|
791
|
-
|
|
792
|
-
// If low confidence, return early with clarification request
|
|
793
|
-
if (visualAnalysis.confidence === "low") {
|
|
794
|
-
debugLog("Phase A: Low confidence - requesting clarification");
|
|
795
|
-
return NextResponse.json({
|
|
796
|
-
success: false,
|
|
797
|
-
needsClarification: true,
|
|
798
|
-
analysis: visualAnalysis,
|
|
799
|
-
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?`,
|
|
800
|
-
});
|
|
801
|
-
}
|
|
802
|
-
} catch (parseError) {
|
|
803
|
-
debugLog("Phase A: Failed to parse analysis response", {
|
|
804
|
-
error: String(parseError),
|
|
805
|
-
response: jsonText.substring(0, 500)
|
|
806
|
-
});
|
|
807
|
-
// Continue without analysis if parsing fails
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
} catch (analysisError) {
|
|
811
|
-
debugLog("Phase A: Analysis call failed", { error: String(analysisError) });
|
|
812
|
-
// Continue without analysis - fall back to existing behavior
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
|
|
816
650
|
// ========== SMART CONTEXT BUDGETING ==========
|
|
817
651
|
// Claude can handle 200k tokens (~800k chars), so we can safely include large files
|
|
818
652
|
// Priority: Recommended file (NEVER truncate) > Page file (limited) > Other components (dynamic)
|
|
@@ -846,24 +680,6 @@ User Request: "${userPrompt}"
|
|
|
846
680
|
textContent += `FOCUSED ELEMENTS (user clicked on these):
|
|
847
681
|
${focusedElements.map((el) => `- ${el.name} (${el.type}) at (${el.coordinates.x}, ${el.coordinates.y})`).join("\n")}
|
|
848
682
|
|
|
849
|
-
`;
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
// ========== VISUAL PROBLEM ANALYSIS (from Phase A) ==========
|
|
853
|
-
if (visualAnalysis) {
|
|
854
|
-
textContent += `═══════════════════════════════════════════════════════════════════════════════
|
|
855
|
-
🔍 VISUAL PROBLEM ANALYSIS (I analyzed the screenshot first)
|
|
856
|
-
═══════════════════════════════════════════════════════════════════════════════
|
|
857
|
-
|
|
858
|
-
**Element:** ${visualAnalysis.element}
|
|
859
|
-
**Problem:** ${visualAnalysis.problem}
|
|
860
|
-
**Current State:** ${visualAnalysis.currentState}
|
|
861
|
-
**Required Fix:** ${visualAnalysis.solution}
|
|
862
|
-
**Confidence:** ${visualAnalysis.confidence}
|
|
863
|
-
|
|
864
|
-
⚠️ IMPORTANT: Your patches MUST implement the fix described above for the element described above.
|
|
865
|
-
Find the code that renders "${visualAnalysis.element}" and apply "${visualAnalysis.solution}".
|
|
866
|
-
|
|
867
683
|
`;
|
|
868
684
|
}
|
|
869
685
|
|
|
@@ -931,17 +747,27 @@ ${truncatedContent}${wasTruncated ? "\n// ... (truncated)" : ""}
|
|
|
931
747
|
}
|
|
932
748
|
}
|
|
933
749
|
|
|
934
|
-
// ========== THEME DISCOVERY ==========
|
|
750
|
+
// ========== THEME DISCOVERY (REFERENCE ONLY) ==========
|
|
935
751
|
// Dynamically discover theme tokens from the target codebase
|
|
752
|
+
// This is marked as REFERENCE ONLY so the LLM doesn't use it to justify extra changes
|
|
936
753
|
const discoveredTheme = await discoverTheme(projectRoot);
|
|
937
754
|
const themeContext = formatThemeForPrompt(discoveredTheme);
|
|
938
755
|
|
|
939
756
|
if (discoveredTheme.discoveredFiles.length > 0) {
|
|
940
757
|
textContent += `
|
|
941
758
|
═══════════════════════════════════════════════════════════════════════════════
|
|
942
|
-
|
|
759
|
+
REFERENCE ONLY (do not use this to justify additional changes)
|
|
943
760
|
═══════════════════════════════════════════════════════════════════════════════
|
|
944
761
|
|
|
762
|
+
If you need to pick a color for a VISIBILITY fix, these are safe choices:
|
|
763
|
+
- bg-accent text-white (cyan button with white text)
|
|
764
|
+
- bg-primary text-white (charcoal button with white text)
|
|
765
|
+
- bg-success text-white (green button with white text)
|
|
766
|
+
- bg-destructive text-white (red button with white text)
|
|
767
|
+
|
|
768
|
+
But ONLY use these if the user is asking for a color/visibility change.
|
|
769
|
+
Do NOT rebrand or change other elements to match.
|
|
770
|
+
|
|
945
771
|
`;
|
|
946
772
|
debugLog("Theme discovery complete", {
|
|
947
773
|
filesFound: discoveredTheme.discoveredFiles,
|
|
@@ -1,16 +1,148 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import React, { useState, useEffect } from "react";
|
|
4
|
-
import { X, Zap, Loader2, Check,
|
|
3
|
+
import React, { useState, useEffect, useCallback } from "react";
|
|
4
|
+
import { X, Zap, Loader2, Check, ChevronDown, ChevronRight, FileCode, AlertTriangle, Info, Eye, EyeOff } from "lucide-react";
|
|
5
5
|
import { cn } from "../../../lib/utils";
|
|
6
6
|
import { ApplyFirstSession, ApplyFirstStatus, VisionFileModification } from "../types";
|
|
7
7
|
|
|
8
|
+
// CSS Injection Preview ID
|
|
9
|
+
const PREVIEW_STYLE_ID = "sonance-preview-css";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Extract className changes from a diff
|
|
13
|
+
* Returns: { removed: string[], added: string[] }
|
|
14
|
+
*/
|
|
15
|
+
function extractClassNameChanges(diff: string): { removed: string[]; added: string[] } {
|
|
16
|
+
const removed: string[] = [];
|
|
17
|
+
const added: string[] = [];
|
|
18
|
+
|
|
19
|
+
// Regex to find className="..." patterns
|
|
20
|
+
const classNameRegex = /className=["']([^"']+)["']/g;
|
|
21
|
+
|
|
22
|
+
const lines = diff.split("\n");
|
|
23
|
+
for (const line of lines) {
|
|
24
|
+
if (line.startsWith("-") && !line.startsWith("---")) {
|
|
25
|
+
// Removed line - extract className
|
|
26
|
+
let match;
|
|
27
|
+
while ((match = classNameRegex.exec(line)) !== null) {
|
|
28
|
+
removed.push(...match[1].split(/\s+/));
|
|
29
|
+
}
|
|
30
|
+
} else if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
31
|
+
// Added line - extract className
|
|
32
|
+
let match;
|
|
33
|
+
while ((match = classNameRegex.exec(line)) !== null) {
|
|
34
|
+
added.push(...match[1].split(/\s+/));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return { removed, added };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Common Tailwind class to CSS mapping for preview
|
|
44
|
+
* Only covers the most common styling classes for quick previews
|
|
45
|
+
*/
|
|
46
|
+
const TAILWIND_TO_CSS: Record<string, string> = {
|
|
47
|
+
// Text colors
|
|
48
|
+
"text-white": "color: white !important;",
|
|
49
|
+
"text-black": "color: black !important;",
|
|
50
|
+
"text-gray-900": "color: rgb(17, 24, 39) !important;",
|
|
51
|
+
"text-gray-700": "color: rgb(55, 65, 81) !important;",
|
|
52
|
+
"text-gray-500": "color: rgb(107, 114, 128) !important;",
|
|
53
|
+
|
|
54
|
+
// Sonance brand colors
|
|
55
|
+
"text-sonance-charcoal": "color: #333F48 !important;",
|
|
56
|
+
"text-sonance-blue": "color: #00D3C8 !important;",
|
|
57
|
+
"border-sonance-charcoal": "border-color: #333F48 !important;",
|
|
58
|
+
"border-sonance-blue": "border-color: #00D3C8 !important;",
|
|
59
|
+
"bg-sonance-charcoal": "background-color: #333F48 !important;",
|
|
60
|
+
"bg-sonance-blue": "background-color: #00D3C8 !important;",
|
|
61
|
+
|
|
62
|
+
// Semantic colors
|
|
63
|
+
"text-primary": "color: var(--primary) !important;",
|
|
64
|
+
"text-primary-foreground": "color: var(--primary-foreground) !important;",
|
|
65
|
+
"text-accent": "color: var(--accent) !important;",
|
|
66
|
+
"text-accent-foreground": "color: var(--accent-foreground) !important;",
|
|
67
|
+
"bg-primary": "background-color: hsl(var(--primary)) !important;",
|
|
68
|
+
"bg-accent": "background-color: hsl(var(--accent)) !important;",
|
|
69
|
+
|
|
70
|
+
// Common backgrounds
|
|
71
|
+
"bg-white": "background-color: white !important;",
|
|
72
|
+
"bg-black": "background-color: black !important;",
|
|
73
|
+
"bg-gray-100": "background-color: rgb(243, 244, 246) !important;",
|
|
74
|
+
"bg-gray-200": "background-color: rgb(229, 231, 235) !important;",
|
|
75
|
+
|
|
76
|
+
// Borders
|
|
77
|
+
"border-gray-200": "border-color: rgb(229, 231, 235) !important;",
|
|
78
|
+
"border-gray-300": "border-color: rgb(209, 213, 219) !important;",
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Generate CSS for preview based on class changes
|
|
83
|
+
*/
|
|
84
|
+
function generatePreviewCSS(modifications: VisionFileModification[]): string {
|
|
85
|
+
const cssRules: string[] = [];
|
|
86
|
+
|
|
87
|
+
for (const mod of modifications) {
|
|
88
|
+
const { added } = extractClassNameChanges(mod.diff);
|
|
89
|
+
|
|
90
|
+
// Generate CSS for added classes
|
|
91
|
+
for (const cls of added) {
|
|
92
|
+
if (TAILWIND_TO_CSS[cls]) {
|
|
93
|
+
// We can't know the exact selector, so we apply to elements with data-sonance-preview
|
|
94
|
+
cssRules.push(TAILWIND_TO_CSS[cls]);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (cssRules.length === 0) return "";
|
|
100
|
+
|
|
101
|
+
// Apply to changed elements (marked by InspectorOverlay)
|
|
102
|
+
return `
|
|
103
|
+
[data-sonance-changed="true"] {
|
|
104
|
+
${cssRules.join("\n ")}
|
|
105
|
+
outline: 2px dashed #00D3C8 !important;
|
|
106
|
+
outline-offset: 2px !important;
|
|
107
|
+
}
|
|
108
|
+
`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Inject preview CSS into the document
|
|
113
|
+
*/
|
|
114
|
+
function injectPreviewCSS(css: string): void {
|
|
115
|
+
// Remove existing preview styles first
|
|
116
|
+
removePreviewCSS();
|
|
117
|
+
|
|
118
|
+
if (!css.trim()) return;
|
|
119
|
+
|
|
120
|
+
const style = document.createElement("style");
|
|
121
|
+
style.id = PREVIEW_STYLE_ID;
|
|
122
|
+
style.textContent = css;
|
|
123
|
+
document.head.appendChild(style);
|
|
124
|
+
|
|
125
|
+
console.log("[Preview CSS] Injected:", css.substring(0, 200));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Remove preview CSS from the document
|
|
130
|
+
*/
|
|
131
|
+
function removePreviewCSS(): void {
|
|
132
|
+
const existing = document.getElementById(PREVIEW_STYLE_ID);
|
|
133
|
+
if (existing) {
|
|
134
|
+
existing.remove();
|
|
135
|
+
console.log("[Preview CSS] Removed");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
8
139
|
export interface ApplyFirstPreviewProps {
|
|
9
140
|
session: ApplyFirstSession;
|
|
10
141
|
status: ApplyFirstStatus;
|
|
11
142
|
onAccept: () => void;
|
|
12
143
|
onRevert: () => void;
|
|
13
144
|
onForceClear?: () => void; // Force clear stale sessions
|
|
145
|
+
onApplyPreview?: () => void; // Apply previewed changes (for preview mode)
|
|
14
146
|
}
|
|
15
147
|
|
|
16
148
|
function FileModificationCard({
|
|
@@ -78,7 +210,7 @@ function HMRStatusBadge({ status }: { status: ApplyFirstStatus }) {
|
|
|
78
210
|
if (status === "waiting-hmr") {
|
|
79
211
|
return (
|
|
80
212
|
<span className="flex items-center gap-1 text-[10px] px-1.5 py-0.5 rounded bg-amber-100 text-amber-700">
|
|
81
|
-
<
|
|
213
|
+
<Loader2 className="h-3 w-3 animate-spin" />
|
|
82
214
|
Refreshing...
|
|
83
215
|
</span>
|
|
84
216
|
);
|
|
@@ -92,6 +224,15 @@ function HMRStatusBadge({ status }: { status: ApplyFirstStatus }) {
|
|
|
92
224
|
</span>
|
|
93
225
|
);
|
|
94
226
|
}
|
|
227
|
+
|
|
228
|
+
if (status === "previewing") {
|
|
229
|
+
return (
|
|
230
|
+
<span className="flex items-center gap-1 text-[10px] px-1.5 py-0.5 rounded bg-blue-100 text-blue-700">
|
|
231
|
+
<Eye className="h-3 w-3" />
|
|
232
|
+
Preview Mode
|
|
233
|
+
</span>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
95
236
|
|
|
96
237
|
return null;
|
|
97
238
|
}
|
|
@@ -102,8 +243,10 @@ export function ApplyFirstPreview({
|
|
|
102
243
|
onAccept,
|
|
103
244
|
onRevert,
|
|
104
245
|
onForceClear,
|
|
246
|
+
onApplyPreview,
|
|
105
247
|
}: ApplyFirstPreviewProps) {
|
|
106
248
|
const [expandedFiles, setExpandedFiles] = useState<Set<string>>(new Set());
|
|
249
|
+
const [cssPreviewEnabled, setCssPreviewEnabled] = useState(true);
|
|
107
250
|
|
|
108
251
|
// Expand first file by default
|
|
109
252
|
useEffect(() => {
|
|
@@ -111,6 +254,32 @@ export function ApplyFirstPreview({
|
|
|
111
254
|
setExpandedFiles(new Set([session.modifications[0].filePath]));
|
|
112
255
|
}
|
|
113
256
|
}, [session]);
|
|
257
|
+
|
|
258
|
+
// CSS Preview injection for preview mode
|
|
259
|
+
useEffect(() => {
|
|
260
|
+
const isPreviewMode = session.isPreview === true;
|
|
261
|
+
|
|
262
|
+
if (isPreviewMode && cssPreviewEnabled) {
|
|
263
|
+
// Generate and inject preview CSS
|
|
264
|
+
const previewCSS = generatePreviewCSS(session.modifications);
|
|
265
|
+
if (previewCSS) {
|
|
266
|
+
injectPreviewCSS(previewCSS);
|
|
267
|
+
}
|
|
268
|
+
} else {
|
|
269
|
+
// Remove preview CSS when not in preview mode or disabled
|
|
270
|
+
removePreviewCSS();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Cleanup on unmount
|
|
274
|
+
return () => {
|
|
275
|
+
removePreviewCSS();
|
|
276
|
+
};
|
|
277
|
+
}, [session, cssPreviewEnabled]);
|
|
278
|
+
|
|
279
|
+
// Toggle CSS preview
|
|
280
|
+
const toggleCssPreview = useCallback(() => {
|
|
281
|
+
setCssPreviewEnabled(prev => !prev);
|
|
282
|
+
}, []);
|
|
114
283
|
|
|
115
284
|
const toggleFile = (filePath: string) => {
|
|
116
285
|
setExpandedFiles((prev) => {
|
|
@@ -125,26 +294,55 @@ export function ApplyFirstPreview({
|
|
|
125
294
|
};
|
|
126
295
|
|
|
127
296
|
const fileCount = session.modifications.length;
|
|
128
|
-
const isLoading = status === "accepting" || status === "reverting";
|
|
297
|
+
const isLoading = status === "accepting" || status === "reverting" || status === "applying";
|
|
129
298
|
const isStaleSession = fileCount === 0 || status === "error";
|
|
299
|
+
const isPreviewMode = session.isPreview === true;
|
|
130
300
|
|
|
131
301
|
return (
|
|
132
|
-
<div className=
|
|
302
|
+
<div className={cn(
|
|
303
|
+
"space-y-3 p-3 rounded border",
|
|
304
|
+
isPreviewMode
|
|
305
|
+
? "border-blue-300 bg-blue-50"
|
|
306
|
+
: "border-green-300 bg-green-50"
|
|
307
|
+
)}>
|
|
133
308
|
{/* Header */}
|
|
134
309
|
<div className="flex items-center justify-between">
|
|
135
310
|
<div className="flex items-center gap-2">
|
|
136
|
-
<Zap className="h-4 w-4 text-green-600" />
|
|
311
|
+
<Zap className={cn("h-4 w-4", isPreviewMode ? "text-blue-600" : "text-green-600")} />
|
|
137
312
|
<span className="text-xs font-semibold text-gray-900">
|
|
138
|
-
Changes Applied
|
|
313
|
+
{isPreviewMode ? "Proposed Changes" : "Changes Applied"}
|
|
139
314
|
</span>
|
|
140
|
-
<span className=
|
|
315
|
+
<span className={cn(
|
|
316
|
+
"text-[10px] px-1.5 py-0.5 rounded font-medium",
|
|
317
|
+
isPreviewMode
|
|
318
|
+
? "bg-blue-200 text-blue-700"
|
|
319
|
+
: "bg-green-200 text-green-700"
|
|
320
|
+
)}>
|
|
141
321
|
{fileCount} file{fileCount !== 1 ? "s" : ""}
|
|
142
322
|
</span>
|
|
143
323
|
</div>
|
|
144
|
-
<
|
|
324
|
+
<div className="flex items-center gap-2">
|
|
325
|
+
{/* CSS Preview Toggle (only in preview mode) */}
|
|
326
|
+
{isPreviewMode && (
|
|
327
|
+
<button
|
|
328
|
+
onClick={toggleCssPreview}
|
|
329
|
+
className={cn(
|
|
330
|
+
"flex items-center gap-1 text-[10px] px-1.5 py-0.5 rounded transition-colors",
|
|
331
|
+
cssPreviewEnabled
|
|
332
|
+
? "bg-blue-200 text-blue-700"
|
|
333
|
+
: "bg-gray-200 text-gray-500"
|
|
334
|
+
)}
|
|
335
|
+
title={cssPreviewEnabled ? "Hide CSS preview" : "Show CSS preview"}
|
|
336
|
+
>
|
|
337
|
+
{cssPreviewEnabled ? <Eye className="h-3 w-3" /> : <EyeOff className="h-3 w-3" />}
|
|
338
|
+
CSS
|
|
339
|
+
</button>
|
|
340
|
+
)}
|
|
341
|
+
<HMRStatusBadge status={status} />
|
|
342
|
+
</div>
|
|
145
343
|
</div>
|
|
146
344
|
|
|
147
|
-
{/* Info Banner -
|
|
345
|
+
{/* Info Banner - different messages for preview vs applied vs stale */}
|
|
148
346
|
{isStaleSession ? (
|
|
149
347
|
<div className="flex items-start gap-2 p-2 rounded bg-amber-50 border border-amber-200">
|
|
150
348
|
<AlertTriangle className="h-3.5 w-3.5 text-amber-600 mt-0.5 flex-shrink-0" />
|
|
@@ -153,6 +351,14 @@ export function ApplyFirstPreview({
|
|
|
153
351
|
Use "Force Clear" to dismiss this panel.
|
|
154
352
|
</span>
|
|
155
353
|
</div>
|
|
354
|
+
) : isPreviewMode ? (
|
|
355
|
+
<div className="flex items-start gap-2 p-2 rounded bg-blue-50 border border-blue-200">
|
|
356
|
+
<Info className="h-3.5 w-3.5 text-blue-600 mt-0.5 flex-shrink-0" />
|
|
357
|
+
<span className="text-xs text-blue-700">
|
|
358
|
+
<strong>Review the changes below.</strong> Click Accept to apply them to your files,
|
|
359
|
+
or Reject to discard.
|
|
360
|
+
</span>
|
|
361
|
+
</div>
|
|
156
362
|
) : (
|
|
157
363
|
<div className="flex items-start gap-2 p-2 rounded bg-blue-50 border border-blue-200">
|
|
158
364
|
<Info className="h-3.5 w-3.5 text-blue-600 mt-0.5 flex-shrink-0" />
|
|
@@ -175,43 +381,47 @@ export function ApplyFirstPreview({
|
|
|
175
381
|
))}
|
|
176
382
|
</div>
|
|
177
383
|
|
|
178
|
-
{/* Warning about navigation */}
|
|
179
|
-
|
|
180
|
-
<
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
384
|
+
{/* Warning about navigation - only show for applied mode */}
|
|
385
|
+
{!isPreviewMode && (
|
|
386
|
+
<div className="flex items-start gap-2 p-2 rounded bg-amber-50 border border-amber-200">
|
|
387
|
+
<AlertTriangle className="h-3.5 w-3.5 text-amber-600 mt-0.5 flex-shrink-0" />
|
|
388
|
+
<span className="text-xs text-amber-700">
|
|
389
|
+
Navigating away will automatically revert changes. Make sure to accept or revert first.
|
|
390
|
+
</span>
|
|
391
|
+
</div>
|
|
392
|
+
)}
|
|
185
393
|
|
|
186
394
|
{/* Action Buttons */}
|
|
187
395
|
<div className="flex gap-2">
|
|
188
|
-
{/* Accept Button -
|
|
396
|
+
{/* Accept/Apply Button - behavior depends on mode */}
|
|
189
397
|
{!isStaleSession && (
|
|
190
398
|
<button
|
|
191
|
-
onClick={onAccept}
|
|
399
|
+
onClick={isPreviewMode && onApplyPreview ? onApplyPreview : onAccept}
|
|
192
400
|
disabled={isLoading}
|
|
193
401
|
className={cn(
|
|
194
402
|
"flex-1 flex items-center justify-center gap-2 py-2.5",
|
|
195
403
|
"text-xs font-medium rounded transition-colors",
|
|
196
|
-
|
|
404
|
+
isPreviewMode
|
|
405
|
+
? "bg-blue-600 text-white hover:bg-blue-700"
|
|
406
|
+
: "bg-green-600 text-white hover:bg-green-700",
|
|
197
407
|
"disabled:opacity-50 disabled:cursor-not-allowed"
|
|
198
408
|
)}
|
|
199
409
|
>
|
|
200
|
-
{status === "accepting" ? (
|
|
410
|
+
{status === "accepting" || status === "applying" ? (
|
|
201
411
|
<>
|
|
202
412
|
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
|
203
|
-
Accepting...
|
|
413
|
+
{isPreviewMode ? "Applying..." : "Accepting..."}
|
|
204
414
|
</>
|
|
205
415
|
) : (
|
|
206
416
|
<>
|
|
207
417
|
<Check className="h-3.5 w-3.5" />
|
|
208
|
-
Keep Changes
|
|
418
|
+
{isPreviewMode ? "Accept & Apply" : "Keep Changes"}
|
|
209
419
|
</>
|
|
210
420
|
)}
|
|
211
421
|
</button>
|
|
212
422
|
)}
|
|
213
423
|
|
|
214
|
-
{/* Revert Button */}
|
|
424
|
+
{/* Reject/Revert Button */}
|
|
215
425
|
<button
|
|
216
426
|
onClick={onRevert}
|
|
217
427
|
disabled={isLoading}
|
|
@@ -225,12 +435,12 @@ export function ApplyFirstPreview({
|
|
|
225
435
|
{status === "reverting" ? (
|
|
226
436
|
<>
|
|
227
437
|
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
|
228
|
-
Reverting...
|
|
438
|
+
{isPreviewMode ? "Discarding..." : "Reverting..."}
|
|
229
439
|
</>
|
|
230
440
|
) : (
|
|
231
441
|
<>
|
|
232
|
-
<
|
|
233
|
-
Revert
|
|
442
|
+
<X className="h-3.5 w-3.5" />
|
|
443
|
+
{isPreviewMode ? "Reject" : "Revert"}
|
|
234
444
|
</>
|
|
235
445
|
)}
|
|
236
446
|
</button>
|
|
@@ -255,7 +465,7 @@ export function ApplyFirstPreview({
|
|
|
255
465
|
|
|
256
466
|
{/* Session Info (for debugging) */}
|
|
257
467
|
<div className="text-[10px] text-gray-400 font-mono">
|
|
258
|
-
Session: {session.sessionId} | Applied: {new Date(session.appliedAt).toLocaleTimeString()}
|
|
468
|
+
{isPreviewMode ? "Preview" : "Session"}: {session.sessionId} | {isPreviewMode ? "Generated" : "Applied"}: {new Date(session.appliedAt).toLocaleTimeString()}
|
|
259
469
|
</div>
|
|
260
470
|
</div>
|
|
261
471
|
);
|
|
@@ -144,6 +144,7 @@ export function ChatInterface({
|
|
|
144
144
|
method: "POST",
|
|
145
145
|
headers: { "Content-Type": "application/json" },
|
|
146
146
|
body: JSON.stringify({
|
|
147
|
+
// Apply-First: write files immediately so HMR shows changes
|
|
147
148
|
action: useApplyFirst ? "apply" : "edit",
|
|
148
149
|
screenshot,
|
|
149
150
|
pageRoute: window.location.pathname,
|
|
@@ -176,7 +177,7 @@ export function ChatInterface({
|
|
|
176
177
|
|
|
177
178
|
if (data.success && data.modifications) {
|
|
178
179
|
if (useApplyFirst && onApplyFirstComplete) {
|
|
179
|
-
// Apply-First mode: files are already written
|
|
180
|
+
// Apply-First mode: files are already written, user can see changes via HMR
|
|
180
181
|
console.log("[Apply-First] Calling onApplyFirstComplete with:", {
|
|
181
182
|
sessionId: data.sessionId,
|
|
182
183
|
modifications: data.modifications.map((m: { filePath: string }) => m.filePath),
|
|
@@ -50,13 +50,14 @@ export interface ComponentsPanelProps {
|
|
|
50
50
|
visionPendingEdit?: VisionPendingEdit | null;
|
|
51
51
|
onSaveVisionEdit?: () => void;
|
|
52
52
|
onClearVisionPendingEdit?: () => void;
|
|
53
|
-
// Apply-First Mode props
|
|
53
|
+
// Apply-First Mode props (now Preview-First for Cursor-style flow)
|
|
54
54
|
applyFirstSession?: ApplyFirstSession | null;
|
|
55
55
|
applyFirstStatus?: ApplyFirstStatus;
|
|
56
56
|
onApplyFirstComplete?: (session: ApplyFirstSession) => void;
|
|
57
57
|
onApplyFirstAccept?: () => void;
|
|
58
58
|
onApplyFirstRevert?: () => void;
|
|
59
59
|
onApplyFirstForceClear?: () => void;
|
|
60
|
+
onApplyPreview?: () => void; // Apply previewed modifications to files
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
export function ComponentsPanel({
|
|
@@ -97,6 +98,7 @@ export function ComponentsPanel({
|
|
|
97
98
|
onApplyFirstAccept,
|
|
98
99
|
onApplyFirstRevert,
|
|
99
100
|
onApplyFirstForceClear,
|
|
101
|
+
onApplyPreview,
|
|
100
102
|
}: ComponentsPanelProps) {
|
|
101
103
|
// Auto-activate inspector when entering this tab
|
|
102
104
|
useEffect(() => {
|
|
@@ -573,7 +575,7 @@ export function ComponentsPanel({
|
|
|
573
575
|
/>
|
|
574
576
|
)}
|
|
575
577
|
|
|
576
|
-
{/*
|
|
578
|
+
{/* Preview-First Mode - Shows diff, user accepts/rejects before applying */}
|
|
577
579
|
{applyFirstSession && onApplyFirstAccept && onApplyFirstRevert && (
|
|
578
580
|
<ApplyFirstPreview
|
|
579
581
|
session={applyFirstSession}
|
|
@@ -581,6 +583,7 @@ export function ComponentsPanel({
|
|
|
581
583
|
onAccept={onApplyFirstAccept}
|
|
582
584
|
onRevert={onApplyFirstRevert}
|
|
583
585
|
onForceClear={onApplyFirstForceClear}
|
|
586
|
+
onApplyPreview={onApplyPreview}
|
|
584
587
|
/>
|
|
585
588
|
)}
|
|
586
589
|
|
|
@@ -255,28 +255,31 @@ export interface VisionPendingEdit {
|
|
|
255
255
|
explanation: string;
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
-
// ----
|
|
258
|
+
// ---- Preview-First Mode Types (Cursor-style) ----
|
|
259
|
+
// Flow: LLM generates patches -> Show diff preview -> User accepts/rejects -> Apply to files
|
|
259
260
|
|
|
260
261
|
export interface ApplyFirstSession {
|
|
261
262
|
sessionId: string;
|
|
262
263
|
modifications: VisionFileModification[];
|
|
263
264
|
appliedAt: number;
|
|
264
|
-
status: 'applied' | 'accepted' | 'reverted';
|
|
265
|
+
status: 'preview' | 'applied' | 'accepted' | 'reverted';
|
|
265
266
|
backupPaths: string[];
|
|
267
|
+
isPreview?: boolean; // True if changes are not yet written to disk
|
|
266
268
|
}
|
|
267
269
|
|
|
268
270
|
export type ApplyFirstStatus =
|
|
269
271
|
| 'idle' // No active session
|
|
270
272
|
| 'generating' // AI is generating changes
|
|
273
|
+
| 'previewing' // User is reviewing proposed changes (not yet applied)
|
|
271
274
|
| 'applying' // Writing files to disk
|
|
272
275
|
| 'waiting-hmr' // Waiting for HMR to show changes
|
|
273
|
-
| 'reviewing' // User is reviewing actual changes
|
|
276
|
+
| 'reviewing' // User is reviewing actual changes (files written)
|
|
274
277
|
| 'accepting' // User clicked accept, cleaning up backups
|
|
275
278
|
| 'reverting' // User clicked revert, restoring backups
|
|
276
279
|
| 'error'; // Something went wrong
|
|
277
280
|
|
|
278
281
|
export interface ApplyFirstRequest {
|
|
279
|
-
action: "apply" | "accept" | "revert";
|
|
282
|
+
action: "apply" | "preview" | "accept" | "revert";
|
|
280
283
|
sessionId?: string;
|
|
281
284
|
screenshot?: string;
|
|
282
285
|
pageRoute?: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sonance-brand-mcp",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.58",
|
|
4
4
|
"description": "MCP Server for Sonance Brand Guidelines and Component Library - gives Claude instant access to brand colors, typography, and UI components.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|