sonance-brand-mcp 1.3.60 → 1.3.61
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,82 +507,20 @@ 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. Make ONLY the change requested.
|
|
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
|
-
RESPONSE FORMAT
|
|
558
|
-
═══════════════════════════════════════════════════════════════════════════════
|
|
559
|
-
|
|
560
|
-
Return ONLY raw JSON:
|
|
518
|
+
Return JSON:
|
|
561
519
|
{
|
|
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
520
|
"modifications": [{
|
|
569
|
-
"filePath": "path
|
|
570
|
-
"patches": [{
|
|
571
|
-
|
|
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."
|
|
521
|
+
"filePath": "path",
|
|
522
|
+
"patches": [{ "search": "exact code from file", "replace": "changed code" }]
|
|
523
|
+
}]
|
|
586
524
|
}`;
|
|
587
525
|
|
|
588
526
|
export async function POST(request: Request) {
|
|
@@ -813,172 +751,92 @@ ${focusedElements.map((el) => `- ${el.name} (${el.type}) at (${el.coordinates.x}
|
|
|
813
751
|
`;
|
|
814
752
|
}
|
|
815
753
|
|
|
816
|
-
// ========== TARGET COMPONENT (
|
|
754
|
+
// ========== TARGET COMPONENT ONLY (with line numbers) ==========
|
|
755
|
+
// CRITICAL: Only include the TARGET file to avoid overwhelming the LLM with noise
|
|
817
756
|
if (recommendedFileContent) {
|
|
818
|
-
// Never truncate the recommended file - AI needs full context to avoid hallucination
|
|
819
757
|
const content = recommendedFileContent.content;
|
|
820
758
|
|
|
759
|
+
// Add line numbers to make it easy for LLM to reference exact code
|
|
760
|
+
const linesWithNumbers = content.split('\n').map((line, i) =>
|
|
761
|
+
`${String(i + 1).padStart(4, ' ')}| ${line}`
|
|
762
|
+
).join('\n');
|
|
763
|
+
|
|
821
764
|
textContent += `═══════════════════════════════════════════════════════════════════════════════
|
|
822
|
-
|
|
765
|
+
FILE TO EDIT: ${recommendedFileContent.path}
|
|
823
766
|
═══════════════════════════════════════════════════════════════════════════════
|
|
824
767
|
|
|
825
|
-
|
|
826
|
-
|
|
768
|
+
IMPORTANT: Copy code EXACTLY as shown below (including line numbers for reference).
|
|
769
|
+
Your "search" string must match the code CHARACTER FOR CHARACTER.
|
|
827
770
|
|
|
828
771
|
\`\`\`tsx
|
|
829
|
-
${
|
|
772
|
+
${linesWithNumbers}
|
|
830
773
|
\`\`\`
|
|
831
774
|
|
|
832
775
|
`;
|
|
833
776
|
usedContext += content.length;
|
|
834
|
-
debugLog("Added TARGET COMPONENT
|
|
777
|
+
debugLog("Added TARGET COMPONENT with line numbers", {
|
|
835
778
|
path: recommendedFileContent.path,
|
|
836
|
-
|
|
837
|
-
|
|
779
|
+
lines: content.split('\n').length,
|
|
780
|
+
size: content.length
|
|
838
781
|
});
|
|
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`;
|
|
782
|
+
} else if (pageContext.pageContent) {
|
|
783
|
+
// Fallback: use page file if no recommended file
|
|
784
|
+
const content = pageContext.pageContent;
|
|
785
|
+
const linesWithNumbers = content.split('\n').map((line, i) =>
|
|
786
|
+
`${String(i + 1).padStart(4, ' ')}| ${line}`
|
|
787
|
+
).join('\n');
|
|
860
788
|
|
|
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}
|
|
789
|
+
textContent += `═══════════════════════════════════════════════════════════════════════════════
|
|
790
|
+
FILE TO EDIT: ${pageContext.pageFile}
|
|
791
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
792
|
+
|
|
872
793
|
\`\`\`tsx
|
|
873
|
-
${
|
|
794
|
+
${linesWithNumbers}
|
|
874
795
|
\`\`\`
|
|
796
|
+
|
|
875
797
|
`;
|
|
876
|
-
|
|
877
|
-
}
|
|
798
|
+
usedContext += content.length;
|
|
878
799
|
}
|
|
800
|
+
|
|
801
|
+
// NOTE: We intentionally skip SUPPORTING COMPONENTS to reduce noise
|
|
802
|
+
// The LLM only needs the TARGET file to make accurate edits
|
|
879
803
|
|
|
880
|
-
//
|
|
881
|
-
// Dynamically discover theme tokens from the target codebase
|
|
804
|
+
// Dynamically discover theme tokens (minimal - just for logging)
|
|
882
805
|
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
806
|
|
|
888
807
|
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
808
|
debugLog("Theme discovery complete", {
|
|
931
809
|
filesFound: discoveredTheme.discoveredFiles,
|
|
932
|
-
cssVariableCount: Object.keys(discoveredTheme.cssVariables).length,
|
|
933
|
-
tailwindColorCount: Object.keys(discoveredTheme.tailwindColors).length,
|
|
934
|
-
isVisibilityRequest,
|
|
935
810
|
});
|
|
936
811
|
}
|
|
937
812
|
|
|
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;
|
|
813
|
+
// ========== SIMPLIFIED INSTRUCTIONS ==========
|
|
814
|
+
const targetPath = recommendedFileContent?.path || pageContext.pageFile || "unknown";
|
|
951
815
|
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
// Add page file (if not the recommended file)
|
|
958
|
-
if (pageContext.pageFile && pageContext.pageFile !== recommendedPath) {
|
|
959
|
-
validFilesList.push(`- ${pageContext.pageFile} (wrapper only)`);
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
// Add other component sources (excluding recommended file)
|
|
963
|
-
for (const comp of pageContext.componentSources) {
|
|
964
|
-
if (comp.path !== recommendedPath) {
|
|
965
|
-
validFilesList.push(`- ${comp.path}`);
|
|
966
|
-
}
|
|
967
|
-
}
|
|
816
|
+
textContent += `═══════════════════════════════════════════════════════════════════════════════
|
|
817
|
+
HOW TO MAKE YOUR EDIT
|
|
818
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
968
819
|
|
|
969
|
-
|
|
970
|
-
|
|
820
|
+
1. Find the EXACT code in the file above that needs to change
|
|
821
|
+
2. COPY that code CHARACTER FOR CHARACTER (use the line numbers as reference)
|
|
822
|
+
3. Make your change to the copied code
|
|
823
|
+
4. Return a patch with the exact "search" and "replace" strings
|
|
971
824
|
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
825
|
+
EXAMPLE - If line 84 shows:
|
|
826
|
+
84| <div className="mb-10 md:mb-16 grid grid-cols-2">
|
|
827
|
+
|
|
828
|
+
Your patch should be:
|
|
829
|
+
{
|
|
830
|
+
"modifications": [{
|
|
831
|
+
"filePath": "${targetPath}",
|
|
832
|
+
"patches": [{
|
|
833
|
+
"search": "<div className=\\"mb-10 md:mb-16 grid grid-cols-2\\">",
|
|
834
|
+
"replace": "<div className=\\"mb-10 md:mb-16 grid grid-cols-2 bg-accent\\">"
|
|
835
|
+
}]
|
|
836
|
+
}]
|
|
837
|
+
}
|
|
980
838
|
|
|
981
|
-
CRITICAL:
|
|
839
|
+
CRITICAL: Your "search" string MUST exist in the file. If you can't find the exact code, return empty modifications.`;
|
|
982
840
|
|
|
983
841
|
messageContent.push({
|
|
984
842
|
type: "text",
|
|
@@ -1003,8 +861,6 @@ CRITICAL: Edit the TARGET COMPONENT (marked with ***), not the page wrapper.`;
|
|
|
1003
861
|
let lastPatchErrors: string[] = [];
|
|
1004
862
|
let modificationsWithOriginals: VisionFileModification[] = [];
|
|
1005
863
|
let finalExplanation: string | undefined;
|
|
1006
|
-
let finalReasoning: string | undefined;
|
|
1007
|
-
let finalAggregatedCSS: string | undefined;
|
|
1008
864
|
|
|
1009
865
|
while (retryCount <= MAX_RETRIES) {
|
|
1010
866
|
// Build messages for this attempt
|
|
@@ -1054,110 +910,86 @@ This is better than generating patches with made-up code.`,
|
|
|
1054
910
|
});
|
|
1055
911
|
}
|
|
1056
912
|
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
913
|
+
const response = await anthropic.messages.create({
|
|
914
|
+
model: "claude-sonnet-4-20250514",
|
|
915
|
+
max_tokens: 16384,
|
|
1060
916
|
messages: currentMessages,
|
|
1061
|
-
|
|
1062
|
-
|
|
917
|
+
system: VISION_SYSTEM_PROMPT,
|
|
918
|
+
});
|
|
1063
919
|
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
920
|
+
// Extract text content from response
|
|
921
|
+
const textResponse = response.content.find((block) => block.type === "text");
|
|
922
|
+
if (!textResponse || textResponse.type !== "text") {
|
|
923
|
+
return NextResponse.json(
|
|
924
|
+
{ error: "No text response from AI" },
|
|
925
|
+
{ status: 500 }
|
|
926
|
+
);
|
|
927
|
+
}
|
|
1072
928
|
|
|
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;
|
|
929
|
+
// Parse AI response - expecting modifications array
|
|
930
|
+
let aiResponse: {
|
|
931
|
+
modifications: Array<{
|
|
932
|
+
filePath: string;
|
|
933
|
+
patches?: Patch[];
|
|
1090
934
|
explanation?: string;
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
// Try to extract JSON from markdown code blocks
|
|
1097
|
-
const jsonMatch = jsonText.match(/```json\n([\s\S]*?)\n```/) ||
|
|
1098
|
-
jsonText.match(/```\n([\s\S]*?)\n```/);
|
|
1099
|
-
|
|
1100
|
-
if (jsonMatch) {
|
|
1101
|
-
jsonText = jsonMatch[1];
|
|
1102
|
-
} else if (jsonText.includes("```json")) {
|
|
1103
|
-
// Fallback for cases where regex might miss due to newlines
|
|
1104
|
-
const start = jsonText.indexOf("```json") + 7;
|
|
1105
|
-
const end = jsonText.lastIndexOf("```");
|
|
1106
|
-
if (end > start) {
|
|
1107
|
-
jsonText = jsonText.substring(start, end);
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
935
|
+
previewCSS?: string;
|
|
936
|
+
}>;
|
|
937
|
+
explanation?: string;
|
|
938
|
+
};
|
|
1110
939
|
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
{ status: 500 }
|
|
1128
|
-
);
|
|
940
|
+
try {
|
|
941
|
+
let jsonText = textResponse.text.trim();
|
|
942
|
+
|
|
943
|
+
// Try to extract JSON from markdown code blocks
|
|
944
|
+
const jsonMatch = jsonText.match(/```json\n([\s\S]*?)\n```/) ||
|
|
945
|
+
jsonText.match(/```\n([\s\S]*?)\n```/);
|
|
946
|
+
|
|
947
|
+
if (jsonMatch) {
|
|
948
|
+
jsonText = jsonMatch[1];
|
|
949
|
+
} else if (jsonText.includes("```json")) {
|
|
950
|
+
// Fallback for cases where regex might miss due to newlines
|
|
951
|
+
const start = jsonText.indexOf("```json") + 7;
|
|
952
|
+
const end = jsonText.lastIndexOf("```");
|
|
953
|
+
if (end > start) {
|
|
954
|
+
jsonText = jsonText.substring(start, end);
|
|
955
|
+
}
|
|
1129
956
|
}
|
|
1130
957
|
|
|
1131
|
-
//
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
958
|
+
// Clean up any remaining whitespace
|
|
959
|
+
jsonText = jsonText.trim();
|
|
960
|
+
|
|
961
|
+
// Robust JSON extraction: find the first { and last } to extract JSON object
|
|
962
|
+
// This handles cases where the LLM includes preamble text before the JSON
|
|
963
|
+
const firstBrace = jsonText.indexOf('{');
|
|
964
|
+
const lastBrace = jsonText.lastIndexOf('}');
|
|
965
|
+
if (firstBrace !== -1 && lastBrace > firstBrace) {
|
|
966
|
+
jsonText = jsonText.substring(firstBrace, lastBrace + 1);
|
|
1138
967
|
}
|
|
968
|
+
|
|
969
|
+
aiResponse = JSON.parse(jsonText);
|
|
970
|
+
} catch {
|
|
971
|
+
console.error("Failed to parse AI response:", textResponse.text);
|
|
972
|
+
return NextResponse.json(
|
|
973
|
+
{ error: "Failed to parse AI response. Please try again." },
|
|
974
|
+
{ status: 500 }
|
|
975
|
+
);
|
|
976
|
+
}
|
|
1139
977
|
|
|
1140
978
|
finalExplanation = aiResponse.explanation;
|
|
1141
|
-
finalReasoning = aiResponse.reasoning;
|
|
1142
|
-
finalAggregatedCSS = aiResponse.aggregatedPreviewCSS;
|
|
1143
979
|
|
|
1144
|
-
|
|
1145
|
-
if (aiResponse.analysis?.fixStrategy === "clarification_needed" &&
|
|
1146
|
-
(!aiResponse.modifications || aiResponse.modifications.length === 0)) {
|
|
980
|
+
if (!aiResponse.modifications || aiResponse.modifications.length === 0) {
|
|
1147
981
|
return NextResponse.json({
|
|
1148
982
|
success: true,
|
|
1149
|
-
needsClarification: true,
|
|
1150
|
-
analysis: aiResponse.analysis,
|
|
1151
|
-
explanation: aiResponse.explanation || "The element appears correct. Please specify what needs to change.",
|
|
1152
983
|
modifications: [],
|
|
984
|
+
explanation: aiResponse.explanation || "No changes needed.",
|
|
1153
985
|
});
|
|
1154
|
-
|
|
986
|
+
}
|
|
1155
987
|
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
988
|
+
debugLog("VALIDATION: Known file paths from page context", {
|
|
989
|
+
pageFile: pageContext.pageFile,
|
|
990
|
+
knownPaths: Array.from(knownPaths),
|
|
991
|
+
aiRequestedFiles: (aiResponse.modifications || []).map(m => m.filePath)
|
|
992
|
+
});
|
|
1161
993
|
|
|
1162
994
|
// Validate AI response - trust the LLM to identify the correct file
|
|
1163
995
|
// Only reject paths that are outside the project or don't exist
|
|
@@ -1198,9 +1030,9 @@ This is better than generating patches with made-up code.`,
|
|
|
1198
1030
|
}
|
|
1199
1031
|
}
|
|
1200
1032
|
|
|
1201
|
-
|
|
1033
|
+
// Process modifications - apply patches to get modified content
|
|
1202
1034
|
modificationsWithOriginals = [];
|
|
1203
|
-
|
|
1035
|
+
const patchErrors: string[] = [];
|
|
1204
1036
|
|
|
1205
1037
|
for (const mod of aiResponse.modifications || []) {
|
|
1206
1038
|
const fullPath = path.join(projectRoot, mod.filePath);
|
|
@@ -1278,20 +1110,9 @@ This is better than generating patches with made-up code.`,
|
|
|
1278
1110
|
modifiedContent = patchResult.modifiedContent;
|
|
1279
1111
|
console.log(`[Vision Mode] All ${mod.patches.length} patches applied successfully to ${mod.filePath}`);
|
|
1280
1112
|
}
|
|
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
1113
|
} else {
|
|
1293
|
-
// No patches
|
|
1294
|
-
console.warn(`[Vision Mode] No patches
|
|
1114
|
+
// No patches - skip
|
|
1115
|
+
console.warn(`[Vision Mode] No patches for ${mod.filePath}`);
|
|
1295
1116
|
continue;
|
|
1296
1117
|
}
|
|
1297
1118
|
|
|
@@ -1306,7 +1127,7 @@ This is better than generating patches with made-up code.`,
|
|
|
1306
1127
|
}
|
|
1307
1128
|
|
|
1308
1129
|
// If all modifications failed, check if we should retry
|
|
1309
|
-
|
|
1130
|
+
if (patchErrors.length > 0 && modificationsWithOriginals.length === 0) {
|
|
1310
1131
|
if (retryCount < MAX_RETRIES) {
|
|
1311
1132
|
console.warn(`[Vision Mode] All patches failed, retrying (attempt ${retryCount + 1}/${MAX_RETRIES + 1})...`);
|
|
1312
1133
|
debugLog("Retry triggered due to patch failures", {
|
|
@@ -1321,19 +1142,19 @@ This is better than generating patches with made-up code.`,
|
|
|
1321
1142
|
|
|
1322
1143
|
// Exhausted retries, return error
|
|
1323
1144
|
console.error("All AI patches failed after retries:", patchErrors);
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1145
|
+
return NextResponse.json(
|
|
1146
|
+
{
|
|
1147
|
+
success: false,
|
|
1327
1148
|
error: `Patch application failed (after ${retryCount} retry attempts):\n\n${patchErrors.join("\n\n")}`,
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1149
|
+
} as VisionEditResponse,
|
|
1150
|
+
{ status: 400 }
|
|
1151
|
+
);
|
|
1152
|
+
}
|
|
1332
1153
|
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1154
|
+
// Log patch errors as warnings if some modifications succeeded
|
|
1155
|
+
if (patchErrors.length > 0) {
|
|
1156
|
+
console.warn("Some patches failed:", patchErrors);
|
|
1157
|
+
}
|
|
1337
1158
|
|
|
1338
1159
|
// Successfully processed at least some modifications - break out of retry loop
|
|
1339
1160
|
break;
|
|
@@ -1348,9 +1169,8 @@ This is better than generating patches with made-up code.`,
|
|
|
1348
1169
|
return NextResponse.json({
|
|
1349
1170
|
success: true,
|
|
1350
1171
|
modifications: modificationsWithOriginals,
|
|
1351
|
-
aggregatedPreviewCSS:
|
|
1172
|
+
aggregatedPreviewCSS: aggregatedCSS,
|
|
1352
1173
|
explanation: finalExplanation,
|
|
1353
|
-
reasoning: finalReasoning,
|
|
1354
1174
|
} as VisionEditResponse);
|
|
1355
1175
|
}
|
|
1356
1176
|
|
|
@@ -2042,13 +1862,13 @@ function getPathAliases(projectRoot: string): Map<string, string> {
|
|
|
2042
1862
|
|
|
2043
1863
|
// Only log parse error once to avoid log spam
|
|
2044
1864
|
if (!cachedParseError) {
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
1865
|
+
const posMatch = errorStr.match(/position (\d+)/);
|
|
1866
|
+
let context = "";
|
|
1867
|
+
if (posMatch) {
|
|
1868
|
+
const pos = parseInt(posMatch[1], 10);
|
|
1869
|
+
const content = fs.readFileSync(tsconfigPath, "utf-8");
|
|
1870
|
+
context = `Near: "${content.substring(Math.max(0, pos - 20), pos + 20)}"`;
|
|
1871
|
+
}
|
|
2052
1872
|
debugLog("[edit] Failed to parse tsconfig.json (will use defaults, logging once)", { error: errorStr, context });
|
|
2053
1873
|
cachedParseError = errorStr;
|
|
2054
1874
|
}
|
|
@@ -2071,14 +1891,14 @@ function getPathAliases(projectRoot: string): Map<string, string> {
|
|
|
2071
1891
|
}
|
|
2072
1892
|
// Only log default alias once (not when using cached error fallback)
|
|
2073
1893
|
if (!cachedParseError) {
|
|
2074
|
-
|
|
1894
|
+
debugLog("[edit] Using default @/ alias", { alias: aliases.get("@/") });
|
|
2075
1895
|
}
|
|
2076
1896
|
}
|
|
2077
1897
|
|
|
2078
1898
|
// Cache aliases if parsed successfully, no tsconfig exists, OR we have a parse error (cache fallback)
|
|
2079
1899
|
if (parsedSuccessfully || !fs.existsSync(tsconfigPath) || cachedParseError) {
|
|
2080
|
-
|
|
2081
|
-
|
|
1900
|
+
cachedPathAliases = aliases;
|
|
1901
|
+
cachedProjectRoot = projectRoot;
|
|
2082
1902
|
}
|
|
2083
1903
|
|
|
2084
1904
|
return aliases;
|
|
@@ -2386,105 +2206,3 @@ function applyPatches(originalContent: string, patches: Patch[]): ApplyPatchesRe
|
|
|
2386
2206
|
failedPatches,
|
|
2387
2207
|
};
|
|
2388
2208
|
}
|
|
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
|
-
}
|