Logo

SmartIDE

AI-First Development

Logo

Gateway Key

SmartIDE v1.0

Logo

SmartIDE

skAIxu Flow
Gemini 2.0 Flash · Surgical Edits
Build
index.html
`; // ─── STATE ─────────────────────────────────────── const state = { kaixuKey: localStorage.getItem('KAIXU_VIRTUAL_KEY') || '', currentCode: DEFAULT_CODE, originalCode: DEFAULT_CODE, viewMode: 'current', isProcessing: false, history: [], historyIdx: 0, queue: [], conversation: [], fileSystem: { 'Default Project': { 'index.html': { type: 'file', content: DEFAULT_CODE } } }, activeProject: localStorage.getItem('sk_active_project') || 'Default Project', activeFile: localStorage.getItem('sk_active_filepath') || 'index.html', selectedElement: null, activeTab: 'editor', debounceTimer: null }; // ─── IDB STORAGE ──────────────────────────────── const idb = { _db: null, async open() { if (this._db) return this._db; return new Promise((res, rej) => { const r = indexedDB.open('SmartIDE_DB', 1); r.onupgradeneeded = () => r.result.createObjectStore('kv'); r.onsuccess = () => { this._db = r.result; res(this._db); }; r.onerror = () => rej(r.error); }); }, async get(k) { const db = await this.open(); return new Promise((res, rej) => { const r = db.transaction('kv').objectStore('kv').get(k); r.onsuccess=()=>res(r.result); r.onerror=()=>rej(r.error); }); }, async set(k, v) { const db = await this.open(); return new Promise((res, rej) => { const r = db.transaction('kv','readwrite').objectStore('kv').put(v,k); r.onsuccess=()=>res(); r.onerror=()=>rej(r.error); }); } }; // ─── DOM CACHE ─────────────────────────────────── const el = {}; function cache() { ['chat-messages','user-input','send-btn','send-icon','code-editor','preview-iframe', 'mode-toggle','mode-label','file-tree','filename-label','key-modal','key-form','key-input', 'btn-undo','btn-redo','btn-orig','btn-curr','tab-editor','tab-preview','tab-files', 'view-editor','view-preview','view-files','preview-controls','workspace-preview', 'sel-indicator','sel-tag','telemetry-status','telemetry-budget','telemetry-brain', 'ai-status-dot','ai-panel','resize-handle','workspace-panel','splash' ].forEach(id => { const e = document.getElementById(id); if (e) el[id] = e; }); } // ─── KAIXU GATEWAY CLIENT (NON-STREAM) ────────── async function kaixuChat(key, payload) { broadcastLog(`Request: ${payload.provider}/${payload.model} | ${payload.messages.length} msgs`, 'info'); const bases = [GATEWAY_BASE]; if (_isLocal) bases.push(GATEWAY_DIRECT); let lastErr; for (const base of bases) { try { const res = await fetch(`${base}/.netlify/functions/gateway-chat`, { method: 'POST', headers: { 'Authorization': `Bearer ${key}`, 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (!res.ok) { const body = await res.text().catch(() => ''); if (_isLocal && base === GATEWAY_LOCAL && res.status === 404) { broadcastLog('Proxy miss, trying direct...', 'warn'); continue; } let msg = body.slice(0, 300) || `HTTP ${res.status}`; if (res.status === 401) msg = 'Invalid or missing Kaixu Key'; else if (res.status === 402) msg = 'Monthly cap reached'; else if (res.status === 429) msg = 'Rate limited'; throw new Error(`[${base}] ${msg}`); } const data = await res.json(); broadcastLog(`Response OK from ${base}`, 'success'); return data; } catch (err) { lastErr = err; if (base !== bases[bases.length - 1]) { broadcastLog(`Fallback: ${err.message}`, 'warn'); continue; } throw lastErr; } } if (lastErr) throw lastErr; } // ─── SMART EDIT ENGINE ─────────────────────────── // The core innovation: AI returns surgical SEARCH/REPLACE // blocks instead of entire files. Falls back gracefully. // // Format the AI uses: // <<>> // <<>> // // Multiple blocks can be chained. // Normalize malformed delimiters the AI might produce // e.g., <>>, >>, ```html wrapping, etc. function normalizeDelimiters(text) { // STEP 1: Normalize chevron count FIRST (before fence stripping) text = text.replace(/<{1,5}\s*SEARCH\b/gi, '<<{1,5}/gi, 'SEARCH>>>'); text = text.replace(/<{1,5}\s*REPLACE\b/gi, '<<{1,5}/gi, 'REPLACE>>>'); // STEP 2: Strip code fences that wrap SEARCH/REPLACE blocks // Now that chevrons are normalized, look for the canonical format inside fences text = text.replace(/```[\w]*\s*\n([\s\S]*?<<>>[\s\S]*?)\n?```/gi, '$1'); // Also strip any remaining code fences if SEARCH/REPLACE exist outside them if (/<<>>/.test(text)) { text = text.replace(/```[\w]*\s*\n?/g, '').replace(/\n?```/g, ''); } // STEP 3: Handle <<>> (closing >>> on same keyword) → <<>>/g, '<<>>/g, '<<>>\s*\n<<>>/g, /<<>>\n([\s\S]*?)\n<<<\/SEARCH>>>\s*\n<<>>\n([\s\S]*?)\n<<<\/REPLACE>>>/g, /<<<\s*SEARCH\s*\n([\s\S]*?)\n\s*SEARCH\s*>>>\s*\n<<<\s*REPLACE\s*\n([\s\S]*?)\n\s*REPLACE\s*>>>/g, /SEARCH:\s*```[\w]*\n([\s\S]*?)\n```\s*REPLACE:\s*```[\w]*\n([\s\S]*?)\n```/g ]; const edits = []; for (const regex of patterns) { let m; regex.lastIndex = 0; while ((m = regex.exec(normalized)) !== null) { edits.push({ search: m[1], replace: m[2] }); } if (edits.length > 0) break; } // Diagnostic: if no edits found but response looks like it tried SEARCH/REPLACE if (edits.length === 0 && /SEARCH|REPLACE/i.test(aiResponse)) { const rawDelims = aiResponse.match(/.{0,40}(?:SEARCH|REPLACE).{0,20}/gi)?.slice(0, 5); broadcastLog(`⚠ AI attempted SEARCH/REPLACE but parser failed. Normalized (first 300): ${normalized.substring(0, 300).replace(/\n/g, '⏎')} | Raw delimiters: ${rawDelims?.join(' | ') || 'none'}`, 'warn'); } if (edits.length > 0) { let modified = currentCode; let applied = 0; let failed = 0; const details = []; for (const edit of edits) { // Try exact match first let idx = modified.indexOf(edit.search); // If exact fails, try trimmed match (AI sometimes adds/removes trailing whitespace) if (idx === -1) { const searchTrimmed = edit.search.split('\n').map(l => l.trimEnd()).join('\n'); const modifiedTrimmed = modified.split('\n').map(l => l.trimEnd()).join('\n'); const trimIdx = modifiedTrimmed.indexOf(searchTrimmed); if (trimIdx !== -1) { // Find the real position by counting chars up to the trimmed position const linesBeforeTrim = modifiedTrimmed.substring(0, trimIdx).split('\n').length - 1; const linesInSearch = edit.search.split('\n').length; const realLines = modified.split('\n'); const beforeStr = realLines.slice(0, linesBeforeTrim).join('\n') + (linesBeforeTrim > 0 ? '\n' : ''); const oldStr = realLines.slice(linesBeforeTrim, linesBeforeTrim + linesInSearch).join('\n'); modified = beforeStr + edit.replace + '\n' + realLines.slice(linesBeforeTrim + linesInSearch).join('\n'); applied++; details.push({ status: 'ok', line: linesBeforeTrim + 1, preview: edit.replace.split('\n')[0].trim().substring(0, 60) }); continue; } } if (idx !== -1) { modified = modified.substring(0, idx) + edit.replace + modified.substring(idx + edit.search.length); applied++; const lineNum = modified.substring(0, idx).split('\n').length; details.push({ status: 'ok', line: lineNum, preview: edit.replace.split('\n')[0].trim().substring(0, 60) }); } else { failed++; details.push({ status: 'fail', preview: edit.search.split('\n')[0].trim().substring(0, 60) }); } } if (applied > 0) { return { code: modified, method: 'surgical', applied, failed, total: edits.length, details }; } } // Strategy 2: Fenced code block (full file) // Only use this if the response does NOT contain SEARCH/REPLACE attempts const hasSRAttempt = /SEARCH|REPLACE/i.test(aiResponse); const fenced = aiResponse.match(/```(?:html|HTML)?\s*\n([\s\S]*?)\n```/); if (fenced && fenced[1].trim().length > 50 && !hasSRAttempt) { let code = fenced[1].trim(); return { code, method: 'full-replace', applied: 0, failed: 0, total: 0, details: [] }; } // Strategy 3: Raw HTML extraction const htmlIdx = aiResponse.search(/ const closeIdx = code.lastIndexOf(''); if (closeIdx !== -1) code = code.substring(0, closeIdx + 7); return { code, method: 'raw-extract', applied: 0, failed: 0, total: 0, details: [] }; } return null; // No code found } // Extract explanation text (everything outside code blocks and edit blocks) function extractExplanation(aiResponse) { let text = aiResponse; // Remove SEARCH/REPLACE blocks text = text.replace(/<<>>/g, ''); // Remove fenced code blocks text = text.replace(/```[\s\S]*?```/g, ''); // Remove raw HTML if it starts with doctype const htmlIdx = text.search(/${state.selectedElement.id ? ' id="'+state.selectedElement.id+'"' : ''}${state.selectedElement.className ? ' class="'+state.selectedElement.className.substring(0,100)+'"' : ''}\n HTML: ${elHtml}\n\nYour SEARCH block MUST contain lines from the current file that include this element. Find it in the code below and modify it.\n`; // Build a concrete example using the actual selected element elementExample = `\nEXAMPLE for this specific selection — if user says "make it orange":\n\n<<>>\n<<>>\n`; } // For large files, send a summary + targeted section instead of the whole thing let codeSection; if (state.currentCode.length > 20000) { const lines = state.currentCode.split('\n'); const head = lines.slice(0, 60).join('\n'); const tail = lines.slice(-40).join('\n'); codeSection = `[FILE IS LARGE: ${lines.length} lines, ${state.currentCode.length} chars]\n--- FIRST 60 LINES ---\n${head}\n--- LAST 40 LINES ---\n${tail}\n--- Use SEARCH/REPLACE with exact text from the visible lines above. ---`; } else { codeSection = state.currentCode; } return `You are skAIxu Flow, the AI code editor inside SmartIDE. You edit the user's EXISTING file surgically. You NEVER create new files or rewrite from scratch. PROJECT: ${state.activeProject} | FILE: ${state.activeFile} | ALL FILES: ${fileList} ${elementCtx} ══════════════════════════════════════════════════════ OUTPUT FORMAT — MANDATORY — USE EXACTLY THIS: ══════════════════════════════════════════════════════ The delimiter is THREE angle brackets: <<< and >>> <<>> <<>> CRITICAL DELIMITER SYNTAX: - Opening: three less-than signs then keyword: <<>> and REPLACE>>> - NO spaces between <<< and the keyword - Each on its own line - You can chain multiple blocks for multiple changes. EXAMPLE 1 — Changing a heading color: << Hello World SEARCH>>> << Hello World REPLACE>>> Changed the gradient from indigo/purple to emerald/green. EXAMPLE 2 — Adding a Google Font import: << SEARCH>>> << REPLACE>>> Added Roboto font import after charset meta tag. ${elementExample} RULES: 1. SEARCH text must EXACTLY match lines in the file below (same whitespace, same indentation). 2. Keep SEARCH blocks small (3-8 lines) — just enough to uniquely find the spot. 3. NEVER output the entire file. Only output the <<>> blocks for the parts that change. 4. Add a 1-line explanation after your blocks. 5. To ADD code: find nearby existing lines for SEARCH, include them + your new lines in REPLACE. 6. To DELETE: put the lines to remove in SEARCH, leave REPLACE with just the surrounding context. 7. ONLY use a \`\`\`html code block when creating a brand-new file from nothing. 8. NEVER wrap your <<>> output inside \`\`\`html or any markdown code fence. Output the raw <<>> delimiters directly — no wrapping. 9. Use EXACTLY three angle brackets: <<< and >>>. Not two, not one, not four. Three. ══════════════════════════════════════════════════════ THE USER'S CURRENT FILE (copy SEARCH text from here): ══════════════════════════════════════════════════════ ${codeSection}`; } function buildConsultPrompt() { const fileList = Object.keys(state.fileSystem[state.activeProject] || {}).join(', '); const codePreview = state.currentCode.substring(0, 12000); return `You are skAIxu Flow, expert coding advisor in SmartIDE. Project: ${state.activeProject} | File: ${state.activeFile} | Files: ${fileList} Current code (may be truncated): \`\`\` ${codePreview} \`\`\` Be concise, specific, reference the code directly. Use markdown for formatting. Use code snippets in backticks.`; } // ─── CHAT UI ───────────────────────────────────── function addMsg(role, html) { if (!el['chat-messages']) return null; const wrap = document.createElement('div'); wrap.className = `flex ${role === 'user' ? 'justify-end' : role === 'system' ? 'justify-center' : 'justify-start'} ani-fade`; if (role === 'assistant') { wrap.innerHTML = `
AI skAIxu Flow
${html}
`; } else if (role === 'user') { wrap.innerHTML = `
${escHtml(html)}
`; } else { wrap.innerHTML = `
${html}
`; } el['chat-messages'].appendChild(wrap); el['chat-messages'].scrollTop = el['chat-messages'].scrollHeight; // Return the content div for later updates return role === 'assistant' ? wrap.querySelector('.msg-ai') : null; } function addEditCard(result) { if (!el['chat-messages']) return; const wrap = document.createElement('div'); wrap.className = 'ani-fade'; if (result.method === 'surgical') { const detailsHtml = result.details.map(d => d.status === 'ok' ? `
Line ~${d.line}: ${escHtml(d.preview)}
` : `
Not found: ${escHtml(d.preview)}
` ).join(''); wrap.innerHTML = `
Surgical Edit — ${result.applied}/${result.total} applied ${result.failed > 0 ? `${result.failed} missed` : ''}
${detailsHtml}
`; } else { wrap.innerHTML = `
Full Replace ${result.method}
`; } el['chat-messages'].appendChild(wrap); el['chat-messages'].scrollTop = el['chat-messages'].scrollHeight; lucide.createIcons(); } function escHtml(s) { return String(s).replace(/&/g,'&').replace(//g,'>'); } // ─── TOAST ─────────────────────────────────────── function toast(msg, type = 'info', ms = 3000) { const t = document.createElement('div'); t.className = `toast toast-${type}`; t.innerHTML = `${msg}`; document.body.appendChild(t); setTimeout(() => { t.style.opacity = '0'; t.style.transition = 'opacity 0.3s'; setTimeout(() => t.remove(), 300); }, ms); } // ─── PROCESSING STATE ──────────────────────────── function setProcessing(on) { state.isProcessing = on; const icon = el['send-icon']; if (icon) { icon.setAttribute('data-lucide', on ? 'loader' : 'send'); if (on) icon.classList.add('ani-spin'); else icon.classList.remove('ani-spin'); lucide.createIcons(); } const dot = el['ai-status-dot']; if (dot) { dot.className = on ? 'ml-2 w-2 h-2 rounded-full bg-indigo-500 shadow-[0_0_6px_#6366f1] animate-pulse' : 'ml-2 w-2 h-2 rounded-full bg-emerald-500 shadow-[0_0_6px_#10b981]'; } if (el['telemetry-status']) { el['telemetry-status'].innerText = on ? `Working (${state.queue.length} queued)` : 'Idle'; el['telemetry-status'].className = on ? 'text-indigo-400 animate-pulse' : 'text-slate-600'; } } function updateBudget(month) { if (el['telemetry-budget'] && month) { const rem = (month.cap_cents - month.spent_cents) / 100; el['telemetry-budget'].innerText = `Budget: $${rem.toFixed(2)}`; } } // ─── FILE SYSTEM ───────────────────────────────── function fsGet(proj, path) { const parts = path.split('/').filter(Boolean); let cur = state.fileSystem[proj]; if (!cur) return null; for (let i = 0; i < parts.length; i++) { if (!cur[parts[i]]) return null; if (i === parts.length - 1) return cur[parts[i]]; if (cur[parts[i]].type === 'folder') cur = cur[parts[i]].children; else return null; } return null; } async function fsSet(proj, path, entry) { const parts = path.split('/').filter(Boolean); const name = parts.pop(); if (!state.fileSystem[proj]) state.fileSystem[proj] = {}; let cur = state.fileSystem[proj]; for (const p of parts) { if (!cur[p]) cur[p] = { type: 'folder', children: {} }; cur = cur[p].children; } cur[name] = entry; await idb.set('smartide_fs', state.fileSystem); } async function fsDel(proj, path) { const parts = path.split('/').filter(Boolean); const name = parts.pop(); let cur = state.fileSystem[proj]; for (const p of parts) cur = cur[p].children; delete cur[name]; await idb.set('smartide_fs', state.fileSystem); renderFiles(); if (state.activeFile.startsWith(path)) { state.activeFile = 'index.html'; if (!fsGet(proj, 'index.html')) await fsSet(proj, 'index.html', { type: 'file', content: DEFAULT_CODE }); loadFile(); } } async function fsRename(proj, oldPath, newName) { const entry = fsGet(proj, oldPath); if (!entry) return; const parts = oldPath.split('/').filter(Boolean); const oldName = parts.pop(); let parent = state.fileSystem[proj]; for (const p of parts) parent = parent[p].children; delete parent[oldName]; const newPath = [...parts, newName].join('/'); await fsSet(proj, newPath, entry); if (state.activeFile === oldPath) { state.activeFile = newPath; localStorage.setItem('sk_active_filepath', newPath); } renderFiles(); } function loadFile() { const entry = fsGet(state.activeProject, state.activeFile); state.currentCode = (entry && entry.type === 'file') ? entry.content : DEFAULT_CODE; if (el['code-editor']) el['code-editor'].value = state.currentCode; if (el['filename-label']) el['filename-label'].innerText = state.activeFile; state.history = [state.currentCode]; state.historyIdx = 0; updatePreview(); updateHistoryBtns(); } function renderFiles() { const tree = el['file-tree']; if (!tree) return; tree.innerHTML = ''; Object.keys(state.fileSystem).forEach(proj => { const d = document.createElement('div'); d.innerHTML = `
${proj}
`; const content = document.createElement('div'); content.className = 'pl-3 ml-2 border-l border-white/5'; renderTreeLevel(content, state.fileSystem[proj], '', proj); d.appendChild(content); tree.appendChild(d); }); lucide.createIcons(); } function renderTreeLevel(container, obj, path, proj) { Object.keys(obj).forEach(key => { const item = obj[key]; const itemPath = path ? `${path}/${key}` : key; const div = document.createElement('div'); if (item.type === 'folder') { div.innerHTML = `
${key}
`; const sub = document.createElement('div'); sub.className = 'pl-3 ml-1 border-l border-white/5'; renderTreeLevel(sub, item.children, itemPath, proj); div.appendChild(sub); } else { const isActive = state.activeFile === itemPath; div.innerHTML = `
${key}
`; } container.appendChild(div); }); } // ─── PREVIEW ───────────────────────────────────── function updatePreview() { if (!el['preview-iframe']) return; const html = state.viewMode === 'original' ? state.originalCode : state.currentCode; const inject = `