sonance-brand-mcp 1.3.99 → 1.3.101
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.
|
@@ -88,6 +88,40 @@ interface BackupManifest {
|
|
|
88
88
|
const BACKUP_ROOT = ".sonance-backups";
|
|
89
89
|
const DEBUG_LOG_FILE = "sonance-debug.log";
|
|
90
90
|
|
|
91
|
+
/**
|
|
92
|
+
* TWO-PHASE DESIGN ANALYSIS TYPES
|
|
93
|
+
* Phase 1 identifies problems, Phase 2 generates patches that reference those problems
|
|
94
|
+
*/
|
|
95
|
+
|
|
96
|
+
/** Categories of visual/UX problems that can be identified */
|
|
97
|
+
type DesignProblemCategory = 'hierarchy' | 'spacing' | 'alignment' | 'feedback' | 'grouping';
|
|
98
|
+
|
|
99
|
+
/** A specific visual problem identified in Phase 1 analysis */
|
|
100
|
+
interface DesignProblem {
|
|
101
|
+
/** Unique identifier (e.g., "hierarchy-1", "spacing-2") */
|
|
102
|
+
id: string;
|
|
103
|
+
/** Problem category */
|
|
104
|
+
category: DesignProblemCategory;
|
|
105
|
+
/** Specific description of the issue */
|
|
106
|
+
description: string;
|
|
107
|
+
/** Visual impact severity */
|
|
108
|
+
severity: 'high' | 'medium' | 'low';
|
|
109
|
+
/** Hint for what to look for in the code */
|
|
110
|
+
codeHint?: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Extended patch that references a specific problem from Phase 1 */
|
|
114
|
+
interface DesignPatch {
|
|
115
|
+
/** Reference to the problem this patch fixes */
|
|
116
|
+
problemId: string;
|
|
117
|
+
/** Exact code to find */
|
|
118
|
+
search: string;
|
|
119
|
+
/** Replacement code */
|
|
120
|
+
replace: string;
|
|
121
|
+
/** Explanation of how this fixes the problem */
|
|
122
|
+
rationale: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
91
125
|
/**
|
|
92
126
|
* Debug logging utility - writes to sonance-debug.log in project root
|
|
93
127
|
* This helps diagnose issues with file discovery, validation, and revert
|
|
@@ -889,6 +923,139 @@ Be smart - use your knowledge of React patterns to identify what type of element
|
|
|
889
923
|
}
|
|
890
924
|
}
|
|
891
925
|
|
|
926
|
+
/**
|
|
927
|
+
* PHASE 1: Design Problem Analysis
|
|
928
|
+
*
|
|
929
|
+
* Analyzes the screenshot to identify specific visual/UX problems.
|
|
930
|
+
* This is a SEPARATE LLM call that ONLY identifies problems - no code changes.
|
|
931
|
+
* The problems are then passed to Phase 2 to generate targeted patches.
|
|
932
|
+
*/
|
|
933
|
+
async function analyzeDesignProblems(
|
|
934
|
+
screenshot: string,
|
|
935
|
+
fileContent: string,
|
|
936
|
+
userPrompt: string,
|
|
937
|
+
apiKey: string
|
|
938
|
+
): Promise<DesignProblem[]> {
|
|
939
|
+
const anthropic = new Anthropic({ apiKey });
|
|
940
|
+
const base64Data = screenshot.split(",")[1] || screenshot;
|
|
941
|
+
|
|
942
|
+
debugLog("Phase 1 Design Analysis: Starting visual problem identification", {
|
|
943
|
+
promptPreview: userPrompt?.substring(0, 50),
|
|
944
|
+
fileContentLength: fileContent.length
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
try {
|
|
948
|
+
const response = await anthropic.messages.create({
|
|
949
|
+
model: "claude-sonnet-4-20250514",
|
|
950
|
+
max_tokens: 2048,
|
|
951
|
+
messages: [
|
|
952
|
+
{
|
|
953
|
+
role: "user",
|
|
954
|
+
content: [
|
|
955
|
+
{
|
|
956
|
+
type: "image",
|
|
957
|
+
source: {
|
|
958
|
+
type: "base64",
|
|
959
|
+
media_type: "image/png",
|
|
960
|
+
data: base64Data,
|
|
961
|
+
},
|
|
962
|
+
},
|
|
963
|
+
{
|
|
964
|
+
type: "text",
|
|
965
|
+
text: `You are a senior UI/UX designer. Analyze this screenshot and identify SPECIFIC visual problems.
|
|
966
|
+
|
|
967
|
+
User's request: "${userPrompt}"
|
|
968
|
+
|
|
969
|
+
Examine the UI for issues in these categories:
|
|
970
|
+
|
|
971
|
+
1. HIERARCHY: Is there a clear visual focus? Are elements competing for attention?
|
|
972
|
+
- Look for: competing headings, unclear primary actions, visual clutter
|
|
973
|
+
|
|
974
|
+
2. SPACING: Are related elements grouped? Is there awkward empty space?
|
|
975
|
+
- Look for: disconnected labels/values, uneven gaps, cramped or overly spread content
|
|
976
|
+
|
|
977
|
+
3. ALIGNMENT: Do elements align naturally? Are there visual disconnects?
|
|
978
|
+
- Look for: misaligned text, floating elements, broken visual flow
|
|
979
|
+
|
|
980
|
+
4. FEEDBACK: Are progress indicators, states, and actions clear?
|
|
981
|
+
- Look for: weak progress visualization, unclear states, orphaned status indicators
|
|
982
|
+
|
|
983
|
+
5. GROUPING: Are related items visually connected? Do containers make sense?
|
|
984
|
+
- Look for: related content that appears separate, missing visual boundaries
|
|
985
|
+
|
|
986
|
+
For each problem found, provide:
|
|
987
|
+
- id: unique identifier (e.g., "hierarchy-1", "spacing-2")
|
|
988
|
+
- category: one of [hierarchy, spacing, alignment, feedback, grouping]
|
|
989
|
+
- description: specific description of what's wrong
|
|
990
|
+
- severity: high/medium/low based on visual impact
|
|
991
|
+
- codeHint: what to look for in the code to fix it
|
|
992
|
+
|
|
993
|
+
Return ONLY a JSON array of problems. Example:
|
|
994
|
+
[
|
|
995
|
+
{
|
|
996
|
+
"id": "grouping-1",
|
|
997
|
+
"category": "grouping",
|
|
998
|
+
"description": "The 'Essence Quality' label and '0% COMPLETE' badge appear visually disconnected - they should be grouped together",
|
|
999
|
+
"severity": "high",
|
|
1000
|
+
"codeHint": "Check if label and badge are in separate containers or using absolute positioning"
|
|
1001
|
+
},
|
|
1002
|
+
{
|
|
1003
|
+
"id": "spacing-1",
|
|
1004
|
+
"category": "spacing",
|
|
1005
|
+
"description": "Too much vertical gap between the header section and the quality indicator",
|
|
1006
|
+
"severity": "medium",
|
|
1007
|
+
"codeHint": "Look for large margin or padding values like mt-6, mb-8, py-6"
|
|
1008
|
+
}
|
|
1009
|
+
]
|
|
1010
|
+
|
|
1011
|
+
IMPORTANT:
|
|
1012
|
+
- Only identify problems that are actually visible in the screenshot
|
|
1013
|
+
- Be specific - vague problems lead to vague fixes
|
|
1014
|
+
- Maximum 5 problems - focus on the most impactful ones
|
|
1015
|
+
- If no significant problems are found, return an empty array []
|
|
1016
|
+
- Do NOT suggest code changes - only identify problems
|
|
1017
|
+
|
|
1018
|
+
Return ONLY the JSON array, no other text.`,
|
|
1019
|
+
},
|
|
1020
|
+
],
|
|
1021
|
+
},
|
|
1022
|
+
],
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
const textBlock = response.content.find((block) => block.type === "text");
|
|
1026
|
+
if (!textBlock || textBlock.type !== "text") {
|
|
1027
|
+
debugLog("Phase 1 Design Analysis: No text response from LLM");
|
|
1028
|
+
return [];
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
let jsonText = textBlock.text.trim();
|
|
1032
|
+
|
|
1033
|
+
// Extract JSON from code fences if present
|
|
1034
|
+
const jsonMatch = jsonText.match(/```(?:json)?\n?([\s\S]*?)\n?```/) ||
|
|
1035
|
+
jsonText.match(/\[[\s\S]*\]/);
|
|
1036
|
+
if (jsonMatch) {
|
|
1037
|
+
jsonText = jsonMatch[1] || jsonMatch[0];
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
const problems: DesignProblem[] = JSON.parse(jsonText);
|
|
1041
|
+
|
|
1042
|
+
// Validate the structure
|
|
1043
|
+
const validatedProblems = problems.filter(p =>
|
|
1044
|
+
p.id && p.category && p.description && p.severity
|
|
1045
|
+
).slice(0, 5); // Maximum 5 problems
|
|
1046
|
+
|
|
1047
|
+
debugLog("Phase 1 Design Analysis: Identified problems", {
|
|
1048
|
+
count: validatedProblems.length,
|
|
1049
|
+
problems: validatedProblems.map(p => ({ id: p.id, category: p.category, severity: p.severity }))
|
|
1050
|
+
});
|
|
1051
|
+
|
|
1052
|
+
return validatedProblems;
|
|
1053
|
+
} catch (e) {
|
|
1054
|
+
debugLog("Phase 1 Design Analysis: Failed to analyze", { error: String(e) });
|
|
1055
|
+
return [];
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
|
|
892
1059
|
/**
|
|
893
1060
|
* Search candidate files for JSX code matching the focused element
|
|
894
1061
|
* This helps identify which file actually contains the element the user clicked on
|
|
@@ -1342,91 +1509,166 @@ Output format:
|
|
|
1342
1509
|
The "search" field must match the file EXACTLY (copy-paste from the code provided).`;
|
|
1343
1510
|
|
|
1344
1511
|
/**
|
|
1345
|
-
*
|
|
1346
|
-
*
|
|
1347
|
-
*
|
|
1512
|
+
* PHASE 2: Targeted Patch Generation Prompt
|
|
1513
|
+
*
|
|
1514
|
+
* This prompt is used AFTER Phase 1 has identified specific problems.
|
|
1515
|
+
* It requires each patch to reference a problem ID from Phase 1.
|
|
1348
1516
|
*/
|
|
1349
|
-
|
|
1517
|
+
function buildPhase2DesignPrompt(problems: DesignProblem[]): string {
|
|
1518
|
+
if (problems.length === 0) {
|
|
1519
|
+
return ''; // No problems identified, skip design protocol
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
const problemsJson = JSON.stringify(problems, null, 2);
|
|
1523
|
+
|
|
1524
|
+
return `
|
|
1350
1525
|
═══════════════════════════════════════════════════════════════════════════════
|
|
1351
|
-
|
|
1526
|
+
PHASE 2: TARGETED PATCH GENERATION
|
|
1352
1527
|
═══════════════════════════════════════════════════════════════════════════════
|
|
1353
1528
|
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
STEP 1 - VISUAL AUDIT (analyze the screenshot)
|
|
1357
|
-
Examine the UI and identify specific issues in these categories:
|
|
1358
|
-
|
|
1359
|
-
• HIERARCHY: Is there a clear visual focus? Are elements competing for attention?
|
|
1360
|
-
- Look for: competing headings, unclear primary actions, visual clutter
|
|
1361
|
-
|
|
1362
|
-
• SPACING: Are related elements grouped? Is there awkward empty space?
|
|
1363
|
-
- Look for: disconnected labels/values, uneven gaps, cramped or overly spread content
|
|
1364
|
-
|
|
1365
|
-
• ALIGNMENT: Do elements align naturally? Are there visual disconnects?
|
|
1366
|
-
- Look for: misaligned text, floating elements, broken visual flow
|
|
1367
|
-
|
|
1368
|
-
• FEEDBACK: Are progress indicators, states, and actions clear?
|
|
1369
|
-
- Look for: weak progress visualization, unclear states, orphaned status indicators
|
|
1370
|
-
|
|
1371
|
-
• GROUPING: Are related items visually connected? Do containers make sense?
|
|
1372
|
-
- Look for: related content that appears separate, missing visual boundaries
|
|
1529
|
+
A visual analysis identified these specific problems in the UI:
|
|
1373
1530
|
|
|
1374
|
-
|
|
1531
|
+
${problemsJson}
|
|
1375
1532
|
|
|
1376
|
-
|
|
1377
|
-
For each problem identified in Step 1, find the EXACT code in the file that causes it:
|
|
1533
|
+
YOUR TASK: Generate patches that fix ONLY these specific problems.
|
|
1378
1534
|
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1535
|
+
STRICT RULES:
|
|
1536
|
+
1. Each patch MUST include a "problemId" field referencing one of the problems above
|
|
1537
|
+
2. Do NOT make changes unrelated to the identified problems
|
|
1538
|
+
3. Make the SMALLEST change that fixes each problem
|
|
1539
|
+
4. Do NOT change layout systems (flex→grid) unless absolutely necessary to fix a problem
|
|
1540
|
+
5. Preserve existing structure - adjust properties, don't restructure
|
|
1383
1541
|
|
|
1384
|
-
|
|
1542
|
+
For each problem, find the exact code that causes it and create a targeted fix.
|
|
1385
1543
|
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1544
|
+
OUTPUT FORMAT (note the problemId and rationale fields):
|
|
1545
|
+
{
|
|
1546
|
+
"modifications": [{
|
|
1547
|
+
"filePath": "components/Example.tsx",
|
|
1548
|
+
"patches": [
|
|
1549
|
+
{
|
|
1550
|
+
"problemId": "grouping-1",
|
|
1551
|
+
"search": "<Badge className=\"absolute right-4\">",
|
|
1552
|
+
"replace": "<Badge className=\"ml-2\">",
|
|
1553
|
+
"rationale": "Removes absolute positioning to group badge with label"
|
|
1554
|
+
},
|
|
1555
|
+
{
|
|
1556
|
+
"problemId": "spacing-1",
|
|
1557
|
+
"search": "className=\"mt-8\"",
|
|
1558
|
+
"replace": "className=\"mt-4\"",
|
|
1559
|
+
"rationale": "Reduces excessive vertical gap"
|
|
1560
|
+
}
|
|
1561
|
+
]
|
|
1562
|
+
}]
|
|
1563
|
+
}
|
|
1392
1564
|
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
• Every change must have a clear reason tied to your analysis
|
|
1565
|
+
VALIDATION:
|
|
1566
|
+
- Patches without a valid problemId will be REJECTED
|
|
1567
|
+
- Patches that don't trace to an identified problem will be REJECTED
|
|
1568
|
+
- Each patch must have search, replace, problemId, and rationale fields
|
|
1398
1569
|
`;
|
|
1570
|
+
}
|
|
1399
1571
|
|
|
1400
1572
|
/**
|
|
1401
|
-
* DESIGN DECISION RULES
|
|
1402
|
-
* Informed decision rules that allow fixing real problems while preventing unnecessary rewrites.
|
|
1573
|
+
* DESIGN DECISION RULES for Phase 2
|
|
1403
1574
|
*/
|
|
1404
1575
|
const DESIGN_GUARDRAILS = `
|
|
1405
1576
|
═══════════════════════════════════════════════════════════════════════════════
|
|
1406
|
-
DESIGN
|
|
1577
|
+
DESIGN CONSTRAINTS
|
|
1407
1578
|
═══════════════════════════════════════════════════════════════════════════════
|
|
1408
1579
|
|
|
1409
1580
|
✓ ALLOWED when fixing identified problems:
|
|
1410
|
-
• Adjust spacing, padding
|
|
1411
|
-
•
|
|
1412
|
-
•
|
|
1413
|
-
•
|
|
1414
|
-
•
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
•
|
|
1419
|
-
•
|
|
1420
|
-
• Removing
|
|
1421
|
-
•
|
|
1422
|
-
• Making changes that aren't traceable to a specific visual problem you identified
|
|
1581
|
+
• Adjust spacing (margin, padding) - e.g., mt-4 → mt-2
|
|
1582
|
+
• Adjust alignment within containers - e.g., items-start → items-center
|
|
1583
|
+
• Move elements within the same container to improve grouping
|
|
1584
|
+
• Adjust typography - font size, weight, line-height
|
|
1585
|
+
• Add/remove whitespace classes
|
|
1586
|
+
|
|
1587
|
+
✗ FORBIDDEN (will cause rejection):
|
|
1588
|
+
• Changing layout system (flex → grid, grid → flex)
|
|
1589
|
+
• Restructuring component hierarchy
|
|
1590
|
+
• Wrapping elements in new containers (unless specifically needed for a grouping problem)
|
|
1591
|
+
• Removing or changing functionality
|
|
1592
|
+
• Changes without a problemId reference
|
|
1423
1593
|
|
|
1424
|
-
═══════════════════════════════════════════════════════════════════════════════
|
|
1425
|
-
PRINCIPLE: Every change must trace back to a specific visual problem you identified in Step 1.
|
|
1426
|
-
If you can't explain WHY a change fixes a specific problem, don't make that change.
|
|
1427
1594
|
═══════════════════════════════════════════════════════════════════════════════
|
|
1428
1595
|
`;
|
|
1429
1596
|
|
|
1597
|
+
/**
|
|
1598
|
+
* Validate patches against identified problems
|
|
1599
|
+
* Rejects patches that don't reference a valid problem ID
|
|
1600
|
+
*/
|
|
1601
|
+
function validateDesignPatches(
|
|
1602
|
+
patches: Array<{ search: string; replace: string; problemId?: string; rationale?: string }>,
|
|
1603
|
+
problems: DesignProblem[]
|
|
1604
|
+
): { valid: Array<{ search: string; replace: string; problemId: string; rationale: string }>; rejected: Array<{ patch: unknown; reason: string }> } {
|
|
1605
|
+
const validProblemIds = new Set(problems.map(p => p.id));
|
|
1606
|
+
const valid: Array<{ search: string; replace: string; problemId: string; rationale: string }> = [];
|
|
1607
|
+
const rejected: Array<{ patch: unknown; reason: string }> = [];
|
|
1608
|
+
|
|
1609
|
+
for (const patch of patches) {
|
|
1610
|
+
// Check for required fields
|
|
1611
|
+
if (!patch.search || !patch.replace) {
|
|
1612
|
+
rejected.push({ patch, reason: "Missing search or replace field" });
|
|
1613
|
+
continue;
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
// For design-heavy requests, require problemId
|
|
1617
|
+
if (!patch.problemId) {
|
|
1618
|
+
rejected.push({ patch, reason: "Missing problemId - patch doesn't reference an identified problem" });
|
|
1619
|
+
continue;
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
// Validate problemId exists in the problems list
|
|
1623
|
+
if (!validProblemIds.has(patch.problemId)) {
|
|
1624
|
+
rejected.push({ patch, reason: `Invalid problemId "${patch.problemId}" - not in identified problems list` });
|
|
1625
|
+
continue;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
// Check for forbidden structural changes
|
|
1629
|
+
const searchLower = patch.search.toLowerCase();
|
|
1630
|
+
const replaceLower = patch.replace.toLowerCase();
|
|
1631
|
+
|
|
1632
|
+
// Detect flex → grid or grid → flex conversions
|
|
1633
|
+
const hadFlex = searchLower.includes('flex') || searchLower.includes('items-') || searchLower.includes('justify-');
|
|
1634
|
+
const hasGrid = replaceLower.includes('grid') && !searchLower.includes('grid');
|
|
1635
|
+
const hadGrid = searchLower.includes('grid');
|
|
1636
|
+
const hasFlex = (replaceLower.includes('flex') || replaceLower.includes('items-') || replaceLower.includes('justify-')) && !hadFlex;
|
|
1637
|
+
|
|
1638
|
+
if ((hadFlex && hasGrid) || (hadGrid && hasFlex)) {
|
|
1639
|
+
rejected.push({ patch, reason: "Forbidden: Changing layout system (flex ↔ grid)" });
|
|
1640
|
+
continue;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
// Detect space-y/space-x additions that fundamentally change layout
|
|
1644
|
+
const addedSpaceY = replaceLower.includes('space-y') && !searchLower.includes('space-y');
|
|
1645
|
+
const addedSpaceX = replaceLower.includes('space-x') && !searchLower.includes('space-x');
|
|
1646
|
+
const hadJustifyBetween = searchLower.includes('justify-between');
|
|
1647
|
+
|
|
1648
|
+
if ((addedSpaceY || addedSpaceX) && hadJustifyBetween) {
|
|
1649
|
+
rejected.push({ patch, reason: "Forbidden: Converting flex layout to stack layout" });
|
|
1650
|
+
continue;
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
valid.push({
|
|
1654
|
+
search: patch.search,
|
|
1655
|
+
replace: patch.replace,
|
|
1656
|
+
problemId: patch.problemId,
|
|
1657
|
+
rationale: patch.rationale || 'No rationale provided'
|
|
1658
|
+
});
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
if (rejected.length > 0) {
|
|
1662
|
+
debugLog("Design patch validation: Some patches rejected", {
|
|
1663
|
+
validCount: valid.length,
|
|
1664
|
+
rejectedCount: rejected.length,
|
|
1665
|
+
rejected: rejected.map(r => ({ reason: r.reason, searchPreview: String(r.patch).substring(0, 50) }))
|
|
1666
|
+
});
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
return { valid, rejected };
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1430
1672
|
/**
|
|
1431
1673
|
* Detect if a user request is design-heavy and needs the reasoning protocol.
|
|
1432
1674
|
* Simple requests like "change button color to blue" don't need it.
|
|
@@ -1845,151 +2087,175 @@ export async function POST(request: Request) {
|
|
|
1845
2087
|
}
|
|
1846
2088
|
}
|
|
1847
2089
|
|
|
1848
|
-
//
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
`;
|
|
1855
|
-
|
|
1856
|
-
// ========== DESIGN REASONING PROTOCOL ==========
|
|
1857
|
-
// For design-heavy requests, add structured reasoning and guardrails
|
|
1858
|
-
// This prevents over-engineering and ensures thoughtful changes
|
|
1859
|
-
const isDesignRequest = isDesignHeavyRequest(userPrompt || '');
|
|
2090
|
+
// ========== FILE REDIRECT LOGIC (runs BEFORE building instructions) ==========
|
|
2091
|
+
// This determines the ACTUAL target file by checking for text matches in imported components
|
|
2092
|
+
// Must run first so that all subsequent code uses the correct file path
|
|
2093
|
+
let actualTargetFile = recommendedFileContent || (pageContext.pageContent ? { path: pageContext.pageFile, content: pageContext.pageContent } : null);
|
|
2094
|
+
let elementLocation: { lineNumber: number; snippet: string; confidence: 'high' | 'medium' | 'low'; matchedBy: string } | null = null;
|
|
1860
2095
|
|
|
1861
|
-
if (
|
|
1862
|
-
|
|
1863
|
-
prompt: userPrompt?.substring(0, 50),
|
|
1864
|
-
triggerKeywords: ['redesign', 'better', 'improve', 'cleaner', 'layout', 'modernize']
|
|
1865
|
-
.filter(k => userPrompt?.toLowerCase().includes(k))
|
|
1866
|
-
});
|
|
1867
|
-
|
|
1868
|
-
textContent += DESIGN_REASONING_PROMPT;
|
|
1869
|
-
textContent += DESIGN_GUARDRAILS;
|
|
1870
|
-
textContent += '\n';
|
|
1871
|
-
}
|
|
1872
|
-
|
|
1873
|
-
// ========== TARGET COMPONENT ONLY (with line numbers) ==========
|
|
1874
|
-
// CRITICAL: Only include the TARGET file to avoid overwhelming the LLM with noise
|
|
1875
|
-
if (recommendedFileContent) {
|
|
1876
|
-
const content = recommendedFileContent.content;
|
|
2096
|
+
if (actualTargetFile && focusedElements && focusedElements.length > 0) {
|
|
2097
|
+
const content = actualTargetFile.content;
|
|
1877
2098
|
|
|
1878
2099
|
// Search for focused element in the file using multiple strategies
|
|
1879
2100
|
// Priority: DOM id > textContent > className patterns
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
confidence: elementLocation.confidence,
|
|
1891
|
-
file: recommendedFileContent.path,
|
|
1892
|
-
});
|
|
1893
|
-
break;
|
|
1894
|
-
}
|
|
1895
|
-
}
|
|
1896
|
-
|
|
1897
|
-
// TEXT SCORING REDIRECT: Score ALL imported files by how many discovered
|
|
1898
|
-
// text strings they contain. The file with the highest score is the TRUE target.
|
|
1899
|
-
// This handles parent/child component cases accurately.
|
|
1900
|
-
if (elementLocation && elementLocation.confidence !== 'high') {
|
|
1901
|
-
// Collect ALL text content from focused elements
|
|
1902
|
-
const allTextContent = focusedElements
|
|
1903
|
-
.filter(e => e.textContent && e.textContent.length > 5)
|
|
1904
|
-
.map(e => e.textContent!);
|
|
1905
|
-
|
|
1906
|
-
if (allTextContent.length > 0) {
|
|
1907
|
-
debugLog("Medium/low confidence match - scoring imports for all text", {
|
|
1908
|
-
currentFile: recommendedFileContent.path,
|
|
1909
|
-
currentConfidence: elementLocation.confidence,
|
|
1910
|
-
textCount: allTextContent.length,
|
|
1911
|
-
texts: allTextContent.slice(0, 5).map(t => t.substring(0, 25))
|
|
1912
|
-
});
|
|
1913
|
-
|
|
1914
|
-
// Score all imported files
|
|
1915
|
-
const bestMatch = scoreFilesForTextContent(
|
|
1916
|
-
allTextContent,
|
|
1917
|
-
pageContext.componentSources
|
|
1918
|
-
);
|
|
1919
|
-
|
|
1920
|
-
// Also score the current file for comparison
|
|
1921
|
-
const currentFileScore = allTextContent.filter(text =>
|
|
1922
|
-
recommendedFileContent.content.includes(text.substring(0, 20))
|
|
1923
|
-
).length;
|
|
1924
|
-
|
|
1925
|
-
debugLog("Text scoring comparison", {
|
|
1926
|
-
currentFile: recommendedFileContent.path,
|
|
1927
|
-
currentScore: currentFileScore,
|
|
1928
|
-
bestImport: bestMatch?.path || 'none',
|
|
1929
|
-
bestImportScore: bestMatch?.score || 0
|
|
1930
|
-
});
|
|
1931
|
-
|
|
1932
|
-
// Redirect only if imported file has MORE matches than current file
|
|
1933
|
-
if (bestMatch && bestMatch.score > currentFileScore) {
|
|
1934
|
-
debugLog("TEXT REDIRECT: Imported file has more text matches", {
|
|
1935
|
-
originalFile: recommendedFileContent.path,
|
|
1936
|
-
originalScore: currentFileScore,
|
|
1937
|
-
redirectTo: bestMatch.path,
|
|
1938
|
-
redirectScore: bestMatch.score,
|
|
1939
|
-
matchedTexts: bestMatch.matchedTexts
|
|
1940
|
-
});
|
|
1941
|
-
|
|
1942
|
-
// Switch target file to where most text content lives
|
|
1943
|
-
actualTargetFile = {
|
|
1944
|
-
path: bestMatch.path,
|
|
1945
|
-
content: bestMatch.content
|
|
1946
|
-
};
|
|
1947
|
-
|
|
1948
|
-
// Find a line with matched text for element location
|
|
1949
|
-
const lines = bestMatch.content.split('\n');
|
|
1950
|
-
const snippetStart = Math.max(0, bestMatch.firstMatchLine - 4);
|
|
1951
|
-
const snippetEnd = Math.min(lines.length, bestMatch.firstMatchLine + 5);
|
|
1952
|
-
|
|
1953
|
-
elementLocation = {
|
|
1954
|
-
lineNumber: bestMatch.firstMatchLine,
|
|
1955
|
-
snippet: lines.slice(snippetStart, snippetEnd).join('\n'),
|
|
1956
|
-
confidence: 'high',
|
|
1957
|
-
matchedBy: `text scoring: ${bestMatch.score} matches in imported file`
|
|
1958
|
-
};
|
|
1959
|
-
}
|
|
1960
|
-
}
|
|
2101
|
+
for (const el of focusedElements) {
|
|
2102
|
+
elementLocation = findElementLineInFile(content, el);
|
|
2103
|
+
if (elementLocation) {
|
|
2104
|
+
debugLog("Found focused element in file", {
|
|
2105
|
+
matchedBy: elementLocation.matchedBy,
|
|
2106
|
+
lineNumber: elementLocation.lineNumber,
|
|
2107
|
+
confidence: elementLocation.confidence,
|
|
2108
|
+
file: actualTargetFile.path,
|
|
2109
|
+
});
|
|
2110
|
+
break;
|
|
1961
2111
|
}
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
// TEXT SCORING REDIRECT: Score ALL imported files by how many discovered
|
|
2115
|
+
// text strings they contain. The file with the highest score is the TRUE target.
|
|
2116
|
+
if (elementLocation && elementLocation.confidence !== 'high') {
|
|
2117
|
+
const allTextContent = focusedElements
|
|
2118
|
+
.filter(e => e.textContent && e.textContent.length > 5)
|
|
2119
|
+
.map(e => e.textContent!);
|
|
1962
2120
|
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
2121
|
+
if (allTextContent.length > 0) {
|
|
2122
|
+
debugLog("Medium/low confidence match - scoring imports for all text", {
|
|
2123
|
+
currentFile: actualTargetFile.path,
|
|
2124
|
+
currentConfidence: elementLocation.confidence,
|
|
2125
|
+
textCount: allTextContent.length,
|
|
2126
|
+
texts: allTextContent.slice(0, 5).map(t => t.substring(0, 25))
|
|
1968
2127
|
});
|
|
1969
2128
|
|
|
1970
|
-
const
|
|
1971
|
-
|
|
2129
|
+
const bestMatch = scoreFilesForTextContent(
|
|
2130
|
+
allTextContent,
|
|
1972
2131
|
pageContext.componentSources
|
|
1973
2132
|
);
|
|
1974
2133
|
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
2134
|
+
const currentFileScore = allTextContent.filter(text =>
|
|
2135
|
+
actualTargetFile!.content.includes(text.substring(0, 20))
|
|
2136
|
+
).length;
|
|
2137
|
+
|
|
2138
|
+
debugLog("Text scoring comparison", {
|
|
2139
|
+
currentFile: actualTargetFile.path,
|
|
2140
|
+
currentScore: currentFileScore,
|
|
2141
|
+
bestImport: bestMatch?.path || 'none',
|
|
2142
|
+
bestImportScore: bestMatch?.score || 0
|
|
2143
|
+
});
|
|
2144
|
+
|
|
2145
|
+
if (bestMatch && bestMatch.score > currentFileScore) {
|
|
2146
|
+
debugLog("TEXT REDIRECT: Imported file has more text matches", {
|
|
2147
|
+
originalFile: actualTargetFile.path,
|
|
2148
|
+
originalScore: currentFileScore,
|
|
2149
|
+
redirectTo: bestMatch.path,
|
|
2150
|
+
redirectScore: bestMatch.score,
|
|
2151
|
+
matchedTexts: bestMatch.matchedTexts
|
|
1981
2152
|
});
|
|
1982
2153
|
|
|
1983
|
-
// Switch target file to where element actually is
|
|
1984
2154
|
actualTargetFile = {
|
|
1985
|
-
path:
|
|
1986
|
-
content:
|
|
2155
|
+
path: bestMatch.path,
|
|
2156
|
+
content: bestMatch.content
|
|
2157
|
+
};
|
|
2158
|
+
|
|
2159
|
+
const lines = bestMatch.content.split('\n');
|
|
2160
|
+
const snippetStart = Math.max(0, bestMatch.firstMatchLine - 4);
|
|
2161
|
+
const snippetEnd = Math.min(lines.length, bestMatch.firstMatchLine + 5);
|
|
2162
|
+
|
|
2163
|
+
elementLocation = {
|
|
2164
|
+
lineNumber: bestMatch.firstMatchLine,
|
|
2165
|
+
snippet: lines.slice(snippetStart, snippetEnd).join('\n'),
|
|
2166
|
+
confidence: 'high',
|
|
2167
|
+
matchedBy: `text scoring: ${bestMatch.score} matches in imported file`
|
|
1987
2168
|
};
|
|
1988
|
-
elementLocation = findElementLineInFile(importedMatch.content, focusedElements[0]);
|
|
1989
2169
|
}
|
|
1990
2170
|
}
|
|
1991
2171
|
}
|
|
1992
2172
|
|
|
2173
|
+
// DYNAMIC IMPORT SEARCH: If not found in main file, search imported components
|
|
2174
|
+
if (!elementLocation) {
|
|
2175
|
+
debugLog("Element not in main file, searching imported components...", {
|
|
2176
|
+
mainFile: actualTargetFile.path,
|
|
2177
|
+
importedFilesCount: pageContext.componentSources.length
|
|
2178
|
+
});
|
|
2179
|
+
|
|
2180
|
+
const importedMatch = findElementInImportedFiles(
|
|
2181
|
+
focusedElements[0],
|
|
2182
|
+
pageContext.componentSources
|
|
2183
|
+
);
|
|
2184
|
+
|
|
2185
|
+
if (importedMatch) {
|
|
2186
|
+
debugLog("REDIRECT: Element found in imported component", {
|
|
2187
|
+
originalFile: actualTargetFile.path,
|
|
2188
|
+
redirectTo: importedMatch.path,
|
|
2189
|
+
matchedBy: importedMatch.matchedBy,
|
|
2190
|
+
lineNumber: importedMatch.lineNumber
|
|
2191
|
+
});
|
|
2192
|
+
|
|
2193
|
+
actualTargetFile = {
|
|
2194
|
+
path: importedMatch.path,
|
|
2195
|
+
content: importedMatch.content
|
|
2196
|
+
};
|
|
2197
|
+
elementLocation = findElementLineInFile(importedMatch.content, focusedElements[0]);
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
debugLog("File redirect complete", {
|
|
2203
|
+
originalRecommended: recommendedFileContent?.path || 'none',
|
|
2204
|
+
actualTarget: actualTargetFile?.path || 'none',
|
|
2205
|
+
wasRedirected: actualTargetFile?.path !== recommendedFileContent?.path
|
|
2206
|
+
});
|
|
2207
|
+
|
|
2208
|
+
// Build text content
|
|
2209
|
+
let textContent = `VISION MODE EDIT REQUEST
|
|
2210
|
+
|
|
2211
|
+
Page Route: ${pageRoute}
|
|
2212
|
+
User Request: "${userPrompt}"
|
|
2213
|
+
|
|
2214
|
+
`;
|
|
2215
|
+
|
|
2216
|
+
// ========== TWO-PHASE DESIGN ANALYSIS ==========
|
|
2217
|
+
// Phase 1: Identify specific visual problems (separate LLM call)
|
|
2218
|
+
// Phase 2: Generate patches that reference those problems
|
|
2219
|
+
const isDesignRequest = isDesignHeavyRequest(userPrompt || '');
|
|
2220
|
+
let identifiedProblems: DesignProblem[] = [];
|
|
2221
|
+
|
|
2222
|
+
if (isDesignRequest && screenshot) {
|
|
2223
|
+
debugLog("Design-heavy request detected, running Phase 1 analysis", {
|
|
2224
|
+
prompt: userPrompt?.substring(0, 50),
|
|
2225
|
+
triggerKeywords: ['redesign', 'better', 'improve', 'cleaner', 'layout', 'modernize']
|
|
2226
|
+
.filter(k => userPrompt?.toLowerCase().includes(k))
|
|
2227
|
+
});
|
|
2228
|
+
|
|
2229
|
+
// Phase 1: Analyze the screenshot to identify specific problems
|
|
2230
|
+
// This is a SEPARATE LLM call that only identifies problems
|
|
2231
|
+
// Use actualTargetFile since redirect has already happened
|
|
2232
|
+
const fileContentForAnalysis = actualTargetFile?.content || pageContext.pageContent || '';
|
|
2233
|
+
identifiedProblems = await analyzeDesignProblems(
|
|
2234
|
+
screenshot,
|
|
2235
|
+
fileContentForAnalysis,
|
|
2236
|
+
userPrompt || '',
|
|
2237
|
+
apiKey
|
|
2238
|
+
);
|
|
2239
|
+
|
|
2240
|
+
debugLog("Phase 1 complete: Problems identified", {
|
|
2241
|
+
problemCount: identifiedProblems.length,
|
|
2242
|
+
problems: identifiedProblems.map(p => `${p.id}: ${p.description.substring(0, 50)}`)
|
|
2243
|
+
});
|
|
2244
|
+
|
|
2245
|
+
// Add Phase 2 prompt with the identified problems
|
|
2246
|
+
if (identifiedProblems.length > 0) {
|
|
2247
|
+
textContent += buildPhase2DesignPrompt(identifiedProblems);
|
|
2248
|
+
textContent += DESIGN_GUARDRAILS;
|
|
2249
|
+
textContent += '\n';
|
|
2250
|
+
} else {
|
|
2251
|
+
debugLog("No problems identified - proceeding with standard edit");
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
// ========== TARGET COMPONENT ONLY (with line numbers) ==========
|
|
2256
|
+
// CRITICAL: Only include the TARGET file to avoid overwhelming the LLM with noise
|
|
2257
|
+
// Note: Redirect logic already ran above, so actualTargetFile is the correct file
|
|
2258
|
+
if (actualTargetFile) {
|
|
1993
2259
|
// Build focused elements section with precise targeting info
|
|
1994
2260
|
if (focusedElements && focusedElements.length > 0) {
|
|
1995
2261
|
textContent += `FOCUSED ELEMENTS (user clicked on these):\n`;
|
|
@@ -2053,7 +2319,7 @@ ${elementLocation.snippet}
|
|
|
2053
2319
|
// Element NOT found in main file OR any imported components
|
|
2054
2320
|
// BLOCK the LLM from guessing - require empty modifications
|
|
2055
2321
|
debugLog("BLOCK: Could not locate focused element anywhere", {
|
|
2056
|
-
mainFile:
|
|
2322
|
+
mainFile: actualTargetFile.path,
|
|
2057
2323
|
searchedImports: pageContext.componentSources.length,
|
|
2058
2324
|
focusedElements: focusedElements.map(el => ({
|
|
2059
2325
|
name: el.name,
|
|
@@ -2069,7 +2335,7 @@ ${elementLocation.snippet}
|
|
|
2069
2335
|
⛔ STOP: CANNOT LOCATE THE CLICKED ELEMENT
|
|
2070
2336
|
|
|
2071
2337
|
The user clicked on a specific element, but it could NOT be found in:
|
|
2072
|
-
- ${
|
|
2338
|
+
- ${actualTargetFile.path} (main target file)
|
|
2073
2339
|
- Any of the ${pageContext.componentSources.length} imported component files
|
|
2074
2340
|
|
|
2075
2341
|
The element may be:
|
|
@@ -2184,7 +2450,8 @@ ${variantsMatch[0]}
|
|
|
2184
2450
|
}
|
|
2185
2451
|
|
|
2186
2452
|
// ========== SIMPLIFIED INSTRUCTIONS ==========
|
|
2187
|
-
|
|
2453
|
+
// Use actualTargetFile which may have been redirected to an imported component
|
|
2454
|
+
const targetPath = actualTargetFile?.path || pageContext.pageFile || "unknown";
|
|
2188
2455
|
|
|
2189
2456
|
textContent += `═══════════════════════════════════════════════════════════════════════════════
|
|
2190
2457
|
HOW TO MAKE YOUR EDIT
|
|
@@ -2228,10 +2495,14 @@ CRITICAL: Your "search" string MUST exist in the file. If you can't find the exa
|
|
|
2228
2495
|
validFilePaths.add(comp.path);
|
|
2229
2496
|
}
|
|
2230
2497
|
|
|
2231
|
-
// FIX: Add the recommended file
|
|
2498
|
+
// FIX: Add the recommended file and actual target file to validFilePaths
|
|
2499
|
+
// (recommended was spliced out for display, actual may be different due to redirect)
|
|
2232
2500
|
if (recommendedFileContent) {
|
|
2233
2501
|
validFilePaths.add(recommendedFileContent.path);
|
|
2234
2502
|
}
|
|
2503
|
+
if (actualTargetFile && actualTargetFile.path !== recommendedFileContent?.path) {
|
|
2504
|
+
validFilePaths.add(actualTargetFile.path);
|
|
2505
|
+
}
|
|
2235
2506
|
|
|
2236
2507
|
// Retry loop for handling patch failures
|
|
2237
2508
|
const MAX_RETRIES = 1;
|
|
@@ -2367,7 +2638,8 @@ This is better than generating patches with made-up code.`,
|
|
|
2367
2638
|
|
|
2368
2639
|
// CRITICAL: Warn if LLM is trying to modify a file OTHER than the TARGET COMPONENT
|
|
2369
2640
|
// This usually means the LLM is trying to modify a file it doesn't have full visibility into
|
|
2370
|
-
|
|
2641
|
+
// Use actualTargetFile since it may have been redirected from recommendedFileContent
|
|
2642
|
+
const targetComponentPath = actualTargetFile?.path;
|
|
2371
2643
|
if (targetComponentPath && mod.filePath !== targetComponentPath) {
|
|
2372
2644
|
debugLog("WARNING: LLM trying to modify non-target file", {
|
|
2373
2645
|
targetComponent: targetComponentPath,
|
|
@@ -2392,9 +2664,41 @@ This is better than generating patches with made-up code.`,
|
|
|
2392
2664
|
// New patch-based approach
|
|
2393
2665
|
console.log(`[Apply-First] Applying ${mod.patches.length} patches to ${mod.filePath}`);
|
|
2394
2666
|
|
|
2667
|
+
// DESIGN VALIDATION: For design-heavy requests, validate patches have problemIds
|
|
2668
|
+
let patchesToApply = mod.patches;
|
|
2669
|
+
if (isDesignRequest && identifiedProblems.length > 0) {
|
|
2670
|
+
const validationResult = validateDesignPatches(mod.patches, identifiedProblems);
|
|
2671
|
+
|
|
2672
|
+
if (validationResult.rejected.length > 0) {
|
|
2673
|
+
debugLog("Design patch validation rejected some patches", {
|
|
2674
|
+
filePath: mod.filePath,
|
|
2675
|
+
validCount: validationResult.valid.length,
|
|
2676
|
+
rejectedCount: validationResult.rejected.length,
|
|
2677
|
+
rejectedReasons: validationResult.rejected.map(r => r.reason)
|
|
2678
|
+
});
|
|
2679
|
+
console.warn(`[Apply-First] ${validationResult.rejected.length} patches rejected for missing/invalid problemId`);
|
|
2680
|
+
}
|
|
2681
|
+
|
|
2682
|
+
// Only use validated patches
|
|
2683
|
+
if (validationResult.valid.length === 0) {
|
|
2684
|
+
patchErrors.push(`${mod.filePath}: All patches were rejected - they don't reference identified problems`);
|
|
2685
|
+
continue;
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2688
|
+
// Log which problems are being fixed
|
|
2689
|
+
const problemsBeingFixed = [...new Set(validationResult.valid.map(p => p.problemId))];
|
|
2690
|
+
debugLog("Design patches validated", {
|
|
2691
|
+
filePath: mod.filePath,
|
|
2692
|
+
problemsBeingFixed,
|
|
2693
|
+
patchCount: validationResult.valid.length
|
|
2694
|
+
});
|
|
2695
|
+
|
|
2696
|
+
patchesToApply = validationResult.valid;
|
|
2697
|
+
}
|
|
2698
|
+
|
|
2395
2699
|
// PRE-VALIDATION: Check if all search strings exist in the file BEFORE applying
|
|
2396
2700
|
const preValidationErrors: string[] = [];
|
|
2397
|
-
for (const patch of
|
|
2701
|
+
for (const patch of patchesToApply) {
|
|
2398
2702
|
const normalizedSearch = patch.search.replace(/\\n/g, "\n");
|
|
2399
2703
|
if (!originalContent.includes(normalizedSearch)) {
|
|
2400
2704
|
// Try fuzzy match as fallback
|
|
@@ -2433,7 +2737,7 @@ This is better than generating patches with made-up code.`,
|
|
|
2433
2737
|
continue;
|
|
2434
2738
|
}
|
|
2435
2739
|
|
|
2436
|
-
const patchResult = applyPatches(originalContent,
|
|
2740
|
+
const patchResult = applyPatches(originalContent, patchesToApply);
|
|
2437
2741
|
|
|
2438
2742
|
if (!patchResult.success) {
|
|
2439
2743
|
const failedMessages = patchResult.failedPatches.map(
|
|
@@ -2443,15 +2747,15 @@ This is better than generating patches with made-up code.`,
|
|
|
2443
2747
|
|
|
2444
2748
|
// If some patches succeeded, use partial result
|
|
2445
2749
|
if (patchResult.appliedPatches > 0) {
|
|
2446
|
-
console.warn(`[Apply-First] ${patchResult.appliedPatches}/${
|
|
2750
|
+
console.warn(`[Apply-First] ${patchResult.appliedPatches}/${patchesToApply.length} patches applied to ${mod.filePath}`);
|
|
2447
2751
|
modifiedContent = patchResult.modifiedContent;
|
|
2448
|
-
explanation += ` (${patchResult.appliedPatches}/${
|
|
2752
|
+
explanation += ` (${patchResult.appliedPatches}/${patchesToApply.length} patches applied)`;
|
|
2449
2753
|
} else {
|
|
2450
2754
|
continue; // Skip this file entirely if no patches worked
|
|
2451
2755
|
}
|
|
2452
2756
|
} else {
|
|
2453
2757
|
modifiedContent = patchResult.modifiedContent;
|
|
2454
|
-
console.log(`[Apply-First] All ${
|
|
2758
|
+
console.log(`[Apply-First] All ${patchesToApply.length} patches applied successfully to ${mod.filePath}`);
|
|
2455
2759
|
}
|
|
2456
2760
|
|
|
2457
2761
|
// AST VALIDATION: Use Babel parser to catch actual syntax errors
|