@lorikeetai/react-native-sdk 1.1.0 → 2.0.0-alpha.1
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/LICENSE +21 -0
- package/README.md +144 -2
- package/dist/index.cjs +696 -171
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +93 -41
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +93 -41
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +666 -171
- package/dist/index.js.map +1 -1
- package/package.json +34 -27
package/dist/index.cjs
CHANGED
|
@@ -1,187 +1,712 @@
|
|
|
1
|
-
'
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
//#region \0rolldown/runtime.js
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
12
|
+
key = keys[i];
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except) {
|
|
14
|
+
__defProp(to, key, {
|
|
15
|
+
get: ((k) => from[k]).bind(null, key),
|
|
16
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return to;
|
|
22
|
+
};
|
|
23
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
|
|
24
|
+
value: mod,
|
|
25
|
+
enumerable: true
|
|
26
|
+
}) : target, mod));
|
|
2
27
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
28
|
+
//#endregion
|
|
29
|
+
let react = require("react");
|
|
30
|
+
let react_native = require("react-native");
|
|
31
|
+
let react_native_safe_area_context = require("react-native-safe-area-context");
|
|
32
|
+
let react_native_webview = require("react-native-webview");
|
|
33
|
+
let react_native_svg = require("react-native-svg");
|
|
34
|
+
react_native_svg = __toESM(react_native_svg, 1);
|
|
35
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
9
36
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
37
|
+
//#region src/bridge.ts
|
|
38
|
+
/**
|
|
39
|
+
* The public key, base URL and token are one unit, and a conversation belongs to
|
|
40
|
+
* the credentials that created it. Stamping an observation with them is what
|
|
41
|
+
* stops a remount resuming one environment's ticket against another's key.
|
|
42
|
+
*/
|
|
43
|
+
const conversationIdentity = ({ authToken, baseUrl, publicKey }) => `${publicKey} ${baseUrl !== null && baseUrl !== void 0 ? baseUrl : ""} ${authToken}`;
|
|
44
|
+
/**
|
|
45
|
+
* A remount has to resume the conversation it was in, or the recovery abandons
|
|
46
|
+
* the ticket it was meant to rescue. Only usable when the credentials still
|
|
47
|
+
* match the ones that produced it.
|
|
48
|
+
*/
|
|
49
|
+
const resumableConversation = (observed, identity, intendedTicketId) => {
|
|
50
|
+
if (observed === null || observed.identity !== identity || observed.intendedTicketId !== intendedTicketId || observed.ticketId === void 0) return null;
|
|
51
|
+
return {
|
|
52
|
+
session: observed.session,
|
|
53
|
+
ticketId: observed.ticketId
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
//#endregion
|
|
58
|
+
//#region src/encode.ts
|
|
59
|
+
const BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
60
|
+
/**
|
|
61
|
+
* Base64 of a string's UTF-8 bytes.
|
|
62
|
+
*
|
|
63
|
+
* The chat page decodes this as UTF-8 (`Buffer.from(value, 'base64')`), so
|
|
64
|
+
* UTF-8 is the contract. The global `btoa` cannot meet it: it accepts Latin-1
|
|
65
|
+
* only, so a typographic apostrophe or an emoji in a title throws
|
|
66
|
+
* "Found invalid character when converting to base64" and takes the render with
|
|
67
|
+
* it. Hermes also had no `btoa` at all before React Native 0.74, which is
|
|
68
|
+
* inside this package's supported peer range.
|
|
69
|
+
*/
|
|
70
|
+
function base64EncodeUtf8(value) {
|
|
71
|
+
const bytes = encodeURIComponent(value).replace(/%([0-9A-F]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
|
|
72
|
+
let encoded = "";
|
|
73
|
+
for (let index = 0; index < bytes.length; index += 3) {
|
|
74
|
+
const first = bytes.charCodeAt(index);
|
|
75
|
+
const second = index + 1 < bytes.length ? bytes.charCodeAt(index + 1) : void 0;
|
|
76
|
+
const third = index + 2 < bytes.length ? bytes.charCodeAt(index + 2) : void 0;
|
|
77
|
+
encoded += BASE64_ALPHABET[first >> 2];
|
|
78
|
+
encoded += BASE64_ALPHABET[(first & 3) << 4 | (second !== null && second !== void 0 ? second : 0) >> 4];
|
|
79
|
+
encoded += second === void 0 ? "=" : BASE64_ALPHABET[(second & 15) << 2 | (third !== null && third !== void 0 ? third : 0) >> 6];
|
|
80
|
+
encoded += third === void 0 ? "=" : BASE64_ALPHABET[third & 63];
|
|
81
|
+
}
|
|
82
|
+
return encoded;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
//#endregion
|
|
86
|
+
//#region \0@oxc-project+runtime@0.147.0/helpers/esm/typeof.js
|
|
87
|
+
function _typeof(o) {
|
|
88
|
+
"@babel/helpers - typeof";
|
|
89
|
+
return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(o) {
|
|
90
|
+
return typeof o;
|
|
91
|
+
} : function(o) {
|
|
92
|
+
return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
|
|
93
|
+
}, _typeof(o);
|
|
94
|
+
}
|
|
13
95
|
|
|
14
|
-
|
|
96
|
+
//#endregion
|
|
97
|
+
//#region \0@oxc-project+runtime@0.147.0/helpers/esm/toPrimitive.js
|
|
98
|
+
function toPrimitive(t, r) {
|
|
99
|
+
if ("object" != _typeof(t) || !t) return t;
|
|
100
|
+
var e = t[Symbol.toPrimitive];
|
|
101
|
+
if (void 0 !== e) {
|
|
102
|
+
var i = e.call(t, r || "default");
|
|
103
|
+
if ("object" != _typeof(i)) return i;
|
|
104
|
+
throw new TypeError("@@toPrimitive must return a primitive value.");
|
|
105
|
+
}
|
|
106
|
+
return ("string" === r ? String : Number)(t);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
//#endregion
|
|
110
|
+
//#region \0@oxc-project+runtime@0.147.0/helpers/esm/toPropertyKey.js
|
|
111
|
+
function toPropertyKey(t) {
|
|
112
|
+
var i = toPrimitive(t, "string");
|
|
113
|
+
return "symbol" == _typeof(i) ? i : i + "";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
//#endregion
|
|
117
|
+
//#region \0@oxc-project+runtime@0.147.0/helpers/esm/defineProperty.js
|
|
118
|
+
function _defineProperty(e, r, t) {
|
|
119
|
+
return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
|
|
120
|
+
value: t,
|
|
121
|
+
enumerable: !0,
|
|
122
|
+
configurable: !0,
|
|
123
|
+
writable: !0
|
|
124
|
+
}) : e[r] = t, e;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
//#endregion
|
|
128
|
+
//#region src/client.ts
|
|
15
129
|
var LorikeetClient = class {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
url.searchParams.append(
|
|
39
|
-
"customisations",
|
|
40
|
-
btoa(JSON.stringify(this.customisations))
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
return url.toString();
|
|
44
|
-
});
|
|
45
|
-
if (api_base) {
|
|
46
|
-
this.baseUrl = api_base;
|
|
47
|
-
}
|
|
48
|
-
this.publicKey = public_key;
|
|
49
|
-
if (customisations) {
|
|
50
|
-
this.customisations = customisations;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
130
|
+
constructor({ customisations, api_base, metadata, public_key }) {
|
|
131
|
+
_defineProperty(this, "baseUrl", "https://chat.lorikeetcx.ai");
|
|
132
|
+
_defineProperty(this, "customisations", null);
|
|
133
|
+
_defineProperty(this, "metadata", null);
|
|
134
|
+
_defineProperty(this, "publicKey", void 0);
|
|
135
|
+
_defineProperty(this, "getWebviewUrl", ({ customer_email, customer_name, token, ticket_id, session }) => {
|
|
136
|
+
const url = new URL(this.baseUrl);
|
|
137
|
+
url.pathname = ticket_id ? `/tickets/${this.publicKey}/${ticket_id}` : "/widget";
|
|
138
|
+
url.searchParams.append("public_key", this.publicKey);
|
|
139
|
+
if (customer_email) url.searchParams.append("customer_email", customer_email);
|
|
140
|
+
if (customer_name) url.searchParams.append("customer_name", customer_name);
|
|
141
|
+
url.searchParams.append("token", token);
|
|
142
|
+
if (ticket_id && session) url.searchParams.append("session", session);
|
|
143
|
+
if (this.customisations) url.searchParams.append("customisations", base64EncodeUtf8(JSON.stringify(this.customisations)));
|
|
144
|
+
if (this.metadata) url.searchParams.append("metadata", JSON.stringify(this.metadata));
|
|
145
|
+
return url.toString();
|
|
146
|
+
});
|
|
147
|
+
if (api_base) this.baseUrl = api_base;
|
|
148
|
+
this.publicKey = public_key;
|
|
149
|
+
if (customisations) this.customisations = customisations;
|
|
150
|
+
if (metadata) this.metadata = metadata;
|
|
151
|
+
}
|
|
53
152
|
};
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
153
|
+
|
|
154
|
+
//#endregion
|
|
155
|
+
//#region src/errors.ts
|
|
156
|
+
/**
|
|
157
|
+
* Codes the chat page posts to the host as `{ type: 'error', error: CODE }`.
|
|
158
|
+
* Mirrored here because the page publishes them nowhere else, so a host would
|
|
159
|
+
* otherwise be matching on strings it discovered by observation.
|
|
160
|
+
*/
|
|
161
|
+
const LORIKEET_ERROR_CODES = [
|
|
162
|
+
"MISSING_TOKEN",
|
|
163
|
+
"CORRUPT_TOKEN_DATA",
|
|
164
|
+
"DECRYPT_FAILED",
|
|
165
|
+
"EXPIRED_TOKEN",
|
|
166
|
+
"MALFORMED_TOKEN",
|
|
167
|
+
"MISSING_PUBLIC_KEY",
|
|
168
|
+
"UNKNOWN_PUBLIC_KEY",
|
|
169
|
+
"CUSTOMER_ID_CONFLICT",
|
|
170
|
+
"CUSTOMER_ID_MISSING",
|
|
171
|
+
"FATAL_ERROR",
|
|
172
|
+
"INVALID_SESSION",
|
|
173
|
+
"DOMAIN_NOT_ALLOWED",
|
|
174
|
+
"TICKET_NOT_FOUND"
|
|
175
|
+
];
|
|
176
|
+
/**
|
|
177
|
+
* The codes that mean the stored conversation cannot be reopened, as opposed to
|
|
178
|
+
* the trusted token being bad or the integration being misconfigured. Only
|
|
179
|
+
* these are worth abandoning a resume for: a token error fails again with the
|
|
180
|
+
* same token, and a configuration error fails again regardless.
|
|
181
|
+
*/
|
|
182
|
+
const UNUSABLE_CONVERSATION_CODES = [
|
|
183
|
+
"CUSTOMER_ID_MISSING",
|
|
184
|
+
"INVALID_SESSION",
|
|
185
|
+
"TICKET_NOT_FOUND"
|
|
186
|
+
];
|
|
187
|
+
/** Whether an error code means the stored conversation cannot be reopened. */
|
|
188
|
+
function isUnusableConversationCode(code) {
|
|
189
|
+
return UNUSABLE_CONVERSATION_CODES.includes(code);
|
|
190
|
+
}
|
|
191
|
+
function isUnusableConversation(message) {
|
|
192
|
+
if (typeof message !== "object" || message === null) return false;
|
|
193
|
+
const { type, error } = message;
|
|
194
|
+
return type === "error" && typeof error === "string" ? isUnusableConversationCode(error) : false;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
//#endregion
|
|
198
|
+
//#region src/events.ts
|
|
199
|
+
const asRecord = (message) => typeof message === "object" && message !== null && !Array.isArray(message) ? message : null;
|
|
200
|
+
const asNonEmptyString = (value) => typeof value === "string" && value !== "" ? value : null;
|
|
201
|
+
/**
|
|
202
|
+
* The event a bridge message carries, or `null` for one that carries none.
|
|
203
|
+
*
|
|
204
|
+
* `ticket_created` is not derived here: telling a fresh ticket from a resumed
|
|
205
|
+
* one needs the URL the page was loaded with, which only the component holds.
|
|
206
|
+
*/
|
|
207
|
+
const eventFromMessage = (message) => {
|
|
208
|
+
const record = asRecord(message);
|
|
209
|
+
if (record === null || typeof record.type !== "string") return null;
|
|
210
|
+
switch (record.type) {
|
|
211
|
+
case "ready": return { type: "ready" };
|
|
212
|
+
case "customerMessage": return { type: "customer_message" };
|
|
213
|
+
case "agentMessage": return { type: "agent_message" };
|
|
214
|
+
case "closeChat": {
|
|
215
|
+
const reason = asNonEmptyString(record.reason);
|
|
216
|
+
return reason === "ended_by_customer" || reason === "minimised" ? {
|
|
217
|
+
type: "close",
|
|
218
|
+
reason
|
|
219
|
+
} : { type: "close" };
|
|
220
|
+
}
|
|
221
|
+
case "conversationEnded": return { type: "conversation_ended" };
|
|
222
|
+
case "startNewChat": return { type: "new_chat" };
|
|
223
|
+
case "persistLorikeetTicketId": {
|
|
224
|
+
const ticketId = asNonEmptyString(record.ticketId);
|
|
225
|
+
return ticketId === null ? null : {
|
|
226
|
+
type: "persist_ticket_id",
|
|
227
|
+
ticketId
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
case "persistLorikeetSession": {
|
|
231
|
+
const session = asNonEmptyString(record.session);
|
|
232
|
+
return session === null ? null : {
|
|
233
|
+
type: "persist_session",
|
|
234
|
+
session
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
case "error": {
|
|
238
|
+
const error = asNonEmptyString(record.error);
|
|
239
|
+
return error === null ? null : {
|
|
240
|
+
type: "error",
|
|
241
|
+
error
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
case "preloadComplete": return {
|
|
245
|
+
type: "preload_complete",
|
|
246
|
+
clientStart: record.clientStart === true,
|
|
247
|
+
lazyTicketCreationEnabled: record.lazyTicketCreationEnabled === true
|
|
248
|
+
};
|
|
249
|
+
default: return null;
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
/**
|
|
253
|
+
* The ticket a URL is trying to resume, or `null` for one that creates its own.
|
|
254
|
+
*
|
|
255
|
+
* Read from the URL rather than from any state, so it holds for a host that
|
|
256
|
+
* built the URL itself. An unreadable URL reports nothing, which at worst
|
|
257
|
+
* announces a resumed ticket as created once.
|
|
258
|
+
*/
|
|
259
|
+
const resumedTicketIdFromUri = (uri) => {
|
|
260
|
+
var _segments$;
|
|
261
|
+
let pathname;
|
|
262
|
+
try {
|
|
263
|
+
pathname = new URL(uri).pathname;
|
|
264
|
+
} catch {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
const segments = pathname.split("/").filter((part) => part !== "");
|
|
268
|
+
if (segments.length < 3 || segments[0] !== "tickets") return null;
|
|
269
|
+
return (_segments$ = segments[2]) !== null && _segments$ !== void 0 ? _segments$ : null;
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
//#endregion
|
|
273
|
+
//#region src/lifecycle.ts
|
|
274
|
+
/**
|
|
275
|
+
* The page gives its message stream ten attempts with exponential backoff
|
|
276
|
+
* capped at 30s (`message-stream-utils.tsx`), so the budget runs out roughly
|
|
277
|
+
* 151s after the stream drops, or ~196s once jitter is counted. It then sits at
|
|
278
|
+
* `failed` with nothing to reset it, so a customer who comes back to a
|
|
279
|
+
* backgrounded app is looking at a stream that will never reconnect.
|
|
280
|
+
*
|
|
281
|
+
* Three minutes sits inside that band. Shorter would remount while the page's
|
|
282
|
+
* own retries could still win, which costs the customer their draft message for
|
|
283
|
+
* nothing; much longer leaves a dead stream on screen.
|
|
284
|
+
*/
|
|
285
|
+
const BACKGROUND_RELOAD_THRESHOLD_MS = 18e4;
|
|
286
|
+
/**
|
|
287
|
+
* Elapsed time is the only signal available: the bridge reports `ready`,
|
|
288
|
+
* `error` and the persistence messages, but never stream health, so the SDK
|
|
289
|
+
* cannot ask whether the connection actually survived.
|
|
290
|
+
*/
|
|
291
|
+
const shouldReloadAfterBackground = (backgroundedAt, now, thresholdMs = BACKGROUND_RELOAD_THRESHOLD_MS) => {
|
|
292
|
+
if (backgroundedAt === null) return false;
|
|
293
|
+
const elapsed = now - backgroundedAt;
|
|
294
|
+
if (elapsed < 0) return false;
|
|
295
|
+
return elapsed >= thresholdMs;
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
//#endregion
|
|
299
|
+
//#region src/links.ts
|
|
300
|
+
/**
|
|
301
|
+
* Schemes the WebView resolves for itself. None of them is a page the customer
|
|
302
|
+
* asked to leave the app for, and the OS cannot open them anyway.
|
|
303
|
+
*/
|
|
304
|
+
const WEBVIEW_OWNED_SCHEMES = /* @__PURE__ */ new Set([
|
|
305
|
+
"about:",
|
|
306
|
+
"blob:",
|
|
307
|
+
"data:",
|
|
308
|
+
"javascript:"
|
|
309
|
+
]);
|
|
310
|
+
const parseUrl = (value) => {
|
|
311
|
+
try {
|
|
312
|
+
return new URL(value);
|
|
313
|
+
} catch {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
/**
|
|
318
|
+
* Whether the OS could be asked to open this at all. `window.open()` with no
|
|
319
|
+
* argument gives `about:blank`, and a scheme the WebView resolves for itself is
|
|
320
|
+
* not something the OS can place.
|
|
321
|
+
*/
|
|
322
|
+
const canOpenOutsideApp = (url) => {
|
|
323
|
+
const target = parseUrl(url);
|
|
324
|
+
return target !== null && !WEBVIEW_OWNED_SCHEMES.has(target.protocol);
|
|
325
|
+
};
|
|
326
|
+
/**
|
|
327
|
+
* Whether a navigation belongs inside the widget.
|
|
328
|
+
*
|
|
329
|
+
* An origin comparison rather than the substring match this replaces. `baseUrl`
|
|
330
|
+
* appearing anywhere in a URL counted as internal, so
|
|
331
|
+
* `https://evil.example/?next=https://chat.lorikeetcx.ai` loaded in place of the
|
|
332
|
+
* conversation instead of opening in the browser. Comparing origins rather than
|
|
333
|
+
* matching a prefix also means a port, a trailing slash and a different path all
|
|
334
|
+
* still resolve to the same widget.
|
|
335
|
+
*/
|
|
336
|
+
const isInternalNavigation = (url, baseUrl) => {
|
|
337
|
+
const base = parseUrl(baseUrl);
|
|
338
|
+
const target = parseUrl(url);
|
|
339
|
+
if (base === null || target === null) return true;
|
|
340
|
+
if (WEBVIEW_OWNED_SCHEMES.has(target.protocol)) return true;
|
|
341
|
+
return target.origin === base.origin;
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
//#endregion
|
|
345
|
+
//#region src/message-circle-plus-icon.tsx
|
|
346
|
+
const MessageCirclePlusIcon = ({ size = 24, color = "currentColor", strokeWidth = 2 }) => {
|
|
347
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_native_svg.default, {
|
|
348
|
+
fill: "none",
|
|
349
|
+
height: size,
|
|
350
|
+
viewBox: "0 0 24 24",
|
|
351
|
+
width: size,
|
|
352
|
+
children: [
|
|
353
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native_svg.Path, {
|
|
354
|
+
d: "M7.9 20A9 9 0 1 0 4 16.1L2 22Z",
|
|
355
|
+
stroke: color,
|
|
356
|
+
strokeLinecap: "round",
|
|
357
|
+
strokeLinejoin: "round",
|
|
358
|
+
strokeWidth
|
|
359
|
+
}),
|
|
360
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native_svg.Line, {
|
|
361
|
+
stroke: color,
|
|
362
|
+
strokeLinecap: "round",
|
|
363
|
+
strokeWidth,
|
|
364
|
+
x1: "12",
|
|
365
|
+
x2: "12",
|
|
366
|
+
y1: "8",
|
|
367
|
+
y2: "16"
|
|
368
|
+
}),
|
|
369
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native_svg.Line, {
|
|
370
|
+
stroke: color,
|
|
371
|
+
strokeLinecap: "round",
|
|
372
|
+
strokeWidth,
|
|
373
|
+
x1: "8",
|
|
374
|
+
x2: "16",
|
|
375
|
+
y1: "12",
|
|
376
|
+
y2: "12"
|
|
377
|
+
})
|
|
378
|
+
]
|
|
379
|
+
});
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
//#endregion
|
|
383
|
+
//#region src/widget.tsx
|
|
384
|
+
const stylesheet = react_native.StyleSheet.create({
|
|
385
|
+
root: {
|
|
386
|
+
flex: 1,
|
|
387
|
+
backgroundColor: "#fff"
|
|
388
|
+
},
|
|
389
|
+
container: {
|
|
390
|
+
flex: 1,
|
|
391
|
+
borderRadius: 8,
|
|
392
|
+
overflow: "hidden",
|
|
393
|
+
shadowColor: "#000",
|
|
394
|
+
shadowOffset: {
|
|
395
|
+
width: 0,
|
|
396
|
+
height: 2
|
|
397
|
+
},
|
|
398
|
+
shadowOpacity: .1,
|
|
399
|
+
shadowRadius: 4,
|
|
400
|
+
elevation: 3
|
|
401
|
+
},
|
|
402
|
+
header: {
|
|
403
|
+
padding: 16,
|
|
404
|
+
display: "flex",
|
|
405
|
+
flexDirection: "row",
|
|
406
|
+
justifyContent: "space-between"
|
|
407
|
+
},
|
|
408
|
+
headerIcon: { width: 24 },
|
|
409
|
+
headerText: {
|
|
410
|
+
textAlign: "center",
|
|
411
|
+
fontSize: 16
|
|
412
|
+
},
|
|
413
|
+
webView: { flex: 1 },
|
|
414
|
+
closed: {
|
|
415
|
+
flex: 1,
|
|
416
|
+
opacity: 0
|
|
417
|
+
}
|
|
82
418
|
});
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
419
|
+
const LorikeetWidgetWebView = (0, react.forwardRef)(({ uri, baseUrl = "https://chat.lorikeetcx.ai", webviewProps = {}, onEvent }, ref) => {
|
|
420
|
+
const isInternal = (url) => isInternalNavigation(url, baseUrl);
|
|
421
|
+
const accounted = (0, react.useRef)(/* @__PURE__ */ new Set());
|
|
422
|
+
const seenUri = (0, react.useRef)(null);
|
|
423
|
+
if (seenUri.current !== uri) {
|
|
424
|
+
seenUri.current = uri;
|
|
425
|
+
const resumed = resumedTicketIdFromUri(uri);
|
|
426
|
+
if (resumed !== null) accounted.current.add(resumed);
|
|
427
|
+
}
|
|
428
|
+
const emit = (event) => {
|
|
429
|
+
onEvent === null || onEvent === void 0 || onEvent(event);
|
|
430
|
+
if (event.type !== "persist_ticket_id") return;
|
|
431
|
+
if (accounted.current.has(event.ticketId)) return;
|
|
432
|
+
accounted.current.add(event.ticketId);
|
|
433
|
+
onEvent === null || onEvent === void 0 || onEvent({
|
|
434
|
+
ticketId: event.ticketId,
|
|
435
|
+
type: "ticket_created"
|
|
436
|
+
});
|
|
437
|
+
};
|
|
438
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native_webview.WebView, {
|
|
439
|
+
style: stylesheet.webView,
|
|
440
|
+
...webviewProps,
|
|
441
|
+
onMessage: (event) => {
|
|
442
|
+
var _webviewProps$onMessa;
|
|
443
|
+
let message;
|
|
444
|
+
try {
|
|
445
|
+
message = JSON.parse(event.nativeEvent.data);
|
|
446
|
+
} catch {}
|
|
447
|
+
const widgetEvent = eventFromMessage(message);
|
|
448
|
+
if (widgetEvent !== null) emit(widgetEvent);
|
|
449
|
+
(_webviewProps$onMessa = webviewProps.onMessage) === null || _webviewProps$onMessa === void 0 || _webviewProps$onMessa.call(webviewProps, event);
|
|
450
|
+
},
|
|
451
|
+
onNavigationStateChange: (navState) => {
|
|
452
|
+
var _webviewProps$onNavig;
|
|
453
|
+
if (!isInternal(navState.url)) react_native.Linking.openURL(navState.url);
|
|
454
|
+
(_webviewProps$onNavig = webviewProps.onNavigationStateChange) === null || _webviewProps$onNavig === void 0 || _webviewProps$onNavig.call(webviewProps, navState);
|
|
455
|
+
},
|
|
456
|
+
onOpenWindow: (event) => {
|
|
457
|
+
var _webviewProps$onOpenW;
|
|
458
|
+
const { targetUrl } = event.nativeEvent;
|
|
459
|
+
if (canOpenOutsideApp(targetUrl)) react_native.Linking.openURL(targetUrl);
|
|
460
|
+
(_webviewProps$onOpenW = webviewProps.onOpenWindow) === null || _webviewProps$onOpenW === void 0 || _webviewProps$onOpenW.call(webviewProps, event);
|
|
461
|
+
},
|
|
462
|
+
onShouldStartLoadWithRequest: (request) => {
|
|
463
|
+
var _webviewProps$onShoul;
|
|
464
|
+
if (((_webviewProps$onShoul = webviewProps.onShouldStartLoadWithRequest) === null || _webviewProps$onShoul === void 0 ? void 0 : _webviewProps$onShoul.call(webviewProps, request)) === false) return false;
|
|
465
|
+
if (request.isTopFrame === false || isInternal(request.url)) return true;
|
|
466
|
+
react_native.Linking.openURL(request.url);
|
|
467
|
+
return false;
|
|
468
|
+
},
|
|
469
|
+
ref,
|
|
470
|
+
source: { uri }
|
|
471
|
+
});
|
|
103
472
|
});
|
|
104
473
|
LorikeetWidgetWebView.displayName = "LorikeetWidgetWebView";
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
474
|
+
const LorikeetWidgetHeader = ({ title, style, icon = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessageCirclePlusIcon, { size: 16 }), onIconPress }) => {
|
|
475
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_native.View, {
|
|
476
|
+
style: stylesheet.header,
|
|
477
|
+
children: [
|
|
478
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.View, { style: stylesheet.headerIcon }),
|
|
479
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.Text, {
|
|
480
|
+
style: [stylesheet.headerText, style],
|
|
481
|
+
children: title
|
|
482
|
+
}),
|
|
483
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.TouchableOpacity, {
|
|
484
|
+
onPress: onIconPress,
|
|
485
|
+
style: stylesheet.headerIcon,
|
|
486
|
+
children: icon
|
|
487
|
+
})
|
|
488
|
+
]
|
|
489
|
+
});
|
|
116
490
|
};
|
|
117
|
-
function LorikeetWidgetView({
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
491
|
+
function LorikeetWidgetView({ publicKey, baseUrl, authToken, customisations, styles, onEvent, onWebViewMessage, ticketId, session, metadata, webviewProps }) {
|
|
492
|
+
var _customisations$title;
|
|
493
|
+
const [lorikeetClient, setLorikeetClient] = (0, react.useState)(null);
|
|
494
|
+
const [loaded, setLoaded] = (0, react.useState)(null);
|
|
495
|
+
const [generation, setGeneration] = (0, react.useState)(0);
|
|
496
|
+
const generationRef = (0, react.useRef)(generation);
|
|
497
|
+
generationRef.current = generation;
|
|
498
|
+
const remount = (0, react.useCallback)(() => {
|
|
499
|
+
setGeneration((previous) => previous + 1);
|
|
500
|
+
}, []);
|
|
501
|
+
const webViewRef = (0, react.useRef)(null);
|
|
502
|
+
const [refusedTicketId, setRefusedTicketId] = (0, react.useState)(null);
|
|
503
|
+
const observedRef = (0, react.useRef)(null);
|
|
504
|
+
const backgroundedAtRef = (0, react.useRef)(null);
|
|
505
|
+
const uriRef = (0, react.useRef)(null);
|
|
506
|
+
const loadedForRef = (0, react.useRef)({
|
|
507
|
+
identity: "",
|
|
508
|
+
intendedTicketId: void 0
|
|
509
|
+
});
|
|
510
|
+
const identity = conversationIdentity({
|
|
511
|
+
authToken,
|
|
512
|
+
baseUrl,
|
|
513
|
+
publicKey
|
|
514
|
+
});
|
|
515
|
+
const resumingTicketId = ticketId !== void 0 && ticketId !== refusedTicketId ? ticketId : void 0;
|
|
516
|
+
const [attemptedTicketId, setAttemptedTicketId] = (0, react.useState)(void 0);
|
|
517
|
+
const [closed, setClosed] = (0, react.useState)(false);
|
|
518
|
+
const load = (0, react.useCallback)((client, forIdentity, intendedTicketId, resume, force = false) => {
|
|
519
|
+
const next = client.getWebviewUrl({
|
|
520
|
+
token: authToken,
|
|
521
|
+
ticket_id: resume === null || resume === void 0 ? void 0 : resume.ticketId,
|
|
522
|
+
session: resume === null || resume === void 0 ? void 0 : resume.session
|
|
523
|
+
});
|
|
524
|
+
const unchanged = uriRef.current === next;
|
|
525
|
+
if (loadedForRef.current.identity !== forIdentity || loadedForRef.current.intendedTicketId !== intendedTicketId) setClosed(false);
|
|
526
|
+
uriRef.current = next;
|
|
527
|
+
loadedForRef.current = {
|
|
528
|
+
identity: forIdentity,
|
|
529
|
+
intendedTicketId
|
|
530
|
+
};
|
|
531
|
+
if (unchanged && !force) {
|
|
532
|
+
setLoaded((previous) => previous === null || previous.identity !== forIdentity || previous.intendedTicketId !== intendedTicketId ? {
|
|
533
|
+
identity: forIdentity,
|
|
534
|
+
intendedTicketId,
|
|
535
|
+
uri: next
|
|
536
|
+
} : previous);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
setLoaded({
|
|
540
|
+
identity: forIdentity,
|
|
541
|
+
intendedTicketId,
|
|
542
|
+
uri: next
|
|
543
|
+
});
|
|
544
|
+
setAttemptedTicketId(resume === null || resume === void 0 ? void 0 : resume.ticketId);
|
|
545
|
+
remount();
|
|
546
|
+
}, [authToken, remount]);
|
|
547
|
+
(0, react.useEffect)(() => {
|
|
548
|
+
const lorikeetClient = new LorikeetClient({
|
|
549
|
+
public_key: publicKey,
|
|
550
|
+
api_base: baseUrl,
|
|
551
|
+
metadata,
|
|
552
|
+
customisations: {
|
|
553
|
+
...customisations,
|
|
554
|
+
hideHeader: (customisations === null || customisations === void 0 ? void 0 : customisations.hideHeader) === void 0 ? true : customisations.hideHeader
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
setLorikeetClient(lorikeetClient);
|
|
558
|
+
const resume = resumingTicketId !== void 0 ? {
|
|
559
|
+
session,
|
|
560
|
+
ticketId: resumingTicketId
|
|
561
|
+
} : resumableConversation(observedRef.current, identity, void 0);
|
|
562
|
+
load(lorikeetClient, identity, resumingTicketId, resume);
|
|
563
|
+
}, [
|
|
564
|
+
publicKey,
|
|
565
|
+
baseUrl,
|
|
566
|
+
authToken,
|
|
567
|
+
customisations,
|
|
568
|
+
metadata,
|
|
569
|
+
session,
|
|
570
|
+
resumingTicketId,
|
|
571
|
+
identity,
|
|
572
|
+
load
|
|
573
|
+
]);
|
|
574
|
+
const reload = (0, react.useCallback)(() => {
|
|
575
|
+
const resume = resumableConversation(observedRef.current, identity, resumingTicketId);
|
|
576
|
+
if (lorikeetClient !== null && resume !== null) {
|
|
577
|
+
load(lorikeetClient, identity, resumingTicketId, resume, true);
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
remount();
|
|
581
|
+
}, [
|
|
582
|
+
identity,
|
|
583
|
+
load,
|
|
584
|
+
lorikeetClient,
|
|
585
|
+
remount,
|
|
586
|
+
resumingTicketId
|
|
587
|
+
]);
|
|
588
|
+
(0, react.useEffect)(() => {
|
|
589
|
+
const subscription = react_native.AppState.addEventListener("change", (next) => {
|
|
590
|
+
if (next === "active") {
|
|
591
|
+
if (shouldReloadAfterBackground(backgroundedAtRef.current, Date.now())) reload();
|
|
592
|
+
backgroundedAtRef.current = null;
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
if (next === "background") {
|
|
596
|
+
var _backgroundedAtRef$cu;
|
|
597
|
+
(_backgroundedAtRef$cu = backgroundedAtRef.current) !== null && _backgroundedAtRef$cu !== void 0 || (backgroundedAtRef.current = Date.now());
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
return () => {
|
|
601
|
+
subscription.remove();
|
|
602
|
+
};
|
|
603
|
+
}, [reload]);
|
|
604
|
+
const startNewChat = (0, react.useCallback)(() => {
|
|
605
|
+
setClosed(false);
|
|
606
|
+
if (lorikeetClient !== null && resumingTicketId === void 0) {
|
|
607
|
+
observedRef.current = null;
|
|
608
|
+
load(lorikeetClient, identity, void 0, null, true);
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
remount();
|
|
612
|
+
}, [
|
|
613
|
+
identity,
|
|
614
|
+
load,
|
|
615
|
+
lorikeetClient,
|
|
616
|
+
remount,
|
|
617
|
+
resumingTicketId
|
|
618
|
+
]);
|
|
619
|
+
const handleEvent = (0, react.useCallback)((event) => {
|
|
620
|
+
if (generation === generationRef.current && loadedForRef.current.identity === identity && loadedForRef.current.intendedTicketId === resumingTicketId) {
|
|
621
|
+
if (event.type === "persist_ticket_id" || event.type === "persist_session") {
|
|
622
|
+
var _observedRef$current;
|
|
623
|
+
const current = ((_observedRef$current = observedRef.current) === null || _observedRef$current === void 0 ? void 0 : _observedRef$current.identity) === identity && observedRef.current.intendedTicketId === resumingTicketId ? observedRef.current : null;
|
|
624
|
+
observedRef.current = {
|
|
625
|
+
...current,
|
|
626
|
+
...event.type === "persist_ticket_id" ? { ticketId: event.ticketId } : { session: event.session },
|
|
627
|
+
identity,
|
|
628
|
+
intendedTicketId: resumingTicketId
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
if (event.type === "close") setClosed(true);
|
|
632
|
+
if (event.type === "new_chat") startNewChat();
|
|
633
|
+
if (event.type === "error" && attemptedTicketId !== void 0 && isUnusableConversationCode(event.error)) {
|
|
634
|
+
observedRef.current = null;
|
|
635
|
+
setRefusedTicketId(attemptedTicketId);
|
|
636
|
+
if (lorikeetClient !== null) load(lorikeetClient, identity, void 0, null, true);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
onEvent === null || onEvent === void 0 || onEvent(event);
|
|
640
|
+
}, [
|
|
641
|
+
attemptedTicketId,
|
|
642
|
+
generation,
|
|
643
|
+
identity,
|
|
644
|
+
load,
|
|
645
|
+
lorikeetClient,
|
|
646
|
+
onEvent,
|
|
647
|
+
resumingTicketId,
|
|
648
|
+
startNewChat
|
|
649
|
+
]);
|
|
650
|
+
const handleMessage = (0, react.useCallback)((event) => {
|
|
651
|
+
var _webviewProps$onMessa2;
|
|
652
|
+
onWebViewMessage === null || onWebViewMessage === void 0 || onWebViewMessage(event);
|
|
653
|
+
webviewProps === null || webviewProps === void 0 || (_webviewProps$onMessa2 = webviewProps.onMessage) === null || _webviewProps$onMessa2 === void 0 || _webviewProps$onMessa2.call(webviewProps, event);
|
|
654
|
+
}, [onWebViewMessage, webviewProps]);
|
|
655
|
+
if (lorikeetClient === null || loaded === null || loaded.identity !== identity || loaded.intendedTicketId !== resumingTicketId) return null;
|
|
656
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native_safe_area_context.SafeAreaView, {
|
|
657
|
+
edges: [
|
|
658
|
+
"top",
|
|
659
|
+
"left",
|
|
660
|
+
"right"
|
|
661
|
+
],
|
|
662
|
+
style: stylesheet.root,
|
|
663
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_native.View, {
|
|
664
|
+
style: stylesheet.container,
|
|
665
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LorikeetWidgetHeader, {
|
|
666
|
+
onIconPress: startNewChat,
|
|
667
|
+
style: styles === null || styles === void 0 ? void 0 : styles.header,
|
|
668
|
+
title: (_customisations$title = customisations === null || customisations === void 0 ? void 0 : customisations.title) !== null && _customisations$title !== void 0 ? _customisations$title : "Lorikeet AI Agent"
|
|
669
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.View, {
|
|
670
|
+
accessibilityElementsHidden: closed,
|
|
671
|
+
importantForAccessibility: closed ? "no-hide-descendants" : "auto",
|
|
672
|
+
pointerEvents: closed ? "none" : "auto",
|
|
673
|
+
style: closed ? stylesheet.closed : stylesheet.webView,
|
|
674
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LorikeetWidgetWebView, {
|
|
675
|
+
baseUrl: lorikeetClient.baseUrl,
|
|
676
|
+
onEvent: handleEvent,
|
|
677
|
+
ref: webViewRef,
|
|
678
|
+
uri: loaded.uri,
|
|
679
|
+
webviewProps: {
|
|
680
|
+
allowsInlineMediaPlayback: true,
|
|
681
|
+
mediaPlaybackRequiresUserAction: false,
|
|
682
|
+
scalesPageToFit: true,
|
|
683
|
+
startInLoadingState: true,
|
|
684
|
+
style: stylesheet.webView,
|
|
685
|
+
...webviewProps,
|
|
686
|
+
onMessage: handleMessage,
|
|
687
|
+
onRenderProcessGone: (event) => {
|
|
688
|
+
var _webviewProps$onRende;
|
|
689
|
+
if (generation === generationRef.current) reload();
|
|
690
|
+
webviewProps === null || webviewProps === void 0 || (_webviewProps$onRende = webviewProps.onRenderProcessGone) === null || _webviewProps$onRende === void 0 || _webviewProps$onRende.call(webviewProps, event);
|
|
691
|
+
},
|
|
692
|
+
onContentProcessDidTerminate: (event) => {
|
|
693
|
+
var _webviewProps$onConte;
|
|
694
|
+
if (generation === generationRef.current) reload();
|
|
695
|
+
webviewProps === null || webviewProps === void 0 || (_webviewProps$onConte = webviewProps.onContentProcessDidTerminate) === null || _webviewProps$onConte === void 0 || _webviewProps$onConte.call(webviewProps, event);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}, `${identity}|${resumingTicketId !== null && resumingTicketId !== void 0 ? resumingTicketId : ""}|${generation}`)
|
|
699
|
+
})]
|
|
700
|
+
})
|
|
701
|
+
});
|
|
180
702
|
}
|
|
181
703
|
|
|
704
|
+
//#endregion
|
|
705
|
+
exports.LORIKEET_ERROR_CODES = LORIKEET_ERROR_CODES;
|
|
182
706
|
exports.LorikeetClient = LorikeetClient;
|
|
183
707
|
exports.LorikeetWidgetHeader = LorikeetWidgetHeader;
|
|
184
708
|
exports.LorikeetWidgetView = LorikeetWidgetView;
|
|
185
709
|
exports.LorikeetWidgetWebView = LorikeetWidgetWebView;
|
|
186
|
-
|
|
710
|
+
exports.isUnusableConversation = isUnusableConversation;
|
|
711
|
+
exports.isUnusableConversationCode = isUnusableConversationCode;
|
|
187
712
|
//# sourceMappingURL=index.cjs.map
|