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)
Externe AI code tools

Naast deze editor: Claude Code (terminal/IDE) en Cursor (ChatGPT) – installeren en naast elkaar gebruiken.

Claude Code Cursor AI Providers
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?
broken to avoid closing parser) function generateBedrijvencheckTemplate() { return ` Bedrijvencheck - IT Live

🔍 Bedrijvencheck

Zoek bedrijven in de KvK database

Voer een zoekterm in om bedrijven te vinden.

async function searchCompanies() { const query = document.getElementById('searchInput').value.trim(); if (!query) return; const resultsDiv = document.getElementById('results'); resultsDiv.innerHTML = '

Zoeken...

'; try { const response = await fetch('/api.itlive.nl/bedrijvencheck-kvk-api.php?q=' + encodeURIComponent(query)); const data = await response.json(); if (data.ok && data.resultaten) { resultsDiv.innerHTML = data.resultaten.map(function(c) { return '
' + '
' + (c.naam || '') + '
' + '
KvK: ' + (c.kvkNummer || 'Onbekend') + ' | Plaats: ' + (c.plaats || 'Onbekend') + ' | SBI: ' + (c.sbi && c.sbi[0] ? c.sbi[0].code : 'Onbekend') + '
'; }).join(''); } else { resultsDiv.innerHTML = '

Geen resultaten gevonden.

'; } } catch (error) { resultsDiv.innerHTML = '

Fout bij zoeken: ' + error.message + '

'; } } document.getElementById('searchInput').addEventListener('keypress', function(e) { if (e.key === 'Enter') searchCompanies(); });
`; } 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

async function searchCompany() { var query = document.getElementById('companySearch').value.trim(); if (!query) return; var resultsDiv = document.getElementById('searchResults'); resultsDiv.innerHTML = '

Zoeken...

'; try { var response = await fetch('/api.itlive.nl/bedrijvencheck-kvk-api.php?q=' + encodeURIComponent(query)); var data = await response.json(); if (data.ok && data.resultaten) { resultsDiv.innerHTML = data.resultaten.map(function(c) { var naam = (c.naam || '').replace(/'/g, "\\'"); var kvk = (c.kvkNummer || '').replace(/'/g, "\\'"); var plaats = (c.plaats || ''); return `
${c.naam || ''}
${plaats} | KvK: ${c.kvkNummer || ''}
`; }).join(''); } else { resultsDiv.innerHTML = '

Geen resultaten gevonden.

'; } } catch (err) { resultsDiv.innerHTML = '

Fout: ' + err.message + '

'; } } function selectCompany(name, kvk) { document.getElementById('pipelineResults').innerHTML = '

Geselecteerd: ' + (name || '') + '

KvK: ' + (kvk || '') + '

'; } function startPipeline() { var steps = document.querySelectorAll('.step'); steps.forEach(function(step, index) { setTimeout(function() { step.classList.add('active'); }, index * 500); }); }
`; } 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.

