clinic-connect-widget 1.0.1 → 1.0.3

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.
@@ -105,355 +105,356 @@ const CONFIG = {
105
105
  LIVEKIT_URL: "wss://livekit.longvan.vn"
106
106
  };
107
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}';
108
- const collapsedHtml = `<div class="td-widget-fab" id="td-widget-fab">
109
- <div class="td-fab-card">
110
- <button class="td-close-fab" id="td-fab-close">
111
- <span class="material-symbols-outlined" style="font-size: 16px;">close</span>
112
- </button>
113
- <div class="td-avatar-container">
114
- <div class="td-avatar" style="background-image: url('{{doctorAvatar}}');"></div>
115
- <div class="td-status-dot" style="background: #22c55e;"></div>
116
- </div>
117
- <div class="td-info">
118
- <div class="td-brand">
119
- <span class="material-symbols-outlined" style="font-size: 14px;">local_hospital</span>
120
- <span>Clinic</span>
121
- </div>
122
- <h3 class="td-cta">Khám online ngay</h3>
123
- <p class="td-sub">Bác sĩ đang Online</p>
124
- </div>
125
- </div>
108
+ const collapsedHtml = `<div class="td-widget-fab" id="td-widget-fab">\r
109
+ <div class="td-fab-card">\r
110
+ <button class="td-close-fab" id="td-fab-close">\r
111
+ <span class="material-symbols-outlined" style="font-size: 16px;">close</span>\r
112
+ </button>\r
113
+ <div class="td-avatar-container">\r
114
+ <div class="td-avatar" style="background-image: url('{{doctorAvatar}}');"></div>\r
115
+ <div class="td-status-dot" style="background: #22c55e;"></div>\r
116
+ </div>\r
117
+ <div class="td-info">\r
118
+ <div class="td-brand">\r
119
+ <span class="material-symbols-outlined" style="font-size: 14px;">local_hospital</span>\r
120
+ <span>Tư vấn ngay</span>\r
121
+ </div>\r
122
+ <h3 class="td-cta">Khám online ngay</h3>\r
123
+ <p class="td-sub">Bác sĩ đang Online</p>\r
124
+ </div>\r
125
+ </div>\r
126
126
  </div>`;
127
- const expandedHtml = `<div class="td-widget-expanded" id="td-widget-expanded">
128
- <header class="td-header">
129
- <div class="td-header-title">
130
- <div class="td-icon-box">
131
- <span class="material-symbols-outlined">medical_services</span>
132
- </div>
133
- <span>Clinic</span>
134
- </div>
135
- <button class="td-close-btn" id="td-expanded-close">
136
- <span class="material-symbols-outlined">close</span>
137
- </button>
138
- </header>
139
-
140
- <div class="td-content">
141
- <div class="td-doctor-profile">
142
- <div class="td-doctor-avatar" style="background-image: url('{{doctorAvatar}}');">
143
- <div class="td-status-dot"
144
- style="width: 20px; height: 20px; border-width: 3px; bottom: 4px; right: 4px; background: #22c55e;">
145
- </div>
146
- </div>
147
- <h1 class="td-doctor-name">{{doctorName}}</h1>
148
- <p class="td-doctor-specialty">{{doctorSpecialty}}</p>
149
-
150
- <div class="td-online-badge">
151
- <span class="td-pulse"></span>
152
- <span class="td-badge-text">Đang trực tuyến</span>
153
- </div>
154
- </div>
155
-
156
- <div class="td-trust-grid">
157
- <div class="td-trust-item">
158
- <div class="td-icon-box">
159
- <span class="material-symbols-outlined">verified</span>
160
- </div>
161
- <h3 class="td-trust-title">Bác sĩ được xác thực</h3>
162
- </div>
163
- <div class="td-trust-item">
164
- <div class="td-icon-box">
165
- <span class="material-symbols-outlined">security</span>
166
- </div>
167
- <h3 class="td-trust-title">Bảo mật thông tin</h3>
168
- </div>
169
- </div>
170
-
171
- <div class="td-actions">
172
- <button class="td-btn td-btn-primary" id="td-btn-consult">
173
- <span class="material-symbols-outlined" style="margin-right: 8px;">videocam</span>
174
- Bắt đầu tư vấn ngay
175
- </button>
176
- <button class="td-btn td-btn-outline" id="td-btn-schedule">
177
- Đặt lịch hẹn sau
178
- </button>
179
- </div>
180
- </div>
181
-
182
- <div class="td-footer">
183
- Powered by <b>Clinic Connect</b>
184
- </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
132
+ </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
139
+ \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
145
+ </div>\r
146
+ </div>\r
147
+ <h1 class="td-doctor-name">{{doctorName}}</h1>\r
148
+ <p class="td-doctor-specialty">{{doctorSpecialty}}</p>\r
149
+ \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
153
+ </div>\r
154
+ </div>\r
155
+ \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
160
+ </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
166
+ </div>\r
167
+ <h3 class="td-trust-title">Bảo mật thông tin</h3>\r
168
+ </div>\r
169
+ </div>\r
170
+ \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
179
+ </div>\r
180
+ </div>\r
181
+ \r
182
+ <div class="td-footer">\r
183
+ Powered by <b>Clinic Connect</b>\r
184
+ </div>\r
185
185
  </div>`;
186
- const consultationHtml = `<div class="td-consultation-modal">
187
- <!-- Left: Video Panel -->
188
- <div class="td-video-panel">
189
- <div class="td-main-video"
190
- style="background-image: url('https://lh3.googleusercontent.com/aida-public/AB6AXuC54B68xwAiOWZFQNVMIVKNBTKwo-nPCTsEgkaekhx9PbGDRCwcy18cFeALhbiaeOyaIS88c9RZst2sRWUyKTeSif-HqEfgU5Aoht-mk6Tjtuts2M4XmoMaxNSAi7qjKoQRYx61I4MolGfjCkxRN0lW5rlSlQfMwY0T8K1440SLM9hW_4u0N1Vg6kQTcSUitouGRmWktBHr-yH5QTu78AC1IOZLd5-HyWw0NUb6uESPpNZoZHoIEKIjFhsGBnCRmzHCUP9Ex6mP0XvW');">
191
- </div>
192
- <div class="td-overlay"></div>
193
-
194
- <div class="td-video-header">
195
- <div class="td-badge-glass">
196
- <span class="material-symbols-outlined"
197
- style="color: #ef4444; font-size: 16px;">radio_button_checked</span>
198
- REC
199
- </div>
200
- <div class="td-badge-glass">
201
- <span class="material-symbols-outlined" style="font-size: 16px;">signal_cellular_alt</span>
202
- HD Quality
203
- </div>
204
- </div>
205
-
206
- <div class="td-pip"
207
- style="background-image: url('https://lh3.googleusercontent.com/aida-public/AB6AXuDnITTxxwp-J6t7xkVqnh6C9NPQS8i_k6_4F4IfzTfDM_ZgdX2HRvc947W1BU0LJYKOAA8qtHglPXseoj0Vk4sLfoRnKDOz88bUmK_ytSEvP_oYeeDKGRCfcEa6ugQO6aKR2l9W8glr63dswTODHe4am5IB3FRknTxNnZ0XuDU0z1xzkDMGpspRrXWwdoi8j9teleBeRsmkCdSe8dVb0q1r5eS8YjQ-nzo5f3qBB2-Eo9zyegUSaOCXhuaqSmqpVyICmTLvoIppbKNo');">
208
- <span class="td-pip-label">You</span>
209
- </div>
210
-
211
- <div class="td-controls">
212
- <button class="td-ctrl-btn">
213
- <span class="material-symbols-outlined">mic</span>
214
- </button>
215
- <button class="td-ctrl-btn">
216
- <span class="material-symbols-outlined">videocam</span>
217
- </button>
218
- <button class="td-ctrl-end" id="td-consult-end">
219
- <span class="material-symbols-outlined">call_end</span>
220
- Kết thúc
221
- </button>
222
- <button class="td-ctrl-btn">
223
- <span class="material-symbols-outlined">settings</span>
224
- </button>
225
- </div>
226
- </div>
227
-
228
- <!-- Right: Chat Panel -->
229
- <div class="td-chat-panel">
230
- <div class="td-chat-header">
231
- <div class="td-chat-doc-info">
232
- <div class="td-chat-avatar" style="background-image: url('{{doctorAvatar}}');"></div>
233
- <div>
234
- <h3 style="font-size: 16px; font-weight: 700;">{{doctorName}}</h3>
235
- <p style="font-size: 12px; color: var(--td-text-sub);">Online • Verified</p>
236
- </div>
237
- </div>
238
- <div class="td-chat-timer">
239
- <div style="display: flex; align-items: center; gap: 4px; font-size: 12px;">
240
- <span class="material-symbols-outlined"
241
- style="font-size: 16px; color: var(--td-primary);">timer</span>
242
- SESSION
243
- </div>
244
- <div class="td-timer-val">00:03:12</div>
245
- </div>
246
- </div>
247
-
248
- <div class="td-chat-messages">
249
- <div class="td-msg-system">
250
- Consultation started
251
- </div>
252
- <div class="td-msg-row doctor">
253
- <div class="td-msg-bubble">
254
- Xin chào. Tôi có thể giúp gì cho bạn hôm nay?
255
- <div class="td-msg-time">14:01</div>
256
- </div>
257
- </div>
258
- <div class="td-msg-row own">
259
- <div class="td-msg-bubble">
260
- Chào bác sĩ, tôi bị đau đầu...
261
- <div class="td-msg-time">14:02</div>
262
- </div>
263
- </div>
264
- </div>
265
-
266
- <div class="td-chat-input-area">
267
- <div class="td-input-wrapper">
268
- <textarea class="td-chat-input" placeholder="Nhập tin nhắn..."></textarea>
269
- <button class="td-send-btn">
270
- <span class="material-symbols-outlined">send</span>
271
- </button>
272
- </div>
273
- <div style="text-align: center; margin-top: 8px; font-size: 10px; color: var(--td-text-sub);">
274
- <span class="material-symbols-outlined" style="font-size: 10px; vertical-align: middle;">lock</span>
275
- Mã hóa đầu cuối
276
- </div>
277
- </div>
278
- </div>
186
+ const consultationHtml = `<div class="td-consultation-modal">\r
187
+ <!-- Left: Video Panel -->\r
188
+ <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>\r
192
+ <div class="td-overlay"></div>\r
193
+ \r
194
+ <div class="td-video-header">\r
195
+ <div class="td-badge-glass">\r
196
+ <span class="material-symbols-outlined"\r
197
+ style="color: #ef4444; font-size: 16px;">radio_button_checked</span>\r
198
+ REC\r
199
+ </div>\r
200
+ <div class="td-badge-glass">\r
201
+ <span class="material-symbols-outlined" style="font-size: 16px;">signal_cellular_alt</span>\r
202
+ HD Quality\r
203
+ </div>\r
204
+ </div>\r
205
+ \r
206
+ <div class="td-pip"\r
207
+ style="background-image: url('https://lh3.googleusercontent.com/aida-public/AB6AXuDnITTxxwp-J6t7xkVqnh6C9NPQS8i_k6_4F4IfzTfDM_ZgdX2HRvc947W1BU0LJYKOAA8qtHglPXseoj0Vk4sLfoRnKDOz88bUmK_ytSEvP_oYeeDKGRCfcEa6ugQO6aKR2l9W8glr63dswTODHe4am5IB3FRknTxNnZ0XuDU0z1xzkDMGpspRrXWwdoi8j9teleBeRsmkCdSe8dVb0q1r5eS8YjQ-nzo5f3qBB2-Eo9zyegUSaOCXhuaqSmqpVyICmTLvoIppbKNo');">\r
208
+ <span class="td-pip-label">You</span>\r
209
+ </div>\r
210
+ \r
211
+ <div class="td-controls">\r
212
+ <button class="td-ctrl-btn">\r
213
+ <span class="material-symbols-outlined">mic</span>\r
214
+ </button>\r
215
+ <button class="td-ctrl-btn">\r
216
+ <span class="material-symbols-outlined">videocam</span>\r
217
+ </button>\r
218
+ <button class="td-ctrl-end" id="td-consult-end">\r
219
+ <span class="material-symbols-outlined">call_end</span>\r
220
+ Kết thúc\r
221
+ </button>\r
222
+ <button class="td-ctrl-btn">\r
223
+ <span class="material-symbols-outlined">settings</span>\r
224
+ </button>\r
225
+ </div>\r
226
+ </div>\r
227
+ \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
279
  </div>`;
