deepsite-ai-builder / script.js
Vehicoule's picture
# DeepSite - Complete Project Setup
83cb890 verified
(() => {
// State
let chats = loadChats();
let currentChatId = null;
let abortController = null;
let currentModel = null;
let modelLoading = false;
let previewVisible = false;
let generatedHTML = '';
let selectedLocalModel = null;
// Templates for different project types
const templates = {
vibecode: {
name: "VibeCode App",
description: "Modern web app with vibecoding aesthetics",
structure: {
html: `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VibeApp</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
.gradient-bg { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
.glass { backdrop-filter: blur(16px) saturate(180%); background-color: rgba(255, 255, 255, 0.75); }
.neon { text-shadow: 0 0 10px currentColor; }
</style>
</head>
<body class="gradient-bg min-h-screen">
<div class="container mx-auto px-4 py-8">
<header class="text-center mb-12">
<h1 class="text-4xl font-bold text-white mb-4 neon">VibeApp</h1>
<p class="text-white/80">Modern vibes, clean code, smooth UX</p>
</header>
<div class="grid md:grid-cols-3 gap-6">
<div class="glass rounded-lg p-6 shadow-lg">
<h3 class="text-lg font-semibold mb-3">Dashboard</h3>
<p class="text-sm opacity-80">Clean overview of your data</p>
</div>
<div class="glass rounded-lg p-6 shadow-lg">
<h3 class="text-lg font-semibold mb-3">Analytics</h3>
<p class="text-sm opacity-80">Real-time insights</p>
</div>
<div class="glass rounded-lg p-6 shadow-lg">
<h3 class="text-lg font-semibold mb-3">Settings</h3>
<p class="text-sm opacity-80">Configure your vibe</p>
</div>
</div>
</div>
</body>
</html>`,
js: `// VibeApp JavaScript
console.log('VibeApp loaded!');
document.addEventListener('DOMContentLoaded', function() {
// Add smooth animations
const cards = document.querySelectorAll('.glass');
cards.forEach((card, index) => {
card.style.opacity = '0';
card.style.transform = 'translateY(20px)';
setTimeout(() => {
card.style.transition = 'all 0.5s ease';
card.style.opacity = '1';
card.style.transform = 'translateY(0)';
}, index * 100);
});
});`
}
},
website: {
name: "Clean Website",
description: "Professional business website",
structure: {
html: `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Professional Website</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-white">
<nav class="bg-white shadow-sm border-b">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex items-center">
<h1 class="text-xl font-bold text-gray-900">YourBrand</h1>
</div>
<div class="hidden md:flex items-center space-x-8">
<a href="#" class="text-gray-600 hover:text-gray-900">Home</a>
<a href="#" class="text-gray-600 hover:text-gray-900">About</a>
<a href="#" class="text-gray-600 hover:text-gray-900">Services</a>
<a href="#" class="text-gray-600 hover:text-gray-900">Contact</a>
</div>
</div>
</div>
</nav>
<main>
<section class="bg-gradient-to-r from-blue-600 to-purple-600 text-white">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-24">
<div class="text-center">
<h1 class="text-5xl font-bold mb-6">Professional Solutions</h1>
<p class="text-xl mb-8">We create amazing digital experiences</p>
<button class="bg-white text-blue-600 px-8 py-3 rounded-lg font-semibold hover:bg-gray-100 transition">
Get Started
</button>
</div>
</div>
</section>
</main>
</body>
</html>`,
js: `// Professional Website JavaScript
document.addEventListener('DOMContentLoaded', function() {
console.log('Website loaded successfully');
});`
}
},
saas: {
name: "SaaS Dashboard",
description: "Analytics & admin panel",
structure: {
html: `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SaaS Dashboard</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body class="bg-gray-50">
<div class="min-h-screen flex">
<!-- Sidebar -->
<div class="w-64 bg-white shadow-sm">
<div class="p-6">
<h2 class="text-xl font-bold text-gray-800">Dashboard</h2>
</div>
<nav class="px-4 space-y-2">
<a href="#" class="block px-3 py-2 text-gray-600 rounded-md bg-blue-50">Overview</a>
<a href="#" class="block px-3 py-2 text-gray-600 hover:bg-gray-50 rounded-md">Analytics</a>
<a href="#" class="block px-3 py-2 text-gray-600 hover:bg-gray-50 rounded-md">Users</a>
<a href="#" class="block px-3 py-2 text-gray-600 hover:bg-gray-50 rounded-md">Settings</a>
</nav>
</div>
<!-- Main Content -->
<div class="flex-1">
<header class="bg-white shadow-sm border-b px-6 py-4">
<h1 class="text-2xl font-semibold text-gray-900">Overview</h1>
</header>
<main class="p-6">
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div class="bg-white p-6 rounded-lg shadow-sm">
<h3 class="text-sm font-medium text-gray-500">Total Users</h3>
<p class="text-2xl font-semibold text-gray-900">1,234</p>
</div>
<div class="bg-white p-6 rounded-lg shadow-sm">
<h3 class="text-sm font-medium text-gray-500">Revenue</h3>
<p class="text-2xl font-semibold text-gray-900">$12,345</p>
</div>
<div class="bg-white p-6 rounded-lg shadow-sm">
<h3 class="text-sm font-medium text-gray-500">Growth</h3>
<p class="text-2xl font-semibold text-gray-900">+23%</p>
</div>
<div class="bg-white p-6 rounded-lg shadow-sm">
<h3 class="text-sm font-medium text-gray-500">Churn</h3>
<p class="text-2xl font-semibold text-gray-900">-2.1%</p>
</div>
</div>
<div class="bg-white rounded-lg shadow-sm p-6">
<h3 class="text-lg font-medium text-gray-900 mb-4">Revenue Chart</h3>
<canvas id="revenueChart" width="400" height="200"></canvas>
</div>
</main>
</div>
</div>
</body>
</html>`,
js: `// SaaS Dashboard JavaScript
document.addEventListener('DOMContentLoaded', function() {
// Initialize chart
const ctx = document.getElementById('revenueChart').getContext('2d');
new Chart(ctx, {
type: 'line',
data: {
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
datasets: [{
label: 'Revenue',
data: [12000, 19000, 15000, 25000, 22000, 30000],
borderColor: 'rgb(59, 130, 246)',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
tension: 0.4
}]
},
options: {
responsive: true,
scales: {
y: {
beginAtZero: true
}
}
}
});
});`
}
}
};
// Elements
const els = {
mobileMenuBtn: document.getElementById('mobile-menu-btn'),
chatList: document.getElementById('chat-list'),
newChatBtn: document.getElementById('new-chat'),
exportChatBtn: document.getElementById('export-chat'),
togglePreviewBtn: document.getElementById('toggle-preview'),
messages: document.getElementById('messages'),
welcome: document.getElementById('welcome'),
prompt: document.getElementById('prompt'),
sendBtn: document.getElementById('send-btn'),
stopBtn: document.getElementById('stop-btn'),
hfToken: document.getElementById('hf-token'),
modelSelect: document.getElementById('model-select'),
templateSelect: document.getElementById('template-select'),
statusText: document.getElementById('status-text'),
modelStatus: document.getElementById('model-status'),
previewPanel: document.getElementById('preview-panel'),
previewFrame: document.getElementById('preview-frame'),
refreshPreview: document.getElementById('refresh-preview'),
openPreview: document.getElementById('open-preview'),
closePreview: document.getElementById('close-preview'),
};
// Init
init();
function init() {
// Load saved settings
const savedToken = localStorage.getItem('hf_token') || '';
els.hfToken.value = savedToken;
// Events
els.newChatBtn.addEventListener('click', newChat);
els.exportChatBtn.addEventListener('click', exportCurrentChat);
els.togglePreviewBtn.addEventListener('click', togglePreview);
els.sendBtn.addEventListener('click', onSend);
els.stopBtn.addEventListener('click', stopGeneration);
els.prompt.addEventListener('keydown', onPromptKeyDown);
els.hfToken.addEventListener('change', () => {
localStorage.setItem('hf_token', els.hfToken.value.trim());
toast('Hugging Face token saved.');
});
els.modelSelect.addEventListener('change', () => {
localStorage.setItem('selected_model', els.modelSelect.value);
updateModelStatus('Model changed. Will load on first use.');
handleModelSelectionChange();
});
els.templateSelect.addEventListener('change', () => {
localStorage.setItem('selected_template', els.templateSelect.value);
});
// Local model upload events
els.uploadModelBtn.addEventListener('click', () => {
els.modelFileInput.click();
});
els.modelFileInput.addEventListener('change', handleModelFileSelect);
// Template selection events
els.templateSelect.addEventListener('change', () => {
localStorage.setItem('selected_template', els.templateSelect.value);
});
// Enhanced button interactions
addButtonInteractions();
// Preview events
els.refreshPreview.addEventListener('click', refreshPreview);
els.openPreview.addEventListener('click', openPreviewInWindow);
els.closePreview.addEventListener('click', togglePreview);
// Restore settings
const savedModel = localStorage.getItem('selected_model');
if (savedModel) els.modelSelect.value = savedModel;
const savedLocalModel = localStorage.getItem('selected_local_model');
if (savedLocalModel) {
selectedLocalModel = JSON.parse(savedLocalModel);
updateLocalModelDisplay();
}
const savedTemplate = localStorage.getItem('selected_template');
if (savedTemplate) els.templateSelect.value = savedTemplate;
// Render chat list
renderChatList();
// Auto-select last chat or create new
if (chats.length > 0) {
openChat(chats[chats.length - 1].id);
} else {
newChat();
}
// Auto-resize textarea
autoResize(els.prompt);
els.prompt.addEventListener('input', () => autoResize(els.prompt));
}
function addButtonInteractions() {
// Add ripple effect to all buttons
document.querySelectorAll('button').forEach(button => {
button.addEventListener('click', function(e) {
const ripple = document.createElement('span');
const rect = this.getBoundingClientRect();
const size = Math.max(rect.width, rect.height);
const x = e.clientX - rect.left - size / 2;
const y = e.clientY - rect.top - size / 2;
ripple.style.width = ripple.style.height = size + 'px';
ripple.style.left = x + 'px';
ripple.style.top = y + 'px';
ripple.classList.add('ripple');
this.appendChild(ripple);
setTimeout(() => {
ripple.remove();
}, 600);
});
});
}
// Add CSS for ripple effect
const style = document.createElement('style');
style.textContent = `
.ripple {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.3);
transform: scale(0);
animation: ripple-animation 0.6s linear;
pointer-events: none;
}
@keyframes ripple-animation {
to {
transform: scale(4);
opacity: 0;
}
}
`;
document.head.appendChild(style);
function loadChats() {
try {
return JSON.parse(localStorage.getItem('deepsite_chats') || '[]');
} catch {
return [];
}
}
function saveChats() {
localStorage.setItem('deepsite_chats', JSON.stringify(chats));
}
function renderChatList() {
els.chatList.innerHTML = '';
if (chats.length === 0) {
els.chatList.innerHTML = '<li class="text-sm text-gray-500 px-3 py-2">No chats yet</li>';
return;
}
chats.forEach(c => {
const li = document.createElement('li');
li.className = 'group';
li.innerHTML = `
<button class="w-full text-left px-3 py-2 rounded-md hover:bg-gray-100 ${currentChatId === c.id ? 'bg-gray-100' : ''}">
<div class="flex items-start justify-between">
<div class="truncate text-sm">
${escapeHTML(c.title || 'Untitled')}
</div>
<div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition">
<button data-action="rename" class="p-1 hover:bg-gray-200 rounded" title="Rename">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 text-gray-500" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-width="2" d="M4 21h4l10.5-10.5a1.5 1.5 0 000-2.12l-2.88-2.88a1.5 1.5 0 00-2.12 0L3 16v5z"/>
</svg>
</button>
<button data-action="delete" class="p-1 hover:bg-gray-200 rounded" title="Delete">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 text-red-600" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-width="2" d="M3 6h18M8 6V4h8v2M6 6l1 14h10l1-14"/>
</svg>
</button>
</div>
</div>
<div class="text-[11px] text-gray-500">${formatDate(c.createdAt)}</div>
</button>
`;
li.addEventListener('click', (e) => {
const actBtn = e.target.closest('button[data-action]');
if (actBtn) {
e.stopPropagation();
const action = actBtn.getAttribute('data-action');
if (action === 'rename') renameChat(c.id);
if (action === 'delete') deleteChat(c.id);
return;
}
openChat(c.id);
});
els.chatList.appendChild(li);
});
}
function newChat() {
const id = uid();
const chat = { id, title: 'New Chat', createdAt: Date.now(), messages: [] };
chats.push(chat);
saveChats();
renderChatList();
openChat(id);
}
function openChat(id) {
currentChatId = id;
const chat = chats.find(c => c.id === id);
if (!chat) return;
renderMessages(chat);
renderChatList();
els.prompt.focus();
}
function deleteChat(id) {
const idx = chats.findIndex(c => c.id === id);
if (idx === -1) return;
chats.splice(idx, 1);
saveChats();
renderChatList();
if (currentChatId === id) {
if (chats.length > 0) openChat(chats[chats.length - 1].id);
else newChat();
}
}
function renameChat(id) {
const chat = chats.find(c => c.id === id);
if (!chat) return;
const title = prompt('Rename chat:', chat.title);
if (title && title.trim()) {
chat.title = title.trim();
saveChats();
renderChatList();
}
}
function renderMessages(chat) {
els.messages.innerHTML = '';
if (!chat.messages || chat.messages.length === 0) {
els.messages.appendChild(els.welcome);
els.welcome.classList.remove('hidden');
} else {
els.welcome.classList.add('hidden');
chat.messages.forEach(m => {
const el = renderMessage(m);
els.messages.appendChild(el);
});
scrollToBottom();
}
}
function renderMessage(message) {
const wrap = document.createElement('div');
wrap.className = `message ${message.role}`;
const avatar = document.createElement('div');
avatar.className = 'avatar';
avatar.textContent = message.role === 'user' ? 'U' : 'AI';
const bubble = document.createElement('div');
bubble.className = 'bubble';
bubble.innerHTML = escapeHTML(message.content || '');
wrap.appendChild(avatar);
wrap.appendChild(bubble);
return wrap;
}
async function onSend() {
const text = els.prompt.value.trim();
if (!text) return;
// Ensure chat exists
if (!currentChatId) newChat();
const chat = chats.find(c => c.id === currentChatId);
// Show status
setStatus('Thinking...');
// Append user message
const userMsg = { role: 'user', content: text };
chat.messages.push(userMsg);
const userEl = renderMessage(userMsg);
els.messages.appendChild(userEl);
els.welcome.classList.add('hidden');
scrollToBottom();
// Clear prompt
els.prompt.value = '';
autoResize(els.prompt);
// Prepare assistant message placeholder
const assistantMsg = { role: 'assistant', content: '', type: 'code' };
chat.messages.push(assistantMsg);
const assistantEl = renderMessage(assistantMsg);
assistantEl.querySelector('.bubble').innerHTML = '';
els.messages.appendChild(assistantEl);
// UI state
els.sendBtn.disabled = true;
els.stopBtn.classList.remove('hidden');
setStatus('Generating code...');
// Create enhanced prompt for code generation
const template = els.templateSelect.value;
const enhancedPrompt = createEnhancedPrompt(text, template);
try {
// Simulate AI response with template-based generation
const generatedCode = await generateCode(enhancedPrompt, template);
if (generatedCode) {
assistantMsg.content = generatedCode;
const bubble = assistantEl.querySelector('.bubble');
bubble.innerHTML = formatCodeDisplay(generatedCode);
// Auto-update preview if visible
if (previewVisible) {
updatePreview(generatedCode);
}
// Add "Run Preview" button
addPreviewButton(assistantEl, generatedCode);
}
} catch (error) {
console.error('Generation error:', error);
toast('Generation failed. Please try again.');
const bubble = assistantEl.querySelector('.bubble');
if (bubble && !bubble.textContent.trim()) {
bubble.textContent = 'Sorry, something went wrong while generating the code.';
}
} finally {
setStatus('Ready');
els.sendBtn.disabled = false;
els.stopBtn.classList.add('hidden');
saveChats();
}
}
function createEnhancedPrompt(userInput, template) {
const systemPrompts = {
vibecode: `You are an expert full-stack developer specializing in modern, trendy web applications with vibecoding aesthetics. Create clean, responsive code using:
- Tailwind CSS for styling
- Modern JavaScript (ES6+)
- Gradient backgrounds and glassmorphism effects
- Smooth animations and transitions
- Mobile-first responsive design
Generate complete, functional HTML code for: ${userInput}`,
website: `You are a professional web developer creating clean, modern websites. Focus on:
- Semantic HTML structure
- Tailwind CSS for professional styling
- Responsive design principles
- Accessibility best practices
- Clean, maintainable code
Generate complete HTML for: ${userInput}`,
saas: `You are a SaaS developer creating admin dashboards and business applications. Create:
- Professional dashboard layouts
- Data visualization components
- User management interfaces
- Analytics and reporting features
- Modern UI/UX patterns
Generate complete HTML for: ${userInput}`,
landing: `You are a landing page specialist creating conversion-optimized pages. Focus on:
- Hero sections with compelling CTAs
- Feature showcases
- Social proof sections
- Contact/lead capture forms
- Mobile-optimized designs
Generate complete HTML for: ${userInput}`,
portfolio: `You are a portfolio designer creating impressive personal websites. Create:
- Creative layouts showcasing projects
- About sections with personality
- Contact forms and social links
- Interactive elements
- Performance-optimized code
Generate complete HTML for: ${userInput}`,
ecommerce: `You are an e-commerce developer creating online store interfaces. Build:
- Product catalog layouts
- Shopping cart functionality
- Checkout processes
- Search and filtering
- Modern e-commerce UX
Generate complete HTML for: ${userInput}`,
blog: `You are a blog/CMS developer creating content-focused websites. Create:
- Clean reading layouts
- Article metadata
- Comment systems
- Author profiles
- Content organization
Generate complete HTML for: ${userInput}`
};
return systemPrompts[template] || systemPrompts.vibecode;
}
async function generateCode(prompt, template) {
// For demo purposes, return template-based generated code
// In a real implementation, this would call the local model
// Simulate processing time
await new Promise(resolve => setTimeout(resolve, 1500));
const templateStruct = templates[template] || templates.vibecode;
// Enhance the template based on user input
let generatedHTML = templateStruct.structure.html;
let generatedJS = templateStruct.structure.js;
// Simple keyword replacement based on user input
const keywords = prompt.toLowerCase();
if (keywords.includes('dark') || keywords.includes('night')) {
generatedHTML = generatedHTML.replace('bg-white', 'bg-gray-900')
.replace('text-gray-900', 'text-white')
.replace('text-gray-600', 'text-gray-300');
}
if (keywords.includes('colorful') || keywords.includes('rainbow')) {
generatedHTML = generatedHTML.replace(
'background: linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
'background: linear-gradient(135deg, #ff6b6b, #4ecdc4, #45b7d1, #96ceb4, #feca57)'
);
}
if (keywords.includes('dashboard') || keywords.includes('analytics')) {
generatedHTML = generatedHTML.replace('VibeApp', 'DataViz Dashboard')
.replace('Modern vibes, clean code, smooth UX', 'Real-time insights and analytics');
}
return {
html: generatedHTML,
js: generatedJS,
preview: generatedHTML.replace('</body>', `<script>${generatedJS}</script></body>`)
};
}
function formatCodeDisplay(codeObj) {
return `
<div class="code-generation">
<div class="mb-4">
<h4 class="font-semibold text-sm mb-2 flex items-center gap-2">
<span class="w-2 h-2 bg-green-500 rounded-full"></span>
Generated HTML
</h4>
<div class="bg-gray-50 p-3 rounded border text-xs overflow-x-auto">
<pre><code>${escapeHTML(codeObj.html)}</code></pre>
</div>
</div>
<div class="mb-4">
<h4 class="font-semibold text-sm mb-2 flex items-center gap-2">
<span class="w-2 h-2 bg-blue-500 rounded-full"></span>
JavaScript
</h4>
<div class="bg-gray-50 p-3 rounded border text-xs overflow-x-auto">
<pre><code>${escapeHTML(codeObj.js)}</code></pre>
</div>
</div>
<button onclick="runPreview('${escapeHTML(codeObj.preview)}')" class="w-full bg-neon-600 text-white px-4 py-2 rounded hover:bg-neon-700 transition">
🚀 Run Live Preview
</button>
</div>
`;
}
function addPreviewButton(assistantEl, codeObj) {
// Already handled in formatCodeDisplay
}
function runPreview(previewHTML) {
generatedHTML = previewHTML;
if (!previewVisible) {
togglePreview();
}
updatePreview(previewHTML);
}
function togglePreview() {
previewVisible = !previewVisible;
if (previewVisible) {
els.previewPanel.classList.remove('hidden');
els.togglePreviewBtn.textContent = 'Hide Preview';
if (generatedHTML) {
updatePreview(generatedHTML);
}
} else {
els.previewPanel.classList.add('hidden');
els.togglePreviewBtn.textContent = 'Preview';
}
}
function updatePreview(htmlContent) {
// Create a complete HTML document if needed
if (!htmlContent.includes('<!DOCTYPE') && !htmlContent.includes('<html')) {
htmlContent = `<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Generated Code</title></head>${htmlContent}</html>`;
}
// Update iframe
els.previewFrame.srcdoc = htmlContent;
toast('Preview updated!');
}
function refreshPreview() {
if (generatedHTML) {
updatePreview(generatedHTML);
toast('Preview refreshed!');
}
}
function openPreviewInWindow() {
if (generatedHTML) {
const newWindow = window.open('', '_blank');
newWindow.document.write(generatedHTML);
newWindow.document.close();
}
}
function stopGeneration() {
abortController = null;
els.stopBtn.classList.add('hidden');
els.sendBtn.disabled = false;
setStatus('Stopped');
}
function onPromptKeyDown(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
onSend();
}
}
function exportCurrentChat() {
const chat = chats.find(c => c.id === currentChatId);
if (!chat) return;
const data = {
title: chat.title,
model: els.modelSelect.value,
createdAt: chat.createdAt,
messages: chat.messages
};
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${sanitizeFilename(chat.title || 'chat')}.json`;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
}
function scrollToBottom() {
els.messages.scrollTo({ top: els.messages.scrollHeight, behavior: 'smooth' });
}
function autoResize(el) {
el.style.height = 'auto';
el.style.height = Math.min(el.scrollHeight, 200) + 'px';
}
function setStatus(text) {
els.statusText.textContent = text;
}
function toast(msg) {
// Simple non-blocking toast
const t = document.createElement('div');
t.textContent = msg;
t.className = 'fixed bottom-4 left-1/2 -translate-x-1/2 px-4 py-2 bg-gray-900 text-white text-sm rounded-md shadow-lg z-[60]';
document.body.appendChild(t);
setTimeout(() => t.remove(), 2200);
}
function updateModelStatus(msg) {
els.modelStatus.textContent = msg ? `• ${msg}` : '';
}
// Quick template functions
window.quickTemplate = function(template) {
els.templateSelect.value = template;
const templateInfo = templates[template];
if (templateInfo) {
els.prompt.value = `Create a ${templateInfo.description}`;
autoResize(els.prompt);
}
};
window.runPreview = runPreview;
function uid() {
return Math.random().toString(36).slice(2) + Date.now().toString(36);
}
function escapeHTML(str) {
return String(str).replace(/[&<>"']/g, s => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
}[s]));
}
function formatDate(ts) {
try {
return new Date(ts).toLocaleString();
} catch {
return '';
}
}
function sanitizeFilename(name) {
return String(name).replace(/[^\w\-]+/g, '_').slice(0, 64);
}
async
function safeText(res) {
try { return await res.text(); } catch { return ''; }
}
})();