sonance-brand-mcp 1.3.92 → 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 +46 -0
- package/dist/assets/api/sonance-vision-edit/route.ts +46 -0
- 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 {
|
|
@@ -1703,6 +1719,36 @@ User Request: "${userPrompt}"
|
|
|
1703
1719
|
textContent += ` with text "${el.textContent.substring(0, 30)}"`;
|
|
1704
1720
|
}
|
|
1705
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
|
+
`;
|
|
1706
1752
|
}
|
|
1707
1753
|
|
|
1708
1754
|
// Add precise targeting with line number and snippet
|
|
@@ -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 {
|
|
@@ -1672,6 +1688,36 @@ User Request: "${userPrompt}"
|
|
|
1672
1688
|
textContent += ` with text "${el.textContent.substring(0, 30)}"`;
|
|
1673
1689
|
}
|
|
1674
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
|
+
`;
|
|
1675
1721
|
}
|
|
1676
1722
|
|
|
1677
1723
|
// Add precise targeting with line number and snippet
|
|
@@ -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",
|