sonance-brand-mcp 1.3.71 → 1.3.73
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.
|
@@ -34,6 +34,10 @@ interface VisionFocusedElement {
|
|
|
34
34
|
textContent?: string;
|
|
35
35
|
/** The className string of the element for pattern matching */
|
|
36
36
|
className?: string;
|
|
37
|
+
/** The element's DOM id attribute for precise code targeting */
|
|
38
|
+
elementId?: string;
|
|
39
|
+
/** IDs of child elements for more precise targeting */
|
|
40
|
+
childIds?: string[];
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
interface VisionFileModification {
|
|
@@ -120,6 +124,52 @@ function validateSyntaxWithAST(content: string, filePath: string): { valid: bool
|
|
|
120
124
|
}
|
|
121
125
|
}
|
|
122
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Search for element ID in file content and return line number
|
|
129
|
+
* This enables precise targeting of the exact element the user clicked
|
|
130
|
+
*/
|
|
131
|
+
function findElementIdInFile(
|
|
132
|
+
fileContent: string,
|
|
133
|
+
elementId: string | undefined,
|
|
134
|
+
childIds: string[] | undefined
|
|
135
|
+
): { lineNumber: number; matchedId: string; snippet: string } | null {
|
|
136
|
+
if (!fileContent) return null;
|
|
137
|
+
|
|
138
|
+
const lines = fileContent.split('\n');
|
|
139
|
+
|
|
140
|
+
// Try the element's own ID first
|
|
141
|
+
if (elementId) {
|
|
142
|
+
const pattern = new RegExp(`id=["'\`]${elementId}["'\`]`);
|
|
143
|
+
for (let i = 0; i < lines.length; i++) {
|
|
144
|
+
if (pattern.test(lines[i])) {
|
|
145
|
+
return {
|
|
146
|
+
lineNumber: i + 1,
|
|
147
|
+
matchedId: elementId,
|
|
148
|
+
snippet: lines.slice(Math.max(0, i - 2), i + 5).join('\n'),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Try child IDs
|
|
155
|
+
if (childIds && childIds.length > 0) {
|
|
156
|
+
for (const childId of childIds) {
|
|
157
|
+
const pattern = new RegExp(`id=["'\`]${childId}["'\`]`);
|
|
158
|
+
for (let i = 0; i < lines.length; i++) {
|
|
159
|
+
if (pattern.test(lines[i])) {
|
|
160
|
+
return {
|
|
161
|
+
lineNumber: i + 1,
|
|
162
|
+
matchedId: childId,
|
|
163
|
+
snippet: lines.slice(Math.max(0, i - 2), i + 5).join('\n'),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
123
173
|
/**
|
|
124
174
|
* Result of LLM screenshot analysis for smart file discovery
|
|
125
175
|
*/
|
|
@@ -269,6 +319,28 @@ function findFilesContainingElement(
|
|
|
269
319
|
// not the focused component the user is looking at.
|
|
270
320
|
// Instead, we rely on Phase 2a matches and textContent matching.
|
|
271
321
|
|
|
322
|
+
// HIGHEST VALUE: Element ID match - this is THE file if it contains the exact ID
|
|
323
|
+
// IDs are unique identifiers and provide the most reliable targeting
|
|
324
|
+
if (el.elementId) {
|
|
325
|
+
const idPattern = new RegExp(`id=["'\`]${el.elementId}["'\`]`);
|
|
326
|
+
if (idPattern.test(content)) {
|
|
327
|
+
score += 500; // VERY HIGH - exact ID match is definitive!
|
|
328
|
+
matches.push(`contains id="${el.elementId}"`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Also check child IDs
|
|
333
|
+
if (el.childIds && el.childIds.length > 0) {
|
|
334
|
+
for (const childId of el.childIds) {
|
|
335
|
+
const idPattern = new RegExp(`id=["'\`]${childId}["'\`]`);
|
|
336
|
+
if (idPattern.test(content)) {
|
|
337
|
+
score += 400; // Very high - child ID match is also definitive
|
|
338
|
+
matches.push(`contains child id="${childId}"`);
|
|
339
|
+
break; // Only count once
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
272
344
|
// VERY HIGH VALUE: Exact text content match (e.g., button says "REFRESH")
|
|
273
345
|
// This is THE file if it contains the exact text the user clicked on
|
|
274
346
|
if (el.textContent && el.textContent.length >= 2) {
|
|
@@ -497,7 +569,7 @@ function searchFilesSmart(
|
|
|
497
569
|
results.set(foundPath, {
|
|
498
570
|
path: foundPath,
|
|
499
571
|
content,
|
|
500
|
-
score:
|
|
572
|
+
score: 1500, // HIGHEST priority - component name matched visible text
|
|
501
573
|
filenameMatch: true
|
|
502
574
|
});
|
|
503
575
|
debugLog("Phase 2a: Found file by component name", { componentName, foundPath });
|
|
@@ -599,17 +671,12 @@ function searchFilesSmart(
|
|
|
599
671
|
return sortedResults.map(r => ({ path: r.path, content: r.content, score: r.score, filenameMatch: r.filenameMatch }));
|
|
600
672
|
}
|
|
601
673
|
|
|
602
|
-
const VISION_SYSTEM_PROMPT = `You
|
|
674
|
+
const VISION_SYSTEM_PROMPT = `You are an expert frontend developer. Edit the code to fulfill the user's request.
|
|
603
675
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
2. Copy code EXACTLY from the file - character for character
|
|
607
|
-
3. Make the SMALLEST possible change
|
|
608
|
-
4. For color changes, just change the className
|
|
609
|
-
5. Do not restructure or reorganize code
|
|
676
|
+
Output ONLY this JSON (no other text):
|
|
677
|
+
{"modifications":[{"filePath":"path","patches":[{"search":"exact original code","replace":"modified code"}]}]}
|
|
610
678
|
|
|
611
|
-
|
|
612
|
-
{"modifications":[{"filePath":"path","patches":[{"search":"exact code","replace":"changed code"}]}]}`;
|
|
679
|
+
The "search" field must match the file EXACTLY (copy-paste from the code provided).`;
|
|
613
680
|
|
|
614
681
|
export async function POST(request: Request) {
|
|
615
682
|
// Only allow in development
|
|
@@ -864,10 +931,15 @@ export async function POST(request: Request) {
|
|
|
864
931
|
// Extract recommended file from component sources (to show first, avoid duplication)
|
|
865
932
|
let recommendedFileContent: { path: string; content: string } | null = null;
|
|
866
933
|
if (recommendedFile) {
|
|
934
|
+
// Check componentSources first
|
|
867
935
|
const idx = pageContext.componentSources.findIndex(c => c.path === recommendedFile.path);
|
|
868
936
|
if (idx !== -1) {
|
|
869
937
|
recommendedFileContent = pageContext.componentSources[idx];
|
|
870
938
|
pageContext.componentSources.splice(idx, 1); // Remove to avoid duplication
|
|
939
|
+
}
|
|
940
|
+
// Fallback: Check if it's the page file itself
|
|
941
|
+
else if (pageContext.pageFile === recommendedFile.path && pageContext.pageContent) {
|
|
942
|
+
recommendedFileContent = { path: pageContext.pageFile, content: pageContext.pageContent };
|
|
871
943
|
}
|
|
872
944
|
}
|
|
873
945
|
|
|
@@ -879,18 +951,50 @@ User Request: "${userPrompt}"
|
|
|
879
951
|
|
|
880
952
|
`;
|
|
881
953
|
|
|
882
|
-
if (focusedElements && focusedElements.length > 0) {
|
|
883
|
-
textContent += `FOCUSED ELEMENTS (user clicked on these):
|
|
884
|
-
${focusedElements.map((el) => `- ${el.name} (${el.type}) at (${el.coordinates.x}, ${el.coordinates.y})`).join("\n")}
|
|
885
|
-
|
|
886
|
-
`;
|
|
887
|
-
}
|
|
888
|
-
|
|
889
954
|
// ========== TARGET COMPONENT ONLY (with line numbers) ==========
|
|
890
955
|
// CRITICAL: Only include the TARGET file to avoid overwhelming the LLM with noise
|
|
891
956
|
if (recommendedFileContent) {
|
|
892
957
|
const content = recommendedFileContent.content;
|
|
893
958
|
|
|
959
|
+
// Search for element IDs in the file to enable precise targeting
|
|
960
|
+
let idMatch: { lineNumber: number; matchedId: string; snippet: string } | null = null;
|
|
961
|
+
if (focusedElements && focusedElements.length > 0) {
|
|
962
|
+
for (const el of focusedElements) {
|
|
963
|
+
idMatch = findElementIdInFile(content, el.elementId, el.childIds);
|
|
964
|
+
if (idMatch) break;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// Build focused elements section with precise targeting info
|
|
969
|
+
if (focusedElements && focusedElements.length > 0) {
|
|
970
|
+
textContent += `FOCUSED ELEMENTS (user clicked on these):\n`;
|
|
971
|
+
for (const el of focusedElements) {
|
|
972
|
+
textContent += `- ${el.name} (${el.type})`;
|
|
973
|
+
if (el.textContent) {
|
|
974
|
+
textContent += ` with text "${el.textContent.substring(0, 30)}"`;
|
|
975
|
+
}
|
|
976
|
+
textContent += `\n`;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// Add precise targeting if we found an ID match
|
|
980
|
+
if (idMatch) {
|
|
981
|
+
textContent += `
|
|
982
|
+
PRECISE TARGET (found by element ID):
|
|
983
|
+
→ ID: "${idMatch.matchedId}"
|
|
984
|
+
→ Line: ${idMatch.lineNumber}
|
|
985
|
+
→ Look for this ID in the code and modify the element that contains it.
|
|
986
|
+
|
|
987
|
+
`;
|
|
988
|
+
debugLog("Found element ID in file", {
|
|
989
|
+
matchedId: idMatch.matchedId,
|
|
990
|
+
lineNumber: idMatch.lineNumber,
|
|
991
|
+
file: recommendedFileContent.path,
|
|
992
|
+
});
|
|
993
|
+
} else {
|
|
994
|
+
textContent += `\n`;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
894
998
|
// Add line numbers to make it easy for LLM to reference exact code
|
|
895
999
|
const linesWithNumbers = content.split('\n').map((line, i) =>
|
|
896
1000
|
`${String(i + 1).padStart(4, ' ')}| ${line}`
|
|
@@ -933,8 +1037,33 @@ ${linesWithNumbers}
|
|
|
933
1037
|
usedContext += content.length;
|
|
934
1038
|
}
|
|
935
1039
|
|
|
936
|
-
//
|
|
937
|
-
//
|
|
1040
|
+
// ========== KEY UI COMPONENTS ==========
|
|
1041
|
+
// Include UI component definitions (Button, Card, etc.) so LLM knows available variants/props
|
|
1042
|
+
const uiComponentPaths = ['components/ui/button', 'components/ui/card', 'components/ui/input'];
|
|
1043
|
+
const includedUIComponents: string[] = [];
|
|
1044
|
+
|
|
1045
|
+
for (const comp of pageContext.componentSources) {
|
|
1046
|
+
// Check if this is a UI component we should include
|
|
1047
|
+
const isUIComponent = uiComponentPaths.some(uiPath => comp.path.includes(uiPath));
|
|
1048
|
+
if (isUIComponent && usedContext + comp.content.length < TOTAL_CONTEXT_BUDGET) {
|
|
1049
|
+
// Only include the variants/props section, not the whole file
|
|
1050
|
+
const variantsMatch = comp.content.match(/variants:\s*\{[\s\S]*?\n\s*\},\n\s*defaultVariants/);
|
|
1051
|
+
if (variantsMatch) {
|
|
1052
|
+
textContent += `
|
|
1053
|
+
--- UI Component: ${comp.path} (variants only) ---
|
|
1054
|
+
${variantsMatch[0]}
|
|
1055
|
+
---
|
|
1056
|
+
|
|
1057
|
+
`;
|
|
1058
|
+
includedUIComponents.push(comp.path);
|
|
1059
|
+
usedContext += variantsMatch[0].length + 100;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
if (includedUIComponents.length > 0) {
|
|
1065
|
+
debugLog("Included UI component definitions", { components: includedUIComponents });
|
|
1066
|
+
}
|
|
938
1067
|
|
|
939
1068
|
// ========== THEME DISCOVERY ==========
|
|
940
1069
|
// Discover theme tokens and contrast analysis to help LLM make informed color decisions
|
|
@@ -33,6 +33,10 @@ interface VisionFocusedElement {
|
|
|
33
33
|
textContent?: string;
|
|
34
34
|
/** The className string of the element for pattern matching */
|
|
35
35
|
className?: string;
|
|
36
|
+
/** The element's DOM id attribute for precise code targeting */
|
|
37
|
+
elementId?: string;
|
|
38
|
+
/** IDs of child elements for more precise targeting */
|
|
39
|
+
childIds?: string[];
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
interface VisionFileModification {
|
|
@@ -116,6 +120,52 @@ function validateSyntaxWithAST(content: string, filePath: string): { valid: bool
|
|
|
116
120
|
}
|
|
117
121
|
}
|
|
118
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Search for element ID in file content and return line number
|
|
125
|
+
* This enables precise targeting of the exact element the user clicked
|
|
126
|
+
*/
|
|
127
|
+
function findElementIdInFile(
|
|
128
|
+
fileContent: string,
|
|
129
|
+
elementId: string | undefined,
|
|
130
|
+
childIds: string[] | undefined
|
|
131
|
+
): { lineNumber: number; matchedId: string; snippet: string } | null {
|
|
132
|
+
if (!fileContent) return null;
|
|
133
|
+
|
|
134
|
+
const lines = fileContent.split('\n');
|
|
135
|
+
|
|
136
|
+
// Try the element's own ID first
|
|
137
|
+
if (elementId) {
|
|
138
|
+
const pattern = new RegExp(`id=["'\`]${elementId}["'\`]`);
|
|
139
|
+
for (let i = 0; i < lines.length; i++) {
|
|
140
|
+
if (pattern.test(lines[i])) {
|
|
141
|
+
return {
|
|
142
|
+
lineNumber: i + 1,
|
|
143
|
+
matchedId: elementId,
|
|
144
|
+
snippet: lines.slice(Math.max(0, i - 2), i + 5).join('\n'),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Try child IDs
|
|
151
|
+
if (childIds && childIds.length > 0) {
|
|
152
|
+
for (const childId of childIds) {
|
|
153
|
+
const pattern = new RegExp(`id=["'\`]${childId}["'\`]`);
|
|
154
|
+
for (let i = 0; i < lines.length; i++) {
|
|
155
|
+
if (pattern.test(lines[i])) {
|
|
156
|
+
return {
|
|
157
|
+
lineNumber: i + 1,
|
|
158
|
+
matchedId: childId,
|
|
159
|
+
snippet: lines.slice(Math.max(0, i - 2), i + 5).join('\n'),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
119
169
|
/**
|
|
120
170
|
* Result of LLM screenshot analysis for smart file discovery
|
|
121
171
|
*/
|
|
@@ -265,6 +315,28 @@ function findFilesContainingElement(
|
|
|
265
315
|
// not the focused component the user is looking at.
|
|
266
316
|
// Instead, we rely on Phase 2a matches and textContent matching.
|
|
267
317
|
|
|
318
|
+
// HIGHEST VALUE: Element ID match - this is THE file if it contains the exact ID
|
|
319
|
+
// IDs are unique identifiers and provide the most reliable targeting
|
|
320
|
+
if (el.elementId) {
|
|
321
|
+
const idPattern = new RegExp(`id=["'\`]${el.elementId}["'\`]`);
|
|
322
|
+
if (idPattern.test(content)) {
|
|
323
|
+
score += 500; // VERY HIGH - exact ID match is definitive!
|
|
324
|
+
matches.push(`contains id="${el.elementId}"`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Also check child IDs
|
|
329
|
+
if (el.childIds && el.childIds.length > 0) {
|
|
330
|
+
for (const childId of el.childIds) {
|
|
331
|
+
const idPattern = new RegExp(`id=["'\`]${childId}["'\`]`);
|
|
332
|
+
if (idPattern.test(content)) {
|
|
333
|
+
score += 400; // Very high - child ID match is also definitive
|
|
334
|
+
matches.push(`contains child id="${childId}"`);
|
|
335
|
+
break; // Only count once
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
268
340
|
// VERY HIGH VALUE: Exact text content match (e.g., button says "REFRESH")
|
|
269
341
|
// This is THE file if it contains the exact text the user clicked on
|
|
270
342
|
if (el.textContent && el.textContent.length >= 2) {
|
|
@@ -493,7 +565,7 @@ function searchFilesSmart(
|
|
|
493
565
|
results.set(foundPath, {
|
|
494
566
|
path: foundPath,
|
|
495
567
|
content,
|
|
496
|
-
score:
|
|
568
|
+
score: 1500, // HIGHEST priority - component name matched visible text
|
|
497
569
|
filenameMatch: true
|
|
498
570
|
});
|
|
499
571
|
debugLog("Phase 2a: Found file by component name", { componentName, foundPath });
|
|
@@ -595,17 +667,12 @@ function searchFilesSmart(
|
|
|
595
667
|
return sortedResults.map(r => ({ path: r.path, content: r.content, score: r.score, filenameMatch: r.filenameMatch }));
|
|
596
668
|
}
|
|
597
669
|
|
|
598
|
-
const VISION_SYSTEM_PROMPT = `You
|
|
670
|
+
const VISION_SYSTEM_PROMPT = `You are an expert frontend developer. Edit the code to fulfill the user's request.
|
|
599
671
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
2. Copy code EXACTLY from the file - character for character
|
|
603
|
-
3. Make the SMALLEST possible change
|
|
604
|
-
4. For color changes, just change the className
|
|
605
|
-
5. Do not restructure or reorganize code
|
|
672
|
+
Output ONLY this JSON (no other text):
|
|
673
|
+
{"modifications":[{"filePath":"path","patches":[{"search":"exact original code","replace":"modified code"}]}]}
|
|
606
674
|
|
|
607
|
-
|
|
608
|
-
{"modifications":[{"filePath":"path","patches":[{"search":"exact code","replace":"changed code"}]}]}`;
|
|
675
|
+
The "search" field must match the file EXACTLY (copy-paste from the code provided).`;
|
|
609
676
|
|
|
610
677
|
export async function POST(request: Request) {
|
|
611
678
|
// Only allow in development
|
|
@@ -833,10 +900,15 @@ export async function POST(request: Request) {
|
|
|
833
900
|
// Extract recommended file from component sources (to show first, avoid duplication)
|
|
834
901
|
let recommendedFileContent: { path: string; content: string } | null = null;
|
|
835
902
|
if (recommendedFile) {
|
|
903
|
+
// Check componentSources first
|
|
836
904
|
const idx = pageContext.componentSources.findIndex(c => c.path === recommendedFile.path);
|
|
837
905
|
if (idx !== -1) {
|
|
838
906
|
recommendedFileContent = pageContext.componentSources[idx];
|
|
839
907
|
pageContext.componentSources.splice(idx, 1); // Remove to avoid duplication
|
|
908
|
+
}
|
|
909
|
+
// Fallback: Check if it's the page file itself
|
|
910
|
+
else if (pageContext.pageFile === recommendedFile.path && pageContext.pageContent) {
|
|
911
|
+
recommendedFileContent = { path: pageContext.pageFile, content: pageContext.pageContent };
|
|
840
912
|
}
|
|
841
913
|
}
|
|
842
914
|
|
|
@@ -848,18 +920,50 @@ User Request: "${userPrompt}"
|
|
|
848
920
|
|
|
849
921
|
`;
|
|
850
922
|
|
|
851
|
-
if (focusedElements && focusedElements.length > 0) {
|
|
852
|
-
textContent += `FOCUSED ELEMENTS (user clicked on these):
|
|
853
|
-
${focusedElements.map((el) => `- ${el.name} (${el.type}) at (${el.coordinates.x}, ${el.coordinates.y})`).join("\n")}
|
|
854
|
-
|
|
855
|
-
`;
|
|
856
|
-
}
|
|
857
|
-
|
|
858
923
|
// ========== TARGET COMPONENT ONLY (with line numbers) ==========
|
|
859
924
|
// CRITICAL: Only include the TARGET file to avoid overwhelming the LLM with noise
|
|
860
925
|
if (recommendedFileContent) {
|
|
861
926
|
const content = recommendedFileContent.content;
|
|
862
927
|
|
|
928
|
+
// Search for element IDs in the file to enable precise targeting
|
|
929
|
+
let idMatch: { lineNumber: number; matchedId: string; snippet: string } | null = null;
|
|
930
|
+
if (focusedElements && focusedElements.length > 0) {
|
|
931
|
+
for (const el of focusedElements) {
|
|
932
|
+
idMatch = findElementIdInFile(content, el.elementId, el.childIds);
|
|
933
|
+
if (idMatch) break;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// Build focused elements section with precise targeting info
|
|
938
|
+
if (focusedElements && focusedElements.length > 0) {
|
|
939
|
+
textContent += `FOCUSED ELEMENTS (user clicked on these):\n`;
|
|
940
|
+
for (const el of focusedElements) {
|
|
941
|
+
textContent += `- ${el.name} (${el.type})`;
|
|
942
|
+
if (el.textContent) {
|
|
943
|
+
textContent += ` with text "${el.textContent.substring(0, 30)}"`;
|
|
944
|
+
}
|
|
945
|
+
textContent += `\n`;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// Add precise targeting if we found an ID match
|
|
949
|
+
if (idMatch) {
|
|
950
|
+
textContent += `
|
|
951
|
+
PRECISE TARGET (found by element ID):
|
|
952
|
+
→ ID: "${idMatch.matchedId}"
|
|
953
|
+
→ Line: ${idMatch.lineNumber}
|
|
954
|
+
→ Look for this ID in the code and modify the element that contains it.
|
|
955
|
+
|
|
956
|
+
`;
|
|
957
|
+
debugLog("Found element ID in file", {
|
|
958
|
+
matchedId: idMatch.matchedId,
|
|
959
|
+
lineNumber: idMatch.lineNumber,
|
|
960
|
+
file: recommendedFileContent.path,
|
|
961
|
+
});
|
|
962
|
+
} else {
|
|
963
|
+
textContent += `\n`;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
863
967
|
// Add line numbers to make it easy for LLM to reference exact code
|
|
864
968
|
const linesWithNumbers = content.split('\n').map((line, i) =>
|
|
865
969
|
`${String(i + 1).padStart(4, ' ')}| ${line}`
|
|
@@ -902,8 +1006,33 @@ ${linesWithNumbers}
|
|
|
902
1006
|
usedContext += content.length;
|
|
903
1007
|
}
|
|
904
1008
|
|
|
905
|
-
//
|
|
906
|
-
//
|
|
1009
|
+
// ========== KEY UI COMPONENTS ==========
|
|
1010
|
+
// Include UI component definitions (Button, Card, etc.) so LLM knows available variants/props
|
|
1011
|
+
const uiComponentPaths = ['components/ui/button', 'components/ui/card', 'components/ui/input'];
|
|
1012
|
+
const includedUIComponents: string[] = [];
|
|
1013
|
+
|
|
1014
|
+
for (const comp of pageContext.componentSources) {
|
|
1015
|
+
// Check if this is a UI component we should include
|
|
1016
|
+
const isUIComponent = uiComponentPaths.some(uiPath => comp.path.includes(uiPath));
|
|
1017
|
+
if (isUIComponent && usedContext + comp.content.length < TOTAL_CONTEXT_BUDGET) {
|
|
1018
|
+
// Only include the variants/props section, not the whole file
|
|
1019
|
+
const variantsMatch = comp.content.match(/variants:\s*\{[\s\S]*?\n\s*\},\n\s*defaultVariants/);
|
|
1020
|
+
if (variantsMatch) {
|
|
1021
|
+
textContent += `
|
|
1022
|
+
--- UI Component: ${comp.path} (variants only) ---
|
|
1023
|
+
${variantsMatch[0]}
|
|
1024
|
+
---
|
|
1025
|
+
|
|
1026
|
+
`;
|
|
1027
|
+
includedUIComponents.push(comp.path);
|
|
1028
|
+
usedContext += variantsMatch[0].length + 100;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
if (includedUIComponents.length > 0) {
|
|
1034
|
+
debugLog("Included UI component definitions", { components: includedUIComponents });
|
|
1035
|
+
}
|
|
907
1036
|
|
|
908
1037
|
// ========== THEME DISCOVERY ==========
|
|
909
1038
|
// Discover theme tokens and contrast analysis to help LLM make informed color decisions
|
|
@@ -791,7 +791,13 @@ export function SonanceDevTools() {
|
|
|
791
791
|
const textContent = (el.textContent?.trim() || "").substring(0, 100); // Cap at 100 chars
|
|
792
792
|
const className = el.className?.toString() || "";
|
|
793
793
|
|
|
794
|
-
|
|
794
|
+
// Capture element ID and child IDs for precise code targeting
|
|
795
|
+
const elementId = el.id || undefined;
|
|
796
|
+
const childIds = Array.from(el.querySelectorAll('[id]'))
|
|
797
|
+
.map(child => child.id)
|
|
798
|
+
.filter(id => id) as string[];
|
|
799
|
+
|
|
800
|
+
newTagged.push({ name, rect, type: "component", variantId, textContent, className, elementId, childIds: childIds.length > 0 ? childIds : undefined });
|
|
795
801
|
}
|
|
796
802
|
}
|
|
797
803
|
});
|
|
@@ -834,7 +840,13 @@ export function SonanceDevTools() {
|
|
|
834
840
|
const textContent = (el.textContent?.trim() || "").substring(0, 100); // Cap at 100 chars
|
|
835
841
|
const elClassName = el.className?.toString() || "";
|
|
836
842
|
|
|
837
|
-
|
|
843
|
+
// Capture element ID and child IDs for precise code targeting
|
|
844
|
+
const elementId = el.id || undefined;
|
|
845
|
+
const childIds = Array.from(el.querySelectorAll('[id]'))
|
|
846
|
+
.map(child => child.id)
|
|
847
|
+
.filter(id => id) as string[];
|
|
848
|
+
|
|
849
|
+
newTagged.push({ name: genericName, rect, type: "component", variantId, textContent, className: elClassName, elementId, childIds: childIds.length > 0 ? childIds : undefined });
|
|
838
850
|
}
|
|
839
851
|
});
|
|
840
852
|
});
|
|
@@ -1025,9 +1037,12 @@ export function SonanceDevTools() {
|
|
|
1025
1037
|
width: element.rect.width,
|
|
1026
1038
|
height: element.rect.height,
|
|
1027
1039
|
},
|
|
1028
|
-
//
|
|
1040
|
+
// Capture text and className for dynamic file matching
|
|
1029
1041
|
textContent: element.textContent,
|
|
1030
1042
|
className: element.className,
|
|
1043
|
+
// Capture element ID and child IDs for precise code targeting
|
|
1044
|
+
elementId: element.elementId,
|
|
1045
|
+
childIds: element.childIds,
|
|
1031
1046
|
};
|
|
1032
1047
|
|
|
1033
1048
|
setVisionFocusedElements((prev) => {
|
|
@@ -2393,6 +2408,11 @@ export function SonanceDevTools() {
|
|
|
2393
2408
|
colorScheme: "light",
|
|
2394
2409
|
transform: `translate(${dragPosition.x}px, ${dragPosition.y}px)`,
|
|
2395
2410
|
}}
|
|
2411
|
+
// Event isolation - prevent clicks from closing popups in the main app
|
|
2412
|
+
onMouseDown={(e) => e.stopPropagation()}
|
|
2413
|
+
onClick={(e) => e.stopPropagation()}
|
|
2414
|
+
onFocus={(e) => e.stopPropagation()}
|
|
2415
|
+
onBlur={(e) => e.stopPropagation()}
|
|
2396
2416
|
>
|
|
2397
2417
|
{/* Header - Draggable */}
|
|
2398
2418
|
<div
|
|
@@ -28,6 +28,10 @@ export interface DetectedElement {
|
|
|
28
28
|
className?: string;
|
|
29
29
|
/** Component variant ID (hash of styles/classes) to distinguish visual styles */
|
|
30
30
|
variantId?: string;
|
|
31
|
+
/** The element's DOM id attribute for precise code targeting */
|
|
32
|
+
elementId?: string;
|
|
33
|
+
/** IDs of child elements for more precise targeting */
|
|
34
|
+
childIds?: string[];
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
// Logo asset from the API
|
|
@@ -226,6 +230,10 @@ export interface VisionFocusedElement {
|
|
|
226
230
|
textContent?: string;
|
|
227
231
|
/** The className string of the element for pattern matching */
|
|
228
232
|
className?: string;
|
|
233
|
+
/** The element's DOM id attribute for precise code targeting */
|
|
234
|
+
elementId?: string;
|
|
235
|
+
/** IDs of child elements for more precise targeting */
|
|
236
|
+
childIds?: string[];
|
|
229
237
|
}
|
|
230
238
|
|
|
231
239
|
export interface VisionEditRequest {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sonance-brand-mcp",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.73",
|
|
4
4
|
"description": "MCP Server for Sonance Brand Guidelines and Component Library - gives Claude instant access to brand colors, typography, and UI components.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|