sonance-brand-mcp 1.3.60 → 1.3.62
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.
|
@@ -2,7 +2,7 @@ import { NextResponse } from "next/server";
|
|
|
2
2
|
import * as fs from "fs";
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
import Anthropic from "@anthropic-ai/sdk";
|
|
5
|
-
import { discoverTheme
|
|
5
|
+
import { discoverTheme } from "../sonance-vision-apply/theme-discovery";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Sonance DevTools API - Vision Mode Editor
|
|
@@ -507,83 +507,16 @@ function searchFilesSmart(
|
|
|
507
507
|
return sortedResults.map(r => ({ path: r.path, content: r.content, score: r.score }));
|
|
508
508
|
}
|
|
509
509
|
|
|
510
|
-
const VISION_SYSTEM_PROMPT = `You
|
|
511
|
-
|
|
512
|
-
═══════════════════════════════════════════════════════════════════════════════
|
|
513
|
-
STEP 1: ANALYZE (Think like Cursor)
|
|
514
|
-
═══════════════════════════════════════════════════════════════════════════════
|
|
515
|
-
|
|
516
|
-
Before writing any patches, analyze the screenshot and answer:
|
|
517
|
-
|
|
518
|
-
1. CURRENT STATE: What do I see in the screenshot?
|
|
519
|
-
- Is the element visible/readable right now?
|
|
520
|
-
- What colors/styles are currently applied?
|
|
521
|
-
|
|
522
|
-
2. PROBLEM IDENTIFICATION: What's actually wrong?
|
|
523
|
-
- Contrast issue? (text blending with background)
|
|
524
|
-
- Hidden element? (not showing at all)
|
|
525
|
-
- Hover state issue? (only visible on interaction)
|
|
526
|
-
- Layout issue? (wrong size/position)
|
|
527
|
-
- Or is it already correct?
|
|
528
|
-
|
|
529
|
-
3. FIX STRATEGY: What should I change?
|
|
530
|
-
- DEFAULT state only (element needs to look different normally)
|
|
531
|
-
- HOVER state only (element needs different hover effect)
|
|
532
|
-
- BOTH states (element needs overall visibility improvement)
|
|
533
|
-
- ASK for clarification (request is too vague)
|
|
534
|
-
|
|
535
|
-
═══════════════════════════════════════════════════════════════════════════════
|
|
536
|
-
STEP 2: GENERATE PATCHES (Only after reasoning)
|
|
537
|
-
═══════════════════════════════════════════════════════════════════════════════
|
|
538
|
-
|
|
539
|
-
CRITICAL - FILE SELECTION:
|
|
540
|
-
- You MUST modify the file marked "TARGET COMPONENT" - this is the file you have FULL visibility into
|
|
541
|
-
- Do NOT try to modify other files listed in context - you only see their imports/exports, not full content
|
|
542
|
-
- If the TARGET COMPONENT is not the right file, return clarification_needed
|
|
543
|
-
|
|
544
|
-
COPY EXACTLY from the TARGET COMPONENT section:
|
|
545
|
-
- Your "search" string must match the file CHARACTER-FOR-CHARACTER
|
|
546
|
-
- Include exact indentation and at least 3 lines of context
|
|
547
|
-
- If the element looks FINE in the screenshot, say so
|
|
510
|
+
const VISION_SYSTEM_PROMPT = `You edit code. Return ONLY valid JSON - no explanation, no preamble, no markdown.
|
|
548
511
|
|
|
549
512
|
RULES:
|
|
550
|
-
1.
|
|
551
|
-
2.
|
|
552
|
-
3.
|
|
553
|
-
4.
|
|
554
|
-
5. If visibility issue: prefer explicit colors (text-white, text-black) over semantic tokens
|
|
513
|
+
1. Copy code EXACTLY from the file - character for character
|
|
514
|
+
2. Make the SMALLEST possible change
|
|
515
|
+
3. For color changes, just change the className
|
|
516
|
+
4. Do not restructure or reorganize code
|
|
555
517
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
═══════════════════════════════════════════════════════════════════════════════
|
|
559
|
-
|
|
560
|
-
Return ONLY raw JSON:
|
|
561
|
-
{
|
|
562
|
-
"analysis": {
|
|
563
|
-
"currentState": "what I see in the screenshot",
|
|
564
|
-
"problem": "the actual issue (or 'none - element appears correct')",
|
|
565
|
-
"fixStrategy": "default|hover|both|clarification_needed"
|
|
566
|
-
},
|
|
567
|
-
"reasoning": "my diagnosis and approach",
|
|
568
|
-
"modifications": [{
|
|
569
|
-
"filePath": "path/to/file.tsx",
|
|
570
|
-
"patches": [{
|
|
571
|
-
"search": "EXACT copy from file",
|
|
572
|
-
"replace": "changed code",
|
|
573
|
-
"explanation": "what this changes and why"
|
|
574
|
-
}],
|
|
575
|
-
"previewCSS": "optional CSS for live preview"
|
|
576
|
-
}],
|
|
577
|
-
"aggregatedPreviewCSS": "combined CSS for all changes",
|
|
578
|
-
"explanation": "summary of what was changed"
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
If the element looks correct, return:
|
|
582
|
-
{
|
|
583
|
-
"analysis": { "currentState": "...", "problem": "none", "fixStrategy": "clarification_needed" },
|
|
584
|
-
"modifications": [],
|
|
585
|
-
"explanation": "The element appears visible/correct in the screenshot. Please specify what needs to change."
|
|
586
|
-
}`;
|
|
518
|
+
RESPOND WITH ONLY THIS JSON FORMAT (nothing else):
|
|
519
|
+
{"modifications":[{"filePath":"path","patches":[{"search":"exact code","replace":"changed code"}]}]}`;
|
|
587
520
|
|
|
588
521
|
export async function POST(request: Request) {
|
|
589
522
|
// Only allow in development
|
|
@@ -813,172 +746,92 @@ ${focusedElements.map((el) => `- ${el.name} (${el.type}) at (${el.coordinates.x}
|
|
|
813
746
|
`;
|
|
814
747
|
}
|
|
815
748
|
|
|
816
|
-
// ========== TARGET COMPONENT (
|
|
749
|
+
// ========== TARGET COMPONENT ONLY (with line numbers) ==========
|
|
750
|
+
// CRITICAL: Only include the TARGET file to avoid overwhelming the LLM with noise
|
|
817
751
|
if (recommendedFileContent) {
|
|
818
|
-
// Never truncate the recommended file - AI needs full context to avoid hallucination
|
|
819
752
|
const content = recommendedFileContent.content;
|
|
820
753
|
|
|
754
|
+
// Add line numbers to make it easy for LLM to reference exact code
|
|
755
|
+
const linesWithNumbers = content.split('\n').map((line, i) =>
|
|
756
|
+
`${String(i + 1).padStart(4, ' ')}| ${line}`
|
|
757
|
+
).join('\n');
|
|
758
|
+
|
|
821
759
|
textContent += `═══════════════════════════════════════════════════════════════════════════════
|
|
822
|
-
|
|
760
|
+
FILE TO EDIT: ${recommendedFileContent.path}
|
|
823
761
|
═══════════════════════════════════════════════════════════════════════════════
|
|
824
762
|
|
|
825
|
-
|
|
826
|
-
|
|
763
|
+
IMPORTANT: Copy code EXACTLY as shown below (including line numbers for reference).
|
|
764
|
+
Your "search" string must match the code CHARACTER FOR CHARACTER.
|
|
827
765
|
|
|
828
766
|
\`\`\`tsx
|
|
829
|
-
${
|
|
767
|
+
${linesWithNumbers}
|
|
830
768
|
\`\`\`
|
|
831
769
|
|
|
832
770
|
`;
|
|
833
771
|
usedContext += content.length;
|
|
834
|
-
debugLog("Added TARGET COMPONENT
|
|
772
|
+
debugLog("Added TARGET COMPONENT with line numbers", {
|
|
835
773
|
path: recommendedFileContent.path,
|
|
836
|
-
|
|
837
|
-
|
|
774
|
+
lines: content.split('\n').length,
|
|
775
|
+
size: content.length
|
|
838
776
|
});
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
textContent += `PAGE CONTEXT (wrapper only${recommendedFileContent ? " - DO NOT edit this, edit the TARGET COMPONENT above" : ""}):
|
|
846
|
-
|
|
847
|
-
Page File: ${pageContext.pageFile || "Not found"}
|
|
848
|
-
${pageContext.pageContent ? `\`\`\`tsx\n${pageContentTruncated}${pageWasTruncated ? "\n// ... (wrapper truncated)" : ""}\n\`\`\`` : ""}
|
|
849
|
-
|
|
850
|
-
`;
|
|
851
|
-
usedContext += pageContentTruncated.length;
|
|
852
|
-
|
|
853
|
-
// ========== SUPPORTING COMPONENTS (dynamic budget) ==========
|
|
854
|
-
if (pageContext.componentSources.length > 0) {
|
|
855
|
-
const remainingBudget = TOTAL_CONTEXT_BUDGET - usedContext - MAX_GLOBALS_CSS - 5000; // Reserve 5k for instructions
|
|
856
|
-
const filesToInclude = pageContext.componentSources.slice(0, MAX_FILES);
|
|
857
|
-
const perFileLimit = Math.max(1000, Math.floor(remainingBudget / Math.max(filesToInclude.length, 1)));
|
|
858
|
-
|
|
859
|
-
textContent += `SUPPORTING COMPONENTS (${filesToInclude.length} files, ~${Math.round(perFileLimit/1000)}k chars each):\n`;
|
|
777
|
+
} else if (pageContext.pageContent) {
|
|
778
|
+
// Fallback: use page file if no recommended file
|
|
779
|
+
const content = pageContext.pageContent;
|
|
780
|
+
const linesWithNumbers = content.split('\n').map((line, i) =>
|
|
781
|
+
`${String(i + 1).padStart(4, ' ')}| ${line}`
|
|
782
|
+
).join('\n');
|
|
860
783
|
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
const truncatedContent = comp.content.substring(0, perFileLimit);
|
|
868
|
-
const wasTruncated = comp.content.length > perFileLimit;
|
|
869
|
-
|
|
870
|
-
textContent += `
|
|
871
|
-
File: ${comp.path}
|
|
784
|
+
textContent += `═══════════════════════════════════════════════════════════════════════════════
|
|
785
|
+
FILE TO EDIT: ${pageContext.pageFile}
|
|
786
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
787
|
+
|
|
872
788
|
\`\`\`tsx
|
|
873
|
-
${
|
|
789
|
+
${linesWithNumbers}
|
|
874
790
|
\`\`\`
|
|
791
|
+
|
|
875
792
|
`;
|
|
876
|
-
|
|
877
|
-
}
|
|
793
|
+
usedContext += content.length;
|
|
878
794
|
}
|
|
795
|
+
|
|
796
|
+
// NOTE: We intentionally skip SUPPORTING COMPONENTS to reduce noise
|
|
797
|
+
// The LLM only needs the TARGET file to make accurate edits
|
|
879
798
|
|
|
880
|
-
//
|
|
881
|
-
// Dynamically discover theme tokens from the target codebase
|
|
799
|
+
// Dynamically discover theme tokens (minimal - just for logging)
|
|
882
800
|
const discoveredTheme = await discoverTheme(projectRoot);
|
|
883
|
-
const themeContext = formatThemeForPrompt(discoveredTheme);
|
|
884
|
-
|
|
885
|
-
// Check if this is a visibility-related request
|
|
886
|
-
const isVisibilityRequest = /visible|can't see|cant see|not visible|hidden|color|contrast/i.test(userPrompt);
|
|
887
801
|
|
|
888
802
|
if (discoveredTheme.discoveredFiles.length > 0) {
|
|
889
|
-
// Only include detailed color guidance for visibility requests
|
|
890
|
-
if (isVisibilityRequest) {
|
|
891
|
-
// Extract available text color classes from discovered theme
|
|
892
|
-
const textColorClasses = Object.keys(discoveredTheme.cssVariables)
|
|
893
|
-
.filter(key => key.includes('foreground') || key.includes('text'))
|
|
894
|
-
.map(key => `text-${key.replace('--', '').replace(/-/g, '-')}`)
|
|
895
|
-
.slice(0, 10);
|
|
896
|
-
|
|
897
|
-
textContent += `
|
|
898
|
-
═══════════════════════════════════════════════════════════════════════════════
|
|
899
|
-
COLOR FIX GUIDANCE (for visibility issues only)
|
|
900
|
-
═══════════════════════════════════════════════════════════════════════════════
|
|
901
|
-
|
|
902
|
-
AVAILABLE TEXT COLORS (use these for contrast fixes):
|
|
903
|
-
- text-white (always visible on dark backgrounds)
|
|
904
|
-
- text-black (always visible on light backgrounds)
|
|
905
|
-
- text-foreground (theme default)
|
|
906
|
-
- text-primary-foreground (for bg-primary)
|
|
907
|
-
- text-accent-foreground (for bg-accent - CHECK IF THIS HAS GOOD CONTRAST)
|
|
908
|
-
- text-destructive-foreground (for bg-destructive)
|
|
909
|
-
|
|
910
|
-
SAFE BUTTON PATTERNS:
|
|
911
|
-
- bg-accent text-white (cyan button, guaranteed visible)
|
|
912
|
-
- bg-primary text-white (charcoal button, guaranteed visible)
|
|
913
|
-
- bg-destructive text-white (red button, guaranteed visible)
|
|
914
|
-
- bg-muted text-foreground (gray button, guaranteed visible)
|
|
915
|
-
|
|
916
|
-
IMPORTANT: If current code has text-accent-foreground or similar semantic colors
|
|
917
|
-
that result in poor contrast, REPLACE with text-white or text-black.
|
|
918
|
-
|
|
919
|
-
`;
|
|
920
|
-
} else {
|
|
921
|
-
// For non-visibility requests, minimal reference
|
|
922
|
-
textContent += `
|
|
923
|
-
═══════════════════════════════════════════════════════════════════════════════
|
|
924
|
-
REFERENCE ONLY (do not use this to justify additional changes)
|
|
925
|
-
═══════════════════════════════════════════════════════════════════════════════
|
|
926
|
-
Theme discovered from: ${discoveredTheme.discoveredFiles.join(', ')}
|
|
927
|
-
`;
|
|
928
|
-
}
|
|
929
|
-
|
|
930
803
|
debugLog("Theme discovery complete", {
|
|
931
804
|
filesFound: discoveredTheme.discoveredFiles,
|
|
932
|
-
cssVariableCount: Object.keys(discoveredTheme.cssVariables).length,
|
|
933
|
-
tailwindColorCount: Object.keys(discoveredTheme.tailwindColors).length,
|
|
934
|
-
isVisibilityRequest,
|
|
935
805
|
});
|
|
936
806
|
}
|
|
937
807
|
|
|
938
|
-
// ==========
|
|
939
|
-
const
|
|
940
|
-
textContent += `
|
|
941
|
-
GLOBALS.CSS (theme variables):
|
|
942
|
-
\`\`\`css
|
|
943
|
-
${globalsTruncated}${pageContext.globalsCSS.length > MAX_GLOBALS_CSS ? "\n/* ... (truncated) */" : ""}
|
|
944
|
-
\`\`\`
|
|
945
|
-
|
|
946
|
-
`;
|
|
947
|
-
|
|
948
|
-
// ========== VALID FILES LIST ==========
|
|
949
|
-
const validFilesList: string[] = [];
|
|
950
|
-
const recommendedPath = recommendedFile?.path;
|
|
951
|
-
|
|
952
|
-
// Add recommended file first with marker
|
|
953
|
-
if (recommendedPath) {
|
|
954
|
-
validFilesList.push(`- ${recommendedPath} (*** TARGET - EDIT THIS FILE ***)`);
|
|
955
|
-
}
|
|
808
|
+
// ========== SIMPLIFIED INSTRUCTIONS ==========
|
|
809
|
+
const targetPath = recommendedFileContent?.path || pageContext.pageFile || "unknown";
|
|
956
810
|
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
validFilesList.push(`- ${comp.path}`);
|
|
966
|
-
}
|
|
967
|
-
}
|
|
811
|
+
textContent += `═══════════════════════════════════════════════════════════════════════════════
|
|
812
|
+
HOW TO MAKE YOUR EDIT
|
|
813
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
814
|
+
|
|
815
|
+
1. Find the EXACT code in the file above that needs to change
|
|
816
|
+
2. COPY that code CHARACTER FOR CHARACTER (use the line numbers as reference)
|
|
817
|
+
3. Make your change to the copied code
|
|
818
|
+
4. Return a patch with the exact "search" and "replace" strings
|
|
968
819
|
|
|
969
|
-
|
|
970
|
-
|
|
820
|
+
EXAMPLE - If line 84 shows:
|
|
821
|
+
84| <div className="mb-10 md:mb-16 grid grid-cols-2">
|
|
971
822
|
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
823
|
+
Your patch should be:
|
|
824
|
+
{
|
|
825
|
+
"modifications": [{
|
|
826
|
+
"filePath": "${targetPath}",
|
|
827
|
+
"patches": [{
|
|
828
|
+
"search": "<div className=\\"mb-10 md:mb-16 grid grid-cols-2\\">",
|
|
829
|
+
"replace": "<div className=\\"mb-10 md:mb-16 grid grid-cols-2 bg-accent\\">"
|
|
830
|
+
}]
|
|
831
|
+
}]
|
|
832
|
+
}
|
|
980
833
|
|
|
981
|
-
CRITICAL:
|
|
834
|
+
CRITICAL: Your "search" string MUST exist in the file. If you can't find the exact code, return empty modifications.`;
|
|
982
835
|
|
|
983
836
|
messageContent.push({
|
|
984
837
|
type: "text",
|
|
@@ -1003,8 +856,6 @@ CRITICAL: Edit the TARGET COMPONENT (marked with ***), not the page wrapper.`;
|
|
|
1003
856
|
let lastPatchErrors: string[] = [];
|
|
1004
857
|
let modificationsWithOriginals: VisionFileModification[] = [];
|
|
1005
858
|
let finalExplanation: string | undefined;
|
|
1006
|
-
let finalReasoning: string | undefined;
|
|
1007
|
-
let finalAggregatedCSS: string | undefined;
|
|
1008
859
|
|
|
1009
860
|
while (retryCount <= MAX_RETRIES) {
|
|
1010
861
|
// Build messages for this attempt
|
|
@@ -1054,110 +905,116 @@ This is better than generating patches with made-up code.`,
|
|
|
1054
905
|
});
|
|
1055
906
|
}
|
|
1056
907
|
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
908
|
+
const response = await anthropic.messages.create({
|
|
909
|
+
model: "claude-sonnet-4-20250514",
|
|
910
|
+
max_tokens: 16384,
|
|
1060
911
|
messages: currentMessages,
|
|
1061
|
-
|
|
1062
|
-
|
|
912
|
+
system: VISION_SYSTEM_PROMPT,
|
|
913
|
+
});
|
|
1063
914
|
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
915
|
+
// Extract text content from response
|
|
916
|
+
const textResponse = response.content.find((block) => block.type === "text");
|
|
917
|
+
if (!textResponse || textResponse.type !== "text") {
|
|
918
|
+
return NextResponse.json(
|
|
919
|
+
{ error: "No text response from AI" },
|
|
920
|
+
{ status: 500 }
|
|
921
|
+
);
|
|
922
|
+
}
|
|
1072
923
|
|
|
1073
|
-
// Parse AI response -
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
fixStrategy?: string;
|
|
1079
|
-
};
|
|
1080
|
-
reasoning?: string;
|
|
1081
|
-
modifications: Array<{
|
|
1082
|
-
filePath: string;
|
|
1083
|
-
patches?: Patch[];
|
|
1084
|
-
// Legacy support for modifiedContent (will be deprecated)
|
|
1085
|
-
modifiedContent?: string;
|
|
1086
|
-
explanation?: string;
|
|
1087
|
-
previewCSS?: string;
|
|
1088
|
-
}>;
|
|
1089
|
-
aggregatedPreviewCSS?: string;
|
|
924
|
+
// Parse AI response - expecting modifications array
|
|
925
|
+
let aiResponse: {
|
|
926
|
+
modifications: Array<{
|
|
927
|
+
filePath: string;
|
|
928
|
+
patches?: Patch[];
|
|
1090
929
|
explanation?: string;
|
|
1091
|
-
|
|
930
|
+
previewCSS?: string;
|
|
931
|
+
}>;
|
|
932
|
+
explanation?: string;
|
|
933
|
+
};
|
|
1092
934
|
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
935
|
+
try {
|
|
936
|
+
let jsonText = textResponse.text.trim();
|
|
937
|
+
|
|
938
|
+
// Try to extract JSON from markdown code blocks
|
|
939
|
+
const jsonMatch = jsonText.match(/```json\n([\s\S]*?)\n```/) ||
|
|
940
|
+
jsonText.match(/```\n([\s\S]*?)\n```/);
|
|
941
|
+
|
|
942
|
+
if (jsonMatch) {
|
|
943
|
+
jsonText = jsonMatch[1];
|
|
944
|
+
} else if (jsonText.includes("```json")) {
|
|
945
|
+
// Fallback for cases where regex might miss due to newlines
|
|
946
|
+
const start = jsonText.indexOf("```json") + 7;
|
|
947
|
+
const end = jsonText.lastIndexOf("```");
|
|
948
|
+
if (end > start) {
|
|
949
|
+
jsonText = jsonText.substring(start, end);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
1110
952
|
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
953
|
+
// Clean up any remaining whitespace
|
|
954
|
+
jsonText = jsonText.trim();
|
|
955
|
+
|
|
956
|
+
// Robust JSON extraction: look for {"modifications" pattern specifically
|
|
957
|
+
// This handles cases where the LLM includes preamble text with code blocks
|
|
958
|
+
const jsonStartPatterns = ['{"modifications"', '{ "modifications"', '{\n "modifications"'];
|
|
959
|
+
let jsonStart = -1;
|
|
960
|
+
|
|
961
|
+
for (const pattern of jsonStartPatterns) {
|
|
962
|
+
const idx = jsonText.indexOf(pattern);
|
|
963
|
+
if (idx !== -1 && (jsonStart === -1 || idx < jsonStart)) {
|
|
964
|
+
jsonStart = idx;
|
|
1120
965
|
}
|
|
1121
|
-
|
|
1122
|
-
aiResponse = JSON.parse(jsonText);
|
|
1123
|
-
} catch {
|
|
1124
|
-
console.error("Failed to parse AI response:", textResponse.text);
|
|
1125
|
-
return NextResponse.json(
|
|
1126
|
-
{ error: "Failed to parse AI response. Please try again." },
|
|
1127
|
-
{ status: 500 }
|
|
1128
|
-
);
|
|
1129
966
|
}
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
967
|
+
|
|
968
|
+
if (jsonStart !== -1) {
|
|
969
|
+
// Find the matching closing brace by counting braces
|
|
970
|
+
let braceCount = 0;
|
|
971
|
+
let jsonEnd = -1;
|
|
972
|
+
for (let i = jsonStart; i < jsonText.length; i++) {
|
|
973
|
+
if (jsonText[i] === '{') braceCount++;
|
|
974
|
+
if (jsonText[i] === '}') {
|
|
975
|
+
braceCount--;
|
|
976
|
+
if (braceCount === 0) {
|
|
977
|
+
jsonEnd = i;
|
|
978
|
+
break;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
if (jsonEnd !== -1) {
|
|
983
|
+
jsonText = jsonText.substring(jsonStart, jsonEnd + 1);
|
|
984
|
+
}
|
|
985
|
+
} else {
|
|
986
|
+
// Fallback: try first { to last }
|
|
987
|
+
const firstBrace = jsonText.indexOf('{');
|
|
988
|
+
const lastBrace = jsonText.lastIndexOf('}');
|
|
989
|
+
if (firstBrace !== -1 && lastBrace > firstBrace) {
|
|
990
|
+
jsonText = jsonText.substring(firstBrace, lastBrace + 1);
|
|
991
|
+
}
|
|
1138
992
|
}
|
|
993
|
+
|
|
994
|
+
aiResponse = JSON.parse(jsonText);
|
|
995
|
+
} catch {
|
|
996
|
+
console.error("Failed to parse AI response:", textResponse.text);
|
|
997
|
+
return NextResponse.json(
|
|
998
|
+
{ error: "Failed to parse AI response. Please try again." },
|
|
999
|
+
{ status: 500 }
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
1139
1002
|
|
|
1140
1003
|
finalExplanation = aiResponse.explanation;
|
|
1141
|
-
finalReasoning = aiResponse.reasoning;
|
|
1142
|
-
finalAggregatedCSS = aiResponse.aggregatedPreviewCSS;
|
|
1143
1004
|
|
|
1144
|
-
|
|
1145
|
-
if (aiResponse.analysis?.fixStrategy === "clarification_needed" &&
|
|
1146
|
-
(!aiResponse.modifications || aiResponse.modifications.length === 0)) {
|
|
1005
|
+
if (!aiResponse.modifications || aiResponse.modifications.length === 0) {
|
|
1147
1006
|
return NextResponse.json({
|
|
1148
1007
|
success: true,
|
|
1149
|
-
needsClarification: true,
|
|
1150
|
-
analysis: aiResponse.analysis,
|
|
1151
|
-
explanation: aiResponse.explanation || "The element appears correct. Please specify what needs to change.",
|
|
1152
1008
|
modifications: [],
|
|
1009
|
+
explanation: aiResponse.explanation || "No changes needed.",
|
|
1153
1010
|
});
|
|
1154
|
-
|
|
1011
|
+
}
|
|
1155
1012
|
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1013
|
+
debugLog("VALIDATION: Known file paths from page context", {
|
|
1014
|
+
pageFile: pageContext.pageFile,
|
|
1015
|
+
knownPaths: Array.from(knownPaths),
|
|
1016
|
+
aiRequestedFiles: (aiResponse.modifications || []).map(m => m.filePath)
|
|
1017
|
+
});
|
|
1161
1018
|
|
|
1162
1019
|
// Validate AI response - trust the LLM to identify the correct file
|
|
1163
1020
|
// Only reject paths that are outside the project or don't exist
|
|
@@ -1198,9 +1055,9 @@ This is better than generating patches with made-up code.`,
|
|
|
1198
1055
|
}
|
|
1199
1056
|
}
|
|
1200
1057
|
|
|
1201
|
-
|
|
1058
|
+
// Process modifications - apply patches to get modified content
|
|
1202
1059
|
modificationsWithOriginals = [];
|
|
1203
|
-
|
|
1060
|
+
const patchErrors: string[] = [];
|
|
1204
1061
|
|
|
1205
1062
|
for (const mod of aiResponse.modifications || []) {
|
|
1206
1063
|
const fullPath = path.join(projectRoot, mod.filePath);
|
|
@@ -1278,20 +1135,9 @@ This is better than generating patches with made-up code.`,
|
|
|
1278
1135
|
modifiedContent = patchResult.modifiedContent;
|
|
1279
1136
|
console.log(`[Vision Mode] All ${mod.patches.length} patches applied successfully to ${mod.filePath}`);
|
|
1280
1137
|
}
|
|
1281
|
-
} else if (mod.modifiedContent) {
|
|
1282
|
-
// Legacy: AI returned full file content
|
|
1283
|
-
console.warn(`[Vision Mode] Legacy modifiedContent received for ${mod.filePath} - patch-based format preferred`);
|
|
1284
|
-
modifiedContent = mod.modifiedContent;
|
|
1285
|
-
|
|
1286
|
-
// Validate the modification using legacy validation
|
|
1287
|
-
const validation = validateModification(originalContent, modifiedContent, mod.filePath);
|
|
1288
|
-
if (!validation.valid) {
|
|
1289
|
-
patchErrors.push(`${mod.filePath}: ${validation.error}`);
|
|
1290
|
-
continue;
|
|
1291
|
-
}
|
|
1292
1138
|
} else {
|
|
1293
|
-
// No patches
|
|
1294
|
-
console.warn(`[Vision Mode] No patches
|
|
1139
|
+
// No patches - skip
|
|
1140
|
+
console.warn(`[Vision Mode] No patches for ${mod.filePath}`);
|
|
1295
1141
|
continue;
|
|
1296
1142
|
}
|
|
1297
1143
|
|
|
@@ -1306,7 +1152,7 @@ This is better than generating patches with made-up code.`,
|
|
|
1306
1152
|
}
|
|
1307
1153
|
|
|
1308
1154
|
// If all modifications failed, check if we should retry
|
|
1309
|
-
|
|
1155
|
+
if (patchErrors.length > 0 && modificationsWithOriginals.length === 0) {
|
|
1310
1156
|
if (retryCount < MAX_RETRIES) {
|
|
1311
1157
|
console.warn(`[Vision Mode] All patches failed, retrying (attempt ${retryCount + 1}/${MAX_RETRIES + 1})...`);
|
|
1312
1158
|
debugLog("Retry triggered due to patch failures", {
|
|
@@ -1321,19 +1167,19 @@ This is better than generating patches with made-up code.`,
|
|
|
1321
1167
|
|
|
1322
1168
|
// Exhausted retries, return error
|
|
1323
1169
|
console.error("All AI patches failed after retries:", patchErrors);
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1170
|
+
return NextResponse.json(
|
|
1171
|
+
{
|
|
1172
|
+
success: false,
|
|
1327
1173
|
error: `Patch application failed (after ${retryCount} retry attempts):\n\n${patchErrors.join("\n\n")}`,
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1174
|
+
} as VisionEditResponse,
|
|
1175
|
+
{ status: 400 }
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
1332
1178
|
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1179
|
+
// Log patch errors as warnings if some modifications succeeded
|
|
1180
|
+
if (patchErrors.length > 0) {
|
|
1181
|
+
console.warn("Some patches failed:", patchErrors);
|
|
1182
|
+
}
|
|
1337
1183
|
|
|
1338
1184
|
// Successfully processed at least some modifications - break out of retry loop
|
|
1339
1185
|
break;
|
|
@@ -1348,9 +1194,8 @@ This is better than generating patches with made-up code.`,
|
|
|
1348
1194
|
return NextResponse.json({
|
|
1349
1195
|
success: true,
|
|
1350
1196
|
modifications: modificationsWithOriginals,
|
|
1351
|
-
aggregatedPreviewCSS:
|
|
1197
|
+
aggregatedPreviewCSS: aggregatedCSS,
|
|
1352
1198
|
explanation: finalExplanation,
|
|
1353
|
-
reasoning: finalReasoning,
|
|
1354
1199
|
} as VisionEditResponse);
|
|
1355
1200
|
}
|
|
1356
1201
|
|
|
@@ -2042,13 +1887,13 @@ function getPathAliases(projectRoot: string): Map<string, string> {
|
|
|
2042
1887
|
|
|
2043
1888
|
// Only log parse error once to avoid log spam
|
|
2044
1889
|
if (!cachedParseError) {
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
1890
|
+
const posMatch = errorStr.match(/position (\d+)/);
|
|
1891
|
+
let context = "";
|
|
1892
|
+
if (posMatch) {
|
|
1893
|
+
const pos = parseInt(posMatch[1], 10);
|
|
1894
|
+
const content = fs.readFileSync(tsconfigPath, "utf-8");
|
|
1895
|
+
context = `Near: "${content.substring(Math.max(0, pos - 20), pos + 20)}"`;
|
|
1896
|
+
}
|
|
2052
1897
|
debugLog("[edit] Failed to parse tsconfig.json (will use defaults, logging once)", { error: errorStr, context });
|
|
2053
1898
|
cachedParseError = errorStr;
|
|
2054
1899
|
}
|
|
@@ -2071,14 +1916,14 @@ function getPathAliases(projectRoot: string): Map<string, string> {
|
|
|
2071
1916
|
}
|
|
2072
1917
|
// Only log default alias once (not when using cached error fallback)
|
|
2073
1918
|
if (!cachedParseError) {
|
|
2074
|
-
|
|
1919
|
+
debugLog("[edit] Using default @/ alias", { alias: aliases.get("@/") });
|
|
2075
1920
|
}
|
|
2076
1921
|
}
|
|
2077
1922
|
|
|
2078
1923
|
// Cache aliases if parsed successfully, no tsconfig exists, OR we have a parse error (cache fallback)
|
|
2079
1924
|
if (parsedSuccessfully || !fs.existsSync(tsconfigPath) || cachedParseError) {
|
|
2080
|
-
|
|
2081
|
-
|
|
1925
|
+
cachedPathAliases = aliases;
|
|
1926
|
+
cachedProjectRoot = projectRoot;
|
|
2082
1927
|
}
|
|
2083
1928
|
|
|
2084
1929
|
return aliases;
|
|
@@ -2386,105 +2231,3 @@ function applyPatches(originalContent: string, patches: Patch[]): ApplyPatchesRe
|
|
|
2386
2231
|
failedPatches,
|
|
2387
2232
|
};
|
|
2388
2233
|
}
|
|
2389
|
-
|
|
2390
|
-
/**
|
|
2391
|
-
* Validate that AI modifications are surgical edits, not complete rewrites
|
|
2392
|
-
*/
|
|
2393
|
-
interface ValidationResult {
|
|
2394
|
-
valid: boolean;
|
|
2395
|
-
error?: string;
|
|
2396
|
-
warnings: string[];
|
|
2397
|
-
}
|
|
2398
|
-
|
|
2399
|
-
function validateModification(
|
|
2400
|
-
originalContent: string,
|
|
2401
|
-
modifiedContent: string,
|
|
2402
|
-
filePath: string
|
|
2403
|
-
): ValidationResult {
|
|
2404
|
-
const warnings: string[] = [];
|
|
2405
|
-
|
|
2406
|
-
// Skip validation for new files (no original content)
|
|
2407
|
-
if (!originalContent || originalContent.trim() === "") {
|
|
2408
|
-
return { valid: true, warnings: ["New file - no original to compare"] };
|
|
2409
|
-
}
|
|
2410
|
-
|
|
2411
|
-
const originalLines = originalContent.split("\n");
|
|
2412
|
-
const modifiedLines = modifiedContent.split("\n");
|
|
2413
|
-
|
|
2414
|
-
// Check 1: Truncation detection - look for placeholder comments
|
|
2415
|
-
const truncationPatterns = [
|
|
2416
|
-
/\/\/\s*\.\.\.\s*existing/i,
|
|
2417
|
-
/\/\/\s*\.\.\.\s*rest\s*of/i,
|
|
2418
|
-
/\/\/\s*\.\.\.\s*more\s*code/i,
|
|
2419
|
-
/\/\*\s*\.\.\.\s*\*\//,
|
|
2420
|
-
/\/\/\s*\.\.\./,
|
|
2421
|
-
];
|
|
2422
|
-
|
|
2423
|
-
for (const pattern of truncationPatterns) {
|
|
2424
|
-
if (pattern.test(modifiedContent)) {
|
|
2425
|
-
return {
|
|
2426
|
-
valid: false,
|
|
2427
|
-
error: `File ${filePath} contains truncation placeholder (e.g., "// ... existing code"). The AI must return the complete file content. Please try again.`,
|
|
2428
|
-
warnings,
|
|
2429
|
-
};
|
|
2430
|
-
}
|
|
2431
|
-
}
|
|
2432
|
-
|
|
2433
|
-
// Check 2: Line count shrinkage - reject if file shrinks by more than 30%
|
|
2434
|
-
const lineDelta = modifiedLines.length - originalLines.length;
|
|
2435
|
-
const shrinkagePercent = (lineDelta / originalLines.length) * 100;
|
|
2436
|
-
|
|
2437
|
-
if (shrinkagePercent < -30) {
|
|
2438
|
-
return {
|
|
2439
|
-
valid: false,
|
|
2440
|
-
error: `File ${filePath} shrank from ${originalLines.length} to ${modifiedLines.length} lines (${Math.abs(shrinkagePercent).toFixed(0)}% reduction). This suggests the AI rewrote the file instead of making surgical edits. Please try a more specific request.`,
|
|
2441
|
-
warnings,
|
|
2442
|
-
};
|
|
2443
|
-
}
|
|
2444
|
-
|
|
2445
|
-
if (shrinkagePercent < -15) {
|
|
2446
|
-
warnings.push(`File shrank by ${Math.abs(shrinkagePercent).toFixed(0)}% - review carefully`);
|
|
2447
|
-
}
|
|
2448
|
-
|
|
2449
|
-
// Check 3: Change percentage - warn if too many lines are different
|
|
2450
|
-
let changedLines = 0;
|
|
2451
|
-
const minLines = Math.min(originalLines.length, modifiedLines.length);
|
|
2452
|
-
|
|
2453
|
-
for (let i = 0; i < minLines; i++) {
|
|
2454
|
-
if (originalLines[i] !== modifiedLines[i]) {
|
|
2455
|
-
changedLines++;
|
|
2456
|
-
}
|
|
2457
|
-
}
|
|
2458
|
-
|
|
2459
|
-
// Add lines that were added or removed
|
|
2460
|
-
changedLines += Math.abs(originalLines.length - modifiedLines.length);
|
|
2461
|
-
|
|
2462
|
-
const changePercent = (changedLines / originalLines.length) * 100;
|
|
2463
|
-
|
|
2464
|
-
if (changePercent > 50) {
|
|
2465
|
-
return {
|
|
2466
|
-
valid: false,
|
|
2467
|
-
error: `File ${filePath} has ${changePercent.toFixed(0)}% of lines changed. This suggests the AI rewrote the file instead of making surgical edits. For safety, this change has been rejected. Please try a more specific request.`,
|
|
2468
|
-
warnings,
|
|
2469
|
-
};
|
|
2470
|
-
}
|
|
2471
|
-
|
|
2472
|
-
if (changePercent > 30) {
|
|
2473
|
-
warnings.push(`${changePercent.toFixed(0)}% of lines changed - larger than expected for a surgical edit`);
|
|
2474
|
-
}
|
|
2475
|
-
|
|
2476
|
-
// Check 4: Import preservation - ensure imports aren't removed
|
|
2477
|
-
const importRegex = /^import\s+/gm;
|
|
2478
|
-
const originalImports = (originalContent.match(importRegex) || []).length;
|
|
2479
|
-
const modifiedImports = (modifiedContent.match(importRegex) || []).length;
|
|
2480
|
-
|
|
2481
|
-
if (modifiedImports < originalImports * 0.5 && originalImports > 2) {
|
|
2482
|
-
return {
|
|
2483
|
-
valid: false,
|
|
2484
|
-
error: `File ${filePath} went from ${originalImports} imports to ${modifiedImports}. Imports should not be removed. Please try again.`,
|
|
2485
|
-
warnings,
|
|
2486
|
-
};
|
|
2487
|
-
}
|
|
2488
|
-
|
|
2489
|
-
return { valid: true, warnings };
|
|
2490
|
-
}
|