sonance-brand-mcp 1.3.53 → 1.3.56
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.
|
@@ -686,8 +686,9 @@ export async function POST(request: Request) {
|
|
|
686
686
|
const messageContent: Anthropic.MessageCreateParams["messages"][0]["content"] = [];
|
|
687
687
|
|
|
688
688
|
// Add screenshot if provided
|
|
689
|
+
let base64Data = "";
|
|
689
690
|
if (screenshot) {
|
|
690
|
-
|
|
691
|
+
base64Data = screenshot.split(",")[1] || screenshot;
|
|
691
692
|
messageContent.push({
|
|
692
693
|
type: "image",
|
|
693
694
|
source: {
|
|
@@ -698,6 +699,111 @@ export async function POST(request: Request) {
|
|
|
698
699
|
});
|
|
699
700
|
}
|
|
700
701
|
|
|
702
|
+
// ========== PHASE A: VISUAL PROBLEM ANALYSIS ==========
|
|
703
|
+
// Before generating patches, analyze WHAT is visually wrong
|
|
704
|
+
// This forces the LLM to articulate the problem before trying to fix it
|
|
705
|
+
interface VisualAnalysis {
|
|
706
|
+
element: string;
|
|
707
|
+
problem: string;
|
|
708
|
+
currentState: string;
|
|
709
|
+
solution: string;
|
|
710
|
+
confidence: "high" | "medium" | "low";
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
let visualAnalysis: VisualAnalysis | null = null;
|
|
714
|
+
|
|
715
|
+
if (screenshot && userPrompt) {
|
|
716
|
+
debugLog("Phase A: Starting visual problem analysis");
|
|
717
|
+
|
|
718
|
+
const analysisClient = new Anthropic({ apiKey });
|
|
719
|
+
|
|
720
|
+
try {
|
|
721
|
+
const analysisResponse = await analysisClient.messages.create({
|
|
722
|
+
model: "claude-sonnet-4-20250514",
|
|
723
|
+
max_tokens: 1024,
|
|
724
|
+
messages: [
|
|
725
|
+
{
|
|
726
|
+
role: "user",
|
|
727
|
+
content: [
|
|
728
|
+
{
|
|
729
|
+
type: "image",
|
|
730
|
+
source: {
|
|
731
|
+
type: "base64",
|
|
732
|
+
media_type: "image/png",
|
|
733
|
+
data: base64Data,
|
|
734
|
+
},
|
|
735
|
+
},
|
|
736
|
+
{
|
|
737
|
+
type: "text",
|
|
738
|
+
text: `You are analyzing a UI screenshot to understand a visual problem.
|
|
739
|
+
|
|
740
|
+
User request: "${userPrompt}"
|
|
741
|
+
|
|
742
|
+
${focusedElements && focusedElements.length > 0 ? `User clicked on these elements:
|
|
743
|
+
${focusedElements.map((el) => `- ${el.name} (${el.type}) at position (${el.coordinates.x}, ${el.coordinates.y})`).join("\n")}
|
|
744
|
+
` : ""}
|
|
745
|
+
|
|
746
|
+
BEFORE any code changes can be made, you must analyze the visual problem.
|
|
747
|
+
|
|
748
|
+
Answer these questions:
|
|
749
|
+
1. ELEMENT: Which specific UI element has the problem? (describe its location, text content, appearance)
|
|
750
|
+
2. PROBLEM: What exactly is visually wrong? (invisible text, wrong color, poor contrast, wrong size, etc.)
|
|
751
|
+
3. CURRENT STATE: What styling/colors appear to be applied to this element?
|
|
752
|
+
4. SOLUTION: What specific CSS/Tailwind change would fix this problem?
|
|
753
|
+
5. CONFIDENCE: How confident are you that you've identified the correct element and problem? (high/medium/low)
|
|
754
|
+
|
|
755
|
+
Return ONLY valid JSON with no other text:
|
|
756
|
+
{
|
|
757
|
+
"element": "specific description of the UI element",
|
|
758
|
+
"problem": "what is visually wrong",
|
|
759
|
+
"currentState": "what styling appears to be applied",
|
|
760
|
+
"solution": "specific CSS/Tailwind fix needed",
|
|
761
|
+
"confidence": "high"
|
|
762
|
+
}`,
|
|
763
|
+
},
|
|
764
|
+
],
|
|
765
|
+
},
|
|
766
|
+
],
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
const analysisText = analysisResponse.content.find((block) => block.type === "text");
|
|
770
|
+
if (analysisText && analysisText.type === "text") {
|
|
771
|
+
// Parse the JSON response
|
|
772
|
+
let jsonText = analysisText.text.trim();
|
|
773
|
+
// Extract JSON if wrapped in code blocks
|
|
774
|
+
const jsonMatch = jsonText.match(/```(?:json)?\s*([\s\S]*?)\s*```/) || jsonText.match(/(\{[\s\S]*\})/);
|
|
775
|
+
if (jsonMatch) {
|
|
776
|
+
jsonText = jsonMatch[1];
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
try {
|
|
780
|
+
visualAnalysis = JSON.parse(jsonText) as VisualAnalysis;
|
|
781
|
+
debugLog("Phase A: Visual problem analysis complete", visualAnalysis);
|
|
782
|
+
|
|
783
|
+
// If low confidence, return early with clarification request
|
|
784
|
+
if (visualAnalysis.confidence === "low") {
|
|
785
|
+
debugLog("Phase A: Low confidence - requesting clarification");
|
|
786
|
+
return NextResponse.json({
|
|
787
|
+
success: false,
|
|
788
|
+
needsClarification: true,
|
|
789
|
+
analysis: visualAnalysis,
|
|
790
|
+
message: `I can see "${visualAnalysis.element}" but I'm not certain about the problem: "${visualAnalysis.problem}". Can you be more specific about what needs to change?`,
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
} catch (parseError) {
|
|
794
|
+
debugLog("Phase A: Failed to parse analysis response", {
|
|
795
|
+
error: String(parseError),
|
|
796
|
+
response: jsonText.substring(0, 500)
|
|
797
|
+
});
|
|
798
|
+
// Continue without analysis if parsing fails
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
} catch (analysisError) {
|
|
802
|
+
debugLog("Phase A: Analysis call failed", { error: String(analysisError) });
|
|
803
|
+
// Continue without analysis - fall back to existing behavior
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
701
807
|
// ========== SMART CONTEXT BUDGETING ==========
|
|
702
808
|
// Claude can handle 200k tokens (~800k chars), so we can safely include large files
|
|
703
809
|
// Priority: Recommended file (NEVER truncate) > Page file (limited) > Other components (dynamic)
|
|
@@ -731,6 +837,24 @@ User Request: "${userPrompt}"
|
|
|
731
837
|
textContent += `FOCUSED ELEMENTS (user clicked on these):
|
|
732
838
|
${focusedElements.map((el) => `- ${el.name} (${el.type}) at (${el.coordinates.x}, ${el.coordinates.y})`).join("\n")}
|
|
733
839
|
|
|
840
|
+
`;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// ========== VISUAL PROBLEM ANALYSIS (from Phase A) ==========
|
|
844
|
+
if (visualAnalysis) {
|
|
845
|
+
textContent += `═══════════════════════════════════════════════════════════════════════════════
|
|
846
|
+
🔍 VISUAL PROBLEM ANALYSIS (I analyzed the screenshot first)
|
|
847
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
848
|
+
|
|
849
|
+
**Element:** ${visualAnalysis.element}
|
|
850
|
+
**Problem:** ${visualAnalysis.problem}
|
|
851
|
+
**Current State:** ${visualAnalysis.currentState}
|
|
852
|
+
**Required Fix:** ${visualAnalysis.solution}
|
|
853
|
+
**Confidence:** ${visualAnalysis.confidence}
|
|
854
|
+
|
|
855
|
+
⚠️ IMPORTANT: Your patches MUST implement the fix described above for the element described above.
|
|
856
|
+
Find the code that renders "${visualAnalysis.element}" and apply "${visualAnalysis.solution}".
|
|
857
|
+
|
|
734
858
|
`;
|
|
735
859
|
}
|
|
736
860
|
|
|
@@ -4,11 +4,53 @@
|
|
|
4
4
|
* Dynamically discovers and parses theme/design system files from target codebases.
|
|
5
5
|
* Extracts CSS variables, Tailwind colors, and available semantic tokens to provide
|
|
6
6
|
* context to the LLM for intelligent styling decisions.
|
|
7
|
+
*
|
|
8
|
+
* NEW: Includes WCAG contrast ratio calculation to warn about bad color combinations.
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
11
|
import * as fs from "fs";
|
|
10
12
|
import * as path from "path";
|
|
11
13
|
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// TYPE DEFINITIONS
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* RGB color representation
|
|
20
|
+
*/
|
|
21
|
+
interface RGB {
|
|
22
|
+
r: number;
|
|
23
|
+
g: number;
|
|
24
|
+
b: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Contrast warning for a failing color combination
|
|
29
|
+
*/
|
|
30
|
+
interface ContrastWarning {
|
|
31
|
+
background: string;
|
|
32
|
+
text: string;
|
|
33
|
+
ratio: string;
|
|
34
|
+
suggestion: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Safe color pattern that passes WCAG
|
|
39
|
+
*/
|
|
40
|
+
interface SafePattern {
|
|
41
|
+
background: string;
|
|
42
|
+
text: string;
|
|
43
|
+
ratio: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Result of contrast analysis
|
|
48
|
+
*/
|
|
49
|
+
interface ContrastAnalysis {
|
|
50
|
+
warnings: ContrastWarning[];
|
|
51
|
+
safePatterns: SafePattern[];
|
|
52
|
+
}
|
|
53
|
+
|
|
12
54
|
/**
|
|
13
55
|
* Discovered theme information from a codebase
|
|
14
56
|
*/
|
|
@@ -27,8 +69,208 @@ export interface DiscoveredTheme {
|
|
|
27
69
|
|
|
28
70
|
// Files that were discovered and parsed
|
|
29
71
|
discoveredFiles: string[];
|
|
72
|
+
|
|
73
|
+
// Contrast analysis results
|
|
74
|
+
contrastAnalysis?: ContrastAnalysis;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ============================================================================
|
|
78
|
+
// COLOR PARSING FUNCTIONS
|
|
79
|
+
// ============================================================================
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Convert HSL values to RGB
|
|
83
|
+
* Handles formats: "195 100% 41%" or "hsl(195, 100%, 41%)" or "195deg 100% 41%"
|
|
84
|
+
*/
|
|
85
|
+
function hslToRgb(h: number, s: number, l: number): RGB {
|
|
86
|
+
// Normalize h to 0-360, s and l to 0-1
|
|
87
|
+
h = ((h % 360) + 360) % 360;
|
|
88
|
+
s = Math.max(0, Math.min(1, s / 100));
|
|
89
|
+
l = Math.max(0, Math.min(1, l / 100));
|
|
90
|
+
|
|
91
|
+
const k = (n: number) => (n + h / 30) % 12;
|
|
92
|
+
const a = s * Math.min(l, 1 - l);
|
|
93
|
+
const f = (n: number) => l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
r: Math.round(f(0) * 255),
|
|
97
|
+
g: Math.round(f(8) * 255),
|
|
98
|
+
b: Math.round(f(4) * 255),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Parse hex color to RGB
|
|
104
|
+
* Handles: #fff, #ffffff, fff, ffffff
|
|
105
|
+
*/
|
|
106
|
+
function hexToRgb(hex: string): RGB | null {
|
|
107
|
+
// Remove # if present
|
|
108
|
+
hex = hex.replace(/^#/, "");
|
|
109
|
+
|
|
110
|
+
// Expand shorthand (e.g., "fff" -> "ffffff")
|
|
111
|
+
if (hex.length === 3) {
|
|
112
|
+
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (hex.length !== 6) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const r = parseInt(hex.substring(0, 2), 16);
|
|
120
|
+
const g = parseInt(hex.substring(2, 4), 16);
|
|
121
|
+
const b = parseInt(hex.substring(4, 6), 16);
|
|
122
|
+
|
|
123
|
+
if (isNaN(r) || isNaN(g) || isNaN(b)) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return { r, g, b };
|
|
30
128
|
}
|
|
31
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Parse CSS color value to RGB
|
|
132
|
+
* Handles multiple formats:
|
|
133
|
+
* - Hex: #fff, #ffffff
|
|
134
|
+
* - RGB: rgb(255, 255, 255), rgb(255 255 255)
|
|
135
|
+
* - HSL: hsl(195, 100%, 41%), hsl(195 100% 41%)
|
|
136
|
+
* - Space-separated HSL: 195 100% 41% (common in Tailwind/shadcn)
|
|
137
|
+
* - oklch: oklch(0.7 0.15 195) - approximated
|
|
138
|
+
*/
|
|
139
|
+
function parseColorToRgb(value: string): RGB | null {
|
|
140
|
+
if (!value || typeof value !== "string") {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const trimmed = value.trim().toLowerCase();
|
|
145
|
+
|
|
146
|
+
// Skip CSS variable references - we can't resolve these without runtime
|
|
147
|
+
if (trimmed.includes("var(")) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Handle hex colors
|
|
152
|
+
if (trimmed.startsWith("#") || /^[0-9a-f]{3,6}$/i.test(trimmed)) {
|
|
153
|
+
return hexToRgb(trimmed);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Handle rgb() and rgba()
|
|
157
|
+
const rgbMatch = trimmed.match(/rgba?\s*\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)/);
|
|
158
|
+
if (rgbMatch) {
|
|
159
|
+
return {
|
|
160
|
+
r: parseInt(rgbMatch[1], 10),
|
|
161
|
+
g: parseInt(rgbMatch[2], 10),
|
|
162
|
+
b: parseInt(rgbMatch[3], 10),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Handle hsl() and hsla()
|
|
167
|
+
const hslFuncMatch = trimmed.match(/hsla?\s*\(\s*([\d.]+)(?:deg)?[\s,]+([\d.]+)%[\s,]+([\d.]+)%/);
|
|
168
|
+
if (hslFuncMatch) {
|
|
169
|
+
const h = parseFloat(hslFuncMatch[1]);
|
|
170
|
+
const s = parseFloat(hslFuncMatch[2]);
|
|
171
|
+
const l = parseFloat(hslFuncMatch[3]);
|
|
172
|
+
return hslToRgb(h, s, l);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Handle space-separated HSL (common in Tailwind/shadcn): "195 100% 41%"
|
|
176
|
+
const hslSpaceMatch = trimmed.match(/^([\d.]+)(?:deg)?\s+([\d.]+)%\s+([\d.]+)%$/);
|
|
177
|
+
if (hslSpaceMatch) {
|
|
178
|
+
const h = parseFloat(hslSpaceMatch[1]);
|
|
179
|
+
const s = parseFloat(hslSpaceMatch[2]);
|
|
180
|
+
const l = parseFloat(hslSpaceMatch[3]);
|
|
181
|
+
return hslToRgb(h, s, l);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Handle oklch() - approximate conversion (not perfect but gives reasonable results)
|
|
185
|
+
const oklchMatch = trimmed.match(/oklch\s*\(\s*([\d.]+)(?:%?)\s+([\d.]+)\s+([\d.]+)/);
|
|
186
|
+
if (oklchMatch) {
|
|
187
|
+
// Simplified oklch to RGB approximation
|
|
188
|
+
// L is lightness (0-1 or 0-100%), C is chroma, H is hue
|
|
189
|
+
let l = parseFloat(oklchMatch[1]);
|
|
190
|
+
if (l > 1) l = l / 100; // Handle percentage format
|
|
191
|
+
const c = parseFloat(oklchMatch[2]);
|
|
192
|
+
const h = parseFloat(oklchMatch[3]);
|
|
193
|
+
|
|
194
|
+
// Very rough approximation: treat as HSL with adjusted values
|
|
195
|
+
// This won't be perfect but gives a reasonable estimate for contrast
|
|
196
|
+
const approxS = Math.min(100, c * 300); // Chroma to saturation approximation
|
|
197
|
+
const approxL = l * 100;
|
|
198
|
+
return hslToRgb(h, approxS, approxL);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Handle named colors (common ones)
|
|
202
|
+
const namedColors: Record<string, RGB> = {
|
|
203
|
+
white: { r: 255, g: 255, b: 255 },
|
|
204
|
+
black: { r: 0, g: 0, b: 0 },
|
|
205
|
+
transparent: { r: 0, g: 0, b: 0 }, // Treat as black for contrast purposes
|
|
206
|
+
red: { r: 255, g: 0, b: 0 },
|
|
207
|
+
green: { r: 0, g: 128, b: 0 },
|
|
208
|
+
blue: { r: 0, g: 0, b: 255 },
|
|
209
|
+
yellow: { r: 255, g: 255, b: 0 },
|
|
210
|
+
cyan: { r: 0, g: 255, b: 255 },
|
|
211
|
+
magenta: { r: 255, g: 0, b: 255 },
|
|
212
|
+
gray: { r: 128, g: 128, b: 128 },
|
|
213
|
+
grey: { r: 128, g: 128, b: 128 },
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
if (namedColors[trimmed]) {
|
|
217
|
+
return namedColors[trimmed];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ============================================================================
|
|
224
|
+
// WCAG CONTRAST CALCULATION
|
|
225
|
+
// ============================================================================
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Calculate relative luminance using WCAG formula
|
|
229
|
+
* https://www.w3.org/TR/WCAG21/#dfn-relative-luminance
|
|
230
|
+
*/
|
|
231
|
+
function getLuminance(r: number, g: number, b: number): number {
|
|
232
|
+
const [rs, gs, bs] = [r, g, b].map((c) => {
|
|
233
|
+
c /= 255;
|
|
234
|
+
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
235
|
+
});
|
|
236
|
+
return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Calculate WCAG contrast ratio between two colors
|
|
241
|
+
* Returns ratio like 4.5, 7.0, 21.0 (max), etc.
|
|
242
|
+
* WCAG AA requires 4.5:1 for normal text, 3:1 for large text
|
|
243
|
+
* WCAG AAA requires 7:1 for normal text, 4.5:1 for large text
|
|
244
|
+
*/
|
|
245
|
+
function getContrastRatio(rgb1: RGB, rgb2: RGB): number {
|
|
246
|
+
const l1 = getLuminance(rgb1.r, rgb1.g, rgb1.b);
|
|
247
|
+
const l2 = getLuminance(rgb2.r, rgb2.g, rgb2.b);
|
|
248
|
+
const lighter = Math.max(l1, l2);
|
|
249
|
+
const darker = Math.min(l1, l2);
|
|
250
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Determine which alternative (white or black) provides better contrast
|
|
255
|
+
*/
|
|
256
|
+
function getBestAlternative(bgRgb: RGB): { color: string; ratio: number } {
|
|
257
|
+
const whiteRgb: RGB = { r: 255, g: 255, b: 255 };
|
|
258
|
+
const blackRgb: RGB = { r: 0, g: 0, b: 0 };
|
|
259
|
+
|
|
260
|
+
const whiteRatio = getContrastRatio(bgRgb, whiteRgb);
|
|
261
|
+
const blackRatio = getContrastRatio(bgRgb, blackRgb);
|
|
262
|
+
|
|
263
|
+
if (whiteRatio >= blackRatio) {
|
|
264
|
+
return { color: "text-white", ratio: whiteRatio };
|
|
265
|
+
} else {
|
|
266
|
+
return { color: "text-black", ratio: blackRatio };
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ============================================================================
|
|
271
|
+
// THEME FILE DISCOVERY
|
|
272
|
+
// ============================================================================
|
|
273
|
+
|
|
32
274
|
/**
|
|
33
275
|
* Patterns for finding theme-related files
|
|
34
276
|
*/
|
|
@@ -221,49 +463,120 @@ export function extractAvailableTokens(cssVariables: Record<string, string>): st
|
|
|
221
463
|
return [...new Set([...tokens, ...commonTokens])];
|
|
222
464
|
}
|
|
223
465
|
|
|
466
|
+
// ============================================================================
|
|
467
|
+
// CONTRAST ANALYSIS
|
|
468
|
+
// ============================================================================
|
|
469
|
+
|
|
224
470
|
/**
|
|
225
471
|
* Analyze CSS variables to identify contrast relationships
|
|
226
|
-
*
|
|
472
|
+
* Calculates actual WCAG contrast ratios and returns warnings for failing combinations
|
|
227
473
|
*/
|
|
228
474
|
export function analyzeContrastRelationships(
|
|
229
475
|
cssVariables: Record<string, string>
|
|
230
|
-
):
|
|
231
|
-
const warnings:
|
|
232
|
-
const
|
|
476
|
+
): ContrastAnalysis {
|
|
477
|
+
const warnings: ContrastWarning[] = [];
|
|
478
|
+
const safePatterns: SafePattern[] = [];
|
|
233
479
|
|
|
234
|
-
//
|
|
235
|
-
|
|
480
|
+
// Track which backgrounds we've analyzed to add white/black alternatives
|
|
481
|
+
const analyzedBackgrounds: { name: string; rgb: RGB }[] = [];
|
|
236
482
|
|
|
483
|
+
// Find all bg/foreground pairs automatically
|
|
237
484
|
for (const [varName, value] of Object.entries(cssVariables)) {
|
|
238
485
|
// Skip if this is a foreground variable
|
|
239
486
|
if (varName.includes("foreground")) continue;
|
|
240
487
|
|
|
488
|
+
// Skip non-color variables
|
|
489
|
+
if (
|
|
490
|
+
varName.includes("radius") ||
|
|
491
|
+
varName.includes("shadow") ||
|
|
492
|
+
varName.includes("ring") ||
|
|
493
|
+
varName.includes("border") && !varName.match(/border$/)
|
|
494
|
+
) {
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
|
|
241
498
|
const baseName = varName.replace(/^--/, "");
|
|
242
499
|
const foregroundVar = `--${baseName}-foreground`;
|
|
243
500
|
|
|
501
|
+
// Parse background color
|
|
502
|
+
const bgRgb = parseColorToRgb(value);
|
|
503
|
+
if (!bgRgb) continue;
|
|
504
|
+
|
|
505
|
+
// Track this background for alternative suggestions
|
|
506
|
+
analyzedBackgrounds.push({ name: baseName, rgb: bgRgb });
|
|
507
|
+
|
|
508
|
+
// Check if there's a corresponding foreground variable
|
|
244
509
|
if (cssVariables[foregroundVar]) {
|
|
245
|
-
const
|
|
246
|
-
const fgValue = cssVariables[foregroundVar];
|
|
510
|
+
const fgRgb = parseColorToRgb(cssVariables[foregroundVar]);
|
|
247
511
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
512
|
+
if (fgRgb) {
|
|
513
|
+
const ratio = getContrastRatio(bgRgb, fgRgb);
|
|
514
|
+
const passes = ratio >= 4.5; // WCAG AA for normal text
|
|
515
|
+
|
|
516
|
+
if (!passes) {
|
|
517
|
+
// Find the best alternative
|
|
518
|
+
const alternative = getBestAlternative(bgRgb);
|
|
519
|
+
|
|
520
|
+
warnings.push({
|
|
521
|
+
background: `bg-${baseName}`,
|
|
522
|
+
text: `text-${baseName}-foreground`,
|
|
523
|
+
ratio: ratio.toFixed(1),
|
|
524
|
+
suggestion: `Use ${alternative.color} instead (${alternative.ratio.toFixed(1)}:1 contrast)`,
|
|
525
|
+
});
|
|
526
|
+
} else {
|
|
527
|
+
safePatterns.push({
|
|
528
|
+
background: `bg-${baseName}`,
|
|
529
|
+
text: `text-${baseName}-foreground`,
|
|
530
|
+
ratio: ratio.toFixed(1),
|
|
531
|
+
});
|
|
532
|
+
}
|
|
254
533
|
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Add white/black alternatives for all analyzed backgrounds
|
|
538
|
+
const whiteRgb: RGB = { r: 255, g: 255, b: 255 };
|
|
539
|
+
const blackRgb: RGB = { r: 0, g: 0, b: 0 };
|
|
540
|
+
|
|
541
|
+
for (const { name, rgb } of analyzedBackgrounds) {
|
|
542
|
+
// Only add alternatives for semantic color names (primary, accent, etc.)
|
|
543
|
+
if (
|
|
544
|
+
name.includes("primary") ||
|
|
545
|
+
name.includes("secondary") ||
|
|
546
|
+
name.includes("accent") ||
|
|
547
|
+
name.includes("destructive") ||
|
|
548
|
+
name.includes("success") ||
|
|
549
|
+
name.includes("warning") ||
|
|
550
|
+
name.includes("muted")
|
|
551
|
+
) {
|
|
552
|
+
const whiteRatio = getContrastRatio(rgb, whiteRgb);
|
|
553
|
+
const blackRatio = getContrastRatio(rgb, blackRgb);
|
|
255
554
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
555
|
+
if (whiteRatio >= 4.5) {
|
|
556
|
+
safePatterns.push({
|
|
557
|
+
background: `bg-${name}`,
|
|
558
|
+
text: "text-white",
|
|
559
|
+
ratio: whiteRatio.toFixed(1),
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (blackRatio >= 4.5) {
|
|
564
|
+
safePatterns.push({
|
|
565
|
+
background: `bg-${name}`,
|
|
566
|
+
text: "text-black",
|
|
567
|
+
ratio: blackRatio.toFixed(1),
|
|
568
|
+
});
|
|
569
|
+
}
|
|
261
570
|
}
|
|
262
571
|
}
|
|
263
572
|
|
|
264
|
-
return { warnings,
|
|
573
|
+
return { warnings, safePatterns };
|
|
265
574
|
}
|
|
266
575
|
|
|
576
|
+
// ============================================================================
|
|
577
|
+
// MAIN ENTRY POINTS
|
|
578
|
+
// ============================================================================
|
|
579
|
+
|
|
267
580
|
/**
|
|
268
581
|
* Main entry point: Discover theme from a project
|
|
269
582
|
*/
|
|
@@ -308,11 +621,15 @@ export async function discoverTheme(projectRoot: string): Promise<DiscoveredThem
|
|
|
308
621
|
// Extract available tokens
|
|
309
622
|
result.availableTokens = extractAvailableTokens(result.cssVariables);
|
|
310
623
|
|
|
624
|
+
// Run contrast analysis
|
|
625
|
+
result.contrastAnalysis = analyzeContrastRelationships(result.cssVariables);
|
|
626
|
+
|
|
311
627
|
return result;
|
|
312
628
|
}
|
|
313
629
|
|
|
314
630
|
/**
|
|
315
631
|
* Format discovered theme as context for the LLM prompt
|
|
632
|
+
* Includes contrast warnings to prevent bad color combinations
|
|
316
633
|
*/
|
|
317
634
|
export function formatThemeForPrompt(theme: DiscoveredTheme): string {
|
|
318
635
|
const lines: string[] = [];
|
|
@@ -369,6 +686,57 @@ export function formatThemeForPrompt(theme: DiscoveredTheme): string {
|
|
|
369
686
|
lines.push("");
|
|
370
687
|
}
|
|
371
688
|
|
|
689
|
+
// ========== CONTRAST ANALYSIS (NEW) ==========
|
|
690
|
+
// Run contrast analysis if not already done
|
|
691
|
+
const contrastAnalysis = theme.contrastAnalysis || analyzeContrastRelationships(theme.cssVariables);
|
|
692
|
+
|
|
693
|
+
// Show warnings first (most important for LLM)
|
|
694
|
+
if (contrastAnalysis.warnings.length > 0) {
|
|
695
|
+
lines.push("");
|
|
696
|
+
lines.push("⚠️ CONTRAST WARNINGS (these combinations FAIL WCAG in this codebase - DO NOT USE):");
|
|
697
|
+
for (const w of contrastAnalysis.warnings) {
|
|
698
|
+
lines.push(` ❌ ${w.background} + ${w.text}: ${w.ratio}:1 → ${w.suggestion}`);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// Show safe patterns
|
|
703
|
+
if (contrastAnalysis.safePatterns.length > 0) {
|
|
704
|
+
lines.push("");
|
|
705
|
+
lines.push("✓ SAFE COLOR COMBINATIONS (use these instead):");
|
|
706
|
+
|
|
707
|
+
// Deduplicate and sort by background, prioritize semantic foregrounds
|
|
708
|
+
const seen = new Set<string>();
|
|
709
|
+
const sorted = contrastAnalysis.safePatterns
|
|
710
|
+
.filter((p) => {
|
|
711
|
+
const key = `${p.background}+${p.text}`;
|
|
712
|
+
if (seen.has(key)) return false;
|
|
713
|
+
seen.add(key);
|
|
714
|
+
return true;
|
|
715
|
+
})
|
|
716
|
+
.sort((a, b) => {
|
|
717
|
+
// Sort by background name, then prioritize -foreground over white/black
|
|
718
|
+
if (a.background !== b.background) {
|
|
719
|
+
return a.background.localeCompare(b.background);
|
|
720
|
+
}
|
|
721
|
+
// Prioritize -foreground variants
|
|
722
|
+
const aIsForeground = a.text.includes("-foreground");
|
|
723
|
+
const bIsForeground = b.text.includes("-foreground");
|
|
724
|
+
if (aIsForeground && !bIsForeground) return -1;
|
|
725
|
+
if (!aIsForeground && bIsForeground) return 1;
|
|
726
|
+
return 0;
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
for (const s of sorted.slice(0, 15)) { // Limit to avoid prompt bloat
|
|
730
|
+
lines.push(` ✓ ${s.background} + ${s.text}: ${s.ratio}:1`);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
if (sorted.length > 15) {
|
|
734
|
+
lines.push(` ... and ${sorted.length - 15} more safe combinations`);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
lines.push("");
|
|
739
|
+
|
|
372
740
|
// Available Tokens
|
|
373
741
|
if (theme.availableTokens.length > 0) {
|
|
374
742
|
lines.push(`Available Semantic Tokens: ${theme.availableTokens.slice(0, 20).join(", ")}`);
|
|
@@ -385,4 +753,3 @@ export function formatThemeForPrompt(theme: DiscoveredTheme): string {
|
|
|
385
753
|
|
|
386
754
|
return lines.join("\n");
|
|
387
755
|
}
|
|
388
|
-
|
|
@@ -695,8 +695,9 @@ export async function POST(request: Request) {
|
|
|
695
695
|
const messageContent: Anthropic.MessageCreateParams["messages"][0]["content"] = [];
|
|
696
696
|
|
|
697
697
|
// Add screenshot if provided
|
|
698
|
+
let base64Data = "";
|
|
698
699
|
if (screenshot) {
|
|
699
|
-
|
|
700
|
+
base64Data = screenshot.split(",")[1] || screenshot;
|
|
700
701
|
messageContent.push({
|
|
701
702
|
type: "image",
|
|
702
703
|
source: {
|
|
@@ -707,6 +708,111 @@ export async function POST(request: Request) {
|
|
|
707
708
|
});
|
|
708
709
|
}
|
|
709
710
|
|
|
711
|
+
// ========== PHASE A: VISUAL PROBLEM ANALYSIS ==========
|
|
712
|
+
// Before generating patches, analyze WHAT is visually wrong
|
|
713
|
+
// This forces the LLM to articulate the problem before trying to fix it
|
|
714
|
+
interface VisualAnalysis {
|
|
715
|
+
element: string;
|
|
716
|
+
problem: string;
|
|
717
|
+
currentState: string;
|
|
718
|
+
solution: string;
|
|
719
|
+
confidence: "high" | "medium" | "low";
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
let visualAnalysis: VisualAnalysis | null = null;
|
|
723
|
+
|
|
724
|
+
if (screenshot && userPrompt) {
|
|
725
|
+
debugLog("Phase A: Starting visual problem analysis");
|
|
726
|
+
|
|
727
|
+
const analysisClient = new Anthropic({ apiKey });
|
|
728
|
+
|
|
729
|
+
try {
|
|
730
|
+
const analysisResponse = await analysisClient.messages.create({
|
|
731
|
+
model: "claude-sonnet-4-20250514",
|
|
732
|
+
max_tokens: 1024,
|
|
733
|
+
messages: [
|
|
734
|
+
{
|
|
735
|
+
role: "user",
|
|
736
|
+
content: [
|
|
737
|
+
{
|
|
738
|
+
type: "image",
|
|
739
|
+
source: {
|
|
740
|
+
type: "base64",
|
|
741
|
+
media_type: "image/png",
|
|
742
|
+
data: base64Data,
|
|
743
|
+
},
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
type: "text",
|
|
747
|
+
text: `You are analyzing a UI screenshot to understand a visual problem.
|
|
748
|
+
|
|
749
|
+
User request: "${userPrompt}"
|
|
750
|
+
|
|
751
|
+
${focusedElements && focusedElements.length > 0 ? `User clicked on these elements:
|
|
752
|
+
${focusedElements.map((el) => `- ${el.name} (${el.type}) at position (${el.coordinates.x}, ${el.coordinates.y})`).join("\n")}
|
|
753
|
+
` : ""}
|
|
754
|
+
|
|
755
|
+
BEFORE any code changes can be made, you must analyze the visual problem.
|
|
756
|
+
|
|
757
|
+
Answer these questions:
|
|
758
|
+
1. ELEMENT: Which specific UI element has the problem? (describe its location, text content, appearance)
|
|
759
|
+
2. PROBLEM: What exactly is visually wrong? (invisible text, wrong color, poor contrast, wrong size, etc.)
|
|
760
|
+
3. CURRENT STATE: What styling/colors appear to be applied to this element?
|
|
761
|
+
4. SOLUTION: What specific CSS/Tailwind change would fix this problem?
|
|
762
|
+
5. CONFIDENCE: How confident are you that you've identified the correct element and problem? (high/medium/low)
|
|
763
|
+
|
|
764
|
+
Return ONLY valid JSON with no other text:
|
|
765
|
+
{
|
|
766
|
+
"element": "specific description of the UI element",
|
|
767
|
+
"problem": "what is visually wrong",
|
|
768
|
+
"currentState": "what styling appears to be applied",
|
|
769
|
+
"solution": "specific CSS/Tailwind fix needed",
|
|
770
|
+
"confidence": "high"
|
|
771
|
+
}`,
|
|
772
|
+
},
|
|
773
|
+
],
|
|
774
|
+
},
|
|
775
|
+
],
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
const analysisText = analysisResponse.content.find((block) => block.type === "text");
|
|
779
|
+
if (analysisText && analysisText.type === "text") {
|
|
780
|
+
// Parse the JSON response
|
|
781
|
+
let jsonText = analysisText.text.trim();
|
|
782
|
+
// Extract JSON if wrapped in code blocks
|
|
783
|
+
const jsonMatch = jsonText.match(/```(?:json)?\s*([\s\S]*?)\s*```/) || jsonText.match(/(\{[\s\S]*\})/);
|
|
784
|
+
if (jsonMatch) {
|
|
785
|
+
jsonText = jsonMatch[1];
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
try {
|
|
789
|
+
visualAnalysis = JSON.parse(jsonText) as VisualAnalysis;
|
|
790
|
+
debugLog("Phase A: Visual problem analysis complete", visualAnalysis);
|
|
791
|
+
|
|
792
|
+
// If low confidence, return early with clarification request
|
|
793
|
+
if (visualAnalysis.confidence === "low") {
|
|
794
|
+
debugLog("Phase A: Low confidence - requesting clarification");
|
|
795
|
+
return NextResponse.json({
|
|
796
|
+
success: false,
|
|
797
|
+
needsClarification: true,
|
|
798
|
+
analysis: visualAnalysis,
|
|
799
|
+
message: `I can see "${visualAnalysis.element}" but I'm not certain about the problem: "${visualAnalysis.problem}". Can you be more specific about what needs to change?`,
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
} catch (parseError) {
|
|
803
|
+
debugLog("Phase A: Failed to parse analysis response", {
|
|
804
|
+
error: String(parseError),
|
|
805
|
+
response: jsonText.substring(0, 500)
|
|
806
|
+
});
|
|
807
|
+
// Continue without analysis if parsing fails
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
} catch (analysisError) {
|
|
811
|
+
debugLog("Phase A: Analysis call failed", { error: String(analysisError) });
|
|
812
|
+
// Continue without analysis - fall back to existing behavior
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
710
816
|
// ========== SMART CONTEXT BUDGETING ==========
|
|
711
817
|
// Claude can handle 200k tokens (~800k chars), so we can safely include large files
|
|
712
818
|
// Priority: Recommended file (NEVER truncate) > Page file (limited) > Other components (dynamic)
|
|
@@ -740,6 +846,24 @@ User Request: "${userPrompt}"
|
|
|
740
846
|
textContent += `FOCUSED ELEMENTS (user clicked on these):
|
|
741
847
|
${focusedElements.map((el) => `- ${el.name} (${el.type}) at (${el.coordinates.x}, ${el.coordinates.y})`).join("\n")}
|
|
742
848
|
|
|
849
|
+
`;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// ========== VISUAL PROBLEM ANALYSIS (from Phase A) ==========
|
|
853
|
+
if (visualAnalysis) {
|
|
854
|
+
textContent += `═══════════════════════════════════════════════════════════════════════════════
|
|
855
|
+
🔍 VISUAL PROBLEM ANALYSIS (I analyzed the screenshot first)
|
|
856
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
857
|
+
|
|
858
|
+
**Element:** ${visualAnalysis.element}
|
|
859
|
+
**Problem:** ${visualAnalysis.problem}
|
|
860
|
+
**Current State:** ${visualAnalysis.currentState}
|
|
861
|
+
**Required Fix:** ${visualAnalysis.solution}
|
|
862
|
+
**Confidence:** ${visualAnalysis.confidence}
|
|
863
|
+
|
|
864
|
+
⚠️ IMPORTANT: Your patches MUST implement the fix described above for the element described above.
|
|
865
|
+
Find the code that renders "${visualAnalysis.element}" and apply "${visualAnalysis.solution}".
|
|
866
|
+
|
|
743
867
|
`;
|
|
744
868
|
}
|
|
745
869
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sonance-brand-mcp",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.56",
|
|
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",
|