sonance-brand-mcp 1.3.110 → 1.3.112
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 +30 -7
- package/dist/assets/api/sonance-save-image/route.ts +625 -0
- package/dist/assets/api/sonance-vision-apply/image-styling-detection.ts +1360 -0
- package/dist/assets/api/sonance-vision-apply/route.ts +1020 -64
- package/dist/assets/api/sonance-vision-apply/styling-detection.ts +730 -0
- package/dist/assets/api/sonance-vision-apply/theme-discovery.ts +1 -1
- package/dist/assets/api/sonance-vision-edit/route.ts +33 -8
- package/dist/assets/brand-system.ts +13 -12
- package/dist/assets/components/accordion.tsx +15 -7
- package/dist/assets/components/alert-dialog.tsx +35 -10
- package/dist/assets/components/alert.tsx +11 -10
- package/dist/assets/components/avatar.tsx +4 -4
- package/dist/assets/components/badge.tsx +16 -12
- package/dist/assets/components/button.stories.tsx +3 -3
- package/dist/assets/components/button.tsx +50 -31
- package/dist/assets/components/calendar.tsx +12 -8
- package/dist/assets/components/card.tsx +35 -29
- package/dist/assets/components/checkbox.tsx +9 -8
- package/dist/assets/components/code.tsx +19 -11
- package/dist/assets/components/command.tsx +32 -13
- package/dist/assets/components/context-menu.tsx +37 -16
- package/dist/assets/components/dialog.tsx +8 -5
- package/dist/assets/components/divider.tsx +15 -5
- package/dist/assets/components/drawer.tsx +4 -3
- package/dist/assets/components/dropdown-menu.tsx +15 -13
- package/dist/assets/components/hover-card.tsx +4 -1
- package/dist/assets/components/image.tsx +1 -1
- package/dist/assets/components/input.tsx +29 -14
- package/dist/assets/components/kbd.stories.tsx +3 -3
- package/dist/assets/components/kbd.tsx +29 -13
- package/dist/assets/components/listbox.tsx +8 -8
- package/dist/assets/components/menubar.tsx +50 -23
- package/dist/assets/components/navbar.stories.tsx +140 -13
- package/dist/assets/components/navbar.tsx +22 -5
- package/dist/assets/components/navigation-menu.tsx +28 -6
- package/dist/assets/components/pagination.tsx +10 -10
- package/dist/assets/components/popover.tsx +10 -8
- package/dist/assets/components/progress.tsx +6 -4
- package/dist/assets/components/radio-group.tsx +5 -5
- package/dist/assets/components/select.tsx +49 -29
- package/dist/assets/components/separator.tsx +3 -3
- package/dist/assets/components/sheet.tsx +4 -4
- package/dist/assets/components/sidebar.tsx +10 -10
- package/dist/assets/components/skeleton.tsx +13 -5
- package/dist/assets/components/slider.tsx +12 -10
- package/dist/assets/components/switch.tsx +4 -4
- package/dist/assets/components/table.tsx +5 -5
- package/dist/assets/components/tabs.tsx +8 -8
- package/dist/assets/components/textarea.tsx +11 -9
- package/dist/assets/components/toast.tsx +7 -7
- package/dist/assets/components/toggle.tsx +27 -7
- package/dist/assets/components/tooltip.tsx +10 -8
- package/dist/assets/components/user.tsx +8 -6
- package/dist/assets/dev-tools/SonanceDevTools.tsx +851 -708
- package/dist/assets/dev-tools/components/ApplyFirstPreview.tsx +10 -10
- package/dist/assets/dev-tools/components/ChatHistory.tsx +145 -0
- package/dist/assets/dev-tools/components/ChatInterface.tsx +444 -295
- package/dist/assets/dev-tools/components/ChatTabBar.tsx +82 -0
- package/dist/assets/dev-tools/components/DiffPreview.tsx +1 -1
- package/dist/assets/dev-tools/components/InlineDiffPreview.tsx +528 -0
- package/dist/assets/dev-tools/components/InspectorOverlay.tsx +21 -18
- package/dist/assets/dev-tools/components/PropertiesPanel.tsx +1345 -0
- package/dist/assets/dev-tools/components/ScreenshotAnnotator.tsx +1 -1
- package/dist/assets/dev-tools/components/SectionHighlight.tsx +1 -1
- package/dist/assets/dev-tools/components/VisionDiffPreview.tsx +7 -7
- package/dist/assets/dev-tools/components/VisionModeBorder.tsx +12 -63
- package/dist/assets/dev-tools/constants.ts +38 -6
- package/dist/assets/dev-tools/hooks/index.ts +69 -0
- package/dist/assets/dev-tools/hooks/useComponentDetection.ts +132 -0
- package/dist/assets/dev-tools/hooks/useComputedStyles.ts +471 -0
- package/dist/assets/dev-tools/hooks/useContentHash.ts +212 -0
- package/dist/assets/dev-tools/hooks/useElementScanner.ts +398 -0
- package/dist/assets/dev-tools/hooks/useImageDetection.ts +162 -0
- package/dist/assets/dev-tools/hooks/useTextDetection.ts +217 -0
- package/dist/assets/dev-tools/index.ts +3 -0
- package/dist/assets/dev-tools/panels/AnalysisPanel.tsx +32 -32
- package/dist/assets/dev-tools/panels/ComponentsPanel.tsx +384 -131
- package/dist/assets/dev-tools/panels/TextPanel.tsx +10 -10
- package/dist/assets/dev-tools/types.ts +93 -2
- package/dist/assets/globals.css +225 -9
- package/dist/assets/styles/brand-overrides.css +3 -2
- package/dist/assets/utils.ts +2 -1
- package/dist/index.js +22 -3
- package/package.json +2 -1
|
@@ -4,6 +4,7 @@ import * as path from "path";
|
|
|
4
4
|
import Anthropic from "@anthropic-ai/sdk";
|
|
5
5
|
import { randomUUID } from "crypto";
|
|
6
6
|
import { discoverTheme, formatThemeForPrompt } from "./theme-discovery";
|
|
7
|
+
import { detectStylingSystem, generateStylingGuidance, type StylingAnalysis } from "./styling-detection";
|
|
7
8
|
import * as babelParser from "@babel/parser";
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -54,6 +55,8 @@ interface VisionFocusedElement {
|
|
|
54
55
|
childIds?: string[];
|
|
55
56
|
/** Parent section context for section-level changes */
|
|
56
57
|
parentSection?: ParentSectionInfo;
|
|
58
|
+
/** The src attribute for image elements (for tracing to source code) */
|
|
59
|
+
imageSrc?: string;
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
interface VisionFileModification {
|
|
@@ -64,6 +67,11 @@ interface VisionFileModification {
|
|
|
64
67
|
explanation: string;
|
|
65
68
|
}
|
|
66
69
|
|
|
70
|
+
interface ChatHistoryMessage {
|
|
71
|
+
role: string;
|
|
72
|
+
content: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
67
75
|
interface ApplyFirstRequest {
|
|
68
76
|
action: "apply" | "accept" | "revert" | "preview";
|
|
69
77
|
sessionId?: string;
|
|
@@ -73,6 +81,29 @@ interface ApplyFirstRequest {
|
|
|
73
81
|
focusedElements?: VisionFocusedElement[];
|
|
74
82
|
// For applying a previously previewed set of modifications
|
|
75
83
|
previewedModifications?: VisionFileModification[];
|
|
84
|
+
/** Conversation history for multi-turn context */
|
|
85
|
+
chatHistory?: ChatHistoryMessage[];
|
|
86
|
+
/** Current theme mode (light/dark) for theme-specific color changes */
|
|
87
|
+
currentTheme?: "light" | "dark";
|
|
88
|
+
/** Property edits from the PropertiesPanel (includes colorLight/colorDark) */
|
|
89
|
+
propertyEdits?: {
|
|
90
|
+
textContent?: string;
|
|
91
|
+
width?: string;
|
|
92
|
+
height?: string;
|
|
93
|
+
opacity?: string;
|
|
94
|
+
borderRadius?: string;
|
|
95
|
+
fontSize?: string;
|
|
96
|
+
fontWeight?: string;
|
|
97
|
+
lineHeight?: string;
|
|
98
|
+
letterSpacing?: string;
|
|
99
|
+
color?: string;
|
|
100
|
+
colorLight?: string;
|
|
101
|
+
colorDark?: string;
|
|
102
|
+
backgroundColor?: string;
|
|
103
|
+
padding?: string;
|
|
104
|
+
margin?: string;
|
|
105
|
+
gap?: string;
|
|
106
|
+
};
|
|
76
107
|
}
|
|
77
108
|
|
|
78
109
|
interface BackupManifest {
|
|
@@ -144,9 +175,10 @@ function debugLog(message: string, data?: unknown) {
|
|
|
144
175
|
/**
|
|
145
176
|
* Sanitize a JSON string by finding the correct end point using bracket balancing.
|
|
146
177
|
* Handles cases where LLM outputs trailing garbage like extra ]} characters.
|
|
178
|
+
* Also handles leading conversational text before the JSON payload.
|
|
147
179
|
*/
|
|
148
180
|
function sanitizeJsonString(text: string): string {
|
|
149
|
-
|
|
181
|
+
let trimmed = text.trim();
|
|
150
182
|
|
|
151
183
|
debugLog("[sanitizeJsonString] Starting", {
|
|
152
184
|
inputLength: trimmed.length,
|
|
@@ -154,6 +186,21 @@ function sanitizeJsonString(text: string): string {
|
|
|
154
186
|
last100: trimmed.substring(trimmed.length - 100)
|
|
155
187
|
});
|
|
156
188
|
|
|
189
|
+
// Skip leading conversational preamble by finding the first '{'
|
|
190
|
+
// This handles cases like "Looking at line 76... { ... }"
|
|
191
|
+
const firstBrace = trimmed.indexOf('{');
|
|
192
|
+
if (firstBrace > 0) {
|
|
193
|
+
const preamble = trimmed.substring(0, firstBrace);
|
|
194
|
+
debugLog("[sanitizeJsonString] Found preamble, skipping", {
|
|
195
|
+
preambleLength: firstBrace,
|
|
196
|
+
preamblePreview: preamble.substring(0, 100)
|
|
197
|
+
});
|
|
198
|
+
trimmed = trimmed.substring(firstBrace);
|
|
199
|
+
} else if (firstBrace === -1) {
|
|
200
|
+
debugLog("[sanitizeJsonString] No JSON object found in text");
|
|
201
|
+
return trimmed; // Return as-is if no { found
|
|
202
|
+
}
|
|
203
|
+
|
|
157
204
|
// Try parsing as-is first
|
|
158
205
|
try {
|
|
159
206
|
JSON.parse(trimmed);
|
|
@@ -509,26 +556,31 @@ function findElementLineInFile(
|
|
|
509
556
|
|
|
510
557
|
// PRIORITY 2c: Word-based matching for text with special characters (bullets, etc.)
|
|
511
558
|
// Extract significant words and find lines containing multiple of them
|
|
512
|
-
|
|
513
|
-
|
|
559
|
+
// IMPROVED: Lower threshold to 3-char words and flexible match requirements
|
|
560
|
+
if (normalizedText.length > 8) {
|
|
561
|
+
const commonWords = ['the', 'this', 'that', 'with', 'from', 'have', 'will', 'your', 'for', 'and', 'use', 'are', 'you', 'can', 'its', 'not', 'but', 'all', 'was', 'has', 'any'];
|
|
514
562
|
const significantWords = normalizedText.split(' ')
|
|
515
|
-
.filter(word => word.length >=
|
|
516
|
-
.slice(0,
|
|
517
|
-
|
|
518
|
-
|
|
563
|
+
.filter(word => word.length >= 3 && !commonWords.includes(word.toLowerCase()))
|
|
564
|
+
.slice(0, 8); // Take up to 8 significant words for longer text
|
|
565
|
+
|
|
566
|
+
// Flexible threshold: require fewer matches for longer text
|
|
567
|
+
const minSignificantWords = normalizedText.length > 50 ? 1 : 2;
|
|
568
|
+
const minMatches = normalizedText.length > 50 ? 1 : 2;
|
|
569
|
+
|
|
570
|
+
if (significantWords.length >= minSignificantWords) {
|
|
519
571
|
for (let i = 0; i < lines.length; i++) {
|
|
520
572
|
const lineLower = lines[i].toLowerCase();
|
|
521
|
-
const matchCount = significantWords.filter(word =>
|
|
573
|
+
const matchCount = significantWords.filter(word =>
|
|
522
574
|
lineLower.includes(word.toLowerCase())
|
|
523
575
|
).length;
|
|
524
|
-
|
|
525
|
-
//
|
|
526
|
-
if (matchCount >=
|
|
576
|
+
|
|
577
|
+
// Flexible matching: longer text can match with fewer words
|
|
578
|
+
if (matchCount >= minMatches) {
|
|
527
579
|
return {
|
|
528
580
|
lineNumber: i + 1,
|
|
529
581
|
snippet: lines.slice(Math.max(0, i - 3), i + 5).join('\n'),
|
|
530
|
-
confidence: 'medium',
|
|
531
|
-
matchedBy: `word match: ${significantWords.slice(0, 3).join(', ')}... (${matchCount} words)`
|
|
582
|
+
confidence: matchCount >= 3 ? 'medium' : 'low',
|
|
583
|
+
matchedBy: `word match: ${significantWords.slice(0, 3).join(', ')}... (${matchCount}/${significantWords.length} words)`
|
|
532
584
|
};
|
|
533
585
|
}
|
|
534
586
|
}
|
|
@@ -554,6 +606,70 @@ function findElementLineInFile(
|
|
|
554
606
|
}
|
|
555
607
|
}
|
|
556
608
|
|
|
609
|
+
// PRIORITY 2c: Image source matching (for logo/image elements)
|
|
610
|
+
if (focusedElement.type === 'logo') {
|
|
611
|
+
// If we have imageSrc, try to match it
|
|
612
|
+
if (focusedElement.imageSrc) {
|
|
613
|
+
// Clean the image src - remove Next.js image optimization params
|
|
614
|
+
let cleanSrc = focusedElement.imageSrc;
|
|
615
|
+
if (cleanSrc.includes("/_next/image")) {
|
|
616
|
+
const urlMatch = cleanSrc.match(/url=([^&]+)/);
|
|
617
|
+
if (urlMatch) {
|
|
618
|
+
cleanSrc = decodeURIComponent(urlMatch[1]);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Extract just the filename for flexible matching
|
|
623
|
+
const filename = cleanSrc.split('/').pop()?.replace(/\.[^.]+$/, '') || '';
|
|
624
|
+
const fullPath = cleanSrc.startsWith('/') ? cleanSrc : `/${cleanSrc}`;
|
|
625
|
+
|
|
626
|
+
// Try exact path match first
|
|
627
|
+
for (let i = 0; i < lines.length; i++) {
|
|
628
|
+
if (lines[i].includes(fullPath) || lines[i].includes(cleanSrc)) {
|
|
629
|
+
return {
|
|
630
|
+
lineNumber: i + 1,
|
|
631
|
+
snippet: lines.slice(Math.max(0, i - 3), i + 5).join('\n'),
|
|
632
|
+
confidence: 'high',
|
|
633
|
+
matchedBy: `image src="${cleanSrc}"`
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Try filename match (handles dynamic sources like brandLogos)
|
|
639
|
+
if (filename && filename.length > 5) {
|
|
640
|
+
for (let i = 0; i < lines.length; i++) {
|
|
641
|
+
if (lines[i].includes(filename)) {
|
|
642
|
+
return {
|
|
643
|
+
lineNumber: i + 1,
|
|
644
|
+
snippet: lines.slice(Math.max(0, i - 3), i + 5).join('\n'),
|
|
645
|
+
confidence: 'medium',
|
|
646
|
+
matchedBy: `image filename="${filename}"`
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Always try to match the element name (which is usually derived from the image filename)
|
|
654
|
+
// This works even without imageSrc since the name is captured from alt text or data attributes
|
|
655
|
+
if (focusedElement.name && focusedElement.name.length > 5) {
|
|
656
|
+
// Skip generic names
|
|
657
|
+
const genericNames = ['Logo', 'Image', 'Img', 'Picture', 'Photo', 'Icon'];
|
|
658
|
+
if (!genericNames.includes(focusedElement.name)) {
|
|
659
|
+
for (let i = 0; i < lines.length; i++) {
|
|
660
|
+
if (lines[i].includes(focusedElement.name)) {
|
|
661
|
+
return {
|
|
662
|
+
lineNumber: i + 1,
|
|
663
|
+
snippet: lines.slice(Math.max(0, i - 3), i + 5).join('\n'),
|
|
664
|
+
confidence: 'medium',
|
|
665
|
+
matchedBy: `image name="${focusedElement.name}"`
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
557
673
|
// PRIORITY 3: Distinctive className patterns (semantic classes from design system)
|
|
558
674
|
if (focusedElement.className) {
|
|
559
675
|
// SEMANTIC CLASS DETECTION: Instead of filtering OUT utilities, filter IN semantics
|
|
@@ -669,10 +785,224 @@ function findElementLineInFile(
|
|
|
669
785
|
}
|
|
670
786
|
}
|
|
671
787
|
}
|
|
672
|
-
|
|
788
|
+
|
|
789
|
+
// FALLBACK 3: Multi-line JSX text parsing
|
|
790
|
+
// This handles cases where text spans multiple lines in JSX
|
|
791
|
+
if (focusedElement.textContent && focusedElement.textContent.trim().length > 5) {
|
|
792
|
+
debugLog("Trying multi-line JSX parsing", { text: focusedElement.textContent.substring(0, 50) });
|
|
793
|
+
const multilineMatch = findTextInMultilineJSX(fileContent, focusedElement.textContent);
|
|
794
|
+
if (multilineMatch) {
|
|
795
|
+
debugLog("Multi-line JSX match found", multilineMatch);
|
|
796
|
+
return multilineMatch;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
return null;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Multi-line JSX text parser
|
|
805
|
+
*
|
|
806
|
+
* Handles text that spans multiple lines in JSX:
|
|
807
|
+
* ```jsx
|
|
808
|
+
* <p className="text-base">
|
|
809
|
+
* A list of selectable items displayed in a dropdown menu
|
|
810
|
+
* </p>
|
|
811
|
+
* ```
|
|
812
|
+
*
|
|
813
|
+
* Returns the starting line number of the element if found.
|
|
814
|
+
*/
|
|
815
|
+
function findTextInMultilineJSX(
|
|
816
|
+
fileContent: string,
|
|
817
|
+
searchText: string,
|
|
818
|
+
tagName?: string
|
|
819
|
+
): { lineNumber: number; snippet: string; confidence: 'high' | 'medium'; matchedBy: string } | null {
|
|
820
|
+
if (!fileContent || !searchText) return null;
|
|
821
|
+
|
|
822
|
+
const lines = fileContent.split('\n');
|
|
823
|
+
const normalizedSearch = searchText.trim().replace(/\s+/g, ' ').toLowerCase();
|
|
824
|
+
|
|
825
|
+
// Skip if text is too short
|
|
826
|
+
if (normalizedSearch.length < 5) return null;
|
|
827
|
+
|
|
828
|
+
// Look for JSX elements that might contain this text
|
|
829
|
+
// Strategy: Find opening tags and then look at subsequent lines for text content
|
|
830
|
+
const textTags = tagName ? [tagName] : ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'span', 'div', 'label', 'a', 'li', 'blockquote', 'figcaption'];
|
|
831
|
+
|
|
832
|
+
for (let i = 0; i < lines.length; i++) {
|
|
833
|
+
const line = lines[i];
|
|
834
|
+
|
|
835
|
+
// Check if this line opens a text element
|
|
836
|
+
for (const tag of textTags) {
|
|
837
|
+
const tagPattern = new RegExp(`<${tag}(?:\\s|>|$)`, 'i');
|
|
838
|
+
if (!tagPattern.test(line)) continue;
|
|
839
|
+
|
|
840
|
+
// Found a potential opening tag - collect text until closing tag
|
|
841
|
+
let collectedText = '';
|
|
842
|
+
let endLineIndex = i;
|
|
843
|
+
const closingTag = `</${tag}>`;
|
|
844
|
+
|
|
845
|
+
// Collect text from this line and subsequent lines
|
|
846
|
+
for (let j = i; j < Math.min(i + 10, lines.length); j++) {
|
|
847
|
+
const currentLine = lines[j];
|
|
848
|
+
|
|
849
|
+
// Add text content (strip JSX tags and props)
|
|
850
|
+
const textOnly = currentLine
|
|
851
|
+
.replace(/<[^>]+>/g, ' ') // Remove all tags
|
|
852
|
+
.replace(/\{[^}]*\}/g, '') // Remove JSX expressions
|
|
853
|
+
.replace(/className=["'][^"']*["']/g, '')
|
|
854
|
+
.trim();
|
|
855
|
+
|
|
856
|
+
if (textOnly) {
|
|
857
|
+
collectedText += ' ' + textOnly;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Check if we found the closing tag
|
|
861
|
+
if (currentLine.includes(closingTag) || (j > i && /<\/\w+>/.test(currentLine))) {
|
|
862
|
+
endLineIndex = j;
|
|
863
|
+
break;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// Normalize collected text and compare
|
|
868
|
+
const normalizedCollected = collectedText.trim().replace(/\s+/g, ' ').toLowerCase();
|
|
869
|
+
|
|
870
|
+
// Check for match - allow partial matching for long text
|
|
871
|
+
if (normalizedCollected.includes(normalizedSearch) || normalizedSearch.includes(normalizedCollected)) {
|
|
872
|
+
return {
|
|
873
|
+
lineNumber: i + 1,
|
|
874
|
+
snippet: lines.slice(Math.max(0, i - 2), endLineIndex + 3).join('\n'),
|
|
875
|
+
confidence: normalizedCollected === normalizedSearch ? 'high' : 'medium',
|
|
876
|
+
matchedBy: `multi-line JSX <${tag}> text content`
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// Also check word overlap for fuzzy matching
|
|
881
|
+
const searchWords = normalizedSearch.split(' ').filter(w => w.length >= 3);
|
|
882
|
+
const collectedWords = normalizedCollected.split(' ').filter(w => w.length >= 3);
|
|
883
|
+
|
|
884
|
+
if (searchWords.length >= 3 && collectedWords.length >= 3) {
|
|
885
|
+
const matchingWords = searchWords.filter(w => collectedWords.some(cw => cw.includes(w) || w.includes(cw)));
|
|
886
|
+
const matchRatio = matchingWords.length / searchWords.length;
|
|
887
|
+
|
|
888
|
+
// Require at least 60% word match
|
|
889
|
+
if (matchRatio >= 0.6) {
|
|
890
|
+
return {
|
|
891
|
+
lineNumber: i + 1,
|
|
892
|
+
snippet: lines.slice(Math.max(0, i - 2), endLineIndex + 3).join('\n'),
|
|
893
|
+
confidence: 'medium',
|
|
894
|
+
matchedBy: `multi-line JSX fuzzy match (${Math.round(matchRatio * 100)}% word overlap)`
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
673
901
|
return null;
|
|
674
902
|
}
|
|
675
903
|
|
|
904
|
+
/**
|
|
905
|
+
* AI-assisted element location
|
|
906
|
+
*
|
|
907
|
+
* When regex-based matching fails, use a focused AI call to locate the element.
|
|
908
|
+
* This works for ANY codebase structure since it understands code semantics.
|
|
909
|
+
*/
|
|
910
|
+
async function aiAssistedElementLocation(
|
|
911
|
+
fileContent: string,
|
|
912
|
+
element: VisionFocusedElement,
|
|
913
|
+
filePath: string,
|
|
914
|
+
apiKey: string
|
|
915
|
+
): Promise<{ lineNumber: number; snippet: string; confidence: 'high' | 'medium'; matchedBy: string } | null> {
|
|
916
|
+
debugLog("Trying AI-assisted element location", {
|
|
917
|
+
elementType: element.type,
|
|
918
|
+
textContent: element.textContent?.substring(0, 50),
|
|
919
|
+
file: filePath
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
// Build a focused prompt for element location
|
|
923
|
+
const elementInfo = [
|
|
924
|
+
element.textContent ? `Text content: "${element.textContent}"` : null,
|
|
925
|
+
element.type ? `Element type: ${element.type}` : null,
|
|
926
|
+
element.className ? `CSS classes: ${element.className.substring(0, 100)}` : null,
|
|
927
|
+
element.elementId ? `DOM id: ${element.elementId}` : null,
|
|
928
|
+
element.name ? `Name: ${element.name}` : null,
|
|
929
|
+
].filter(Boolean).join('\n');
|
|
930
|
+
|
|
931
|
+
// Add line numbers to file content for reference
|
|
932
|
+
const lines = fileContent.split('\n');
|
|
933
|
+
const numberedContent = lines.map((line, i) => `${i + 1}| ${line}`).join('\n');
|
|
934
|
+
|
|
935
|
+
// Keep file content reasonable (first 500 lines or 20KB)
|
|
936
|
+
const maxLines = 500;
|
|
937
|
+
const maxChars = 20000;
|
|
938
|
+
let truncatedContent = numberedContent;
|
|
939
|
+
if (lines.length > maxLines || numberedContent.length > maxChars) {
|
|
940
|
+
truncatedContent = lines.slice(0, maxLines).map((line, i) => `${i + 1}| ${line}`).join('\n');
|
|
941
|
+
truncatedContent += '\n... (file truncated for analysis)';
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
const prompt = `You are a code analyzer. Find the LINE NUMBER where a specific UI element is defined in this React/JSX file.
|
|
945
|
+
|
|
946
|
+
ELEMENT TO FIND:
|
|
947
|
+
${elementInfo}
|
|
948
|
+
|
|
949
|
+
FILE CONTENT (with line numbers):
|
|
950
|
+
\`\`\`tsx
|
|
951
|
+
${truncatedContent}
|
|
952
|
+
\`\`\`
|
|
953
|
+
|
|
954
|
+
INSTRUCTIONS:
|
|
955
|
+
1. Find where this element is defined in the JSX
|
|
956
|
+
2. Look for matching text content, className, or element type
|
|
957
|
+
3. Return ONLY a JSON object with the line number
|
|
958
|
+
|
|
959
|
+
Respond with ONLY valid JSON, no explanation:
|
|
960
|
+
{"lineNumber": <number>, "confidence": "high" | "medium"}
|
|
961
|
+
|
|
962
|
+
If you cannot find the element, respond with:
|
|
963
|
+
{"lineNumber": null}`;
|
|
964
|
+
|
|
965
|
+
try {
|
|
966
|
+
const anthropic = new Anthropic({ apiKey });
|
|
967
|
+
const response = await anthropic.messages.create({
|
|
968
|
+
model: "claude-sonnet-4-20250514",
|
|
969
|
+
max_tokens: 100,
|
|
970
|
+
messages: [{ role: "user", content: prompt }]
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
const responseText = response.content[0].type === 'text' ? response.content[0].text : '';
|
|
974
|
+
debugLog("AI element location response", { raw: responseText });
|
|
975
|
+
|
|
976
|
+
// Parse the JSON response
|
|
977
|
+
const jsonMatch = responseText.match(/\{[^}]+\}/);
|
|
978
|
+
if (!jsonMatch) {
|
|
979
|
+
debugLog("AI response not valid JSON");
|
|
980
|
+
return null;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
const result = JSON.parse(jsonMatch[0]);
|
|
984
|
+
if (!result.lineNumber || typeof result.lineNumber !== 'number') {
|
|
985
|
+
debugLog("AI could not locate element");
|
|
986
|
+
return null;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
const lineNum = result.lineNumber;
|
|
990
|
+
const confidence = result.confidence === 'high' ? 'high' : 'medium';
|
|
991
|
+
|
|
992
|
+
debugLog("AI located element successfully", { lineNumber: lineNum, confidence });
|
|
993
|
+
|
|
994
|
+
return {
|
|
995
|
+
lineNumber: lineNum,
|
|
996
|
+
snippet: lines.slice(Math.max(0, lineNum - 4), Math.min(lines.length, lineNum + 5)).join('\n'),
|
|
997
|
+
confidence,
|
|
998
|
+
matchedBy: `AI-assisted location (${confidence} confidence)`
|
|
999
|
+
};
|
|
1000
|
+
} catch (error) {
|
|
1001
|
+
debugLog("AI element location failed", { error: String(error) });
|
|
1002
|
+
return null;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
676
1006
|
/**
|
|
677
1007
|
* Search imported component files for specific TEXT CONTENT
|
|
678
1008
|
* This is used to redirect from parent components to child components
|
|
@@ -814,15 +1144,29 @@ function findElementInImportedFiles(
|
|
|
814
1144
|
file.path.endsWith('.tsx') ||
|
|
815
1145
|
file.path.endsWith('.jsx');
|
|
816
1146
|
|
|
817
|
-
//
|
|
818
|
-
|
|
1147
|
+
// For logo elements, also search config files that may contain image paths
|
|
1148
|
+
const isConfigFile = focusedElement.type === 'logo' && (
|
|
1149
|
+
file.path.includes('brand') ||
|
|
1150
|
+
file.path.includes('config') ||
|
|
1151
|
+
file.path.includes('constants') ||
|
|
1152
|
+
file.path.includes('assets') ||
|
|
1153
|
+
file.path.includes('images') ||
|
|
1154
|
+
file.path.includes('theme')
|
|
1155
|
+
);
|
|
1156
|
+
|
|
1157
|
+
// Skip non-component files but allow page files and config files for logos
|
|
1158
|
+
if (!isComponentFile && !isConfigFile) continue;
|
|
819
1159
|
|
|
820
|
-
// Skip known non-UI files
|
|
821
|
-
|
|
1160
|
+
// Skip known non-UI files (but allow config files for logos)
|
|
1161
|
+
const isSkippable = file.path.includes('/types') ||
|
|
822
1162
|
file.path.includes('/hooks/') ||
|
|
823
|
-
file.path.includes('
|
|
824
|
-
|
|
825
|
-
|
|
1163
|
+
file.path.includes('.d.ts');
|
|
1164
|
+
|
|
1165
|
+
// For non-logo elements, also skip utils and lib
|
|
1166
|
+
const isUtilFile = file.path.includes('/utils/') || file.path.includes('/lib/');
|
|
1167
|
+
|
|
1168
|
+
if (isSkippable) continue;
|
|
1169
|
+
if (isUtilFile && focusedElement.type !== 'logo') continue;
|
|
826
1170
|
|
|
827
1171
|
const result = findElementLineInFile(file.content, focusedElement);
|
|
828
1172
|
// Accept ALL matches including low confidence - let the scoring decide
|
|
@@ -1715,7 +2059,8 @@ function searchFilesSmart(
|
|
|
1715
2059
|
return sortedResults.map(r => ({ path: r.path, content: r.content, score: r.score, filenameMatch: r.filenameMatch }));
|
|
1716
2060
|
}
|
|
1717
2061
|
|
|
1718
|
-
|
|
2062
|
+
// Base system prompt - styling guidance is added dynamically based on codebase detection
|
|
2063
|
+
const VISION_SYSTEM_PROMPT_BASE = `You are an expert frontend developer. Edit the code to fulfill the user's request.
|
|
1719
2064
|
|
|
1720
2065
|
CRITICAL: Return ONLY the JSON object. No explanations, no preamble, no markdown code fences.
|
|
1721
2066
|
Start your response with { and end with }
|
|
@@ -1725,6 +2070,54 @@ Output format:
|
|
|
1725
2070
|
|
|
1726
2071
|
The "search" field must match the file EXACTLY (copy-paste from the code provided).`;
|
|
1727
2072
|
|
|
2073
|
+
// Cache for styling analysis to avoid re-detecting on each request
|
|
2074
|
+
let cachedStylingAnalysis: StylingAnalysis | null = null;
|
|
2075
|
+
let cachedStylingGuidance: string | null = null;
|
|
2076
|
+
let cacheTimestamp: number = 0;
|
|
2077
|
+
const CACHE_TTL = 60000; // 1 minute cache
|
|
2078
|
+
|
|
2079
|
+
/**
|
|
2080
|
+
* Build the complete system prompt with dynamic styling guidance
|
|
2081
|
+
*/
|
|
2082
|
+
async function buildVisionSystemPrompt(projectRoot: string): Promise<string> {
|
|
2083
|
+
const now = Date.now();
|
|
2084
|
+
|
|
2085
|
+
// Use cache if available and fresh
|
|
2086
|
+
if (cachedStylingGuidance && (now - cacheTimestamp) < CACHE_TTL) {
|
|
2087
|
+
return VISION_SYSTEM_PROMPT_BASE + "\n\n" + cachedStylingGuidance;
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
// Detect styling system
|
|
2091
|
+
try {
|
|
2092
|
+
cachedStylingAnalysis = await detectStylingSystem(projectRoot);
|
|
2093
|
+
cachedStylingGuidance = generateStylingGuidance(cachedStylingAnalysis);
|
|
2094
|
+
cacheTimestamp = now;
|
|
2095
|
+
|
|
2096
|
+
debugLog("Styling system detected", {
|
|
2097
|
+
primary: cachedStylingAnalysis.primarySystem,
|
|
2098
|
+
secondary: cachedStylingAnalysis.secondarySystems,
|
|
2099
|
+
libraries: cachedStylingAnalysis.componentLibraries,
|
|
2100
|
+
confidence: cachedStylingAnalysis.confidence,
|
|
2101
|
+
hasClassMerging: cachedStylingAnalysis.hasClassMerging,
|
|
2102
|
+
});
|
|
2103
|
+
} catch (error) {
|
|
2104
|
+
debugLog("Styling detection failed, using generic guidance", { error });
|
|
2105
|
+
cachedStylingGuidance = generateStylingGuidance({
|
|
2106
|
+
primarySystem: "unknown",
|
|
2107
|
+
secondarySystems: [],
|
|
2108
|
+
componentLibraries: [],
|
|
2109
|
+
hasClassMerging: false,
|
|
2110
|
+
hasCSSVariables: false,
|
|
2111
|
+
hasDesignTokens: false,
|
|
2112
|
+
confidence: 0,
|
|
2113
|
+
evidence: { files: [], patterns: [] },
|
|
2114
|
+
});
|
|
2115
|
+
cacheTimestamp = now;
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
return VISION_SYSTEM_PROMPT_BASE + "\n\n" + cachedStylingGuidance;
|
|
2119
|
+
}
|
|
2120
|
+
|
|
1728
2121
|
/**
|
|
1729
2122
|
* PHASE 2: Targeted Patch Generation Prompt
|
|
1730
2123
|
*
|
|
@@ -1824,6 +2217,129 @@ DESIGN CONSTRAINTS
|
|
|
1824
2217
|
═══════════════════════════════════════════════════════════════════════════════
|
|
1825
2218
|
`;
|
|
1826
2219
|
|
|
2220
|
+
/**
|
|
2221
|
+
* Modification Intent Types
|
|
2222
|
+
* - 'instance': User wants to modify specific instances (e.g., "make THESE cards sharp")
|
|
2223
|
+
* - 'component': User wants to modify the component definition (e.g., "make ALL cards sharp")
|
|
2224
|
+
* - 'unknown': Cannot determine intent, default to instance behavior
|
|
2225
|
+
*/
|
|
2226
|
+
type ModificationIntent = 'instance' | 'component' | 'unknown';
|
|
2227
|
+
|
|
2228
|
+
/**
|
|
2229
|
+
* Analyze user prompt to determine modification scope
|
|
2230
|
+
*
|
|
2231
|
+
* This helps the AI understand whether to:
|
|
2232
|
+
* - Modify instances in the page file (instance)
|
|
2233
|
+
* - Modify the component definition file (component)
|
|
2234
|
+
*
|
|
2235
|
+
* Examples:
|
|
2236
|
+
* - "make THESE cards sharp" → instance (modify page file)
|
|
2237
|
+
* - "make ALL cards sharp" → component (modify Card.tsx)
|
|
2238
|
+
* - "change the Card component" → component (modify Card.tsx)
|
|
2239
|
+
* - "give this button a red background" → instance (modify page file)
|
|
2240
|
+
*/
|
|
2241
|
+
function analyzeModificationIntent(prompt: string): {
|
|
2242
|
+
intent: ModificationIntent;
|
|
2243
|
+
reason: string;
|
|
2244
|
+
componentName?: string; // e.g., "Card", "Button" - extracted from prompt
|
|
2245
|
+
} {
|
|
2246
|
+
const promptLower = prompt.toLowerCase();
|
|
2247
|
+
|
|
2248
|
+
// Component-level patterns (modify the component definition)
|
|
2249
|
+
const componentPatterns = [
|
|
2250
|
+
{ pattern: /\b(all|every|each)\s+(cards?|buttons?|inputs?|dialogs?|modals?)/i, reason: 'Plural "all/every" indicates global change' },
|
|
2251
|
+
{ pattern: /\bthe\s+(card|button|input|dialog|modal)\s+component/i, reason: 'Explicit component reference' },
|
|
2252
|
+
{ pattern: /\bcomponent\s+(default|definition|style)/i, reason: 'Explicit component modification' },
|
|
2253
|
+
{ pattern: /\bglobally?\b/i, reason: 'Global keyword detected' },
|
|
2254
|
+
{ pattern: /\bdefault\s+(style|behavior|look)/i, reason: 'Default modification requested' },
|
|
2255
|
+
{ pattern: /\beverywhere\b/i, reason: 'Everywhere keyword detected' },
|
|
2256
|
+
];
|
|
2257
|
+
|
|
2258
|
+
// Instance-level patterns (modify specific instances)
|
|
2259
|
+
const instancePatterns = [
|
|
2260
|
+
{ pattern: /\b(this|these|that|those)\s+\w+/i, reason: 'Demonstrative pronoun indicates specific instance' },
|
|
2261
|
+
{ pattern: /\bthe\s+\w+\s+(here|on this page|in this section)/i, reason: 'Location-specific reference' },
|
|
2262
|
+
{ pattern: /\bjust\s+(this|the)/i, reason: 'Restrictive "just" indicates specific instance' },
|
|
2263
|
+
{ pattern: /\bonly\s+(this|the|these)/i, reason: 'Restrictive "only" indicates specific instance' },
|
|
2264
|
+
];
|
|
2265
|
+
|
|
2266
|
+
// Check for component patterns first (they're more specific)
|
|
2267
|
+
for (const { pattern, reason } of componentPatterns) {
|
|
2268
|
+
const match = promptLower.match(pattern);
|
|
2269
|
+
if (match) {
|
|
2270
|
+
// Extract component name if present
|
|
2271
|
+
const componentMatch = prompt.match(/\b(card|button|input|dialog|modal|badge|tabs?|dropdown)/i);
|
|
2272
|
+
return {
|
|
2273
|
+
intent: 'component',
|
|
2274
|
+
reason,
|
|
2275
|
+
componentName: componentMatch ? componentMatch[1].charAt(0).toUpperCase() + componentMatch[1].slice(1).toLowerCase() : undefined
|
|
2276
|
+
};
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
// Check for instance patterns
|
|
2281
|
+
for (const { pattern, reason } of instancePatterns) {
|
|
2282
|
+
if (pattern.test(promptLower)) {
|
|
2283
|
+
return { intent: 'instance', reason };
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
// Default to instance when user clicks on specific element
|
|
2288
|
+
return {
|
|
2289
|
+
intent: 'unknown',
|
|
2290
|
+
reason: 'No clear intent markers detected, will use element location as guide'
|
|
2291
|
+
};
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
/**
|
|
2295
|
+
* Find the component definition file for a given component name
|
|
2296
|
+
* Searches common component directories: components/ui/, components/, etc.
|
|
2297
|
+
*/
|
|
2298
|
+
function findComponentDefinitionFile(
|
|
2299
|
+
componentName: string,
|
|
2300
|
+
projectRoot: string
|
|
2301
|
+
): { path: string; content: string } | null {
|
|
2302
|
+
// Common component directory patterns
|
|
2303
|
+
const searchPaths = [
|
|
2304
|
+
`src/components/ui/${componentName.toLowerCase()}.tsx`,
|
|
2305
|
+
`src/components/ui/${componentName.toLowerCase()}.jsx`,
|
|
2306
|
+
`src/components/${componentName.toLowerCase()}.tsx`,
|
|
2307
|
+
`src/components/${componentName.toLowerCase()}.jsx`,
|
|
2308
|
+
`components/ui/${componentName.toLowerCase()}.tsx`,
|
|
2309
|
+
`components/${componentName.toLowerCase()}.tsx`,
|
|
2310
|
+
];
|
|
2311
|
+
|
|
2312
|
+
for (const searchPath of searchPaths) {
|
|
2313
|
+
const fullPath = path.join(projectRoot, searchPath);
|
|
2314
|
+
if (fs.existsSync(fullPath)) {
|
|
2315
|
+
try {
|
|
2316
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
2317
|
+
// Verify it's actually a component definition (has export, cva, or component patterns)
|
|
2318
|
+
if (content.includes('export') && (
|
|
2319
|
+
content.includes('cva(') ||
|
|
2320
|
+
content.includes('forwardRef') ||
|
|
2321
|
+
content.includes('function ' + componentName) ||
|
|
2322
|
+
content.includes('const ' + componentName)
|
|
2323
|
+
)) {
|
|
2324
|
+
debugLog("Found component definition file", {
|
|
2325
|
+
componentName,
|
|
2326
|
+
path: searchPath
|
|
2327
|
+
});
|
|
2328
|
+
return { path: searchPath, content };
|
|
2329
|
+
}
|
|
2330
|
+
} catch {
|
|
2331
|
+
// Ignore read errors
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
debugLog("Component definition file not found", {
|
|
2337
|
+
componentName,
|
|
2338
|
+
searchedPaths: searchPaths
|
|
2339
|
+
});
|
|
2340
|
+
return null;
|
|
2341
|
+
}
|
|
2342
|
+
|
|
1827
2343
|
/**
|
|
1828
2344
|
* Validate patches against identified problems
|
|
1829
2345
|
* Rejects patches that don't reference a valid problem ID
|
|
@@ -1929,7 +2445,7 @@ export async function POST(request: Request) {
|
|
|
1929
2445
|
|
|
1930
2446
|
try {
|
|
1931
2447
|
const body: ApplyFirstRequest = await request.json();
|
|
1932
|
-
const { action, sessionId, screenshot, pageRoute, userPrompt, focusedElements, previewedModifications } = body;
|
|
2448
|
+
const { action, sessionId, screenshot, pageRoute, userPrompt, focusedElements, previewedModifications, chatHistory, currentTheme, propertyEdits } = body;
|
|
1933
2449
|
const projectRoot = process.cwd();
|
|
1934
2450
|
|
|
1935
2451
|
// ========== ACCEPT ACTION ==========
|
|
@@ -2030,6 +2546,9 @@ export async function POST(request: Request) {
|
|
|
2030
2546
|
// Generate a unique session ID
|
|
2031
2547
|
const newSessionId = randomUUID().slice(0, 8);
|
|
2032
2548
|
|
|
2549
|
+
// Build dynamic system prompt based on detected styling system
|
|
2550
|
+
const visionSystemPrompt = await buildVisionSystemPrompt(projectRoot);
|
|
2551
|
+
|
|
2033
2552
|
// Initialize file discovery variables
|
|
2034
2553
|
let smartSearchFiles: { path: string; content: string }[] = [];
|
|
2035
2554
|
let recommendedFile: { path: string; reason: string } | null = null;
|
|
@@ -2449,7 +2968,7 @@ export async function POST(request: Request) {
|
|
|
2449
2968
|
const importedMatch = findElementInImportedFiles(
|
|
2450
2969
|
focusedElements[0],
|
|
2451
2970
|
pageContext.componentSources,
|
|
2452
|
-
actualTargetFile?.path // Pass page file for proximity scoring
|
|
2971
|
+
actualTargetFile?.path ?? undefined // Pass page file for proximity scoring
|
|
2453
2972
|
);
|
|
2454
2973
|
|
|
2455
2974
|
if (importedMatch) {
|
|
@@ -2512,17 +3031,71 @@ export async function POST(request: Request) {
|
|
|
2512
3031
|
}
|
|
2513
3032
|
|
|
2514
3033
|
// ========== SMART SEARCH FALLBACK ==========
|
|
2515
|
-
//
|
|
2516
|
-
//
|
|
2517
|
-
|
|
3034
|
+
// Use smart search fallback when:
|
|
3035
|
+
// 1. Element was NOT found OR found with LOW confidence in the current target file
|
|
3036
|
+
// 2. Current target is still the page wrapper file
|
|
3037
|
+
// 3. We have a smart search result to redirect to
|
|
3038
|
+
// This allows weak matches (low confidence) to be overridden by better search results
|
|
3039
|
+
const isWeakOrNoMatch = !elementLocation || elementLocation.confidence === 'low';
|
|
3040
|
+
|
|
3041
|
+
if (isWeakOrNoMatch && actualTargetFile && actualTargetFile.path === pageContext.pageFile && smartSearchTopPath) {
|
|
2518
3042
|
const smartSearchFullPath = path.join(projectRoot, smartSearchTopPath);
|
|
2519
3043
|
if (fs.existsSync(smartSearchFullPath)) {
|
|
2520
3044
|
const smartSearchContent = fs.readFileSync(smartSearchFullPath, 'utf-8');
|
|
3045
|
+
const previousConfidence = elementLocation?.confidence || 'none';
|
|
2521
3046
|
actualTargetFile = { path: smartSearchTopPath, content: smartSearchContent };
|
|
2522
|
-
|
|
3047
|
+
// Reset elementLocation so we search for the element in the new file
|
|
3048
|
+
elementLocation = null;
|
|
3049
|
+
debugLog("SMART SEARCH FALLBACK: Weak/no match in page file, using smart search result", {
|
|
2523
3050
|
originalFile: pageContext.pageFile,
|
|
2524
3051
|
redirectTo: smartSearchTopPath,
|
|
2525
|
-
contentLength: smartSearchContent.length
|
|
3052
|
+
contentLength: smartSearchContent.length,
|
|
3053
|
+
previousConfidence,
|
|
3054
|
+
reason: previousConfidence === 'low'
|
|
3055
|
+
? "Low confidence match overridden by smart search"
|
|
3056
|
+
: "Element was not located in the page file"
|
|
3057
|
+
});
|
|
3058
|
+
}
|
|
3059
|
+
} else if (elementLocation && elementLocation.confidence !== 'low' && actualTargetFile?.path === pageContext.pageFile) {
|
|
3060
|
+
debugLog("SMART SEARCH FALLBACK: SKIPPED - Element found with sufficient confidence in page file", {
|
|
3061
|
+
file: actualTargetFile.path,
|
|
3062
|
+
matchedBy: elementLocation.matchedBy,
|
|
3063
|
+
confidence: elementLocation.confidence,
|
|
3064
|
+
wouldHaveRedirectedTo: smartSearchTopPath || 'none'
|
|
3065
|
+
});
|
|
3066
|
+
}
|
|
3067
|
+
|
|
3068
|
+
// ========== INTENT-BASED COMPONENT REDIRECT ==========
|
|
3069
|
+
// Analyze user prompt to determine if they want to modify:
|
|
3070
|
+
// - Instance: specific elements on this page (keep current target)
|
|
3071
|
+
// - Component: the component definition (redirect to component file)
|
|
3072
|
+
const intentAnalysis = analyzeModificationIntent(userPrompt);
|
|
3073
|
+
debugLog("Intent analysis", {
|
|
3074
|
+
intent: intentAnalysis.intent,
|
|
3075
|
+
reason: intentAnalysis.reason,
|
|
3076
|
+
componentName: intentAnalysis.componentName || 'none'
|
|
3077
|
+
});
|
|
3078
|
+
|
|
3079
|
+
if (intentAnalysis.intent === 'component' && intentAnalysis.componentName) {
|
|
3080
|
+
// User wants to modify the component definition (e.g., "make ALL cards sharp")
|
|
3081
|
+
const componentFile = findComponentDefinitionFile(intentAnalysis.componentName, projectRoot);
|
|
3082
|
+
|
|
3083
|
+
if (componentFile) {
|
|
3084
|
+
debugLog("INTENT REDIRECT: Switching to component definition file", {
|
|
3085
|
+
originalTarget: actualTargetFile?.path || 'none',
|
|
3086
|
+
componentName: intentAnalysis.componentName,
|
|
3087
|
+
redirectTo: componentFile.path,
|
|
3088
|
+
reason: intentAnalysis.reason
|
|
3089
|
+
});
|
|
3090
|
+
|
|
3091
|
+
actualTargetFile = componentFile;
|
|
3092
|
+
|
|
3093
|
+
// Clear element location since we're now targeting the component file
|
|
3094
|
+
elementLocation = null;
|
|
3095
|
+
} else {
|
|
3096
|
+
debugLog("INTENT REDIRECT: Could not find component definition, keeping instance target", {
|
|
3097
|
+
componentName: intentAnalysis.componentName,
|
|
3098
|
+
fallbackTarget: actualTargetFile?.path || 'none'
|
|
2526
3099
|
});
|
|
2527
3100
|
}
|
|
2528
3101
|
}
|
|
@@ -2530,7 +3103,8 @@ export async function POST(request: Request) {
|
|
|
2530
3103
|
debugLog("File redirect complete", {
|
|
2531
3104
|
originalRecommended: recommendedFileContent?.path || 'none',
|
|
2532
3105
|
actualTarget: actualTargetFile?.path || 'none',
|
|
2533
|
-
wasRedirected: actualTargetFile?.path !== recommendedFileContent?.path
|
|
3106
|
+
wasRedirected: actualTargetFile?.path !== recommendedFileContent?.path,
|
|
3107
|
+
intent: intentAnalysis.intent
|
|
2534
3108
|
});
|
|
2535
3109
|
|
|
2536
3110
|
// Build text content
|
|
@@ -2538,6 +3112,8 @@ export async function POST(request: Request) {
|
|
|
2538
3112
|
|
|
2539
3113
|
Page Route: ${pageRoute}
|
|
2540
3114
|
User Request: "${userPrompt}"
|
|
3115
|
+
Modification Scope: ${intentAnalysis.intent === 'component' ? 'COMPONENT DEFINITION (modify all instances globally)' : intentAnalysis.intent === 'instance' ? 'SPECIFIC INSTANCE (modify only what user clicked)' : 'INSTANCE (default - modify in current file)'}
|
|
3116
|
+
${intentAnalysis.componentName ? `Target Component: ${intentAnalysis.componentName}` : ''}
|
|
2541
3117
|
|
|
2542
3118
|
`;
|
|
2543
3119
|
|
|
@@ -2590,7 +3166,18 @@ User Request: "${userPrompt}"
|
|
|
2590
3166
|
for (const el of focusedElements) {
|
|
2591
3167
|
textContent += `- ${el.name} (${el.type})`;
|
|
2592
3168
|
if (el.textContent) {
|
|
2593
|
-
|
|
3169
|
+
// Provide more text content for better element identification (up to 100 chars)
|
|
3170
|
+
const truncatedText = el.textContent.length > 100
|
|
3171
|
+
? el.textContent.substring(0, 100) + '...'
|
|
3172
|
+
: el.textContent;
|
|
3173
|
+
textContent += ` with text "${truncatedText}"`;
|
|
3174
|
+
}
|
|
3175
|
+
if (el.className) {
|
|
3176
|
+
// Include className for additional matching context
|
|
3177
|
+
const truncatedClass = el.className.length > 80
|
|
3178
|
+
? el.className.substring(0, 80) + '...'
|
|
3179
|
+
: el.className;
|
|
3180
|
+
textContent += `\n className="${truncatedClass}"`;
|
|
2594
3181
|
}
|
|
2595
3182
|
textContent += `\n`;
|
|
2596
3183
|
|
|
@@ -2643,28 +3230,163 @@ ${elementLocation.snippet}
|
|
|
2643
3230
|
⚠️ IMPORTANT: Modify ONLY the element at line ${elementLocation.lineNumber}, NOT other similar elements in the file.
|
|
2644
3231
|
|
|
2645
3232
|
`;
|
|
3233
|
+
// Add special guidance for text elements with low/medium confidence
|
|
3234
|
+
if (elementLocation.confidence !== 'high' && focusedElements[0]?.type === 'text') {
|
|
3235
|
+
const colorMatch = userPrompt?.match(/#[0-9A-Fa-f]{6}|#[0-9A-Fa-f]{3}|rgb\([^)]+\)|rgba\([^)]+\)/);
|
|
3236
|
+
const targetColor = colorMatch ? colorMatch[0] : '';
|
|
3237
|
+
|
|
3238
|
+
// Determine theme-specific color from propertyEdits
|
|
3239
|
+
const themeColor = currentTheme === 'dark'
|
|
3240
|
+
? propertyEdits?.colorDark
|
|
3241
|
+
: propertyEdits?.colorLight;
|
|
3242
|
+
const effectiveColor = themeColor || targetColor || '#hexcolor';
|
|
3243
|
+
|
|
3244
|
+
// Build the appropriate class based on current theme
|
|
3245
|
+
const newThemeClass = currentTheme === 'dark'
|
|
3246
|
+
? `dark:text-[${effectiveColor}]`
|
|
3247
|
+
: `text-[${effectiveColor}]`;
|
|
3248
|
+
|
|
3249
|
+
// The OTHER mode's prefix pattern to preserve
|
|
3250
|
+
const otherModePattern = currentTheme === 'dark'
|
|
3251
|
+
? 'text-[' // In dark mode, preserve unprefixed light mode classes
|
|
3252
|
+
: 'dark:text-['; // In light mode, preserve dark: prefixed classes
|
|
3253
|
+
|
|
3254
|
+
textContent += `
|
|
3255
|
+
📝 TEXT ELEMENT MODIFICATION GUIDANCE:
|
|
3256
|
+
This is a text element matched with ${elementLocation.confidence} confidence.
|
|
3257
|
+
PROCEED with the modification - the element is at line ${elementLocation.lineNumber}.
|
|
3258
|
+
|
|
3259
|
+
🎨 THEME-AWARE COLOR CHANGE (Current mode: ${currentTheme || 'light'}):
|
|
3260
|
+
The user is changing the color ONLY for ${currentTheme || 'light'} mode.
|
|
3261
|
+
|
|
3262
|
+
THEME-SPECIFIC COLOR RULES:
|
|
3263
|
+
- Light mode colors use UNPREFIXED classes: text-[#hexcolor]
|
|
3264
|
+
- Dark mode colors use dark: PREFIX: dark:text-[#hexcolor]
|
|
3265
|
+
- BOTH can coexist: text-[#lightcolor] dark:text-[#darkcolor]
|
|
3266
|
+
|
|
3267
|
+
Current mode is "${currentTheme || 'light'}", so:
|
|
3268
|
+
${currentTheme === 'dark'
|
|
3269
|
+
? `- ADD/REPLACE: dark:text-[${effectiveColor}] (NEW dark mode color)
|
|
3270
|
+
- PRESERVE: any existing text-[#...] class (light mode - DO NOT REMOVE)`
|
|
3271
|
+
: `- ADD/REPLACE: text-[${effectiveColor}] (NEW light mode color)
|
|
3272
|
+
- PRESERVE: any existing dark:text-[#...] class (dark mode - DO NOT REMOVE)`
|
|
3273
|
+
}
|
|
3274
|
+
|
|
3275
|
+
⚠️ INLINE STYLE HANDLING:
|
|
3276
|
+
If element has inline style with color, REMOVE the inline color and use Tailwind class instead.
|
|
3277
|
+
|
|
3278
|
+
EXAMPLES:
|
|
3279
|
+
|
|
3280
|
+
Example 1 - Preserving other mode's color:
|
|
3281
|
+
{
|
|
3282
|
+
"search": "<span className=\\"${currentTheme === 'dark' ? 'text-[#333333]' : 'dark:text-[#FFFFFF]'}\\">",
|
|
3283
|
+
"replace": "<span className=\\"${currentTheme === 'dark' ? 'text-[#333333] ' + newThemeClass : newThemeClass + ' dark:text-[#FFFFFF]'}\\">"
|
|
3284
|
+
}
|
|
3285
|
+
|
|
3286
|
+
Example 2 - Removing inline style:
|
|
3287
|
+
{
|
|
3288
|
+
"search": "<span style={{color: '#D9D9D6'}}>",
|
|
3289
|
+
"replace": "<span className=\\"${newThemeClass}\\">"
|
|
3290
|
+
}
|
|
3291
|
+
|
|
3292
|
+
DO NOT return empty modifications. The element exists at line ${elementLocation.lineNumber}.
|
|
3293
|
+
|
|
3294
|
+
`;
|
|
3295
|
+
}
|
|
2646
3296
|
} else {
|
|
2647
|
-
// Element NOT found
|
|
2648
|
-
|
|
2649
|
-
debugLog("BLOCK: Could not locate focused element anywhere", {
|
|
3297
|
+
// Element NOT found by regex - try AI-assisted location as last resort
|
|
3298
|
+
debugLog("Regex matching failed, trying AI-assisted location...", {
|
|
2650
3299
|
mainFile: actualTargetFile.path,
|
|
2651
|
-
|
|
2652
|
-
focusedElements: focusedElements.map(el => ({
|
|
2653
|
-
name: el.name,
|
|
2654
|
-
type: el.type,
|
|
2655
|
-
textContent: el.textContent?.substring(0, 30),
|
|
2656
|
-
className: el.className?.substring(0, 50),
|
|
2657
|
-
elementId: el.elementId,
|
|
2658
|
-
}))
|
|
3300
|
+
focusedElement: focusedElements[0]?.textContent?.substring(0, 50)
|
|
2659
3301
|
});
|
|
2660
|
-
|
|
2661
|
-
//
|
|
2662
|
-
|
|
3302
|
+
|
|
3303
|
+
// Try AI-assisted location for each focused element
|
|
3304
|
+
let aiLocation = null;
|
|
3305
|
+
for (const el of focusedElements) {
|
|
3306
|
+
if (el.textContent && el.textContent.length > 5) {
|
|
3307
|
+
aiLocation = await aiAssistedElementLocation(
|
|
3308
|
+
actualTargetFile.content,
|
|
3309
|
+
el,
|
|
3310
|
+
actualTargetFile.path ?? '',
|
|
3311
|
+
apiKey
|
|
3312
|
+
);
|
|
3313
|
+
if (aiLocation) {
|
|
3314
|
+
elementLocation = aiLocation;
|
|
3315
|
+
debugLog("AI located element successfully", {
|
|
3316
|
+
lineNumber: aiLocation.lineNumber,
|
|
3317
|
+
confidence: aiLocation.confidence,
|
|
3318
|
+
matchedBy: aiLocation.matchedBy
|
|
3319
|
+
});
|
|
3320
|
+
break;
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
|
|
3325
|
+
if (elementLocation) {
|
|
3326
|
+
// AI found the element - add precise targeting info
|
|
3327
|
+
textContent += `
|
|
3328
|
+
══════════════════════════════════════════════════════════════════════════════
|
|
3329
|
+
PRECISE TARGET LOCATION (${elementLocation.confidence} confidence) - AI-Assisted
|
|
3330
|
+
══════════════════════════════════════════════════════════════════════════════
|
|
3331
|
+
→ Matched by: ${elementLocation.matchedBy}
|
|
3332
|
+
→ Line: ${elementLocation.lineNumber}
|
|
3333
|
+
|
|
3334
|
+
THE USER CLICKED ON THE ELEMENT AT LINE ${elementLocation.lineNumber}.
|
|
3335
|
+
Here is the exact code around that element:
|
|
3336
|
+
\`\`\`
|
|
3337
|
+
${elementLocation.snippet}
|
|
3338
|
+
\`\`\`
|
|
3339
|
+
|
|
3340
|
+
⚠️ IMPORTANT: Modify ONLY the element at line ${elementLocation.lineNumber}, NOT other similar elements in the file.
|
|
3341
|
+
|
|
3342
|
+
`;
|
|
3343
|
+
// Add special guidance for text elements (AI-assisted location)
|
|
3344
|
+
if (focusedElements[0]?.type === 'text') {
|
|
3345
|
+
const colorMatch = userPrompt?.match(/#[0-9A-Fa-f]{6}|#[0-9A-Fa-f]{3}|rgb\([^)]+\)|rgba\([^)]+\)/);
|
|
3346
|
+
const targetColor = colorMatch ? colorMatch[0] : '';
|
|
3347
|
+
|
|
3348
|
+
textContent += `
|
|
3349
|
+
📝 TEXT ELEMENT MODIFICATION GUIDANCE:
|
|
3350
|
+
This is a text element located by AI at line ${elementLocation.lineNumber}.
|
|
3351
|
+
PROCEED with the modification.
|
|
3352
|
+
|
|
3353
|
+
For TEXT COLOR changes, use one of these approaches:
|
|
3354
|
+
1. Tailwind arbitrary value (preferred): className="text-[${targetColor || '#hexcolor'}]"
|
|
3355
|
+
2. Add style prop: style={{ color: '${targetColor || '#hexcolor'}' }}
|
|
3356
|
+
|
|
3357
|
+
Search for the opening tag of the text element at line ${elementLocation.lineNumber} and add the color styling.
|
|
3358
|
+
Example modification:
|
|
3359
|
+
{
|
|
3360
|
+
"search": "<p className=\\"existing-classes\\">",
|
|
3361
|
+
"replace": "<p className=\\"existing-classes text-[${targetColor || '#hexcolor'}]\\">"
|
|
3362
|
+
}
|
|
3363
|
+
|
|
3364
|
+
DO NOT return empty modifications. The element exists at line ${elementLocation.lineNumber}.
|
|
3365
|
+
|
|
3366
|
+
`;
|
|
3367
|
+
}
|
|
3368
|
+
} else {
|
|
3369
|
+
// Element NOT found even with AI - BLOCK the LLM from guessing
|
|
3370
|
+
debugLog("BLOCK: Could not locate focused element anywhere (including AI fallback)", {
|
|
3371
|
+
mainFile: actualTargetFile.path,
|
|
3372
|
+
searchedImports: pageContext.componentSources.length,
|
|
3373
|
+
focusedElements: focusedElements.map(el => ({
|
|
3374
|
+
name: el.name,
|
|
3375
|
+
type: el.type,
|
|
3376
|
+
textContent: el.textContent?.substring(0, 30),
|
|
3377
|
+
className: el.className?.substring(0, 50),
|
|
3378
|
+
elementId: el.elementId,
|
|
3379
|
+
}))
|
|
3380
|
+
});
|
|
3381
|
+
|
|
3382
|
+
// STRONG BLOCK instruction - tell LLM to NOT guess
|
|
3383
|
+
textContent += `
|
|
2663
3384
|
⛔ STOP: CANNOT LOCATE THE CLICKED ELEMENT
|
|
2664
3385
|
|
|
2665
3386
|
The user clicked on a specific element, but it could NOT be found in:
|
|
2666
3387
|
- ${actualTargetFile.path} (main target file)
|
|
2667
3388
|
- Any of the ${pageContext.componentSources.length} imported component files
|
|
3389
|
+
- AI-assisted search also failed to locate the element
|
|
2668
3390
|
|
|
2669
3391
|
The element may be:
|
|
2670
3392
|
- Deeply nested in a component not in the import tree
|
|
@@ -2678,6 +3400,7 @@ DO NOT GUESS. Return this exact response:
|
|
|
2678
3400
|
}
|
|
2679
3401
|
|
|
2680
3402
|
`;
|
|
3403
|
+
}
|
|
2681
3404
|
}
|
|
2682
3405
|
}
|
|
2683
3406
|
|
|
@@ -2726,32 +3449,45 @@ ${linesWithNumbers}
|
|
|
2726
3449
|
usedContext += content.length;
|
|
2727
3450
|
}
|
|
2728
3451
|
|
|
2729
|
-
// ========== KEY UI COMPONENTS ==========
|
|
3452
|
+
// ========== KEY UI COMPONENTS (CVA) ==========
|
|
2730
3453
|
// Include UI component definitions (Button, Card, etc.) so LLM knows available variants/props
|
|
2731
|
-
|
|
3454
|
+
// This helps the AI understand what default styles exist and how to properly override them
|
|
3455
|
+
const uiComponentPaths = [
|
|
3456
|
+
'components/ui/button',
|
|
3457
|
+
'components/ui/card',
|
|
3458
|
+
'components/ui/input',
|
|
3459
|
+
'components/ui/badge',
|
|
3460
|
+
'components/ui/dialog',
|
|
3461
|
+
'components/ui/tabs',
|
|
3462
|
+
];
|
|
2732
3463
|
const includedUIComponents: string[] = [];
|
|
2733
3464
|
|
|
2734
3465
|
for (const comp of pageContext.componentSources) {
|
|
2735
3466
|
// Check if this is a UI component we should include
|
|
2736
3467
|
const isUIComponent = uiComponentPaths.some(uiPath => comp.path.includes(uiPath));
|
|
2737
3468
|
if (isUIComponent && usedContext + comp.content.length < TOTAL_CONTEXT_BUDGET) {
|
|
2738
|
-
//
|
|
2739
|
-
const
|
|
2740
|
-
if (
|
|
2741
|
-
|
|
2742
|
-
--- UI Component: ${comp.path} (
|
|
2743
|
-
${
|
|
3469
|
+
// Capture the full CVA definition including variants AND defaultVariants
|
|
3470
|
+
const cvaMatch = comp.content.match(/const \w+Variants = cva\(\s*["`'][\s\S]*?defaultVariants:\s*\{[^}]*\},?\s*\}\s*\)/);
|
|
3471
|
+
if (cvaMatch) {
|
|
3472
|
+
textContent += `
|
|
3473
|
+
--- UI Component: ${comp.path} (CVA definition) ---
|
|
3474
|
+
${cvaMatch[0]}
|
|
3475
|
+
|
|
3476
|
+
⚠️ NOTE: This component has DEFAULT STYLES via CVA.
|
|
3477
|
+
- Use variant props when available: size="compact", variant="elevated"
|
|
3478
|
+
- Or override with className: className="rounded-none" (tailwind-merge handles conflicts)
|
|
3479
|
+
- The cn() utility merges classes intelligently, so className WILL override defaults
|
|
2744
3480
|
---
|
|
2745
3481
|
|
|
2746
3482
|
`;
|
|
2747
3483
|
includedUIComponents.push(comp.path);
|
|
2748
|
-
usedContext +=
|
|
3484
|
+
usedContext += cvaMatch[0].length + 200;
|
|
2749
3485
|
}
|
|
2750
3486
|
}
|
|
2751
3487
|
}
|
|
2752
3488
|
|
|
2753
3489
|
if (includedUIComponents.length > 0) {
|
|
2754
|
-
debugLog("Included UI component definitions", { components: includedUIComponents });
|
|
3490
|
+
debugLog("Included UI component CVA definitions", { components: includedUIComponents });
|
|
2755
3491
|
}
|
|
2756
3492
|
|
|
2757
3493
|
// ========== THEME DISCOVERY ==========
|
|
@@ -2804,7 +3540,209 @@ Your patch should be:
|
|
|
2804
3540
|
}]
|
|
2805
3541
|
}
|
|
2806
3542
|
|
|
3543
|
+
`;
|
|
3544
|
+
|
|
3545
|
+
// Add CRITICAL instruction - softer for text elements with color changes
|
|
3546
|
+
const isTextColorChange = focusedElements?.some(el => el.type === 'text') &&
|
|
3547
|
+
userPrompt?.toLowerCase().includes('color');
|
|
3548
|
+
|
|
3549
|
+
if (isTextColorChange && elementLocation) {
|
|
3550
|
+
// Determine theme-specific color from propertyEdits
|
|
3551
|
+
const themeColor = currentTheme === 'dark'
|
|
3552
|
+
? propertyEdits?.colorDark
|
|
3553
|
+
: propertyEdits?.colorLight;
|
|
3554
|
+
const colorMatch = userPrompt?.match(/#[0-9A-Fa-f]{6}|#[0-9A-Fa-f]{3}|rgb\([^)]+\)|rgba\([^)]+\)/);
|
|
3555
|
+
const effectiveColor = themeColor || (colorMatch ? colorMatch[0] : '#hexcolor');
|
|
3556
|
+
|
|
3557
|
+
// Build the appropriate class based on current theme
|
|
3558
|
+
// Light mode: unprefixed text-[#color]
|
|
3559
|
+
// Dark mode: dark:text-[#color]
|
|
3560
|
+
const newThemeClass = currentTheme === 'dark'
|
|
3561
|
+
? `dark:text-[${effectiveColor}]`
|
|
3562
|
+
: `text-[${effectiveColor}]`;
|
|
3563
|
+
|
|
3564
|
+
// The OTHER mode's prefix pattern to preserve
|
|
3565
|
+
const otherModePattern = currentTheme === 'dark'
|
|
3566
|
+
? 'text-[' // In dark mode, preserve unprefixed light mode classes
|
|
3567
|
+
: 'dark:text-['; // In light mode, preserve dark: prefixed classes
|
|
3568
|
+
|
|
3569
|
+
debugLog("Using theme-aware instruction for text color change", {
|
|
3570
|
+
elementLine: elementLocation.lineNumber,
|
|
3571
|
+
confidence: elementLocation.confidence,
|
|
3572
|
+
matchedBy: elementLocation.matchedBy,
|
|
3573
|
+
currentTheme: currentTheme || 'light',
|
|
3574
|
+
newThemeClass,
|
|
3575
|
+
otherModePattern
|
|
3576
|
+
});
|
|
3577
|
+
textContent += `
|
|
3578
|
+
IMPORTANT: For this THEME-AWARE text color change (${currentTheme || 'light'} mode):
|
|
3579
|
+
1. Find the text element at line ${elementLocation.lineNumber}
|
|
3580
|
+
2. ADD or REPLACE the ${currentTheme || 'light'} mode color class: "${newThemeClass}"
|
|
3581
|
+
3. PRESERVE any existing ${currentTheme === 'dark' ? 'light' : 'dark'} mode color classes (look for "${otherModePattern}...")
|
|
3582
|
+
4. Your "search" string should match the opening tag of the element
|
|
3583
|
+
|
|
3584
|
+
🎨 THEME-SPECIFIC COLOR RULES:
|
|
3585
|
+
- Light mode colors use UNPREFIXED classes: text-[#hexcolor]
|
|
3586
|
+
- Dark mode colors use dark: PREFIX: dark:text-[#hexcolor]
|
|
3587
|
+
- BOTH can coexist on the same element: text-[#lightcolor] dark:text-[#darkcolor]
|
|
3588
|
+
|
|
3589
|
+
Current mode is "${currentTheme || 'light'}", so:
|
|
3590
|
+
${currentTheme === 'dark'
|
|
3591
|
+
? `- ADD/REPLACE: dark:text-[${effectiveColor}] (this is the NEW dark mode color)
|
|
3592
|
+
- PRESERVE: any existing text-[#...] class (the light mode color - DO NOT REMOVE IT)`
|
|
3593
|
+
: `- ADD/REPLACE: text-[${effectiveColor}] (this is the NEW light mode color)
|
|
3594
|
+
- PRESERVE: any existing dark:text-[#...] class (the dark mode color - DO NOT REMOVE IT)`
|
|
3595
|
+
}
|
|
3596
|
+
|
|
3597
|
+
⚠️ CRITICAL - INLINE STYLE HANDLING:
|
|
3598
|
+
If the element has an inline style with color (e.g., style={{color: '#...'}}) or style="color: ..."),
|
|
3599
|
+
you MUST REMOVE the inline color from the style prop and use the Tailwind class instead.
|
|
3600
|
+
Inline styles override Tailwind classes due to CSS specificity.
|
|
3601
|
+
|
|
3602
|
+
EXAMPLES:
|
|
3603
|
+
|
|
3604
|
+
Example 1 - Changing light mode color (preserving dark mode):
|
|
3605
|
+
- BEFORE: <span className="dark:text-[#FFFFFF]">text</span>
|
|
3606
|
+
- AFTER: <span className="${currentTheme !== 'dark' ? newThemeClass : 'text-[#333333]'} dark:text-[#FFFFFF]">text</span>
|
|
3607
|
+
|
|
3608
|
+
Example 2 - Changing dark mode color (preserving light mode):
|
|
3609
|
+
- BEFORE: <span className="text-[#333333]">text</span>
|
|
3610
|
+
- AFTER: <span className="text-[#333333] ${currentTheme === 'dark' ? newThemeClass : 'dark:text-[#FFFFFF]'}">text</span>
|
|
3611
|
+
|
|
3612
|
+
Example 3 - Removing inline style and adding theme class:
|
|
3613
|
+
- BEFORE: <span style={{color: '#D9D9D6'}}>text</span>
|
|
3614
|
+
- AFTER: <span className="${newThemeClass}">text</span>
|
|
3615
|
+
|
|
3616
|
+
Example 4 - Both modes already exist, updating ${currentTheme || 'light'} mode:
|
|
3617
|
+
- BEFORE: <span className="text-[#oldLight] dark:text-[#oldDark]">text</span>
|
|
3618
|
+
- AFTER: <span className="${currentTheme === 'dark' ? 'text-[#oldLight] ' + newThemeClass : newThemeClass + ' dark:text-[#oldDark]'}">text</span>
|
|
3619
|
+
|
|
3620
|
+
DO NOT return empty modifications. The element exists at line ${elementLocation.lineNumber}.
|
|
3621
|
+
`;
|
|
3622
|
+
} else {
|
|
3623
|
+
textContent += `
|
|
2807
3624
|
CRITICAL: Your "search" string MUST exist in the file. If you can't find the exact code, return empty modifications.`;
|
|
3625
|
+
}
|
|
3626
|
+
|
|
3627
|
+
// Add property-specific Tailwind guidance for non-color style changes
|
|
3628
|
+
if (propertyEdits && elementLocation) {
|
|
3629
|
+
const propertyGuidance: string[] = [];
|
|
3630
|
+
|
|
3631
|
+
// Typography properties
|
|
3632
|
+
if (propertyEdits.fontSize) {
|
|
3633
|
+
propertyGuidance.push(`
|
|
3634
|
+
📐 FONT SIZE: Change to ${propertyEdits.fontSize}
|
|
3635
|
+
Use Tailwind arbitrary value in className: text-[${propertyEdits.fontSize}]
|
|
3636
|
+
Or use standard sizes: text-xs, text-sm, text-base, text-lg, text-xl, text-2xl, etc.
|
|
3637
|
+
Example: className="text-[${propertyEdits.fontSize}]"`);
|
|
3638
|
+
}
|
|
3639
|
+
|
|
3640
|
+
if (propertyEdits.fontWeight) {
|
|
3641
|
+
const weightMap: Record<string, string> = {
|
|
3642
|
+
'100': 'font-thin', '200': 'font-extralight', '300': 'font-light',
|
|
3643
|
+
'400': 'font-normal', '500': 'font-medium', '600': 'font-semibold',
|
|
3644
|
+
'700': 'font-bold', '800': 'font-extrabold', '900': 'font-black'
|
|
3645
|
+
};
|
|
3646
|
+
const tailwindClass = weightMap[propertyEdits.fontWeight] || `font-[${propertyEdits.fontWeight}]`;
|
|
3647
|
+
propertyGuidance.push(`
|
|
3648
|
+
⚖️ FONT WEIGHT: Change to ${propertyEdits.fontWeight}
|
|
3649
|
+
Use Tailwind class: ${tailwindClass}
|
|
3650
|
+
Example: className="${tailwindClass}"`);
|
|
3651
|
+
}
|
|
3652
|
+
|
|
3653
|
+
if (propertyEdits.lineHeight) {
|
|
3654
|
+
propertyGuidance.push(`
|
|
3655
|
+
↕️ LINE HEIGHT: Change to ${propertyEdits.lineHeight}
|
|
3656
|
+
Use Tailwind arbitrary value: leading-[${propertyEdits.lineHeight}]
|
|
3657
|
+
Or use standard values: leading-none, leading-tight, leading-normal, leading-relaxed, leading-loose
|
|
3658
|
+
Example: className="leading-[${propertyEdits.lineHeight}]"`);
|
|
3659
|
+
}
|
|
3660
|
+
|
|
3661
|
+
if (propertyEdits.letterSpacing) {
|
|
3662
|
+
propertyGuidance.push(`
|
|
3663
|
+
↔️ LETTER SPACING: Change to ${propertyEdits.letterSpacing}
|
|
3664
|
+
Use Tailwind arbitrary value: tracking-[${propertyEdits.letterSpacing}]
|
|
3665
|
+
Or use standard values: tracking-tighter, tracking-tight, tracking-normal, tracking-wide, tracking-wider, tracking-widest
|
|
3666
|
+
Example: className="tracking-[${propertyEdits.letterSpacing}]"`);
|
|
3667
|
+
}
|
|
3668
|
+
|
|
3669
|
+
// Layout properties
|
|
3670
|
+
if (propertyEdits.width) {
|
|
3671
|
+
propertyGuidance.push(`
|
|
3672
|
+
📏 WIDTH: Change to ${propertyEdits.width}
|
|
3673
|
+
Use Tailwind arbitrary value: w-[${propertyEdits.width}]
|
|
3674
|
+
Or use standard widths: w-full, w-1/2, w-auto, w-screen, w-64, w-96, etc.
|
|
3675
|
+
Example: className="w-[${propertyEdits.width}]"`);
|
|
3676
|
+
}
|
|
3677
|
+
|
|
3678
|
+
if (propertyEdits.height) {
|
|
3679
|
+
propertyGuidance.push(`
|
|
3680
|
+
📐 HEIGHT: Change to ${propertyEdits.height}
|
|
3681
|
+
Use Tailwind arbitrary value: h-[${propertyEdits.height}]
|
|
3682
|
+
Or use standard heights: h-full, h-1/2, h-auto, h-screen, h-64, h-96, etc.
|
|
3683
|
+
Example: className="h-[${propertyEdits.height}]"`);
|
|
3684
|
+
}
|
|
3685
|
+
|
|
3686
|
+
// Appearance properties
|
|
3687
|
+
if (propertyEdits.opacity) {
|
|
3688
|
+
const opacityValue = parseInt(propertyEdits.opacity);
|
|
3689
|
+
propertyGuidance.push(`
|
|
3690
|
+
👁️ OPACITY: Change to ${propertyEdits.opacity}%
|
|
3691
|
+
Use Tailwind class: opacity-${opacityValue}
|
|
3692
|
+
Standard values: opacity-0, opacity-25, opacity-50, opacity-75, opacity-100
|
|
3693
|
+
Example: className="opacity-${opacityValue}"`);
|
|
3694
|
+
}
|
|
3695
|
+
|
|
3696
|
+
if (propertyEdits.borderRadius) {
|
|
3697
|
+
propertyGuidance.push(`
|
|
3698
|
+
⬜ BORDER RADIUS: Change to ${propertyEdits.borderRadius}
|
|
3699
|
+
Use Tailwind arbitrary value: rounded-[${propertyEdits.borderRadius}]
|
|
3700
|
+
Or use standard values: rounded-none, rounded-sm, rounded, rounded-md, rounded-lg, rounded-xl, rounded-2xl, rounded-full
|
|
3701
|
+
Example: className="rounded-[${propertyEdits.borderRadius}]"`);
|
|
3702
|
+
}
|
|
3703
|
+
|
|
3704
|
+
// Spacing properties
|
|
3705
|
+
if (propertyEdits.padding) {
|
|
3706
|
+
propertyGuidance.push(`
|
|
3707
|
+
📦 PADDING: Change to ${propertyEdits.padding}
|
|
3708
|
+
Use Tailwind arbitrary value: p-[${propertyEdits.padding}]
|
|
3709
|
+
Or use standard values: p-0, p-1, p-2, p-4, p-6, p-8, px-4, py-2, pt-4, pb-4, etc.
|
|
3710
|
+
Example: className="p-[${propertyEdits.padding}]"`);
|
|
3711
|
+
}
|
|
3712
|
+
|
|
3713
|
+
if (propertyEdits.margin) {
|
|
3714
|
+
propertyGuidance.push(`
|
|
3715
|
+
↔️ MARGIN: Change to ${propertyEdits.margin}
|
|
3716
|
+
Use Tailwind arbitrary value: m-[${propertyEdits.margin}]
|
|
3717
|
+
Or use standard values: m-0, m-1, m-2, m-4, m-6, m-8, mx-auto, my-4, mt-4, mb-4, etc.
|
|
3718
|
+
Example: className="m-[${propertyEdits.margin}]"`);
|
|
3719
|
+
}
|
|
3720
|
+
|
|
3721
|
+
if (propertyEdits.gap) {
|
|
3722
|
+
propertyGuidance.push(`
|
|
3723
|
+
🔲 GAP: Change to ${propertyEdits.gap}
|
|
3724
|
+
Use Tailwind arbitrary value: gap-[${propertyEdits.gap}]
|
|
3725
|
+
Or use standard values: gap-0, gap-1, gap-2, gap-4, gap-6, gap-8, gap-x-4, gap-y-4, etc.
|
|
3726
|
+
Example: className="gap-[${propertyEdits.gap}]"`);
|
|
3727
|
+
}
|
|
3728
|
+
|
|
3729
|
+
if (propertyGuidance.length > 0) {
|
|
3730
|
+
textContent += `
|
|
3731
|
+
|
|
3732
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
3733
|
+
PROPERTY-SPECIFIC TAILWIND GUIDANCE
|
|
3734
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
3735
|
+
${propertyGuidance.join('\n')}
|
|
3736
|
+
|
|
3737
|
+
IMPORTANT:
|
|
3738
|
+
1. Add these Tailwind classes to the element's className prop
|
|
3739
|
+
2. If the element already has a className, ADD the new class to it
|
|
3740
|
+
3. If using arbitrary values like w-[200px], ensure the square brackets are included
|
|
3741
|
+
4. Prefer arbitrary values [value] when exact values are needed
|
|
3742
|
+
5. Check for existing conflicting classes and replace them (e.g., replace text-sm with text-lg)
|
|
3743
|
+
`;
|
|
3744
|
+
}
|
|
3745
|
+
}
|
|
2808
3746
|
|
|
2809
3747
|
messageContent.push({
|
|
2810
3748
|
type: "text",
|
|
@@ -2828,7 +3766,7 @@ CRITICAL: Your "search" string MUST exist in the file. If you can't find the exa
|
|
|
2828
3766
|
if (recommendedFileContent) {
|
|
2829
3767
|
validFilePaths.add(recommendedFileContent.path);
|
|
2830
3768
|
}
|
|
2831
|
-
if (actualTargetFile && actualTargetFile.path !== recommendedFileContent?.path) {
|
|
3769
|
+
if (actualTargetFile && actualTargetFile.path && actualTargetFile.path !== recommendedFileContent?.path) {
|
|
2832
3770
|
validFilePaths.add(actualTargetFile.path);
|
|
2833
3771
|
}
|
|
2834
3772
|
|
|
@@ -2840,13 +3778,31 @@ CRITICAL: Your "search" string MUST exist in the file. If you can't find the exa
|
|
|
2840
3778
|
let finalExplanation: string | undefined;
|
|
2841
3779
|
|
|
2842
3780
|
while (retryCount <= MAX_RETRIES) {
|
|
2843
|
-
// Build messages for this attempt
|
|
2844
|
-
const currentMessages: Anthropic.MessageCreateParams["messages"] = [
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
3781
|
+
// Build messages for this attempt, starting with chat history if available
|
|
3782
|
+
const currentMessages: Anthropic.MessageCreateParams["messages"] = [];
|
|
3783
|
+
|
|
3784
|
+
// Add conversation history for multi-turn context (e.g., "make it darker", "undo that")
|
|
3785
|
+
if (chatHistory && chatHistory.length > 0) {
|
|
3786
|
+
for (const msg of chatHistory) {
|
|
3787
|
+
// Only include user and assistant messages, skip system messages
|
|
3788
|
+
if (msg.role === "user" || msg.role === "assistant") {
|
|
3789
|
+
currentMessages.push({
|
|
3790
|
+
role: msg.role as "user" | "assistant",
|
|
3791
|
+
content: msg.content,
|
|
3792
|
+
});
|
|
3793
|
+
}
|
|
3794
|
+
}
|
|
3795
|
+
debugLog("Added chat history to context", {
|
|
3796
|
+
messageCount: chatHistory.length,
|
|
3797
|
+
preview: chatHistory.slice(-2).map(m => ({ role: m.role, content: m.content.substring(0, 50) }))
|
|
3798
|
+
});
|
|
3799
|
+
}
|
|
3800
|
+
|
|
3801
|
+
// Add current user message with screenshot
|
|
3802
|
+
currentMessages.push({
|
|
3803
|
+
role: "user",
|
|
3804
|
+
content: messageContent,
|
|
3805
|
+
});
|
|
2850
3806
|
|
|
2851
3807
|
// If this is a retry, add feedback about what went wrong
|
|
2852
3808
|
if (retryCount > 0 && lastPatchErrors.length > 0) {
|
|
@@ -2891,7 +3847,7 @@ This is better than generating patches with made-up code.`,
|
|
|
2891
3847
|
model: "claude-sonnet-4-20250514",
|
|
2892
3848
|
max_tokens: 16384,
|
|
2893
3849
|
messages: currentMessages,
|
|
2894
|
-
system:
|
|
3850
|
+
system: visionSystemPrompt,
|
|
2895
3851
|
});
|
|
2896
3852
|
|
|
2897
3853
|
// Extract text content from response
|