sonance-brand-mcp 1.3.91 → 1.3.93
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.
- package/dist/assets/api/sonance-vision-apply/route.ts +150 -16
- package/dist/assets/api/sonance-vision-edit/route.ts +150 -16
- package/dist/assets/dev-tools/SonanceDevTools.tsx +121 -0
- package/dist/assets/dev-tools/components/ChatInterface.tsx +130 -9
- package/dist/assets/dev-tools/components/SectionHighlight.tsx +124 -0
- package/dist/assets/dev-tools/types.ts +16 -0
- package/package.json +1 -1
|
@@ -19,6 +19,20 @@ import * as babelParser from "@babel/parser";
|
|
|
19
19
|
* DEVELOPMENT ONLY.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
+
/** Parent section context for section-level targeting */
|
|
23
|
+
interface ParentSectionInfo {
|
|
24
|
+
/** Semantic container type (section, article, form, card, dialog, etc.) */
|
|
25
|
+
type: string;
|
|
26
|
+
/** Key text content in the section (first heading, labels) */
|
|
27
|
+
sectionText?: string;
|
|
28
|
+
/** The parent container's className for code matching */
|
|
29
|
+
className?: string;
|
|
30
|
+
/** Coordinates of the parent section */
|
|
31
|
+
coordinates?: { x: number; y: number; width: number; height: number };
|
|
32
|
+
/** Parent's element ID if available */
|
|
33
|
+
elementId?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
22
36
|
interface VisionFocusedElement {
|
|
23
37
|
name: string;
|
|
24
38
|
type: string;
|
|
@@ -38,6 +52,8 @@ interface VisionFocusedElement {
|
|
|
38
52
|
elementId?: string;
|
|
39
53
|
/** IDs of child elements for more precise targeting */
|
|
40
54
|
childIds?: string[];
|
|
55
|
+
/** Parent section context for section-level changes */
|
|
56
|
+
parentSection?: ParentSectionInfo;
|
|
41
57
|
}
|
|
42
58
|
|
|
43
59
|
interface VisionFileModification {
|
|
@@ -500,6 +516,50 @@ function findElementLineInFile(
|
|
|
500
516
|
return null;
|
|
501
517
|
}
|
|
502
518
|
|
|
519
|
+
/**
|
|
520
|
+
* Search imported component files for the focused element
|
|
521
|
+
* Reuses findElementLineInFile() for consistent detection
|
|
522
|
+
*
|
|
523
|
+
* This is called when the element cannot be found in the main target file,
|
|
524
|
+
* allowing us to redirect to the actual file containing the element.
|
|
525
|
+
*/
|
|
526
|
+
function findElementInImportedFiles(
|
|
527
|
+
focusedElement: VisionFocusedElement,
|
|
528
|
+
importedFiles: { path: string; content: string }[]
|
|
529
|
+
): { path: string; lineNumber: number; matchedBy: string; content: string; confidence: string } | null {
|
|
530
|
+
debugLog("Searching imported components for element", {
|
|
531
|
+
elementType: focusedElement.type,
|
|
532
|
+
textContent: focusedElement.textContent?.substring(0, 30),
|
|
533
|
+
filesCount: importedFiles.length
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
for (const file of importedFiles) {
|
|
537
|
+
// Focus on component files (where UI elements live)
|
|
538
|
+
// Skip types, stores, utils, hooks - they don't contain JSX elements
|
|
539
|
+
if (!file.path.includes('components/') && !file.path.includes('/ui/')) continue;
|
|
540
|
+
|
|
541
|
+
const result = findElementLineInFile(file.content, focusedElement);
|
|
542
|
+
if (result && result.confidence !== 'low') {
|
|
543
|
+
debugLog("Found element in imported component", {
|
|
544
|
+
path: file.path,
|
|
545
|
+
lineNumber: result.lineNumber,
|
|
546
|
+
matchedBy: result.matchedBy,
|
|
547
|
+
confidence: result.confidence
|
|
548
|
+
});
|
|
549
|
+
return {
|
|
550
|
+
path: file.path,
|
|
551
|
+
lineNumber: result.lineNumber,
|
|
552
|
+
matchedBy: result.matchedBy,
|
|
553
|
+
content: file.content,
|
|
554
|
+
confidence: result.confidence
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
debugLog("Element not found in any imported component files");
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
|
|
503
563
|
/**
|
|
504
564
|
* PHASE 0: Deterministic Element ID Search (Cursor-style)
|
|
505
565
|
* Grep entire codebase for the element ID. If found in multiple files,
|
|
@@ -1604,6 +1664,8 @@ User Request: "${userPrompt}"
|
|
|
1604
1664
|
// Search for focused element in the file using multiple strategies
|
|
1605
1665
|
// Priority: DOM id > textContent > className patterns
|
|
1606
1666
|
let elementLocation: { lineNumber: number; snippet: string; confidence: 'high' | 'medium' | 'low'; matchedBy: string } | null = null;
|
|
1667
|
+
let actualTargetFile = recommendedFileContent; // May change if we redirect
|
|
1668
|
+
|
|
1607
1669
|
if (focusedElements && focusedElements.length > 0) {
|
|
1608
1670
|
for (const el of focusedElements) {
|
|
1609
1671
|
elementLocation = findElementLineInFile(content, el);
|
|
@@ -1617,6 +1679,35 @@ User Request: "${userPrompt}"
|
|
|
1617
1679
|
break;
|
|
1618
1680
|
}
|
|
1619
1681
|
}
|
|
1682
|
+
|
|
1683
|
+
// DYNAMIC IMPORT SEARCH: If not found in main file, search imported components
|
|
1684
|
+
if (!elementLocation) {
|
|
1685
|
+
debugLog("Element not in main file, searching imported components...", {
|
|
1686
|
+
mainFile: recommendedFileContent.path,
|
|
1687
|
+
importedFilesCount: pageContext.componentSources.length
|
|
1688
|
+
});
|
|
1689
|
+
|
|
1690
|
+
const importedMatch = findElementInImportedFiles(
|
|
1691
|
+
focusedElements[0],
|
|
1692
|
+
pageContext.componentSources
|
|
1693
|
+
);
|
|
1694
|
+
|
|
1695
|
+
if (importedMatch) {
|
|
1696
|
+
debugLog("REDIRECT: Element found in imported component", {
|
|
1697
|
+
originalFile: recommendedFileContent.path,
|
|
1698
|
+
redirectTo: importedMatch.path,
|
|
1699
|
+
matchedBy: importedMatch.matchedBy,
|
|
1700
|
+
lineNumber: importedMatch.lineNumber
|
|
1701
|
+
});
|
|
1702
|
+
|
|
1703
|
+
// Switch target file to where element actually is
|
|
1704
|
+
actualTargetFile = {
|
|
1705
|
+
path: importedMatch.path,
|
|
1706
|
+
content: importedMatch.content
|
|
1707
|
+
};
|
|
1708
|
+
elementLocation = findElementLineInFile(importedMatch.content, focusedElements[0]);
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1620
1711
|
}
|
|
1621
1712
|
|
|
1622
1713
|
// Build focused elements section with precise targeting info
|
|
@@ -1628,6 +1719,36 @@ User Request: "${userPrompt}"
|
|
|
1628
1719
|
textContent += ` with text "${el.textContent.substring(0, 30)}"`;
|
|
1629
1720
|
}
|
|
1630
1721
|
textContent += `\n`;
|
|
1722
|
+
|
|
1723
|
+
// Include parent section context for section-level targeting
|
|
1724
|
+
if (el.parentSection) {
|
|
1725
|
+
textContent += ` └─ Parent Section: <${el.parentSection.type}>`;
|
|
1726
|
+
if (el.parentSection.sectionText) {
|
|
1727
|
+
textContent += ` containing "${el.parentSection.sectionText.substring(0, 60)}${el.parentSection.sectionText.length > 60 ? '...' : ''}"`;
|
|
1728
|
+
}
|
|
1729
|
+
if (el.parentSection.elementId) {
|
|
1730
|
+
textContent += ` (id="${el.parentSection.elementId}")`;
|
|
1731
|
+
}
|
|
1732
|
+
if (el.parentSection.className) {
|
|
1733
|
+
// Show first 80 chars of className for context
|
|
1734
|
+
const truncatedClass = el.parentSection.className.length > 80
|
|
1735
|
+
? el.parentSection.className.substring(0, 80) + '...'
|
|
1736
|
+
: el.parentSection.className;
|
|
1737
|
+
textContent += `\n className="${truncatedClass}"`;
|
|
1738
|
+
}
|
|
1739
|
+
textContent += `\n`;
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
// Detect section-level prompts and provide guidance
|
|
1744
|
+
const sectionKeywords = ['section', 'area', 'container', 'card', 'panel', 'header', 'form', 'region', 'compact', 'modernize', 'redesign', 'layout'];
|
|
1745
|
+
const isSectionLevelChange = sectionKeywords.some(kw => userPrompt?.toLowerCase().includes(kw));
|
|
1746
|
+
if (isSectionLevelChange && focusedElements.some(el => el.parentSection)) {
|
|
1747
|
+
textContent += `
|
|
1748
|
+
📍 SECTION-LEVEL CHANGE DETECTED: The user's prompt mentions section/area keywords.
|
|
1749
|
+
If modifying the parent section (not just the clicked element), target the container
|
|
1750
|
+
mentioned above. Look for elements matching the section className or id.
|
|
1751
|
+
`;
|
|
1631
1752
|
}
|
|
1632
1753
|
|
|
1633
1754
|
// Add precise targeting with line number and snippet
|
|
@@ -1649,10 +1770,11 @@ ${elementLocation.snippet}
|
|
|
1649
1770
|
|
|
1650
1771
|
`;
|
|
1651
1772
|
} else {
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1773
|
+
// Element NOT found in main file OR any imported components
|
|
1774
|
+
// BLOCK the LLM from guessing - require empty modifications
|
|
1775
|
+
debugLog("BLOCK: Could not locate focused element anywhere", {
|
|
1776
|
+
mainFile: recommendedFileContent.path,
|
|
1777
|
+
searchedImports: pageContext.componentSources.length,
|
|
1656
1778
|
focusedElements: focusedElements.map(el => ({
|
|
1657
1779
|
name: el.name,
|
|
1658
1780
|
type: el.type,
|
|
@@ -1662,27 +1784,38 @@ ${elementLocation.snippet}
|
|
|
1662
1784
|
}))
|
|
1663
1785
|
});
|
|
1664
1786
|
|
|
1665
|
-
//
|
|
1787
|
+
// STRONG BLOCK instruction - tell LLM to NOT guess
|
|
1666
1788
|
textContent += `
|
|
1667
|
-
|
|
1789
|
+
⛔ STOP: CANNOT LOCATE THE CLICKED ELEMENT
|
|
1790
|
+
|
|
1791
|
+
The user clicked on a specific element, but it could NOT be found in:
|
|
1792
|
+
- ${recommendedFileContent.path} (main target file)
|
|
1793
|
+
- Any of the ${pageContext.componentSources.length} imported component files
|
|
1794
|
+
|
|
1668
1795
|
The element may be:
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1796
|
+
- Deeply nested in a component not in the import tree
|
|
1797
|
+
- Dynamically generated at runtime
|
|
1798
|
+
- Part of a third-party library component
|
|
1672
1799
|
|
|
1673
|
-
|
|
1800
|
+
DO NOT GUESS. Return this exact response:
|
|
1801
|
+
{
|
|
1802
|
+
"modifications": [],
|
|
1803
|
+
"explanation": "Could not locate the clicked element in this file or any of its ${pageContext.componentSources.length} imported components. The element may be rendered by a deeply nested or third-party component."
|
|
1804
|
+
}
|
|
1674
1805
|
|
|
1675
1806
|
`;
|
|
1676
1807
|
}
|
|
1677
1808
|
}
|
|
1678
1809
|
|
|
1679
1810
|
// Add line numbers to make it easy for LLM to reference exact code
|
|
1680
|
-
|
|
1811
|
+
// Use actualTargetFile which may have been redirected to an imported component
|
|
1812
|
+
const targetContent = actualTargetFile.content;
|
|
1813
|
+
const linesWithNumbers = targetContent.split('\n').map((line, i) =>
|
|
1681
1814
|
`${String(i + 1).padStart(4, ' ')}| ${line}`
|
|
1682
1815
|
).join('\n');
|
|
1683
1816
|
|
|
1684
1817
|
textContent += `═══════════════════════════════════════════════════════════════════════════════
|
|
1685
|
-
FILE TO EDIT: ${
|
|
1818
|
+
FILE TO EDIT: ${actualTargetFile.path}
|
|
1686
1819
|
═══════════════════════════════════════════════════════════════════════════════
|
|
1687
1820
|
|
|
1688
1821
|
IMPORTANT: Copy code EXACTLY as shown below (including line numbers for reference).
|
|
@@ -1693,11 +1826,12 @@ ${linesWithNumbers}
|
|
|
1693
1826
|
\`\`\`
|
|
1694
1827
|
|
|
1695
1828
|
`;
|
|
1696
|
-
usedContext +=
|
|
1829
|
+
usedContext += targetContent.length;
|
|
1697
1830
|
debugLog("Added TARGET COMPONENT with line numbers", {
|
|
1698
|
-
path:
|
|
1699
|
-
lines:
|
|
1700
|
-
size:
|
|
1831
|
+
path: actualTargetFile.path,
|
|
1832
|
+
lines: targetContent.split('\n').length,
|
|
1833
|
+
size: targetContent.length,
|
|
1834
|
+
wasRedirected: actualTargetFile.path !== recommendedFileContent.path
|
|
1701
1835
|
});
|
|
1702
1836
|
} else if (pageContext.pageContent) {
|
|
1703
1837
|
// Fallback: use page file if no recommended file
|
|
@@ -18,6 +18,20 @@ import * as babelParser from "@babel/parser";
|
|
|
18
18
|
* DEVELOPMENT ONLY.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
+
/** Parent section context for section-level targeting */
|
|
22
|
+
interface ParentSectionInfo {
|
|
23
|
+
/** Semantic container type (section, article, form, card, dialog, etc.) */
|
|
24
|
+
type: string;
|
|
25
|
+
/** Key text content in the section (first heading, labels) */
|
|
26
|
+
sectionText?: string;
|
|
27
|
+
/** The parent container's className for code matching */
|
|
28
|
+
className?: string;
|
|
29
|
+
/** Coordinates of the parent section */
|
|
30
|
+
coordinates?: { x: number; y: number; width: number; height: number };
|
|
31
|
+
/** Parent's element ID if available */
|
|
32
|
+
elementId?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
21
35
|
interface VisionFocusedElement {
|
|
22
36
|
name: string;
|
|
23
37
|
type: string;
|
|
@@ -37,6 +51,8 @@ interface VisionFocusedElement {
|
|
|
37
51
|
elementId?: string;
|
|
38
52
|
/** IDs of child elements for more precise targeting */
|
|
39
53
|
childIds?: string[];
|
|
54
|
+
/** Parent section context for section-level changes */
|
|
55
|
+
parentSection?: ParentSectionInfo;
|
|
40
56
|
}
|
|
41
57
|
|
|
42
58
|
interface VisionFileModification {
|
|
@@ -496,6 +512,50 @@ function findElementLineInFile(
|
|
|
496
512
|
return null;
|
|
497
513
|
}
|
|
498
514
|
|
|
515
|
+
/**
|
|
516
|
+
* Search imported component files for the focused element
|
|
517
|
+
* Reuses findElementLineInFile() for consistent detection
|
|
518
|
+
*
|
|
519
|
+
* This is called when the element cannot be found in the main target file,
|
|
520
|
+
* allowing us to redirect to the actual file containing the element.
|
|
521
|
+
*/
|
|
522
|
+
function findElementInImportedFiles(
|
|
523
|
+
focusedElement: VisionFocusedElement,
|
|
524
|
+
importedFiles: { path: string; content: string }[]
|
|
525
|
+
): { path: string; lineNumber: number; matchedBy: string; content: string; confidence: string } | null {
|
|
526
|
+
debugLog("Searching imported components for element", {
|
|
527
|
+
elementType: focusedElement.type,
|
|
528
|
+
textContent: focusedElement.textContent?.substring(0, 30),
|
|
529
|
+
filesCount: importedFiles.length
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
for (const file of importedFiles) {
|
|
533
|
+
// Focus on component files (where UI elements live)
|
|
534
|
+
// Skip types, stores, utils, hooks - they don't contain JSX elements
|
|
535
|
+
if (!file.path.includes('components/') && !file.path.includes('/ui/')) continue;
|
|
536
|
+
|
|
537
|
+
const result = findElementLineInFile(file.content, focusedElement);
|
|
538
|
+
if (result && result.confidence !== 'low') {
|
|
539
|
+
debugLog("Found element in imported component", {
|
|
540
|
+
path: file.path,
|
|
541
|
+
lineNumber: result.lineNumber,
|
|
542
|
+
matchedBy: result.matchedBy,
|
|
543
|
+
confidence: result.confidence
|
|
544
|
+
});
|
|
545
|
+
return {
|
|
546
|
+
path: file.path,
|
|
547
|
+
lineNumber: result.lineNumber,
|
|
548
|
+
matchedBy: result.matchedBy,
|
|
549
|
+
content: file.content,
|
|
550
|
+
confidence: result.confidence
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
debugLog("Element not found in any imported component files");
|
|
556
|
+
return null;
|
|
557
|
+
}
|
|
558
|
+
|
|
499
559
|
/**
|
|
500
560
|
* PHASE 0: Deterministic Element ID Search (Cursor-style)
|
|
501
561
|
* Grep entire codebase for the element ID. If found in multiple files,
|
|
@@ -1573,6 +1633,8 @@ User Request: "${userPrompt}"
|
|
|
1573
1633
|
// Search for focused element in the file using multiple strategies
|
|
1574
1634
|
// Priority: DOM id > textContent > className patterns
|
|
1575
1635
|
let elementLocation: { lineNumber: number; snippet: string; confidence: 'high' | 'medium' | 'low'; matchedBy: string } | null = null;
|
|
1636
|
+
let actualTargetFile = recommendedFileContent; // May change if we redirect
|
|
1637
|
+
|
|
1576
1638
|
if (focusedElements && focusedElements.length > 0) {
|
|
1577
1639
|
for (const el of focusedElements) {
|
|
1578
1640
|
elementLocation = findElementLineInFile(content, el);
|
|
@@ -1586,6 +1648,35 @@ User Request: "${userPrompt}"
|
|
|
1586
1648
|
break;
|
|
1587
1649
|
}
|
|
1588
1650
|
}
|
|
1651
|
+
|
|
1652
|
+
// DYNAMIC IMPORT SEARCH: If not found in main file, search imported components
|
|
1653
|
+
if (!elementLocation) {
|
|
1654
|
+
debugLog("Element not in main file, searching imported components...", {
|
|
1655
|
+
mainFile: recommendedFileContent.path,
|
|
1656
|
+
importedFilesCount: pageContext.componentSources.length
|
|
1657
|
+
});
|
|
1658
|
+
|
|
1659
|
+
const importedMatch = findElementInImportedFiles(
|
|
1660
|
+
focusedElements[0],
|
|
1661
|
+
pageContext.componentSources
|
|
1662
|
+
);
|
|
1663
|
+
|
|
1664
|
+
if (importedMatch) {
|
|
1665
|
+
debugLog("REDIRECT: Element found in imported component", {
|
|
1666
|
+
originalFile: recommendedFileContent.path,
|
|
1667
|
+
redirectTo: importedMatch.path,
|
|
1668
|
+
matchedBy: importedMatch.matchedBy,
|
|
1669
|
+
lineNumber: importedMatch.lineNumber
|
|
1670
|
+
});
|
|
1671
|
+
|
|
1672
|
+
// Switch target file to where element actually is
|
|
1673
|
+
actualTargetFile = {
|
|
1674
|
+
path: importedMatch.path,
|
|
1675
|
+
content: importedMatch.content
|
|
1676
|
+
};
|
|
1677
|
+
elementLocation = findElementLineInFile(importedMatch.content, focusedElements[0]);
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1589
1680
|
}
|
|
1590
1681
|
|
|
1591
1682
|
// Build focused elements section with precise targeting info
|
|
@@ -1597,6 +1688,36 @@ User Request: "${userPrompt}"
|
|
|
1597
1688
|
textContent += ` with text "${el.textContent.substring(0, 30)}"`;
|
|
1598
1689
|
}
|
|
1599
1690
|
textContent += `\n`;
|
|
1691
|
+
|
|
1692
|
+
// Include parent section context for section-level targeting
|
|
1693
|
+
if (el.parentSection) {
|
|
1694
|
+
textContent += ` └─ Parent Section: <${el.parentSection.type}>`;
|
|
1695
|
+
if (el.parentSection.sectionText) {
|
|
1696
|
+
textContent += ` containing "${el.parentSection.sectionText.substring(0, 60)}${el.parentSection.sectionText.length > 60 ? '...' : ''}"`;
|
|
1697
|
+
}
|
|
1698
|
+
if (el.parentSection.elementId) {
|
|
1699
|
+
textContent += ` (id="${el.parentSection.elementId}")`;
|
|
1700
|
+
}
|
|
1701
|
+
if (el.parentSection.className) {
|
|
1702
|
+
// Show first 80 chars of className for context
|
|
1703
|
+
const truncatedClass = el.parentSection.className.length > 80
|
|
1704
|
+
? el.parentSection.className.substring(0, 80) + '...'
|
|
1705
|
+
: el.parentSection.className;
|
|
1706
|
+
textContent += `\n className="${truncatedClass}"`;
|
|
1707
|
+
}
|
|
1708
|
+
textContent += `\n`;
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
// Detect section-level prompts and provide guidance
|
|
1713
|
+
const sectionKeywords = ['section', 'area', 'container', 'card', 'panel', 'header', 'form', 'region', 'compact', 'modernize', 'redesign', 'layout'];
|
|
1714
|
+
const isSectionLevelChange = sectionKeywords.some(kw => userPrompt?.toLowerCase().includes(kw));
|
|
1715
|
+
if (isSectionLevelChange && focusedElements.some(el => el.parentSection)) {
|
|
1716
|
+
textContent += `
|
|
1717
|
+
📍 SECTION-LEVEL CHANGE DETECTED: The user's prompt mentions section/area keywords.
|
|
1718
|
+
If modifying the parent section (not just the clicked element), target the container
|
|
1719
|
+
mentioned above. Look for elements matching the section className or id.
|
|
1720
|
+
`;
|
|
1600
1721
|
}
|
|
1601
1722
|
|
|
1602
1723
|
// Add precise targeting with line number and snippet
|
|
@@ -1618,10 +1739,11 @@ ${elementLocation.snippet}
|
|
|
1618
1739
|
|
|
1619
1740
|
`;
|
|
1620
1741
|
} else {
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1742
|
+
// Element NOT found in main file OR any imported components
|
|
1743
|
+
// BLOCK the LLM from guessing - require empty modifications
|
|
1744
|
+
debugLog("BLOCK: Could not locate focused element anywhere", {
|
|
1745
|
+
mainFile: recommendedFileContent.path,
|
|
1746
|
+
searchedImports: pageContext.componentSources.length,
|
|
1625
1747
|
focusedElements: focusedElements.map(el => ({
|
|
1626
1748
|
name: el.name,
|
|
1627
1749
|
type: el.type,
|
|
@@ -1631,27 +1753,38 @@ ${elementLocation.snippet}
|
|
|
1631
1753
|
}))
|
|
1632
1754
|
});
|
|
1633
1755
|
|
|
1634
|
-
//
|
|
1756
|
+
// STRONG BLOCK instruction - tell LLM to NOT guess
|
|
1635
1757
|
textContent += `
|
|
1636
|
-
|
|
1758
|
+
⛔ STOP: CANNOT LOCATE THE CLICKED ELEMENT
|
|
1759
|
+
|
|
1760
|
+
The user clicked on a specific element, but it could NOT be found in:
|
|
1761
|
+
- ${recommendedFileContent.path} (main target file)
|
|
1762
|
+
- Any of the ${pageContext.componentSources.length} imported component files
|
|
1763
|
+
|
|
1637
1764
|
The element may be:
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1765
|
+
- Deeply nested in a component not in the import tree
|
|
1766
|
+
- Dynamically generated at runtime
|
|
1767
|
+
- Part of a third-party library component
|
|
1641
1768
|
|
|
1642
|
-
|
|
1769
|
+
DO NOT GUESS. Return this exact response:
|
|
1770
|
+
{
|
|
1771
|
+
"modifications": [],
|
|
1772
|
+
"explanation": "Could not locate the clicked element in this file or any of its ${pageContext.componentSources.length} imported components. The element may be rendered by a deeply nested or third-party component."
|
|
1773
|
+
}
|
|
1643
1774
|
|
|
1644
1775
|
`;
|
|
1645
1776
|
}
|
|
1646
1777
|
}
|
|
1647
1778
|
|
|
1648
1779
|
// Add line numbers to make it easy for LLM to reference exact code
|
|
1649
|
-
|
|
1780
|
+
// Use actualTargetFile which may have been redirected to an imported component
|
|
1781
|
+
const targetContent = actualTargetFile.content;
|
|
1782
|
+
const linesWithNumbers = targetContent.split('\n').map((line, i) =>
|
|
1650
1783
|
`${String(i + 1).padStart(4, ' ')}| ${line}`
|
|
1651
1784
|
).join('\n');
|
|
1652
1785
|
|
|
1653
1786
|
textContent += `═══════════════════════════════════════════════════════════════════════════════
|
|
1654
|
-
FILE TO EDIT: ${
|
|
1787
|
+
FILE TO EDIT: ${actualTargetFile.path}
|
|
1655
1788
|
═══════════════════════════════════════════════════════════════════════════════
|
|
1656
1789
|
|
|
1657
1790
|
IMPORTANT: Copy code EXACTLY as shown below (including line numbers for reference).
|
|
@@ -1662,11 +1795,12 @@ ${linesWithNumbers}
|
|
|
1662
1795
|
\`\`\`
|
|
1663
1796
|
|
|
1664
1797
|
`;
|
|
1665
|
-
usedContext +=
|
|
1798
|
+
usedContext += targetContent.length;
|
|
1666
1799
|
debugLog("Added TARGET COMPONENT with line numbers", {
|
|
1667
|
-
path:
|
|
1668
|
-
lines:
|
|
1669
|
-
size:
|
|
1800
|
+
path: actualTargetFile.path,
|
|
1801
|
+
lines: targetContent.split('\n').length,
|
|
1802
|
+
size: targetContent.length,
|
|
1803
|
+
wasRedirected: actualTargetFile.path !== recommendedFileContent.path
|
|
1670
1804
|
});
|
|
1671
1805
|
} else if (pageContext.pageContent) {
|
|
1672
1806
|
// Fallback: use page file if no recommended file
|
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
ApplyFirstStatus,
|
|
49
49
|
TextOverride,
|
|
50
50
|
OriginalTextState,
|
|
51
|
+
ParentSectionInfo,
|
|
51
52
|
} from "./types";
|
|
52
53
|
import {
|
|
53
54
|
tabs,
|
|
@@ -71,6 +72,7 @@ import { InspectorOverlay } from "./components/InspectorOverlay";
|
|
|
71
72
|
import { ChatInterface } from "./components/ChatInterface";
|
|
72
73
|
import { DiffPreview } from "./components/DiffPreview";
|
|
73
74
|
import { VisionModeBorder } from "./components/VisionModeBorder";
|
|
75
|
+
import { SectionHighlight } from "./components/SectionHighlight";
|
|
74
76
|
import { AnalysisPanel } from "./panels/AnalysisPanel";
|
|
75
77
|
import { TextPanel } from "./panels/TextPanel";
|
|
76
78
|
import { LogoToolsPanel } from "./panels/LogoToolsPanel";
|
|
@@ -82,6 +84,100 @@ import { ComponentsPanel } from "./panels/ComponentsPanel";
|
|
|
82
84
|
// A floating development overlay for brand theming
|
|
83
85
|
// ============================================
|
|
84
86
|
|
|
87
|
+
// ---- Helper Functions ----
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Extract the first meaningful heading or text content from a section
|
|
91
|
+
*/
|
|
92
|
+
function extractSectionHeading(container: Element): string | undefined {
|
|
93
|
+
// Look for headings first
|
|
94
|
+
const heading = container.querySelector('h1, h2, h3, h4, h5, h6');
|
|
95
|
+
if (heading && heading.textContent) {
|
|
96
|
+
const text = heading.textContent.trim();
|
|
97
|
+
// Return first 100 chars max
|
|
98
|
+
return text.length > 100 ? text.substring(0, 100) + '...' : text;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Look for labels or title elements
|
|
102
|
+
const label = container.querySelector('label, [class*="title"], [class*="heading"]');
|
|
103
|
+
if (label && label.textContent) {
|
|
104
|
+
const text = label.textContent.trim();
|
|
105
|
+
return text.length > 100 ? text.substring(0, 100) + '...' : text;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Look for aria-label or title attribute
|
|
109
|
+
const ariaLabel = container.getAttribute('aria-label');
|
|
110
|
+
if (ariaLabel) return ariaLabel;
|
|
111
|
+
|
|
112
|
+
const title = container.getAttribute('title');
|
|
113
|
+
if (title) return title;
|
|
114
|
+
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Find the nearest meaningful parent section/container for an element
|
|
120
|
+
* This helps the AI understand the broader context when making section-level changes
|
|
121
|
+
*/
|
|
122
|
+
function findParentSection(element: Element): ParentSectionInfo | undefined {
|
|
123
|
+
// Selectors for common section-like containers
|
|
124
|
+
const sectionSelectors = [
|
|
125
|
+
'section', 'article', 'form', 'dialog', 'aside', 'header', 'footer', 'main',
|
|
126
|
+
'[role="dialog"]', '[role="region"]', '[role="form"]', '[role="main"]',
|
|
127
|
+
'[role="complementary"]', '[role="banner"]', '[role="contentinfo"]'
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
// Class patterns that indicate section-like containers
|
|
131
|
+
const sectionClassPatterns = /\b(card|panel|section|container|modal|drawer|header|footer|sidebar|content|wrapper|box|block|group|region|area)\b/i;
|
|
132
|
+
|
|
133
|
+
let current = element.parentElement;
|
|
134
|
+
let depth = 0;
|
|
135
|
+
const maxDepth = 10; // Don't traverse too far up
|
|
136
|
+
|
|
137
|
+
while (current && current !== document.body && depth < maxDepth) {
|
|
138
|
+
// Skip DevTools elements
|
|
139
|
+
if (current.hasAttribute('data-sonance-devtools')) {
|
|
140
|
+
current = current.parentElement;
|
|
141
|
+
depth++;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Check if current matches a section-like container
|
|
146
|
+
const matchesSelector = sectionSelectors.some(sel => {
|
|
147
|
+
try {
|
|
148
|
+
return current!.matches(sel);
|
|
149
|
+
} catch {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const matchesClassPattern = current.className && sectionClassPatterns.test(current.className);
|
|
155
|
+
|
|
156
|
+
if (matchesSelector || matchesClassPattern) {
|
|
157
|
+
const rect = current.getBoundingClientRect();
|
|
158
|
+
// Store document-relative coordinates (add scroll offset)
|
|
159
|
+
// This ensures the highlight stays with the section when scrolling
|
|
160
|
+
return {
|
|
161
|
+
type: current.tagName.toLowerCase(),
|
|
162
|
+
sectionText: extractSectionHeading(current),
|
|
163
|
+
className: current.className || undefined,
|
|
164
|
+
coordinates: {
|
|
165
|
+
x: rect.left + window.scrollX,
|
|
166
|
+
y: rect.top + window.scrollY,
|
|
167
|
+
width: rect.width,
|
|
168
|
+
height: rect.height,
|
|
169
|
+
},
|
|
170
|
+
elementId: current.id || undefined,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
current = current.parentElement;
|
|
175
|
+
depth++;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
|
|
85
181
|
// ---- Main Component ----
|
|
86
182
|
|
|
87
183
|
export function SonanceDevTools() {
|
|
@@ -1079,6 +1175,24 @@ export function SonanceDevTools() {
|
|
|
1079
1175
|
const handleVisionElementClick = useCallback((element: DetectedElement) => {
|
|
1080
1176
|
if (!visionModeActive) return;
|
|
1081
1177
|
|
|
1178
|
+
// Find the actual DOM element using coordinates (center of the element)
|
|
1179
|
+
const centerX = element.rect.left + element.rect.width / 2;
|
|
1180
|
+
const centerY = element.rect.top + element.rect.height / 2;
|
|
1181
|
+
const domElement = document.elementFromPoint(centerX, centerY);
|
|
1182
|
+
|
|
1183
|
+
// Find parent section context for section-level targeting
|
|
1184
|
+
let parentSection: ParentSectionInfo | undefined;
|
|
1185
|
+
if (domElement) {
|
|
1186
|
+
parentSection = findParentSection(domElement);
|
|
1187
|
+
if (parentSection) {
|
|
1188
|
+
console.log("[Vision Mode] Captured parent section:", {
|
|
1189
|
+
type: parentSection.type,
|
|
1190
|
+
sectionText: parentSection.sectionText?.substring(0, 50),
|
|
1191
|
+
elementId: parentSection.elementId,
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1082
1196
|
const focusedElement: VisionFocusedElement = {
|
|
1083
1197
|
name: element.name,
|
|
1084
1198
|
type: element.type,
|
|
@@ -1095,6 +1209,8 @@ export function SonanceDevTools() {
|
|
|
1095
1209
|
// Capture element ID and child IDs for precise code targeting
|
|
1096
1210
|
elementId: element.elementId,
|
|
1097
1211
|
childIds: element.childIds,
|
|
1212
|
+
// Parent section context for section-level changes
|
|
1213
|
+
parentSection,
|
|
1098
1214
|
};
|
|
1099
1215
|
|
|
1100
1216
|
setVisionFocusedElements((prev) => {
|
|
@@ -2707,6 +2823,11 @@ export function SonanceDevTools() {
|
|
|
2707
2823
|
active={visionModeActive}
|
|
2708
2824
|
focusedCount={visionFocusedElements.length}
|
|
2709
2825
|
/>
|
|
2826
|
+
{/* Section Highlight - shows detected parent section when element is clicked */}
|
|
2827
|
+
<SectionHighlight
|
|
2828
|
+
active={visionModeActive}
|
|
2829
|
+
focusedElements={visionFocusedElements}
|
|
2830
|
+
/>
|
|
2710
2831
|
{/* Visual Inspector Overlay - switches to preview mode when AI changes are pending */}
|
|
2711
2832
|
{(inspectorEnabled || isPreviewActive || visionModeActive || changedElements.length > 0 || (viewMode === "inspector" && selectedComponentType !== "all")) && filteredOverlayElements.length > 0 && (
|
|
2712
2833
|
<InspectorOverlay
|
|
@@ -1,11 +1,59 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import React, { useState, useEffect, useCallback, useRef } from "react";
|
|
4
|
-
import { Loader2, Send, Sparkles, Eye } from "lucide-react";
|
|
4
|
+
import { Loader2, Send, Sparkles, Eye, AlertCircle, X } from "lucide-react";
|
|
5
5
|
import { cn } from "../../../lib/utils";
|
|
6
6
|
import { ChatMessage, AIEditResult, PendingEdit, VisionFocusedElement, VisionPendingEdit, ApplyFirstSession } from "../types";
|
|
7
7
|
import html2canvas from "html2canvas-pro";
|
|
8
8
|
|
|
9
|
+
// Helper to detect location failure in explanation
|
|
10
|
+
function isLocationFailure(explanation: string | undefined): boolean {
|
|
11
|
+
if (!explanation) return false;
|
|
12
|
+
const lowerExplanation = explanation.toLowerCase();
|
|
13
|
+
return (
|
|
14
|
+
lowerExplanation.includes('could not locate') ||
|
|
15
|
+
lowerExplanation.includes('element_not_found') ||
|
|
16
|
+
lowerExplanation.includes('cannot find the clicked element') ||
|
|
17
|
+
lowerExplanation.includes('unable to locate') ||
|
|
18
|
+
lowerExplanation.includes('could not find the element')
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Draw a section highlight border on a screenshot image
|
|
24
|
+
* This helps the LLM visually identify the target section for modifications
|
|
25
|
+
*/
|
|
26
|
+
function drawSectionHighlight(
|
|
27
|
+
screenshotDataUrl: string,
|
|
28
|
+
sectionCoords: { x: number; y: number; width: number; height: number }
|
|
29
|
+
): Promise<string> {
|
|
30
|
+
return new Promise((resolve) => {
|
|
31
|
+
const img = new Image();
|
|
32
|
+
img.onload = () => {
|
|
33
|
+
const canvas = document.createElement('canvas');
|
|
34
|
+
canvas.width = img.width;
|
|
35
|
+
canvas.height = img.height;
|
|
36
|
+
const ctx = canvas.getContext('2d')!;
|
|
37
|
+
|
|
38
|
+
// Draw original screenshot
|
|
39
|
+
ctx.drawImage(img, 0, 0);
|
|
40
|
+
|
|
41
|
+
// Draw section highlight border (teal/cyan to match Sonance brand)
|
|
42
|
+
ctx.strokeStyle = '#00D3C8';
|
|
43
|
+
ctx.lineWidth = 3;
|
|
44
|
+
ctx.setLineDash([8, 4]); // Dashed line for visibility
|
|
45
|
+
ctx.strokeRect(sectionCoords.x, sectionCoords.y, sectionCoords.width, sectionCoords.height);
|
|
46
|
+
|
|
47
|
+
// Semi-transparent fill to subtly highlight the area
|
|
48
|
+
ctx.fillStyle = 'rgba(0, 211, 200, 0.08)';
|
|
49
|
+
ctx.fillRect(sectionCoords.x, sectionCoords.y, sectionCoords.width, sectionCoords.height);
|
|
50
|
+
|
|
51
|
+
resolve(canvas.toDataURL('image/png', 0.8));
|
|
52
|
+
};
|
|
53
|
+
img.src = screenshotDataUrl;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
9
57
|
// Variant styles captured from the DOM
|
|
10
58
|
export interface VariantStyles {
|
|
11
59
|
backgroundColor: string;
|
|
@@ -56,8 +104,17 @@ export function ChatInterface({
|
|
|
56
104
|
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
|
57
105
|
const [input, setInput] = useState("");
|
|
58
106
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
107
|
+
const [toastMessage, setToastMessage] = useState<{ message: string; type: 'error' | 'warning' } | null>(null);
|
|
59
108
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
60
109
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
110
|
+
|
|
111
|
+
// Auto-dismiss toast after 5 seconds
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
if (toastMessage) {
|
|
114
|
+
const timer = setTimeout(() => setToastMessage(null), 5000);
|
|
115
|
+
return () => clearTimeout(timer);
|
|
116
|
+
}
|
|
117
|
+
}, [toastMessage]);
|
|
61
118
|
|
|
62
119
|
// Scroll to bottom when messages change
|
|
63
120
|
useEffect(() => {
|
|
@@ -133,8 +190,22 @@ export function ChatInterface({
|
|
|
133
190
|
try {
|
|
134
191
|
// Capture screenshot
|
|
135
192
|
console.log("[Vision Mode] Capturing screenshot...");
|
|
136
|
-
const
|
|
137
|
-
console.log("[Vision Mode] Screenshot captured:",
|
|
193
|
+
const rawScreenshot = await captureScreenshot();
|
|
194
|
+
console.log("[Vision Mode] Screenshot captured:", rawScreenshot ? `${rawScreenshot.length} bytes` : "null");
|
|
195
|
+
|
|
196
|
+
// Annotate screenshot with section highlight if parent section exists
|
|
197
|
+
// This helps the LLM visually identify the target area for modifications
|
|
198
|
+
let screenshot = rawScreenshot;
|
|
199
|
+
if (rawScreenshot && visionFocusedElements.length > 0) {
|
|
200
|
+
const parentSection = visionFocusedElements[0].parentSection;
|
|
201
|
+
if (parentSection?.coordinates) {
|
|
202
|
+
screenshot = await drawSectionHighlight(rawScreenshot, parentSection.coordinates);
|
|
203
|
+
console.log("[Vision Mode] Added section highlight to screenshot:", {
|
|
204
|
+
sectionType: parentSection.type,
|
|
205
|
+
sectionText: parentSection.sectionText?.substring(0, 30),
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
138
209
|
|
|
139
210
|
// Choose API endpoint based on mode
|
|
140
211
|
const endpoint = useApplyFirst ? "/api/sonance-vision-apply" : "/api/sonance-vision-edit";
|
|
@@ -162,20 +233,49 @@ export function ChatInterface({
|
|
|
162
233
|
error: data.error,
|
|
163
234
|
});
|
|
164
235
|
|
|
236
|
+
// Check if this is a "location failure" case - element could not be found in code
|
|
237
|
+
const hasLocationFailure = isLocationFailure(data.explanation);
|
|
238
|
+
const hasNoModifications = !data.modifications || data.modifications.length === 0;
|
|
239
|
+
const isElementNotFound = hasLocationFailure && hasNoModifications;
|
|
240
|
+
|
|
241
|
+
// Build appropriate message based on result
|
|
242
|
+
let messageContent: string;
|
|
243
|
+
if (isElementNotFound) {
|
|
244
|
+
// Element not found - provide helpful guidance
|
|
245
|
+
messageContent = (data.explanation || "Could not locate the clicked element in the source code.") +
|
|
246
|
+
"\n\nTry clicking on a different element or describe what you want to change in more detail.";
|
|
247
|
+
} else if (data.success) {
|
|
248
|
+
messageContent = useApplyFirst
|
|
249
|
+
? data.explanation || "Changes applied! Review and accept or revert."
|
|
250
|
+
: data.explanation || "Vision mode changes ready for preview.";
|
|
251
|
+
} else {
|
|
252
|
+
messageContent = data.error || "Failed to generate changes.";
|
|
253
|
+
}
|
|
254
|
+
|
|
165
255
|
const assistantMessage: ChatMessage = {
|
|
166
256
|
id: `msg-${Date.now()}-response`,
|
|
167
257
|
role: "assistant",
|
|
168
|
-
content:
|
|
169
|
-
? useApplyFirst
|
|
170
|
-
? data.explanation || "Changes applied! Review and accept or revert."
|
|
171
|
-
: data.explanation || "Vision mode changes ready for preview."
|
|
172
|
-
: data.error || "Failed to generate changes.",
|
|
258
|
+
content: messageContent,
|
|
173
259
|
timestamp: new Date(),
|
|
174
260
|
};
|
|
175
261
|
|
|
176
262
|
setMessages((prev) => [...prev, assistantMessage]);
|
|
177
263
|
|
|
178
|
-
|
|
264
|
+
// Handle element not found case - show toast and do NOT trigger page refresh
|
|
265
|
+
if (isElementNotFound) {
|
|
266
|
+
console.log("[Vision Mode] Element not found - blocking page refresh:", {
|
|
267
|
+
explanation: data.explanation,
|
|
268
|
+
modifications: data.modifications?.length || 0,
|
|
269
|
+
});
|
|
270
|
+
setToastMessage({
|
|
271
|
+
message: "Could not locate the clicked element in the source code",
|
|
272
|
+
type: 'warning'
|
|
273
|
+
});
|
|
274
|
+
// Do NOT call onApplyFirstComplete - this prevents page refresh
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (data.success && data.modifications && data.modifications.length > 0) {
|
|
179
279
|
if (useApplyFirst && onApplyFirstComplete) {
|
|
180
280
|
// Apply-First mode: files are already written, user can see changes via HMR
|
|
181
281
|
console.log("[Apply-First] Calling onApplyFirstComplete with:", {
|
|
@@ -344,6 +444,27 @@ export function ChatInterface({
|
|
|
344
444
|
|
|
345
445
|
return (
|
|
346
446
|
<div className="space-y-3">
|
|
447
|
+
{/* Toast Notification */}
|
|
448
|
+
{toastMessage && (
|
|
449
|
+
<div
|
|
450
|
+
className={cn(
|
|
451
|
+
"flex items-center gap-2 p-3 rounded-md text-sm animate-in slide-in-from-top-2",
|
|
452
|
+
toastMessage.type === 'error'
|
|
453
|
+
? "bg-red-50 border border-red-200 text-red-700"
|
|
454
|
+
: "bg-amber-50 border border-amber-200 text-amber-700"
|
|
455
|
+
)}
|
|
456
|
+
>
|
|
457
|
+
<AlertCircle className="h-4 w-4 flex-shrink-0" />
|
|
458
|
+
<span className="flex-1">{toastMessage.message}</span>
|
|
459
|
+
<button
|
|
460
|
+
onClick={() => setToastMessage(null)}
|
|
461
|
+
className="p-0.5 hover:bg-black/5 rounded"
|
|
462
|
+
>
|
|
463
|
+
<X className="h-3 w-3" />
|
|
464
|
+
</button>
|
|
465
|
+
</div>
|
|
466
|
+
)}
|
|
467
|
+
|
|
347
468
|
{/* Vision Mode Banner */}
|
|
348
469
|
{visionMode && (
|
|
349
470
|
<div className="p-2 bg-purple-50 border border-purple-200 rounded-md">
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useState } from "react";
|
|
4
|
+
import { createPortal } from "react-dom";
|
|
5
|
+
import { Box } from "lucide-react";
|
|
6
|
+
import type { VisionFocusedElement } from "../types";
|
|
7
|
+
|
|
8
|
+
interface SectionHighlightProps {
|
|
9
|
+
/** Whether vision mode is active */
|
|
10
|
+
active: boolean;
|
|
11
|
+
/** The focused elements that may have parent section info */
|
|
12
|
+
focusedElements: VisionFocusedElement[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* SectionHighlight renders a visible dashed border around the detected
|
|
17
|
+
* parent section when a user clicks an element in vision mode.
|
|
18
|
+
* This provides visual feedback so users can verify the correct section
|
|
19
|
+
* will be targeted by the AI.
|
|
20
|
+
*/
|
|
21
|
+
export function SectionHighlight({ active, focusedElements }: SectionHighlightProps) {
|
|
22
|
+
const [mounted, setMounted] = useState(false);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
setMounted(true);
|
|
26
|
+
return () => setMounted(false);
|
|
27
|
+
}, []);
|
|
28
|
+
|
|
29
|
+
if (!active || !mounted) return null;
|
|
30
|
+
|
|
31
|
+
// Find the first focused element with parent section coordinates
|
|
32
|
+
const elementWithSection = focusedElements.find(
|
|
33
|
+
(el) => el.parentSection?.coordinates
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
if (!elementWithSection?.parentSection?.coordinates) return null;
|
|
37
|
+
|
|
38
|
+
const { coordinates, type, sectionText, elementId } = elementWithSection.parentSection;
|
|
39
|
+
|
|
40
|
+
// Coordinates are already document-relative (scroll offset added at capture time)
|
|
41
|
+
// so we can use them directly with absolute positioning
|
|
42
|
+
|
|
43
|
+
// Build the label text
|
|
44
|
+
let labelText = `<${type}>`;
|
|
45
|
+
if (sectionText) {
|
|
46
|
+
const truncatedText = sectionText.length > 40
|
|
47
|
+
? sectionText.substring(0, 40) + "..."
|
|
48
|
+
: sectionText;
|
|
49
|
+
labelText += ` "${truncatedText}"`;
|
|
50
|
+
}
|
|
51
|
+
if (elementId) {
|
|
52
|
+
labelText += ` #${elementId}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return createPortal(
|
|
56
|
+
<>
|
|
57
|
+
{/* Section border overlay - uses absolute positioning to stay with content on scroll */}
|
|
58
|
+
<div
|
|
59
|
+
data-section-highlight="true"
|
|
60
|
+
style={{
|
|
61
|
+
position: "absolute",
|
|
62
|
+
top: coordinates.y,
|
|
63
|
+
left: coordinates.x,
|
|
64
|
+
width: coordinates.width,
|
|
65
|
+
height: coordinates.height,
|
|
66
|
+
pointerEvents: "none",
|
|
67
|
+
zIndex: 9995, // Below VisionModeBorder (9996)
|
|
68
|
+
border: "3px dashed #00D3C8",
|
|
69
|
+
borderRadius: "4px",
|
|
70
|
+
backgroundColor: "rgba(0, 211, 200, 0.06)",
|
|
71
|
+
boxShadow: "0 0 0 1px rgba(0, 211, 200, 0.3), inset 0 0 20px rgba(0, 211, 200, 0.05)",
|
|
72
|
+
transition: "all 0.2s ease-out",
|
|
73
|
+
}}
|
|
74
|
+
>
|
|
75
|
+
{/* Section label */}
|
|
76
|
+
<div
|
|
77
|
+
style={{
|
|
78
|
+
position: "absolute",
|
|
79
|
+
top: "-28px",
|
|
80
|
+
left: "0",
|
|
81
|
+
display: "flex",
|
|
82
|
+
alignItems: "center",
|
|
83
|
+
gap: "6px",
|
|
84
|
+
backgroundColor: "#00D3C8",
|
|
85
|
+
color: "#1a1a1a",
|
|
86
|
+
padding: "4px 10px",
|
|
87
|
+
borderRadius: "4px",
|
|
88
|
+
fontSize: "11px",
|
|
89
|
+
fontWeight: 600,
|
|
90
|
+
fontFamily: "'Montserrat', system-ui, -apple-system, sans-serif",
|
|
91
|
+
boxShadow: "0 2px 8px rgba(0, 211, 200, 0.4)",
|
|
92
|
+
whiteSpace: "nowrap",
|
|
93
|
+
maxWidth: "350px",
|
|
94
|
+
overflow: "hidden",
|
|
95
|
+
textOverflow: "ellipsis",
|
|
96
|
+
}}
|
|
97
|
+
>
|
|
98
|
+
<Box size={12} />
|
|
99
|
+
<span>Section: {labelText}</span>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
{/* Corner markers for emphasis */}
|
|
103
|
+
{[
|
|
104
|
+
{ top: -2, left: -2, borderTop: "3px solid #00D3C8", borderLeft: "3px solid #00D3C8" },
|
|
105
|
+
{ top: -2, right: -2, borderTop: "3px solid #00D3C8", borderRight: "3px solid #00D3C8" },
|
|
106
|
+
{ bottom: -2, left: -2, borderBottom: "3px solid #00D3C8", borderLeft: "3px solid #00D3C8" },
|
|
107
|
+
{ bottom: -2, right: -2, borderBottom: "3px solid #00D3C8", borderRight: "3px solid #00D3C8" },
|
|
108
|
+
].map((style, i) => (
|
|
109
|
+
<div
|
|
110
|
+
key={i}
|
|
111
|
+
style={{
|
|
112
|
+
position: "absolute",
|
|
113
|
+
width: "12px",
|
|
114
|
+
height: "12px",
|
|
115
|
+
...style,
|
|
116
|
+
}}
|
|
117
|
+
/>
|
|
118
|
+
))}
|
|
119
|
+
</div>
|
|
120
|
+
</>,
|
|
121
|
+
document.body
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
@@ -215,6 +215,20 @@ export type ComponentsViewMode = "visual" | "inspector";
|
|
|
215
215
|
|
|
216
216
|
// ---- Vision Mode Types ----
|
|
217
217
|
|
|
218
|
+
/** Parent section/container context for section-level targeting */
|
|
219
|
+
export interface ParentSectionInfo {
|
|
220
|
+
/** Semantic container type (section, article, form, card, dialog, etc.) */
|
|
221
|
+
type: string;
|
|
222
|
+
/** Key text content in the section (first heading, labels) */
|
|
223
|
+
sectionText?: string;
|
|
224
|
+
/** The parent container's className for code matching */
|
|
225
|
+
className?: string;
|
|
226
|
+
/** Coordinates of the parent section */
|
|
227
|
+
coordinates?: { x: number; y: number; width: number; height: number };
|
|
228
|
+
/** Parent's element ID if available */
|
|
229
|
+
elementId?: string;
|
|
230
|
+
}
|
|
231
|
+
|
|
218
232
|
export interface VisionFocusedElement {
|
|
219
233
|
name: string;
|
|
220
234
|
type: DetectedElementType;
|
|
@@ -234,6 +248,8 @@ export interface VisionFocusedElement {
|
|
|
234
248
|
elementId?: string;
|
|
235
249
|
/** IDs of child elements for more precise targeting */
|
|
236
250
|
childIds?: string[];
|
|
251
|
+
/** Parent section context for section-level targeting */
|
|
252
|
+
parentSection?: ParentSectionInfo;
|
|
237
253
|
}
|
|
238
254
|
|
|
239
255
|
export interface VisionEditRequest {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sonance-brand-mcp",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.93",
|
|
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",
|