sonance-brand-mcp 1.3.14 → 1.3.16
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-analyze/route.ts +1 -1
- package/dist/assets/api/sonance-save-logo/route.ts +2 -2
- package/dist/assets/brand-system.ts +4 -1
- package/dist/assets/components/image.tsx +3 -1
- package/dist/assets/components/select.tsx +3 -0
- package/dist/assets/dev-tools/SonanceDevTools.tsx +1837 -3579
- package/dist/assets/dev-tools/components/ApplyFirstPreview.tsx +230 -0
- package/dist/assets/dev-tools/components/ChatInterface.tsx +455 -0
- package/dist/assets/dev-tools/components/DiffPreview.tsx +190 -0
- package/dist/assets/dev-tools/components/InspectorOverlay.tsx +353 -0
- package/dist/assets/dev-tools/components/VisionDiffPreview.tsx +199 -0
- package/dist/assets/dev-tools/components/VisionModeBorder.tsx +116 -0
- package/dist/assets/dev-tools/components/common.tsx +94 -0
- package/dist/assets/dev-tools/constants.ts +616 -0
- package/dist/assets/dev-tools/index.ts +29 -8
- package/dist/assets/dev-tools/panels/AnalysisPanel.tsx +329 -0
- package/dist/assets/dev-tools/panels/ComponentsPanel.tsx +623 -0
- package/dist/assets/dev-tools/panels/LogoToolsPanel.tsx +621 -0
- package/dist/assets/dev-tools/panels/LogosPanel.tsx +16 -0
- package/dist/assets/dev-tools/panels/TextPanel.tsx +332 -0
- package/dist/assets/dev-tools/types.ts +295 -0
- package/dist/assets/dev-tools/utils.ts +360 -0
- package/dist/index.js +278 -3
- package/package.json +1 -1
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState, useEffect } from "react";
|
|
4
|
+
import { X, Zap, Loader2, Check, Undo, ChevronDown, ChevronRight, FileCode, AlertTriangle, RefreshCw, Info } from "lucide-react";
|
|
5
|
+
import { cn } from "../../../lib/utils";
|
|
6
|
+
import { ApplyFirstSession, ApplyFirstStatus, VisionFileModification } from "../types";
|
|
7
|
+
|
|
8
|
+
export interface ApplyFirstPreviewProps {
|
|
9
|
+
session: ApplyFirstSession;
|
|
10
|
+
status: ApplyFirstStatus;
|
|
11
|
+
onAccept: () => void;
|
|
12
|
+
onRevert: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function FileModificationCard({
|
|
16
|
+
modification,
|
|
17
|
+
isExpanded,
|
|
18
|
+
onToggle,
|
|
19
|
+
}: {
|
|
20
|
+
modification: VisionFileModification;
|
|
21
|
+
isExpanded: boolean;
|
|
22
|
+
onToggle: () => void;
|
|
23
|
+
}) {
|
|
24
|
+
return (
|
|
25
|
+
<div className="border border-gray-200 rounded bg-white overflow-hidden">
|
|
26
|
+
{/* File Header */}
|
|
27
|
+
<button
|
|
28
|
+
onClick={onToggle}
|
|
29
|
+
className="w-full flex items-center gap-2 p-2 hover:bg-gray-50 transition-colors text-left"
|
|
30
|
+
>
|
|
31
|
+
{isExpanded ? (
|
|
32
|
+
<ChevronDown className="h-3.5 w-3.5 text-gray-400 flex-shrink-0" />
|
|
33
|
+
) : (
|
|
34
|
+
<ChevronRight className="h-3.5 w-3.5 text-gray-400 flex-shrink-0" />
|
|
35
|
+
)}
|
|
36
|
+
<FileCode className="h-3.5 w-3.5 text-green-500 flex-shrink-0" />
|
|
37
|
+
<span className="text-xs font-mono text-gray-700 truncate flex-1">
|
|
38
|
+
{modification.filePath}
|
|
39
|
+
</span>
|
|
40
|
+
</button>
|
|
41
|
+
|
|
42
|
+
{/* Explanation */}
|
|
43
|
+
<div className="px-2 pb-2">
|
|
44
|
+
<p className="text-[10px] text-gray-500">{modification.explanation}</p>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
{/* Expanded Diff */}
|
|
48
|
+
{isExpanded && (
|
|
49
|
+
<div className="border-t border-gray-100">
|
|
50
|
+
<div className="max-h-60 overflow-auto">
|
|
51
|
+
<pre className="p-2 text-[10px] font-mono whitespace-pre-wrap">
|
|
52
|
+
{modification.diff.split("\n").map((line, i) => (
|
|
53
|
+
<div
|
|
54
|
+
key={i}
|
|
55
|
+
className={cn(
|
|
56
|
+
line.startsWith("+") && !line.startsWith("@@")
|
|
57
|
+
? "bg-green-50 text-green-700"
|
|
58
|
+
: line.startsWith("-") && !line.startsWith("@@")
|
|
59
|
+
? "bg-red-50 text-red-700"
|
|
60
|
+
: line.startsWith("@@")
|
|
61
|
+
? "bg-blue-50 text-blue-600 font-semibold"
|
|
62
|
+
: "text-gray-600"
|
|
63
|
+
)}
|
|
64
|
+
>
|
|
65
|
+
{line || " "}
|
|
66
|
+
</div>
|
|
67
|
+
))}
|
|
68
|
+
</pre>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
)}
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function HMRStatusBadge({ status }: { status: ApplyFirstStatus }) {
|
|
77
|
+
if (status === "waiting-hmr") {
|
|
78
|
+
return (
|
|
79
|
+
<span className="flex items-center gap-1 text-[10px] px-1.5 py-0.5 rounded bg-amber-100 text-amber-700">
|
|
80
|
+
<RefreshCw className="h-3 w-3 animate-spin" />
|
|
81
|
+
Refreshing...
|
|
82
|
+
</span>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (status === "reviewing") {
|
|
87
|
+
return (
|
|
88
|
+
<span className="flex items-center gap-1 text-[10px] px-1.5 py-0.5 rounded bg-green-100 text-green-700">
|
|
89
|
+
<Check className="h-3 w-3" />
|
|
90
|
+
Changes Live
|
|
91
|
+
</span>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function ApplyFirstPreview({
|
|
99
|
+
session,
|
|
100
|
+
status,
|
|
101
|
+
onAccept,
|
|
102
|
+
onRevert,
|
|
103
|
+
}: ApplyFirstPreviewProps) {
|
|
104
|
+
const [expandedFiles, setExpandedFiles] = useState<Set<string>>(new Set());
|
|
105
|
+
|
|
106
|
+
// Expand first file by default
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
if (session.modifications.length > 0) {
|
|
109
|
+
setExpandedFiles(new Set([session.modifications[0].filePath]));
|
|
110
|
+
}
|
|
111
|
+
}, [session]);
|
|
112
|
+
|
|
113
|
+
const toggleFile = (filePath: string) => {
|
|
114
|
+
setExpandedFiles((prev) => {
|
|
115
|
+
const next = new Set(prev);
|
|
116
|
+
if (next.has(filePath)) {
|
|
117
|
+
next.delete(filePath);
|
|
118
|
+
} else {
|
|
119
|
+
next.add(filePath);
|
|
120
|
+
}
|
|
121
|
+
return next;
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const fileCount = session.modifications.length;
|
|
126
|
+
const isLoading = status === "accepting" || status === "reverting";
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<div className="space-y-3 p-3 rounded border border-green-300 bg-green-50">
|
|
130
|
+
{/* Header */}
|
|
131
|
+
<div className="flex items-center justify-between">
|
|
132
|
+
<div className="flex items-center gap-2">
|
|
133
|
+
<Zap className="h-4 w-4 text-green-600" />
|
|
134
|
+
<span className="text-xs font-semibold text-gray-900">
|
|
135
|
+
Changes Applied
|
|
136
|
+
</span>
|
|
137
|
+
<span className="text-[10px] px-1.5 py-0.5 rounded bg-green-200 text-green-700 font-medium">
|
|
138
|
+
{fileCount} file{fileCount !== 1 ? "s" : ""}
|
|
139
|
+
</span>
|
|
140
|
+
</div>
|
|
141
|
+
<HMRStatusBadge status={status} />
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
{/* Info Banner */}
|
|
145
|
+
<div className="flex items-start gap-2 p-2 rounded bg-blue-50 border border-blue-200">
|
|
146
|
+
<Info className="h-3.5 w-3.5 text-blue-600 mt-0.5 flex-shrink-0" />
|
|
147
|
+
<span className="text-xs text-blue-700">
|
|
148
|
+
<strong>Changes are live!</strong> Scroll around to see the actual result.
|
|
149
|
+
Your original files are safely backed up.
|
|
150
|
+
</span>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
{/* File Modifications List */}
|
|
154
|
+
<div className="space-y-2 max-h-80 overflow-y-auto">
|
|
155
|
+
{session.modifications.map((mod) => (
|
|
156
|
+
<FileModificationCard
|
|
157
|
+
key={mod.filePath}
|
|
158
|
+
modification={mod}
|
|
159
|
+
isExpanded={expandedFiles.has(mod.filePath)}
|
|
160
|
+
onToggle={() => toggleFile(mod.filePath)}
|
|
161
|
+
/>
|
|
162
|
+
))}
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
{/* Warning about navigation */}
|
|
166
|
+
<div className="flex items-start gap-2 p-2 rounded bg-amber-50 border border-amber-200">
|
|
167
|
+
<AlertTriangle className="h-3.5 w-3.5 text-amber-600 mt-0.5 flex-shrink-0" />
|
|
168
|
+
<span className="text-xs text-amber-700">
|
|
169
|
+
Navigating away will automatically revert changes. Make sure to accept or revert first.
|
|
170
|
+
</span>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
{/* Action Buttons */}
|
|
174
|
+
<div className="flex gap-2">
|
|
175
|
+
{/* Accept Button */}
|
|
176
|
+
<button
|
|
177
|
+
onClick={onAccept}
|
|
178
|
+
disabled={isLoading}
|
|
179
|
+
className={cn(
|
|
180
|
+
"flex-1 flex items-center justify-center gap-2 py-2.5",
|
|
181
|
+
"text-xs font-medium rounded transition-colors",
|
|
182
|
+
"bg-green-600 text-white hover:bg-green-700",
|
|
183
|
+
"disabled:opacity-50 disabled:cursor-not-allowed"
|
|
184
|
+
)}
|
|
185
|
+
>
|
|
186
|
+
{status === "accepting" ? (
|
|
187
|
+
<>
|
|
188
|
+
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
|
189
|
+
Accepting...
|
|
190
|
+
</>
|
|
191
|
+
) : (
|
|
192
|
+
<>
|
|
193
|
+
<Check className="h-3.5 w-3.5" />
|
|
194
|
+
Keep Changes
|
|
195
|
+
</>
|
|
196
|
+
)}
|
|
197
|
+
</button>
|
|
198
|
+
|
|
199
|
+
{/* Revert Button */}
|
|
200
|
+
<button
|
|
201
|
+
onClick={onRevert}
|
|
202
|
+
disabled={isLoading}
|
|
203
|
+
className={cn(
|
|
204
|
+
"flex-1 flex items-center justify-center gap-2 py-2.5",
|
|
205
|
+
"text-xs font-medium rounded transition-colors",
|
|
206
|
+
"border border-gray-300 text-gray-700 bg-white hover:bg-gray-50",
|
|
207
|
+
"disabled:opacity-50 disabled:cursor-not-allowed"
|
|
208
|
+
)}
|
|
209
|
+
>
|
|
210
|
+
{status === "reverting" ? (
|
|
211
|
+
<>
|
|
212
|
+
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
|
213
|
+
Reverting...
|
|
214
|
+
</>
|
|
215
|
+
) : (
|
|
216
|
+
<>
|
|
217
|
+
<Undo className="h-3.5 w-3.5" />
|
|
218
|
+
Revert
|
|
219
|
+
</>
|
|
220
|
+
)}
|
|
221
|
+
</button>
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
{/* Session Info (for debugging) */}
|
|
225
|
+
<div className="text-[10px] text-gray-400 font-mono">
|
|
226
|
+
Session: {session.sessionId} | Applied: {new Date(session.appliedAt).toLocaleTimeString()}
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState, useEffect, useCallback, useRef } from "react";
|
|
4
|
+
import { Loader2, Send, Sparkles, Eye } from "lucide-react";
|
|
5
|
+
import { cn } from "../../../lib/utils";
|
|
6
|
+
import { ChatMessage, AIEditResult, PendingEdit, VisionFocusedElement, VisionPendingEdit, ApplyFirstSession } from "../types";
|
|
7
|
+
import html2canvas from "html2canvas-pro";
|
|
8
|
+
|
|
9
|
+
// Variant styles captured from the DOM
|
|
10
|
+
export interface VariantStyles {
|
|
11
|
+
backgroundColor: string;
|
|
12
|
+
color: string;
|
|
13
|
+
borderColor: string;
|
|
14
|
+
borderRadius: string;
|
|
15
|
+
borderWidth: string;
|
|
16
|
+
padding: string;
|
|
17
|
+
fontSize: string;
|
|
18
|
+
fontWeight: string;
|
|
19
|
+
boxShadow: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ChatInterfaceProps {
|
|
23
|
+
componentType: string;
|
|
24
|
+
componentName: string;
|
|
25
|
+
onEditComplete: (result: AIEditResult) => void;
|
|
26
|
+
onSaveRequest: (edit: PendingEdit) => void;
|
|
27
|
+
pendingEdit: PendingEdit | null;
|
|
28
|
+
onClearPending: () => void;
|
|
29
|
+
// Variant-scoped editing
|
|
30
|
+
editScope?: "component" | "variant";
|
|
31
|
+
variantId?: string | null;
|
|
32
|
+
variantStyles?: VariantStyles | null;
|
|
33
|
+
// Vision mode props
|
|
34
|
+
visionMode?: boolean;
|
|
35
|
+
visionFocusedElements?: VisionFocusedElement[];
|
|
36
|
+
onVisionEditComplete?: (result: VisionPendingEdit) => void;
|
|
37
|
+
// Apply-first mode - NEW: writes files immediately
|
|
38
|
+
onApplyFirstComplete?: (session: ApplyFirstSession) => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function ChatInterface({
|
|
42
|
+
componentType,
|
|
43
|
+
componentName,
|
|
44
|
+
onEditComplete,
|
|
45
|
+
onSaveRequest,
|
|
46
|
+
pendingEdit,
|
|
47
|
+
onClearPending,
|
|
48
|
+
editScope = "component",
|
|
49
|
+
variantId,
|
|
50
|
+
variantStyles,
|
|
51
|
+
visionMode = false,
|
|
52
|
+
visionFocusedElements = [],
|
|
53
|
+
onVisionEditComplete,
|
|
54
|
+
onApplyFirstComplete,
|
|
55
|
+
}: ChatInterfaceProps) {
|
|
56
|
+
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
|
57
|
+
const [input, setInput] = useState("");
|
|
58
|
+
const [isProcessing, setIsProcessing] = useState(false);
|
|
59
|
+
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
60
|
+
|
|
61
|
+
// Scroll to bottom when messages change
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
64
|
+
}, [messages]);
|
|
65
|
+
|
|
66
|
+
// Dynamically discover component file path via API
|
|
67
|
+
const findComponentFile = useCallback(async (): Promise<string | null> => {
|
|
68
|
+
try {
|
|
69
|
+
const response = await fetch(
|
|
70
|
+
`/api/sonance-find-component?component=${encodeURIComponent(componentType)}`
|
|
71
|
+
);
|
|
72
|
+
const data = await response.json();
|
|
73
|
+
|
|
74
|
+
if (data.found && data.filePath) {
|
|
75
|
+
return data.filePath;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
console.warn(`Component file not found for: ${componentType}`, data);
|
|
79
|
+
return null;
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error("Error finding component file:", error);
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}, [componentType]);
|
|
85
|
+
|
|
86
|
+
// Capture screenshot for vision mode
|
|
87
|
+
const captureScreenshot = useCallback(async (): Promise<string | null> => {
|
|
88
|
+
try {
|
|
89
|
+
const canvas = await html2canvas(document.body, {
|
|
90
|
+
ignoreElements: (element) => {
|
|
91
|
+
// Exclude DevTools overlay and vision mode border
|
|
92
|
+
return (
|
|
93
|
+
element.hasAttribute("data-sonance-devtools") ||
|
|
94
|
+
element.hasAttribute("data-vision-mode-border")
|
|
95
|
+
);
|
|
96
|
+
},
|
|
97
|
+
useCORS: true,
|
|
98
|
+
allowTaint: true,
|
|
99
|
+
scale: 1, // Lower scale for smaller file size
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return canvas.toDataURL("image/png", 0.8);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.error("Failed to capture screenshot:", error);
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}, []);
|
|
108
|
+
|
|
109
|
+
// Handle vision mode edit request
|
|
110
|
+
const handleVisionEdit = async (prompt: string) => {
|
|
111
|
+
// Use Apply-First mode if callback is provided (new Cursor-style workflow)
|
|
112
|
+
const useApplyFirst = !!onApplyFirstComplete;
|
|
113
|
+
|
|
114
|
+
console.log("[Vision Mode] Starting edit request:", {
|
|
115
|
+
prompt,
|
|
116
|
+
focusedElements: visionFocusedElements.length,
|
|
117
|
+
mode: useApplyFirst ? "apply-first" : "preview-first"
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const userMessage: ChatMessage = {
|
|
121
|
+
id: `msg-${Date.now()}`,
|
|
122
|
+
role: "user",
|
|
123
|
+
content: prompt,
|
|
124
|
+
timestamp: new Date(),
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
setMessages((prev) => [...prev, userMessage]);
|
|
128
|
+
setInput("");
|
|
129
|
+
setIsProcessing(true);
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
// Capture screenshot
|
|
133
|
+
console.log("[Vision Mode] Capturing screenshot...");
|
|
134
|
+
const screenshot = await captureScreenshot();
|
|
135
|
+
console.log("[Vision Mode] Screenshot captured:", screenshot ? `${screenshot.length} bytes` : "null");
|
|
136
|
+
|
|
137
|
+
// Choose API endpoint based on mode
|
|
138
|
+
const endpoint = useApplyFirst ? "/api/sonance-vision-apply" : "/api/sonance-vision-edit";
|
|
139
|
+
console.log("[Vision Mode] Sending to API:", endpoint);
|
|
140
|
+
|
|
141
|
+
const response = await fetch(endpoint, {
|
|
142
|
+
method: "POST",
|
|
143
|
+
headers: { "Content-Type": "application/json" },
|
|
144
|
+
body: JSON.stringify({
|
|
145
|
+
action: useApplyFirst ? "apply" : "edit",
|
|
146
|
+
screenshot,
|
|
147
|
+
pageRoute: window.location.pathname,
|
|
148
|
+
userPrompt: prompt,
|
|
149
|
+
focusedElements: visionFocusedElements,
|
|
150
|
+
}),
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const data = await response.json();
|
|
154
|
+
console.log("[Vision Mode] API response:", {
|
|
155
|
+
success: data.success,
|
|
156
|
+
sessionId: data.sessionId,
|
|
157
|
+
modificationsCount: data.modifications?.length || 0,
|
|
158
|
+
hasCss: !!data.aggregatedPreviewCSS,
|
|
159
|
+
error: data.error,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const assistantMessage: ChatMessage = {
|
|
163
|
+
id: `msg-${Date.now()}-response`,
|
|
164
|
+
role: "assistant",
|
|
165
|
+
content: data.success
|
|
166
|
+
? useApplyFirst
|
|
167
|
+
? data.explanation || "Changes applied! Review and accept or revert."
|
|
168
|
+
: data.explanation || "Vision mode changes ready for preview."
|
|
169
|
+
: data.error || "Failed to generate changes.",
|
|
170
|
+
timestamp: new Date(),
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
setMessages((prev) => [...prev, assistantMessage]);
|
|
174
|
+
|
|
175
|
+
if (data.success && data.modifications) {
|
|
176
|
+
if (useApplyFirst && onApplyFirstComplete) {
|
|
177
|
+
// Apply-First mode: files are already written
|
|
178
|
+
console.log("[Apply-First] Calling onApplyFirstComplete with:", {
|
|
179
|
+
sessionId: data.sessionId,
|
|
180
|
+
modifications: data.modifications.map((m: { filePath: string }) => m.filePath),
|
|
181
|
+
});
|
|
182
|
+
onApplyFirstComplete({
|
|
183
|
+
sessionId: data.sessionId,
|
|
184
|
+
modifications: data.modifications,
|
|
185
|
+
appliedAt: Date.now(),
|
|
186
|
+
status: 'applied',
|
|
187
|
+
backupPaths: data.backupPaths || [],
|
|
188
|
+
});
|
|
189
|
+
} else if (onVisionEditComplete) {
|
|
190
|
+
// Preview-First mode (legacy): just preview CSS
|
|
191
|
+
console.log("[Vision Mode] Calling onVisionEditComplete with:", {
|
|
192
|
+
modifications: data.modifications.map((m: { filePath: string }) => m.filePath),
|
|
193
|
+
cssLength: data.aggregatedPreviewCSS?.length || 0,
|
|
194
|
+
});
|
|
195
|
+
onVisionEditComplete({
|
|
196
|
+
modifications: data.modifications,
|
|
197
|
+
aggregatedPreviewCSS: data.aggregatedPreviewCSS || "",
|
|
198
|
+
explanation: data.explanation || "",
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
} else if (!data.success) {
|
|
202
|
+
console.error("[Vision Mode] API returned error:", data.error);
|
|
203
|
+
}
|
|
204
|
+
} catch (error) {
|
|
205
|
+
console.error("[Vision Mode] Request failed:", error);
|
|
206
|
+
const errorMessage: ChatMessage = {
|
|
207
|
+
id: `msg-${Date.now()}-error`,
|
|
208
|
+
role: "assistant",
|
|
209
|
+
content: error instanceof Error ? error.message : "Vision mode error occurred",
|
|
210
|
+
timestamp: new Date(),
|
|
211
|
+
};
|
|
212
|
+
setMessages((prev) => [...prev, errorMessage]);
|
|
213
|
+
} finally {
|
|
214
|
+
setIsProcessing(false);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const handleSend = async (prompt: string) => {
|
|
219
|
+
if (!prompt.trim() || isProcessing) return;
|
|
220
|
+
|
|
221
|
+
// Use vision mode handler if vision mode is active
|
|
222
|
+
if (visionMode) {
|
|
223
|
+
return handleVisionEdit(prompt);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// If no component is selected, intercept the request
|
|
227
|
+
if (componentType === "all") {
|
|
228
|
+
const userMessage: ChatMessage = {
|
|
229
|
+
id: `msg-${Date.now()}`,
|
|
230
|
+
role: "user",
|
|
231
|
+
content: prompt,
|
|
232
|
+
timestamp: new Date(),
|
|
233
|
+
};
|
|
234
|
+
setMessages((prev) => [...prev, userMessage]);
|
|
235
|
+
setInput("");
|
|
236
|
+
|
|
237
|
+
setTimeout(() => {
|
|
238
|
+
const assistantMessage: ChatMessage = {
|
|
239
|
+
id: `msg-${Date.now()}-response`,
|
|
240
|
+
role: "assistant",
|
|
241
|
+
content: "Please select a component using the cursor icon in the header to edit it.",
|
|
242
|
+
timestamp: new Date(),
|
|
243
|
+
};
|
|
244
|
+
setMessages((prev) => [...prev, assistantMessage]);
|
|
245
|
+
}, 300);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const userMessage: ChatMessage = {
|
|
250
|
+
id: `msg-${Date.now()}`,
|
|
251
|
+
role: "user",
|
|
252
|
+
content: prompt,
|
|
253
|
+
timestamp: new Date(),
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
setMessages((prev) => [...prev, userMessage]);
|
|
257
|
+
setInput("");
|
|
258
|
+
setIsProcessing(true);
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
// Dynamically find the component file
|
|
262
|
+
const filePath = await findComponentFile();
|
|
263
|
+
|
|
264
|
+
if (!filePath) {
|
|
265
|
+
throw new Error(`Could not locate component file for "${componentType}". The component may not exist in the expected directories.`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// First, fetch the current component source
|
|
269
|
+
const sourceResponse = await fetch(
|
|
270
|
+
`/api/sonance-component-source?file=${encodeURIComponent(filePath)}`
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
if (!sourceResponse.ok) {
|
|
274
|
+
throw new Error(`Could not read component file: ${filePath}`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const sourceData = await sourceResponse.json();
|
|
278
|
+
|
|
279
|
+
// Then, send to AI for editing
|
|
280
|
+
const editResponse = await fetch("/api/sonance-ai-edit", {
|
|
281
|
+
method: "POST",
|
|
282
|
+
headers: { "Content-Type": "application/json" },
|
|
283
|
+
body: JSON.stringify({
|
|
284
|
+
action: "edit",
|
|
285
|
+
componentType,
|
|
286
|
+
filePath,
|
|
287
|
+
currentCode: sourceData.content,
|
|
288
|
+
userRequest: prompt,
|
|
289
|
+
// Variant-scoped editing context
|
|
290
|
+
editScope,
|
|
291
|
+
variantId: editScope === "variant" ? variantId : undefined,
|
|
292
|
+
variantStyles: editScope === "variant" ? variantStyles : undefined,
|
|
293
|
+
}),
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
const editData = await editResponse.json();
|
|
297
|
+
|
|
298
|
+
const assistantMessage: ChatMessage = {
|
|
299
|
+
id: `msg-${Date.now()}-response`,
|
|
300
|
+
role: "assistant",
|
|
301
|
+
content: editData.success
|
|
302
|
+
? editData.explanation || "Changes ready for preview."
|
|
303
|
+
: editData.error || "Failed to generate changes.",
|
|
304
|
+
timestamp: new Date(),
|
|
305
|
+
editResult: editData,
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
setMessages((prev) => [...prev, assistantMessage]);
|
|
309
|
+
|
|
310
|
+
if (editData.success && editData.modifiedCode) {
|
|
311
|
+
onEditComplete(editData);
|
|
312
|
+
// Set up pending edit for save
|
|
313
|
+
onSaveRequest({
|
|
314
|
+
filePath,
|
|
315
|
+
originalCode: sourceData.content,
|
|
316
|
+
modifiedCode: editData.modifiedCode,
|
|
317
|
+
diff: editData.diff || "",
|
|
318
|
+
explanation: editData.explanation || "",
|
|
319
|
+
// AI-provided CSS for live preview (no parsing needed)
|
|
320
|
+
previewCSS: editData.previewCSS || "",
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
} catch (error) {
|
|
324
|
+
const errorMessage: ChatMessage = {
|
|
325
|
+
id: `msg-${Date.now()}-error`,
|
|
326
|
+
role: "assistant",
|
|
327
|
+
content: error instanceof Error ? error.message : "An error occurred",
|
|
328
|
+
timestamp: new Date(),
|
|
329
|
+
editResult: { success: false, error: String(error) },
|
|
330
|
+
};
|
|
331
|
+
setMessages((prev) => [...prev, errorMessage]);
|
|
332
|
+
} finally {
|
|
333
|
+
setIsProcessing(false);
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
return (
|
|
338
|
+
<div className="space-y-3">
|
|
339
|
+
{/* Vision Mode Banner */}
|
|
340
|
+
{visionMode && (
|
|
341
|
+
<div className="p-2 bg-purple-50 border border-purple-200 rounded-md">
|
|
342
|
+
<div className="flex items-center gap-2 text-purple-700 font-medium text-xs mb-1">
|
|
343
|
+
<Eye className="h-3 w-3" />
|
|
344
|
+
<span>Vision Mode Active</span>
|
|
345
|
+
</div>
|
|
346
|
+
{visionFocusedElements.length > 0 ? (
|
|
347
|
+
<div className="text-purple-600 text-xs">
|
|
348
|
+
{visionFocusedElements.length} element{visionFocusedElements.length !== 1 ? "s" : ""} focused
|
|
349
|
+
</div>
|
|
350
|
+
) : (
|
|
351
|
+
<div className="text-purple-500 text-xs">
|
|
352
|
+
Click elements to focus AI attention, then describe your changes
|
|
353
|
+
</div>
|
|
354
|
+
)}
|
|
355
|
+
</div>
|
|
356
|
+
)}
|
|
357
|
+
|
|
358
|
+
{/* AI Hint - only show when no messages yet and not in vision mode */}
|
|
359
|
+
{messages.length === 0 && componentType !== "all" && !visionMode && (
|
|
360
|
+
<p className="text-xs text-gray-500 italic">
|
|
361
|
+
Describe any styling changes you'd like to make to this component.
|
|
362
|
+
</p>
|
|
363
|
+
)}
|
|
364
|
+
|
|
365
|
+
{/* Chat Messages */}
|
|
366
|
+
{messages.length > 0 && (
|
|
367
|
+
<div className="max-h-48 overflow-y-auto space-y-2 p-2 rounded border border-gray-200 bg-gray-50">
|
|
368
|
+
{messages.map((msg) => (
|
|
369
|
+
<div
|
|
370
|
+
key={msg.id}
|
|
371
|
+
className={cn(
|
|
372
|
+
"text-xs p-2 rounded",
|
|
373
|
+
msg.role === "user"
|
|
374
|
+
? "bg-[#00A3E1]/10 text-gray-800 ml-4"
|
|
375
|
+
: "bg-white border border-gray-200 mr-4"
|
|
376
|
+
)}
|
|
377
|
+
>
|
|
378
|
+
<div className="flex items-start gap-2">
|
|
379
|
+
{msg.role === "assistant" && (
|
|
380
|
+
<Sparkles className="h-3 w-3 text-[#00A3E1] mt-0.5 flex-shrink-0" />
|
|
381
|
+
)}
|
|
382
|
+
<span id="summary-row-span-msgcontent">{msg.content}</span>
|
|
383
|
+
</div>
|
|
384
|
+
</div>
|
|
385
|
+
))}
|
|
386
|
+
<div ref={messagesEndRef} />
|
|
387
|
+
</div>
|
|
388
|
+
)}
|
|
389
|
+
|
|
390
|
+
{/* Input */}
|
|
391
|
+
<div className="flex gap-2">
|
|
392
|
+
<input
|
|
393
|
+
type="text"
|
|
394
|
+
value={input}
|
|
395
|
+
onChange={(e) => setInput(e.target.value)}
|
|
396
|
+
onKeyDown={(e) => {
|
|
397
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
398
|
+
e.preventDefault();
|
|
399
|
+
handleSend(input);
|
|
400
|
+
}
|
|
401
|
+
}}
|
|
402
|
+
placeholder={
|
|
403
|
+
visionMode
|
|
404
|
+
? "Describe what changes you want to make on this page..."
|
|
405
|
+
: componentType === "all"
|
|
406
|
+
? "Select a component to start editing..."
|
|
407
|
+
: `Describe changes to ${componentName}...`
|
|
408
|
+
}
|
|
409
|
+
disabled={isProcessing}
|
|
410
|
+
className={cn(
|
|
411
|
+
"flex-1 px-3 py-2 text-xs rounded border",
|
|
412
|
+
visionMode
|
|
413
|
+
? "border-purple-200 focus:ring-purple-500 focus:border-purple-500"
|
|
414
|
+
: "border-gray-200 focus:ring-[#00A3E1] focus:border-[#00A3E1]",
|
|
415
|
+
"focus:outline-none focus:ring-1",
|
|
416
|
+
"placeholder:text-gray-400",
|
|
417
|
+
"disabled:opacity-50 disabled:bg-gray-50"
|
|
418
|
+
)}
|
|
419
|
+
/>
|
|
420
|
+
<button
|
|
421
|
+
onClick={() => handleSend(input)}
|
|
422
|
+
disabled={isProcessing || !input.trim()}
|
|
423
|
+
className={cn(
|
|
424
|
+
"px-3 py-2 rounded transition-colors",
|
|
425
|
+
visionMode
|
|
426
|
+
? "bg-purple-600 text-white hover:bg-purple-700"
|
|
427
|
+
: "bg-[#00A3E1] text-white hover:bg-[#0090c8]",
|
|
428
|
+
"disabled:opacity-50 disabled:cursor-not-allowed"
|
|
429
|
+
)}
|
|
430
|
+
>
|
|
431
|
+
{isProcessing ? (
|
|
432
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
433
|
+
) : (
|
|
434
|
+
<Send className="h-4 w-4" />
|
|
435
|
+
)}
|
|
436
|
+
</button>
|
|
437
|
+
</div>
|
|
438
|
+
|
|
439
|
+
{/* Processing Indicator */}
|
|
440
|
+
{isProcessing && (
|
|
441
|
+
<div className={cn(
|
|
442
|
+
"flex items-center gap-2 text-xs",
|
|
443
|
+
visionMode ? "text-purple-600" : "text-gray-500"
|
|
444
|
+
)}>
|
|
445
|
+
<Loader2 className="h-3 w-3 animate-spin" />
|
|
446
|
+
<span>
|
|
447
|
+
{visionMode
|
|
448
|
+
? "AI is analyzing the page screenshot and generating changes..."
|
|
449
|
+
: "AI is analyzing and generating changes..."}
|
|
450
|
+
</span>
|
|
451
|
+
</div>
|
|
452
|
+
)}
|
|
453
|
+
</div>
|
|
454
|
+
);
|
|
455
|
+
}
|