clinic-connect-widget 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -104,7 +104,7 @@ const CONFIG = {
104
104
  // LiveKit Server URL
105
105
  LIVEKIT_URL: "wss://livekit.longvan.vn"
106
106
  };
107
- const styles = ':host{--td-primary: #0e65f1;--td-primary-hover: #0b50c0;--td-bg-light: #ffffff;--td-bg-dark: #1a202c;--td-text-main: #0d131c;--td-text-sub: #49699c;--td-border: #e7ecf4;--td-success: #22c55e;--td-danger: #ef4444;--font-family: "Inter", system-ui, -apple-system, sans-serif;--z-index: 9999;font-family:var(--font-family);color:var(--td-text-main)}*{box-sizing:border-box;margin:0;padding:0}.hidden{display:none!important}.material-symbols-outlined{font-family:Material Symbols Outlined;font-weight:400;font-style:normal;font-size:24px;line-height:1;letter-spacing:normal;text-transform:none;display:inline-block;white-space:nowrap;word-wrap:normal;direction:ltr}.td-widget-fab{position:fixed;bottom:24px;right:24px;display:flex;align-items:center;gap:12px;z-index:var(--z-index);cursor:pointer;font-family:var(--font-family)}.td-fab-card{background:var(--td-bg-light);border:1px solid var(--td-border);border-radius:12px;box-shadow:0 8px 30px #0000001f;display:flex;align-items:center;padding:6px 16px 6px 6px;transition:transform .2s ease,box-shadow .2s ease;max-width:320px;position:relative;overflow:hidden}.td-fab-card:hover{transform:translateY(-4px);box-shadow:0 20px 40px #00000026}.td-avatar-container{width:64px;height:64px;border-radius:8px;overflow:hidden;position:relative;flex-shrink:0}.td-avatar{width:100%;height:100%;background-size:cover;background-position:center}.td-status-dot{position:absolute;bottom:4px;right:4px;width:14px;height:14px;background:var(--td-success);border:2px solid #fff;border-radius:50%}.td-info{margin-left:12px;display:flex;flex-direction:column}.td-brand{display:flex;align-items:center;gap:4px;font-size:10px;font-weight:700;text-transform:uppercase;color:var(--td-primary);letter-spacing:.05em}.td-cta{font-size:15px;font-weight:700;color:var(--td-text-main);margin-top:2px}.td-sub{font-size:12px;color:var(--td-text-sub);margin-top:2px}.td-close-fab{position:absolute;top:4px;right:4px;opacity:0;cursor:pointer;background:none;border:none;color:#94a3b8;transition:opacity .2s;padding:4px}.td-fab-card:hover .td-close-fab{opacity:1}.td-widget-expanded{position:fixed;bottom:100px;right:24px;width:380px;max-height:80vh;background:var(--td-bg-light);border-radius:12px;box-shadow:0 25px 50px -12px #00000040;border:1px solid var(--td-border);display:flex;flex-direction:column;overflow:hidden;z-index:var(--z-index);animation:slideUp .3s cubic-bezier(.16,1,.3,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.td-header{display:flex;align-items:center;justify-content:space-between;padding:16px;border-bottom:1px solid var(--td-border)}.td-header-title{display:flex;align-items:center;gap:12px;font-weight:700;font-size:18px}.td-icon-box{width:32px;height:32px;background:#0e65f11a;color:var(--td-primary);border-radius:8px;display:flex;align-items:center;justify-content:center}.td-close-btn{background:transparent;border:none;cursor:pointer;color:#64748b;padding:4px;border-radius:50%;display:flex;align-items:center;justify-content:center}.td-close-btn:hover{background:#f1f5f9}.td-content{flex:1;overflow-y:auto;padding:0}.td-doctor-profile{display:flex;flex-direction:column;align-items:center;padding:32px 24px 24px}.td-online-badge{display:inline-flex;align-items:center;gap:6px;padding:4px 12px;background:#f0fdf4;border:1px solid #dcfce7;border-radius:999px}.td-pulse{width:8px;height:8px;background:var(--td-success);border-radius:50%}.td-badge-text{font-size:12px;font-weight:600;color:#15803d;text-transform:uppercase}.td-trust-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px;padding:0 24px 24px}.td-trust-item{background:#f8f9fc;border:1px solid var(--td-border);border-radius:12px;padding:12px;text-align:center;display:flex;flex-direction:column;align-items:center;gap:8px}.td-trust-title{font-size:12px;font-weight:700}.td-actions{padding:0 24px 24px;display:flex;flex-direction:column;gap:12px}.td-btn{width:100%;height:48px;border-radius:8px;font-size:16px;font-weight:700;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:all .2s;border:none}.td-btn-primary{background:var(--td-primary);color:#fff;box-shadow:0 10px 15px -3px #0e65f133}.td-btn-primary:hover{background:var(--td-primary-hover)}.td-btn-outline{background:transparent;border:1px solid var(--td-border);color:var(--td-text-main);font-size:14px}.td-btn-outline:hover{background:#f8f9fc}.td-footer{padding:12px;text-align:center;background:#f8f9fc;border-top:1px solid var(--td-border);font-size:12px;color:var(--td-text-sub)}.td-consultation-modal{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:90vw;height:90vh;max-width:1200px;max-height:800px;background:var(--td-bg-light);border-radius:16px;box-shadow:0 25px 50px -12px #00000080;border:1px solid var(--td-border);display:flex;overflow:hidden;z-index:var(--z-index);animation:fadeIn .3s ease-out}@keyframes fadeIn{0%{opacity:0;transform:translate(-50%,-48%)}to{opacity:1;transform:translate(-50%,-50%)}}.td-video-panel{flex:1;background:#000;position:relative;display:flex;align-items:center;justify-content:center;overflow:hidden}.td-main-video{width:100%;height:100%;background-size:cover;background-position:center}.td-overlay{position:absolute;top:0;right:0;bottom:0;left:0;background:linear-gradient(to bottom,rgba(0,0,0,.3),transparent,rgba(0,0,0,.6))}.td-video-header{position:absolute;top:16px;left:16px;right:16px;display:flex;justify-content:space-between;z-index:10}.td-badge-glass{background:#0006;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);padding:6px 12px;border-radius:20px;display:flex;align-items:center;gap:8px;color:#fff;font-size:12px;font-weight:500}.td-pip{position:absolute;top:24px;right:24px;width:160px;aspect-ratio:16/9;background:#333;border-radius:8px;border:2px solid rgba(255,255,255,.2);overflow:hidden;z-index:20;background-size:cover;background-position:center;box-shadow:0 4px 6px #0000004d}.td-pip-label{position:absolute;bottom:4px;left:4px;background:#0009;color:#fff;font-size:10px;padding:2px 6px;border-radius:4px}.td-controls{position:absolute;bottom:32px;left:50%;transform:translate(-50%);display:flex;align-items:center;gap:16px;z-index:20}.td-ctrl-btn{width:48px;height:48px;border-radius:50%;background:#fff3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);border:none;color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s}.td-ctrl-btn:hover{background:#fff;color:var(--td-primary)}.td-ctrl-end{height:48px;padding:0 24px;border-radius:24px;background:var(--td-danger);color:#fff;display:flex;align-items:center;gap:8px;font-weight:600;border:none;cursor:pointer}.td-ctrl-end:hover{background:#dc2626}.td-chat-panel{width:400px;background:var(--td-bg-light);border-left:1px solid var(--td-border);display:flex;flex-direction:column}.td-chat-header{padding:16px;border-bottom:1px solid var(--td-border)}.td-chat-doc-info{display:flex;gap:12px;align-items:center;margin-bottom:12px}.td-chat-avatar{width:48px;height:48px;border-radius:50%;background-size:cover;background-position:center}.td-chat-timer{background:#f8f9fc;padding:8px 12px;border-radius:8px;display:flex;justify-content:space-between;align-items:center}.td-timer-val{font-family:monospace;font-weight:700;font-size:16px}.td-chat-messages{flex:1;overflow-y:auto;padding:16px;background:#f8f9fc;display:flex;flex-direction:column;gap:16px}.td-msg-system{text-align:center;font-size:12px;color:var(--td-text-sub);background:#e2e8f0;padding:4px 12px;border-radius:12px;align-self:center;max-width:90%}.td-msg-row{display:flex;gap:8px}.td-msg-row.own{flex-direction:row-reverse}.td-msg-bubble{padding:10px 14px;border-radius:12px;font-size:14px;line-height:1.5;max-width:85%}.td-msg-row.doctor .td-msg-bubble{background:#fff;border:1px solid var(--td-border);border-top-left-radius:2px}.td-msg-row.own .td-msg-bubble{background:#e0f2fe;color:var(--td-text-main);border:1px solid #bae6fd;border-top-right-radius:2px}.td-msg-time{font-size:10px;color:var(--td-text-sub);margin-top:2px}.td-chat-input-area{padding:16px;border-top:1px solid var(--td-border);background:var(--td-bg-light)}.td-input-wrapper{display:flex;gap:8px;align-items:flex-end}.td-chat-input{flex:1;border:1px solid var(--td-border);border-radius:8px;padding:10px;font-family:inherit;font-size:14px;resize:none;height:48px}.td-send-btn{width:48px;height:48px;background:var(--td-primary);color:#fff;border:none;border-radius:8px;cursor:pointer;display:flex;align-items:center;justify-content:center}.td-send-btn:hover{background:var(--td-primary-hover)}@media (max-width: 768px){.td-consultation-modal{width:100vw;height:100vh;border-radius:0;flex-direction:column}.td-video-panel{height:40vh}.td-chat-panel{width:100%;flex:1}}.td-form-container{display:flex;flex-direction:column;height:100%;padding:0 24px}.td-form-page-header{padding:24px 0 16px}.td-form-title{font-size:24px;font-weight:700;color:var(--td-text-main);margin-bottom:8px}.td-form-desc{font-size:14px;color:var(--td-text-sub);line-height:normal}.td-form{display:flex;flex-direction:column;gap:20px;padding-bottom:24px}.td-input-group{display:flex;flex-direction:column;gap:8px}.td-label{font-size:14px;font-weight:500;color:var(--td-text-main)}.td-input{width:100%;height:48px;padding:0 16px;border-radius:8px;border:1px solid var(--td-border);background:#f8f9fc;font-family:inherit;font-size:16px;color:var(--td-text-main);transition:all .2s}.td-input:focus{outline:none;border-color:var(--td-primary);box-shadow:0 0 0 2px #0e65f133}.td-textarea{width:100%;min-height:100px;padding:16px;border-radius:8px;border:1px solid var(--td-border);background:#f8f9fc;font-family:inherit;font-size:16px;color:var(--td-text-main);resize:none}.td-textarea:focus{outline:none;border-color:var(--td-primary)}.td-chips-group{display:flex;flex-wrap:wrap;gap:8px}.td-chip{padding:8px 16px;border-radius:99px;font-size:14px;font-weight:500;cursor:pointer;border:1px solid var(--td-border);background:#fff;color:var(--td-text-sub);transition:all .2s}.td-chip:hover{border-color:var(--td-primary);color:var(--td-primary);background:#0e65f10d}.td-chip.active{border-color:var(--td-primary);background:#0e65f11a;color:var(--td-primary);display:flex;align-items:center;gap:6px}.td-secure-note{display:flex;align-items:center;justify-content:center;gap:6px;font-size:12px;color:var(--td-text-sub);opacity:.8;margin-top:8px}.td-summary-container{display:flex;flex-direction:column;height:100%}.td-success-header{display:flex;flex-direction:column;align-items:center;padding:32px 24px;text-align:center}.td-success-icon{width:80px;height:80px;background:#0e65f11a;color:var(--td-primary);border-radius:50%;display:flex;align-items:center;justify-content:center;margin-bottom:16px}.td-summary-title{font-size:24px;font-weight:700;margin-bottom:8px}.td-summary-desc{font-size:14px;color:var(--td-text-sub);max-width:360px;margin:0 auto}.td-summary-card{margin:0 24px;padding:16px;border-radius:12px;border:1px solid var(--td-border);background:#f8f9fc;display:flex;align-items:center;gap:16px}.td-summary-details{padding:8px 24px}.td-detail-row{display:flex;justify-content:space-between;padding:12px 0;border-bottom:1px dashed var(--td-border);font-size:14px}.td-note-box{margin:16px 24px;padding:16px;background:#0e65f10d;border:1px solid rgba(14,101,241,.1);border-radius:8px}.td-note-title{font-size:14px;font-weight:700;margin-bottom:8px;display:flex;align-items:center;gap:8px}.td-grid-actions{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:12px}';
107
+ const styles = ':host{--td-primary: #0e65f1;--td-primary-hover: #0b50c0;--td-bg-light: #ffffff;--td-bg-dark: #1a202c;--td-text-main: #0d131c;--td-text-sub: #49699c;--td-border: #e7ecf4;--td-success: #22c55e;--td-danger: #ef4444;--font-family: "Inter", system-ui, -apple-system, sans-serif;--z-index: 9999;font-family:var(--font-family);color:var(--td-text-main)}*{box-sizing:border-box;margin:0;padding:0}.hidden{display:none!important}.material-symbols-outlined{font-family:Material Symbols Outlined;font-weight:400;font-style:normal;font-size:24px;line-height:1;letter-spacing:normal;text-transform:none;display:inline-block;white-space:nowrap;word-wrap:normal;direction:ltr}.td-widget-fab{position:fixed;bottom:24px;right:24px;display:flex;align-items:center;gap:12px;z-index:var(--z-index);cursor:pointer;font-family:var(--font-family)}.td-fab-card{background:var(--td-bg-light);border:1px solid var(--td-border);border-radius:12px;box-shadow:0 8px 30px #0000001f;display:flex;align-items:center;padding:6px 16px 6px 6px;transition:transform .2s ease,box-shadow .2s ease;max-width:320px;position:relative;overflow:hidden}.td-fab-card:hover{transform:translateY(-4px);box-shadow:0 20px 40px #00000026}.td-avatar-container{width:64px;height:64px;border-radius:8px;overflow:hidden;position:relative;flex-shrink:0}.td-avatar{width:100%;height:100%;background-size:cover;background-position:center}.td-status-dot{position:absolute;bottom:4px;right:4px;width:14px;height:14px;background:var(--td-success);border:2px solid #fff;border-radius:50%}.td-info{margin-left:12px;display:flex;flex-direction:column}.td-brand{display:flex;align-items:center;gap:4px;font-size:10px;font-weight:700;text-transform:uppercase;color:var(--td-primary);letter-spacing:.05em}.td-cta{font-size:15px;font-weight:700;color:var(--td-text-main);margin-top:2px}.td-sub{font-size:12px;color:var(--td-text-sub);margin-top:2px}.td-close-fab{position:absolute;top:4px;right:4px;opacity:0;cursor:pointer;background:none;border:none;color:#94a3b8;transition:opacity .2s;padding:4px}.td-fab-card:hover .td-close-fab{opacity:1}.td-widget-expanded{position:fixed;bottom:100px;right:24px;width:380px;max-height:80vh;background:var(--td-bg-light);border-radius:12px;box-shadow:0 25px 50px -12px #00000040;border:1px solid var(--td-border);display:flex;flex-direction:column;overflow:hidden;z-index:var(--z-index);animation:slideUp .3s cubic-bezier(.16,1,.3,1)}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.td-header{display:flex;align-items:center;justify-content:space-between;padding:16px;border-bottom:1px solid var(--td-border)}.td-header-title{display:flex;align-items:center;gap:12px;font-weight:700;font-size:18px}.td-icon-box{width:32px;height:32px;background:#0e65f11a;color:var(--td-primary);border-radius:8px;display:flex;align-items:center;justify-content:center}.td-close-btn{background:transparent;border:none;cursor:pointer;color:#64748b;padding:4px;border-radius:50%;display:flex;align-items:center;justify-content:center}.td-close-btn:hover{background:#f1f5f9}.td-content{flex:1;overflow-y:auto;padding:0}.td-doctor-profile{display:flex;flex-direction:column;align-items:center;padding:32px 24px 24px}.td-online-badge{display:inline-flex;align-items:center;gap:6px;padding:4px 12px;background:#f0fdf4;border:1px solid #dcfce7;border-radius:999px}.td-pulse{width:8px;height:8px;background:var(--td-success);border-radius:50%}.td-badge-text{font-size:12px;font-weight:600;color:#15803d;text-transform:uppercase}.td-online-badge.td-offline{background:#f3f4f6;border-color:#e5e7eb}.td-online-badge.td-offline .td-badge-text{color:#6b7280}.td-online-badge.td-offline .td-pulse{background:#9ca3af;animation:none}.td-trust-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px;padding:0 24px 24px}.td-trust-item{background:#f8f9fc;border:1px solid var(--td-border);border-radius:12px;padding:12px;text-align:center;display:flex;flex-direction:column;align-items:center;gap:8px}.td-trust-title{font-size:12px;font-weight:700}.td-actions{padding:0 24px 24px;display:flex;flex-direction:column;gap:12px}.td-btn{width:100%;height:48px;border-radius:8px;font-size:16px;font-weight:700;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:all .2s;border:none}.td-btn-primary{background:var(--td-primary);color:#fff;box-shadow:0 10px 15px -3px #0e65f133}.td-btn-primary:hover{background:var(--td-primary-hover)}.td-btn-outline{background:transparent;border:1px solid var(--td-border);color:var(--td-text-main);font-size:14px}.td-btn-outline:hover{background:#f8f9fc}.td-footer{padding:12px;text-align:center;background:#f8f9fc;border-top:1px solid var(--td-border);font-size:12px;color:var(--td-text-sub)}.td-consultation-modal{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:90vw;height:90vh;max-width:1200px;max-height:800px;background:var(--td-bg-light);border-radius:16px;box-shadow:0 25px 50px -12px #00000080;border:1px solid var(--td-border);display:flex;overflow:hidden;z-index:var(--z-index);animation:fadeIn .3s ease-out}@keyframes fadeIn{0%{opacity:0;transform:translate(-50%,-48%)}to{opacity:1;transform:translate(-50%,-50%)}}.td-video-panel{flex:1;background:#000;position:relative;display:flex;align-items:center;justify-content:center;overflow:hidden}.td-main-video{width:100%;height:100%;background-size:cover;background-position:center}.td-overlay{position:absolute;top:0;right:0;bottom:0;left:0;background:linear-gradient(to bottom,rgba(0,0,0,.3),transparent,rgba(0,0,0,.6))}.td-video-header{position:absolute;top:16px;left:16px;right:16px;display:flex;justify-content:space-between;z-index:10}.td-badge-glass{background:#0006;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);padding:6px 12px;border-radius:20px;display:flex;align-items:center;gap:8px;color:#fff;font-size:12px;font-weight:500}.td-pip{position:absolute;top:24px;right:24px;width:160px;aspect-ratio:16/9;background:#333;border-radius:8px;border:2px solid rgba(255,255,255,.2);overflow:hidden;z-index:20;background-size:cover;background-position:center;box-shadow:0 4px 6px #0000004d}.td-pip-label{position:absolute;bottom:4px;left:4px;background:#0009;color:#fff;font-size:10px;padding:2px 6px;border-radius:4px}.td-controls{position:absolute;bottom:32px;left:50%;transform:translate(-50%);display:flex;align-items:center;gap:16px;z-index:20}.td-ctrl-btn{width:48px;height:48px;border-radius:50%;background:#fff3;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);border:none;color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s}.td-ctrl-btn:hover{background:#fff;color:var(--td-primary)}.td-ctrl-end{height:48px;padding:0 24px;border-radius:24px;background:var(--td-danger);color:#fff;display:flex;align-items:center;gap:8px;font-weight:600;border:none;cursor:pointer}.td-ctrl-end:hover{background:#dc2626}@media (max-width: 768px){.td-consultation-modal{width:100vw;height:100vh;border-radius:0;flex-direction:column}.td-video-panel{height:40vh}.td-chat-panel{width:100%;flex:1}}.td-overlay-wrapper{position:fixed;top:0;right:0;bottom:0;left:0;background:#0006;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);z-index:var(--z-index);display:flex;align-items:center;justify-content:center;animation:fadeInOpacity .3s ease-out}.td-modal-centered{background:var(--td-bg-light);width:90%;max-width:480px;border-radius:16px;box-shadow:0 25px 50px -12px #00000040;border:1px solid var(--td-border);display:flex;flex-direction:column;overflow:hidden;max-height:90vh;animation:scaleIn .3s cubic-bezier(.16,1,.3,1);position:relative}@keyframes fadeInOpacity{0%{opacity:0}to{opacity:1}}@keyframes scaleIn{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}.td-form-container{display:flex;flex-direction:column;height:100%;padding:0 24px}.td-form-page-header{padding:24px 0 16px}.td-form-title{font-size:24px;font-weight:700;color:var(--td-text-main);margin-bottom:8px}.td-form-desc{font-size:14px;color:var(--td-text-sub);line-height:normal}.td-form{display:flex;flex-direction:column;gap:20px;padding-bottom:24px}.td-input-group{display:flex;flex-direction:column;gap:8px}.td-label{font-size:14px;font-weight:500;color:var(--td-text-main)}.td-input{width:100%;height:48px;padding:0 16px;border-radius:8px;border:1px solid var(--td-border);background:#f8f9fc;font-family:inherit;font-size:16px;color:var(--td-text-main);transition:all .2s}.td-input:focus{outline:none;border-color:var(--td-primary);box-shadow:0 0 0 2px #0e65f133}.td-textarea{width:100%;min-height:100px;padding:16px;border-radius:8px;border:1px solid var(--td-border);background:#f8f9fc;font-family:inherit;font-size:16px;color:var(--td-text-main);resize:none}.td-textarea:focus{outline:none;border-color:var(--td-primary)}.td-chips-group{display:flex;flex-wrap:wrap;gap:8px}.td-chip{padding:8px 16px;border-radius:99px;font-size:14px;font-weight:500;cursor:pointer;border:1px solid var(--td-border);background:#fff;color:var(--td-text-sub);transition:all .2s}.td-chip:hover{border-color:var(--td-primary);color:var(--td-primary);background:#0e65f10d}.td-chip.active{border-color:var(--td-primary);background:#0e65f11a;color:var(--td-primary);display:flex;align-items:center;gap:6px}.td-secure-note{display:flex;align-items:center;justify-content:center;gap:6px;font-size:12px;color:var(--td-text-sub);opacity:.8;margin-top:8px}.td-summary-container{display:flex;flex-direction:column;height:100%}.td-success-header{display:flex;flex-direction:column;align-items:center;padding:32px 24px;text-align:center}.td-success-icon{width:80px;height:80px;background:#0e65f11a;color:var(--td-primary);border-radius:50%;display:flex;align-items:center;justify-content:center;margin-bottom:16px}.td-summary-avatar{width:56px;height:56px;border-radius:50%;background-size:cover;background-position:center;flex-shrink:0}.td-summary-title{font-size:24px;font-weight:700;margin-bottom:8px}.td-summary-desc{font-size:14px;color:var(--td-text-sub);max-width:360px;margin:0 auto}.td-summary-card{margin:0 24px;padding:16px;border-radius:12px;border:1px solid var(--td-border);background:#f8f9fc;display:flex;align-items:center;gap:16px}.td-summary-details{padding:8px 24px}.td-detail-row{display:flex;justify-content:space-between;padding:12px 0;border-bottom:1px dashed var(--td-border);font-size:14px}.td-note-box{margin:16px 24px;padding:16px;background:#0e65f10d;border:1px solid rgba(14,101,241,.1);border-radius:8px}.td-note-title{font-size:14px;font-weight:700;margin-bottom:8px;display:flex;align-items:center;gap:8px}.td-grid-actions{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:12px}';
108
108
  const collapsedHtml = `<div class="td-widget-fab" id="td-widget-fab">\r
109
109
  <div class="td-fab-card">\r
110
110
  <button class="td-close-fab" id="td-fab-close">\r
@@ -112,7 +112,7 @@ const collapsedHtml = `<div class="td-widget-fab" id="td-widget-fab">\r
112
112
  </button>\r
