Ready
Project Files
Project
0%
Wacht op input
Flow (live) Chain: code-gen
Input Analyseren Plan Code Toepassen Klaar
Typ een opdracht in de chat →
index.html
style.css
script.js
Sessie Sessies blijven na refresh. Deel link →zelfde sessie in Orchestrator / Live Build.
Klant / bedrijfsnaam (optioneel)
Plan
Tasks
Skeleton
Blueprint
Code
Preview
Flow
Offerte
Bibliotheek

Plan verschijnt hier na je opdracht (stap 1).

Taken verschijnen hier (stap 2).

Structuur/skeleton na code-generatie.

Blauwdruk / design-notities.

Opgeslagen functies en stijlen uit de chat. Klik op Bibliotheek om te laden.

Maak een offerte vanuit het Plan-tab (knop «Maak offerte uit dit plan») of bekijk hier de laatst gegenereerde offerte.

Flow (grote weergave)
InputAnalyserenPlanCodeToepassenKlaar
Workflow
Analyseren → branche ophalen via SBI resolve + get_branch_info (keywords, diensten, GMB).
Plan + Code → krijgen branch_data mee voor sector-specifieke content (Strawberry-structuur + ~19k datapunten).
AI Agent – antwoorden naast preview
AI Agent Geactiveerd
Auto (standaard): de chain kiest automatisch – alleen chat (vraag → antwoord) of plan + code bouwen. Er is één doorlopend gesprek.
Plan + code bouwen: Ask → Plan → Agent: plan, taken, code, preview. Tabs: Plan → Tasks → Skeleton → Blueprint → Code → Preview → Flow.

Mijn Skills:
HTML/CSS/JS JavaScript Video/Media React PHP API's UI/UX
Tip: klik op een snelkeuze hieronder of typ je eigen opdracht. AI genereert direct code en preview.
Modus:
Wat wil je maken?
Snel (onderdelen):
Modus (chain auto + meer)
Plan/code – max tokens
Tokenverbruik (gratis = Groq, Google, Perplexity)
Laden…
Complete website voor klant
Positie balk
Spraaktaal (microfoon)
Spraak antwoord (TTS)
Snelheid:

Standaard: alleen afspelen als je op 🔊 bij een antwoord klikt.

Wat wil je maken? (klik = direct versturen)
Wat wil je maken?
`; } function generateAIOrchestratorTemplate() { return ` AI Orchestrator - IT Live

🔍 Bedrijf Zoeken

Zoek een bedrijf om te beginnen.

🤖 AI Pipeline

1. Business Analyse
2. Content Strategie
3. Website Structuur
4. Content Generatie
5. SEO Plan
6. Markt Analyse
7. Implementatie Plan

Selecteer een bedrijf om de pipeline te starten.

📊 Analytics

AI Agents: 12

Success Rate: 98%

Avg Response: 0.3s

`; } function generateSBIAnalyzerTemplate() { return ` SBI Analyzer - IT Live

📊 SBI Analyzer

Marktanalyse en concurrentieonderzoek voor SBI codes

Markt Grootte

Concurrentie

📈 Analyse Resultaten

Voer een SBI code in om de analyse te starten.

