Spaces:
Running
Running
| <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">×</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;">×</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">☆</span><span data-val="2">☆</span><span data-val="3">☆</span><span data-val="4">☆</span><span data-val="5">☆</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 | |
| & 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;"> | |
| 📊 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">✕</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…</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 ? '★' : (sv <= _selectedRating ? '★' : '☆'); | |
| }); | |
| }); | |
| s.addEventListener('mouseleave', () => { | |
| stars.forEach(st => { | |
| st.classList.remove('hovered'); | |
| const sv = parseInt(st.dataset.val); | |
| st.innerHTML = sv <= _selectedRating ? '★' : '☆'; | |
| }); | |
| }); | |
| // 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 ? '★' : '☆'; | |
| }); | |
| // 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 = '☆'; 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}">⚠️ ${curMonthName} · +${excess} day${excess>1?'s':''} liable · ${remaining}</div>` | |
| : `<div class="pr-chip pr-chip-ok" id="prorata-chip-${id}">✓ Within prorata · ${curMonthName} · ${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> | |