113
113
  <div class="td-avatar-container">\r
114
114
  <div class="td-avatar" style="background-image: url('{{doctorAvatar}}');"></div>\r
115
- <div class="td-status-dot" style="background: #22c55e;"></div>\r
115
+ <div class="td-status-dot" style="background: {{statusDotColor}};"></div>\r
116
116
  </div>\r
117
117
  <div class="td-info">\r
118
118
  <div class="td-brand">\r
@@ -120,87 +120,191 @@ const collapsedHtml = `<div class="td-widget-fab" id="td-widget-fab">\r
120
120
  <span>Tư vấn ngay</span>\r
121
121
  </div>\r
122
122
  <h3 class="td-cta">Khám online ngay</h3>\r
123
- <p class="td-sub">Bác sĩ đang Online</p>\r
123
+ <p class="td-sub">{{statusText}}</p>\r
124
124
  </div>\r
125
125
  </div>\r
126
126
  </div>`;
127
- const expandedHtml = `<div class="td-widget-expanded" id="td-widget-expanded">\r
128
- <header class="td-header">\r
129
- <div class="td-header-title">\r
130
- <div class="td-icon-box">\r
131
- <span class="material-symbols-outlined">medical_services</span>\r
127
+ const expandedHtml = `<div class="td-overlay-wrapper">\r
128
+ <div class="td-modal-centered" id="td-widget-expanded">\r
129
+ <header class="td-header">\r
130
+ <div class="td-header-title">\r
131
+ <div class="td-icon-box">\r
132
+ <span class="material-symbols-outlined">medical_services</span>\r
133
+ </div>\r
134
+ <span>Tư vấn trực tuyến</span>\r
132
135
  </div>\r
