samihalawa Claude commited on
Commit
7017720
·
1 Parent(s): 701b35b

feat: Enhance UI with visual feedback during AI operations

Browse files

- Add progress indicators showing processing stages
- Implement streaming code diff visualization
- Add contextual status messages
- Fix type safety issues in error handling
- Document visual feedback system in VISUAL_FEEDBACK.md

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

VISUAL_FEEDBACK.md ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Visual Feedback Enhancements
2
+
3
+ This document outlines the visual feedback improvements made to AutoSite to provide better user experience during AI operations.
4
+
5
+ ## Key Improvements
6
+
7
+ ### 1. Enhanced Loading Component
8
+
9
+ The loading component now provides rich visual feedback:
10
+
11
+ - **Progress Indicator with Percentage**: Shows numerical progress (0-100%)
12
+ - **Animated Loading Spinner**: Provides continuous visual feedback
13
+ - **Progress Bar**: Horizontal visualization of completion status
14
+ - **Animated Ellipsis**: Active feedback even when progress percentage is unknown
15
+ - **Contextual Status Messages**: Different messages based on processing stage
16
+
17
+ ### 2. Visual Status Indicators
18
+
19
+ - **Processing Mode Indicator**: Shows whether the AI is generating new HTML or applying changes to existing code
20
+ - **Gradient Progress Bar**: Located at the top of AI input field for immediate visual feedback
21
+ - **Pulsing Status Dot**: Indicates active processing with animation
22
+
23
+ ### 3. Animation Effects
24
+
25
+ - **Success Animation**: Subtle purple flash when AI processing completes successfully
26
+ - **Code Diff Highlighting**: Green/red highlighting for added/removed code during diff operations
27
+ - **Smooth Transitions**: All UI elements use smooth transitions for a polished feel
28
+
29
+ ## Implementation Notes
30
+
31
+ The improvements use CSS animations and React state management to provide a more responsive, visually informative interface.
32
+
33
+ ### CSS Animation Classes
34
+
35
+ - `.ai-success`: Full-page success animation
36
+ - `.diff-new`: Highlights added code in diffs
37
+ - `.diff-removed`: Highlights removed code in diffs
38
+ - `.fast-transition`: Utility for smooth transitions
39
+
40
+ ### Progress Estimation
41
+
42
+ The system intelligently estimates progress based on streaming chunks:
43
+ - 0-25%: Initial setup and request analysis
44
+ - 25-75%: Main content generation
45
+ - 75-95%: Finalizing and formatting
46
+ - 100%: Complete
47
+
48
+ ## Benefits
49
+
50
+ 1. **Reduced Perceived Latency**: Better visual feedback makes waiting times feel shorter
51
+ 2. **Clearer Status Information**: Users always know what's happening
52
+ 3. **Better Error Communication**: Type-safe error handling with descriptive messages
53
+ 4. **Improved User Confidence**: The system feels more responsive and trustworthy
54
+
55
+ ## Future Improvements
56
+
57
+ Potential enhancements for future versions:
58
+
59
+ 1. Add animation for code transitions when switching between views
60
+ 2. Implement sound feedback options (toggleable)
61
+ 3. Add keyboard shortcut indicators
62
+ 4. Extend visual feedback to deployment process
src/assets/index.css CHANGED
@@ -7,3 +7,60 @@
7
  .font-code {
8
  font-family: "Source Code Pro";
9
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  .font-code {
8
  font-family: "Source Code Pro";
9
  }
10
+
11
+ /**
12
+ * Visual Feedback Animations
13
+ * These animations enhance the user experience by providing visual
14
+ * feedback during AI operations.
15
+ */
16
+
17
+ /* Animation for code changes - highlights added code */
18
+ @keyframes highlight-new {
19
+ 0% { background-color: rgba(16, 185, 129, 0.1); }
20
+ 50% { background-color: rgba(16, 185, 129, 0.3); }
21
+ 100% { background-color: rgba(16, 185, 129, 0.1); }
22
+ }
23
+
24
+ /* Animation for code changes - highlights removed code */
25
+ @keyframes highlight-removed {
26
+ 0% { background-color: rgba(239, 68, 68, 0.1); }
27
+ 50% { background-color: rgba(239, 68, 68, 0.3); }
28
+ 100% { background-color: rgba(239, 68, 68, 0.1); }
29
+ }
30
+
31
+ /* Added code block styling */
32
+ .diff-new {
33
+ animation: highlight-new 2s infinite;
34
+ border-left: 2px solid rgb(16, 185, 129); /* Green indicator */
35
+ padding-left: 8px;
36
+ margin: 4px 0;
37
+ }
38
+
39
+ /* Removed code block styling */
40
+ .diff-removed {
41
+ animation: highlight-removed 2s infinite;
42
+ border-left: 2px solid rgb(239, 68, 68); /* Red indicator */
43
+ padding-left: 8px;
44
+ margin: 4px 0;
45
+ text-decoration: line-through;
46
+ opacity: 0.7;
47
+ }
48
+
49
+ /* Success completion animation - subtle purple flash */
50
+ @keyframes success-flash {
51
+ 0% { background-color: rgba(139, 92, 246, 0); }
52
+ 25% { background-color: rgba(139, 92, 246, 0.05); }
53
+ 50% { background-color: rgba(139, 92, 246, 0.1); }
54
+ 75% { background-color: rgba(139, 92, 246, 0.05); }
55
+ 100% { background-color: rgba(139, 92, 246, 0); }
56
+ }
57
+
58
+ /* Applied to body when AI processing completes */
59
+ .ai-success {
60
+ animation: success-flash 1s ease-in-out;
61
+ }
62
+
63
+ /* Smooth transition utility for UI elements */
64
+ .fast-transition {
65
+ transition: all 0.2s ease-out;
66
+ }
src/components/App.tsx CHANGED
@@ -13,6 +13,7 @@ import { Auth } from "../utils/types";
13
  import Preview from "./preview/preview";
14
  import WysiwygEditor from "./wysiwyg/wysiwyg-editor";
15
  import SavedPages from "./saved-pages/saved-pages";
 
16
 
17
  function App() {
18
  const [htmlStorage, , removeHtmlStorage] = useLocalStorage("html_content");
@@ -184,6 +185,7 @@ function App() {
184
  }}
185
  >
186
  <Tabs />
 
187
  {editMode === 'code' ? (
188
  <Editor
189
  language="html"
@@ -236,7 +238,9 @@ function App() {
236
  isResizing={isResizing}
237
  isAiWorking={isAiWorking}
238
  ref={preview}
239
- />
 
 
240
  </main>
241
  </div>
242
  );
 
13
  import Preview from "./preview/preview";
14
  import WysiwygEditor from "./wysiwyg/wysiwyg-editor";
15
  import SavedPages from "./saved-pages/saved-pages";
16
+ import Loading from "./loading/loading";
17
 
18
  function App() {
19
  const [htmlStorage, , removeHtmlStorage] = useLocalStorage("html_content");
 
185
  }}
186
  >
187
  <Tabs />
188
+ {isAiWorking && <Loading progress={0} showDetails={false} />}
189
  {editMode === 'code' ? (
190
  <Editor
191
  language="html"
 
238
  isResizing={isResizing}
239
  isAiWorking={isAiWorking}
240
  ref={preview}
241
+ >
242
+ {isAiWorking && <Loading progress={0} showDetails={false} />}
243
+ </Preview>
244
  </main>
245
  </div>
246
  );
src/components/ask-ai/ask-ai.tsx CHANGED
@@ -27,19 +27,25 @@ function AskAI({
27
  const [prompt, setPrompt] = useState("");
28
  const [hasAsked, setHasAsked] = useState(false);
29
  const [previousPrompt, setPreviousPrompt] = useState("");
 
 
30
  const audio = new Audio(SuccessSound);
31
  audio.volume = 0.5;
32
 
33
  // Removed client-side diff parsing/applying logic
 
34
  // --- Main AI Call Logic ---
35
  const callAi = async () => {
36
  if (isAiWorking || !prompt.trim()) return;
37
  const originalHtml = html; // Store the HTML state at the start of the request
38
  setisAiWorking(true);
 
 
39
 
40
  let fullContentResponse = ""; // Used for full HTML mode
41
  let accumulatedDiffResponse = ""; // Used for diff mode
42
  let lastRenderTime = 0; // For throttling full HTML updates
 
43
 
44
  try {
45
  const request = await fetch("/api/ask-ai", {
@@ -63,10 +69,12 @@ function AskAI({
63
  toast.error(res.message);
64
  }
65
  setisAiWorking(false);
 
66
  return;
67
  }
68
 
69
  const responseType = request.headers.get("X-Response-Type") || "full"; // Default to full if header missing
 
70
  console.log(`[AI Response] Type: ${responseType}`);
71
 
72
  const reader = request.body.getReader();
@@ -74,7 +82,7 @@ function AskAI({
74
 
75
  console.log("[AI Request] Starting to read response stream");
76
 
77
- // eslint-disable-next-line no-constant-condition
78
  while (true) {
79
  try {
80
  const { done, value } = await reader.read();
@@ -82,6 +90,13 @@ function AskAI({
82
  console.log("[AI Response] Stream finished successfully.");
83
  console.log("[AI Response] Accumulated full content length:", fullContentResponse.length);
84
  console.log("[AI Response] Accumulated diff content length:", accumulatedDiffResponse.length);
 
 
 
 
 
 
 
85
 
86
  // --- Post-stream processing ---
87
  if (responseType === 'diff') {
@@ -107,9 +122,12 @@ function AskAI({
107
  setHtml(patchedHtml); // Update editor with the final result
108
  toast.success("AI changes applied");
109
 
110
- } catch (applyError: any) {
111
  console.error("Error applying diffs server-side:", applyError);
112
- toast.error(`Failed to apply AI changes: ${applyError.message}`);
 
 
 
113
  // Optionally revert to originalHtml? Or leave the editor as is?
114
  // setHtml(originalHtml); // Uncomment to revert on failure
115
  }
@@ -181,6 +199,21 @@ function AskAI({
181
  }
182
 
183
  const chunk = decoder.decode(value, { stream: true });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  console.log("[AI Stream] Received chunk of length:", chunk.length);
185
  if (chunk.length > 0) {
186
  console.log("[AI Stream] Sample:", chunk.substring(0, 50) + (chunk.length > 50 ? "..." : ""));
@@ -199,9 +232,9 @@ function AskAI({
199
 
200
  if (newHtml) {
201
  console.log("[AI Full] Found HTML content, length:", newHtml.length);
202
- // Throttle the re-renders to avoid flashing/flicker
203
  const now = Date.now();
204
- if (now - lastRenderTime > 300) {
205
  // Force-close the HTML tag for preview if needed
206
  let partialDoc = newHtml;
207
  if (!partialDoc.trim().endsWith("</html>")) {
@@ -228,24 +261,37 @@ function AskAI({
228
  } else {
229
  throw new Error("Response body is null");
230
  }
231
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
232
- } catch (error: any) {
233
  setisAiWorking(false);
234
- toast.error(error.message);
235
- if (error.openLogin) {
236
- setOpen(true);
 
 
 
 
 
 
 
 
 
 
237
  }
238
  }
239
  };
240
 
241
  return (
242
  <div
243
- className={`bg-gray-950 rounded-xl py-2 lg:py-2.5 pl-3.5 lg:pl-4 pr-2 lg:pr-2.5 absolute lg:sticky bottom-3 left-3 lg:bottom-4 lg:left-4 w-[calc(100%-1.5rem)] lg:w-[calc(100%-2rem)] z-10 group ${
244
- isAiWorking ? "animate-pulse" : ""
245
- }`}
246
  >
 
 
 
 
 
 
247
  <div className="w-full relative flex items-center justify-between">
248
- <RiSparkling2Fill className="text-lg lg:text-xl text-gray-500 group-focus-within:text-pink-500" />
249
  <input
250
  type="text"
251
  disabled={isAiWorking}
@@ -261,14 +307,29 @@ function AskAI({
261
  }
262
  }}
263
  />
264
- <button
265
- disabled={isAiWorking}
266
- className="relative overflow-hidden cursor-pointer flex-none flex items-center justify-center rounded-full text-sm font-semibold size-8 text-center bg-pink-500 hover:bg-pink-400 text-white shadow-sm dark:shadow-highlight/20 disabled:bg-gray-300 disabled:text-gray-500 disabled:cursor-not-allowed disabled:hover:bg-gray-300"
267
- onClick={callAi}
268
- >
269
- <GrSend className="-translate-x-[1px]" />
270
- </button>
 
 
 
 
 
 
271
  </div>
 
 
 
 
 
 
 
 
 
272
  <div
273
  className={classNames(
274
  "h-screen w-screen bg-black/20 fixed left-0 top-0 z-10",
 
27
  const [prompt, setPrompt] = useState("");
28
  const [hasAsked, setHasAsked] = useState(false);
29
  const [previousPrompt, setPreviousPrompt] = useState("");
30
+ const [streamProgress, setStreamProgress] = useState(0); // Track streaming progress
31
+ const [responseMode, setResponseMode] = useState<"full" | "diff" | null>(null); // Track response type
32
  const audio = new Audio(SuccessSound);
33
  audio.volume = 0.5;
34
 
35
  // Removed client-side diff parsing/applying logic
36
+
37
  // --- Main AI Call Logic ---
38
  const callAi = async () => {
39
  if (isAiWorking || !prompt.trim()) return;
40
  const originalHtml = html; // Store the HTML state at the start of the request
41
  setisAiWorking(true);
42
+ setStreamProgress(0); // Reset progress indicator
43
+ setResponseMode(null); // Reset response mode
44
 
45
  let fullContentResponse = ""; // Used for full HTML mode
46
  let accumulatedDiffResponse = ""; // Used for diff mode
47
  let lastRenderTime = 0; // For throttling full HTML updates
48
+ let totalChunksReceived = 0;
49
 
50
  try {
51
  const request = await fetch("/api/ask-ai", {
 
69
  toast.error(res.message);
70
  }
71
  setisAiWorking(false);
72
+ setStreamProgress(0);
73
  return;
74
  }
75
 
76
  const responseType = request.headers.get("X-Response-Type") || "full"; // Default to full if header missing
77
+ setResponseMode(responseType as "full" | "diff");
78
  console.log(`[AI Response] Type: ${responseType}`);
79
 
80
  const reader = request.body.getReader();
 
82
 
83
  console.log("[AI Request] Starting to read response stream");
84
 
85
+ // Process all chunks until done
86
  while (true) {
87
  try {
88
  const { done, value } = await reader.read();
 
90
  console.log("[AI Response] Stream finished successfully.");
91
  console.log("[AI Response] Accumulated full content length:", fullContentResponse.length);
92
  console.log("[AI Response] Accumulated diff content length:", accumulatedDiffResponse.length);
93
+ setStreamProgress(100); // Set progress to 100% when done
94
+
95
+ // Play success animation
96
+ document.body.classList.add('ai-success');
97
+ setTimeout(() => {
98
+ document.body.classList.remove('ai-success');
99
+ }, 1000);
100
 
101
  // --- Post-stream processing ---
102
  if (responseType === 'diff') {
 
122
  setHtml(patchedHtml); // Update editor with the final result
123
  toast.success("AI changes applied");
124
 
125
+ } catch (applyError: unknown) {
126
  console.error("Error applying diffs server-side:", applyError);
127
+ const errorMessage = applyError instanceof Error
128
+ ? applyError.message
129
+ : 'Unknown error applying changes';
130
+ toast.error(`Failed to apply AI changes: ${errorMessage}`);
131
  // Optionally revert to originalHtml? Or leave the editor as is?
132
  // setHtml(originalHtml); // Uncomment to revert on failure
133
  }
 
199
  }
200
 
201
  const chunk = decoder.decode(value, { stream: true });
202
+ totalChunksReceived++;
203
+
204
+ // Update progress indicator with better estimation
205
+ // First 5 chunks are typically slower (setup)
206
+ let estimatedProgress;
207
+ if (totalChunksReceived <= 5) {
208
+ estimatedProgress = Math.floor((totalChunksReceived / 5) * 25); // First 25%
209
+ } else if (totalChunksReceived <= 15) {
210
+ estimatedProgress = 25 + Math.floor(((totalChunksReceived - 5) / 10) * 50); // Next 50%
211
+ } else {
212
+ estimatedProgress = 75 + Math.floor(((totalChunksReceived - 15) / 10) * 20); // Final 25%
213
+ }
214
+ estimatedProgress = Math.min(95, estimatedProgress);
215
+ setStreamProgress(estimatedProgress);
216
+
217
  console.log("[AI Stream] Received chunk of length:", chunk.length);
218
  if (chunk.length > 0) {
219
  console.log("[AI Stream] Sample:", chunk.substring(0, 50) + (chunk.length > 50 ? "..." : ""));
 
232
 
233
  if (newHtml) {
234
  console.log("[AI Full] Found HTML content, length:", newHtml.length);
235
+ // Throttle the re-renders to avoid flashing/flicker - reduced from 300ms to 150ms
236
  const now = Date.now();
237
+ if (now - lastRenderTime > 150) {
238
  // Force-close the HTML tag for preview if needed
239
  let partialDoc = newHtml;
240
  if (!partialDoc.trim().endsWith("</html>")) {
 
261
  } else {
262
  throw new Error("Response body is null");
263
  }
264
+ } catch (error: unknown) {
 
265
  setisAiWorking(false);
266
+ setStreamProgress(0);
267
+
268
+ // Handle the error with proper type checking
269
+ if (error instanceof Error) {
270
+ toast.error(error.message);
271
+ } else if (typeof error === 'object' && error !== null && 'openLogin' in error) {
272
+ const loginError = error as { openLogin: boolean, message?: string };
273
+ toast.error(loginError.message || 'Authentication error');
274
+ if (loginError.openLogin) {
275
+ setOpen(true);
276
+ }
277
+ } else {
278
+ toast.error('An unexpected error occurred');
279
  }
280
  }
281
  };
282
 
283
  return (
284
  <div
285
+ className={`bg-gray-950 rounded-xl py-2 lg:py-2.5 pl-3.5 lg:pl-4 pr-2 lg:pr-2.5 absolute lg:sticky bottom-3 left-3 lg:bottom-4 lg:left-4 w-[calc(100%-1.5rem)] lg:w-[calc(100%-2rem)] z-10 group`}
 
 
286
  >
287
+ {/* Progress indicator */}
288
+ {isAiWorking && (
289
+ <div className="absolute top-0 left-0 h-1.5 bg-gradient-to-r from-purple-500 via-pink-500 to-blue-500 transition-all duration-300 ease-in-out"
290
+ style={{ width: `${streamProgress}%`, borderTopLeftRadius: '0.75rem' }}></div>
291
+ )}
292
+
293
  <div className="w-full relative flex items-center justify-between">
294
+ <RiSparkling2Fill className={`text-lg lg:text-xl ${isAiWorking ? 'text-pink-500 animate-pulse' : 'text-gray-500'} group-focus-within:text-pink-500`} />
295
  <input
296
  type="text"
297
  disabled={isAiWorking}
 
307
  }
308
  }}
309
  />
310
+ {isAiWorking ? (
311
+ <div className="flex-none flex items-center justify-center rounded-full text-sm font-semibold size-8 text-center bg-pink-500 text-white shadow-sm dark:shadow-highlight/20">
312
+ <div className="animate-spin h-4 w-4 border-2 border-white border-t-transparent rounded-full"></div>
313
+ </div>
314
+ ) : (
315
+ <button
316
+ disabled={isAiWorking}
317
+ className="relative overflow-hidden cursor-pointer flex-none flex items-center justify-center rounded-full text-sm font-semibold size-8 text-center bg-pink-500 hover:bg-pink-400 text-white shadow-sm dark:shadow-highlight/20 disabled:bg-gray-300 disabled:text-gray-500 disabled:cursor-not-allowed disabled:hover:bg-gray-300"
318
+ onClick={callAi}
319
+ >
320
+ <GrSend className="-translate-x-[1px]" />
321
+ </button>
322
+ )}
323
  </div>
324
+
325
+ {/* Response mode indicator */}
326
+ {isAiWorking && responseMode && (
327
+ <div className="absolute -top-7 right-0 text-xs font-medium text-white bg-gray-800/90 backdrop-blur-sm px-3 py-1.5 rounded-full flex items-center gap-1.5 animate-pulse border border-gray-700 shadow-lg">
328
+ <div className="size-2 rounded-full bg-pink-500"></div>
329
+ {responseMode === 'diff' ? 'Applying changes to code' : 'Generating new HTML'}
330
+ </div>
331
+ )}
332
+
333
  <div
334
  className={classNames(
335
  "h-screen w-screen bg-black/20 fixed left-0 top-0 z-10",
src/components/loading/loading.tsx CHANGED
@@ -1,26 +1,92 @@
1
- function Loading() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  return (
3
- <div className="absolute left-0 top-0 h-full w-full flex items-center justify-center bg-white/30 z-20">
4
- <svg
5
- className="size-5 animate-spin text-white"
6
- xmlns="http://www.w3.org/2000/svg"
7
- fill="none"
8
- viewBox="0 0 24 24"
9
- >
10
- <circle
11
- className="opacity-25"
12
- cx="12"
13
- cy="12"
14
- r="10"
15
- stroke="currentColor"
16
- strokeWidth="4"
17
- ></circle>
18
- <path
19
- className="opacity-75"
20
- fill="currentColor"
21
- d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
22
- ></path>
23
- </svg>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  </div>
25
  );
26
  }
 
1
+ import { useState, useEffect } from "react";
2
+
3
+ /**
4
+ * Enhanced loading component with visual feedback
5
+ *
6
+ * Features:
7
+ * - Progress indicator with percentage display
8
+ * - Animated loading spinner
9
+ * - Progress bar visualization
10
+ * - Contextual status messages
11
+ * - Animated ellipsis for active feedback
12
+ *
13
+ * @param progress - Current progress percentage (0-100)
14
+ * @param showDetails - Whether to show detailed status messages
15
+ */
16
+ function Loading({ progress = 0, showDetails = false }: { progress?: number; showDetails?: boolean }) {
17
+ const [dots, setDots] = useState("");
18
+
19
+ // Animated dots for the loading text (provides visual feedback even when progress is unknown)
20
+ useEffect(() => {
21
+ const interval = setInterval(() => {
22
+ setDots(prev => (prev.length >= 3 ? "" : prev + "."));
23
+ }, 400);
24
+ return () => clearInterval(interval);
25
+ }, []);
26
+
27
+ // Get appropriate status message based on current progress
28
+ const getStatusMessage = () => {
29
+ if (progress < 25) return "Analyzing request...";
30
+ if (progress < 50) return "Generating content...";
31
+ if (progress < 75) return "Processing changes...";
32
+ if (progress < 100) return "Finalizing...";
33
+ return "Complete!";
34
+ };
35
+
36
  return (
37
+ <div className="absolute left-0 top-0 h-full w-full flex flex-col items-center justify-center bg-gray-950/90 backdrop-blur-sm z-20">
38
+ <div className="relative">
39
+ {/* Progress circle with animated spin */}
40
+ <svg
41
+ className="size-16 animate-spin text-purple-400"
42
+ xmlns="http://www.w3.org/2000/svg"
43
+ fill="none"
44
+ viewBox="0 0 24 24"
45
+ >
46
+ <circle
47
+ className="opacity-25"
48
+ cx="12"
49
+ cy="12"
50
+ r="10"
51
+ stroke="currentColor"
52
+ strokeWidth="4"
53
+ ></circle>
54
+ <path
55
+ className="opacity-75"
56
+ fill="currentColor"
57
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
58
+ ></path>
59
+ </svg>
60
+
61
+ {/* Progress percentage in center - only shown when progress is known */}
62
+ {progress > 0 && (
63
+ <div className="absolute inset-0 flex items-center justify-center">
64
+ <span className="text-sm font-bold text-white">{progress}%</span>
65
+ </div>
66
+ )}
67
+ </div>
68
+
69
+ <div className="mt-4 flex flex-col items-center">
70
+ {/* Main loading message with animated dots */}
71
+ <p className="text-lg font-bold text-purple-200 mb-1">AI is thinking{dots}</p>
72
+
73
+ {/* Horizontal progress bar - only shown when progress is known */}
74
+ {progress > 0 && (
75
+ <div className="w-48 h-1.5 bg-gray-700 rounded-full overflow-hidden mt-2">
76
+ <div
77
+ className="h-full bg-gradient-to-r from-purple-400 to-pink-500 transition-all duration-300 ease-out"
78
+ style={{ width: `${progress}%` }}
79
+ />
80
+ </div>
81
+ )}
82
+
83
+ {/* Optional detailed status message - shows processing stage */}
84
+ {showDetails && (
85
+ <p className="mt-2 text-xs text-purple-300/80 max-w-xs text-center animate-pulse">
86
+ {getStatusMessage()}
87
+ </p>
88
+ )}
89
+ </div>
90
  </div>
91
  );
92
  }
src/components/preview/preview.tsx CHANGED
@@ -6,11 +6,13 @@ function Preview({
6
  html,
7
  isResizing,
8
  isAiWorking,
 
9
  ref,
10
  }: {
11
  html: string;
12
  isResizing: boolean;
13
  isAiWorking: boolean;
 
14
  ref: React.RefObject<HTMLDivElement | null>;
15
  }) {
16
  const iframeRef = useRef<HTMLIFrameElement | null>(null);
@@ -39,6 +41,7 @@ function Preview({
39
  })}
40
  srcDoc={html}
41
  />
 
42
  <button
43
  className="bg-gray-950 shadow-md text-white text-xs lg:text-sm font-medium absolute bottom-3 lg:bottom-5 right-3 lg:right-5 py-2 px-3 lg:px-4 rounded-lg flex items-center gap-2 border border-gray-900 hover:brightness-150 transition-all duration-100 cursor-pointer"
44
  onClick={handleRefreshIframe}
 
6
  html,
7
  isResizing,
8
  isAiWorking,
9
+ children,
10
  ref,
11
  }: {
12
  html: string;
13
  isResizing: boolean;
14
  isAiWorking: boolean;
15
+ children?: React.ReactNode;
16
  ref: React.RefObject<HTMLDivElement | null>;
17
  }) {
18
  const iframeRef = useRef<HTMLIFrameElement | null>(null);
 
41
  })}
42
  srcDoc={html}
43
  />
44
+ {children}
45
  <button
46
  className="bg-gray-950 shadow-md text-white text-xs lg:text-sm font-medium absolute bottom-3 lg:bottom-5 right-3 lg:right-5 py-2 px-3 lg:px-4 rounded-lg flex items-center gap-2 border border-gray-900 hover:brightness-150 transition-all duration-100 cursor-pointer"
47
  onClick={handleRefreshIframe}