280
- const patientFormHtml = '<div class="td-widget-expanded" id="td-patient-form">\n <header class="td-header">\n <div class="td-header-title">\n <div class="td-icon-box">\n <span class="material-symbols-outlined">medical_services</span>\n </div>\n <span>Clinic</span>\n </div>\n <button class="td-close-btn" id="td-form-close">\n <span class="material-symbols-outlined">close</span>\n </button>\n </header>\n\n <div class="td-content">\n <div class="td-form-container">\n <div class="td-form-page-header">\n <h1 class="td-form-title">Kết nối với Bác sĩ</h1>\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>\n </div>\n\n <form class="td-form" id="td-info-form">\n <div class="td-input-group">\n <label class="td-label">Họ và tên</label>\n <input type="text" class="td-input" name="name" placeholder="VD: Nguyễn Văn A" required />\n </div>\n\n <div class="td-input-group">\n <label class="td-label">Số điện thoại</label>\n <div style="position: relative;">\n <input type="tel" class="td-input" name="phone" placeholder="VD: 0912 345 678" required />\n <div\n style="position: absolute; right: 12px; top: 50%; transform: translateY(-50%); color: var(--td-primary);">\n <span class="material-symbols-outlined" style="font-size: 20px;">smartphone</span>\n </div>\n </div>\n </div>\n\n <div class="td-input-group">\n <label class="td-label">Triệu chứng của bạn?</label>\n <div class="td-chips-group">\n <button type="button" class="td-chip active">Sốt <span class="material-symbols-outlined"\n style="font-size: 16px;">check</span></button>\n <button type="button" class="td-chip">Ho / Cảm cúm</button>\n <button type="button" class="td-chip">Đau đầu</button>\n <button type="button" class="td-chip">Đau bụng</button>\n </div>\n </div>\n\n <div class="td-input-group">\n <label class="td-label">Mô tả thêm <span style="color: var(--td-text-sub); font-weight: 400;">(Tùy\n chọn)</span></label>\n <textarea class="td-textarea" name="symptoms"\n placeholder="Bạn đang cảm thấy như thế nào?"></textarea>\n </div>\n\n <div style="margin-top: 8px;">\n <button type="submit" class="td-btn td-btn-primary" id="td-form-submit">\n Bắt đầu tư vấn\n <span class="material-symbols-outlined" style="margin-left: 8px;">arrow_forward</span>\n </button>\n </div>\n\n <div class="td-secure-note">\n <span class="material-symbols-outlined" style="font-size: 14px;">lock</span>\n Thông tin y tế được bảo mật 100%\n </div>\n </form>\n </div>\n </div>\n</div>';
281
- const postConsultationHtml = `<div class="td-widget-expanded" id="td-summary-view">
282
- <header class="td-header">
283
- <h2 class="td-header-title" style="font-size: 16px;">Tổng kết tư vấn</h2>
284
- <button class="td-close-btn" id="td-summary-close">
285
- <span class="material-symbols-outlined">close</span>
286
- </button>
287
- </header>
288
-
289
- <div class="td-content">
290
- <div class="td-summary-container">
291
- <div class="td-success-header">
292
- <div class="td-success-icon">
293
- <span class="material-symbols-outlined" style="font-size: 48px;">check_circle</span>
294
- </div>
295
- <h3 class="td-summary-title">Tư vấn Hoàn tất</h3>
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>
297
- </div>
298
-
299
- <div class="td-summary-card">
300
- <div class="td-chat-avatar"
301
- style="background-image: url('{{doctorAvatar}}'); width: 56px; height: 56px;"></div>
302
- <div>
303
- <div style="font-weight: 700;">{{doctorName}}</div>
304
- <div style="color: var(--td-text-sub); font-size: 14px;">{{doctorSpecialty}}</div>
305
- </div>
306
- <div style="margin-left: auto; color: var(--td-primary);">
307
- <span class="material-symbols-outlined">verified_user</span>
308
- </div>
309
- </div>
310
-
311
- <div class="td-summary-details">
312
- <div class="td-detail-row">
313
- <span style="color: var(--td-text-sub); display: flex; gap: 6px;">
314
- <span class="material-symbols-outlined" style="font-size: 18px;">tag</span> Mã tư vấn
315
- </span>
316
- <span style="font-weight: 600;">#TD-93821</span>
317
- </div>
318
- <div class="td-detail-row">
319
- <span style="color: var(--td-text-sub); display: flex; gap: 6px;">
320
- <span class="material-symbols-outlined" style="font-size: 18px;">schedule</span> Thời gian
321
- </span>
322
- <span style="font-weight: 600;">10:00 AM, Today</span>
323
- </div>
324
- </div>
325
-
326
- <div class="td-note-box">
327
- <div class="td-note-title">
328
- <span class="material-symbols-outlined"
329
- style="font-size: 16px; color: var(--td-primary);">clinical_notes</span>
330
- Ghi chú bác
331
- </div>
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
333
- động mạnh trong 2 ngày tới.</p>
334
- </div>
335
-
336
- <div class="td-actions">
337
- <div class="td-grid-actions">
338
- <button class="td-btn td-btn-outline" style="font-size: 12px; height: 40px; margin-bottom: 0;">
339
- Xem kết luận
340
- </button>
341
- <button class="td-btn td-btn-outline" style="font-size: 12px; height: 40px; margin-bottom: 0;">
342
- Tải đơn thuốc
343
- </button>
344
- </div>
345
- <button class="td-btn td-btn-primary">
346
- <span class="material-symbols-outlined" style="margin-right: 8px;">calendar_month</span>
347
- Đặt lịch tái khám
348
- </button>
349
- </div>
350
- </div>
351
- </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
288
+ \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
294
+ </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
+ \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
308
+ </div>\r
309
+ </div>\r
310
+ \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
317
+ </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
+ \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
331
+ </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
+ \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
343
+ </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
349
+ </div>\r
350
+ </div>\r
351
+ </div>\r
352
352
  </div>`;
