sonance-brand-mcp 1.3.110 → 1.3.112
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-ai-edit/route.ts +30 -7
- 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 +1020 -64
- 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/api/sonance-vision-edit/route.ts +33 -8
- 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 +851 -708
- package/dist/assets/dev-tools/components/ApplyFirstPreview.tsx +10 -10
- package/dist/assets/dev-tools/components/ChatHistory.tsx +145 -0
- package/dist/assets/dev-tools/components/ChatInterface.tsx +444 -295
- package/dist/assets/dev-tools/components/ChatTabBar.tsx +82 -0
- package/dist/assets/dev-tools/components/DiffPreview.tsx +1 -1
- package/dist/assets/dev-tools/components/InlineDiffPreview.tsx +528 -0
- package/dist/assets/dev-tools/components/InspectorOverlay.tsx +21 -18
- package/dist/assets/dev-tools/components/PropertiesPanel.tsx +1345 -0
- 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 +12 -63
- package/dist/assets/dev-tools/constants.ts +38 -6
- 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 +471 -0
- 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/index.ts +3 -0
- package/dist/assets/dev-tools/panels/AnalysisPanel.tsx +32 -32
- package/dist/assets/dev-tools/panels/ComponentsPanel.tsx +384 -131
- package/dist/assets/dev-tools/panels/TextPanel.tsx +10 -10
- package/dist/assets/dev-tools/types.ts +93 -2
- 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 +22 -3
- package/package.json +2 -1
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import React, { useState, useEffect, useCallback, useRef } from "react";
|
|
4
|
-
import { Loader2, Send, Sparkles, Eye, AlertCircle, X, Crop } from "lucide-react";
|
|
3
|
+
import React, { useState, useEffect, useCallback, useRef, useMemo } from "react";
|
|
4
|
+
import { Loader2, Send, Sparkles, Eye, AlertCircle, X, Crop, User, Bot } from "lucide-react";
|
|
5
5
|
import { cn } from "../../../lib/utils";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
ChatMessage,
|
|
8
|
+
ChatSession,
|
|
9
|
+
AIEditResult,
|
|
10
|
+
PendingEdit,
|
|
11
|
+
VisionFocusedElement,
|
|
12
|
+
VisionPendingEdit,
|
|
13
|
+
ApplyFirstSession
|
|
14
|
+
} from "../types";
|
|
7
15
|
import html2canvas from "html2canvas-pro";
|
|
8
16
|
import { ScreenshotAnnotator, Rectangle } from "./ScreenshotAnnotator";
|
|
17
|
+
import { ChatTabBar } from "./ChatTabBar";
|
|
18
|
+
import { ChatHistory } from "./ChatHistory";
|
|
9
19
|
|
|
10
20
|
// Helper to detect location failure in explanation
|
|
11
21
|
function isLocationFailure(explanation: string | undefined): boolean {
|
|
@@ -22,7 +32,6 @@ function isLocationFailure(explanation: string | undefined): boolean {
|
|
|
22
32
|
|
|
23
33
|
/**
|
|
24
34
|
* Draw a section highlight border on a screenshot image
|
|
25
|
-
* This helps the LLM visually identify the target section for modifications
|
|
26
35
|
*/
|
|
27
36
|
function drawSectionHighlight(
|
|
28
37
|
screenshotDataUrl: string,
|
|
@@ -36,16 +45,13 @@ function drawSectionHighlight(
|
|
|
36
45
|
canvas.height = img.height;
|
|
37
46
|
const ctx = canvas.getContext('2d')!;
|
|
38
47
|
|
|
39
|
-
// Draw original screenshot
|
|
40
48
|
ctx.drawImage(img, 0, 0);
|
|
41
49
|
|
|
42
|
-
// Draw section highlight border (teal/cyan to match Sonance brand)
|
|
43
50
|
ctx.strokeStyle = '#00D3C8';
|
|
44
51
|
ctx.lineWidth = 3;
|
|
45
|
-
ctx.setLineDash([8, 4]);
|
|
52
|
+
ctx.setLineDash([8, 4]);
|
|
46
53
|
ctx.strokeRect(sectionCoords.x, sectionCoords.y, sectionCoords.width, sectionCoords.height);
|
|
47
54
|
|
|
48
|
-
// Semi-transparent fill to subtly highlight the area
|
|
49
55
|
ctx.fillStyle = 'rgba(0, 211, 200, 0.08)';
|
|
50
56
|
ctx.fillRect(sectionCoords.x, sectionCoords.y, sectionCoords.width, sectionCoords.height);
|
|
51
57
|
|
|
@@ -75,18 +81,26 @@ export interface ChatInterfaceProps {
|
|
|
75
81
|
onSaveRequest: (edit: PendingEdit) => void;
|
|
76
82
|
pendingEdit: PendingEdit | null;
|
|
77
83
|
onClearPending: () => void;
|
|
78
|
-
// Variant-scoped editing
|
|
79
84
|
editScope?: "component" | "variant";
|
|
80
85
|
variantId?: string | null;
|
|
81
86
|
variantStyles?: VariantStyles | null;
|
|
82
|
-
// Vision mode props
|
|
83
87
|
visionMode?: boolean;
|
|
84
88
|
visionFocusedElements?: VisionFocusedElement[];
|
|
85
89
|
onVisionEditComplete?: (result: VisionPendingEdit) => void;
|
|
86
|
-
// Apply-first mode - NEW: writes files immediately
|
|
87
90
|
onApplyFirstComplete?: (session: ApplyFirstSession) => void;
|
|
91
|
+
// Apply-first session state for inline display in chat
|
|
92
|
+
applyFirstSession?: ApplyFirstSession | null;
|
|
93
|
+
applyFirstStatus?: "idle" | "generating" | "previewing" | "applying" | "waiting-hmr" | "reviewing" | "accepting" | "reverting" | "error";
|
|
94
|
+
onApplyFirstAccept?: () => Promise<void>;
|
|
95
|
+
onApplyFirstRevert?: () => Promise<void>;
|
|
88
96
|
}
|
|
89
97
|
|
|
98
|
+
// Helper to generate a unique session ID
|
|
99
|
+
const generateSessionId = () => `session-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
|
100
|
+
|
|
101
|
+
// Local storage key for sessions
|
|
102
|
+
const SESSIONS_STORAGE_KEY = 'sonance-devtools-chat-sessions';
|
|
103
|
+
|
|
90
104
|
export function ChatInterface({
|
|
91
105
|
componentType,
|
|
92
106
|
componentName,
|
|
@@ -101,21 +115,91 @@ export function ChatInterface({
|
|
|
101
115
|
visionFocusedElements = [],
|
|
102
116
|
onVisionEditComplete,
|
|
103
117
|
onApplyFirstComplete,
|
|
118
|
+
applyFirstSession,
|
|
119
|
+
applyFirstStatus = "idle",
|
|
120
|
+
onApplyFirstAccept,
|
|
121
|
+
onApplyFirstRevert,
|
|
104
122
|
}: ChatInterfaceProps) {
|
|
105
|
-
|
|
106
|
-
const [
|
|
123
|
+
// Session management
|
|
124
|
+
const [sessions, setSessions] = useState<ChatSession[]>([]);
|
|
125
|
+
const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
|
|
126
|
+
const [sessionsInitialized, setSessionsInitialized] = useState(false);
|
|
127
|
+
|
|
128
|
+
// Processing state
|
|
107
129
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
130
|
+
const [input, setInput] = useState("");
|
|
108
131
|
const [toastMessage, setToastMessage] = useState<{ message: string; type: 'error' | 'warning' } | null>(null);
|
|
109
|
-
|
|
132
|
+
|
|
110
133
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
111
134
|
|
|
112
135
|
// Screenshot annotation state
|
|
113
136
|
const [isAnnotating, setIsAnnotating] = useState(false);
|
|
114
137
|
const [annotatedScreenshot, setAnnotatedScreenshot] = useState<string | null>(null);
|
|
115
138
|
const [manualFocusBounds, setManualFocusBounds] = useState<Rectangle | null>(null);
|
|
116
|
-
// Discovered elements from annotation tool (for targeting when no element was clicked)
|
|
117
139
|
const [annotationDiscoveredElements, setAnnotationDiscoveredElements] = useState<VisionFocusedElement[]>([]);
|
|
118
140
|
|
|
141
|
+
// Get current session and messages
|
|
142
|
+
const activeSession = useMemo(() =>
|
|
143
|
+
sessions.find(s => s.id === activeSessionId) || null,
|
|
144
|
+
[sessions, activeSessionId]
|
|
145
|
+
);
|
|
146
|
+
const messages = activeSession?.messages || [];
|
|
147
|
+
|
|
148
|
+
// Initialize sessions from localStorage on mount
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
try {
|
|
151
|
+
const stored = localStorage.getItem(SESSIONS_STORAGE_KEY);
|
|
152
|
+
if (stored) {
|
|
153
|
+
const parsed = JSON.parse(stored);
|
|
154
|
+
// Convert date strings back to Date objects
|
|
155
|
+
const hydratedSessions: ChatSession[] = parsed.map((s: ChatSession) => ({
|
|
156
|
+
...s,
|
|
157
|
+
createdAt: new Date(s.createdAt),
|
|
158
|
+
updatedAt: new Date(s.updatedAt),
|
|
159
|
+
messages: s.messages.map((m: ChatMessage) => ({
|
|
160
|
+
...m,
|
|
161
|
+
timestamp: new Date(m.timestamp),
|
|
162
|
+
})),
|
|
163
|
+
}));
|
|
164
|
+
setSessions(hydratedSessions);
|
|
165
|
+
if (hydratedSessions.length > 0) {
|
|
166
|
+
setActiveSessionId(hydratedSessions[0].id);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
} catch (e) {
|
|
170
|
+
console.warn('Failed to load chat sessions from localStorage:', e);
|
|
171
|
+
}
|
|
172
|
+
// Mark as initialized regardless of whether we found sessions
|
|
173
|
+
setSessionsInitialized(true);
|
|
174
|
+
}, []);
|
|
175
|
+
|
|
176
|
+
// Create initial session if none exist (only after initialization is complete)
|
|
177
|
+
useEffect(() => {
|
|
178
|
+
if (sessionsInitialized && sessions.length === 0) {
|
|
179
|
+
const initialSession: ChatSession = {
|
|
180
|
+
id: generateSessionId(),
|
|
181
|
+
name: "New Chat",
|
|
182
|
+
messages: [],
|
|
183
|
+
createdAt: new Date(),
|
|
184
|
+
updatedAt: new Date(),
|
|
185
|
+
context: { visionMode },
|
|
186
|
+
};
|
|
187
|
+
setSessions([initialSession]);
|
|
188
|
+
setActiveSessionId(initialSession.id);
|
|
189
|
+
}
|
|
190
|
+
}, [sessionsInitialized, sessions.length, visionMode]);
|
|
191
|
+
|
|
192
|
+
// Persist sessions to localStorage
|
|
193
|
+
useEffect(() => {
|
|
194
|
+
if (sessions.length > 0) {
|
|
195
|
+
try {
|
|
196
|
+
localStorage.setItem(SESSIONS_STORAGE_KEY, JSON.stringify(sessions));
|
|
197
|
+
} catch (e) {
|
|
198
|
+
console.warn('Failed to save chat sessions to localStorage:', e);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}, [sessions]);
|
|
202
|
+
|
|
119
203
|
// Auto-dismiss toast after 5 seconds
|
|
120
204
|
useEffect(() => {
|
|
121
205
|
if (toastMessage) {
|
|
@@ -124,10 +208,63 @@ export function ChatInterface({
|
|
|
124
208
|
}
|
|
125
209
|
}, [toastMessage]);
|
|
126
210
|
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
211
|
+
// Session management handlers
|
|
212
|
+
const createSession = useCallback(() => {
|
|
213
|
+
const newSession: ChatSession = {
|
|
214
|
+
id: generateSessionId(),
|
|
215
|
+
name: "New Chat",
|
|
216
|
+
messages: [],
|
|
217
|
+
createdAt: new Date(),
|
|
218
|
+
updatedAt: new Date(),
|
|
219
|
+
context: { visionMode },
|
|
220
|
+
};
|
|
221
|
+
setSessions(prev => [newSession, ...prev]);
|
|
222
|
+
setActiveSessionId(newSession.id);
|
|
223
|
+
}, [visionMode]);
|
|
224
|
+
|
|
225
|
+
const closeSession = useCallback((sessionId: string) => {
|
|
226
|
+
setSessions(prev => {
|
|
227
|
+
const filtered = prev.filter(s => s.id !== sessionId);
|
|
228
|
+
// If closing active session, switch to first remaining
|
|
229
|
+
if (activeSessionId === sessionId && filtered.length > 0) {
|
|
230
|
+
setActiveSessionId(filtered[0].id);
|
|
231
|
+
}
|
|
232
|
+
return filtered;
|
|
233
|
+
});
|
|
234
|
+
}, [activeSessionId]);
|
|
235
|
+
|
|
236
|
+
const addMessage = useCallback((message: ChatMessage) => {
|
|
237
|
+
setSessions(prev => prev.map(s =>
|
|
238
|
+
s.id === activeSessionId
|
|
239
|
+
? {
|
|
240
|
+
...s,
|
|
241
|
+
messages: [...s.messages, message],
|
|
242
|
+
updatedAt: new Date(),
|
|
243
|
+
// Update session name based on first user message
|
|
244
|
+
name: s.name === "New Chat" && message.role === "user"
|
|
245
|
+
? message.content.slice(0, 30) + (message.content.length > 30 ? "..." : "")
|
|
246
|
+
: s.name,
|
|
247
|
+
}
|
|
248
|
+
: s
|
|
249
|
+
));
|
|
250
|
+
}, [activeSessionId]);
|
|
251
|
+
|
|
252
|
+
// Update a message's action status (for accept/revert)
|
|
253
|
+
const updateMessageAction = useCallback((messageId: string, status: "pending" | "accepted" | "reverted" | "error") => {
|
|
254
|
+
setSessions(prev => prev.map(s =>
|
|
255
|
+
s.id === activeSessionId
|
|
256
|
+
? {
|
|
257
|
+
...s,
|
|
258
|
+
messages: s.messages.map(m =>
|
|
259
|
+
m.id === messageId && m.action
|
|
260
|
+
? { ...m, action: { ...m.action, status } }
|
|
261
|
+
: m
|
|
262
|
+
),
|
|
263
|
+
updatedAt: new Date(),
|
|
264
|
+
}
|
|
265
|
+
: s
|
|
266
|
+
));
|
|
267
|
+
}, [activeSessionId]);
|
|
131
268
|
|
|
132
269
|
// Dynamically discover component file path via API
|
|
133
270
|
const findComponentFile = useCallback(async (): Promise<string | null> => {
|
|
@@ -154,7 +291,6 @@ export function ChatInterface({
|
|
|
154
291
|
try {
|
|
155
292
|
const canvas = await html2canvas(document.body, {
|
|
156
293
|
ignoreElements: (element) => {
|
|
157
|
-
// Exclude DevTools overlay and vision mode border
|
|
158
294
|
return (
|
|
159
295
|
element.hasAttribute("data-sonance-devtools") ||
|
|
160
296
|
element.hasAttribute("data-vision-mode-border")
|
|
@@ -162,7 +298,7 @@ export function ChatInterface({
|
|
|
162
298
|
},
|
|
163
299
|
useCORS: true,
|
|
164
300
|
allowTaint: true,
|
|
165
|
-
scale: 1,
|
|
301
|
+
scale: 1,
|
|
166
302
|
});
|
|
167
303
|
|
|
168
304
|
return canvas.toDataURL("image/png", 0.8);
|
|
@@ -172,38 +308,26 @@ export function ChatInterface({
|
|
|
172
308
|
}
|
|
173
309
|
}, []);
|
|
174
310
|
|
|
175
|
-
// Start screenshot annotation
|
|
311
|
+
// Start screenshot annotation
|
|
176
312
|
const startAnnotation = useCallback(() => {
|
|
177
313
|
console.log("[Vision Mode] Starting screenshot annotation overlay...");
|
|
178
314
|
setIsAnnotating(true);
|
|
179
315
|
}, []);
|
|
180
316
|
|
|
181
|
-
// Handle annotation confirmation
|
|
182
|
-
// Now also receives discovered elements from within the drawn rectangle
|
|
317
|
+
// Handle annotation confirmation
|
|
183
318
|
const handleAnnotationConfirm = useCallback((annotated: string, bounds: Rectangle, discoveredElements: VisionFocusedElement[]) => {
|
|
184
|
-
console.log("[Vision Mode] Annotation confirmed:", {
|
|
185
|
-
bounds,
|
|
186
|
-
discoveredElementsCount: discoveredElements.length,
|
|
187
|
-
discoveredElements: discoveredElements.map(e => ({
|
|
188
|
-
name: e.name,
|
|
189
|
-
text: e.textContent?.substring(0, 30),
|
|
190
|
-
id: e.elementId,
|
|
191
|
-
})),
|
|
192
|
-
});
|
|
319
|
+
console.log("[Vision Mode] Annotation confirmed:", { bounds, discoveredElementsCount: discoveredElements.length });
|
|
193
320
|
setAnnotatedScreenshot(annotated);
|
|
194
321
|
setManualFocusBounds(bounds);
|
|
195
322
|
setAnnotationDiscoveredElements(discoveredElements);
|
|
196
323
|
setIsAnnotating(false);
|
|
197
|
-
// Focus the input so user can type their prompt
|
|
198
324
|
setTimeout(() => inputRef.current?.focus(), 100);
|
|
199
325
|
}, []);
|
|
200
326
|
|
|
201
|
-
// Handle annotation cancel
|
|
202
327
|
const handleAnnotationCancel = useCallback(() => {
|
|
203
328
|
setIsAnnotating(false);
|
|
204
329
|
}, []);
|
|
205
330
|
|
|
206
|
-
// Clear the current annotation and discovered elements
|
|
207
331
|
const clearAnnotation = useCallback(() => {
|
|
208
332
|
setAnnotatedScreenshot(null);
|
|
209
333
|
setManualFocusBounds(null);
|
|
@@ -212,20 +336,14 @@ export function ChatInterface({
|
|
|
212
336
|
|
|
213
337
|
// Handle vision mode edit request
|
|
214
338
|
const handleVisionEdit = async (prompt: string) => {
|
|
215
|
-
// Use Apply-First mode if callback is provided (new Cursor-style workflow)
|
|
216
339
|
const useApplyFirst = !!onApplyFirstComplete;
|
|
217
340
|
|
|
218
|
-
// Determine which focused elements to use:
|
|
219
|
-
// - If user clicked an element, use visionFocusedElements (passed from parent)
|
|
220
|
-
// - If user used annotation tool without clicking, use annotationDiscoveredElements
|
|
221
341
|
const effectiveFocusedElements = visionFocusedElements.length > 0
|
|
222
342
|
? visionFocusedElements
|
|
223
343
|
: annotationDiscoveredElements;
|
|
224
344
|
|
|
225
345
|
console.log("[Vision Mode] Starting edit request:", {
|
|
226
346
|
prompt,
|
|
227
|
-
focusedElementsFromClick: visionFocusedElements.length,
|
|
228
|
-
focusedElementsFromAnnotation: annotationDiscoveredElements.length,
|
|
229
347
|
effectiveFocusedElements: effectiveFocusedElements.length,
|
|
230
348
|
mode: useApplyFirst ? "apply-first" : "preview-first"
|
|
231
349
|
});
|
|
@@ -237,7 +355,7 @@ export function ChatInterface({
|
|
|
237
355
|
timestamp: new Date(),
|
|
238
356
|
};
|
|
239
357
|
|
|
240
|
-
|
|
358
|
+
addMessage(userMessage);
|
|
241
359
|
setInput("");
|
|
242
360
|
if (inputRef.current) inputRef.current.value = "";
|
|
243
361
|
setIsProcessing(true);
|
|
@@ -245,63 +363,39 @@ export function ChatInterface({
|
|
|
245
363
|
try {
|
|
246
364
|
let screenshot: string | null;
|
|
247
365
|
|
|
248
|
-
// PRIORITY 1: Use manually annotated screenshot if available
|
|
249
|
-
// This is when user drew a focus area using the annotation tool
|
|
250
366
|
if (annotatedScreenshot) {
|
|
251
|
-
console.log("[Vision Mode] Using manually annotated screenshot with discovered elements:", {
|
|
252
|
-
discoveredCount: annotationDiscoveredElements.length,
|
|
253
|
-
elements: annotationDiscoveredElements.slice(0, 3).map(e => ({
|
|
254
|
-
name: e.name,
|
|
255
|
-
text: e.textContent?.substring(0, 20),
|
|
256
|
-
id: e.elementId,
|
|
257
|
-
})),
|
|
258
|
-
});
|
|
259
367
|
screenshot = annotatedScreenshot;
|
|
260
|
-
// Clear the annotation after use (but keep discovered elements for the API call)
|
|
261
368
|
setAnnotatedScreenshot(null);
|
|
262
369
|
setManualFocusBounds(null);
|
|
263
370
|
} else {
|
|
264
|
-
// PRIORITY 2: Capture fresh screenshot and auto-annotate with section highlight
|
|
265
|
-
console.log("[Vision Mode] Capturing screenshot...");
|
|
266
371
|
const rawScreenshot = await captureScreenshot();
|
|
267
|
-
console.log("[Vision Mode] Screenshot captured:", rawScreenshot ? `${rawScreenshot.length} bytes` : "null");
|
|
268
|
-
|
|
269
|
-
// Annotate screenshot with section highlight if parent section exists
|
|
270
|
-
// This helps the LLM visually identify the target area for modifications
|
|
271
372
|
screenshot = rawScreenshot;
|
|
272
373
|
if (rawScreenshot && effectiveFocusedElements.length > 0) {
|
|
273
374
|
const parentSection = effectiveFocusedElements[0].parentSection;
|
|
274
375
|
if (parentSection?.coordinates) {
|
|
275
376
|
screenshot = await drawSectionHighlight(rawScreenshot, parentSection.coordinates);
|
|
276
|
-
console.log("[Vision Mode] Added section highlight to screenshot:", {
|
|
277
|
-
sectionType: parentSection.type,
|
|
278
|
-
sectionText: parentSection.sectionText?.substring(0, 30),
|
|
279
|
-
});
|
|
280
377
|
}
|
|
281
378
|
}
|
|
282
379
|
}
|
|
283
380
|
|
|
284
|
-
// Choose API endpoint based on mode
|
|
285
381
|
const endpoint = useApplyFirst ? "/api/sonance-vision-apply" : "/api/sonance-vision-edit";
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
});
|
|
382
|
+
|
|
383
|
+
// Build chat history for context
|
|
384
|
+
const chatHistory = messages.map(m => ({ role: m.role, content: m.content }));
|
|
289
385
|
|
|
290
386
|
const response = await fetch(endpoint, {
|
|
291
387
|
method: "POST",
|
|
292
388
|
headers: { "Content-Type": "application/json" },
|
|
293
389
|
body: JSON.stringify({
|
|
294
|
-
// Apply-First: write files immediately so HMR shows changes
|
|
295
390
|
action: useApplyFirst ? "apply" : "edit",
|
|
296
391
|
screenshot,
|
|
297
392
|
pageRoute: window.location.pathname,
|
|
298
393
|
userPrompt: prompt,
|
|
299
|
-
// Use effective focused elements (from click OR from annotation discovery)
|
|
300
394
|
focusedElements: effectiveFocusedElements,
|
|
395
|
+
chatHistory,
|
|
301
396
|
}),
|
|
302
397
|
});
|
|
303
398
|
|
|
304
|
-
// Clear annotation discovered elements after API call
|
|
305
399
|
setAnnotationDiscoveredElements([]);
|
|
306
400
|
|
|
307
401
|
const data = await response.json();
|
|
@@ -309,19 +403,14 @@ export function ChatInterface({
|
|
|
309
403
|
success: data.success,
|
|
310
404
|
sessionId: data.sessionId,
|
|
311
405
|
modificationsCount: data.modifications?.length || 0,
|
|
312
|
-
hasCss: !!data.aggregatedPreviewCSS,
|
|
313
|
-
error: data.error,
|
|
314
406
|
});
|
|
315
407
|
|
|
316
|
-
// Check if this is a "location failure" case - element could not be found in code
|
|
317
408
|
const hasLocationFailure = isLocationFailure(data.explanation);
|
|
318
409
|
const hasNoModifications = !data.modifications || data.modifications.length === 0;
|
|
319
410
|
const isElementNotFound = hasLocationFailure && hasNoModifications;
|
|
320
411
|
|
|
321
|
-
// Build appropriate message based on result
|
|
322
412
|
let messageContent: string;
|
|
323
413
|
if (isElementNotFound) {
|
|
324
|
-
// Element not found - provide helpful guidance
|
|
325
414
|
messageContent = (data.explanation || "Could not locate the clicked element in the source code.") +
|
|
326
415
|
"\n\nTry clicking on a different element or describe what you want to change in more detail.";
|
|
327
416
|
} else if (data.success) {
|
|
@@ -332,36 +421,39 @@ export function ChatInterface({
|
|
|
332
421
|
messageContent = data.error || "Failed to generate changes.";
|
|
333
422
|
}
|
|
334
423
|
|
|
424
|
+
// Build assistant message with inline action for diff display
|
|
335
425
|
const assistantMessage: ChatMessage = {
|
|
336
426
|
id: `msg-${Date.now()}-response`,
|
|
337
427
|
role: "assistant",
|
|
338
428
|
content: messageContent,
|
|
339
429
|
timestamp: new Date(),
|
|
430
|
+
// Add inline action if we have modifications
|
|
431
|
+
action: data.success && data.modifications && data.modifications.length > 0 ? {
|
|
432
|
+
type: "diff",
|
|
433
|
+
status: "pending",
|
|
434
|
+
sessionId: data.sessionId,
|
|
435
|
+
explanation: data.explanation,
|
|
436
|
+
files: data.modifications.map((m: { filePath: string; diff: string; originalContent?: string; modifiedContent?: string }) => ({
|
|
437
|
+
path: m.filePath,
|
|
438
|
+
diff: m.diff,
|
|
439
|
+
originalContent: m.originalContent,
|
|
440
|
+
modifiedContent: m.modifiedContent,
|
|
441
|
+
})),
|
|
442
|
+
} : undefined,
|
|
340
443
|
};
|
|
341
444
|
|
|
342
|
-
|
|
445
|
+
addMessage(assistantMessage);
|
|
343
446
|
|
|
344
|
-
// Handle element not found case - show toast and do NOT trigger page refresh
|
|
345
447
|
if (isElementNotFound) {
|
|
346
|
-
console.log("[Vision Mode] Element not found - blocking page refresh:", {
|
|
347
|
-
explanation: data.explanation,
|
|
348
|
-
modifications: data.modifications?.length || 0,
|
|
349
|
-
});
|
|
350
448
|
setToastMessage({
|
|
351
449
|
message: "Could not locate the clicked element in the source code",
|
|
352
450
|
type: 'warning'
|
|
353
451
|
});
|
|
354
|
-
// Do NOT call onApplyFirstComplete - this prevents page refresh
|
|
355
452
|
return;
|
|
356
453
|
}
|
|
357
454
|
|
|
358
455
|
if (data.success && data.modifications && data.modifications.length > 0) {
|
|
359
456
|
if (useApplyFirst && onApplyFirstComplete) {
|
|
360
|
-
// Apply-First mode: files are already written, user can see changes via HMR
|
|
361
|
-
console.log("[Apply-First] Calling onApplyFirstComplete with:", {
|
|
362
|
-
sessionId: data.sessionId,
|
|
363
|
-
modifications: data.modifications.map((m: { filePath: string }) => m.filePath),
|
|
364
|
-
});
|
|
365
457
|
onApplyFirstComplete({
|
|
366
458
|
sessionId: data.sessionId,
|
|
367
459
|
modifications: data.modifications,
|
|
@@ -370,19 +462,12 @@ export function ChatInterface({
|
|
|
370
462
|
backupPaths: data.backupPaths || [],
|
|
371
463
|
});
|
|
372
464
|
} else if (onVisionEditComplete) {
|
|
373
|
-
// Preview-First mode (legacy): just preview CSS
|
|
374
|
-
console.log("[Vision Mode] Calling onVisionEditComplete with:", {
|
|
375
|
-
modifications: data.modifications.map((m: { filePath: string }) => m.filePath),
|
|
376
|
-
cssLength: data.aggregatedPreviewCSS?.length || 0,
|
|
377
|
-
});
|
|
378
465
|
onVisionEditComplete({
|
|
379
466
|
modifications: data.modifications,
|
|
380
467
|
aggregatedPreviewCSS: data.aggregatedPreviewCSS || "",
|
|
381
468
|
explanation: data.explanation || "",
|
|
382
469
|
});
|
|
383
470
|
}
|
|
384
|
-
} else if (!data.success) {
|
|
385
|
-
console.error("[Vision Mode] API returned error:", data.error);
|
|
386
471
|
}
|
|
387
472
|
} catch (error) {
|
|
388
473
|
console.error("[Vision Mode] Request failed:", error);
|
|
@@ -392,24 +477,21 @@ export function ChatInterface({
|
|
|
392
477
|
content: error instanceof Error ? error.message : "Vision mode error occurred",
|
|
393
478
|
timestamp: new Date(),
|
|
394
479
|
};
|
|
395
|
-
|
|
480
|
+
addMessage(errorMessage);
|
|
396
481
|
} finally {
|
|
397
482
|
setIsProcessing(false);
|
|
398
483
|
}
|
|
399
484
|
};
|
|
400
485
|
|
|
401
486
|
const handleSend = async (prompt: string) => {
|
|
402
|
-
// Fallback: read from DOM if React state is empty (browser automation compatibility)
|
|
403
487
|
const actualPrompt = prompt || inputRef.current?.value || "";
|
|
404
488
|
|
|
405
489
|
if (!actualPrompt.trim() || isProcessing) return;
|
|
406
490
|
|
|
407
|
-
// Use vision mode handler if vision mode is active
|
|
408
491
|
if (visionMode) {
|
|
409
492
|
return handleVisionEdit(actualPrompt);
|
|
410
493
|
}
|
|
411
494
|
|
|
412
|
-
// If no component is selected, intercept the request
|
|
413
495
|
if (componentType === "all") {
|
|
414
496
|
const userMessage: ChatMessage = {
|
|
415
497
|
id: `msg-${Date.now()}`,
|
|
@@ -417,7 +499,7 @@ export function ChatInterface({
|
|
|
417
499
|
content: actualPrompt,
|
|
418
500
|
timestamp: new Date(),
|
|
419
501
|
};
|
|
420
|
-
|
|
502
|
+
addMessage(userMessage);
|
|
421
503
|
setInput("");
|
|
422
504
|
if (inputRef.current) inputRef.current.value = "";
|
|
423
505
|
|
|
@@ -428,7 +510,7 @@ export function ChatInterface({
|
|
|
428
510
|
content: "Please select a component using the cursor icon in the header to edit it.",
|
|
429
511
|
timestamp: new Date(),
|
|
430
512
|
};
|
|
431
|
-
|
|
513
|
+
addMessage(assistantMessage);
|
|
432
514
|
}, 300);
|
|
433
515
|
return;
|
|
434
516
|
}
|
|
@@ -440,20 +522,18 @@ export function ChatInterface({
|
|
|
440
522
|
timestamp: new Date(),
|
|
441
523
|
};
|
|
442
524
|
|
|
443
|
-
|
|
525
|
+
addMessage(userMessage);
|
|
444
526
|
setInput("");
|
|
445
527
|
if (inputRef.current) inputRef.current.value = "";
|
|
446
528
|
setIsProcessing(true);
|
|
447
529
|
|
|
448
530
|
try {
|
|
449
|
-
// Dynamically find the component file
|
|
450
531
|
const filePath = await findComponentFile();
|
|
451
532
|
|
|
452
533
|
if (!filePath) {
|
|
453
|
-
throw new Error(`Could not locate component file for "${componentType}"
|
|
534
|
+
throw new Error(`Could not locate component file for "${componentType}".`);
|
|
454
535
|
}
|
|
455
536
|
|
|
456
|
-
// First, fetch the current component source
|
|
457
537
|
const sourceResponse = await fetch(
|
|
458
538
|
`/api/sonance-component-source?file=${encodeURIComponent(filePath)}`
|
|
459
539
|
);
|
|
@@ -464,7 +544,8 @@ export function ChatInterface({
|
|
|
464
544
|
|
|
465
545
|
const sourceData = await sourceResponse.json();
|
|
466
546
|
|
|
467
|
-
|
|
547
|
+
const chatHistory = messages.map(m => ({ role: m.role, content: m.content }));
|
|
548
|
+
|
|
468
549
|
const editResponse = await fetch("/api/sonance-ai-edit", {
|
|
469
550
|
method: "POST",
|
|
470
551
|
headers: { "Content-Type": "application/json" },
|
|
@@ -474,10 +555,10 @@ export function ChatInterface({
|
|
|
474
555
|
filePath,
|
|
475
556
|
currentCode: sourceData.content,
|
|
476
557
|
userRequest: actualPrompt,
|
|
477
|
-
// Variant-scoped editing context
|
|
478
558
|
editScope,
|
|
479
559
|
variantId: editScope === "variant" ? variantId : undefined,
|
|
480
560
|
variantStyles: editScope === "variant" ? variantStyles : undefined,
|
|
561
|
+
chatHistory,
|
|
481
562
|
}),
|
|
482
563
|
});
|
|
483
564
|
|
|
@@ -491,20 +572,30 @@ export function ChatInterface({
|
|
|
491
572
|
: editData.error || "Failed to generate changes.",
|
|
492
573
|
timestamp: new Date(),
|
|
493
574
|
editResult: editData,
|
|
575
|
+
// Add inline action for component edit diffs
|
|
576
|
+
action: editData.success && editData.modifiedCode ? {
|
|
577
|
+
type: "diff",
|
|
578
|
+
status: "pending",
|
|
579
|
+
explanation: editData.explanation,
|
|
580
|
+
files: [{
|
|
581
|
+
path: filePath,
|
|
582
|
+
diff: editData.diff || "",
|
|
583
|
+
originalContent: sourceData.content,
|
|
584
|
+
modifiedContent: editData.modifiedCode,
|
|
585
|
+
}],
|
|
586
|
+
} : undefined,
|
|
494
587
|
};
|
|
495
588
|
|
|
496
|
-
|
|
589
|
+
addMessage(assistantMessage);
|
|
497
590
|
|
|
498
591
|
if (editData.success && editData.modifiedCode) {
|
|
499
592
|
onEditComplete(editData);
|
|
500
|
-
// Set up pending edit for save
|
|
501
593
|
onSaveRequest({
|
|
502
594
|
filePath,
|
|
503
595
|
originalCode: sourceData.content,
|
|
504
596
|
modifiedCode: editData.modifiedCode,
|
|
505
597
|
diff: editData.diff || "",
|
|
506
598
|
explanation: editData.explanation || "",
|
|
507
|
-
// AI-provided CSS for live preview (no parsing needed)
|
|
508
599
|
previewCSS: editData.previewCSS || "",
|
|
509
600
|
});
|
|
510
601
|
}
|
|
@@ -516,238 +607,296 @@ export function ChatInterface({
|
|
|
516
607
|
timestamp: new Date(),
|
|
517
608
|
editResult: { success: false, error: String(error) },
|
|
518
609
|
};
|
|
519
|
-
|
|
610
|
+
addMessage(errorMessage);
|
|
520
611
|
} finally {
|
|
521
612
|
setIsProcessing(false);
|
|
522
613
|
}
|
|
523
614
|
};
|
|
524
615
|
|
|
616
|
+
// Handle accept/revert from inline diff preview
|
|
617
|
+
const handleAcceptChanges = useCallback(async (messageId: string) => {
|
|
618
|
+
console.log("[Chat] Accept changes for message:", messageId);
|
|
619
|
+
// Call actual accept API if we have an active session
|
|
620
|
+
if (onApplyFirstAccept && applyFirstSession) {
|
|
621
|
+
try {
|
|
622
|
+
// Call async accept - parent will set status to "accepting"
|
|
623
|
+
await onApplyFirstAccept();
|
|
624
|
+
// Brief delay so user sees the "Accepting..." state
|
|
625
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
626
|
+
// Update message status after accept completes
|
|
627
|
+
updateMessageAction(messageId, "accepted");
|
|
628
|
+
} catch (error) {
|
|
629
|
+
console.error("[Chat] Accept failed:", error);
|
|
630
|
+
// Keep pending on error so user can retry
|
|
631
|
+
}
|
|
632
|
+
} else {
|
|
633
|
+
updateMessageAction(messageId, "accepted");
|
|
634
|
+
}
|
|
635
|
+
}, [updateMessageAction, onApplyFirstAccept, applyFirstSession]);
|
|
636
|
+
|
|
637
|
+
const handleRevertChanges = useCallback(async (messageId: string) => {
|
|
638
|
+
console.log("[Chat] Revert changes for message:", messageId);
|
|
639
|
+
// Call actual revert API if we have an active session
|
|
640
|
+
if (onApplyFirstRevert && applyFirstSession) {
|
|
641
|
+
try {
|
|
642
|
+
// Call async revert - parent will set status to "reverting"
|
|
643
|
+
await onApplyFirstRevert();
|
|
644
|
+
// Brief delay so user sees the "Reverting..." state
|
|
645
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
646
|
+
// Update message status after revert completes
|
|
647
|
+
updateMessageAction(messageId, "reverted");
|
|
648
|
+
} catch (error) {
|
|
649
|
+
console.error("[Chat] Revert failed:", error);
|
|
650
|
+
// Keep pending on error so user can retry
|
|
651
|
+
}
|
|
652
|
+
} else {
|
|
653
|
+
updateMessageAction(messageId, "reverted");
|
|
654
|
+
}
|
|
655
|
+
}, [updateMessageAction, onApplyFirstRevert, applyFirstSession]);
|
|
656
|
+
|
|
525
657
|
return (
|
|
526
|
-
<div className="
|
|
658
|
+
<div className="flex flex-col h-full">
|
|
527
659
|
{/* Toast Notification */}
|
|
528
660
|
{toastMessage && (
|
|
529
661
|
<div
|
|
530
662
|
className={cn(
|
|
531
|
-
"flex items-center gap-2
|
|
663
|
+
"flex items-center gap-2 px-3 py-2 rounded-lg text-xs shadow-lg mb-2",
|
|
664
|
+
"animate-in slide-in-from-top-2 duration-200",
|
|
532
665
|
toastMessage.type === 'error'
|
|
533
|
-
? "bg-red-
|
|
534
|
-
: "bg-amber-
|
|
666
|
+
? "bg-red-500 text-white"
|
|
667
|
+
: "bg-amber-500 text-white"
|
|
535
668
|
)}
|
|
536
669
|
>
|
|
537
|
-
<AlertCircle className="h-
|
|
538
|
-
<span className="flex-1">{toastMessage.message}</span>
|
|
670
|
+
<AlertCircle className="h-3.5 w-3.5 flex-shrink-0" />
|
|
671
|
+
<span id="span-toastmessagemessage" className="flex-1">{toastMessage.message}</span>
|
|
539
672
|
<button
|
|
540
673
|
onClick={() => setToastMessage(null)}
|
|
541
|
-
className="p-0.5 hover:bg-
|
|
674
|
+
className="p-0.5 hover:bg-white/20 rounded flex-shrink-0 transition-colors"
|
|
542
675
|
>
|
|
543
676
|
<X className="h-3 w-3" />
|
|
544
677
|
</button>
|
|
545
678
|
</div>
|
|
546
679
|
)}
|
|
547
680
|
|
|
548
|
-
{/*
|
|
681
|
+
{/* Chat Tab Bar */}
|
|
682
|
+
<ChatTabBar
|
|
683
|
+
sessions={sessions}
|
|
684
|
+
activeSessionId={activeSessionId}
|
|
685
|
+
onSelectSession={setActiveSessionId}
|
|
686
|
+
onCreateSession={createSession}
|
|
687
|
+
onCloseSession={closeSession}
|
|
688
|
+
/>
|
|
689
|
+
|
|
690
|
+
{/* Vision Mode Header */}
|
|
549
691
|
{visionMode && (
|
|
550
|
-
<div className="
|
|
551
|
-
<div className="
|
|
552
|
-
<Eye className="h-3 w-3" />
|
|
553
|
-
<span>Vision Mode Active</span>
|
|
692
|
+
<div className="flex items-center gap-2 px-3 py-2 bg-gradient-to-r from-purple-500/10 to-purple-600/5 dark:from-purple-500/20 dark:to-purple-600/10 border-b border-purple-200/50 dark:border-purple-700/50">
|
|
693
|
+
<div className="w-6 h-6 rounded-full bg-purple-500 flex items-center justify-center">
|
|
694
|
+
<Eye className="h-3 w-3 text-white" />
|
|
554
695
|
</div>
|
|
555
|
-
|
|
556
|
-
<
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
696
|
+
<div className="flex-1">
|
|
697
|
+
<p id="p-vision-mode-active" className="text-[11px] font-semibold text-purple-700 dark:text-purple-300">Vision Mode Active</p>
|
|
698
|
+
<p id="p-visionfocusedelement" className="text-[10px] text-purple-500 dark:text-purple-400">
|
|
699
|
+
{visionFocusedElements.length > 0
|
|
700
|
+
? `${visionFocusedElements.length} element${visionFocusedElements.length > 1 ? 's' : ''} selected`
|
|
701
|
+
: 'Click elements to focus AI attention'}
|
|
702
|
+
</p>
|
|
703
|
+
</div>
|
|
704
|
+
{visionFocusedElements.length > 0 && (
|
|
705
|
+
<span id="span-visionfocusedelement" className="text-xs px-2 py-0.5 bg-purple-500 text-white rounded-full font-medium">
|
|
706
|
+
{visionFocusedElements.length}
|
|
707
|
+
</span>
|
|
563
708
|
)}
|
|
564
709
|
</div>
|
|
565
710
|
)}
|
|
566
711
|
|
|
567
|
-
{/*
|
|
568
|
-
{messages.length
|
|
569
|
-
<
|
|
570
|
-
|
|
571
|
-
|
|
712
|
+
{/* Chat History - Scrollable */}
|
|
713
|
+
{messages.length > 0 ? (
|
|
714
|
+
<ChatHistory
|
|
715
|
+
messages={messages}
|
|
716
|
+
onAcceptChanges={handleAcceptChanges}
|
|
717
|
+
onRevertChanges={handleRevertChanges}
|
|
718
|
+
visionMode={visionMode}
|
|
719
|
+
liveStatus={applyFirstStatus}
|
|
720
|
+
/>
|
|
721
|
+
) : (
|
|
722
|
+
/* Empty State */
|
|
723
|
+
<div className="flex-1 flex flex-col items-center justify-center py-6 px-4 text-center bg-background">
|
|
724
|
+
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-[#00A3E1] to-[#00D3C8] flex items-center justify-center mb-3 shadow-lg">
|
|
725
|
+
<Sparkles className="h-5 w-5 text-white" />
|
|
726
|
+
</div>
|
|
727
|
+
<p id="p-ai-design-assistant" className="text-xs font-medium text-foreground mb-1">AI Design Assistant</p>
|
|
728
|
+
<p id="p-visionmode-click-ele" className="text-[10px] text-foreground-muted max-w-[180px]">
|
|
729
|
+
{visionMode
|
|
730
|
+
? "Click elements or draw a focus area, then describe changes"
|
|
731
|
+
: componentType === "all"
|
|
732
|
+
? "Select a component to start editing"
|
|
733
|
+
: `Describe changes for ${componentName}`}
|
|
734
|
+
</p>
|
|
735
|
+
</div>
|
|
572
736
|
)}
|
|
573
737
|
|
|
574
|
-
{/*
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
className=
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
738
|
+
{/* Input Area */}
|
|
739
|
+
<div className="mt-auto pt-2 border-t border-border px-3 pb-2 bg-background">
|
|
740
|
+
{/* Annotation indicator */}
|
|
741
|
+
{annotatedScreenshot && visionMode && (
|
|
742
|
+
<div className="flex items-center justify-between text-[10px] text-[#00D3C8] bg-gradient-to-r from-[#00D3C8]/10 to-transparent dark:from-[#00D3C8]/20 px-3 py-1.5 rounded-lg mb-2">
|
|
743
|
+
<span id="span-title" className="flex items-center gap-1.5 font-medium">
|
|
744
|
+
<Crop className="h-3 w-3" />
|
|
745
|
+
Focus area selected
|
|
746
|
+
</span>
|
|
747
|
+
<button
|
|
748
|
+
onClick={clearAnnotation}
|
|
749
|
+
className="text-[#00D3C8] hover:text-[#00b3a8] p-1 hover:bg-[#00D3C8]/10 rounded transition-colors"
|
|
750
|
+
title="Clear focus"
|
|
586
751
|
>
|
|
587
|
-
<
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
752
|
+
<X className="h-3 w-3" />
|
|
753
|
+
</button>
|
|
754
|
+
</div>
|
|
755
|
+
)}
|
|
756
|
+
|
|
757
|
+
{/* Processing Indicator */}
|
|
758
|
+
{isProcessing && (
|
|
759
|
+
<div className={cn(
|
|
760
|
+
"flex items-center gap-2 text-xs px-3 py-2 rounded-lg mb-2",
|
|
761
|
+
visionMode
|
|
762
|
+
? "bg-gradient-to-r from-purple-500/10 to-purple-600/5 dark:from-purple-500/20 dark:to-purple-600/10 text-purple-600 dark:text-purple-400"
|
|
763
|
+
: "bg-gradient-to-r from-[#00A3E1]/10 to-[#00D3C8]/5 dark:from-[#00A3E1]/20 dark:to-[#00D3C8]/10 text-[#00A3E1]"
|
|
764
|
+
)}>
|
|
765
|
+
<div className="relative">
|
|
766
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
767
|
+
<div className="absolute inset-0 rounded-full animate-ping opacity-20 bg-current" />
|
|
593
768
|
</div>
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
769
|
+
<span id="span-visionmode-analyzing" className="font-medium">
|
|
770
|
+
{visionMode ? "Analyzing page..." : "Generating changes..."}
|
|
771
|
+
</span>
|
|
772
|
+
</div>
|
|
773
|
+
)}
|
|
598
774
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
// Force focus to input when clicking anywhere in this container
|
|
604
|
-
// This bypasses modal focus traps by using requestAnimationFrame
|
|
605
|
-
e.stopPropagation();
|
|
606
|
-
const input = inputRef.current;
|
|
607
|
-
if (input && !isProcessing) {
|
|
608
|
-
// Blur any currently focused element first (escape focus trap)
|
|
609
|
-
if (document.activeElement && document.activeElement !== input) {
|
|
610
|
-
(document.activeElement as HTMLElement).blur?.();
|
|
611
|
-
}
|
|
612
|
-
// Use rAF to ensure focus happens after any focus trap logic runs
|
|
613
|
-
requestAnimationFrame(() => {
|
|
614
|
-
input.focus();
|
|
615
|
-
// Also try native focus method as fallback
|
|
616
|
-
input.click();
|
|
617
|
-
});
|
|
618
|
-
}
|
|
619
|
-
}}
|
|
620
|
-
>
|
|
621
|
-
<input
|
|
622
|
-
ref={inputRef}
|
|
623
|
-
type="text"
|
|
624
|
-
value={input}
|
|
625
|
-
onChange={(e) => setInput(e.target.value)}
|
|
626
|
-
onClick={(e) => {
|
|
775
|
+
{/* Input Row */}
|
|
776
|
+
<div
|
|
777
|
+
className="flex items-center gap-2"
|
|
778
|
+
onPointerDown={(e) => {
|
|
627
779
|
e.stopPropagation();
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
if (input) {
|
|
632
|
-
// Escape any focus trap
|
|
633
|
-
if (document.activeElement && document.activeElement !== input) {
|
|
780
|
+
const inputEl = inputRef.current;
|
|
781
|
+
if (inputEl && !isProcessing) {
|
|
782
|
+
if (document.activeElement && document.activeElement !== inputEl) {
|
|
634
783
|
(document.activeElement as HTMLElement).blur?.();
|
|
635
784
|
}
|
|
636
|
-
|
|
785
|
+
requestAnimationFrame(() => {
|
|
786
|
+
inputEl.focus();
|
|
787
|
+
inputEl.click();
|
|
788
|
+
});
|
|
637
789
|
}
|
|
638
790
|
}}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
handleSend(input || inputRef.current?.value || "");
|
|
658
|
-
}
|
|
659
|
-
}}
|
|
660
|
-
placeholder={
|
|
661
|
-
visionMode
|
|
662
|
-
? "Describe what changes you want to make on this page..."
|
|
663
|
-
: componentType === "all"
|
|
664
|
-
? "Select a component to start editing..."
|
|
665
|
-
: `Describe changes to ${componentName}...`
|
|
666
|
-
}
|
|
667
|
-
disabled={isProcessing}
|
|
668
|
-
className={cn(
|
|
669
|
-
"flex-1 px-3 py-2 text-xs rounded border",
|
|
670
|
-
visionMode
|
|
671
|
-
? "border-purple-200 focus:ring-purple-500 focus:border-purple-500"
|
|
672
|
-
: "border-gray-200 focus:ring-[#00A3E1] focus:border-[#00A3E1]",
|
|
673
|
-
"focus:outline-none focus:ring-1",
|
|
674
|
-
"placeholder:text-gray-400",
|
|
675
|
-
"disabled:opacity-50 disabled:bg-gray-50"
|
|
791
|
+
>
|
|
792
|
+
{/* Annotate button - only in vision mode */}
|
|
793
|
+
{visionMode && (
|
|
794
|
+
<button
|
|
795
|
+
onClick={startAnnotation}
|
|
796
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
797
|
+
disabled={isProcessing}
|
|
798
|
+
title="Draw focus area"
|
|
799
|
+
className={cn(
|
|
800
|
+
"p-2 rounded-lg transition-all duration-200",
|
|
801
|
+
annotatedScreenshot
|
|
802
|
+
? "bg-[#00D3C8] text-white shadow-md shadow-[#00D3C8]/30"
|
|
803
|
+
: "bg-secondary text-foreground-secondary hover:bg-secondary-hover hover:text-foreground",
|
|
804
|
+
"disabled:opacity-50 disabled:cursor-not-allowed"
|
|
805
|
+
)}
|
|
806
|
+
>
|
|
807
|
+
<Crop className="h-4 w-4" />
|
|
808
|
+
</button>
|
|
676
809
|
)}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
810
|
+
|
|
811
|
+
{/* Input Field */}
|
|
812
|
+
<div className="flex-1 relative">
|
|
813
|
+
<input
|
|
814
|
+
ref={inputRef}
|
|
815
|
+
type="text"
|
|
816
|
+
value={input}
|
|
817
|
+
onChange={(e) => setInput(e.target.value)}
|
|
818
|
+
onClick={(e) => {
|
|
819
|
+
e.stopPropagation();
|
|
820
|
+
e.preventDefault();
|
|
821
|
+
const inputEl = inputRef.current;
|
|
822
|
+
if (inputEl) {
|
|
823
|
+
if (document.activeElement && document.activeElement !== inputEl) {
|
|
824
|
+
(document.activeElement as HTMLElement).blur?.();
|
|
825
|
+
}
|
|
826
|
+
inputEl.focus();
|
|
827
|
+
}
|
|
828
|
+
}}
|
|
829
|
+
onPointerDown={(e) => {
|
|
830
|
+
e.stopPropagation();
|
|
831
|
+
const inputEl = inputRef.current;
|
|
832
|
+
if (inputEl) {
|
|
833
|
+
requestAnimationFrame(() => inputEl.focus());
|
|
834
|
+
}
|
|
835
|
+
}}
|
|
836
|
+
onMouseDown={(e) => {
|
|
837
|
+
e.stopPropagation();
|
|
838
|
+
const inputEl = inputRef.current;
|
|
839
|
+
if (inputEl) {
|
|
840
|
+
requestAnimationFrame(() => inputEl.focus());
|
|
841
|
+
}
|
|
842
|
+
}}
|
|
843
|
+
onKeyDown={(e) => {
|
|
844
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
845
|
+
e.preventDefault();
|
|
846
|
+
handleSend(input || inputRef.current?.value || "");
|
|
847
|
+
}
|
|
848
|
+
}}
|
|
849
|
+
placeholder={
|
|
850
|
+
visionMode
|
|
851
|
+
? "Describe the changes you want..."
|
|
852
|
+
: componentType === "all"
|
|
853
|
+
? "Select a component first..."
|
|
854
|
+
: `What would you like to change?`
|
|
855
|
+
}
|
|
856
|
+
disabled={isProcessing}
|
|
857
|
+
className={cn(
|
|
858
|
+
"w-full px-3 py-2.5 text-xs rounded-xl border-2 transition-all duration-200",
|
|
859
|
+
"bg-background text-foreground",
|
|
860
|
+
visionMode
|
|
861
|
+
? "border-purple-200 dark:border-purple-700 focus:border-purple-400 dark:focus:border-purple-500 focus:ring-2 focus:ring-purple-500/20"
|
|
862
|
+
: "border-border focus:border-[#00A3E1] focus:ring-2 focus:ring-[#00A3E1]/20",
|
|
863
|
+
"focus:outline-none",
|
|
864
|
+
"placeholder:text-foreground-muted",
|
|
865
|
+
"disabled:opacity-50 disabled:bg-secondary",
|
|
866
|
+
"shadow-sm"
|
|
867
|
+
)}
|
|
868
|
+
/>
|
|
869
|
+
</div>
|
|
870
|
+
|
|
871
|
+
{/* Send button */}
|
|
681
872
|
<button
|
|
682
|
-
onClick={
|
|
873
|
+
onClick={() => handleSend(input || inputRef.current?.value || "")}
|
|
683
874
|
onPointerDown={(e) => e.stopPropagation()}
|
|
684
|
-
disabled={isProcessing}
|
|
685
|
-
title="Draw on screenshot to focus AI attention"
|
|
875
|
+
disabled={isProcessing || !input.trim()}
|
|
686
876
|
className={cn(
|
|
687
|
-
"
|
|
688
|
-
|
|
689
|
-
? "bg-
|
|
690
|
-
: "bg-
|
|
691
|
-
"disabled:opacity-
|
|
877
|
+
"p-2.5 rounded-xl transition-all duration-200 shadow-md",
|
|
878
|
+
visionMode
|
|
879
|
+
? "bg-gradient-to-br from-purple-500 to-purple-600 text-white hover:from-purple-600 hover:to-purple-700 shadow-purple-500/30"
|
|
880
|
+
: "bg-gradient-to-br from-[#00A3E1] to-[#0090c8] text-white hover:from-[#0090c8] hover:to-[#007ab3] shadow-[#00A3E1]/30",
|
|
881
|
+
"disabled:opacity-40 disabled:cursor-not-allowed disabled:shadow-none",
|
|
882
|
+
!isProcessing && input.trim() && "hover:scale-105 active:scale-95"
|
|
692
883
|
)}
|
|
693
884
|
>
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
onClick={() => handleSend(input || inputRef.current?.value || "")}
|
|
700
|
-
onPointerDown={(e) => e.stopPropagation()}
|
|
701
|
-
disabled={isProcessing}
|
|
702
|
-
className={cn(
|
|
703
|
-
"px-3 py-2 rounded transition-colors",
|
|
704
|
-
visionMode
|
|
705
|
-
? "bg-purple-600 text-white hover:bg-purple-700"
|
|
706
|
-
: "bg-[#00A3E1] text-white hover:bg-[#0090c8]",
|
|
707
|
-
"disabled:opacity-50 disabled:cursor-not-allowed"
|
|
708
|
-
)}
|
|
709
|
-
>
|
|
710
|
-
{isProcessing ? (
|
|
711
|
-
<Loader2 className="h-4 w-4 animate-spin" />
|
|
712
|
-
) : (
|
|
713
|
-
<Send className="h-4 w-4" />
|
|
714
|
-
)}
|
|
715
|
-
</button>
|
|
716
|
-
</div>
|
|
717
|
-
|
|
718
|
-
{/* Annotation indicator */}
|
|
719
|
-
{annotatedScreenshot && visionMode && (
|
|
720
|
-
<div className="flex items-center justify-between text-xs text-[#00D3C8] bg-[#00D3C8]/10 px-2 py-1 rounded">
|
|
721
|
-
<span className="flex items-center gap-1">
|
|
722
|
-
<Crop className="h-3 w-3" />
|
|
723
|
-
Focus area selected - your prompt will target this region
|
|
724
|
-
</span>
|
|
725
|
-
<button
|
|
726
|
-
onClick={clearAnnotation}
|
|
727
|
-
className="text-[#00D3C8] hover:text-[#00b3a8] p-0.5"
|
|
728
|
-
title="Clear annotation"
|
|
729
|
-
>
|
|
730
|
-
<X className="h-3 w-3" />
|
|
885
|
+
{isProcessing ? (
|
|
886
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
887
|
+
) : (
|
|
888
|
+
<Send className="h-4 w-4" />
|
|
889
|
+
)}
|
|
731
890
|
</button>
|
|
732
891
|
</div>
|
|
733
|
-
)}
|
|
734
892
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
)}>
|
|
741
|
-
<Loader2 className="h-3 w-3 animate-spin" />
|
|
742
|
-
<span>
|
|
743
|
-
{visionMode
|
|
744
|
-
? "AI is analyzing the page screenshot and generating changes..."
|
|
745
|
-
: "AI is analyzing and generating changes..."}
|
|
746
|
-
</span>
|
|
747
|
-
</div>
|
|
748
|
-
)}
|
|
893
|
+
{/* Quick tip */}
|
|
894
|
+
<p id="p-press-enter-to-send-" className="text-[9px] text-foreground-muted text-center mt-2">
|
|
895
|
+
Press Enter to send • Keep chatting to refine changes
|
|
896
|
+
</p>
|
|
897
|
+
</div>
|
|
749
898
|
|
|
750
|
-
{/* Screenshot Annotator Overlay
|
|
899
|
+
{/* Screenshot Annotator Overlay */}
|
|
751
900
|
{isAnnotating && (
|
|
752
901
|
<ScreenshotAnnotator
|
|
753
902
|
onConfirm={handleAnnotationConfirm}
|