133
- <span>Clinic</span>\r
134
- </div>\r
135
- <button class="td-close-btn" id="td-expanded-close">\r
136
- <span class="material-symbols-outlined">close</span>\r
137
- </button>\r
138
- </header>\r
136
+ <button class="td-close-btn" id="td-expanded-close">\r
137
+ <span class="material-symbols-outlined">close</span>\r
138
+ </button>\r
139
+ </header>\r
139
140
  \r
140
- <div class="td-content">\r
141
- <div class="td-doctor-profile">\r
142
- <div class="td-doctor-avatar" style="background-image: url('{{doctorAvatar}}');">\r
143
- <div class="td-status-dot"\r
144
- style="width: 20px; height: 20px; border-width: 3px; bottom: 4px; right: 4px; background: #22c55e;">\r
141
+ <div class="td-content">\r
142
+ <div class="td-doctor-profile">\r
143
+ <div class="td-doctor-avatar" style="background-image: url('{{doctorAvatar}}');">\r
144
+ <!-- <div class="td-status-dot"\r
145
+ style="width: 20px; height: 20px; border-width: 3px; bottom: 4px; right: 4px; background: #22c55e;">\r
146
+ </div> -->\r
145
147
  </div>\r
146
- </div>\r
147
- <h1 class="td-doctor-name">{{doctorName}}</h1>\r
148
- <p class="td-doctor-specialty">{{doctorSpecialty}}</p>\r
148
+ <h1 class="td-doctor-name">{{doctorName}}</h1>\r
149
+ <p class="td-doctor-specialty">{{doctorSpecialty}}</p>\r
149
150
  \r
150
- <div class="td-online-badge">\r
151
- <span class="td-pulse"></span>\r
152
- <span class="td-badge-text">Đang trực tuyến</span>\r
151
+ <div class="{{badgeClass}}">\r
152
+ <span class="td-pulse" style="{{pulseStyle}}"></span>\r
153
+ <span class="td-badge-text">{{badgeText}}</span>\r
154
+ </div>\r
153
155
  </div>\r
154
- </div>\r
155
156
  \r
156
- <div class="td-trust-grid">\r
157
- <div class="td-trust-item">\r
158
- <div class="td-icon-box">\r
159
- <span class="material-symbols-outlined">verified</span>\r
157
+ <div class="td-trust-grid">\r
158
+ <div class="td-trust-item">\r
159
+ <div class="td-icon-box">\r
160
+ <span class="material-symbols-outlined">verified</span>\r
161
+ </div>\r
162
+ <h3 class="td-trust-title">Bác sĩ được xác thực</h3>\r
160
163
  </div>\r
161
- <h3 class="td-trust-title">Bác sĩ được xác thực</h3>\r
162
- </div>\r
163
- <div class="td-trust-item">\r
164
- <div class="td-icon-box">\r
165
- <span class="material-symbols-outlined">security</span>\r
164
+ <div class="td-trust-item">\r
165
+ <div class="td-icon-box">\r
166
+ <span class="material-symbols-outlined">security</span>\r
167
+ </div>\r
168
+ <h3 class="td-trust-title">Bảo mật thông tin</h3>\r
166
169
  </div>\r
167
- <h3 class="td-trust-title">Bảo mật thông tin</h3>\r
168
170
  </div>\r
169
- </div>\r
170
171
  \r
171
- <div class="td-actions">\r
172
- <button class="td-btn td-btn-primary" id="td-btn-consult">\r
173
- <span class="material-symbols-outlined" style="margin-right: 8px;">videocam</span>\r
174
- Bắt đầu tư vấn ngay\r
175
- </button>\r
176
- <button class="td-btn td-btn-outline" id="td-btn-schedule">\r
177
- Đặt lịch hẹn sau\r
178
- </button>\r
172
+ <div class="td-actions">\r
173
+ <button class="td-btn td-btn-primary" id="td-btn-consult" style="{{btnStyle}}">\r
174
+ <span class="material-symbols-outlined" style="margin-right: 8px;">videocam</span>\r
175
+ Bắt đầu tư vấn ngay\r
176
+ </button>\r
177
+ <!-- <button class="td-btn td-btn-outline" id="td-btn-schedule">\r
178
+ Đặt lịch hẹn sau\r
179
+ </button> -->\r
180
+ </div>\r
179
181
  </div>\r
180
- </div>\r
181
182
  \r
182
- <div class="td-footer">\r
183
- Powered by <b>Clinic Connect</b>\r
183
+ <div class="td-footer">\r
184
+ Powered by <b>Clinic Connect</b>\r
185
+ </div>\r
184
186
  </div>\r
185
187
  </div>`;
186
188
  const consultationHtml = `<div class="td-consultation-modal">\r
187
189
  <!-- Left: Video Panel -->\r
188
190
  <div class="td-video-panel">\r
