sonance-brand-mcp 1.3.111 → 1.3.113
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-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 +988 -57
- 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/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 +429 -362
- package/dist/assets/dev-tools/components/ApplyFirstPreview.tsx +10 -10
- package/dist/assets/dev-tools/components/ChatHistory.tsx +11 -7
- package/dist/assets/dev-tools/components/ChatInterface.tsx +61 -20
- package/dist/assets/dev-tools/components/ChatTabBar.tsx +1 -1
- package/dist/assets/dev-tools/components/DiffPreview.tsx +1 -1
- package/dist/assets/dev-tools/components/InlineDiffPreview.tsx +360 -36
- package/dist/assets/dev-tools/components/InspectorOverlay.tsx +9 -9
- package/dist/assets/dev-tools/components/PropertiesPanel.tsx +743 -93
- 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 +4 -64
- 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 +171 -65
- 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/panels/ComponentsPanel.tsx +160 -57
- package/dist/assets/dev-tools/panels/TextPanel.tsx +10 -10
- package/dist/assets/dev-tools/types.ts +42 -0
- 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 +32 -1
- package/package.json +1 -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 {
|
|
@@ -80,6 +83,27 @@ interface ApplyFirstRequest {
|
|
|
80
83
|
previewedModifications?: VisionFileModification[];
|
|
81
84
|
/** Conversation history for multi-turn context */
|
|
82
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
|
+
};
|
|
83
107
|
}
|
|
84
108
|
|
|
85
109
|
interface BackupManifest {
|
|
@@ -151,9 +175,10 @@ function debugLog(message: string, data?: unknown) {
|
|
|
151
175
|
/**
|
|
152
176
|
* Sanitize a JSON string by finding the correct end point using bracket balancing.
|
|
153
177
|
* Handles cases where LLM outputs trailing garbage like extra ]} characters.
|
|
178
|
+
* Also handles leading conversational text before the JSON payload.
|
|
154
179
|
*/
|
|
155
180
|
function sanitizeJsonString(text: string): string {
|
|
156
|
-
|
|
181
|
+
let trimmed = text.trim();
|
|
157
182
|
|
|
158
183
|
debugLog("[sanitizeJsonString] Starting", {
|
|
159
184
|
inputLength: trimmed.length,
|
|
@@ -161,6 +186,21 @@ function sanitizeJsonString(text: string): string {
|
|
|
161
186
|
last100: trimmed.substring(trimmed.length - 100)
|
|
162
187
|
});
|
|
163
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
|
+
|
|
164
204
|
// Try parsing as-is first
|
|
165
205
|
try {
|
|
166
206
|
JSON.parse(trimmed);
|
|
@@ -516,26 +556,31 @@ function findElementLineInFile(
|
|
|
516
556
|
|
|
517
557
|
// PRIORITY 2c: Word-based matching for text with special characters (bullets, etc.)
|
|
518
558
|
// Extract significant words and find lines containing multiple of them
|
|
519
|
-
|
|
520
|
-
|
|
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'];
|
|
521
562
|
const significantWords = normalizedText.split(' ')
|
|
522
|
-
.filter(word => word.length >=
|
|
523
|
-
.slice(0,
|
|
524
|
-
|
|
525
|
-
|
|
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) {
|
|
526
571
|
for (let i = 0; i < lines.length; i++) {
|
|
527
572
|
const lineLower = lines[i].toLowerCase();
|
|
528
|
-
const matchCount = significantWords.filter(word =>
|
|
573
|
+
const matchCount = significantWords.filter(word =>
|
|
529
574
|
lineLower.includes(word.toLowerCase())
|
|
530
575
|
).length;
|
|
531
|
-
|
|
532
|
-
//
|
|
533
|
-
if (matchCount >=
|
|
576
|
+
|
|
577
|
+
// Flexible matching: longer text can match with fewer words
|
|
578
|
+
if (matchCount >= minMatches) {
|
|
534
579
|
return {
|
|
535
580
|
lineNumber: i + 1,
|
|
536
581
|
snippet: lines.slice(Math.max(0, i - 3), i + 5).join('\n'),
|
|
537
|
-
confidence: 'medium',
|
|
538
|
-
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)`
|
|
539
584
|
};
|
|
540
585
|
}
|
|
541
586
|
}
|
|
@@ -561,6 +606,70 @@ function findElementLineInFile(
|
|
|
561
606
|
}
|
|
562
607
|
}
|
|
563
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
|
+
|
|
564
673
|
// PRIORITY 3: Distinctive className patterns (semantic classes from design system)
|
|
565
674
|
if (focusedElement.className) {
|
|
566
675
|
// SEMANTIC CLASS DETECTION: Instead of filtering OUT utilities, filter IN semantics
|
|
@@ -676,10 +785,224 @@ function findElementLineInFile(
|
|
|
676
785
|
}
|
|
677
786
|
}
|
|
678
787
|
}
|
|
679
|
-
|
|
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
|
+
|
|
680
800
|
return null;
|
|
681
801
|
}
|
|
682
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
|
+
|
|
901
|
+
return null;
|
|
902
|
+
}
|
|
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
|
+
|
|
683
1006
|
/**
|
|
684
1007
|
* Search imported component files for specific TEXT CONTENT
|
|
685
1008
|
* This is used to redirect from parent components to child components
|
|
@@ -821,15 +1144,29 @@ function findElementInImportedFiles(
|
|
|
821
1144
|
file.path.endsWith('.tsx') ||
|
|
822
1145
|
file.path.endsWith('.jsx');
|
|
823
1146
|
|
|
824
|
-
//
|
|
825
|
-
|
|
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;
|
|
826
1159
|
|
|
827
|
-
// Skip known non-UI files
|
|
828
|
-
|
|
1160
|
+
// Skip known non-UI files (but allow config files for logos)
|
|
1161
|
+
const isSkippable = file.path.includes('/types') ||
|
|
829
1162
|
file.path.includes('/hooks/') ||
|
|
830
|
-
file.path.includes('
|
|
831
|
-
|
|
832
|
-
|
|
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;
|
|
833
1170
|
|
|
834
1171
|
const result = findElementLineInFile(file.content, focusedElement);
|
|
835
1172
|
// Accept ALL matches including low confidence - let the scoring decide
|
|
@@ -1722,7 +2059,8 @@ function searchFilesSmart(
|
|
|
1722
2059
|
return sortedResults.map(r => ({ path: r.path, content: r.content, score: r.score, filenameMatch: r.filenameMatch }));
|
|
1723
2060
|
}
|
|
1724
2061
|
|
|
1725
|
-
|
|
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.
|
|
1726
2064
|
|
|
1727
2065
|
CRITICAL: Return ONLY the JSON object. No explanations, no preamble, no markdown code fences.
|
|
1728
2066
|
Start your response with { and end with }
|
|
@@ -1732,6 +2070,54 @@ Output format:
|
|
|
1732
2070
|
|
|
1733
2071
|
The "search" field must match the file EXACTLY (copy-paste from the code provided).`;
|
|
1734
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
|
+
|
|
1735
2121
|
/**
|
|
1736
2122
|
* PHASE 2: Targeted Patch Generation Prompt
|
|
1737
2123
|
*
|
|
@@ -1831,6 +2217,129 @@ DESIGN CONSTRAINTS
|
|
|
1831
2217
|
═══════════════════════════════════════════════════════════════════════════════
|
|
1832
2218
|
`;
|
|
1833
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
|
+
|
|
1834
2343
|
/**
|
|
1835
2344
|
* Validate patches against identified problems
|
|
1836
2345
|
* Rejects patches that don't reference a valid problem ID
|
|
@@ -1936,7 +2445,7 @@ export async function POST(request: Request) {
|
|
|
1936
2445
|
|
|
1937
2446
|
try {
|
|
1938
2447
|
const body: ApplyFirstRequest = await request.json();
|
|
1939
|
-
const { action, sessionId, screenshot, pageRoute, userPrompt, focusedElements, previewedModifications, chatHistory } = body;
|
|
2448
|
+
const { action, sessionId, screenshot, pageRoute, userPrompt, focusedElements, previewedModifications, chatHistory, currentTheme, propertyEdits } = body;
|
|
1940
2449
|
const projectRoot = process.cwd();
|
|
1941
2450
|
|
|
1942
2451
|
// ========== ACCEPT ACTION ==========
|
|
@@ -2037,6 +2546,9 @@ export async function POST(request: Request) {
|
|
|
2037
2546
|
// Generate a unique session ID
|
|
2038
2547
|
const newSessionId = randomUUID().slice(0, 8);
|
|
2039
2548
|
|
|
2549
|
+
// Build dynamic system prompt based on detected styling system
|
|
2550
|
+
const visionSystemPrompt = await buildVisionSystemPrompt(projectRoot);
|
|
2551
|
+
|
|
2040
2552
|
// Initialize file discovery variables
|
|
2041
2553
|
let smartSearchFiles: { path: string; content: string }[] = [];
|
|
2042
2554
|
let recommendedFile: { path: string; reason: string } | null = null;
|
|
@@ -2456,7 +2968,7 @@ export async function POST(request: Request) {
|
|
|
2456
2968
|
const importedMatch = findElementInImportedFiles(
|
|
2457
2969
|
focusedElements[0],
|
|
2458
2970
|
pageContext.componentSources,
|
|
2459
|
-
actualTargetFile?.path // Pass page file for proximity scoring
|
|
2971
|
+
actualTargetFile?.path ?? undefined // Pass page file for proximity scoring
|
|
2460
2972
|
);
|
|
2461
2973
|
|
|
2462
2974
|
if (importedMatch) {
|
|
@@ -2519,17 +3031,71 @@ export async function POST(request: Request) {
|
|
|
2519
3031
|
}
|
|
2520
3032
|
|
|
2521
3033
|
// ========== SMART SEARCH FALLBACK ==========
|
|
2522
|
-
//
|
|
2523
|
-
//
|
|
2524
|
-
|
|
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) {
|
|
2525
3042
|
const smartSearchFullPath = path.join(projectRoot, smartSearchTopPath);
|
|
2526
3043
|
if (fs.existsSync(smartSearchFullPath)) {
|
|
2527
3044
|
const smartSearchContent = fs.readFileSync(smartSearchFullPath, 'utf-8');
|
|
3045
|
+
const previousConfidence = elementLocation?.confidence || 'none';
|
|
2528
3046
|
actualTargetFile = { path: smartSearchTopPath, content: smartSearchContent };
|
|
2529
|
-
|
|
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", {
|
|
2530
3050
|
originalFile: pageContext.pageFile,
|
|
2531
3051
|
redirectTo: smartSearchTopPath,
|
|
2532
|
-
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'
|
|
2533
3099
|
});
|
|
2534
3100
|
}
|
|
2535
3101
|
}
|
|
@@ -2537,7 +3103,8 @@ export async function POST(request: Request) {
|
|
|
2537
3103
|
debugLog("File redirect complete", {
|
|
2538
3104
|
originalRecommended: recommendedFileContent?.path || 'none',
|
|
2539
3105
|
actualTarget: actualTargetFile?.path || 'none',
|
|
2540
|
-
wasRedirected: actualTargetFile?.path !== recommendedFileContent?.path
|
|
3106
|
+
wasRedirected: actualTargetFile?.path !== recommendedFileContent?.path,
|
|
3107
|
+
intent: intentAnalysis.intent
|
|
2541
3108
|
});
|
|
2542
3109
|
|
|
2543
3110
|
// Build text content
|
|
@@ -2545,6 +3112,8 @@ export async function POST(request: Request) {
|
|
|
2545
3112
|
|
|
2546
3113
|
Page Route: ${pageRoute}
|
|
2547
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}` : ''}
|
|
2548
3117
|
|
|
2549
3118
|
`;
|
|
2550
3119
|
|
|
@@ -2597,7 +3166,18 @@ User Request: "${userPrompt}"
|
|
|
2597
3166
|
for (const el of focusedElements) {
|
|
2598
3167
|
textContent += `- ${el.name} (${el.type})`;
|
|
2599
3168
|
if (el.textContent) {
|
|
2600
|
-
|
|
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}"`;
|
|
2601
3181
|
}
|
|
2602
3182
|
textContent += `\n`;
|
|
2603
3183
|
|
|
@@ -2650,28 +3230,163 @@ ${elementLocation.snippet}
|
|
|
2650
3230
|
⚠️ IMPORTANT: Modify ONLY the element at line ${elementLocation.lineNumber}, NOT other similar elements in the file.
|
|
2651
3231
|
|
|
2652
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
|
+
}
|
|
2653
3296
|
} else {
|
|
2654
|
-
// Element NOT found
|
|
2655
|
-
|
|
2656
|
-
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...", {
|
|
2657
3299
|
mainFile: actualTargetFile.path,
|
|
2658
|
-
|
|
2659
|
-
focusedElements: focusedElements.map(el => ({
|
|
2660
|
-
name: el.name,
|
|
2661
|
-
type: el.type,
|
|
2662
|
-
textContent: el.textContent?.substring(0, 30),
|
|
2663
|
-
className: el.className?.substring(0, 50),
|
|
2664
|
-
elementId: el.elementId,
|
|
2665
|
-
}))
|
|
3300
|
+
focusedElement: focusedElements[0]?.textContent?.substring(0, 50)
|
|
2666
3301
|
});
|
|
2667
|
-
|
|
2668
|
-
//
|
|
2669
|
-
|
|
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 += `
|
|
2670
3384
|
⛔ STOP: CANNOT LOCATE THE CLICKED ELEMENT
|
|
2671
3385
|
|
|
2672
3386
|
The user clicked on a specific element, but it could NOT be found in:
|
|
2673
3387
|
- ${actualTargetFile.path} (main target file)
|
|
2674
3388
|
- Any of the ${pageContext.componentSources.length} imported component files
|
|
3389
|
+
- AI-assisted search also failed to locate the element
|
|
2675
3390
|
|
|
2676
3391
|
The element may be:
|
|
2677
3392
|
- Deeply nested in a component not in the import tree
|
|
@@ -2685,6 +3400,7 @@ DO NOT GUESS. Return this exact response:
|
|
|
2685
3400
|
}
|
|
2686
3401
|
|
|
2687
3402
|
`;
|
|
3403
|
+
}
|
|
2688
3404
|
}
|
|
2689
3405
|
}
|
|
2690
3406
|
|
|
@@ -2733,32 +3449,45 @@ ${linesWithNumbers}
|
|
|
2733
3449
|
usedContext += content.length;
|
|
2734
3450
|
}
|
|
2735
3451
|
|
|
2736
|
-
// ========== KEY UI COMPONENTS ==========
|
|
3452
|
+
// ========== KEY UI COMPONENTS (CVA) ==========
|
|
2737
3453
|
// Include UI component definitions (Button, Card, etc.) so LLM knows available variants/props
|
|
2738
|
-
|
|
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
|
+
];
|
|
2739
3463
|
const includedUIComponents: string[] = [];
|
|
2740
3464
|
|
|
2741
3465
|
for (const comp of pageContext.componentSources) {
|
|
2742
3466
|
// Check if this is a UI component we should include
|
|
2743
3467
|
const isUIComponent = uiComponentPaths.some(uiPath => comp.path.includes(uiPath));
|
|
2744
3468
|
if (isUIComponent && usedContext + comp.content.length < TOTAL_CONTEXT_BUDGET) {
|
|
2745
|
-
//
|
|
2746
|
-
const
|
|
2747
|
-
if (
|
|
2748
|
-
|
|
2749
|
-
--- UI Component: ${comp.path} (
|
|
2750
|
-
${
|
|
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
|
|
2751
3480
|
---
|
|
2752
3481
|
|
|
2753
3482
|
`;
|
|
2754
3483
|
includedUIComponents.push(comp.path);
|
|
2755
|
-
usedContext +=
|
|
3484
|
+
usedContext += cvaMatch[0].length + 200;
|
|
2756
3485
|
}
|
|
2757
3486
|
}
|
|
2758
3487
|
}
|
|
2759
3488
|
|
|
2760
3489
|
if (includedUIComponents.length > 0) {
|
|
2761
|
-
debugLog("Included UI component definitions", { components: includedUIComponents });
|
|
3490
|
+
debugLog("Included UI component CVA definitions", { components: includedUIComponents });
|
|
2762
3491
|
}
|
|
2763
3492
|
|
|
2764
3493
|
// ========== THEME DISCOVERY ==========
|
|
@@ -2811,7 +3540,209 @@ Your patch should be:
|
|
|
2811
3540
|
}]
|
|
2812
3541
|
}
|
|
2813
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 += `
|
|
2814
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
|
+
}
|
|
2815
3746
|
|
|
2816
3747
|
messageContent.push({
|
|
2817
3748
|
type: "text",
|
|
@@ -2835,7 +3766,7 @@ CRITICAL: Your "search" string MUST exist in the file. If you can't find the exa
|
|
|
2835
3766
|
if (recommendedFileContent) {
|
|
2836
3767
|
validFilePaths.add(recommendedFileContent.path);
|
|
2837
3768
|
}
|
|
2838
|
-
if (actualTargetFile && actualTargetFile.path !== recommendedFileContent?.path) {
|
|
3769
|
+
if (actualTargetFile && actualTargetFile.path && actualTargetFile.path !== recommendedFileContent?.path) {
|
|
2839
3770
|
validFilePaths.add(actualTargetFile.path);
|
|
2840
3771
|
}
|
|
2841
3772
|
|
|
@@ -2916,7 +3847,7 @@ This is better than generating patches with made-up code.`,
|
|
|
2916
3847
|
model: "claude-sonnet-4-20250514",
|
|
2917
3848
|
max_tokens: 16384,
|
|
2918
3849
|
messages: currentMessages,
|
|
2919
|
-
system:
|
|
3850
|
+
system: visionSystemPrompt,
|
|
2920
3851
|
});
|
|
2921
3852
|
|
|
2922
3853
|
// Extract text content from response
|