clinic-connect-widget 1.0.3 → 1.0.4

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-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
@@ -124,63 +124,65 @@ const collapsedHtml = `<div class="td-widget-fab" id="td-widget-fab">\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="td-online-badge">\r
152
+ <span class="td-pulse"></span>\r
153
+ <span class="td-badge-text">Đang trực tuyến</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">\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
@@ -192,15 +194,15 @@ const consultationHtml = `<div class="td-consultation-modal">\r
192
194
  <div class="td-overlay"></div>\r
193
195
  \r
194
196
  <div class="td-video-header">\r
195
- <div class="td-badge-glass">\r
197
+ <!-- <div class="td-badge-glass">\r
196
198
  <span class="material-symbols-outlined"\r
197
199
  style="color: #ef4444; font-size: 16px;">radio_button_checked</span>\r
198
200
  REC\r
199
- </div>\r
200
- <div class="td-badge-glass">\r
201
+ </div> -->\r
202
+ <!-- <div class="td-badge-glass">\r
201
203
  <span class="material-symbols-outlined" style="font-size: 16px;">signal_cellular_alt</span>\r
202
204
  HD Quality\r
203
- </div>\r
205
+ </div> -->\r
204
206
  </div>\r
205
207
  \r
206
208
  <div class="td-pip"\r
@@ -209,10 +211,10 @@ const consultationHtml = `<div class="td-consultation-modal">\r
209
211
  </div>\r
210
212
  \r
211
213
  <div class="td-controls">\r
212
- <button class="td-ctrl-btn">\r
214
+ <button class="td-ctrl-btn" id="td-btn-mic">\r
213
215
  <span class="material-symbols-outlined">mic</span>\r
214
216
  </button>\r
215
- <button class="td-ctrl-btn">\r
217
+ <button class="td-ctrl-btn" id="td-btn-cam">\r
216
218
  <span class="material-symbols-outlined">videocam</span>\r
217
219
  </button>\r
218
220
  <button class="td-ctrl-end" id="td-consult-end">\r
@@ -225,127 +227,78 @@ const consultationHtml = `<div class="td-consultation-modal">\r
225
227
  </div>\r
226
228
  </div>\r
227
229
  \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
230
  </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
