Skip to content

Commit 8bbeb31

Browse files
authored
Add files via upload
1 parent b40ebde commit 8bbeb31

3 files changed

Lines changed: 112 additions & 28 deletions

File tree

js/app.js

Lines changed: 58 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
return;
1010
}
1111

12-
const { Styles, Icon, SettingsModal, StatusBar, ChatMessage, LogsModal, CodePreview, HistoryModal } = window.VC.Components;
12+
const { Styles, Icon, SettingsModal, StatusBar, ChatMessage, LogsModal, CodePreview, HistoryModal, VibeStarters } = window.VC.Components;
1313
const Utils = window.VC.Utils;
1414

1515
// --- DEFAULT TEMPLATE FOR VIRTUAL MODE ---
@@ -147,7 +147,7 @@ button:active { transform: translateY(1px); }`,
147147
apiUrl: 'http://localhost:1234/v1',
148148
model: 'local-model',
149149
mode: 'auto',
150-
temperature: null, // Default to null (Use Model Default)
150+
temperature: null,
151151
});
152152

153153
const [setupDone, setSetupDone] = useState(false);
@@ -165,8 +165,12 @@ button:active { transform: translateY(1px); }`,
165165
const [showLogs, setShowLogs] = useState(false);
166166

167167
const [logs, setLogs] = useState([]);
168-
const [previewLogs, setPreviewLogs] = useState([]); // Separate logs for the Preview Console
168+
const [previewLogs, setPreviewLogs] = useState([]);
169169

170+
// Auto-Fix State
171+
const [autoFixCount, setAutoFixCount] = useState(0);
172+
const MAX_AUTO_RETRIES = 3;
173+
170174
// Session Logging
171175
const [sessionLogText, setSessionLogText] = useState('');
172176
const [sessionLogFileName, setSessionLogFileName] = useState('');
@@ -262,6 +266,7 @@ button:active { transform: translateY(1px); }`,
262266

263267
const abortControllerRef = useRef(null);
264268
const activeAssistantMsgIdRef = useRef(null);
269+
const autoFixTimeoutRef = useRef(null);
265270

266271
const filesRef = useRef(files);
267272
const dirHandleRef = useRef(dirHandle);
@@ -290,13 +295,32 @@ button:active { transform: translateY(1px); }`,
290295
setShowScrollButton(!isNearBottom);
291296
};
292297

