Spaces:
Running
Running
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 +62 -0
- src/assets/index.css +57 -0
- src/components/App.tsx +5 -1
- src/components/ask-ai/ask-ai.tsx +82 -21
- src/components/loading/loading.tsx +88 -22
- src/components/preview/preview.tsx +3 -0
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 |
-
//
|
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:
|
111 |
console.error("Error applying diffs server-side:", applyError);
|
112 |
-
|
|
|
|
|
|
|
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 >
|
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 |
-
|
232 |
-
} catch (error: any) {
|
233 |
setisAiWorking(false);
|
234 |
-
|
235 |
-
|
236 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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=
|
249 |
<input
|
250 |
type="text"
|
251 |
disabled={isAiWorking}
|
@@ -261,14 +307,29 @@ function AskAI({
|
|
261 |
}
|
262 |
}}
|
263 |
/>
|
264 |
-
|
265 |
-
|
266 |
-
|
267 |
-
|
268 |
-
|
269 |
-
<
|
270 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2 |
return (
|
3 |
-
<div className="absolute left-0 top-0 h-full w-full flex items-center justify-center bg-
|
4 |
-
<
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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}
|