(function () { 'use strict'; // ── inject styles ───────────────────────────────────────────────────────── const style = document.createElement('style'); style.textContent = ` #cb-fab{ position:fixed;bottom:24px;right:24px;z-index:9999; width:52px;height:52px;border-radius:50%;border:none;cursor:pointer; background:linear-gradient(135deg,#047e7e,#065f46); box-shadow:0 4px 16px rgba(4,126,126,.45); display:flex;align-items:center;justify-content:center; transition:transform .2s,box-shadow .2s; } #cb-fab:hover{transform:scale(1.08);box-shadow:0 6px 20px rgba(4,126,126,.6)} #cb-fab svg{width:26px;height:26px;fill:#fff} #cb-fab .cb-badge{ position:absolute;top:-3px;right:-3px; background:#ef4444;color:#fff;font-size:10px;font-weight:700; border-radius:50%;width:18px;height:18px; display:none;align-items:center;justify-content:center; } #cb-panel{ position:fixed;bottom:84px;right:24px;z-index:9998; width:420px;height:540px;border-radius:16px; background:#fff;box-shadow:0 8px 40px rgba(0,0,0,.18); display:flex;flex-direction:column;overflow:hidden; transform:scale(.85) translateY(20px);opacity:0;pointer-events:none; transition:transform .22s cubic-bezier(.34,1.56,.64,1),opacity .18s; } #cb-panel.cb-open{transform:scale(1) translateY(0);opacity:1;pointer-events:all} #cb-header{ background:linear-gradient(135deg,#047e7e,#065f46); padding:14px 16px;display:flex;align-items:center;gap:10px;flex-shrink:0; } #cb-header .cb-avatar{ width:36px;height:36px;border-radius:50%;background:rgba(255,255,255,.2); display:flex;align-items:center;justify-content:center;flex-shrink:0; } #cb-header .cb-avatar svg{width:20px;height:20px;fill:#fff} #cb-header .cb-info{flex:1;min-width:0} #cb-header .cb-name{font-size:14px;font-weight:700;color:#fff;letter-spacing:.2px} #cb-header .cb-status{font-size:11px;color:rgba(255,255,255,.75);display:flex;align-items:center;gap:4px} #cb-header .cb-dot{width:7px;height:7px;border-radius:50%;background:#4ade80} #cb-close{ background:none;border:none;cursor:pointer;padding:4px; color:rgba(255,255,255,.8);border-radius:6px;line-height:0; transition:background .15s; } #cb-close:hover{background:rgba(255,255,255,.15);color:#fff} #cb-close svg{width:18px;height:18px;fill:currentColor} #cb-msgs{ flex:1;overflow-y:auto;padding:14px 14px 6px; display:flex;flex-direction:column;gap:8px; scroll-behavior:smooth; } #cb-msgs::-webkit-scrollbar{width:4px} #cb-msgs::-webkit-scrollbar-thumb{background:#d1dae3;border-radius:4px} .cb-msg{display:flex;gap:8px;align-items:flex-end;max-width:100%} .cb-msg.cb-user{flex-direction:row-reverse} .cb-bubble{ padding:8px 12px;border-radius:14px;font-size:13px;line-height:1.5; max-width:82%;word-break:break-word;white-space:pre-wrap; } .cb-msg.cb-bot .cb-bubble{background:#f0f4f8;color:#1a1a2e;border-bottom-left-radius:4px} .cb-msg.cb-user .cb-bubble{background:linear-gradient(135deg,#047e7e,#065f46);color:#fff;border-bottom-right-radius:4px} .cb-msg-avatar{ width:28px;height:28px;border-radius:50%;flex-shrink:0; display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700; } .cb-msg.cb-bot .cb-msg-avatar{background:#e0f4f4;color:#047e7e} .cb-msg.cb-user .cb-msg-avatar{background:#065f46;color:#fff} .cb-typing{display:flex;gap:5px;padding:10px 14px;align-items:center} .cb-typing span{ width:7px;height:7px;border-radius:50%;background:#9ca3af; animation:cb-bounce .9s infinite; } .cb-typing span:nth-child(2){animation-delay:.15s} .cb-typing span:nth-child(3){animation-delay:.3s} @keyframes cb-bounce{0%,60%,100%{transform:translateY(0)}30%{transform:translateY(-6px)}} #cb-footer{ padding:10px 12px;border-top:1px solid #f0f2f4;flex-shrink:0; display:flex;gap:8px;align-items:flex-end;background:#fff; } #cb-input{ flex:1;border:1.5px solid #e8eaed;border-radius:12px; padding:9px 12px;font-size:13px;font-family:inherit;color:#1a1a2e; resize:none;max-height:100px;overflow-y:auto;outline:none; transition:border-color .15s;line-height:1.4; } #cb-input:focus{border-color:#047e7e} #cb-input::placeholder{color:#9ca3af} #cb-send{ width:38px;height:38px;border-radius:10px;border:none;cursor:pointer; background:linear-gradient(135deg,#047e7e,#065f46); display:flex;align-items:center;justify-content:center;flex-shrink:0; transition:opacity .15s,transform .1s; } #cb-send:hover{opacity:.88;transform:scale(1.05)} #cb-send:disabled{opacity:.4;cursor:not-allowed;transform:none} #cb-send svg{width:18px;height:18px;fill:#fff} #cb-suggestions{ padding:0 12px 10px;display:flex;flex-wrap:wrap;gap:6px;flex-shrink:0; } .cb-chip{ padding:5px 10px;border-radius:20px;font-size:11.5px;cursor:pointer; background:#f0f4f8;color:#047e7e;border:1px solid #d1e8e8; transition:background .15s;white-space:nowrap; } .cb-chip:hover{background:#e0f4f4} /* ── markdown rendering inside bot bubbles ── */ .cb-bubble.cb-md{white-space:normal} .cb-bubble.cb-md p{margin:0 0 6px} .cb-bubble.cb-md p:last-child{margin-bottom:0} .cb-bubble.cb-md strong{font-weight:700} .cb-bubble.cb-md em{font-style:italic} .cb-bubble.cb-md code{background:rgba(0,0,0,.07);padding:1px 5px;border-radius:4px;font-family:monospace;font-size:11.5px} .cb-bubble.cb-md h3{font-size:13px;font-weight:700;margin:8px 0 4px;color:#004d40} .cb-bubble.cb-md h4{font-size:12px;font-weight:700;margin:6px 0 3px;color:#047e7e} .cb-bubble.cb-md ul,.cb-bubble.cb-md ol{padding-left:18px;margin:4px 0} .cb-bubble.cb-md li{margin:2px 0} .cb-bubble.cb-md table{border-collapse:collapse;width:100%;margin:8px 0;font-size:11.5px} .cb-bubble.cb-md th{background:#047e7e;color:#fff;padding:5px 9px;text-align:left;font-weight:700;white-space:nowrap} .cb-bubble.cb-md td{padding:4px 9px;border-bottom:1px solid #e8eaed;white-space:nowrap} .cb-bubble.cb-md tr:nth-child(even) td{background:rgba(4,126,126,.05)} .cb-bubble.cb-md .cb-tbl-wrap{overflow-x:auto;border-radius:6px;border:1px solid #d1e8e8} .cb-bubble.cb-md hr{border:none;border-top:1px solid #e8eaed;margin:8px 0} `; document.head.appendChild(style); // ── inject HTML ─────────────────────────────────────────────────────────── const fab = document.createElement('button'); fab.id = 'cb-fab'; fab.title = 'AI Assistant'; fab.innerHTML = ` `; const panel = document.createElement('div'); panel.id = 'cb-panel'; panel.innerHTML = `
Portal Assistant
Gemini AI · Mitra Industries
Show today's sales Blood Bag BOM prices Monthly revenue CAPD BOM costs How to create a BOM? Explain P&L Summary
`; document.body.appendChild(fab); document.body.appendChild(panel); // ── state ───────────────────────────────────────────────────────────────── let isOpen = false; let loading = false; const history = []; // [{role:'user'|'assistant', content:'...'}] let unread = 0; const msgsEl = document.getElementById('cb-msgs'); const inputEl = document.getElementById('cb-input'); const sendBtn = document.getElementById('cb-send'); const badge = document.getElementById('cb-badge'); // ── helpers ─────────────────────────────────────────────────────────────── function getToken() { return sessionStorage.getItem('portal_token') || ''; } // ── markdown renderer ───────────────────────────────────────────────────── function escHtml(s) { return String(s).replace(/&/g,'&').replace(//g,'>'); } function inlineMd(text) { return escHtml(text) .replace(/\*\*\*(.+?)\*\*\*/g, '$1') .replace(/\*\*(.+?)\*\*/g, '$1') .replace(/\*(.+?)\*/g, '$1') .replace(/`(.+?)`/g, '$1'); } function renderMd(raw) { const lines = raw.split('\n'); let html = '', i = 0; while (i < lines.length) { const line = lines[i]; const trim = line.trim(); // Blank line if (!trim) { html += '
'; i++; continue; } // Heading if (trim.startsWith('### ')) { html += `

${inlineMd(trim.slice(4))}

`; i++; continue; } if (trim.startsWith('## ')) { html += `

${inlineMd(trim.slice(3))}

`; i++; continue; } if (trim.startsWith('# ')) { html += `

${inlineMd(trim.slice(2))}

`; i++; continue; } // Horizontal rule if (/^[-*_]{3,}$/.test(trim)) { html += '
'; i++; continue; } // Table: line starts with | and next non-empty line is a separator if (trim.startsWith('|')) { const nextTrim = (lines[i + 1] || '').trim(); if (nextTrim.startsWith('|') && /^[\|:\- ]+$/.test(nextTrim)) { const parseRow = l => l.trim().replace(/^\||\|$/g,'').split('|').map(c => c.trim()); const headers = parseRow(lines[i]); const aligns = parseRow(lines[i + 1]).map(c => c.startsWith(':') && c.endsWith(':') ? 'center' : c.endsWith(':') ? 'right' : 'left'); i += 2; let tbl = '
'; headers.forEach((h, ci) => { tbl += ``; }); tbl += ''; while (i < lines.length && lines[i].trim().startsWith('|')) { const cells = parseRow(lines[i]); tbl += ''; cells.forEach((c, ci) => { tbl += ``; }); tbl += ''; i++; } tbl += '
${inlineMd(h)}
${inlineMd(c)}
'; html += tbl; continue; } } // Unordered list if (/^[-*+]\s/.test(trim)) { let ul = ''; continue; } // Ordered list if (/^\d+\.\s/.test(trim)) { let ol = '
    '; while (i < lines.length && /^\d+\.\s/.test(lines[i].trim())) { ol += `
  1. ${inlineMd(lines[i].trim().replace(/^\d+\.\s/,''))}
  2. `; i++; } html += ol + '
'; continue; } // Plain paragraph html += `

${inlineMd(line)}

`; i++; } // Clean up excess
tags return html.replace(/(
){3,}/g,'

').replace(/^
+|
+$/g,''); } function setUnread(n) { unread = n; if (n > 0 && !isOpen) { badge.textContent = n > 9 ? '9+' : n; badge.style.display = 'flex'; } else { badge.style.display = 'none'; } } function appendMsg(role, text) { // remove suggestions after first real message const sug = document.getElementById('cb-suggestions'); if (sug) sug.remove(); const wrap = document.createElement('div'); wrap.className = `cb-msg cb-${role}`; const av = document.createElement('div'); av.className = 'cb-msg-avatar'; av.textContent = role === 'bot' ? 'AI' : '👤'; const bub = document.createElement('div'); if (role === 'bot') { bub.className = 'cb-bubble cb-md'; bub.innerHTML = renderMd(text); } else { bub.className = 'cb-bubble'; bub.textContent = text; } if (role === 'user') { wrap.appendChild(bub); wrap.appendChild(av); } else { wrap.appendChild(av); wrap.appendChild(bub); } msgsEl.appendChild(wrap); msgsEl.scrollTop = msgsEl.scrollHeight; return bub; } function showTyping() { const wrap = document.createElement('div'); wrap.className = 'cb-msg cb-bot'; wrap.id = 'cb-typing-row'; wrap.innerHTML = `
AI
`; msgsEl.appendChild(wrap); msgsEl.scrollTop = msgsEl.scrollHeight; } function removeTyping() { const t = document.getElementById('cb-typing-row'); if (t) t.remove(); } // ── welcome message ─────────────────────────────────────────────────────── function showWelcome() { appendMsg('bot', 'Hi! I\'m your Portal Assistant. I can help you navigate the portal, understand P&L data, cost sheets, BOM workflows, and more.\n\nWhat would you like to know?'); } // ── send message ────────────────────────────────────────────────────────── async function sendMessage() { const text = inputEl.value.trim(); if (!text || loading) return; inputEl.value = ''; inputEl.style.height = 'auto'; loading = true; sendBtn.disabled = true; appendMsg('user', text); history.push({ role: 'user', content: text }); showTyping(); try { const token = getToken(); const res = await fetch('/api/chat/message', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`, }, body: JSON.stringify({ message: text, history: history.slice(0, -1) }), }); const data = await res.json(); removeTyping(); const reply = data.success ? data.reply : (data.message || 'Something went wrong. Please try again.'); appendMsg('bot', reply); history.push({ role: 'assistant', content: reply }); if (!isOpen) setUnread(unread + 1); } catch { removeTyping(); appendMsg('bot', 'Network error. Please check your connection and try again.'); } finally { loading = false; sendBtn.disabled = false; inputEl.focus(); } } // ── toggle panel ────────────────────────────────────────────────────────── function openPanel() { isOpen = true; panel.classList.add('cb-open'); setUnread(0); if (!msgsEl.children.length) showWelcome(); setTimeout(() => inputEl.focus(), 250); } function closePanel() { isOpen = false; panel.classList.remove('cb-open'); } // ── events ──────────────────────────────────────────────────────────────── fab.addEventListener('click', () => isOpen ? closePanel() : openPanel()); document.getElementById('cb-close').addEventListener('click', closePanel); // quick chips panel.querySelectorAll('.cb-chip').forEach(chip => { chip.addEventListener('click', () => { inputEl.value = chip.textContent; sendMessage(); }); }); // send on Enter (Shift+Enter = newline) inputEl.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }); // auto-resize textarea inputEl.addEventListener('input', () => { inputEl.style.height = 'auto'; inputEl.style.height = Math.min(inputEl.scrollHeight, 100) + 'px'; }); sendBtn.addEventListener('click', sendMessage); })();