test2 / static /addon.html
joedown11's picture
Fix prorata overlay click interception issue
edbf356
Raw
History Blame Contribute Delete
73.7 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Martechsol Assistant Widget</title>
<style>
/* Premium Design for Martechsol Assistant */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap');
#martech-chat-wrapper {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
}
/* Floating Action Button (FAB) */
#martech-chat-fab {
position: fixed;
right: 10px;
bottom: 25px;
width: auto;
height: auto;
background: transparent;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 999999;
transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1), background 0.3s ease, box-shadow 0.3s ease;
-webkit-tap-highlight-color: transparent;
}
#martech-chat-fab:hover {
transform: scale(1.1);
}
#martech-chat-fab.is-active {
background: #1e293b;
box-shadow: 0 10px 25px rgba(30, 41, 59, 0.4);
transform: scale(0.8);
}
/* Chat Window Styling */
#martech-chat-box {
position: fixed;
right: 25px;
bottom: 100px;
width: 400px;
height: 550px;
background: #ffffff;
border-radius: 16px;
overflow: hidden;
display: none;
flex-direction: column;
box-shadow: 0 12px 50px rgba(0, 0, 0, 0.2);
z-index: 999998;
border: 1px solid #e2e8f0;
opacity: 0;
transform: translateY(20px);
transition: opacity 0.3s ease, transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
#martech-chat-box.is-visible {
display: flex;
opacity: 1;
transform: translateY(0);
}
#martech-chat-header {
background: #ffffff;
padding: 16px 20px;
border-bottom: 1px solid #f1f5f9;
display: flex;
align-items: center;
gap: 12px;
}
@keyframes pulse-dot {
0% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7);
}
70% {
transform: scale(1.1);
box-shadow: 0 0 0 10px rgba(16, 185, 129, 0);
}
100% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0);
}
}
#martech-chat-header .dot {
width: 10px;
height: 10px;
background: #10b981;
border-radius: 50%;
animation: pulse-dot 2s infinite;
transition: background 0.3s;
}
#martech-chat-header span {
font-weight: 600;
color: #1e293b;
font-size: 15px;
padding-left: 20px;
}
.header-actions {
margin-left: auto;
display: flex;
gap: 8px;
}
.btn-end-chat {
background: #fee2e2;
color: #ef4444;
border: none;
padding: 4px 10px;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.btn-end-chat:hover {
background: #fecaca;
}
#dismiss-widget {
position: absolute;
top: 15px;
left: 15px;
background: #6b6868;
color: #ffffff;
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
font-weight: bold;
cursor: pointer;
z-index: 9999999;
transition: background 0.2s, transform 0.2s;
font-family: sans-serif;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.5);
}
#dismiss-widget:hover {
background: #333333;
transform: scale(1.1);
}
#rating-overlay {
position: absolute;
inset: 0;
background: rgba(255, 255, 255, 0.95);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 100;
opacity: 0;
visibility: hidden;
transition: all 0.3s;
}
#rating-overlay.show {
opacity: 1;
visibility: visible;
}
.stars {
display: flex;
gap: 8px;
font-size: 36px;
cursor: pointer;
margin: 16px 0;
}
.stars span {
color: #cbd5e1;
transition: color 0.15s, transform 0.1s;
user-select: none;
}
.stars span.hovered {
color: #fbbf24;
transform: scale(1.15);
}
.stars span.active {
color: #f59e0b;
}
/* Custom Chat Area Styling */
#chat-messages {
flex: 1;
overflow-y: auto;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
background: #f8fafc;
scroll-behavior: smooth;
}
/* Custom Scrollbar for messages */
#chat-messages::-webkit-scrollbar {
width: 6px;
}
#chat-messages::-webkit-scrollbar-track {
background: transparent;
}
#chat-messages::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 10px;
}
.chat-message {
max-width: 85%;
padding: 12px 16px;
border-radius: 16px;
font-size: 14px;
line-height: 1.5;
word-wrap: break-word;
white-space: pre-wrap;
}
.chat-message.user {
align-self: flex-end;
background: #1e293b;
color: white;
border-bottom-right-radius: 4px;
}
.chat-message.bot {
align-self: flex-start;
background: #ffffff;
color: #334155;
border-bottom-left-radius: 4px;
border: 1px solid #e2e8f0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.02);
}
/* Surgical Bolding & List Style */
.chat-message b {
font-weight: 800;
color: #0f172a;
}
.chat-message.user b {
color: #ffffff;
}
.queue-actions {
display: flex;
gap: 8px;
padding: 8px 16px;
background: #f8fafc;
border-top: 1px solid #e2e8f0;
justify-content: center;
}
.btn-queue {
padding: 8px 12px;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
border: 1px solid transparent;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 6px;
}
.btn-remind {
background: #fff;
border-color: #f59e0b;
color: #d97706;
}
.btn-remind:hover:not(:disabled) {
background: #fef3c7;
}
.btn-remind:disabled {
opacity: 0.5;
cursor: not-allowed;
border-color: #cbd5e1;
color: #94a3b8;
}
.btn-exit-queue {
background: #fff;
border-color: #e2e8f0;
color: #475569;
}
.btn-exit-queue:hover {
background: #f1f5f9;
}
#chat-input-area {
padding: 16px;
background: #ffffff;
border-top: 1px solid #f1f5f9;
display: flex;
gap: 8px;
align-items: center;
}
#chat-input {
flex: 1;
border: 1px solid #e2e8f0;
border-radius: 20px;
padding: 12px 16px;
font-size: 14px;
outline: none;
transition: border-color 0.2s;
font-family: inherit;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.02);
}
#chat-input:focus {
border-color: #10b981;
}
#chat-input:disabled {
background: #f1f5f9;
cursor: not-allowed;
}
#chat-send {
background: #10b981;
color: white;
border: none;
width: 44px;
height: 44px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background 0.2s, transform 0.1s;
}
#chat-send:hover {
background: #059669;
}
#chat-send:active {
transform: scale(0.95);
}
#chat-send:disabled {
background: #cbd5e1;
cursor: not-allowed;
transform: none;
}
/* Loading / Typing Animation */
.typing-indicator {
display: flex;
gap: 4px;
padding: 4px 2px;
align-items: center;
height: 18px;
}
.typing-indicator span {
width: 6px;
height: 6px;
background: #94a3b8;
border-radius: 50%;
animation: typing-dots 1.4s infinite ease-in-out both;
}
.typing-indicator span:nth-child(1) {
animation-delay: -0.32s;
}
.typing-indicator span:nth-child(2) {
animation-delay: -0.16s;
}
@keyframes typing-dots {
0%,
80%,
100% {
transform: scale(0);
}
40% {
transform: scale(1);
}
}
@media (max-width: 480px) {
#martech-chat-box {
right: 15px;
bottom: 100px;
width: calc(100vw - 30px);
height: 70vh;
}
}
/* ===== Prorata Dashboard Overlay ===== */
#prorata-overlay {
position: absolute;
inset: 0;
background: hsl(220, 20%, 97%);
display: flex;
flex-direction: column;
z-index: 102;
opacity: 0;
visibility: hidden;
pointer-events: none;
transform: translateY(10px);
transition: opacity 280ms cubic-bezier(0.4,0,0.2,1), transform 280ms cubic-bezier(0.4,0,0.2,1), visibility 280ms;
overflow-y: auto;
}
#prorata-overlay.show {
opacity: 1;
visibility: visible;
pointer-events: auto;
transform: translateY(0);
}
#prorata-overlay::-webkit-scrollbar { width: 4px; }
#prorata-overlay::-webkit-scrollbar-track { background: transparent; }
#prorata-overlay::-webkit-scrollbar-thumb { background: hsl(220,13%,82%); border-radius: 999px; }
#prorata-header {
padding: 16px 18px 14px;
background: #fff;
border-bottom: 1px solid hsl(220,13%,91%);
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
}
#prorata-header .ph-title {
font-size: 14px;
font-weight: 700;
color: hsl(222, 47%, 11%);
letter-spacing: -0.01em;
}
#prorata-header .ph-right {
display: flex;
align-items: center;
gap: 10px;
}
#prorata-current-date {
font-size: 11px;
font-weight: 500;
color: hsl(215, 16%, 47%);
background: hsl(220,13%,91%);
padding: 3px 8px;
border-radius: 999px;
}
#prorata-close-btn {
background: none;
border: none;
cursor: pointer;
color: hsl(215,16%,47%);
font-size: 18px;
line-height: 1;
padding: 2px 4px;
border-radius: 6px;
transition: background 0.15s, color 0.15s;
display: flex; align-items: center; justify-content: center;
}
#prorata-close-btn:hover { background: hsl(220,13%,91%); color: hsl(222,47%,11%); }
#prorata-body {
padding: 14px 14px 18px;
display: flex;
flex-direction: column;
gap: 10px;
}
.pr-card {
background: #fff;
border: 1px solid hsl(220,13%,91%);
border-radius: 12px;
padding: 14px 16px;
box-shadow: 0 1px 4px rgba(0,0,0,0.04);
opacity: 0;
transform: translateY(8px);
transition: opacity 220ms ease, transform 220ms ease;
}
.pr-card.visible {
opacity: 1;
transform: translateY(0);
}
.pr-card-top {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.pr-label {
font-size: 12px;
font-weight: 600;
color: hsl(215,16%,47%);
text-transform: uppercase;
letter-spacing: 0.06em;
}
.pr-count {
font-size: 12px;
font-weight: 700;
color: hsl(222,47%,11%);
}
.pr-count span { font-weight: 400; color: hsl(215,16%,47%); }
.pr-bar-track {
height: 9px;
background: hsl(220,13%,91%);
border-radius: 999px;
overflow: hidden;
margin-bottom: 8px;
}
.pr-bar-fill {
height: 100%;
width: 0;
border-radius: 999px;
transition: width 600ms ease-out;
}
.pr-chip {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 10.5px;
font-weight: 600;
color: hsl(32, 95%, 35%);
background: hsl(38, 100%, 95%);
border: 1px solid hsl(38,90%,80%);
padding: 3px 8px;
border-radius: 999px;
margin-top: 2px;
}
.pr-chip-ok {
color: hsl(152,60%,30%);
background: hsl(152,60%,96%);
border-color: hsl(152,50%,75%);
}
#prorata-loading, #prorata-error {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1;
gap: 10px;
color: hsl(215,16%,47%);
font-size: 13px;
}
.pr-spinner {
width: 28px; height: 28px;
border: 3px solid hsl(220,13%,88%);
border-top-color: hsl(152,69%,43%);
border-radius: 50%;
animation: pr-spin 0.75s linear infinite;
}
@keyframes pr-spin { to { transform: rotate(360deg); } }
#prorata-retry-btn {
margin-top: 6px;
padding: 6px 16px;
font-size: 12px;
font-weight: 600;
background: hsl(152,69%,43%);
color: #fff;
border: none;
border-radius: 8px;
cursor: pointer;
transition: background 0.15s;
}
#prorata-retry-btn:hover { background: hsl(152,69%,37%); }
#prorata-info-btn {
background: hsl(215, 20%, 94%);
border: 1px solid hsl(220, 13%, 88%);
cursor: pointer;
color: hsl(215, 25%, 40%);
font-size: 12px;
font-weight: 600;
width: 20px;
height: 20px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
font-family: inherit;
}
#prorata-info-btn:hover {
background: hsl(215, 25%, 90%);
color: hsl(222, 47%, 11%);
transform: scale(1.05);
box-shadow: 0 2px 4px rgba(0,0,0,0.08);
}
#prorata-info-btn:active, #prorata-info-btn.active {
background: hsl(215, 30%, 85%);
transform: scale(0.95);
}
</style>
</head>
<body>
<!-- Lottie Player Script -->
<script src="https://unpkg.com/@dotlottie/player-component@latest/dist/dotlottie-player.mjs" type="module"></script>
<div id="martech-chat-wrapper">
<div id="martech-chat-fab">
<!-- Dismiss Widget Button -->
<div id="dismiss-widget" title="Dismiss widget">&times;</div>
<!-- Animated Chatbot Icon -->
<div id="chat-icon"
style="width: 233px; height: 238px; display: flex; align-items: center; justify-content: center; pointer-events: none;">
<dotlottie-player src="https://testingweb647-test2.hf.space/static/bot-icon.lottie"
background="transparent" speed="1" style="width: 100%; height: 100%; will-change: transform;" loop
autoplay>
</dotlottie-player>
</div>
</div>
<div id="martech-chat-box">
<div id="martech-chat-header" style="display:flex; justify-content:space-between; align-items:center;">
<div style="display: flex; align-items: center; gap: 0px; flex: 1;">
<div class="dot" id="chat-status-dot"></div>
<span id="chat-header-title">Martechsol Assistant</span>
</div>
<div style="display:flex; align-items:center; gap:8px;">
<div style="position: relative;">
<button id="chat-options-btn" title="Options"
style="background:none; border:none; cursor:pointer; color:#334155; padding:6px; display:flex; align-items:center; justify-content:center; border-radius:50%; transition:all 0.2s cubic-bezier(0.4, 0, 0.2, 1);"
onmouseover="this.style.background='#e2e8f0'; this.style.color='#0f172a'"
onmouseout="this.style.background='transparent'; this.style.color='#334155'">
<svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="1.5"></circle>
<circle cx="5" cy="12" r="1.5"></circle>
<circle cx="19" cy="12" r="1.5"></circle>
</svg>
</button>
<div id="chat-options-menu"
style="display:none; position:absolute; right:0; top:100%; margin-top:8px; background:white; border:1px solid #e2e8f0; border-radius:12px; box-shadow:0 10px 25px rgba(0,0,0,0.1); padding:8px; min-width:180px; z-index:100;">
<div id="opt-toggle-sound"
style="padding:10px 12px; font-size:14px; font-weight:500; color:#334155; cursor:pointer; border-radius:8px; transition:all 0.2s ease; display:flex; align-items:center; justify-content:space-between; gap:12px;"
onmouseover="this.style.background='#f1f5f9'; this.style.color='#0f172a'"
onmouseout="this.style.background='transparent'; this.style.color='#334155'">
<div style="display:flex; align-items:center; gap:10px;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon>
<path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"></path>
</svg>
Sounds
</div>
<div id="opt-sound-switch"
style="position:relative; width:34px; height:20px; background-color:#10b981; border-radius:20px; transition:background-color 0.3s;">
<div id="opt-sound-knob"
style="position:absolute; top:2px; left:2px; width:16px; height:16px; background:white; border-radius:50%; transition:transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); box-shadow:0 2px 4px rgba(0,0,0,0.2); transform:translateX(14px);">
</div>
</div>
</div>
<div id="opt-export-chat"
style="margin-top:4px; padding:10px 12px; font-size:14px; font-weight:500; color:#334155; cursor:pointer; border-radius:8px; transition:all 0.2s ease; display:flex; align-items:center; gap:10px;"
onmouseover="this.style.background='#f1f5f9'; this.style.color='#0f172a'"
onmouseout="this.style.background='transparent'; this.style.color='#334155'">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
Export chat
</div>
<div id="opt-prorata-dashboard"
style="margin-top:4px; padding:10px 12px; font-size:14px; font-weight:500; color:#334155; cursor:pointer; border-radius:8px; transition:all 0.2s ease; display:flex; align-items:center; gap:10px;"
onmouseover="this.style.background='#f1f5f9'; this.style.color='#0f172a'"
onmouseout="this.style.background='transparent'; this.style.color='#334155'">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="7" height="7" rx="1"></rect>
<rect x="14" y="3" width="7" height="7" rx="1"></rect>
<rect x="3" y="14" width="7" height="7" rx="1"></rect>
<path d="M14 17h7M17 14v7"></path>
</svg>
Prorata Dashboard
</div>
</div>
</div>
<button id="minimize-chat" title="Minimize"
style="background:none; border:none; color:#1e293b; font-size: 28px; font-weight: 300; cursor: pointer; padding: 0 5px; line-height: 0.5; outline: none;">&times;</button>
</div>
</div>
<!-- Custom Chat UI replacing iframe -->
<div id="chat-messages">
<div class="chat-message bot">Welcome! How can I help you today?</div>
</div>
<div id="queue-actions-area" class="queue-actions" style="display: none;">
<button id="btn-remind" class="btn-queue btn-remind" disabled>
<span id="remind-text">Wait 3:00</span>
</button>
<button id="btn-exit-queue" class="btn-queue btn-exit-queue">
🚪 Exit Queue
</button>
</div>
<div id="chat-input-area">
<input type="text" id="chat-input" placeholder="Type your question here..." autocomplete="off" />
<button id="chat-send" title="Send">
<!-- Send Icon -->
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<line x1="22" y1="2" x2="11" y2="13"></line>
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
</svg>
</button>
</div>
<div id="rating-overlay">
<h3 style="color:#1e293b; margin-bottom:8px">Rate Your Experience</h3>
<p style="color:#64748b; font-size:14px">How was your chat?</p>
<div class="stars" id="rating-stars">
<span data-val="1">&#9734;</span><span data-val="2">&#9734;</span><span data-val="3">&#9734;</span><span data-val="4">&#9734;</span><span data-val="5">&#9734;</span>
</div>
<button id="btn-submit-rating" disabled
style="background:#10b981; color:#fff; border:none; padding:10px 24px; border-radius:8px; font-weight:600; cursor:not-allowed; margin-top:8px; opacity:0.5; transition:opacity 0.2s;">Submit
&amp; Close</button>
</div>
<!-- Prorata Dashboard Overlay -->
<div id="prorata-overlay">
<div id="prorata-header">
<span class="ph-title" style="display:flex; align-items:center; gap:8px;">
&#x1F4CA; Leave Summary
<button id="prorata-info-btn" class="active" title="What is Prorata?">i</button>
</span>
<div class="ph-right">
<span id="prorata-current-date"></span>
<button id="prorata-close-btn" title="Close">&#x2715;</button>
</div>
</div>
<div id="prorata-info-panel" style="display:block; padding:12px 18px; background:hsl(220,40%,96%); border-bottom:1px solid hsl(220,13%,91%); font-size:12.5px; color:hsl(215,20%,40%); line-height:1.45;">
<strong>Prorata Leave Policy:</strong> Your leaves are granted proportionally as the year progresses. If you resign before year-end, any leaves you have taken that exceed your current earned (prorated) balance <strong>may be subject to recovery</strong>.
</div>
<div id="prorata-loading">
<div class="pr-spinner"></div>
<span>Loading leave data&hellip;</span>
</div>
<div id="prorata-error" style="display:none;">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="hsl(4,86%,58%)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
<span>Unable to load leave data.</span>
<button id="prorata-retry-btn">Retry</button>
</div>
<div id="prorata-body" style="display:none;"></div>
</div>
</div>
</div>
<script>
(function () {
// UI Elements
const fab = document.getElementById('martech-chat-fab');
const box = document.getElementById('martech-chat-box');
const chatIcon = document.getElementById('chat-icon');
const minimizeBtn = document.getElementById('minimize-chat');
const dismissWidgetBtn = document.getElementById('dismiss-widget');
const messagesContainer = document.getElementById('chat-messages');
const inputField = document.getElementById('chat-input');
const sendButton = document.getElementById('chat-send');
// Dismiss Widget Logic
if (dismissWidgetBtn) {
dismissWidgetBtn.addEventListener('click', function (e) {
e.stopPropagation();
fab.style.display = 'none';
});
}
// Toggle Logic
function openChat() {
box.style.display = 'flex';
setTimeout(() => { box.classList.add('is-visible'); inputField.focus(); }, 10);
fab.style.display = 'none'; // Hide FAB completely when open
}
function closeChat() {
box.classList.remove('is-visible');
setTimeout(() => { box.style.display = 'none'; }, 300);
fab.style.display = 'flex'; // Restore FAB
}
fab.addEventListener('click', openChat);
if (minimizeBtn) {
minimizeBtn.addEventListener('click', function (e) {
e.stopPropagation();
closeChat();
});
}
// Chat Logic
const API_URL = "https://testingweb647-test2.hf.space/api/chat";
const BASE_URL = API_URL.replace("/api/chat", "");
let chatHistory = [];
let _pollInterval = null;
let _lastPollId = 0;
let _wasBlocked = false;
let _wasQueued = false;
// Audio Feedback
const sendSound = new Audio(BASE_URL + '/static/chatbot-send.wav');
const receiveSound = new Audio(BASE_URL + '/static/chatbot-receive.wav');
sendSound.volume = 0.5;
receiveSound.volume = 0.5;
let playSounds = true;
let _humanTypingEl = null; // Persistent 'agent typing' indicator during HUMAN_TAKEOVER
let _isWaiting = false; // True while a user message is in-flight (API call + typing animation)
// Queue Timer State
let _queueEnteredTime = 0;
let _reminderUses = 3;
let _lastReminderTime = 0;
let _queueInterval = null;
function updateUIState(status, blocked) {
if (blocked) {
inputField.disabled = true;
inputField.placeholder = "Session blocked.";
sendButton.disabled = true;
if (!_wasBlocked) {
appendMessage('bot', "Your session has been restricted by the administrator. You cannot send further messages.");
_wasBlocked = true;
}
return;
}
if (status === 'QUEUED') {
inputField.disabled = true;
inputField.placeholder = "Waiting for human agent...";
sendButton.disabled = true;
if (!_wasQueued) {
_wasQueued = true;
_queueEnteredTime = Date.now();
_reminderUses = 3;
_lastReminderTime = 0;
startQueueTimer();
}
document.getElementById('queue-actions-area').style.display = 'flex';
} else if (status === 'ENDED') {
inputField.disabled = true;
inputField.placeholder = "Chat ended.";
sendButton.disabled = true;
document.getElementById('queue-actions-area').style.display = 'none';
stopQueueTimer();
_wasQueued = false;
if (!document.getElementById('btn-new-chat')) {
const newChatBtn = document.createElement('button');
newChatBtn.id = 'btn-new-chat';
newChatBtn.innerHTML = 'Start New Chat';
newChatBtn.style.cssText = 'width:calc(100% - 32px); margin:0 16px 16px 16px; padding:12px; background:#10b981; color:white; border:none; border-radius:8px; font-weight:600; cursor:pointer; font-family:inherit;';
newChatBtn.onclick = () => { ratingOverlay.classList.add('show'); };
document.getElementById('martech-chat-box').appendChild(newChatBtn);
}
} else {
// AI or HUMAN_TAKEOVER (comment removed)
if (!_isWaiting) {
inputField.disabled = false;
inputField.placeholder = "Type your question here...";
sendButton.disabled = false;
}
_wasQueued = false; // Reset queue state if taken over
document.getElementById('queue-actions-area').style.display = 'none';
stopQueueTimer();
// Remove 'Start New Chat' button if it exists
const btnNewChat = document.getElementById('btn-new-chat');
if (btnNewChat) btnNewChat.remove();
document.getElementById('chat-input-area').style.display = 'flex';
}
}
function startQueueTimer() {
if (_queueInterval) clearInterval(_queueInterval);
_queueInterval = setInterval(checkQueueTimer, 1000);
checkQueueTimer();
}
function stopQueueTimer() {
if (_queueInterval) clearInterval(_queueInterval);
}
function checkQueueTimer() {
const now = Date.now();
const btnRemind = document.getElementById('btn-remind');
const remindText = document.getElementById('remind-text');
if (_reminderUses <= 0) {
btnRemind.disabled = true;
remindText.textContent = "No reminders left";
return;
}
// Needs 3 minutes (180000 ms) from enter time OR from last reminder
const timeSinceEntered = now - _queueEnteredTime;
const timeSinceLastReminder = now - _lastReminderTime;
if (timeSinceEntered >= 180000 && (_lastReminderTime === 0 || timeSinceLastReminder >= 180000)) {
btnRemind.disabled = false;
remindText.textContent = `Remind Admin (${_reminderUses} left)`;
} else {
btnRemind.disabled = true;
// Calculate remaining time
const targetTime = _lastReminderTime > 0 ? _lastReminderTime + 180000 : _queueEnteredTime + 180000;
const remainingSec = Math.ceil((targetTime - now) / 1000);
const min = Math.floor(remainingSec / 60);
const sec = remainingSec % 60;
remindText.textContent = `Wait ${min}:${sec.toString().padStart(2, '0')}`;
}
}
// (comment removed)
let sessionId = localStorage.getItem('martech_session_id');
if (!sessionId) {
// Use a temporary name/id until we get the real one
sessionId = 'user-' + Date.now() + '-' + Math.random().toString(36).slice(2, 9);
localStorage.setItem('martech_session_id', sessionId);
}
// Bug fix: if existing session is still a generic temp ID, mark it for re-resolution
const _isGenericId = (id) => id && id.startsWith('user-') && /^user-\d+-[a-z0-9]+$/.test(id);
function formatMessage(text) {
if (!text) return "";
// 1. Convert Markdown Bold to HTML Bold (safety)
let formatted = text.replace(/\*\*(.*?)\*\*/g, '<b>$1</b>');
// 2. Remove the '*' at the start of lines and use a bullet
formatted = formatted.replace(/^\* /gm, '• ');
// 3. Newlines to BR
return formatted.replace(/\n/g, '<br>');
}
function appendMessage(role, content) {
const div = document.createElement('div');
div.className = `chat-message ${role}`;
div.innerHTML = formatMessage(content);
messagesContainer.appendChild(div);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
return div;
}
// (comment removed)
async function _resolveUserName() {
// (comment removed)
// The HR portal page sets window.HR_USER_NAME = "John Doe" before this script loads
if (window.HR_USER_NAME && typeof window.HR_USER_NAME === 'string' && window.HR_USER_NAME.trim().length > 1) {
return window.HR_USER_NAME.trim();
}
// (comment removed)
try {
const storageKeys = ['userInfo', 'user', 'profile', 'auth', 'currentUser', 'account', 'userData', 'loginUser'];
for (const key of storageKeys) {
const stored = localStorage.getItem(key) || sessionStorage.getItem(key);
if (stored) {
try {
const parsed = JSON.parse(stored);
// Covers: {name}, {fullName}, {user:{name}}, {display_name}, {personalDetails:{fullName}}
const found = parsed.name
|| parsed.fullName
|| (parsed.user && parsed.user.name)
|| parsed.display_name
|| (parsed.personalDetails && parsed.personalDetails.fullName);
if (found && found.trim().length > 1) return found.trim();
} catch (err) {
// Plain string value (e.g. name stored directly)
if (typeof stored === 'string' && stored.length < 80 && !stored.includes('{')) {
return stored.trim();
}
}
}
}
} catch (e) { }
// (comment removed)
// API confirmed to return: { name: "Ghulam Baqir", ... }
try {
const tokenKeys = ['token', 'accessToken', 'access_token', 'authToken', 'auth_token', 'jwt', 'jwtToken'];
let authToken = null;
for (const tk of tokenKeys) {
const tv = localStorage.getItem(tk) || sessionStorage.getItem(tk);
if (tv && tv.length > 20) { authToken = tv.replace(/^Bearer\s+/i, ''); break; }
}
const headers = { 'Content-Type': 'application/json' };
if (authToken) headers['Authorization'] = 'Bearer ' + authToken;
const pResp = await fetch("https://hrmmartechsol-ba71e8dc3fa2.herokuapp.com/api/users/profile", {
headers: headers,
credentials: 'include' // also sends cookies (session-based auth)
});
if (pResp.ok) {
const pData = await pResp.json();
// API top-level 'name' field is confirmed; personalDetails.fullName is fallback
const found = pData.name || (pData.personalDetails && pData.personalDetails.fullName);
if (found && found.trim().length > 1) return found.trim();
}
} catch (e) { }
return null; // all options exhausted
}
// (comment removed)
async function initUserSession() {
// Bug fix: if this is a returning visit but the stored ID is still a generic
// temp ID (resolution failed last time), retry resolution now
const needsResolution = _isGenericId(sessionId);
let userName = await _resolveUserName();
// Multi-user logic: if name changed since last visit, start a fresh session
const lastKnownName = localStorage.getItem('martech_last_user_name');
if (userName && lastKnownName && userName !== lastKnownName) {
localStorage.removeItem('martech_session_id');
sessionId = 'user-' + Date.now() + '-' + Math.random().toString(36).slice(2, 9);
localStorage.setItem('martech_session_id', sessionId);
messagesContainer.innerHTML = '<div class="chat-message bot">Welcome! How can I help you today?</div>';
}
if (userName) {
localStorage.setItem('martech_last_user_name', userName);
}
// (comment removed)
// This creates the session with the user's name BEFORE the user even
// opens the chat window, so it is immediately visible in the admin panel.
try {
const initData = await fetch(BASE_URL + "/api/session/init", {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: sessionId, user_name: userName || null })
}).then(r => r.json());
// Backend may return a canonical named session_id (e.g. "Ali_Hassan")
if (initData.session_id && initData.session_id !== sessionId) {
sessionId = initData.session_id;
localStorage.setItem('martech_session_id', sessionId);
}
} catch (e) {
// Non-fatal: session will still be created on first message if this fails
console.warn('Martech: background session pre-creation failed', e);
}
try {
const hResp = await fetch(BASE_URL + "/api/session/history/" + sessionId);
if (hResp.ok) {
const hData = await hResp.json();
if (hData.status === 'HUMAN_TAKEOVER') {
document.getElementById('chat-status-dot').style.background = '#8b5cf6';
document.getElementById('chat-header-title').textContent = 'Live Agent (Admin)';
} else if (hData.status === 'ENDED') {
document.getElementById('chat-status-dot').style.background = '#94a3b8';
document.getElementById('chat-header-title').textContent = 'Chat Ended';
} else {
document.getElementById('chat-status-dot').style.background = '#10b981';
document.getElementById('chat-header-title').textContent = 'Martechsol Assistant';
}
updateUIState(hData.status, hData.blocked);
if (hData.last_id) {
_lastPollId = hData.last_id;
}
if (hData.history && hData.history.length > 0) {
messagesContainer.innerHTML = '';
chatHistory = hData.history;
chatHistory.forEach(m => {
appendMessage(m.role === 'assistant' ? 'bot' : 'user', m.content);
});
}
}
} catch (e) { }
// If name still not found, set up continuous lightweight background resolution
// so the session gets named as soon as auth is available
if (!userName) {
let _isResolving = false;
const _attemptSync = async () => {
// Stop checking if we already got a real name, or if a check is currently running
if (!_isGenericId(sessionId) || _isResolving) return;
_isResolving = true;
const resolved = await _resolveUserName();
if (resolved) {
localStorage.setItem('martech_last_user_name', resolved);
try {
// Re-init with the now-known name to promote temp (comment removed)
const rData = await fetch(BASE_URL + "/api/session/init", {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: sessionId, user_name: resolved })
}).then(r => r.json());
if (rData.session_id && rData.session_id !== sessionId) {
sessionId = rData.session_id;
localStorage.setItem('martech_session_id', sessionId);
}
} catch (e) { }
}
_isResolving = false;
};
// Event 1: Instantly trigger when the main app writes a token to storage
window.addEventListener('storage', _attemptSync);
// Event 2: Instantly trigger when user refocuses the tab (e.g. after auth redirect)
window.addEventListener('focus', _attemptSync);
// Fallback: Check every 15 seconds (virtually zero cost once session is named)
setInterval(_attemptSync, 15000);
}
// Setup polling for admin messages
if (_pollInterval) clearInterval(_pollInterval);
_pollInterval = setInterval(pollMessages, 3000);
}
async function pollMessages() {
if (!sessionId) return;
try {
const r = await fetch(BASE_URL + "/api/chat/poll", {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: sessionId, last_id: _lastPollId })
});
if (r.ok) {
const data = await r.json();
_lastPollId = data.last_id;
if (data.status === 'HUMAN_TAKEOVER') {
document.getElementById('chat-status-dot').style.background = '#8b5cf6';
document.getElementById('chat-header-title').textContent = 'Live Agent (Admin)';
} else if (data.status === 'ENDED') {
document.getElementById('chat-status-dot').style.background = '#94a3b8';
document.getElementById('chat-header-title').textContent = 'Chat Ended';
} else {
document.getElementById('chat-status-dot').style.background = '#10b981';
document.getElementById('chat-header-title').textContent = 'Martechsol Assistant';
}
updateUIState(data.status, data.blocked);
if (data.new_messages && data.new_messages.length > 0) {
// Remove the 'Human Agent is typing...' indicator before showing the real reply
if (_humanTypingEl) {
_humanTypingEl.remove();
_humanTypingEl = null;
}
data.new_messages.forEach(m => {
appendMessage('bot', m.content);
chatHistory.push({ role: "assistant", content: m.content });
});
}
}
} catch (e) { }
}
initUserSession();
function showTypingIndicator() {
const div = document.createElement('div');
div.className = 'chat-message bot';
div.style.padding = '8px 16px';
div.innerHTML = '<div class="typing-indicator"><span></span><span></span><span></span></div>';
messagesContainer.appendChild(div);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
return div;
}
// Typo Configuration for Human-like Typing
const NEARBY_KEYS = {
'a': 'sqwz', 'b': 'vghn', 'c': 'xdfv', 'd': 'sfecx', 'e': 'wrsdf',
'f': 'dgrtcv', 'g': 'fhtybn', 'h': 'gjyunb', 'i': 'uojkl', 'j': 'hkunmi',
'k': 'jlomi', 'l': 'kop', 'm': 'njk', 'n': 'bhjm', 'o': 'ipkl',
'p': 'ol', 'q': 'wa', 'r': 'edft', 's': 'awedxz', 't': 'rfgy',
'u': 'yihj', 'v': 'cfgb', 'w': 'qase', 'x': 'zsdc', 'y': 'tghu',
'z': 'xas',
};
const TYPO_CHANCE = 0.07;
function getNearbyChar(ch) {
const lower = ch.toLowerCase();
if (NEARBY_KEYS[lower]) {
const options = NEARBY_KEYS[lower];
const replacement = options.charAt(Math.floor(Math.random() * options.length));
return ch === ch.toUpperCase() ? replacement.toUpperCase() : replacement;
}
const abc = "abcdefghijklmnopqrstuvwxyz";
return abc.charAt(Math.floor(Math.random() * abc.length));
}
const sleep = ms => new Promise(r => setTimeout(r, ms));
async function typeMessageWithTypos(element, text) {
const fullHtml = formatMessage(text);
// Simple HTML-aware typing: split by tags
const tokens = fullHtml.split(/(<[^>]*>)/);
let currentHtml = "";
for (const token of tokens) {
if (token.startsWith("<")) {
// Instantly add tags
currentHtml += token;
element.innerHTML = currentHtml;
} else {
// Type text content char by char
for (let char of token) {
currentHtml += char;
element.innerHTML = currentHtml;
await sleep(Math.random() * 15 + 5);
}
}
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
}
async function handleSend() {
const text = inputField.value.trim();
if (!text) return;
// Remove any stale 'Human Agent is typing...' indicator before sending a new message
if (_humanTypingEl) {
_humanTypingEl.remove();
_humanTypingEl = null;
}
// UI state lock
inputField.value = '';
inputField.disabled = true;
sendButton.disabled = true;
_isWaiting = true;
if (playSounds) sendSound.play().catch(e => console.log('Audio error:', e));
appendMessage('user', text);
const typingEl = showTypingIndicator();
try {
// Call the FastAPI Backend directly
const response = await fetch(API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: text,
history: chatHistory,
session_id: sessionId
})
});
if (!response.ok) {
if (response.status === 429) throw new Error("Rate limit exceeded.");
throw new Error("Server error.");
}
const data = await response.json();
const reply = data.reply;
if (reply && reply.trim()) {
// Normal AI reply (comment removed)
typingEl.remove();
const botEl = appendMessage('bot', '');
chatHistory.push({ role: "user", content: text });
chatHistory.push({ role: "assistant", content: reply });
if (playSounds) receiveSound.play().catch(e => console.log('Audio error:', e));
await typeMessageWithTypos(botEl, reply);
} else {
// HUMAN_TAKEOVER mode: backend returned empty reply.
// Repurpose the typing indicator as 'Human Agent is typing...' and keep it visible.
typingEl.innerHTML = `
<div style="display:flex; align-items:center; justify-content:flex-start; gap:6px;">
<span style="font-size:13px; color:#8b5cf6; font-weight:600; line-height:1; margin-top:2px;">typing</span>
<div class="typing-indicator" style="height:18px; padding:0; margin:0;"><span></span><span></span><span></span></div>
</div>`;
typingEl.style.padding = '8px 16px';
typingEl.style.lineHeight = '0.2';
typingEl.style.borderColor = 'rgba(139,92,246,0.25)';
typingEl.style.background = 'rgba(139,92,246,0.05)';
_humanTypingEl = typingEl; // Keep reference so pollMessages can remove it
chatHistory.push({ role: "user", content: text });
}
} catch (error) {
console.error(error);
typingEl.remove();
if (_humanTypingEl === typingEl) _humanTypingEl = null;
appendMessage('bot', "⚠️ I'm sorry, I encountered an error. Please try again.");
} finally {
_isWaiting = false;
inputField.disabled = false;
sendButton.disabled = false;
inputField.focus();
}
}
// Event Listeners
sendButton.addEventListener('click', handleSend);
inputField.addEventListener('keypress', function (e) {
if (e.key === 'Enter') handleSend();
});
document.getElementById('btn-remind').addEventListener('click', async () => {
if (_reminderUses <= 0) return;
try {
await fetch(BASE_URL + "/api/chat/remind", {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: sessionId })
});
_reminderUses--;
_lastReminderTime = Date.now();
checkQueueTimer();
const remindText = document.getElementById('remind-text');
remindText.textContent = "Reminder Sent!";
setTimeout(() => checkQueueTimer(), 2000);
} catch (e) { }
});
document.getElementById('btn-exit-queue').addEventListener('click', async () => {
try {
document.getElementById('queue-actions-area').style.display = 'none';
appendMessage('bot', "You have left the queue and are back to chatting with the AI.");
await fetch(BASE_URL + "/api/chat/exit_queue", {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: sessionId })
});
// Force a poll to sync state
pollMessages();
} catch (e) { }
});
// Rating Logic
const ratingOverlay = document.getElementById('rating-overlay');
const stars = document.querySelectorAll('#rating-stars span');
const submitRatingBtn = document.getElementById('btn-submit-rating');
let _selectedRating = 0;
// Hover preview
stars.forEach(s => {
s.addEventListener('mouseenter', () => {
const val = parseInt(s.dataset.val);
stars.forEach(st => {
const sv = parseInt(st.dataset.val);
st.classList.toggle('hovered', sv <= val);
st.innerHTML = sv <= val ? '&#9733;' : (sv <= _selectedRating ? '&#9733;' : '&#9734;');
});
});
s.addEventListener('mouseleave', () => {
stars.forEach(st => {
st.classList.remove('hovered');
const sv = parseInt(st.dataset.val);
st.innerHTML = sv <= _selectedRating ? '&#9733;' : '&#9734;';
});
});
// Click to lock selection
s.addEventListener('click', () => {
_selectedRating = parseInt(s.dataset.val);
stars.forEach(st => {
const sv = parseInt(st.dataset.val);
st.classList.toggle('active', sv <= _selectedRating);
st.classList.remove('hovered');
st.innerHTML = sv <= _selectedRating ? '&#9733;' : '&#9734;';
});
// Enable submit button
submitRatingBtn.disabled = false;
submitRatingBtn.style.opacity = '1';
submitRatingBtn.style.cursor = 'pointer';
});
});
submitRatingBtn.addEventListener('click', async () => {
// Guard: do nothing if button is disabled (no star selected)
if (_selectedRating === 0) return;
if (_selectedRating > 0) {
try {
await fetch(BASE_URL + "/api/chat/rate", {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: sessionId, rating: _selectedRating })
});
} catch (e) { }
}
// Reset current chat thread instead of deleting session
try {
await fetch(BASE_URL + "/api/chat/reset", {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: sessionId })
});
} catch (e) { }
// Reset stars back to empty for next chat
_selectedRating = 0;
stars.forEach(st => { st.innerHTML = '&#9734;'; st.classList.remove('active', 'hovered'); });
submitRatingBtn.disabled = true;
submitRatingBtn.style.opacity = '0.5';
submitRatingBtn.style.cursor = 'not-allowed';
chatHistory = [];
_lastPollId = 0;
_wasBlocked = false;
messagesContainer.innerHTML = '<div class="chat-message bot">Welcome! How can I help you today?</div>';
ratingOverlay.classList.remove('show');
document.getElementById('chat-status-dot').style.background = '#10b981';
const titleEl = document.getElementById('chat-header-title');
titleEl.textContent = 'Martechsol Assistant';
document.getElementById('chat-input-area').style.display = 'flex';
inputField.disabled = false;
sendButton.disabled = false;
inputField.placeholder = "Type your question here...";
const newChatBtn = document.getElementById('btn-new-chat');
if (newChatBtn) newChatBtn.remove();
closeChat(); // Close chat UI
// Prep for next open
setTimeout(() => {
initUserSession();
}, 500);
});
// --- Prorata Dashboard Logic ---
(function () {
const PROFILE_API = 'https://hrmmartechsol-ba71e8dc3fa2.herokuapp.com/api/users/profile';
const overlay = document.getElementById('prorata-overlay');
const loadingEl = document.getElementById('prorata-loading');
const errorEl = document.getElementById('prorata-error');
const bodyEl = document.getElementById('prorata-body');
const dateEl = document.getElementById('prorata-current-date');
function setCurrentDate() {
const now = new Date();
dateEl.textContent = now.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' });
}
// Months elapsed since Jan 1 of current year (1-based, at least 1)
function getElapsedMonths() {
const now = new Date();
return Math.max(1, now.getMonth() + 1); // getMonth() is 0-indexed
}
function getProratedEntitlement(allowed) {
return Math.floor((getElapsedMonths() / 12) * allowed);
}
// Remaining time from today to Dec 31 of this year
function getYearEndRemaining() {
const now = new Date();
const yearEnd = new Date(now.getFullYear(), 11, 31); // Dec 31
const diffMs = yearEnd - now;
const totalDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24));
const months = Math.floor(totalDays / 30);
const days = totalDays % 30;
const monthNames = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
const curMonthName = monthNames[now.getMonth()];
let remaining = '';
if (months > 0 && days > 0) remaining = `${months}mo ${days}d to year-end`;
else if (months > 0) remaining = `${months} month${months>1?'s':''} to year-end`;
else if (days > 0) remaining = `${days} day${days>1?'s':''} to year-end`;
else remaining = 'Year ending today';
return { curMonthName, remaining, isDecember: now.getMonth() === 11, totalDays };
}
function getBarColor(pct) {
if (pct < 50) return 'hsl(152, 69%, 43%)';
if (pct < 80) return 'hsl(38, 92%, 50%)';
return 'hsl(4, 86%, 58%)';
}
function buildCard(id, label, used, allowed) {
const pct = allowed > 0 ? Math.min(100, Math.round((used / allowed) * 100)) : 0;
const prorated = getProratedEntitlement(allowed);
const color = getBarColor(pct);
const { curMonthName, remaining, isDecember } = getYearEndRemaining();
const excess = used - prorated;
const showWarning = excess > 0 && !isDecember;
return `
<div class="pr-card" id="prorata-card-${id}">
<div class="pr-card-top">
<span class="pr-label">${label}</span>
<span class="pr-count">${used} <span>/ ${allowed} days</span></span>
</div>
<div class="pr-bar-track">
<div class="pr-bar-fill" id="prorata-bar-${id}" style="background:${color};"></div>
</div>
${showWarning
? `<div class="pr-chip" id="prorata-chip-${id}">&#x26A0;&#xFE0F; ${curMonthName} &middot; +${excess} day${excess>1?'s':''} liable &middot; ${remaining}</div>`
: `<div class="pr-chip pr-chip-ok" id="prorata-chip-${id}">&#x2713; Within prorata &middot; ${curMonthName} &middot; ${remaining}</div>`
}
</div>`;
}
async function getAuthToken() {
const tokenKeys = ['token', 'accessToken', 'access_token', 'authToken', 'auth_token', 'jwt', 'jwtToken'];
for (const tk of tokenKeys) {
const tv = localStorage.getItem(tk) || sessionStorage.getItem(tk);
if (tv && tv.length > 20) return tv.replace(/^Bearer\s+/i, '');
}
return null;
}
async function loadProrata() {
loadingEl.style.display = 'flex';
errorEl.style.display = 'none';
bodyEl.style.display = 'none';
bodyEl.innerHTML = '';
setCurrentDate();
try {
const token = await getAuthToken();
const headers = { 'Content-Type': 'application/json' };
if (token) headers['Authorization'] = 'Bearer ' + token;
const resp = await fetch(PROFILE_API, { headers, credentials: 'include' });
if (!resp.ok) throw new Error('API error ' + resp.status);
const data = await resp.json();
const leaves = data.leaves || {};
const casual = leaves.casual || { allowed: 0, used: 0 };
const sick = leaves.sick || { allowed: 0, used: 0 };
const annual = leaves.annual || { allowed: 0, used: 0 };
const showAnnual = annual.allowed && annual.allowed > 0;
// Build cards HTML
let html = '';
if (sick.allowed > 0) html += buildCard('sick', 'Sick Leave', sick.used, sick.allowed);
if (casual.allowed > 0) html += buildCard('casual', 'Casual Leave', casual.used, casual.allowed);
if (showAnnual) html += buildCard('annual', 'Annual Leave', annual.used, annual.allowed);
if (!html) {
html = '<div style="text-align:center; padding:24px; color:hsl(215,16%,47%); font-size:13px;">No leave data available.</div>';
}
loadingEl.style.display = 'none';
bodyEl.innerHTML = html;
bodyEl.style.display = 'flex';
// Staggered card entrance + bar animation
const cards = bodyEl.querySelectorAll('.pr-card');
cards.forEach((card, i) => {
setTimeout(() => {
card.classList.add('visible');
const barId = card.id.replace('prorata-card-', 'prorata-bar-');
const fill = document.getElementById(barId);
if (fill) {
// Extract target width from data attribute via pct calculation
const label = card.querySelector('.pr-label').textContent;
let used = 0, allowed = 1;
if (label.includes('Casual')) { used = casual.used; allowed = casual.allowed; }
else if (label.includes('Sick')) { used = sick.used; allowed = sick.allowed; }
else if (label.includes('Annual')) { used = annual.used; allowed = annual.allowed; }
const pct = Math.min(100, Math.round((used / allowed) * 100));
setTimeout(() => { fill.style.width = pct + '%'; }, 80);
}
}, 120 * i);
});
} catch (e) {
loadingEl.style.display = 'none';
errorEl.style.display = 'flex';
}
}
function openProrataDashboard() {
overlay.classList.add('show');
loadProrata();
}
document.getElementById('opt-prorata-dashboard').addEventListener('click', () => {
document.getElementById('chat-options-menu').style.display = 'none';
openProrataDashboard();
});
document.getElementById('prorata-close-btn').addEventListener('click', () => {
overlay.classList.remove('show');
});
// Info panel is always open, no click listener needed
document.getElementById('prorata-retry-btn').addEventListener('click', loadProrata);
})();
// --- Dropdown & Export Logic ---
const optionsBtn = document.getElementById('chat-options-btn');
const optionsMenu = document.getElementById('chat-options-menu');
const optExport = document.getElementById('opt-export-chat');
const optSound = document.getElementById('opt-toggle-sound');
const switchBg = document.getElementById('opt-sound-switch');
const switchKnob = document.getElementById('opt-sound-knob');
optSound.addEventListener('click', (e) => {
e.stopPropagation();
playSounds = !playSounds;
if (playSounds) {
switchBg.style.backgroundColor = '#10b981';
switchKnob.style.transform = 'translateX(14px)';
} else {
switchBg.style.backgroundColor = '#cbd5e1';
switchKnob.style.transform = 'translateX(0)';
}
});
optionsBtn.addEventListener('click', (e) => {
e.stopPropagation();
optionsMenu.style.display = optionsMenu.style.display === 'none' ? 'block' : 'none';
});
document.addEventListener('click', () => {
if (optionsMenu.style.display === 'block') {
optionsMenu.style.display = 'none';
}
});
optExport.addEventListener('click', () => {
optionsMenu.style.display = 'none';
if (chatHistory.length === 0) {
alert("No chat history to export yet.");
return;
}
// CSV Generator
let content = 'Role,Message\n';
chatHistory.forEach(msg => {
const role = msg.role;
let text = msg.content || '';
// Escape quotes for CSV
text = text.replace(/"/g, '""');
// Wrap in quotes if it contains comma, quote, or newline
if (text.search(/("|,|\n)/g) >= 0) {
text = `"${text}"`;
}
content += `${role},${text}\n`;
});
const blob = new Blob([content], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `chat_export_${sessionId.slice(-6)}.csv`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
});
})();
</script>
</body>
</html>