189
- <div class="td-main-video"\r
190
- style="background-image: url('https://lh3.googleusercontent.com/aida-public/AB6AXuC54B68xwAiOWZFQNVMIVKNBTKwo-nPCTsEgkaekhx9PbGDRCwcy18cFeALhbiaeOyaIS88c9RZst2sRWUyKTeSif-HqEfgU5Aoht-mk6Tjtuts2M4XmoMaxNSAi7qjKoQRYx61I4MolGfjCkxRN0lW5rlSlQfMwY0T8K1440SLM9hW_4u0N1Vg6kQTcSUitouGRmWktBHr-yH5QTu78AC1IOZLd5-HyWw0NUb6uESPpNZoZHoIEKIjFhsGBnCRmzHCUP9Ex6mP0XvW');">\r
191
+ <div class="td-main-video">\r
192
+ <div id="td-video-placeholder" style="\r
193
+ position: absolute;\r
194
+ top: 0; left: 0; width: 100%; height: 100%;\r
195
+ background: radial-gradient(circle at center, #1f2937 0%, #111827 100%);\r
196
+ display: flex;\r
197
+ flex-direction: column;\r
198
+ align-items: center;\r
199
+ justify-content: center;\r
200
+ color: white;\r
201
+ z-index: 10;\r
202
+ transition: opacity 0.5s ease;\r
203
+ font-family: 'Inter', system-ui, sans-serif;\r
204
+ ">\r
205
+ <!-- Ripple Container -->\r
206
+ <div\r
207
+ style="position: relative; width: 80px; height: 80px; display: flex; align-items: center; justify-content: center; margin-bottom: 24px;">\r
208
+ <div class="td-ripple" style="animation-delay: 0s;"></div>\r
209
+ <div class="td-ripple" style="animation-delay: 0.5s;"></div>\r
210
+ <div class="td-ripple" style="animation-delay: 1s;"></div>\r
211
+ \r
212
+ <!-- Main Icon Circle with Glass Effect -->\r
213
+ <div style="\r
214
+ position: relative;\r
215
+ width: 60px; height: 60px;\r
216
+ background: rgba(255, 255, 255, 0.1);\r
217
+ backdrop-filter: blur(8px);\r
218
+ -webkit-backdrop-filter: blur(8px);\r
219
+ border: 1px solid rgba(255, 255, 255, 0.2);\r
220
+ border-radius: 50%;\r
221
+ display: flex; align-items: center; justify-content: center;\r
222
+ z-index: 2;\r
223
+ box-shadow: 0 4px 12px rgba(0,0,0,0.2);\r
224
+ ">\r
225
+ <span class="material-symbols-outlined"\r
226
+ style="font-size: 30px; color: #60a5fa; text-shadow: 0 0 10px rgba(96, 165, 250, 0.5);">videocam</span>\r
227
+ </div>\r
228
+ </div>\r
229
+ \r
230
+ <div style="text-align: center;">\r
231
+ <div\r
232
+ style="font-size: 16px; font-weight: 600; color: #f3f4f6; margin-bottom: 6px; letter-spacing: 0.5px;">\r
233
+ Đang kết nối</div>\r
234
+ <div\r
235
+ style="font-size: 13px; color: #9ca3af; display: flex; align-items: center; gap: 4px; justify-content: center;">\r
236
+ Vui lòng đợi trong giây lát\r
237
+ <span class="td-dots"></span>\r
238
+ </div>\r
239
+ </div>\r
240
+ \r
241
+ <style>\r
242
+ .td-ripple {\r
243
+ position: absolute;\r
244
+ border: 2px solid rgba(96, 165, 250, 0.6);\r
245
+ width: 100%;\r
246
+ height: 100%;\r
247
+ border-radius: 50%;\r
248
+ opacity: 0;\r
249
+ animation: td-ripple-anim 2s cubic-bezier(0, 0.2, 0.8, 1) infinite;\r
250
+ }\r
251
+ \r
252
+ @keyframes td-ripple-anim {\r
253
+ 0% {\r
254
+ transform: scale(0.8);\r
255
+ opacity: 0;\r
256
+ }\r
257
+ \r
258
+ 5% {\r
259
+ opacity: 0.5;\r
260
+ }\r
261
+ \r
262
+ 100% {\r
263
+ transform: scale(2.2);\r
264
+ opacity: 0;\r
265
+ }\r
266
+ }\r
267
+ \r
268
+ .td-dots::after {\r
269
+ content: '.';\r
270
+ animation: td-dots-anim 1.5s steps(4, end) infinite;\r
271
+ }\r
272
+ \r
273
+ @keyframes td-dots-anim {\r
274
+ \r
275
+ 0%,\r
276
+ 20% {\r
277
+ content: '.';\r
278
+ }\r
279
+ \r
280
+ 40% {\r
281
+ content: '..';\r
282
+ }\r
283
+ \r
284
+ 60% {\r
285
+ content: '...';\r
286
+ }\r
287
+ \r
288
+ 80%,\r
289
+ 100% {\r
290
+ content: '';\r
291
+ }\r
292
+ }\r
293
+ </style>\r
294
+ </div>\r
191
295
  </div>\r
192
296
  <div class="td-overlay"></div>\r
193
297
  \r
194
298
  <div class="td-video-header">\r
195
- <div class="td-badge-glass">\r
299
+ <!-- <div class="td-badge-glass">\r
196
300
  <span class="material-symbols-outlined"\r
197
301
  style="color: #ef4444; font-size: 16px;">radio_button_checked</span>\r
198
302
  REC\r
199
- </div>\r
200
- <div class="td-badge-glass">\r
303
+ </div> -->\r
304
+ <!-- <div class="td-badge-glass">\r
201
305
  <span class="material-symbols-outlined" style="font-size: 16px;">signal_cellular_alt</span>\r
202
306
  HD Quality\r
203
- </div>\r
307
+ </div> -->\r
204
308
  </div>\r
205
309
  \r
206
310
  <div class="td-pip"\r
@@ -209,143 +313,91 @@ const consultationHtml = `<div class="td-consultation-modal">\r
209
313
  </div>\r
210
314
  \r
211
315
  <div class="td-controls">\r
212
- <button class="td-ctrl-btn">\r
316
+ <button class="td-ctrl-btn" id="td-btn-mic">\r
213
317
  <span class="material-symbols-outlined">mic</span>\r
214
318
  </button>\r
215
- <button class="td-ctrl-btn">\r
319
+ <button class="td-ctrl-btn" id="td-btn-cam">\r
216
320
  <span class="material-symbols-outlined">videocam</span>\r
217
321
  </button>\r
218
322
  <button class="td-ctrl-end" id="td-consult-end">\r
219
323
  <span class="material-symbols-outlined">call_end</span>\r
220
324
  Kết thúc\r
221
325
  </button>\r
222
- <button class="td-ctrl-btn">\r
223
- <span class="material-symbols-outlined">settings</span>\r
224
- </button>\r
225
326
  </div>\r
226
327
  </div>\r
227
328
  \r
228
- <!-- Right: Chat Panel -->\r
229
- <div class="td-chat-panel">\r
230
- <div class="td-chat-header">\r
231
- <div class="td-chat-doc-info">\r
232
- <div class="td-chat-avatar" style="background-image: url('{{doctorAvatar}}');"></div>\r
233
- <div>\r
234
- <h3 style="font-size: 16px; font-weight: 700;">{{doctorName}}</h3>\r
235
- <p style="font-size: 12px; color: var(--td-text-sub);">Online • Verified</p>\r
236
- </div>\r
237
- </div>\r
238
- <div class="td-chat-timer">\r
239
- <div style="display: flex; align-items: center; gap: 4px; font-size: 12px;">\r
240
- <span class="material-symbols-outlined"\r
241
- style="font-size: 16px; color: var(--td-primary);">timer</span>\r
242
- SESSION\r
243
- </div>\r
244
- <div class="td-timer-val">00:03:12</div>\r
245
- </div>\r
246
- </div>\r
247
- \r
248
- <div class="td-chat-messages">\r
249
- <div class="td-msg-system">\r
250
- Consultation started\r
251
- </div>\r
252
- <div class="td-msg-row doctor">\r
253
- <div class="td-msg-bubble">\r
254
- Xin chào. Tôi có thể giúp gì cho bạn hôm nay?\r
255
- <div class="td-msg-time">14:01</div>\r
256
- </div>\r
257
- </div>\r
258
- <div class="td-msg-row own">\r
259
- <div class="td-msg-bubble">\r
260
- Chào bác sĩ, tôi bị đau đầu...\r
261
- <div class="td-msg-time">14:02</div>\r
262
- </div>\r
263
- </div>\r
264
- </div>\r
265
- \r
266
- <div class="td-chat-input-area">\r
267
- <div class="td-input-wrapper">\r
268
- <textarea class="td-chat-input" placeholder="Nhập tin nhắn..."></textarea>\r
269
- <button class="td-send-btn">\r
270
- <span class="material-symbols-outlined">send</span>\r
271
- </button>\r
272
- </div>\r
273
- <div style="text-align: center; margin-top: 8px; font-size: 10px; color: var(--td-text-sub);">\r
274
- <span class="material-symbols-outlined" style="font-size: 10px; vertical-align: middle;">lock</span>\r
275
- Mã hóa đầu cuối\r
276
- </div>\r
277
- </div>\r
278
- </div>\r
279
329
  </div>`;
280
- const patientFormHtml = '<div class="td-widget-expanded" id="td-patient-form">\r\n <header class="td-header">\r\n <div class="td-header-title">\r\n <div class="td-icon-box">\r\n <span class="material-symbols-outlined">medical_services</span>\r\n </div>\r\n <span>Tư vấn trực tuyến</span>\r\n </div>\r\n <button class="td-close-btn" id="td-form-close">\r\n <span class="material-symbols-outlined">close</span>\r\n </button>\r\n </header>\r\n\r\n <div class="td-content">\r\n <div class="td-form-container">\r\n <div class="td-form-page-header">\r\n <h1 class="td-form-title">Kết nối với Bác sĩ</h1>\r\n <p class="td-form-desc">Vui lòng nhập thông tin để bắt đầu tư vấn trực tuyến.</p>\r\n </div>\r\n\r\n <form class="td-form" id="td-info-form">\r\n <div class="td-input-group">\r\n <label class="td-label">Họ và tên</label>\r\n <input type="text" class="td-input" name="name" placeholder="VD: Nguyễn Văn A" required />\r\n </div>\r\n\r\n <div class="td-input-group">\r\n <label class="td-label">Số điện thoại</label>\r\n <div style="position: relative;">\r\n <input type="tel" class="td-input" name="phone" placeholder="VD: 0912 345 678" required />\r\n <!-- <div\r\n style="position: absolute; right: 12px; top: 50%; transform: translateY(-50%); color: var(--td-primary);">\r\n <span class="material-symbols-outlined" style="font-size: 20px;">smartphone</span>\r\n </div> -->\r\n </div>\r\n </div>\r\n\r\n <!-- <div class="td-input-group">\r\n <label class="td-label">Triệu chứng của bạn?</label>\r\n <div class="td-chips-group">\r\n <button type="button" class="td-chip active">Sốt <span class="material-symbols-outlined"\r\n style="font-size: 16px;">check</span></button>\r\n <button type="button" class="td-chip">Ho / Cảm cúm</button>\r\n <button type="button" class="td-chip">Đau đầu</button>\r\n <button type="button" class="td-chip">Đau bụng</button>\r\n </div>\r\n </div> -->\r\n\r\n <div class="td-input-group">\r\n <label class="td-label">Mô tả thêm <span style="color: var(--td-text-sub); font-weight: 400;">(Tùy\r\n chọn)</span></label>\r\n <textarea class="td-textarea" name="symptoms"\r\n placeholder="Bạn đang cảm thấy như thế nào?"></textarea>\r\n </div>\r\n\r\n <div style="margin-top: 8px;">\r\n <button type="submit" class="td-btn td-btn-primary" id="td-form-submit">\r\n Bắt đầu tư vấn\r\n <span class="material-symbols-outlined" style="margin-left: 8px;">arrow_forward</span>\r\n </button>\r\n </div>\r\n\r\n <div class="td-secure-note">\r\n <span class="material-symbols-outlined" style="font-size: 14px;">lock</span>\r\n Thông tin y tế được bảo mật 100%\r\n </div>\r\n </form>\r\n </div>\r\n </div>\r\n</div>';
281
- const postConsultationHtml = `<div class="td-widget-expanded" id="td-summary-view">\r
282
- <header class="td-header">\r
283
- <h2 class="td-header-title" style="font-size: 16px;">Tổng kết tư vấn</h2>\r
284
- <button class="td-close-btn" id="td-summary-close">\r
285
- <span class="material-symbols-outlined">close</span>\r
286
- </button>\r
287
- </header>\r
330
+ const patientFormHtml = '<div class="td-overlay-wrapper">\r\n <div class="td-modal-centered" id="td-patient-form">\r\n <header class="td-header">\r\n <div class="td-header-title">\r\n <div class="td-icon-box">\r\n <span class="material-symbols-outlined">medical_services</span>\r\n </div>\r\n <span>Tư vấn trực tuyến</span>\r\n </div>\r\n <button class="td-close-btn" id="td-form-close">\r\n <span class="material-symbols-outlined">close</span>\r\n </button>\r\n </header>\r\n\r\n <div class="td-content">\r\n <div class="td-form-container">\r\n <div class="td-form-page-header">\r\n <h1 class="td-form-title">Kết nối với Bác sĩ</h1>\r\n <p class="td-form-desc">Vui lòng nhập thông tin để bắt đầu tư vấn trực tuyến.</p>\r\n </div>\r\n\r\n <form class="td-form" id="td-info-form">\r\n <div class="td-input-group">\r\n <label class="td-label">Họ và tên</label>\r\n <input type="text" class="td-input" name="name" placeholder="VD: Nguyễn Văn A" required />\r\n </div>\r\n\r\n <div class="td-input-group">\r\n <label class="td-label">Số điện thoại</label>\r\n <div style="position: relative;">\r\n <input type="tel" class="td-input" name="phone" placeholder="VD: 0912 345 678" required />\r\n <!-- <div\r\n style="position: absolute; right: 12px; top: 50%; transform: translateY(-50%); color: var(--td-primary);">\r\n <span class="material-symbols-outlined" style="font-size: 20px;">smartphone</span>\r\n </div> -->\r\n </div>\r\n </div>\r\n\r\n <!-- <div class="td-input-group">\r\n <label class="td-label">Triệu chứng của bạn?</label>\r\n <div class="td-chips-group">\r\n <button type="button" class="td-chip active">Sốt <span class="material-symbols-outlined"\r\n style="font-size: 16px;">check</span></button>\r\n <button type="button" class="td-chip">Ho / Cảm cúm</button>\r\n <button type="button" class="td-chip">Đau đầu</button>\r\n <button type="button" class="td-chip">Đau bụng</button>\r\n <button type="button" class="td-chip">Đau bụng</button>\r\n </div>\r\n </div> -->\r\n\r\n <div class="td-input-group">\r\n <label class="td-label">Mô tả thêm <span\r\n style="color: var(--td-text-sub); font-weight: 400;">(Tùy\r\n chọn)</span></label>\r\n <textarea class="td-textarea" name="symptoms"\r\n placeholder="Bạn đang cảm thấy như thế nào?"></textarea>\r\n </div>\r\n\r\n <div style="margin-top: 8px;">\r\n <button type="submit" class="td-btn td-btn-primary" id="td-form-submit">\r\n Bắt đầu tư vấn\r\n <span class="material-symbols-outlined" style="margin-left: 8px;">arrow_forward</span>\r\n </button>\r\n </div>\r\n\r\n <div class="td-secure-note">\r\n <span class="material-symbols-outlined" style="font-size: 14px;">lock</span>\r\n Thông tin y tế được bảo mật 100%\r\n </div>\r\n </form>\r\n </div>\r\n </div>\r\n </div>\r\n</div>';
331
+ const postConsultationHtml = `<div class="td-overlay-wrapper">\r
332
+ <div class="td-modal-centered" id="td-summary-view">\r
333
+ <header class="td-header">\r
334
+ <h2 class="td-header-title" style="font-size: 16px;">Tổng kết tư vấn</h2>\r
335
+ <button class="td-close-btn" id="td-summary-close">\r
336
+ <span class="material-symbols-outlined">close</span>\r
337
+ </button>\r
338
+ </header>\r
288
339
  \r
289
- <div class="td-content">\r
290
- <div class="td-summary-container">\r
291
- <div class="td-success-header">\r
292
- <div class="td-success-icon">\r
293
- <span class="material-symbols-outlined" style="font-size: 48px;">check_circle</span>\r
340
+ <div class="td-content">\r
341
+ <div class="td-summary-container">\r
342
+ <div class="td-success-header">\r
343
+ <div class="td-success-icon">\r
344
+ <span class="material-symbols-outlined" style="font-size: 48px;">check_circle</span>\r
345
+ </div>\r
346
+ <h3 class="td-summary-title">Tư vấn Hoàn tất</h3>\r
347
+ <p class="td-summary-desc">Cuộc hẹn đã được ghi nhận vào hồ sơ sức khỏe của bạn.</p>\r
294
348
  </div>\r
295
- <h3 class="td-summary-title">Tư vấn Hoàn tất</h3>\r
296
- <p class="td-summary-desc">Cuộc hẹn đã được ghi nhận vào hồ sơ sức khỏe của bạn.</p>\r
297
- </div>\r
298
349
  \r
299
- <div class="td-summary-card">\r
300
- <div class="td-chat-avatar"\r
301
- style="background-image: url('{{doctorAvatar}}'); width: 56px; height: 56px;"></div>\r
302
- <div>\r
303
- <div style="font-weight: 700;">{{doctorName}}</div>\r
304
- <div style="color: var(--td-text-sub); font-size: 14px;">{{doctorSpecialty}}</div>\r
305
- </div>\r
306
- <div style="margin-left: auto; color: var(--td-primary);">\r
307
- <span class="material-symbols-outlined">verified_user</span>\r
350
+ <!-- <div class="td-summary-card">\r
351
+ <div class="td-summary-avatar" style="background-image: url('{{doctorAvatar}}');"></div>\r
352
+ <div>\r
353
+ <div style="font-weight: 700;">{{doctorName}}</div>\r
354
+ <div style="color: var(--td-text-sub); font-size: 14px;">{{doctorSpecialty}}</div>\r
355
+ </div>\r
356
+ <div style="margin-left: auto; color: var(--td-primary);">\r
357
+ <span class="material-symbols-outlined">verified_user</span>\r
358
+ </div>\r
308
359
  </div>\r
309
- </div>\r
310
360
  \r
311
- <div class="td-summary-details">\r
312
- <div class="td-detail-row">\r
313
- <span style="color: var(--td-text-sub); display: flex; gap: 6px;">\r
314
- <span class="material-symbols-outlined" style="font-size: 18px;">tag</span> Mã tư vấn\r
315
- </span>\r
316
- <span style="font-weight: 600;">#TD-93821</span>\r
361
+ <div class="td-summary-details">\r
362
+ <div class="td-detail-row">\r
363
+ <span style="color: var(--td-text-sub); display: flex; gap: 6px;">\r
364
+ <span class="material-symbols-outlined" style="font-size: 18px;">tag</span> Mã tư vấn\r
365
+ </span>\r
366
+ <span style="font-weight: 600;">#TD-93821</span>\r
367
+ </div>\r
368
+ <div class="td-detail-row">\r
369
+ <span style="color: var(--td-text-sub); display: flex; gap: 6px;">\r
370
+ <span class="material-symbols-outlined" style="font-size: 18px;">schedule</span> Thời gian\r
371
+ </span>\r
372
+ <span style="font-weight: 600;">10:00 AM, Today</span>\r
373
+ </div>\r
317
374
  </div>\r
318
- <div class="td-detail-row">\r
319
- <span style="color: var(--td-text-sub); display: flex; gap: 6px;">\r
320
- <span class="material-symbols-outlined" style="font-size: 18px;">schedule</span> Thời gian\r
321
- </span>\r
322
- <span style="font-weight: 600;">10:00 AM, Today</span>\r
323
- </div>\r
324
- </div>\r
325
375
  \r
326
- <div class="td-note-box">\r
327
- <div class="td-note-title">\r
328
- <span class="material-symbols-outlined"\r
329
- style="font-size: 16px; color: var(--td-primary);">clinical_notes</span>\r
330
- Ghi chú bác sĩ\r
376
+ <div class="td-note-box">\r
377
+ <div class="td-note-title">\r
378
+ <span class="material-symbols-outlined"\r
379
+ style="font-size: 16px; color: var(--td-primary);">clinical_notes</span>\r
380
+ Ghi chú bác sĩ\r
381
+ </div>\r
382
+ <p style="font-size: 14px; line-height: 1.5;">Bệnh nhân cần nghỉ ngơi và uống nhiều nước. Hạn chế\r
383
+ vận\r
384
+ động mạnh trong 2 ngày tới.</p>\r
331
385
  </div>\r
332
- <p style="font-size: 14px; line-height: 1.5;">Bệnh nhân cần nghỉ ngơi và uống nhiều nước. Hạn chế vận\r
333
- động mạnh trong 2 ngày tới.</p>\r
334
- </div>\r
335
386
  \r
336
- <div class="td-actions">\r
337
- <div class="td-grid-actions">\r
338
- <button class="td-btn td-btn-outline" style="font-size: 12px; height: 40px; margin-bottom: 0;">\r
339
- Xem kết luận\r
340
- </button>\r
341
- <button class="td-btn td-btn-outline" style="font-size: 12px; height: 40px; margin-bottom: 0;">\r
342
- Tải đơn thuốc\r
387
+ <div class="td-actions">\r
388
+ <div class="td-grid-actions">\r
389
+ <button class="td-btn td-btn-outline" style="font-size: 12px; height: 40px; margin-bottom: 0;">\r
390
+ Xem kết luận\r
391
+ </button>\r
392
+ <button class="td-btn td-btn-outline" style="font-size: 12px; height: 40px; margin-bottom: 0;">\r
393
+ Tải đơn thuốc\r
394
+ </button>\r
395
+ </div>\r
396
+ <button class="td-btn td-btn-primary">\r
397
+ <span class="material-symbols-outlined" style="margin-right: 8px;">calendar_month</span>\r
398
+ Đặt lịch tái khám\r
343
399
  </button>\r
344
- </div>\r
345
- <button class="td-btn td-btn-primary">\r
346
- <span class="material-symbols-outlined" style="margin-right: 8px;">calendar_month</span>\r
347
- Đặt lịch tái khám\r
348
- </button>\r
400
+ </div> -->\r
349
401
  </div>\r
350
402
  </div>\r
351
403
  </div>\r
@@ -429,8 +481,8 @@ const inlineTriggerHtml = `<style>\r
429
481
  <!-- Bottom: Status & Action -->\r
430
482
  <div class="td-inline-footer">\r
431
483
  <div class="td-inline-status">\r
432
- <span class="td-inline-pulse"></span>\r
433
- <span class="td-inline-text">Bác sĩ đang Online</span>\r
484
+ <span class="td-inline-pulse" style="{{pulseStyle}}"></span>\r
485
+ <span class="td-inline-text" style="{{inlineTextStyle}}">{{statusText}}</span>\r
434
486
  </div>\r
435
487
  \r
436
488
  <button id="td-inline-btn" type="button" style="\r
@@ -448,9 +500,9 @@ const inlineTriggerHtml = `<style>\r
448
500
  white-space: nowrap;\r
449
501
  transition: all 0.2s;\r
450
502
  box-shadow: 0 2px 5px rgba(0,102,255,0.2);\r
503
+ {{inlineBtnStyle}}\r
451
504
  " onmouseover="this.style.background='#0052cc'" onmouseout="this.style.background='#0066ff'">\r
452
- <span class="material-symbols-outlined" style="font-size: 18px;">videocam</span>\r
453
- Tư vấn ngay\r
505
+ {{inlineBtnContent}}\r
454
506
  </button>\r
455
507
  </div>\r
456
508
  </div>`;
@@ -461,9 +513,28 @@ const render = (template, data) => {
461
513
  const doctorName = ((_a = data.doctor) == null ? void 0 : _a.name) || "Bác sĩ Hà Ngọc Mạnh";
462
514
  const doctorSpecialty = ((_b = data.doctor) == null ? void 0 : _b.specialty) || "Nam học - Hiếm muộn";
463
515
  const doctorAvatar = ((_c = data.doctor) == null ? void 0 : _c.avatarUrl) || "";
516
+ const isOnline = !!data.doctorOnline;
517
+ const statusDotColor = isOnline ? "#22c55e" : "#9ca3af";
518
+ const statusText = isOnline ? "Bác sĩ đang Online" : "Bác sĩ Offline";
519
+ const badgeClass = isOnline ? "td-online-badge" : "td-online-badge td-offline";
520
+ const badgeText = isOnline ? "Đang trực tuyến" : "Đang ngoại tuyến";
521
+ const btnStyle = isOnline ? "" : "opacity: 0.5; cursor: not-allowed; background: #9ca3af;";
522
+ const pulseStyle = isOnline ? "" : "background: #9ca3af; animation: none;";
523
+ const inlineTextStyle = isOnline ? "" : "color: #6b7280;";
524
+ const inlineBtnStyle = isOnline ? "" : "opacity: 0.5; cursor: not-allowed; background: #9ca3af;";
525
+ const inlineBtnHtml = isOnline ? '<span class="material-symbols-outlined" style="font-size: 18px;">videocam</span> Tư vấn ngay' : '<span class="material-symbols-outlined" style="font-size: 18px;">videocam_off</span> Bác sĩ Offline';
464
526
  output = output.replace(/{{doctorName}}/g, doctorName);
465
527
  output = output.replace(/{{doctorSpecialty}}/g, doctorSpecialty);
466
528
  output = output.replace(/{{doctorAvatar}}/g, doctorAvatar);
529
+ output = output.replace(/{{statusDotColor}}/g, statusDotColor);
530
+ output = output.replace(/{{statusText}}/g, statusText);
531
+ output = output.replace(/{{badgeClass}}/g, badgeClass);
532
+ output = output.replace(/{{badgeText}}/g, badgeText);
533
+ output = output.replace(/{{btnStyle}}/g, btnStyle);
534
+ output = output.replace(/{{pulseStyle}}/g, pulseStyle);
535
+ output = output.replace(/{{inlineTextStyle}}/g, inlineTextStyle);
536
+ output = output.replace(/{{inlineBtnStyle}}/g, inlineBtnStyle);
537
+ output = output.replace(/{{inlineBtnContent}}/g, inlineBtnHtml);
467
538
  return output;
468
539
  };
469
540
  const getCollapsedHtml = (data) => render(collapsedHtml, data);
@@ -28019,29 +28090,44 @@ class LiveKitService {
28019
28090
  constructor() {
28020
28091
  this.room = null;
28021
28092
  this.wrapper = null;
28022
- this.preAcquiredTracks = [];
28093
+ this.isPermissionGranted = false;
28094
+ this.permissionPromise = null;
28023
28095
  }
28024
28096
  /**
28025
28097
  * Request Camera & Mic permissions early
28026
28098
  */
28027
28099
  async requestPermissions() {
28028
- try {
28029
- console.log("[LiveKit] Requesting early permissions...");
28030
- const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
28031
- this.preAcquiredTracks = stream.getTracks();
28032
- console.log("[LiveKit] Permissions granted, tracks cached:", this.preAcquiredTracks.length);
28033
- return true;
28034
- } catch (error) {
28035
- console.error("[LiveKit] Permission request failed:", error);
28036
- console.log(`Không thể xin quyền: ${error.name} - ${error.message}`);
28037
- return false;
28100
+ if (this.permissionPromise) {
28101
+ console.log("[LiveKit] Joining existing permission request...");
28102
+ return this.permissionPromise;
28038
28103
  }
28104
+ this.permissionPromise = (async () => {
28105
+ try {
28106
+ console.log("[LiveKit] Requesting early permissions...");
28107
+ const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
28108
+ stream.getTracks().forEach((t) => t.stop());
28109
+ this.isPermissionGranted = true;
28110
+ console.log("[LiveKit] Permissions primed successfully.");
28111
+ return true;
28112
+ } catch (error) {
28113
+ console.error("[LiveKit] Permission request failed:", error);
28114
+ console.log(`Không thể xin quyền: ${error.name} - ${error.message}`);
28115
+ return false;
28116
+ } finally {
28117
+ this.permissionPromise = null;
28118
+ }
28119
+ })();
28120
+ return this.permissionPromise;
28039
28121
  }
28040
28122
  /**
28041
28123
  * Check if permissions are already granted
28042
28124
  * @returns {Promise<boolean>}
28043
28125
  */
28044
28126
  async checkPermissions() {
28127
+ if (this.isPermissionGranted) {
28128
+ console.log("[LiveKit] Permission flag is true.");
28129
+ return true;
28130
+ }
28045
28131
  if (!navigator.permissions || !navigator.permissions.query) {
28046
28132
  return false;
28047
28133
  }
@@ -28079,7 +28165,6 @@ class LiveKitService {
28079
28165
  try {
28080
28166
  await this.room.connect(url2, token);
28081
28167
  console.log("[LiveKit] Connected to room:", this.room.name);
28082
- await this.publishLocalTracks();
28083
28168
  } catch (error) {
28084
28169
  console.error("[LiveKit] Connection failed:", error);
28085
28170
  }
@@ -28101,23 +28186,9 @@ class LiveKitService {
28101
28186
  async publishLocalTracks() {
28102
28187
  const localContainer = this.wrapper.querySelector(".td-pip");
28103
28188
  try {
28104
- let videoPub = null;
28105
- if (this.preAcquiredTracks && this.preAcquiredTracks.length > 0) {
28106
- console.log("[LiveKit] Publishing pre-acquired tracks...");
28107
- const videoTrack = this.preAcquiredTracks.find((t) => t.kind === "video");
28108
- const audioTrack = this.preAcquiredTracks.find((t) => t.kind === "audio");
28109
- if (videoTrack) {
28110
- videoPub = await this.room.localParticipant.publishTrack(videoTrack, { source: "camera" });
28111
- }
28112
- if (audioTrack) {
28113
- await this.room.localParticipant.publishTrack(audioTrack, { source: "microphone" });
28114
- }
28115
- this.preAcquiredTracks = [];
28116
- } else {
28117
- await this.room.localParticipant.setCameraEnabled(true);
28118
- await this.room.localParticipant.setMicrophoneEnabled(true);
28119
- videoPub = Array.from(this.room.localParticipant.videoTrackPublications.values()).find((p) => p.source === "camera");
28120
- }
28189
+ await this.room.localParticipant.setCameraEnabled(true);
28190
+ await this.room.localParticipant.setMicrophoneEnabled(true);
28191
+ const videoPub = Array.from(this.room.localParticipant.videoTrackPublications.values()).find((p) => p.source === "camera");
28121
28192
  if (videoPub && videoPub.track && localContainer) {
28122
28193
  localContainer.innerHTML = "";
28123
28194
  localContainer.style.backgroundImage = "none";
@@ -28134,9 +28205,9 @@ class LiveKitService {
28134
28205
  } catch (e2) {
28135
28206
  console.error("[LiveKit] Failed to publish tracks:", e2);
28136
28207
  if (e2.name === "NotAllowedError") {
28137
- alert("Không thể mở Camera/Microphone. Hãy kiểm tra icon ổ khóa trên thanh địa chỉ hoặc cài đặt quyền riêng tư.");
28208
+ console.log("Không thể mở Camera/Microphone. Hãy kiểm tra icon ổ khóa trên thanh địa chỉ hoặc cài đặt quyền riêng tư.");
28138
28209
  } else {
28139
- alert(`Lỗi Camera: ${e2.message}`);
28210
+ console.log(`Lỗi Camera: ${e2.message}`);
28140
28211
  }
28141
28212
  }
28142
28213
  }
@@ -28146,8 +28217,15 @@ class LiveKitService {
28146
28217
  const remoteContainer = this.wrapper.querySelector(".td-main-video");
28147
28218
  console.log("[LiveKit] Looking for remote container (.td-main-video):", remoteContainer);
28148
28219
  if (remoteContainer) {
28220
+ const placeholder = remoteContainer.querySelector("#td-video-placeholder");
28221
+ if (placeholder) {
28222
+ placeholder.style.opacity = "0";
28223
+ setTimeout(() => placeholder.remove(), 500);
28224
+ } else {
28225
+ const existingVideos = remoteContainer.querySelectorAll("video");
28226
+ existingVideos.forEach((v) => v.remove());
28227
+ }
28149
28228
  remoteContainer.style.backgroundImage = "none";
28150
- remoteContainer.innerHTML = "";
28151
28229
  const videoEl = track.attach();
28152
28230
  videoEl.style.width = "100%";
28153
28231
  videoEl.style.height = "100%";
@@ -28164,14 +28242,66 @@ class LiveKitService {
28164
28242
  console.log("[LiveKit] Remote audio attached");
28165
28243
  }
28166
28244
  }
28245
+ toggleMicrophone(enabled) {
28246
+ if (this.room && this.room.localParticipant) {
28247
+ this.room.localParticipant.setMicrophoneEnabled(enabled);
28248
+ console.log(`[LiveKit] Microphone ${enabled ? "enabled" : "disabled"}`);
28249
+ }
28250
+ }
28251
+ toggleCamera(enabled) {
28252
+ if (this.room && this.room.localParticipant) {
28253
+ this.room.localParticipant.setCameraEnabled(enabled);
28254
+ console.log(`[LiveKit] Camera ${enabled ? "enabled" : "disabled"}`);
28255
+ }
28256
+ }
28167
28257
  handleTrackUnsubscribed(track, participant) {
28168
28258
  track.detach().forEach((element) => element.remove());
28169
28259
  }
28170
28260
  disconnect() {
28261
+ var _a, _b;
28171
28262
  if (this.room) {
28172
- this.room.disconnect();
28263
+ if (this.room.localParticipant) {
28264
+ try {
28265
+ const allTracks = [
28266
+ ...((_a = this.room.localParticipant.videoTrackPublications) == null ? void 0 : _a.values()) || [],
28267
+ ...((_b = this.room.localParticipant.audioTrackPublications) == null ? void 0 : _b.values()) || []
28268
+ ];
28269
+ allTracks.forEach((publication) => {
28270
+ if (publication.track) {
28271
+ publication.track.stop();
28272
+ publication.track.detach().forEach((el) => el.remove());
28273
+ console.log(`[LiveKit] Stopped local track: ${publication.kind}`);
28274
+ }
28275
+ });
28276
+ } catch (err) {
28277
+ console.error("[LiveKit] Error stopping tracks:", err);
28278
+ }
28279
+ }
28280
+ try {
28281
+ this.room.disconnect();
28282
+ } catch (err) {
28283
+ console.error("[LiveKit] Error disconnecting room:", err);
28284
+ }
28173
28285
  this.room = null;
28174
28286
  }
28287
+ if (this.preAcquiredTracks) {
28288
+ this.preAcquiredTracks.forEach((t) => {
28289
+ try {
28290
+ t.stop();
28291
+ } catch (e2) {
28292
+ }
28293
+ });
28294
+ this.preAcquiredTracks = [];
28295
+ }
28296
+ if (this.wrapper) {
28297
+ const localContainer = this.wrapper.querySelector(".td-pip");
28298
+ const remoteContainer = this.wrapper.querySelector(".td-main-video");
28299
+ if (localContainer) localContainer.innerHTML = "";
28300
+ if (remoteContainer) {
28301
+ remoteContainer.innerHTML = "";
28302
+ remoteContainer.style.backgroundImage = "";
28303
+ }
28304
+ }
28175
28305
  }
28176
28306
  }
28177
28307
  class ClinicWidget {
@@ -28187,9 +28317,11 @@ class ClinicWidget {
28187
28317
  }
28188
28318
  async init() {
28189
28319
  var _a;
28320
+ this.videoToken = null;
28190
28321
  this.mount();
28191
28322
  this.unsubscribe = store.on(EventTypes.STATE_CHANGE, this.handleStateChange.bind(this));
28192
28323
  store.setState({ status: WidgetStates.LOADING });
28324
+ this.startSocket();
28193
28325
  try {
28194
28326
  const response = await this.api.init(this.config.widgetId);
28195
28327
  if (response.success) {
@@ -28208,7 +28340,7 @@ class ClinicWidget {
28208
28340
  // Default open state
28209
28341
  });
28210
28342
  this.checkInlineTrigger();
28211
- this.startSocket();
28343
+ this.recheckDoctorStatus();
28212
28344
  } else {
28213
28345
  store.setState({ status: WidgetStates.IDLE, error: "Init failed" });
28214
28346
  }
@@ -28243,8 +28375,9 @@ class ClinicWidget {
28243
28375
  this.shadowRoot.appendChild(styleTag);
28244
28376
  }
28245
28377
  handleStateChange({ prev, current }) {
28246
- if (prev.status !== current.status) {
28378
+ if (prev.status !== current.status || prev.doctorOnline !== current.doctorOnline) {
28247
28379
  this.render(current.status);
28380
+ this.checkInlineTrigger();
28248
28381
  }
28249
28382
  }
28250
28383
  render(status) {
@@ -28280,6 +28413,7 @@ class ClinicWidget {
28280
28413
  this.bindEvents(status);
28281
28414
  }
28282
28415
  bindEvents(status) {
28416
+ const state = store.getState();
28283
28417
  if (status === WidgetStates.COLLAPSED) {
28284
28418
  const fab = this.shadowRoot.getElementById("td-widget-fab");
28285
28419
  const closeFab = this.shadowRoot.getElementById("td-fab-close");
@@ -28303,11 +28437,26 @@ class ClinicWidget {
28303
28437
  store.setState({ status: WidgetStates.COLLAPSED });
28304
28438
  });
28305
28439
  }
28440
+ const overlay = this.shadowRoot.querySelector(".td-overlay-wrapper");
28441
+ if (overlay) {
28442
+ overlay.addEventListener("click", (e2) => {
28443
+ if (e2.target === overlay) {
28444
+ store.setState({ status: WidgetStates.COLLAPSED });
28445
+ }
28446
+ });
28447
+ }
28306
28448
  if (consultBtn) {
28449
+ if (!state.doctorOnline) {
28450
+ consultBtn.style.opacity = "0.5";
28451
+ consultBtn.style.cursor = "not-allowed";
28452
+ consultBtn.title = "Bác sĩ hiện đang Offline";
28453
+ }
28307
28454
  consultBtn.addEventListener("click", () => {
28308
- consultBtn.addEventListener("click", () => {
28309
- store.setState({ status: WidgetStates.PATIENT_FORM });
28310
- });
28455
+ if (!state.doctorOnline) {
28456
+ console.log("Bác Offline, không thể bắt đầu tư vấn.");
28457
+ return;
28458
+ }
28459
+ store.setState({ status: WidgetStates.PATIENT_FORM });
28311
28460
  });
28312
28461
  }
28313
28462
  } else if (status === WidgetStates.PATIENT_FORM) {
@@ -28318,6 +28467,14 @@ class ClinicWidget {
28318
28467
  store.setState({ status: WidgetStates.EXPANDED });
28319
28468
  });
28320
28469
  }
28470
+ const overlay = this.shadowRoot.querySelector(".td-overlay-wrapper");
28471
+ if (overlay) {
28472
+ overlay.addEventListener("click", (e2) => {
28473
+ if (e2.target === overlay) {
28474
+ store.setState({ status: WidgetStates.EXPANDED });
28475
+ }
28476
+ });
28477
+ }
28321
28478
  if (form) {
28322
28479
  form.addEventListener("submit", async (e2) => {
28323
28480
  e2.preventDefault();
@@ -28329,6 +28486,23 @@ class ClinicWidget {
28329
28486
  });
28330
28487
  this.triggerCallback("onConsultationStarted", { name, phone });
28331
28488
  store.setState({ status: WidgetStates.CONSULTATION });
28489
+ if (!this.livekit) {
28490
+ this.livekit = new LiveKitService();
28491
+ }
28492
+ const alreadyGranted = await this.livekit.checkPermissions();
28493
+ if (alreadyGranted) {
28494
+ console.log("[Widget] Permissions already granted, warming up...");
28495
+ await this.livekit.requestPermissions();
28496
+ if (this.videoToken) {
28497
+ this.livekit.publishLocalTracks();
28498
+ }
28499
+ } else {
28500
+ this.showPermissionModal(async () => {
28501
+ if (this.videoToken) {
28502
+ await this.livekit.publishLocalTracks();
28503
+ }
28504
+ });
28505
+ }
28332
28506
  });
28333
28507
  }
28334
28508
  } else if (status === WidgetStates.CONSULTATION) {
@@ -28337,28 +28511,41 @@ class ClinicWidget {
28337
28511
  if (endBtn) {
28338
28512
  endBtn.addEventListener("click", () => {
28339
28513
  if (this.livekit) {
28340
- this.livekit.disconnect();
28514
+ try {
28515
+ this.livekit.disconnect();
28516
+ } catch (err) {
28517
+ console.error("[Widget] Error disconnecting LiveKit:", err);
28518
+ }
28519
+ this.livekit = null;
28341
28520
  }
28342
28521
  store.setState({ status: WidgetStates.COMPLETED });
28343
28522
  });
28344
28523
  }
28345
- const sendBtn = this.shadowRoot.querySelector(".td-send-btn");
28346
- const chatInput = this.shadowRoot.querySelector(".td-chat-input");
28347
- const sendMessage = () => {
28348
- const text = chatInput.value.trim();
28349
- if (!text) return;
28350
- this.appendChatMessage(text, "own");
28351
- chatInput.value = "";
28524
+ const micBtn = this.shadowRoot.getElementById("td-btn-mic");
28525
+ const camBtn = this.shadowRoot.getElementById("td-btn-cam");
28526
+ let isMicOn = true;
28527
+ let isCamOn = true;
28528
+ const updateMediaBtn = (btn, isOn, iconOn, iconOff) => {
28529
+ if (!btn) return;
28530
+ btn.style.backgroundColor = isOn ? "rgba(255, 255, 255, 0.2)" : "#ef4444";
28531
+ btn.innerHTML = `<span class="material-symbols-outlined">${isOn ? iconOn : iconOff}</span>`;
28352
28532
  };
28353
- if (sendBtn) {
28354
- sendBtn.addEventListener("click", sendMessage);
28355
- }
28356
- if (chatInput) {
28357
- chatInput.addEventListener("keypress", (e2) => {
28358
- if (e2.key === "Enter" && !e2.shiftKey) {
28359
- e2.preventDefault();
28360
- sendMessage();
28533
+ if (micBtn) {
28534
+ micBtn.addEventListener("click", () => {
28535
+ isMicOn = !isMicOn;
28536
+ if (this.livekit) {
28537
+ this.livekit.toggleMicrophone(isMicOn);
28361
28538
  }
28539
+ updateMediaBtn(micBtn, isMicOn, "mic", "mic_off");
28540
+ });
28541
+ }
28542
+ if (camBtn) {
28543
+ camBtn.addEventListener("click", () => {
28544
+ isCamOn = !isCamOn;
28545
+ if (this.livekit) {
28546
+ this.livekit.toggleCamera(isCamOn);
28547
+ }
28548
+ updateMediaBtn(camBtn, isCamOn, "videocam", "videocam_off");
28362
28549
  });
28363
28550
  }
28364
28551
  } else if (status === WidgetStates.COMPLETED) {
@@ -28368,6 +28555,14 @@ class ClinicWidget {
28368
28555
  store.setState({ status: WidgetStates.COLLAPSED });
28369
28556
  });
28370
28557
  }
28558
+ const overlay = this.shadowRoot.querySelector(".td-overlay-wrapper");
28559
+ if (overlay) {
28560
+ overlay.addEventListener("click", (e2) => {
28561
+ if (e2.target === overlay) {
28562
+ store.setState({ status: WidgetStates.COLLAPSED });
28563
+ }
28564
+ });
28565
+ }
28371
28566
  }
28372
28567
  }
28373
28568
  triggerCallback(name, data) {
@@ -28399,10 +28594,18 @@ class ClinicWidget {
28399
28594
  el.innerHTML = getInlineTriggerHtml(state);
28400
28595
  const btn = el.querySelector("#td-inline-btn");
28401
28596
  if (btn) {
28597
+ if (!state.doctorOnline) {
28598
+ btn.style.opacity = "0.6";
28599
+ btn.style.cursor = "not-allowed";
28600
+ btn.style.background = "#9ca3af";
28601
+ btn.innerHTML = '<span class="material-symbols-outlined" style="font-size: 18px;">videocam_off</span> Bác sĩ Offline';
28602
+ }
28402
28603
  btn.addEventListener("click", () => {
28403
- btn.addEventListener("click", () => {
28404
- store.setState({ status: WidgetStates.PATIENT_FORM });
28405
- });
28604
+ if (!state.doctorOnline) {
28605
+ console.log("Bác Offline, không thể bắt đầu tư vấn.");
28606
+ return;
28607
+ }
28608
+ store.setState({ status: WidgetStates.PATIENT_FORM });
28406
28609
  });
28407
28610
  }
28408
28611
  } else {
@@ -28410,15 +28613,21 @@ class ClinicWidget {
28410
28613
  }
28411
28614
  }
28412
28615
  }
28616
+ /**
28617
+ * Helper to generate random ID for guests
28618
+ */
28619
+ generateGuestId() {
28620
+ return "guest_" + Math.random().toString(36).substr(2, 9);
28621
+ }
28413
28622
  startSocket() {
28414
28623
  const state = store.getState();
28415
28624
  const url2 = state.config.socketUrl || CONFIG.SOCKET_URL;
28416
- const { user, doctor } = state;
28625
+ const { user } = state;
28417
28626
  if (this.socket) return;
28418
28627
  this.socket = new WebSocketService(url2);
28419
28628
  this.socket.connect(() => {
28420
28629
  console.log("Socket connected, registering...");
28421
- const userId = "20.183299.4158";
28630
+ const userId = (user == null ? void 0 : user.id) || this.generateGuestId();
28422
28631
  this.socket.emit("register", {
28423
28632
  userId,
28424
28633
  userName: (user == null ? void 0 : user.name) || "Guest",
@@ -28426,51 +28635,81 @@ class ClinicWidget {
28426
28635
  });
28427
28636
  });
28428
28637
  this.socket.on("users-online", (users) => {
28429
- var _a;
28430
- console.log("[Widget] Received users-online event:", users);
28431
- const onlineUsers = Array.isArray(users) && Array.isArray(users[0]) ? users[0] : users;
28432
- const currentDoctorId = (_a = store.getState().doctor) == null ? void 0 : _a.id;
28433
- console.log("[Widget] Current Doctor ID (Config):", currentDoctorId);
28434
- if (currentDoctorId && Array.isArray(onlineUsers)) {
28435
- const doctorEntry = onlineUsers.find(
28436
- (u) => {
28437
- var _a2;
28438
- return String(u.userId) === String(currentDoctorId) && ((_a2 = u.role) == null ? void 0 : _a2.toUpperCase()) === "DOCTOR";
28439
- }
28440
- );
28441
- const isOnline = !!doctorEntry;
28442
- console.log("[Widget] Doctor Status:", isOnline ? "Online" : "Offline", "Doctor Entry:", doctorEntry);
28443
- store.setState({ doctorOnline: isOnline });
28444
- }
28638
+ this.handleOnlineUsers(users);
28445
28639
  });
28446
28640
  this.socket.on("customer:token", async (data) => {
28447
- console.log("Received token from server:", data);
28448
28641
  if (data.token) {
28449
28642
  if (!this.livekit) {
28450
28643
  this.livekit = new LiveKitService();
28451
28644
  }
28452
- const connectLiveKit = () => {
28453
- const modal = this.shadowRoot.querySelector(".td-consultation-modal");
28454
- if (modal) {
28455
- console.log("[Widget] LiveKit Params:", { url: CONFIG.LIVEKIT_URL, token: data.token });
28456
- this.livekit.connect(CONFIG.LIVEKIT_URL, data.token, modal);
28457
- } else {
28458
- console.error("Consultation modal not found for video rendering");
28459
- }
28460
- };
28461
- const granted = await this.livekit.checkPermissions();
28462
- if (granted) {
28463
- connectLiveKit();
28464
- } else {
28465
- this.showPermissionModal(connectLiveKit);
28466
- }
28645
+ this.videoToken = data.token;
28646
+ this.attemptVideoConnection();
28467
28647
  }
28468
28648
  });
28469
28649
  }
28650
+ /**
28651
+ * Handle users-online event from socket
28652
+ * @param {Array} users
28653
+ */
28654
+ handleOnlineUsers(users) {
28655
+ const onlineUsers = Array.isArray(users) && Array.isArray(users[0]) ? users[0] : users;
28656
+ store.setState({ onlineUsers });
28657
+ this.recheckDoctorStatus(onlineUsers);
28658
+ }
28659
+ /**
28660
+ * Re-evaluates if the configured doctor is online based on current state
28661
+ * @param {Array} [usersList] - Optional optimization to pass list if already known
28662
+ */
28663
+ recheckDoctorStatus(usersList) {
28664
+ var _a;
28665
+ const state = store.getState();
28666
+ const currentDoctorId = (_a = state.doctor) == null ? void 0 : _a.id;
28667
+ const onlineUsers = usersList || state.onlineUsers || [];
28668
+ if (!currentDoctorId) return;
28669
+ const doctorEntry = onlineUsers.find((u) => {
28670
+ const isIdMatch = String(u.userId) === String(currentDoctorId);
28671
+ u.role ? u.role.toUpperCase() === "DOCTOR" : true;
28672
+ return isIdMatch && (u.role ? u.role.toUpperCase() === "DOCTOR" : true);
28673
+ });
28674
+ const isOnline = !!doctorEntry;
28675
+ if (state.doctorOnline !== isOnline) {
28676
+ store.setState({ doctorOnline: isOnline });
28677
+ }
28678
+ }
28679
+ async attemptVideoConnection() {
28680
+ if (!this.videoToken || !this.livekit) {
28681
+ return;
28682
+ }
28683
+ const modal = this.shadowRoot.querySelector(".td-consultation-modal");
28684
+ if (!modal) {
28685
+ console.error("[Widget] Modal not found, retrying...");
28686
+ setTimeout(() => this.attemptVideoConnection(), 500);
28687
+ return;
28688
+ }
28689
+ if (!this.livekit.room || this.livekit.room.state === "disconnected") {
28690
+ console.log("[Widget] Connecting to LiveKit Room immediately...");
28691
+ this.livekit.connect(CONFIG.LIVEKIT_URL, this.videoToken, modal);
28692
+ }
28693
+ this.ensureLocalMedia();
28694
+ }
28695
+ async ensureLocalMedia() {
28696
+ const granted = await this.livekit.checkPermissions();
28697
+ if (granted) {
28698
+ console.log("[Widget] Permissions OK, publishing tracks...");
28699
+ await this.livekit.publishLocalTracks();
28700
+ } else {
28701
+ console.log("[Widget] Permissions missing, showing modal...");
28702
+ this.showPermissionModal(async () => {
28703
+ await this.livekit.publishLocalTracks();
28704
+ });
28705
+ }
28706
+ }
28470
28707
  joinVideoCall() {
28708
+ var _a;
28471
28709
  const state = store.getState();
28472
28710
  const { user, doctor } = state;
28473
28711
  const userId = "20.183299.4158";
28712
+ this.videoToken = null;
28474
28713
  if (this.socket) {
28475
28714
  console.log("Joining video call room with user:", user == null ? void 0 : user.name);
28476
28715
  this.socket.emit("register", {
@@ -28478,12 +28717,15 @@ class ClinicWidget {
28478
28717
  userName: user == null ? void 0 : user.name,
28479
28718
  role: "customer"
28480
28719
  });
28481
- this.socket.emit("customer:join", {
28720
+ const joinPayload = {
28482
28721
  participantId: userId,
28483
28722
  participantName: user.name,
28484
28723
  doctorId: doctor.id,
28724
+ productId: state.productId || ((_a = state.config) == null ? void 0 : _a.productId),
28485
28725
  type: "video"
28486
- });
28726
+ };
28727
+ console.log("[Widget] Emitting customer:join with payload:", joinPayload);
28728
+ this.socket.emit("customer:join", joinPayload);
28487
28729
  } else {
28488
28730
  console.error("Socket not connected, cannot join video call");
28489
28731
  }
@@ -28531,13 +28773,9 @@ class ClinicWidget {
28531
28773
  if (!this.livekit) {
28532
28774
  this.livekit = new LiveKitService();
28533
28775
  }
28534
- const ok = await this.livekit.requestPermissions();
28535
- if (ok) {
28536
- if (typeof onSuccessCallback === "function") {
28537
- onSuccessCallback();
28538
- }
28539
- } else {
28540
- console.warn("User denied permissions via browser prompt");
28776
+ await this.livekit.requestPermissions();
28777
+ if (typeof onSuccessCallback === "function") {
28778
+ onSuccessCallback();
28541
28779
  }
28542
28780
  };
28543
28781
  if (closeBtn) closeBtn.addEventListener("click", closeModal);
@@ -28552,8 +28790,8 @@ class ClinicWidget {
28552
28790
  const locale = currentScript ? currentScript.getAttribute("data-locale") || "vi" : "vi";
28553
28791
  const storeId = currentScript ? currentScript.getAttribute("data-store-id") || "longvan-store" : "longvan-store";
28554
28792
  const orgId = currentScript ? currentScript.getAttribute("data-org-id") || "longvan-org" : "longvan-org";
28555
- const doctorId = currentScript ? currentScript.getAttribute("data-doctor-id") || "20.184041.6561" : "20.184041.6561";
28556
- const productId = currentScript ? currentScript.getAttribute("data-product-id") || "product-1" : "product-1";
28793
+ const doctorId = currentScript ? currentScript.getAttribute("data-doctor-id") : null;
28794
+ const productId = currentScript ? currentScript.getAttribute("data-product-id") : null;
28557
28795
  const config = {
28558
28796
  widgetId,
28559
28797
  triggerElementId,