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.
Files changed (84) hide show
  1. package/dist/assets/api/sonance-ai-edit/route.ts +30 -7
  2. package/dist/assets/api/sonance-save-image/route.ts +625 -0
  3. package/dist/assets/api/sonance-vision-apply/image-styling-detection.ts +1360 -0
  4. package/dist/assets/api/sonance-vision-apply/route.ts +1020 -64
  5. package/dist/assets/api/sonance-vision-apply/styling-detection.ts +730 -0
  6. package/dist/assets/api/sonance-vision-apply/theme-discovery.ts +1 -1
  7. package/dist/assets/api/sonance-vision-edit/route.ts +33 -8
  8. package/dist/assets/brand-system.ts +13 -12
  9. package/dist/assets/components/accordion.tsx +15 -7
  10. package/dist/assets/components/alert-dialog.tsx +35 -10
  11. package/dist/assets/components/alert.tsx +11 -10
  12. package/dist/assets/components/avatar.tsx +4 -4
  13. package/dist/assets/components/badge.tsx +16 -12
  14. package/dist/assets/components/button.stories.tsx +3 -3
  15. package/dist/assets/components/button.tsx +50 -31
  16. package/dist/assets/components/calendar.tsx +12 -8
  17. package/dist/assets/components/card.tsx +35 -29
  18. package/dist/assets/components/checkbox.tsx +9 -8
  19. package/dist/assets/components/code.tsx +19 -11
  20. package/dist/assets/components/command.tsx +32 -13
  21. package/dist/assets/components/context-menu.tsx +37 -16
  22. package/dist/assets/components/dialog.tsx +8 -5
  23. package/dist/assets/components/divider.tsx +15 -5
  24. package/dist/assets/components/drawer.tsx +4 -3
  25. package/dist/assets/components/dropdown-menu.tsx +15 -13
  26. package/dist/assets/components/hover-card.tsx +4 -1
  27. package/dist/assets/components/image.tsx +1 -1
  28. package/dist/assets/components/input.tsx +29 -14
  29. package/dist/assets/components/kbd.stories.tsx +3 -3
  30. package/dist/assets/components/kbd.tsx +29 -13
  31. package/dist/assets/components/listbox.tsx +8 -8
  32. package/dist/assets/components/menubar.tsx +50 -23
  33. package/dist/assets/components/navbar.stories.tsx +140 -13
  34. package/dist/assets/components/navbar.tsx +22 -5
  35. package/dist/assets/components/navigation-menu.tsx +28 -6
  36. package/dist/assets/components/pagination.tsx +10 -10
  37. package/dist/assets/components/popover.tsx +10 -8
  38. package/dist/assets/components/progress.tsx +6 -4
  39. package/dist/assets/components/radio-group.tsx +5 -5
  40. package/dist/assets/components/select.tsx +49 -29
  41. package/dist/assets/components/separator.tsx +3 -3
  42. package/dist/assets/components/sheet.tsx +4 -4
  43. package/dist/assets/components/sidebar.tsx +10 -10
  44. package/dist/assets/components/skeleton.tsx +13 -5
  45. package/dist/assets/components/slider.tsx +12 -10
  46. package/dist/assets/components/switch.tsx +4 -4
  47. package/dist/assets/components/table.tsx +5 -5
  48. package/dist/assets/components/tabs.tsx +8 -8
  49. package/dist/assets/components/textarea.tsx +11 -9
  50. package/dist/assets/components/toast.tsx +7 -7
  51. package/dist/assets/components/toggle.tsx +27 -7
  52. package/dist/assets/components/tooltip.tsx +10 -8
  53. package/dist/assets/components/user.tsx +8 -6
  54. package/dist/assets/dev-tools/SonanceDevTools.tsx +851 -708
  55. package/dist/assets/dev-tools/components/ApplyFirstPreview.tsx +10 -10
  56. package/dist/assets/dev-tools/components/ChatHistory.tsx +145 -0
  57. package/dist/assets/dev-tools/components/ChatInterface.tsx +444 -295
  58. package/dist/assets/dev-tools/components/ChatTabBar.tsx +82 -0
  59. package/dist/assets/dev-tools/components/DiffPreview.tsx +1 -1
  60. package/dist/assets/dev-tools/components/InlineDiffPreview.tsx +528 -0
  61. package/dist/assets/dev-tools/components/InspectorOverlay.tsx +21 -18
  62. package/dist/assets/dev-tools/components/PropertiesPanel.tsx +1345 -0
  63. package/dist/assets/dev-tools/components/ScreenshotAnnotator.tsx +1 -1
  64. package/dist/assets/dev-tools/components/SectionHighlight.tsx +1 -1
  65. package/dist/assets/dev-tools/components/VisionDiffPreview.tsx +7 -7
  66. package/dist/assets/dev-tools/components/VisionModeBorder.tsx +12 -63
  67. package/dist/assets/dev-tools/constants.ts +38 -6
  68. package/dist/assets/dev-tools/hooks/index.ts +69 -0
  69. package/dist/assets/dev-tools/hooks/useComponentDetection.ts +132 -0
  70. package/dist/assets/dev-tools/hooks/useComputedStyles.ts +471 -0
  71. package/dist/assets/dev-tools/hooks/useContentHash.ts +212 -0
  72. package/dist/assets/dev-tools/hooks/useElementScanner.ts +398 -0
  73. package/dist/assets/dev-tools/hooks/useImageDetection.ts +162 -0
  74. package/dist/assets/dev-tools/hooks/useTextDetection.ts +217 -0
  75. package/dist/assets/dev-tools/index.ts +3 -0
  76. package/dist/assets/dev-tools/panels/AnalysisPanel.tsx +32 -32
  77. package/dist/assets/dev-tools/panels/ComponentsPanel.tsx +384 -131
  78. package/dist/assets/dev-tools/panels/TextPanel.tsx +10 -10
  79. package/dist/assets/dev-tools/types.ts +93 -2
  80. package/dist/assets/globals.css +225 -9
  81. package/dist/assets/styles/brand-overrides.css +3 -2
  82. package/dist/assets/utils.ts +2 -1
  83. package/dist/index.js +22 -3
  84. 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 { ChatMessage, AIEditResult, PendingEdit, VisionFocusedElement, VisionPendingEdit, ApplyFirstSession } from "../types";
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]); // Dashed line for visibility
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
- const [messages, setMessages] = useState<ChatMessage[]>([]);
106
- const [input, setInput] = useState("");
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
- const messagesEndRef = useRef<HTMLDivElement>(null);
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
- // Scroll to bottom when messages change
128
- useEffect(() => {
129
- messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
130
- }, [messages]);
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, // Lower scale for smaller file size
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 - show drawing overlay on live app
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 - screenshot is already captured and annotated
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
- setMessages((prev) => [...prev, userMessage]);
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
- console.log("[Vision Mode] Sending to API:", endpoint, {
287
- effectiveFocusedElements: effectiveFocusedElements.length,
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
- setMessages((prev) => [...prev, assistantMessage]);
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
- setMessages((prev) => [...prev, errorMessage]);
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
- setMessages((prev) => [...prev, userMessage]);
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
- setMessages((prev) => [...prev, assistantMessage]);
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
- setMessages((prev) => [...prev, userMessage]);
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}". The component may not exist in the expected directories.`);
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
- // Then, send to AI for editing
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
- setMessages((prev) => [...prev, assistantMessage]);
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
- setMessages((prev) => [...prev, errorMessage]);
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="space-y-3">
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 p-3 rounded-md text-sm animate-in slide-in-from-top-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-50 border border-red-200 text-red-700"
534
- : "bg-amber-50 border border-amber-200 text-amber-700"
666
+ ? "bg-red-500 text-white"
667
+ : "bg-amber-500 text-white"
535
668
  )}
