sonance-brand-mcp 1.3.111 → 1.3.113
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/api/sonance-save-image/route.ts +625 -0
- package/dist/assets/api/sonance-vision-apply/image-styling-detection.ts +1360 -0
- package/dist/assets/api/sonance-vision-apply/route.ts +988 -57
- package/dist/assets/api/sonance-vision-apply/styling-detection.ts +730 -0
- package/dist/assets/api/sonance-vision-apply/theme-discovery.ts +1 -1
- package/dist/assets/brand-system.ts +13 -12
- package/dist/assets/components/accordion.tsx +15 -7
- package/dist/assets/components/alert-dialog.tsx +35 -10
- package/dist/assets/components/alert.tsx +11 -10
- package/dist/assets/components/avatar.tsx +4 -4
- package/dist/assets/components/badge.tsx +16 -12
- package/dist/assets/components/button.stories.tsx +3 -3
- package/dist/assets/components/button.tsx +50 -31
- package/dist/assets/components/calendar.tsx +12 -8
- package/dist/assets/components/card.tsx +35 -29
- package/dist/assets/components/checkbox.tsx +9 -8
- package/dist/assets/components/code.tsx +19 -11
- package/dist/assets/components/command.tsx +32 -13
- package/dist/assets/components/context-menu.tsx +37 -16
- package/dist/assets/components/dialog.tsx +8 -5
- package/dist/assets/components/divider.tsx +15 -5
- package/dist/assets/components/drawer.tsx +4 -3
- package/dist/assets/components/dropdown-menu.tsx +15 -13
- package/dist/assets/components/hover-card.tsx +4 -1
- package/dist/assets/components/image.tsx +1 -1
- package/dist/assets/components/input.tsx +29 -14
- package/dist/assets/components/kbd.stories.tsx +3 -3
- package/dist/assets/components/kbd.tsx +29 -13
- package/dist/assets/components/listbox.tsx +8 -8
- package/dist/assets/components/menubar.tsx +50 -23
- package/dist/assets/components/navbar.stories.tsx +140 -13
- package/dist/assets/components/navbar.tsx +22 -5
- package/dist/assets/components/navigation-menu.tsx +28 -6
- package/dist/assets/components/pagination.tsx +10 -10
- package/dist/assets/components/popover.tsx +10 -8
- package/dist/assets/components/progress.tsx +6 -4
- package/dist/assets/components/radio-group.tsx +5 -5
- package/dist/assets/components/select.tsx +49 -29
- package/dist/assets/components/separator.tsx +3 -3
- package/dist/assets/components/sheet.tsx +4 -4
- package/dist/assets/components/sidebar.tsx +10 -10
- package/dist/assets/components/skeleton.tsx +13 -5
- package/dist/assets/components/slider.tsx +12 -10
- package/dist/assets/components/switch.tsx +4 -4
- package/dist/assets/components/table.tsx +5 -5
- package/dist/assets/components/tabs.tsx +8 -8
- package/dist/assets/components/textarea.tsx +11 -9
- package/dist/assets/components/toast.tsx +7 -7
- package/dist/assets/components/toggle.tsx +27 -7
- package/dist/assets/components/tooltip.tsx +10 -8
- package/dist/assets/components/user.tsx +8 -6
- package/dist/assets/dev-tools/SonanceDevTools.tsx +429 -362
- package/dist/assets/dev-tools/components/ApplyFirstPreview.tsx +10 -10
- package/dist/assets/dev-tools/components/ChatHistory.tsx +11 -7
- package/dist/assets/dev-tools/components/ChatInterface.tsx +61 -20
- package/dist/assets/dev-tools/components/ChatTabBar.tsx +1 -1
- package/dist/assets/dev-tools/components/DiffPreview.tsx +1 -1
- package/dist/assets/dev-tools/components/InlineDiffPreview.tsx +360 -36
- package/dist/assets/dev-tools/components/InspectorOverlay.tsx +9 -9
- package/dist/assets/dev-tools/components/PropertiesPanel.tsx +743 -93
- package/dist/assets/dev-tools/components/ScreenshotAnnotator.tsx +1 -1
- package/dist/assets/dev-tools/components/SectionHighlight.tsx +1 -1
- package/dist/assets/dev-tools/components/VisionDiffPreview.tsx +7 -7
- package/dist/assets/dev-tools/components/VisionModeBorder.tsx +4 -64
- package/dist/assets/dev-tools/hooks/index.ts +69 -0
- package/dist/assets/dev-tools/hooks/useComponentDetection.ts +132 -0
- package/dist/assets/dev-tools/hooks/useComputedStyles.ts +171 -65
- package/dist/assets/dev-tools/hooks/useContentHash.ts +212 -0
- package/dist/assets/dev-tools/hooks/useElementScanner.ts +398 -0
- package/dist/assets/dev-tools/hooks/useImageDetection.ts +162 -0
- package/dist/assets/dev-tools/hooks/useTextDetection.ts +217 -0
- package/dist/assets/dev-tools/panels/ComponentsPanel.tsx +160 -57
- package/dist/assets/dev-tools/panels/TextPanel.tsx +10 -10
- package/dist/assets/dev-tools/types.ts +42 -0
- package/dist/assets/globals.css +225 -9
- package/dist/assets/styles/brand-overrides.css +3 -2
- package/dist/assets/utils.ts +2 -1
- package/dist/index.js +32 -1
- package/package.json +1 -1
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sonance DevTools API - Intelligent Image Save
|
|
3
|
+
*
|
|
4
|
+
* This endpoint intelligently detects how images are styled in the codebase
|
|
5
|
+
* and applies changes using the appropriate strategy:
|
|
6
|
+
* - CSS Variables → Update CSS file
|
|
7
|
+
* - Tailwind → Update class attribute
|
|
8
|
+
* - Inline Styles → Modify component file
|
|
9
|
+
* - Config Files → Update config
|
|
10
|
+
* - Unknown → AI-assisted modification
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { NextResponse } from 'next/server';
|
|
14
|
+
import * as fs from 'fs';
|
|
15
|
+
import * as path from 'path';
|
|
16
|
+
import {
|
|
17
|
+
detectImageStylingPattern,
|
|
18
|
+
buildSaveInstructions,
|
|
19
|
+
type ImageContext,
|
|
20
|
+
type ImageStylingPattern,
|
|
21
|
+
type SaveStrategy
|
|
22
|
+
} from '../sonance-vision-apply/image-styling-detection';
|
|
23
|
+
|
|
24
|
+
interface SaveImageRequest {
|
|
25
|
+
imageId: string;
|
|
26
|
+
imageSrc: string;
|
|
27
|
+
altText?: string;
|
|
28
|
+
className?: string;
|
|
29
|
+
elementId?: string;
|
|
30
|
+
scale?: number;
|
|
31
|
+
width?: number;
|
|
32
|
+
height?: number;
|
|
33
|
+
src?: string; // New source if changing the image
|
|
34
|
+
reset?: boolean;
|
|
35
|
+
pageRoute?: string; // The page where the image was clicked
|
|
36
|
+
dataAttributes?: Record<string, string>; // Data attributes from the DOM element
|
|
37
|
+
currentBrand?: string; // The current brand context from useBrand()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Dynamically find a CSS file that contains CSS variable definitions
|
|
42
|
+
* No hardcoded paths - searches common locations
|
|
43
|
+
*/
|
|
44
|
+
async function findCSSVariablesFile(projectRoot: string): Promise<string> {
|
|
45
|
+
const searchDirs = ['src/styles', 'src/app', 'styles', 'app'];
|
|
46
|
+
|
|
47
|
+
for (const dir of searchDirs) {
|
|
48
|
+
const dirPath = path.join(projectRoot, dir);
|
|
49
|
+
if (!fs.existsSync(dirPath)) continue;
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const files = fs.readdirSync(dirPath);
|
|
53
|
+
for (const file of files) {
|
|
54
|
+
if (!file.endsWith('.css')) continue;
|
|
55
|
+
|
|
56
|
+
const filePath = path.join(dirPath, file);
|
|
57
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
58
|
+
|
|
59
|
+
// Look for :root with CSS variables
|
|
60
|
+
if (content.includes(':root') && content.includes('--')) {
|
|
61
|
+
return path.join(dir, file);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
// Skip unreadable directories
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Fallback to common patterns
|
|
70
|
+
if (fs.existsSync(path.join(projectRoot, 'src/styles'))) {
|
|
71
|
+
return 'src/styles/variables.css';
|
|
72
|
+
}
|
|
73
|
+
if (fs.existsSync(path.join(projectRoot, 'src/app'))) {
|
|
74
|
+
return 'src/app/globals.css';
|
|
75
|
+
}
|
|
76
|
+
return 'styles/globals.css';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface ComponentFile {
|
|
80
|
+
path: string;
|
|
81
|
+
content: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function POST(request: Request) {
|
|
85
|
+
// Security: Only allow in development
|
|
86
|
+
if (process.env.NODE_ENV !== 'development') {
|
|
87
|
+
return NextResponse.json(
|
|
88
|
+
{ error: 'This endpoint is only available in development mode.' },
|
|
89
|
+
{ status: 403 }
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const body: SaveImageRequest = await request.json();
|
|
95
|
+
const { imageId, imageSrc, altText, className, elementId, scale, width, height, src, reset, pageRoute, dataAttributes, currentBrand } = body;
|
|
96
|
+
|
|
97
|
+
console.log('[Image Save] Request received:', { imageId, imageSrc, scale, width, height, elementId, pageRoute, currentBrand });
|
|
98
|
+
|
|
99
|
+
const projectRoot = process.cwd();
|
|
100
|
+
|
|
101
|
+
// Build image context with all available information
|
|
102
|
+
const imageContext: ImageContext = {
|
|
103
|
+
imageSrc: imageSrc || '',
|
|
104
|
+
elementId,
|
|
105
|
+
className,
|
|
106
|
+
altText,
|
|
107
|
+
pageRoute,
|
|
108
|
+
dataAttributes,
|
|
109
|
+
currentBrand, // Pass the current brand context for correct variable targeting
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// Gather component files for analysis
|
|
113
|
+
const componentFiles = await gatherComponentFiles(projectRoot);
|
|
114
|
+
|
|
115
|
+
// Detect the styling pattern
|
|
116
|
+
const detection = await detectImageStylingPattern(projectRoot, imageContext, componentFiles);
|
|
117
|
+
|
|
118
|
+
console.log('[Image Save] Detected pattern:', {
|
|
119
|
+
type: detection.pattern.type,
|
|
120
|
+
strategy: detection.pattern.strategy,
|
|
121
|
+
confidence: detection.pattern.confidence,
|
|
122
|
+
sourceFile: detection.pattern.sourceFile,
|
|
123
|
+
details: detection.pattern.details,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Apply changes based on strategy
|
|
127
|
+
const result = await applyImageChanges(
|
|
128
|
+
projectRoot,
|
|
129
|
+
detection.pattern,
|
|
130
|
+
{ scale, width, height, src },
|
|
131
|
+
imageContext,
|
|
132
|
+
componentFiles,
|
|
133
|
+
reset
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
if (result.success) {
|
|
137
|
+
return NextResponse.json({
|
|
138
|
+
success: true,
|
|
139
|
+
message: result.message,
|
|
140
|
+
strategy: detection.pattern.strategy,
|
|
141
|
+
pattern: detection.pattern.type,
|
|
142
|
+
modifiedFile: detection.pattern.sourceFile,
|
|
143
|
+
details: detection.pattern.details,
|
|
144
|
+
});
|
|
145
|
+
} else {
|
|
146
|
+
return NextResponse.json(
|
|
147
|
+
{ success: false, error: result.error, pattern: detection.pattern },
|
|
148
|
+
{ status: 400 }
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
} catch (error) {
|
|
152
|
+
console.error('[Image Save] Error:', error);
|
|
153
|
+
return NextResponse.json(
|
|
154
|
+
{ success: false, error: String(error) },
|
|
155
|
+
{ status: 500 }
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Apply image changes based on detected strategy
|
|
162
|
+
*/
|
|
163
|
+
async function applyImageChanges(
|
|
164
|
+
projectRoot: string,
|
|
165
|
+
pattern: ImageStylingPattern,
|
|
166
|
+
override: { scale?: number; width?: number; height?: number; src?: string },
|
|
167
|
+
imageContext: ImageContext,
|
|
168
|
+
componentFiles: ComponentFile[],
|
|
169
|
+
reset?: boolean
|
|
170
|
+
): Promise<{ success: boolean; message?: string; error?: string }> {
|
|
171
|
+
|
|
172
|
+
console.log('[Image Save] Applying changes with strategy:', pattern.strategy);
|
|
173
|
+
console.log('[Image Save] Instructions:', buildSaveInstructions(pattern, override));
|
|
174
|
+
|
|
175
|
+
switch (pattern.strategy) {
|
|
176
|
+
case 'css-file':
|
|
177
|
+
return applyCSSVariableChanges(projectRoot, pattern, override, reset);
|
|
178
|
+
|
|
179
|
+
case 'tailwind-class':
|
|
180
|
+
return applyTailwindChanges(projectRoot, pattern, override, imageContext, componentFiles);
|
|
181
|
+
|
|
182
|
+
case 'component-inline':
|
|
183
|
+
return applyInlineStyleChanges(projectRoot, pattern, override, imageContext, componentFiles);
|
|
184
|
+
|
|
185
|
+
case 'config-file':
|
|
186
|
+
return applyConfigFileChanges(projectRoot, pattern, override, imageContext);
|
|
187
|
+
|
|
188
|
+
case 'ai-assisted':
|
|
189
|
+
default:
|
|
190
|
+
return applyAIAssistedChanges(projectRoot, override, imageContext, componentFiles);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Apply changes via CSS variables
|
|
196
|
+
*/
|
|
197
|
+
async function applyCSSVariableChanges(
|
|
198
|
+
projectRoot: string,
|
|
199
|
+
pattern: ImageStylingPattern,
|
|
200
|
+
override: { scale?: number; width?: number; height?: number },
|
|
201
|
+
reset?: boolean
|
|
202
|
+
): Promise<{ success: boolean; message?: string; error?: string }> {
|
|
203
|
+
|
|
204
|
+
// Use the source file from detection, or find a suitable CSS file dynamically
|
|
205
|
+
let cssFilePath = pattern.sourceFile;
|
|
206
|
+
if (!cssFilePath) {
|
|
207
|
+
// Dynamically find a CSS file with variable definitions
|
|
208
|
+
cssFilePath = await findCSSVariablesFile(projectRoot);
|
|
209
|
+
}
|
|
210
|
+
const cssPath = path.join(projectRoot, cssFilePath);
|
|
211
|
+
|
|
212
|
+
// Use the detected variable directly - no hardcoded brand names
|
|
213
|
+
// The detection phase already determined the correct variable name
|
|
214
|
+
const varName = pattern.cssVariable || '--app-scale';
|
|
215
|
+
|
|
216
|
+
console.log('[CSS Save] Updating variable:', { varName, cssPath: pattern.sourceFile, scale: override.scale });
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
// Read existing CSS
|
|
220
|
+
let cssContent = '';
|
|
221
|
+
if (fs.existsSync(cssPath)) {
|
|
222
|
+
cssContent = fs.readFileSync(cssPath, 'utf-8');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Escape special regex characters in variable name (especially dashes)
|
|
226
|
+
const escapedVarName = varName.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
227
|
+
const varPattern = new RegExp(`(${escapedVarName}:\\s*)([^;]+)(;)`, 'g');
|
|
228
|
+
|
|
229
|
+
// Check if variable exists in the file
|
|
230
|
+
const varExists = varPattern.test(cssContent);
|
|
231
|
+
// Reset the regex lastIndex after test
|
|
232
|
+
varPattern.lastIndex = 0;
|
|
233
|
+
|
|
234
|
+
if (reset) {
|
|
235
|
+
// Reset to default (1)
|
|
236
|
+
if (varExists) {
|
|
237
|
+
cssContent = cssContent.replace(varPattern, '$11$3');
|
|
238
|
+
}
|
|
239
|
+
} else if (varExists) {
|
|
240
|
+
// Update existing variable
|
|
241
|
+
cssContent = cssContent.replace(varPattern, `$1${override.scale || 1}$3`);
|
|
242
|
+
} else {
|
|
243
|
+
// Variable doesn't exist - need to add it
|
|
244
|
+
// Check if there's a :root block
|
|
245
|
+
const rootPattern = /(:root\s*\{)([^}]*)/;
|
|
246
|
+
const rootMatch = cssContent.match(rootPattern);
|
|
247
|
+
|
|
248
|
+
if (rootMatch) {
|
|
249
|
+
// Add to existing :root block
|
|
250
|
+
const existingContent = rootMatch[2];
|
|
251
|
+
const newContent = `${existingContent}\n ${varName}: ${override.scale || 1};`;
|
|
252
|
+
cssContent = cssContent.replace(rootPattern, `$1${newContent}`);
|
|
253
|
+
} else {
|
|
254
|
+
// Create new :root block at the beginning
|
|
255
|
+
cssContent = `:root {\n ${varName}: ${override.scale || 1};\n}\n\n${cssContent}`;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Ensure directory exists
|
|
260
|
+
const cssDir = path.dirname(cssPath);
|
|
261
|
+
if (!fs.existsSync(cssDir)) {
|
|
262
|
+
fs.mkdirSync(cssDir, { recursive: true });
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
fs.writeFileSync(cssPath, cssContent, 'utf-8');
|
|
266
|
+
|
|
267
|
+
console.log('[CSS Save] Successfully updated:', { varName, scale: override.scale || 1 });
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
success: true,
|
|
271
|
+
message: `Updated ${varName} to ${override.scale || 1} in ${pattern.sourceFile}`
|
|
272
|
+
};
|
|
273
|
+
} catch (error) {
|
|
274
|
+
return { success: false, error: `Failed to update CSS: ${error}` };
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Apply changes via Tailwind classes
|
|
280
|
+
*/
|
|
281
|
+
async function applyTailwindChanges(
|
|
282
|
+
projectRoot: string,
|
|
283
|
+
pattern: ImageStylingPattern,
|
|
284
|
+
override: { scale?: number; width?: number; height?: number },
|
|
285
|
+
imageContext: ImageContext,
|
|
286
|
+
componentFiles: ComponentFile[]
|
|
287
|
+
): Promise<{ success: boolean; message?: string; error?: string }> {
|
|
288
|
+
|
|
289
|
+
if (!pattern.sourceFile) {
|
|
290
|
+
return { success: false, error: 'No source file identified for Tailwind changes' };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const filePath = path.join(projectRoot, pattern.sourceFile);
|
|
294
|
+
if (!fs.existsSync(filePath)) {
|
|
295
|
+
return { success: false, error: `File not found: ${pattern.sourceFile}` };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
let content = fs.readFileSync(filePath, 'utf-8');
|
|
300
|
+
|
|
301
|
+
// Find the image element
|
|
302
|
+
const imageSrcEscaped = imageContext.imageSrc.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
303
|
+
const imagePattern = new RegExp(`(<(?:img|Image)[^>]*(?:src=["']${imageSrcEscaped}["']|alt=["']${imageContext.altText}["'])[^>]*className=["'])([^"']+)(["'][^>]*>)`, 'g');
|
|
304
|
+
|
|
305
|
+
const newScale = override.scale ? Math.round(override.scale * 100) : null;
|
|
306
|
+
|
|
307
|
+
content = content.replace(imagePattern, (match, before, classes, after) => {
|
|
308
|
+
let newClasses = classes;
|
|
309
|
+
|
|
310
|
+
// Update scale class
|
|
311
|
+
if (newScale !== null) {
|
|
312
|
+
// Remove existing scale class
|
|
313
|
+
newClasses = newClasses.replace(/\bscale-\d+\b/g, '').trim();
|
|
314
|
+
// Add new scale class (only if not 100)
|
|
315
|
+
if (newScale !== 100) {
|
|
316
|
+
newClasses = `${newClasses} scale-${newScale}`.trim();
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Update width class if provided
|
|
321
|
+
if (override.width) {
|
|
322
|
+
newClasses = newClasses.replace(/\bw-\d+\b/g, '').trim();
|
|
323
|
+
newClasses = `${newClasses} w-[${override.width}px]`.trim();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Update height class if provided
|
|
327
|
+
if (override.height) {
|
|
328
|
+
newClasses = newClasses.replace(/\bh-\d+\b/g, '').trim();
|
|
329
|
+
newClasses = `${newClasses} h-[${override.height}px]`.trim();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return `${before}${newClasses}${after}`;
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
success: true,
|
|
339
|
+
message: `Updated Tailwind classes in ${pattern.sourceFile}`
|
|
340
|
+
};
|
|
341
|
+
} catch (error) {
|
|
342
|
+
return { success: false, error: `Failed to update Tailwind classes: ${error}` };
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Apply changes via inline styles
|
|
348
|
+
*/
|
|
349
|
+
async function applyInlineStyleChanges(
|
|
350
|
+
projectRoot: string,
|
|
351
|
+
pattern: ImageStylingPattern,
|
|
352
|
+
override: { scale?: number; width?: number; height?: number },
|
|
353
|
+
imageContext: ImageContext,
|
|
354
|
+
componentFiles: ComponentFile[]
|
|
355
|
+
): Promise<{ success: boolean; message?: string; error?: string }> {
|
|
356
|
+
|
|
357
|
+
if (!pattern.sourceFile) {
|
|
358
|
+
return { success: false, error: 'No source file identified for inline style changes' };
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const filePath = path.join(projectRoot, pattern.sourceFile);
|
|
362
|
+
if (!fs.existsSync(filePath)) {
|
|
363
|
+
return { success: false, error: `File not found: ${pattern.sourceFile}` };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
try {
|
|
367
|
+
let content = fs.readFileSync(filePath, 'utf-8');
|
|
368
|
+
const lines = content.split('\n');
|
|
369
|
+
|
|
370
|
+
// Find the image line (around the detected line number)
|
|
371
|
+
const searchStart = Math.max(0, (pattern.lineNumber || 1) - 5);
|
|
372
|
+
const searchEnd = Math.min(lines.length, (pattern.lineNumber || 1) + 10);
|
|
373
|
+
|
|
374
|
+
for (let i = searchStart; i < searchEnd; i++) {
|
|
375
|
+
const line = lines[i];
|
|
376
|
+
|
|
377
|
+
// Check if this line contains the image
|
|
378
|
+
if (line.includes(imageContext.imageSrc) ||
|
|
379
|
+
(imageContext.altText && line.includes(imageContext.altText)) ||
|
|
380
|
+
(line.includes('<Image') || line.includes('<img'))) {
|
|
381
|
+
|
|
382
|
+
// Update transform scale in style prop
|
|
383
|
+
if (override.scale !== undefined) {
|
|
384
|
+
// Pattern: style={{ ... transform: 'scale(X)' ... }} or style={{ ... transform: `scale(${var})` ... }}
|
|
385
|
+
const scalePattern = /transform:\s*[`'"]?scale\([^)]*\)[`'"]?/;
|
|
386
|
+
const stylePattern = /style=\{\{([^}]*)\}\}/;
|
|
387
|
+
|
|
388
|
+
if (scalePattern.test(line)) {
|
|
389
|
+
// Update existing scale
|
|
390
|
+
lines[i] = line.replace(scalePattern, `transform: 'scale(${override.scale})'`);
|
|
391
|
+
} else if (stylePattern.test(line)) {
|
|
392
|
+
// Add scale to existing style
|
|
393
|
+
lines[i] = line.replace(stylePattern, `style={{$1, transform: 'scale(${override.scale})'}}`);
|
|
394
|
+
} else {
|
|
395
|
+
// Check next few lines for style prop
|
|
396
|
+
for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) {
|
|
397
|
+
if (scalePattern.test(lines[j])) {
|
|
398
|
+
lines[j] = lines[j].replace(scalePattern, `transform: 'scale(${override.scale})'`);
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Update width/height props for Next.js Image
|
|
406
|
+
if (override.width !== undefined) {
|
|
407
|
+
const widthPattern = /width=\{?\d+\}?/;
|
|
408
|
+
if (widthPattern.test(line)) {
|
|
409
|
+
lines[i] = line.replace(widthPattern, `width={${override.width}}`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (override.height !== undefined) {
|
|
414
|
+
const heightPattern = /height=\{?\d+\}?/;
|
|
415
|
+
if (heightPattern.test(line)) {
|
|
416
|
+
lines[i] = line.replace(heightPattern, `height={${override.height}}`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
content = lines.join('\n');
|
|
425
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
426
|
+
|
|
427
|
+
return {
|
|
428
|
+
success: true,
|
|
429
|
+
message: `Updated inline styles in ${pattern.sourceFile}`
|
|
430
|
+
};
|
|
431
|
+
} catch (error) {
|
|
432
|
+
return { success: false, error: `Failed to update inline styles: ${error}` };
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Apply changes via config file
|
|
438
|
+
*/
|
|
439
|
+
async function applyConfigFileChanges(
|
|
440
|
+
projectRoot: string,
|
|
441
|
+
pattern: ImageStylingPattern,
|
|
442
|
+
override: { scale?: number; width?: number; height?: number },
|
|
443
|
+
imageContext: ImageContext
|
|
444
|
+
): Promise<{ success: boolean; message?: string; error?: string }> {
|
|
445
|
+
|
|
446
|
+
if (!pattern.sourceFile) {
|
|
447
|
+
return { success: false, error: 'No config file identified' };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const filePath = path.join(projectRoot, pattern.sourceFile);
|
|
451
|
+
if (!fs.existsSync(filePath)) {
|
|
452
|
+
return { success: false, error: `Config file not found: ${pattern.sourceFile}` };
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
try {
|
|
456
|
+
let content = fs.readFileSync(filePath, 'utf-8');
|
|
457
|
+
const configKey = pattern.configKey || imageContext.imageSrc.split('/').pop()?.replace(/\.[^.]+$/, '') || '';
|
|
458
|
+
|
|
459
|
+
// Look for existing config entry
|
|
460
|
+
const entryPattern = new RegExp(`['"]?${configKey}['"]?\\s*:\\s*\\{([^}]*)\\}`, 'g');
|
|
461
|
+
|
|
462
|
+
if (entryPattern.test(content)) {
|
|
463
|
+
// Update existing entry
|
|
464
|
+
content = content.replace(entryPattern, (match, existing) => {
|
|
465
|
+
let updated = existing;
|
|
466
|
+
|
|
467
|
+
if (override.scale !== undefined) {
|
|
468
|
+
if (/scale\s*:/.test(updated)) {
|
|
469
|
+
updated = updated.replace(/scale\s*:\s*[^,}]+/, `scale: ${override.scale}`);
|
|
470
|
+
} else {
|
|
471
|
+
updated = `${updated.trim()}, scale: ${override.scale}`;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (override.width !== undefined) {
|
|
476
|
+
if (/width\s*:/.test(updated)) {
|
|
477
|
+
updated = updated.replace(/width\s*:\s*[^,}]+/, `width: ${override.width}`);
|
|
478
|
+
} else {
|
|
479
|
+
updated = `${updated.trim()}, width: ${override.width}`;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (override.height !== undefined) {
|
|
484
|
+
if (/height\s*:/.test(updated)) {
|
|
485
|
+
updated = updated.replace(/height\s*:\s*[^,}]+/, `height: ${override.height}`);
|
|
486
|
+
} else {
|
|
487
|
+
updated = `${updated.trim()}, height: ${override.height}`;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return `'${configKey}': {${updated}}`;
|
|
492
|
+
});
|
|
493
|
+
} else {
|
|
494
|
+
// Config entry doesn't exist - this is a warning case
|
|
495
|
+
console.warn(`[Image Save] Config entry for ${configKey} not found in ${pattern.sourceFile}`);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
499
|
+
|
|
500
|
+
return {
|
|
501
|
+
success: true,
|
|
502
|
+
message: `Updated config for ${configKey} in ${pattern.sourceFile}`
|
|
503
|
+
};
|
|
504
|
+
} catch (error) {
|
|
505
|
+
return { success: false, error: `Failed to update config: ${error}` };
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Fallback: Use AI-assisted modification
|
|
511
|
+
*/
|
|
512
|
+
async function applyAIAssistedChanges(
|
|
513
|
+
projectRoot: string,
|
|
514
|
+
override: { scale?: number; width?: number; height?: number; src?: string },
|
|
515
|
+
imageContext: ImageContext,
|
|
516
|
+
componentFiles: ComponentFile[]
|
|
517
|
+
): Promise<{ success: boolean; message?: string; error?: string }> {
|
|
518
|
+
|
|
519
|
+
// For AI-assisted, we'll forward to the vision-apply endpoint
|
|
520
|
+
// This keeps the logic centralized
|
|
521
|
+
|
|
522
|
+
const changes: string[] = [];
|
|
523
|
+
if (override.src) changes.push(`change source to "${override.src}"`);
|
|
524
|
+
if (override.scale) changes.push(`set scale to ${override.scale}`);
|
|
525
|
+
if (override.width) changes.push(`set width to ${override.width}px`);
|
|
526
|
+
if (override.height) changes.push(`set height to ${override.height}px`);
|
|
527
|
+
|
|
528
|
+
const prompt = `For the image "${imageContext.altText || imageContext.imageSrc}", ${changes.join(', ')}.
|
|
529
|
+
The image source is: ${imageContext.imageSrc}
|
|
530
|
+
${imageContext.elementId ? `Element ID: ${imageContext.elementId}` : ''}
|
|
531
|
+
${imageContext.className ? `Classes: ${imageContext.className}` : ''}`;
|
|
532
|
+
|
|
533
|
+
try {
|
|
534
|
+
const response = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'}/api/sonance-vision-apply`, {
|
|
535
|
+
method: 'POST',
|
|
536
|
+
headers: { 'Content-Type': 'application/json' },
|
|
537
|
+
body: JSON.stringify({
|
|
538
|
+
action: 'apply',
|
|
539
|
+
pageRoute: '/',
|
|
540
|
+
userPrompt: prompt,
|
|
541
|
+
focusedElements: [{
|
|
542
|
+
name: imageContext.altText || 'Image',
|
|
543
|
+
type: 'logo',
|
|
544
|
+
imageSrc: imageContext.imageSrc,
|
|
545
|
+
coordinates: { x: 0, y: 0, width: 100, height: 100 }
|
|
546
|
+
}]
|
|
547
|
+
})
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
const result = await response.json();
|
|
551
|
+
|
|
552
|
+
if (result.success) {
|
|
553
|
+
return { success: true, message: 'Applied changes via AI-assisted modification' };
|
|
554
|
+
} else {
|
|
555
|
+
return { success: false, error: result.error || 'AI-assisted modification failed' };
|
|
556
|
+
}
|
|
557
|
+
} catch (error) {
|
|
558
|
+
return { success: false, error: `AI-assisted modification error: ${error}` };
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Gather component files for analysis
|
|
564
|
+
*/
|
|
565
|
+
async function gatherComponentFiles(projectRoot: string): Promise<ComponentFile[]> {
|
|
566
|
+
const files: ComponentFile[] = [];
|
|
567
|
+
const searchDirs = [
|
|
568
|
+
'src/components',
|
|
569
|
+
'src/app',
|
|
570
|
+
'src/lib',
|
|
571
|
+
'src/styles',
|
|
572
|
+
'components',
|
|
573
|
+
'app',
|
|
574
|
+
'lib',
|
|
575
|
+
];
|
|
576
|
+
|
|
577
|
+
for (const dir of searchDirs) {
|
|
578
|
+
const dirPath = path.join(projectRoot, dir);
|
|
579
|
+
if (!fs.existsSync(dirPath)) continue;
|
|
580
|
+
|
|
581
|
+
await walkDirectory(dirPath, files, projectRoot);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return files;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
async function walkDirectory(dirPath: string, files: ComponentFile[], projectRoot: string): Promise<void> {
|
|
588
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
589
|
+
|
|
590
|
+
for (const entry of entries) {
|
|
591
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
592
|
+
|
|
593
|
+
if (entry.isDirectory()) {
|
|
594
|
+
// Skip node_modules, .next, etc.
|
|
595
|
+
if (['node_modules', '.next', 'dist', 'build', '.git'].includes(entry.name)) continue;
|
|
596
|
+
await walkDirectory(fullPath, files, projectRoot);
|
|
597
|
+
} else if (entry.isFile()) {
|
|
598
|
+
// Only include relevant file types
|
|
599
|
+
if (!/\.(tsx?|jsx?|css)$/.test(entry.name)) continue;
|
|
600
|
+
|
|
601
|
+
try {
|
|
602
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
603
|
+
files.push({
|
|
604
|
+
path: fullPath.replace(projectRoot + '/', ''),
|
|
605
|
+
content
|
|
606
|
+
});
|
|
607
|
+
} catch {
|
|
608
|
+
// Skip files we can't read
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
export async function GET() {
|
|
615
|
+
return NextResponse.json({
|
|
616
|
+
description: 'Intelligent Image Save API',
|
|
617
|
+
strategies: [
|
|
618
|
+
'css-file - Updates CSS variables',
|
|
619
|
+
'tailwind-class - Modifies Tailwind classes',
|
|
620
|
+
'component-inline - Updates inline styles',
|
|
621
|
+
'config-file - Modifies config files',
|
|
622
|
+
'ai-assisted - Uses AI to determine changes'
|
|
623
|
+
]
|
|
624
|
+
});
|
|
625
|
+
}
|