]+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 = ' Opslaan in bibliotheek ';
}
return '' + titleHtml + '
' + codeHtml + ' Kopiëren' + 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('\n\n\nIT Live Demo \nVraag 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:
Bouw toch door (met redelijke aannames)
';
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 '' + escapeHtml(s.label || s.slug || '') + ' '; }).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 + '
Ga door met bouwen Bouw hele app Bouw stap voor stap Pas plan aan (typ in chat)
';
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 += '';
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);
}