clinic-connect-widget 1.0.6 → 1.0.7

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.
@@ -225,7 +225,7 @@ const consultationHtml = `<div class="td-consultation-modal">\r
225
225
  </div>\r
226
226
  \r
227
227
  </div>`;
228
- 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>';
228
+ 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 <span style="color: red;">*</span></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 <span style="color: red;">*</span></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>';
229
229
  const postConsultationHtml = `<div class="td-overlay-wrapper">\r
230
230
  <div class="td-modal-centered" id="td-summary-view">\r
231
231
  <header class="td-header">\r
@@ -433,7 +433,7 @@ const render = (template, data) => {
433
433
  output = output.replace(/{{badgeText}}/g, badgeText);
434
434
  output = output.replace(/{{btnStyle}}/g, btnStyle);
435
435
  const showVideo = !!data.urlVideo && data.permissionsGranted;
436
- const innerContent = showVideo ? `<video src="${data.urlVideo}" autoplay loop muted playsinline style="width: 100%; height: 100%; object-fit: cover;"></video>` : "";
436
+ const innerContent = showVideo ? `<video src="${data.urlVideo}" autoplay loop playsinline style="width: 100%; height: 100%; object-fit: cover;"></video>` : "";
437
437
  const videoContainerHtml = `
438
438
  <div id="td-video-container" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 0; overflow: hidden; background: #000;">
439
439
  ${innerContent}
@@ -28086,10 +28086,12 @@ class LiveKitService {
28086
28086
  }
28087
28087
  refreshRemoteTracks() {
28088
28088
  if (!this.room || !this.wrapper) return;
28089
+ console.log("[LiveKit] Refreshing remote tracks for participants:", this.room.remoteParticipants.size);
28089
28090
  this.room.remoteParticipants.forEach((participant) => {
28090
28091
  participant.videoTrackPublications.forEach((pub) => {
28091
28092
  if (pub.isSubscribed && pub.track && pub.kind === "video") {
28092
- this.handleTrackSubscribed(pub.track, pub, participant);
28093
+ console.log("[LiveKit] Found subscribed video track for:", participant.identity);
28094
+ this.handleTrackSubscribed(pub.track, participant);
28093
28095
  }
28094
28096
  });
28095
28097
  });
@@ -28108,6 +28110,21 @@ class LiveKitService {
28108
28110
  console.error("[LiveKit] Media Device Error:", e2);
28109
28111
  });
28110
28112
  }