353
- const inlineTriggerHtml = `<style>
354
- @keyframes td-pulse-animation {
355
- 0% {
356
- box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.4);
357
- }
358
-
359
- 70% {
360
- box-shadow: 0 0 0 6px rgba(34, 197, 94, 0);
361
- }
362
-
363
- 100% {
364
- box-shadow: 0 0 0 0 rgba(34, 197, 94, 0);
365
- }
366
- }
367
-
368
- .td-inline-container {
369
- display: flex;
370
- flex-direction: column;
371
- padding: 14px;
372
- background: #f8faff;
373
- border: 1px solid #e2e8f0;
374
- border-radius: 10px;
375
- font-family: system-ui, -apple-system, sans-serif;
376
- }
377
-
378
- .td-inline-row {
379
- display: flex;
380
- align-items: center;
381
- gap: 12px;
382
- }
383
-
384
- .td-inline-footer {
385
- display: flex;
386
- align-items: center;
387
- justify-content: space-between;
388
- margin-top: 14px;
389
- padding-top: 12px;
390
- border-top: 1px solid #e5e7eb;
391
- }
392
-
393
- .td-inline-status {
394
- display: flex;
395
- align-items: center;
396
- gap: 8px;
397
- }
398
-
399
- .td-inline-pulse {
400
- width: 8px;
401
- height: 8px;
402
- background: #22c55e;
403
- border-radius: 50%;
404
- animation: td-pulse-animation 2s infinite;
405
- }
406
-
407
- .td-inline-text {
408
- color: #16a34a;
409
- font-weight: 600;
410
- font-size: 13px;
411
- }
412
- </style>
413
- <div class="td-inline-container">
414
- <!-- Top: Doctor Info -->
415
- <div class="td-inline-row">
416
- <div
417
- style="width: 48px; height: 48px; border-radius: 50%; background-image: url('{{doctorAvatar}}'); background-size: cover; background-position: center; flex-shrink: 0; background-color: #e5e7eb; border: 2px solid white; box-shadow: 0 2px 4px rgba(0,0,0,0.05);">
418
- </div>
419
- <div style="flex: 1; min-width: 0;">
420
- <div
421
- style="font-weight: 700; font-size: 15px; color: #1f2937; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
422
- {{doctorName}}</div>
423
- <div
424
- style="font-size: 13px; color: #6b7280; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
425
- {{doctorSpecialty}}</div>
426
- </div>
427
- </div>
428
-
429
- <!-- Bottom: Status & Action -->
430
- <div class="td-inline-footer">
431
- <div class="td-inline-status">
432
- <span class="td-inline-pulse"></span>
433
- <span class="td-inline-text">Bác sĩ đang Online</span>
434
- </div>
435
-
436
- <button id="td-inline-btn" style="
437
- background: #0066ff;
438
- color: white;
439
- border: none;
440
- padding: 8px 16px;
441
- border-radius: 6px;
442
- font-weight: 600;
443
- font-size: 13px;
444
- cursor: pointer;
445
- display: flex;
446
- align-items: center;
447
- gap: 6px;
448
- white-space: nowrap;
449
- transition: all 0.2s;
450
- box-shadow: 0 2px 5px rgba(0,102,255,0.2);
451
- " onmouseover="this.style.background='#0052cc'" onmouseout="this.style.background='#0066ff'">
452
- <span class="material-symbols-outlined" style="font-size: 18px;">videocam</span>
453
- Tư vấn ngay
454
- </button>
455
- </div>
353
+ const inlineTriggerHtml = `<style>\r
354
+ @keyframes td-pulse-animation {\r
355
+ 0% {\r
356
+ box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.4);\r
357
+ }\r
358
+ \r
359
+ 70% {\r
360
+ box-shadow: 0 0 0 6px rgba(34, 197, 94, 0);\r
361
+ }\r
362
+ \r
363
+ 100% {\r
364
+ box-shadow: 0 0 0 0 rgba(34, 197, 94, 0);\r
365
+ }\r
366
+ }\r
367
+ \r
368
+ .td-inline-container {\r
369
+ display: flex;\r
370
+ flex-direction: column;\r
371
+ padding: 14px;\r
372
+ background: #f8faff;\r
373
+ border: 1px solid #e2e8f0;\r
374
+ border-radius: 10px;\r
375
+ font-family: system-ui, -apple-system, sans-serif;\r
376
+ }\r
377
+ \r
378
+ .td-inline-row {\r
379
+ display: flex;\r
380
+ align-items: center;\r
381
+ gap: 12px;\r
382
+ }\r
383
+ \r
384
+ .td-inline-footer {\r
385
+ display: flex;\r
386
+ align-items: center;\r
387
+ justify-content: space-between;\r
388
+ margin-top: 14px;\r
389
+ padding-top: 12px;\r
390
+ border-top: 1px solid #e5e7eb;\r
391
+ }\r
392
+ \r
393
+ .td-inline-status {\r
394
+ display: flex;\r
395
+ align-items: center;\r
396
+ gap: 8px;\r
397
+ }\r
398
+ \r
399
+ .td-inline-pulse {\r
400
+ width: 8px;\r
401
+ height: 8px;\r
402
+ background: #22c55e;\r
403
+ border-radius: 50%;\r
404
+ animation: td-pulse-animation 2s infinite;\r
405
+ }\r
406
+ \r
407
+ .td-inline-text {\r
408
+ color: #16a34a;\r
409
+ font-weight: 600;\r
410
+ font-size: 13px;\r
411
+ }\r
412
+ </style>\r
413
+ <div class="td-inline-container">\r
414
+ <!-- Top: Doctor Info -->\r
415
+ <div class="td-inline-row">\r
416
+ <div\r
417
+ style="width: 48px; height: 48px; border-radius: 50%; background-image: url('{{doctorAvatar}}'); background-size: cover; background-position: center; flex-shrink: 0; background-color: #e5e7eb; border: 2px solid white; box-shadow: 0 2px 4px rgba(0,0,0,0.05);">\r
418
+ </div>\r
419
+ <div style="flex: 1; min-width: 0;">\r
420
+ <div\r
421
+ style="font-weight: 700; font-size: 15px; color: #1f2937; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">\r
422
+ {{doctorName}}</div>\r
423
+ <div\r
424
+ style="font-size: 13px; color: #6b7280; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">\r
425
+ {{doctorSpecialty}}</div>\r
426
+ </div>\r
427
+ </div>\r
428
+ \r
429
+ <!-- Bottom: Status & Action -->\r
430
+ <div class="td-inline-footer">\r
431
+ <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
434
+ </div>\r
435
+ \r
436
+ <button id="td-inline-btn" type="button" style="\r
437
+ background: #0066ff; \r
438
+ color: white; \r
439
+ border: none; \r
440
+ padding: 8px 16px; \r
441
+ border-radius: 6px; \r
442
+ font-weight: 600; \r
443
+ font-size: 13px; \r
444
+ cursor: pointer; \r
445
+ display: flex; \r
446
+ align-items: center; \r
447
+ gap: 6px;\r
448
+ white-space: nowrap;\r
449
+ transition: all 0.2s;\r
450
+ box-shadow: 0 2px 5px rgba(0,102,255,0.2);\r
451
+ " 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
454
+ </button>\r
455
+ </div>\r
456
456
  </div>`;
457
+ const permissionModalHtml = '<div class="modalOverlay" id="td-permission-modal">\r\n <div class="modal">\r\n <div class="modalHead">\r\n <h2>Cho phép truy cập Microphone và Camera</h2>\r\n <button id="td-perm-close" class="btn btn-icon" aria-label="Đóng">\r\n <span class="material-symbols-outlined" style="font-size: 20px;">close</span>\r\n </button>\r\n </div>\r\n <div class="modalBody">\r\n Website sắp yêu cầu quyền <strong>Microphone</strong> và <strong>Camera</strong> để:\r\n <ul>\r\n <li>Gọi video trong phiên tư vấn.</li>\r\n <li>Kiểm tra thiết bị trước khi bắt đầu.</li>\r\n </ul>\r\n <div class="note">\r\n Sau khi bấm “Tiếp tục”, trình duyệt sẽ hiện thông báo hệ thống. Vui lòng chọn <strong>Allow</strong>.\r\n </div>\r\n </div>\r\n <div class="modalFoot">\r\n <button id="td-perm-cancel" class="btn">Hủy</button>\r\n <button id="td-perm-allow" class="btn primary">Tiếp tục</button>\r\n </div>\r\n </div>\r\n</div>\r\n\r\n<style>\r\n #td-permission-modal {\r\n --bg: var(--td-bg-light, #ffffff);\r\n --text: var(--td-text-main, #0d131c);\r\n --muted: var(--td-text-sub, #49699c);\r\n --border: var(--td-border, #e7ecf4);\r\n --primary: var(--td-primary, #0e65f1);\r\n --primary-hover: var(--td-primary-hover, #0b50c0);\r\n --shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);\r\n --radius: 12px;\r\n font-family: var(--font-family, system-ui, -apple-system, sans-serif);\r\n z-index: 2147483647;\r\n }\r\n\r\n /* Modal Overlay */\r\n .modalOverlay {\r\n position: fixed;\r\n inset: 0;\r\n top: 0;\r\n left: 0;\r\n right: 0;\r\n bottom: 0;\r\n background: rgba(0, 0, 0, .5);\r\n backdrop-filter: blur(2px);\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n padding: 18px;\r\n z-index: 2147483647;\r\n }\r\n\r\n .modal {\r\n width: min(500px, 94vw);\r\n border-radius: var(--radius);\r\n border: 1px solid var(--border);\r\n background: var(--bg);\r\n box-shadow: var(--shadow);\r\n overflow: hidden;\r\n color: var(--text);\r\n font-size: 14px;\r\n line-height: 1.5;\r\n }\r\n\r\n .modalHead {\r\n padding: 16px 20px;\r\n border-bottom: 1px solid var(--border);\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n gap: 10px;\r\n background: #f8f9fc;\r\n }\r\n\r\n .modalHead h2 {\r\n margin: 0;\r\n font-size: 16px;\r\n font-weight: 700;\r\n color: var(--text);\r\n }\r\n\r\n .modalBody {\r\n padding: 20px;\r\n color: var(--text);\r\n }\r\n\r\n .modalBody ul {\r\n margin: 12px 0 16px 0;\r\n padding-left: 20px;\r\n color: var(--muted);\r\n }\r\n\r\n .modalBody li {\r\n margin-bottom: 6px;\r\n }\r\n\r\n .modalFoot {\r\n padding: 16px 20px;\r\n display: flex;\r\n gap: 12px;\r\n justify-content: flex-end;\r\n border-top: 1px solid var(--border);\r\n background: #f8f9fc;\r\n }\r\n\r\n .note {\r\n margin-top: 12px;\r\n color: var(--muted);\r\n font-size: 13px;\r\n background: rgba(14, 101, 241, 0.05);\r\n padding: 10px;\r\n border-radius: 8px;\r\n border: 1px dashed var(--primary);\r\n }\r\n\r\n .note strong {\r\n color: var(--primary);\r\n }\r\n\r\n /* Buttons */\r\n .btn {\r\n appearance: none;\r\n border: 1px solid var(--border);\r\n background: white;\r\n color: var(--text);\r\n padding: 10px 16px;\r\n border-radius: 8px;\r\n cursor: pointer;\r\n transition: all 0.2s;\r\n font-weight: 600;\r\n font-family: inherit;\r\n font-size: 14px;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n }\r\n\r\n .btn:hover {\r\n background: #f1f5f9;\r\n border-color: #cbd5e1;\r\n }\r\n\r\n .btn-icon {\r\n padding: 4px;\r\n border: none;\r\n background: transparent;\r\n color: #64748b;\r\n border-radius: 50%;\r\n }\r\n\r\n .btn-icon:hover {\r\n background: rgba(0, 0, 0, 0.05);\r\n border-color: transparent;\r\n color: var(--text);\r\n }\r\n\r\n .btn.primary {\r\n background: var(--primary);\r\n border-color: var(--primary);\r\n color: white;\r\n }\r\n\r\n .btn.primary:hover {\r\n background: var(--primary-hover);\r\n border-color: var(--primary-hover);\r\n }\r\n</style>';
457
458
  const render = (template, data) => {
458
459
  var _a, _b, _c;
459
460
  let output = template;
@@ -471,6 +472,7 @@ const getConsultationHtml = (data) => render(consultationHtml, data);
471
472
  const getPatientFormHtml = (data) => render(patientFormHtml, data);
472
473
  const getPostConsultationHtml = (data) => render(postConsultationHtml, data);
473
474
  const getInlineTriggerHtml = (data) => render(inlineTriggerHtml, data);
475
+ const getPermissionModalHtml = (data) => render(permissionModalHtml, data);
474
476
  const PACKET_TYPES = /* @__PURE__ */ Object.create(null);
475
477
  PACKET_TYPES["open"] = "0";
476
478
  PACKET_TYPES["close"] = "1";
@@ -28017,6 +28019,41 @@ class LiveKitService {
28017
28019
  constructor() {
28018
28020
  this.room = null;
28019
28021
  this.wrapper = null;
28022
+ this.preAcquiredTracks = [];
28023
+ }
28024
+ /**
28025
+ * Request Camera & Mic permissions early
28026
+ */
28027
+ 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;
28038
+ }
28039
+ }
28040
+ /**
28041
+ * Check if permissions are already granted
28042
+ * @returns {Promise<boolean>}
28043
+ */
28044
+ async checkPermissions() {
28045
+ if (!navigator.permissions || !navigator.permissions.query) {
28046
+ return false;
28047
+ }
28048
+ try {
28049
+ const cam = await navigator.permissions.query({ name: "camera" });
28050
+ const mic = await navigator.permissions.query({ name: "microphone" });
28051
+ console.log("[LiveKit] Permissions check:", { cam: cam.state, mic: mic.state });
28052
+ return cam.state === "granted" && mic.state === "granted";
28053
+ } catch (error) {
28054
+ console.warn("[LiveKit] Permission check not supported or failed:", error);
28055
+ return false;
28056
+ }
28020
28057
  }
28021
28058
  /**
28022
28059
  * Connect to LiveKit Room
@@ -28064,8 +28101,23 @@ class LiveKitService {
28064
28101
  async publishLocalTracks() {
28065
28102
  const localContainer = this.wrapper.querySelector(".td-pip");
28066
28103
  try {
28067
- await this.room.localParticipant.setCameraEnabled(true);
28068
- const videoPub = Array.from(this.room.localParticipant.videoTrackPublications.values()).find((p) => p.source === "camera");
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
+ }
28069
28121
  if (videoPub && videoPub.track && localContainer) {
28070
28122
  localContainer.innerHTML = "";
28071
28123
  localContainer.style.backgroundImage = "none";
@@ -28076,24 +28128,15 @@ class LiveKitService {
28076
28128
  videoEl.style.transform = "scale(-1, 1)";
28077
28129
  localContainer.appendChild(videoEl);
28078
28130
  console.log("[LiveKit] Local video attached");
28131
+ } else {
28132
+ console.warn("[LiveKit] No Video Publication found to attach!");
28079
28133
  }
28080
28134
  } catch (e2) {
28081
- console.error("[LiveKit] Failed to enable Camera:", e2);
28082
- if (e2.name === "NotAllowedError") {
28083
- alert("Không thể mở Camera. Hãy kiểm tra icon ổ khóa trên thanh địa chỉ hoặc cài đặt Quyền Riêng Tư (Privacy) của máy tính.");
28084
- }
28085
- }
28086
- try {
28087
- const devices = await Room.getLocalDevices("audioinput");
28088
- console.log("[LiveKit] Audio Input Devices:", devices);
28089
- if (devices.length === 0) {
28090
- alert("Không tìm thấy Micro nào trên thiết bị của bạn!");
28091
- }
28092
- await this.room.localParticipant.setMicrophoneEnabled(true);
28093
- } catch (e2) {
28094
- console.error("[LiveKit] Failed to enable Microphone:", e2);
28135
+ console.error("[LiveKit] Failed to publish tracks:", e2);
28095
28136
  if (e2.name === "NotAllowedError") {
28096
- alert("Không thể mở Micro. Cuộc gọi sẽ không tiếng.");
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ư.");
28138
+ } else {
28139
+ alert(`Lỗi Camera: ${e2.message}`);
28097
28140
  }
28098
28141
  }
28099
28142
  }
@@ -28262,7 +28305,9 @@ class ClinicWidget {
28262
28305
  }
28263
28306
  if (consultBtn) {
28264
28307
  consultBtn.addEventListener("click", () => {
28265
- store.setState({ status: WidgetStates.PATIENT_FORM });
28308
+ consultBtn.addEventListener("click", () => {
28309
+ store.setState({ status: WidgetStates.PATIENT_FORM });
28310
+ });
28266
28311
  });
28267
28312
  }
28268
28313
  } else if (status === WidgetStates.PATIENT_FORM) {
@@ -28274,16 +28319,16 @@ class ClinicWidget {
28274
28319
  });
28275
28320
  }
28276
28321
  if (form) {
28277
- form.addEventListener("submit", (e2) => {
28322
+ form.addEventListener("submit", async (e2) => {
28278
28323
  e2.preventDefault();
28279
28324
  const formData = new FormData(form);
28280
28325
  const name = formData.get("name");
28281
28326
  const phone = formData.get("phone");
28282
- this.triggerCallback("onConsultationStarted", { name, phone });
28283
28327
  store.setState({
28284
- user: { name, phone },
28285
- status: WidgetStates.CONSULTATION
28328
+ user: { name, phone }
28286
28329
  });
28330
+ this.triggerCallback("onConsultationStarted", { name, phone });
28331
+ store.setState({ status: WidgetStates.CONSULTATION });
28287
28332
  });
28288
28333
  }
28289
28334
  } else if (status === WidgetStates.CONSULTATION) {
@@ -28291,7 +28336,9 @@ class ClinicWidget {
28291
28336
  const endBtn = this.shadowRoot.getElementById("td-consult-end");
28292
28337
  if (endBtn) {
28293
28338
  endBtn.addEventListener("click", () => {
28294
- this.stopSocket();
28339
+ if (this.livekit) {
28340
+ this.livekit.disconnect();
28341
+ }
28295
28342
  store.setState({ status: WidgetStates.COMPLETED });
28296
28343
  });
28297
28344
  }
@@ -28353,7 +28400,9 @@ class ClinicWidget {
28353
28400
  const btn = el.querySelector("#td-inline-btn");
28354
28401
  if (btn) {
28355
28402
  btn.addEventListener("click", () => {
28356
- store.setState({ status: WidgetStates.PATIENT_FORM });
28403
+ btn.addEventListener("click", () => {
28404
+ store.setState({ status: WidgetStates.PATIENT_FORM });
28405
+ });
28357
28406
  });
28358
28407
  }
28359
28408
  } else {
@@ -28394,18 +28443,26 @@ class ClinicWidget {
28394
28443
  store.setState({ doctorOnline: isOnline });
28395
28444
  }
28396
28445
  });
28397
- this.socket.on("customer:token", (data) => {
28446
+ this.socket.on("customer:token", async (data) => {
28398
28447
  console.log("Received token from server:", data);
28399
28448
  if (data.token) {
28400
28449
  if (!this.livekit) {
28401
28450
  this.livekit = new LiveKitService();
28402
28451
  }
28403
- const modal = this.shadowRoot.querySelector(".td-consultation-modal");
28404
- if (modal) {
28405
- console.log("[Widget] LiveKit Params:", { url: CONFIG.LIVEKIT_URL, token: data.token });
28406
- this.livekit.connect(CONFIG.LIVEKIT_URL, data.token, modal);
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();
28407
28464
  } else {
28408
- console.error("Consultation modal not found for video rendering");
28465
+ this.showPermissionModal(connectLiveKit);
28409
28466
  }
28410
28467
  }
28411
28468
  });
@@ -28454,6 +28511,39 @@ class ClinicWidget {
28454
28511
  this.socket = null;
28455
28512
  }
28456
28513
  }
28514
+ showPermissionModal(onSuccessCallback) {
28515
+ if (this.shadowRoot.getElementById("td-permission-modal")) return;
28516
+ const state = store.getState();
28517
+ const div = document.createElement("div");
28518
+ div.innerHTML = getPermissionModalHtml(state);
28519
+ Array.from(div.children).forEach((child) => {
28520
+ this.shadowRoot.appendChild(child);
28521
+ });
28522
+ const modalEl = this.shadowRoot.getElementById("td-permission-modal");
28523
+ const closeBtn = modalEl.querySelector("#td-perm-close");
28524
+ const cancelBtn = modalEl.querySelector("#td-perm-cancel");
28525
+ const allowBtn = modalEl.querySelector("#td-perm-allow");
28526
+ const closeModal = () => {
28527
+ modalEl.remove();
28528
+ };
28529
+ const onAllow = async () => {
28530
+ closeModal();
28531
+ if (!this.livekit) {
28532
+ this.livekit = new LiveKitService();
28533
+ }
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");
28541
+ }
28542
+ };
28543
+ if (closeBtn) closeBtn.addEventListener("click", closeModal);
28544
+ if (cancelBtn) cancelBtn.addEventListener("click", closeModal);
28545
+ if (allowBtn) allowBtn.addEventListener("click", onAllow);
28546
+ }
28457
28547
  }
28458
28548
  (function() {
28459
28549
  const currentScript = document.querySelector("script[data-widget-id]");