@skippr/live-agent-sdk 0.26.0 → 0.28.0

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.
@@ -1,6 +1,14 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined")
5
+ return require.apply(this, arguments);
6
+ throw Error('Dynamic require of "' + x + '" is not supported');
7
+ });
8
+
1
9
  // src/components/LiveAgent.tsx
2
10
  import { LiveKitRoom, RoomAudioRenderer } from "@livekit/components-react";
3
- import { useCallback as useCallback6, useEffect as useEffect12, useMemo as useMemo5, useRef as useRef6, useState as useState8 } from "react";
11
+ import { useCallback as useCallback6, useEffect as useEffect13, useMemo as useMemo5, useRef as useRef7, useState as useState8 } from "react";
4
12
 
5
13
  // src/context/LiveAgentContext.tsx
6
14
  import { createContext } from "react";
@@ -165,7 +173,13 @@ async function exchangeForBearerToken(appKey, userToken) {
165
173
  const { token } = await resp.json();
166
174
  return token;
167
175
  }
168
- function useSession({ agentId, authToken, appKey, userToken }) {
176
+ function useSession({
177
+ agentId,
178
+ captureMode = "screenshare",
179
+ authToken,
180
+ appKey,
181
+ userToken
182
+ }) {
169
183
  const [connection, setConnection] = useState2(null);
170
184
  const [shouldConnect, setShouldConnect] = useState2(false);
171
185
  const [isStarting, setIsStarting] = useState2(false);
@@ -202,12 +216,14 @@ function useSession({ agentId, authToken, appKey, userToken }) {
202
216
  setError("");
203
217
  setErrorCode(null);
204
218
  let screenStream = null;
205
- try {
206
- screenStream = await navigator.mediaDevices.getDisplayMedia({
207
- video: { displaySurface: "browser" }
208
- });
209
- } catch {
210
- screenStream = null;
219
+ if (captureMode === "screenshare") {
220
+ try {
221
+ screenStream = await navigator.mediaDevices.getDisplayMedia({
222
+ video: { displaySurface: "browser" }
223
+ });
224
+ } catch {
225
+ screenStream = null;
226
+ }
211
227
  }
212
228
  const headers = { Authorization: `Bearer ${bearerToken}` };
213
229
  try {
@@ -215,7 +231,7 @@ function useSession({ agentId, authToken, appKey, userToken }) {
215
231
  method: "POST",
216
232
  credentials: "omit",
217
233
  headers: { "Content-Type": "application/json", ...headers },
218
- body: JSON.stringify({ agentId })
234
+ body: JSON.stringify({ agentId, captureMode })
219
235
  });
220
236
  if (!createResp.ok) {
221
237
  const body = await createResp.json().catch(() => null);
@@ -250,7 +266,7 @@ function useSession({ agentId, authToken, appKey, userToken }) {
250
266
  } finally {
251
267
  setIsStarting(false);
252
268
  }
253
- }, [agentId, bearerToken]);
269
+ }, [agentId, captureMode, bearerToken]);
254
270
  const disconnect = useCallback2(async () => {
255
271
  if (sessionId && bearerToken) {
256
272
  try {
@@ -284,35 +300,14 @@ function useSession({ agentId, authToken, appKey, userToken }) {
284
300
  };
285
301
  }
286
302
 
287
- // src/components/AutoStartMedia.tsx
288
- import { useConnectionState, useLocalParticipant } from "@livekit/components-react";
289
- import { ConnectionState, Track } from "livekit-client";
290
- import { useEffect as useEffect3, useRef } from "react";
291
- function AutoStartMedia({ pendingScreenStream }) {
292
- const { localParticipant } = useLocalParticipant();
293
- const connectionState = useConnectionState();
294
- const didStartRef = useRef(false);
295
- useEffect3(() => {
296
- if (didStartRef.current)
297
- return;
298
- if (connectionState !== ConnectionState.Connected)
299
- return;
300
- didStartRef.current = true;
301
- localParticipant.setMicrophoneEnabled(true).catch((error) => console.error("Failed to enable microphone:", error));
302
- if (pendingScreenStream) {
303
- const videoTrack = pendingScreenStream.getVideoTracks()[0];
304
- if (videoTrack) {
305
- videoTrack.contentHint = "detail";
306
- localParticipant.publishTrack(videoTrack, { source: Track.Source.ScreenShare }).catch((error) => {
307
- console.error("Failed to publish screen share track:", error);
308
- for (const track of pendingScreenStream.getTracks())
309
- track.stop();
310
- });
311
- }
312
- }
313
- }, [connectionState, localParticipant, pendingScreenStream]);
314
- return null;
315
- }
303
+ // src/lib/constants.ts
304
+ var SIDEBAR_WIDTH = 360;
305
+ var WIDGET_ROOT_ID = "skippr-sdk-root";
306
+ var REF_ATTR = "data-skippr-ref";
307
+ var PRIVATE_ATTR = "data-skippr-private";
308
+ var DOM_SNAPSHOT_TOPIC = "skippr.dom-snapshot";
309
+ var DOM_EVENTS_TOPIC = "skippr.dom-events";
310
+ var NAME_MAX_CHARS = 80;
316
311
  // ../../node_modules/.bun/lucide-react@1.8.0+83d5fd7b249dbeef/node_modules/lucide-react/dist/esm/createLucideIcon.js
317
312
  import { forwardRef as forwardRef2, createElement as createElement3 } from "react";
318
313
 
@@ -542,11 +537,8 @@ var __iconNode16 = [
542
537
  ["path", { d: "m21.854 2.147-10.94 10.939", key: "12cjpa" }]
543
538
  ];
544
539
  var Send = createLucideIcon("send", __iconNode16);
545
- // src/components/MinimizedBubble.tsx
546
- import { useEffect as useEffect4 } from "react";
547
-
548
540
  // src/hooks/useAgentVoiceState.ts
549
- import { useVoiceAssistant } from "@livekit/components-react";
541
+ import { useVoiceAssistant } from "@livekit/components-react/hooks";
550
542
  function useAgentVoiceState() {
551
543
  const { state } = useVoiceAssistant();
552
544
  return {
@@ -568,7 +560,7 @@ function useLiveAgent() {
568
560
  }
569
561
 
570
562
  // src/hooks/useMediaControls.ts
571
- import { useLocalParticipant as useLocalParticipant2 } from "@livekit/components-react";
563
+ import { useLocalParticipant } from "@livekit/components-react/hooks";
572
564
  import { ScreenSharePresets } from "livekit-client";
573
565
  import { useCallback as useCallback3 } from "react";
574
566
  var SCREEN_SHARE_OPTIONS = {
@@ -577,7 +569,7 @@ var SCREEN_SHARE_OPTIONS = {
577
569
  contentHint: "detail"
578
570
  };
579
571
  function useMediaControls() {
580
- const { localParticipant } = useLocalParticipant2();
572
+ const { localParticipant } = useLocalParticipant();
581
573
  const isMuted = !localParticipant.isMicrophoneEnabled;
582
574
  const isScreenSharing = localParticipant.isScreenShareEnabled;
583
575
  const toggleMute = useCallback3(async () => {
@@ -597,6 +589,1001 @@ function useMediaControls() {
597
589
  return { isMuted, toggleMute, isScreenSharing, toggleScreenShare };
598
590
  }
599
591
 
592
+ // src/components/AgentStateBanner.tsx
593
+ import { jsx, jsxs, Fragment } from "react/jsx-runtime";
594
+ function AgentStateBanner() {
595
+ const { isConnected } = useLiveAgent();
596
+ const { isScreenSharing } = useMediaControls();
597
+ const { state } = useAgentVoiceState();
598
+ if (!isConnected)
599
+ return null;
600
+ return /* @__PURE__ */ jsx("div", {
601
+ className: "skippr:fixed skippr:top-0 skippr:left-0 skippr:right-0 skippr:z-[2147483647] skippr:flex skippr:justify-center skippr:pointer-events-none",
602
+ children: /* @__PURE__ */ jsx("div", {
603
+ className: "skippr:pointer-events-auto skippr:flex skippr:items-center skippr:gap-2 skippr:bg-indigo-500/95 skippr:backdrop-blur-sm skippr:text-white skippr:text-xs skippr:font-medium skippr:px-4 skippr:py-1.5 skippr:rounded-b-lg skippr:shadow-lg",
604
+ children: /* @__PURE__ */ jsx(BannerContent, {
605
+ state,
606
+ isScreenSharing
607
+ })
608
+ })
609
+ });
610
+ }
611
+ function BannerContent({
612
+ state,
613
+ isScreenSharing
614
+ }) {
615
+ if (state === "speaking") {
616
+ return /* @__PURE__ */ jsxs(Fragment, {
617
+ children: [
618
+ /* @__PURE__ */ jsx(SpeakingBars, {}),
619
+ /* @__PURE__ */ jsx("span", {
620
+ children: "Skippr is talking"
621
+ })
622
+ ]
623
+ });
624
+ }
625
+ if (state === "thinking") {
626
+ return /* @__PURE__ */ jsxs(Fragment, {
627
+ children: [
628
+ /* @__PURE__ */ jsx(ThinkingDots, {}),
629
+ /* @__PURE__ */ jsx("span", {
630
+ children: "Skippr is thinking"
631
+ })
632
+ ]
633
+ });
634
+ }
635
+ if (state === "listening" && isScreenSharing) {
636
+ return /* @__PURE__ */ jsxs(Fragment, {
637
+ children: [
638
+ /* @__PURE__ */ jsx(ObservingDot, {}),
639
+ /* @__PURE__ */ jsx(Eye, {
640
+ className: "skippr:size-3.5"
641
+ }),
642
+ /* @__PURE__ */ jsx("span", {
643
+ children: "Skippr is observing this page"
644
+ })
645
+ ]
646
+ });
647
+ }
648
+ return /* @__PURE__ */ jsxs(Fragment, {
649
+ children: [
650
+ /* @__PURE__ */ jsx(ObservingDot, {}),
651
+ /* @__PURE__ */ jsx("span", {
652
+ children: "Skippr is connected"
653
+ })
654
+ ]
655
+ });
656
+ }
657
+ function ObservingDot() {
658
+ return /* @__PURE__ */ jsxs("span", {
659
+ className: "skippr:relative skippr:flex skippr:size-1.5",
660
+ children: [
661
+ /* @__PURE__ */ jsx("span", {
662
+ className: "skippr:absolute skippr:inline-flex skippr:size-full skippr:animate-ping skippr:rounded-full skippr:bg-emerald-400 skippr:opacity-75"
663
+ }),
664
+ /* @__PURE__ */ jsx("span", {
665
+ className: "skippr:relative skippr:inline-flex skippr:size-1.5 skippr:rounded-full skippr:bg-emerald-400"
666
+ })
667
+ ]
668
+ });
669
+ }
670
+ function ThinkingDots() {
671
+ return /* @__PURE__ */ jsxs("span", {
672
+ className: "skippr:flex skippr:items-center skippr:gap-[3px]",
673
+ children: [
674
+ /* @__PURE__ */ jsx("span", {
675
+ className: "skippr:size-1 skippr:rounded-full skippr:bg-white skippr:animate-bounce skippr:[animation-delay:0ms]"
676
+ }),
677
+ /* @__PURE__ */ jsx("span", {
678
+ className: "skippr:size-1 skippr:rounded-full skippr:bg-white skippr:animate-bounce skippr:[animation-delay:150ms]"
679
+ }),
680
+ /* @__PURE__ */ jsx("span", {
681
+ className: "skippr:size-1 skippr:rounded-full skippr:bg-white skippr:animate-bounce skippr:[animation-delay:300ms]"
682
+ })
683
+ ]
684
+ });
685
+ }
686
+ function SpeakingBars() {
687
+ return /* @__PURE__ */ jsxs("span", {
688
+ className: "skippr:flex skippr:items-center skippr:gap-[2px] skippr:h-3.5",
689
+ children: [
690
+ /* @__PURE__ */ jsx("span", {
691
+ className: "skippr:w-[2px] skippr:rounded-sm skippr:bg-white skippr:animate-[skippr-speak_1.1s_ease-in-out_infinite] skippr:h-1.5"
692
+ }),
693
+ /* @__PURE__ */ jsx("span", {
694
+ className: "skippr:w-[2px] skippr:rounded-sm skippr:bg-white skippr:animate-[skippr-speak_1.1s_ease-in-out_0.15s_infinite] skippr:h-3"
695
+ }),
696
+ /* @__PURE__ */ jsx("span", {
697
+ className: "skippr:w-[2px] skippr:rounded-sm skippr:bg-white skippr:animate-[skippr-speak_1.1s_ease-in-out_0.3s_infinite] skippr:h-2"
698
+ }),
699
+ /* @__PURE__ */ jsx("span", {
700
+ className: "skippr:w-[2px] skippr:rounded-sm skippr:bg-white skippr:animate-[skippr-speak_1.1s_ease-in-out_0.45s_infinite] skippr:h-3.5"
701
+ }),
702
+ /* @__PURE__ */ jsx("span", {
703
+ className: "skippr:w-[2px] skippr:rounded-sm skippr:bg-white skippr:animate-[skippr-speak_1.1s_ease-in-out_0.6s_infinite] skippr:h-1"
704
+ })
705
+ ]
706
+ });
707
+ }
708
+
709
+ // src/components/AutoStartMedia.tsx
710
+ import { useConnectionState, useLocalParticipant as useLocalParticipant2 } from "@livekit/components-react/hooks";
711
+ import { ConnectionState, Track } from "livekit-client";
712
+ import { useEffect as useEffect3, useRef } from "react";
713
+ function AutoStartMedia({ pendingScreenStream }) {
714
+ const { localParticipant } = useLocalParticipant2();
715
+ const connectionState = useConnectionState();
716
+ const didStartRef = useRef(false);
717
+ useEffect3(() => {
718
+ if (didStartRef.current)
719
+ return;
720
+ if (connectionState !== ConnectionState.Connected)
721
+ return;
722
+ didStartRef.current = true;
723
+ localParticipant.setMicrophoneEnabled(true).catch((error) => console.error("Failed to enable microphone:", error));
724
+ if (pendingScreenStream) {
725
+ const videoTrack = pendingScreenStream.getVideoTracks()[0];
726
+ if (videoTrack) {
727
+ videoTrack.contentHint = "detail";
728
+ localParticipant.publishTrack(videoTrack, { source: Track.Source.ScreenShare }).catch((error) => {
729
+ console.error("Failed to publish screen share track:", error);
730
+ for (const track of pendingScreenStream.getTracks())
731
+ track.stop();
732
+ });
733
+ }
734
+ }
735
+ }, [connectionState, localParticipant, pendingScreenStream]);
736
+ return null;
737
+ }
738
+
739
+ // src/components/DomCapture.tsx
740
+ import { useConnectionState as useConnectionState2, useLocalParticipant as useLocalParticipant3 } from "@livekit/components-react/hooks";
741
+ import { ConnectionState as ConnectionState2, ScreenSharePresets as ScreenSharePresets2, Track as Track2 } from "livekit-client";
742
+ import { useEffect as useEffect4, useRef as useRef2 } from "react";
743
+
744
+ // src/capture/a11yUtils.ts
745
+ var ROLE_BY_TAG = {
746
+ a: "link",
747
+ button: "button",
748
+ input: "input",
749
+ select: "select",
750
+ textarea: "textarea",
751
+ label: "label",
752
+ nav: "navigation",
753
+ main: "main",
754
+ header: "header",
755
+ footer: "footer",
756
+ aside: "complementary",
757
+ form: "form",
758
+ h1: "heading",
759
+ h2: "heading",
760
+ h3: "heading",
761
+ h4: "heading",
762
+ h5: "heading",
763
+ h6: "heading",
764
+ img: "image",
765
+ ul: "list",
766
+ ol: "list",
767
+ li: "listitem",
768
+ table: "table",
769
+ tr: "row",
770
+ td: "cell",
771
+ th: "columnheader",
772
+ video: "video",
773
+ iframe: "iframe",
774
+ canvas: "canvas",
775
+ audio: "audio",
776
+ embed: "embed",
777
+ object: "object"
778
+ };
779
+ function inferRole(element) {
780
+ const explicitRole = element.getAttribute("role");
781
+ if (explicitRole)
782
+ return explicitRole;
783
+ const tagName = element.tagName.toLowerCase();
784
+ if (tagName === "input") {
785
+ const inputType = element.type;
786
+ if (inputType === "checkbox")
787
+ return "checkbox";
788
+ if (inputType === "radio")
789
+ return "radio";
790
+ if (inputType === "submit" || inputType === "button")
791
+ return "button";
792
+ return "input";
793
+ }
794
+ return ROLE_BY_TAG[tagName] ?? null;
795
+ }
796
+ function accessibleName(element, options = {}) {
797
+ const ariaLabel = element.getAttribute("aria-label");
798
+ if (ariaLabel)
799
+ return ariaLabel.trim().slice(0, NAME_MAX_CHARS);
800
+ if (!options.quick) {
801
+ const labelledBy = element.getAttribute("aria-labelledby");
802
+ if (labelledBy) {
803
+ const labelElement = document.getElementById(labelledBy);
804
+ if (labelElement)
805
+ return (labelElement.textContent || "").trim().slice(0, NAME_MAX_CHARS);
806
+ }
807
+ if (element.tagName === "INPUT" || element.tagName === "SELECT" || element.tagName === "TEXTAREA") {
808
+ const elementId = element.id;
809
+ if (elementId) {
810
+ const associatedLabel = document.querySelector(`label[for="${CSS.escape(elementId)}"]`);
811
+ if (associatedLabel?.textContent) {
812
+ return associatedLabel.textContent.trim().slice(0, NAME_MAX_CHARS);
813
+ }
814
+ }
815
+ const placeholder = element.getAttribute("placeholder");
816
+ if (placeholder)
817
+ return placeholder.trim().slice(0, NAME_MAX_CHARS);
818
+ }
819
+ }
820
+ if (element.tagName === "IMG") {
821
+ const altText = element.getAttribute("alt");
822
+ if (altText)
823
+ return altText.trim().slice(0, NAME_MAX_CHARS);
824
+ }
825
+ const titleAttr = element.getAttribute("title");
826
+ if (titleAttr)
827
+ return titleAttr.trim().slice(0, NAME_MAX_CHARS);
828
+ const textContent = (element.textContent || "").replace(/\s+/g, " ").trim();
829
+ if (textContent)
830
+ return textContent.slice(0, NAME_MAX_CHARS);
831
+ return "";
832
+ }
833
+
834
+ // src/capture/a11yTree.ts
835
+ var REF_PREFIX = "r-";
836
+ var VALUE_MAX_CHARS = 80;
837
+ var HREF_MAX_CHARS = 60;
838
+ var PAGE_CONTENT_MAX_BYTES = 14000;
839
+ var NODE_LIMIT = 1500;
840
+ var SENSITIVE_IFRAME_HOSTS = [
841
+ "js.stripe.com",
842
+ "checkout.stripe.com",
843
+ "connect.stripe.com",
844
+ "plaid.com",
845
+ "production.plaid.com",
846
+ "cdn.plaid.com",
847
+ "auth0.com",
848
+ "login.auth0.com",
849
+ "recaptcha.net",
850
+ "www.google.com",
851
+ "hcaptcha.com",
852
+ "newassets.hcaptcha.com",
853
+ "challenges.cloudflare.com"
854
+ ];
855
+ var MASK_AUTOCOMPLETE_VALUES = new Set([
856
+ "cc-number",
857
+ "cc-exp",
858
+ "cc-exp-month",
859
+ "cc-exp-year",
860
+ "cc-csc",
861
+ "cc-name",
862
+ "cc-given-name",
863
+ "cc-family-name",
864
+ "current-password",
865
+ "new-password",
866
+ "one-time-code"
867
+ ]);
868
+ var MASK_INPUT_TYPES = new Set(["password", "hidden"]);
869
+ var MASK_NAME_PATTERN = /password|secret|token|api[_-]?key|ssn|tax[_-]?id|social[_-]?security|cvv|cvc|pin/i;
870
+ var refCounter = 0;
871
+ function generateNextRef() {
872
+ refCounter += 1;
873
+ return `${REF_PREFIX}${refCounter}`;
874
+ }
875
+ function advanceCounterPastExisting(ref) {
876
+ const parsedRefNumber = Number.parseInt(ref.slice(REF_PREFIX.length), 10);
877
+ if (Number.isFinite(parsedRefNumber) && parsedRefNumber > refCounter) {
878
+ refCounter = parsedRefNumber;
879
+ }
880
+ }
881
+ function intersectsViewport(rect, viewportWidth, viewportHeight) {
882
+ if (rect.width <= 0 || rect.height <= 0)
883
+ return false;
884
+ if (rect.bottom <= 0 || rect.top >= viewportHeight)
885
+ return false;
886
+ if (rect.right <= 0 || rect.left >= viewportWidth)
887
+ return false;
888
+ return true;
889
+ }
890
+ function isVisible(element, rect) {
891
+ if (!intersectsViewport(rect, window.innerWidth, window.innerHeight))
892
+ return false;
893
+ const style = window.getComputedStyle(element);
894
+ if (style.visibility === "hidden" || style.display === "none")
895
+ return false;
896
+ if (Number.parseFloat(style.opacity || "1") <= 0.01)
897
+ return false;
898
+ return true;
899
+ }
900
+ function isInteractive(element, role) {
901
+ if (role && role !== "heading" && role !== "image" && role !== "list" && role !== "listitem") {
902
+ const interactiveRoles = [
903
+ "button",
904
+ "link",
905
+ "input",
906
+ "select",
907
+ "textarea",
908
+ "checkbox",
909
+ "radio",
910
+ "tab",
911
+ "menuitem",
912
+ "option",
913
+ "switch",
914
+ "slider"
915
+ ];
916
+ if (interactiveRoles.includes(role))
917
+ return true;
918
+ }
919
+ if (element.tabIndex >= 0)
920
+ return true;
921
+ if (element.hasAttribute("onclick") || element.hasAttribute("contenteditable"))
922
+ return true;
923
+ return false;
924
+ }
925
+ function isBlindRegion(role) {
926
+ return role === "video" || role === "iframe" || role === "canvas" || role === "audio" || role === "embed" || role === "object";
927
+ }
928
+ function getIframeHost(element) {
929
+ const src = element.getAttribute("src");
930
+ if (!src)
931
+ return null;
932
+ try {
933
+ return new URL(src, window.location.href).host;
934
+ } catch {
935
+ return null;
936
+ }
937
+ }
938
+ function isSensitiveIframe(element) {
939
+ const host = getIframeHost(element);
940
+ if (!host)
941
+ return false;
942
+ return SENSITIVE_IFRAME_HOSTS.some((sensitiveHost) => host === sensitiveHost || host.endsWith(`.${sensitiveHost}`));
943
+ }
944
+ function isSameOriginIframe(element) {
945
+ const src = element.getAttribute("src");
946
+ if (!src)
947
+ return true;
948
+ try {
949
+ return new URL(src, window.location.href).origin === window.location.origin;
950
+ } catch {
951
+ return false;
952
+ }
953
+ }
954
+ function nearestHeadingText(element) {
955
+ let currentNode = element;
956
+ while (currentNode && currentNode !== document.body) {
957
+ let previousSibling = currentNode.previousElementSibling;
958
+ while (previousSibling) {
959
+ if (/^H[1-6]$/.test(previousSibling.tagName) && previousSibling.textContent) {
960
+ return previousSibling.textContent.trim().slice(0, NAME_MAX_CHARS);
961
+ }
962
+ previousSibling = previousSibling.previousElementSibling;
963
+ }
964
+ currentNode = currentNode.parentElement;
965
+ }
966
+ return null;
967
+ }
968
+ function shouldMaskValue(input) {
969
+ if (input.tagName === "INPUT") {
970
+ const inputElement = input;
971
+ if (MASK_INPUT_TYPES.has(inputElement.type))
972
+ return true;
973
+ const autocompleteAttr = (inputElement.getAttribute("autocomplete") || "").toLowerCase().trim();
974
+ if (MASK_AUTOCOMPLETE_VALUES.has(autocompleteAttr))
975
+ return true;
976
+ }
977
+ const fieldName = (input.getAttribute("name") || "").trim();
978
+ const fieldId = (input.getAttribute("id") || "").trim();
979
+ if (MASK_NAME_PATTERN.test(fieldName) || MASK_NAME_PATTERN.test(fieldId))
980
+ return true;
981
+ return false;
982
+ }
983
+ function escapeAttributeValue(value) {
984
+ return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/[\r\n]+/g, " ");
985
+ }
986
+ function extractAttrs(element, role) {
987
+ const attributes = [];
988
+ if (role === "link") {
989
+ const href = element.getAttribute("href");
990
+ if (href)
991
+ attributes.push(`href="${escapeAttributeValue(href.slice(0, HREF_MAX_CHARS))}"`);
992
+ }
993
+ if (role === "input" || role === "checkbox" || role === "radio") {
994
+ const input = element;
995
+ if (input.type && input.type !== "text")
996
+ attributes.push(`type="${input.type}"`);
997
+ if (shouldMaskValue(input)) {
998
+ attributes.push("valueMasked");
999
+ } else if (input.value && role === "input") {
1000
+ attributes.push(`value="${escapeAttributeValue(input.value.slice(0, VALUE_MAX_CHARS))}"`);
1001
+ }
1002
+ if (input.checked)
1003
+ attributes.push("checked");
1004
+ }
1005
+ if (role === "textarea") {
1006
+ const textarea = element;
1007
+ if (shouldMaskValue(textarea)) {
1008
+ attributes.push("valueMasked");
1009
+ } else if (textarea.value) {
1010
+ attributes.push(`value="${escapeAttributeValue(textarea.value.slice(0, VALUE_MAX_CHARS))}"`);
1011
+ }
1012
+ }
1013
+ if (role === "select") {
1014
+ const select = element;
1015
+ const selectedOption = select.options[select.selectedIndex];
1016
+ if (selectedOption?.value?.trim()) {
1017
+ attributes.push(`selected="${escapeAttributeValue(selectedOption.value.slice(0, VALUE_MAX_CHARS))}"`);
1018
+ }
1019
+ }
1020
+ if (role === "heading") {
1021
+ const headingLevel = element.tagName.match(/^H([1-6])$/)?.[1];
1022
+ if (headingLevel)
1023
+ attributes.push(`level="${headingLevel}"`);
1024
+ }
1025
+ if (role === "video" || role === "audio") {
1026
+ const mediaElement = element;
1027
+ try {
1028
+ if (Number.isFinite(mediaElement.currentTime)) {
1029
+ attributes.push(`currentTime="${mediaElement.currentTime.toFixed(1)}"`);
1030
+ }
1031
+ if (Number.isFinite(mediaElement.duration) && mediaElement.duration > 0) {
1032
+ attributes.push(`duration="${mediaElement.duration.toFixed(1)}"`);
1033
+ }
1034
+ attributes.push(mediaElement.paused ? "paused" : "playing");
1035
+ if (mediaElement.muted)
1036
+ attributes.push("muted");
1037
+ const mediaSrc = mediaElement.currentSrc || mediaElement.getAttribute("src");
1038
+ if (mediaSrc) {
1039
+ try {
1040
+ attributes.push(`srcHost="${new URL(mediaSrc, window.location.href).host}"`);
1041
+ } catch {}
1042
+ }
1043
+ } catch {}
1044
+ }
1045
+ if (role === "iframe") {
1046
+ const host = getIframeHost(element);
1047
+ if (host)
1048
+ attributes.push(`srcHost="${host}"`);
1049
+ if (isSensitiveIframe(element)) {
1050
+ attributes.push("sensitive");
1051
+ } else if (!isSameOriginIframe(element)) {
1052
+ attributes.push("cross-origin");
1053
+ }
1054
+ const iframeRect = element.getBoundingClientRect();
1055
+ attributes.push(`size="${Math.round(iframeRect.width)}x${Math.round(iframeRect.height)}"`);
1056
+ }
1057
+ if (role === "canvas") {
1058
+ const canvas = element;
1059
+ attributes.push(`size="${canvas.width}x${canvas.height}"`);
1060
+ const heading = nearestHeadingText(element);
1061
+ if (heading)
1062
+ attributes.push(`nearestHeading="${escapeAttributeValue(heading)}"`);
1063
+ }
1064
+ if (role === "image") {
1065
+ const image = element;
1066
+ const imageSrc = image.getAttribute("src") || "";
1067
+ if (/\.gif(\?|$)/i.test(imageSrc))
1068
+ attributes.push("animated");
1069
+ }
1070
+ const isNativelyDisabled = "disabled" in element && element.disabled === true;
1071
+ if (isNativelyDisabled || element.getAttribute("aria-disabled") === "true") {
1072
+ attributes.push("disabled");
1073
+ }
1074
+ return attributes;
1075
+ }
1076
+ function ensureStableRef(element) {
1077
+ const existingRef = element.getAttribute(REF_ATTR);
1078
+ if (existingRef) {
1079
+ advanceCounterPastExisting(existingRef);
1080
+ return existingRef;
1081
+ }
1082
+ const newRef = generateNextRef();
1083
+ element.setAttribute(REF_ATTR, newRef);
1084
+ return newRef;
1085
+ }
1086
+ function formatNodeAsLine(entry) {
1087
+ const indent = " ".repeat(entry.depth);
1088
+ const namePart = entry.name ? ` "${escapeAttributeValue(entry.name)}"` : "";
1089
+ const attrsPart = entry.attrs.length > 0 ? ` ${entry.attrs.join(" ")}` : "";
1090
+ return `${indent}${entry.role}${namePart} ${entry.ref}${attrsPart}`;
1091
+ }
1092
+ function shouldSkipSubtree(element) {
1093
+ if (element.id === WIDGET_ROOT_ID)
1094
+ return true;
1095
+ if (element.hasAttribute(PRIVATE_ATTR))
1096
+ return true;
1097
+ const tagName = element.tagName;
1098
+ if (tagName === "SCRIPT" || tagName === "STYLE" || tagName === "NOSCRIPT")
1099
+ return true;
1100
+ return false;
1101
+ }
1102
+ function shouldEmitElement(element, role) {
1103
+ if (role === null)
1104
+ return false;
1105
+ return isInteractive(element, role) || role === "heading" || role === "image" || role === "label" || isBlindRegion(role);
1106
+ }
1107
+ function getSameOriginIframeBody(element) {
1108
+ if (element.tagName !== "IFRAME")
1109
+ return null;
1110
+ if (!isSameOriginIframe(element) || isSensitiveIframe(element))
1111
+ return null;
1112
+ try {
1113
+ return element.contentDocument?.body ?? null;
1114
+ } catch {
1115
+ return null;
1116
+ }
1117
+ }
1118
+ function walkAndCollect(element, depth, entries) {
1119
+ if (shouldSkipSubtree(element))
1120
+ return;
1121
+ const rect = element.getBoundingClientRect();
1122
+ const role = inferRole(element);
1123
+ const isVisibleAndEmittable = role !== null && isVisible(element, rect) && shouldEmitElement(element, role);
1124
+ let childDepth = depth;
1125
+ if (isVisibleAndEmittable && role) {
1126
+ const ref = ensureStableRef(element);
1127
+ entries.push({
1128
+ depth,
1129
+ role,
1130
+ name: accessibleName(element),
1131
+ ref,
1132
+ attrs: extractAttrs(element, role),
1133
+ area: rect.width * rect.height
1134
+ });
1135
+ childDepth = depth + 1;
1136
+ }
1137
+ if (role === "iframe") {
1138
+ const iframeBody = getSameOriginIframeBody(element);
1139
+ if (iframeBody) {
1140
+ for (const child of Array.from(iframeBody.children)) {
1141
+ walkAndCollect(child, childDepth, entries);
1142
+ }
1143
+ }
1144
+ return;
1145
+ }
1146
+ if (role && isBlindRegion(role))
1147
+ return;
1148
+ for (const child of Array.from(element.children)) {
1149
+ walkAndCollect(child, childDepth, entries);
1150
+ }
1151
+ }
1152
+ function dropSmallestAreaEntriesUntilUnderLimit(entries) {
1153
+ if (entries.length <= NODE_LIMIT)
1154
+ return entries;
1155
+ const indexedEntries = entries.map((entry, index2) => ({ entry, index: index2 }));
1156
+ const entriesByAreaAscending = indexedEntries.slice(1).sort((a, b) => a.entry.area - b.entry.area);
1157
+ const indicesToDrop = new Set(entriesByAreaAscending.slice(0, entries.length - NODE_LIMIT).map((indexed) => indexed.index));
1158
+ const keptEntries = [];
1159
+ for (let i = 0;i < entries.length; i++) {
1160
+ if (!indicesToDrop.has(i))
1161
+ keptEntries.push(entries[i]);
1162
+ }
1163
+ return keptEntries;
1164
+ }
1165
+ function buildAccessibilityTree() {
1166
+ const collectedEntries = [];
1167
+ const pageTitle = document.title || window.location.pathname;
1168
+ collectedEntries.push({
1169
+ depth: 0,
1170
+ role: "page",
1171
+ name: pageTitle.slice(0, NAME_MAX_CHARS),
1172
+ ref: `${REF_PREFIX}0`,
1173
+ attrs: [],
1174
+ area: window.innerWidth * window.innerHeight
1175
+ });
1176
+ if (document.body) {
1177
+ for (const child of Array.from(document.body.children)) {
1178
+ walkAndCollect(child, 1, collectedEntries);
1179
+ }
1180
+ }
1181
+ const exceededNodeLimit = collectedEntries.length > NODE_LIMIT;
1182
+ const finalEntries = exceededNodeLimit ? dropSmallestAreaEntriesUntilUnderLimit(collectedEntries) : collectedEntries;
1183
+ const treeLines = finalEntries.map(formatNodeAsLine);
1184
+ let pageContent = treeLines.join(`
1185
+ `);
1186
+ const exceededByteLimit = pageContent.length > PAGE_CONTENT_MAX_BYTES;
1187
+ if (exceededByteLimit) {
1188
+ pageContent = `${pageContent.slice(0, PAGE_CONTENT_MAX_BYTES)}
1189
+ <!-- truncated -->`;
1190
+ }
1191
+ return {
1192
+ pageContent,
1193
+ viewport: { width: window.innerWidth, height: window.innerHeight },
1194
+ refCount: finalEntries.length - 1,
1195
+ truncated: exceededNodeLimit || exceededByteLimit
1196
+ };
1197
+ }
1198
+
1199
+ // src/capture/domEvents.ts
1200
+ var MUTATION_DEBOUNCE_MS = 250;
1201
+ var textEncoder = new TextEncoder;
1202
+ function isInsideWidget(target) {
1203
+ if (!(target instanceof Node))
1204
+ return false;
1205
+ let node = target;
1206
+ while (node) {
1207
+ if (node instanceof Element && node.id === WIDGET_ROOT_ID)
1208
+ return true;
1209
+ node = node.parentNode;
1210
+ }
1211
+ return false;
1212
+ }
1213
+ function findNearestRef(element) {
1214
+ let currentNode = element;
1215
+ while (currentNode) {
1216
+ const ref = currentNode.getAttribute(REF_ATTR);
1217
+ if (ref)
1218
+ return { ref, node: currentNode };
1219
+ currentNode = currentNode.parentElement;
1220
+ }
1221
+ return null;
1222
+ }
1223
+ function installDomEventListeners(localParticipant, options = {}) {
1224
+ const { onTriggerEvent } = options;
1225
+ function publishDomEvent(event) {
1226
+ try {
1227
+ localParticipant.publishData(textEncoder.encode(JSON.stringify(event)), {
1228
+ reliable: true,
1229
+ topic: DOM_EVENTS_TOPIC
1230
+ }).catch(() => {
1231
+ return;
1232
+ });
1233
+ } catch {}
1234
+ if (onTriggerEvent) {
1235
+ try {
1236
+ onTriggerEvent();
1237
+ } catch {}
1238
+ }
1239
+ }
1240
+ const onClick = (event) => {
1241
+ if (isInsideWidget(event.target))
1242
+ return;
1243
+ if (!(event.target instanceof Element))
1244
+ return;
1245
+ const matchedRef = findNearestRef(event.target);
1246
+ const targetElement = matchedRef?.node ?? event.target;
1247
+ publishDomEvent({
1248
+ type: "click",
1249
+ ref: matchedRef?.ref ?? null,
1250
+ role: inferRole(targetElement) ?? targetElement.tagName.toLowerCase(),
1251
+ name: accessibleName(targetElement, { quick: true }),
1252
+ timestamp: Date.now()
1253
+ });
1254
+ };
1255
+ document.addEventListener("click", onClick, true);
1256
+ const onSubmit = (event) => {
1257
+ if (isInsideWidget(event.target))
1258
+ return;
1259
+ if (!(event.target instanceof HTMLFormElement))
1260
+ return;
1261
+ const submittedForm = event.target;
1262
+ publishDomEvent({
1263
+ type: "submit",
1264
+ ref: submittedForm.getAttribute(REF_ATTR),
1265
+ formName: (submittedForm.name || submittedForm.id || "").slice(0, NAME_MAX_CHARS),
1266
+ action: (submittedForm.action || "").slice(0, NAME_MAX_CHARS),
1267
+ timestamp: Date.now()
1268
+ });
1269
+ };
1270
+ document.addEventListener("submit", onSubmit, true);
1271
+ const originalPushState = history.pushState;
1272
+ const originalReplaceState = history.replaceState;
1273
+ let previousUrl = window.location.href;
1274
+ const emitNavigationIfChanged = () => {
1275
+ const nextUrl = window.location.href;
1276
+ if (nextUrl === previousUrl)
1277
+ return;
1278
+ publishDomEvent({
1279
+ type: "navigation",
1280
+ from: previousUrl,
1281
+ to: nextUrl,
1282
+ title: document.title.slice(0, NAME_MAX_CHARS),
1283
+ timestamp: Date.now()
1284
+ });
1285
+ previousUrl = nextUrl;
1286
+ };
1287
+ history.pushState = function patchedPushState(...args) {
1288
+ originalPushState.apply(this, args);
1289
+ queueMicrotask(emitNavigationIfChanged);
1290
+ };
1291
+ history.replaceState = function patchedReplaceState(...args) {
1292
+ originalReplaceState.apply(this, args);
1293
+ queueMicrotask(emitNavigationIfChanged);
1294
+ };
1295
+ const onPopState = () => emitNavigationIfChanged();
1296
+ const onHashChange = () => emitNavigationIfChanged();
1297
+ window.addEventListener("popstate", onPopState);
1298
+ window.addEventListener("hashchange", onHashChange);
1299
+ let pendingAddedRefs = new Set;
1300
+ let pendingRemovedRefs = new Set;
1301
+ let pendingMutationCount = 0;
1302
+ let mutationFlushTimer = null;
1303
+ const flushPendingMutations = () => {
1304
+ mutationFlushTimer = null;
1305
+ if (pendingMutationCount === 0)
1306
+ return;
1307
+ publishDomEvent({
1308
+ type: "mutation",
1309
+ summary: `${pendingMutationCount} subtree changes`,
1310
+ addedRefs: Array.from(pendingAddedRefs),
1311
+ removedRefs: Array.from(pendingRemovedRefs),
1312
+ timestamp: Date.now()
1313
+ });
1314
+ pendingAddedRefs = new Set;
1315
+ pendingRemovedRefs = new Set;
1316
+ pendingMutationCount = 0;
1317
+ };
1318
+ const collectDescendantRefs = (node, targetSet) => {
1319
+ if (!(node instanceof Element))
1320
+ return;
1321
+ if (node.id === WIDGET_ROOT_ID)
1322
+ return;
1323
+ const ref = node.getAttribute(REF_ATTR);
1324
+ if (ref)
1325
+ targetSet.add(ref);
1326
+ for (const child of Array.from(node.children))
1327
+ collectDescendantRefs(child, targetSet);
1328
+ };
1329
+ const observer = new MutationObserver((mutations) => {
1330
+ let observedCount = 0;
1331
+ for (const mutation of mutations) {
1332
+ if (mutation.type !== "childList")
1333
+ continue;
1334
+ if (mutation.target instanceof Element && isInsideWidget(mutation.target))
1335
+ continue;
1336
+ if (mutation.addedNodes.length === 0 && mutation.removedNodes.length === 0)
1337
+ continue;
1338
+ for (const added of Array.from(mutation.addedNodes)) {
1339
+ collectDescendantRefs(added, pendingAddedRefs);
1340
+ }
1341
+ for (const removed of Array.from(mutation.removedNodes)) {
1342
+ collectDescendantRefs(removed, pendingRemovedRefs);
1343
+ }
1344
+ observedCount += 1;
1345
+ }
1346
+ if (observedCount === 0)
1347
+ return;
1348
+ pendingMutationCount += observedCount;
1349
+ if (mutationFlushTimer)
1350
+ clearTimeout(mutationFlushTimer);
1351
+ mutationFlushTimer = setTimeout(flushPendingMutations, MUTATION_DEBOUNCE_MS);
1352
+ });
1353
+ observer.observe(document.body, { childList: true, subtree: true });
1354
+ return () => {
1355
+ document.removeEventListener("click", onClick, true);
1356
+ document.removeEventListener("submit", onSubmit, true);
1357
+ window.removeEventListener("popstate", onPopState);
1358
+ window.removeEventListener("hashchange", onHashChange);
1359
+ history.pushState = originalPushState;
1360
+ history.replaceState = originalReplaceState;
1361
+ if (mutationFlushTimer)
1362
+ clearTimeout(mutationFlushTimer);
1363
+ observer.disconnect();
1364
+ };
1365
+ }
1366
+
1367
+ // src/capture/snapdom.ts
1368
+ var cachedSnapdomModule = null;
1369
+ function loadSnapdom() {
1370
+ if (!cachedSnapdomModule)
1371
+ cachedSnapdomModule = import("@zumer/snapdom");
1372
+ return cachedSnapdomModule;
1373
+ }
1374
+ async function snapToCanvas(element, options = {}) {
1375
+ const snapdomModule = await loadSnapdom();
1376
+ return snapdomModule.snapdom.toCanvas(element, options);
1377
+ }
1378
+
1379
+ // src/components/DomCapture.tsx
1380
+ var SNAPSHOT_INTERVAL_MS = 2000;
1381
+ var A11Y_PUBLISH_INTERVAL_MS = 2000;
1382
+ var CAPTURE_PRESET = ScreenSharePresets2.h1080fps30;
1383
+ var CAPTURE_FPS = CAPTURE_PRESET.encoding.maxFramerate ?? 30;
1384
+ var CAPTURE_BITRATE = 4000000;
1385
+ var DOM_SNAPSHOT_GZIP_THRESHOLD_BYTES = 14000;
1386
+ var MAX_CANVAS_DPR = 1.5;
1387
+ var CONSECUTIVE_FAILURES_BEFORE_REPORT = 3;
1388
+ var textEncoder2 = new TextEncoder;
1389
+ function shouldIncludeInSnapshot(element) {
1390
+ if (!(element instanceof Element))
1391
+ return true;
1392
+ if (element.id === WIDGET_ROOT_ID)
1393
+ return false;
1394
+ if (element.hasAttribute?.(PRIVATE_ATTR))
1395
+ return false;
1396
+ return true;
1397
+ }
1398
+ function createPublishingCanvas() {
1399
+ const dprBoost = Math.min(window.devicePixelRatio || 1, MAX_CANVAS_DPR);
1400
+ const canvas = document.createElement("canvas");
1401
+ canvas.width = Math.round(CAPTURE_PRESET.width * dprBoost);
1402
+ canvas.height = Math.round(CAPTURE_PRESET.height * dprBoost);
1403
+ const ctx = canvas.getContext("2d");
1404
+ if (!ctx)
1405
+ return null;
1406
+ ctx.fillStyle = "#ffffff";
1407
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
1408
+ return { canvas, ctx };
1409
+ }
1410
+ function getCanvasCaptureStream(canvas, fps) {
1411
+ const captureStreamFn = canvas.captureStream;
1412
+ if (typeof captureStreamFn !== "function") {
1413
+ console.error("canvas.captureStream is not supported in this browser");
1414
+ return null;
1415
+ }
1416
+ return captureStreamFn.call(canvas, fps);
1417
+ }
1418
+ async function paintViewportSnapshot(canvas, ctx) {
1419
+ const dpr = window.devicePixelRatio || 1;
1420
+ const snapshotCanvas = await snapToCanvas(document.documentElement, {
1421
+ filter: shouldIncludeInSnapshot,
1422
+ filterMode: "remove",
1423
+ backgroundColor: "#ffffff",
1424
+ fast: true,
1425
+ dpr
1426
+ });
1427
+ const sourceX = window.scrollX * dpr;
1428
+ const sourceY = window.scrollY * dpr;
1429
+ const sourceWidth = window.innerWidth * dpr;
1430
+ const sourceHeight = window.innerHeight * dpr;
1431
+ const fitScale = Math.min(canvas.width / sourceWidth, canvas.height / sourceHeight);
1432
+ const destWidth = sourceWidth * fitScale;
1433
+ const destHeight = sourceHeight * fitScale;
1434
+ const destX = (canvas.width - destWidth) / 2;
1435
+ const destY = (canvas.height - destHeight) / 2;
1436
+ ctx.fillStyle = "#ffffff";
1437
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
1438
+ ctx.drawImage(snapshotCanvas, sourceX, sourceY, sourceWidth, sourceHeight, destX, destY, destWidth, destHeight);
1439
+ }
1440
+ async function gzipIfBeneficial(snapshotBytes) {
1441
+ if (typeof CompressionStream === "undefined" || snapshotBytes.byteLength <= DOM_SNAPSHOT_GZIP_THRESHOLD_BYTES) {
1442
+ return { bytes: snapshotBytes, compressionFlag: 0 };
1443
+ }
1444
+ try {
1445
+ const blob = new Blob([new Uint8Array(snapshotBytes)]);
1446
+ const stream = blob.stream().pipeThrough(new CompressionStream("gzip"));
1447
+ const compressedBytes = new Uint8Array(await new Response(stream).arrayBuffer());
1448
+ return { bytes: compressedBytes, compressionFlag: 1 };
1449
+ } catch {
1450
+ return { bytes: snapshotBytes, compressionFlag: 0 };
1451
+ }
1452
+ }
1453
+ async function buildDomSnapshotFrame() {
1454
+ const a11yTree = buildAccessibilityTree();
1455
+ const snapshotJson = JSON.stringify({
1456
+ pageContent: a11yTree.pageContent,
1457
+ viewport: a11yTree.viewport,
1458
+ truncated: a11yTree.truncated,
1459
+ url: window.location.href,
1460
+ title: document.title,
1461
+ capturedAt: new Date().toISOString()
1462
+ });
1463
+ const snapshotBytes = textEncoder2.encode(snapshotJson);
1464
+ const { bytes, compressionFlag } = await gzipIfBeneficial(snapshotBytes);
1465
+ const frame = new Uint8Array(bytes.byteLength + 1);
1466
+ frame[0] = compressionFlag;
1467
+ frame.set(bytes, 1);
1468
+ return frame;
1469
+ }
1470
+ function reportCaptureError(localParticipant) {
1471
+ try {
1472
+ localParticipant.publishData(textEncoder2.encode(JSON.stringify({ type: "capture_error" })), {
1473
+ reliable: true,
1474
+ topic: DOM_SNAPSHOT_TOPIC
1475
+ }).catch(() => {
1476
+ return;
1477
+ });
1478
+ } catch {}
1479
+ }
1480
+ async function unpublishAndStopTrack(localParticipant, videoTrack) {
1481
+ try {
1482
+ await localParticipant.unpublishTrack(videoTrack);
1483
+ } catch {}
1484
+ videoTrack.stop();
1485
+ }
1486
+ function DomCapture() {
1487
+ const { localParticipant } = useLocalParticipant3();
1488
+ const connectionState = useConnectionState2();
1489
+ const didStartRef = useRef2(false);
1490
+ useEffect4(() => {
1491
+ if (didStartRef.current)
1492
+ return;
1493
+ if (connectionState !== ConnectionState2.Connected)
1494
+ return;
1495
+ const canvasAndCtx = createPublishingCanvas();
1496
+ if (!canvasAndCtx)
1497
+ return;
1498
+ const { canvas, ctx } = canvasAndCtx;
1499
+ const canvasStream = getCanvasCaptureStream(canvas, CAPTURE_FPS);
1500
+ if (!canvasStream)
1501
+ return;
1502
+ const videoTrack = canvasStream.getVideoTracks()[0] ?? null;
1503
+ if (!videoTrack)
1504
+ return;
1505
+ videoTrack.contentHint = "text";
1506
+ didStartRef.current = true;
1507
+ let cancelled = false;
1508
+ let snapshotInFlight = false;
1509
+ let a11yPublishInFlight = false;
1510
+ let consecutiveCaptureFailures = 0;
1511
+ localParticipant.setMicrophoneEnabled(true).catch((error) => console.error("Failed to enable microphone:", error));
1512
+ localParticipant.publishTrack(videoTrack, {
1513
+ source: Track2.Source.ScreenShare,
1514
+ videoEncoding: {
1515
+ maxBitrate: CAPTURE_BITRATE,
1516
+ maxFramerate: CAPTURE_FPS
1517
+ },
1518
+ simulcast: false
1519
+ }).catch((error) => {
1520
+ console.error("Failed to publish DOM capture track:", error);
1521
+ for (const track of canvasStream.getTracks())
1522
+ track.stop();
1523
+ });
1524
+ const tickSnapshot = async () => {
1525
+ if (cancelled || snapshotInFlight)
1526
+ return;
1527
+ snapshotInFlight = true;
1528
+ try {
1529
+ await paintViewportSnapshot(canvas, ctx);
1530
+ if (cancelled)
1531
+ return;
1532
+ consecutiveCaptureFailures = 0;
1533
+ } catch {
1534
+ if (cancelled)
1535
+ return;
1536
+ consecutiveCaptureFailures += 1;
1537
+ if (consecutiveCaptureFailures === CONSECUTIVE_FAILURES_BEFORE_REPORT) {
1538
+ reportCaptureError(localParticipant);
1539
+ }
1540
+ } finally {
1541
+ snapshotInFlight = false;
1542
+ }
1543
+ };
1544
+ const tickA11yPublish = async () => {
1545
+ if (cancelled || a11yPublishInFlight)
1546
+ return;
1547
+ a11yPublishInFlight = true;
1548
+ try {
1549
+ const frame = await buildDomSnapshotFrame();
1550
+ if (cancelled)
1551
+ return;
1552
+ await localParticipant.publishData(frame, {
1553
+ reliable: true,
1554
+ topic: DOM_SNAPSHOT_TOPIC
1555
+ });
1556
+ } catch {} finally {
1557
+ a11yPublishInFlight = false;
1558
+ }
1559
+ };
1560
+ const cleanupDomEventListeners = installDomEventListeners(localParticipant, {
1561
+ onTriggerEvent: () => {
1562
+ tickA11yPublish();
1563
+ tickSnapshot();
1564
+ }
1565
+ });
1566
+ tickSnapshot();
1567
+ tickA11yPublish();
1568
+ const snapshotTimer = setInterval(tickSnapshot, SNAPSHOT_INTERVAL_MS);
1569
+ const a11yPublishTimer = setInterval(tickA11yPublish, A11Y_PUBLISH_INTERVAL_MS);
1570
+ return () => {
1571
+ cancelled = true;
1572
+ didStartRef.current = false;
1573
+ clearInterval(snapshotTimer);
1574
+ clearInterval(a11yPublishTimer);
1575
+ cleanupDomEventListeners();
1576
+ unpublishAndStopTrack(localParticipant, videoTrack);
1577
+ for (const track of canvasStream.getTracks())
1578
+ track.stop();
1579
+ };
1580
+ }, [connectionState, localParticipant]);
1581
+ return null;
1582
+ }
1583
+
1584
+ // src/components/MinimizedBubble.tsx
1585
+ import { useEffect as useEffect5 } from "react";
1586
+
600
1587
  // src/lib/utils.ts
601
1588
  import { clsx } from "clsx";
602
1589
  import { twMerge } from "tailwind-merge";
@@ -606,12 +1593,12 @@ function cn(...inputs) {
606
1593
 
607
1594
  // src/components/Logo.tsx
608
1595
  import { useId } from "react";
609
- import { jsx, jsxs } from "react/jsx-runtime";
1596
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
610
1597
  function Logo({ className }) {
611
1598
  const reactId = useId().replace(/:/g, "");
612
1599
  const clipId = `skippr-logo-clip-${reactId}`;
613
1600
  const gradientId = `skippr-logo-gradient-${reactId}`;
614
- return /* @__PURE__ */ jsxs("svg", {
1601
+ return /* @__PURE__ */ jsxs2("svg", {
615
1602
  width: "1em",
616
1603
  height: "1em",
617
1604
  viewBox: "0 0 30 30",
@@ -621,14 +1608,14 @@ function Logo({ className }) {
621
1608
  "aria-label": "Skippr",
622
1609
  className,
623
1610
  children: [
624
- /* @__PURE__ */ jsxs("g", {
1611
+ /* @__PURE__ */ jsxs2("g", {
625
1612
  clipPath: `url(#${clipId})`,
626
1613
  children: [
627
- /* @__PURE__ */ jsx("path", {
1614
+ /* @__PURE__ */ jsx2("path", {
628
1615
  d: "M0 10C0 4.47715 4.47715 0 10 0H20C25.5228 0 30 4.47715 30 10V20C30 25.5228 25.5228 30 20 30H10C4.47715 30 0 25.5228 0 20V10Z",
629
1616
  fill: "#2D2D3F"
630
1617
  }),
631
- /* @__PURE__ */ jsx("rect", {
1618
+ /* @__PURE__ */ jsx2("rect", {
632
1619
  x: "7.83325",
633
1620
  y: "14.9404",
634
1621
  width: "12.4083",
@@ -637,11 +1624,11 @@ function Logo({ className }) {
637
1624
  transform: "rotate(-45 7.83325 14.9404)",
638
1625
  fill: "#52FFF9"
639
1626
  }),
640
- /* @__PURE__ */ jsx("path", {
1627
+ /* @__PURE__ */ jsx2("path", {
641
1628
  d: "M18.8975 12.5928C20.2728 12.5928 21.3877 13.647 21.3877 14.9474C21.3877 16.2479 20.2728 17.3021 18.8975 17.3021L11.4269 17.3021C10.0516 17.3021 8.93665 16.2479 8.93665 14.9474C8.93665 13.647 10.0516 12.5928 11.4269 12.5928L18.8975 12.5928Z",
642
1629
  fill: `url(#${gradientId})`
643
1630
  }),
644
- /* @__PURE__ */ jsx("rect", {
1631
+ /* @__PURE__ */ jsx2("rect", {
645
1632
  x: "10.1665",
646
1633
  y: "20.4404",
647
1634
  width: "12.4083",
@@ -652,9 +1639,9 @@ function Logo({ className }) {
652
1639
  })
653
1640
  ]
654
1641
  }),
655
- /* @__PURE__ */ jsxs("defs", {
1642
+ /* @__PURE__ */ jsxs2("defs", {
656
1643
  children: [
657
- /* @__PURE__ */ jsxs("linearGradient", {
1644
+ /* @__PURE__ */ jsxs2("linearGradient", {
658
1645
  id: gradientId,
659
1646
  x1: "18.9237",
660
1647
  y1: "16.9997",
@@ -662,19 +1649,19 @@ function Logo({ className }) {
662
1649
  y2: "14.1904",
663
1650
  gradientUnits: "userSpaceOnUse",
664
1651
  children: [
665
- /* @__PURE__ */ jsx("stop", {
1652
+ /* @__PURE__ */ jsx2("stop", {
666
1653
  offset: "0.473958",
667
1654
  stopColor: "white"
668
1655
  }),
669
- /* @__PURE__ */ jsx("stop", {
1656
+ /* @__PURE__ */ jsx2("stop", {
670
1657
  offset: "1",
671
1658
  stopColor: "#52FFF9"
672
1659
  })
673
1660
  ]
674
1661
  }),
675
- /* @__PURE__ */ jsx("clipPath", {
1662
+ /* @__PURE__ */ jsx2("clipPath", {
676
1663
  id: clipId,
677
- children: /* @__PURE__ */ jsx("rect", {
1664
+ children: /* @__PURE__ */ jsx2("rect", {
678
1665
  width: "30",
679
1666
  height: "30",
680
1667
  fill: "white"
@@ -687,18 +1674,18 @@ function Logo({ className }) {
687
1674
  }
688
1675
 
689
1676
  // src/components/ui/tooltip.tsx
690
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1677
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
691
1678
  var ALIGN_CLASSES = {
692
1679
  center: "skippr:left-1/2 skippr:-translate-x-1/2",
693
1680
  start: "skippr:left-0",
694
1681
  end: "skippr:right-0"
695
1682
  };
696
1683
  function Tooltip({ label, children, position = "top", align = "center" }) {
697
- return /* @__PURE__ */ jsxs2("span", {
1684
+ return /* @__PURE__ */ jsxs3("span", {
698
1685
  className: "skippr:relative skippr:inline-flex skippr:group",
699
1686
  children: [
700
1687
  children,
701
- /* @__PURE__ */ jsx2("span", {
1688
+ /* @__PURE__ */ jsx3("span", {
702
1689
  className: cn("skippr:pointer-events-none skippr:absolute skippr:z-10", "skippr:whitespace-nowrap skippr:rounded-md skippr:bg-foreground skippr:px-2 skippr:py-1", "skippr:text-[11px] skippr:text-background skippr:font-medium", "skippr:opacity-0 skippr:scale-95 skippr:transition-all skippr:duration-150", "skippr:group-hover:opacity-100 skippr:group-hover:scale-100", "skippr:group-focus-within:opacity-100 skippr:group-focus-within:scale-100", ALIGN_CLASSES[align], position === "top" && "skippr:bottom-full skippr:mb-1.5", position === "bottom" && "skippr:top-full skippr:mt-1.5"),
703
1690
  "aria-hidden": "true",
704
1691
  children: label
@@ -708,111 +1695,68 @@ function Tooltip({ label, children, position = "top", align = "center" }) {
708
1695
  }
709
1696
 
710
1697
  // src/components/MinimizedBubble.tsx
711
- import { jsx as jsx3, jsxs as jsxs3, Fragment } from "react/jsx-runtime";
1698
+ import { jsx as jsx4, jsxs as jsxs4, Fragment as Fragment2 } from "react/jsx-runtime";
712
1699
  var BUBBLE_BUTTON = "skippr:flex skippr:size-12 skippr:cursor-pointer skippr:items-center skippr:justify-center skippr:rounded-[14px] skippr:shadow-[0_4px_16px_rgba(45,43,61,0.45),0_2px_4px_rgba(0,0,0,0.1)] skippr:transition-all skippr:hover:-translate-y-0.5 skippr:active:translate-y-0";
713
- function AgentBubbleContent({ agentState }) {
714
- if (agentState === "speaking") {
715
- return /* @__PURE__ */ jsxs3("div", {
716
- className: "skippr:flex skippr:h-5 skippr:items-center skippr:justify-center skippr:gap-[3px]",
717
- children: [
718
- /* @__PURE__ */ jsx3("span", {
719
- className: "skippr:w-[3px] skippr:h-2 skippr:rounded-sm skippr:bg-white skippr:animate-[skippr-speak_1.1s_ease-in-out_infinite]"
720
- }),
721
- /* @__PURE__ */ jsx3("span", {
722
- className: "skippr:w-[3px] skippr:h-3.5 skippr:rounded-sm skippr:bg-white skippr:animate-[skippr-speak_1.1s_ease-in-out_0.15s_infinite]"
723
- }),
724
- /* @__PURE__ */ jsx3("span", {
725
- className: "skippr:w-[3px] skippr:h-2.5 skippr:rounded-sm skippr:bg-white skippr:animate-[skippr-speak_1.1s_ease-in-out_0.3s_infinite]"
726
- }),
727
- /* @__PURE__ */ jsx3("span", {
728
- className: "skippr:w-[3px] skippr:h-4 skippr:rounded-sm skippr:bg-white skippr:animate-[skippr-speak_1.1s_ease-in-out_0.45s_infinite]"
729
- }),
730
- /* @__PURE__ */ jsx3("span", {
731
- className: "skippr:w-[3px] skippr:h-1.5 skippr:rounded-sm skippr:bg-white skippr:animate-[skippr-speak_1.1s_ease-in-out_0.6s_infinite]"
732
- })
733
- ]
734
- });
735
- }
736
- if (agentState === "thinking") {
737
- return /* @__PURE__ */ jsxs3("div", {
738
- className: "skippr:flex skippr:items-center skippr:justify-center skippr:gap-[4px]",
739
- children: [
740
- /* @__PURE__ */ jsx3("span", {
741
- className: "skippr:size-1.5 skippr:rounded-full skippr:bg-white skippr:animate-bounce skippr:[animation-delay:0ms]"
742
- }),
743
- /* @__PURE__ */ jsx3("span", {
744
- className: "skippr:size-1.5 skippr:rounded-full skippr:bg-white skippr:animate-bounce skippr:[animation-delay:150ms]"
745
- }),
746
- /* @__PURE__ */ jsx3("span", {
747
- className: "skippr:size-1.5 skippr:rounded-full skippr:bg-white skippr:animate-bounce skippr:[animation-delay:300ms]"
748
- })
749
- ]
750
- });
751
- }
752
- return /* @__PURE__ */ jsx3(Logo, {
753
- className: "skippr:size-7"
754
- });
755
- }
756
1700
  function ConnectedBubbleContent() {
757
- const { expandPanel, disconnect, position } = useLiveAgent();
758
- const { state: agentState } = useAgentVoiceState();
1701
+ const { expandPanel, disconnect, position, captureMode } = useLiveAgent();
759
1702
  const { isMuted, toggleMute, isScreenSharing, toggleScreenShare } = useMediaControls();
760
1703
  const tooltipAlign = position === "right" ? "end" : "start";
761
- return /* @__PURE__ */ jsxs3(Fragment, {
1704
+ const showScreenShareToggle = captureMode === "screenshare";
1705
+ return /* @__PURE__ */ jsxs4(Fragment2, {
762
1706
  children: [
763
- /* @__PURE__ */ jsx3(Tooltip, {
1707
+ /* @__PURE__ */ jsx4(Tooltip, {
764
1708
  label: isMuted ? "Unmute" : "Mute",
765
1709
  align: tooltipAlign,
766
- children: /* @__PURE__ */ jsx3("button", {
1710
+ children: /* @__PURE__ */ jsx4("button", {
767
1711
  type: "button",
768
1712
  onClick: toggleMute,
769
1713
  "aria-label": isMuted ? "Unmute" : "Mute",
770
1714
  className: cn(BUBBLE_BUTTON, isMuted ? "skippr:bg-destructive skippr:text-destructive-foreground skippr:hover:bg-destructive/90" : "skippr:bg-white skippr:text-foreground skippr:hover:bg-muted"),
771
- children: isMuted ? /* @__PURE__ */ jsx3(MicOff, {
1715
+ children: isMuted ? /* @__PURE__ */ jsx4(MicOff, {
772
1716
  className: "skippr:size-5"
773
- }) : /* @__PURE__ */ jsx3(Mic, {
1717
+ }) : /* @__PURE__ */ jsx4(Mic, {
774
1718
  className: "skippr:size-5"
775
1719
  })
776
1720
  })
777
1721
  }),
778
- /* @__PURE__ */ jsx3(Tooltip, {
1722
+ showScreenShareToggle && /* @__PURE__ */ jsx4(Tooltip, {
779
1723
  label: isScreenSharing ? "Stop sharing" : "Share screen",
780
1724
  align: tooltipAlign,
781
- children: /* @__PURE__ */ jsx3("button", {
1725
+ children: /* @__PURE__ */ jsx4("button", {
782
1726
  type: "button",
783
1727
  onClick: toggleScreenShare,
784
1728
  "aria-label": isScreenSharing ? "Stop sharing screen" : "Share screen",
785
1729
  className: cn(BUBBLE_BUTTON, isScreenSharing ? "skippr:bg-bubble skippr:text-white skippr:hover:brightness-110" : "skippr:bg-white skippr:text-foreground skippr:hover:bg-muted"),
786
- children: isScreenSharing ? /* @__PURE__ */ jsx3(MonitorOff, {
1730
+ children: isScreenSharing ? /* @__PURE__ */ jsx4(MonitorOff, {
787
1731
  className: "skippr:size-5"
788
- }) : /* @__PURE__ */ jsx3(Monitor, {
1732
+ }) : /* @__PURE__ */ jsx4(Monitor, {
789
1733
  className: "skippr:size-5"
790
1734
  })
791
1735
  })
792
1736
  }),
793
- /* @__PURE__ */ jsx3(Tooltip, {
1737
+ /* @__PURE__ */ jsx4(Tooltip, {
794
1738
  label: "End session",
795
1739
  align: tooltipAlign,
796
- children: /* @__PURE__ */ jsx3("button", {
1740
+ children: /* @__PURE__ */ jsx4("button", {
797
1741
  type: "button",
798
1742
  onClick: () => disconnect(),
799
1743
  "aria-label": "End session",
800
1744
  className: cn(BUBBLE_BUTTON, "skippr:bg-destructive skippr:text-destructive-foreground skippr:hover:bg-destructive/90"),
801
- children: /* @__PURE__ */ jsx3(PhoneOff, {
1745
+ children: /* @__PURE__ */ jsx4(PhoneOff, {
802
1746
  className: "skippr:size-5"
803
1747
  })
804
1748
  })
805
1749
  }),
806
- /* @__PURE__ */ jsx3(Tooltip, {
1750
+ /* @__PURE__ */ jsx4(Tooltip, {
807
1751
  label: "Open chat & transcript",
808
1752
  align: tooltipAlign,
809
- children: /* @__PURE__ */ jsx3("button", {
1753
+ children: /* @__PURE__ */ jsx4("button", {
810
1754
  type: "button",
811
1755
  onClick: expandPanel,
812
1756
  "aria-label": "Open chat & transcript",
813
1757
  className: cn(BUBBLE_BUTTON, "skippr:bg-bubble skippr:hover:brightness-110"),
814
- children: /* @__PURE__ */ jsx3(AgentBubbleContent, {
815
- agentState
1758
+ children: /* @__PURE__ */ jsx4(Logo, {
1759
+ className: "skippr:size-7"
816
1760
  })
817
1761
  })
818
1762
  })
@@ -822,19 +1766,19 @@ function ConnectedBubbleContent() {
822
1766
  function IdleBubbleContent() {
823
1767
  const { expandPanel, position } = useLiveAgent();
824
1768
  const tooltipAlign = position === "right" ? "end" : "start";
825
- return /* @__PURE__ */ jsx3(Tooltip, {
1769
+ return /* @__PURE__ */ jsx4(Tooltip, {
826
1770
  label: "Open Skippr assistant",
827
1771
  align: tooltipAlign,
828
- children: /* @__PURE__ */ jsxs3("button", {
1772
+ children: /* @__PURE__ */ jsxs4("button", {
829
1773
  type: "button",
830
1774
  onClick: expandPanel,
831
1775
  "aria-label": "Skippr assistant",
832
1776
  className: cn(BUBBLE_BUTTON, "skippr:relative skippr:bg-bubble skippr:hover:brightness-110"),
833
1777
  children: [
834
- /* @__PURE__ */ jsx3(Logo, {
1778
+ /* @__PURE__ */ jsx4(Logo, {
835
1779
  className: "skippr:relative skippr:z-10 skippr:size-7"
836
1780
  }),
837
- /* @__PURE__ */ jsx3("span", {
1781
+ /* @__PURE__ */ jsx4("span", {
838
1782
  className: "skippr:absolute skippr:-inset-[3px] skippr:animate-pulse skippr:rounded-[17px] skippr:border-2 skippr:border-bubble/50"
839
1783
  })
840
1784
  ]
@@ -846,18 +1790,18 @@ function WelcomeBubble({
846
1790
  position,
847
1791
  onDismiss
848
1792
  }) {
849
- useEffect4(() => {
1793
+ useEffect5(() => {
850
1794
  const timer = setTimeout(onDismiss, 5000);
851
1795
  return () => clearTimeout(timer);
852
1796
  }, [onDismiss]);
853
- return /* @__PURE__ */ jsxs3("button", {
1797
+ return /* @__PURE__ */ jsxs4("button", {
854
1798
  type: "button",
855
1799
  className: cn("skippr:absolute skippr:bottom-full skippr:mb-3", "skippr:max-w-64 skippr:rounded-xl skippr:bg-card skippr:shadow-lg", "skippr:border skippr:border-border skippr:px-4 skippr:py-3", "skippr:text-sm skippr:text-foreground skippr:leading-relaxed skippr:text-left", "skippr:animate-[skippr-fade-in_0.3s_ease-out]", "skippr:cursor-pointer", position === "right" ? "skippr:right-0" : "skippr:left-0"),
856
1800
  onClick: onDismiss,
857
1801
  "aria-label": "Dismiss",
858
1802
  children: [
859
1803
  message,
860
- /* @__PURE__ */ jsx3("span", {
1804
+ /* @__PURE__ */ jsx4("span", {
861
1805
  className: cn("skippr:absolute skippr:top-full skippr:size-2.5", "skippr:border-l skippr:border-t skippr:border-border skippr:bg-card", "skippr:rotate-[225deg]", position === "right" ? "skippr:right-5" : "skippr:left-5", "skippr:-mt-[5px]")
862
1806
  })
863
1807
  ]
@@ -870,61 +1814,27 @@ function MinimizedBubble({
870
1814
  }) {
871
1815
  const { isConnected, isStarting, position } = useLiveAgent();
872
1816
  const inSession = isConnected || isStarting;
873
- return /* @__PURE__ */ jsxs3("div", {
1817
+ return /* @__PURE__ */ jsxs4("div", {
874
1818
  className: cn("skippr:fixed skippr:bottom-6 skippr:z-[9999]", "skippr:flex skippr:items-center skippr:gap-2", position === "right" ? "skippr:right-6" : "skippr:left-6"),
875
1819
  children: [
876
- welcomeMessage && !inSession && !welcomeDismissed && /* @__PURE__ */ jsx3(WelcomeBubble, {
1820
+ welcomeMessage && !inSession && !welcomeDismissed && /* @__PURE__ */ jsx4(WelcomeBubble, {
877
1821
  message: welcomeMessage,
878
1822
  position,
879
1823
  onDismiss: onDismissWelcome
880
1824
  }),
881
- inSession ? /* @__PURE__ */ jsx3(ConnectedBubbleContent, {}) : /* @__PURE__ */ jsx3(IdleBubbleContent, {})
1825
+ inSession ? /* @__PURE__ */ jsx4(ConnectedBubbleContent, {}) : /* @__PURE__ */ jsx4(IdleBubbleContent, {})
882
1826
  ]
883
1827
  });
884
1828
  }
885
1829
 
886
- // src/components/ObservingBanner.tsx
887
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
888
- function ObservingBanner() {
889
- const { isConnected } = useLiveAgent();
890
- const { isScreenSharing } = useMediaControls();
891
- if (!isConnected || !isScreenSharing)
892
- return null;
893
- return /* @__PURE__ */ jsx4("div", {
894
- className: "skippr:fixed skippr:top-0 skippr:left-0 skippr:right-0 skippr:z-[2147483647] skippr:flex skippr:justify-center skippr:pointer-events-none",
895
- children: /* @__PURE__ */ jsxs4("div", {
896
- className: "skippr:pointer-events-auto skippr:flex skippr:items-center skippr:gap-2 skippr:bg-indigo-500/95 skippr:backdrop-blur-sm skippr:text-white skippr:text-xs skippr:font-medium skippr:px-4 skippr:py-1.5 skippr:rounded-b-lg skippr:shadow-lg",
897
- children: [
898
- /* @__PURE__ */ jsxs4("span", {
899
- className: "skippr:relative skippr:flex skippr:size-1.5",
900
- children: [
901
- /* @__PURE__ */ jsx4("span", {
902
- className: "skippr:absolute skippr:inline-flex skippr:size-full skippr:animate-ping skippr:rounded-full skippr:bg-emerald-400 skippr:opacity-75"
903
- }),
904
- /* @__PURE__ */ jsx4("span", {
905
- className: "skippr:relative skippr:inline-flex skippr:size-1.5 skippr:rounded-full skippr:bg-emerald-400"
906
- })
907
- ]
908
- }),
909
- /* @__PURE__ */ jsx4(Eye, {
910
- className: "skippr:size-3.5"
911
- }),
912
- /* @__PURE__ */ jsx4("span", {
913
- children: "Skippr is observing this page"
914
- })
915
- ]
916
- })
917
- });
918
- }
919
-
920
1830
  // src/components/Sidebar.tsx
921
- import { useEffect as useEffect11 } from "react";
1831
+ import { useEffect as useEffect12 } from "react";
922
1832
 
923
1833
  // src/hooks/useCombinedMessages.ts
924
1834
  import { useMemo as useMemo4 } from "react";
925
1835
 
926
1836
  // src/hooks/useChatMessages.ts
927
- import { useChat, useLocalParticipant as useLocalParticipant3 } from "@livekit/components-react";
1837
+ import { useChat, useLocalParticipant as useLocalParticipant4 } from "@livekit/components-react/hooks";
928
1838
  import { useMemo as useMemo2 } from "react";
929
1839
 
930
1840
  // src/lib/filterSystemMessages.ts
@@ -936,7 +1846,7 @@ function filterSystemMessages(messages) {
936
1846
  // src/hooks/useChatMessages.ts
937
1847
  function useChatMessages() {
938
1848
  const { chatMessages: rawMessages, send, isSending } = useChat();
939
- const { localParticipant } = useLocalParticipant3();
1849
+ const { localParticipant } = useLocalParticipant4();
940
1850
  const localIdentity = localParticipant.identity;
941
1851
  const chatMessages = useMemo2(() => {
942
1852
  const sortedMessages = rawMessages.map((msg) => ({
@@ -952,11 +1862,11 @@ function useChatMessages() {
952
1862
  }
953
1863
 
954
1864
  // src/hooks/useStreamingTranscript.ts
955
- import { useLocalParticipant as useLocalParticipant4, useTranscriptions } from "@livekit/components-react";
1865
+ import { useLocalParticipant as useLocalParticipant5, useTranscriptions } from "@livekit/components-react/hooks";
956
1866
  import { useMemo as useMemo3 } from "react";
957
1867
  function useStreamingTranscript() {
958
1868
  const transcriptions = useTranscriptions();
959
- const { localParticipant } = useLocalParticipant4();
1869
+ const { localParticipant } = useLocalParticipant5();
960
1870
  const localIdentity = localParticipant.identity;
961
1871
  const transcriptMessages = useMemo3(() => filterSystemMessages(transcriptions.filter((stream) => stream.text.trim().length > 0).map((stream) => ({
962
1872
  id: stream.streamInfo.id,
@@ -1003,12 +1913,12 @@ function useCombinedMessages() {
1003
1913
  import { useCallback as useCallback4 } from "react";
1004
1914
 
1005
1915
  // src/hooks/useAgentState.ts
1006
- import { useRemoteParticipants } from "@livekit/components-react";
1007
- import { useEffect as useEffect5, useState as useState3 } from "react";
1916
+ import { useRemoteParticipants } from "@livekit/components-react/hooks";
1917
+ import { useEffect as useEffect6, useState as useState3 } from "react";
1008
1918
  function useAgentState(attributeKey, parse, initial) {
1009
1919
  const [value, setValue] = useState3(initial);
1010
1920
  const remoteParticipants = useRemoteParticipants();
1011
- useEffect5(() => {
1921
+ useEffect6(() => {
1012
1922
  const agentParticipant = remoteParticipants.find((p) => p.attributes?.[attributeKey]);
1013
1923
  if (agentParticipant) {
1014
1924
  const attr = agentParticipant.attributes?.[attributeKey];
@@ -1060,7 +1970,7 @@ function usePhaseUpdates() {
1060
1970
  }
1061
1971
 
1062
1972
  // src/hooks/useSessionRemaining.ts
1063
- import { useEffect as useEffect6, useRef as useRef2, useState as useState4 } from "react";
1973
+ import { useEffect as useEffect7, useRef as useRef3, useState as useState4 } from "react";
1064
1974
 
1065
1975
  // src/lib/format.ts
1066
1976
  function formatTime(seconds) {
@@ -1076,9 +1986,9 @@ function parseNumber(s) {
1076
1986
  // src/hooks/useSessionRemaining.ts
1077
1987
  function useSessionRemaining() {
1078
1988
  const maxCallDuration = useAgentState("maxCallDuration", parseNumber, null);
1079
- const endTimeRef = useRef2(null);
1989
+ const endTimeRef = useRef3(null);
1080
1990
  const [remaining, setRemaining] = useState4(null);
1081
- useEffect6(() => {
1991
+ useEffect7(() => {
1082
1992
  if (maxCallDuration === null || endTimeRef.current !== null)
1083
1993
  return;
1084
1994
  const endTime = Date.now() + maxCallDuration * 1000;
@@ -1094,14 +2004,11 @@ function useSessionRemaining() {
1094
2004
  return remaining;
1095
2005
  }
1096
2006
 
1097
- // src/lib/constants.ts
1098
- var SIDEBAR_WIDTH = 360;
1099
-
1100
2007
  // src/hooks/useElapsedSeconds.ts
1101
- import { useEffect as useEffect7, useState as useState5 } from "react";
2008
+ import { useEffect as useEffect8, useState as useState5 } from "react";
1102
2009
  function useElapsedSeconds(isRunning) {
1103
2010
  const [elapsed, setElapsed] = useState5(0);
1104
- useEffect7(() => {
2011
+ useEffect8(() => {
1105
2012
  if (!isRunning) {
1106
2013
  setElapsed(0);
1107
2014
  return;
@@ -1199,7 +2106,7 @@ function LoadingDots({ label }) {
1199
2106
  }
1200
2107
 
1201
2108
  // src/components/LoginFlow.tsx
1202
- import { useCallback as useCallback5, useEffect as useEffect8, useRef as useRef3, useState as useState6 } from "react";
2109
+ import { useCallback as useCallback5, useEffect as useEffect9, useRef as useRef4, useState as useState6 } from "react";
1203
2110
 
1204
2111
  // src/components/ui/button.tsx
1205
2112
  import { forwardRef as forwardRef3 } from "react";
@@ -1327,16 +2234,16 @@ function EmailStep({ email, onEmailChange, onSubmit, error, isSubmitting }) {
1327
2234
  function OtpStep({ email, onSubmit, onResend, onBack, error, isSubmitting }) {
1328
2235
  const [digits, setDigits] = useState6(Array(OTP_LENGTH).fill(""));
1329
2236
  const [resendCooldown, setResendCooldown] = useState6(0);
1330
- const inputRefs = useRef3([]);
1331
- const submittedRef = useRef3(false);
1332
- useEffect8(() => {
2237
+ const inputRefs = useRef4([]);
2238
+ const submittedRef = useRef4(false);
2239
+ useEffect9(() => {
1333
2240
  inputRefs.current[0]?.focus();
1334
2241
  }, []);
1335
- useEffect8(() => {
2242
+ useEffect9(() => {
1336
2243
  if (error)
1337
2244
  submittedRef.current = false;
1338
2245
  }, [error]);
1339
- useEffect8(() => {
2246
+ useEffect9(() => {
1340
2247
  if (resendCooldown <= 0)
1341
2248
  return;
1342
2249
  const timer = setTimeout(() => setResendCooldown((c) => c - 1), 1000);
@@ -1479,7 +2386,7 @@ function OtpStep({ email, onSubmit, onResend, onBack, error, isSubmitting }) {
1479
2386
  // src/components/MeetingControls.tsx
1480
2387
  import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1481
2388
  var CONTROL_BUTTON = "skippr:flex skippr:size-11 skippr:cursor-pointer skippr:items-center skippr:justify-center skippr:rounded-full skippr:transition-colors";
1482
- function MeetingControls({ onHangUp }) {
2389
+ function MeetingControls({ onHangUp, showScreenShareToggle = true }) {
1483
2390
  const { isMuted, toggleMute, isScreenSharing, toggleScreenShare } = useMediaControls();
1484
2391
  return /* @__PURE__ */ jsxs8("div", {
1485
2392
  className: "skippr:shrink-0 skippr:border-t skippr:border-border skippr:bg-background skippr:px-4 skippr:py-4",
@@ -1498,7 +2405,7 @@ function MeetingControls({ onHangUp }) {
1498
2405
  className: "skippr:size-5"
1499
2406
  })
1500
2407
  }),
1501
- /* @__PURE__ */ jsx9("button", {
2408
+ showScreenShareToggle && /* @__PURE__ */ jsx9("button", {
1502
2409
  type: "button",
1503
2410
  onClick: toggleScreenShare,
1504
2411
  "aria-label": isScreenSharing ? "Stop sharing screen" : "Share screen",
@@ -1529,17 +2436,17 @@ function MeetingControls({ onHangUp }) {
1529
2436
  }
1530
2437
 
1531
2438
  // src/components/MessageList.tsx
1532
- import { useEffect as useEffect10, useRef as useRef5 } from "react";
2439
+ import { useEffect as useEffect11, useRef as useRef6 } from "react";
1533
2440
 
1534
2441
  // src/components/ChatInput.tsx
1535
- import { useEffect as useEffect9, useRef as useRef4, useState as useState7 } from "react";
2442
+ import { useEffect as useEffect10, useRef as useRef5, useState as useState7 } from "react";
1536
2443
  import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
1537
2444
  var MAX_INPUT_HEIGHT = 60;
1538
2445
  function ChatInput({ sendChatMessage, isSendingChat, autoFocus = false }) {
1539
2446
  const [inputText, setInputText] = useState7("");
1540
- const textareaRef = useRef4(null);
2447
+ const textareaRef = useRef5(null);
1541
2448
  const canSend = inputText.trim().length > 0 && !isSendingChat;
1542
- useEffect9(() => {
2449
+ useEffect10(() => {
1543
2450
  if (autoFocus)
1544
2451
  textareaRef.current?.focus();
1545
2452
  }, [autoFocus]);
@@ -1684,9 +2591,9 @@ function MessageList({
1684
2591
  isSendingChat,
1685
2592
  autoFocus = false
1686
2593
  }) {
1687
- const scrollRef = useRef5(null);
2594
+ const scrollRef = useRef6(null);
1688
2595
  const lastMessage = messages.length > 0 ? messages[messages.length - 1] : undefined;
1689
- useEffect10(() => {
2596
+ useEffect11(() => {
1690
2597
  scrollRef.current?.scrollIntoView({ behavior: "smooth" });
1691
2598
  }, [messages.length, lastMessage?.content]);
1692
2599
  const showTyping = isStreaming && lastMessage?.role === "assistant" && lastMessage.content === "";
@@ -1719,8 +2626,8 @@ function MessageList({
1719
2626
 
1720
2627
  // src/components/SessionAgenda.tsx
1721
2628
  import { jsx as jsx14, jsxs as jsxs13 } from "react/jsx-runtime";
1722
- function SessionAgenda({ phases }) {
1723
- if (phases.length === 0) {
2629
+ function SessionAgenda({ phases, hasStarted }) {
2630
+ if (phases.length === 0 || !hasStarted) {
1724
2631
  return /* @__PURE__ */ jsx14("div", {
1725
2632
  className: "skippr:flex skippr:flex-1 skippr:items-center skippr:justify-center",
1726
2633
  children: /* @__PURE__ */ jsx14(LoadingDots, {
@@ -1816,7 +2723,7 @@ function StartSessionPrompt({
1816
2723
  }
1817
2724
 
1818
2725
  // src/components/Sidebar.tsx
1819
- import { jsx as jsx17, jsxs as jsxs15, Fragment as Fragment2 } from "react/jsx-runtime";
2726
+ import { jsx as jsx17, jsxs as jsxs15, Fragment as Fragment3 } from "react/jsx-runtime";
1820
2727
  function Sidebar({
1821
2728
  hideControls = false,
1822
2729
  hideHeader = false,
@@ -1839,11 +2746,12 @@ function Sidebar({
1839
2746
  isAuthSubmitting,
1840
2747
  sidebarTab: activeTab,
1841
2748
  setSidebarTab: setActiveTab,
1842
- autoFocusChat
2749
+ autoFocusChat,
2750
+ captureMode
1843
2751
  } = useLiveAgent();
1844
2752
  const isFloating = variant === "floating";
1845
2753
  const isSidebar = variant === "sidebar";
1846
- useEffect11(() => {
2754
+ useEffect12(() => {
1847
2755
  if (!isSidebar)
1848
2756
  return;
1849
2757
  const prop = position === "right" ? "marginRight" : "marginLeft";
@@ -1882,7 +2790,8 @@ function Sidebar({
1882
2790
  onTabChange: setActiveTab,
1883
2791
  hideControls,
1884
2792
  startSessionLabel,
1885
- autoFocusChat
2793
+ autoFocusChat,
2794
+ showScreenShareToggle: captureMode === "screenshare"
1886
2795
  })
1887
2796
  ]
1888
2797
  });
@@ -1897,9 +2806,10 @@ function AuthenticatedContent({
1897
2806
  onTabChange,
1898
2807
  hideControls,
1899
2808
  startSessionLabel,
1900
- autoFocusChat
2809
+ autoFocusChat,
2810
+ showScreenShareToggle
1901
2811
  }) {
1902
- return /* @__PURE__ */ jsxs15(Fragment2, {
2812
+ return /* @__PURE__ */ jsxs15(Fragment3, {
1903
2813
  children: [
1904
2814
  isConnected && /* @__PURE__ */ jsx17(ConnectedBanner, {}),
1905
2815
  /* @__PURE__ */ jsxs15("div", {
@@ -1937,7 +2847,7 @@ function AuthenticatedContent({
1937
2847
  }),
1938
2848
  /* @__PURE__ */ jsx17("div", {
1939
2849
  className: "skippr:flex skippr:min-h-0 skippr:flex-1 skippr:flex-col",
1940
- children: isConnected ? /* @__PURE__ */ jsx17(ConnectedBody, {
2850
+ children: isConnected || isStarting ? /* @__PURE__ */ jsx17(ConnectedBody, {
1941
2851
  activeTab,
1942
2852
  autoFocusChat
1943
2853
  }) : /* @__PURE__ */ jsx17("div", {
@@ -1951,7 +2861,8 @@ function AuthenticatedContent({
1951
2861
  }, `${activeTab}-empty`)
1952
2862
  }),
1953
2863
  isConnected && !hideControls && /* @__PURE__ */ jsx17(MeetingControls, {
1954
- onHangUp: onDisconnect
2864
+ onHangUp: onDisconnect,
2865
+ showScreenShareToggle
1955
2866
  })
1956
2867
  ]
1957
2868
  });
@@ -1972,7 +2883,8 @@ function ConnectedBody({
1972
2883
  return /* @__PURE__ */ jsx17("div", {
1973
2884
  className: "skippr:min-h-0 skippr:flex-1 skippr:overflow-y-auto skippr:animate-skippr-tab-fade",
1974
2885
  children: /* @__PURE__ */ jsx17(SessionAgenda, {
1975
- phases
2886
+ phases,
2887
+ hasStarted: allMessages.length > 0 || agentState === "speaking"
1976
2888
  })
1977
2889
  }, "agenda");
1978
2890
  }
@@ -2025,7 +2937,8 @@ function LiveAgent({
2025
2937
  hideHeader = false,
2026
2938
  startSessionLabel = "Talk to Skippr",
2027
2939
  autoFocusChat = true,
2028
- showObservingBanner = true,
2940
+ showAgentStateBanner = true,
2941
+ captureMode = "screenshare",
2029
2942
  children
2030
2943
  }) {
2031
2944
  const auth = useAuth({ appKey });
@@ -2041,6 +2954,7 @@ function LiveAgent({
2041
2954
  pendingScreenStream
2042
2955
  } = useSession({
2043
2956
  agentId,
2957
+ captureMode,
2044
2958
  authToken: effectiveAuthToken,
2045
2959
  appKey,
2046
2960
  userToken
@@ -2079,8 +2993,8 @@ function LiveAgent({
2079
2993
  }, [minimizable]);
2080
2994
  const isConnected = connection !== null;
2081
2995
  const isAuthenticated = !!userToken || !!authTokenProp || auth.isAuthenticated;
2082
- const prevConnectionRef = useRef6(connection);
2083
- useEffect12(() => {
2996
+ const prevConnectionRef = useRef7(connection);
2997
+ useEffect13(() => {
2084
2998
  const connectionChanged = prevConnectionRef.current !== connection;
2085
2999
  prevConnectionRef.current = connection;
2086
3000
  if (connectionChanged && minimizable) {
@@ -2117,7 +3031,8 @@ function LiveAgent({
2117
3031
  isAuthSubmitting: auth.isSubmitting,
2118
3032
  sidebarTab,
2119
3033
  setSidebarTab,
2120
- autoFocusChat
3034
+ autoFocusChat,
3035
+ captureMode
2121
3036
  }), [
2122
3037
  connection,
2123
3038
  shouldConnect,
@@ -2146,7 +3061,8 @@ function LiveAgent({
2146
3061
  auth.logout,
2147
3062
  auth.isSubmitting,
2148
3063
  sidebarTab,
2149
- autoFocusChat
3064
+ autoFocusChat,
3065
+ captureMode
2150
3066
  ]);
2151
3067
  return /* @__PURE__ */ jsx19(LiveAgentContext.Provider, {
2152
3068
  value: ctx,
@@ -2158,20 +3074,26 @@ function LiveAgent({
2158
3074
  onDisconnected: disconnect,
2159
3075
  children: [
2160
3076
  connection && /* @__PURE__ */ jsx19(RoomAudioRenderer, {}),
2161
- showObservingBanner && /* @__PURE__ */ jsx19(ObservingBanner, {}),
2162
- connection && /* @__PURE__ */ jsx19(AutoStartMedia, {
3077
+ connection && captureMode === "screenshare" && /* @__PURE__ */ jsx19(AutoStartMedia, {
2163
3078
  pendingScreenStream
2164
3079
  }),
2165
- isMinimized && /* @__PURE__ */ jsx19(MinimizedBubble, {
2166
- welcomeMessage,
2167
- welcomeDismissed,
2168
- onDismissWelcome: dismissWelcome
2169
- }),
2170
- /* @__PURE__ */ jsx19(SidebarTrigger, {}),
2171
- /* @__PURE__ */ jsx19(Sidebar, {
2172
- hideControls,
2173
- hideHeader,
2174
- startSessionLabel
3080
+ connection && captureMode === "auto" && /* @__PURE__ */ jsx19(DomCapture, {}),
3081
+ /* @__PURE__ */ jsxs16("div", {
3082
+ id: WIDGET_ROOT_ID,
3083
+ children: [
3084
+ showAgentStateBanner && /* @__PURE__ */ jsx19(AgentStateBanner, {}),
3085
+ isMinimized && /* @__PURE__ */ jsx19(MinimizedBubble, {
3086
+ welcomeMessage,
3087
+ welcomeDismissed,
3088
+ onDismissWelcome: dismissWelcome
3089
+ }),
3090
+ /* @__PURE__ */ jsx19(SidebarTrigger, {}),
3091
+ /* @__PURE__ */ jsx19(Sidebar, {
3092
+ hideControls,
3093
+ hideHeader,
3094
+ startSessionLabel
3095
+ })
3096
+ ]
2175
3097
  }),
2176
3098
  children
2177
3099
  ]
@@ -2179,9 +3101,9 @@ function LiveAgent({
2179
3101
  });
2180
3102
  }
2181
3103
  // src/hooks/useIsLocalSpeaking.ts
2182
- import { useIsSpeaking, useLocalParticipant as useLocalParticipant5 } from "@livekit/components-react";
3104
+ import { useIsSpeaking, useLocalParticipant as useLocalParticipant6 } from "@livekit/components-react/hooks";
2183
3105
  function useIsLocalSpeaking() {
2184
- const { localParticipant } = useLocalParticipant5();
3106
+ const { localParticipant } = useLocalParticipant6();
2185
3107
  return useIsSpeaking(localParticipant);
2186
3108
  }
2187
3109
  export {