293-
// Preview Event Handling
298+
// Preview Event Handling & AUTO-FIX TRIGGER
294299
useEffect(() => {
295300
const handler = (e) => {
296301
if (e.data?.type === 'iframe-error') {
297-
console.error("Preview Error:", e.data.message);
298-
setRuntimeError(e.data.message);
299-
appendSessionLog('preview.iframe_error', { message: e.data.message }).catch(() => {});
302+
const errMsg = e.data.message;
303+
console.error("Preview Error:", errMsg);
304+
setRuntimeError(errMsg);
305+
appendSessionLog('preview.iframe_error', { message: errMsg }).catch(() => {});
306+
307+
// AUTO-FIX LOGIC
308+
// Only trigger if idle, not already fixing too much, and it's a real error
309+
if (autoFixTimeoutRef.current) clearTimeout(autoFixTimeoutRef.current);
310+
autoFixTimeoutRef.current = setTimeout(() => {
311+
setAppStatus(prevStatus => {
312+
if (prevStatus === 'idle' && autoFixCount < MAX_AUTO_RETRIES) {
313+
log('info', `Auto-fixing runtime error (Attempt ${autoFixCount + 1}/${MAX_AUTO_RETRIES})`);
314+
setAutoFixCount(c => c + 1);
315+
// Trigger the fix
316+
handleSend(`I detected a runtime error in the preview:\n"${errMsg}"\n\nPlease analyze the code and apply a fix. Do not rewrite the entire application if a small patch works.`);
317+
} else if (autoFixCount >= MAX_AUTO_RETRIES) {
318+
log('warn', 'Max auto-fix retries reached. Stopping.');
319+
setStatusMsg('Automatic fixes paused. Please check manually.');
320+
}
321+
return prevStatus;
322+
});
323+
}, 1000); // Small debounce to let things settle
300324
}
301325
if (e.data?.type === 'iframe-point') {
302326
setPointEvents(prev => {
@@ -312,7 +336,7 @@ button:active { transform: translateY(1px); }`,
312336
};
313337
window.addEventListener('message', handler);
314338
return () => window.removeEventListener('message', handler);
315-
}, [appendSessionLog]);
339+
}, [appendSessionLog, autoFixCount]); // depend on autoFixCount to know when to stop
316340

317341
// Auto-Save
318342
const saveTimerRef = useRef(null);
@@ -444,11 +468,18 @@ NO PYTHON. NO MARKDOWN FENCES.`;
444468
return false;
445469
};
446470

447-
const handleSend = async () => {
471+
const handleSend = async (overrideInput) => {
448472
if (appStatus !== 'idle') { handleStop(); return; }
449-
if ((!input.trim() && !attachments.length)) return;
473+
474+
const txt = overrideInput || input;
475+
// If manually sending, reset auto-fix count
476+
if (!overrideInput) {
477+
setAutoFixCount(0);
478+
}
479+
480+
if ((!txt.trim() && !attachments.length)) return;
450481

451-
const userText = input;
482+
const userText = txt;
452483
const userMsgId = makeId();
453484
const assistantMsgId = makeId();
454485
activeAssistantMsgIdRef.current = assistantMsgId;
@@ -458,7 +489,7 @@ NO PYTHON. NO MARKDOWN FENCES.`;
458489
} catch {}
459490

460491
if (Object.keys(files).length > 0) {
461-
setHistory(prev => [...prev, { timestamp: Date.now(), files: JSON.parse(JSON.stringify(files)), prompt: input || "Upload" }]);
492+
setHistory(prev => [...prev, { timestamp: Date.now(), files: JSON.parse(JSON.stringify(files)), prompt: txt || "Upload" }]);
462493
setCurrentVersionIndex(history.length);
463494
}
464495

@@ -477,13 +508,17 @@ NO PYTHON. NO MARKDOWN FENCES.`;
477508
let promptSuffix = assetNotices.length > 0 ? `\n\nAVAILABLE ASSETS:\n${assetNotices.map(p => `- ${p}`).join('\n')}` : "";
478509

479510
if (attachments.length > 0) {
480-
userContent = [{ type: "text", text: (input || "Analyze images.") + promptSuffix }];
511+
userContent = [{ type: "text", text: (txt || "Analyze images.") + promptSuffix }];
481512
attachments.forEach(att => userContent.push({ type: "image_url", image_url: { url: att.data } }));
482513
} else {
483-
userContent = (input || '') + promptSuffix;
514+
userContent = (txt || '') + promptSuffix;
484515
}
485516

486-
const displayUserText = (input || '[Images Uploaded]') + (assetNotices.length ? `\n[+ Added ${assetNotices.length} assets]` : '');
517+
const displayUserText = (txt || '[Images Uploaded]') + (assetNotices.length ? `\n[+ Added ${assetNotices.length} assets]` : '');
518+
519+
// Don't show "I detected a runtime error..." as a User bubble if it's auto-fix,
520+
// or do show it so they know what's happening?
521+
// User vibe: Transparency is good. "System detected error" is fine.
487522

488523
setMessages(prev => ([
489524
...prev,
@@ -512,7 +547,7 @@ NO PYTHON. NO MARKDOWN FENCES.`;
512547
}).join('\n\n');
513548

514549
let contextString = `PROJECT CONTEXT (CURRENT FILES):\n${contextFiles}\n\nUSER REQUEST: ${userText}`;
515-
if (runtimeError) contextString += `\n\n!!! DETECTED RUNTIME ERROR IN PREVIEW !!!\nError: ${runtimeError}\nPLEASE FIX THIS ERROR.`;
550+
if (runtimeError && !overrideInput) contextString += `\n\n!!! DETECTED RUNTIME ERROR IN PREVIEW !!!\nError: ${runtimeError}\nPLEASE FIX THIS ERROR.`;
516551
if (pointEvents.length) contextString += `\n\nPOINT & VIBE SELECTIONS:\n` + pointEvents.map((p, idx) => `#${idx + 1}: tag=<${p.tag}> text="${(p.text||'').slice(0,50)}"`).join('\n');
517552

518553
await appendSessionLog('request.context_files', contextFilesList);
@@ -740,6 +775,12 @@ NO PYTHON. NO MARKDOWN FENCES.`;
740775
</div>
741776
<div className="flex-1 overflow-y-auto p-4 space-y-4 custom-scrollbar" ref=${chatContainerRef} onScroll=${handleChatScroll}>
742777
${messages.map(m => html`<${ChatMessage} key=${m.id} msg=${m} />`)}
778+
${messages.length === 1 && html`
779+
<div className="flex flex-col items-center justify-center py-6">
780+
<p className="text-gray-500 text-sm mb-2">No idea? Try a vibe starter:</p>
781+
<${VibeStarters} onSelect=${(prompt) => handleSend(prompt)} />
782+
</div>
783+
`}
743784
<div ref=${msgsEndRef}></div>
744785
</div>
745786
${showScrollButton && html`<button onClick=${() => msgsEndRef.current?.scrollIntoView({ behavior: 'smooth' })} className="absolute bottom-[220px] right-6 p-2 bg-gray-800 border border-gray-700 text-white rounded-full shadow-lg shadow-black/50 hover:bg-gray-700 transition z-20"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"></polyline></svg></button>`}
@@ -752,7 +793,7 @@ NO PYTHON. NO MARKDOWN FENCES.`;
752793
<input type="file" id="file-upload" multiple accept="image/*" className="hidden" onChange=${handleFileSelect} />
753794
<label for="file-upload" className="p-2 text-gray-500 hover:text-blue-400 cursor-pointer transition" title="Attach Image"><${Icon} name="Image" /></label>
754795
<textarea value=${input} onInput=${e => setInput(e.target.value)} onKeyDown=${e => e.key === 'Enter' && !e.shiftKey && (e.preventDefault(), handleSend())} className="flex-1 bg-transparent text-sm outline-none resize-none text-gray-200 max-h-32 py-2" rows=${1} style=${{minHeight: '24px'}} placeholder="Ask to change something..." />
755-
<button onClick=${handleSend} disabled=${(!input.trim() && !attachments.length) && appStatus === 'idle'} className=${`p-2 rounded-lg shadow-lg transition flex-shrink-0 ${appStatus !== 'idle' ? 'bg-red-600 hover:bg-red-500 text-white shadow-red-900/20' : 'bg-blue-600 hover:bg-blue-500 text-white shadow-blue-600/20 disabled:opacity-50 disabled:cursor-not-allowed'}`}><${Icon} name=${appStatus !== 'idle' ? 'Stop' : 'Send'} /></button>
796+
<button onClick=${() => handleSend()} disabled=${(!input.trim() && !attachments.length) && appStatus === 'idle'} className=${`p-2 rounded-lg shadow-lg transition flex-shrink-0 ${appStatus !== 'idle' ? 'bg-red-600 hover:bg-red-500 text-white shadow-red-900/20' : 'bg-blue-600 hover:bg-blue-500 text-white shadow-blue-600/20 disabled:opacity-50 disabled:cursor-not-allowed'}`}><${Icon} name=${appStatus !== 'idle' ? 'Stop' : 'Send'} /></button>
756797
</div>
757798
</div>
758799
</div>

js/components.js

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@
2929
color: white;
3030
background: rgba(168, 85, 247, 0.05);
3131
}
32+
.vibe-chip {
33+
transition: all 0.2s;
34+
border: 1px solid rgba(168, 85, 247, 0.2);
35+
}
36+
.vibe-chip:hover {
37+
transform: translateY(-2px);
38+
border-color: rgba(168, 85, 247, 0.6);
39+
background: rgba(168, 85, 247, 0.1);
40+
box-shadow: 0 4px 12px rgba(168, 85, 247, 0.15);
41+
}
3242
input[type=range] {
3343
-webkit-appearance: none;
3444
background: transparent;
@@ -83,11 +93,35 @@
8393
Tablet: '<rect x="4" y="2" width="16" height="20" rx="2" ry="2"></rect><line x1="12" y1="18" x2="12.01" y2="18"></line>',
8494
Monitor: '<rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect><line x1="8" y1="21" x2="16" y2="21"></line><line x1="12" y1="17" x2="12" y2="21"></line>',
8595
Rotate: '<path d="M23 4v6h-6"></path><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>',
86-
Terminal: '<polyline points="4 17 10 11 4 5"></polyline><line x1="12" y1="19" x2="20" y2="19"></line>'
96+
Terminal: '<polyline points="4 17 10 11 4 5"></polyline><line x1="12" y1="19" x2="20" y2="19"></line>',
97+
Wand: '<path d="M14.5 2c1.5 0 4 1.5 4 4.5s-2.5 4.5-4 4.5c-1.5 0-4-1.5-4-4.5s2.5-4.5 4-4.5z"></path><path d="M14.5 11c0 2 1.5 3 4.5 3s4.5-1 4.5-3"></path><line x1="8" y1="13" x2="2" y2="19"></line><line x1="2" y1="13" x2="8" y2="19"></line>'
8798
};
8899
return html`<svg width=${size} height=${size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className=${className} dangerouslySetInnerHTML=${{__html: paths[name] || ''}}></svg>`;
89100
};
90101

102+
const VibeStarters = ({ onSelect }) => {
103+
const templates = [
104+
{ emoji: "🕹️", label: "Retro Snake Game", prompt: "Build a neon-style retro snake game with score tracking and particle effects." },
105+
{ emoji: "🎨", label: "Portfolio Site", prompt: "Create a modern, minimal portfolio website with a dark theme and smooth scroll animations." },
106+
{ emoji: "🍅", label: "Pomodoro Timer", prompt: "Code a beautiful Pomodoro timer with ambient background sounds and a circular progress indicator." },
107+
{ emoji: "🎹", label: "Synth Keyboard", prompt: "Make a browser-based synthesizer keyboard that I can play with my mouse or keys." },
108+
{ emoji: "📊", label: "Dashboard", prompt: "Design a futuristic data dashboard with charts and glassmorphism effects." }
109+
];
110+
111+
return html`
112+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 mt-6 max-w-lg w-full">
113+
${templates.map(t => html`
114+
<button onClick=${() => onSelect(t.prompt)} className="vibe-chip bg-gray-900/50 p-4 rounded-xl text-left flex items-center gap-4 group">
115+
<span className="text-2xl group-hover:scale-110 transition duration-300 filter grayscale group-hover:grayscale-0">${t.emoji}</span>
116+
<div>
117+
<div className="text-sm font-semibold text-gray-200 group-hover:text-purple-300 transition">${t.label}</div>
118+
</div>
119+
</button>
120+
`)}
121+
</div>
122+
`;
123+
};
124+
91125
const StatusBar = ({ status, message }) => {
92126
if ((!status || status === 'idle') && !message) return null;
93127
let iconName = 'Sparkles';
@@ -120,12 +154,7 @@
120154
const thinking = (m.thinking || '').toString();
121155
const output = (m.output != null ? m.output : (m.content || '')).toString();
122156
const isStreaming = !!m.isStreaming;
123-
const [thinkingOpen, setThinkingOpen] = useState(isStreaming && thinking.length > 0);
124-
125-
// Auto-open thinking when it starts streaming
126-
useEffect(() => {
127-
if (isStreaming && thinking.length > 0 && thinking.length < 50) setThinkingOpen(true);
128-
}, [isStreaming, thinking]);
157+
const [thinkingOpen, setThinkingOpen] = useState(false); // Default to CLOSED for vibecoders
129158

130159
if (isUser) {
131160
return html`
@@ -159,7 +188,6 @@
159188
<div className="text-[12px] leading-relaxed opacity-80 whitespace-pre-wrap font-serif">${thinking}</div>
160189
</div>
161190
`}
162-
${!thinkingOpen && html`<div className="mt-1 pl-3 text-[10px] text-gray-600 truncate opacity-50 font-serif italic">${thinkingHint}...</div>`}
163191
</div>
164192
`}
165193
<div className="text-sm whitespace-pre-wrap break-words">${output}</div>
@@ -239,7 +267,9 @@
239267
const containerRef = useRef(null);
240268
const iframeRef = useRef(null);
241269

242-
// ... (FileTree Logic matches original)
270+
// Check for errors to decide whether to show the console toggle
271+
const errorCount = (previewLogs || []).filter(l => l.level === 'error').length;
272+
243273
const buildFileTree = () => {
244274
const root = {};
245275
Object.keys(files || {}).forEach(path => {
@@ -354,7 +384,9 @@
354384
</div>
355385
<button onClick=${() => setIsPlaying(!isPlaying)} className=${`p-2 rounded transition ${isPlaying ? 'text-blue-400 hover:bg-blue-500/10' : 'text-gray-500 hover:text-gray-300'}`} title=${isPlaying ? "Pause" : "Play"}><${Icon} name=${isPlaying ? "Pause" : "Play"} size=${18} /></button>
356386
<button onClick=${handleReload} className="p-2 text-gray-400 hover:text-white hover:bg-gray-800 rounded transition" title="Reload"><${Icon} name="Refresh" size=${18} /></button>
357-
<button onClick=${() => setShowConsole(v => !v)} className=${`flex items-center gap-1 px-2 py-1 rounded text-[10px] uppercase tracking-wide font-semibold transition flex-shrink-0 border ${showConsole ? 'bg-gray-800 text-white border-gray-600' : 'text-gray-500 hover:text-gray-200 border-transparent hover:border-gray-700'}`} title="Toggle Console">Console ${(previewLogs || []).filter(l=>l.level==='error').length > 0 ? html`<span className="w-2 h-2 rounded-full bg-red-500"></span>` : ''}</button>
387+
<button onClick=${() => setShowConsole(v => !v)} className=${`flex items-center gap-1 px-2 py-1 rounded text-[10px] uppercase tracking-wide font-semibold transition flex-shrink-0 border ${showConsole ? 'bg-gray-800 text-white border-gray-600' : 'text-gray-500 hover:text-gray-200 border-transparent hover:border-gray-700'}`} title="Show Console">
388+
${errorCount > 0 ? html`<span className="text-red-400 flex items-center gap-1 animate-pulse"><${Icon} name="Alert" size=${12} /> ${errorCount}</span>` : "Console"}
389+
</button>
358390
</div>
359391
`}
360392
<div className="flex-1"></div>
@@ -474,5 +506,5 @@
474506
};
475507

476508
window.VC = window.VC || {};
477-
window.VC.Components = { Styles, Icon, SettingsModal, StatusBar, ChatMessage, StreamDock, LogDock, LogsModal, CodePreview, HistoryModal };
509+
window.VC.Components = { Styles, Icon, SettingsModal, StatusBar, ChatMessage, StreamDock, LogDock, LogsModal, CodePreview, HistoryModal, VibeStarters };
478510
})();

js/utils.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,17 @@
550550
(function() {
551551
window.__VC_POINT_VIBE_ENABLED__ = false;
552552
553+
// Polyfill for common storage check errors in virtual environments
554+
window.isStorageAvailable = function(type) {
555+
try {
556+
var storage = window[type];
557+
var x = '__storage_test__';
558+
storage.setItem(x, x);
559+
storage.removeItem(x);
560+
return true;
561+
} catch(e) { return false; }
562+
};
563+
553564
function sendLog(type, args) {
554565
try {
555566
if (window.parent) {

0 commit comments

Comments
 (0)