sonance-brand-mcp 1.3.58 → 1.3.60
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.
|
@@ -171,6 +171,73 @@ Be smart - use your knowledge of React patterns to make educated guesses about c
|
|
|
171
171
|
}
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Search candidate files for JSX code matching the focused element
|
|
176
|
+
* This helps identify which file actually contains the element the user clicked on
|
|
177
|
+
*/
|
|
178
|
+
function findFilesContainingElement(
|
|
179
|
+
focusedElements: VisionFocusedElement[] | undefined,
|
|
180
|
+
candidateFiles: { path: string; content: string }[]
|
|
181
|
+
): { path: string; score: number; matches: string[] }[] {
|
|
182
|
+
if (!focusedElements || focusedElements.length === 0) {
|
|
183
|
+
return [];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const results: { path: string; score: number; matches: string[] }[] = [];
|
|
187
|
+
|
|
188
|
+
for (const file of candidateFiles) {
|
|
189
|
+
let score = 0;
|
|
190
|
+
const matches: string[] = [];
|
|
191
|
+
const content = file.content.toLowerCase();
|
|
192
|
+
|
|
193
|
+
for (const el of focusedElements) {
|
|
194
|
+
// Extract element type from the name (e.g., "button #123" -> "button")
|
|
195
|
+
const elementType = el.name.split(/[\s#]/)[0].toLowerCase();
|
|
196
|
+
const componentType = el.type.toLowerCase();
|
|
197
|
+
|
|
198
|
+
// Search for JSX patterns
|
|
199
|
+
// Check for HTML tags like <button, <div, etc.
|
|
200
|
+
if (content.includes(`<${elementType}`)) {
|
|
201
|
+
score += 10;
|
|
202
|
+
matches.push(`<${elementType}>`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Check for React component like <Button, <Card, etc.
|
|
206
|
+
const capitalizedType = el.type.charAt(0).toUpperCase() + el.type.slice(1);
|
|
207
|
+
if (file.content.includes(`<${capitalizedType}`)) {
|
|
208
|
+
score += 15;
|
|
209
|
+
matches.push(`<${capitalizedType}>`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Check for component imports
|
|
213
|
+
if (content.includes(`import`) && content.includes(componentType)) {
|
|
214
|
+
score += 5;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Check for event handlers commonly associated with buttons
|
|
218
|
+
if (elementType === "button" || componentType === "button") {
|
|
219
|
+
if (content.includes("onclick") || content.includes("onpress")) {
|
|
220
|
+
score += 3;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Check for the component being defined in this file
|
|
225
|
+
if (file.content.includes(`function ${capitalizedType}`) ||
|
|
226
|
+
file.content.includes(`const ${capitalizedType}`)) {
|
|
227
|
+
score += 20;
|
|
228
|
+
matches.push(`defines ${capitalizedType}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (score > 0) {
|
|
233
|
+
results.push({ path: file.path, score, matches });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Sort by score descending
|
|
238
|
+
return results.sort((a, b) => b.score - a.score);
|
|
239
|
+
}
|
|
240
|
+
|
|
174
241
|
/**
|
|
175
242
|
* Phase 3: Ask LLM to select the best file from actual file list
|
|
176
243
|
* This replaces guessing - the LLM sees the real filenames and picks one
|
|
@@ -179,7 +246,8 @@ async function selectBestFileFromList(
|
|
|
179
246
|
screenshot: string,
|
|
180
247
|
userPrompt: string,
|
|
181
248
|
candidateFiles: string[],
|
|
182
|
-
apiKey: string
|
|
249
|
+
apiKey: string,
|
|
250
|
+
focusedElementHints?: { path: string; score: number; matches: string[] }[]
|
|
183
251
|
): Promise<string | null> {
|
|
184
252
|
if (candidateFiles.length === 0) return null;
|
|
185
253
|
|
|
@@ -203,7 +271,12 @@ async function selectBestFileFromList(
|
|
|
203
271
|
|
|
204
272
|
Here are the actual component files found in this codebase:
|
|
205
273
|
${candidateFiles.map((f, i) => `${i + 1}. ${f}`).join('\n')}
|
|
274
|
+
${focusedElementHints && focusedElementHints.length > 0 ? `
|
|
275
|
+
FOCUSED ELEMENT ANALYSIS - Files that contain the element the user clicked on:
|
|
276
|
+
${focusedElementHints.slice(0, 3).map(h => `- ${h.path} (confidence: ${h.score}, found: ${h.matches.join(", ")})`).join('\n')}
|
|
206
277
|
|
|
278
|
+
IMPORTANT: Prefer files from the FOCUSED ELEMENT ANALYSIS above, as they contain the actual element the user wants to modify.
|
|
279
|
+
` : ''}
|
|
207
280
|
Looking at the screenshot, which file MOST LIKELY contains the UI elements the user wants to modify?
|
|
208
281
|
|
|
209
282
|
IMPORTANT: Return ONLY the exact file path from the list above (e.g., "components/ProcessCatalogue/ProcessDetailPanel.tsx").
|
|
@@ -438,41 +511,81 @@ function searchFilesSmart(
|
|
|
438
511
|
return sortedResults.map(r => ({ path: r.path, content: r.content, score: r.score }));
|
|
439
512
|
}
|
|
440
513
|
|
|
441
|
-
const VISION_SYSTEM_PROMPT = `You are a code editor.
|
|
514
|
+
const VISION_SYSTEM_PROMPT = `You are a code editor like Cursor. Before making changes, REASON about what needs to be done.
|
|
515
|
+
|
|
516
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
517
|
+
STEP 1: ANALYZE (Think like Cursor)
|
|
518
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
519
|
+
|
|
520
|
+
Before writing any patches, analyze the screenshot and answer:
|
|
521
|
+
|
|
522
|
+
1. CURRENT STATE: What do I see in the screenshot?
|
|
523
|
+
- Is the element visible/readable right now?
|
|
524
|
+
- What colors/styles are currently applied?
|
|
525
|
+
|
|
526
|
+
2. PROBLEM IDENTIFICATION: What's actually wrong?
|
|
527
|
+
- Contrast issue? (text blending with background)
|
|
528
|
+
- Hidden element? (not showing at all)
|
|
529
|
+
- Hover state issue? (only visible on interaction)
|
|
530
|
+
- Layout issue? (wrong size/position)
|
|
531
|
+
- Or is it already correct?
|
|
532
|
+
|
|
533
|
+
3. FIX STRATEGY: What should I change?
|
|
534
|
+
- DEFAULT state only (element needs to look different normally)
|
|
535
|
+
- HOVER state only (element needs different hover effect)
|
|
536
|
+
- BOTH states (element needs overall visibility improvement)
|
|
537
|
+
- ASK for clarification (request is too vague)
|
|
538
|
+
|
|
539
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
540
|
+
STEP 2: GENERATE PATCHES (Only after reasoning)
|
|
541
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
542
|
+
|
|
543
|
+
CRITICAL - FILE SELECTION:
|
|
544
|
+
- You MUST modify the file marked "TARGET COMPONENT" - this is the file you have FULL visibility into
|
|
545
|
+
- Do NOT try to modify other files listed in context - you only see their imports/exports, not full content
|
|
546
|
+
- If the TARGET COMPONENT is not the right file, return clarification_needed
|
|
547
|
+
|
|
548
|
+
COPY EXACTLY from the TARGET COMPONENT section:
|
|
549
|
+
- Your "search" string must match the file CHARACTER-FOR-CHARACTER
|
|
550
|
+
- Include exact indentation and at least 3 lines of context
|
|
551
|
+
- If the element looks FINE in the screenshot, say so
|
|
442
552
|
|
|
443
553
|
RULES:
|
|
444
|
-
1. Make the SMALLEST possible change
|
|
445
|
-
2. Do NOT refactor
|
|
446
|
-
3.
|
|
447
|
-
4.
|
|
448
|
-
5. If
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
- If code references database values (like icon_name, type, status), DO NOT change the mapping keys
|
|
456
|
-
- You cannot see database content - only change STRUCTURE, not DATA MAPPINGS
|
|
457
|
-
- Example: If you see iconMap["EyeOff"] = SomeIcon, do NOT change "EyeOff" to something else
|
|
458
|
-
- If user wants different icons, they must tell you the EXACT icon names they want
|
|
459
|
-
|
|
460
|
-
PATCH FORMAT:
|
|
461
|
-
Return ONLY raw JSON (no markdown, no preamble):
|
|
554
|
+
1. Make the SMALLEST possible change
|
|
555
|
+
2. Do NOT refactor or "improve" other code
|
|
556
|
+
3. NEVER invent code - COPY exact code from file
|
|
557
|
+
4. NEVER change data mappings unless user provides new values
|
|
558
|
+
5. If visibility issue: prefer explicit colors (text-white, text-black) over semantic tokens
|
|
559
|
+
|
|
560
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
561
|
+
RESPONSE FORMAT
|
|
562
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
563
|
+
|
|
564
|
+
Return ONLY raw JSON:
|
|
462
565
|
{
|
|
463
|
-
"
|
|
566
|
+
"analysis": {
|
|
567
|
+
"currentState": "what I see in the screenshot",
|
|
568
|
+
"problem": "the actual issue (or 'none - element appears correct')",
|
|
569
|
+
"fixStrategy": "default|hover|both|clarification_needed"
|
|
570
|
+
},
|
|
571
|
+
"reasoning": "my diagnosis and approach",
|
|
464
572
|
"modifications": [{
|
|
465
573
|
"filePath": "path/to/file.tsx",
|
|
466
574
|
"patches": [{
|
|
467
|
-
"search": "
|
|
468
|
-
"replace": "
|
|
469
|
-
"explanation": "what this
|
|
575
|
+
"search": "EXACT copy from file",
|
|
576
|
+
"replace": "changed code",
|
|
577
|
+
"explanation": "what this changes and why"
|
|
470
578
|
}]
|
|
471
579
|
}],
|
|
472
|
-
"explanation": "summary"
|
|
580
|
+
"explanation": "summary of what was changed"
|
|
473
581
|
}
|
|
474
582
|
|
|
475
|
-
If
|
|
583
|
+
If the element looks correct, return:
|
|
584
|
+
{
|
|
585
|
+
"analysis": { "currentState": "...", "problem": "none", "fixStrategy": "clarification_needed" },
|
|
586
|
+
"modifications": [],
|
|
587
|
+
"explanation": "The element appears visible/correct in the screenshot. Please specify what needs to change."
|
|
588
|
+
}`;
|
|
476
589
|
|
|
477
590
|
export async function POST(request: Request) {
|
|
478
591
|
// Only allow in development
|
|
@@ -604,6 +717,22 @@ export async function POST(request: Request) {
|
|
|
604
717
|
const searchResults = searchFilesSmart(analysis, projectRoot, 10);
|
|
605
718
|
smartSearchFiles = searchResults.map(r => ({ path: r.path, content: r.content }));
|
|
606
719
|
|
|
720
|
+
// PHASE 2.5: Find which files contain the focused element
|
|
721
|
+
const focusedElementHints = findFilesContainingElement(
|
|
722
|
+
focusedElements,
|
|
723
|
+
searchResults.map(r => ({ path: r.path, content: r.content }))
|
|
724
|
+
);
|
|
725
|
+
|
|
726
|
+
if (focusedElementHints.length > 0) {
|
|
727
|
+
debugLog("Focused element search results", {
|
|
728
|
+
topMatches: focusedElementHints.slice(0, 3).map(h => ({
|
|
729
|
+
path: h.path,
|
|
730
|
+
score: h.score,
|
|
731
|
+
matches: h.matches
|
|
732
|
+
}))
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
|
|
607
736
|
// PHASE 3: Ask LLM to pick the best file from actual file list
|
|
608
737
|
if (searchResults.length > 0) {
|
|
609
738
|
const candidateFiles = searchResults.slice(0, 10).map(r => r.path);
|
|
@@ -611,7 +740,8 @@ export async function POST(request: Request) {
|
|
|
611
740
|
screenshot,
|
|
612
741
|
userPrompt,
|
|
613
742
|
candidateFiles,
|
|
614
|
-
apiKey
|
|
743
|
+
apiKey,
|
|
744
|
+
focusedElementHints
|
|
615
745
|
);
|
|
616
746
|
|
|
617
747
|
if (selectedFile) {
|
|
@@ -778,30 +908,59 @@ ${truncatedContent}${wasTruncated ? "\n// ... (truncated)" : ""}
|
|
|
778
908
|
|
|
779
909
|
// ========== THEME DISCOVERY (REFERENCE ONLY) ==========
|
|
780
910
|
// Dynamically discover theme tokens from the target codebase
|
|
781
|
-
// This is marked as REFERENCE ONLY so the LLM doesn't use it to justify extra changes
|
|
782
911
|
const discoveredTheme = await discoverTheme(projectRoot);
|
|
783
912
|
const themeContext = formatThemeForPrompt(discoveredTheme);
|
|
784
913
|
|
|
914
|
+
// Check if this is a visibility-related request
|
|
915
|
+
const isVisibilityRequest = /visible|can't see|cant see|not visible|hidden|color|contrast/i.test(userPrompt);
|
|
916
|
+
|
|
785
917
|
if (discoveredTheme.discoveredFiles.length > 0) {
|
|
786
|
-
|
|
918
|
+
// Only include detailed color guidance for visibility requests
|
|
919
|
+
if (isVisibilityRequest) {
|
|
920
|
+
// Extract available text color classes from discovered theme
|
|
921
|
+
const textColorClasses = Object.keys(discoveredTheme.cssVariables)
|
|
922
|
+
.filter(key => key.includes('foreground') || key.includes('text'))
|
|
923
|
+
.map(key => `text-${key.replace('--', '').replace(/-/g, '-')}`)
|
|
924
|
+
.slice(0, 10);
|
|
925
|
+
|
|
926
|
+
textContent += `
|
|
787
927
|
═══════════════════════════════════════════════════════════════════════════════
|
|
788
|
-
|
|
928
|
+
COLOR FIX GUIDANCE (for visibility issues only)
|
|
789
929
|
═══════════════════════════════════════════════════════════════════════════════
|
|
790
930
|
|
|
791
|
-
|
|
792
|
-
-
|
|
793
|
-
-
|
|
794
|
-
-
|
|
795
|
-
-
|
|
931
|
+
AVAILABLE TEXT COLORS (use these for contrast fixes):
|
|
932
|
+
- text-white (always visible on dark backgrounds)
|
|
933
|
+
- text-black (always visible on light backgrounds)
|
|
934
|
+
- text-foreground (theme default)
|
|
935
|
+
- text-primary-foreground (for bg-primary)
|
|
936
|
+
- text-accent-foreground (for bg-accent - CHECK IF THIS HAS GOOD CONTRAST)
|
|
937
|
+
- text-destructive-foreground (for bg-destructive)
|
|
938
|
+
|
|
939
|
+
SAFE BUTTON PATTERNS:
|
|
940
|
+
- bg-accent text-white (cyan button, guaranteed visible)
|
|
941
|
+
- bg-primary text-white (charcoal button, guaranteed visible)
|
|
942
|
+
- bg-destructive text-white (red button, guaranteed visible)
|
|
943
|
+
- bg-muted text-foreground (gray button, guaranteed visible)
|
|
796
944
|
|
|
797
|
-
|
|
798
|
-
|
|
945
|
+
IMPORTANT: If current code has text-accent-foreground or similar semantic colors
|
|
946
|
+
that result in poor contrast, REPLACE with text-white or text-black.
|
|
799
947
|
|
|
800
948
|
`;
|
|
949
|
+
} else {
|
|
950
|
+
// For non-visibility requests, minimal reference
|
|
951
|
+
textContent += `
|
|
952
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
953
|
+
REFERENCE ONLY (do not use this to justify additional changes)
|
|
954
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
955
|
+
Theme discovered from: ${discoveredTheme.discoveredFiles.join(', ')}
|
|
956
|
+
`;
|
|
957
|
+
}
|
|
958
|
+
|
|
801
959
|
debugLog("Theme discovery complete", {
|
|
802
960
|
filesFound: discoveredTheme.discoveredFiles,
|
|
803
961
|
cssVariableCount: Object.keys(discoveredTheme.cssVariables).length,
|
|
804
962
|
tailwindColorCount: Object.keys(discoveredTheme.tailwindColors).length,
|
|
963
|
+
isVisibilityRequest,
|
|
805
964
|
});
|
|
806
965
|
}
|
|
807
966
|
|
|
@@ -946,6 +1105,11 @@ This is better than generating patches with made-up code.`,
|
|
|
946
1105
|
|
|
947
1106
|
// Parse AI response - now expecting patches instead of full file content
|
|
948
1107
|
let aiResponse: {
|
|
1108
|
+
analysis?: {
|
|
1109
|
+
currentState?: string;
|
|
1110
|
+
problem?: string;
|
|
1111
|
+
fixStrategy?: string;
|
|
1112
|
+
};
|
|
949
1113
|
reasoning?: string;
|
|
950
1114
|
modifications: Array<{
|
|
951
1115
|
filePath: string;
|
|
@@ -993,9 +1157,31 @@ This is better than generating patches with made-up code.`,
|
|
|
993
1157
|
);
|
|
994
1158
|
}
|
|
995
1159
|
|
|
1160
|
+
// Log the LLM's analysis/reasoning (like Cursor showing its thought process)
|
|
1161
|
+
if (aiResponse.analysis) {
|
|
1162
|
+
debugLog("LLM Analysis (Step 1)", {
|
|
1163
|
+
currentState: aiResponse.analysis.currentState,
|
|
1164
|
+
problem: aiResponse.analysis.problem,
|
|
1165
|
+
fixStrategy: aiResponse.analysis.fixStrategy,
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
|
|
996
1169
|
finalExplanation = aiResponse.explanation;
|
|
997
1170
|
finalReasoning = aiResponse.reasoning;
|
|
998
1171
|
|
|
1172
|
+
// If LLM says clarification is needed, return that to the user
|
|
1173
|
+
if (aiResponse.analysis?.fixStrategy === "clarification_needed" &&
|
|
1174
|
+
(!aiResponse.modifications || aiResponse.modifications.length === 0)) {
|
|
1175
|
+
return NextResponse.json({
|
|
1176
|
+
success: true,
|
|
1177
|
+
sessionId: newSessionId,
|
|
1178
|
+
needsClarification: true,
|
|
1179
|
+
analysis: aiResponse.analysis,
|
|
1180
|
+
explanation: aiResponse.explanation || "The element appears correct. Please specify what needs to change.",
|
|
1181
|
+
modifications: [],
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
|
|
999
1185
|
if (!aiResponse.modifications || aiResponse.modifications.length === 0) {
|
|
1000
1186
|
return NextResponse.json({
|
|
1001
1187
|
success: true,
|
|
@@ -1034,6 +1220,19 @@ This is better than generating patches with made-up code.`,
|
|
|
1034
1220
|
continue;
|
|
1035
1221
|
}
|
|
1036
1222
|
|
|
1223
|
+
// CRITICAL: Warn if LLM is trying to modify a file OTHER than the TARGET COMPONENT
|
|
1224
|
+
// This usually means the LLM is trying to modify a file it doesn't have full visibility into
|
|
1225
|
+
const targetComponentPath = recommendedFileContent?.path;
|
|
1226
|
+
if (targetComponentPath && mod.filePath !== targetComponentPath) {
|
|
1227
|
+
debugLog("WARNING: LLM trying to modify non-target file", {
|
|
1228
|
+
targetComponent: targetComponentPath,
|
|
1229
|
+
attemptedFile: mod.filePath,
|
|
1230
|
+
warning: "LLM may be hallucinating code since it only has full content of the TARGET COMPONENT"
|
|
1231
|
+
});
|
|
1232
|
+
console.warn(`[Apply-First] ⚠️ LLM is modifying ${mod.filePath} but TARGET COMPONENT is ${targetComponentPath}`);
|
|
1233
|
+
console.warn(`[Apply-First] ⚠️ This may cause hallucination since LLM only has full visibility into the TARGET COMPONENT`);
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1037
1236
|
const fullPath = path.join(projectRoot, mod.filePath);
|
|
1038
1237
|
let originalContent = "";
|
|
1039
1238
|
if (fs.existsSync(fullPath)) {
|
|
@@ -1109,6 +1308,34 @@ This is better than generating patches with made-up code.`,
|
|
|
1109
1308
|
modifiedContent = patchResult.modifiedContent;
|
|
1110
1309
|
console.log(`[Apply-First] All ${mod.patches.length} patches applied successfully to ${mod.filePath}`);
|
|
1111
1310
|
}
|
|
1311
|
+
|
|
1312
|
+
// SYNTAX VALIDATION: Check for common JSX/HTML tag mismatches
|
|
1313
|
+
// This catches cases where the LLM changes opening tags but not closing tags
|
|
1314
|
+
if (mod.filePath.endsWith('.tsx') || mod.filePath.endsWith('.jsx')) {
|
|
1315
|
+
const openDivs = (modifiedContent.match(/<div[\s>]/g) || []).length;
|
|
1316
|
+
const closeDivs = (modifiedContent.match(/<\/div>/g) || []).length;
|
|
1317
|
+
const openSpans = (modifiedContent.match(/<span[\s>]/g) || []).length;
|
|
1318
|
+
const closeSpans = (modifiedContent.match(/<\/span>/g) || []).length;
|
|
1319
|
+
|
|
1320
|
+
if (openDivs !== closeDivs || openSpans !== closeSpans) {
|
|
1321
|
+
debugLog("SYNTAX WARNING: Tag mismatch detected", {
|
|
1322
|
+
filePath: mod.filePath,
|
|
1323
|
+
divs: { open: openDivs, close: closeDivs },
|
|
1324
|
+
spans: { open: openSpans, close: closeSpans },
|
|
1325
|
+
});
|
|
1326
|
+
console.warn(`[Apply-First] ⚠️ SYNTAX WARNING: Tag mismatch in ${mod.filePath}`);
|
|
1327
|
+
console.warn(`[Apply-First] divs: ${openDivs} open, ${closeDivs} close`);
|
|
1328
|
+
console.warn(`[Apply-First] spans: ${openSpans} open, ${closeSpans} close`);
|
|
1329
|
+
|
|
1330
|
+
// If there's a significant mismatch, reject the change
|
|
1331
|
+
const divDiff = Math.abs(openDivs - closeDivs);
|
|
1332
|
+
const spanDiff = Math.abs(openSpans - closeSpans);
|
|
1333
|
+
if (divDiff > 0 || spanDiff > 0) {
|
|
1334
|
+
patchErrors.push(`${mod.filePath}: LLM introduced syntax error - tag mismatch detected (${divDiff} div, ${spanDiff} span). Change rejected.`);
|
|
1335
|
+
continue;
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1112
1339
|
} else if (mod.modifiedContent) {
|
|
1113
1340
|
// Legacy: AI returned full file content
|
|
1114
1341
|
console.warn(`[Apply-First] Legacy modifiedContent received for ${mod.filePath} - patch-based format preferred`);
|
|
@@ -167,6 +167,73 @@ Be smart - use your knowledge of React patterns to make educated guesses about c
|
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Search candidate files for JSX code matching the focused element
|
|
172
|
+
* This helps identify which file actually contains the element the user clicked on
|
|
173
|
+
*/
|
|
174
|
+
function findFilesContainingElement(
|
|
175
|
+
focusedElements: VisionFocusedElement[] | undefined,
|
|
176
|
+
candidateFiles: { path: string; content: string }[]
|
|
177
|
+
): { path: string; score: number; matches: string[] }[] {
|
|
178
|
+
if (!focusedElements || focusedElements.length === 0) {
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const results: { path: string; score: number; matches: string[] }[] = [];
|
|
183
|
+
|
|
184
|
+
for (const file of candidateFiles) {
|
|
185
|
+
let score = 0;
|
|
186
|
+
const matches: string[] = [];
|
|
187
|
+
const content = file.content.toLowerCase();
|
|
188
|
+
|
|
189
|
+
for (const el of focusedElements) {
|
|
190
|
+
// Extract element type from the name (e.g., "button #123" -> "button")
|
|
191
|
+
const elementType = el.name.split(/[\s#]/)[0].toLowerCase();
|
|
192
|
+
const componentType = el.type.toLowerCase();
|
|
193
|
+
|
|
194
|
+
// Search for JSX patterns
|
|
195
|
+
// Check for HTML tags like <button, <div, etc.
|
|
196
|
+
if (content.includes(`<${elementType}`)) {
|
|
197
|
+
score += 10;
|
|
198
|
+
matches.push(`<${elementType}>`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Check for React component like <Button, <Card, etc.
|
|
202
|
+
const capitalizedType = el.type.charAt(0).toUpperCase() + el.type.slice(1);
|
|
203
|
+
if (file.content.includes(`<${capitalizedType}`)) {
|
|
204
|
+
score += 15;
|
|
205
|
+
matches.push(`<${capitalizedType}>`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Check for component imports
|
|
209
|
+
if (content.includes(`import`) && content.includes(componentType)) {
|
|
210
|
+
score += 5;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Check for event handlers commonly associated with buttons
|
|
214
|
+
if (elementType === "button" || componentType === "button") {
|
|
215
|
+
if (content.includes("onclick") || content.includes("onpress")) {
|
|
216
|
+
score += 3;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Check for the component being defined in this file
|
|
221
|
+
if (file.content.includes(`function ${capitalizedType}`) ||
|
|
222
|
+
file.content.includes(`const ${capitalizedType}`)) {
|
|
223
|
+
score += 20;
|
|
224
|
+
matches.push(`defines ${capitalizedType}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (score > 0) {
|
|
229
|
+
results.push({ path: file.path, score, matches });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Sort by score descending
|
|
234
|
+
return results.sort((a, b) => b.score - a.score);
|
|
235
|
+
}
|
|
236
|
+
|
|
170
237
|
/**
|
|
171
238
|
* Phase 3: Ask LLM to select the best file from actual file list
|
|
172
239
|
* This replaces guessing - the LLM sees the real filenames and picks one
|
|
@@ -175,7 +242,8 @@ async function selectBestFileFromList(
|
|
|
175
242
|
screenshot: string,
|
|
176
243
|
userPrompt: string,
|
|
177
244
|
candidateFiles: string[],
|
|
178
|
-
apiKey: string
|
|
245
|
+
apiKey: string,
|
|
246
|
+
focusedElementHints?: { path: string; score: number; matches: string[] }[]
|
|
179
247
|
): Promise<string | null> {
|
|
180
248
|
if (candidateFiles.length === 0) return null;
|
|
181
249
|
|
|
@@ -199,7 +267,12 @@ async function selectBestFileFromList(
|
|
|
199
267
|
|
|
200
268
|
Here are the actual component files found in this codebase:
|
|
201
269
|
${candidateFiles.map((f, i) => `${i + 1}. ${f}`).join('\n')}
|
|
270
|
+
${focusedElementHints && focusedElementHints.length > 0 ? `
|
|
271
|
+
FOCUSED ELEMENT ANALYSIS - Files that contain the element the user clicked on:
|
|
272
|
+
${focusedElementHints.slice(0, 3).map(h => `- ${h.path} (confidence: ${h.score}, found: ${h.matches.join(", ")})`).join('\n')}
|
|
202
273
|
|
|
274
|
+
IMPORTANT: Prefer files from the FOCUSED ELEMENT ANALYSIS above, as they contain the actual element the user wants to modify.
|
|
275
|
+
` : ''}
|
|
203
276
|
Looking at the screenshot, which file MOST LIKELY contains the UI elements the user wants to modify?
|
|
204
277
|
|
|
205
278
|
IMPORTANT: Return ONLY the exact file path from the list above (e.g., "components/ProcessCatalogue/ProcessDetailPanel.tsx").
|
|
@@ -434,43 +507,83 @@ function searchFilesSmart(
|
|
|
434
507
|
return sortedResults.map(r => ({ path: r.path, content: r.content, score: r.score }));
|
|
435
508
|
}
|
|
436
509
|
|
|
437
|
-
const VISION_SYSTEM_PROMPT = `You are a code editor.
|
|
510
|
+
const VISION_SYSTEM_PROMPT = `You are a code editor like Cursor. Before making changes, REASON about what needs to be done.
|
|
511
|
+
|
|
512
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
513
|
+
STEP 1: ANALYZE (Think like Cursor)
|
|
514
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
515
|
+
|
|
516
|
+
Before writing any patches, analyze the screenshot and answer:
|
|
517
|
+
|
|
518
|
+
1. CURRENT STATE: What do I see in the screenshot?
|
|
519
|
+
- Is the element visible/readable right now?
|
|
520
|
+
- What colors/styles are currently applied?
|
|
521
|
+
|
|
522
|
+
2. PROBLEM IDENTIFICATION: What's actually wrong?
|
|
523
|
+
- Contrast issue? (text blending with background)
|
|
524
|
+
- Hidden element? (not showing at all)
|
|
525
|
+
- Hover state issue? (only visible on interaction)
|
|
526
|
+
- Layout issue? (wrong size/position)
|
|
527
|
+
- Or is it already correct?
|
|
528
|
+
|
|
529
|
+
3. FIX STRATEGY: What should I change?
|
|
530
|
+
- DEFAULT state only (element needs to look different normally)
|
|
531
|
+
- HOVER state only (element needs different hover effect)
|
|
532
|
+
- BOTH states (element needs overall visibility improvement)
|
|
533
|
+
- ASK for clarification (request is too vague)
|
|
534
|
+
|
|
535
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
536
|
+
STEP 2: GENERATE PATCHES (Only after reasoning)
|
|
537
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
538
|
+
|
|
539
|
+
CRITICAL - FILE SELECTION:
|
|
540
|
+
- You MUST modify the file marked "TARGET COMPONENT" - this is the file you have FULL visibility into
|
|
541
|
+
- Do NOT try to modify other files listed in context - you only see their imports/exports, not full content
|
|
542
|
+
- If the TARGET COMPONENT is not the right file, return clarification_needed
|
|
543
|
+
|
|
544
|
+
COPY EXACTLY from the TARGET COMPONENT section:
|
|
545
|
+
- Your "search" string must match the file CHARACTER-FOR-CHARACTER
|
|
546
|
+
- Include exact indentation and at least 3 lines of context
|
|
547
|
+
- If the element looks FINE in the screenshot, say so
|
|
438
548
|
|
|
439
549
|
RULES:
|
|
440
|
-
1. Make the SMALLEST possible change
|
|
441
|
-
2. Do NOT refactor
|
|
442
|
-
3.
|
|
443
|
-
4.
|
|
444
|
-
5. If
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
- If code references database values (like icon_name, type, status), DO NOT change the mapping keys
|
|
452
|
-
- You cannot see database content - only change STRUCTURE, not DATA MAPPINGS
|
|
453
|
-
- Example: If you see iconMap["EyeOff"] = SomeIcon, do NOT change "EyeOff" to something else
|
|
454
|
-
- If user wants different icons, they must tell you the EXACT icon names they want
|
|
455
|
-
|
|
456
|
-
PATCH FORMAT:
|
|
457
|
-
Return ONLY raw JSON (no markdown, no preamble):
|
|
550
|
+
1. Make the SMALLEST possible change
|
|
551
|
+
2. Do NOT refactor or "improve" other code
|
|
552
|
+
3. NEVER invent code - COPY exact code from file
|
|
553
|
+
4. NEVER change data mappings unless user provides new values
|
|
554
|
+
5. If visibility issue: prefer explicit colors (text-white, text-black) over semantic tokens
|
|
555
|
+
|
|
556
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
557
|
+
RESPONSE FORMAT
|
|
558
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
559
|
+
|
|
560
|
+
Return ONLY raw JSON:
|
|
458
561
|
{
|
|
459
|
-
"
|
|
562
|
+
"analysis": {
|
|
563
|
+
"currentState": "what I see in the screenshot",
|
|
564
|
+
"problem": "the actual issue (or 'none - element appears correct')",
|
|
565
|
+
"fixStrategy": "default|hover|both|clarification_needed"
|
|
566
|
+
},
|
|
567
|
+
"reasoning": "my diagnosis and approach",
|
|
460
568
|
"modifications": [{
|
|
461
569
|
"filePath": "path/to/file.tsx",
|
|
462
570
|
"patches": [{
|
|
463
|
-
"search": "
|
|
464
|
-
"replace": "
|
|
465
|
-
"explanation": "what this
|
|
571
|
+
"search": "EXACT copy from file",
|
|
572
|
+
"replace": "changed code",
|
|
573
|
+
"explanation": "what this changes and why"
|
|
466
574
|
}],
|
|
467
575
|
"previewCSS": "optional CSS for live preview"
|
|
468
576
|
}],
|
|
469
577
|
"aggregatedPreviewCSS": "combined CSS for all changes",
|
|
470
|
-
"explanation": "summary"
|
|
578
|
+
"explanation": "summary of what was changed"
|
|
471
579
|
}
|
|
472
580
|
|
|
473
|
-
If
|
|
581
|
+
If the element looks correct, return:
|
|
582
|
+
{
|
|
583
|
+
"analysis": { "currentState": "...", "problem": "none", "fixStrategy": "clarification_needed" },
|
|
584
|
+
"modifications": [],
|
|
585
|
+
"explanation": "The element appears visible/correct in the screenshot. Please specify what needs to change."
|
|
586
|
+
}`;
|
|
474
587
|
|
|
475
588
|
export async function POST(request: Request) {
|
|
476
589
|
// Only allow in development
|
|
@@ -575,6 +688,22 @@ export async function POST(request: Request) {
|
|
|
575
688
|
const searchResults = searchFilesSmart(analysis, projectRoot, 10);
|
|
576
689
|
smartSearchFiles = searchResults.map(r => ({ path: r.path, content: r.content }));
|
|
577
690
|
|
|
691
|
+
// PHASE 2.5: Find which files contain the focused element
|
|
692
|
+
const focusedElementHints = findFilesContainingElement(
|
|
693
|
+
focusedElements,
|
|
694
|
+
searchResults.map(r => ({ path: r.path, content: r.content }))
|
|
695
|
+
);
|
|
696
|
+
|
|
697
|
+
if (focusedElementHints.length > 0) {
|
|
698
|
+
debugLog("Focused element search results", {
|
|
699
|
+
topMatches: focusedElementHints.slice(0, 3).map(h => ({
|
|
700
|
+
path: h.path,
|
|
701
|
+
score: h.score,
|
|
702
|
+
matches: h.matches
|
|
703
|
+
}))
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
|
|
578
707
|
// PHASE 3: Ask LLM to pick the best file from actual file list
|
|
579
708
|
if (searchResults.length > 0) {
|
|
580
709
|
const candidateFiles = searchResults.slice(0, 10).map(r => r.path);
|
|
@@ -582,7 +711,8 @@ export async function POST(request: Request) {
|
|
|
582
711
|
screenshot,
|
|
583
712
|
userPrompt,
|
|
584
713
|
candidateFiles,
|
|
585
|
-
apiKey
|
|
714
|
+
apiKey,
|
|
715
|
+
focusedElementHints
|
|
586
716
|
);
|
|
587
717
|
|
|
588
718
|
if (selectedFile) {
|
|
@@ -749,30 +879,59 @@ ${truncatedContent}${wasTruncated ? "\n// ... (truncated)" : ""}
|
|
|
749
879
|
|
|
750
880
|
// ========== THEME DISCOVERY (REFERENCE ONLY) ==========
|
|
751
881
|
// Dynamically discover theme tokens from the target codebase
|
|
752
|
-
// This is marked as REFERENCE ONLY so the LLM doesn't use it to justify extra changes
|
|
753
882
|
const discoveredTheme = await discoverTheme(projectRoot);
|
|
754
883
|
const themeContext = formatThemeForPrompt(discoveredTheme);
|
|
755
884
|
|
|
885
|
+
// Check if this is a visibility-related request
|
|
886
|
+
const isVisibilityRequest = /visible|can't see|cant see|not visible|hidden|color|contrast/i.test(userPrompt);
|
|
887
|
+
|
|
756
888
|
if (discoveredTheme.discoveredFiles.length > 0) {
|
|
757
|
-
|
|
889
|
+
// Only include detailed color guidance for visibility requests
|
|
890
|
+
if (isVisibilityRequest) {
|
|
891
|
+
// Extract available text color classes from discovered theme
|
|
892
|
+
const textColorClasses = Object.keys(discoveredTheme.cssVariables)
|
|
893
|
+
.filter(key => key.includes('foreground') || key.includes('text'))
|
|
894
|
+
.map(key => `text-${key.replace('--', '').replace(/-/g, '-')}`)
|
|
895
|
+
.slice(0, 10);
|
|
896
|
+
|
|
897
|
+
textContent += `
|
|
758
898
|
═══════════════════════════════════════════════════════════════════════════════
|
|
759
|
-
|
|
899
|
+
COLOR FIX GUIDANCE (for visibility issues only)
|
|
760
900
|
═══════════════════════════════════════════════════════════════════════════════
|
|
761
901
|
|
|
762
|
-
|
|
763
|
-
-
|
|
764
|
-
-
|
|
765
|
-
-
|
|
766
|
-
-
|
|
902
|
+
AVAILABLE TEXT COLORS (use these for contrast fixes):
|
|
903
|
+
- text-white (always visible on dark backgrounds)
|
|
904
|
+
- text-black (always visible on light backgrounds)
|
|
905
|
+
- text-foreground (theme default)
|
|
906
|
+
- text-primary-foreground (for bg-primary)
|
|
907
|
+
- text-accent-foreground (for bg-accent - CHECK IF THIS HAS GOOD CONTRAST)
|
|
908
|
+
- text-destructive-foreground (for bg-destructive)
|
|
909
|
+
|
|
910
|
+
SAFE BUTTON PATTERNS:
|
|
911
|
+
- bg-accent text-white (cyan button, guaranteed visible)
|
|
912
|
+
- bg-primary text-white (charcoal button, guaranteed visible)
|
|
913
|
+
- bg-destructive text-white (red button, guaranteed visible)
|
|
914
|
+
- bg-muted text-foreground (gray button, guaranteed visible)
|
|
767
915
|
|
|
768
|
-
|
|
769
|
-
|
|
916
|
+
IMPORTANT: If current code has text-accent-foreground or similar semantic colors
|
|
917
|
+
that result in poor contrast, REPLACE with text-white or text-black.
|
|
770
918
|
|
|
771
919
|
`;
|
|
920
|
+
} else {
|
|
921
|
+
// For non-visibility requests, minimal reference
|
|
922
|
+
textContent += `
|
|
923
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
924
|
+
REFERENCE ONLY (do not use this to justify additional changes)
|
|
925
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
926
|
+
Theme discovered from: ${discoveredTheme.discoveredFiles.join(', ')}
|
|
927
|
+
`;
|
|
928
|
+
}
|
|
929
|
+
|
|
772
930
|
debugLog("Theme discovery complete", {
|
|
773
931
|
filesFound: discoveredTheme.discoveredFiles,
|
|
774
932
|
cssVariableCount: Object.keys(discoveredTheme.cssVariables).length,
|
|
775
933
|
tailwindColorCount: Object.keys(discoveredTheme.tailwindColors).length,
|
|
934
|
+
isVisibilityRequest,
|
|
776
935
|
});
|
|
777
936
|
}
|
|
778
937
|
|
|
@@ -913,6 +1072,11 @@ This is better than generating patches with made-up code.`,
|
|
|
913
1072
|
|
|
914
1073
|
// Parse AI response - now expecting patches instead of full file content
|
|
915
1074
|
let aiResponse: {
|
|
1075
|
+
analysis?: {
|
|
1076
|
+
currentState?: string;
|
|
1077
|
+
problem?: string;
|
|
1078
|
+
fixStrategy?: string;
|
|
1079
|
+
};
|
|
916
1080
|
reasoning?: string;
|
|
917
1081
|
modifications: Array<{
|
|
918
1082
|
filePath: string;
|
|
@@ -964,10 +1128,31 @@ This is better than generating patches with made-up code.`,
|
|
|
964
1128
|
);
|
|
965
1129
|
}
|
|
966
1130
|
|
|
1131
|
+
// Log the LLM's analysis/reasoning (like Cursor showing its thought process)
|
|
1132
|
+
if (aiResponse.analysis) {
|
|
1133
|
+
debugLog("LLM Analysis (Step 1)", {
|
|
1134
|
+
currentState: aiResponse.analysis.currentState,
|
|
1135
|
+
problem: aiResponse.analysis.problem,
|
|
1136
|
+
fixStrategy: aiResponse.analysis.fixStrategy,
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
|
|
967
1140
|
finalExplanation = aiResponse.explanation;
|
|
968
1141
|
finalReasoning = aiResponse.reasoning;
|
|
969
1142
|
finalAggregatedCSS = aiResponse.aggregatedPreviewCSS;
|
|
970
1143
|
|
|
1144
|
+
// If LLM says clarification is needed, return that to the user
|
|
1145
|
+
if (aiResponse.analysis?.fixStrategy === "clarification_needed" &&
|
|
1146
|
+
(!aiResponse.modifications || aiResponse.modifications.length === 0)) {
|
|
1147
|
+
return NextResponse.json({
|
|
1148
|
+
success: true,
|
|
1149
|
+
needsClarification: true,
|
|
1150
|
+
analysis: aiResponse.analysis,
|
|
1151
|
+
explanation: aiResponse.explanation || "The element appears correct. Please specify what needs to change.",
|
|
1152
|
+
modifications: [],
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
|
|
971
1156
|
debugLog("VALIDATION: Known file paths from page context", {
|
|
972
1157
|
pageFile: pageContext.pageFile,
|
|
973
1158
|
knownPaths: Array.from(knownPaths),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sonance-brand-mcp",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.60",
|
|
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",
|