@bootstrapware/answers 0.1.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/AGENTS.md +245 -0
- package/CHANGELOG.md +14 -0
- package/LICENSE +21 -0
- package/README.md +60 -0
- package/dist/answers.iife.js +72 -0
- package/dist/answers.iife.js.map +1 -0
- package/dist/index.d.ts +78 -0
- package/dist/index.js +629 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +621 -0
- package/package.json +61 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
// src/Answers.tsx
|
|
5
|
+
import { useEffect as useEffect2, useId, useRef, useState as useState2 } from "react";
|
|
6
|
+
|
|
7
|
+
// src/result.ts
|
|
8
|
+
var ANSWER_MAX = 8e3;
|
|
9
|
+
var EXCERPT_MAX = 500;
|
|
10
|
+
var CITATION_MAX = 20;
|
|
11
|
+
var EMPTY_FALLBACK = "No answer was found.";
|
|
12
|
+
function codePointLength(value) {
|
|
13
|
+
return Array.from(value).length;
|
|
14
|
+
}
|
|
15
|
+
function clipCodePoints(value, max) {
|
|
16
|
+
let count = 0;
|
|
17
|
+
let out = "";
|
|
18
|
+
for (const char of value) {
|
|
19
|
+
if (count >= max) break;
|
|
20
|
+
out += char;
|
|
21
|
+
count += 1;
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
function safeHttpUrl(value) {
|
|
26
|
+
if (typeof value !== "string") return void 0;
|
|
27
|
+
const trimmed = value.trim();
|
|
28
|
+
if (!trimmed) return void 0;
|
|
29
|
+
let parsed;
|
|
30
|
+
try {
|
|
31
|
+
parsed = new URL(trimmed);
|
|
32
|
+
} catch {
|
|
33
|
+
return void 0;
|
|
34
|
+
}
|
|
35
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return void 0;
|
|
36
|
+
return parsed.toString();
|
|
37
|
+
}
|
|
38
|
+
function sanitizeCitations(value) {
|
|
39
|
+
if (!Array.isArray(value)) return [];
|
|
40
|
+
const out = [];
|
|
41
|
+
const seen = /* @__PURE__ */ new Set();
|
|
42
|
+
for (const item of value) {
|
|
43
|
+
if (out.length >= CITATION_MAX) break;
|
|
44
|
+
if (!item || typeof item !== "object") continue;
|
|
45
|
+
const raw = item;
|
|
46
|
+
if (typeof raw.id !== "string" || !raw.id.trim()) continue;
|
|
47
|
+
const id = raw.id.trim();
|
|
48
|
+
if (seen.has(id)) continue;
|
|
49
|
+
seen.add(id);
|
|
50
|
+
const title = typeof raw.title === "string" && raw.title.trim() ? clipCodePoints(raw.title.trim(), 200) : id;
|
|
51
|
+
const excerpt = typeof raw.excerpt === "string" && raw.excerpt.trim() ? clipCodePoints(raw.excerpt.trim(), EXCERPT_MAX) : void 0;
|
|
52
|
+
const url = safeHttpUrl(raw.url);
|
|
53
|
+
out.push({ id, title, excerpt, url });
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
function sanitizeResult(input) {
|
|
58
|
+
if (!input || typeof input !== "object") return null;
|
|
59
|
+
const raw = input;
|
|
60
|
+
if (typeof raw.answerId !== "string" || !raw.answerId.trim()) return null;
|
|
61
|
+
if (typeof raw.sessionId !== "string") return null;
|
|
62
|
+
const answerId = raw.answerId.trim();
|
|
63
|
+
const sessionId = raw.sessionId;
|
|
64
|
+
const fallback = typeof raw.fallback === "string" && raw.fallback.trim() ? raw.fallback.trim() : EMPTY_FALLBACK;
|
|
65
|
+
if (raw.outcome === "unanswered") {
|
|
66
|
+
return { sessionId, answerId, outcome: "unanswered", fallback };
|
|
67
|
+
}
|
|
68
|
+
if (raw.outcome !== "answered") return null;
|
|
69
|
+
const answer = typeof raw.answer === "string" ? raw.answer.trim() : "";
|
|
70
|
+
if (!answer) return { sessionId, answerId, outcome: "unanswered", fallback };
|
|
71
|
+
return {
|
|
72
|
+
sessionId,
|
|
73
|
+
answerId,
|
|
74
|
+
outcome: "answered",
|
|
75
|
+
answer: clipCodePoints(answer, ANSWER_MAX),
|
|
76
|
+
citations: sanitizeCitations(raw.citations)
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// src/types.ts
|
|
81
|
+
var QUESTION_MAX = 1e3;
|
|
82
|
+
var WIDGET_ROOT_CLASS = "bsw-an";
|
|
83
|
+
|
|
84
|
+
// src/adapters.ts
|
|
85
|
+
var LOCAL_FALLBACK = "We could not find that in the Northwind Outfitters sample. Ask about hours, returns, or shipping.";
|
|
86
|
+
var LOCAL_PROMPTS = {
|
|
87
|
+
returns: "What is your return window?",
|
|
88
|
+
returnsFollowUp: "How long do I have?",
|
|
89
|
+
hours: "What are your hours?",
|
|
90
|
+
shipping: "Is shipping free?",
|
|
91
|
+
unsupported: "Do you price match?"
|
|
92
|
+
};
|
|
93
|
+
var RETURNS_ANSWER = "Returns are accepted within 30 days of delivery.";
|
|
94
|
+
var RETURNS_FOLLOW = "You have 30 days from delivery. Exchanges use that same window.";
|
|
95
|
+
var HOURS_ANSWER = "Northwind Outfitters is open Monday through Friday, 9am to 5pm US Eastern, and closed Saturday and Sunday.";
|
|
96
|
+
var HOURS_FOLLOW = "Saturday and Sunday are closed. Weekday hours are 9am to 5pm US Eastern.";
|
|
97
|
+
var SHIPPING_ANSWER = "Standard shipping is free on orders over $50.";
|
|
98
|
+
var SHIPPING_FOLLOW = "Free standard shipping starts when the order is over $50.";
|
|
99
|
+
var LOCAL_CITATIONS = {
|
|
100
|
+
hours: {
|
|
101
|
+
id: "ane_hours",
|
|
102
|
+
title: "Store hours",
|
|
103
|
+
url: "https://northwind.example/help/hours",
|
|
104
|
+
excerpt: "Open Monday through Friday, 9am to 5pm US Eastern. Closed Saturday and Sunday."
|
|
105
|
+
},
|
|
106
|
+
returns: {
|
|
107
|
+
id: "ane_returns",
|
|
108
|
+
title: "Returns",
|
|
109
|
+
url: "https://northwind.example/help/returns",
|
|
110
|
+
excerpt: "Returns are accepted within 30 days of delivery. Exchanges use the same 30 day window."
|
|
111
|
+
},
|
|
112
|
+
shipping: {
|
|
113
|
+
id: "ane_shipping",
|
|
114
|
+
title: "Shipping",
|
|
115
|
+
url: "https://northwind.example/help/shipping",
|
|
116
|
+
excerpt: "Standard shipping is free on orders over $50."
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
var TOPICS = [
|
|
120
|
+
{ id: "returns", words: ["return", "returns", "refund", "refunds", "exchange", "exchanges"] },
|
|
121
|
+
{ id: "shipping", words: ["ship", "shipping", "delivery"] },
|
|
122
|
+
{ id: "hours", words: ["hour", "hours", "open", "opening", "weekend", "saturday", "sunday", "weekday", "weekdays"] }
|
|
123
|
+
];
|
|
124
|
+
var ANSWERS = {
|
|
125
|
+
hours: HOURS_ANSWER,
|
|
126
|
+
returns: RETURNS_ANSWER,
|
|
127
|
+
shipping: SHIPPING_ANSWER
|
|
128
|
+
};
|
|
129
|
+
var FOLLOW_ANSWERS = {
|
|
130
|
+
hours: HOURS_FOLLOW,
|
|
131
|
+
returns: RETURNS_FOLLOW,
|
|
132
|
+
shipping: SHIPPING_FOLLOW
|
|
133
|
+
};
|
|
134
|
+
var FOLLOW = [
|
|
135
|
+
{ phrase: "how long" },
|
|
136
|
+
{ phrase: "how long do i have" },
|
|
137
|
+
{ phrase: "are you open then", topic: "hours" },
|
|
138
|
+
{ phrase: "what about then" },
|
|
139
|
+
{ phrase: "is there a minimum", topic: "shipping" },
|
|
140
|
+
{ phrase: "how much is free", topic: "shipping" },
|
|
141
|
+
{ phrase: "tell me more" }
|
|
142
|
+
];
|
|
143
|
+
function normalize(question) {
|
|
144
|
+
return question.trim().toLowerCase().replace(/[?!.,'"]/g, "").replace(/\s+/g, " ").trim();
|
|
145
|
+
}
|
|
146
|
+
function hasWord(question, word) {
|
|
147
|
+
return new RegExp(`\\b${word}\\b`).test(question);
|
|
148
|
+
}
|
|
149
|
+
function matchDirect(question) {
|
|
150
|
+
for (const topic of TOPICS) {
|
|
151
|
+
if (topic.words.some((word) => hasWord(question, word))) return topic.id;
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
function matchFollow(question, last) {
|
|
156
|
+
const hit = FOLLOW.find((item) => item.phrase === question);
|
|
157
|
+
if (!hit || !last) return null;
|
|
158
|
+
if (hit.topic && hit.topic !== last) return null;
|
|
159
|
+
return last;
|
|
160
|
+
}
|
|
161
|
+
function limitProblem(question) {
|
|
162
|
+
const trimmed = question.trim();
|
|
163
|
+
if (!trimmed) return "Enter a question.";
|
|
164
|
+
if (codePointLength(trimmed) > QUESTION_MAX) return "Questions are limited to 1,000 characters.";
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
function createLocalAdapter() {
|
|
168
|
+
const lastTopic = /* @__PURE__ */ new Map();
|
|
169
|
+
let sequence = 0;
|
|
170
|
+
return {
|
|
171
|
+
async ask(input) {
|
|
172
|
+
const problem = limitProblem(input.question);
|
|
173
|
+
if (problem) throw new Error(problem);
|
|
174
|
+
const question = normalize(input.question);
|
|
175
|
+
const direct = matchDirect(question);
|
|
176
|
+
const topic = direct ?? matchFollow(question, lastTopic.get(input.sessionId));
|
|
177
|
+
sequence += 1;
|
|
178
|
+
const answerId = `anq_${sequence.toString(16).padStart(24, "0")}`;
|
|
179
|
+
if (!topic) {
|
|
180
|
+
return {
|
|
181
|
+
sessionId: input.sessionId,
|
|
182
|
+
answerId,
|
|
183
|
+
outcome: "unanswered",
|
|
184
|
+
fallback: LOCAL_FALLBACK
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
lastTopic.set(input.sessionId, topic);
|
|
188
|
+
return {
|
|
189
|
+
sessionId: input.sessionId,
|
|
190
|
+
answerId,
|
|
191
|
+
outcome: "answered",
|
|
192
|
+
answer: direct ? ANSWERS[topic] : FOLLOW_ANSWERS[topic],
|
|
193
|
+
citations: [{ ...LOCAL_CITATIONS[topic] }]
|
|
194
|
+
};
|
|
195
|
+
},
|
|
196
|
+
async feedback() {
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function createByoAdapter(impl) {
|
|
201
|
+
return impl;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// src/frame.tsx
|
|
205
|
+
import { useEffect, useState } from "react";
|
|
206
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
207
|
+
function EmbedFrame({
|
|
208
|
+
presentation = "section",
|
|
209
|
+
defaultOpen = false,
|
|
210
|
+
label,
|
|
211
|
+
className,
|
|
212
|
+
theme,
|
|
213
|
+
children
|
|
214
|
+
}) {
|
|
215
|
+
const [open, setOpen] = useState(defaultOpen);
|
|
216
|
+
const openKey = `${presentation}\0${defaultOpen ? "1" : "0"}`;
|
|
217
|
+
const [openKeySeen, setOpenKeySeen] = useState(openKey);
|
|
218
|
+
if (openKey !== openKeySeen) {
|
|
219
|
+
setOpenKeySeen(openKey);
|
|
220
|
+
setOpen(defaultOpen);
|
|
221
|
+
}
|
|
222
|
+
const launcher = presentation === "launcher";
|
|
223
|
+
const floating = launcher || presentation === "window";
|
|
224
|
+
useEffect(() => {
|
|
225
|
+
if (presentation === "section" || !open) return;
|
|
226
|
+
function onKey(event) {
|
|
227
|
+
if (event.key === "Escape") setOpen(false);
|
|
228
|
+
}
|
|
229
|
+
window.addEventListener("keydown", onKey);
|
|
230
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
231
|
+
}, [open, presentation]);
|
|
232
|
+
if (presentation === "section") return children;
|
|
233
|
+
const classes = [
|
|
234
|
+
"bsw-an-frame",
|
|
235
|
+
`bsw-an-frame--${presentation}`,
|
|
236
|
+
open ? "bsw-an-frame--open" : "",
|
|
237
|
+
floating ? "bsw-an--launcher-host" : "",
|
|
238
|
+
floating && open ? "bsw-an--launcher-open" : "",
|
|
239
|
+
floating && !open ? "bsw-an--launcher-closed" : "",
|
|
240
|
+
className ?? ""
|
|
241
|
+
].filter(Boolean).join(" ");
|
|
242
|
+
return /* @__PURE__ */ jsxs("div", { className: classes, "data-theme": theme, "data-state": open ? "open" : "closed", children: [
|
|
243
|
+
open && (presentation === "modal" || launcher) ? /* @__PURE__ */ jsx("button", { type: "button", className: "bsw-an-frame__backdrop", "aria-label": `Close ${label}`, onClick: () => setOpen(false) }) : null,
|
|
244
|
+
/* @__PURE__ */ jsxs(
|
|
245
|
+
"div",
|
|
246
|
+
{
|
|
247
|
+
className: "bsw-an-frame__panel",
|
|
248
|
+
hidden: !open,
|
|
249
|
+
role: open && presentation === "modal" ? "dialog" : open ? "region" : void 0,
|
|
250
|
+
"aria-modal": open && presentation === "modal" ? true : void 0,
|
|
251
|
+
"aria-label": open ? label : void 0,
|
|
252
|
+
children: [
|
|
253
|
+
/* @__PURE__ */ jsxs("button", { type: "button", className: "bsw-an-frame__close", onClick: () => setOpen(false), children: [
|
|
254
|
+
/* @__PURE__ */ jsx("span", { className: "bsw-an-frame__close-label", children: "Close" }),
|
|
255
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xD7" })
|
|
256
|
+
] }),
|
|
257
|
+
children
|
|
258
|
+
]
|
|
259
|
+
}
|
|
260
|
+
),
|
|
261
|
+
open ? null : /* @__PURE__ */ jsx(
|
|
262
|
+
"button",
|
|
263
|
+
{
|
|
264
|
+
type: "button",
|
|
265
|
+
className: launcher ? "bsw-an-frame__fab" : "bsw-an-frame__trigger",
|
|
266
|
+
"aria-label": launcher ? "Open answers" : void 0,
|
|
267
|
+
onClick: () => setOpen(true),
|
|
268
|
+
children: launcher ? "Ask" : label
|
|
269
|
+
}
|
|
270
|
+
)
|
|
271
|
+
] });
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// src/markdown.tsx
|
|
275
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
276
|
+
function inlineMarkup(text, allowed, keyPrefix) {
|
|
277
|
+
const pattern = /(\*\*[^*\n]+\*\*|(?<![A-Za-z0-9])_[^_\n]+_(?![A-Za-z0-9])|`[^`\n]+`|\[[^\]\n]+\]\(https?:\/\/[^)\s]+\))/g;
|
|
278
|
+
const parts = [];
|
|
279
|
+
let last = 0;
|
|
280
|
+
let index = 0;
|
|
281
|
+
let match;
|
|
282
|
+
while (match = pattern.exec(text)) {
|
|
283
|
+
if (match.index > last) parts.push(text.slice(last, match.index));
|
|
284
|
+
const token = match[0];
|
|
285
|
+
const key = `${keyPrefix}-${index++}`;
|
|
286
|
+
if (token.startsWith("**") && token.endsWith("**") && token.length >= 4) {
|
|
287
|
+
parts.push(/* @__PURE__ */ jsx2("strong", { children: token.slice(2, -2) }, key));
|
|
288
|
+
} else if (token.startsWith("_") && token.endsWith("_") && token.length >= 3) {
|
|
289
|
+
parts.push(/* @__PURE__ */ jsx2("em", { children: token.slice(1, -1) }, key));
|
|
290
|
+
} else if (token.startsWith("`") && token.endsWith("`") && token.length >= 3) {
|
|
291
|
+
parts.push(/* @__PURE__ */ jsx2("code", { children: token.slice(1, -1) }, key));
|
|
292
|
+
} else if (token.startsWith("[")) {
|
|
293
|
+
const link = /^\[([^\]\n]+)\]\((https?:\/\/[^)\s]+)\)$/.exec(token);
|
|
294
|
+
const href = link?.[2] ?? "";
|
|
295
|
+
const safe = safeHttpUrl(href);
|
|
296
|
+
if (link && safe && (allowed.has(href) || allowed.has(safe))) {
|
|
297
|
+
parts.push(
|
|
298
|
+
/* @__PURE__ */ jsx2("a", { href: safe, target: "_blank", rel: "noopener noreferrer", children: link[1] }, key)
|
|
299
|
+
);
|
|
300
|
+
} else {
|
|
301
|
+
parts.push(token);
|
|
302
|
+
}
|
|
303
|
+
} else {
|
|
304
|
+
parts.push(token);
|
|
305
|
+
}
|
|
306
|
+
last = match.index + token.length;
|
|
307
|
+
}
|
|
308
|
+
if (last < text.length) parts.push(text.slice(last));
|
|
309
|
+
return parts;
|
|
310
|
+
}
|
|
311
|
+
function renderAnswerMarkdown(text, allowedUrls) {
|
|
312
|
+
const lines = text.split("\n");
|
|
313
|
+
const nodes = [];
|
|
314
|
+
let paragraph = [];
|
|
315
|
+
let list = [];
|
|
316
|
+
let key = 0;
|
|
317
|
+
function flushParagraph() {
|
|
318
|
+
if (!paragraph.length) return;
|
|
319
|
+
const content = paragraph.join("\n");
|
|
320
|
+
nodes.push(/* @__PURE__ */ jsx2("p", { children: inlineMarkup(content, allowedUrls, `p-${key}`) }, `p-${key++}`));
|
|
321
|
+
paragraph = [];
|
|
322
|
+
}
|
|
323
|
+
function flushList() {
|
|
324
|
+
if (!list.length) return;
|
|
325
|
+
const items = list;
|
|
326
|
+
nodes.push(
|
|
327
|
+
/* @__PURE__ */ jsx2("ul", { children: items.map((item, index) => /* @__PURE__ */ jsx2("li", { children: inlineMarkup(item, allowedUrls, `li-${key}-${index}`) }, `li-${key}-${index}`)) }, `ul-${key++}`)
|
|
328
|
+
);
|
|
329
|
+
list = [];
|
|
330
|
+
}
|
|
331
|
+
for (const line of lines) {
|
|
332
|
+
const item = /^\s*[-*]\s+(.*)$/.exec(line);
|
|
333
|
+
if (item) {
|
|
334
|
+
flushParagraph();
|
|
335
|
+
list.push(item[1] ?? "");
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
if (!line.trim()) {
|
|
339
|
+
flushParagraph();
|
|
340
|
+
flushList();
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
flushList();
|
|
344
|
+
paragraph.push(line);
|
|
345
|
+
}
|
|
346
|
+
flushParagraph();
|
|
347
|
+
flushList();
|
|
348
|
+
return nodes;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// src/session.ts
|
|
352
|
+
var SESSION_ID = /^ans_[0-9a-f]{24}$/;
|
|
353
|
+
function sessionStorageKey(appId) {
|
|
354
|
+
const scope = appId?.trim() ? appId.trim() : "local";
|
|
355
|
+
return `bsw_an_session:${scope}`;
|
|
356
|
+
}
|
|
357
|
+
function createOpaqueSessionId() {
|
|
358
|
+
const bytes = new Uint8Array(12);
|
|
359
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
360
|
+
let hex = "";
|
|
361
|
+
for (const byte of bytes) hex += byte.toString(16).padStart(2, "0");
|
|
362
|
+
return `ans_${hex}`;
|
|
363
|
+
}
|
|
364
|
+
function readSessionId(storage, appId) {
|
|
365
|
+
if (!storage) return createOpaqueSessionId();
|
|
366
|
+
const key = sessionStorageKey(appId);
|
|
367
|
+
const existing = storage.getItem(key);
|
|
368
|
+
if (existing && SESSION_ID.test(existing)) return existing;
|
|
369
|
+
const id = createOpaqueSessionId();
|
|
370
|
+
storage.setItem(key, id);
|
|
371
|
+
return id;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// src/Answers.tsx
|
|
375
|
+
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
376
|
+
var LIMIT_MESSAGE = "Questions are limited to 1,000 characters.";
|
|
377
|
+
var ERROR_MESSAGE = "Something went wrong. Your question is still here. Try again.";
|
|
378
|
+
var FEEDBACK_ERROR = "Feedback could not be saved. Try again.";
|
|
379
|
+
function useSessionId(appId) {
|
|
380
|
+
const key = sessionStorageKey(appId);
|
|
381
|
+
const [state, setState] = useState2(() => ({
|
|
382
|
+
key,
|
|
383
|
+
id: readSessionId(typeof window === "undefined" ? null : window.sessionStorage, appId)
|
|
384
|
+
}));
|
|
385
|
+
if (state.key !== key) {
|
|
386
|
+
setState({
|
|
387
|
+
key,
|
|
388
|
+
id: readSessionId(typeof window === "undefined" ? null : window.sessionStorage, appId)
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
return state.id;
|
|
392
|
+
}
|
|
393
|
+
function sourcesLabel(count) {
|
|
394
|
+
return count === 1 ? "1 source" : `${count} sources`;
|
|
395
|
+
}
|
|
396
|
+
function CitationSources({ citations }) {
|
|
397
|
+
if (!citations.length) return null;
|
|
398
|
+
return /* @__PURE__ */ jsxs2("details", { className: "bsw-an__sources-wrap", children: [
|
|
399
|
+
/* @__PURE__ */ jsx3("summary", { className: "bsw-an__sources-toggle", children: sourcesLabel(citations.length) }),
|
|
400
|
+
/* @__PURE__ */ jsx3("ul", { className: "bsw-an__sources", children: citations.map((citation) => /* @__PURE__ */ jsxs2("li", { className: "bsw-an__source", "data-citation-id": citation.id, children: [
|
|
401
|
+
citation.url ? /* @__PURE__ */ jsx3("a", { href: citation.url, target: "_blank", rel: "noopener noreferrer", children: citation.title }) : /* @__PURE__ */ jsx3("span", { className: "bsw-an__source-title", children: citation.title }),
|
|
402
|
+
citation.excerpt ? /* @__PURE__ */ jsx3("p", { className: "bsw-an__excerpt", children: citation.excerpt }) : null
|
|
403
|
+
] }, citation.id)) })
|
|
404
|
+
] });
|
|
405
|
+
}
|
|
406
|
+
function pageContextForAsk(explicit) {
|
|
407
|
+
if (explicit) {
|
|
408
|
+
const url = typeof explicit.url === "string" ? explicit.url : void 0;
|
|
409
|
+
const title = typeof explicit.title === "string" ? explicit.title : void 0;
|
|
410
|
+
return { url, title };
|
|
411
|
+
}
|
|
412
|
+
if (typeof window === "undefined") return {};
|
|
413
|
+
return { url: window.location.href, title: document.title };
|
|
414
|
+
}
|
|
415
|
+
function Answers(props) {
|
|
416
|
+
const presentation = props.presentation ?? "launcher";
|
|
417
|
+
const sessionId = useSessionId(props.appId);
|
|
418
|
+
const localRef = useRef(null);
|
|
419
|
+
const adapter = props.adapter ?? (props.publishableKey ? null : localRef.current ??= createLocalAdapter());
|
|
420
|
+
const mode = props.adapter ? "byo" : props.publishableKey ? "unwired" : "local";
|
|
421
|
+
const [draft, setDraft] = useState2("");
|
|
422
|
+
const [pending, setPending] = useState2(false);
|
|
423
|
+
const [pendingQuestion, setPendingQuestion] = useState2(null);
|
|
424
|
+
const [error, setError] = useState2(null);
|
|
425
|
+
const [turns, setTurns] = useState2([]);
|
|
426
|
+
const pendingRef = useRef(false);
|
|
427
|
+
const logRef = useRef(null);
|
|
428
|
+
const feedbackLock = useRef(/* @__PURE__ */ new Set());
|
|
429
|
+
const onFallbackRef = useRef(props.onFallback);
|
|
430
|
+
onFallbackRef.current = props.onFallback;
|
|
431
|
+
useEffect2(() => {
|
|
432
|
+
const log = logRef.current;
|
|
433
|
+
if (!log) return;
|
|
434
|
+
log.scrollTop = log.scrollHeight;
|
|
435
|
+
}, [turns, pending, pendingQuestion, error]);
|
|
436
|
+
const trimmed = draft.trim();
|
|
437
|
+
const length = codePointLength(trimmed);
|
|
438
|
+
const overLimit = length > QUESTION_MAX;
|
|
439
|
+
const inputId = useId();
|
|
440
|
+
async function onSubmit(event) {
|
|
441
|
+
event.preventDefault();
|
|
442
|
+
if (!adapter || pendingRef.current) return;
|
|
443
|
+
if (!trimmed) {
|
|
444
|
+
setError("Enter a question.");
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
if (overLimit) {
|
|
448
|
+
setError(LIMIT_MESSAGE);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
const question = trimmed;
|
|
452
|
+
const pageContext = pageContextForAsk(props.pageContext);
|
|
453
|
+
pendingRef.current = true;
|
|
454
|
+
setPending(true);
|
|
455
|
+
setPendingQuestion(question);
|
|
456
|
+
setDraft("");
|
|
457
|
+
setError(null);
|
|
458
|
+
try {
|
|
459
|
+
const raw = await adapter.ask({ sessionId, question, pageContext });
|
|
460
|
+
const result = sanitizeResult(raw);
|
|
461
|
+
if (!result) {
|
|
462
|
+
setDraft(question);
|
|
463
|
+
setError(ERROR_MESSAGE);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
if (result.outcome === "unanswered") onFallbackRef.current?.(result);
|
|
467
|
+
setTurns((current) => [...current, { question, result, feedback: "idle" }]);
|
|
468
|
+
} catch {
|
|
469
|
+
setDraft(question);
|
|
470
|
+
setError(ERROR_MESSAGE);
|
|
471
|
+
} finally {
|
|
472
|
+
pendingRef.current = false;
|
|
473
|
+
setPending(false);
|
|
474
|
+
setPendingQuestion(null);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
async function onFeedback(answerId, helpful) {
|
|
478
|
+
if (!adapter?.feedback || feedbackLock.current.has(answerId)) return;
|
|
479
|
+
const current = turns.find((turn) => turn.result.answerId === answerId);
|
|
480
|
+
if (!current || current.feedback === "pending" || current.feedback === "done") return;
|
|
481
|
+
feedbackLock.current.add(answerId);
|
|
482
|
+
setTurns((items) => items.map((turn) => turn.result.answerId === answerId ? { ...turn, feedback: "pending" } : turn));
|
|
483
|
+
try {
|
|
484
|
+
await adapter.feedback({ sessionId, answerId, helpful });
|
|
485
|
+
setTurns((items) => items.map((turn) => turn.result.answerId === answerId ? { ...turn, feedback: "done" } : turn));
|
|
486
|
+
} catch {
|
|
487
|
+
feedbackLock.current.delete(answerId);
|
|
488
|
+
setTurns((items) => items.map((turn) => turn.result.answerId === answerId ? { ...turn, feedback: "error" } : turn));
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
const panel = /* @__PURE__ */ jsxs2(
|
|
492
|
+
"div",
|
|
493
|
+
{
|
|
494
|
+
className: [WIDGET_ROOT_CLASS, presentation === "section" ? props.className : ""].filter(Boolean).join(" "),
|
|
495
|
+
"data-theme": props.theme,
|
|
496
|
+
"data-mode": mode,
|
|
497
|
+
children: [
|
|
498
|
+
/* @__PURE__ */ jsxs2("header", { className: "bsw-an__header", children: [
|
|
499
|
+
/* @__PURE__ */ jsx3("span", { className: "bsw-an__mark", "aria-hidden": "true" }),
|
|
500
|
+
/* @__PURE__ */ jsxs2("div", { className: "bsw-an__heading", children: [
|
|
501
|
+
/* @__PURE__ */ jsx3("h2", { className: "bsw-an__title", children: "Answers" }),
|
|
502
|
+
mode === "local" ? /* @__PURE__ */ jsx3("p", { className: "bsw-an__note", children: "Sample answers for Northwind Outfitters." }) : null
|
|
503
|
+
] })
|
|
504
|
+
] }),
|
|
505
|
+
/* @__PURE__ */ jsx3("div", { className: "bsw-an__log", ref: logRef, children: turns.length === 0 && !pendingQuestion ? /* @__PURE__ */ jsx3("p", { className: "bsw-an__empty", children: mode === "unwired" ? "This page is not ready to answer yet." : mode === "local" ? "Ask a question. The sample covers hours, returns, and shipping for Northwind Outfitters." : "Ask a question about this page." }) : /* @__PURE__ */ jsxs2("ol", { className: "bsw-an__turns", children: [
|
|
506
|
+
turns.map((turn, index) => {
|
|
507
|
+
const citations = turn.result.outcome === "answered" ? turn.result.citations ?? [] : [];
|
|
508
|
+
const allowed = new Set(citations.map((citation) => citation.url).filter((url) => Boolean(url)));
|
|
509
|
+
return /* @__PURE__ */ jsxs2("li", { className: "bsw-an__turn", "data-outcome": turn.result.outcome, children: [
|
|
510
|
+
/* @__PURE__ */ jsx3("div", { className: "bsw-an__row bsw-an__row--visitor", children: /* @__PURE__ */ jsx3("p", { className: "bsw-an__question", children: turn.question }) }),
|
|
511
|
+
/* @__PURE__ */ jsxs2("div", { className: "bsw-an__row bsw-an__row--reply", children: [
|
|
512
|
+
turn.result.outcome === "answered" && turn.result.answer ? /* @__PURE__ */ jsx3("div", { className: "bsw-an__answer", children: renderAnswerMarkdown(turn.result.answer, allowed) }) : /* @__PURE__ */ jsx3("p", { className: "bsw-an__fallback", children: turn.result.fallback }),
|
|
513
|
+
/* @__PURE__ */ jsx3(CitationSources, { citations }),
|
|
514
|
+
adapter?.feedback ? turn.feedback === "done" ? /* @__PURE__ */ jsx3("p", { className: "bsw-an__thanks", children: "Thanks for the feedback." }) : /* @__PURE__ */ jsxs2("div", { className: "bsw-an__feedback", children: [
|
|
515
|
+
/* @__PURE__ */ jsx3(
|
|
516
|
+
"button",
|
|
517
|
+
{
|
|
518
|
+
type: "button",
|
|
519
|
+
className: "bsw-an__button bsw-an__button--ghost",
|
|
520
|
+
disabled: turn.feedback === "pending",
|
|
521
|
+
onClick: () => void onFeedback(turn.result.answerId, true),
|
|
522
|
+
children: "Helpful"
|
|
523
|
+
}
|
|
524
|
+
),
|
|
525
|
+
/* @__PURE__ */ jsx3(
|
|
526
|
+
"button",
|
|
527
|
+
{
|
|
528
|
+
type: "button",
|
|
529
|
+
className: "bsw-an__button bsw-an__button--ghost",
|
|
530
|
+
disabled: turn.feedback === "pending",
|
|
531
|
+
onClick: () => void onFeedback(turn.result.answerId, false),
|
|
532
|
+
children: "Not helpful"
|
|
533
|
+
}
|
|
534
|
+
),
|
|
535
|
+
turn.feedback === "error" ? /* @__PURE__ */ jsx3("p", { className: "bsw-an__error", role: "alert", children: FEEDBACK_ERROR }) : null
|
|
536
|
+
] }) : null
|
|
537
|
+
] })
|
|
538
|
+
] }, `${turn.result.answerId}:${index}`);
|
|
539
|
+
}),
|
|
540
|
+
pendingQuestion ? /* @__PURE__ */ jsxs2("li", { className: "bsw-an__turn", "data-pending": "true", children: [
|
|
541
|
+
/* @__PURE__ */ jsx3("div", { className: "bsw-an__row bsw-an__row--visitor", children: /* @__PURE__ */ jsx3("p", { className: "bsw-an__question", children: pendingQuestion }) }),
|
|
542
|
+
/* @__PURE__ */ jsx3("div", { className: "bsw-an__row bsw-an__row--reply", children: /* @__PURE__ */ jsxs2("p", { className: "bsw-an__status", role: "status", children: [
|
|
543
|
+
/* @__PURE__ */ jsxs2("span", { className: "bsw-an__dots", "aria-hidden": "true", children: [
|
|
544
|
+
/* @__PURE__ */ jsx3("i", {}),
|
|
545
|
+
/* @__PURE__ */ jsx3("i", {}),
|
|
546
|
+
/* @__PURE__ */ jsx3("i", {})
|
|
547
|
+
] }),
|
|
548
|
+
"Looking that up."
|
|
549
|
+
] }) })
|
|
550
|
+
] }) : null
|
|
551
|
+
] }) }),
|
|
552
|
+
/* @__PURE__ */ jsxs2("form", { className: "bsw-an__composer", autoComplete: "off", onSubmit: (event) => void onSubmit(event), children: [
|
|
553
|
+
error ? /* @__PURE__ */ jsx3("p", { className: "bsw-an__error", role: "alert", children: error }) : null,
|
|
554
|
+
/* @__PURE__ */ jsx3("label", { className: "bsw-an__label", htmlFor: inputId, children: "Question" }),
|
|
555
|
+
/* @__PURE__ */ jsxs2("div", { className: "bsw-an__composer-row", children: [
|
|
556
|
+
/* @__PURE__ */ jsx3(
|
|
557
|
+
"textarea",
|
|
558
|
+
{
|
|
559
|
+
id: inputId,
|
|
560
|
+
className: "bsw-an__textarea",
|
|
561
|
+
rows: 2,
|
|
562
|
+
value: draft,
|
|
563
|
+
disabled: !adapter || pending,
|
|
564
|
+
placeholder: mode === "local" ? "Ask about hours, returns, or shipping" : "Ask a question",
|
|
565
|
+
enterKeyHint: "send",
|
|
566
|
+
onChange: (event) => {
|
|
567
|
+
setDraft(event.target.value);
|
|
568
|
+
if (error) setError(null);
|
|
569
|
+
},
|
|
570
|
+
onKeyDown: (event) => {
|
|
571
|
+
if (event.key !== "Enter" || event.shiftKey) return;
|
|
572
|
+
event.preventDefault();
|
|
573
|
+
event.currentTarget.form?.requestSubmit();
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
),
|
|
577
|
+
/* @__PURE__ */ jsx3("button", { className: "bsw-an__button", type: "submit", disabled: !adapter || pending || !trimmed || overLimit, children: "Ask" })
|
|
578
|
+
] }),
|
|
579
|
+
overLimit ? /* @__PURE__ */ jsxs2("p", { className: "bsw-an__count", children: [
|
|
580
|
+
length,
|
|
581
|
+
" / 1,000",
|
|
582
|
+
/* @__PURE__ */ jsxs2("span", { className: "bsw-an__error", children: [
|
|
583
|
+
" ",
|
|
584
|
+
LIMIT_MESSAGE
|
|
585
|
+
] })
|
|
586
|
+
] }) : length > 800 ? /* @__PURE__ */ jsxs2("p", { className: "bsw-an__count", children: [
|
|
587
|
+
length,
|
|
588
|
+
" / 1,000"
|
|
589
|
+
] }) : null
|
|
590
|
+
] })
|
|
591
|
+
]
|
|
592
|
+
}
|
|
593
|
+
);
|
|
594
|
+
if (presentation === "section") return panel;
|
|
595
|
+
return /* @__PURE__ */ jsx3(EmbedFrame, { presentation, defaultOpen: props.defaultOpen, label: "Answers", className: props.className, theme: props.theme, children: panel });
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// src/mount.tsx
|
|
599
|
+
import { createRoot } from "react-dom/client";
|
|
600
|
+
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
601
|
+
var roots = /* @__PURE__ */ new WeakMap();
|
|
602
|
+
function mount(el, props = {}) {
|
|
603
|
+
let root = roots.get(el);
|
|
604
|
+
if (!root) {
|
|
605
|
+
root = createRoot(el);
|
|
606
|
+
roots.set(el, root);
|
|
607
|
+
}
|
|
608
|
+
root.render(/* @__PURE__ */ jsx4(Answers, { ...props }));
|
|
609
|
+
return () => unmount(el);
|
|
610
|
+
}
|
|
611
|
+
function unmount(el) {
|
|
612
|
+
const root = roots.get(el);
|
|
613
|
+
if (!root) return;
|
|
614
|
+
root.unmount();
|
|
615
|
+
roots.delete(el);
|
|
616
|
+
}
|
|
617
|
+
export {
|
|
618
|
+
Answers,
|
|
619
|
+
LOCAL_CITATIONS,
|
|
620
|
+
LOCAL_FALLBACK,
|
|
621
|
+
LOCAL_PROMPTS,
|
|
622
|
+
QUESTION_MAX,
|
|
623
|
+
WIDGET_ROOT_CLASS,
|
|
624
|
+
createByoAdapter,
|
|
625
|
+
createLocalAdapter,
|
|
626
|
+
mount,
|
|
627
|
+
unmount
|
|
628
|
+
};
|
|
629
|
+
//# sourceMappingURL=index.js.map
|