sonance-brand-mcp 1.3.92 → 1.3.94
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 +222 -11
- package/dist/assets/dev-tools/components/InspectorOverlay.tsx +7 -4
- package/dist/assets/dev-tools/components/ScreenshotAnnotator.tsx +352 -0
- 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,10 +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, Crop } 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
|
+
import { ScreenshotAnnotator, Rectangle } from "./ScreenshotAnnotator";
|
|
9
|
+
|
|
10
|
+
// Helper to detect location failure in explanation
|
|
11
|
+
function isLocationFailure(explanation: string | undefined): boolean {
|
|
12
|
+
if (!explanation) return false;
|
|
13
|
+
const lowerExplanation = explanation.toLowerCase();
|
|
14
|
+
return (
|
|
15
|
+
lowerExplanation.includes('could not locate') ||
|
|
16
|
+
lowerExplanation.includes('element_not_found') ||
|
|
17
|
+
lowerExplanation.includes('cannot find the clicked element') ||
|
|
18
|
+
lowerExplanation.includes('unable to locate') ||
|
|
19
|
+
lowerExplanation.includes('could not find the element')
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Draw a section highlight border on a screenshot image
|
|
25
|
+
* This helps the LLM visually identify the target section for modifications
|
|
26
|
+
*/
|
|
27
|
+
function drawSectionHighlight(
|
|
28
|
+
screenshotDataUrl: string,
|
|
29
|
+
sectionCoords: { x: number; y: number; width: number; height: number }
|
|
30
|
+
): Promise<string> {
|
|
31
|
+
return new Promise((resolve) => {
|
|
32
|
+
const img = new Image();
|
|
33
|
+
img.onload = () => {
|
|
34
|
+
const canvas = document.createElement('canvas');
|
|
35
|
+
canvas.width = img.width;
|
|
36
|
+
canvas.height = img.height;
|
|
37
|
+
const ctx = canvas.getContext('2d')!;
|
|
38
|
+
|
|
39
|
+
// Draw original screenshot
|
|
40
|
+
ctx.drawImage(img, 0, 0);
|
|
41
|
+
|
|
42
|
+
// Draw section highlight border (teal/cyan to match Sonance brand)
|
|
43
|
+
ctx.strokeStyle = '#00D3C8';
|
|
44
|
+
ctx.lineWidth = 3;
|
|
45
|
+
ctx.setLineDash([8, 4]); // Dashed line for visibility
|
|
46
|
+
ctx.strokeRect(sectionCoords.x, sectionCoords.y, sectionCoords.width, sectionCoords.height);
|
|
47
|
+
|
|
48
|
+
// Semi-transparent fill to subtly highlight the area
|
|
49
|
+
ctx.fillStyle = 'rgba(0, 211, 200, 0.08)';
|
|
50
|
+
ctx.fillRect(sectionCoords.x, sectionCoords.y, sectionCoords.width, sectionCoords.height);
|
|
51
|
+
|
|
52
|
+
resolve(canvas.toDataURL('image/png', 0.8));
|
|
53
|
+
};
|
|
54
|
+
img.src = screenshotDataUrl;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
8
57
|
|
|
9
58
|
// Variant styles captured from the DOM
|
|
10
59
|
export interface VariantStyles {
|
|
@@ -56,8 +105,22 @@ export function ChatInterface({
|
|
|
56
105
|
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
|
57
106
|
const [input, setInput] = useState("");
|
|
58
107
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
108
|
+
const [toastMessage, setToastMessage] = useState<{ message: string; type: 'error' | 'warning' } | null>(null);
|
|
59
109
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
60
110
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
111
|
+
|
|
112
|
+
// Screenshot annotation state
|
|
113
|
+
const [isAnnotating, setIsAnnotating] = useState(false);
|
|
114
|
+
const [annotatedScreenshot, setAnnotatedScreenshot] = useState<string | null>(null);
|
|
115
|
+
const [manualFocusBounds, setManualFocusBounds] = useState<Rectangle | null>(null);
|
|
116
|
+
|
|
117
|
+
// Auto-dismiss toast after 5 seconds
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
if (toastMessage) {
|
|
120
|
+
const timer = setTimeout(() => setToastMessage(null), 5000);
|
|
121
|
+
return () => clearTimeout(timer);
|
|
122
|
+
}
|
|
123
|
+
}, [toastMessage]);
|
|
61
124
|
|
|
62
125
|
// Scroll to bottom when messages change
|
|
63
126
|
useEffect(() => {
|
|
@@ -107,6 +170,33 @@ export function ChatInterface({
|
|
|
107
170
|
}
|
|
108
171
|
}, []);
|
|
109
172
|
|
|
173
|
+
// Start screenshot annotation - show drawing overlay on live app
|
|
174
|
+
const startAnnotation = useCallback(() => {
|
|
175
|
+
console.log("[Vision Mode] Starting screenshot annotation overlay...");
|
|
176
|
+
setIsAnnotating(true);
|
|
177
|
+
}, []);
|
|
178
|
+
|
|
179
|
+
// Handle annotation confirmation - screenshot is already captured and annotated
|
|
180
|
+
const handleAnnotationConfirm = useCallback((annotated: string, bounds: Rectangle) => {
|
|
181
|
+
console.log("[Vision Mode] Annotation confirmed:", { bounds });
|
|
182
|
+
setAnnotatedScreenshot(annotated);
|
|
183
|
+
setManualFocusBounds(bounds);
|
|
184
|
+
setIsAnnotating(false);
|
|
185
|
+
// Focus the input so user can type their prompt
|
|
186
|
+
setTimeout(() => inputRef.current?.focus(), 100);
|
|
187
|
+
}, []);
|
|
188
|
+
|
|
189
|
+
// Handle annotation cancel
|
|
190
|
+
const handleAnnotationCancel = useCallback(() => {
|
|
191
|
+
setIsAnnotating(false);
|
|
192
|
+
}, []);
|
|
193
|
+
|
|
194
|
+
// Clear the current annotation
|
|
195
|
+
const clearAnnotation = useCallback(() => {
|
|
196
|
+
setAnnotatedScreenshot(null);
|
|
197
|
+
setManualFocusBounds(null);
|
|
198
|
+
}, []);
|
|
199
|
+
|
|
110
200
|
// Handle vision mode edit request
|
|
111
201
|
const handleVisionEdit = async (prompt: string) => {
|
|
112
202
|
// Use Apply-First mode if callback is provided (new Cursor-style workflow)
|
|
@@ -131,10 +221,36 @@ export function ChatInterface({
|
|
|
131
221
|
setIsProcessing(true);
|
|
132
222
|
|
|
133
223
|
try {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
224
|
+
let screenshot: string | null;
|
|
225
|
+
|
|
226
|
+
// PRIORITY 1: Use manually annotated screenshot if available
|
|
227
|
+
// This is when user drew a focus area using the annotation tool
|
|
228
|
+
if (annotatedScreenshot) {
|
|
229
|
+
console.log("[Vision Mode] Using manually annotated screenshot");
|
|
230
|
+
screenshot = annotatedScreenshot;
|
|
231
|
+
// Clear the annotation after use
|
|
232
|
+
setAnnotatedScreenshot(null);
|
|
233
|
+
setManualFocusBounds(null);
|
|
234
|
+
} else {
|
|
235
|
+
// PRIORITY 2: Capture fresh screenshot and auto-annotate with section highlight
|
|
236
|
+
console.log("[Vision Mode] Capturing screenshot...");
|
|
237
|
+
const rawScreenshot = await captureScreenshot();
|
|
238
|
+
console.log("[Vision Mode] Screenshot captured:", rawScreenshot ? `${rawScreenshot.length} bytes` : "null");
|
|
239
|
+
|
|
240
|
+
// Annotate screenshot with section highlight if parent section exists
|
|
241
|
+
// This helps the LLM visually identify the target area for modifications
|
|
242
|
+
screenshot = rawScreenshot;
|
|
243
|
+
if (rawScreenshot && visionFocusedElements.length > 0) {
|
|
244
|
+
const parentSection = visionFocusedElements[0].parentSection;
|
|
245
|
+
if (parentSection?.coordinates) {
|
|
246
|
+
screenshot = await drawSectionHighlight(rawScreenshot, parentSection.coordinates);
|
|
247
|
+
console.log("[Vision Mode] Added section highlight to screenshot:", {
|
|
248
|
+
sectionType: parentSection.type,
|
|
249
|
+
sectionText: parentSection.sectionText?.substring(0, 30),
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
138
254
|
|
|
139
255
|
// Choose API endpoint based on mode
|
|
140
256
|
const endpoint = useApplyFirst ? "/api/sonance-vision-apply" : "/api/sonance-vision-edit";
|
|
@@ -162,20 +278,49 @@ export function ChatInterface({
|
|
|
162
278
|
error: data.error,
|
|
163
279
|
});
|
|
164
280
|
|
|
281
|
+
// Check if this is a "location failure" case - element could not be found in code
|
|
282
|
+
const hasLocationFailure = isLocationFailure(data.explanation);
|
|
283
|
+
const hasNoModifications = !data.modifications || data.modifications.length === 0;
|
|
284
|
+
const isElementNotFound = hasLocationFailure && hasNoModifications;
|
|
285
|
+
|
|
286
|
+
// Build appropriate message based on result
|
|
287
|
+
let messageContent: string;
|
|
288
|
+
if (isElementNotFound) {
|
|
289
|
+
// Element not found - provide helpful guidance
|
|
290
|
+
messageContent = (data.explanation || "Could not locate the clicked element in the source code.") +
|
|
291
|
+
"\n\nTry clicking on a different element or describe what you want to change in more detail.";
|
|
292
|
+
} else if (data.success) {
|
|
293
|
+
messageContent = useApplyFirst
|
|
294
|
+
? data.explanation || "Changes applied! Review and accept or revert."
|
|
295
|
+
: data.explanation || "Vision mode changes ready for preview.";
|
|
296
|
+
} else {
|
|
297
|
+
messageContent = data.error || "Failed to generate changes.";
|
|
298
|
+
}
|
|
299
|
+
|
|
165
300
|
const assistantMessage: ChatMessage = {
|
|
166
301
|
id: `msg-${Date.now()}-response`,
|
|
167
302
|
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.",
|
|
303
|
+
content: messageContent,
|
|
173
304
|
timestamp: new Date(),
|
|
174
305
|
};
|
|
175
306
|
|
|
176
307
|
setMessages((prev) => [...prev, assistantMessage]);
|
|
177
308
|
|
|
178
|
-
|
|
309
|
+
// Handle element not found case - show toast and do NOT trigger page refresh
|
|
310
|
+
if (isElementNotFound) {
|
|
311
|
+
console.log("[Vision Mode] Element not found - blocking page refresh:", {
|
|
312
|
+
explanation: data.explanation,
|
|
313
|
+
modifications: data.modifications?.length || 0,
|
|
314
|
+
});
|
|
315
|
+
setToastMessage({
|
|
316
|
+
message: "Could not locate the clicked element in the source code",
|
|
317
|
+
type: 'warning'
|
|
318
|
+
});
|
|
319
|
+
// Do NOT call onApplyFirstComplete - this prevents page refresh
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (data.success && data.modifications && data.modifications.length > 0) {
|
|
179
324
|
if (useApplyFirst && onApplyFirstComplete) {
|
|
180
325
|
// Apply-First mode: files are already written, user can see changes via HMR
|
|
181
326
|
console.log("[Apply-First] Calling onApplyFirstComplete with:", {
|
|
@@ -344,6 +489,27 @@ export function ChatInterface({
|
|
|
344
489
|
|
|
345
490
|
return (
|
|
346
491
|
<div className="space-y-3">
|
|
492
|
+
{/* Toast Notification */}
|
|
493
|
+
{toastMessage && (
|
|
494
|
+
<div
|
|
495
|
+
className={cn(
|
|
496
|
+
"flex items-center gap-2 p-3 rounded-md text-sm animate-in slide-in-from-top-2",
|
|
497
|
+
toastMessage.type === 'error'
|
|
498
|
+
? "bg-red-50 border border-red-200 text-red-700"
|
|
499
|
+
: "bg-amber-50 border border-amber-200 text-amber-700"
|
|
500
|
+
)}
|
|
501
|
+
>
|
|
502
|
+
<AlertCircle className="h-4 w-4 flex-shrink-0" />
|
|
503
|
+
<span className="flex-1">{toastMessage.message}</span>
|
|
504
|
+
<button
|
|
505
|
+
onClick={() => setToastMessage(null)}
|
|
506
|
+
className="p-0.5 hover:bg-black/5 rounded"
|
|
507
|
+
>
|
|
508
|
+
<X className="h-3 w-3" />
|
|
509
|
+
</button>
|
|
510
|
+
</div>
|
|
511
|
+
)}
|
|
512
|
+
|
|
347
513
|
{/* Vision Mode Banner */}
|
|
348
514
|
{visionMode && (
|
|
349
515
|
<div className="p-2 bg-purple-50 border border-purple-200 rounded-md">
|
|
@@ -474,6 +640,26 @@ export function ChatInterface({
|
|
|
474
640
|
"disabled:opacity-50 disabled:bg-gray-50"
|
|
475
641
|
)}
|
|
476
642
|
/>
|
|
643
|
+
|
|
644
|
+
{/* Annotate screenshot button - only in vision mode */}
|
|
645
|
+
{visionMode && (
|
|
646
|
+
<button
|
|
647
|
+
onClick={startAnnotation}
|
|
648
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
649
|
+
disabled={isProcessing}
|
|
650
|
+
title="Draw on screenshot to focus AI attention"
|
|
651
|
+
className={cn(
|
|
652
|
+
"px-3 py-2 rounded transition-colors",
|
|
653
|
+
annotatedScreenshot
|
|
654
|
+
? "bg-[#00D3C8] text-[#1a1a1a]" // Teal when annotation is active
|
|
655
|
+
: "bg-gray-100 text-gray-600 hover:bg-gray-200",
|
|
656
|
+
"disabled:opacity-50 disabled:cursor-not-allowed"
|
|
657
|
+
)}
|
|
658
|
+
>
|
|
659
|
+
<Crop className="h-4 w-4" />
|
|
660
|
+
</button>
|
|
661
|
+
)}
|
|
662
|
+
|
|
477
663
|
<button
|
|
478
664
|
onClick={() => handleSend(input || inputRef.current?.value || "")}
|
|
479
665
|
onPointerDown={(e) => e.stopPropagation()}
|
|
@@ -493,6 +679,23 @@ export function ChatInterface({
|
|
|
493
679
|
)}
|
|
494
680
|
</button>
|
|
495
681
|
</div>
|
|
682
|
+
|
|
683
|
+
{/* Annotation indicator */}
|
|
684
|
+
{annotatedScreenshot && visionMode && (
|
|
685
|
+
<div className="flex items-center justify-between text-xs text-[#00D3C8] bg-[#00D3C8]/10 px-2 py-1 rounded">
|
|
686
|
+
<span className="flex items-center gap-1">
|
|
687
|
+
<Crop className="h-3 w-3" />
|
|
688
|
+
Focus area selected - your prompt will target this region
|
|
689
|
+
</span>
|
|
690
|
+
<button
|
|
691
|
+
onClick={clearAnnotation}
|
|
692
|
+
className="text-[#00D3C8] hover:text-[#00b3a8] p-0.5"
|
|
693
|
+
title="Clear annotation"
|
|
694
|
+
>
|
|
695
|
+
<X className="h-3 w-3" />
|
|
696
|
+
</button>
|
|
697
|
+
</div>
|
|
698
|
+
)}
|
|
496
699
|
|
|
497
700
|
{/* Processing Indicator */}
|
|
498
701
|
{isProcessing && (
|
|
@@ -508,6 +711,14 @@ export function ChatInterface({
|
|
|
508
711
|
</span>
|
|
509
712
|
</div>
|
|
510
713
|
)}
|
|
714
|
+
|
|
715
|
+
{/* Screenshot Annotator Overlay - draws on live app */}
|
|
716
|
+
{isAnnotating && (
|
|
717
|
+
<ScreenshotAnnotator
|
|
718
|
+
onConfirm={handleAnnotationConfirm}
|
|
719
|
+
onCancel={handleAnnotationCancel}
|
|
720
|
+
/>
|
|
721
|
+
)}
|
|
511
722
|
</div>
|
|
512
723
|
);
|
|
513
724
|
}
|
|
@@ -88,11 +88,14 @@ export function InspectorOverlay({
|
|
|
88
88
|
const clickX = e.clientX;
|
|
89
89
|
const clickY = e.clientY;
|
|
90
90
|
|
|
91
|
-
// Don't capture clicks on
|
|
92
|
-
|
|
91
|
+
// Don't capture clicks on ANY DevTools element (panel, annotation overlay, etc.)
|
|
92
|
+
// Check if the click target or any of its ancestors have data-sonance-devtools
|
|
93
|
+
const target = e.target as HTMLElement;
|
|
94
|
+
if (target.closest('[data-sonance-devtools="true"]')) return;
|
|
95
|
+
if (target.closest('[data-annotator-overlay="true"]')) return;
|
|
96
|
+
|
|
93
97
|
const devToolsRoot = document.getElementById('sonance-devtools-root');
|
|
94
|
-
if (
|
|
95
|
-
if (devToolsRoot?.contains(e.target as Node)) return;
|
|
98
|
+
if (devToolsRoot?.contains(target)) return;
|
|
96
99
|
|
|
97
100
|
// Find ALL elements that contain the click point
|
|
98
101
|
const matchingElements = elements.filter(el =>
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useState, useCallback } from "react";
|
|
4
|
+
import { createPortal } from "react-dom";
|
|
5
|
+
import { Check, X, RotateCcw, Crop } from "lucide-react";
|
|
6
|
+
import html2canvas from "html2canvas-pro";
|
|
7
|
+
|
|
8
|
+
export interface Rectangle {
|
|
9
|
+
x: number;
|
|
10
|
+
y: number;
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ScreenshotAnnotatorProps {
|
|
16
|
+
/** Called when user confirms their selection with captured screenshot */
|
|
17
|
+
onConfirm: (annotatedScreenshot: string, bounds: Rectangle) => void;
|
|
18
|
+
/** Called when user cancels */
|
|
19
|
+
onCancel: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* ScreenshotAnnotator displays a transparent overlay where users can
|
|
24
|
+
* draw a rectangle on the LIVE app to define the focus area.
|
|
25
|
+
* On confirm, it captures a screenshot and annotates it with the selection.
|
|
26
|
+
*/
|
|
27
|
+
export function ScreenshotAnnotator({
|
|
28
|
+
onConfirm,
|
|
29
|
+
onCancel,
|
|
30
|
+
}: ScreenshotAnnotatorProps) {
|
|
31
|
+
const [mounted, setMounted] = useState(false);
|
|
32
|
+
const [isDrawing, setIsDrawing] = useState(false);
|
|
33
|
+
const [startPos, setStartPos] = useState<{ x: number; y: number } | null>(null);
|
|
34
|
+
const [currentRect, setCurrentRect] = useState<Rectangle | null>(null);
|
|
35
|
+
const [isCapturing, setIsCapturing] = useState(false);
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
setMounted(true);
|
|
39
|
+
|
|
40
|
+
// Handle escape key to cancel
|
|
41
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
42
|
+
if (e.key === "Escape") {
|
|
43
|
+
onCancel();
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
47
|
+
|
|
48
|
+
return () => {
|
|
49
|
+
setMounted(false);
|
|
50
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
51
|
+
};
|
|
52
|
+
}, [onCancel]);
|
|
53
|
+
|
|
54
|
+
// Mouse handlers for drawing on the live page
|
|
55
|
+
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
|
56
|
+
// Ignore clicks on the toolbar buttons
|
|
57
|
+
if ((e.target as HTMLElement).closest('[data-annotator-toolbar]')) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Stop propagation to prevent InspectorOverlay from detecting this as element selection
|
|
62
|
+
e.stopPropagation();
|
|
63
|
+
|
|
64
|
+
setStartPos({ x: e.clientX, y: e.clientY });
|
|
65
|
+
setIsDrawing(true);
|
|
66
|
+
setCurrentRect(null);
|
|
67
|
+
}, []);
|
|
68
|
+
|
|
69
|
+
const handleMouseMove = useCallback((e: React.MouseEvent) => {
|
|
70
|
+
if (!isDrawing || !startPos) return;
|
|
71
|
+
|
|
72
|
+
// Calculate rectangle (handle drawing in any direction)
|
|
73
|
+
const x = Math.min(startPos.x, e.clientX);
|
|
74
|
+
const y = Math.min(startPos.y, e.clientY);
|
|
75
|
+
const width = Math.abs(e.clientX - startPos.x);
|
|
76
|
+
const height = Math.abs(e.clientY - startPos.y);
|
|
77
|
+
|
|
78
|
+
setCurrentRect({ x, y, width, height });
|
|
79
|
+
}, [isDrawing, startPos]);
|
|
80
|
+
|
|
81
|
+
const handleMouseUp = useCallback(() => {
|
|
82
|
+
setIsDrawing(false);
|
|
83
|
+
}, []);
|
|
84
|
+
|
|
85
|
+
// Clear the current selection
|
|
86
|
+
const handleRedraw = useCallback(() => {
|
|
87
|
+
setCurrentRect(null);
|
|
88
|
+
setStartPos(null);
|
|
89
|
+
}, []);
|
|
90
|
+
|
|
91
|
+
// Confirm: capture screenshot and annotate it
|
|
92
|
+
const handleConfirm = useCallback(async () => {
|
|
93
|
+
if (!currentRect || isCapturing) return;
|
|
94
|
+
|
|
95
|
+
setIsCapturing(true);
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
// Capture the full page screenshot (excluding DevTools elements)
|
|
99
|
+
const canvas = await html2canvas(document.body, {
|
|
100
|
+
ignoreElements: (element) => {
|
|
101
|
+
return (
|
|
102
|
+
element.hasAttribute("data-sonance-devtools") ||
|
|
103
|
+
element.hasAttribute("data-vision-mode-border") ||
|
|
104
|
+
element.hasAttribute("data-annotator-overlay")
|
|
105
|
+
);
|
|
106
|
+
},
|
|
107
|
+
useCORS: true,
|
|
108
|
+
allowTaint: true,
|
|
109
|
+
scale: 1,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Create annotated screenshot with the focus rectangle
|
|
113
|
+
const ctx = canvas.getContext("2d")!;
|
|
114
|
+
|
|
115
|
+
// Draw the focus rectangle (teal dashed border)
|
|
116
|
+
ctx.strokeStyle = "#00D3C8";
|
|
117
|
+
ctx.lineWidth = 4;
|
|
118
|
+
ctx.setLineDash([12, 6]);
|
|
119
|
+
ctx.strokeRect(currentRect.x, currentRect.y, currentRect.width, currentRect.height);
|
|
120
|
+
|
|
121
|
+
// Semi-transparent fill
|
|
122
|
+
ctx.fillStyle = "rgba(0, 211, 200, 0.1)";
|
|
123
|
+
ctx.fillRect(currentRect.x, currentRect.y, currentRect.width, currentRect.height);
|
|
124
|
+
|
|
125
|
+
// Add "FOCUS AREA" label
|
|
126
|
+
ctx.setLineDash([]); // Reset dash
|
|
127
|
+
ctx.fillStyle = "#00D3C8";
|
|
128
|
+
ctx.font = "bold 14px Montserrat, system-ui, sans-serif";
|
|
129
|
+
const labelText = "FOCUS AREA";
|
|
130
|
+
const labelPadding = 8;
|
|
131
|
+
const labelHeight = 22;
|
|
132
|
+
const labelWidth = ctx.measureText(labelText).width + labelPadding * 2;
|
|
133
|
+
|
|
134
|
+
// Position label above the rectangle
|
|
135
|
+
const labelX = currentRect.x;
|
|
136
|
+
const labelY = Math.max(currentRect.y - labelHeight - 4, 0);
|
|
137
|
+
|
|
138
|
+
ctx.fillRect(labelX, labelY, labelWidth, labelHeight);
|
|
139
|
+
ctx.fillStyle = "#1a1a1a";
|
|
140
|
+
ctx.fillText(labelText, labelX + labelPadding, labelY + 16);
|
|
141
|
+
|
|
142
|
+
const annotatedScreenshot = canvas.toDataURL("image/png", 0.9);
|
|
143
|
+
onConfirm(annotatedScreenshot, currentRect);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
console.error("Failed to capture screenshot:", error);
|
|
146
|
+
setIsCapturing(false);
|
|
147
|
+
}
|
|
148
|
+
}, [currentRect, isCapturing, onConfirm]);
|
|
149
|
+
|
|
150
|
+
if (!mounted) return null;
|
|
151
|
+
|
|
152
|
+
return createPortal(
|
|
153
|
+
<div
|
|
154
|
+
data-annotator-overlay="true"
|
|
155
|
+
data-sonance-devtools="true"
|
|
156
|
+
onMouseDown={(e) => {
|
|
157
|
+
e.stopPropagation();
|
|
158
|
+
e.preventDefault();
|
|
159
|
+
handleMouseDown(e);
|
|
160
|
+
}}
|
|
161
|
+
onMouseMove={handleMouseMove}
|
|
162
|
+
onMouseUp={handleMouseUp}
|
|
163
|
+
onMouseLeave={handleMouseUp}
|
|
164
|
+
onClick={(e) => {
|
|
165
|
+
// Prevent clicks from bubbling to InspectorOverlay
|
|
166
|
+
e.stopPropagation();
|
|
167
|
+
}}
|
|
168
|
+
style={{
|
|
169
|
+
position: "fixed",
|
|
170
|
+
top: 0,
|
|
171
|
+
left: 0,
|
|
172
|
+
right: 0,
|
|
173
|
+
bottom: 0,
|
|
174
|
+
zIndex: 10000,
|
|
175
|
+
cursor: isDrawing ? "crosshair" : "crosshair",
|
|
176
|
+
// Transparent overlay - user sees the live app
|
|
177
|
+
backgroundColor: "rgba(0, 0, 0, 0.1)",
|
|
178
|
+
// Ensure we capture all pointer events
|
|
179
|
+
pointerEvents: "all",
|
|
180
|
+
}}
|
|
181
|
+
>
|
|
182
|
+
{/* Instructions banner at top */}
|
|
183
|
+
<div
|
|
184
|
+
data-sonance-devtools="true"
|
|
185
|
+
data-annotator-toolbar="true"
|
|
186
|
+
style={{
|
|
187
|
+
position: "fixed",
|
|
188
|
+
top: 20,
|
|
189
|
+
left: "50%",
|
|
190
|
+
transform: "translateX(-50%)",
|
|
191
|
+
display: "flex",
|
|
192
|
+
alignItems: "center",
|
|
193
|
+
gap: 8,
|
|
194
|
+
backgroundColor: "#00D3C8",
|
|
195
|
+
color: "#1a1a1a",
|
|
196
|
+
padding: "10px 20px",
|
|
197
|
+
borderRadius: 8,
|
|
198
|
+
fontSize: 14,
|
|
199
|
+
fontWeight: 600,
|
|
200
|
+
fontFamily: "'Montserrat', system-ui, -apple-system, sans-serif",
|
|
201
|
+
boxShadow: "0 4px 20px rgba(0, 211, 200, 0.4)",
|
|
202
|
+
pointerEvents: "none",
|
|
203
|
+
userSelect: "none",
|
|
204
|
+
}}
|
|
205
|
+
>
|
|
206
|
+
<Crop size={18} />
|
|
207
|
+
<span>Click and drag to select the area you want to focus on</span>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
{/* Current rectangle selection */}
|
|
211
|
+
{currentRect && (
|
|
212
|
+
<div
|
|
213
|
+
data-sonance-devtools="true"
|
|
214
|
+
style={{
|
|
215
|
+
position: "fixed",
|
|
216
|
+
left: currentRect.x,
|
|
217
|
+
top: currentRect.y,
|
|
218
|
+
width: currentRect.width,
|
|
219
|
+
height: currentRect.height,
|
|
220
|
+
border: "3px dashed #00D3C8",
|
|
221
|
+
backgroundColor: "rgba(0, 211, 200, 0.15)",
|
|
222
|
+
borderRadius: 4,
|
|
223
|
+
boxShadow: "0 0 0 2px rgba(0, 211, 200, 0.3), 0 0 20px rgba(0, 211, 200, 0.2)",
|
|
224
|
+
pointerEvents: "none",
|
|
225
|
+
}}
|
|
226
|
+
/>
|
|
227
|
+
)}
|
|
228
|
+
|
|
229
|
+
{/* Toolbar at bottom */}
|
|
230
|
+
<div
|
|
231
|
+
data-sonance-devtools="true"
|
|
232
|
+
data-annotator-toolbar="true"
|
|
233
|
+
style={{
|
|
234
|
+
position: "fixed",
|
|
235
|
+
bottom: 30,
|
|
236
|
+
left: "50%",
|
|
237
|
+
transform: "translateX(-50%)",
|
|
238
|
+
display: "flex",
|
|
239
|
+
gap: 12,
|
|
240
|
+
}}
|
|
241
|
+
>
|
|
242
|
+
{/* Cancel button */}
|
|
243
|
+
<button
|
|
244
|
+
data-sonance-devtools="true"
|
|
245
|
+
onClick={(e) => {
|
|
246
|
+
e.stopPropagation();
|
|
247
|
+
onCancel();
|
|
248
|
+
}}
|
|
249
|
+
onMouseDown={(e) => e.stopPropagation()}
|
|
250
|
+
style={{
|
|
251
|
+
display: "flex",
|
|
252
|
+
alignItems: "center",
|
|
253
|
+
gap: 6,
|
|
254
|
+
padding: "10px 20px",
|
|
255
|
+
backgroundColor: "rgba(30, 30, 30, 0.95)",
|
|
256
|
+
color: "white",
|
|
257
|
+
border: "1px solid rgba(255, 255, 255, 0.2)",
|
|
258
|
+
borderRadius: 6,
|
|
259
|
+
fontSize: 14,
|
|
260
|
+
fontWeight: 500,
|
|
261
|
+
fontFamily: "'Montserrat', system-ui, sans-serif",
|
|
262
|
+
cursor: "pointer",
|
|
263
|
+
transition: "all 0.2s",
|
|
264
|
+
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.3)",
|
|
265
|
+
}}
|
|
266
|
+
>
|
|
267
|
+
<X size={16} />
|
|
268
|
+
Cancel
|
|
269
|
+
</button>
|
|
270
|
+
|
|
271
|
+
{/* Redraw button */}
|
|
272
|
+
<button
|
|
273
|
+
data-sonance-devtools="true"
|
|
274
|
+
onClick={(e) => {
|
|
275
|
+
e.stopPropagation();
|
|
276
|
+
handleRedraw();
|
|
277
|
+
}}
|
|
278
|
+
onMouseDown={(e) => e.stopPropagation()}
|
|
279
|
+
disabled={!currentRect}
|
|
280
|
+
style={{
|
|
281
|
+
display: "flex",
|
|
282
|
+
alignItems: "center",
|
|
283
|
+
gap: 6,
|
|
284
|
+
padding: "10px 20px",
|
|
285
|
+
backgroundColor: currentRect ? "rgba(30, 30, 30, 0.95)" : "rgba(30, 30, 30, 0.6)",
|
|
286
|
+
color: currentRect ? "white" : "rgba(255, 255, 255, 0.4)",
|
|
287
|
+
border: "1px solid rgba(255, 255, 255, 0.2)",
|
|
288
|
+
borderRadius: 6,
|
|
289
|
+
fontSize: 14,
|
|
290
|
+
fontWeight: 500,
|
|
291
|
+
fontFamily: "'Montserrat', system-ui, sans-serif",
|
|
292
|
+
cursor: currentRect ? "pointer" : "not-allowed",
|
|
293
|
+
transition: "all 0.2s",
|
|
294
|
+
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.3)",
|
|
295
|
+
}}
|
|
296
|
+
>
|
|
297
|
+
<RotateCcw size={16} />
|
|
298
|
+
Redraw
|
|
299
|
+
</button>
|
|
300
|
+
|
|
301
|
+
{/* Confirm button */}
|
|
302
|
+
<button
|
|
303
|
+
data-sonance-devtools="true"
|
|
304
|
+
onClick={(e) => {
|
|
305
|
+
e.stopPropagation();
|
|
306
|
+
handleConfirm();
|
|
307
|
+
}}
|
|
308
|
+
onMouseDown={(e) => e.stopPropagation()}
|
|
309
|
+
disabled={!currentRect || isCapturing}
|
|
310
|
+
style={{
|
|
311
|
+
display: "flex",
|
|
312
|
+
alignItems: "center",
|
|
313
|
+
gap: 6,
|
|
314
|
+
padding: "10px 24px",
|
|
315
|
+
backgroundColor: currentRect ? "#00D3C8" : "rgba(0, 211, 200, 0.3)",
|
|
316
|
+
color: currentRect ? "#1a1a1a" : "rgba(26, 26, 26, 0.5)",
|
|
317
|
+
border: "none",
|
|
318
|
+
borderRadius: 6,
|
|
319
|
+
fontSize: 14,
|
|
320
|
+
fontWeight: 600,
|
|
321
|
+
fontFamily: "'Montserrat', system-ui, sans-serif",
|
|
322
|
+
cursor: currentRect && !isCapturing ? "pointer" : "not-allowed",
|
|
323
|
+
transition: "all 0.2s",
|
|
324
|
+
boxShadow: currentRect ? "0 4px 20px rgba(0, 211, 200, 0.4)" : "none",
|
|
325
|
+
}}
|
|
326
|
+
>
|
|
327
|
+
<Check size={16} />
|
|
328
|
+
{isCapturing ? "Capturing..." : "Confirm Selection"}
|
|
329
|
+
</button>
|
|
330
|
+
</div>
|
|
331
|
+
|
|
332
|
+
{/* Keyboard hint */}
|
|
333
|
+
<div
|
|
334
|
+
data-sonance-devtools="true"
|
|
335
|
+
style={{
|
|
336
|
+
position: "fixed",
|
|
337
|
+
bottom: 10,
|
|
338
|
+
left: "50%",
|
|
339
|
+
transform: "translateX(-50%)",
|
|
340
|
+
color: "rgba(255, 255, 255, 0.6)",
|
|
341
|
+
fontSize: 12,
|
|
342
|
+
fontFamily: "'Montserrat', system-ui, sans-serif",
|
|
343
|
+
textShadow: "0 1px 2px rgba(0, 0, 0, 0.5)",
|
|
344
|
+
pointerEvents: "none",
|
|
345
|
+
}}
|
|
346
|
+
>
|
|
347
|
+
Press <kbd style={{ padding: "2px 6px", backgroundColor: "rgba(0,0,0,0.4)", borderRadius: 4 }}>Esc</kbd> to cancel
|
|
348
|
+
</div>
|
|
349
|
+
</div>,
|
|
350
|
+
document.body
|
|
351
|
+
);
|
|
352
|
+
}
|
|
@@ -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.94",
|
|
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",
|