28113
+ /**
28114
+ * Check if any remote participant has a video track
28115
+ */
28116
+ hasRemoteVideo() {
28117
+ if (!this.room) return false;
28118
+ let hasVideo = false;
28119
+ this.room.remoteParticipants.forEach((p) => {
28120
+ p.videoTrackPublications.forEach((pub) => {
28121
+ if (pub.isSubscribed && pub.kind === "video") {
28122
+ hasVideo = true;
28123
+ }
28124
+ });
28125
+ });
28126
+ return hasVideo;
28127
+ }
28111
28128
  async publishLocalTracks() {
28112
28129
  try {
28113
28130
  await this.room.localParticipant.setCameraEnabled(true);
@@ -28141,17 +28158,14 @@ class LiveKitService {
28141
28158
  handleTrackSubscribed(track, participant) {
28142
28159
  console.log("[LiveKit] Track subscribed:", track.kind, participant.identity);
28143
28160
  if (track.kind === "video") {
28161
+ if (!this.wrapper.isConnected) {
28162
+ console.warn("[LiveKit] WARNING: Wrapper is DETACHED from DOM! Video update may not be visible.");
28163
+ }
28144
28164
  const remoteContainer = this.wrapper.querySelector(".td-main-video");
28145
- console.log("[LiveKit] Looking for remote container (.td-main-video):", remoteContainer);
28165
+ console.log("[LiveKit] Remote Container found:", remoteContainer, "Is Connected:", remoteContainer == null ? void 0 : remoteContainer.isConnected);
28146
28166
  if (remoteContainer) {
28147
- const placeholder = remoteContainer.querySelector("#td-video-placeholder");
28148
- if (placeholder) {
28149
- placeholder.style.opacity = "0";
28150
- setTimeout(() => placeholder.remove(), 500);
28151
- } else {
28152
- const existingVideos = remoteContainer.querySelectorAll("video");
28153
- existingVideos.forEach((v) => v.remove());
28154
- }
28167
+ console.log("[LiveKit] Clearing container content. Current innerHTML length:", remoteContainer.innerHTML.length);
28168
+ remoteContainer.innerHTML = "";
28155
28169
  remoteContainer.style.backgroundImage = "none";
28156
28170
  const videoEl = track.attach();
28157
28171
  videoEl.style.width = "100%";
@@ -28309,6 +28323,19 @@ class ClinicWidget {
28309
28323
  if (prev.status !== current.status || prev.doctorOnline !== current.doctorOnline || prev.permissionsGranted !== current.permissionsGranted) {
28310
28324
  this.render(current.status, current);
28311
28325
  this.checkInlineTrigger();
28326
+ if (current.status === WidgetStates.CONSULTATION) {
28327
+ if (prev.status !== WidgetStates.CONSULTATION) {
28328
+ this.joinVideoCall();
28329
+ }
28330
+ if (this.livekit) {
28331
+ const modal = this.shadowRoot.querySelector(".td-consultation-modal");
28332
+ if (modal) {
28333
+ console.log("[Widget] State Changed -> Updating LiveKit wrapper to new DOM");
28334
+ this.livekit.updateWrapper(modal);
28335
+ this.livekit.refreshRemoteTracks();
28336
+ }
28337
+ }
28338
+ }
28312
28339
  }
28313
28340
  }
28314
28341
  render(status, stateOverride = null) {
@@ -28405,6 +28432,25 @@ class ClinicWidget {
28405
28432
  });
28406
28433
  }
28407
28434
  if (form) {
28435
+ const inputs = form.querySelectorAll("input");
28436
+ inputs.forEach((input) => {
28437
+ input.addEventListener("invalid", () => {
28438
+ if (input.validity.valueMissing) {
28439
+ if (input.name === "name") {
28440
+ input.setCustomValidity("Vui lòng nhập họ và tên của bạn.");
28441
+ } else if (input.name === "phone") {
28442
+ input.setCustomValidity("Vui lòng nhập số điện thoại.");
28443
+ } else {
28444
+ input.setCustomValidity("Vui lòng điền thông tin vào trường này.");
28445
+ }
28446
+ } else {
28447
+ input.setCustomValidity("Dữ liệu không hợp lệ.");
28448
+ }
28449
+ });
28450
+ input.addEventListener("input", () => {
28451
+ input.setCustomValidity("");
28452
+ });
28453
+ });
28408
28454
  form.addEventListener("submit", async (e2) => {
28409
28455
  e2.preventDefault();
28410
28456
  const formData = new FormData(form);
@@ -28435,7 +28481,6 @@ class ClinicWidget {
28435
28481
  });
28436
28482
  }
28437
28483
  } else if (status === WidgetStates.CONSULTATION) {
28438
- this.joinVideoCall();
28439
28484
  const modal = this.shadowRoot.querySelector(".td-consultation-modal");
28440
28485
  if (this.livekit && modal) {
28441
28486
  this.livekit.updateWrapper(modal);
@@ -28619,11 +28664,25 @@ class ClinicWidget {
28619
28664
  if (!state.permissionsGranted) return;
28620
28665
  const videoUrl = this.config.urlVideo || state.urlVideo;
28621
28666
  if (!videoUrl) return;
28667
+ if (this.livekit && this.livekit.hasRemoteVideo()) {
28668
+ console.log("[Widget] Remote video active, skipping intro video injection.");
28669
+ return;
28670
+ }
28622
28671
  const placeholder = this.shadowRoot.querySelector("#td-video-placeholder");
28623
28672
  const videoContainer = this.shadowRoot.querySelector("#td-video-container") || this.shadowRoot.querySelector(".td-main-video");
28624
28673
  const existingVideo = this.shadowRoot.querySelector("video");
28625
28674
  if (existingVideo) {
28626
- if (existingVideo.paused) existingVideo.play().catch((e2) => console.error(e2));
28675
+ if (existingVideo.muted) {
28676
+ console.log("[Widget] Found muted existing video, attempting to unmute...");
28677
+ existingVideo.muted = false;
28678
+ }
28679
+ if (existingVideo.paused) {
28680
+ existingVideo.play().catch((e2) => {
28681
+ console.warn("[Widget] Play existing video failed, fallback to muted:", e2);
28682
+ existingVideo.muted = true;
28683
+ existingVideo.play();
28684
+ });
28685
+ }
28627
28686
  if (placeholder) placeholder.remove();
28628
28687
  return;
28629
28688
  }
@@ -28637,7 +28696,7 @@ class ClinicWidget {
28637
28696
  videoEl.src = videoUrl;
28638
28697
  videoEl.autoplay = true;
28639
28698
  videoEl.loop = true;
28640
- videoEl.muted = true;
28699
+ videoEl.muted = false;
28641
28700
  videoEl.playsInline = true;
28642
28701
  videoEl.style.cssText = "width: 100%; height: 100%; object-fit: cover; position: absolute; top: 0; left: 0; z-index: 0;";
28643
28702
  if (placeholder) {
@@ -28650,7 +28709,11 @@ class ClinicWidget {
28650
28709
  videoContainer.prepend(videoEl);
28651
28710
  }
28652
28711
  }
28653
- videoEl.play().catch((e2) => console.log("Autoplay handled:", e2));
28712
+ videoEl.play().catch((e2) => {
28713
+ console.warn("[Widget] Autoplay with sound failed, falling back to muted.", e2);
28714
+ videoEl.muted = true;
28715
+ videoEl.play().catch((err) => console.error("[Widget] Autoplay completely failed:", err));
28716
+ });
28654
28717
  }
28655
28718
  async attemptVideoConnection() {
28656
28719
  if (!this.videoToken || !this.livekit) {
@@ -28690,7 +28753,7 @@ class ClinicWidget {
28690
28753
  var _a;
28691
28754
  const state = store.getState();
28692
28755
  const { user, doctor } = state;
28693
- const userId = "20.183299.4158";
28756
+ const userId = "guest_6rakxwiai";
28694
28757
  this.videoToken = null;
28695
28758
  if (this.socket) {
28696
28759
  console.log("Joining video call room with user:", user == null ? void 0 : user.name);
@@ -28754,9 +28817,10 @@ class ClinicWidget {
28754
28817
  closeModal();
28755
28818
  store.setState({
28756
28819
  permissionsGranted: true,
28757
- urlVideo: this.config.urlVideo
28820
+ urlVideo: this.config.urlVideo,
28821
+ // Force switch to consultation if not already (should be handled by logic, but ensuring state reflects it)
28822
+ status: WidgetStates.CONSULTATION
28758
28823
  });
28759
- this.render(WidgetStates.CONSULTATION, store.getState());
28760
28824
  await new Promise((resolve) => setTimeout(resolve, 50));
28761
28825
  if (!this.livekit) {
28762
28826
  this.livekit = new LiveKitService();