sonance-brand-mcp 1.3.74 → 1.3.76
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.
|
@@ -91,6 +91,39 @@ function debugLog(message: string, data?: unknown) {
|
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Extract JSON from LLM response that may contain preamble text
|
|
96
|
+
* Handles: pure JSON, markdown code fences, and text with embedded JSON
|
|
97
|
+
*/
|
|
98
|
+
function extractJsonFromResponse(text: string): string {
|
|
99
|
+
// Try direct parse first - if it starts with {, it's likely pure JSON
|
|
100
|
+
const trimmed = text.trim();
|
|
101
|
+
if (trimmed.startsWith('{')) return trimmed;
|
|
102
|
+
|
|
103
|
+
// Extract from markdown code fence (```json ... ``` or ``` ... ```)
|
|
104
|
+
const fenceMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
105
|
+
if (fenceMatch) {
|
|
106
|
+
const extracted = fenceMatch[1].trim();
|
|
107
|
+
debugLog("Extracted JSON from markdown fence", { previewLength: extracted.length });
|
|
108
|
+
return extracted;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Find the JSON object in the text (for responses with preamble)
|
|
112
|
+
const jsonStart = text.indexOf('{');
|
|
113
|
+
const jsonEnd = text.lastIndexOf('}');
|
|
114
|
+
if (jsonStart !== -1 && jsonEnd > jsonStart) {
|
|
115
|
+
const extracted = text.slice(jsonStart, jsonEnd + 1);
|
|
116
|
+
debugLog("Extracted JSON from preamble text", {
|
|
117
|
+
preambleLength: jsonStart,
|
|
118
|
+
jsonLength: extracted.length
|
|
119
|
+
});
|
|
120
|
+
return extracted;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Return as-is if no JSON structure found
|
|
124
|
+
return text;
|
|
125
|
+
}
|
|
126
|
+
|
|
94
127
|
/**
|
|
95
128
|
* AST-based syntax validation using Babel parser
|
|
96
129
|
* This catches actual syntax errors, not just tag counting
|
|
@@ -177,6 +210,48 @@ interface ScreenshotAnalysis {
|
|
|
177
210
|
visibleText: string[];
|
|
178
211
|
componentNames: string[];
|
|
179
212
|
codePatterns: string[];
|
|
213
|
+
elementTypes: string[]; // Detected UI element types: table, button, form, card, list, modal, dropdown, input
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Map element types to JSX/HTML patterns to search for in files
|
|
218
|
+
* This is the key to Cursor-style file discovery - search for WHAT we see, not guessed names
|
|
219
|
+
*/
|
|
220
|
+
const ELEMENT_PATTERNS: Record<string, string[]> = {
|
|
221
|
+
table: ['<Table', 'TableRow', 'TableCell', 'TableHeader', 'TableBody', '<table', '<tr', '<td', '<th'],
|
|
222
|
+
button: ['<Button', '<button', 'onClick={', 'handleClick'],
|
|
223
|
+
form: ['<Form', '<form', 'onSubmit', '<Input', '<input', 'handleSubmit'],
|
|
224
|
+
card: ['<Card', 'CardContent', 'CardHeader', 'CardTitle', 'CardDescription'],
|
|
225
|
+
list: ['<List', '<ul', '<li', 'ListItem', '.map(('],
|
|
226
|
+
modal: ['<Modal', '<Dialog', 'DialogContent', 'isOpen', 'onClose', 'onOpenChange'],
|
|
227
|
+
dropdown: ['<Dropdown', '<Select', 'DropdownMenu', 'SelectContent', 'SelectTrigger'],
|
|
228
|
+
input: ['<Input', '<input', '<Textarea', '<textarea', 'onChange={'],
|
|
229
|
+
header: ['<Header', '<header', '<Navbar', '<Nav', 'navigation'],
|
|
230
|
+
sidebar: ['<Sidebar', '<aside', 'SidebarContent', 'NavigationMenu'],
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Score a file based on whether it contains patterns for a specific element type
|
|
235
|
+
* Higher scores indicate the file is more likely to contain the target element
|
|
236
|
+
*/
|
|
237
|
+
function scoreFileByElementType(content: string, elementType: string): number {
|
|
238
|
+
const patterns = ELEMENT_PATTERNS[elementType.toLowerCase()] || [];
|
|
239
|
+
let score = 0;
|
|
240
|
+
let matchCount = 0;
|
|
241
|
+
|
|
242
|
+
for (const pattern of patterns) {
|
|
243
|
+
if (content.includes(pattern)) {
|
|
244
|
+
score += 300; // High score per pattern match
|
|
245
|
+
matchCount++;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Bonus for multiple pattern matches (indicates primary component for this element type)
|
|
250
|
+
if (matchCount >= 3) {
|
|
251
|
+
score += 500;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return score;
|
|
180
255
|
}
|
|
181
256
|
|
|
182
257
|
/**
|
|
@@ -216,15 +291,17 @@ Analyze this UI to help find the correct source file. Return:
|
|
|
216
291
|
1. **visibleText**: Extract the EXACT text visible in UI elements (button labels, headings, tab names, etc.)
|
|
217
292
|
2. **componentNames**: Deduce likely React component names based on what you see (e.g., "ProcessDetailPanel", "UserSettings", "DataTable"). Think about common React naming conventions.
|
|
218
293
|
3. **codePatterns**: Suggest code identifiers that might exist (e.g., "handleEdit", "isLoading", "activeTab", "onDelete")
|
|
294
|
+
4. **elementTypes**: What TYPE of UI element is the user's request about? Pick from: table, button, form, card, list, modal, dropdown, input, header, sidebar. List the most relevant first.
|
|
219
295
|
|
|
220
296
|
Return ONLY valid JSON:
|
|
221
297
|
{
|
|
222
298
|
"visibleText": ["Assets", "Flowchart", "Edit", "Delete"],
|
|
223
299
|
"componentNames": ["ProcessDetailPanel", "ProcessActions", "QuickActions"],
|
|
224
|
-
"codePatterns": ["handleEdit", "handleDelete", "activeTab", "setActiveTab"]
|
|
300
|
+
"codePatterns": ["handleEdit", "handleDelete", "activeTab", "setActiveTab"],
|
|
301
|
+
"elementTypes": ["table", "button"]
|
|
225
302
|
}
|
|
226
303
|
|
|
227
|
-
Be smart - use your knowledge of React patterns to
|
|
304
|
+
Be smart - use your knowledge of React patterns to identify what type of element the user wants to modify.`,
|
|
228
305
|
},
|
|
229
306
|
],
|
|
230
307
|
},
|
|
@@ -234,7 +311,7 @@ Be smart - use your knowledge of React patterns to make educated guesses about c
|
|
|
234
311
|
try {
|
|
235
312
|
const textBlock = response.content.find((block) => block.type === "text");
|
|
236
313
|
if (!textBlock || textBlock.type !== "text") {
|
|
237
|
-
return { visibleText: [], componentNames: [], codePatterns: [] };
|
|
314
|
+
return { visibleText: [], componentNames: [], codePatterns: [], elementTypes: [] };
|
|
238
315
|
}
|
|
239
316
|
|
|
240
317
|
let jsonText = textBlock.text.trim();
|
|
@@ -249,13 +326,14 @@ Be smart - use your knowledge of React patterns to make educated guesses about c
|
|
|
249
326
|
visibleText: parsed.visibleText || parsed.textStrings || [],
|
|
250
327
|
componentNames: parsed.componentNames || [],
|
|
251
328
|
codePatterns: parsed.codePatterns || [],
|
|
329
|
+
elementTypes: parsed.elementTypes || [],
|
|
252
330
|
};
|
|
253
331
|
|
|
254
332
|
debugLog("Phase 1: Analyzed screenshot for search", result);
|
|
255
333
|
return result;
|
|
256
334
|
} catch (e) {
|
|
257
335
|
debugLog("Phase 1: Failed to parse screenshot analysis response", { error: String(e) });
|
|
258
|
-
return { visibleText: [], componentNames: [], codePatterns: [] };
|
|
336
|
+
return { visibleText: [], componentNames: [], codePatterns: [], elementTypes: [] };
|
|
259
337
|
}
|
|
260
338
|
}
|
|
261
339
|
|
|
@@ -547,12 +625,18 @@ function searchFilesSmart(
|
|
|
547
625
|
projectRoot: string,
|
|
548
626
|
maxFiles: number = 10
|
|
549
627
|
): { path: string; content: string; score: number; filenameMatch: boolean }[] {
|
|
550
|
-
const { visibleText, componentNames, codePatterns } = analysis;
|
|
628
|
+
const { visibleText, componentNames, codePatterns, elementTypes } = analysis;
|
|
551
629
|
|
|
552
630
|
// Combine all search terms
|
|
553
631
|
const allSearchTerms = [...visibleText, ...codePatterns];
|
|
554
632
|
|
|
555
|
-
|
|
633
|
+
// Get the primary element type for pattern-based scoring (Cursor-style)
|
|
634
|
+
const primaryElementType = elementTypes?.[0] || null;
|
|
635
|
+
if (primaryElementType) {
|
|
636
|
+
debugLog("Phase 2: Element type detected for pattern matching", { primaryElementType });
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if (allSearchTerms.length === 0 && componentNames.length === 0 && !primaryElementType) return [];
|
|
556
640
|
|
|
557
641
|
const results: Map<string, { path: string; content: string; score: number; filenameMatch: boolean }> = new Map();
|
|
558
642
|
const searchDirs = ["components", "src/components", "app", "src/app", "pages", "src/pages"];
|
|
@@ -619,23 +703,47 @@ function searchFilesSmart(
|
|
|
619
703
|
}
|
|
620
704
|
}
|
|
621
705
|
|
|
706
|
+
// Calculate content score based on text matches
|
|
707
|
+
let contentScore = 0;
|
|
622
708
|
if (uniqueMatches > 0) {
|
|
623
709
|
// Score: unique matches * 10 + total matches
|
|
624
|
-
|
|
625
|
-
|
|
710
|
+
contentScore = (uniqueMatches * 10) + totalMatches;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// CURSOR-STYLE: Add element type pattern scoring
|
|
714
|
+
// Files containing patterns for the detected element type get a big boost
|
|
715
|
+
let elementTypeScore = 0;
|
|
716
|
+
if (primaryElementType) {
|
|
717
|
+
elementTypeScore = scoreFileByElementType(content, primaryElementType);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const totalScore = contentScore + elementTypeScore;
|
|
721
|
+
|
|
722
|
+
if (totalScore > 0) {
|
|
626
723
|
// Check if we already have this file (from component name search)
|
|
627
724
|
const existing = results.get(entryPath);
|
|
628
725
|
if (existing) {
|
|
629
|
-
// Add
|
|
630
|
-
existing.score +=
|
|
726
|
+
// Add scores to existing entry
|
|
727
|
+
existing.score += totalScore;
|
|
631
728
|
} else {
|
|
632
729
|
results.set(entryPath, {
|
|
633
730
|
path: entryPath,
|
|
634
731
|
content,
|
|
635
|
-
score:
|
|
732
|
+
score: totalScore,
|
|
636
733
|
filenameMatch: false
|
|
637
734
|
});
|
|
638
735
|
}
|
|
736
|
+
|
|
737
|
+
// Log high-confidence element type matches
|
|
738
|
+
if (elementTypeScore >= 800) {
|
|
739
|
+
debugLog("Phase 2: High-confidence element type match", {
|
|
740
|
+
file: entryPath,
|
|
741
|
+
elementType: primaryElementType,
|
|
742
|
+
elementScore: elementTypeScore,
|
|
743
|
+
contentScore,
|
|
744
|
+
totalScore
|
|
745
|
+
});
|
|
746
|
+
}
|
|
639
747
|
}
|
|
640
748
|
} catch {
|
|
641
749
|
// Skip files that can't be read
|
|
@@ -673,7 +781,10 @@ function searchFilesSmart(
|
|
|
673
781
|
|
|
674
782
|
const VISION_SYSTEM_PROMPT = `You are an expert frontend developer. Edit the code to fulfill the user's request.
|
|
675
783
|
|
|
676
|
-
|
|
784
|
+
CRITICAL: Return ONLY the JSON object. No explanations, no preamble, no markdown code fences.
|
|
785
|
+
Start your response with { and end with }
|
|
786
|
+
|
|
787
|
+
Output format:
|
|
677
788
|
{"modifications":[{"filePath":"path","patches":[{"search":"exact original code","replace":"modified code"}]}]}
|
|
678
789
|
|
|
679
790
|
The "search" field must match the file EXACTLY (copy-paste from the code provided).`;
|
|
@@ -958,7 +1069,7 @@ User Request: "${userPrompt}"
|
|
|
958
1069
|
|
|
959
1070
|
// Search for element IDs in the file to enable precise targeting
|
|
960
1071
|
let idMatch: { lineNumber: number; matchedId: string; snippet: string } | null = null;
|
|
961
|
-
|
|
1072
|
+
if (focusedElements && focusedElements.length > 0) {
|
|
962
1073
|
for (const el of focusedElements) {
|
|
963
1074
|
idMatch = findElementIdInFile(content, el.elementId, el.childIds);
|
|
964
1075
|
if (idMatch) break;
|
|
@@ -1049,7 +1160,7 @@ ${linesWithNumbers}
|
|
|
1049
1160
|
// Only include the variants/props section, not the whole file
|
|
1050
1161
|
const variantsMatch = comp.content.match(/variants:\s*\{[\s\S]*?\n\s*\},\n\s*defaultVariants/);
|
|
1051
1162
|
if (variantsMatch) {
|
|
1052
|
-
|
|
1163
|
+
textContent += `
|
|
1053
1164
|
--- UI Component: ${comp.path} (variants only) ---
|
|
1054
1165
|
${variantsMatch[0]}
|
|
1055
1166
|
---
|
|
@@ -1123,7 +1234,7 @@ CRITICAL: Your "search" string MUST exist in the file. If you can't find the exa
|
|
|
1123
1234
|
|
|
1124
1235
|
// Call Claude Vision API with retry mechanism
|
|
1125
1236
|
const anthropic = new Anthropic({ apiKey });
|
|
1126
|
-
|
|
1237
|
+
|
|
1127
1238
|
// Build set of valid file paths from page context (needed for validation)
|
|
1128
1239
|
const validFilePaths = new Set<string>();
|
|
1129
1240
|
if (pageContext.pageFile) {
|
|
@@ -1220,62 +1331,8 @@ This is better than generating patches with made-up code.`,
|
|
|
1220
1331
|
};
|
|
1221
1332
|
|
|
1222
1333
|
try {
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
// Try to extract JSON from markdown code blocks
|
|
1226
|
-
const jsonMatch = jsonText.match(/```json\n([\s\S]*?)\n```/) ||
|
|
1227
|
-
jsonText.match(/```\n([\s\S]*?)\n```/);
|
|
1228
|
-
|
|
1229
|
-
if (jsonMatch) {
|
|
1230
|
-
jsonText = jsonMatch[1];
|
|
1231
|
-
} else if (jsonText.includes("```json")) {
|
|
1232
|
-
const start = jsonText.indexOf("```json") + 7;
|
|
1233
|
-
const end = jsonText.lastIndexOf("```");
|
|
1234
|
-
if (end > start) {
|
|
1235
|
-
jsonText = jsonText.substring(start, end);
|
|
1236
|
-
}
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
jsonText = jsonText.trim();
|
|
1240
|
-
|
|
1241
|
-
// Robust JSON extraction: look for {"modifications" pattern specifically
|
|
1242
|
-
// This handles cases where the LLM includes preamble text with code blocks
|
|
1243
|
-
const jsonStartPatterns = ['{"modifications"', '{ "modifications"', '{\n "modifications"'];
|
|
1244
|
-
let jsonStart = -1;
|
|
1245
|
-
|
|
1246
|
-
for (const pattern of jsonStartPatterns) {
|
|
1247
|
-
const idx = jsonText.indexOf(pattern);
|
|
1248
|
-
if (idx !== -1 && (jsonStart === -1 || idx < jsonStart)) {
|
|
1249
|
-
jsonStart = idx;
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
if (jsonStart !== -1) {
|
|
1254
|
-
// Find the matching closing brace by counting braces
|
|
1255
|
-
let braceCount = 0;
|
|
1256
|
-
let jsonEnd = -1;
|
|
1257
|
-
for (let i = jsonStart; i < jsonText.length; i++) {
|
|
1258
|
-
if (jsonText[i] === '{') braceCount++;
|
|
1259
|
-
if (jsonText[i] === '}') {
|
|
1260
|
-
braceCount--;
|
|
1261
|
-
if (braceCount === 0) {
|
|
1262
|
-
jsonEnd = i;
|
|
1263
|
-
break;
|
|
1264
|
-
}
|
|
1265
|
-
}
|
|
1266
|
-
}
|
|
1267
|
-
if (jsonEnd !== -1) {
|
|
1268
|
-
jsonText = jsonText.substring(jsonStart, jsonEnd + 1);
|
|
1269
|
-
}
|
|
1270
|
-
} else {
|
|
1271
|
-
// Fallback: try first { to last }
|
|
1272
|
-
const firstBrace = jsonText.indexOf('{');
|
|
1273
|
-
const lastBrace = jsonText.lastIndexOf('}');
|
|
1274
|
-
if (firstBrace !== -1 && lastBrace > firstBrace) {
|
|
1275
|
-
jsonText = jsonText.substring(firstBrace, lastBrace + 1);
|
|
1276
|
-
}
|
|
1277
|
-
}
|
|
1278
|
-
|
|
1334
|
+
// Use robust JSON extraction that handles preamble text, code fences, etc.
|
|
1335
|
+
const jsonText = extractJsonFromResponse(textResponse.text);
|
|
1279
1336
|
aiResponse = JSON.parse(jsonText);
|
|
1280
1337
|
} catch {
|
|
1281
1338
|
console.error("Failed to parse AI response:", textResponse.text);
|
|
@@ -87,6 +87,39 @@ function debugLog(message: string, data?: unknown) {
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Extract JSON from LLM response that may contain preamble text
|
|
92
|
+
* Handles: pure JSON, markdown code fences, and text with embedded JSON
|
|
93
|
+
*/
|
|
94
|
+
function extractJsonFromResponse(text: string): string {
|
|
95
|
+
// Try direct parse first - if it starts with {, it's likely pure JSON
|
|
96
|
+
const trimmed = text.trim();
|
|
97
|
+
if (trimmed.startsWith('{')) return trimmed;
|
|
98
|
+
|
|
99
|
+
// Extract from markdown code fence (```json ... ``` or ``` ... ```)
|
|
100
|
+
const fenceMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
101
|
+
if (fenceMatch) {
|
|
102
|
+
const extracted = fenceMatch[1].trim();
|
|
103
|
+
debugLog("Extracted JSON from markdown fence", { previewLength: extracted.length });
|
|
104
|
+
return extracted;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Find the JSON object in the text (for responses with preamble)
|
|
108
|
+
const jsonStart = text.indexOf('{');
|
|
109
|
+
const jsonEnd = text.lastIndexOf('}');
|
|
110
|
+
if (jsonStart !== -1 && jsonEnd > jsonStart) {
|
|
111
|
+
const extracted = text.slice(jsonStart, jsonEnd + 1);
|
|
112
|
+
debugLog("Extracted JSON from preamble text", {
|
|
113
|
+
preambleLength: jsonStart,
|
|
114
|
+
jsonLength: extracted.length
|
|
115
|
+
});
|
|
116
|
+
return extracted;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Return as-is if no JSON structure found
|
|
120
|
+
return text;
|
|
121
|
+
}
|
|
122
|
+
|
|
90
123
|
/**
|
|
91
124
|
* AST-based syntax validation using Babel parser
|
|
92
125
|
* This catches actual syntax errors, not just tag counting
|
|
@@ -173,6 +206,48 @@ interface ScreenshotAnalysis {
|
|
|
173
206
|
visibleText: string[];
|
|
174
207
|
componentNames: string[];
|
|
175
208
|
codePatterns: string[];
|
|
209
|
+
elementTypes: string[]; // Detected UI element types: table, button, form, card, list, modal, dropdown, input
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Map element types to JSX/HTML patterns to search for in files
|
|
214
|
+
* This is the key to Cursor-style file discovery - search for WHAT we see, not guessed names
|
|
215
|
+
*/
|
|
216
|
+
const ELEMENT_PATTERNS: Record<string, string[]> = {
|
|
217
|
+
table: ['<Table', 'TableRow', 'TableCell', 'TableHeader', 'TableBody', '<table', '<tr', '<td', '<th'],
|
|
218
|
+
button: ['<Button', '<button', 'onClick={', 'handleClick'],
|
|
219
|
+
form: ['<Form', '<form', 'onSubmit', '<Input', '<input', 'handleSubmit'],
|
|
220
|
+
card: ['<Card', 'CardContent', 'CardHeader', 'CardTitle', 'CardDescription'],
|
|
221
|
+
list: ['<List', '<ul', '<li', 'ListItem', '.map(('],
|
|
222
|
+
modal: ['<Modal', '<Dialog', 'DialogContent', 'isOpen', 'onClose', 'onOpenChange'],
|
|
223
|
+
dropdown: ['<Dropdown', '<Select', 'DropdownMenu', 'SelectContent', 'SelectTrigger'],
|
|
224
|
+
input: ['<Input', '<input', '<Textarea', '<textarea', 'onChange={'],
|
|
225
|
+
header: ['<Header', '<header', '<Navbar', '<Nav', 'navigation'],
|
|
226
|
+
sidebar: ['<Sidebar', '<aside', 'SidebarContent', 'NavigationMenu'],
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Score a file based on whether it contains patterns for a specific element type
|
|
231
|
+
* Higher scores indicate the file is more likely to contain the target element
|
|
232
|
+
*/
|
|
233
|
+
function scoreFileByElementType(content: string, elementType: string): number {
|
|
234
|
+
const patterns = ELEMENT_PATTERNS[elementType.toLowerCase()] || [];
|
|
235
|
+
let score = 0;
|
|
236
|
+
let matchCount = 0;
|
|
237
|
+
|
|
238
|
+
for (const pattern of patterns) {
|
|
239
|
+
if (content.includes(pattern)) {
|
|
240
|
+
score += 300; // High score per pattern match
|
|
241
|
+
matchCount++;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Bonus for multiple pattern matches (indicates primary component for this element type)
|
|
246
|
+
if (matchCount >= 3) {
|
|
247
|
+
score += 500;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return score;
|
|
176
251
|
}
|
|
177
252
|
|
|
178
253
|
/**
|
|
@@ -212,15 +287,17 @@ Analyze this UI to help find the correct source file. Return:
|
|
|
212
287
|
1. **visibleText**: Extract the EXACT text visible in UI elements (button labels, headings, tab names, etc.)
|
|
213
288
|
2. **componentNames**: Deduce likely React component names based on what you see (e.g., "ProcessDetailPanel", "UserSettings", "DataTable"). Think about common React naming conventions.
|
|
214
289
|
3. **codePatterns**: Suggest code identifiers that might exist (e.g., "handleEdit", "isLoading", "activeTab", "onDelete")
|
|
290
|
+
4. **elementTypes**: What TYPE of UI element is the user's request about? Pick from: table, button, form, card, list, modal, dropdown, input, header, sidebar. List the most relevant first.
|
|
215
291
|
|
|
216
292
|
Return ONLY valid JSON:
|
|
217
293
|
{
|
|
218
294
|
"visibleText": ["Assets", "Flowchart", "Edit", "Delete"],
|
|
219
295
|
"componentNames": ["ProcessDetailPanel", "ProcessActions", "QuickActions"],
|
|
220
|
-
"codePatterns": ["handleEdit", "handleDelete", "activeTab", "setActiveTab"]
|
|
296
|
+
"codePatterns": ["handleEdit", "handleDelete", "activeTab", "setActiveTab"],
|
|
297
|
+
"elementTypes": ["table", "button"]
|
|
221
298
|
}
|
|
222
299
|
|
|
223
|
-
Be smart - use your knowledge of React patterns to
|
|
300
|
+
Be smart - use your knowledge of React patterns to identify what type of element the user wants to modify.`,
|
|
224
301
|
},
|
|
225
302
|
],
|
|
226
303
|
},
|
|
@@ -230,7 +307,7 @@ Be smart - use your knowledge of React patterns to make educated guesses about c
|
|
|
230
307
|
try {
|
|
231
308
|
const textBlock = response.content.find((block) => block.type === "text");
|
|
232
309
|
if (!textBlock || textBlock.type !== "text") {
|
|
233
|
-
return { visibleText: [], componentNames: [], codePatterns: [] };
|
|
310
|
+
return { visibleText: [], componentNames: [], codePatterns: [], elementTypes: [] };
|
|
234
311
|
}
|
|
235
312
|
|
|
236
313
|
let jsonText = textBlock.text.trim();
|
|
@@ -245,13 +322,14 @@ Be smart - use your knowledge of React patterns to make educated guesses about c
|
|
|
245
322
|
visibleText: parsed.visibleText || parsed.textStrings || [],
|
|
246
323
|
componentNames: parsed.componentNames || [],
|
|
247
324
|
codePatterns: parsed.codePatterns || [],
|
|
325
|
+
elementTypes: parsed.elementTypes || [],
|
|
248
326
|
};
|
|
249
327
|
|
|
250
328
|
debugLog("Phase 1: Analyzed screenshot for search", result);
|
|
251
329
|
return result;
|
|
252
330
|
} catch (e) {
|
|
253
331
|
debugLog("Phase 1: Failed to parse screenshot analysis response", { error: String(e) });
|
|
254
|
-
return { visibleText: [], componentNames: [], codePatterns: [] };
|
|
332
|
+
return { visibleText: [], componentNames: [], codePatterns: [], elementTypes: [] };
|
|
255
333
|
}
|
|
256
334
|
}
|
|
257
335
|
|
|
@@ -543,12 +621,18 @@ function searchFilesSmart(
|
|
|
543
621
|
projectRoot: string,
|
|
544
622
|
maxFiles: number = 10
|
|
545
623
|
): { path: string; content: string; score: number; filenameMatch: boolean }[] {
|
|
546
|
-
const { visibleText, componentNames, codePatterns } = analysis;
|
|
624
|
+
const { visibleText, componentNames, codePatterns, elementTypes } = analysis;
|
|
547
625
|
|
|
548
626
|
// Combine all search terms
|
|
549
627
|
const allSearchTerms = [...visibleText, ...codePatterns];
|
|
550
628
|
|
|
551
|
-
|
|
629
|
+
// Get the primary element type for pattern-based scoring (Cursor-style)
|
|
630
|
+
const primaryElementType = elementTypes?.[0] || null;
|
|
631
|
+
if (primaryElementType) {
|
|
632
|
+
debugLog("Phase 2: Element type detected for pattern matching", { primaryElementType });
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (allSearchTerms.length === 0 && componentNames.length === 0 && !primaryElementType) return [];
|
|
552
636
|
|
|
553
637
|
const results: Map<string, { path: string; content: string; score: number; filenameMatch: boolean }> = new Map();
|
|
554
638
|
const searchDirs = ["components", "src/components", "app", "src/app", "pages", "src/pages"];
|
|
@@ -615,23 +699,47 @@ function searchFilesSmart(
|
|
|
615
699
|
}
|
|
616
700
|
}
|
|
617
701
|
|
|
702
|
+
// Calculate content score based on text matches
|
|
703
|
+
let contentScore = 0;
|
|
618
704
|
if (uniqueMatches > 0) {
|
|
619
705
|
// Score: unique matches * 10 + total matches
|
|
620
|
-
|
|
621
|
-
|
|
706
|
+
contentScore = (uniqueMatches * 10) + totalMatches;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// CURSOR-STYLE: Add element type pattern scoring
|
|
710
|
+
// Files containing patterns for the detected element type get a big boost
|
|
711
|
+
let elementTypeScore = 0;
|
|
712
|
+
if (primaryElementType) {
|
|
713
|
+
elementTypeScore = scoreFileByElementType(content, primaryElementType);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const totalScore = contentScore + elementTypeScore;
|
|
717
|
+
|
|
718
|
+
if (totalScore > 0) {
|
|
622
719
|
// Check if we already have this file (from component name search)
|
|
623
720
|
const existing = results.get(entryPath);
|
|
624
721
|
if (existing) {
|
|
625
|
-
// Add
|
|
626
|
-
existing.score +=
|
|
722
|
+
// Add scores to existing entry
|
|
723
|
+
existing.score += totalScore;
|
|
627
724
|
} else {
|
|
628
725
|
results.set(entryPath, {
|
|
629
726
|
path: entryPath,
|
|
630
727
|
content,
|
|
631
|
-
score:
|
|
728
|
+
score: totalScore,
|
|
632
729
|
filenameMatch: false
|
|
633
730
|
});
|
|
634
731
|
}
|
|
732
|
+
|
|
733
|
+
// Log high-confidence element type matches
|
|
734
|
+
if (elementTypeScore >= 800) {
|
|
735
|
+
debugLog("Phase 2: High-confidence element type match", {
|
|
736
|
+
file: entryPath,
|
|
737
|
+
elementType: primaryElementType,
|
|
738
|
+
elementScore: elementTypeScore,
|
|
739
|
+
contentScore,
|
|
740
|
+
totalScore
|
|
741
|
+
});
|
|
742
|
+
}
|
|
635
743
|
}
|
|
636
744
|
} catch {
|
|
637
745
|
// Skip files that can't be read
|
|
@@ -669,7 +777,10 @@ function searchFilesSmart(
|
|
|
669
777
|
|
|
670
778
|
const VISION_SYSTEM_PROMPT = `You are an expert frontend developer. Edit the code to fulfill the user's request.
|
|
671
779
|
|
|
672
|
-
|
|
780
|
+
CRITICAL: Return ONLY the JSON object. No explanations, no preamble, no markdown code fences.
|
|
781
|
+
Start your response with { and end with }
|
|
782
|
+
|
|
783
|
+
Output format:
|
|
673
784
|
{"modifications":[{"filePath":"path","patches":[{"search":"exact original code","replace":"modified code"}]}]}
|
|
674
785
|
|
|
675
786
|
The "search" field must match the file EXACTLY (copy-paste from the code provided).`;
|
|
@@ -927,7 +1038,7 @@ User Request: "${userPrompt}"
|
|
|
927
1038
|
|
|
928
1039
|
// Search for element IDs in the file to enable precise targeting
|
|
929
1040
|
let idMatch: { lineNumber: number; matchedId: string; snippet: string } | null = null;
|
|
930
|
-
|
|
1041
|
+
if (focusedElements && focusedElements.length > 0) {
|
|
931
1042
|
for (const el of focusedElements) {
|
|
932
1043
|
idMatch = findElementIdInFile(content, el.elementId, el.childIds);
|
|
933
1044
|
if (idMatch) break;
|
|
@@ -1018,7 +1129,7 @@ ${linesWithNumbers}
|
|
|
1018
1129
|
// Only include the variants/props section, not the whole file
|
|
1019
1130
|
const variantsMatch = comp.content.match(/variants:\s*\{[\s\S]*?\n\s*\},\n\s*defaultVariants/);
|
|
1020
1131
|
if (variantsMatch) {
|
|
1021
|
-
|
|
1132
|
+
textContent += `
|
|
1022
1133
|
--- UI Component: ${comp.path} (variants only) ---
|
|
1023
1134
|
${variantsMatch[0]}
|
|
1024
1135
|
---
|
|
@@ -1092,7 +1203,7 @@ CRITICAL: Your "search" string MUST exist in the file. If you can't find the exa
|
|
|
1092
1203
|
|
|
1093
1204
|
// Call Claude Vision API with retry mechanism
|
|
1094
1205
|
const anthropic = new Anthropic({ apiKey });
|
|
1095
|
-
|
|
1206
|
+
|
|
1096
1207
|
// Build list of known file paths (for logging)
|
|
1097
1208
|
const knownPaths = new Set<string>();
|
|
1098
1209
|
if (pageContext.pageFile) {
|
|
@@ -1185,64 +1296,8 @@ This is better than generating patches with made-up code.`,
|
|
|
1185
1296
|
};
|
|
1186
1297
|
|
|
1187
1298
|
try {
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
// Try to extract JSON from markdown code blocks
|
|
1191
|
-
const jsonMatch = jsonText.match(/```json\n([\s\S]*?)\n```/) ||
|
|
1192
|
-
jsonText.match(/```\n([\s\S]*?)\n```/);
|
|
1193
|
-
|
|
1194
|
-
if (jsonMatch) {
|
|
1195
|
-
jsonText = jsonMatch[1];
|
|
1196
|
-
} else if (jsonText.includes("```json")) {
|
|
1197
|
-
// Fallback for cases where regex might miss due to newlines
|
|
1198
|
-
const start = jsonText.indexOf("```json") + 7;
|
|
1199
|
-
const end = jsonText.lastIndexOf("```");
|
|
1200
|
-
if (end > start) {
|
|
1201
|
-
jsonText = jsonText.substring(start, end);
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
// Clean up any remaining whitespace
|
|
1206
|
-
jsonText = jsonText.trim();
|
|
1207
|
-
|
|
1208
|
-
// Robust JSON extraction: look for {"modifications" pattern specifically
|
|
1209
|
-
// This handles cases where the LLM includes preamble text with code blocks
|
|
1210
|
-
const jsonStartPatterns = ['{"modifications"', '{ "modifications"', '{\n "modifications"'];
|
|
1211
|
-
let jsonStart = -1;
|
|
1212
|
-
|
|
1213
|
-
for (const pattern of jsonStartPatterns) {
|
|
1214
|
-
const idx = jsonText.indexOf(pattern);
|
|
1215
|
-
if (idx !== -1 && (jsonStart === -1 || idx < jsonStart)) {
|
|
1216
|
-
jsonStart = idx;
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
if (jsonStart !== -1) {
|
|
1221
|
-
// Find the matching closing brace by counting braces
|
|
1222
|
-
let braceCount = 0;
|
|
1223
|
-
let jsonEnd = -1;
|
|
1224
|
-
for (let i = jsonStart; i < jsonText.length; i++) {
|
|
1225
|
-
if (jsonText[i] === '{') braceCount++;
|
|
1226
|
-
if (jsonText[i] === '}') {
|
|
1227
|
-
braceCount--;
|
|
1228
|
-
if (braceCount === 0) {
|
|
1229
|
-
jsonEnd = i;
|
|
1230
|
-
break;
|
|
1231
|
-
}
|
|
1232
|
-
}
|
|
1233
|
-
}
|
|
1234
|
-
if (jsonEnd !== -1) {
|
|
1235
|
-
jsonText = jsonText.substring(jsonStart, jsonEnd + 1);
|
|
1236
|
-
}
|
|
1237
|
-
} else {
|
|
1238
|
-
// Fallback: try first { to last }
|
|
1239
|
-
const firstBrace = jsonText.indexOf('{');
|
|
1240
|
-
const lastBrace = jsonText.lastIndexOf('}');
|
|
1241
|
-
if (firstBrace !== -1 && lastBrace > firstBrace) {
|
|
1242
|
-
jsonText = jsonText.substring(firstBrace, lastBrace + 1);
|
|
1243
|
-
}
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1299
|
+
// Use robust JSON extraction that handles preamble text, code fences, etc.
|
|
1300
|
+
const jsonText = extractJsonFromResponse(textResponse.text);
|
|
1246
1301
|
aiResponse = JSON.parse(jsonText);
|
|
1247
1302
|
} catch {
|
|
1248
1303
|
console.error("Failed to parse AI response:", textResponse.text);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sonance-brand-mcp",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.76",
|
|
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",
|