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.
@@ -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
- // Capture screenshot
135
- console.log("[Vision Mode] Capturing screenshot...");
136
- const screenshot = await captureScreenshot();
137
- console.log("[Vision Mode] Screenshot captured:", screenshot ? `${screenshot.length} bytes` : "null");
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: data.success
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
- if (data.success && data.modifications) {
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 the DevTools panel or its portal container
92
- const devToolsPanel = document.querySelector('[data-sonance-devtools="true"]');
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 (devToolsPanel?.contains(e.target as Node)) return;
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.92",
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",