@cossistant/react 0.0.26 → 0.0.28
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.
- package/api.d.ts +1 -1
- package/api.d.ts.map +1 -1
- package/checks.d.ts +1 -1
- package/checks.d.ts.map +1 -1
- package/clsx.d.ts +1 -1
- package/clsx.d.ts.map +1 -1
- package/coerce.d.ts +1 -1
- package/coerce.d.ts.map +1 -1
- package/conversation.d.ts +3 -0
- package/conversation.d.ts.map +1 -1
- package/core.d.ts +1 -1
- package/core.d.ts.map +1 -1
- package/errors.d.ts +1 -1
- package/errors.d.ts.map +1 -1
- package/errors2.d.ts +1 -1
- package/errors2.d.ts.map +1 -1
- package/hooks/index.d.ts +2 -1
- package/hooks/index.js +6 -5
- package/hooks/private/store/use-website-store.js +2 -1
- package/hooks/private/store/use-website-store.js.map +1 -1
- package/hooks/private/use-client-query.d.ts +6 -0
- package/hooks/private/use-client-query.d.ts.map +1 -1
- package/hooks/private/use-client-query.js +26 -3
- package/hooks/private/use-client-query.js.map +1 -1
- package/hooks/private/use-multimodal-input.d.ts.map +1 -1
- package/hooks/private/use-multimodal-input.js +7 -5
- package/hooks/private/use-multimodal-input.js.map +1 -1
- package/hooks/private/use-visitor-typing-reporter.d.ts +18 -1
- package/hooks/private/use-visitor-typing-reporter.d.ts.map +1 -1
- package/hooks/private/use-visitor-typing-reporter.js +34 -4
- package/hooks/private/use-visitor-typing-reporter.js.map +1 -1
- package/hooks/use-conversation-page.d.ts +1 -0
- package/hooks/use-conversation-page.d.ts.map +1 -1
- package/hooks/use-conversation-page.js +6 -1
- package/hooks/use-conversation-page.js.map +1 -1
- package/hooks/use-conversation-preview.d.ts +2 -1
- package/hooks/use-conversation-preview.d.ts.map +1 -1
- package/hooks/use-conversation-preview.js +1 -1
- package/hooks/use-conversation-preview.js.map +1 -1
- package/hooks/use-conversation-timeline-items.js +2 -1
- package/hooks/use-conversation-timeline-items.js.map +1 -1
- package/hooks/use-conversation.js +2 -1
- package/hooks/use-conversation.js.map +1 -1
- package/hooks/use-conversations.js +1 -0
- package/hooks/use-conversations.js.map +1 -1
- package/hooks/use-create-conversation.d.ts.map +1 -1
- package/hooks/use-file-upload.d.ts +55 -0
- package/hooks/use-file-upload.d.ts.map +1 -0
- package/hooks/use-file-upload.js +100 -0
- package/hooks/use-file-upload.js.map +1 -0
- package/hooks/use-message-composer.d.ts +11 -0
- package/hooks/use-message-composer.d.ts.map +1 -1
- package/hooks/use-message-composer.js +7 -3
- package/hooks/use-message-composer.js.map +1 -1
- package/hooks/use-send-message.d.ts +1 -0
- package/hooks/use-send-message.d.ts.map +1 -1
- package/hooks/use-send-message.js +63 -11
- package/hooks/use-send-message.js.map +1 -1
- package/index.d.ts +6 -3
- package/index.js +13 -10
- package/openapi30.d.ts +1 -1
- package/openapi30.d.ts.map +1 -1
- package/openapi31.d.ts +1 -1
- package/openapi31.d.ts.map +1 -1
- package/package.json +4 -3
- package/parse.d.ts +1 -1
- package/parse.d.ts.map +1 -1
- package/primitives/avatar/image.d.ts +1 -1
- package/primitives/conversation-timeline.d.ts.map +1 -1
- package/primitives/conversation-timeline.js +10 -5
- package/primitives/conversation-timeline.js.map +1 -1
- package/primitives/index.d.ts +4 -3
- package/primitives/index.js +12 -5
- package/primitives/index.parts.d.ts +3 -2
- package/primitives/index.parts.js +4 -3
- package/primitives/multimodal-input.d.ts +2 -2
- package/primitives/multimodal-input.d.ts.map +1 -1
- package/primitives/timeline-item-attachments.d.ts +100 -0
- package/primitives/timeline-item-attachments.d.ts.map +1 -0
- package/primitives/timeline-item-attachments.js +151 -0
- package/primitives/timeline-item-attachments.js.map +1 -0
- package/primitives/trigger.d.ts +91 -0
- package/primitives/trigger.d.ts.map +1 -0
- package/primitives/trigger.js +74 -0
- package/primitives/trigger.js.map +1 -0
- package/primitives/window.d.ts +22 -1
- package/primitives/window.d.ts.map +1 -1
- package/primitives/window.js +91 -5
- package/primitives/window.js.map +1 -1
- package/provider.d.ts.map +1 -1
- package/provider.js +8 -3
- package/provider.js.map +1 -1
- package/realtime/index.js +1 -1
- package/realtime/provider.js +1 -1
- package/realtime/support-provider.js +1 -1
- package/realtime/support-provider.js.map +1 -1
- package/realtime-events.d.ts +40 -1
- package/realtime-events.d.ts.map +1 -1
- package/registries.d.ts +1 -1
- package/registries.d.ts.map +1 -1
- package/schemas.d.ts +1 -1
- package/schemas.d.ts.map +1 -1
- package/schemas2.d.ts +1 -1
- package/schemas2.d.ts.map +1 -1
- package/schemas3.d.ts +1 -0
- package/schemas3.d.ts.map +1 -1
- package/specification-extension.d.ts +1 -1
- package/specification-extension.d.ts.map +1 -1
- package/standard-schema.d.ts +1 -1
- package/standard-schema.d.ts.map +1 -1
- package/support/components/content.d.ts +30 -0
- package/support/components/content.d.ts.map +1 -0
- package/support/components/content.js +282 -0
- package/support/components/content.js.map +1 -0
- package/support/components/conversation-button-link.js +1 -1
- package/support/components/conversation-timeline.js +3 -3
- package/support/components/conversation-timeline.js.map +1 -1
- package/support/components/header.js +1 -1
- package/support/components/image-lightbox.d.ts +49 -0
- package/support/components/image-lightbox.d.ts.map +1 -0
- package/support/components/image-lightbox.js +142 -0
- package/support/components/image-lightbox.js.map +1 -0
- package/support/components/index.d.ts +5 -4
- package/support/components/index.js +4 -4
- package/support/components/multimodal-input.d.ts +4 -1
- package/support/components/multimodal-input.d.ts.map +1 -1
- package/support/components/multimodal-input.js +71 -45
- package/support/components/multimodal-input.js.map +1 -1
- package/support/components/navigation-tab.js +1 -1
- package/support/components/root.d.ts +23 -0
- package/support/components/root.d.ts.map +1 -0
- package/support/components/root.js +36 -0
- package/support/components/root.js.map +1 -0
- package/support/components/timeline-message-item.d.ts.map +1 -1
- package/support/components/timeline-message-item.js +82 -18
- package/support/components/timeline-message-item.js.map +1 -1
- package/support/components/trigger.d.ts +14 -0
- package/support/components/trigger.d.ts.map +1 -0
- package/support/components/{bubble.js → trigger.js} +16 -12
- package/support/components/trigger.js.map +1 -0
- package/support/components/typing-indicator.d.ts.map +1 -1
- package/support/components/typing-indicator.js +1 -0
- package/support/components/typing-indicator.js.map +1 -1
- package/support/context/controlled-state.d.ts +46 -0
- package/support/context/controlled-state.d.ts.map +1 -0
- package/support/context/controlled-state.js +34 -0
- package/support/context/controlled-state.js.map +1 -0
- package/support/context/events.d.ts +103 -0
- package/support/context/events.d.ts.map +1 -0
- package/support/context/events.js +139 -0
- package/support/context/events.js.map +1 -0
- package/support/context/handle.d.ts +90 -0
- package/support/context/handle.d.ts.map +1 -0
- package/support/context/handle.js +79 -0
- package/support/context/handle.js.map +1 -0
- package/support/context/positioning.d.ts +17 -0
- package/support/context/positioning.d.ts.map +1 -0
- package/support/context/positioning.js +26 -0
- package/support/context/positioning.js.map +1 -0
- package/support/context/slots.d.ts +85 -0
- package/support/context/slots.d.ts.map +1 -0
- package/support/context/slots.js +115 -0
- package/support/context/slots.js.map +1 -0
- package/support/context/websocket.d.ts +8 -1
- package/support/context/websocket.d.ts.map +1 -1
- package/support/context/websocket.js +8 -1
- package/support/context/websocket.js.map +1 -1
- package/support/index.d.ts +239 -54
- package/support/index.d.ts.map +1 -1
- package/support/index.js +254 -33
- package/support/index.js.map +1 -1
- package/support/pages/articles.d.ts.map +1 -1
- package/support/pages/articles.js +3 -4
- package/support/pages/articles.js.map +1 -1
- package/support/pages/conversation-history.js +2 -2
- package/support/pages/conversation.js +6 -5
- package/support/pages/conversation.js.map +1 -1
- package/support/pages/home.js +2 -2
- package/support/router.d.ts +52 -12
- package/support/router.d.ts.map +1 -1
- package/support/router.js +78 -30
- package/support/router.js.map +1 -1
- package/support/store/index.d.ts +2 -2
- package/support/store/support-store.d.ts +26 -20
- package/support/store/support-store.d.ts.map +1 -1
- package/support/store/support-store.js +47 -6
- package/support/store/support-store.js.map +1 -1
- package/support/{support-D2EgfIts.css → support-C7Xaw-N6.css} +1 -2
- package/support/support-C7Xaw-N6.css.map +1 -0
- package/support/text/index.js.map +1 -1
- package/support/types.d.ts +75 -12
- package/support/types.d.ts.map +1 -1
- package/support.css +2 -2
- package/tailwind.css +0 -1
- package/timeline-item.d.ts +68 -2
- package/timeline-item.d.ts.map +1 -1
- package/util.d.ts +1 -1
- package/util.d.ts.map +1 -1
- package/utils/index.d.ts +2 -1
- package/utils/index.js +2 -1
- package/utils/merge-refs.d.ts +30 -0
- package/utils/merge-refs.d.ts.map +1 -0
- package/utils/merge-refs.js +46 -0
- package/utils/merge-refs.js.map +1 -0
- package/utils/use-render-element.d.ts.map +1 -1
- package/utils/use-render-element.js +20 -7
- package/utils/use-render-element.js.map +1 -1
- package/versions.d.ts +1 -1
- package/versions.d.ts.map +1 -1
- package/zod-extensions.d.ts +1 -1
- package/zod-extensions.d.ts.map +1 -1
- package/primitives/bubble.d.ts +0 -38
- package/primitives/bubble.d.ts.map +0 -1
- package/primitives/bubble.js +0 -57
- package/primitives/bubble.js.map +0 -1
- package/support/components/bubble.d.ts +0 -10
- package/support/components/bubble.d.ts.map +0 -1
- package/support/components/bubble.js.map +0 -1
- package/support/components/container.d.ts +0 -13
- package/support/components/container.d.ts.map +0 -1
- package/support/components/container.js +0 -109
- package/support/components/container.js.map +0 -1
- package/support/components/support-content.d.ts +0 -22
- package/support/components/support-content.d.ts.map +0 -1
- package/support/components/support-content.js +0 -48
- package/support/components/support-content.js.map +0 -1
- package/support/support-D2EgfIts.css.map +0 -1
package/primitives/window.js
CHANGED
|
@@ -4,7 +4,35 @@ import * as React$1 from "react";
|
|
|
4
4
|
|
|
5
5
|
//#region src/primitives/window.tsx
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
7
|
+
* Selector for focusable elements within a container
|
|
8
|
+
*/
|
|
9
|
+
const FOCUSABLE_SELECTOR = [
|
|
10
|
+
"a[href]",
|
|
11
|
+
"area[href]",
|
|
12
|
+
"input:not([disabled])",
|
|
13
|
+
"select:not([disabled])",
|
|
14
|
+
"textarea:not([disabled])",
|
|
15
|
+
"button:not([disabled])",
|
|
16
|
+
"iframe",
|
|
17
|
+
"object",
|
|
18
|
+
"embed",
|
|
19
|
+
"[tabindex]:not([tabindex='-1'])",
|
|
20
|
+
"[contenteditable]",
|
|
21
|
+
"audio[controls]",
|
|
22
|
+
"video[controls]"
|
|
23
|
+
].join(",");
|
|
24
|
+
/**
|
|
25
|
+
* Get all focusable elements within a container
|
|
26
|
+
*/
|
|
27
|
+
function getFocusableElements(container) {
|
|
28
|
+
return Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR)).filter((el) => {
|
|
29
|
+
const style = window.getComputedStyle(el);
|
|
30
|
+
return style.display !== "none" && style.visibility !== "hidden";
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Dialog container with open/close state, escape key handling,
|
|
35
|
+
* focus trap, and focus restoration.
|
|
8
36
|
*
|
|
9
37
|
* @example
|
|
10
38
|
* <Window isOpen={isOpen} onOpenChange={setOpen}>
|
|
@@ -14,25 +42,82 @@ import * as React$1 from "react";
|
|
|
14
42
|
* </Window>
|
|
15
43
|
*/
|
|
16
44
|
const SupportWindow = (() => {
|
|
17
|
-
const Component = React$1.forwardRef(({ isOpen: isOpenProp, onOpenChange, children, className, asChild = false, closeOnEscape = true, id = "cossistant-window",...props }, ref) => {
|
|
45
|
+
const Component = React$1.forwardRef(({ isOpen: isOpenProp, onOpenChange, children, className, asChild = false, closeOnEscape = true, trapFocus = true, restoreFocus = true, id = "cossistant-window",...props }, ref) => {
|
|
18
46
|
const { isOpen, close } = useSupportConfig();
|
|
47
|
+
const containerRef = React$1.useRef(null);
|
|
48
|
+
const previouslyFocusedRef = React$1.useRef(null);
|
|
19
49
|
const open = isOpenProp ?? isOpen ?? false;
|
|
20
50
|
const closeFn = React$1.useCallback(() => {
|
|
21
51
|
if (onOpenChange) onOpenChange(false);
|
|
22
52
|
else if (close) close();
|
|
23
53
|
}, [onOpenChange, close]);
|
|
54
|
+
React$1.useEffect(() => {
|
|
55
|
+
if (open) {
|
|
56
|
+
previouslyFocusedRef.current = document.activeElement;
|
|
57
|
+
const timer = setTimeout(() => {
|
|
58
|
+
const container = containerRef.current;
|
|
59
|
+
if (container) {
|
|
60
|
+
const firstElement = getFocusableElements(container)[0];
|
|
61
|
+
if (firstElement) firstElement.focus();
|
|
62
|
+
else container.focus();
|
|
63
|
+
}
|
|
64
|
+
}, 50);
|
|
65
|
+
return () => clearTimeout(timer);
|
|
66
|
+
}
|
|
67
|
+
if (!open && restoreFocus && previouslyFocusedRef.current) {
|
|
68
|
+
previouslyFocusedRef.current.focus();
|
|
69
|
+
previouslyFocusedRef.current = null;
|
|
70
|
+
}
|
|
71
|
+
}, [open, restoreFocus]);
|
|
24
72
|
React$1.useEffect(() => {
|
|
25
73
|
if (!(open && closeOnEscape)) return;
|
|
26
74
|
const onKey = (e) => {
|
|
27
|
-
if (e.key === "Escape")
|
|
75
|
+
if (e.key === "Escape") closeFn();
|
|
28
76
|
};
|
|
29
77
|
window.addEventListener("keydown", onKey);
|
|
30
78
|
return () => window.removeEventListener("keydown", onKey);
|
|
31
79
|
}, [
|
|
32
80
|
open,
|
|
33
|
-
|
|
81
|
+
closeFn,
|
|
34
82
|
closeOnEscape
|
|
35
83
|
]);
|
|
84
|
+
React$1.useEffect(() => {
|
|
85
|
+
if (!(open && trapFocus)) return;
|
|
86
|
+
const container = containerRef.current;
|
|
87
|
+
if (!container) return;
|
|
88
|
+
const handleKeyDown = (e) => {
|
|
89
|
+
if (e.key !== "Tab") return;
|
|
90
|
+
const focusable = getFocusableElements(container);
|
|
91
|
+
if (focusable.length === 0) {
|
|
92
|
+
e.preventDefault();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const first = focusable[0];
|
|
96
|
+
const last = focusable.at(-1);
|
|
97
|
+
const active = document.activeElement;
|
|
98
|
+
if (e.shiftKey && active === first) {
|
|
99
|
+
e.preventDefault();
|
|
100
|
+
last?.focus();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (!e.shiftKey && active === last) {
|
|
104
|
+
e.preventDefault();
|
|
105
|
+
first?.focus();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (!container.contains(active)) {
|
|
109
|
+
e.preventDefault();
|
|
110
|
+
first?.focus();
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
114
|
+
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
115
|
+
}, [open, trapFocus]);
|
|
116
|
+
const mergedRef = React$1.useCallback((node) => {
|
|
117
|
+
containerRef.current = node;
|
|
118
|
+
if (typeof ref === "function") ref(node);
|
|
119
|
+
else if (ref) ref.current = node;
|
|
120
|
+
}, [ref]);
|
|
36
121
|
const renderProps = {
|
|
37
122
|
isOpen: open,
|
|
38
123
|
close: closeFn
|
|
@@ -42,12 +127,13 @@ const SupportWindow = (() => {
|
|
|
42
127
|
className,
|
|
43
128
|
asChild
|
|
44
129
|
}, {
|
|
45
|
-
ref,
|
|
130
|
+
ref: mergedRef,
|
|
46
131
|
state: renderProps,
|
|
47
132
|
props: {
|
|
48
133
|
role: "dialog",
|
|
49
134
|
"aria-modal": "true",
|
|
50
135
|
id,
|
|
136
|
+
tabIndex: -1,
|
|
51
137
|
...props,
|
|
52
138
|
children: content
|
|
53
139
|
},
|
package/primitives/window.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"window.js","names":["React","renderProps: WindowRenderProps"],"sources":["../../src/primitives/window.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { useSupportConfig } from \"../support/store/support-store\";\nimport { useRenderElement } from \"../utils/use-render-element\";\n\nexport type WindowRenderProps = {\n\tisOpen: boolean;\n\tclose: () => void;\n};\n\nexport type WindowProps = Omit<\n\tReact.HTMLAttributes<HTMLDivElement>,\n\t\"children\"\n> & {\n\tisOpen?: boolean;\n\tonOpenChange?: (open: boolean) => void;\n\tchildren?: React.ReactNode | ((props: WindowRenderProps) => React.ReactNode);\n\tasChild?: boolean;\n\tcloseOnEscape?: boolean;\n\tid?: string;\n};\n\n/**\n * Dialog container with open/close state
|
|
1
|
+
{"version":3,"file":"window.js","names":["React","renderProps: WindowRenderProps"],"sources":["../../src/primitives/window.tsx"],"sourcesContent":["import * as React from \"react\";\nimport { useSupportConfig } from \"../support/store/support-store\";\nimport { useRenderElement } from \"../utils/use-render-element\";\n\nexport type WindowRenderProps = {\n\tisOpen: boolean;\n\tclose: () => void;\n};\n\nexport type WindowProps = Omit<\n\tReact.HTMLAttributes<HTMLDivElement>,\n\t\"children\"\n> & {\n\tisOpen?: boolean;\n\tonOpenChange?: (open: boolean) => void;\n\tchildren?: React.ReactNode | ((props: WindowRenderProps) => React.ReactNode);\n\tasChild?: boolean;\n\tcloseOnEscape?: boolean;\n\t/**\n\t * Whether to trap focus within the dialog when open.\n\t * @default true\n\t */\n\ttrapFocus?: boolean;\n\t/**\n\t * Whether to restore focus to the previously focused element when closing.\n\t * @default true\n\t */\n\trestoreFocus?: boolean;\n\tid?: string;\n};\n\n/**\n * Selector for focusable elements within a container\n */\nconst FOCUSABLE_SELECTOR = [\n\t\"a[href]\",\n\t\"area[href]\",\n\t\"input:not([disabled])\",\n\t\"select:not([disabled])\",\n\t\"textarea:not([disabled])\",\n\t\"button:not([disabled])\",\n\t\"iframe\",\n\t\"object\",\n\t\"embed\",\n\t\"[tabindex]:not([tabindex='-1'])\",\n\t\"[contenteditable]\",\n\t\"audio[controls]\",\n\t\"video[controls]\",\n].join(\",\");\n\n/**\n * Get all focusable elements within a container\n */\nfunction getFocusableElements(container: HTMLElement): HTMLElement[] {\n\treturn Array.from(\n\t\tcontainer.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR)\n\t).filter((el) => {\n\t\t// Check visibility\n\t\tconst style = window.getComputedStyle(el);\n\t\treturn style.display !== \"none\" && style.visibility !== \"hidden\";\n\t});\n}\n\n/**\n * Dialog container with open/close state, escape key handling,\n * focus trap, and focus restoration.\n *\n * @example\n * <Window isOpen={isOpen} onOpenChange={setOpen}>\n * {({ isOpen, close }) => (\n * <div>Content here</div>\n * )}\n * </Window>\n */\nexport const SupportWindow = (() => {\n\tconst Component = React.forwardRef<HTMLDivElement, WindowProps>(\n\t\t(\n\t\t\t{\n\t\t\t\tisOpen: isOpenProp,\n\t\t\t\tonOpenChange,\n\t\t\t\tchildren,\n\t\t\t\tclassName,\n\t\t\t\tasChild = false,\n\t\t\t\tcloseOnEscape = true,\n\t\t\t\ttrapFocus = true,\n\t\t\t\trestoreFocus = true,\n\t\t\t\tid = \"cossistant-window\",\n\t\t\t\t...props\n\t\t\t},\n\t\t\tref\n\t\t) => {\n\t\t\tconst { isOpen, close } = useSupportConfig();\n\t\t\tconst containerRef = React.useRef<HTMLDivElement>(null);\n\t\t\tconst previouslyFocusedRef = React.useRef<HTMLElement | null>(null);\n\n\t\t\tconst open = isOpenProp ?? isOpen ?? false;\n\n\t\t\tconst closeFn = React.useCallback(() => {\n\t\t\t\tif (onOpenChange) {\n\t\t\t\t\tonOpenChange(false);\n\t\t\t\t} else if (close) {\n\t\t\t\t\tclose();\n\t\t\t\t}\n\t\t\t}, [onOpenChange, close]);\n\n\t\t\t// Store previously focused element and focus first element when opening\n\t\t\tReact.useEffect(() => {\n\t\t\t\tif (open) {\n\t\t\t\t\t// Store the currently focused element\n\t\t\t\t\tpreviouslyFocusedRef.current = document.activeElement as HTMLElement;\n\n\t\t\t\t\t// Focus the first focusable element after a short delay\n\t\t\t\t\t// to allow the DOM to render\n\t\t\t\t\tconst timer = setTimeout(() => {\n\t\t\t\t\t\tconst container = containerRef.current;\n\t\t\t\t\t\tif (container) {\n\t\t\t\t\t\t\tconst focusable = getFocusableElements(container);\n\t\t\t\t\t\t\tconst firstElement = focusable[0];\n\t\t\t\t\t\t\tif (firstElement) {\n\t\t\t\t\t\t\t\tfirstElement.focus();\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t// If no focusable elements, focus the container itself\n\t\t\t\t\t\t\t\tcontainer.focus();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}, 50);\n\n\t\t\t\t\treturn () => clearTimeout(timer);\n\t\t\t\t}\n\t\t\t\t// Restore focus when closing\n\t\t\t\tif (!open && restoreFocus && previouslyFocusedRef.current) {\n\t\t\t\t\tpreviouslyFocusedRef.current.focus();\n\t\t\t\t\tpreviouslyFocusedRef.current = null;\n\t\t\t\t}\n\t\t\t}, [open, restoreFocus]);\n\n\t\t\t// Close on Escape\n\t\t\tReact.useEffect(() => {\n\t\t\t\tif (!(open && closeOnEscape)) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst onKey = (e: KeyboardEvent) => {\n\t\t\t\t\tif (e.key === \"Escape\") {\n\t\t\t\t\t\tcloseFn();\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t\twindow.addEventListener(\"keydown\", onKey);\n\t\t\t\treturn () => window.removeEventListener(\"keydown\", onKey);\n\t\t\t}, [open, closeFn, closeOnEscape]);\n\n\t\t\t// Focus trap - trap Tab and Shift+Tab within the dialog\n\t\t\tReact.useEffect(() => {\n\t\t\t\tif (!(open && trapFocus)) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tconst container = containerRef.current;\n\t\t\t\tif (!container) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tconst handleKeyDown = (e: KeyboardEvent) => {\n\t\t\t\t\tif (e.key !== \"Tab\") {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst focusable = getFocusableElements(container);\n\t\t\t\t\tif (focusable.length === 0) {\n\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst first = focusable[0];\n\t\t\t\t\tconst last = focusable.at(-1);\n\t\t\t\t\tconst active = document.activeElement;\n\n\t\t\t\t\t// Shift+Tab from first element wraps to last\n\t\t\t\t\tif (e.shiftKey && active === first) {\n\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\tlast?.focus();\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Tab from last element wraps to first\n\t\t\t\t\tif (!e.shiftKey && active === last) {\n\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\tfirst?.focus();\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\t// If focus is outside the container, bring it back\n\t\t\t\t\tif (!container.contains(active)) {\n\t\t\t\t\t\te.preventDefault();\n\t\t\t\t\t\tfirst?.focus();\n\t\t\t\t\t}\n\t\t\t\t};\n\n\t\t\t\tdocument.addEventListener(\"keydown\", handleKeyDown);\n\t\t\t\treturn () => document.removeEventListener(\"keydown\", handleKeyDown);\n\t\t\t}, [open, trapFocus]);\n\n\t\t\t// Merge refs\n\t\t\tconst mergedRef = React.useCallback(\n\t\t\t\t(node: HTMLDivElement | null) => {\n\t\t\t\t\tcontainerRef.current = node;\n\t\t\t\t\tif (typeof ref === \"function\") {\n\t\t\t\t\t\tref(node);\n\t\t\t\t\t} else if (ref) {\n\t\t\t\t\t\tref.current = node;\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t[ref]\n\t\t\t);\n\n\t\t\tconst renderProps: WindowRenderProps = { isOpen: open, close: closeFn };\n\n\t\t\tconst content =\n\t\t\t\ttypeof children === \"function\" ? children(renderProps) : children;\n\n\t\t\treturn useRenderElement(\n\t\t\t\t\"div\",\n\t\t\t\t{\n\t\t\t\t\tclassName,\n\t\t\t\t\tasChild,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tref: mergedRef,\n\t\t\t\t\tstate: renderProps,\n\t\t\t\t\tprops: {\n\t\t\t\t\t\trole: \"dialog\",\n\t\t\t\t\t\t\"aria-modal\": \"true\",\n\t\t\t\t\t\tid,\n\t\t\t\t\t\ttabIndex: -1, // Allow container to receive focus\n\t\t\t\t\t\t...props,\n\t\t\t\t\t\tchildren: content,\n\t\t\t\t\t},\n\t\t\t\t\tenabled: open,\n\t\t\t\t}\n\t\t\t);\n\t\t}\n\t);\n\n\tComponent.displayName = \"SupportWindow\";\n\treturn Component;\n})();\n"],"mappings":"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"}
|
package/provider.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.d.ts","names":[],"sources":["../src/provider.tsx"],"sourcesContent":[],"mappings":";;;;;KAgBY,oBAAA;YACD,KAAA,CAAM;EADL,WAAA,CAAA,EAAA,OAAA;EACD,MAAM,CAAA,EAAA,MAAA;EAKE,KAAA,CAAA,EAAA,MAAA;EAKE,SAAA,CAAA,EAAA,MAAA;EAAK,eAAA,CAAA,EALP,cAKO,EAAA;EAId,YAAA,CAAA,EAAA,MAAA,EAAA;EAEA,WAAA,CAAA,EAAA,OAAA;EACF,WAAA,CAAA,EAAA,GAAA,GAAA,IAAA;EACQ,cAAA,CAAA,EAAA,GAAA,GAAA,IAAA;EAEc,SAAA,CAAA,EAAA,CAAA,KAAA,EAVX,KAUW,EAAA,GAAA,IAAA;EAKxB,IAAA,CAAA,EAAA,QAAA,GAAA,QAAA;CACC;AAAgB,KAZb,uBAAA,GAA0B,oBAYb;AAOpB,KAjBO,sBAAA,GAiBmB;EAE1B,OAAA,EAlBK,qBAkBY,GAAA,IAAA;EAAG,eAAA,EAjBP,cAiBO,EAAA;EAEV,YAAA,EAAA,MAAA,EAAA;EAAZ,kBAAA,EAAA,CAAA,QAAA,EAjB6B,cAiB7B,EAAA,EAAA,GAAA,IAAA;EAAW,eAAA,EAAA,CAAA,OAAA,EAAA,MAAA,EAAA,EAAA,GAAA,IAAA;
|
|
1
|
+
{"version":3,"file":"provider.d.ts","names":[],"sources":["../src/provider.tsx"],"sourcesContent":[],"mappings":";;;;;KAgBY,oBAAA;YACD,KAAA,CAAM;EADL,WAAA,CAAA,EAAA,OAAA;EACD,MAAM,CAAA,EAAA,MAAA;EAKE,KAAA,CAAA,EAAA,MAAA;EAKE,SAAA,CAAA,EAAA,MAAA;EAAK,eAAA,CAAA,EALP,cAKO,EAAA;EAId,YAAA,CAAA,EAAA,MAAA,EAAA;EAEA,WAAA,CAAA,EAAA,OAAA;EACF,WAAA,CAAA,EAAA,GAAA,GAAA,IAAA;EACQ,cAAA,CAAA,EAAA,GAAA,GAAA,IAAA;EAEc,SAAA,CAAA,EAAA,CAAA,KAAA,EAVX,KAUW,EAAA,GAAA,IAAA;EAKxB,IAAA,CAAA,EAAA,QAAA,GAAA,QAAA;CACC;AAAgB,KAZb,uBAAA,GAA0B,oBAYb;AAOpB,KAjBO,sBAAA,GAiBmB;EAE1B,OAAA,EAlBK,qBAkBY,GAAA,IAAA;EAAG,eAAA,EAjBP,cAiBO,EAAA;EAEV,YAAA,EAAA,MAAA,EAAA;EAAZ,kBAAA,EAAA,CAAA,QAAA,EAjB6B,cAiB7B,EAAA,EAAA,GAAA,IAAA;EAAW,eAAA,EAAA,CAAA,OAAA,EAAA,MAAA,EAAA,EAAA,GAAA,IAAA;EA6CF,WAAA,EAAA,MAAe;EAAG,cAAA,EAAA,CAAA,KAAA,EAAA,MAAA,EAAA,GAAA,IAAA;EACK,SAAA,EAAA,OAAA;EAAZ,KAAA,EA1Df,KA0De,GAAA,IAAA;EACS,MAAA,EA1DvB,gBA0DuB;EAAZ,MAAA,EAAA,OAAA;EACT,IAAA,EAAA,GAAA,GAAA,IAAA;EAAiB,KAAA,EAAA,GAAA,GAAA,IAAA;EAIf,MAAA,EAAA,GAAA,GAAA,IAED;AA4RZ,CAAA;KAtVK,WAAA,GAAc,WAuVlB,CAvV8B,sBAuV9B,CAAA,SAAA,CAAA,CAAA;KArVI,iBAAA,GAAoB,WAsVxB,CAAA,SAAA,CAAA,SAAA,IAAA,GAAA,SAAA,GAAA,SAAA,GApVE,WAoVF,CApVc,WAoVd,CAAA,SAAA,CAAA,CAAA,GAAA;EACA,MAAA,EAAA,MAAA,GAAA,IAAA;CACA;AACA,KA1SW,eAAA,GAAkB,sBA0S7B,GAAA;EACA,oBAAA,EA1SsB,WA0StB,CA1SkC,WA0SlC,CAAA,sBAAA,CAAA,CAAA,GAAA,EAAA;EACA,iBAAA,EA1SmB,WA0SnB,CA1S+B,WA0S/B,CAAA,mBAAA,CAAA,CAAA,GAAA,EAAA;EACA,OAAA,CAAA,EA1SU,iBA0SV;EACA,IAAA,EAAA,QAAA,GAAA,QAAA;CACA;AACA,cAzSY,cAySZ,EAzS0B,KAAA,CAAA,OAyS1B,CAzS0B,sBAyS1B,GAAA,SAAA,CAAA;;;;;AA0BD;;iBArCgB,eAAA;;;;;;;;;;;;;GAab,uBAAuB,KAAA,CAAM;;;;;iBAwBhB,UAAA,CAAA,GAAc"}
|
package/provider.js
CHANGED
|
@@ -20,7 +20,7 @@ function areConversationSnapshotsEqual(a, b) {
|
|
|
20
20
|
if (!snapshotB) return false;
|
|
21
21
|
const aLastCreatedAt = snapshotA.lastTimelineItem?.createdAt ?? null;
|
|
22
22
|
const bLastCreatedAt = snapshotB.lastTimelineItem?.createdAt ?? null;
|
|
23
|
-
if (snapshotA.id !== snapshotB.id || aLastCreatedAt !== bLastCreatedAt) return false;
|
|
23
|
+
if (snapshotA.id !== snapshotB.id || aLastCreatedAt !== bLastCreatedAt || snapshotA.visitorLastSeenAt !== snapshotB.visitorLastSeenAt) return false;
|
|
24
24
|
}
|
|
25
25
|
return true;
|
|
26
26
|
}
|
|
@@ -57,18 +57,23 @@ function SupportProviderInner({ children, apiUrl, wsUrl, publicKey, defaultMessa
|
|
|
57
57
|
if (!conversation) return null;
|
|
58
58
|
return {
|
|
59
59
|
id: conversation.id,
|
|
60
|
-
lastTimelineItem: conversation.lastTimelineItem ?? null
|
|
60
|
+
lastTimelineItem: conversation.lastTimelineItem ?? null,
|
|
61
|
+
visitorLastSeenAt: conversation.visitorLastSeenAt ?? null
|
|
61
62
|
};
|
|
62
63
|
}).filter((snapshot) => snapshot !== null), []), areConversationSnapshotsEqual);
|
|
63
64
|
const derivedUnreadCount = React.useMemo(() => {
|
|
64
65
|
if (!visitorId) return 0;
|
|
65
66
|
let count = 0;
|
|
66
|
-
for (const { id: conversationId, lastTimelineItem } of conversationSnapshots) {
|
|
67
|
+
for (const { id: conversationId, lastTimelineItem, visitorLastSeenAt } of conversationSnapshots) {
|
|
67
68
|
if (!lastTimelineItem) continue;
|
|
68
69
|
if (lastTimelineItem.type !== ConversationTimelineType.MESSAGE) continue;
|
|
69
70
|
if (lastTimelineItem.visitorId && lastTimelineItem.visitorId === visitorId) continue;
|
|
70
71
|
const createdAtTime = Date.parse(lastTimelineItem.createdAt);
|
|
71
72
|
if (Number.isNaN(createdAtTime)) continue;
|
|
73
|
+
if (visitorLastSeenAt) {
|
|
74
|
+
const lastSeenTime = Date.parse(visitorLastSeenAt);
|
|
75
|
+
if (!Number.isNaN(lastSeenTime) && createdAtTime <= lastSeenTime) continue;
|
|
76
|
+
}
|
|
72
77
|
const seenEntries = seenEntriesByConversation[conversationId];
|
|
73
78
|
if (seenEntries) {
|
|
74
79
|
const visitorSeenEntry = Object.values(seenEntries).find((entry) => entry.actorType === "visitor" && entry.actorId === visitorId);
|
package/provider.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.js","names":[],"sources":["../src/provider.tsx"],"sourcesContent":["import type { CossistantClient } from \"@cossistant/core\";\nimport { normalizeLocale } from \"@cossistant/core\";\nimport type { DefaultMessage, PublicWebsiteResponse } from \"@cossistant/types\";\nimport type { TimelineItem } from \"@cossistant/types/api/timeline-item\";\nimport { ConversationTimelineType } from \"@cossistant/types/enums\";\nimport React from \"react\";\nimport { useStoreSelector } from \"./hooks/private/store/use-store-selector\";\nimport { useWebsiteStore } from \"./hooks/private/store/use-website-store\";\nimport { useClient } from \"./hooks/private/use-rest-client\";\nimport { useSeenStore } from \"./realtime/seen-store\";\nimport { WebSocketProvider } from \"./support\";\nimport {\n\tinitializeSupportStore,\n\tuseSupportStore,\n} from \"./support/store/support-store\";\n\nexport type SupportProviderProps = {\n\tchildren: React.ReactNode;\n\tdefaultOpen?: boolean;\n\tapiUrl?: string;\n\twsUrl?: string;\n\tpublicKey?: string;\n\tdefaultMessages?: DefaultMessage[];\n\tquickOptions?: string[];\n\tautoConnect?: boolean;\n\tonWsConnect?: () => void;\n\tonWsDisconnect?: () => void;\n\tonWsError?: (error: Error) => void;\n\tsize?: \"normal\" | \"larger\";\n};\n\nexport type CossistantProviderProps = SupportProviderProps;\n\nexport type CossistantContextValue = {\n\twebsite: PublicWebsiteResponse | null;\n\tdefaultMessages: DefaultMessage[];\n\tquickOptions: string[];\n\tsetDefaultMessages: (messages: DefaultMessage[]) => void;\n\tsetQuickOptions: (options: string[]) => void;\n\tunreadCount: number;\n\tsetUnreadCount: (count: number) => void;\n\tisLoading: boolean;\n\terror: Error | null;\n\tclient: CossistantClient;\n\tisOpen: boolean;\n\topen: () => void;\n\tclose: () => void;\n\ttoggle: () => void;\n};\n\ntype WebsiteData = NonNullable<CossistantContextValue[\"website\"]>;\n\ntype VisitorWithLocale = WebsiteData[\"visitor\"] extends null | undefined\n\t? undefined\n\t: NonNullable<WebsiteData[\"visitor\"]> & { locale: string | null };\n\ntype ConversationSnapshot = {\n\tid: string;\n\tlastTimelineItem: TimelineItem | null;\n};\n\nfunction areConversationSnapshotsEqual(\n\ta: ConversationSnapshot[],\n\tb: ConversationSnapshot[]\n): boolean {\n\tif (a === b) {\n\t\treturn true;\n\t}\n\n\tif (a.length !== b.length) {\n\t\treturn false;\n\t}\n\n\tfor (let index = 0; index < a.length; index += 1) {\n\t\tconst snapshotA = a[index];\n\t\tconst snapshotB = b[index];\n\n\t\tif (!snapshotA) {\n\t\t\treturn false;\n\t\t}\n\t\tif (!snapshotB) {\n\t\t\treturn false;\n\t\t}\n\n\t\tconst aLastCreatedAt = snapshotA.lastTimelineItem?.createdAt ?? null;\n\t\tconst bLastCreatedAt = snapshotB.lastTimelineItem?.createdAt ?? null;\n\t\tif (snapshotA.id !== snapshotB.id || aLastCreatedAt !== bLastCreatedAt) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\treturn true;\n}\n\nexport type UseSupportValue = CossistantContextValue & {\n\tavailableHumanAgents: NonNullable<WebsiteData[\"availableHumanAgents\"]> | [];\n\tavailableAIAgents: NonNullable<WebsiteData[\"availableAIAgents\"]> | [];\n\tvisitor?: VisitorWithLocale;\n\tsize: \"normal\" | \"larger\";\n};\n\nexport const SupportContext = React.createContext<\n\tCossistantContextValue | undefined\n>(undefined);\n\n/**\n * Internal implementation that wires the REST client and websocket provider\n * together before exposing the combined context.\n */\nfunction SupportProviderInner({\n\tchildren,\n\tapiUrl,\n\twsUrl,\n\tpublicKey,\n\tdefaultMessages,\n\tquickOptions,\n\tautoConnect,\n\tonWsConnect,\n\tonWsDisconnect,\n\tonWsError,\n\tsize = \"normal\",\n\tdefaultOpen = false,\n}: SupportProviderProps) {\n\tconst [unreadCount, setUnreadCount] = React.useState(0);\n\tconst prefetchedVisitorRef = React.useRef<string | null>(null);\n\tconst [_defaultMessages, _setDefaultMessages] = React.useState<\n\t\tDefaultMessage[]\n\t>(defaultMessages ?? []);\n\tconst [_quickOptions, _setQuickOptions] = React.useState<string[]>(\n\t\tquickOptions ?? []\n\t);\n\n\t// Initialize support store with configuration\n\tReact.useEffect(() => {\n\t\tinitializeSupportStore({ size, defaultOpen });\n\t}, [size, defaultOpen]);\n\n\t// Get support store state and actions\n\tconst { config, open, close, toggle } = useSupportStore();\n\n\t// Update state when props change (for initial values from provider)\n\tReact.useEffect(() => {\n\t\tif (defaultMessages?.length) {\n\t\t\t_setDefaultMessages(defaultMessages);\n\t\t}\n\t}, [defaultMessages]);\n\n\tReact.useEffect(() => {\n\t\tif (quickOptions?.length) {\n\t\t\t_setQuickOptions(quickOptions);\n\t\t}\n\t}, [quickOptions]);\n\n\tconst { client } = useClient(publicKey, apiUrl, wsUrl);\n\tconst { website, isLoading, error: websiteError } = useWebsiteStore(client);\n\tconst isVisitorBlocked = website?.visitor?.isBlocked ?? false;\n\tconst visitorId = website?.visitor?.id ?? null;\n\n\tconst seenEntriesByConversation = useSeenStore(\n\t\tReact.useCallback((state) => state.conversations, [])\n\t);\n\n\tconst conversationSnapshots = useStoreSelector(\n\t\tclient.conversationsStore,\n\t\tReact.useCallback(\n\t\t\t(state) =>\n\t\t\t\tstate.ids\n\t\t\t\t\t.map((id) => {\n\t\t\t\t\t\tconst conversation = state.byId[id];\n\n\t\t\t\t\t\tif (!conversation) {\n\t\t\t\t\t\t\treturn null;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tid: conversation.id,\n\t\t\t\t\t\t\tlastTimelineItem: conversation.lastTimelineItem ?? null,\n\t\t\t\t\t\t} satisfies ConversationSnapshot;\n\t\t\t\t\t})\n\t\t\t\t\t.filter(\n\t\t\t\t\t\t(snapshot): snapshot is ConversationSnapshot => snapshot !== null\n\t\t\t\t\t),\n\t\t\t[]\n\t\t),\n\t\tareConversationSnapshotsEqual\n\t);\n\n\tconst derivedUnreadCount = React.useMemo(() => {\n\t\tif (!visitorId) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tlet count = 0;\n\n\t\tfor (const {\n\t\t\tid: conversationId,\n\t\t\tlastTimelineItem,\n\t\t} of conversationSnapshots) {\n\t\t\tif (!lastTimelineItem) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (lastTimelineItem.type !== ConversationTimelineType.MESSAGE) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (\n\t\t\t\tlastTimelineItem.visitorId &&\n\t\t\t\tlastTimelineItem.visitorId === visitorId\n\t\t\t) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst createdAtTime = Date.parse(lastTimelineItem.createdAt);\n\n\t\t\tif (Number.isNaN(createdAtTime)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst seenEntries = seenEntriesByConversation[conversationId];\n\n\t\t\tif (seenEntries) {\n\t\t\t\tconst visitorSeenEntry = Object.values(seenEntries).find(\n\t\t\t\t\t(entry) =>\n\t\t\t\t\t\tentry.actorType === \"visitor\" && entry.actorId === visitorId\n\t\t\t\t);\n\n\t\t\t\tif (visitorSeenEntry) {\n\t\t\t\t\tconst lastSeenTime = Date.parse(visitorSeenEntry.lastSeenAt);\n\n\t\t\t\t\tif (!Number.isNaN(lastSeenTime) && createdAtTime <= lastSeenTime) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tcount += 1;\n\t\t}\n\n\t\treturn count;\n\t}, [conversationSnapshots, seenEntriesByConversation, visitorId]);\n\n\tReact.useEffect(() => {\n\t\tsetUnreadCount(derivedUnreadCount);\n\t}, [derivedUnreadCount, setUnreadCount]);\n\n\t// Prime REST client with website/visitor context so headers are sent reliably\n\tReact.useEffect(() => {\n\t\tif (!website) {\n\t\t\treturn;\n\t\t}\n\n\t\tclient.setWebsiteContext(website.id, website.visitor?.id ?? undefined);\n\t}, [client, website]);\n\n\tReact.useEffect(() => {\n\t\tif (isVisitorBlocked) {\n\t\t\tprefetchedVisitorRef.current = null;\n\t\t\treturn;\n\t\t}\n\n\t\tif (!autoConnect) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (!website) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (!visitorId) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (prefetchedVisitorRef.current === visitorId) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst hasExistingConversations =\n\t\t\tclient.conversationsStore.getState().ids.length > 0;\n\n\t\tprefetchedVisitorRef.current = visitorId;\n\n\t\tif (hasExistingConversations) {\n\t\t\treturn;\n\t\t}\n\n\t\tvoid client.listConversations().catch((err) => {\n\t\t\tconsole.error(\"[SupportProvider] Failed to prefetch conversations\", err);\n\t\t\tprefetchedVisitorRef.current = null;\n\t\t});\n\t}, [autoConnect, client, isVisitorBlocked, visitorId, website]);\n\n\tconst error = websiteError;\n\n\tReact.useEffect(() => {\n\t\tclient.setVisitorBlocked(isVisitorBlocked);\n\t}, [client, isVisitorBlocked]);\n\n\tconst setDefaultMessages = React.useCallback((messages: DefaultMessage[]) => {\n\t\t_setDefaultMessages(messages);\n\t}, []);\n\n\tconst setQuickOptions = React.useCallback((options: string[]) => {\n\t\t_setQuickOptions(options);\n\t}, []);\n\n\tconst value = React.useMemo<CossistantContextValue>(\n\t\t() => ({\n\t\t\twebsite,\n\t\t\tunreadCount,\n\t\t\tsetUnreadCount,\n\t\t\tisLoading,\n\t\t\terror,\n\t\t\tclient,\n\t\t\tdefaultMessages: _defaultMessages,\n\t\t\tsetDefaultMessages,\n\t\t\tquickOptions: _quickOptions,\n\t\t\tsetQuickOptions,\n\t\t\tisOpen: config.isOpen,\n\t\t\topen,\n\t\t\tclose,\n\t\t\ttoggle,\n\t\t}),\n\t\t[\n\t\t\twebsite,\n\t\t\tunreadCount,\n\t\t\tisLoading,\n\t\t\terror,\n\t\t\tclient,\n\t\t\t_defaultMessages,\n\t\t\t_quickOptions,\n\t\t\tsetDefaultMessages,\n\t\t\tsetQuickOptions,\n\t\t\tconfig.isOpen,\n\t\t\topen,\n\t\t\tclose,\n\t\t\ttoggle,\n\t\t]\n\t);\n\n\tconst webSocketKey = React.useMemo(() => {\n\t\tif (!website) {\n\t\t\treturn \"no-website\";\n\t\t}\n\n\t\tconst visitorKey = website.visitor?.id ?? \"anonymous\";\n\t\tconst blockedState = isVisitorBlocked ? \"blocked\" : \"active\";\n\n\t\treturn `${website.id}:${visitorKey}:${blockedState}`;\n\t}, [isVisitorBlocked, website]);\n\n\treturn (\n\t\t<SupportContext.Provider value={value}>\n\t\t\t<WebSocketProvider\n\t\t\t\tautoConnect={autoConnect && !isVisitorBlocked}\n\t\t\t\tkey={webSocketKey}\n\t\t\t\tonConnect={onWsConnect}\n\t\t\t\tonDisconnect={onWsDisconnect}\n\t\t\t\tonError={onWsError}\n\t\t\t\tpublicKey={publicKey}\n\t\t\t\tvisitorId={isVisitorBlocked ? undefined : website?.visitor?.id}\n\t\t\t\twebsiteId={website?.id}\n\t\t\t\twsUrl={wsUrl}\n\t\t\t>\n\t\t\t\t{children}\n\t\t\t</WebSocketProvider>\n\t\t</SupportContext.Provider>\n\t);\n}\n\n/**\n * Hosts the entire customer support widget ecosystem by handing out context\n * about the current website, visitor, unread counts, realtime subscriptions\n * and the REST client. Provide your Cossistant public key plus optional\n * defaults to configure the widget behaviour.\n */\nexport function SupportProvider({\n\tchildren,\n\tapiUrl = \"https://api.cossistant.com/v1\",\n\twsUrl = \"wss://api.cossistant.com/ws\",\n\tpublicKey,\n\tdefaultMessages,\n\tquickOptions,\n\tautoConnect = true,\n\tonWsConnect,\n\tonWsDisconnect,\n\tonWsError,\n\tsize = \"normal\",\n\tdefaultOpen = false,\n}: SupportProviderProps): React.ReactElement {\n\treturn (\n\t\t<SupportProviderInner\n\t\t\tapiUrl={apiUrl}\n\t\t\tautoConnect={autoConnect}\n\t\t\tdefaultMessages={defaultMessages}\n\t\t\tdefaultOpen={defaultOpen}\n\t\t\tonWsConnect={onWsConnect}\n\t\t\tonWsDisconnect={onWsDisconnect}\n\t\t\tonWsError={onWsError}\n\t\t\tpublicKey={publicKey}\n\t\t\tquickOptions={quickOptions}\n\t\t\tsize={size}\n\t\t\twsUrl={wsUrl}\n\t\t>\n\t\t\t{children}\n\t\t</SupportProviderInner>\n\t);\n}\n\n/**\n * Convenience hook that exposes the aggregated support context. Throws when it\n * is consumed outside of `SupportProvider` to catch integration mistakes.\n */\nexport function useSupport(): UseSupportValue {\n\tconst context = React.useContext(SupportContext);\n\tif (!context) {\n\t\tthrow new Error(\n\t\t\t\"useSupport must be used within a cossistant SupportProvider\"\n\t\t);\n\t}\n\n\tconst availableHumanAgents = context.website?.availableHumanAgents || [];\n\tconst availableAIAgents = context.website?.availableAIAgents || [];\n\tconst visitorLanguage = context.website?.visitor?.language || null;\n\n\t// Get additional config from support store\n\tconst { config } = useSupportStore();\n\n\t// Create visitor object with normalized locale\n\tconst visitor = context.website?.visitor\n\t\t? {\n\t\t\t\t...context.website.visitor,\n\t\t\t\tlocale: normalizeLocale(visitorLanguage),\n\t\t\t}\n\t\t: undefined;\n\n\treturn {\n\t\t...context,\n\t\tavailableHumanAgents,\n\t\tavailableAIAgents,\n\t\tvisitor,\n\t\tsize: config.size,\n\t};\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"provider.js","names":[],"sources":["../src/provider.tsx"],"sourcesContent":["import type { CossistantClient } from \"@cossistant/core\";\nimport { normalizeLocale } from \"@cossistant/core\";\nimport type { DefaultMessage, PublicWebsiteResponse } from \"@cossistant/types\";\nimport type { TimelineItem } from \"@cossistant/types/api/timeline-item\";\nimport { ConversationTimelineType } from \"@cossistant/types/enums\";\nimport React from \"react\";\nimport { useStoreSelector } from \"./hooks/private/store/use-store-selector\";\nimport { useWebsiteStore } from \"./hooks/private/store/use-website-store\";\nimport { useClient } from \"./hooks/private/use-rest-client\";\nimport { useSeenStore } from \"./realtime/seen-store\";\nimport { WebSocketProvider } from \"./support\";\nimport {\n\tinitializeSupportStore,\n\tuseSupportStore,\n} from \"./support/store/support-store\";\n\nexport type SupportProviderProps = {\n\tchildren: React.ReactNode;\n\tdefaultOpen?: boolean;\n\tapiUrl?: string;\n\twsUrl?: string;\n\tpublicKey?: string;\n\tdefaultMessages?: DefaultMessage[];\n\tquickOptions?: string[];\n\tautoConnect?: boolean;\n\tonWsConnect?: () => void;\n\tonWsDisconnect?: () => void;\n\tonWsError?: (error: Error) => void;\n\tsize?: \"normal\" | \"larger\";\n};\n\nexport type CossistantProviderProps = SupportProviderProps;\n\nexport type CossistantContextValue = {\n\twebsite: PublicWebsiteResponse | null;\n\tdefaultMessages: DefaultMessage[];\n\tquickOptions: string[];\n\tsetDefaultMessages: (messages: DefaultMessage[]) => void;\n\tsetQuickOptions: (options: string[]) => void;\n\tunreadCount: number;\n\tsetUnreadCount: (count: number) => void;\n\tisLoading: boolean;\n\terror: Error | null;\n\tclient: CossistantClient;\n\tisOpen: boolean;\n\topen: () => void;\n\tclose: () => void;\n\ttoggle: () => void;\n};\n\ntype WebsiteData = NonNullable<CossistantContextValue[\"website\"]>;\n\ntype VisitorWithLocale = WebsiteData[\"visitor\"] extends null | undefined\n\t? undefined\n\t: NonNullable<WebsiteData[\"visitor\"]> & { locale: string | null };\n\ntype ConversationSnapshot = {\n\tid: string;\n\tlastTimelineItem: TimelineItem | null;\n\tvisitorLastSeenAt: string | null;\n};\n\nfunction areConversationSnapshotsEqual(\n\ta: ConversationSnapshot[],\n\tb: ConversationSnapshot[]\n): boolean {\n\tif (a === b) {\n\t\treturn true;\n\t}\n\n\tif (a.length !== b.length) {\n\t\treturn false;\n\t}\n\n\tfor (let index = 0; index < a.length; index += 1) {\n\t\tconst snapshotA = a[index];\n\t\tconst snapshotB = b[index];\n\n\t\tif (!snapshotA) {\n\t\t\treturn false;\n\t\t}\n\t\tif (!snapshotB) {\n\t\t\treturn false;\n\t\t}\n\n\t\tconst aLastCreatedAt = snapshotA.lastTimelineItem?.createdAt ?? null;\n\t\tconst bLastCreatedAt = snapshotB.lastTimelineItem?.createdAt ?? null;\n\t\tif (\n\t\t\tsnapshotA.id !== snapshotB.id ||\n\t\t\taLastCreatedAt !== bLastCreatedAt ||\n\t\t\tsnapshotA.visitorLastSeenAt !== snapshotB.visitorLastSeenAt\n\t\t) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\treturn true;\n}\n\nexport type UseSupportValue = CossistantContextValue & {\n\tavailableHumanAgents: NonNullable<WebsiteData[\"availableHumanAgents\"]> | [];\n\tavailableAIAgents: NonNullable<WebsiteData[\"availableAIAgents\"]> | [];\n\tvisitor?: VisitorWithLocale;\n\tsize: \"normal\" | \"larger\";\n};\n\nexport const SupportContext = React.createContext<\n\tCossistantContextValue | undefined\n>(undefined);\n\n/**\n * Internal implementation that wires the REST client and websocket provider\n * together before exposing the combined context.\n */\nfunction SupportProviderInner({\n\tchildren,\n\tapiUrl,\n\twsUrl,\n\tpublicKey,\n\tdefaultMessages,\n\tquickOptions,\n\tautoConnect,\n\tonWsConnect,\n\tonWsDisconnect,\n\tonWsError,\n\tsize = \"normal\",\n\tdefaultOpen = false,\n}: SupportProviderProps) {\n\tconst [unreadCount, setUnreadCount] = React.useState(0);\n\tconst prefetchedVisitorRef = React.useRef<string | null>(null);\n\tconst [_defaultMessages, _setDefaultMessages] = React.useState<\n\t\tDefaultMessage[]\n\t>(defaultMessages ?? []);\n\tconst [_quickOptions, _setQuickOptions] = React.useState<string[]>(\n\t\tquickOptions ?? []\n\t);\n\n\t// Initialize support store with configuration\n\tReact.useEffect(() => {\n\t\tinitializeSupportStore({ size, defaultOpen });\n\t}, [size, defaultOpen]);\n\n\t// Get support store state and actions\n\tconst { config, open, close, toggle } = useSupportStore();\n\n\t// Update state when props change (for initial values from provider)\n\tReact.useEffect(() => {\n\t\tif (defaultMessages?.length) {\n\t\t\t_setDefaultMessages(defaultMessages);\n\t\t}\n\t}, [defaultMessages]);\n\n\tReact.useEffect(() => {\n\t\tif (quickOptions?.length) {\n\t\t\t_setQuickOptions(quickOptions);\n\t\t}\n\t}, [quickOptions]);\n\n\tconst { client } = useClient(publicKey, apiUrl, wsUrl);\n\tconst { website, isLoading, error: websiteError } = useWebsiteStore(client);\n\tconst isVisitorBlocked = website?.visitor?.isBlocked ?? false;\n\tconst visitorId = website?.visitor?.id ?? null;\n\n\tconst seenEntriesByConversation = useSeenStore(\n\t\tReact.useCallback((state) => state.conversations, [])\n\t);\n\n\tconst conversationSnapshots = useStoreSelector(\n\t\tclient.conversationsStore,\n\t\tReact.useCallback(\n\t\t\t(state) =>\n\t\t\t\tstate.ids\n\t\t\t\t\t.map((id) => {\n\t\t\t\t\t\tconst conversation = state.byId[id];\n\n\t\t\t\t\t\tif (!conversation) {\n\t\t\t\t\t\t\treturn null;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tid: conversation.id,\n\t\t\t\t\t\t\tlastTimelineItem: conversation.lastTimelineItem ?? null,\n\t\t\t\t\t\t\tvisitorLastSeenAt: conversation.visitorLastSeenAt ?? null,\n\t\t\t\t\t\t} satisfies ConversationSnapshot;\n\t\t\t\t\t})\n\t\t\t\t\t.filter(\n\t\t\t\t\t\t(snapshot): snapshot is ConversationSnapshot => snapshot !== null\n\t\t\t\t\t),\n\t\t\t[]\n\t\t),\n\t\tareConversationSnapshotsEqual\n\t);\n\n\tconst derivedUnreadCount = React.useMemo(() => {\n\t\tif (!visitorId) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tlet count = 0;\n\n\t\tfor (const {\n\t\t\tid: conversationId,\n\t\t\tlastTimelineItem,\n\t\t\tvisitorLastSeenAt,\n\t\t} of conversationSnapshots) {\n\t\t\tif (!lastTimelineItem) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (lastTimelineItem.type !== ConversationTimelineType.MESSAGE) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (\n\t\t\t\tlastTimelineItem.visitorId &&\n\t\t\t\tlastTimelineItem.visitorId === visitorId\n\t\t\t) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst createdAtTime = Date.parse(lastTimelineItem.createdAt);\n\n\t\t\tif (Number.isNaN(createdAtTime)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// First check visitorLastSeenAt from the API response (available immediately)\n\t\t\tif (visitorLastSeenAt) {\n\t\t\t\tconst lastSeenTime = Date.parse(visitorLastSeenAt);\n\t\t\t\tif (!Number.isNaN(lastSeenTime) && createdAtTime <= lastSeenTime) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Fall back to seen store (updated via realtime events)\n\t\t\tconst seenEntries = seenEntriesByConversation[conversationId];\n\n\t\t\tif (seenEntries) {\n\t\t\t\tconst visitorSeenEntry = Object.values(seenEntries).find(\n\t\t\t\t\t(entry) =>\n\t\t\t\t\t\tentry.actorType === \"visitor\" && entry.actorId === visitorId\n\t\t\t\t);\n\n\t\t\t\tif (visitorSeenEntry) {\n\t\t\t\t\tconst lastSeenTime = Date.parse(visitorSeenEntry.lastSeenAt);\n\n\t\t\t\t\tif (!Number.isNaN(lastSeenTime) && createdAtTime <= lastSeenTime) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tcount += 1;\n\t\t}\n\n\t\treturn count;\n\t}, [conversationSnapshots, seenEntriesByConversation, visitorId]);\n\n\tReact.useEffect(() => {\n\t\tsetUnreadCount(derivedUnreadCount);\n\t}, [derivedUnreadCount, setUnreadCount]);\n\n\t// Prime REST client with website/visitor context so headers are sent reliably\n\tReact.useEffect(() => {\n\t\tif (!website) {\n\t\t\treturn;\n\t\t}\n\n\t\tclient.setWebsiteContext(website.id, website.visitor?.id ?? undefined);\n\t}, [client, website]);\n\n\tReact.useEffect(() => {\n\t\tif (isVisitorBlocked) {\n\t\t\tprefetchedVisitorRef.current = null;\n\t\t\treturn;\n\t\t}\n\n\t\tif (!autoConnect) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (!website) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (!visitorId) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (prefetchedVisitorRef.current === visitorId) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst hasExistingConversations =\n\t\t\tclient.conversationsStore.getState().ids.length > 0;\n\n\t\tprefetchedVisitorRef.current = visitorId;\n\n\t\tif (hasExistingConversations) {\n\t\t\treturn;\n\t\t}\n\n\t\tvoid client.listConversations().catch((err) => {\n\t\t\tconsole.error(\"[SupportProvider] Failed to prefetch conversations\", err);\n\t\t\tprefetchedVisitorRef.current = null;\n\t\t});\n\t}, [autoConnect, client, isVisitorBlocked, visitorId, website]);\n\n\tconst error = websiteError;\n\n\tReact.useEffect(() => {\n\t\tclient.setVisitorBlocked(isVisitorBlocked);\n\t}, [client, isVisitorBlocked]);\n\n\tconst setDefaultMessages = React.useCallback((messages: DefaultMessage[]) => {\n\t\t_setDefaultMessages(messages);\n\t}, []);\n\n\tconst setQuickOptions = React.useCallback((options: string[]) => {\n\t\t_setQuickOptions(options);\n\t}, []);\n\n\tconst value = React.useMemo<CossistantContextValue>(\n\t\t() => ({\n\t\t\twebsite,\n\t\t\tunreadCount,\n\t\t\tsetUnreadCount,\n\t\t\tisLoading,\n\t\t\terror,\n\t\t\tclient,\n\t\t\tdefaultMessages: _defaultMessages,\n\t\t\tsetDefaultMessages,\n\t\t\tquickOptions: _quickOptions,\n\t\t\tsetQuickOptions,\n\t\t\tisOpen: config.isOpen,\n\t\t\topen,\n\t\t\tclose,\n\t\t\ttoggle,\n\t\t}),\n\t\t[\n\t\t\twebsite,\n\t\t\tunreadCount,\n\t\t\tisLoading,\n\t\t\terror,\n\t\t\tclient,\n\t\t\t_defaultMessages,\n\t\t\t_quickOptions,\n\t\t\tsetDefaultMessages,\n\t\t\tsetQuickOptions,\n\t\t\tconfig.isOpen,\n\t\t\topen,\n\t\t\tclose,\n\t\t\ttoggle,\n\t\t]\n\t);\n\n\tconst webSocketKey = React.useMemo(() => {\n\t\tif (!website) {\n\t\t\treturn \"no-website\";\n\t\t}\n\n\t\tconst visitorKey = website.visitor?.id ?? \"anonymous\";\n\t\tconst blockedState = isVisitorBlocked ? \"blocked\" : \"active\";\n\n\t\treturn `${website.id}:${visitorKey}:${blockedState}`;\n\t}, [isVisitorBlocked, website]);\n\n\treturn (\n\t\t<SupportContext.Provider value={value}>\n\t\t\t<WebSocketProvider\n\t\t\t\tautoConnect={autoConnect && !isVisitorBlocked}\n\t\t\t\tkey={webSocketKey}\n\t\t\t\tonConnect={onWsConnect}\n\t\t\t\tonDisconnect={onWsDisconnect}\n\t\t\t\tonError={onWsError}\n\t\t\t\tpublicKey={publicKey}\n\t\t\t\tvisitorId={isVisitorBlocked ? undefined : website?.visitor?.id}\n\t\t\t\twebsiteId={website?.id}\n\t\t\t\twsUrl={wsUrl}\n\t\t\t>\n\t\t\t\t{children}\n\t\t\t</WebSocketProvider>\n\t\t</SupportContext.Provider>\n\t);\n}\n\n/**\n * Hosts the entire customer support widget ecosystem by handing out context\n * about the current website, visitor, unread counts, realtime subscriptions\n * and the REST client. Provide your Cossistant public key plus optional\n * defaults to configure the widget behaviour.\n */\nexport function SupportProvider({\n\tchildren,\n\tapiUrl = \"https://api.cossistant.com/v1\",\n\twsUrl = \"wss://api.cossistant.com/ws\",\n\tpublicKey,\n\tdefaultMessages,\n\tquickOptions,\n\tautoConnect = true,\n\tonWsConnect,\n\tonWsDisconnect,\n\tonWsError,\n\tsize = \"normal\",\n\tdefaultOpen = false,\n}: SupportProviderProps): React.ReactElement {\n\treturn (\n\t\t<SupportProviderInner\n\t\t\tapiUrl={apiUrl}\n\t\t\tautoConnect={autoConnect}\n\t\t\tdefaultMessages={defaultMessages}\n\t\t\tdefaultOpen={defaultOpen}\n\t\t\tonWsConnect={onWsConnect}\n\t\t\tonWsDisconnect={onWsDisconnect}\n\t\t\tonWsError={onWsError}\n\t\t\tpublicKey={publicKey}\n\t\t\tquickOptions={quickOptions}\n\t\t\tsize={size}\n\t\t\twsUrl={wsUrl}\n\t\t>\n\t\t\t{children}\n\t\t</SupportProviderInner>\n\t);\n}\n\n/**\n * Convenience hook that exposes the aggregated support context. Throws when it\n * is consumed outside of `SupportProvider` to catch integration mistakes.\n */\nexport function useSupport(): UseSupportValue {\n\tconst context = React.useContext(SupportContext);\n\tif (!context) {\n\t\tthrow new Error(\n\t\t\t\"useSupport must be used within a cossistant SupportProvider\"\n\t\t);\n\t}\n\n\tconst availableHumanAgents = context.website?.availableHumanAgents || [];\n\tconst availableAIAgents = context.website?.availableAIAgents || [];\n\tconst visitorLanguage = context.website?.visitor?.language || null;\n\n\t// Get additional config from support store\n\tconst { config } = useSupportStore();\n\n\t// Create visitor object with normalized locale\n\tconst visitor = context.website?.visitor\n\t\t? {\n\t\t\t\t...context.website.visitor,\n\t\t\t\tlocale: normalizeLocale(visitorLanguage),\n\t\t\t}\n\t\t: undefined;\n\n\treturn {\n\t\t...context,\n\t\tavailableHumanAgents,\n\t\tavailableAIAgents,\n\t\tvisitor,\n\t\tsize: config.size,\n\t};\n}\n"],"mappings":"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"}
|
package/realtime/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { applyConversationSeenEvent, hydrateConversationSeen, upsertConversationSeen } from "./seen-store.js";
|
|
2
|
-
import { RealtimeProvider, useRealtimeConnection } from "./provider.js";
|
|
3
2
|
import { applyConversationTypingEvent, clearTypingFromTimelineItem, clearTypingState, setTypingState } from "./typing-store.js";
|
|
3
|
+
import { RealtimeProvider, useRealtimeConnection } from "./provider.js";
|
|
4
4
|
import { useRealtime } from "./use-realtime.js";
|
|
5
5
|
import { SupportRealtimeProvider } from "./support-provider.js";
|
|
6
6
|
|
package/realtime/provider.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
5
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
6
|
import { isValidEventType, validateRealtimeEvent } from "@cossistant/types/realtime-events";
|
|
6
7
|
import useWebSocket, { ReadyState } from "react-use-websocket";
|
|
7
|
-
import { jsx } from "react/jsx-runtime";
|
|
8
8
|
|
|
9
9
|
//#region src/realtime/provider.tsx
|
|
10
10
|
const DEFAULT_HEARTBEAT_INTERVAL_MS = 15e3;
|
|
@@ -31,7 +31,7 @@ function SupportRealtimeProvider({ children }) {
|
|
|
31
31
|
},
|
|
32
32
|
conversationSeen: (_data, { event, context }) => {
|
|
33
33
|
if (context.websiteId && event.payload.websiteId !== context.websiteId) return;
|
|
34
|
-
applyConversationSeenEvent(event);
|
|
34
|
+
applyConversationSeenEvent(event, { ignoreVisitorId: context.visitorId });
|
|
35
35
|
},
|
|
36
36
|
conversationTyping: (_data, { event, context }) => {
|
|
37
37
|
if (context.websiteId && event.payload.websiteId !== context.websiteId) return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"support-provider.js","names":[],"sources":["../../src/realtime/support-provider.tsx"],"sourcesContent":["import type { CossistantClient } from \"@cossistant/core\";\nimport type { RealtimeEvent } from \"@cossistant/types/realtime-events\";\nimport type React from \"react\";\nimport { useMemo } from \"react\";\nimport { useSupport } from \"../provider\";\nimport { applyConversationSeenEvent } from \"./seen-store\";\nimport {\n\tapplyConversationTypingEvent,\n\tclearTypingFromTimelineItem,\n} from \"./typing-store\";\nimport { useRealtime } from \"./use-realtime\";\n\ntype SupportRealtimeContext = {\n\twebsiteId: string | null;\n\tvisitorId: string | null;\n\tclient: CossistantClient;\n};\n\ntype SupportRealtimeProviderProps = {\n\tchildren: React.ReactNode;\n};\n\n/**\n * Bridges websocket events into the core client stores so support hooks stay\n * in sync without forcing refetches.\n */\nexport function SupportRealtimeProvider({\n\tchildren,\n}: SupportRealtimeProviderProps): React.ReactElement {\n\tconst { website, client, visitor } = useSupport();\n\n\tconst realtimeContext = useMemo<SupportRealtimeContext>(\n\t\t() => ({\n\t\t\twebsiteId: website?.id ?? null,\n\t\t\tvisitorId: visitor?.id ?? null,\n\t\t\tclient,\n\t\t}),\n\t\t[website?.id, visitor?.id, client]\n\t);\n\n\tconst events = useMemo(\n\t\t() => ({\n\t\t\ttimelineItemCreated: (\n\t\t\t\t_data: unknown,\n\t\t\t\t{\n\t\t\t\t\tevent,\n\t\t\t\t\tcontext,\n\t\t\t\t}: {\n\t\t\t\t\tevent: RealtimeEvent<\"timelineItemCreated\">;\n\t\t\t\t\tcontext: SupportRealtimeContext;\n\t\t\t\t}\n\t\t\t) => {\n\t\t\t\tif (\n\t\t\t\t\tcontext.websiteId &&\n\t\t\t\t\tevent.payload.websiteId !== context.websiteId\n\t\t\t\t) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// Clear typing state when a timeline item is created\n\t\t\t\tclearTypingFromTimelineItem(event);\n\n\t\t\t\tcontext.client.handleRealtimeEvent(event);\n\t\t\t},\n\t\t\tconversationSeen: (\n\t\t\t\t_data: unknown,\n\t\t\t\t{\n\t\t\t\t\tevent,\n\t\t\t\t\tcontext,\n\t\t\t\t}: {\n\t\t\t\t\tevent: RealtimeEvent<\"conversationSeen\">;\n\t\t\t\t\tcontext: SupportRealtimeContext;\n\t\t\t\t}\n\t\t\t) => {\n\t\t\t\tif (\n\t\t\t\t\tcontext.websiteId &&\n\t\t\t\t\tevent.payload.websiteId !== context.websiteId\n\t\t\t\t) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// Update the seen store so the UI reflects who has seen messages\n\t\t\t\tapplyConversationSeenEvent(event);\n\t\t\t},\n\t\t\tconversationTyping: (\n\t\t\t\t_data: unknown,\n\t\t\t\t{\n\t\t\t\t\tevent,\n\t\t\t\t\tcontext,\n\t\t\t\t}: {\n\t\t\t\t\tevent: RealtimeEvent<\"conversationTyping\">;\n\t\t\t\t\tcontext: SupportRealtimeContext;\n\t\t\t\t}\n\t\t\t) => {\n\t\t\t\tif (\n\t\t\t\t\tcontext.websiteId &&\n\t\t\t\t\tevent.payload.websiteId !== context.websiteId\n\t\t\t\t) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// Update typing store, but ignore events from the current visitor (their own typing)\n\t\t\t\t// Note: We use context.visitorId which is fresh from the context object\n\t\t\t\tapplyConversationTypingEvent(event, {\n\t\t\t\t\tignoreVisitorId: context.visitorId,\n\t\t\t\t});\n\t\t\t},\n\t\t}),\n\t\t// Empty dependencies is fine here since we use the context parameter\n\t\t// which always has fresh data from the memoized realtimeContext\n\t\t[]\n\t);\n\n\tuseRealtime<SupportRealtimeContext>({\n\t\tcontext: realtimeContext,\n\t\tevents,\n\t\twebsiteId: realtimeContext.websiteId,\n\t\tvisitorId: realtimeContext.visitorId,\n\t});\n\n\treturn <>{children}</>;\n}\n"],"mappings":";;;;;;;;;;;;AA0BA,SAAgB,wBAAwB,EACvC,YACoD;CACpD,MAAM,EAAE,SAAS,QAAQ,YAAY,YAAY;CAEjD,MAAM,kBAAkB,eAChB;EACN,WAAW,SAAS,MAAM;EAC1B,WAAW,SAAS,MAAM;EAC1B;EACA,GACD;EAAC,SAAS;EAAI,SAAS;EAAI;EAAO,CAClC;
|
|
1
|
+
{"version":3,"file":"support-provider.js","names":[],"sources":["../../src/realtime/support-provider.tsx"],"sourcesContent":["import type { CossistantClient } from \"@cossistant/core\";\nimport type { RealtimeEvent } from \"@cossistant/types/realtime-events\";\nimport type React from \"react\";\nimport { useMemo } from \"react\";\nimport { useSupport } from \"../provider\";\nimport { applyConversationSeenEvent } from \"./seen-store\";\nimport {\n\tapplyConversationTypingEvent,\n\tclearTypingFromTimelineItem,\n} from \"./typing-store\";\nimport { useRealtime } from \"./use-realtime\";\n\ntype SupportRealtimeContext = {\n\twebsiteId: string | null;\n\tvisitorId: string | null;\n\tclient: CossistantClient;\n};\n\ntype SupportRealtimeProviderProps = {\n\tchildren: React.ReactNode;\n};\n\n/**\n * Bridges websocket events into the core client stores so support hooks stay\n * in sync without forcing refetches.\n */\nexport function SupportRealtimeProvider({\n\tchildren,\n}: SupportRealtimeProviderProps): React.ReactElement {\n\tconst { website, client, visitor } = useSupport();\n\n\tconst realtimeContext = useMemo<SupportRealtimeContext>(\n\t\t() => ({\n\t\t\twebsiteId: website?.id ?? null,\n\t\t\tvisitorId: visitor?.id ?? null,\n\t\t\tclient,\n\t\t}),\n\t\t[website?.id, visitor?.id, client]\n\t);\n\n\tconst events = useMemo(\n\t\t() => ({\n\t\t\ttimelineItemCreated: (\n\t\t\t\t_data: unknown,\n\t\t\t\t{\n\t\t\t\t\tevent,\n\t\t\t\t\tcontext,\n\t\t\t\t}: {\n\t\t\t\t\tevent: RealtimeEvent<\"timelineItemCreated\">;\n\t\t\t\t\tcontext: SupportRealtimeContext;\n\t\t\t\t}\n\t\t\t) => {\n\t\t\t\tif (\n\t\t\t\t\tcontext.websiteId &&\n\t\t\t\t\tevent.payload.websiteId !== context.websiteId\n\t\t\t\t) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// Clear typing state when a timeline item is created\n\t\t\t\tclearTypingFromTimelineItem(event);\n\n\t\t\t\tcontext.client.handleRealtimeEvent(event);\n\t\t\t},\n\t\t\tconversationSeen: (\n\t\t\t\t_data: unknown,\n\t\t\t\t{\n\t\t\t\t\tevent,\n\t\t\t\t\tcontext,\n\t\t\t\t}: {\n\t\t\t\t\tevent: RealtimeEvent<\"conversationSeen\">;\n\t\t\t\t\tcontext: SupportRealtimeContext;\n\t\t\t\t}\n\t\t\t) => {\n\t\t\t\tif (\n\t\t\t\t\tcontext.websiteId &&\n\t\t\t\t\tevent.payload.websiteId !== context.websiteId\n\t\t\t\t) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// Update the seen store so the UI reflects who has seen messages\n\t\t\t\t// Ignore events from the current visitor (their own seen updates are handled optimistically)\n\t\t\t\tapplyConversationSeenEvent(event, {\n\t\t\t\t\tignoreVisitorId: context.visitorId,\n\t\t\t\t});\n\t\t\t},\n\t\t\tconversationTyping: (\n\t\t\t\t_data: unknown,\n\t\t\t\t{\n\t\t\t\t\tevent,\n\t\t\t\t\tcontext,\n\t\t\t\t}: {\n\t\t\t\t\tevent: RealtimeEvent<\"conversationTyping\">;\n\t\t\t\t\tcontext: SupportRealtimeContext;\n\t\t\t\t}\n\t\t\t) => {\n\t\t\t\tif (\n\t\t\t\t\tcontext.websiteId &&\n\t\t\t\t\tevent.payload.websiteId !== context.websiteId\n\t\t\t\t) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// Update typing store, but ignore events from the current visitor (their own typing)\n\t\t\t\t// Note: We use context.visitorId which is fresh from the context object\n\t\t\t\tapplyConversationTypingEvent(event, {\n\t\t\t\t\tignoreVisitorId: context.visitorId,\n\t\t\t\t});\n\t\t\t},\n\t\t}),\n\t\t// Empty dependencies is fine here since we use the context parameter\n\t\t// which always has fresh data from the memoized realtimeContext\n\t\t[]\n\t);\n\n\tuseRealtime<SupportRealtimeContext>({\n\t\tcontext: realtimeContext,\n\t\tevents,\n\t\twebsiteId: realtimeContext.websiteId,\n\t\tvisitorId: realtimeContext.visitorId,\n\t});\n\n\treturn <>{children}</>;\n}\n"],"mappings":";;;;;;;;;;;;AA0BA,SAAgB,wBAAwB,EACvC,YACoD;CACpD,MAAM,EAAE,SAAS,QAAQ,YAAY,YAAY;CAEjD,MAAM,kBAAkB,eAChB;EACN,WAAW,SAAS,MAAM;EAC1B,WAAW,SAAS,MAAM;EAC1B;EACA,GACD;EAAC,SAAS;EAAI,SAAS;EAAI;EAAO,CAClC;AA8ED,aAAoC;EACnC,SAAS;EACT,QA9Ec,eACP;GACN,sBACC,OACA,EACC,OACA,cAKG;AACJ,QACC,QAAQ,aACR,MAAM,QAAQ,cAAc,QAAQ,UAEpC;AAID,gCAA4B,MAAM;AAElC,YAAQ,OAAO,oBAAoB,MAAM;;GAE1C,mBACC,OACA,EACC,OACA,cAKG;AACJ,QACC,QAAQ,aACR,MAAM,QAAQ,cAAc,QAAQ,UAEpC;AAKD,+BAA2B,OAAO,EACjC,iBAAiB,QAAQ,WACzB,CAAC;;GAEH,qBACC,OACA,EACC,OACA,cAKG;AACJ,QACC,QAAQ,aACR,MAAM,QAAQ,cAAc,QAAQ,UAEpC;AAKD,iCAA6B,OAAO,EACnC,iBAAiB,QAAQ,WACzB,CAAC;;GAEH,GAGD,EAAE,CACF;EAKA,WAAW,gBAAgB;EAC3B,WAAW,gBAAgB;EAC3B,CAAC;AAEF,QAAO,gCAAG,WAAY"}
|
package/realtime-events.d.ts
CHANGED
|
@@ -116,6 +116,7 @@ declare const realtimeSchema: {
|
|
|
116
116
|
spam: "spam";
|
|
117
117
|
}>>;
|
|
118
118
|
deletedAt: ZodDefault<ZodNullable<ZodString>>;
|
|
119
|
+
visitorLastSeenAt: ZodOptional<ZodNullable<ZodString>>;
|
|
119
120
|
lastTimelineItem: ZodOptional<ZodObject<{
|
|
120
121
|
id: ZodOptional<ZodString>;
|
|
121
122
|
conversationId: ZodString;
|
|
@@ -194,9 +195,9 @@ declare const realtimeSchema: {
|
|
|
194
195
|
spam: "spam";
|
|
195
196
|
}>;
|
|
196
197
|
priority: ZodEnum<{
|
|
197
|
-
normal: "normal";
|
|
198
198
|
high: "high";
|
|
199
199
|
low: "low";
|
|
200
|
+
normal: "normal";
|
|
200
201
|
urgent: "urgent";
|
|
201
202
|
}>;
|
|
202
203
|
organizationId: ZodString;
|
|
@@ -429,6 +430,44 @@ declare const realtimeSchema: {
|
|
|
429
430
|
}, $strip>>;
|
|
430
431
|
}, $strip>;
|
|
431
432
|
}, $strip>;
|
|
433
|
+
readonly conversationEventCreated: ZodObject<{
|
|
434
|
+
websiteId: ZodString;
|
|
435
|
+
organizationId: ZodString;
|
|
436
|
+
visitorId: ZodNullable<ZodString>;
|
|
437
|
+
userId: ZodNullable<ZodString>;
|
|
438
|
+
conversationId: ZodString;
|
|
439
|
+
aiAgentId: ZodNullable<ZodString>;
|
|
440
|
+
event: ZodObject<{
|
|
441
|
+
id: ZodString;
|
|
442
|
+
conversationId: ZodString;
|
|
443
|
+
organizationId: ZodString;
|
|
444
|
+
type: ZodEnum<{
|
|
445
|
+
assigned: "assigned";
|
|
446
|
+
unassigned: "unassigned";
|
|
447
|
+
participant_requested: "participant_requested";
|
|
448
|
+
participant_joined: "participant_joined";
|
|
449
|
+
participant_left: "participant_left";
|
|
450
|
+
status_changed: "status_changed";
|
|
451
|
+
priority_changed: "priority_changed";
|
|
452
|
+
tag_added: "tag_added";
|
|
453
|
+
tag_removed: "tag_removed";
|
|
454
|
+
resolved: "resolved";
|
|
455
|
+
reopened: "reopened";
|
|
456
|
+
visitor_blocked: "visitor_blocked";
|
|
457
|
+
visitor_unblocked: "visitor_unblocked";
|
|
458
|
+
visitor_identified: "visitor_identified";
|
|
459
|
+
}>;
|
|
460
|
+
actorUserId: ZodNullable<ZodString>;
|
|
461
|
+
actorAiAgentId: ZodNullable<ZodString>;
|
|
462
|
+
targetUserId: ZodNullable<ZodString>;
|
|
463
|
+
targetAiAgentId: ZodNullable<ZodString>;
|
|
464
|
+
message: ZodNullable<ZodString>;
|
|
465
|
+
metadata: ZodNullable<ZodRecord<ZodString, ZodUnknown>>;
|
|
466
|
+
createdAt: ZodString;
|
|
467
|
+
updatedAt: ZodString;
|
|
468
|
+
deletedAt: ZodNullable<ZodString>;
|
|
469
|
+
}, $strip>;
|
|
470
|
+
}, $strip>;
|
|
432
471
|
};
|
|
433
472
|
type RealtimeEventType = keyof typeof realtimeSchema;
|
|
434
473
|
type RealtimeEventPayload<T extends RealtimeEventType> = output<(typeof realtimeSchema)[T]>;
|
package/realtime-events.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"realtime-events.d.ts","names":[],"sources":["../../types/src/realtime-events.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"realtime-events.d.ts","names":[],"sources":["../../types/src/realtime-events.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;cAqBa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAqGD,iBAAA,gBAAiC;KAEjC,+BAA+B,qBAAqB,eACvD,gBAAgB;KAGb,wBAAwB;QAC7B;WACG,qBAAqB;;KAGnB,gBAAA,WACL,oBAAoB,cAAc,KACvC;KAEU,4BAA4B,qBACvC,qBAAqB"}
|
package/registries.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { $ZodType } from "./schemas.js";
|
|
2
2
|
import { input, output } from "./core.js";
|
|
3
3
|
|
|
4
|
-
//#region ../../node_modules
|
|
4
|
+
//#region ../../node_modules/zod/v4/core/registries.d.cts
|
|
5
5
|
declare const $output: unique symbol;
|
|
6
6
|
type $output = typeof $output;
|
|
7
7
|
declare const $input: unique symbol;
|
package/registries.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registries.d.ts","names":["core","$ZodType","$output","$input","$replace","Meta","S","output","input","M","P","K","R","MetadataType","$ZodRegistry","Schema","WeakMap","Map","JSONSchemaMeta","GlobalMeta","registry","T","globalRegistry"],"sources":["../../../node_modules
|
|
1
|
+
{"version":3,"file":"registries.d.ts","names":["core","$ZodType","$output","$input","$replace","Meta","S","output","input","M","P","K","R","MetadataType","$ZodRegistry","Schema","WeakMap","Map","JSONSchemaMeta","GlobalMeta","registry","T","globalRegistry"],"sources":["../../../node_modules/zod/v4/core/registries.d.cts"],"sourcesContent":["import type * as core from \"./core.cjs\";\nimport type { $ZodType } from \"./schemas.cjs\";\nexport declare const $output: unique symbol;\nexport type $output = typeof $output;\nexport declare const $input: unique symbol;\nexport type $input = typeof $input;\nexport type $replace<Meta, S extends $ZodType> = Meta extends $output ? core.output<S> : Meta extends $input ? core.input<S> : Meta extends (infer M)[] ? $replace<M, S>[] : Meta extends (...args: infer P) => infer R ? (...args: {\n [K in keyof P]: $replace<P[K], S>;\n}) => $replace<R, S> : Meta extends object ? {\n [K in keyof Meta]: $replace<Meta[K], S>;\n} : Meta;\ntype MetadataType = object | undefined;\nexport declare class $ZodRegistry<Meta extends MetadataType = MetadataType, Schema extends $ZodType = $ZodType> {\n _meta: Meta;\n _schema: Schema;\n _map: WeakMap<Schema, $replace<Meta, Schema>>;\n _idmap: Map<string, Schema>;\n add<S extends Schema>(schema: S, ..._meta: undefined extends Meta ? [$replace<Meta, S>?] : [$replace<Meta, S>]): this;\n clear(): this;\n remove(schema: Schema): this;\n get<S extends Schema>(schema: S): $replace<Meta, S> | undefined;\n has(schema: Schema): boolean;\n}\nexport interface JSONSchemaMeta {\n id?: string | undefined;\n title?: string | undefined;\n description?: string | undefined;\n deprecated?: boolean | undefined;\n [k: string]: unknown;\n}\nexport interface GlobalMeta extends JSONSchemaMeta {\n}\nexport declare function registry<T extends MetadataType = MetadataType, S extends $ZodType = $ZodType>(): $ZodRegistry<T, S>;\nexport declare const globalRegistry: $ZodRegistry<GlobalMeta>;\nexport {};\n"],"x_google_ignoreList":[0],"mappings":";;;;cAEqBE;KACTA,OAAAA,UAAiBA;AADRA,cAEAC,MAFsB,EAAA,OAAA,MAAA;AAC/BD,KAEAC,MAAAA,GAFO,OAESA,MAFCD;AACRC,KAETC,QAF8B,CAAA,IAAA,EAAA,UAELH,QAFK,CAAA,GAEOI,IAFP,SAEoBH,OAFpB,GAE8BF,MAF9B,CAE0CM,CAF1C,CAAA,GAE+CD,IAF/C,SAE4DF,MAF5D,GAEqEH,KAFrE,CAEgFM,CAFhF,CAAA,GAEqFD,IAFrF,SAAA,CAAA,KAAA,EAAA,CAAA,EAAA,GAEgHD,QAFhH,CAEyHK,CAFzH,EAE4HH,CAF5H,CAAA,EAAA,GAEmID,IAFnI,UAAA,CAAA,GAAA,IAAA,EAAA,KAAA,EAAA,EAAA,GAAA,KAAA,EAAA,IAAA,CAAA,GAAA,IAAA,EAAA,QAC9BF,MAEIO,CAFE,GAEEN,QAFQD,CAECO,CAFDP,CAEGQ,CAFHR,CAAAA,EAEOG,CAFD,CAAA,EAClC,EAAA,GAEMF,QAFMA,CAEGQ,CAFK,EAEFN,CAFE,CAAA,GAEGD,IAFHA,SAAA,MAAA,GAAA,QAAiBJ,MAGrBI,IAHqBJ,GAGdG,QAHcH,CAGLI,IAHKJ,CAGAU,CAHAV,CAAAA,EAGIK,CAHJL,CAAAA,EAAYI,GAI7CA,IAJ6CA;KAK5CQ,YAAAA,GALyDX,MAAAA,GAAAA,SAAAA;AAAsBI,cAM/DQ,YAN+DR,CAAAA,aAMrCO,YANqCP,GAMtBO,YANsBP,EAAAA,eAMOL,QANPK,GAMkBL,QANlBK,CAAAA,CAAAA;EAAZN,KAAAA,EAO7DK,IAP6DL;EAAiBK,OAAAA,EAQ5EU,MAR4EV;EAAaF,IAAAA,EAS5Fa,OAT4Fb,CASpFY,MAToFZ,EAS5EC,QAT4ED,CASnEE,IATmEF,EAS7DY,MAT6DZ,CAAAA,CAAAA;EAAoBG,MAAAA,EAU9GW,GAV8GX,CAAAA,MAAAA,EAUlGS,MAVkGT,CAAAA;EAAXN,GAAAA,CAAAA,UAW7Fe,MAX6Ff,CAAAA,CAAAA,MAAAA,EAW7EM,CAX6EN,EAAAA,GAAAA,KAAAA,EAAAA,SAAAA,SAW9CK,IAX8CL,GAAAA,CAWtCI,QAXsCJ,CAW7BK,IAX6BL,EAWvBM,CAXuBN,CAAAA,CAAAA,CAAAA,GAAAA,CAWfI,QAXeJ,CAWNK,IAXML,EAWAM,CAXAN,CAAAA,CAAAA,CAAAA,EAAAA,IAAAA;EAAgBK,KAAAA,CAAAA,CAAAA,EAAAA,IAAAA;EAAoCI,MAAAA,CAAAA,MAAAA,EAahJM,MAbgJN,CAAAA,EAAAA,IAAAA;EAAGH,GAAAA,CAAAA,UAcpJS,MAdoJT,CAAAA,CAAAA,MAAAA,EAcpIA,CAdoIA,CAAAA,EAchIF,QAdgIE,CAcvHD,IAduHC,EAcjHA,CAdiHA,CAAAA,GAAAA,SAAAA;EAAZF,GAAAA,CAAAA,MAAAA,EAe1IW,MAf0IX,CAAAA,EAAAA,OAAAA;;AAC1IM,UAgBCQ,cAAAA,CAhBDR;EAAaA,EAAAA,CAAAA,EAAAA,MAAAA,GAAAA,SAAAA;EAAEC,KAAAA,CAAAA,EAAAA,MAAAA,GAAAA,SAAAA;EAAIL,WAAAA,CAAAA,EAAAA,MAAAA,GAAAA,SAAAA;EAAfF,UAAAA,CAAAA,EAAAA,OAAAA,GAAAA,SAAAA;EACLQ,CAAAA,CAAAA,EAAAA,MAAAA,CAAAA,EAAAA,OAAAA;;AAATR,UAsBWe,UAAAA,SAAmBD,cAtB9Bd,CAAAA"}
|
package/schemas.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { $ZodCheck, $ZodCheckDef, $ZodCheckInternals, $ZodCheckStringFormatDef,
|
|
|
5
5
|
import { $ZodErrorMap, $ZodIssue, $ZodIssueBase, $ZodIssueInvalidKey, $ZodIssueInvalidType, $ZodIssueInvalidUnion, $ZodIssueInvalidValue, $ZodIssueUnrecognizedKeys, $ZodRawIssue } from "./errors.js";
|
|
6
6
|
import { $constructor, input, output } from "./core.js";
|
|
7
7
|
|
|
8
|
-
//#region ../../node_modules
|
|
8
|
+
//#region ../../node_modules/zod/v4/core/schemas.d.cts
|
|
9
9
|
interface ParseContext<T$1 extends $ZodIssueBase = never> {
|
|
10
10
|
/** Customize error messages. */
|
|
11
11
|
readonly error?: $ZodErrorMap<T$1>;
|