AI_Portal / static /chat.html
Moncey10's picture
Update static/chat.html
05ef322 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>New Age β€” Internal AI Support</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #F5F4F0;
--surface: #FFFFFF;
--surface-2: #F0EEE9;
--border: #E2E0DA;
--border-strong:#C8C6BF;
--text: #1A1916;
--muted: #6B6A65;
--hint: #9E9C97;
--accent: #1A1916;
--accent-fg: #FFFFFF;
--user-bubble: #1A1916;
--user-fg: #FFFFFF;
--bot-bubble: #FFFFFF;
--bot-fg: #1A1916;
--radius: 12px;
--radius-sm: 8px;
--radius-xs: 6px;
--shadow: 0 1px 3px rgba(0,0,0,0.06), 0 4px 16px rgba(0,0,0,0.06);
}
body {
font-family: 'DM Sans', sans-serif;
background: var(--bg);
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* ── Top navbar ── */
.navbar {
background: var(--surface);
border-bottom: 1px solid var(--border);
padding: 0 1.5rem;
height: 56px;
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
z-index: 10;
}
.navbar-left {
display: flex;
align-items: center;
gap: 12px;
}
.logo-mark {
width: 32px;
height: 32px;
background: var(--accent);
border-radius: var(--radius-xs);
display: flex;
align-items: center;
justify-content: center;
}
.logo-mark svg {
width: 17px;
height: 17px;
stroke: #fff;
fill: none;
stroke-width: 1.8;
stroke-linecap: round;
stroke-linejoin: round;
}
.brand-name {
font-size: 0.9375rem;
font-weight: 600;
color: var(--text);
letter-spacing: -0.01em;
}
.brand-tag {
font-size: 0.75rem;
color: var(--hint);
font-weight: 400;
margin-left: 2px;
}
.navbar-right {
display: flex;
align-items: center;
gap: 8px;
}
.nav-btn {
height: 32px;
padding: 0 12px;
border: 1px solid var(--border);
border-radius: var(--radius-xs);
background: none;
font-family: 'DM Sans', sans-serif;
font-size: 0.8125rem;
color: var(--muted);
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
transition: border-color 0.15s, color 0.15s;
}
.nav-btn:hover { border-color: var(--border-strong); color: var(--text); }
.nav-btn svg {
width: 14px;
height: 14px;
stroke: currentColor;
fill: none;
stroke-width: 1.8;
stroke-linecap: round;
stroke-linejoin: round;
}
.user-pill {
height: 32px;
padding: 0 10px;
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: 99px;
display: flex;
align-items: center;
gap: 7px;
font-size: 0.8125rem;
color: var(--text);
font-weight: 500;
}
.avatar {
width: 20px;
height: 20px;
background: var(--accent);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.625rem;
color: #fff;
font-weight: 600;
}
/* ── Main layout ── */
.main {
flex: 1;
display: flex;
flex-direction: column;
max-width: 800px;
width: 100%;
margin: 0 auto;
padding: 0 1rem;
overflow: hidden;
}
/* ── Messages area ── */
.messages {
flex: 1;
overflow-y: auto;
padding: 1.5rem 0;
display: flex;
flex-direction: column;
gap: 1rem;
scrollbar-width: thin;
scrollbar-color: var(--border) transparent;
}
.messages::-webkit-scrollbar { width: 4px; }
.messages::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
/* ── Welcome state ── */
.welcome {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 2rem;
gap: 1rem;
}
.welcome-icon {
width: 48px;
height: 48px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
display: flex;
align-items: center;
justify-content: center;
}
.welcome-icon svg {
width: 24px;
height: 24px;
stroke: var(--muted);
fill: none;
stroke-width: 1.6;
stroke-linecap: round;
stroke-linejoin: round;
}
.welcome h2 {
font-size: 1.25rem;
font-weight: 600;
color: var(--text);
letter-spacing: -0.01em;
}
.welcome p {
font-size: 0.875rem;
color: var(--muted);
max-width: 380px;
line-height: 1.6;
}
/* Suggestion chips */
.suggestions {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: center;
margin-top: 0.5rem;
}
.chip {
padding: 7px 14px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 99px;
font-size: 0.8125rem;
color: var(--muted);
cursor: pointer;
font-family: 'DM Sans', sans-serif;
transition: border-color 0.15s, color 0.15s, background 0.15s;
}
.chip:hover {
border-color: var(--border-strong);
color: var(--text);
background: var(--surface-2);
}
/* ── Message bubbles ── */
.msg-row {
display: flex;
gap: 10px;
animation: fadeUp 0.2s ease both;
}
@keyframes fadeUp {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.msg-row.user { flex-direction: row-reverse; }
.msg-avatar {
width: 28px;
height: 28px;
border-radius: 50%;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.6875rem;
font-weight: 600;
margin-top: 2px;
}
.msg-avatar.bot {
background: var(--accent);
color: #fff;
}
.msg-avatar.user-av {
background: var(--surface-2);
border: 1px solid var(--border);
color: var(--muted);
}
.msg-content { max-width: 72%; display: flex; flex-direction: column; gap: 4px; }
.msg-row.user .msg-content { align-items: flex-end; }
.msg-sender {
font-size: 0.75rem;
color: var(--hint);
font-weight: 500;
padding: 0 4px;
}
.bubble {
padding: 10px 14px;
border-radius: var(--radius);
font-size: 0.875rem;
line-height: 1.6;
word-break: break-word;
}
.bubble.bot {
background: var(--bot-bubble);
color: var(--bot-fg);
border: 1px solid var(--border);
border-top-left-radius: 4px;
}
.bubble.user {
background: var(--user-bubble);
color: var(--user-fg);
border-top-right-radius: 4px;
}
/* Image in bubble */
.bubble img {
max-width: 100%;
border-radius: var(--radius-xs);
display: block;
margin-top: 6px;
}
.bubble .img-only {
max-width: 260px;
border-radius: var(--radius-sm);
}
.msg-time {
font-size: 0.6875rem;
color: var(--hint);
padding: 0 4px;
}
/* Typing indicator */
.typing-bubble {
padding: 12px 16px;
background: var(--bot-bubble);
border: 1px solid var(--border);
border-radius: var(--radius);
border-top-left-radius: 4px;
display: flex;
gap: 4px;
align-items: center;
}
.typing-bubble span {
width: 6px;
height: 6px;
background: var(--hint);
border-radius: 50%;
animation: blink 1.2s ease infinite;
}
.typing-bubble span:nth-child(2) { animation-delay: 0.2s; }
.typing-bubble span:nth-child(3) { animation-delay: 0.4s; }
@keyframes blink {
0%, 80%, 100% { opacity: 0.3; transform: scale(0.85); }
40% { opacity: 1; transform: scale(1); }
}
/* ── Image preview bar ── */
.image-preview-bar {
display: none;
padding: 8px 0;
gap: 8px;
flex-wrap: wrap;
}
.image-preview-bar.visible { display: flex; }
.img-thumb {
position: relative;
width: 60px;
height: 60px;
}
.img-thumb img {
width: 60px;
height: 60px;
object-fit: cover;
border-radius: var(--radius-xs);
border: 1px solid var(--border);
}
.img-remove {
position: absolute;
top: -6px;
right: -6px;
width: 18px;
height: 18px;
background: var(--text);
color: #fff;
border: none;
border-radius: 50%;
font-size: 10px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
line-height: 1;
}
/* ── Input area ── */
.input-area {
padding: 0.75rem 0 1rem;
flex-shrink: 0;
}
.input-box {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow);
transition: border-color 0.15s;
overflow: hidden;
}
.input-box:focus-within { border-color: var(--border-strong); }
.input-top {
display: flex;
align-items: flex-end;
gap: 8px;
padding: 10px 12px;
}
textarea {
flex: 1;
border: none;
outline: none;
font-family: 'DM Sans', sans-serif;
font-size: 0.875rem;
color: var(--text);
background: transparent;
resize: none;
min-height: 24px;
max-height: 160px;
line-height: 1.6;
padding: 0;
}
textarea::placeholder { color: var(--hint); }
.input-bottom {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 12px 10px;
border-top: 1px solid var(--border);
}
.input-actions { display: flex; align-items: center; gap: 4px; }
.action-btn {
width: 32px;
height: 32px;
border: none;
background: none;
border-radius: var(--radius-xs);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--hint);
transition: background 0.15s, color 0.15s;
}
.action-btn:hover { background: var(--surface-2); color: var(--muted); }
.action-btn svg {
width: 16px;
height: 16px;
stroke: currentColor;
fill: none;
stroke-width: 1.8;
stroke-linecap: round;
stroke-linejoin: round;
}
.send-btn {
height: 32px;
padding: 0 14px;
background: var(--accent);
color: var(--accent-fg);
border: none;
border-radius: var(--radius-xs);
font-family: 'DM Sans', sans-serif;
font-size: 0.8125rem;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
transition: opacity 0.15s;
}
.send-btn:hover { opacity: 0.85; }
.send-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.send-btn svg {
width: 14px;
height: 14px;
stroke: currentColor;
fill: none;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
.disclaimer {
text-align: center;
font-size: 0.6875rem;
color: var(--hint);
margin-top: 8px;
}
/* ── New chat btn ── */
#newChatBtn { display: none; }
/* ── Confidential badge on bot messages ── */
.conf-badge {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 0.6875rem;
color: #B91C1C;
background: #FEF2F2;
border: 1px solid #FECACA;
border-radius: 99px;
padding: 2px 8px;
margin-top: 4px;
}
/* Hidden file input */
#imageInput { display: none; }
/* ── NAR inline link ── */
.nar-inline-link {
display: inline-block;
margin-top: 6px;
color: #4f8ef7;
font-size: 0.875rem;
font-weight: 500;
text-decoration: none;
}
.nar-inline-link:hover { text-decoration: underline; }
</style>
<!-- Microsoft Clarity -->
<script type="text/javascript">
(function(c,l,a,r,i,t,y){
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
})(window, document, "clarity", "script", "x13hxsbvnw");
</script>
</head>
<body>
<!-- NAVBAR -->
<nav class="navbar">
<div class="navbar-left">
<div class="logo-mark" aria-hidden="true">
<svg viewBox="0 0 24 24"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
</div>
<div>
<span class="brand-name">New Age</span>
<span class="brand-tag">/ AI Support</span>
</div>
</div>
<div class="navbar-right">
<button class="nav-btn" id="newChatBtn" onclick="startNewChat()">
<svg viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
New chat
</button>
<div class="user-pill">
<div class="avatar" id="userAvatar">A</div>
<span id="userName">Admin</span>
</div>
<button class="nav-btn" id="adminBtn" onclick="window.location.href='/admin'" title="Admin Panel" style="display:none">
<svg viewBox="0 0 24 24"><path d="M12 2a10 10 0 1 0 0 20A10 10 0 0 0 12 2z"/><path d="M12 8v4l3 3"/></svg>
Admin
</button>
<button class="nav-btn" onclick="logout()" title="Logout">
<svg viewBox="0 0 24 24"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
Logout
</button>
</div>
</nav>
<!-- MAIN -->
<div class="main">
<div class="messages" id="messages">
<!-- Welcome screen -->
<div class="welcome" id="welcome">
<div class="welcome-icon">
<svg viewBox="0 0 24 24"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
</div>
<h2>How can I help you today?</h2>
<p>Ask me anything about New Age β€” teams, policies, divisions, contacts, or NAR requests.</p>
<div class="suggestions">
<button class="chip" onclick="useChip(this)">What is NAR?</button>
<button class="chip" onclick="useChip(this)">Who should I contact for IT issues?</button>
<button class="chip" onclick="useChip(this)">What are the office timings?</button>
<button class="chip" onclick="useChip(this)">How do I submit a reimbursement?</button>
<button class="chip" onclick="useChip(this)">Tell me about New Age divisions</button>
</div>
</div>
</div>
<!-- Image preview bar -->
<div class="image-preview-bar" id="imagePreviewBar"></div>
<!-- Input area -->
<div class="input-area">
<div class="input-box">
<div class="input-top">
<textarea
id="msgInput"
placeholder="Ask anything about New Age..."
rows="1"
onkeydown="handleKey(event)"
oninput="autoResize(this)"
></textarea>
</div>
<div class="input-bottom">
<div class="input-actions">
<!-- Image upload -->
<button class="action-btn" title="Attach image" onclick="document.getElementById('imageInput').click()">
<svg viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
</button>
<input type="file" id="imageInput" accept="image/*" multiple onchange="handleImages(this)"/>
<span style="font-size:0.75rem; color:var(--hint); margin-left:4px;">Add image for context</span>
</div>
<button class="send-btn" id="sendBtn" onclick="sendMessage()" disabled>
Send
<svg viewBox="0 0 24 24"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
</button>
</div>
</div>
<p class="disclaimer">New Age AI will not share confidential information such as salary, revenue, or profit.</p>
</div>
</div>
<script>
/* ── State ── */
const pendingImages = []; // { file, dataUrl }
let isTyping = false;
let msgCount = 0;
/* ── Load user from session ── */
window.addEventListener('DOMContentLoaded', () => {
// Read user info from sessionStorage (set by login page)
const savedUser = sessionStorage.getItem('loggedInUser');
if (!savedUser) {
// Not logged in β€” redirect to login page
window.location.href = '/';
return;
}
const user = JSON.parse(savedUser);
document.getElementById('userName').textContent = user.name;
document.getElementById('userAvatar').textContent = user.initials;
// Show Admin button only for Manager/Admin roles
const adminRoles = ['Manager', 'Admin', 'HR', 'Operations'];
if (adminRoles.includes(user.role)) {
document.getElementById('adminBtn').style.display = '';
}
// Enable send on input
document.getElementById('msgInput').addEventListener('input', updateSendBtn);
});
/* ── Enable/disable send button ── */
function updateSendBtn() {
const hasText = document.getElementById('msgInput').value.trim().length > 0;
const hasImg = pendingImages.length > 0;
document.getElementById('sendBtn').disabled = !(hasText || hasImg);
}
/* ── Auto-resize textarea ── */
function autoResize(el) {
el.style.height = 'auto';
el.style.height = Math.min(el.scrollHeight, 160) + 'px';
updateSendBtn();
}
/* ── Handle Enter key ── */
function handleKey(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
if (!document.getElementById('sendBtn').disabled) sendMessage();
}
}
/* ── Image handling ── */
function handleImages(input) {
const files = Array.from(input.files);
files.forEach(file => {
const reader = new FileReader();
reader.onload = e => {
pendingImages.push({ file, dataUrl: e.target.result });
renderImagePreviews();
updateSendBtn();
};
reader.readAsDataURL(file);
});
input.value = '';
}
function renderImagePreviews() {
const bar = document.getElementById('imagePreviewBar');
bar.innerHTML = '';
if (pendingImages.length === 0) { bar.classList.remove('visible'); return; }
bar.classList.add('visible');
pendingImages.forEach((img, i) => {
const wrap = document.createElement('div');
wrap.className = 'img-thumb';
wrap.innerHTML = `<img src="${img.dataUrl}" alt="Preview"/><button class="img-remove" onclick="removeImage(${i})">βœ•</button>`;
bar.appendChild(wrap);
});
}
function removeImage(i) {
pendingImages.splice(i, 1);
renderImagePreviews();
updateSendBtn();
}
/* ── Send message ── */
async function sendMessage() {
const input = document.getElementById('msgInput');
const text = input.value.trim();
const images = [...pendingImages];
if (!text && images.length === 0) return;
if (isTyping) return;
// Protection 1: Minimum message length
if (text && text.length < 3) {
alert('Please type a proper question!');
return;
}
// Protection 2: Simple cache β€” avoid duplicate API calls
const cacheKey = text.trim().toLowerCase();
if (messageCache[cacheKey]) {
document.getElementById('welcome') && (document.getElementById('welcome').style.display = 'none');
document.getElementById('newChatBtn').style.display = 'flex';
addMessage('user', text, images);
document.getElementById('msgInput').value = '';
document.getElementById('msgInput').style.height = 'auto';
updateSendBtn();
addMessage('bot', messageCache[cacheKey] + ' *(cached)*');
return;
}
// Hide welcome screen
document.getElementById('welcome').style.display = 'none';
document.getElementById('newChatBtn').style.display = 'flex';
// Render user message
addMessage('user', text, images);
// Clear input
input.value = '';
input.style.height = 'auto';
pendingImages.length = 0;
renderImagePreviews();
updateSendBtn();
// Show typing and call Groq API
showTyping();
await callGroqAPI(text, images);
}
/* ── Add message to DOM ── */
function addMessage(role, text, images = []) {
msgCount++;
const msgs = document.getElementById('messages');
const isUser = role === 'user';
const row = document.createElement('div');
row.className = `msg-row ${isUser ? 'user' : ''}`;
const initials = isUser ? (document.getElementById('userAvatar').textContent || 'U') : 'NA';
const now = new Date();
const time = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
let imagesHtml = '';
if (images.length > 0) {
imagesHtml = images.map(img =>
`<img src="${img.dataUrl}" alt="Attached image" class="${!text ? 'img-only' : ''}"/>`
).join('');
}
// Parse NAR suggestion block out of bot reply
let narCardHtml = '';
let cleanText = text;
if (!isUser) {
const narStart = text.indexOf(':::nar-suggestion');
if (narStart !== -1) {
const jsonStart = text.indexOf('{', narStart);
// Find closing ::: β€” if missing, treat end of string as end
let narEnd = text.indexOf(':::', narStart + 17);
if (narEnd === -1) narEnd = text.length;
if (jsonStart !== -1) {
// Find last } before narEnd
const jsonEnd = text.lastIndexOf('}', narEnd) + 1;
const jsonStr = text.slice(jsonStart, jsonEnd).trim();
try {
narCardHtml = buildNarCard(JSON.parse(jsonStr));
cleanText = text.slice(0, narStart).trim();
} catch(e) { cleanText = text.slice(0, narStart).trim(); }
}
}
}
const isConfidential = !isUser && /salary|revenue|profit|confidential/i.test(cleanText);
const confidentialBadge = isConfidential
? `<span class="conf-badge">⚠ Confidential info not disclosed</span>` : '';
row.innerHTML = `
<div class="msg-avatar ${isUser ? 'user-av' : 'bot'}">${initials}</div>
<div class="msg-content">
<span class="msg-sender">${isUser ? 'You' : 'New Age AI'}</span>
<div class="bubble ${isUser ? 'user' : 'bot'}">
${cleanText ? `<span>${isUser ? escHtml(cleanText) : renderText(cleanText)}</span>` : ''}
${imagesHtml}
</div>
${narCardHtml}
${confidentialBadge}
<span class="msg-time">${time}</span>
</div>
`;
msgs.appendChild(row);
msgs.scrollTop = msgs.scrollHeight;
}
/* ── Build simple NAR inline link ── */
function buildNarCard(data) {
const MONDAY_BOARD_URL = 'https://forms.monday.com/forms/37620e81b30597f2867f3cbdc8aa2e51?r=use1&Request+Type=';
return `<a href="${MONDAY_BOARD_URL}" target="_blank" class="nar-inline-link">β†’ Submit ${data.title}</a>`;
}
/* ── Typing indicator ── */
function showTyping() {
isTyping = true;
document.getElementById('sendBtn').disabled = true;
const msgs = document.getElementById('messages');
const row = document.createElement('div');
row.className = 'msg-row';
row.id = 'typingRow';
row.innerHTML = `
<div class="msg-avatar bot">NA</div>
<div class="msg-content">
<span class="msg-sender">New Age AI</span>
<div class="typing-bubble"><span></span><span></span><span></span></div>
</div>
`;
msgs.appendChild(row);
msgs.scrollTop = msgs.scrollHeight;
}
function hideTyping() {
isTyping = false;
const row = document.getElementById('typingRow');
if (row) row.remove();
updateSendBtn();
}
/* ── Suggestion chips ── */
function useChip(btn) {
document.getElementById('msgInput').value = btn.textContent;
autoResize(document.getElementById('msgInput'));
updateSendBtn();
sendMessage();
}
/* ── New chat ── */
function startNewChat() {
const msgs = document.getElementById('messages');
msgs.innerHTML = `
<div class="welcome" id="welcome">
<div class="welcome-icon">
<svg viewBox="0 0 24 24" stroke="var(--muted)" fill="none" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" width="24" height="24"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
</div>
<h2>How can I help you today?</h2>
<p>Ask me anything about New Age β€” teams, policies, divisions, contacts, or NAR requests.</p>
<div class="suggestions">
<button class="chip" onclick="useChip(this)">What is NAR?</button>
<button class="chip" onclick="useChip(this)">Who should I contact for IT issues?</button>
<button class="chip" onclick="useChip(this)">What are the office timings?</button>
<button class="chip" onclick="useChip(this)">How do I submit a reimbursement?</button>
<button class="chip" onclick="useChip(this)">Tell me about New Age divisions</button>
</div>
</div>`;
document.getElementById('newChatBtn').style.display = 'none';
msgCount = 0;
}
/* ── Logout ── */
function logout() {
if (confirm('Are you sure you want to logout?')) {
sessionStorage.removeItem('loggedInUser');
window.location.href = '/';
}
}
/* ── Escape HTML ── */
function escHtml(str) {
return str.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\n/g,'<br>');
}
function renderText(str) {
// Strip NAR suggestion block β€” handles multiline
const narStart = str.indexOf(':::nar-suggestion');
if (narStart !== -1) {
str = str.slice(0, narStart).trim();
}
str = str.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
str = str.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
str = str.replace(/\n/g, '<br>');
return str;
}
/* ══════════════════════════════════════════════
NAR DATABASE β€” all 28 request types
══════════════════════════════════════════════ */
const NAR_TYPES = [
{
id:1, title:'General Requests', category:'General',
keywords:['general','misc','other','help','info'],
fields:'β€’ Request Title: Short summary of what you need\nβ€’ Description: Explain your request in detail\nβ€’ Priority: Low / Medium / High\nβ€’ Any supporting documents: Attach if relevant'
},
{
id:2, title:'Access Request', category:'IT',
keywords:['access','permission','login','account','tool','software','system','monday','slack','drive','github','credentials'],
fields:'β€’ Tool / Platform Name: e.g. Monday.com, Slack, GitHub, Google Drive\nβ€’ Type of Access Needed: View only / Edit / Admin\nβ€’ Reason: Why do you need this access?\nβ€’ Urgency: When do you need it by?'
},
{
id:3, title:'Leave Request', category:'HR',
keywords:['leave','vacation','time off','holiday','sick','absence','pto','break'],
fields:'β€’ Leave Type: Paid / Unpaid / Sick\nβ€’ From Date: Start date of leave\nβ€’ To Date: End date of leave\nβ€’ Total Days: Number of working days\nβ€’ Reason: Brief reason for leave\nβ€’ Coverage Plan: Who will handle your work while you\'re away?'
},
{
id:4, title:'Purchase Request', category:'Finance',
keywords:['buy','purchase','order','laptop','monitor','hardware','equipment','device','phone','keyboard','mouse','screen'],
fields:'β€’ Item Name: What do you want to purchase?\nβ€’ Quantity: How many units?\nβ€’ Estimated Cost: Approximate price in INR\nβ€’ Vendor / Link: Where to buy from (Amazon link etc.)\nβ€’ Reason / Justification: Why is this needed for your work?\nβ€’ Urgency: When do you need it?'
},
{
id:5, title:'Payment Request', category:'Finance',
keywords:['payment','pay','invoice','reimbursement','expense','reimburse','refund','bill','money'],
fields:'β€’ Payment Type: Reimbursement / Invoice / Vendor Payment\nβ€’ Amount: Exact amount in INR\nβ€’ Description: What was this expense for?\nβ€’ Date of Expense: When was it incurred?\nβ€’ Payment Method Used: UPI / Card / Cash\nβ€’ Attachment: Upload receipt or invoice (mandatory)'
},
{
id:6, title:'Contract Change', category:'Legal',
keywords:['contract','agreement','change','modify','update','terms','scope','role','rate','hours'],
fields:'β€’ Current Contract Details: Your current role / rate / hours\nβ€’ Requested Change: What exactly needs to change?\nβ€’ Reason: Why is this change needed?\nβ€’ Effective Date: From when should this apply?\nβ€’ Supporting Evidence: Any email threads or agreements to attach?'
},
{
id:7, title:'Billing / Subscription Concern', category:'Finance',
keywords:['billing','subscription','charge','overcharge','plan','renewal','pricing','overage'],
fields:'β€’ Subscription / Service Name: Which tool or service?\nβ€’ Issue Type: Overcharge / Wrong Plan / Unexpected Charge\nβ€’ Amount in Question: How much was charged?\nβ€’ Date of Charge: When did this appear?\nβ€’ Expected Amount: What should it have been?\nβ€’ Screenshot: Attach billing screenshot'
},
{
id:8, title:'Subscription Cancellation Request', category:'Finance',
keywords:['cancel','cancellation','unsubscribe','stop subscription','terminate plan'],
fields:'β€’ Service / Tool Name: What subscription to cancel?\nβ€’ Current Plan: Which plan are you on?\nβ€’ Reason for Cancellation: Why cancel?\nβ€’ Last Required Date: When can it be cancelled from?\nβ€’ Alternative: Is there a replacement tool being used?'
},
{
id:9, title:'Data Report Blocker', category:'Ops',
keywords:['report blocked','data stuck','blocker','report issue','data error','dashboard broken','analytics issue'],
fields:'β€’ Report / Dashboard Name: Which report is blocked?\nβ€’ Platform: Monday.com / Google Sheets / Looker / Other\nβ€’ Issue Description: What exactly is broken or missing?\nβ€’ Since When: When did this blocker start?\nβ€’ Impact: What work is being blocked because of this?\nβ€’ Screenshot: Attach error or broken state screenshot'
},
{
id:10, title:'Data Analysis and Reporting Request', category:'Ops',
keywords:['data analysis','reporting','analytics','dashboard','metrics','stats','report','insight','kpi'],
fields:'β€’ Report Name / Title: What should this report be called?\nβ€’ Data Source: Where is the data coming from?\nβ€’ Metrics Needed: What KPIs or numbers do you need?\nβ€’ Time Period: Date range for the report\nβ€’ Format: Dashboard / Spreadsheet / PDF\nβ€’ Deadline: When do you need this by?'
},
{
id:11, title:'Grievance / Strategic Alignment Requests', category:'HR',
keywords:['grievance','complaint','alignment','strategy','concern','issue with team','unfair','conflict','disagreement'],
fields:'β€’ Subject: One line summary of the grievance\nβ€’ People Involved: Who is this regarding? (names/roles)\nβ€’ Incident Date: When did this happen?\nβ€’ Detailed Description: Explain the full situation factually\nβ€’ What Resolution Are You Seeking?: What outcome do you want?\nβ€’ Is this confidential?: Yes / No'
},
{
id:12, title:'Onboard New Access', category:'IT',
keywords:['onboard','new joiner','new employee','new access','setup','new hire','joining','first day','create account'],
fields:'β€’ New Member Name: Full name of the person joining\nβ€’ Role / Designation: Their job title\nβ€’ Start Date: When do they join?\nβ€’ Tools Required: List all tools they need access to (Slack, Monday, Drive, GitHub etc.)\nβ€’ Access Level: For each tool β€” View / Edit / Admin\nβ€’ Reporting Manager: Who will they report to?'
},
{
id:13, title:'Complaint', category:'HR',
keywords:['complaint','problem','unhappy','dissatisfied','bad experience','report problem'],
fields:'β€’ Complaint Subject: Brief title\nβ€’ Against Whom / What: Person, team, or process\nβ€’ Date of Incident: When did this happen?\nβ€’ Description: Explain clearly what happened\nβ€’ Evidence: Any screenshots, messages, or documents?\nβ€’ Desired Resolution: What do you want done about it?'
},
{
id:14, title:'Offer Release Request', category:'HR',
keywords:['offer letter','offer release','job offer','send offer','hiring offer','appointment letter'],
fields:'β€’ Candidate Name: Full name\nβ€’ Role Being Offered: Job title\nβ€’ Division: Product / Services\nβ€’ Agreed Rate: Compensation as discussed\nβ€’ Start Date: Expected joining date\nβ€’ Contract Type: C1 / C2 / Freelance\nβ€’ Approved By: Who approved this hire?'
},
{
id:15, title:'Contract Termination', category:'Legal',
keywords:['terminate','termination','end contract','fire','exit','notice period','last day'],
fields:'β€’ Person\'s Name: Who is being offboarded?\nβ€’ Role: Their designation\nβ€’ Last Working Day: Final date\nβ€’ Reason for Termination: Performance / Mutual / Resignation / Other\nβ€’ Notice Period Served: Yes / No / Waived\nβ€’ Access Revocation Required: List tools to revoke (Slack, Monday, GitHub etc.)\nβ€’ Final Payment Status: Settled / Pending'
},
{
id:16, title:'Resignation Letter', category:'HR',
keywords:['resign','resignation','quit','leaving','notice','last working day','stepping down'],
fields:'β€’ Your Name: Full name\nβ€’ Current Role: Your designation\nβ€’ Notice Period: How many weeks/days notice are you giving?\nβ€’ Last Working Day: Your proposed final date\nβ€’ Reason (Optional): Brief reason if comfortable sharing\nβ€’ Handover Plan: What will you hand over and to whom?'
},
{
id:17, title:'Job Requisition', category:'HR',
keywords:['hire','hiring','job opening','vacancy','recruit','new position','headcount','open role'],
fields:'β€’ Role Title: What position to hire for?\nβ€’ Division: Product / Services\nβ€’ Number of Openings: How many people needed?\nβ€’ Key Skills Required: Must-have skills\nβ€’ Experience Level: Fresher / 1-2 yrs / 3-5 yrs / Senior\nβ€’ Expected Start Date: When should they ideally join?\nβ€’ Budget Range: Approximate compensation range\nβ€’ Reason for Hire: New role / Replacement / Growth'
},
{
id:18, title:'HR Support', category:'HR',
keywords:['hr','human resources','hr help','policy','payroll','hr query','documents','certificate','letter'],
fields:'β€’ Query Type: Payroll / Document / Policy / Certificate / Other\nβ€’ Description: What do you need help with?\nβ€’ Document Needed (if any): Experience letter / Salary certificate / NOC etc.\nβ€’ Urgency: When do you need this?\nβ€’ Any Reference: Previous conversation or ticket number if applicable'
},
{
id:19, title:'Project Request', category:'General',
keywords:['project','new project','start project','initiate','kick off','new work','client project'],
fields:'β€’ Project Name: What to call this project?\nβ€’ Project Type: Internal / Client / R&D\nβ€’ Objective: What should this project achieve?\nβ€’ Scope: What is in scope and out of scope?\nβ€’ Team Required: Roles needed (dev, designer, tester etc.)\nβ€’ Timeline: Expected start and end date\nβ€’ Budget Estimate: If applicable'
},
{
id:20, title:'Automation Request', category:'Ops',
keywords:['automate','automation','workflow','bot','script','automatic','zapier','make','n8n','monday automation'],
fields:'β€’ What to Automate: Describe the current manual process\nβ€’ Tools Involved: Which apps need to be connected?\nβ€’ Trigger: What event should start the automation?\nβ€’ Action: What should happen automatically?\nβ€’ Expected Output: What\'s the end result?\nβ€’ Priority: How critical is this automation?'
},
{
id:21, title:'Process Change Request', category:'Ops',
keywords:['process','change process','improve process','workflow change','sop','procedure','operations'],
fields:'β€’ Current Process: Describe how it works today\nβ€’ Proposed Change: What should change and how?\nβ€’ Reason: Why is this change needed?\nβ€’ Impact: Who / what will be affected?\nβ€’ Implementation Timeline: When should this go live?\nβ€’ Owner: Who will be responsible for this new process?'
},
{
id:22, title:'Root Cause Analysis', category:'Ops',
keywords:['root cause','rca','why did','investigation','post mortem','incident','bug cause','what went wrong'],
fields:'β€’ Incident Title: Short name for what went wrong\nβ€’ Date & Time: When did it happen?\nβ€’ What Happened: Clear description of the issue\nβ€’ Impact: What was affected (users, revenue, features)?\nβ€’ Immediate Fix Applied: What was done to resolve it temporarily?\nβ€’ Root Cause (if known): What caused it?\nβ€’ Permanent Fix: What needs to be done to prevent recurrence?'
},
{
id:23, title:'Emergency Protocol', category:'General',
keywords:['emergency','urgent','critical','asap','immediately','down','outage','crisis','production issue'],
fields:'β€’ Emergency Type: App Down / Data Loss / Security Breach / Other\nβ€’ Severity: P0 (total outage) / P1 (major) / P2 (partial)\nβ€’ What Is Affected: Which app, feature, or system?\nβ€’ Since When: When did this start?\nβ€’ Current Impact: How many users affected?\nβ€’ Steps Already Taken: What have you tried so far?\nβ€’ Who Knows: Which team members are already aware?'
},
{
id:24, title:'Growth Team Bug Report', category:'Growth',
keywords:['bug','error','crash','broken','not working','fix','glitch','app crash','defect'],
fields:'β€’ App / Feature Name: Where is the bug?\nβ€’ Bug Description: What is happening vs what should happen?\nβ€’ Steps to Reproduce: Step-by-step how to trigger the bug\nβ€’ Device / Platform: Android / iOS / Web + device model\nβ€’ App Version: Which version are you on?\nβ€’ Frequency: Always / Sometimes / Rarely\nβ€’ Screenshot or Recording: Attach evidence'
},
{
id:25, title:'Training Required', category:'HR',
keywords:['training','learn','course','skill','workshop','upskill','tutorial','mentorship','education'],
fields:'β€’ Training Topic: What skill or subject?\nβ€’ Training Type: Online course / Workshop / Internal session / Mentorship\nβ€’ Why Needed: How will this help your work?\nβ€’ Preferred Timeline: When do you want to complete it?\nβ€’ Estimated Cost (if paid): Amount in INR\nβ€’ Platform / Provider: Udemy / Coursera / YouTube / Other'
},
{
id:26, title:'Meeting Request with CEO', category:'Management',
keywords:['ceo','meeting with ceo','talk to ceo','bhargav','leadership','executive','founder'],
fields:'β€’ Your Name & Role: Who is requesting the meeting?\nβ€’ Meeting Purpose: What do you want to discuss? (1-2 lines)\nβ€’ Agenda Points: List the specific topics (max 3 recommended)\nβ€’ Preferred Duration: 15 min / 30 min / 45 min\nβ€’ Preferred Time Slot: Give 2-3 options\nβ€’ Is this Urgent?: Yes / No β€” explain why if yes'
},
{
id:27, title:'WFH Request', category:'HR',
keywords:['wfh','work from home','remote','work remotely','home office','apply for wfh','want to wfh'],
fields:'β€’ Your Name: Full name\nβ€’ WFH Date(s): Which day(s) do you want to WFH?\nβ€’ Reason: Why are you requesting WFH?\nβ€’ Will you be available on calls?: Yes / No\nβ€’ Any impact on deliverables?: Mention if any deadlines are affected\nβ€’ Manager Informed?: Yes / No'
},
{
id:28, title:'Maintenance Task', category:'Ops',
keywords:['maintenance','maintain','upkeep','routine task','scheduled','infra','server maintenance'],
fields:'β€’ Task Title: What needs to be maintained?\nβ€’ System / Tool: Which server, app, or infrastructure?\nβ€’ Type: Routine / Emergency / Upgrade\nβ€’ Scheduled Date & Time: When should this run?\nβ€’ Expected Downtime: Will there be any outage? How long?\nβ€’ Who Will Execute: Person responsible\nβ€’ Rollback Plan: What to do if something goes wrong?'
},
];
/* ── Keyword-score a query against NAR types, return top 3 ── */
function matchNarTypes(query) {
const q = query.toLowerCase();
return NAR_TYPES
.map(n => ({ ...n, score: n.keywords.reduce((s,kw) => s + (q.includes(kw) ? kw.split(' ').length : 0), 0) }))
.filter(n => n.score > 0)
.sort((a,b) => b.score - a.score)
.slice(0, 3);
}
/* ══════════════════════════════════════════════
GROQ AI API
══════════════════════════════════════════════ */
const chatHistory = [];
const messageCache = {};
async function callGroqAPI(userText, images = []) {
try {
let userContent = userText || '';
if (images.length > 0) userContent += `\n[User attached ${images.length} image(s)]`;
chatHistory.push({ role: 'user', content: userContent });
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ messages: chatHistory })
});
const data = await response.json();
if (!data.success) {
throw new Error(data.message || 'Something went wrong');
}
const reply = data.reply;
console.log('RAW REPLY:', reply);
chatHistory.push({ role: 'assistant', content: reply });
if (chatHistory.length > 20) chatHistory.splice(0, 2);
hideTyping();
addMessage('bot', reply);
} catch (err) {
hideTyping();
addMessage('bot', `Sorry, I ran into an error: ${err.message}. Please try again.`);
console.error('Chat API error:', err);
}
}
</script>
</body>
</html>