`; } // Enhanced prompt builder with new features function enhancePromptWithFeatures(basePrompt, features = {}) { let enhancedPrompt = basePrompt; if (features.kvkIntegration) { enhancedPrompt += ' Gebruik de KvK API voor bedrijfsgegevens en SBI informatie.'; } if (features.aiOrchestration) { enhancedPrompt += ' Integreer AI pipeline stappen voor analyse en content generatie.'; } if (features.sbiAnalysis) { enhancedPrompt += ' Voeg SBI marktanalyse en concurrentieonderzoek toe.'; } if (features.schemaMarkup) { enhancedPrompt += ' Genereer JSON-LD schema markup voor SEO.'; } if (features.mediaServer) { enhancedPrompt += ' Integreer media server functionaliteit voor video en bestanden.'; } if (features.customerPortal) { enhancedPrompt += ' Bouw een customer portal met dashboard en project management.'; } if (features.unifiedConcept) { enhancedPrompt += ' Gebruik unified concept generator met SBI data integratie.'; } if (features.cacheOptimization) { enhancedPrompt += ' Implementeer caching voor betere performance en kostenbesparing.'; } return enhancedPrompt; } // Auto-detect features from prompt function detectFeaturesFromPrompt(prompt) { const features = {}; const lowerPrompt = prompt.toLowerCase(); if (lowerPrompt.includes('kvk') || lowerPrompt.includes('bedrijfs') || lowerPrompt.includes('bedrijvencheck')) { features.kvkIntegration = true; } if (lowerPrompt.includes('orchestrator') || lowerPrompt.includes('pipeline') || lowerPrompt.includes('ai')) { features.aiOrchestration = true; } if (lowerPrompt.includes('sbi') || lowerPrompt.includes('markt') || lowerPrompt.includes('concurrentie')) { features.sbiAnalysis = true; } if (lowerPrompt.includes('schema') || lowerPrompt.includes('json-ld') || lowerPrompt.includes('seo')) { features.schemaMarkup = true; } if (lowerPrompt.includes('media') || lowerPrompt.includes('video') || lowerPrompt.includes('bestanden')) { features.mediaServer = true; } if (lowerPrompt.includes('customer') || lowerPrompt.includes('portal') || lowerPrompt.includes('dashboard')) { features.customerPortal = true; } if (lowerPrompt.includes('concept') || lowerPrompt.includes('unified') || lowerPrompt.includes('generator')) { features.unifiedConcept = true; } if (lowerPrompt.includes('cache') || lowerPrompt.includes('performance') || lowerPrompt.includes('optimalisatie')) { features.cacheOptimization = true; } return features; } // Enhanced template generator based on detected features function generateEnhancedTemplate(prompt) { const features = detectFeaturesFromPrompt(prompt); if (features.kvkIntegration) { return generateBedrijvencheckTemplate(); } else if (features.aiOrchestration) { return generateAIOrchestratorTemplate(); } else if (features.sbiAnalysis) { return generateSBIAnalyzerTemplate(); } return null; // Fall back to default behavior } function saveFilesToStorage() { try { sessionStorage.setItem(AUTO_SAVE_KEY, JSON.stringify(files)); } catch (e) {} } function loadFilesFromStorage() { try { var raw = sessionStorage.getItem(AUTO_SAVE_KEY); if (!raw) return false; var loaded = JSON.parse(raw); if (loaded && typeof loaded === 'object' && (loaded['index.html'] || loaded['style.css'] || loaded['script.js'])) { if (loaded['index.html']) files['index.html'] = loaded['index.html']; if (loaded['style.css']) files['style.css'] = loaded['style.css']; if (loaded['script.js']) files['script.js'] = loaded['script.js']; return true; } } catch (e) {} return false; } require.config({ paths: { vs: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.44.0/min/vs' }}); require(['vs/editor/editor.main'], function() { var hadAuto = loadFilesFromStorage(); editor = monaco.editor.create(document.getElementById('editor'), { value: files[activeFile], language: 'html', theme: 'vs-dark', automaticLayout: true }); editor.onDidChangeModelContent(() => { files[activeFile] = editor.getValue(); updatePreview(); saveFilesToStorage(); }); if (hadAuto && activeFile in files) editor.setValue(files[activeFile]); updatePreview(); }); function switchCenterTab(view) { document.querySelectorAll('#centerTabs .tab').forEach(x => x.classList.remove('active')); const t = document.querySelector('#centerTabs .tab[data-view="' + view + '"]'); if (t) t.classList.add('active'); document.querySelectorAll('.center-pane').forEach(p => p.classList.remove('active')); const pane = document.getElementById('pane-' + view); if (pane) pane.classList.add('active'); if (view === 'preview') updatePreview(); if (view === 'library') loadLibrarySnippets(); if (view === 'code' && typeof editor !== 'undefined' && editor) { setTimeout(function() { editor.layout(); }, 50); } } function loadLibrarySnippets() { var el = document.getElementById('contentLibrary'); if (!el) return; el.innerHTML = '

Laden…

'; fetch('/api/ai-code-editor-api.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'list_snippets' }) }) .then(function(r) { return r.json(); }) .then(function(d) { if (!d.success || !d.data.snippets || d.data.snippets.length === 0) { el.innerHTML = '

Nog geen opgeslagen snippets. Genereer code in de chat en klik daar op "Opslaan in bibliotheek".

'; return; } el.innerHTML = '

Functiebibliotheek

Snel stijlen of code hergebruiken per project.

' + d.data.snippets.map(function(s) { return '
' + escapeHtml(s.name) + '' + escapeHtml(s.type) + '
'; }).join(''); el.querySelectorAll('.btn-apply').forEach(function(btn) { btn.addEventListener('click', function() { var id = this.getAttribute('data-id'); fetch('/api/ai-code-editor-api.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'get_snippet', id: id }) }) .then(function(r) { return r.json(); }) .then(function(res) { if (!res.success || !res.data.snippet) return; var s = res.data.snippet; if (s.type === 'style') { files['style.css'] = (files['style.css'] || '') + '\n\n/* ' + s.name + ' */\n' + s.content; if (activeFile === 'style.css' && editor) editor.setValue(files['style.css']); updatePreview(); } else if (s.type === 'script') { files['script.js'] = (files['script.js'] || '') + '\n\n// ' + s.name + '\n' + s.content; if (activeFile === 'script.js' && editor) editor.setValue(files['script.js']); updatePreview(); } else if (s.type === 'html' && s.content.indexOf(']*>([\s\S]*?)<\/body>/i); if (htmlMatch) files['index.html'] = (files['index.html'] || '').replace(/]*>[\s\S]*/i, htmlMatch[0]); else files['index.html'] = (files['index.html'] || '') + '\n\n' + s.content; if (activeFile === 'index.html' && editor) editor.setValue(files['index.html']); updatePreview(); } else { if (editor) { var pos = editor.getPosition(); editor.executeEdits('', [{ range: { startLineNumber: pos.lineNumber, startColumn: pos.column, endLineNumber: pos.lineNumber, endColumn: pos.column }, text: s.content }]); } } switchCenterTab('preview'); }); }); }); el.querySelectorAll('.btn-send').forEach(function(btn) { btn.addEventListener('click', function() { var id = this.getAttribute('data-id'); fetch('/api/ai-code-editor-api.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'get_snippet', id: id }) }) .then(function(r) { return r.json(); }) .then(function(res) { if (!res.success || !res.data.snippet) return; var input = document.getElementById('input'); if (input) { input.value = 'Pas deze snippet toe: ' + res.data.snippet.name + '\n\n' + (res.data.snippet.content.slice(0, 500) + (res.data.snippet.content.length > 500 ? '…' : '')); input.focus(); } }); }); }); el.querySelectorAll('.btn-snippet-del').forEach(function(btn) { btn.addEventListener('click', function() { if (!confirm('Snippet verwijderen?')) return; var id = this.getAttribute('data-id'); fetch('/api/ai-code-editor-api.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'delete_snippet', id: id }) }) .then(function(r) { return r.json(); }) .then(function(res) { if (res.success) loadLibrarySnippets(); }); }); }); }) .catch(function() { el.innerHTML = '

Kon bibliotheek niet laden.

'; }); } document.querySelectorAll('#centerTabs .tab').forEach(t => { t.addEventListener('click', function() { switchCenterTab(this.dataset.view); }); }); function switchToPreviewTab() { switchCenterTab('preview'); } const planButtonsHtml = '

'; function setPlanContent(html, planTextForOffer) { const el = document.getElementById('contentPlan'); if (el) el.innerHTML = html + planButtonsHtml; if (planTextForOffer !== undefined) lastPlanText = planTextForOffer; const offerRow = document.getElementById('planOfferRow'); if (offerRow) offerRow.style.display = lastPlanText ? 'block' : 'none'; document.getElementById('showPlanTemplate').addEventListener('click', onShowPlanTemplate); document.getElementById('btnMakeOffer').addEventListener('click', makeOffer); } async function onShowPlanTemplate() { let box = document.getElementById('planTemplateBox'); if (box) { box.style.display = box.style.display === 'none' ? 'block' : 'none'; return; } try { const r = await fetch('/api/ai-code-editor-api.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'get_plan_template' }) }); const d = await r.json(); if (d.success && d.data && d.data.template) { box = document.createElement('div'); box.id = 'planTemplateBox'; box.style.marginTop = '1rem'; box.style.padding = '0.75rem'; box.style.background = 'rgba(0,0,0,0.2)'; box.style.borderRadius = '8px'; box.innerHTML = '

Standaard plan-structuur (alle pro planen)

' + escapeHtml(d.data.template) + '
'; document.getElementById('contentPlan').appendChild(box); } } catch (e) { console.error(e); } } function setTasksContent(html) { const el = document.getElementById('contentTasks'); if (el) el.innerHTML = html; } function setSkeletonContent(html) { const el = document.getElementById('contentSkeleton'); if (el) el.innerHTML = html; } function setBlueprintContent(html) { const el = document.getElementById('contentBlueprint'); if (el) el.innerHTML = html; } function extractSkeletonFromHtml(html) { if (!html) return []; const tags = []; const re = /<\/?([a-z][a-z0-9]*)\b/gi; let m; const seen = new Set(); while ((m = re.exec(html)) !== null) { const tag = m[1].toLowerCase(); if (!seen.has(tag) && !['html','head','meta','link','style','script','title'].includes(tag)) { seen.add(tag); tags.push(tag); } } return tags; } function extractJsSummary(js) { if (!js || !js.trim()) return { functions: [], events: [], summary: 'Geen JavaScript' }; const functions = []; const events = []; const fnRe = /function\s+(\w+)\s*\(|(\w+)\s*=\s*function\s*\(|const\s+(\w+)\s*=\s*\([^)]*\)\s*=>/g; let fn; while ((fn = fnRe.exec(js)) !== null) { const name = fn[1] || fn[2] || fn[3]; if (name && functions.indexOf(name) === -1) functions.push(name); } const evRe = /\.addEventListener\s*\(\s*['"](\w+)['"]|\.on(\w+)\s*=|['"](\w+)['"]\s*:\s*function/g; while ((fn = evRe.exec(js)) !== null) { const e = (fn[1] || fn[2] || fn[3]); if (e && events.indexOf(e) === -1) events.push(e); } const summary = functions.length || events.length ? 'Functies: ' + (functions.slice(0, 8).join(', ') || '—') + (events.length ? ' · Events: ' + events.slice(0, 6).join(', ') : '') : 'Script aanwezig (' + js.length + ' tekens)'; return { functions, events, summary }; } function extractMediaFromHtml(html) { if (!html) return []; const items = []; const videoRe = /]/gi; const audioRe = /]/gi; const iframeRe = /]+src\s*=\s*['"]([^'"]+)['"]/gi; const sourceRe = /]+src\s*=\s*['"]([^'"]+)['"]/gi; if (videoRe.test(html)) items.push({ type: 'video', label: 'HTML5 video' }); if (audioRe.test(html)) items.push({ type: 'audio', label: 'HTML5 audio' }); let ifm; while ((ifm = iframeRe.exec(html)) !== null) { const src = (ifm[1] || '').toLowerCase(); const label = src.indexOf('youtube') !== -1 ? 'YouTube' : src.indexOf('vimeo') !== -1 ? 'Vimeo' : 'iframe'; if (!items.some(i => i.src === ifm[1])) items.push({ type: 'iframe', label: label, src: ifm[1] }); } let src; while ((src = sourceRe.exec(html)) !== null) { if (!items.some(i => i.src === src[1])) items.push({ type: 'source', label: 'source', src: src[1] }); } return items; } function isChatAtBottom() { const m = document.getElementById('messages'); if (!m) return true; const threshold = 80; return m.scrollHeight - m.scrollTop <= m.clientHeight + threshold; } /** Scroll chat naar onder. Zonder force alleen als gebruiker al onderaan zit (gesprek volgt mee). Met force altijd, o.a. na nieuw bericht. */ function scrollChatToBottom(force) { const m = document.getElementById('messages'); if (!m) return; if (!force && !isChatAtBottom()) return; function doScroll() { m.scrollTop = m.scrollHeight; const last = m.lastElementChild; if (last) last.scrollIntoView({ behavior: 'smooth', block: 'end' }); } doScroll(); if (force) { requestAnimationFrame(function() { doScroll(); requestAnimationFrame(doScroll); }); setTimeout(function() { const last = m.lastElementChild; if (last) last.scrollIntoView({ behavior: 'smooth', block: 'end' }); }, 200); setTimeout(function() { const last = m.lastElementChild; if (last) last.scrollIntoView({ behavior: 'smooth', block: 'end' }); }, 450); } } document.querySelectorAll('.file-item').forEach(f => { f.addEventListener('click', function() { document.querySelectorAll('.file-item').forEach(x => x.classList.remove('active')); this.classList.add('active'); activeFile = this.getAttribute('data-file'); if (editor) { editor.setValue(files[activeFile] || ''); const lang = activeFile.endsWith('.html') ? 'html' : activeFile.endsWith('.css') ? 'css' : 'javascript'; monaco.editor.setModelLanguage(editor.getModel(), lang); } switchCenterTab('code'); updatePreview(); }); }); function updatePreview() { const iframe = document.getElementById('previewFrame'); if (!iframe) return; let html = files['index.html'] || ''; const css = (files['style.css'] || '').trim(); const js = (files['script.js'] || '').trim(); const basePreviewCss = 'html{box-sizing:border-box;scroll-behavior:smooth}*,*::before,*::after{box-sizing:inherit}body{margin:0;padding:2rem clamp(1rem,4vw,3rem);min-height:100vh;line-height:1.6}body:not([style*="padding"]):not(.no-preview-padding){padding:2rem clamp(1.5rem,5vw,4rem)}@media(min-width:800px){body:not([style*="max-width"]){max-width:900px;margin-left:auto;margin-right:auto;padding-left:2.5rem;padding-right:2.5rem}}video,iframe{max-width:100%;height:auto;display:block}object,embed{max-width:100%}form,input,textarea,button,select{max-width:100%;box-sizing:border-box}img{max-width:100%;height:auto}'; if (html.indexOf('') !== -1) { const styleBlock = ''; html = html.replace('', styleBlock + ''); } else if (html.indexOf('') !== -1) { html = html.replace('', ''); } else { html = html.replace(/') !== -1) { html = html.replace('', '' + js + ''); } iframe.srcdoc = html; } function escapeHtml(s) { const div = document.createElement('div'); div.textContent = s; return div.innerHTML; } /** Parse response as JSON; on empty/invalid return friendly error object so UI shows "AI optioneel" instead of crash */ async function safeJsonFromResponse(response) { const text = await response.text(); if (!text || !text.trim()) return { success: false, error: 'Geen antwoord van server. Controleer AI Providers of probeer later.', data: {} }; try { return JSON.parse(text); } catch (e) { return { success: false, error: 'Server gaf geen geldig antwoord. Stel API keys in of probeer later.', data: {} }; } } /** TTS: alleen afspelen als user expliciet kiest (klik op 🔊) of als "Automatisch afspelen" aan staat */ var TTS_AUTO_KEY = 'ai_code_agent_tts_auto'; var TTS_RATE_KEY = 'ai_code_agent_tts_rate'; function getTtsAutoPlay() { try { return sessionStorage.getItem(TTS_AUTO_KEY) === '1'; } catch (e) { return false; } } function getTtsRate() { try { var r = parseFloat(sessionStorage.getItem(TTS_RATE_KEY) || '1', 10); return isNaN(r) ? 1 : Math.max(0.5, Math.min(2, r)); } catch (e) { return 1; } } function speakResponse(plainText, forcePlay) { if (!plainText || !window.speechSynthesis) return; if (!forcePlay && !getTtsAutoPlay()) return; var t = (plainText + '').trim().replace(/\s+/g, ' ').substring(0, 500); if (!t) return; try { var u = new SpeechSynthesisUtterance(t); var langEl = document.querySelector('#chat-bar-options .bar-lang-btn.active'); u.lang = (langEl && langEl.getAttribute) ? (langEl.getAttribute('data-lang') || 'nl-NL') : 'nl-NL'; u.rate = getTtsRate(); u.volume = 1; speechSynthesis.cancel(); speechSynthesis.speak(u); } catch (e) {} } /** Voeg 🔊-knop toe aan het laatste AI-bericht (aanroepen na innerHTML += van .msg.ai) */ function addTtsButtonToLastAiMessage() { var container = document.getElementById('messages'); if (!container) return; var aiMsgs = container.querySelectorAll('.msg.ai'); var last = aiMsgs[aiMsgs.length - 1]; if (!last || last.querySelector('.msg-tts-btn')) return; var btn = document.createElement('button'); btn.type = 'button'; btn.className = 'msg-tts-btn'; btn.title = 'Antwoord afspelen'; btn.setAttribute('aria-label', 'Antwoord afspelen'); btn.innerHTML = ''; last.appendChild(btn); } window._codeBlocksForSave = {}; /** Maak HTML voor een AI-bericht met codeblok + kopieer + opslaan in bibliotheek */ function aiMsgWithCodeBlock(titleHtml, codeHtml, rawCode) { var saveBtn = ''; if (rawCode && rawCode.length > 0) { var codeId = 'cb' + Date.now(); window._codeBlocksForSave[codeId] = rawCode; saveBtn = ''; } return '
' + titleHtml + '
' + codeHtml + '
' + saveBtn + '
'; } /** Event delegation: kopieer code bij klik op .btn-copy-code */ document.getElementById('messages').addEventListener('click', function(e) { const btn = e.target.closest('.btn-copy-code'); if (btn) { const wrap = btn.closest('.msg-code-wrap'); const pre = wrap && wrap.querySelector('.msg-code'); if (pre && navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(pre.textContent).then(function() { const orig = btn.innerHTML; btn.innerHTML = ' Gekopieerd'; setTimeout(function() { btn.innerHTML = orig; }, 1500); }); } return; } const ttsBtn = e.target.closest('.msg-tts-btn'); if (ttsBtn) { var msgEl = ttsBtn.closest('.msg.ai'); if (msgEl) { var clone = msgEl.cloneNode(true); var ttsBtnInClone = clone.querySelector('.msg-tts-btn'); if (ttsBtnInClone) ttsBtnInClone.remove(); var text = (clone.textContent || '').trim().replace(/\s+/g, ' ').substring(0, 500); if (text) speakResponse(text, true); } return; } const saveBtn = e.target.closest('.btn-save-snippet'); if (saveBtn) { var codeId = saveBtn.getAttribute('data-code-id'); var raw = window._codeBlocksForSave && window._codeBlocksForSave[codeId]; if (!raw) return; var name = prompt('Naam voor in de bibliotheek:', 'AI Code Agent – ' + new Date().toLocaleTimeString('nl-NL', { hour: '2-digit', minute: '2-digit' })); if (!name || !name.trim()) return; var type = 'snippet'; if (raw.indexOf('') !== -1 || raw.indexOf('') !== -1 || raw.indexOf('') !== -1 || raw.indexOf(' Opgeslagen'; saveBtn.disabled = true; } else alert('Fout: ' + (d.error || 'Kon niet opslaan')); }) .catch(function(err) { alert('Fout: ' + err.message); }); } }); function getChatHistoryForSave() { var list = [], messagesEl = document.getElementById('messages'); if (!messagesEl) return list; messagesEl.querySelectorAll('.msg.user, .msg.ai').forEach(function(el) { var role = el.classList.contains('user') ? 'user' : 'assistant'; var content = (el.textContent || '').trim(); if (content.length > 8000) content = content.slice(0, 8000) + ' …'; list.push({ role: role, content: content }); }); return list; } async function saveSession() { var sessionId = prompt('Naam voor deze sessie:', lastSessionId || ('sessie_' + Date.now())); if (!sessionId || !sessionId.trim()) return; try { var r = await fetch('/api/ai-code-editor-api.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'save_session', session_id: sessionId.trim(), chat_history: getChatHistoryForSave(), code: files, metadata: { updated: Date.now(), company: window._companyName || '' } }) }); var d = await r.json(); if (d.success) { lastSessionId = sessionId.trim(); } alert(d.success ? 'Sessie opgeslagen. Gebruik "Deel sessie" om de link te kopiëren.' : 'Fout: ' + (d.error || 'Kon niet opslaan')); } catch (e) { alert('Fout: ' + e.message); } } function getShareableSessionUrl() { var base = window.location.origin + (window.location.pathname || '/ai-code-editor-agent.php'); var q = base.indexOf('?') >= 0 ? '&' : '?'; return lastSessionId ? (base + q + 'session=' + encodeURIComponent(lastSessionId)) : ''; } async function shareSession() { if (!lastSessionId) { var saveFirst = confirm('Eerst opslaan om een deelbare link te maken? (Kies OK om nu op te slaan.)'); if (saveFirst) { await saveSession(); if (!lastSessionId) return; } else return; } var url = getShareableSessionUrl(); if (!url) { alert('Sla eerst een sessie op (Opslaan), daarna kun je de link delen.'); return; } try { if (navigator.clipboard && navigator.clipboard.writeText) { await navigator.clipboard.writeText(url); alert('Link gekopieerd! Dezelfde sessie openen: in deze Code Agent, in de Orchestrator of in Live Build. Link: ' + url.slice(0, 60) + '…'); } else { prompt('Kopieer deze link om de sessie te delen (Orchestrator / Live Build):', url); } } catch (e) { prompt('Kopieer deze link:', url); } } async function loadSession() { try { var r = await fetch('/api/ai-code-editor-api.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'get_sessions' }) }); var d = await r.json(); if (!d.success || !d.data.sessions || d.data.sessions.length === 0) { alert('Geen opgeslagen sessies.'); return; } var list = d.data.sessions; var choice = prompt('Kies sessie (1-' + list.length + '):\n' + list.map(function(s, i) { return (i + 1) + '. ' + s.session_id; }).join('\n'), '1'); if (!choice) return; var idx = parseInt(choice, 10); if (idx < 1 || idx > list.length) return; var loadR = await fetch('/api/ai-code-editor-api.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'load_session', session_id: list[idx - 1].session_id }) }); var loadD = await loadR.json(); if (!loadD.success || !loadD.data.session) { alert('Laden mislukt.'); return; } var session = loadD.data.session; if (session.code && typeof session.code === 'object') { files = session.code; if (editor && files[activeFile]) editor.setValue(files[activeFile]); updatePreview(); } if (session.chat_history && session.chat_history.length) { var messagesEl = document.getElementById('messages'); var systemHtml = []; messagesEl.querySelectorAll('.msg.system').forEach(function(n) { systemHtml.push(n.outerHTML); }); messagesEl.innerHTML = systemHtml.join(''); session.chat_history.forEach(function(item) { var esc = escapeHtml(item.content || '').replace(/\n/g, '
'); messagesEl.innerHTML += item.role === 'user' ? '
' + esc + '
' : '
' + esc + '
'; }); scrollChatToBottom(true); } lastSessionId = list[idx - 1].session_id; if (session.metadata && session.metadata.company) { window._companyName = session.metadata.company; updateCompanyDisplay(); } } catch (e) { alert('Fout: ' + e.message); } } async function loadSessionById(sessionId) { if (!sessionId || !sessionId.trim()) return; try { var r = await fetch('/api/ai-code-editor-api.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'load_session', session_id: sessionId.trim() }) }); var d = await r.json(); if (!d.success || !d.data.session) { alert('Sessie niet gevonden of laden mislukt.'); return; } var session = d.data.session; if (session.code && typeof session.code === 'object') { files = session.code; if (editor && files[activeFile]) editor.setValue(files[activeFile]); updatePreview(); } if (session.chat_history && session.chat_history.length) { var messagesEl = document.getElementById('messages'); var systemHtml = []; messagesEl.querySelectorAll('.msg.system').forEach(function(n) { systemHtml.push(n.outerHTML); }); messagesEl.innerHTML = systemHtml.join(''); session.chat_history.forEach(function(item) { var esc = escapeHtml(item.content || '').replace(/\n/g, '
'); messagesEl.innerHTML += item.role === 'user' ? '
' + esc + '
' : '
' + esc + '
'; }); scrollChatToBottom(true); } lastSessionId = sessionId.trim(); if (session.metadata && session.metadata.company) { window._companyName = session.metadata.company; updateCompanyDisplay(); } } catch (e) { alert('Fout: ' + e.message); } } function updateCompanyDisplay() { var el = document.getElementById('companyNameDisplay'); var inp = document.getElementById('companyNameInput'); if (el) el.textContent = window._companyName ? 'Klant: ' + window._companyName : ''; if (inp && !inp.value && window._companyName) inp.placeholder = window._companyName; } async function companyKvkSearch() { var inp = document.getElementById('companyNameInput'); var naam = (inp && inp.value && inp.value.trim()) ? inp.value.trim() : prompt('Zoekterm (bedrijfsnaam of plaats):', ''); if (!naam) return; try { var url = '/admin_portal/api/kvk-lookup.php?naam=' + encodeURIComponent(naam) + '&only_search=1'; var r = await fetch(url, { method: 'GET', credentials: 'same-origin' }); var d = await r.json(); if (!d.ok) { alert(d.error || 'KVK zoeken mislukt.'); return; } var list = d.zoekResultaten || []; if (list.length === 0) { alert('Geen bedrijven gevonden. Probeer een andere zoekterm.'); return; } if (list.length === 1) { window._companyName = list[0].naam || list[0].bedrijfsnaam || naam; updateCompanyDisplay(); return; } var lines = list.slice(0, 15).map(function(x, i) { return (i + 1) + '. ' + (x.naam || x.bedrijfsnaam || '') + (x.plaats ? ' – ' + x.plaats : ''); }); var choice = prompt('Kies bedrijf (nummer 1-' + lines.length + '):\n\n' + lines.join('\n'), '1'); if (!choice) return; var idx = parseInt(choice, 10); if (idx < 1 || idx > list.length) return; window._companyName = list[idx - 1].naam || list[idx - 1].bedrijfsnaam || naam; updateCompanyDisplay(); } catch (e) { alert('Fout: ' + e.message); } } function newSession() { if (!confirm('Nieuw project starten? Huidige bestanden en chat worden gereset. Opgeslagen sessies blijven beschikbaar onder Laden.')) return; try { sessionStorage.removeItem(AUTO_SAVE_KEY); } catch (e) {} var defaultFiles = { 'index.html': '\n\n\n\n\nDemo\n\n\n\n

IT Live Demo

\n

Vraag de AI om iets te bouwen!

\n\n', 'style.css': 'body{margin:0;padding:0}', 'script.js': 'console.log("Ready");' }; files = defaultFiles; activeFile = 'index.html'; var messagesEl = document.getElementById('messages'); var systemHtml = []; messagesEl.querySelectorAll('.msg.system').forEach(function(n) { systemHtml.push(n.outerHTML); }); messagesEl.innerHTML = systemHtml.join(''); if (editor) { editor.setValue(files[activeFile]); monaco.editor.setModelLanguage(editor.getModel(), 'html'); } updatePreview(); updateProjectCard({ task: '—', pct: 0, status: 'Wacht op input', busy: false }); document.getElementById('projectTaskText').textContent = '—'; document.getElementById('projectProgressFill').style.width = '0%'; document.getElementById('projectProgressPct').textContent = '0%'; document.getElementById('flowChain').textContent = 'Chain: code-gen'; updateFlowDesc(['Typ een opdracht in de chat →']); updateFlowchart(0); setPlanContent('

Plan verschijnt hier na je opdracht (stap 1).

', ''); setTasksContent('

Taken verschijnen hier (stap 2).

'); setSkeletonContent('

Structuur/skeleton na code-generatie.

'); setBlueprintContent('

Blauwdruk / design-notities.

'); switchCenterTab('preview'); scrollChatToBottom(true); } document.getElementById('btnSaveSession').addEventListener('click', saveSession); document.getElementById('btnLoadSession').addEventListener('click', loadSession); document.getElementById('btnShareSession').addEventListener('click', shareSession); document.getElementById('btnNewSession').addEventListener('click', newSession); document.getElementById('btnCompanySelf').addEventListener('click', function() { var v = (document.getElementById('companyNameInput') || {}).value.trim(); window._companyName = v; updateCompanyDisplay(); }); document.getElementById('btnCompanyKvk').addEventListener('click', companyKvkSearch); /** Open gedeelde sessie uit URL (?session=... of ?share=...) */ (function() { var m = /[?&](?:session|share)=([^&]+)/.exec(window.location.search || ''); if (m && m[1]) { var id = decodeURIComponent(m[1]).trim(); if (id) loadSessionById(id); } })(); /** Flow interactief: klik op stap opent bijbehorend tab */ document.addEventListener('click', function(e) { const node = e.target.closest('.flow-node'); if (!node || !node.getAttribute('data-step')) return; var step = node.getAttribute('data-step'); var view = { input: 'plan', analyze: 'plan', plan: 'plan', code: 'code', apply: 'preview', done: 'preview' }[step] || 'flow'; if (typeof switchCenterTab === 'function') switchCenterTab(view); }); /** Voorkom dat hele pagina/chat als opdracht wordt gebruikt of getoond */ const MAX_DESCRIPTION_API = 2000; const MAX_OPDRACHT_DISPLAY = 400; const MAX_USER_MSG_DISPLAY = 600; function truncateForDisplay(s, max, suffix) { if (!s || s.length <= max) return s || ''; return s.slice(0, max).trim() + (suffix || ' … (ingekort)'); } function descriptionForApi(msg) { var raw = (msg || '').trim().slice(0, MAX_DESCRIPTION_API); if (window._companyName && window._companyName.trim()) { raw = raw + ' Bedrijfsnaam/klant: ' + (window._companyName || '').trim().slice(0, 200); } const vague = /\b(maak|doe|bouw)\s+iets\b|\biets\s+(maken|bouwen)\b|iets\s+neef|maak\s+iets\s+neef/i.test(raw); if (vague && raw.length < 80) { return 'Bouw iets creatiefs: een aantrekkelijke sectie met heading, tekst en stijl (bijv. call-to-action of icoon) in IT Live stijl (donkerblauw/goud). Gebruiker vroeg: ' + raw; } return raw.slice(0, MAX_DESCRIPTION_API); } function getMaxTokensForBuild() { var el = document.querySelector('input[name="maxTokensBuild"]:checked'); if (!el || !el.value) return 0; var n = parseInt(el.value, 10); return isNaN(n) ? 0 : n; } /** Bij vage korte input (maak het, goed) de laatste concrete opdracht uit de chat gebruiken voor plan/code. */ function getEffectiveDescription(msg, messagesEl) { var m = (msg || '').trim(); var vagueShort = m.length < 40 || /^(maak het|doe het|goed|ja|doe maar|bouwen|ga door|ok|oké|yes|doe|doen)$/i.test(m); if (!messagesEl || !vagueShort) return m || msg; var users = messagesEl.querySelectorAll('.msg.user'); if (users.length < 2) return m || msg; var prev = users[users.length - 2]; var prevText = (prev.textContent || '').trim(); if (prevText.length < 30) return m || msg; return prevText + (m ? ' Verduidelijking: ' + m : ''); } /** Of de opdracht het Strawberry-voorbeeld vraagt (API gebruikt dan vaste sectorcontent, geen Lorem). */ function isStrawberryRequest(desc) { return (desc || '').toLowerCase().indexOf('strawberry') !== -1; } /** Restaurant/horeca: leer van SBI + Strawberry, werkende menukaart, keuken, plan verbeterd. */ function isRestaurantRequest(desc) { return /\b(restaurant|horeca|menukaart|eetgelegenheid|keuken|reserveren)\b/i.test(desc || ''); } /** Haal brancheprofiel op (SBI, keywords, diensten, GMB) voor sector-specifieke websites. */ async function fetchBranchData(description) { if (!description || (description || '').trim().length < 5) return null; var q = (description || '').trim().slice(0, 150); try { var resolveRes = await fetch('/api/sbi-branches-api.php?action=resolve&q=' + encodeURIComponent(q)); var resolveData = await resolveRes.json().catch(function() { return {}; }); var sbiCode = resolveData.sbi_code || (resolveData.data && (resolveData.data.sbi_code || (Array.isArray(resolveData.data) && resolveData.data[0] && resolveData.data[0].sbi_code ? resolveData.data[0].sbi_code : null))); if (!sbiCode) return null; var branchRes = await fetch('/api/sbi-branches-api.php?action=get_branch_info&sbi_code=' + encodeURIComponent(sbiCode) + '&source=code_agent'); var branchData = await branchRes.json().catch(function() { return {}; }); if (branchData.success && branchData.data) return branchData.data; return null; } catch (e) { return null; } } function planStepLevel(step) { if (/^\d+[\.\)]\s+/.test(step)) return 0; if (/^\d+\.\d+\.\d+([\.\)]\s*|\s+)/.test(step) || /^[a-zA-Z]\d+[\.\)]?\s+/.test(step)) return 2; if (/^\d+\.\d+([\.\)]\s*|\s+)/.test(step) || /^[a-zA-Z][\.\)]\s+/.test(step)) return 1; return 0; } function renderPlanStepsHtml(steps) { if (!steps || !steps.length) return ''; return steps.slice(0, 40).map(function(s) { var level = planStepLevel(s); return '
  • ' + escapeHtml(s) + '
  • '; }).join(''); } document.getElementById('input').addEventListener('keydown', function(e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }); document.querySelectorAll('.quick-btn').forEach(function(btn) { btn.addEventListener('click', function() { var prompt = this.getAttribute('data-prompt'); var input = document.getElementById('input'); var barInput = document.getElementById('chatBarInput'); if (prompt && input) { input.value = prompt; if (barInput) barInput.value = prompt; sendMessage(); } }); }); (function() { var wrapper = document.getElementById('chat-bar-wrapper'); var bar = document.getElementById('chat-input-bar'); var barInput = document.getElementById('chatBarInput'); var input = document.getElementById('input'); var barModel = document.getElementById('chatBarModel'); var grip = document.getElementById('chatBarGrip'); var resizeHandle = document.getElementById('chatBarResize'); var quickRow = document.getElementById('chat-bar-quick-row'); var STORAGE_KEY = 'ai_code_agent_chat_bar'; /* Snelkeuzes alleen in optiepanel (⋯); quickRow niet meer vullen */ function loadBarState() { if (!wrapper) return; try { var s = sessionStorage.getItem(STORAGE_KEY); if (s) { var o = JSON.parse(s); if (o.pos === 'left' || o.pos === 'right' || o.pos === 'center') { if (o.pos === 'center') { wrapper.style.left = '50%'; wrapper.style.right = 'auto'; wrapper.style.bottom = '16px'; wrapper.style.transform = 'translateX(-50%)'; } else if (o.pos === 'left') { wrapper.style.left = '16px'; wrapper.style.right = 'auto'; wrapper.style.bottom = '16px'; wrapper.style.transform = 'none'; } else { wrapper.style.left = 'auto'; wrapper.style.right = '16px'; wrapper.style.bottom = '16px'; wrapper.style.transform = 'none'; } } else if (o.left != null && o.bottom != null) { wrapper.style.left = o.left + 'px'; wrapper.style.bottom = o.bottom + 'px'; wrapper.style.transform = 'none'; wrapper.style.right = 'auto'; } if (o.width && o.width >= 320) { wrapper.style.width = Math.min(o.width, 900) + 'px'; } } } catch (e) {} } function saveBarState() { if (!wrapper) return; try { var width = wrapper.offsetWidth; var isPercent = (wrapper.style.left || '').indexOf('%') >= 0; if (isPercent) { var s = sessionStorage.getItem(STORAGE_KEY); var o = (s ? JSON.parse(s) : {}) || {}; sessionStorage.setItem(STORAGE_KEY, JSON.stringify({ pos: o.pos || 'center', width: width })); } else { var left = parseInt(wrapper.style.left, 10); var bottom = parseInt(wrapper.style.bottom, 10); if (!isNaN(left) && !isNaN(bottom)) { sessionStorage.setItem(STORAGE_KEY, JSON.stringify({ left: left, bottom: bottom, width: width })); } } } catch (e) {} } loadBarState(); if (barInput && input) { function barInputSync() { input.value = barInput.value; } function barInputGrow() { barInput.style.height = 'auto'; barInput.style.height = Math.min(barInput.scrollHeight, 96) + 'px'; } barInput.addEventListener('input', function() { barInputSync(); barInputGrow(); }); barInput.addEventListener('keydown', function(e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); input.value = barInput.value; sendMessage(); barInput.value = ''; input.value = ''; barInputGrow(); } }); barInputGrow(); } var optionsPanel = document.getElementById('chat-bar-options'); var barOptsBtn = document.getElementById('chatBarOpts'); if (optionsPanel && barOptsBtn) { var qc = document.getElementById('quickChoices'); var qa = document.getElementById('quickActions'); var dest = document.getElementById('chatBarQuickActions'); if (dest && (qc || qa)) { [qc, qa].forEach(function(container) { if (!container) return; container.querySelectorAll('.quick-btn').forEach(function(btn) { var clone = btn.cloneNode(true); clone.addEventListener('click', function() { var p = this.getAttribute('data-prompt'); if (p) { if (barInput) barInput.value = p; if (input) input.value = p; optionsPanel.classList.remove('visible'); if (typeof sendMessage === 'function') sendMessage(); } }); dest.appendChild(clone); }); }); } var posBtns = optionsPanel.querySelectorAll('.bar-pos-btn'); function applyBarPosition(pos) { if (!wrapper) return; if (pos === 'center' || pos === 'reset') { wrapper.style.left = '50%'; wrapper.style.right = 'auto'; wrapper.style.bottom = '16px'; wrapper.style.transform = 'translateX(-50%)'; } else if (pos === 'left') { wrapper.style.left = '16px'; wrapper.style.right = 'auto'; wrapper.style.bottom = '16px'; wrapper.style.transform = 'none'; } else if (pos === 'right') { wrapper.style.left = 'auto'; wrapper.style.right = '16px'; wrapper.style.bottom = '16px'; wrapper.style.transform = 'none'; } try { if (pos === 'reset') { sessionStorage.removeItem(STORAGE_KEY); } else { sessionStorage.setItem(STORAGE_KEY, JSON.stringify({ pos: pos, width: wrapper.offsetWidth })); } } catch (e) {} } posBtns.forEach(function(btn) { btn.addEventListener('click', function() { var pos = this.getAttribute('data-pos'); if (pos) applyBarPosition(pos); }); }); document.querySelectorAll('input[name="sendModeBar"]').forEach(function(r) { r.addEventListener('change', function() { var main = document.querySelector('input[name="sendMode"][value="' + this.value + '"]'); if (main) main.checked = true; if (barModel) barModel.value = this.value; }); }); barOptsBtn.addEventListener('click', function(e) { e.stopPropagation(); optionsPanel.classList.toggle('visible'); if (optionsPanel.classList.contains('visible')) loadTokenUsage(); var checked = document.querySelector('input[name="sendMode"]:checked'); if (checked) document.querySelectorAll('input[name="sendModeBar"]').forEach(function(r) { r.checked = (r.value === checked.value); }); }); document.addEventListener('click', function() { optionsPanel.classList.remove('visible'); }); optionsPanel.addEventListener('click', function(e) { e.stopPropagation(); }); function loadTokenUsage() { var el = document.getElementById('tokenUsageText'); if (!el) return; el.textContent = 'Laden…'; fetch('/api/ai-code-editor-api.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'token_usage' }) }) .then(function(r) { return r.json(); }) .then(function(data) { if (data.success && data.data) { var t = data.data.usage_today || {}; var tot = (t.total || 0); var gratis = data.data.gratis_tokens_today || 0; var g7 = data.data.gratis_tokens_7d || 0; el.textContent = 'Vandaag: ' + tot.toLocaleString('nl-NL') + ' (' + gratis.toLocaleString('nl-NL') + ' gratis)'; el.title = '7 dagen: ' + (data.data.usage_7d && data.data.usage_7d.total != null ? data.data.usage_7d.total.toLocaleString('nl-NL') : '—') + ' totaal, ' + g7.toLocaleString('nl-NL') + ' gratis'; } else { el.textContent = '—'; } }) .catch(function() { el.textContent = '—'; }); } var tokenRefresh = document.getElementById('tokenUsageRefresh'); if (tokenRefresh) tokenRefresh.addEventListener('click', loadTokenUsage); var completeSiteBtn = document.getElementById('completeSiteBtn'); var completeClient = document.getElementById('completeSiteClientName'); var completeSector = document.getElementById('completeSiteSector'); if (completeSiteBtn && completeClient && completeSector) { completeSiteBtn.addEventListener('click', function() { var name = (completeClient.value || '').trim(); var sector = (completeSector.value || '').trim(); if (!name) { alert('Vul minimaal een klantnaam in.'); return; } completeSiteBtn.disabled = true; completeSiteBtn.innerHTML = ' Bezig…'; fetch('/api/ai-code-editor-api.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'complete_website', client_name: name, sector: sector }) }) .then(function(r) { return r.json(); }) .then(function(data) { completeSiteBtn.disabled = false; completeSiteBtn.innerHTML = ' Start complete website'; if (!data.success || !data.data || !data.data.code) { alert(data.error || 'Mislukt'); return; } var code = data.data.code; var htmlM = code.match(/```html\n([\s\S]*?)```/); var cssM = code.match(/```css\n([\s\S]*?)```/); var jsM = code.match(/```javascript\n([\s\S]*?)```/); if (htmlM) files['index.html'] = htmlM[1].trim(); if (cssM) files['style.css'] = cssM[1].trim(); if (jsM) files['script.js'] = jsM[1].trim(); if (editor && activeFile in files) editor.setValue(files[activeFile]); updatePreview(); if (typeof saveFilesToStorage === 'function') saveFilesToStorage(); var inst = data.data.instructie_md || ''; if (inst && typeof setPlanContent === 'function') setPlanContent('

    INSTRUCTIE voor klant

    ' + escapeHtml(inst) + '
    ', null); optionsPanel.classList.remove('visible'); switchToPreviewTab && switchToPreviewTab(); var toast = document.createElement('div'); toast.className = 'complete-website-toast'; toast.style.cssText = 'position:fixed;bottom:80px;left:50%;transform:translateX(-50%);background:#10b981;color:#fff;padding:10px 20px;border-radius:8px;font-size:0.9rem;z-index:9999;box-shadow:0 4px 12px rgba(0,0,0,.2);'; toast.textContent = 'Complete website geladen. Instructie staat in tab Plan.'; document.body.appendChild(toast); setTimeout(function() { if (toast.parentNode) toast.parentNode.removeChild(toast); }, 4000); }) .catch(function(err) { completeSiteBtn.disabled = false; completeSiteBtn.innerHTML = ' Start complete website'; alert('Fout: ' + (err.message || 'Netwerkfout')); }); }); } } var micBtn = document.getElementById('chatBarMic'); var micWrap = micBtn && micBtn.closest('.chat-bar-mic-wrap'); var micDurationEl = document.getElementById('chatBarMicDuration'); var SPEECH_LANG_KEY = 'ai_code_agent_speech_lang'; function getSpeechLang() { try { return sessionStorage.getItem(SPEECH_LANG_KEY) || 'nl-NL'; } catch (e) { return 'nl-NL'; } } if (optionsPanel) { document.querySelectorAll('#chat-bar-options .bar-lang-btn').forEach(function(btn) { if (getSpeechLang() === btn.getAttribute('data-lang')) btn.classList.add('active'); else btn.classList.remove('active'); btn.addEventListener('click', function() { var lang = this.getAttribute('data-lang'); try { sessionStorage.setItem(SPEECH_LANG_KEY, lang); } catch (e) {} document.querySelectorAll('#chat-bar-options .bar-lang-btn').forEach(function(b) { b.classList.toggle('active', b.getAttribute('data-lang') === lang); }); }); }); var ttsAutoEl = document.getElementById('ttsAutoPlay'); if (ttsAutoEl) { ttsAutoEl.checked = getTtsAutoPlay(); ttsAutoEl.addEventListener('change', function() { try { sessionStorage.setItem(TTS_AUTO_KEY, this.checked ? '1' : '0'); } catch (e) {} }); } document.querySelectorAll('#chat-bar-options .bar-tts-rate-btn').forEach(function(btn) { var rate = parseFloat(btn.getAttribute('data-rate') || '1', 10); if (Math.abs(getTtsRate() - rate) < 0.01) btn.classList.add('active'); else btn.classList.remove('active'); btn.addEventListener('click', function() { var r = parseFloat(this.getAttribute('data-rate') || '1', 10); try { sessionStorage.setItem(TTS_RATE_KEY, String(r)); } catch (e) {} document.querySelectorAll('#chat-bar-options .bar-tts-rate-btn').forEach(function(b) { b.classList.toggle('active', parseFloat(b.getAttribute('data-rate') || '1', 10) === r); }); }); }); } if (micBtn && barInput) { var Recognition = window.SpeechRecognition || window.webkitSpeechRecognition; var recognition = null; var recordingTimer = null; var releaseHandler = null; if (Recognition) { try { recognition = new Recognition(); recognition.continuous = true; recognition.interimResults = true; recognition.onresult = function(e) { var i, s = ''; for (i = 0; i < e.results.length; i++) { s += e.results[i][0].transcript; } barInput.value = s; if (input) input.value = s; if (barInput.scrollHeight > barInput.offsetHeight) barInput.style.height = Math.min(barInput.scrollHeight, 96) + 'px'; }; } catch (err) { recognition = null; } } function stopAndSend() { if (!micBtn.classList.contains('recording')) return; if (recognition) try { recognition.stop(); } catch (e) {} micBtn.classList.remove('recording'); if (micWrap) micWrap.classList.remove('recording'); if (recordingTimer) { clearInterval(recordingTimer); recordingTimer = null; } if (micDurationEl) micDurationEl.textContent = ''; var text = (barInput && barInput.value) ? barInput.value.trim() : ''; micBtn.classList.add('mic-sent'); if (micWrap) micWrap.classList.add('mic-sent'); micBtn.title = 'Spraak invoer – vasthouden om op te nemen'; if (text && typeof sendMessage === 'function') { if (input) input.value = text; if (barInput) barInput.value = text; sendMessage(); if (barInput) barInput.value = ''; if (input) input.value = ''; } setTimeout(function() { micBtn.classList.remove('mic-sent'); if (micWrap) micWrap.classList.remove('mic-sent'); }, 600); } function startRecording() { if (!recognition) { micBtn.title = 'Spraak niet ondersteund in deze browser'; return; } recognition.lang = getSpeechLang(); try { recognition.start(); } catch (e) { return; } micBtn.classList.add('recording'); if (micWrap) micWrap.classList.add('recording'); micBtn.title = 'Loslaten om te versturen'; var startMs = Date.now(); if (micDurationEl) { function tick() { var sec = Math.floor((Date.now() - startMs) / 1000); var m = Math.floor(sec / 60); var s = sec % 60; micDurationEl.textContent = m + ':' + (s < 10 ? '0' : '') + s; } tick(); recordingTimer = setInterval(tick, 1000); } releaseHandler = function() { stopAndSend(); document.removeEventListener('mouseup', releaseHandler); document.removeEventListener('touchend', releaseHandler); releaseHandler = null; }; document.addEventListener('mouseup', releaseHandler); document.addEventListener('touchend', releaseHandler, { passive: true }); } micBtn.addEventListener('mousedown', function(e) { e.preventDefault(); startRecording(); }); micBtn.addEventListener('touchstart', function(e) { e.preventDefault(); startRecording(); }, { passive: false }); micBtn.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); }); } if (barModel) { barModel.addEventListener('change', function() { var r = document.querySelector('input[name="sendMode"][value="' + barModel.value + '"]'); if (r) r.checked = true; }); document.querySelectorAll('input[name="sendMode"]').forEach(function(radio) { radio.addEventListener('change', function() { if (barModel) barModel.value = this.value; }); }); } if (document.getElementById('chatBarSend')) { document.getElementById('chatBarSend').addEventListener('click', function() { if (input) input.value = barInput ? barInput.value : ''; sendMessage(); if (barInput) barInput.value = ''; if (input) input.value = ''; }); } if (document.getElementById('chatBarPlus')) { document.getElementById('chatBarPlus').addEventListener('click', function() { if (typeof newSession === 'function') newSession(); }); } if (grip && wrapper) { var dragging = false, startX, startY, startLeft, startBottom; grip.addEventListener('mousedown', function(e) { e.preventDefault(); dragging = true; startX = e.clientX; startY = e.clientY; startLeft = wrapper.style.left ? parseInt(wrapper.style.left, 10) : wrapper.getBoundingClientRect().left; startBottom = wrapper.style.bottom ? parseInt(wrapper.style.bottom, 10) : (window.innerHeight - wrapper.getBoundingClientRect().bottom); if (isNaN(startLeft)) startLeft = wrapper.getBoundingClientRect().left; if (isNaN(startBottom)) startBottom = 16; }); document.addEventListener('mousemove', function(e) { if (!dragging) return; var dx = e.clientX - startX; var dy = startY - e.clientY; var newLeft = Math.max(0, startLeft + dx); var newBottom = Math.max(0, startBottom + dy); wrapper.style.left = newLeft + 'px'; wrapper.style.bottom = newBottom + 'px'; wrapper.style.transform = 'none'; }); document.addEventListener('mouseup', function() { if (dragging) { dragging = false; saveBarState(); } }); } if (resizeHandle && wrapper) { var resizing = false, startX, startW; resizeHandle.addEventListener('mousedown', function(e) { e.preventDefault(); resizing = true; startX = e.clientX; startW = wrapper.offsetWidth; }); document.addEventListener('mousemove', function(e) { if (!resizing) return; var dw = e.clientX - startX; var w = Math.max(320, Math.min(900, startW + dw)); wrapper.style.width = w + 'px'; }); document.addEventListener('mouseup', function() { if (resizing) { resizing = false; saveBarState(); } }); } })(); /** Clarity: herkenning chat vs plan/build – praktisch, in lijn met API classify */ function computeClarity(msg) { const t = (msg || '').trim().toLowerCase(); if (!t) return { score: 0, suggestedMode: 'chat', reason: 'Leeg bericht' }; const buildWords = ['bouw', 'maak', 'website', 'pagina', 'formulier', 'landing', 'app', 'html', 'css', 'component', 'sectie', 'knop', 'menu', 'footer', 'header', 'layout', 'widget', 'chatbot', 'iets', 'doe iets', 'maak iets', 'creatief', 'webshop', 'order', 'bestel', 'strawberry', 'loodgieter', 'schilder', 'genereer', 'contactformulier', 'offerte', 'reservering', 'afspraak']; const chatWords = ['hoe ', 'wat is', 'waarom', 'uitleg', 'help', '?', 'leg uit', 'verschil', 'advies', 'hallo', 'hoi', 'dag']; let score = Math.min(100, Math.round(t.length * 1.2)); let suggestedMode = 'chat'; let reason = 'Korte tekst → chat'; const hasBuild = buildWords.some(function(w) { return t.indexOf(w) !== -1; }); const hasChat = chatWords.some(function(w) { return t.indexOf(w) !== -1; }) || t.indexOf('?') !== -1; const vagueBuild = /\b(maak|doe|bouw)\s+iets\b|\biets\s+(maken|bouwen)\b/i.test(t); if (vagueBuild || (hasBuild && t.length >= 4)) { suggestedMode = 'build'; score = Math.max(score, vagueBuild ? 70 : 65); reason = vagueBuild ? 'Maak iets → plan + code' : 'Bouw-instructie → plan + code'; } else if (hasChat && t.length < 60) { suggestedMode = 'chat'; if (t.length < 10) score = Math.min(score, 35); reason = 'Vraag of groet → chat'; } else if (t.length >= 25 && !hasChat) { score = Math.max(score, 60); suggestedMode = 'build'; reason = 'Uitgebreide opdracht → plan + code'; } return { score: Math.min(100, score), suggestedMode, reason }; } function updateClarityUI() { const input = document.getElementById('input'); const msg = (input && input.value || '').trim(); const row = document.getElementById('clarityRow'); const scoreEl = document.getElementById('clarityScore'); const suggEl = document.getElementById('claritySuggestion'); const modeAuto = document.getElementById('modeAuto'); if (!row || !scoreEl || !suggEl) return; if (!msg || !modeAuto || !modeAuto.checked) { row.style.display = 'none'; return; } const r = computeClarity(msg); row.style.display = 'flex'; scoreEl.textContent = r.score + '%'; suggEl.textContent = r.suggestedMode === 'build' ? '→ Chain kiest waarschijnlijk: plan + code' : '→ Chain kiest waarschijnlijk: chat'; } document.getElementById('input').addEventListener('input', updateClarityUI); document.getElementById('input').addEventListener('focus', updateClarityUI); document.querySelectorAll('input[name="sendMode"]').forEach(function(radio) { radio.addEventListener('change', updateClarityUI); }); document.getElementById('showPlanTemplate').addEventListener('click', onShowPlanTemplate); window._lastOfferte = null; function showOfferteModal(offerText, projectName, clientName) { // Offerte via mail var title = (projectName || 'Offerte') + (clientName ? ' – ' + clientName : ''); var notes = offerText.slice(0, 2000); var mailLink = 'mailto:info@itlive.nl?subject=' + encodeURIComponent('Offerte aanvraag: ' + title) + '&body=' + encodeURIComponent(notes.slice(0, 500)); window._lastOfferte = { text: offerText, projectName: projectName, clientName: clientName, mailLink: mailLink }; var bodyHtml = escapeHtml(offerText).replace(/\n/g, '
    '); var contentOfferte = document.getElementById('contentOfferte'); if (contentOfferte) { contentOfferte.innerHTML = '
    ' + bodyHtml + '
    '; var copyBtn = document.getElementById('offerteTabCopy'); if (copyBtn) copyBtn.addEventListener('click', function() { if (window._lastOfferte && navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(window._lastOfferte.text).then(function() { copyBtn.innerHTML = ' Gekopieerd'; setTimeout(function() { copyBtn.innerHTML = ' Kopieer'; }, 2000); }); } }); if (typeof switchCenterTab === 'function') switchCenterTab('offerte'); } var overlay = document.createElement('div'); overlay.className = 'offerte-modal-overlay'; overlay.innerHTML = '

    Offerte / aanbod

    ' + bodyHtml + '
    '; document.body.appendChild(overlay); var modalCopyBtn = overlay.querySelector('.btn-copy-offerte'); modalCopyBtn.addEventListener('click', function() { if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(offerText).then(function() { modalCopyBtn.innerHTML = ' Gekopieerd'; setTimeout(function() { modalCopyBtn.innerHTML = ' Kopieer'; }, 2000); }); } }); overlay.querySelector('.offerte-modal-close').addEventListener('click', function() { overlay.remove(); }); overlay.addEventListener('click', function(e) { if (e.target === overlay) overlay.remove(); }); } async function makeOffer() { if (!lastPlanText) { alert('Er is nog geen plan. Voer eerst een opdracht uit (Plan + code bouwen).'); return; } const projectName = prompt('Projectnaam (optioneel):', 'Website / webapp') || 'Project'; const clientName = prompt('Klantnaam (optioneel):', '') || ''; const messages = document.getElementById('messages'); messages.innerHTML += '
    Offerte wordt gegenereerd…
    '; scrollChatToBottom(true); var previewUrl = window.location.origin + (window.location.pathname || '/ai-code-editor-agent.php'); try { const r = await fetch('/api/ai-code-editor-api.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'offer', plan: lastPlanText, project_name: projectName, client_name: clientName, preview_url: previewUrl }) }); const d = await r.json(); const lastMsg = messages.querySelector('.msg.ai:last-child'); if (lastMsg && lastMsg.querySelector('.ai-thinking')) lastMsg.remove(); if (d.success && d.data && d.data.offer) { showOfferteModal(d.data.offer, d.data.project_name || projectName, d.data.client_name || clientName); messages.innerHTML += '
    Offerte gegenereerd. Bekijk in het midden (modal).
    '; } else { messages.innerHTML += '
    ⚠️ ' + escapeHtml(d.error || 'Offerte kon niet worden gegenereerd.') + '
    '; } } catch (e) { const lastMsg = messages.querySelector('.msg.ai:last-child'); if (lastMsg && lastMsg.querySelector('.ai-thinking')) lastMsg.remove(); messages.innerHTML += '
    ❌ ' + escapeHtml(e.message) + '
    '; } scrollChatToBottom(true); } document.getElementById('btnMakeOffer').addEventListener('click', makeOffer); const FLOW_STEPS = ['input', 'analyze', 'plan', 'code', 'apply', 'done']; const FLOW_DESC_STEPS = [ '1. Analyseer vraag en context', '2. Maak plan (HTML/CSS/JS)', '3. Genereer code', '4. Pas toe in editor + preview' ]; function updateFlowDesc(lines, activeIndex) { const el = document.getElementById('flowDesc'); if (!el) return; if (typeof lines === 'string') lines = lines.split('\n').filter(Boolean); if (!Array.isArray(lines)) return; activeIndex = activeIndex == null ? -1 : activeIndex; el.innerHTML = lines.map((line, i) => { let cls = 'step'; if (activeIndex >= 0) { if (i < activeIndex) cls += ' done'; else if (i === activeIndex) cls += ' active'; } return '
    ' + escapeHtml(line) + '
    '; }).join(''); const centerDesc = document.getElementById('flowDescCenter'); if (centerDesc) centerDesc.innerHTML = el.innerHTML; } function updateFlowchart(activeIndex, isError) { ['flowchart', 'flowchartCenter'].forEach(id => { const container = document.getElementById(id); if (!container) return; const nodes = container.querySelectorAll('.flow-node'); const arrows = container.querySelectorAll('.flow-arrow'); for (let i = 0; i < nodes.length; i++) { nodes[i].classList.remove('pending', 'active', 'done', 'error'); if (isError && i === activeIndex) nodes[i].classList.add('error'); else if (i < activeIndex) nodes[i].classList.add('done'); else if (i === activeIndex) nodes[i].classList.add('active'); else nodes[i].classList.add('pending'); } for (let j = 0; j < arrows.length; j++) { arrows[j].classList.remove('pending', 'active', 'done'); if (j < activeIndex) arrows[j].classList.add('done'); else if (j === activeIndex - 1) arrows[j].classList.add('active'); else arrows[j].classList.add('pending'); } }); const descCenter = document.getElementById('flowDescCenter'); if (descCenter && document.getElementById('flowDesc')) descCenter.innerHTML = document.getElementById('flowDesc').innerHTML; } function updateProjectCard(opts) { const card = document.getElementById('projectCard'); const taskEl = document.getElementById('projectTaskText'); const fillEl = document.getElementById('projectProgressFill'); const pctEl = document.getElementById('projectProgressPct'); const statusEl = document.getElementById('projectStatus'); if (!card) return; if (opts.task !== undefined) taskEl.textContent = opts.task || '—'; if (opts.pct !== undefined) { const pct = Math.min(100, Math.max(0, opts.pct)); fillEl.style.width = pct + '%'; pctEl.textContent = pct + '%'; } if (opts.status !== undefined) statusEl.textContent = opts.status; card.classList.remove('idle', 'busy', 'done'); card.classList.add(opts.busy ? 'busy' : (opts.pct >= 100 ? 'done' : 'idle')); } /** Alleen chatten: vraag → agent antwoordt, geen plan/code. Flow toont Chat → Antwoord. */ async function sendChatOnly(msg, messagesEl) { const chainEl = document.getElementById('flowChain'); if (chainEl) chainEl.textContent = 'Chain: chat'; updateProjectCard({ task: msg.slice(0, 60) + (msg.length > 60 ? '…' : ''), pct: 0, status: 'Bezig…', busy: true }); updateFlowDesc(['1. Vraag ontvangen', '2. Antwoord genereren…'], 1); const thinkingId = 'thinking-chat-' + Date.now(); messagesEl.innerHTML += `
    Antwoord wordt gegenereerd…
    `; scrollChatToBottom(true); try { const response = await fetch('/api/ai-code-editor-api.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'chat', prompt: msg, message: msg, code: files['index.html'], context: files }) }); const data = await safeJsonFromResponse(response); const thinkingDiv = document.getElementById(thinkingId); if (thinkingDiv && thinkingDiv.parentNode) thinkingDiv.remove(); if (data.success && (data.data?.text || data.data?.reply)) { const text = (data.data.text || data.data.reply || '').trim(); updateProjectCard({ pct: 100, status: 'Klaar', busy: false }); updateFlowDesc(['1. Vraag ontvangen', '2. Antwoord gegeven']); messagesEl.innerHTML += `
    ${escapeHtml(text).replace(/\n/g, '
    ')}
    `; addTtsButtonToLastAiMessage(); scrollChatToBottom(true); speakResponse(text); } else { let err = data.error || data.data?.error || 'Geen antwoord ontvangen.'; const isNoApiKey = (data.data?.error === 'no_api_key') || /geen.*ai.*provider|stel.*api.*key|admin.*portal.*ai.*provider|geen antwoord|geldig antwoord/i.test(err); updateProjectCard({ pct: 0, status: isNoApiKey ? 'Klaar' : 'Fout', busy: false }); if (isNoApiKey) { updateFlowDesc(['AI wordt geladen… even geduld.']); updateFlowchart(0, false); messagesEl.innerHTML += '
    AI is momenteel niet beschikbaar. Probeer het later opnieuw of neem contact op met support.
    '; } else { updateFlowDesc(['Fout: ' + (err + '').replace(/<[^>]+>/g, '').trim().substring(0, 50)]); updateFlowchart(0, true); messagesEl.innerHTML += '
    ⚠️ ' + escapeHtml(err).replace(/\n/g, '
    ') + '
    '; } } } catch (error) { const thinkingDiv = document.getElementById(thinkingId); if (thinkingDiv && thinkingDiv.parentNode) thinkingDiv.remove(); updateProjectCard({ pct: 0, status: 'Fout', busy: false }); updateFlowDesc(['Fout: ' + error.message]); updateFlowchart(0, true); messagesEl.innerHTML += `
    ❌ ${escapeHtml(error.message)}
    `; } scrollChatToBottom(true); } async function doGenerateWithPlan(description, planText) { const messages = document.getElementById('messages'); const thinkingId = 'thinking-gen-' + Date.now(); messages.innerHTML += '
    Code genereren met verduidelijking…
    '; scrollChatToBottom(true); updateProjectCard({ pct: 70, status: 'Bezig…', busy: true }); updateFlowchart(4); try { const genPayload = { action: 'generate', description: descriptionForApi(description), language: 'html', context: files, plan: planText }; if (isStrawberryRequest(description)) genPayload.template = 'strawberry'; if (isRestaurantRequest(description)) genPayload.template = 'restaurant'; if (lastBranchData) genPayload.branch_data = lastBranchData; var maxT = getMaxTokensForBuild(); if (maxT > 0) genPayload.max_tokens = maxT; if (planText && planText.length > 100) { try { var stepEl = document.querySelector('#' + thinkingId + '-step .thinking-text'); if (stepEl) stepEl.textContent = 'Sitemap ophalen…'; const smRes = await fetch('/api/ai-code-editor-api.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'sitemap', plan: planText, description: descriptionForApi(description) }) }); const smData = await safeJsonFromResponse(smRes); if (smData.success && smData.data && Array.isArray(smData.data.sitemap) && smData.data.sitemap.length > 0) genPayload.sitemap = smData.data.sitemap; if (stepEl) stepEl.textContent = 'Afbeeldingen ophalen…'; var imageUrls = []; var qs = /\brestaurant|horeca|eten|menu\b/i.test(description) ? ['restaurant hero interior', 'restaurant food dish'] : (/\bwebshop|shop|winkel\b/i.test(description) ? ['online shop hero', 'product ecommerce'] : ['professional business hero']); for (var qi = 0; qi < Math.min(2, qs.length); qi++) { var ir = await fetch('/api/ai-code-editor-api.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'image_for_code', q: qs[qi], context: description.slice(0, 100) }) }); var idata = await ir.json().catch(function() { return {}; }); if (idata.success && idata.data && idata.data.url) imageUrls.push({ url: idata.data.url, alt: idata.data.alt_suggestion || qs[qi] }); } if (imageUrls.length > 0) genPayload.image_urls = imageUrls; if (stepEl) stepEl.textContent = 'Code genereren…'; } catch (e) { /* sitemap/images optioneel */ } } const response = await fetch('/api/ai-code-editor-api.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(genPayload) }); const data = await safeJsonFromResponse(response); const thinkingDiv = document.getElementById(thinkingId); if (thinkingDiv && thinkingDiv.parentNode) thinkingDiv.remove(); updateProjectCard({ pct: 100, status: 'Klaar', busy: false }); updateFlowchart(6); if (data.success && data.data && data.data.code) { const code = data.data.code; const htmlMatch = code.match(/```html\n([\s\S]*?)```/); const cssMatch = code.match(/```css\n([\s\S]*?)```/); const jsMatch = code.match(/```javascript\n([\s\S]*?)```/); let applied = []; if (htmlMatch) { files['index.html'] = htmlMatch[1].trim(); applied.push('HTML'); } if (cssMatch) { files['style.css'] = cssMatch[1].trim(); applied.push('CSS'); } if (jsMatch) { files['script.js'] = jsMatch[1].trim(); applied.push('JavaScript'); } if (applied.length > 0) { if (editor && activeFile in files) editor.setValue(files[activeFile]); updatePreview(); if (typeof saveFilesToStorage === 'function') saveFilesToStorage(); updateFlowDesc(['Klaar – toegepast: ' + applied.join(', ')]); const planHtml = '

    Plan (gebruikt voor code)

    Opdracht: ' + escapeHtml(truncateForDisplay(description, MAX_OPDRACHT_DISPLAY)) + '

    ' + escapeHtml(planText || '').replace(/\n/g, '
    ') + '

    Toegepast: ' + escapeHtml(applied.join(', ')) + '

    '; setPlanContent(planHtml, planText || null); setTasksContent('

    Uitgevoerde stappen

    • ✓ Verduidelijking
    • ✓ Code generatie
    • ✓ Toepassen (' + escapeHtml(applied.join(', ')) + ')
    '); const skeletonTags = extractSkeletonFromHtml(files['index.html']); const jsSummary = extractJsSummary(files['script.js'] || ''); const mediaItems = extractMediaFromHtml(files['index.html'] || ''); let skeletonHtml = '

    HTML-structuur

    ' + escapeHtml(skeletonTags.join(', ')) + '

    JavaScript

    ' + escapeHtml(jsSummary.summary) + '

    Video / media

    ' + (mediaItems.length ? '
      ' + mediaItems.map(m => '
    • ' + escapeHtml(m.label) + '
    • ').join('') + '
    ' : '

    Geen video/audio in HTML.

    '); setSkeletonContent(skeletonHtml); setBlueprintContent('

    Blauwdruk

    Frontend: HTML, CSS, JavaScript. Toegepast: ' + escapeHtml(applied.join(', ')) + '.

    '); var liveEl = document.getElementById('liveLinkDisplay'); if (liveEl) liveEl.innerHTML = 'Preview in tabblad →'; const codeDisplay = escapeHtml(code.replace(/```/g, '')); var sitemapInfo = (data.data && data.data.sitemap_used && data.data.sitemap_steps) ? ' (sitemap: ' + data.data.sitemap_steps + ' stappen)' : ''; messages.innerHTML += aiMsgWithCodeBlock('✅ Code Gegenereerd!
    Toegepast: ' + escapeHtml(applied.join(', ')) + sitemapInfo + '
    ', codeDisplay, code); addTtsButtonToLastAiMessage(); switchToPreviewTab(); speakResponse('Code gegenereerd. Toegepast: ' + applied.join(', ') + '.'); } else { setPlanContent('

    Opdracht

    ' + escapeHtml(truncateForDisplay(description, MAX_OPDRACHT_DISPLAY)) + '

    Geen code-blokken toegepast.

    '); var errMsg = data.error || data.data?.text || 'Kon geen code genereren'; var noKey = (data.data && data.data.error === 'no_api_key') || /geen.*ai.*provider|stel.*api.*key/i.test(errMsg + ''); if (noKey) { updateProjectCard({ pct: 0, status: 'Klaar', busy: false }); updateFlowDesc(['AI wordt geladen…']); messages.innerHTML += '
    AI is momenteel niet beschikbaar. Probeer het later opnieuw of neem contact op met support.
    '; } else { messages.innerHTML += '
    ⚠️ ' + escapeHtml(errMsg) + '
    '; } } } else { var errMsg2 = data.error || data.data?.text || 'Kon geen code genereren'; var noKey2 = (data.data && data.data.error === 'no_api_key') || /geen.*ai.*provider|stel.*api.*key/i.test(errMsg2 + ''); if (noKey2) { updateProjectCard({ pct: 0, status: 'Klaar', busy: false }); updateFlowDesc(['AI wordt geladen…']); messages.innerHTML += '
    AI is momenteel niet beschikbaar. Probeer het later opnieuw of neem contact op met support.
    '; } else { messages.innerHTML += '
    ⚠️ ' + escapeHtml(errMsg2) + '
    '; } } } catch (err) { const thinkingDiv = document.getElementById(thinkingId); if (thinkingDiv && thinkingDiv.parentNode) thinkingDiv.remove(); updateProjectCard({ pct: 0, status: 'Fout', busy: false }); messages.innerHTML += '
    ❌ ' + escapeHtml(err.message) + '
    '; } scrollChatToBottom(true); } async function sendMessage() { const input = document.getElementById('input'); const msg = input.value.trim(); if (!msg) return; // Detect features and enhance prompt const features = detectFeaturesFromPrompt(msg); const enhancedMsg = enhancePromptWithFeatures(msg, features); // Check if we should use a specific template const template = generateEnhancedTemplate(msg); if (template) { // Apply the template directly for specific features files['index.html'] = template; if (editor && activeFile === 'index.html') { editor.setValue(template); } updatePreview(); if (typeof saveFilesToStorage === 'function') saveFilesToStorage(); // Add success message const messages = document.getElementById('messages'); messages.innerHTML += userMsg(msg); messages.innerHTML += aiMsgWithCodeBlock( '✅ Template Gegenereerd!
    Features gedetecteerd: ' + Object.keys(features).join(', ') + '
    ', template, template ); addTtsButtonToLastAiMessage(); switchToPreviewTab(); speakResponse('Template gegenereerd voor ' + Object.keys(features).join(', ') + '.'); input.value = ''; return; } // Enhanced API calls based on detected features let apiData = null; if (features.kvkIntegration && msg.includes('zoek')) { // Extract search query for KvK const searchQuery = msg.replace(/zoek|vind|bedrijfs|bedrijven/gi, '').trim(); if (searchQuery) { apiData = await callKvkAPI(searchQuery); } } else if (features.sbiAnalysis && msg.includes('analyseer')) { // Extract SBI code for analysis const sbiMatch = msg.match(/\b(\d{4})\b/); if (sbiMatch) { apiData = await callSBIAnalyzer(sbiMatch[1]); } } else if (features.aiOrchestration && msg.includes('start')) { // Extract company name for orchestrator const companyMatch = msg.match(/(?:voor|met|start)\s+(.+?)(?:\s|$)/i); if (companyMatch) { apiData = await callAIOrchestrator(companyMatch[1]); } } if (pendingClarification) { const answer = msg; const fullDesc = pendingClarification.description + '\n\nVerduidelijking klant: ' + answer; const savedPlan = pendingClarification.planText; pendingClarification = null; const messages = document.getElementById('messages'); messages.innerHTML += '
    ' + escapeHtml(truncateForDisplay(answer, MAX_USER_MSG_DISPLAY, ' …')) + '
    '; input.value = ''; scrollChatToBottom(true); await doGenerateWithPlan(fullDesc, savedPlan); return; } let mode = (document.querySelector('input[name="sendMode"]:checked') || {}).value || 'auto'; if (mode === 'auto') { try { const classifyRes = await fetch('/api/ai-code-editor-api.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'classify', message: enhancedMsg.slice(0, 500), prompt: enhancedMsg.slice(0, 500), features: features }) }); const classifyData = await safeJsonFromResponse(classifyRes); if (classifyData.success && classifyData.data && classifyData.data.chain) { mode = classifyData.data.chain === 'code-gen' ? 'build' : 'chat'; window._lastClassifyReason = classifyData.data.reason || ''; } else { window._lastClassifyReason = ''; const r = computeClarity(enhancedMsg); mode = r.suggestedMode; } } catch (e) { window._lastClassifyReason = ''; const r = computeClarity(enhancedMsg); mode = r.suggestedMode; } } const messages = document.getElementById('messages'); messages.innerHTML += `
    ${escapeHtml(truncateForDisplay(msg, MAX_USER_MSG_DISPLAY, ' …'))}
    `; input.value = ''; scrollChatToBottom(true); if (mode === 'chat') { await sendChatOnly(msg, messages); return; } var effectiveDesc = getEffectiveDescription(msg, messages); // ——— Plan + code bouwen ——— const chainEl = document.getElementById('flowChain'); var classifyReason = window._lastClassifyReason || ''; if (chainEl) chainEl.textContent = 'Chain: code-gen' + (classifyReason && classifyReason !== 'ai' && classifyReason !== 'empty' ? ' (' + classifyReason + ')' : ''); updateProjectCard({ task: msg.slice(0, 80) + (msg.length > 80 ? '…' : ''), pct: 0, status: 'Bezig…', busy: true }); updateFlowchart(1); updateFlowDesc(FLOW_DESC_STEPS, 0); // Tabs: start op Plan, toon opdracht switchCenterTab('plan'); var opdrachtDisplay = truncateForDisplay(msg, MAX_OPDRACHT_DISPLAY); var longHint = msg.length > MAX_DESCRIPTION_API ? '

    (Alleen eerste ' + MAX_DESCRIPTION_API + ' tekens naar AI gestuurd.)

    ' : ''; setPlanContent('

    Opdracht

    ' + escapeHtml(opdrachtDisplay) + '

    ' + longHint + '

    Plan wordt gegenereerd…

    '); setTasksContent('

    Taken volgen na plan.

    '); // Show AI thinking steps const thinkingId = 'thinking-' + Date.now(); messages.innerHTML += `
    Analyseren van je vraag...
    Plan maken...
    Code genereren...
    Toepassen...
    `; scrollChatToBottom(true); const thinkingDiv = document.getElementById(thinkingId); const steps = thinkingDiv.querySelectorAll('.thinking-step'); const progressBar = thinkingDiv.querySelector('.progress-fill'); let planText = ''; let planSteps = []; let needClarification = false; let clarifyingQuestions = []; for (let i = 0; i < steps.length; i++) { await new Promise(r => setTimeout(r, 400)); if (i > 0) steps[i-1].classList.add('done'); steps[i].classList.add('active'); const pct = Math.round((i + 1) / steps.length * 100); progressBar.style.width = pct + '%'; updateProjectCard({ pct: pct, status: 'Bezig…', busy: true }); updateFlowchart(i + 2); updateFlowDesc(FLOW_DESC_STEPS, i + 1); if (i === 0) { var isRestaurant = isRestaurantRequest(effectiveDesc); setTasksContent('

    Taken

    • Branche ophalen (SBI/GMB)…
    • ' + (isRestaurant ? '
    • Plan met menukaart (live), keuken, reserveren
    • ' : '') + '
    • Plan genereren…
    • Code genereren
    • Toepassen in editor + preview
    '); try { lastBranchData = await fetchBranchData(effectiveDesc); if (lastBranchData && lastBranchData.branch_name) { var flowLines = ['Branche: ' + (lastBranchData.branch_name || '').slice(0, 40) + (lastBranchData.sbi_code ? ' (SBI ' + lastBranchData.sbi_code + ')' : ''), 'Plan maken…']; if (isRestaurant) flowLines = ['SBI/Horeca', 'Plan (menukaart live, keuken, reserveren)…']; updateFlowDesc(flowLines); } } catch (e) { lastBranchData = null; } } if (i === 1) { try { const planPayload = { action: 'plan', description: descriptionForApi(effectiveDesc), context: files }; if (isStrawberryRequest(effectiveDesc)) planPayload.template = 'strawberry'; if (isRestaurantRequest(effectiveDesc)) planPayload.template = 'restaurant'; if (lastBranchData) planPayload.branch_data = lastBranchData; var maxT = getMaxTokensForBuild(); if (maxT > 0) planPayload.max_tokens = maxT; const planRes = await fetch('/api/ai-code-editor-api.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(planPayload) }); const planData = await safeJsonFromResponse(planRes); if (planData.success && planData.data?.plan) { planText = planData.data.plan; planSteps = planData.data.steps || []; clarifyingQuestions = planData.data.clarifying_questions || []; if (clarifyingQuestions.length > 0) needClarification = true; const planHtml = '

    Plan

    Opdracht: ' + escapeHtml(truncateForDisplay(effectiveDesc, MAX_OPDRACHT_DISPLAY)) + '

    ' + escapeHtml(planText).replace(/\n/g, '
    ') + '
    '; setPlanContent(planHtml, planText); if (planSteps.length) setTasksContent('

    Taken (uit plan)

      ' + renderPlanStepsHtml(planSteps) + '
    '); switchCenterTab('flow'); } else { setPlanContent('

    Opdracht

    ' + escapeHtml(truncateForDisplay(effectiveDesc, MAX_OPDRACHT_DISPLAY)) + '

    Plan niet beschikbaar; code wordt direct gegenereerd.

    '); } } catch (e) { setPlanContent('

    Opdracht

    ' + escapeHtml(truncateForDisplay(effectiveDesc, MAX_OPDRACHT_DISPLAY)) + '

    Plan-fase mislukt: ' + escapeHtml(e.message) + '

    '); } } } if (needClarification && clarifyingQuestions.length > 0) { thinkingDiv.remove(); updateProjectCard({ pct: 40, status: 'Wacht op verduidelijking', busy: false }); updateFlowDesc(['Plan klaar', 'Wacht op je antwoorden op de vragen hieronder']); var planSummary = (planText || effectiveDesc || '').trim().split('\n')[0].slice(0, 140) || effectiveDesc.slice(0, 140) || 'Opdracht'; var qList = clarifyingQuestions.map(function(q, i) { return '
    Vraag ' + (i + 1) + ': ' + escapeHtml(q) + '
    '; }).join(''); const qHtml = '
    Opdracht → Plan ✓ → Verduidelijking → Code
    Plan: ' + escapeHtml(planSummary) + (planSummary.length >= 140 ? '…' : '') + '
    Om beter te kunnen bouwen:
    ' + qList + '

    Beantwoord hieronder en verstuur, of kies:

    '; messages.innerHTML += '
    ' + qHtml + '
    '; pendingClarification = { planText: planText, description: effectiveDesc, planSteps: planSteps }; scrollChatToBottom(true); document.getElementById('btnBouwTochDoor').addEventListener('click', function() { if (!pendingClarification) return; var desc = pendingClarification.description; var pPlan = pendingClarification.planText; pendingClarification = null; var clarifyEl = document.querySelector('.msg.ai.clarify'); if (clarifyEl && clarifyEl.parentNode) clarifyEl.remove(); updateProjectCard({ pct: 50, status: 'Bezig…', busy: true }); doContinueBuild(desc, pPlan); }); return; } pendingPlanContinue = { planText: planText, description: effectiveDesc, planSteps: planSteps }; thinkingDiv.remove(); updateProjectCard({ pct: 50, status: 'Plan klaar – kies actie', busy: false }); updateFlowDesc(['Plan klaar', 'Kies hieronder wat je wilt']); var planReadyId = 'plan-ready-' + Date.now(); var sitemapHtml = ''; try { var sitemapRes = await fetch('/api/ai-code-editor-api.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'sitemap', plan: planText, description: effectiveDesc }) }); var sitemapData = await sitemapRes.json().catch(function() { return {}; }); if (sitemapData.success && sitemapData.data && sitemapData.data.sitemap && sitemapData.data.sitemap.length > 0) { var sitemapSteps = sitemapData.data.sitemap; pendingPlanContinue.sitemap = sitemapSteps; sitemapHtml = '
    Sitemap:
      ' + sitemapSteps.map(function(s) { return '
    1. ' + escapeHtml(s.label || s.slug || '') + '
    2. '; }).join('') + '
    '; } } catch (e) { pendingPlanContinue.sitemap = []; } var cardHtml = '
    Opdracht → Plan ✓ → Kies actie → Code

    Plan klaar. Wat wil je?

    Kies om door te gaan of het plan eerst aan te passen.

    ' + sitemapHtml + '
    '; messages.innerHTML += cardHtml; scrollChatToBottom(true); document.getElementById(planReadyId).querySelectorAll('.btns button').forEach(function(btn) { btn.addEventListener('click', function() { var action = this.getAttribute('data-action'); if (action === 'edit') { var input = document.getElementById('input'); if (input) { input.placeholder = 'Beschrijf je aanpassing aan het plan en verstuur…'; input.focus(); } pendingPlanContinue = null; return; } if (action === 'steps') { if (!pendingPlanContinue || !pendingPlanContinue.sitemap || pendingPlanContinue.sitemap.length === 0) { alert('Geen sitemap beschikbaar. Kies "Ga door met bouwen" of "Bouw hele app".'); return; } var cardEl = document.getElementById(planReadyId); if (cardEl && cardEl.parentNode) cardEl.remove(); doBuildStepByStep(pendingPlanContinue.description, pendingPlanContinue.planText, pendingPlanContinue.sitemap); pendingPlanContinue = null; return; } var wholeApp = (action === 'app'); if (!pendingPlanContinue) return; var desc = pendingPlanContinue.description + (wholeApp ? ' Bouw als volledige app (alle stappen uit het plan).' : ''); var pPlan = pendingPlanContinue.planText; pendingPlanContinue = null; var cardEl = document.getElementById(planReadyId); if (cardEl && cardEl.parentNode) cardEl.remove(); doContinueBuild(desc, pPlan); }); }); return; } async function doBuildStepByStep(description, planText, sitemap) { const messages = document.getElementById('messages'); const total = sitemap.length; var accumulatedFiles = { 'index.html': files['index.html'] || '', 'style.css': files['style.css'] || '', 'script.js': files['script.js'] || '' }; for (var step = 0; step < total; step++) { var item = sitemap[step]; var label = item.label || item.slug || ('Stap ' + (step + 1)); var thinkingId = 'thinking-step-' + step + '-' + Date.now(); messages.innerHTML += '
    Stap ' + (step + 1) + '/' + total + ': ' + escapeHtml(label) + '…
    '; scrollChatToBottom(true); updateProjectCard({ pct: 50 + Math.round((step + 1) / total * 50), status: 'Stap ' + (step + 1) + '/' + total + ': ' + label, busy: true }); try { var genPayload = { action: 'generate', description: description, language: 'html', context: accumulatedFiles, plan: planText, sitemap: sitemap, build_step: step + 1, step_label: label }; if (isRestaurantRequest(description)) genPayload.template = 'restaurant'; if (lastBranchData) genPayload.branch_data = lastBranchData; var maxT = getMaxTokensForBuild(); if (maxT > 0) genPayload.max_tokens = maxT; var res = await fetch('/api/ai-code-editor-api.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(genPayload) }); var data = await res.json().catch(function() { return {}; }); var thinkingDiv = document.getElementById(thinkingId); if (thinkingDiv && thinkingDiv.parentNode) thinkingDiv.remove(); if (data.success && data.data && data.data.code) { var code = data.data.code; var htmlM = code.match(/```html\n([\s\S]*?)```/); var cssM = code.match(/```css\n([\s\S]*?)```/); var jsM = code.match(/```javascript\n([\s\S]*?)```/); if (htmlM) accumulatedFiles['index.html'] = htmlM[1].trim(); if (cssM) accumulatedFiles['style.css'] = cssM[1].trim(); if (jsM) accumulatedFiles['script.js'] = jsM[1].trim(); files['index.html'] = accumulatedFiles['index.html']; files['style.css'] = accumulatedFiles['style.css']; files['script.js'] = accumulatedFiles['script.js']; if (editor && activeFile in files) editor.setValue(files[activeFile]); updatePreview(); saveFilesToStorage(); messages.innerHTML += '
    ✓ Stap ' + (step + 1) + '/' + total + ': ' + escapeHtml(label) + ' toegepast.
    '; } else { messages.innerHTML += '
    ⚠️ Stap ' + (step + 1) + ': ' + escapeHtml((data.error || data.data?.text || 'Mislukt') + '') + '
    '; } } catch (err) { var thinkingDiv = document.getElementById(thinkingId); if (thinkingDiv && thinkingDiv.parentNode) thinkingDiv.remove(); messages.innerHTML += '
    ❌ Stap ' + (step + 1) + ': ' + escapeHtml(err.message) + '
    '; } scrollChatToBottom(true); } updateProjectCard({ pct: 100, status: 'Klaar', busy: false }); updateFlowchart(6); switchToPreviewTab(); } async function doContinueBuild(description, planText) { const messages = document.getElementById('messages'); const thinkingId = 'thinking-continue-' + Date.now(); messages.innerHTML += '
    Code genereren…
    '; scrollChatToBottom(true); updateProjectCard({ pct: 70, status: 'Bezig…', busy: true }); updateFlowchart(4); try { const genPayload = { action: 'generate', description: descriptionForApi(description), language: 'html', context: files, plan: planText }; if (isStrawberryRequest(description)) genPayload.template = 'strawberry'; if (isRestaurantRequest(description)) genPayload.template = 'restaurant'; if (lastBranchData) genPayload.branch_data = lastBranchData; var maxT2 = getMaxTokensForBuild(); if (maxT2 > 0) genPayload.max_tokens = maxT2; if (planText && planText.length > 100) { try { var stepEl2 = document.querySelector('#' + thinkingId + '-step .thinking-text'); if (stepEl2) stepEl2.textContent = 'Sitemap ophalen…'; const smRes = await fetch('/api/ai-code-editor-api.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'sitemap', plan: planText, description: descriptionForApi(description) }) }); const smData = await safeJsonFromResponse(smRes); if (smData.success && smData.data && Array.isArray(smData.data.sitemap) && smData.data.sitemap.length > 0) genPayload.sitemap = smData.data.sitemap; if (stepEl2) stepEl2.textContent = 'Afbeeldingen ophalen…'; var imgUrls = []; var qs2 = /\brestaurant|horeca|eten|menu\b/i.test(description) ? ['restaurant hero interior', 'restaurant food dish'] : (/\bwebshop|shop|winkel\b/i.test(description) ? ['online shop hero', 'product ecommerce'] : ['professional business hero']); for (var qk = 0; qk < Math.min(2, qs2.length); qk++) { var imgR = await fetch('/api/ai-code-editor-api.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'image_for_code', q: qs2[qk], context: description.slice(0, 100) }) }); var imgD = await imgR.json().catch(function() { return {}; }); if (imgD.success && imgD.data && imgD.data.url) imgUrls.push({ url: imgD.data.url, alt: imgD.data.alt_suggestion || qs2[qk] }); } if (imgUrls.length > 0) genPayload.image_urls = imgUrls; if (stepEl2) stepEl2.textContent = 'Code genereren…'; } catch (e) { /* sitemap/images optioneel */ } } const response = await fetch('/api/ai-code-editor-api.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(genPayload) }); const data = await safeJsonFromResponse(response); const thinkingDiv = document.getElementById(thinkingId); if (thinkingDiv && thinkingDiv.parentNode) thinkingDiv.remove(); updateProjectCard({ pct: 100, status: 'Klaar', busy: false }); updateFlowchart(6); if (data.success && data.data && data.data.code) { const code = data.data.code; const htmlMatch = code.match(/```html\n([\s\S]*?)```/); const cssMatch = code.match(/```css\n([\s\S]*?)```/); const jsMatch = code.match(/```javascript\n([\s\S]*?)```/); let applied = []; if (htmlMatch) { files['index.html'] = htmlMatch[1].trim(); applied.push('HTML'); } if (cssMatch) { files['style.css'] = cssMatch[1].trim(); applied.push('CSS'); } if (jsMatch) { files['script.js'] = jsMatch[1].trim(); applied.push('JavaScript'); } if (applied.length > 0) { if (editor && activeFile in files) editor.setValue(files[activeFile]); updatePreview(); if (typeof saveFilesToStorage === 'function') saveFilesToStorage(); updateFlowDesc(['Klaar – toegepast: ' + applied.join(', ')]); setPlanContent('

    Plan (gebruikt voor code)

    Opdracht: ' + escapeHtml(truncateForDisplay(description, MAX_OPDRACHT_DISPLAY)) + '

    ' + escapeHtml(planText || '').replace(/\n/g, '
    ') + '

    Toegepast: ' + escapeHtml(applied.join(', ')) + '

    ', planText || null); setTasksContent('

    Uitgevoerd

    • ✓ Plan
    • ✓ Code generatie
    • ✓ Toepassen (' + escapeHtml(applied.join(', ')) + ')
    '); const skeletonTags = extractSkeletonFromHtml(files['index.html']); const jsSummary = extractJsSummary(files['script.js'] || ''); const mediaItems = extractMediaFromHtml(files['index.html'] || ''); let skeletonHtml = '

    HTML-structuur

    ' + escapeHtml(skeletonTags.join(', ')) + '

    JavaScript

    ' + escapeHtml(jsSummary.summary) + '

    Video / media

    ' + (mediaItems.length ? '
      ' + mediaItems.map(m => '
    • ' + escapeHtml(m.label) + '
    • ').join('') + '
    ' : '

    Geen video/audio in HTML.

    '); setSkeletonContent(skeletonHtml); setBlueprintContent('

    Blauwdruk

    Toegepast: ' + escapeHtml(applied.join(', ')) + '.

    '); var liveEl = document.getElementById('liveLinkDisplay'); if (liveEl) liveEl.innerHTML = 'Preview in tabblad →'; const codeDisplay = escapeHtml(code.replace(/```/g, '')); var sitemapInfo2 = (data.data && data.data.sitemap_used && data.data.sitemap_steps) ? ' (sitemap: ' + data.data.sitemap_steps + ' stappen)' : ''; messages.innerHTML += aiMsgWithCodeBlock('✅ Code Gegenereerd!
    Toegepast: ' + escapeHtml(applied.join(', ')) + sitemapInfo2 + '
    ', codeDisplay, code); addTtsButtonToLastAiMessage(); switchToPreviewTab(); speakResponse('Code gegenereerd. Toegepast: ' + applied.join(', ') + '.'); } else { setPlanContent('

    Opdracht

    ' + escapeHtml(truncateForDisplay(description, MAX_OPDRACHT_DISPLAY)) + '

    Geen code-blokken toegepast.

    '); var errMsgM = data.error || data.data?.text || 'Kon geen code genereren'; var noKeyM = (data.data && data.data.error === 'no_api_key') || /geen.*ai.*provider|stel.*api.*key/i.test(errMsgM + ''); if (noKeyM) { updateProjectCard({ pct: 0, status: 'Klaar', busy: false }); updateFlowDesc(['AI wordt geladen…']); messages.innerHTML += '
    AI is momenteel niet beschikbaar. Probeer het later opnieuw of neem contact op met support.
    '; } else { messages.innerHTML += '
    ⚠️ ' + escapeHtml(errMsgM) + '
    '; } } } else { var errMsgM2 = data.error || data.data?.text || 'Kon geen code genereren'; var noKeyM2 = (data.data && data.data.error === 'no_api_key') || /geen.*ai.*provider|stel.*api.*key/i.test(errMsgM2 + ''); if (noKeyM2) { updateProjectCard({ pct: 0, status: 'Klaar', busy: false }); updateFlowDesc(['AI wordt geladen…']); messages.innerHTML += '
    AI is momenteel niet beschikbaar. Probeer het later opnieuw of neem contact op met support.
    '; } else { messages.innerHTML += '
    ⚠️ ' + escapeHtml(errMsgM2) + '
    '; } } } catch (err) { const thinkingDiv = document.getElementById(thinkingId); if (thinkingDiv && thinkingDiv.parentNode) thinkingDiv.remove(); updateProjectCard({ pct: 0, status: 'Fout', busy: false }); messages.innerHTML += '
    ❌ ' + escapeHtml(err.message) + '
    '; } scrollChatToBottom(true); } ?>