sonance-brand-mcp 1.3.93 → 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.
@@ -1,10 +1,11 @@
1
1
  "use client";
2
2
 
3
3
  import React, { useState, useEffect, useCallback, useRef } from "react";
4
- import { Loader2, Send, Sparkles, Eye, AlertCircle, X } 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";
8
9
 
9
10
  // Helper to detect location failure in explanation
10
11
  function isLocationFailure(explanation: string | undefined): boolean {
@@ -108,6 +109,11 @@ export function ChatInterface({
108
109
  const messagesEndRef = useRef<HTMLDivElement>(null);
109
110
  const inputRef = useRef<HTMLInputElement>(null);
110
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
+
111
117
  // Auto-dismiss toast after 5 seconds
112
118
  useEffect(() => {
113
119
  if (toastMessage) {
@@ -164,6 +170,33 @@ export function ChatInterface({
164
170
  }
165
171
  }, []);
166
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
+
167
200
  // Handle vision mode edit request
168
201
  const handleVisionEdit = async (prompt: string) => {
169
202
  // Use Apply-First mode if callback is provided (new Cursor-style workflow)
@@ -188,22 +221,34 @@ export function ChatInterface({
188
221
  setIsProcessing(true);
189
222
 
190
223
  try {
191
- // Capture screenshot
192
- console.log("[Vision Mode] Capturing screenshot...");
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
- });
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
+ }
207
252
  }
208
253
  }
209
254
 
@@ -595,6 +640,26 @@ export function ChatInterface({
595
640
  "disabled:opacity-50 disabled:bg-gray-50"
596
641
  )}
597
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
+
598
663
  <button
599
664
  onClick={() => handleSend(input || inputRef.current?.value || "")}
600
665
  onPointerDown={(e) => e.stopPropagation()}
@@ -614,6 +679,23 @@ export function ChatInterface({
614
679
  )}
615
680
  </button>
616
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
+ )}
617
699
 
618
700
  {/* Processing Indicator */}
619
701
  {isProcessing && (
@@ -629,6 +711,14 @@ export function ChatInterface({
629
711
  </span>
630
712
  </div>
631
713
  )}
714
+
715
+ {/* Screenshot Annotator Overlay - draws on live app */}
716
+ {isAnnotating && (
717
+ <ScreenshotAnnotator
718
+ onConfirm={handleAnnotationConfirm}
719
+ onCancel={handleAnnotationCancel}
720
+ />
721
+ )}
632
722
  </div>
633
723
  );
634
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sonance-brand-mcp",
3
- "version": "1.3.93",
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",