sonance-brand-mcp 1.3.38 → 1.3.40
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.
|
@@ -80,14 +80,23 @@ function debugLog(message: string, data?: unknown) {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
/**
|
|
83
|
-
*
|
|
84
|
-
* The LLM reads the screenshot and extracts text content related to the user's edit request
|
|
83
|
+
* Result of LLM screenshot analysis for smart file discovery
|
|
85
84
|
*/
|
|
86
|
-
|
|
85
|
+
interface ScreenshotAnalysis {
|
|
86
|
+
visibleText: string[];
|
|
87
|
+
componentNames: string[];
|
|
88
|
+
codePatterns: string[];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Phase 1: Analyze screenshot using LLM vision for smart file discovery
|
|
93
|
+
* The LLM extracts visible text AND deduces likely component names and code patterns
|
|
94
|
+
*/
|
|
95
|
+
async function analyzeScreenshotForSearch(
|
|
87
96
|
screenshot: string,
|
|
88
97
|
userPrompt: string,
|
|
89
98
|
apiKey: string
|
|
90
|
-
): Promise<
|
|
99
|
+
): Promise<ScreenshotAnalysis> {
|
|
91
100
|
const anthropic = new Anthropic({ apiKey });
|
|
92
101
|
|
|
93
102
|
const base64Data = screenshot.split(",")[1] || screenshot;
|
|
@@ -109,15 +118,22 @@ async function extractTextFromScreenshot(
|
|
|
109
118
|
},
|
|
110
119
|
{
|
|
111
120
|
type: "text",
|
|
112
|
-
text: `Look at this screenshot. The user wants to: "${userPrompt}"
|
|
121
|
+
text: `Look at this screenshot of a React application. The user wants to: "${userPrompt}"
|
|
122
|
+
|
|
123
|
+
Analyze this UI to help find the correct source file. Return:
|
|
113
124
|
|
|
114
|
-
Extract the EXACT text
|
|
115
|
-
|
|
125
|
+
1. **visibleText**: Extract the EXACT text visible in UI elements (button labels, headings, tab names, etc.)
|
|
126
|
+
2. **componentNames**: Deduce likely React component names based on what you see (e.g., "ProcessDetailPanel", "UserSettings", "DataTable"). Think about common React naming conventions.
|
|
127
|
+
3. **codePatterns**: Suggest code identifiers that might exist (e.g., "handleEdit", "isLoading", "activeTab", "onDelete")
|
|
116
128
|
|
|
117
129
|
Return ONLY valid JSON:
|
|
118
|
-
{
|
|
130
|
+
{
|
|
131
|
+
"visibleText": ["Assets", "Flowchart", "Edit", "Delete"],
|
|
132
|
+
"componentNames": ["ProcessDetailPanel", "ProcessActions", "QuickActions"],
|
|
133
|
+
"codePatterns": ["handleEdit", "handleDelete", "activeTab", "setActiveTab"]
|
|
134
|
+
}
|
|
119
135
|
|
|
120
|
-
Be
|
|
136
|
+
Be smart - use your knowledge of React patterns to make educated guesses about component and variable names.`,
|
|
121
137
|
},
|
|
122
138
|
],
|
|
123
139
|
},
|
|
@@ -126,7 +142,9 @@ Be precise - return the exact text as it appears, no extra words.`,
|
|
|
126
142
|
|
|
127
143
|
try {
|
|
128
144
|
const textBlock = response.content.find((block) => block.type === "text");
|
|
129
|
-
if (!textBlock || textBlock.type !== "text")
|
|
145
|
+
if (!textBlock || textBlock.type !== "text") {
|
|
146
|
+
return { visibleText: [], componentNames: [], codePatterns: [] };
|
|
147
|
+
}
|
|
130
148
|
|
|
131
149
|
let jsonText = textBlock.text.trim();
|
|
132
150
|
const jsonMatch = jsonText.match(/```json\n([\s\S]*?)\n```/) ||
|
|
@@ -136,34 +154,131 @@ Be precise - return the exact text as it appears, no extra words.`,
|
|
|
136
154
|
}
|
|
137
155
|
|
|
138
156
|
const parsed = JSON.parse(jsonText);
|
|
139
|
-
const
|
|
157
|
+
const result: ScreenshotAnalysis = {
|
|
158
|
+
visibleText: parsed.visibleText || parsed.textStrings || [],
|
|
159
|
+
componentNames: parsed.componentNames || [],
|
|
160
|
+
codePatterns: parsed.codePatterns || [],
|
|
161
|
+
};
|
|
140
162
|
|
|
141
|
-
debugLog("Phase 1:
|
|
142
|
-
return
|
|
163
|
+
debugLog("Phase 1: Analyzed screenshot for search", result);
|
|
164
|
+
return result;
|
|
143
165
|
} catch (e) {
|
|
144
|
-
debugLog("Phase 1: Failed to parse
|
|
145
|
-
return [];
|
|
166
|
+
debugLog("Phase 1: Failed to parse screenshot analysis response", { error: String(e) });
|
|
167
|
+
return { visibleText: [], componentNames: [], codePatterns: [] };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Search for component files by name across the project
|
|
173
|
+
* Returns the file path if found, null otherwise
|
|
174
|
+
*/
|
|
175
|
+
function findComponentFileByName(
|
|
176
|
+
componentName: string,
|
|
177
|
+
projectRoot: string
|
|
178
|
+
): string | null {
|
|
179
|
+
const normalizedName = componentName.toLowerCase();
|
|
180
|
+
const SEARCH_DIRS = ["src/components", "components", "src", "app"];
|
|
181
|
+
const EXTENSIONS = [".tsx", ".jsx", ".ts", ".js"];
|
|
182
|
+
|
|
183
|
+
function searchRecursive(
|
|
184
|
+
dir: string,
|
|
185
|
+
fileName: string,
|
|
186
|
+
depth = 0
|
|
187
|
+
): string | null {
|
|
188
|
+
if (depth > 5) return null;
|
|
189
|
+
try {
|
|
190
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
191
|
+
// Check direct matches (case-insensitive)
|
|
192
|
+
for (const ext of EXTENSIONS) {
|
|
193
|
+
const match = entries.find(
|
|
194
|
+
(e) => e.isFile() && e.name.toLowerCase() === `${fileName}${ext}`
|
|
195
|
+
);
|
|
196
|
+
if (match) return path.join(dir, match.name);
|
|
197
|
+
}
|
|
198
|
+
// Also check for partial matches (e.g., "ProcessDetail" matches "ProcessDetailPanel.tsx")
|
|
199
|
+
for (const entry of entries) {
|
|
200
|
+
if (entry.isFile()) {
|
|
201
|
+
const entryNameLower = entry.name.toLowerCase();
|
|
202
|
+
if (entryNameLower.includes(fileName) && EXTENSIONS.some(ext => entryNameLower.endsWith(ext))) {
|
|
203
|
+
return path.join(dir, entry.name);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// Recurse into subdirs
|
|
208
|
+
for (const entry of entries) {
|
|
209
|
+
if (
|
|
210
|
+
entry.isDirectory() &&
|
|
211
|
+
!entry.name.startsWith(".") &&
|
|
212
|
+
entry.name !== "node_modules"
|
|
213
|
+
) {
|
|
214
|
+
const result = searchRecursive(
|
|
215
|
+
path.join(dir, entry.name),
|
|
216
|
+
fileName,
|
|
217
|
+
depth + 1
|
|
218
|
+
);
|
|
219
|
+
if (result) return result;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
} catch {
|
|
223
|
+
/* skip unreadable */
|
|
224
|
+
}
|
|
225
|
+
return null;
|
|
146
226
|
}
|
|
227
|
+
|
|
228
|
+
for (const dir of SEARCH_DIRS) {
|
|
229
|
+
const searchDir = path.join(projectRoot, dir);
|
|
230
|
+
if (fs.existsSync(searchDir)) {
|
|
231
|
+
const result = searchRecursive(searchDir, normalizedName);
|
|
232
|
+
if (result) return path.relative(projectRoot, result);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return null;
|
|
147
236
|
}
|
|
148
237
|
|
|
149
238
|
/**
|
|
150
|
-
*
|
|
151
|
-
*
|
|
239
|
+
* Smart file search using LLM-derived analysis
|
|
240
|
+
* Uses intelligent scoring: filename matches > unique text matches > total matches
|
|
152
241
|
*/
|
|
153
|
-
function
|
|
154
|
-
|
|
242
|
+
function searchFilesSmart(
|
|
243
|
+
analysis: ScreenshotAnalysis,
|
|
155
244
|
projectRoot: string,
|
|
156
245
|
maxFiles: number = 10
|
|
157
|
-
): { path: string; content: string }[] {
|
|
158
|
-
|
|
246
|
+
): { path: string; content: string; score: number }[] {
|
|
247
|
+
const { visibleText, componentNames, codePatterns } = analysis;
|
|
248
|
+
|
|
249
|
+
// Combine all search terms
|
|
250
|
+
const allSearchTerms = [...visibleText, ...codePatterns];
|
|
251
|
+
|
|
252
|
+
if (allSearchTerms.length === 0 && componentNames.length === 0) return [];
|
|
159
253
|
|
|
160
|
-
const results: { path: string; content: string;
|
|
161
|
-
const searchDirs = ["components", "src/components", "app", "src/app"];
|
|
254
|
+
const results: Map<string, { path: string; content: string; score: number; filenameMatch: boolean }> = new Map();
|
|
255
|
+
const searchDirs = ["components", "src/components", "app", "src/app", "pages", "src/pages"];
|
|
162
256
|
const extensions = [".tsx", ".jsx"];
|
|
163
257
|
const visited = new Set<string>();
|
|
164
258
|
|
|
259
|
+
// Phase 2a: Search by component name (highest priority)
|
|
260
|
+
for (const componentName of componentNames) {
|
|
261
|
+
const foundPath = findComponentFileByName(componentName, projectRoot);
|
|
262
|
+
if (foundPath && !results.has(foundPath)) {
|
|
263
|
+
try {
|
|
264
|
+
const fullPath = path.join(projectRoot, foundPath);
|
|
265
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
266
|
+
results.set(foundPath, {
|
|
267
|
+
path: foundPath,
|
|
268
|
+
content,
|
|
269
|
+
score: 50, // High base score for filename match
|
|
270
|
+
filenameMatch: true
|
|
271
|
+
});
|
|
272
|
+
debugLog("Phase 2a: Found file by component name", { componentName, foundPath });
|
|
273
|
+
} catch {
|
|
274
|
+
// Skip files that can't be read
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Phase 2b: Search by text content (case-insensitive, unique term scoring)
|
|
165
280
|
function searchDir(dirPath: string, depth: number = 0) {
|
|
166
|
-
if (depth > 5
|
|
281
|
+
if (depth > 5) return;
|
|
167
282
|
|
|
168
283
|
const fullDirPath = path.join(projectRoot, dirPath);
|
|
169
284
|
if (!fs.existsSync(fullDirPath) || visited.has(fullDirPath)) return;
|
|
@@ -173,8 +288,6 @@ function searchFilesForExactText(
|
|
|
173
288
|
const entries = fs.readdirSync(fullDirPath, { withFileTypes: true });
|
|
174
289
|
|
|
175
290
|
for (const entry of entries) {
|
|
176
|
-
if (results.length >= maxFiles) break;
|
|
177
|
-
|
|
178
291
|
const entryPath = path.join(dirPath, entry.name);
|
|
179
292
|
const fullEntryPath = path.join(projectRoot, entryPath);
|
|
180
293
|
|
|
@@ -185,17 +298,41 @@ function searchFilesForExactText(
|
|
|
185
298
|
} else if (entry.isFile() && extensions.some(ext => entry.name.endsWith(ext))) {
|
|
186
299
|
try {
|
|
187
300
|
const content = fs.readFileSync(fullEntryPath, "utf-8");
|
|
301
|
+
const contentLower = content.toLowerCase();
|
|
188
302
|
|
|
189
|
-
// Count
|
|
190
|
-
let
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
303
|
+
// Count unique matches and total matches
|
|
304
|
+
let uniqueMatches = 0;
|
|
305
|
+
let totalMatches = 0;
|
|
306
|
+
|
|
307
|
+
for (const searchTerm of allSearchTerms) {
|
|
308
|
+
const searchLower = searchTerm.toLowerCase();
|
|
309
|
+
// Check if term exists (case-insensitive)
|
|
310
|
+
if (contentLower.includes(searchLower)) {
|
|
311
|
+
uniqueMatches++;
|
|
312
|
+
// Count occurrences
|
|
313
|
+
const regex = new RegExp(searchLower.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi");
|
|
314
|
+
const matches = content.match(regex);
|
|
315
|
+
totalMatches += matches ? matches.length : 0;
|
|
194
316
|
}
|
|
195
317
|
}
|
|
196
318
|
|
|
197
|
-
if (
|
|
198
|
-
|
|
319
|
+
if (uniqueMatches > 0) {
|
|
320
|
+
// Score: unique matches * 10 + total matches
|
|
321
|
+
const contentScore = (uniqueMatches * 10) + totalMatches;
|
|
322
|
+
|
|
323
|
+
// Check if we already have this file (from component name search)
|
|
324
|
+
const existing = results.get(entryPath);
|
|
325
|
+
if (existing) {
|
|
326
|
+
// Add content score to existing filename match score
|
|
327
|
+
existing.score += contentScore;
|
|
328
|
+
} else {
|
|
329
|
+
results.set(entryPath, {
|
|
330
|
+
path: entryPath,
|
|
331
|
+
content,
|
|
332
|
+
score: contentScore,
|
|
333
|
+
filenameMatch: false
|
|
334
|
+
});
|
|
335
|
+
}
|
|
199
336
|
}
|
|
200
337
|
} catch {
|
|
201
338
|
// Skip files that can't be read
|
|
@@ -209,19 +346,26 @@ function searchFilesForExactText(
|
|
|
209
346
|
|
|
210
347
|
for (const dir of searchDirs) {
|
|
211
348
|
searchDir(dir);
|
|
212
|
-
if (results.length >= maxFiles) break;
|
|
213
349
|
}
|
|
214
350
|
|
|
215
|
-
// Sort by
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
351
|
+
// Sort by score (highest first)
|
|
352
|
+
const sortedResults = Array.from(results.values())
|
|
353
|
+
.sort((a, b) => b.score - a.score)
|
|
354
|
+
.slice(0, maxFiles);
|
|
355
|
+
|
|
356
|
+
debugLog("Phase 2: Smart search results", {
|
|
357
|
+
componentNames,
|
|
358
|
+
visibleText: visibleText.slice(0, 5),
|
|
359
|
+
codePatterns: codePatterns.slice(0, 5),
|
|
360
|
+
filesFound: sortedResults.length,
|
|
361
|
+
topMatches: sortedResults.slice(0, 5).map(r => ({
|
|
362
|
+
path: r.path,
|
|
363
|
+
score: r.score,
|
|
364
|
+
filenameMatch: r.filenameMatch
|
|
365
|
+
}))
|
|
222
366
|
});
|
|
223
367
|
|
|
224
|
-
return
|
|
368
|
+
return sortedResults.map(r => ({ path: r.path, content: r.content, score: r.score }));
|
|
225
369
|
}
|
|
226
370
|
|
|
227
371
|
const VISION_SYSTEM_PROMPT = `You are an expert React/TypeScript developer with vision capabilities. You can see screenshots and modify code.
|
|
@@ -357,19 +501,56 @@ export async function POST(request: Request) {
|
|
|
357
501
|
// Generate a unique session ID
|
|
358
502
|
const newSessionId = randomUUID().slice(0, 8);
|
|
359
503
|
|
|
360
|
-
// PHASE 1:
|
|
361
|
-
//
|
|
362
|
-
let
|
|
504
|
+
// PHASE 1+2: LLM-driven smart file discovery
|
|
505
|
+
// The LLM analyzes the screenshot and deduces component names, code patterns, and visible text
|
|
506
|
+
let smartSearchFiles: { path: string; content: string }[] = [];
|
|
507
|
+
let recommendedFile: { path: string; reason: string } | null = null;
|
|
508
|
+
|
|
363
509
|
if (screenshot) {
|
|
364
|
-
debugLog("Starting Phase 1:
|
|
365
|
-
const
|
|
510
|
+
debugLog("Starting Phase 1: LLM screenshot analysis");
|
|
511
|
+
const analysis = await analyzeScreenshotForSearch(screenshot, userPrompt, apiKey);
|
|
366
512
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
513
|
+
const hasSearchTerms = analysis.visibleText.length > 0 ||
|
|
514
|
+
analysis.componentNames.length > 0 ||
|
|
515
|
+
analysis.codePatterns.length > 0;
|
|
516
|
+
|
|
517
|
+
if (hasSearchTerms) {
|
|
518
|
+
// PHASE 2: Smart search using component names, text, and code patterns
|
|
519
|
+
const searchResults = searchFilesSmart(analysis, projectRoot, 10);
|
|
520
|
+
smartSearchFiles = searchResults.map(r => ({ path: r.path, content: r.content }));
|
|
521
|
+
|
|
522
|
+
// Identify the recommended file (highest scoring file, prefer filename matches)
|
|
523
|
+
if (searchResults.length > 0) {
|
|
524
|
+
// Find the best filename match first
|
|
525
|
+
const filenameMatch = searchResults.find(r =>
|
|
526
|
+
analysis.componentNames.some(name =>
|
|
527
|
+
r.path.toLowerCase().includes(name.toLowerCase())
|
|
528
|
+
)
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
if (filenameMatch) {
|
|
532
|
+
recommendedFile = {
|
|
533
|
+
path: filenameMatch.path,
|
|
534
|
+
reason: `Component name match from screenshot analysis`
|
|
535
|
+
};
|
|
536
|
+
} else {
|
|
537
|
+
// Fall back to highest score
|
|
538
|
+
recommendedFile = {
|
|
539
|
+
path: searchResults[0].path,
|
|
540
|
+
reason: `Highest content match score (${searchResults[0].score} points)`
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
debugLog("Recommended file for editing", recommendedFile);
|
|
544
|
+
}
|
|
545
|
+
|
|
370
546
|
debugLog("Phase 1+2 complete", {
|
|
371
|
-
|
|
372
|
-
|
|
547
|
+
analysis: {
|
|
548
|
+
visibleText: analysis.visibleText.slice(0, 5),
|
|
549
|
+
componentNames: analysis.componentNames,
|
|
550
|
+
codePatterns: analysis.codePatterns.slice(0, 5)
|
|
551
|
+
},
|
|
552
|
+
filesFound: smartSearchFiles.map(f => f.path),
|
|
553
|
+
recommendedFile
|
|
373
554
|
});
|
|
374
555
|
}
|
|
375
556
|
}
|
|
@@ -377,17 +558,18 @@ export async function POST(request: Request) {
|
|
|
377
558
|
// Gather page context (with keyword search based on user prompt)
|
|
378
559
|
const pageContext = gatherPageContext(pageRoute || "/", projectRoot, userPrompt);
|
|
379
560
|
|
|
380
|
-
// Add
|
|
561
|
+
// Add smart-search discovered files to component sources (if not already present)
|
|
562
|
+
// These files are prioritized because they were identified by LLM analysis
|
|
381
563
|
const existingPaths = new Set([
|
|
382
564
|
pageContext.pageFile,
|
|
383
565
|
...pageContext.componentSources.map(c => c.path)
|
|
384
566
|
]);
|
|
385
567
|
|
|
386
|
-
for (const file of
|
|
568
|
+
for (const file of smartSearchFiles) {
|
|
387
569
|
if (!existingPaths.has(file.path)) {
|
|
388
570
|
// Insert at the beginning so these get priority
|
|
389
571
|
pageContext.componentSources.unshift(file);
|
|
390
|
-
debugLog("Added file from
|
|
572
|
+
debugLog("Added file from smart search to context", { path: file.path });
|
|
391
573
|
}
|
|
392
574
|
}
|
|
393
575
|
|
|
@@ -415,6 +597,22 @@ User Request: "${userPrompt}"
|
|
|
415
597
|
|
|
416
598
|
`;
|
|
417
599
|
|
|
600
|
+
// Add recommendation if smart search identified a best match
|
|
601
|
+
if (recommendedFile) {
|
|
602
|
+
textContent += `═══════════════════════════════════════════════════════════════════════════════
|
|
603
|
+
⚡ RECOMMENDED FILE TO EDIT
|
|
604
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
605
|
+
|
|
606
|
+
Based on the screenshot analysis, the component you should edit is:
|
|
607
|
+
**${recommendedFile.path}**
|
|
608
|
+
Reason: ${recommendedFile.reason}
|
|
609
|
+
|
|
610
|
+
The page file (${pageContext.pageFile}) is just a wrapper - the actual UI elements are in the component above.
|
|
611
|
+
STRONGLY PREFER editing the recommended file unless you have a specific reason not to.
|
|
612
|
+
|
|
613
|
+
`;
|
|
614
|
+
}
|
|
615
|
+
|
|
418
616
|
if (focusedElements && focusedElements.length > 0) {
|
|
419
617
|
textContent += `FOCUSED ELEMENTS (user clicked on these):
|
|
420
618
|
${focusedElements.map((el) => `- ${el.name} (${el.type}) at (${el.coordinates.x}, ${el.coordinates.y})`).join("\n")}
|
|
@@ -78,14 +78,23 @@ function debugLog(message: string, data?: unknown) {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
/**
|
|
81
|
-
*
|
|
82
|
-
* The LLM reads the screenshot and extracts text content related to the user's edit request
|
|
81
|
+
* Result of LLM screenshot analysis for smart file discovery
|
|
83
82
|
*/
|
|
84
|
-
|
|
83
|
+
interface ScreenshotAnalysis {
|
|
84
|
+
visibleText: string[];
|
|
85
|
+
componentNames: string[];
|
|
86
|
+
codePatterns: string[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Phase 1: Analyze screenshot using LLM vision for smart file discovery
|
|
91
|
+
* The LLM extracts visible text AND deduces likely component names and code patterns
|
|
92
|
+
*/
|
|
93
|
+
async function analyzeScreenshotForSearch(
|
|
85
94
|
screenshot: string,
|
|
86
95
|
userPrompt: string,
|
|
87
96
|
apiKey: string
|
|
88
|
-
): Promise<
|
|
97
|
+
): Promise<ScreenshotAnalysis> {
|
|
89
98
|
const anthropic = new Anthropic({ apiKey });
|
|
90
99
|
|
|
91
100
|
const base64Data = screenshot.split(",")[1] || screenshot;
|
|
@@ -107,15 +116,22 @@ async function extractTextFromScreenshot(
|
|
|
107
116
|
},
|
|
108
117
|
{
|
|
109
118
|
type: "text",
|
|
110
|
-
text: `Look at this screenshot. The user wants to: "${userPrompt}"
|
|
119
|
+
text: `Look at this screenshot of a React application. The user wants to: "${userPrompt}"
|
|
111
120
|
|
|
112
|
-
|
|
113
|
-
|
|
121
|
+
Analyze this UI to help find the correct source file. Return:
|
|
122
|
+
|
|
123
|
+
1. **visibleText**: Extract the EXACT text visible in UI elements (button labels, headings, tab names, etc.)
|
|
124
|
+
2. **componentNames**: Deduce likely React component names based on what you see (e.g., "ProcessDetailPanel", "UserSettings", "DataTable"). Think about common React naming conventions.
|
|
125
|
+
3. **codePatterns**: Suggest code identifiers that might exist (e.g., "handleEdit", "isLoading", "activeTab", "onDelete")
|
|
114
126
|
|
|
115
127
|
Return ONLY valid JSON:
|
|
116
|
-
{
|
|
128
|
+
{
|
|
129
|
+
"visibleText": ["Assets", "Flowchart", "Edit", "Delete"],
|
|
130
|
+
"componentNames": ["ProcessDetailPanel", "ProcessActions", "QuickActions"],
|
|
131
|
+
"codePatterns": ["handleEdit", "handleDelete", "activeTab", "setActiveTab"]
|
|
132
|
+
}
|
|
117
133
|
|
|
118
|
-
Be
|
|
134
|
+
Be smart - use your knowledge of React patterns to make educated guesses about component and variable names.`,
|
|
119
135
|
},
|
|
120
136
|
],
|
|
121
137
|
},
|
|
@@ -124,7 +140,9 @@ Be precise - return the exact text as it appears, no extra words.`,
|
|
|
124
140
|
|
|
125
141
|
try {
|
|
126
142
|
const textBlock = response.content.find((block) => block.type === "text");
|
|
127
|
-
if (!textBlock || textBlock.type !== "text")
|
|
143
|
+
if (!textBlock || textBlock.type !== "text") {
|
|
144
|
+
return { visibleText: [], componentNames: [], codePatterns: [] };
|
|
145
|
+
}
|
|
128
146
|
|
|
129
147
|
let jsonText = textBlock.text.trim();
|
|
130
148
|
const jsonMatch = jsonText.match(/```json\n([\s\S]*?)\n```/) ||
|
|
@@ -134,34 +152,131 @@ Be precise - return the exact text as it appears, no extra words.`,
|
|
|
134
152
|
}
|
|
135
153
|
|
|
136
154
|
const parsed = JSON.parse(jsonText);
|
|
137
|
-
const
|
|
155
|
+
const result: ScreenshotAnalysis = {
|
|
156
|
+
visibleText: parsed.visibleText || parsed.textStrings || [],
|
|
157
|
+
componentNames: parsed.componentNames || [],
|
|
158
|
+
codePatterns: parsed.codePatterns || [],
|
|
159
|
+
};
|
|
138
160
|
|
|
139
|
-
debugLog("Phase 1:
|
|
140
|
-
return
|
|
161
|
+
debugLog("Phase 1: Analyzed screenshot for search", result);
|
|
162
|
+
return result;
|
|
141
163
|
} catch (e) {
|
|
142
|
-
debugLog("Phase 1: Failed to parse
|
|
143
|
-
return [];
|
|
164
|
+
debugLog("Phase 1: Failed to parse screenshot analysis response", { error: String(e) });
|
|
165
|
+
return { visibleText: [], componentNames: [], codePatterns: [] };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Search for component files by name across the project (for smart search)
|
|
171
|
+
* Returns the file path if found, null otherwise
|
|
172
|
+
*/
|
|
173
|
+
function findComponentFileByNameSmart(
|
|
174
|
+
componentName: string,
|
|
175
|
+
projectRoot: string
|
|
176
|
+
): string | null {
|
|
177
|
+
const normalizedName = componentName.toLowerCase();
|
|
178
|
+
const SEARCH_DIRS = ["src/components", "components", "src", "app"];
|
|
179
|
+
const EXTENSIONS = [".tsx", ".jsx", ".ts", ".js"];
|
|
180
|
+
|
|
181
|
+
function searchRecursive(
|
|
182
|
+
dir: string,
|
|
183
|
+
fileName: string,
|
|
184
|
+
depth = 0
|
|
185
|
+
): string | null {
|
|
186
|
+
if (depth > 5) return null;
|
|
187
|
+
try {
|
|
188
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
189
|
+
// Check direct matches (case-insensitive)
|
|
190
|
+
for (const ext of EXTENSIONS) {
|
|
191
|
+
const match = entries.find(
|
|
192
|
+
(e) => e.isFile() && e.name.toLowerCase() === `${fileName}${ext}`
|
|
193
|
+
);
|
|
194
|
+
if (match) return path.join(dir, match.name);
|
|
195
|
+
}
|
|
196
|
+
// Also check for partial matches (e.g., "ProcessDetail" matches "ProcessDetailPanel.tsx")
|
|
197
|
+
for (const entry of entries) {
|
|
198
|
+
if (entry.isFile()) {
|
|
199
|
+
const entryNameLower = entry.name.toLowerCase();
|
|
200
|
+
if (entryNameLower.includes(fileName) && EXTENSIONS.some(ext => entryNameLower.endsWith(ext))) {
|
|
201
|
+
return path.join(dir, entry.name);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// Recurse into subdirs
|
|
206
|
+
for (const entry of entries) {
|
|
207
|
+
if (
|
|
208
|
+
entry.isDirectory() &&
|
|
209
|
+
!entry.name.startsWith(".") &&
|
|
210
|
+
entry.name !== "node_modules"
|
|
211
|
+
) {
|
|
212
|
+
const result = searchRecursive(
|
|
213
|
+
path.join(dir, entry.name),
|
|
214
|
+
fileName,
|
|
215
|
+
depth + 1
|
|
216
|
+
);
|
|
217
|
+
if (result) return result;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
} catch {
|
|
221
|
+
/* skip unreadable */
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
for (const dir of SEARCH_DIRS) {
|
|
227
|
+
const searchDir = path.join(projectRoot, dir);
|
|
228
|
+
if (fs.existsSync(searchDir)) {
|
|
229
|
+
const result = searchRecursive(searchDir, normalizedName);
|
|
230
|
+
if (result) return path.relative(projectRoot, result);
|
|
231
|
+
}
|
|
144
232
|
}
|
|
233
|
+
return null;
|
|
145
234
|
}
|
|
146
235
|
|
|
147
236
|
/**
|
|
148
|
-
*
|
|
149
|
-
*
|
|
237
|
+
* Smart file search using LLM-derived analysis
|
|
238
|
+
* Uses intelligent scoring: filename matches > unique text matches > total matches
|
|
150
239
|
*/
|
|
151
|
-
function
|
|
152
|
-
|
|
240
|
+
function searchFilesSmart(
|
|
241
|
+
analysis: ScreenshotAnalysis,
|
|
153
242
|
projectRoot: string,
|
|
154
243
|
maxFiles: number = 10
|
|
155
|
-
): { path: string; content: string }[] {
|
|
156
|
-
|
|
244
|
+
): { path: string; content: string; score: number }[] {
|
|
245
|
+
const { visibleText, componentNames, codePatterns } = analysis;
|
|
246
|
+
|
|
247
|
+
// Combine all search terms
|
|
248
|
+
const allSearchTerms = [...visibleText, ...codePatterns];
|
|
249
|
+
|
|
250
|
+
if (allSearchTerms.length === 0 && componentNames.length === 0) return [];
|
|
157
251
|
|
|
158
|
-
const results: { path: string; content: string;
|
|
159
|
-
const searchDirs = ["components", "src/components", "app", "src/app"];
|
|
252
|
+
const results: Map<string, { path: string; content: string; score: number; filenameMatch: boolean }> = new Map();
|
|
253
|
+
const searchDirs = ["components", "src/components", "app", "src/app", "pages", "src/pages"];
|
|
160
254
|
const extensions = [".tsx", ".jsx"];
|
|
161
255
|
const visited = new Set<string>();
|
|
162
256
|
|
|
257
|
+
// Phase 2a: Search by component name (highest priority)
|
|
258
|
+
for (const componentName of componentNames) {
|
|
259
|
+
const foundPath = findComponentFileByNameSmart(componentName, projectRoot);
|
|
260
|
+
if (foundPath && !results.has(foundPath)) {
|
|
261
|
+
try {
|
|
262
|
+
const fullPath = path.join(projectRoot, foundPath);
|
|
263
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
264
|
+
results.set(foundPath, {
|
|
265
|
+
path: foundPath,
|
|
266
|
+
content,
|
|
267
|
+
score: 50, // High base score for filename match
|
|
268
|
+
filenameMatch: true
|
|
269
|
+
});
|
|
270
|
+
debugLog("Phase 2a: Found file by component name", { componentName, foundPath });
|
|
271
|
+
} catch {
|
|
272
|
+
// Skip files that can't be read
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Phase 2b: Search by text content (case-insensitive, unique term scoring)
|
|
163
278
|
function searchDir(dirPath: string, depth: number = 0) {
|
|
164
|
-
if (depth > 5
|
|
279
|
+
if (depth > 5) return;
|
|
165
280
|
|
|
166
281
|
const fullDirPath = path.join(projectRoot, dirPath);
|
|
167
282
|
if (!fs.existsSync(fullDirPath) || visited.has(fullDirPath)) return;
|
|
@@ -171,8 +286,6 @@ function searchFilesForExactText(
|
|
|
171
286
|
const entries = fs.readdirSync(fullDirPath, { withFileTypes: true });
|
|
172
287
|
|
|
173
288
|
for (const entry of entries) {
|
|
174
|
-
if (results.length >= maxFiles) break;
|
|
175
|
-
|
|
176
289
|
const entryPath = path.join(dirPath, entry.name);
|
|
177
290
|
const fullEntryPath = path.join(projectRoot, entryPath);
|
|
178
291
|
|
|
@@ -183,17 +296,41 @@ function searchFilesForExactText(
|
|
|
183
296
|
} else if (entry.isFile() && extensions.some(ext => entry.name.endsWith(ext))) {
|
|
184
297
|
try {
|
|
185
298
|
const content = fs.readFileSync(fullEntryPath, "utf-8");
|
|
299
|
+
const contentLower = content.toLowerCase();
|
|
186
300
|
|
|
187
|
-
// Count
|
|
188
|
-
let
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
301
|
+
// Count unique matches and total matches
|
|
302
|
+
let uniqueMatches = 0;
|
|
303
|
+
let totalMatches = 0;
|
|
304
|
+
|
|
305
|
+
for (const searchTerm of allSearchTerms) {
|
|
306
|
+
const searchLower = searchTerm.toLowerCase();
|
|
307
|
+
// Check if term exists (case-insensitive)
|
|
308
|
+
if (contentLower.includes(searchLower)) {
|
|
309
|
+
uniqueMatches++;
|
|
310
|
+
// Count occurrences
|
|
311
|
+
const regex = new RegExp(searchLower.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi");
|
|
312
|
+
const matches = content.match(regex);
|
|
313
|
+
totalMatches += matches ? matches.length : 0;
|
|
192
314
|
}
|
|
193
315
|
}
|
|
194
316
|
|
|
195
|
-
if (
|
|
196
|
-
|
|
317
|
+
if (uniqueMatches > 0) {
|
|
318
|
+
// Score: unique matches * 10 + total matches
|
|
319
|
+
const contentScore = (uniqueMatches * 10) + totalMatches;
|
|
320
|
+
|
|
321
|
+
// Check if we already have this file (from component name search)
|
|
322
|
+
const existing = results.get(entryPath);
|
|
323
|
+
if (existing) {
|
|
324
|
+
// Add content score to existing filename match score
|
|
325
|
+
existing.score += contentScore;
|
|
326
|
+
} else {
|
|
327
|
+
results.set(entryPath, {
|
|
328
|
+
path: entryPath,
|
|
329
|
+
content,
|
|
330
|
+
score: contentScore,
|
|
331
|
+
filenameMatch: false
|
|
332
|
+
});
|
|
333
|
+
}
|
|
197
334
|
}
|
|
198
335
|
} catch {
|
|
199
336
|
// Skip files that can't be read
|
|
@@ -207,19 +344,26 @@ function searchFilesForExactText(
|
|
|
207
344
|
|
|
208
345
|
for (const dir of searchDirs) {
|
|
209
346
|
searchDir(dir);
|
|
210
|
-
if (results.length >= maxFiles) break;
|
|
211
347
|
}
|
|
212
348
|
|
|
213
|
-
// Sort by
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
349
|
+
// Sort by score (highest first)
|
|
350
|
+
const sortedResults = Array.from(results.values())
|
|
351
|
+
.sort((a, b) => b.score - a.score)
|
|
352
|
+
.slice(0, maxFiles);
|
|
353
|
+
|
|
354
|
+
debugLog("Phase 2: Smart search results", {
|
|
355
|
+
componentNames,
|
|
356
|
+
visibleText: visibleText.slice(0, 5),
|
|
357
|
+
codePatterns: codePatterns.slice(0, 5),
|
|
358
|
+
filesFound: sortedResults.length,
|
|
359
|
+
topMatches: sortedResults.slice(0, 5).map(r => ({
|
|
360
|
+
path: r.path,
|
|
361
|
+
score: r.score,
|
|
362
|
+
filenameMatch: r.filenameMatch
|
|
363
|
+
}))
|
|
220
364
|
});
|
|
221
365
|
|
|
222
|
-
return
|
|
366
|
+
return sortedResults.map(r => ({ path: r.path, content: r.content, score: r.score }));
|
|
223
367
|
}
|
|
224
368
|
|
|
225
369
|
const VISION_SYSTEM_PROMPT = `You are an expert React/TypeScript developer with vision capabilities. You can see screenshots and modify code.
|
|
@@ -367,19 +511,56 @@ export async function POST(request: Request) {
|
|
|
367
511
|
);
|
|
368
512
|
}
|
|
369
513
|
|
|
370
|
-
// PHASE 1:
|
|
371
|
-
//
|
|
372
|
-
let
|
|
514
|
+
// PHASE 1+2: LLM-driven smart file discovery
|
|
515
|
+
// The LLM analyzes the screenshot and deduces component names, code patterns, and visible text
|
|
516
|
+
let smartSearchFiles: { path: string; content: string }[] = [];
|
|
517
|
+
let recommendedFile: { path: string; reason: string } | null = null;
|
|
518
|
+
|
|
373
519
|
if (screenshot) {
|
|
374
|
-
debugLog("Starting Phase 1:
|
|
375
|
-
const
|
|
520
|
+
debugLog("Starting Phase 1: LLM screenshot analysis");
|
|
521
|
+
const analysis = await analyzeScreenshotForSearch(screenshot, userPrompt, apiKey);
|
|
376
522
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
523
|
+
const hasSearchTerms = analysis.visibleText.length > 0 ||
|
|
524
|
+
analysis.componentNames.length > 0 ||
|
|
525
|
+
analysis.codePatterns.length > 0;
|
|
526
|
+
|
|
527
|
+
if (hasSearchTerms) {
|
|
528
|
+
// PHASE 2: Smart search using component names, text, and code patterns
|
|
529
|
+
const searchResults = searchFilesSmart(analysis, projectRoot, 10);
|
|
530
|
+
smartSearchFiles = searchResults.map(r => ({ path: r.path, content: r.content }));
|
|
531
|
+
|
|
532
|
+
// Identify the recommended file (highest scoring file, prefer filename matches)
|
|
533
|
+
if (searchResults.length > 0) {
|
|
534
|
+
// Find the best filename match first
|
|
535
|
+
const filenameMatch = searchResults.find(r =>
|
|
536
|
+
analysis.componentNames.some(name =>
|
|
537
|
+
r.path.toLowerCase().includes(name.toLowerCase())
|
|
538
|
+
)
|
|
539
|
+
);
|
|
540
|
+
|
|
541
|
+
if (filenameMatch) {
|
|
542
|
+
recommendedFile = {
|
|
543
|
+
path: filenameMatch.path,
|
|
544
|
+
reason: `Component name match from screenshot analysis`
|
|
545
|
+
};
|
|
546
|
+
} else {
|
|
547
|
+
// Fall back to highest score
|
|
548
|
+
recommendedFile = {
|
|
549
|
+
path: searchResults[0].path,
|
|
550
|
+
reason: `Highest content match score (${searchResults[0].score} points)`
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
debugLog("Recommended file for editing", recommendedFile);
|
|
554
|
+
}
|
|
555
|
+
|
|
380
556
|
debugLog("Phase 1+2 complete", {
|
|
381
|
-
|
|
382
|
-
|
|
557
|
+
analysis: {
|
|
558
|
+
visibleText: analysis.visibleText.slice(0, 5),
|
|
559
|
+
componentNames: analysis.componentNames,
|
|
560
|
+
codePatterns: analysis.codePatterns.slice(0, 5)
|
|
561
|
+
},
|
|
562
|
+
filesFound: smartSearchFiles.map(f => f.path),
|
|
563
|
+
recommendedFile
|
|
383
564
|
});
|
|
384
565
|
}
|
|
385
566
|
}
|
|
@@ -387,17 +568,18 @@ export async function POST(request: Request) {
|
|
|
387
568
|
// Gather page context (including focused element files and keyword search)
|
|
388
569
|
const pageContext = gatherPageContext(pageRoute, projectRoot, focusedElements, userPrompt);
|
|
389
570
|
|
|
390
|
-
// Add
|
|
571
|
+
// Add smart-search discovered files to component sources (if not already present)
|
|
572
|
+
// These files are prioritized because they were identified by LLM analysis
|
|
391
573
|
const existingPaths = new Set([
|
|
392
574
|
pageContext.pageFile,
|
|
393
575
|
...pageContext.componentSources.map(c => c.path)
|
|
394
576
|
]);
|
|
395
577
|
|
|
396
|
-
for (const file of
|
|
578
|
+
for (const file of smartSearchFiles) {
|
|
397
579
|
if (!existingPaths.has(file.path)) {
|
|
398
580
|
// Insert at the beginning so these get priority
|
|
399
581
|
pageContext.componentSources.unshift(file);
|
|
400
|
-
debugLog("Added file from
|
|
582
|
+
debugLog("Added file from smart search to context", { path: file.path });
|
|
401
583
|
}
|
|
402
584
|
}
|
|
403
585
|
|
|
@@ -425,6 +607,22 @@ User Request: "${userPrompt}"
|
|
|
425
607
|
|
|
426
608
|
`;
|
|
427
609
|
|
|
610
|
+
// Add recommendation if smart search identified a best match
|
|
611
|
+
if (recommendedFile) {
|
|
612
|
+
textContent += `═══════════════════════════════════════════════════════════════════════════════
|
|
613
|
+
⚡ RECOMMENDED FILE TO EDIT
|
|
614
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
615
|
+
|
|
616
|
+
Based on the screenshot analysis, the component you should edit is:
|
|
617
|
+
**${recommendedFile.path}**
|
|
618
|
+
Reason: ${recommendedFile.reason}
|
|
619
|
+
|
|
620
|
+
The page file (${pageContext.pageFile}) is just a wrapper - the actual UI elements are in the component above.
|
|
621
|
+
STRONGLY PREFER editing the recommended file unless you have a specific reason not to.
|
|
622
|
+
|
|
623
|
+
`;
|
|
624
|
+
}
|
|
625
|
+
|
|
428
626
|
if (focusedElements && focusedElements.length > 0) {
|
|
429
627
|
textContent += `FOCUSED ELEMENTS (user clicked on these):
|
|
430
628
|
${focusedElements.map((el) => `- ${el.name} (${el.type}) at (${el.coordinates.x}, ${el.coordinates.y})`).join("\n")}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sonance-brand-mcp",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.40",
|
|
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",
|