@paramms/chat-widget 1.0.19 → 1.0.20
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/dist/uid.d.ts +0 -2
- package/dist/uid.js +61 -39
- package/dist/uid.js.map +1 -1
- package/package.json +1 -1
package/dist/uid.d.ts
CHANGED
package/dist/uid.js
CHANGED
|
@@ -1,69 +1,91 @@
|
|
|
1
|
-
function
|
|
1
|
+
function $(t, s) {
|
|
2
2
|
var n;
|
|
3
|
-
const
|
|
4
|
-
return { wsUrl: `${
|
|
3
|
+
const o = t.trim().replace(/\/+$/, ""), e = (n = o.match(/^(https|http|wss|ws):\/\//)) == null ? void 0 : n[1], r = e ? e === "https" || e === "wss" : !0, c = (e ? o.slice(e.length + 3) : o).replace(/\/ws$/, ""), l = s ? s.trim().replace(/\/+$/, "") : `${r ? "https" : "http"}://${c}`;
|
|
4
|
+
return { wsUrl: `${r ? "wss" : "ws"}://${c}/ws`, httpBase: l };
|
|
5
5
|
}
|
|
6
|
-
function
|
|
7
|
-
return
|
|
6
|
+
function w(t) {
|
|
7
|
+
return $(t).httpBase;
|
|
8
8
|
}
|
|
9
|
-
function
|
|
10
|
-
const
|
|
9
|
+
function d(t, s, o, e) {
|
|
10
|
+
const r = `beforeSeq=${o}&limit=${e}`;
|
|
11
11
|
return [
|
|
12
|
-
`${
|
|
13
|
-
`${
|
|
12
|
+
`${t}/conversations/${s}/messages?${r}`,
|
|
13
|
+
`${t}/conversations/${s}/history?${r}`
|
|
14
14
|
];
|
|
15
15
|
}
|
|
16
|
-
async function g(
|
|
17
|
-
for (const
|
|
16
|
+
async function g(t, s, o, e, r = 20) {
|
|
17
|
+
for (const c of d(t, s, e, r))
|
|
18
18
|
try {
|
|
19
|
-
const
|
|
20
|
-
if (!
|
|
21
|
-
const
|
|
22
|
-
return { messages:
|
|
19
|
+
const l = await fetch(c, { headers: { authorization: `Bearer ${o}` } });
|
|
20
|
+
if (!l.ok) continue;
|
|
21
|
+
const a = await l.json();
|
|
22
|
+
return { messages: a.messages ?? [], hasMore: a.hasMore ?? !1 };
|
|
23
23
|
} catch {
|
|
24
24
|
}
|
|
25
25
|
return null;
|
|
26
26
|
}
|
|
27
|
-
async function
|
|
28
|
-
const
|
|
29
|
-
if (!
|
|
27
|
+
async function A(t, s, o, e, r, c) {
|
|
28
|
+
const l = c ? c.replace(/\/+$/, "") : w(t), a = await g(l, o, s, Number.MAX_SAFE_INTEGER);
|
|
29
|
+
if (!a || (a.messages.length ? (e.apply({ type: "sync", conversationId: o, messages: a.messages }), e.apply({ type: "history", conversationId: o, messages: [], hasMore: a.hasMore }), r.render(e)) : e.apply({ type: "history", conversationId: o, messages: [], hasMore: !1 }), !a.hasMore)) return;
|
|
30
30
|
let n = !1;
|
|
31
|
-
const
|
|
32
|
-
if (n || !
|
|
31
|
+
const y = async () => {
|
|
32
|
+
if (n || !e.hasMoreHistory) return;
|
|
33
33
|
n = !0;
|
|
34
|
-
const f =
|
|
34
|
+
const f = e.messages()[0];
|
|
35
35
|
if (!f) {
|
|
36
36
|
n = !1;
|
|
37
37
|
return;
|
|
38
38
|
}
|
|
39
|
-
const h = await g(
|
|
40
|
-
h && (
|
|
41
|
-
}, i =
|
|
39
|
+
const h = await g(l, o, s, f.seq);
|
|
40
|
+
h && (e.apply({ type: "history", conversationId: o, messages: h.messages, hasMore: h.hasMore }), r.render(e)), n = !1;
|
|
41
|
+
}, i = r.getScrollEl();
|
|
42
42
|
if (!i) return;
|
|
43
|
-
let
|
|
43
|
+
let m = !1;
|
|
44
44
|
setTimeout(() => {
|
|
45
|
-
|
|
45
|
+
m = !0;
|
|
46
46
|
}, 300);
|
|
47
47
|
const p = () => {
|
|
48
|
-
|
|
48
|
+
m && i.scrollTop < 80 && e.hasMoreHistory && !n && y();
|
|
49
49
|
};
|
|
50
|
-
i.addEventListener("scroll", p, { passive: !0 }),
|
|
50
|
+
i.addEventListener("scroll", p, { passive: !0 }), r.setScrollCleanup(() => i.removeEventListener("scroll", p));
|
|
51
51
|
}
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
const u = "oc_uid";
|
|
53
|
+
function M(t) {
|
|
54
54
|
try {
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
const t = `g_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
|
|
58
|
-
return localStorage.setItem(e, t), t;
|
|
55
|
+
const s = document.cookie.match(new RegExp(`(?:^|; )${t}=([^;]*)`));
|
|
56
|
+
return s ? decodeURIComponent(s[1]) : null;
|
|
59
57
|
} catch {
|
|
60
|
-
return
|
|
58
|
+
return null;
|
|
61
59
|
}
|
|
62
60
|
}
|
|
61
|
+
function S(t, s) {
|
|
62
|
+
try {
|
|
63
|
+
const e = location.protocol === "https:" ? "; Secure" : "";
|
|
64
|
+
document.cookie = `${t}=${encodeURIComponent(s)}; Max-Age=31536000; Path=/; SameSite=Lax${e}`;
|
|
65
|
+
} catch {
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function E() {
|
|
69
|
+
return `g_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
|
|
70
|
+
}
|
|
71
|
+
function U() {
|
|
72
|
+
let t = null;
|
|
73
|
+
try {
|
|
74
|
+
t = localStorage.getItem(u);
|
|
75
|
+
} catch {
|
|
76
|
+
}
|
|
77
|
+
t || (t = M(u));
|
|
78
|
+
const s = t ?? E();
|
|
79
|
+
try {
|
|
80
|
+
localStorage.setItem(u, s);
|
|
81
|
+
} catch {
|
|
82
|
+
}
|
|
83
|
+
return S(u, s), s;
|
|
84
|
+
}
|
|
63
85
|
export {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
86
|
+
A as a,
|
|
87
|
+
w as h,
|
|
88
|
+
U as p,
|
|
89
|
+
$ as r
|
|
68
90
|
};
|
|
69
91
|
//# sourceMappingURL=uid.js.map
|
package/dist/uid.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"uid.js","sources":["../src/history.ts","../src/uid.ts"],"sourcesContent":["// history.ts — shared REST history-fetch logic for both the guest widget\n// (index.ts) and the agent dashboard (operate.ts).\n//\n// Strategy:\n// • On open: fetch the latest 20 messages. Fast, cheap, covers most chats.\n// • hasMore=true → show a sentinel div at the top of the scroll area.\n// • When the sentinel scrolls into view (IntersectionObserver) fetch the\n// next 20 older messages and prepend — no button click required.\n// • This is the same infinite-scroll-upward pattern used by WhatsApp/Telegram.\n\nimport type { ChatStore } from './store.js'\nimport type { Message, ConversationId } from './protocol/index.js'\nimport type { Renderer } from './renderer.js'\n\nconst PAGE = 20 // messages per fetch — fast first load, smooth pagination\n\n/** Resolve a single user-supplied relay URL into the concrete WebSocket URL and\n * HTTP(S) base the client needs.\n *\n * Accepts any scheme — `https://`, `http://`, `wss://`, or `ws://` — and any of\n * these shapes: bare origin (`https://api.example.com`), origin with `/ws`\n * (`wss://api.example.com/ws`), or a sub-path (`https://api.example.com/relay`).\n *\n * https://api.example.com → ws wss://api.example.com/ws · http https://api.example.com\n * wss://api.example.com/ws → ws wss://api.example.com/ws · http https://api.example.com\n * http://localhost:3000 → ws ws://localhost:3000/ws · http http://localhost:3000\n *\n * `https`/`wss` map to a secure socket (`wss`); `http`/`ws` map to `ws`. A bare\n * host with no scheme is assumed secure. Pass `apiBaseOverride` only when the\n * REST API lives on a different origin than the socket. */\nexport function resolveRelayUrls(input: string, apiBaseOverride?: string): { wsUrl: string; httpBase: string } {\n const trimmed = input.trim().replace(/\\/+$/, '')\n const scheme = trimmed.match(/^(https|http|wss|ws):\\/\\//)?.[1]\n const secure = scheme ? scheme === 'https' || scheme === 'wss' : true\n const authorityAndPath = (scheme ? trimmed.slice(scheme.length + 3) : trimmed).replace(/\\/ws$/, '')\n const httpBase = apiBaseOverride\n ? apiBaseOverride.trim().replace(/\\/+$/, '')\n : `${secure ? 'https' : 'http'}://${authorityAndPath}`\n const wsUrl = `${secure ? 'wss' : 'ws'}://${authorityAndPath}/ws`\n return { wsUrl, httpBase }\n}\n\n/** Derive the HTTP(S) base origin from a ws(s):// URL.\n * @deprecated prefer {@link resolveRelayUrls}; kept for back-compat. */\nexport function httpBaseFromWsUrl(wsUrl: string): string {\n return resolveRelayUrls(wsUrl).httpBase\n}\n\n/** Build the best history URL for the given token context.\n * Staff tokens use /messages (full access); guest tokens use /history. */\nfunction historyUrl(httpBase: string, conversationId: string, beforeSeq: number, limit: number): string[] {\n const qs = `beforeSeq=${beforeSeq}&limit=${limit}`\n return [\n `${httpBase}/conversations/${conversationId}/messages?${qs}`,\n `${httpBase}/conversations/${conversationId}/history?${qs}`,\n ]\n}\n\n/** Fetch one page of history. Tries staff endpoint first, falls back to guest. */\nasync function fetchPage(\n httpBase: string,\n conversationId: string,\n token: string,\n beforeSeq: number,\n limit = PAGE,\n): Promise<{ messages: Message[]; hasMore: boolean } | null> {\n for (const url of historyUrl(httpBase, conversationId, beforeSeq, limit)) {\n try {\n const res = await fetch(url, { headers: { authorization: `Bearer ${token}` } })\n if (!res.ok) continue\n const data = await res.json() as { messages?: Message[]; hasMore?: boolean }\n return { messages: data.messages ?? [], hasMore: data.hasMore ?? false }\n } catch { /* try next */ }\n }\n return null\n}\n\n/** Initial history restore on conversation open.\n *\n * Fetches the latest PAGE messages and sets up scroll-triggered loading for\n * older messages via IntersectionObserver on a sentinel at the top of the\n * scroll container. No buttons — scrolling up loads more automatically.\n *\n * Returns a cleanup function — call it when the conversation is closed to\n * disconnect the observer and prevent stale updates. */\nexport async function restoreHistory(\n wsUrl: string,\n token: string,\n conversationId: ConversationId,\n store: ChatStore,\n renderer: Renderer,\n apiBase?: string,\n): Promise<void> {\n const httpBase = apiBase ? apiBase.replace(/\\/+$/, '') : httpBaseFromWsUrl(wsUrl)\n\n const page = await fetchPage(httpBase, conversationId as string, token, Number.MAX_SAFE_INTEGER)\n if (!page) return\n\n if (page.messages.length) {\n store.apply({ type: 'sync', conversationId, messages: page.messages })\n // Always set hasMore from the response\n store.apply({ type: 'history', conversationId, messages: [], hasMore: page.hasMore })\n renderer.render(store)\n } else {\n // No messages — still record hasMore=false so the sentinel doesn't show\n store.apply({ type: 'history', conversationId, messages: [], hasMore: false })\n }\n\n if (!page.hasMore) return\n\n // ── Scroll-triggered load-more ────────────────────────────────────────────\n // Watch the sentinel div (the \"↑ Load earlier messages\" button rendered by\n // the Renderer at the top of the scroll area). When it becomes visible,\n // fetch the next page of older messages.\n let loading = false\n\n const loadOlder = async () => {\n if (loading || !store.hasMoreHistory) return\n loading = true\n const oldest = store.messages()[0]\n if (!oldest) { loading = false; return }\n const page2 = await fetchPage(httpBase, conversationId as string, token, oldest.seq)\n if (page2) {\n store.apply({ type: 'history', conversationId, messages: page2.messages, hasMore: page2.hasMore })\n renderer.render(store)\n }\n loading = false\n }\n\n // Use IntersectionObserver to detect when the user scrolls to the top.\n // We observe the scroll container itself — when scrollTop < 40px, load more.\n const scrollEl = renderer.getScrollEl()\n if (!scrollEl) return\n\n // Wait 300ms before arming the scroll listener — the initial render scrolls\n // to the bottom, which briefly passes through scrollTop=0 and could trigger\n // a spurious load before the user actually scrolls up.\n let armed = false\n setTimeout(() => { armed = true }, 300)\n\n const onScroll = () => {\n if (!armed) return\n if (scrollEl.scrollTop < 80 && store.hasMoreHistory && !loading) {\n void loadOlder()\n }\n }\n scrollEl.addEventListener('scroll', onScroll, { passive: true })\n renderer.setScrollCleanup(() => scrollEl.removeEventListener('scroll', onScroll))\n}\n","/** Persistent anonymous identity, reused across reloads (per the old widget's\n * oc_uid pattern). */\nexport function persistentUid(): string {\n const key = 'oc_uid'\n try {\n const existing = localStorage.getItem(key)\n if (existing) return existing\n const id = `g_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`\n localStorage.setItem(key, id)\n return id\n } catch {\n return `g_${Math.random().toString(36).slice(2)}`\n }\n}\n"],"names":["resolveRelayUrls","input","apiBaseOverride","_a","trimmed","scheme","secure","authorityAndPath","httpBase","httpBaseFromWsUrl","wsUrl","historyUrl","conversationId","beforeSeq","limit","qs","fetchPage","token","url","res","data","restoreHistory","store","renderer","apiBase","page","loading","loadOlder","oldest","page2","scrollEl","armed","onScroll","persistentUid","key","existing","id"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"uid.js","sources":["../src/history.ts","../src/uid.ts"],"sourcesContent":["// history.ts — shared REST history-fetch logic for both the guest widget\n// (index.ts) and the agent dashboard (operate.ts).\n//\n// Strategy:\n// • On open: fetch the latest 20 messages. Fast, cheap, covers most chats.\n// • hasMore=true → show a sentinel div at the top of the scroll area.\n// • When the sentinel scrolls into view (IntersectionObserver) fetch the\n// next 20 older messages and prepend — no button click required.\n// • This is the same infinite-scroll-upward pattern used by WhatsApp/Telegram.\n\nimport type { ChatStore } from './store.js'\nimport type { Message, ConversationId } from './protocol/index.js'\nimport type { Renderer } from './renderer.js'\n\nconst PAGE = 20 // messages per fetch — fast first load, smooth pagination\n\n/** Resolve a single user-supplied relay URL into the concrete WebSocket URL and\n * HTTP(S) base the client needs.\n *\n * Accepts any scheme — `https://`, `http://`, `wss://`, or `ws://` — and any of\n * these shapes: bare origin (`https://api.example.com`), origin with `/ws`\n * (`wss://api.example.com/ws`), or a sub-path (`https://api.example.com/relay`).\n *\n * https://api.example.com → ws wss://api.example.com/ws · http https://api.example.com\n * wss://api.example.com/ws → ws wss://api.example.com/ws · http https://api.example.com\n * http://localhost:3000 → ws ws://localhost:3000/ws · http http://localhost:3000\n *\n * `https`/`wss` map to a secure socket (`wss`); `http`/`ws` map to `ws`. A bare\n * host with no scheme is assumed secure. Pass `apiBaseOverride` only when the\n * REST API lives on a different origin than the socket. */\nexport function resolveRelayUrls(input: string, apiBaseOverride?: string): { wsUrl: string; httpBase: string } {\n const trimmed = input.trim().replace(/\\/+$/, '')\n const scheme = trimmed.match(/^(https|http|wss|ws):\\/\\//)?.[1]\n const secure = scheme ? scheme === 'https' || scheme === 'wss' : true\n const authorityAndPath = (scheme ? trimmed.slice(scheme.length + 3) : trimmed).replace(/\\/ws$/, '')\n const httpBase = apiBaseOverride\n ? apiBaseOverride.trim().replace(/\\/+$/, '')\n : `${secure ? 'https' : 'http'}://${authorityAndPath}`\n const wsUrl = `${secure ? 'wss' : 'ws'}://${authorityAndPath}/ws`\n return { wsUrl, httpBase }\n}\n\n/** Derive the HTTP(S) base origin from a ws(s):// URL.\n * @deprecated prefer {@link resolveRelayUrls}; kept for back-compat. */\nexport function httpBaseFromWsUrl(wsUrl: string): string {\n return resolveRelayUrls(wsUrl).httpBase\n}\n\n/** Build the best history URL for the given token context.\n * Staff tokens use /messages (full access); guest tokens use /history. */\nfunction historyUrl(httpBase: string, conversationId: string, beforeSeq: number, limit: number): string[] {\n const qs = `beforeSeq=${beforeSeq}&limit=${limit}`\n return [\n `${httpBase}/conversations/${conversationId}/messages?${qs}`,\n `${httpBase}/conversations/${conversationId}/history?${qs}`,\n ]\n}\n\n/** Fetch one page of history. Tries staff endpoint first, falls back to guest. */\nasync function fetchPage(\n httpBase: string,\n conversationId: string,\n token: string,\n beforeSeq: number,\n limit = PAGE,\n): Promise<{ messages: Message[]; hasMore: boolean } | null> {\n for (const url of historyUrl(httpBase, conversationId, beforeSeq, limit)) {\n try {\n const res = await fetch(url, { headers: { authorization: `Bearer ${token}` } })\n if (!res.ok) continue\n const data = await res.json() as { messages?: Message[]; hasMore?: boolean }\n return { messages: data.messages ?? [], hasMore: data.hasMore ?? false }\n } catch { /* try next */ }\n }\n return null\n}\n\n/** Initial history restore on conversation open.\n *\n * Fetches the latest PAGE messages and sets up scroll-triggered loading for\n * older messages via IntersectionObserver on a sentinel at the top of the\n * scroll container. No buttons — scrolling up loads more automatically.\n *\n * Returns a cleanup function — call it when the conversation is closed to\n * disconnect the observer and prevent stale updates. */\nexport async function restoreHistory(\n wsUrl: string,\n token: string,\n conversationId: ConversationId,\n store: ChatStore,\n renderer: Renderer,\n apiBase?: string,\n): Promise<void> {\n const httpBase = apiBase ? apiBase.replace(/\\/+$/, '') : httpBaseFromWsUrl(wsUrl)\n\n const page = await fetchPage(httpBase, conversationId as string, token, Number.MAX_SAFE_INTEGER)\n if (!page) return\n\n if (page.messages.length) {\n store.apply({ type: 'sync', conversationId, messages: page.messages })\n // Always set hasMore from the response\n store.apply({ type: 'history', conversationId, messages: [], hasMore: page.hasMore })\n renderer.render(store)\n } else {\n // No messages — still record hasMore=false so the sentinel doesn't show\n store.apply({ type: 'history', conversationId, messages: [], hasMore: false })\n }\n\n if (!page.hasMore) return\n\n // ── Scroll-triggered load-more ────────────────────────────────────────────\n // Watch the sentinel div (the \"↑ Load earlier messages\" button rendered by\n // the Renderer at the top of the scroll area). When it becomes visible,\n // fetch the next page of older messages.\n let loading = false\n\n const loadOlder = async () => {\n if (loading || !store.hasMoreHistory) return\n loading = true\n const oldest = store.messages()[0]\n if (!oldest) { loading = false; return }\n const page2 = await fetchPage(httpBase, conversationId as string, token, oldest.seq)\n if (page2) {\n store.apply({ type: 'history', conversationId, messages: page2.messages, hasMore: page2.hasMore })\n renderer.render(store)\n }\n loading = false\n }\n\n // Use IntersectionObserver to detect when the user scrolls to the top.\n // We observe the scroll container itself — when scrollTop < 40px, load more.\n const scrollEl = renderer.getScrollEl()\n if (!scrollEl) return\n\n // Wait 300ms before arming the scroll listener — the initial render scrolls\n // to the bottom, which briefly passes through scrollTop=0 and could trigger\n // a spurious load before the user actually scrolls up.\n let armed = false\n setTimeout(() => { armed = true }, 300)\n\n const onScroll = () => {\n if (!armed) return\n if (scrollEl.scrollTop < 80 && store.hasMoreHistory && !loading) {\n void loadOlder()\n }\n }\n scrollEl.addEventListener('scroll', onScroll, { passive: true })\n renderer.setScrollCleanup(() => scrollEl.removeEventListener('scroll', onScroll))\n}\n","/** Persistent anonymous identity, reused across reloads.\n *\n * Persists through BOTH localStorage and a first-party cookie. On a top-level\n * page (e.g. a standalone hosted widget at relay.example.com) the cookie\n * survives even if localStorage is unavailable or cleared, so a guest keeps\n * the same id across refreshes — which is what lets the server resolve their\n * existing conversation and load history. (In a cross-origin iframe both may\n * be partitioned; pass an explicit `userId`/`token` for that case.)\n */\nconst KEY = 'oc_uid'\n\nfunction readCookie(name: string): string | null {\n try {\n const m = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`))\n return m ? decodeURIComponent(m[1]!) : null\n } catch { return null }\n}\n\nfunction writeCookie(name: string, value: string): void {\n try {\n const maxAge = 60 * 60 * 24 * 365 // 1 year\n const secure = location.protocol === 'https:' ? '; Secure' : ''\n document.cookie = `${name}=${encodeURIComponent(value)}; Max-Age=${maxAge}; Path=/; SameSite=Lax${secure}`\n } catch { /* cookies disabled — best effort */ }\n}\n\nfunction newId(): string {\n return `g_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`\n}\n\nexport function persistentUid(): string {\n let existing: string | null = null\n try { existing = localStorage.getItem(KEY) } catch { /* unavailable */ }\n if (!existing) existing = readCookie(KEY)\n\n const id = existing ?? newId()\n\n // Write through to both stores so whichever is available carries it forward.\n try { localStorage.setItem(KEY, id) } catch { /* unavailable */ }\n writeCookie(KEY, id)\n\n return id\n}\n"],"names":["resolveRelayUrls","input","apiBaseOverride","_a","trimmed","scheme","secure","authorityAndPath","httpBase","httpBaseFromWsUrl","wsUrl","historyUrl","conversationId","beforeSeq","limit","qs","fetchPage","token","url","res","data","restoreHistory","store","renderer","apiBase","page","loading","loadOlder","oldest","page2","scrollEl","armed","onScroll","KEY","readCookie","name","m","writeCookie","value","newId","persistentUid","existing","id"],"mappings":"AA8BO,SAASA,EAAiBC,GAAeC,GAA+D;AAhB/G,MAAAC;AAiBE,QAAMC,IAAUH,EAAM,KAAA,EAAO,QAAQ,QAAQ,EAAE,GACzCI,KAASF,IAAAC,EAAQ,MAAM,2BAA2B,MAAzC,gBAAAD,EAA6C,IACtDG,IAASD,IAASA,MAAW,WAAWA,MAAW,QAAQ,IAC3DE,KAAoBF,IAASD,EAAQ,MAAMC,EAAO,SAAS,CAAC,IAAID,GAAS,QAAQ,SAAS,EAAE,GAC5FI,IAAWN,IACbA,EAAgB,KAAA,EAAO,QAAQ,QAAQ,EAAE,IACzC,GAAGI,IAAS,UAAU,MAAM,MAAMC,CAAgB;AAEtD,SAAO,EAAE,OADK,GAAGD,IAAS,QAAQ,IAAI,MAAMC,CAAgB,OAC5C,UAAAC,EAAA;AAClB;AAIO,SAASC,EAAkBC,GAAuB;AACvD,SAAOV,EAAiBU,CAAK,EAAE;AACjC;AAIA,SAASC,EAAWH,GAAkBI,GAAwBC,GAAmBC,GAAyB;AACxG,QAAMC,IAAK,aAAaF,CAAS,UAAUC,CAAK;AAChD,SAAO;AAAA,IACL,GAAGN,CAAQ,kBAAkBI,CAAc,aAAaG,CAAE;AAAA,IAC1D,GAAGP,CAAQ,kBAAkBI,CAAc,YAAYG,CAAE;AAAA,EAAA;AAE7D;AAGA,eAAeC,EACbR,GACAI,GACAK,GACAJ,GACAC,IAAQ,IACmD;AAC3D,aAAWI,KAAOP,EAAWH,GAAUI,GAAgBC,GAAWC,CAAK;AACrE,QAAI;AACF,YAAMK,IAAM,MAAM,MAAMD,GAAK,EAAE,SAAS,EAAE,eAAe,UAAUD,CAAK,GAAA,EAAG,CAAG;AAC9E,UAAI,CAACE,EAAI,GAAI;AACb,YAAMC,IAAO,MAAMD,EAAI,KAAA;AACvB,aAAO,EAAE,UAAUC,EAAK,YAAY,CAAA,GAAI,SAASA,EAAK,WAAW,GAAA;AAAA,IACnE,QAAQ;AAAA,IAAiB;AAE3B,SAAO;AACT;AAUA,eAAsBC,EACpBX,GACAO,GACAL,GACAU,GACAC,GACAC,GACe;AACf,QAAMhB,IAAWgB,IAAUA,EAAQ,QAAQ,QAAQ,EAAE,IAAIf,EAAkBC,CAAK,GAE1Ee,IAAO,MAAMT,EAAUR,GAAUI,GAA0BK,GAAO,OAAO,gBAAgB;AAa/F,MAZI,CAACQ,MAEDA,EAAK,SAAS,UAChBH,EAAM,MAAM,EAAE,MAAM,QAAQ,gBAAAV,GAAgB,UAAUa,EAAK,UAAU,GAErEH,EAAM,MAAM,EAAE,MAAM,WAAW,gBAAAV,GAAgB,UAAU,IAAI,SAASa,EAAK,QAAA,CAAS,GACpFF,EAAS,OAAOD,CAAK,KAGrBA,EAAM,MAAM,EAAE,MAAM,WAAW,gBAAAV,GAAgB,UAAU,CAAA,GAAI,SAAS,IAAO,GAG3E,CAACa,EAAK,SAAS;AAMnB,MAAIC,IAAU;AAEd,QAAMC,IAAY,YAAY;AAC5B,QAAID,KAAW,CAACJ,EAAM,eAAgB;AACtC,IAAAI,IAAU;AACV,UAAME,IAASN,EAAM,SAAA,EAAW,CAAC;AACjC,QAAI,CAACM,GAAQ;AAAE,MAAAF,IAAU;AAAO;AAAA,IAAO;AACvC,UAAMG,IAAQ,MAAMb,EAAUR,GAAUI,GAA0BK,GAAOW,EAAO,GAAG;AACnF,IAAIC,MACFP,EAAM,MAAM,EAAE,MAAM,WAAW,gBAAAV,GAAgB,UAAUiB,EAAM,UAAU,SAASA,EAAM,QAAA,CAAS,GACjGN,EAAS,OAAOD,CAAK,IAEvBI,IAAU;AAAA,EACZ,GAIMI,IAAWP,EAAS,YAAA;AAC1B,MAAI,CAACO,EAAU;AAKf,MAAIC,IAAQ;AACZ,aAAW,MAAM;AAAE,IAAAA,IAAQ;AAAA,EAAK,GAAG,GAAG;AAEtC,QAAMC,IAAW,MAAM;AACrB,IAAKD,KACDD,EAAS,YAAY,MAAMR,EAAM,kBAAkB,CAACI,KACjDC,EAAA;AAAA,EAET;AACA,EAAAG,EAAS,iBAAiB,UAAUE,GAAU,EAAE,SAAS,IAAM,GAC/DT,EAAS,iBAAiB,MAAMO,EAAS,oBAAoB,UAAUE,CAAQ,CAAC;AAClF;AC3IA,MAAMC,IAAM;AAEZ,SAASC,EAAWC,GAA6B;AAC/C,MAAI;AACF,UAAMC,IAAI,SAAS,OAAO,MAAM,IAAI,OAAO,WAAWD,CAAI,UAAU,CAAC;AACrE,WAAOC,IAAI,mBAAmBA,EAAE,CAAC,CAAE,IAAI;AAAA,EACzC,QAAQ;AAAE,WAAO;AAAA,EAAK;AACxB;AAEA,SAASC,EAAYF,GAAcG,GAAqB;AACtD,MAAI;AAEF,UAAMhC,IAAS,SAAS,aAAa,WAAW,aAAa;AAC7D,aAAS,SAAS,GAAG6B,CAAI,IAAI,mBAAmBG,CAAK,CAAC,2CAA4ChC,CAAM;AAAA,EAC1G,QAAQ;AAAA,EAAuC;AACjD;AAEA,SAASiC,IAAgB;AACvB,SAAO,KAAK,KAAK,OAAA,EAAS,SAAS,EAAE,EAAE,MAAM,CAAC,CAAC,GAAG,KAAK,IAAA,EAAM,SAAS,EAAE,CAAC;AAC3E;AAEO,SAASC,IAAwB;AACtC,MAAIC,IAA0B;AAC9B,MAAI;AAAE,IAAAA,IAAW,aAAa,QAAQR,CAAG;AAAA,EAAE,QAAQ;AAAA,EAAoB;AACvE,EAAKQ,MAAUA,IAAWP,EAAWD,CAAG;AAExC,QAAMS,IAAKD,KAAYF,EAAA;AAGvB,MAAI;AAAE,iBAAa,QAAQN,GAAKS,CAAE;AAAA,EAAE,QAAQ;AAAA,EAAoB;AAChE,SAAAL,EAAYJ,GAAKS,CAAE,GAEZA;AACT;"}
|