/* ---------- Config ---------- */ const API_CORE = "https://thevera-backendvera.hf.space"; const API_DB = "https://thevera-botveradb.hf.space"; /* ---------- Elements ---------- */ const chatBody = document.getElementById("chatBody"); const userInput = document.getElementById("userInput"); const sendButton = document.getElementById("sendButton"); const micButton = document.getElementById("micButton"); const categorySelect = document.getElementById("categorySelect"); const speakToggle = document.getElementById("speakToggle"); const voiceSelect = document.getElementById("voiceSelect"); const settingsModal = document.getElementById("settingsModal"); const ttsLang = document.getElementById("ttsLang"); const ttsRate = document.getElementById("ttsRate"); const ttsPitch = document.getElementById("ttsPitch"); const attachButton = document.getElementById("attachButton"); const healthImage = document.getElementById("healthImage"); attachButton?.addEventListener("click", ()=> healthImage?.click()); healthImage?.addEventListener("change", ()=>{ const f = healthImage.files?.[0]; if (f) toast(`Attached: ${f.name}`); }); /* Mobile sidebar controls */ const openSidebarBtn = document.getElementById("openSidebar"); const closeSidebarBtn = document.getElementById("closeSidebar"); const sidebar = document.getElementById("sidebar"); const scrim = document.getElementById("scrim"); /* Header drawer controls (mobile) */ const openHeaderDrawer = document.getElementById("openHeaderDrawer"); const closeHeaderDrawer = document.getElementById("closeHeaderDrawer"); const headerDrawer = document.getElementById("headerDrawer"); const drawerScrim = document.getElementById("drawerScrim"); const headerControls = document.getElementById("headerControls"); const drawerControls = document.getElementById("drawerControls"); /* Theme toggle */ const themeToggle = document.getElementById("themeToggle"); /* Username pill */ const userPill = document.getElementById("userPill"); /* ---------- Toasts ---------- */ function toast(msg){ const host = document.getElementById("toasts"); const el = document.createElement("div"); el.className = "toast"; el.textContent = msg; host.appendChild(el); setTimeout(()=> host.contains(el) && host.removeChild(el), 3000); } /* ---------- Sidebar toggle (mobile) ---------- */ function openSidebar(){ sidebar.classList.add("open"); scrim.classList.add("show"); } function closeSidebar(){ sidebar.classList.remove("open"); scrim.classList.remove("show"); } openSidebarBtn?.addEventListener("click", openSidebar); closeSidebarBtn?.addEventListener("click", closeSidebar); scrim?.addEventListener("click", closeSidebar); /* ---------- Header drawer (mobile) ---------- */ const headerControlsEl = document.querySelector(".header-controls"); // safer than getElementById function mountDrawerControls(){ if (!drawerControls) return; // Build a SAFE mirror (no duplicate IDs) drawerControls.innerHTML = `
`; // Mirror value both ways const drawerCat = document.getElementById("drawerCategory"); if (drawerCat && categorySelect) { drawerCat.value = categorySelect.value || "auto"; drawerCat.onchange = () => { categorySelect.value = drawerCat.value; }; categorySelect.onchange = () => { drawerCat.value = categorySelect.value; }; } const drawerBtn = document.getElementById("drawerOpenSettings"); drawerBtn && (drawerBtn.onclick = () => settingsModal.showModal()); } mountDrawerControls(); window.addEventListener("resize", mountDrawerControls); function openDrawer(){ headerDrawer.classList.add("open"); drawerScrim.classList.add("show"); headerDrawer.setAttribute("aria-hidden","false"); } function closeDrawer(){ headerDrawer.classList.remove("open"); drawerScrim.classList.remove("show"); headerDrawer.setAttribute("aria-hidden","true"); } openHeaderDrawer?.addEventListener("click", openDrawer); closeHeaderDrawer?.addEventListener("click", closeDrawer); drawerScrim?.addEventListener("click", closeDrawer); // ---------- Settings persistence ---------- const SETTINGS_KEY = "vera_settings_v1"; const defaultSettings = { ttsLang: "auto", voiceName: "", // store by voice.name (more stable than index) alwaysSpeak: true, rate: 0.95, pitch: 0.95, theme: "saffron", imgVision: false }; function loadSettings(){ // migrate old theme if present const legacyTheme = localStorage.getItem("vera_theme"); let s = {}; try { s = JSON.parse(localStorage.getItem(SETTINGS_KEY) || "{}"); } catch {} s = { ...defaultSettings, ...s }; if (legacyTheme && (legacyTheme === "dark" || legacyTheme === "saffron")) { s.theme = legacyTheme; localStorage.removeItem("vera_theme"); } return s; } function saveSettings(){ const s = { ttsLang: ttsLang?.value || "auto", voiceName: (voiceSelect && voiceSelect.selectedOptions[0]?.text?.split(" — ")?.[0]) || "", alwaysSpeak: !!speakToggle?.checked, rate: Number(ttsRate?.value ?? defaultSettings.rate), pitch: Number(ttsPitch?.value ?? defaultSettings.pitch), theme: themeToggle?.checked ? "dark" : "saffron", imgVision: !!imgVision?.checked }; localStorage.setItem(SETTINGS_KEY, JSON.stringify(s)); // apply immediately where needed document.documentElement.setAttribute("data-theme", s.theme); if (themeToggle) themeToggle.checked = (s.theme === "dark"); return s; } function applySettings(s){ if (!s) s = loadSettings(); // UI controls if (ttsLang) ttsLang.value = s.ttsLang; if (speakToggle) speakToggle.checked = !!s.alwaysSpeak; if (ttsRate) ttsRate.value = String(s.rate); if (ttsPitch) ttsPitch.value = String(s.pitch); if (themeToggle) themeToggle.checked = (s.theme === "dark"); if (imgVision) imgVision.checked = !!s.imgVision; document.documentElement.setAttribute("data-theme", s.theme); // Voice selection happens after voices load if (voiceSelect && s.voiceName) { const pickByName = () => { const options = Array.from(voiceSelect.options); const ix = options.findIndex(o => (o.text || "").split(" — ")[0] === s.voiceName); if (ix >= 0) voiceSelect.selectedIndex = ix; }; // try now & also after voices refresh pickByName(); setTimeout(pickByName, 300); } } // Wire control changes to saveSettings() [ttsLang, voiceSelect, speakToggle, ttsRate, ttsPitch, themeToggle, imgVision] .filter(Boolean) .forEach(el => { const ev = (el.tagName === "SELECT") ? "change" : (el.type === "checkbox") ? "change" : (el.type === "range") ? "input" : "change"; el.addEventListener(ev, saveSettings); }); // On boot: load & apply applySettings(loadSettings()); // When voices arrive, try to re-select stored voice if ("speechSynthesis" in window) { try { speechSynthesis.addEventListener("voiceschanged", () => { const s = loadSettings(); applySettings(s); // re-apply to pick voiceName }); } catch {} } /* ---------- Theme handling ---------- */ (function initTheme(){ const saved = localStorage.getItem("vera_theme") || "saffron"; document.documentElement.setAttribute("data-theme", saved); if (themeToggle) themeToggle.checked = (saved === "dark"); })(); themeToggle?.addEventListener("change", () => { const mode = themeToggle.checked ? "dark" : "saffron"; document.documentElement.setAttribute("data-theme", mode); localStorage.setItem("vera_theme", mode); }); /* ---------- Username from memory/login ---------- */ (function showUserFromMemory(){ try{ let name = localStorage.getItem("username") || localStorage.getItem("userEmail"); const token = localStorage.getItem("authToken") || localStorage.getItem("token"); if (token && window.jwt_decode){ const info = jwt_decode(token); const email = info?.email || info?.preferred_username || ""; if (email && !name) name = email.split("@")[0]; } if (name && userPill){ userPill.textContent = name; userPill.style.display = "inline-block"; } }catch(e){ /* ignore */ } })(); /* ---------- Chips ---------- */ document.querySelectorAll(".chip").forEach(chip=>{ chip.onclick = ()=>{ userInput.value = chip.dataset.chip; sendMessage(); }; }); /* ---------- TTS ---------- */ let availableVoices = []; function loadVoices(){ availableVoices = speechSynthesis.getVoices() || []; if (!voiceSelect) return; voiceSelect.innerHTML = ""; availableVoices.forEach((v,i)=>{ const opt = document.createElement("option"); opt.value=i; opt.textContent = `${v.name} — ${v.lang}`; voiceSelect.appendChild(opt); }); const idx = availableVoices.findIndex(v=>/sa-IN|hi-IN|en-IN/i.test(v.lang) && /(male|india|google)/i.test(v.name)); voiceSelect.value = String(idx>=0?idx:0); } if ("speechSynthesis" in window){ try { speechSynthesis.onvoiceschanged = loadVoices; } catch {} setTimeout(loadVoices, 200); } function pickVoiceByLang(langWanted){ let v = availableVoices.find(v=> (v.lang||"").toLowerCase() === (langWanted||"").toLowerCase()); if(v) return v; const base = (langWanted||"").split("-")[0]; v = availableVoices.find(v=> (v.lang||"").toLowerCase().startsWith((base||"").toLowerCase())); return v || availableVoices[parseInt(voiceSelect.value||"0",10)] || null; } function plain(input) { // HTML/MD -> readable speech text const div = document.createElement("div"); div.innerHTML = input || ""; let s = (div.textContent || div.innerText || "").trim(); s = s // [text](url) -> text .replace(/\[(.*?)\]\((https?:\/\/[^)]+)\)/g, "$1") // raw URLs (incl. angle-bracket style) .replace(/?|www\.\S+/gi, "") // emails .replace(/\b[\w.+-]+@[\w-]+\.[\w.-]+\b/g, "") // lines starting with Source/Link/URL: .replace(/(?:^|\n)\s*(source|sources|link|url)\s*:\s*.*$/gmi, "") // bracketed refs like [1], [2] .replace(/\s*\[\d+\]/g, "") // markdown/symbol noise .replace(/[•*_#[\]<>`~|]/g, " ") // collapse spaces .replace(/\s{2,}/g, " ") .trim(); // keep TTS snappy const MAX = 2800; // ~2–3 minutes at normal rate if (s.length > MAX) s = s.slice(0, MAX) + "…"; return s; } let lastBotPlain = ""; async function speak(text) { try { // 1) Early exits & capability checks if (!speakToggle?.checked) return; if (!("speechSynthesis" in window)) { return (typeof toast === "function" ? toast("TTS not supported") : console.warn("TTS not supported")); } if (!text || typeof text !== "string") return; // 2) Stop any current speech (if you have your own stopSpeak, call it; else cancel) if (typeof stopSpeak === "function") stopSpeak(true); else window.speechSynthesis.cancel(); // 3) Wait for voices to be ready (some browsers load them async) await ensureVoicesReady(); // 4) Resolve TTS controls safely const forcedLang = (window.ttsLang && window.ttsLang.value) ? window.ttsLang.value : "auto"; const rate = clamp(parseFloat(window?.ttsRate?.value ?? "0.95"), 0.1, 10); const pitch = clamp(parseFloat(window?.ttsPitch?.value ?? "0.95"), 0, 2); // 5) Split text to chunks by script/lang if function exists; else single chunk const chunks = (forcedLang === "auto" && typeof splitByScript === "function") ? splitByScript(text) : [{ text, lang: forcedLang }]; // 6) Build utterances queue const queue = chunks .filter(ch => ch?.text) // keep valid chunks only .map((ch) => { const u = new SpeechSynthesisUtterance(ch.text); // Pick a voice: use your custom picker if present, else fallback by lang, else default let v = (typeof pickVoiceByLang === "function") ? pickVoiceByLang(ch.lang) : null; if (!v && ch.lang && window.speechSynthesis.getVoices) { v = window.speechSynthesis.getVoices().find(vc => vc.lang?.toLowerCase()?.startsWith((ch.lang || "").toLowerCase())); } if (v) u.voice = v; // Set language (prefer voice.lang if present) u.lang = v?.lang || ch.lang || undefined; u.rate = rate; u.pitch = pitch; // Optional: smoother prosody on some engines // u.volume = 1; // Error handler per chunk u.onerror = (e) => console.warn("TTS error:", e?.error || e); return u; }); if (!queue.length) return; // 7) Chain queue: when one ends, speak the next queue.forEach((u, i) => { u.onend = () => { if (i + 1 < queue.length) window.speechSynthesis.speak(queue[i + 1]); }; }); // 8) Start window.speechSynthesis.speak(queue[0]); } catch (err) { if (typeof toast === "function") toast("TTS error: " + (err?.message || err)); else console.error(err); } // --- helpers --- function clamp(n, min, max) { n = Number.isFinite(n) ? n : min; return Math.max(min, Math.min(max, n)); } function ensureVoicesReady() { return new Promise((resolve) => { const ready = () => { const voices = window.speechSynthesis.getVoices?.() || []; if (voices.length) resolve(); else resolve(); // resolve anyway to avoid hanging; many engines work without explicit voices }; // If voices already loaded if (window.speechSynthesis.getVoices?.().length) return resolve(); // Safari/Chrome fire this when ready window.speechSynthesis.onvoiceschanged = () => { window.speechSynthesis.onvoiceschanged = null; ready(); }; // Fallback timeout in case the event never fires setTimeout(ready, 300); }); } } function splitByScript(s){ const parts=[], re=/(^[\u0900-\u097F\s,|।]+|[^,|।\u0900-\u097F]+|[,|।])/g; const tokens = s.match(re)||[s]; let cur=null, buf=""; const langOf = t=>(/[\u0900-\u097F]/.test(t) ? "hi-IN" : "en-IN"); tokens.forEach(t=>{ const l = langOf(t); if(cur && l!==cur){ parts.push({text:buf.trim(), lang:cur}); buf=t; cur=l; } else { buf+=t; cur=cur||l; } }); if(buf.trim()) parts.push({text:buf.trim(), lang:cur}); return parts; } function stopSpeak(silent){ try{ speechSynthesis.cancel(); }catch{} if(!silent){} } function pauseSpeak(){ try{ speechSynthesis.pause(); }catch{} } function resumeSpeak(){ try{ speechSynthesis.resume(); }catch{} } document.getElementById("btnPlay").onclick = ()=> lastBotPlain && speak(lastBotPlain); document.getElementById("btnPause").onclick = pauseSpeak; document.getElementById("btnResume").onclick = resumeSpeak; document.getElementById("btnStop").onclick = ()=> stopSpeak(); /* ---------- Input ---------- */ userInput.addEventListener("keydown", (e)=>{ // Prevent send while composing (Hindi, etc.) if (e.isComposing) return; if (e.key === "Enter" && !e.shiftKey){ e.preventDefault(); sendMessage(); } }); // Smooth auto-grow const BASE_H = 52, MAX_H = 200; function growInput(){ userInput.style.height = "auto"; userInput.style.height = Math.min(userInput.scrollHeight, MAX_H) + "px"; } userInput.addEventListener("input", growInput); growInput(); // initial /* ---------- Mic ---------- */ const SR = window.SpeechRecognition || window.webkitSpeechRecognition; if (SR){ const rec = new SR(); rec.continuous = false; rec.interimResults = false; const pickLang = () => (ttsLang?.value && ttsLang.value !== "auto") ? ttsLang.value : (navigator.language || "en-IN"); rec.lang = pickLang(); micButton.addEventListener("click", ()=>{ try{ rec.lang = pickLang(); if(micButton.classList.contains("on")) rec.stop(); else rec.start(); }catch{} }); rec.onstart = ()=> micButton.classList.add("on"); rec.onend = ()=> micButton.classList.remove("on"); rec.onresult = (e)=>{ const txt = e.results?.[0]?.[0]?.transcript || ""; if (txt) { userInput.value = txt; growInput(); sendMessage(); } }; rec.onerror = ()=> micButton.classList.remove("on"); }else{ micButton.disabled = true; micButton.title = "Not supported"; } /* -------------- Read Image --------------*/ async function ocrHealthImage() { const f = document.getElementById("healthImage")?.files?.[0]; if (!f) return ""; try { const { data } = await Tesseract.recognize(f, 'eng'); // add 'hin' model if needed return (data?.text || "").trim(); } catch (e) { console.warn("OCR failed:", e); return ""; } } /* ---------- Send message ---------- */ function scrollToBottom(){ // ensure it happens after DOM/layout + md render requestAnimationFrame(()=>{ chatBody.scrollTop = chatBody.scrollHeight; requestAnimationFrame(()=> chatBody.scrollTop = chatBody.scrollHeight); }); } function appendMessage(html, role){ const el = document.createElement("div"); el.className = (role === "user") ? "user" : "bot"; if (role === "user") { // Render user text as plain text (no HTML injection) el.textContent = html; } else { // Sanitize and allow target/rel on links const clean = DOMPurify.sanitize(html, { ADD_ATTR: ["target", "rel"] }); el.innerHTML = clean; // Open links safely in a new tab el.querySelectorAll('a[href]').forEach(a => { a.setAttribute('target', '_blank'); a.setAttribute('rel', 'noopener noreferrer nofollow'); }); // Add copy buttons for code blocks el.querySelectorAll('pre code').forEach(block => { const actions = document.createElement("div"); actions.className = "code-actions"; const btn = document.createElement("button"); btn.className = "copy-btn"; btn.type = "button"; btn.textContent = "Copy"; btn.addEventListener("click", async () => { const text = block.innerText || block.textContent || ""; try { if (navigator.clipboard?.writeText) { await navigator.clipboard.writeText(text); } else { // Fallback copy const ta = document.createElement("textarea"); ta.value = text; ta.style.position = "fixed"; ta.style.opacity = "0"; document.body.appendChild(ta); ta.select(); document.execCommand("copy"); document.body.removeChild(ta); } if (typeof toast === "function") toast("Copied."); } catch (e) { console.warn("Copy failed:", e); } }); block.parentElement.after(actions); actions.appendChild(btn); }); // Ensure images trigger a follow-up scroll once loaded el.querySelectorAll("img").forEach(img => { img.addEventListener("load", scrollToBottom, { once: true }); }); } chatBody.appendChild(el); scrollToBottom(); // reliable bottom lock (double-RAF inside helper) return el; } function appendPill(text){ const el = document.createElement("div"); el.className = "bot"; el.innerHTML = `${text}`; chatBody.appendChild(el); chatBody.scrollTop = chatBody.scrollHeight; } function addChatItem(role, content){ const c = activeChat(); c.items.push({role, content, ts: Date.now()}); if(c.title==="New chat" && c.items.length>=2){ c.title = (c.items.find(x=>x.role==="user")?.content || "Chat").slice(0,40); } store.save(state); } function withTimeout(ms){ const ctrl = new AbortController(); const id = setTimeout(()=> ctrl.abort(new DOMException("Timeout", "AbortError")), ms); return { signal: ctrl.signal, cancel: ()=> clearTimeout(id) }; } async function sendMessage(){ const message = (userInput.value || "").trim(); if (!message) return; // render user bubble appendMessage(message, "user"); addChatItem("user", message); userInput.value = ""; if (typeof BASE_H !== "undefined") userInput.style.height = BASE_H + "px"; // loading row const wrap = document.createElement("div"); wrap.className = "bot loading"; wrap.innerHTML = `Thinking…`; chatBody.appendChild(wrap); if (typeof scrollToBottom === "function") scrollToBottom(); // build prompt const cat = categorySelect?.value; let finalMessage = message; if (cat && cat !== "auto") finalMessage = `::category=${cat}\n` + message; // optional OCR for health (client-side, free) let image_text = ""; try { if (imgVision?.checked && cat === "health_wellness" && typeof ocrHealthImage === "function") { image_text = await ocrHealthImage(); } } catch (e) { console.warn("OCR failed:", e); } // request with timeout const { signal, cancel } = withTimeout(30000); sendButton.disabled = true; try { const res = await fetch(`${API_CORE}/generate_result`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message: finalMessage, image_text }), signal }); cancel(); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); // remove loader wrap.remove(); // parse "category\ncontent" const raw = data?.response || ""; const nl = raw.indexOf("\n"); const category = (nl >= 0 ? raw.slice(0, nl) : "other_query").trim(); const content = (nl >= 0 ? raw.slice(nl + 1) : raw).trim(); appendPill(category.replace(/_/g, " ")); addChatItem("pill", category.replace(/_/g, " ")); const html = marked.parse(content || "", { breaks: true }); appendMessage(html, "bot"); addChatItem("assistant", content); // TTS (clean text) lastBotPlain = plain(html); if (speakToggle?.checked && lastBotPlain) speak(lastBotPlain); // fire-and-forget analytics/storage fetch(`${API_DB}/store_anonymous_chat`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ user_data: message, bot_data: raw }) }).catch(()=>{}); } catch (err) { cancel(); wrap.remove(); toast(/AbortError|Timeout/i.test(String(err)) ? "Request timed out" : "Failed to connect"); console.error(err); } finally { sendButton.disabled = false; if (typeof scrollToBottom === "function") scrollToBottom(); } } /* ---------- Settings modal ---------- */ document.getElementById("openSettings").onclick = ()=> settingsModal.showModal(); settingsModal.querySelector(".close").onclick = ()=> settingsModal.close(); /* ---------- Chats & Folders (local, mirrored to server) ---------- */ const store = { load(){ try{ return JSON.parse(localStorage.getItem("vera_store")||"{}"); }catch{ return {}; } }, save(obj){ localStorage.setItem("vera_store", JSON.stringify(obj||{})); } }; let state = store.load(); if(!state.folders) state.folders = []; // [{id,name}] if(!state.chats) state.chats = []; // [{id,title,folderId,created,items:[{role,content,ts}], lastCategory}] if(!state.activeChatId){ const id = crypto.randomUUID(); state.activeChatId = id; state.chats.push({id,title:"New chat",created:Date.now(),items:[]}); store.save(state); } const folderList = document.getElementById("folderList"); const chatList = document.getElementById("chatList"); function renderLists(){ folderList.innerHTML = ""; state.folders.forEach(f=>{ const li = document.createElement("li"); li.innerHTML = `
${f.name}
`; li.querySelectorAll("button")[0].onclick = ()=>{ const name = prompt("Folder name:", f.name); if(name){ f.name = name; store.save(state); renderLists(); } }; li.querySelectorAll("button")[1].onclick = ()=>{ if(confirm("Delete folder? It won't delete chats, just unassign.")){ state.chats.forEach(c=>{ if(c.folderId===f.id) c.folderId=null; }); state.folders = state.folders.filter(x=>x.id!==f.id); store.save(state); renderLists(); } }; folderList.appendChild(li); }); chatList.innerHTML = ""; state.chats .slice() .sort((a,b)=>b.created-a.created) .forEach(c=>{ const li = document.createElement("li"); li.style.borderColor = c.id===state.activeChatId ? "var(--brand)" : "var(--border)"; li.innerHTML = `
${c.title}
`; li.onclick = (e)=>{ if(e.target.tagName==="BUTTON") return; state.activeChatId = c.id; store.save(state); drawChat(true); }; const [btnFolder, btnRename, btnDelete] = li.querySelectorAll("button"); btnFolder.onclick = ()=>{ const options = ["(No folder)", ...state.folders.map(f=>f.name)]; const choice = prompt("Move to folder:\n" + options.map((n,i)=>`${i}. ${n}`).join("\n"), "0"); const idx = parseInt(choice||"0",10); if(!isNaN(idx)){ if(idx===0) c.folderId = null; else { const f=state.folders[idx-1]; if(f) c.folderId=f.id; } store.save(state); renderLists(); } }; btnRename.onclick = ()=>{ const name = prompt("Chat title:", c.title); if(name){ c.title = name; store.save(state); renderLists(); } }; btnDelete.onclick = ()=>{ if(confirm("Delete this chat?")){ state.chats = state.chats.filter(x=>x.id!==c.id); if(state.activeChatId===c.id){ const id = crypto.randomUUID(); state.activeChatId = id; state.chats.push({id,title:"New chat",created:Date.now(),items:[]}); } store.save(state); renderLists(); drawChat(true); } }; chatList.appendChild(li); }); } document.getElementById("btnNewChat").onclick = ()=>{ const id = crypto.randomUUID(); state.chats.push({id,title:"New chat",created:Date.now(),items:[]}); state.activeChatId = id; store.save(state); renderLists(); drawChat(true); }; document.getElementById("btnNewFolder").onclick = ()=>{ const name = prompt("Folder name:"); if(name){ state.folders.push({id:crypto.randomUUID(), name}); store.save(state); renderLists(); } }; function activeChat(){ return state.chats.find(c=>c.id===state.activeChatId); } function drawChat(scrollToEnd=false){ chatBody.innerHTML = ""; const c = activeChat(); if(!c || !c.items.length){ // show welcome const sec = document.createElement("section"); sec.className = "welcome bot"; sec.innerHTML = `

New conversation. Ask anything.

`; chatBody.appendChild(sec); return; } c.items.forEach(item => { if(item.role==="pill"){ const el = document.createElement("div"); el.className = "bot"; el.innerHTML = `${item.content}`; chatBody.appendChild(el); return; } const el = document.createElement("div"); el.className = item.role==="user" ? "user" : "bot"; const html = marked.parse(item.content || "", { breaks:true }); el.innerHTML = DOMPurify.sanitize(html); el.querySelectorAll("pre code").forEach(block=>{ const wrap = document.createElement("div"); wrap.className = "code-actions"; const btn = document.createElement("button"); btn.className = "copy-btn"; btn.textContent = "Copy"; btn.onclick = ()=> navigator.clipboard.writeText(block.innerText).then(()=> toast("Copied.")); block.parentElement.after(wrap); wrap.appendChild(btn); }); chatBody.appendChild(el); }); if(scrollToEnd) chatBody.scrollTop = chatBody.scrollHeight; } renderLists(); drawChat();