| <!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> |
| |
| @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; |
| } |
| |
| |
| #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); |
| } |
| |
| |
| #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; |
| } |
| |
| .thumbs-rating { |
| display: flex; |
| gap: 16px; |
| margin: 16px 0; |
| } |
| |
| .thumb-btn { |
| background: transparent; |
| border: 2px solid #e2e8f0; |
| border-radius: 50%; |
| width: 50px; |
| height: 50px; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| cursor: pointer; |
| color: #64748b; |
| transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); |
| outline: none; |
| } |
| |
| .thumb-btn:hover { |
| border-color: #cbd5e1; |
| background: #f8fafc; |
| color: #334155; |
| transform: scale(1.05); |
| } |
| |
| .thumb-btn.active[data-val="up"] { |
| border-color: #10b981; |
| background: #ecfdf5; |
| color: #10b981; |
| } |
| |
| .thumb-btn.active[data-val="down"] { |
| border-color: #ef4444; |
| background: #fef2f2; |
| color: #ef4444; |
| } |
| |
| |
| #chat-messages { |
| flex: 1; |
| overflow-y: auto; |
| padding: 16px; |
| display: flex; |
| flex-direction: column; |
| gap: 12px; |
| background: #f8fafc; |
| scroll-behavior: smooth; |
| } |
| |
| |
| #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); |
| } |
| |
| |
| .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; |
| } |
| |
| |
| .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-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> |
|
|
| |
| <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"> |
| |
| <div id="dismiss-widget" title="Dismiss widget">×</div> |
| |
| <div id="chat-icon" |
| style="width: 233px; height: 238px; display: flex; align-items: center; justify-content: center; pointer-events: none;"> |
| <dotlottie-player src="https://joedown11-chatrag.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; 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); outline:none;" |
| onmouseover="this.style.background='#e2e8f0'; this.style.color='#0f172a'" |
| onmouseout="this.style.background='transparent'; this.style.color='#334155'"> |
| <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
| <line x1="18" y1="6" x2="6" y2="18"></line> |
| <line x1="6" y1="6" x2="18" y2="18"></line> |
| </svg> |
| </button> |
| </div> |
| </div> |
|
|
| |
| <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"> |
| |
| <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="thumbs-rating" id="rating-thumbs"> |
| <button data-val="up" class="thumb-btn" title="Thumbs Up"> |
| <svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"></path></svg> |
| </button> |
| <button data-val="down" class="thumb-btn" title="Thumbs Down"> |
| <svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h3a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-3"></path></svg> |
| </button> |
| </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> |
|
|
| |
| <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 () { |
| |
| 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'); |
| |
| |
| if (dismissWidgetBtn) { |
| dismissWidgetBtn.addEventListener('click', function (e) { |
| e.stopPropagation(); |
| fab.style.display = 'none'; |
| }); |
| } |
| |
| |
| function openChat() { |
| box.style.display = 'flex'; |
| setTimeout(() => { box.classList.add('is-visible'); inputField.focus(); }, 10); |
| fab.style.display = 'none'; |
| } |
| |
| function closeChat() { |
| box.classList.remove('is-visible'); |
| setTimeout(() => { box.style.display = 'none'; }, 300); |
| fab.style.display = 'flex'; |
| } |
| |
| fab.addEventListener('click', openChat); |
| |
| if (minimizeBtn) { |
| minimizeBtn.addEventListener('click', function (e) { |
| e.stopPropagation(); |
| closeChat(); |
| }); |
| } |
| |
| |
| const API_URL = "https://joedown11-chatrag.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; |
| |
| |
| 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; |
| let _isWaiting = false; |
| |
| |
| 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 { |
| |
| if (!_isWaiting) { |
| inputField.disabled = false; |
| inputField.placeholder = "Type your question here..."; |
| sendButton.disabled = false; |
| } |
| _wasQueued = false; |
| |
| document.getElementById('queue-actions-area').style.display = 'none'; |
| stopQueueTimer(); |
| |
| |
| 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; |
| } |
| |
| |
| 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; |
| |
| 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')}`; |
| } |
| } |
| |
| |
| let sessionId = localStorage.getItem('martech_session_id'); |
| if (!sessionId) { |
| |
| sessionId = 'user-' + Date.now() + '-' + Math.random().toString(36).slice(2, 9); |
| localStorage.setItem('martech_session_id', sessionId); |
| } |
| |
| const _isGenericId = (id) => id && id.startsWith('user-') && /^user-\d+-[a-z0-9]+$/.test(id); |
| |
| function formatMessage(text) { |
| if (!text) return ""; |
| |
| let formatted = text.replace(/\*\*(.*?)\*\*/g, '<b>$1</b>'); |
| |
| formatted = formatted.replace(/^\* /gm, '• '); |
| |
| 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; |
| } |
| |
| |
| async function _resolveUserName() { |
| |
| |
| if (window.HR_USER_NAME && typeof window.HR_USER_NAME === 'string' && window.HR_USER_NAME.trim().length > 1) { |
| return window.HR_USER_NAME.trim(); |
| } |
| |
| |
| 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); |
| |
| 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) { |
| |
| if (typeof stored === 'string' && stored.length < 80 && !stored.includes('{')) { |
| return stored.trim(); |
| } |
| } |
| } |
| } |
| } catch (e) { } |
| |
| |
| |
| 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' |
| }); |
| if (pResp.ok) { |
| const pData = await pResp.json(); |
| |
| const found = pData.name || (pData.personalDetails && pData.personalDetails.fullName); |
| if (found && found.trim().length > 1) return found.trim(); |
| } |
| } catch (e) { } |
| |
| return null; |
| } |
| |
| |
| async function initUserSession() { |
| |
| |
| |
| const needsResolution = _isGenericId(sessionId); |
| |
| let userName = await _resolveUserName(); |
| |
| |
| 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); |
| } |
| |
| |
| |
| |
| 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()); |
| |
| if (initData.session_id && initData.session_id !== sessionId) { |
| sessionId = initData.session_id; |
| localStorage.setItem('martech_session_id', sessionId); |
| } |
| } catch (e) { |
| |
| 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 = hData.takeover_by ? ('Live Agent (' + hData.takeover_by + ')') : 'Live Agent'; |
| } 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 (!userName) { |
| let _isResolving = false; |
| |
| const _attemptSync = async () => { |
| |
| if (!_isGenericId(sessionId) || _isResolving) return; |
| |
| _isResolving = true; |
| const resolved = await _resolveUserName(); |
| if (resolved) { |
| localStorage.setItem('martech_last_user_name', resolved); |
| try { |
| |
| 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; |
| }; |
| |
| |
| window.addEventListener('storage', _attemptSync); |
| |
| |
| window.addEventListener('focus', _attemptSync); |
| |
| |
| setInterval(_attemptSync, 15000); |
| } |
| |
| |
| 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'; |
| const agentLabel = data.takeover_by ? ('Live Agent (' + data.takeover_by + ')') : 'Live Agent'; |
| document.getElementById('chat-header-title').textContent = agentLabel; |
| } 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) { |
| |
| 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; |
| } |
| |
| |
| 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); |
| |
| |
| const tokens = fullHtml.split(/(<[^>]*>)/); |
| let currentHtml = ""; |
| |
| for (const token of tokens) { |
| if (token.startsWith("<")) { |
| |
| currentHtml += token; |
| element.innerHTML = currentHtml; |
| } else { |
| |
| 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; |
| |
| |
| if (_humanTypingEl) { |
| _humanTypingEl.remove(); |
| _humanTypingEl = null; |
| } |
| |
| |
| 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 { |
| |
| 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()) { |
| |
| const PRORATA_TAG_RE = /<\s*OPEN_PRORATA_DASHBOARD\s*>/gi; |
| const hasProrataTag = PRORATA_TAG_RE.test(reply); |
| |
| const cleanReply = reply.replace(/<\s*OPEN_PRORATA_DASHBOARD\s*>/gi, '').trim(); |
| |
| typingEl.remove(); |
| const botEl = appendMessage('bot', ''); |
| chatHistory.push({ role: "user", content: text }); |
| chatHistory.push({ role: "assistant", content: cleanReply }); |
| if (playSounds) receiveSound.play().catch(e => console.log('Audio error:', e)); |
| await typeMessageWithTypos(botEl, cleanReply); |
| |
| |
| if (hasProrataTag && typeof openProrataDashboard === 'function') { |
| const btnWrapper = document.createElement('div'); |
| btnWrapper.style.cssText = 'display:flex; justify-content:flex-start; padding:4px 12px 10px 12px;'; |
| const prBtn = document.createElement('button'); |
| prBtn.id = 'chat-open-prorata-btn'; |
| prBtn.innerHTML = '📊 View Leave Summary'; |
| prBtn.style.cssText = [ |
| 'background: linear-gradient(135deg, hsl(152,69%,43%), hsl(152,69%,36%))', |
| 'color: #fff', |
| 'border: none', |
| 'padding: 8px 16px', |
| 'border-radius: 20px', |
| 'font-size: 13px', |
| 'font-weight: 600', |
| 'cursor: pointer', |
| 'font-family: inherit', |
| 'box-shadow: 0 2px 8px rgba(16,185,129,0.25)', |
| 'transition: transform 0.15s, box-shadow 0.15s', |
| 'display: flex', |
| 'align-items: center', |
| 'gap: 6px' |
| ].join(';'); |
| prBtn.onmouseover = () => { |
| prBtn.style.transform = 'translateY(-1px)'; |
| prBtn.style.boxShadow = '0 4px 12px rgba(16,185,129,0.35)'; |
| }; |
| prBtn.onmouseout = () => { |
| prBtn.style.transform = 'translateY(0)'; |
| prBtn.style.boxShadow = '0 2px 8px rgba(16,185,129,0.25)'; |
| }; |
| prBtn.addEventListener('click', () => { |
| openProrataDashboard(); |
| }); |
| btnWrapper.appendChild(prBtn); |
| messagesContainer.appendChild(btnWrapper); |
| messagesContainer.scrollTop = messagesContainer.scrollHeight; |
| } |
| } else { |
| |
| |
| 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; |
| 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(); |
| } |
| } |
| |
| |
| 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 }) |
| }); |
| |
| |
| pollMessages(); |
| } catch (e) { } |
| }); |
| |
| |
| const ratingOverlay = document.getElementById('rating-overlay'); |
| const thumbBtns = document.querySelectorAll('.thumb-btn'); |
| const submitRatingBtn = document.getElementById('btn-submit-rating'); |
| let _selectedRating = null; |
| |
| thumbBtns.forEach(btn => { |
| btn.addEventListener('click', () => { |
| _selectedRating = btn.dataset.val === 'up' ? 5 : 1; |
| |
| thumbBtns.forEach(b => b.classList.remove('active')); |
| btn.classList.add('active'); |
| |
| |
| submitRatingBtn.disabled = false; |
| submitRatingBtn.style.opacity = '1'; |
| submitRatingBtn.style.cursor = 'pointer'; |
| }); |
| }); |
| |
| submitRatingBtn.addEventListener('click', async () => { |
| |
| if (_selectedRating === null) return; |
| |
| 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) { } |
| |
| |
| try { |
| await fetch(BASE_URL + "/api/chat/reset", { |
| method: 'POST', headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ session_id: sessionId }) |
| }); |
| } catch (e) { } |
| |
| |
| _selectedRating = null; |
| thumbBtns.forEach(b => b.classList.remove('active')); |
| 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(); |
| |
| |
| setTimeout(() => { |
| initUserSession(); |
| }, 500); |
| }); |
| |
| |
| |
| var openProrataDashboard = null; |
| (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' }); |
| } |
| |
| |
| function getElapsedMonths() { |
| const now = new Date(); |
| return Math.max(1, now.getMonth() + 1); |
| } |
| |
| function getProratedEntitlement(allowed) { |
| return Math.floor((getElapsedMonths() / 12) * allowed); |
| } |
| |
| |
| function getYearEndRemaining() { |
| const now = new Date(); |
| const yearEnd = new Date(now.getFullYear(), 11, 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; |
| |
| |
| 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'; |
| |
| |
| 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) { |
| |
| 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'; |
| } |
| } |
| |
| openProrataDashboard = 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'); |
| }); |
| |
| |
| |
| document.getElementById('prorata-retry-btn').addEventListener('click', loadProrata); |
| })(); |
| |
| |
| 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; |
| } |
| |
| |
| let content = 'Role,Message\n'; |
| chatHistory.forEach(msg => { |
| const role = msg.role; |
| let text = msg.content || ''; |
| |
| text = text.replace(/"/g, '""'); |
| |
| 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> |
|
|