231
+ 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>';
232
+ const postConsultationHtml = `<div class="td-overlay-wrapper">\r
233
+ <div class="td-modal-centered" id="td-summary-view">\r
234
+ <header class="td-header">\r
235
+ <h2 class="td-header-title" style="font-size: 16px;">Tổng kết tư vấn</h2>\r
236
+ <button class="td-close-btn" id="td-summary-close">\r
237
+ <span class="material-symbols-outlined">close</span>\r
238
+ </button>\r
239
+ </header>\r
288
240
  \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
241
+ <div class="td-content">\r
242
+ <div class="td-summary-container">\r
243
+ <div class="td-success-header">\r
244
+ <div class="td-success-icon">\r
245
+ <span class="material-symbols-outlined" style="font-size: 48px;">check_circle</span>\r
246
+ </div>\r
247
+ <h3 class="td-summary-title">Tư vấn Hoàn tất</h3>\r
248
+ <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
249
  </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
250
  \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
251
+ <div class="td-summary-card">\r
252
+ <div class="td-summary-avatar" style="background-image: url('{{doctorAvatar}}');"></div>\r
253
+ <div>\r
254
+ <div style="font-weight: 700;">{{doctorName}}</div>\r
255
+ <div style="color: var(--td-text-sub); font-size: 14px;">{{doctorSpecialty}}</div>\r
256
+ </div>\r
257
+ <div style="margin-left: auto; color: var(--td-primary);">\r
258
+ <span class="material-symbols-outlined">verified_user</span>\r
259
+ </div>\r
305
260
  </div>\r
306
- <div style="margin-left: auto; color: var(--td-primary);">\r
307
- <span class="material-symbols-outlined">verified_user</span>\r
308
- </div>\r
309
- </div>\r
310
261
  \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
262
+ <div class="td-summary-details">\r
263
+ <div class="td-detail-row">\r
264
+ <span style="color: var(--td-text-sub); display: flex; gap: 6px;">\r
265
+ <span class="material-symbols-outlined" style="font-size: 18px;">tag</span> Mã tư vấn\r
266
+ </span>\r
267
+ <span style="font-weight: 600;">#TD-93821</span>\r
268
+ </div>\r
269
+ <div class="td-detail-row">\r
270
+ <span style="color: var(--td-text-sub); display: flex; gap: 6px;">\r
271
+ <span class="material-symbols-outlined" style="font-size: 18px;">schedule</span> Thời gian\r
272
+ </span>\r
273
+ <span style="font-weight: 600;">10:00 AM, Today</span>\r
274
+ </div>\r
317
275
  </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
276
  \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
277
+ <div class="td-note-box">\r
278
+ <div class="td-note-title">\r
279
+ <span class="material-symbols-outlined"\r
280
+ style="font-size: 16px; color: var(--td-primary);">clinical_notes</span>\r
281
+ Ghi chú bác sĩ\r
282
+ </div>\r
283
+ <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
284
+ vận\r
285
+ động mạnh trong 2 ngày tới.</p>\r
331
286
  </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
287
  \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
288
+ <div class="td-actions">\r
289
+ <div class="td-grid-actions">\r
290
+ <button class="td-btn td-btn-outline" style="font-size: 12px; height: 40px; margin-bottom: 0;">\r
291
+ Xem kết luận\r
292
+ </button>\r
293
+ <button class="td-btn td-btn-outline" style="font-size: 12px; height: 40px; margin-bottom: 0;">\r
294
+ Tải đơn thuốc\r
295
+ </button>\r
296
+ </div>\r
297
+ <button class="td-btn td-btn-primary">\r
298
+ <span class="material-symbols-outlined" style="margin-right: 8px;">calendar_month</span>\r
299
+ Đặt lịch tái khám\r
343
300
  </button>\r
344
301
  </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
349
302
  </div>\r
350
303
  </div>\r
351
304
  </div>\r
@@ -28019,29 +27972,44 @@ class LiveKitService {
28019
27972
  constructor() {
28020
27973
  this.room = null;
28021
27974
  this.wrapper = null;
28022
- this.preAcquiredTracks = [];
27975
+ this.isPermissionGranted = false;
27976
+ this.permissionPromise = null;
28023
27977
  }
28024
27978
  /**
28025
27979
  * Request Camera & Mic permissions early
28026
27980
  */
28027
27981
  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;
27982
+ if (this.permissionPromise) {
27983
+ console.log("[LiveKit] Joining existing permission request...");
27984
+ return this.permissionPromise;
28038
27985
  }
27986
+ this.permissionPromise = (async () => {
27987
+ try {
27988
+ console.log("[LiveKit] Requesting early permissions...");
27989
+ const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
27990
+ stream.getTracks().forEach((t) => t.stop());
27991
+ this.isPermissionGranted = true;
27992
+ console.log("[LiveKit] Permissions primed successfully.");
27993
+ return true;
27994
+ } catch (error) {
27995
+ console.error("[LiveKit] Permission request failed:", error);
27996
+ console.log(`Không thể xin quyền: ${error.name} - ${error.message}`);
27997
+ return false;
27998
+ } finally {
27999
+ this.permissionPromise = null;
28000
+ }
28001
+ })();
28002
+ return this.permissionPromise;
28039
28003
  }
28040
28004
  /**
28041
28005
  * Check if permissions are already granted
28042
28006
  * @returns {Promise<boolean>}
28043
28007
  */
28044
28008
  async checkPermissions() {
28009
+ if (this.isPermissionGranted) {
28010
+ console.log("[LiveKit] Permission flag is true.");
28011
+ return true;
28012
+ }
28045
28013
  if (!navigator.permissions || !navigator.permissions.query) {
28046
28014
  return false;
28047
28015
  }
@@ -28079,7 +28047,6 @@ class LiveKitService {
28079
28047
  try {
28080
28048
  await this.room.connect(url2, token);
28081
28049
  console.log("[LiveKit] Connected to room:", this.room.name);
28082
- await this.publishLocalTracks();
28083
28050
  } catch (error) {
28084
28051
  console.error("[LiveKit] Connection failed:", error);
28085
28052
  }
@@ -28101,23 +28068,9 @@ class LiveKitService {
28101
28068
  async publishLocalTracks() {
28102
28069
  const localContainer = this.wrapper.querySelector(".td-pip");
28103
28070
  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
- }
28071
+ await this.room.localParticipant.setCameraEnabled(true);
28072
+ await this.room.localParticipant.setMicrophoneEnabled(true);
28073
+ const videoPub = Array.from(this.room.localParticipant.videoTrackPublications.values()).find((p) => p.source === "camera");
28121
28074
  if (videoPub && videoPub.track && localContainer) {
28122
28075
  localContainer.innerHTML = "";
28123
28076
  localContainer.style.backgroundImage = "none";
@@ -28134,9 +28087,9 @@ class LiveKitService {
28134
28087
  } catch (e2) {
28135
28088
  console.error("[LiveKit] Failed to publish tracks:", e2);
28136
28089
  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ư.");
28090
+ 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
28091
  } else {
28139
- alert(`Lỗi Camera: ${e2.message}`);
28092
+ console.log(`Lỗi Camera: ${e2.message}`);
28140
28093
  }
28141
28094
  }
28142
28095
  }
@@ -28164,14 +28117,66 @@ class LiveKitService {
28164
28117
  console.log("[LiveKit] Remote audio attached");
28165
28118
  }
28166
28119
  }
28120
+ toggleMicrophone(enabled) {
28121
+ if (this.room && this.room.localParticipant) {
28122
+ this.room.localParticipant.setMicrophoneEnabled(enabled);
28123
+ console.log(`[LiveKit] Microphone ${enabled ? "enabled" : "disabled"}`);
28124
+ }
28125
+ }
28126
+ toggleCamera(enabled) {
28127
+ if (this.room && this.room.localParticipant) {
28128
+ this.room.localParticipant.setCameraEnabled(enabled);
28129
+ console.log(`[LiveKit] Camera ${enabled ? "enabled" : "disabled"}`);
28130
+ }
28131
+ }
28167
28132
  handleTrackUnsubscribed(track, participant) {
28168
28133
  track.detach().forEach((element) => element.remove());
28169
28134
  }
28170
28135
  disconnect() {
28136
+ var _a, _b;
28171
28137
  if (this.room) {
28172
- this.room.disconnect();
28138
+ if (this.room.localParticipant) {
28139
+ try {
28140
+ const allTracks = [
28141
+ ...((_a = this.room.localParticipant.videoTrackPublications) == null ? void 0 : _a.values()) || [],
28142
+ ...((_b = this.room.localParticipant.audioTrackPublications) == null ? void 0 : _b.values()) || []
28143
+ ];
28144
+ allTracks.forEach((publication) => {
28145
+ if (publication.track) {
28146
+ publication.track.stop();
28147
+ publication.track.detach().forEach((el) => el.remove());
28148
+ console.log(`[LiveKit] Stopped local track: ${publication.kind}`);
28149
+ }
28150
+ });
28151
+ } catch (err) {
28152
+ console.error("[LiveKit] Error stopping tracks:", err);
28153
+ }
28154
+ }
28155
+ try {
28156
+ this.room.disconnect();
28157
+ } catch (err) {
28158
+ console.error("[LiveKit] Error disconnecting room:", err);
28159
+ }
28173
28160
  this.room = null;
28174
28161
  }
28162
+ if (this.preAcquiredTracks) {
28163
+ this.preAcquiredTracks.forEach((t) => {
28164
+ try {
28165
+ t.stop();
28166
+ } catch (e2) {
28167
+ }
28168
+ });
28169
+ this.preAcquiredTracks = [];
28170
+ }
28171
+ if (this.wrapper) {
28172
+ const localContainer = this.wrapper.querySelector(".td-pip");
28173
+ const remoteContainer = this.wrapper.querySelector(".td-main-video");
28174
+ if (localContainer) localContainer.innerHTML = "";
28175
+ if (remoteContainer) {
28176
+ remoteContainer.innerHTML = "";
28177
+ remoteContainer.style.backgroundImage = "";
28178
+ }
28179
+ }
28175
28180
  }
28176
28181
  }
28177
28182
  class ClinicWidget {
@@ -28187,9 +28192,11 @@ class ClinicWidget {
28187
28192
  }
28188
28193
  async init() {
28189
28194
  var _a;
28195
+ this.videoToken = null;
28190
28196
  this.mount();
28191
28197
  this.unsubscribe = store.on(EventTypes.STATE_CHANGE, this.handleStateChange.bind(this));
28192
28198
  store.setState({ status: WidgetStates.LOADING });
28199
+ this.startSocket();
28193
28200
  try {
28194
28201
  const response = await this.api.init(this.config.widgetId);
28195
28202
  if (response.success) {
@@ -28208,7 +28215,6 @@ class ClinicWidget {
28208
28215
  // Default open state
28209
28216
  });
28210
28217
  this.checkInlineTrigger();
28211
- this.startSocket();
28212
28218
  } else {
28213
28219
  store.setState({ status: WidgetStates.IDLE, error: "Init failed" });
28214
28220
  }
@@ -28303,6 +28309,14 @@ class ClinicWidget {
28303
28309
  store.setState({ status: WidgetStates.COLLAPSED });
28304
28310
  });
28305
28311
  }
28312
+ const overlay = this.shadowRoot.querySelector(".td-overlay-wrapper");
28313
+ if (overlay) {
28314
+ overlay.addEventListener("click", (e2) => {
28315
+ if (e2.target === overlay) {
28316
+ store.setState({ status: WidgetStates.COLLAPSED });
28317
+ }
28318
+ });
28319
+ }
28306
28320
  if (consultBtn) {
28307
28321
  consultBtn.addEventListener("click", () => {
28308
28322
  consultBtn.addEventListener("click", () => {
@@ -28318,6 +28332,14 @@ class ClinicWidget {
28318
28332
  store.setState({ status: WidgetStates.EXPANDED });
28319
28333
  });
28320
28334
  }
28335
+ const overlay = this.shadowRoot.querySelector(".td-overlay-wrapper");
28336
+ if (overlay) {
28337
+ overlay.addEventListener("click", (e2) => {
28338
+ if (e2.target === overlay) {
28339
+ store.setState({ status: WidgetStates.EXPANDED });
28340
+ }
28341
+ });
28342
+ }
28321
28343
  if (form) {
28322
28344
  form.addEventListener("submit", async (e2) => {
28323
28345
  e2.preventDefault();
@@ -28329,6 +28351,23 @@ class ClinicWidget {
28329
28351
  });
28330
28352
  this.triggerCallback("onConsultationStarted", { name, phone });
28331
28353
  store.setState({ status: WidgetStates.CONSULTATION });
28354
+ if (!this.livekit) {
28355
+ this.livekit = new LiveKitService();
28356
+ }
28357
+ const alreadyGranted = await this.livekit.checkPermissions();
28358
+ if (alreadyGranted) {
28359
+ console.log("[Widget] Permissions already granted, warming up...");
28360
+ await this.livekit.requestPermissions();
28361
+ if (this.videoToken) {
28362
+ this.livekit.publishLocalTracks();
28363
+ }
28364
+ } else {
28365
+ this.showPermissionModal(async () => {
28366
+ if (this.videoToken) {
28367
+ await this.livekit.publishLocalTracks();
28368
+ }
28369
+ });
28370
+ }
28332
28371
  });
28333
28372
  }
28334
28373
  } else if (status === WidgetStates.CONSULTATION) {
@@ -28337,28 +28376,41 @@ class ClinicWidget {
28337
28376
  if (endBtn) {
28338
28377
  endBtn.addEventListener("click", () => {
28339
28378
  if (this.livekit) {
28340
- this.livekit.disconnect();
28379
+ try {
28380
+ this.livekit.disconnect();
28381
+ } catch (err) {
28382
+ console.error("[Widget] Error disconnecting LiveKit:", err);
28383
+ }
28384
+ this.livekit = null;
28341
28385
  }
28342
28386
  store.setState({ status: WidgetStates.COMPLETED });
28343
28387
  });
28344
28388
  }
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 = "";
28389
+ const micBtn = this.shadowRoot.getElementById("td-btn-mic");
28390
+ const camBtn = this.shadowRoot.getElementById("td-btn-cam");
28391
+ let isMicOn = true;
28392
+ let isCamOn = true;
28393
+ const updateMediaBtn = (btn, isOn, iconOn, iconOff) => {
28394
+ if (!btn) return;
28395
+ btn.style.backgroundColor = isOn ? "rgba(255, 255, 255, 0.2)" : "#ef4444";
28396
+ btn.innerHTML = `<span class="material-symbols-outlined">${isOn ? iconOn : iconOff}</span>`;
28352
28397
  };
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();
28398
+ if (micBtn) {
28399
+ micBtn.addEventListener("click", () => {
28400
+ isMicOn = !isMicOn;
28401
+ if (this.livekit) {
28402
+ this.livekit.toggleMicrophone(isMicOn);
28361
28403
  }
28404
+ updateMediaBtn(micBtn, isMicOn, "mic", "mic_off");
28405
+ });
28406
+ }
28407
+ if (camBtn) {
28408
+ camBtn.addEventListener("click", () => {
28409
+ isCamOn = !isCamOn;
28410
+ if (this.livekit) {
28411
+ this.livekit.toggleCamera(isCamOn);
28412
+ }
28413
+ updateMediaBtn(camBtn, isCamOn, "videocam", "videocam_off");
28362
28414
  });
28363
28415
  }
28364
28416
  } else if (status === WidgetStates.COMPLETED) {
@@ -28368,6 +28420,14 @@ class ClinicWidget {
28368
28420
  store.setState({ status: WidgetStates.COLLAPSED });
28369
28421
  });
28370
28422
  }
28423
+ const overlay = this.shadowRoot.querySelector(".td-overlay-wrapper");
28424
+ if (overlay) {
28425
+ overlay.addEventListener("click", (e2) => {
28426
+ if (e2.target === overlay) {
28427
+ store.setState({ status: WidgetStates.COLLAPSED });
28428
+ }
28429
+ });
28430
+ }
28371
28431
  }
28372
28432
  }
28373
28433
  triggerCallback(name, data) {
@@ -28449,28 +28509,44 @@ class ClinicWidget {
28449
28509
  if (!this.livekit) {
28450
28510
  this.livekit = new LiveKitService();
28451
28511
  }
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
- }
28512
+ this.videoToken = data.token;
28513
+ this.attemptVideoConnection();
28467
28514
  }
28468
28515
  });
28469
28516
  }
28517
+ async attemptVideoConnection() {
28518
+ if (!this.videoToken || !this.livekit) {
28519
+ return;
28520
+ }
28521
+ const modal = this.shadowRoot.querySelector(".td-consultation-modal");
28522
+ if (!modal) {
28523
+ console.error("[Widget] Modal not found, retrying...");
28524
+ setTimeout(() => this.attemptVideoConnection(), 500);
28525
+ return;
28526
+ }
28527
+ if (!this.livekit.room || this.livekit.room.state === "disconnected") {
28528
+ console.log("[Widget] Connecting to LiveKit Room immediately...");
28529
+ this.livekit.connect(CONFIG.LIVEKIT_URL, this.videoToken, modal);
28530
+ }
28531
+ this.ensureLocalMedia();
28532
+ }
28533
+ async ensureLocalMedia() {
28534
+ const granted = await this.livekit.checkPermissions();
28535
+ if (granted) {
28536
+ console.log("[Widget] Permissions OK, publishing tracks...");
28537
+ await this.livekit.publishLocalTracks();
28538
+ } else {
28539
+ console.log("[Widget] Permissions missing, showing modal...");
28540
+ this.showPermissionModal(async () => {
28541
+ await this.livekit.publishLocalTracks();
28542
+ });
28543
+ }
28544
+ }
28470
28545
  joinVideoCall() {
28471
28546
  const state = store.getState();
28472
28547
  const { user, doctor } = state;
28473
28548
  const userId = "20.183299.4158";
28549
+ this.videoToken = null;
28474
28550
  if (this.socket) {
28475
28551
  console.log("Joining video call room with user:", user == null ? void 0 : user.name);
28476
28552
  this.socket.emit("register", {
@@ -28478,12 +28554,16 @@ class ClinicWidget {
28478
28554
  userName: user == null ? void 0 : user.name,
28479
28555
  role: "customer"
28480
28556
  });
28481
- this.socket.emit("customer:join", {
28557
+ const joinPayload = {
28482
28558
  participantId: userId,
28483
28559
  participantName: user.name,
28484
28560
  doctorId: doctor.id,
28561
+ productId: "147365",
28562
+ // Hardcoded as requested
28485
28563
  type: "video"
28486
- });
28564
+ };
28565
+ console.log("[Widget] Emitting customer:join with payload:", joinPayload);
28566
+ this.socket.emit("customer:join", joinPayload);
28487
28567
  } else {
28488
28568
  console.error("Socket not connected, cannot join video call");
28489
28569
  }
@@ -28531,13 +28611,9 @@ class ClinicWidget {
28531
28611
  if (!this.livekit) {
28532
28612
  this.livekit = new LiveKitService();
28533
28613
  }
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");
28614
+ await this.livekit.requestPermissions();
28615
+ if (typeof onSuccessCallback === "function") {
28616
+ onSuccessCallback();
28541
28617
  }
28542
28618
  };
28543
28619
  if (closeBtn) closeBtn.addEventListener("click", closeModal);