536
669
  >
537
- <AlertCircle className="h-4 w-4 flex-shrink-0" />
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-black/5 rounded"
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
- {/* Vision Mode Banner */}
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="p-2 bg-purple-50 border border-purple-200 rounded-md">
551
- <div className="flex items-center gap-2 text-purple-700 font-medium text-xs mb-1">
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
- {visionFocusedElements.length > 0 ? (
556
- <div className="text-purple-600 text-xs">
557
- {visionFocusedElements.length} element{visionFocusedElements.length !== 1 ? "s" : ""} focused
558
- </div>
559
- ) : (
560
- <div className="text-purple-500 text-xs">
561
- Click elements to focus AI attention, then describe your changes
562
- </div>
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
- {/* AI Hint - only show when no messages yet and not in vision mode */}
568
- {messages.length === 0 && componentType !== "all" && !visionMode && (
569
- <p className="text-xs text-gray-500 italic">
570
- Describe any styling changes you'd like to make to this component.
571
- </p>
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
- {/* Chat Messages */}
575
- {messages.length > 0 && (
576
- <div className="max-h-48 overflow-y-auto space-y-2 p-2 rounded border border-gray-200 bg-gray-50">
577
- {messages.map((msg) => (
578
- <div
579
- key={msg.id}
580
- className={cn(
581
- "text-xs p-2 rounded",
582
- msg.role === "user"
583
- ? "bg-[#00A3E1]/10 text-gray-800 ml-4"
584
- : "bg-white border border-gray-200 mr-4"
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
- <div className="flex items-start gap-2">
588
- {msg.role === "assistant" && (
589
- <Sparkles className="h-3 w-3 text-[#00A3E1] mt-0.5 flex-shrink-0" />
590
- )}
591
- <span id="summary-row-span-msgcontent">{msg.content}</span>
592
- </div>
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
- <div ref={messagesEndRef} />
596
- </div>
597
- )}
769
+ <span id="span-visionmode-analyzing" className="font-medium">
770
+ {visionMode ? "Analyzing page..." : "Generating changes..."}
771
+ </span>
772
+ </div>
773
+ )}
598
774
 
599
- {/* Input */}
600
- <div
601
- className="flex gap-2"
602
- onPointerDown={(e) => {
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
- e.preventDefault();
629
- // Force focus using multiple strategies
630
- const input = inputRef.current;
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
- input.focus();
785
+ requestAnimationFrame(() => {
786
+ inputEl.focus();
787
+ inputEl.click();
788
+ });
637
789
  }
638
790
  }}
639
- onPointerDown={(e) => {
640
- e.stopPropagation();
641
- // Don't preventDefault here - let native click handling work
642
- const input = inputRef.current;
643
- if (input) {
644
- requestAnimationFrame(() => input.focus());
645
- }
646
- }}
647
- onMouseDown={(e) => {
648
- e.stopPropagation();
649
- const input = inputRef.current;
650
- if (input) {
651
- requestAnimationFrame(() => input.focus());
652
- }
653
- }}
654
- onKeyDown={(e) => {
655
- if (e.key === "Enter" && !e.shiftKey) {
656
- e.preventDefault();
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
- {/* Annotate screenshot button - only in vision mode */}
680
- {visionMode && (
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={startAnnotation}
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
- "px-3 py-2 rounded transition-colors",
688
- annotatedScreenshot
689
- ? "bg-[#00D3C8] text-[#1a1a1a]" // Teal when annotation is active
690
- : "bg-gray-100 text-gray-600 hover:bg-gray-200",
691
- "disabled:opacity-50 disabled:cursor-not-allowed"
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
- <Crop className="h-4 w-4" />
695
- </button>
696
- )}
697
-
698
- <button
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
- {/* Processing Indicator */}
736
- {isProcessing && (
737
- <div className={cn(
738
- "flex items-center gap-2 text-xs",
739
- visionMode ? "text-purple-600" : "text-gray-500"
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 - draws on live app */}
899
+ {/* Screenshot Annotator Overlay */}
751
900
  {isAnnotating && (
752
901
  <ScreenshotAnnotator
753
902
  onConfirm={handleAnnotationConfirm}