var marketChart, competitionChart; async function analyzeSBI() { var sbiCode = document.getElementById('sbiInput').value.trim(); var location = document.getElementById('locationInput').value.trim(); if (!sbiCode) { alert('Voer een SBI code in'); return; } var resultsDiv = document.getElementById('analysisResults'); resultsDiv.innerHTML = '

Analyseren...

'; try { var params = new URLSearchParams({ sbi: sbiCode }); if (location) params.append('plaats', location); var response = await fetch('/api/sbi-analyzer-api.php?' + params.toString()); var data = await response.json(); if (data.success) { displayAnalysis(data.data); updateCharts(data.data); } else { resultsDiv.innerHTML = '

Analyse mislukt: ' + (data.error || 'Onbekende fout') + '

'; } } catch (err) { resultsDiv.innerHTML = '

Fout: ' + err.message + '

'; } } function displayAnalysis(data) { var resultsDiv = document.getElementById('analysisResults'); var d = data || {}; resultsDiv.innerHTML = '
' + '
' + (d.market_size || 'N/A') + '
Markt Grootte
' + '
' + (d.competitors || 'N/A') + '
Concurrenten
' + '
' + (d.growth_rate || 'N/A') + '%
Groei Rate
' + '

Markt Analyse

' + (d.market_analysis || 'Geen analyse beschikbaar.') + '

' + '

Concurrentie Analyse

' + (d.competition_analysis || 'Geen concurrentie analyse beschikbaar.') + '

' + '

Trends

' + (d.trends || 'Geen trends beschikbaar.') + '

'; } function updateCharts(data) { // Market size chart const marketCtx = document.getElementById('marketChart').getContext('2d'); if (marketChart) marketChart.destroy(); marketChart = new Chart(marketCtx, { type: 'bar', data: { labels: ['Huidig', 'Potentieel'], datasets: [{ label: 'Markt Grootte', data: [data.current_market || 0, data.potential_market || 0], backgroundColor: ['#10b981', '#059669'] }] }, options: { responsive: true, plugins: { legend: { display: false } } } }); // Competition chart const competitionCtx = document.getElementById('competitionChart').getContext('2d'); if (competitionChart) competitionChart.destroy(); competitionChart = new Chart(competitionCtx, { type: 'doughnut', data: { labels: ['Direct', 'Indirect', 'Potentieel'], datasets: [{ data: [data.direct_competitors || 0, data.indirect_competitors || 0, data.potential_competitors || 0], backgroundColor: ['#ef4444', '#f59e0b', '#10b981'] }] }, options: { responsive: true, plugins: { legend: { position: 'bottom' } } } }); }
`; } // 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.itlive.nl/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.itlive.nl/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.itlive.nl/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.itlive.nl/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.itlive.nl/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.itlive.nl/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.itlive.nl/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.itlive.nl/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.itlive.nl/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); } })(); /** Base path uit URL (?base=...) – o.a. vanuit Mijn Webspace / admin code-editor; orchestrators gebruiken dezezelfde editor. */ (function() { var m = /[?&]base=([^&]+)/.exec(window.location.search || ''); if (m && m[1]) { var basePath = decodeURIComponent(m[1]).trim(); if (basePath) try { sessionStorage.setItem('ai_editor_base_path', basePath); } catch (e) {} } })(); /** 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.itlive.nl/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.itlive.nl/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.itlive.nl/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.itlive.nl/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.itlive.nl/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.itlive.nl/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.itlive.nl/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); } // Missing API functions - add these before sendMessage() async function callKvkAPI(query) { try { const response = await fetch('/api.itlive.nl/bedrijvencheck-kvk-api.php?naam=' + encodeURIComponent(query)); const data = await response.json(); return data; } catch (e) { console.error('KVK API error:', e); return null; } } async function callSBIAnalyzer(sbiCode) { try { const response = await fetch('/api/sbi-analyzer-api.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'analyze', sbi_code: sbiCode }) }); const data = await response.json(); return data; } catch (e) { console.error('SBI Analyzer error:', e); return null; } } async function callAIOrchestrator(company) { try { const response = await fetch('/api/ai-orchestrator-api.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'start_pipeline', company: company }) }); const data = await response.json(); return data; } catch (e) { console.error('AI Orchestrator error:', e); return null; } } async function sendMessage() { const input = document.getElementById('input'); const msg = input.value.trim(); if (!msg) return; // Simple template detection for special cases if (msg.toLowerCase().includes('kvk') && msg.toLowerCase().includes('zoek')) { // Handle KvK search const searchQuery = msg.replace(/zoek|vind|bedrijfs|bedrijven/gi, '').trim(); if (searchQuery) { const apiData = await callKvkAPI(searchQuery); if (apiData && apiData.success) { messages.innerHTML += '
    KVK resultaten gevonden: ' + JSON.stringify(apiData.data, null, 2) + '
    '; scrollChatToBottom(true); input.value = ''; return; } } } if (msg.toLowerCase().includes('sbi') && msg.toLowerCase().includes('analyseer')) { // Handle SBI analysis const sbiMatch = msg.match(/\b(\d{4})\b/); if (sbiMatch) { const apiData = await callSBIAnalyzer(sbiMatch[1]); if (apiData && apiData.success) { messages.innerHTML += '
    SBI analyse: ' + JSON.stringify(apiData.data, null, 2) + '
    '; scrollChatToBottom(true); input.value = ''; return; } } } 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.itlive.nl/ai-code-editor-api.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'classify', message: msg.slice(0, 500), prompt: msg.slice(0, 500) }) }); 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(msg); mode = r.suggestedMode; } } catch (e) { window._lastClassifyReason = ''; const r = computeClarity(msg); 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.itlive.nl/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.itlive.nl/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.itlive.nl/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.itlive.nl/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.itlive.nl/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.itlive.nl/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); }