universal-chatbot-saas 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +22 -0
- package/README.md +93 -0
- package/package.json +28 -0
- package/public/chatbot.js +987 -0
- package/public/portal/admin-bots.html +142 -0
- package/public/portal/admin-bots.js +265 -0
- package/public/portal/admin-users.html +149 -0
- package/public/portal/admin-users.js +286 -0
- package/public/portal/login.html +65 -0
- package/public/portal/portal.js +266 -0
- package/react-wrapper/LICENSE +21 -0
- package/react-wrapper/README.md +48 -0
- package/react-wrapper/package.json +37 -0
- package/react-wrapper/src/index.tsx +137 -0
- package/react-wrapper/tsconfig.json +18 -0
- package/server/index.js +2331 -0
|
@@ -0,0 +1,987 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
if (window.UniversalChatbotWidget) {
|
|
3
|
+
return;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function safeJsonParse(value, fallback) {
|
|
7
|
+
try {
|
|
8
|
+
return JSON.parse(value);
|
|
9
|
+
} catch {
|
|
10
|
+
return fallback;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function toPositiveInteger(value, fallback) {
|
|
15
|
+
const parsed = Number(value);
|
|
16
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
17
|
+
return fallback;
|
|
18
|
+
}
|
|
19
|
+
return Math.floor(parsed);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function escapeHtml(value) {
|
|
23
|
+
return String(value)
|
|
24
|
+
.replace(/&/g, "&")
|
|
25
|
+
.replace(/</g, "<")
|
|
26
|
+
.replace(/>/g, ">")
|
|
27
|
+
.replace(/\"/g, """)
|
|
28
|
+
.replace(/'/g, "'");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function escapeAttribute(value) {
|
|
32
|
+
return escapeHtml(String(value).replace(/`/g, ""));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isSafeUrl(url) {
|
|
36
|
+
try {
|
|
37
|
+
const parsed = new URL(url, location.href);
|
|
38
|
+
return ["http:", "https:", "mailto:", "tel:"].includes(parsed.protocol);
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function markdownToSafeHtml(text) {
|
|
45
|
+
const source = String(text || "");
|
|
46
|
+
const placeholders = [];
|
|
47
|
+
|
|
48
|
+
const withPlaceholders = source.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (full, label, url) => {
|
|
49
|
+
const safe = isSafeUrl(url);
|
|
50
|
+
const html = safe
|
|
51
|
+
? `<a href="${escapeAttribute(url)}" target="_blank" rel="noopener noreferrer nofollow">${escapeHtml(label)}</a>`
|
|
52
|
+
: escapeHtml(full);
|
|
53
|
+
const marker = `__UCB_LINK_${placeholders.length}__`;
|
|
54
|
+
placeholders.push(html);
|
|
55
|
+
return marker;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
let html = escapeHtml(withPlaceholders);
|
|
59
|
+
html = html.replace(/`([^`]+)`/g, "<code>$1</code>");
|
|
60
|
+
html = html.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
|
61
|
+
html = html.replace(/(^|\s)\*([^*]+)\*(?=\s|$)/g, "$1<em>$2</em>");
|
|
62
|
+
html = html.replace(/\n/g, "<br>");
|
|
63
|
+
|
|
64
|
+
for (let i = 0; i < placeholders.length; i += 1) {
|
|
65
|
+
html = html.replace(`__UCB_LINK_${i}__`, placeholders[i]);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return html;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function toSiteSlug(value) {
|
|
72
|
+
return String(value || "")
|
|
73
|
+
.trim()
|
|
74
|
+
.toLowerCase()
|
|
75
|
+
.replace(/https?:\/\//g, "")
|
|
76
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
77
|
+
.replace(/^-+|-+$/g, "")
|
|
78
|
+
.slice(0, 80);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function deriveSiteIdentity(dataset) {
|
|
82
|
+
const explicitSiteId = String(dataset.siteId || dataset.siteKey || "").trim();
|
|
83
|
+
const siteName = String(dataset.siteName || "").trim();
|
|
84
|
+
const siteUrl = String(dataset.siteUrl || location.origin || "").trim();
|
|
85
|
+
|
|
86
|
+
if (explicitSiteId) {
|
|
87
|
+
return {
|
|
88
|
+
siteId: explicitSiteId,
|
|
89
|
+
siteKey: String(dataset.siteKey || explicitSiteId).trim(),
|
|
90
|
+
siteName,
|
|
91
|
+
siteUrl
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let derivedId = "";
|
|
96
|
+
try {
|
|
97
|
+
const parsed = new URL(siteUrl, location.href);
|
|
98
|
+
const hostSlug = toSiteSlug(parsed.hostname || "");
|
|
99
|
+
const pathSlug = toSiteSlug(parsed.pathname || "");
|
|
100
|
+
derivedId = pathSlug ? `${hostSlug}-${pathSlug}` : hostSlug;
|
|
101
|
+
} catch {
|
|
102
|
+
derivedId = toSiteSlug(siteName || siteUrl || location.host || "default-site");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const fallbackId = derivedId || toSiteSlug(siteName || "default-site") || "default-site";
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
siteId: fallbackId,
|
|
109
|
+
siteKey: fallbackId,
|
|
110
|
+
siteName,
|
|
111
|
+
siteUrl
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function normalizePosition(rawPosition) {
|
|
116
|
+
const value = String(rawPosition || "").trim().toLowerCase();
|
|
117
|
+
if (value === "left" || value === "bottom-left") {
|
|
118
|
+
return "left";
|
|
119
|
+
}
|
|
120
|
+
return "right";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function createWidgetSessionId() {
|
|
124
|
+
if (window.crypto && typeof window.crypto.randomUUID === "function") {
|
|
125
|
+
return `sess_${window.crypto.randomUUID()}`;
|
|
126
|
+
}
|
|
127
|
+
return `sess_${Date.now()}_${Math.random().toString(36).slice(2, 12)}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function getOrCreateSessionId(storageKey, explicitSessionId) {
|
|
131
|
+
const direct = String(explicitSessionId || "").trim();
|
|
132
|
+
if (direct) {
|
|
133
|
+
return direct;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const sessionStorageKey = `${storageKey}_session_id`;
|
|
137
|
+
try {
|
|
138
|
+
const existing = String(localStorage.getItem(sessionStorageKey) || "").trim();
|
|
139
|
+
if (existing) {
|
|
140
|
+
return existing;
|
|
141
|
+
}
|
|
142
|
+
const next = createWidgetSessionId();
|
|
143
|
+
localStorage.setItem(sessionStorageKey, next);
|
|
144
|
+
return next;
|
|
145
|
+
} catch {
|
|
146
|
+
return createWidgetSessionId();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const scriptEl = document.currentScript;
|
|
151
|
+
const dataset = (scriptEl && scriptEl.dataset) || {};
|
|
152
|
+
const derivedSite = deriveSiteIdentity(dataset);
|
|
153
|
+
const requestedAiModel = (dataset.aimodel || "").trim();
|
|
154
|
+
const normalizedAiModel = requestedAiModel.toLowerCase();
|
|
155
|
+
const providerFromAiModel = ["claude", "openai", "groq", "mistral"].includes(normalizedAiModel)
|
|
156
|
+
? normalizedAiModel
|
|
157
|
+
: "";
|
|
158
|
+
|
|
159
|
+
const config = {
|
|
160
|
+
botName: dataset.botName || "Aria",
|
|
161
|
+
color: dataset.color || "#0ea5a4",
|
|
162
|
+
welcome: dataset.welcome || "Hi. How can I help today?",
|
|
163
|
+
apiBase: dataset.apiBase || "http://localhost:8787",
|
|
164
|
+
placeholder: dataset.placeholder || "Type your question...",
|
|
165
|
+
systemPrompt: dataset.systemPrompt || "You are a helpful support agent.",
|
|
166
|
+
knowledge: (() => {
|
|
167
|
+
const rawKnowledge = String(dataset.knowledgeJson || dataset.knowledge || "").trim();
|
|
168
|
+
if (!rawKnowledge) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
return safeJsonParse(rawKnowledge, rawKnowledge);
|
|
172
|
+
})(),
|
|
173
|
+
knowledgeUrl: String(dataset.knowledgeUrl || "").trim(),
|
|
174
|
+
knowledgeClientFetch: String(dataset.knowledgeClientFetch || "false").toLowerCase() === "true",
|
|
175
|
+
knowledgeQuery: safeJsonParse(dataset.knowledgeQueryJson || dataset.knowledgeQuery || "{}", {}),
|
|
176
|
+
includeKnowledgeContext: String(dataset.knowledgeUrlIncludeContext || "true").toLowerCase() !== "false",
|
|
177
|
+
knowledgeSyncMs: Math.max(
|
|
178
|
+
toPositiveInteger(dataset.knowledgeSyncMs || dataset.knowledgeRefreshMs || "60000", 60000),
|
|
179
|
+
5000
|
|
180
|
+
),
|
|
181
|
+
knowledgeSyncOnLoad: String(dataset.knowledgeSyncOnLoad || "true").toLowerCase() !== "false",
|
|
182
|
+
model: dataset.model || (providerFromAiModel ? "" : requestedAiModel),
|
|
183
|
+
provider: dataset.provider || providerFromAiModel || "claude",
|
|
184
|
+
authToken: dataset.authToken || dataset.aiApikey || dataset.aiApiKey || dataset.apikey || "",
|
|
185
|
+
botId: dataset.botId || dataset.bot_id || "",
|
|
186
|
+
botApiKey: dataset.botApiKey || dataset.bot_api_key || "",
|
|
187
|
+
tenantId: dataset.tenantId || dataset.tenant || "",
|
|
188
|
+
userId: dataset.userId || dataset.visitorId || "",
|
|
189
|
+
visitorId: dataset.visitorId || dataset.userId || "",
|
|
190
|
+
userHandle: dataset.userHandle || dataset.user || "default",
|
|
191
|
+
siteId: derivedSite.siteId,
|
|
192
|
+
siteKey: derivedSite.siteKey,
|
|
193
|
+
siteName: derivedSite.siteName,
|
|
194
|
+
siteUrl: derivedSite.siteUrl,
|
|
195
|
+
pageUrl: location.href,
|
|
196
|
+
pageOrigin: location.origin,
|
|
197
|
+
referrer: document.referrer || "",
|
|
198
|
+
position: normalizePosition(dataset.position),
|
|
199
|
+
customCss: (dataset.customCss || "").trim(),
|
|
200
|
+
storageKey: dataset.storageKey || `chatbot_history_${location.host}_${derivedSite.siteId}`,
|
|
201
|
+
sessionId: ""
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
config.sessionId = getOrCreateSessionId(config.storageKey, dataset.sessionId || dataset.chatSessionId);
|
|
205
|
+
|
|
206
|
+
const root = document.createElement("div");
|
|
207
|
+
root.className = "ucb-root";
|
|
208
|
+
root.setAttribute("data-position", config.position);
|
|
209
|
+
|
|
210
|
+
function applyRootPosition() {
|
|
211
|
+
root.style.setProperty("position", "fixed", "important");
|
|
212
|
+
// root.style.setProperty("top", "auto", "important");
|
|
213
|
+
// root.style.setProperty("bottom", "20px", "important");
|
|
214
|
+
// if (config.position === "left") {
|
|
215
|
+
// root.style.setProperty("left", "20px", "important");
|
|
216
|
+
// root.style.setProperty("right", "auto", "important");
|
|
217
|
+
// } else {
|
|
218
|
+
// root.style.setProperty("right", "20px", "important");
|
|
219
|
+
// root.style.setProperty("left", "auto", "important");
|
|
220
|
+
// }
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
applyRootPosition();
|
|
224
|
+
|
|
225
|
+
root.innerHTML = `
|
|
226
|
+
<button class="ucb-fab" aria-label="Open ${escapeHtml(config.botName)} chat">
|
|
227
|
+
<span class="ucb-fab-icon">\u2709</span>
|
|
228
|
+
</button>
|
|
229
|
+
|
|
230
|
+
<section class="ucb-panel" aria-live="polite">
|
|
231
|
+
<header class="ucb-header">
|
|
232
|
+
<div class="ucb-title-wrap">
|
|
233
|
+
<div class="ucb-dot"></div>
|
|
234
|
+
<div class="ucb-title-meta">
|
|
235
|
+
<h2 class="ucb-title">${escapeHtml(config.botName)}</h2>
|
|
236
|
+
<p class="ucb-subtitle">Online now</p>
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
<div class="ucb-header-actions">
|
|
240
|
+
<button class="ucb-clear" type="button" aria-label="Clear chat">Clear</button>
|
|
241
|
+
<button class="ucb-close" type="button" aria-label="Close chat">\u2715</button>
|
|
242
|
+
</div>
|
|
243
|
+
</header>
|
|
244
|
+
|
|
245
|
+
<div class="ucb-messages"></div>
|
|
246
|
+
|
|
247
|
+
<div class="ucb-typing" hidden>
|
|
248
|
+
<span></span><span></span><span></span>
|
|
249
|
+
</div>
|
|
250
|
+
|
|
251
|
+
<form class="ucb-input-row">
|
|
252
|
+
<input class="ucb-input" type="text" autocomplete="off" placeholder="${escapeHtml(config.placeholder)}" />
|
|
253
|
+
<button class="ucb-send" type="submit">Send</button>
|
|
254
|
+
</form>
|
|
255
|
+
</section>
|
|
256
|
+
`;
|
|
257
|
+
|
|
258
|
+
const style = document.createElement("style");
|
|
259
|
+
style.textContent = `
|
|
260
|
+
.ucb-root {
|
|
261
|
+
--ucb-color: ${config.color};
|
|
262
|
+
--ucb-text: #0f172a;
|
|
263
|
+
--ucb-soft: #f8fafc;
|
|
264
|
+
--ucb-border: #e2e8f0;
|
|
265
|
+
--ucb-muted: #64748b;
|
|
266
|
+
--ucb-shadow: 0 18px 50px rgba(2, 6, 23, 0.16);
|
|
267
|
+
position: fixed;
|
|
268
|
+
bottom: 20px;
|
|
269
|
+
top: auto;
|
|
270
|
+
left: auto;
|
|
271
|
+
right: auto;
|
|
272
|
+
z-index: 2147483000;
|
|
273
|
+
font-family: "Segoe UI", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
|
274
|
+
display: flex;
|
|
275
|
+
flex-direction: column-reverse;
|
|
276
|
+
align-items: flex-end;
|
|
277
|
+
gap: 12px;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.ucb-root,
|
|
281
|
+
.ucb-root * {
|
|
282
|
+
box-sizing: border-box;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.ucb-root[data-position="right"] {
|
|
286
|
+
right: 20px;
|
|
287
|
+
left: auto;
|
|
288
|
+
top: auto;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.ucb-root[data-position="left"] {
|
|
292
|
+
left: 20px;
|
|
293
|
+
right: auto;
|
|
294
|
+
top: auto;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.ucb-fab {
|
|
298
|
+
width: 58px;
|
|
299
|
+
height: 58px;
|
|
300
|
+
border: 0;
|
|
301
|
+
border-radius: 999px;
|
|
302
|
+
position: relative;
|
|
303
|
+
top: auto;
|
|
304
|
+
right: auto;
|
|
305
|
+
left: auto;
|
|
306
|
+
bottom: auto;
|
|
307
|
+
background: linear-gradient(135deg, var(--ucb-color), #0b1220);
|
|
308
|
+
color: #fff;
|
|
309
|
+
cursor: pointer;
|
|
310
|
+
box-shadow: var(--ucb-shadow);
|
|
311
|
+
display: inline-flex;
|
|
312
|
+
justify-content: center;
|
|
313
|
+
align-items: center;
|
|
314
|
+
transition: transform 180ms ease;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.ucb-fab:hover { transform: translateY(-2px); }
|
|
318
|
+
|
|
319
|
+
.ucb-fab-icon {
|
|
320
|
+
font-size: 20px;
|
|
321
|
+
line-height: 1;
|
|
322
|
+
position: static !important;
|
|
323
|
+
top: auto !important;
|
|
324
|
+
right: auto !important;
|
|
325
|
+
bottom: auto !important;
|
|
326
|
+
left: auto !important;
|
|
327
|
+
transform: none !important;
|
|
328
|
+
display: inline-block;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.ucb-panel {
|
|
332
|
+
width: min(370px, calc(100vw - 28px));
|
|
333
|
+
height: min(580px, calc(100vh - 96px));
|
|
334
|
+
background: linear-gradient(180deg, #ffffff, #f8fbff);
|
|
335
|
+
border: 1px solid var(--ucb-border);
|
|
336
|
+
border-radius: 20px;
|
|
337
|
+
box-shadow: var(--ucb-shadow);
|
|
338
|
+
display: grid;
|
|
339
|
+
grid-template-rows: auto 1fr auto auto;
|
|
340
|
+
margin-bottom: 0;
|
|
341
|
+
overflow: hidden;
|
|
342
|
+
opacity: 0;
|
|
343
|
+
transform: translateY(8px) scale(0.98);
|
|
344
|
+
pointer-events: none;
|
|
345
|
+
transition: opacity 200ms ease, transform 200ms ease;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.ucb-root.ucb-open .ucb-panel {
|
|
349
|
+
opacity: 1;
|
|
350
|
+
transform: translateY(0) scale(1);
|
|
351
|
+
pointer-events: auto;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.ucb-header {
|
|
355
|
+
background: linear-gradient(180deg, #f8fafc, #eef3f9);
|
|
356
|
+
border-bottom: 1px solid var(--ucb-border);
|
|
357
|
+
padding: 14px 14px;
|
|
358
|
+
display: flex;
|
|
359
|
+
align-items: center;
|
|
360
|
+
justify-content: space-between;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.ucb-title-wrap {
|
|
364
|
+
display: flex;
|
|
365
|
+
align-items: center;
|
|
366
|
+
gap: 8px;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.ucb-title-meta {
|
|
370
|
+
display: flex;
|
|
371
|
+
flex-direction: column;
|
|
372
|
+
gap: 1px;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
.ucb-dot {
|
|
376
|
+
width: 10px;
|
|
377
|
+
height: 10px;
|
|
378
|
+
border-radius: 999px;
|
|
379
|
+
background: var(--ucb-color);
|
|
380
|
+
box-shadow: 0 0 0 4px color-mix(in srgb, var(--ucb-color) 18%, transparent);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.ucb-title {
|
|
384
|
+
margin: 0;
|
|
385
|
+
color: var(--ucb-text);
|
|
386
|
+
font-size: 15px;
|
|
387
|
+
font-weight: 600;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.ucb-subtitle {
|
|
391
|
+
margin: 0;
|
|
392
|
+
color: var(--ucb-muted);
|
|
393
|
+
font-size: 11px;
|
|
394
|
+
line-height: 1.3;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.ucb-header-actions {
|
|
398
|
+
display: inline-flex;
|
|
399
|
+
align-items: center;
|
|
400
|
+
gap: 6px;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.ucb-clear {
|
|
404
|
+
border: 1px solid #dbe5f0;
|
|
405
|
+
background: #fff;
|
|
406
|
+
color: #334155;
|
|
407
|
+
cursor: pointer;
|
|
408
|
+
font-size: 12px;
|
|
409
|
+
padding: 5px 8px;
|
|
410
|
+
border-radius: 8px;
|
|
411
|
+
line-height: 1;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
.ucb-clear:hover {
|
|
415
|
+
background: #f8fafc;
|
|
416
|
+
border-color: #cbd5e1;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
.ucb-close {
|
|
420
|
+
border: 0;
|
|
421
|
+
background: transparent;
|
|
422
|
+
color: #64748b;
|
|
423
|
+
cursor: pointer;
|
|
424
|
+
font-size: 16px;
|
|
425
|
+
width: 28px;
|
|
426
|
+
height: 28px;
|
|
427
|
+
border-radius: 8px;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
.ucb-close:hover {
|
|
431
|
+
background: #eef2f7;
|
|
432
|
+
color: #1e293b;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
.ucb-messages {
|
|
436
|
+
overflow-y: auto;
|
|
437
|
+
padding: 14px;
|
|
438
|
+
background:
|
|
439
|
+
radial-gradient(120% 80% at 50% 0%, rgba(14, 165, 164, 0.06), rgba(255, 255, 255, 0) 55%),
|
|
440
|
+
linear-gradient(180deg, #ffffff, #f8fafc);
|
|
441
|
+
display: flex;
|
|
442
|
+
flex-direction: column;
|
|
443
|
+
gap: 10px;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
.ucb-message {
|
|
447
|
+
max-width: 88%;
|
|
448
|
+
border-radius: 14px;
|
|
449
|
+
padding: 10px 12px;
|
|
450
|
+
font-size: 14px;
|
|
451
|
+
line-height: 1.45;
|
|
452
|
+
white-space: pre-wrap;
|
|
453
|
+
word-break: break-word;
|
|
454
|
+
animation: ucb-pop 170ms ease;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
.ucb-message.user {
|
|
458
|
+
align-self: flex-end;
|
|
459
|
+
color: #fff;
|
|
460
|
+
background: var(--ucb-color);
|
|
461
|
+
border-bottom-right-radius: 4px;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
.ucb-message.assistant {
|
|
465
|
+
align-self: flex-start;
|
|
466
|
+
color: #0f172a;
|
|
467
|
+
background: #fff;
|
|
468
|
+
border: 1px solid var(--ucb-border);
|
|
469
|
+
border-bottom-left-radius: 4px;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
.ucb-message.assistant a {
|
|
473
|
+
color: #0f766e;
|
|
474
|
+
text-decoration: underline;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
.ucb-message.assistant code {
|
|
478
|
+
background: #f1f5f9;
|
|
479
|
+
border: 1px solid #e2e8f0;
|
|
480
|
+
border-radius: 5px;
|
|
481
|
+
padding: 1px 4px;
|
|
482
|
+
font-family: Consolas, "Courier New", monospace;
|
|
483
|
+
font-size: 12px;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
.ucb-typing {
|
|
487
|
+
height: 20px;
|
|
488
|
+
padding: 0 16px 8px;
|
|
489
|
+
display: flex;
|
|
490
|
+
align-items: center;
|
|
491
|
+
gap: 5px;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
.ucb-typing span {
|
|
495
|
+
width: 7px;
|
|
496
|
+
height: 7px;
|
|
497
|
+
border-radius: 999px;
|
|
498
|
+
background: #cbd5e1;
|
|
499
|
+
animation: ucb-bounce 1s infinite ease-in-out;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
.ucb-typing span:nth-child(2) { animation-delay: 0.16s; }
|
|
503
|
+
.ucb-typing span:nth-child(3) { animation-delay: 0.32s; }
|
|
504
|
+
|
|
505
|
+
@keyframes ucb-bounce {
|
|
506
|
+
0%, 80%, 100% { transform: scale(0.75); opacity: 0.6; }
|
|
507
|
+
40% { transform: scale(1); opacity: 1; }
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
@keyframes ucb-pop {
|
|
511
|
+
from {
|
|
512
|
+
opacity: 0;
|
|
513
|
+
transform: translateY(4px);
|
|
514
|
+
}
|
|
515
|
+
to {
|
|
516
|
+
opacity: 1;
|
|
517
|
+
transform: translateY(0);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
.ucb-input-row {
|
|
522
|
+
padding: 10px;
|
|
523
|
+
border-top: 1px solid var(--ucb-border);
|
|
524
|
+
display: grid;
|
|
525
|
+
grid-template-columns: 1fr auto;
|
|
526
|
+
gap: 8px;
|
|
527
|
+
background: #fff;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
.ucb-input {
|
|
531
|
+
border: 1px solid var(--ucb-border);
|
|
532
|
+
border-radius: 10px;
|
|
533
|
+
padding: 10px 11px;
|
|
534
|
+
font-size: 14px;
|
|
535
|
+
outline: none;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
.ucb-input:focus {
|
|
539
|
+
border-color: var(--ucb-color);
|
|
540
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--ucb-color) 18%, transparent);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
.ucb-send {
|
|
544
|
+
border: 0;
|
|
545
|
+
border-radius: 10px;
|
|
546
|
+
padding: 10px 14px;
|
|
547
|
+
font-size: 13px;
|
|
548
|
+
font-weight: 600;
|
|
549
|
+
color: #fff;
|
|
550
|
+
background: var(--ucb-color);
|
|
551
|
+
cursor: pointer;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
.ucb-send:disabled {
|
|
555
|
+
opacity: 0.65;
|
|
556
|
+
cursor: wait;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
@media (max-width: 600px) {
|
|
560
|
+
/* Closed state: keep launcher pinned to bottom-right. */
|
|
561
|
+
.ucb-root[data-position="right"]:not(.ucb-open) {
|
|
562
|
+
right: 12px !important;
|
|
563
|
+
left: auto !important;
|
|
564
|
+
bottom: 12px !important;
|
|
565
|
+
top: auto !important;
|
|
566
|
+
align-items: flex-end;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
.ucb-root[data-position="left"]:not(.ucb-open) {
|
|
570
|
+
left: 12px !important;
|
|
571
|
+
right: auto !important;
|
|
572
|
+
bottom: 12px !important;
|
|
573
|
+
top: auto !important;
|
|
574
|
+
align-items: flex-start;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/* Open state: show a usable bottom-sheet style panel on mobile. */
|
|
578
|
+
.ucb-root.ucb-open[data-position="right"],
|
|
579
|
+
.ucb-root.ucb-open[data-position="left"] {
|
|
580
|
+
left: max(12px, env(safe-area-inset-left)) !important;
|
|
581
|
+
right: max(12px, env(safe-area-inset-right)) !important;
|
|
582
|
+
bottom: 12px !important;
|
|
583
|
+
top: auto !important;
|
|
584
|
+
align-items: stretch;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
.ucb-root.ucb-open .ucb-panel {
|
|
588
|
+
width: calc(100vw - 24px - env(safe-area-inset-left) - env(safe-area-inset-right));
|
|
589
|
+
max-width: 100%;
|
|
590
|
+
height: min(72vh, 620px);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
.ucb-root.ucb-open .ucb-fab {
|
|
594
|
+
display: none;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
`;
|
|
598
|
+
|
|
599
|
+
document.head.appendChild(style);
|
|
600
|
+
if (config.customCss) {
|
|
601
|
+
const customCssStyle = document.createElement("style");
|
|
602
|
+
customCssStyle.setAttribute("data-ucb-custom-css", "inline");
|
|
603
|
+
customCssStyle.textContent = config.customCss;
|
|
604
|
+
document.head.appendChild(customCssStyle);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
document.body.appendChild(root);
|
|
608
|
+
|
|
609
|
+
const fab = root.querySelector(".ucb-fab");
|
|
610
|
+
const panel = root.querySelector(".ucb-panel");
|
|
611
|
+
const closeBtn = root.querySelector(".ucb-close");
|
|
612
|
+
const clearBtn = root.querySelector(".ucb-clear");
|
|
613
|
+
const form = root.querySelector(".ucb-input-row");
|
|
614
|
+
const input = root.querySelector(".ucb-input");
|
|
615
|
+
const messagesEl = root.querySelector(".ucb-messages");
|
|
616
|
+
const sendBtn = root.querySelector(".ucb-send");
|
|
617
|
+
const typingEl = root.querySelector(".ucb-typing");
|
|
618
|
+
|
|
619
|
+
let messages = safeJsonParse(localStorage.getItem(config.storageKey), []);
|
|
620
|
+
let inFlight = false;
|
|
621
|
+
let knowledgeUrlCache = null;
|
|
622
|
+
let knowledgeUrlInFlight = null;
|
|
623
|
+
let knowledgeUrlCacheAt = 0;
|
|
624
|
+
let knowledgeSyncTimer = null;
|
|
625
|
+
|
|
626
|
+
if (!Array.isArray(messages) || !messages.length) {
|
|
627
|
+
messages = [{ role: "assistant", content: config.welcome }];
|
|
628
|
+
persistMessages();
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
renderMessages();
|
|
632
|
+
|
|
633
|
+
if (config.knowledgeClientFetch && config.knowledgeUrl && isSafeUrl(config.knowledgeUrl) && config.knowledgeSyncOnLoad) {
|
|
634
|
+
loadKnowledgeFromUrl({ force: true }).catch(() => {});
|
|
635
|
+
|
|
636
|
+
knowledgeSyncTimer = setInterval(() => {
|
|
637
|
+
loadKnowledgeFromUrl({ force: true }).catch(() => {});
|
|
638
|
+
}, config.knowledgeSyncMs);
|
|
639
|
+
|
|
640
|
+
window.addEventListener("beforeunload", () => {
|
|
641
|
+
if (knowledgeSyncTimer) {
|
|
642
|
+
clearInterval(knowledgeSyncTimer);
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
fab.addEventListener("click", () => {
|
|
648
|
+
root.classList.toggle("ucb-open");
|
|
649
|
+
if (root.classList.contains("ucb-open")) {
|
|
650
|
+
input.focus();
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
closeBtn.addEventListener("click", () => {
|
|
655
|
+
root.classList.remove("ucb-open");
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
clearBtn.addEventListener("click", () => {
|
|
659
|
+
messages = [{ role: "assistant", content: config.welcome }];
|
|
660
|
+
persistMessages();
|
|
661
|
+
renderMessages();
|
|
662
|
+
input.focus();
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
document.addEventListener("keydown", (event) => {
|
|
666
|
+
if (event.key === "Escape") {
|
|
667
|
+
root.classList.remove("ucb-open");
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
form.addEventListener("submit", async (event) => {
|
|
672
|
+
event.preventDefault();
|
|
673
|
+
if (inFlight) {
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const content = input.value.trim();
|
|
678
|
+
if (!content) {
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
input.value = "";
|
|
683
|
+
messages.push({ role: "user", content });
|
|
684
|
+
persistMessages();
|
|
685
|
+
renderMessages();
|
|
686
|
+
|
|
687
|
+
inFlight = true;
|
|
688
|
+
sendBtn.disabled = true;
|
|
689
|
+
typingEl.hidden = false;
|
|
690
|
+
|
|
691
|
+
const assistantMessage = { role: "assistant", content: "" };
|
|
692
|
+
messages.push(assistantMessage);
|
|
693
|
+
renderMessages();
|
|
694
|
+
|
|
695
|
+
try {
|
|
696
|
+
await streamAssistantReply(messages, (token) => {
|
|
697
|
+
assistantMessage.content += token;
|
|
698
|
+
updateLastAssistantMessage(assistantMessage.content);
|
|
699
|
+
});
|
|
700
|
+
persistMessages();
|
|
701
|
+
} catch (error) {
|
|
702
|
+
assistantMessage.content = `Sorry, there was a problem: ${error.message || "unknown error"}`;
|
|
703
|
+
updateLastAssistantMessage(assistantMessage.content);
|
|
704
|
+
persistMessages();
|
|
705
|
+
} finally {
|
|
706
|
+
inFlight = false;
|
|
707
|
+
sendBtn.disabled = false;
|
|
708
|
+
typingEl.hidden = true;
|
|
709
|
+
scrollToBottom();
|
|
710
|
+
}
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
function persistMessages() {
|
|
714
|
+
localStorage.setItem(config.storageKey, JSON.stringify(messages.slice(-40)));
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function scrollToBottom() {
|
|
718
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function renderMessages() {
|
|
722
|
+
messagesEl.innerHTML = "";
|
|
723
|
+
for (const message of messages) {
|
|
724
|
+
const messageEl = document.createElement("div");
|
|
725
|
+
messageEl.className = `ucb-message ${message.role}`;
|
|
726
|
+
if (message.role === "assistant") {
|
|
727
|
+
messageEl.innerHTML = markdownToSafeHtml(message.content);
|
|
728
|
+
} else {
|
|
729
|
+
messageEl.textContent = message.content;
|
|
730
|
+
}
|
|
731
|
+
messagesEl.appendChild(messageEl);
|
|
732
|
+
}
|
|
733
|
+
scrollToBottom();
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function updateLastAssistantMessage(content) {
|
|
737
|
+
const last = messagesEl.lastElementChild;
|
|
738
|
+
if (!last || !last.classList.contains("assistant")) {
|
|
739
|
+
renderMessages();
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
last.innerHTML = markdownToSafeHtml(content);
|
|
743
|
+
scrollToBottom();
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function toKnowledgeEntries(inputValue, sourceLabel) {
|
|
747
|
+
if (!inputValue) {
|
|
748
|
+
return [];
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
if (Array.isArray(inputValue)) {
|
|
752
|
+
return inputValue;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
if (typeof inputValue === "object") {
|
|
756
|
+
return Array.isArray(inputValue.knowledge)
|
|
757
|
+
? inputValue.knowledge
|
|
758
|
+
: Array.isArray(inputValue.items)
|
|
759
|
+
? inputValue.items
|
|
760
|
+
: [inputValue];
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
const text = String(inputValue).trim();
|
|
764
|
+
if (!text) {
|
|
765
|
+
return [];
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
return [{ title: "Knowledge", content: text, source: sourceLabel }];
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function appendKnowledgeUrlParams(baseUrl) {
|
|
772
|
+
if (!isSafeUrl(baseUrl)) {
|
|
773
|
+
return baseUrl;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const url = new URL(baseUrl, location.href);
|
|
777
|
+
|
|
778
|
+
if (config.includeKnowledgeContext) {
|
|
779
|
+
const contextParams = {
|
|
780
|
+
tenantId: config.tenantId,
|
|
781
|
+
userId: config.userId,
|
|
782
|
+
userHandle: config.userHandle,
|
|
783
|
+
siteId: config.siteId,
|
|
784
|
+
siteKey: config.siteKey,
|
|
785
|
+
siteName: config.siteName,
|
|
786
|
+
siteUrl: config.siteUrl,
|
|
787
|
+
pageUrl: config.pageUrl,
|
|
788
|
+
origin: config.pageOrigin,
|
|
789
|
+
referrer: config.referrer
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
Object.entries(contextParams).forEach(([key, value]) => {
|
|
793
|
+
const text = String(value || "").trim();
|
|
794
|
+
if (text && !url.searchParams.has(key)) {
|
|
795
|
+
url.searchParams.set(key, text);
|
|
796
|
+
}
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
if (config.knowledgeQuery && typeof config.knowledgeQuery === "object") {
|
|
801
|
+
Object.entries(config.knowledgeQuery).forEach(([key, value]) => {
|
|
802
|
+
if (value === null || value === undefined) {
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
if (!url.searchParams.has(key)) {
|
|
806
|
+
url.searchParams.set(key, String(value));
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
return url.toString();
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
async function loadKnowledgeFromUrl(options) {
|
|
815
|
+
const opts = options && typeof options === "object" ? options : {};
|
|
816
|
+
const forceRefresh = opts.force === true;
|
|
817
|
+
|
|
818
|
+
// Default behavior: keep knowledge URL hidden from browser network.
|
|
819
|
+
if (!config.knowledgeClientFetch) {
|
|
820
|
+
return [];
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
if (!config.knowledgeUrl || !isSafeUrl(config.knowledgeUrl)) {
|
|
824
|
+
return [];
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
const cacheIsFresh =
|
|
828
|
+
knowledgeUrlCache &&
|
|
829
|
+
knowledgeUrlCacheAt > 0 &&
|
|
830
|
+
Date.now() - knowledgeUrlCacheAt < config.knowledgeSyncMs;
|
|
831
|
+
|
|
832
|
+
if (!forceRefresh && cacheIsFresh) {
|
|
833
|
+
return knowledgeUrlCache;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
if (knowledgeUrlInFlight) {
|
|
837
|
+
return knowledgeUrlInFlight;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
knowledgeUrlInFlight = (async () => {
|
|
841
|
+
try {
|
|
842
|
+
const resolvedKnowledgeUrl = appendKnowledgeUrlParams(config.knowledgeUrl);
|
|
843
|
+
const response = await fetch(resolvedKnowledgeUrl, {
|
|
844
|
+
method: "GET",
|
|
845
|
+
headers: {
|
|
846
|
+
Accept: "application/json"
|
|
847
|
+
}
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
if (!response.ok) {
|
|
851
|
+
throw new Error(`Knowledge URL request failed with status ${response.status}`);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
const payload = await response.json();
|
|
855
|
+
knowledgeUrlCache = toKnowledgeEntries(payload, "knowledge-url");
|
|
856
|
+
knowledgeUrlCacheAt = Date.now();
|
|
857
|
+
return knowledgeUrlCache;
|
|
858
|
+
} catch (error) {
|
|
859
|
+
console.warn("Unable to load data-knowledge-url:", error?.message || error);
|
|
860
|
+
if (!knowledgeUrlCache) {
|
|
861
|
+
knowledgeUrlCache = [];
|
|
862
|
+
}
|
|
863
|
+
return knowledgeUrlCache;
|
|
864
|
+
} finally {
|
|
865
|
+
knowledgeUrlInFlight = null;
|
|
866
|
+
}
|
|
867
|
+
})();
|
|
868
|
+
|
|
869
|
+
return knowledgeUrlInFlight;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
async function streamAssistantReply(fullHistory, onToken) {
|
|
873
|
+
const headers = {
|
|
874
|
+
"Content-Type": "application/json"
|
|
875
|
+
};
|
|
876
|
+
|
|
877
|
+
if (config.authToken) {
|
|
878
|
+
headers.Authorization = `Bearer ${config.authToken}`;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
const inlineKnowledge = toKnowledgeEntries(config.knowledge, "inline");
|
|
882
|
+
const remoteKnowledge = await loadKnowledgeFromUrl();
|
|
883
|
+
const mergedKnowledge = [...inlineKnowledge, ...remoteKnowledge];
|
|
884
|
+
|
|
885
|
+
const requestBody = {
|
|
886
|
+
messages: fullHistory,
|
|
887
|
+
sessionId: config.sessionId,
|
|
888
|
+
siteConfig: {
|
|
889
|
+
userHandle: config.userHandle,
|
|
890
|
+
provider: config.provider,
|
|
891
|
+
systemPrompt: config.systemPrompt,
|
|
892
|
+
knowledge: mergedKnowledge,
|
|
893
|
+
model: config.model,
|
|
894
|
+
siteId: config.siteId,
|
|
895
|
+
siteKey: config.siteKey,
|
|
896
|
+
siteName: config.siteName,
|
|
897
|
+
siteUrl: config.siteUrl,
|
|
898
|
+
pageUrl: config.pageUrl,
|
|
899
|
+
pageOrigin: config.pageOrigin,
|
|
900
|
+
referrer: config.referrer,
|
|
901
|
+
visitorId: config.visitorId,
|
|
902
|
+
userId: config.userId
|
|
903
|
+
}
|
|
904
|
+
};
|
|
905
|
+
|
|
906
|
+
// Add bot credentials if provided
|
|
907
|
+
if (config.botId) {
|
|
908
|
+
requestBody.botId = config.botId;
|
|
909
|
+
}
|
|
910
|
+
if (config.botApiKey) {
|
|
911
|
+
requestBody.apiKey = config.botApiKey;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
const response = await fetch(`${config.apiBase.replace(/\/$/, "")}/api/chat/stream`, {
|
|
915
|
+
method: "POST",
|
|
916
|
+
headers,
|
|
917
|
+
body: JSON.stringify(requestBody)
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
if (!response.ok || !response.body) {
|
|
921
|
+
const text = await response.text();
|
|
922
|
+
throw new Error(text || "Request failed");
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
const reader = response.body.getReader();
|
|
926
|
+
const decoder = new TextDecoder();
|
|
927
|
+
let buffer = "";
|
|
928
|
+
|
|
929
|
+
while (true) {
|
|
930
|
+
const { done, value } = await reader.read();
|
|
931
|
+
if (done) {
|
|
932
|
+
break;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
buffer += decoder.decode(value, { stream: true });
|
|
936
|
+
const events = buffer.split("\n\n");
|
|
937
|
+
buffer = events.pop() || "";
|
|
938
|
+
|
|
939
|
+
for (const block of events) {
|
|
940
|
+
let eventName = "message";
|
|
941
|
+
let dataRaw = "{}";
|
|
942
|
+
|
|
943
|
+
for (const line of block.split("\n")) {
|
|
944
|
+
if (line.startsWith("event:")) {
|
|
945
|
+
eventName = line.slice(6).trim();
|
|
946
|
+
}
|
|
947
|
+
if (line.startsWith("data:")) {
|
|
948
|
+
dataRaw = line.slice(5).trim();
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
let data;
|
|
953
|
+
try {
|
|
954
|
+
data = JSON.parse(dataRaw);
|
|
955
|
+
} catch {
|
|
956
|
+
data = {};
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
if (eventName === "token" && typeof data.token === "string") {
|
|
960
|
+
onToken(data.token);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
if (eventName === "error") {
|
|
964
|
+
throw new Error(data.error || "Streaming error");
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
window.UniversalChatbotWidget = {
|
|
971
|
+
open() {
|
|
972
|
+
root.classList.add("ucb-open");
|
|
973
|
+
input.focus();
|
|
974
|
+
},
|
|
975
|
+
close() {
|
|
976
|
+
root.classList.remove("ucb-open");
|
|
977
|
+
},
|
|
978
|
+
refreshKnowledge() {
|
|
979
|
+
return loadKnowledgeFromUrl({ force: true });
|
|
980
|
+
},
|
|
981
|
+
clear() {
|
|
982
|
+
messages = [{ role: "assistant", content: config.welcome }];
|
|
983
|
+
persistMessages();
|
|
984
|
+
renderMessages();
|
|
985
|
+
}
|
|
986
|
+
};
|
|
987
|
+
})();
|