@huskel/sdk 0.3.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +56 -3
- package/dist/index.d.ts +56 -3
- package/dist/index.js +307 -21
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +303 -19
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -32,21 +32,33 @@ async function sleep(ms) {
|
|
|
32
32
|
return new Promise((r) => setTimeout(r, ms));
|
|
33
33
|
}
|
|
34
34
|
var HuskelAPI = class {
|
|
35
|
-
constructor(apiUrl, siteId, apiToken) {
|
|
35
|
+
constructor(apiUrl, siteId, apiToken, getShopperId, getSessionId) {
|
|
36
36
|
this.apiUrl = apiUrl;
|
|
37
37
|
this.siteId = siteId;
|
|
38
38
|
this.apiToken = apiToken;
|
|
39
|
+
this.getShopperId = getShopperId;
|
|
40
|
+
this.getSessionId = getSessionId;
|
|
39
41
|
}
|
|
40
42
|
async post(path, body, attempt = 0) {
|
|
43
|
+
var _a, _b;
|
|
41
44
|
const url = `${this.apiUrl}${path}`;
|
|
42
45
|
try {
|
|
46
|
+
const headers = {
|
|
47
|
+
"Content-Type": "application/json",
|
|
48
|
+
"X-Huskel-Token": this.apiToken,
|
|
49
|
+
"X-Huskel-Site": this.siteId
|
|
50
|
+
};
|
|
51
|
+
const shopperId = (_a = this.getShopperId) == null ? void 0 : _a.call(this);
|
|
52
|
+
if (shopperId) {
|
|
53
|
+
headers["X-Huskel-Shopper-Id"] = shopperId;
|
|
54
|
+
}
|
|
55
|
+
const sessionId = (_b = this.getSessionId) == null ? void 0 : _b.call(this);
|
|
56
|
+
if (sessionId) {
|
|
57
|
+
headers["X-Huskel-Session-Id"] = sessionId;
|
|
58
|
+
}
|
|
43
59
|
const res = await fetch(url, {
|
|
44
60
|
method: "POST",
|
|
45
|
-
headers
|
|
46
|
-
"Content-Type": "application/json",
|
|
47
|
-
"X-Huskel-Token": this.apiToken,
|
|
48
|
-
"X-Huskel-Site": this.siteId
|
|
49
|
-
},
|
|
61
|
+
headers,
|
|
50
62
|
body: JSON.stringify(body)
|
|
51
63
|
});
|
|
52
64
|
if (!res.ok) {
|
|
@@ -89,6 +101,15 @@ var HuskelAPI = class {
|
|
|
89
101
|
log("info", "search query", query);
|
|
90
102
|
return this.post("/search", { query, siteId: this.siteId, limit });
|
|
91
103
|
}
|
|
104
|
+
// Pure vector search — no LLM, instant results. This is what the SearchBar uses.
|
|
105
|
+
async searchVector(query, limit = 10) {
|
|
106
|
+
return this.post("/search/vector", { query, siteId: this.siteId, limit });
|
|
107
|
+
}
|
|
108
|
+
// LLM chat — conversational search with history context.
|
|
109
|
+
async chat(query, history = []) {
|
|
110
|
+
log("info", "chat query", query);
|
|
111
|
+
return this.post("/chat", { query, siteId: this.siteId, history });
|
|
112
|
+
}
|
|
92
113
|
};
|
|
93
114
|
|
|
94
115
|
// src/client.ts
|
|
@@ -172,19 +193,39 @@ function mapRawProduct(input) {
|
|
|
172
193
|
slug
|
|
173
194
|
};
|
|
174
195
|
}
|
|
175
|
-
|
|
196
|
+
function generateUUID() {
|
|
197
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
198
|
+
return crypto.randomUUID();
|
|
199
|
+
}
|
|
200
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
201
|
+
const r = Math.random() * 16 | 0;
|
|
202
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
203
|
+
return v.toString(16);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
var _HuskelClient = class _HuskelClient {
|
|
176
207
|
constructor(config) {
|
|
177
208
|
this.ingestQueue = [];
|
|
178
209
|
this.ingestTimer = null;
|
|
179
210
|
this.ingestedUrls = /* @__PURE__ */ new Set();
|
|
180
211
|
this.onlineHandler = null;
|
|
212
|
+
this.sessionId = "";
|
|
181
213
|
const siteId = config.siteId || getEnvVar("NEXT_PUBLIC_HUSKEL_SITE_ID") || "";
|
|
182
214
|
const apiUrl = config.apiUrl || getEnvVar("NEXT_PUBLIC_HUSKEL_API_URL") || "";
|
|
183
215
|
const apiToken = config.apiToken || getEnvVar("NEXT_PUBLIC_HUSKEL_API_TOKEN") || "";
|
|
184
216
|
if (!siteId) console.error('[Huskel] Missing siteId. Set it via <HuskelProvider siteId="..."> or NEXT_PUBLIC_HUSKEL_SITE_ID.');
|
|
185
217
|
if (!apiUrl) console.error('[Huskel] Missing apiUrl. Set it via <HuskelProvider apiUrl="..."> or NEXT_PUBLIC_HUSKEL_API_URL.');
|
|
186
218
|
if (!apiToken) console.error('[Huskel] Missing apiToken. Set it via <HuskelProvider apiToken="..."> or NEXT_PUBLIC_HUSKEL_API_TOKEN.');
|
|
187
|
-
this.
|
|
219
|
+
this.shopperId = config.shopperId;
|
|
220
|
+
this.initSession();
|
|
221
|
+
this.loadIngestedCache();
|
|
222
|
+
this.api = new HuskelAPI(
|
|
223
|
+
apiUrl,
|
|
224
|
+
siteId,
|
|
225
|
+
apiToken,
|
|
226
|
+
() => this.shopperId,
|
|
227
|
+
() => this.sessionId
|
|
228
|
+
);
|
|
188
229
|
instance = this;
|
|
189
230
|
if (typeof window !== "undefined") {
|
|
190
231
|
this.onlineHandler = () => {
|
|
@@ -194,6 +235,55 @@ var HuskelClient = class {
|
|
|
194
235
|
window.addEventListener("online", this.onlineHandler);
|
|
195
236
|
}
|
|
196
237
|
}
|
|
238
|
+
// 24h
|
|
239
|
+
loadIngestedCache() {
|
|
240
|
+
if (typeof window === "undefined") return;
|
|
241
|
+
try {
|
|
242
|
+
const raw = localStorage.getItem(_HuskelClient.INGEST_CACHE_KEY);
|
|
243
|
+
if (!raw) return;
|
|
244
|
+
const { ts, urls } = JSON.parse(raw);
|
|
245
|
+
if (Date.now() - ts > _HuskelClient.INGEST_CACHE_TTL) {
|
|
246
|
+
localStorage.removeItem(_HuskelClient.INGEST_CACHE_KEY);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
this.ingestedUrls = new Set(urls);
|
|
250
|
+
} catch (e) {
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
saveIngestedCache() {
|
|
254
|
+
if (typeof window === "undefined") return;
|
|
255
|
+
try {
|
|
256
|
+
localStorage.setItem(
|
|
257
|
+
_HuskelClient.INGEST_CACHE_KEY,
|
|
258
|
+
JSON.stringify({ ts: Date.now(), urls: [...this.ingestedUrls] })
|
|
259
|
+
);
|
|
260
|
+
} catch (e) {
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
setShopperId(id) {
|
|
264
|
+
this.shopperId = id;
|
|
265
|
+
}
|
|
266
|
+
getShopperId() {
|
|
267
|
+
return this.shopperId;
|
|
268
|
+
}
|
|
269
|
+
getSessionId() {
|
|
270
|
+
return this.sessionId;
|
|
271
|
+
}
|
|
272
|
+
initSession() {
|
|
273
|
+
if (typeof window !== "undefined" && window.sessionStorage) {
|
|
274
|
+
try {
|
|
275
|
+
let sid = window.sessionStorage.getItem("huskel_session_id");
|
|
276
|
+
if (!sid) {
|
|
277
|
+
sid = generateUUID();
|
|
278
|
+
window.sessionStorage.setItem("huskel_session_id", sid);
|
|
279
|
+
}
|
|
280
|
+
this.sessionId = sid;
|
|
281
|
+
return;
|
|
282
|
+
} catch (e) {
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
this.sessionId = generateUUID();
|
|
286
|
+
}
|
|
197
287
|
destroy() {
|
|
198
288
|
if (typeof window !== "undefined" && this.onlineHandler) {
|
|
199
289
|
window.removeEventListener("online", this.onlineHandler);
|
|
@@ -212,6 +302,7 @@ var HuskelClient = class {
|
|
|
212
302
|
return;
|
|
213
303
|
}
|
|
214
304
|
this.ingestedUrls.add(product.url);
|
|
305
|
+
this.saveIngestedCache();
|
|
215
306
|
this.ingestQueue.push(product);
|
|
216
307
|
this.scheduleFlush();
|
|
217
308
|
}
|
|
@@ -226,6 +317,7 @@ var HuskelClient = class {
|
|
|
226
317
|
this.ingestQueue.push(product);
|
|
227
318
|
});
|
|
228
319
|
if (this.ingestQueue.length > 0) {
|
|
320
|
+
this.saveIngestedCache();
|
|
229
321
|
this.scheduleFlush();
|
|
230
322
|
}
|
|
231
323
|
}
|
|
@@ -257,6 +349,9 @@ var HuskelClient = class {
|
|
|
257
349
|
}
|
|
258
350
|
}
|
|
259
351
|
};
|
|
352
|
+
_HuskelClient.INGEST_CACHE_KEY = "huskel_ingested_v1";
|
|
353
|
+
_HuskelClient.INGEST_CACHE_TTL = 24 * 60 * 60 * 1e3;
|
|
354
|
+
var HuskelClient = _HuskelClient;
|
|
260
355
|
var instance = null;
|
|
261
356
|
function initHuskel(config) {
|
|
262
357
|
instance = new HuskelClient(config);
|
|
@@ -294,11 +389,15 @@ import { useState, useCallback, useRef as useRef3 } from "react";
|
|
|
294
389
|
import { createContext, useContext, useEffect, useRef as useRef2 } from "react";
|
|
295
390
|
import { jsx } from "react/jsx-runtime";
|
|
296
391
|
var HuskelContext = createContext(null);
|
|
297
|
-
function HuskelProvider({ siteId, apiUrl, apiToken, children }) {
|
|
392
|
+
function HuskelProvider({ siteId, apiUrl, apiToken, shopperId, children }) {
|
|
298
393
|
const clientRef = useRef2(null);
|
|
299
394
|
if (!clientRef.current) {
|
|
300
|
-
clientRef.current = new HuskelClient({ siteId, apiUrl, apiToken });
|
|
395
|
+
clientRef.current = new HuskelClient({ siteId, apiUrl, apiToken, shopperId });
|
|
301
396
|
}
|
|
397
|
+
useEffect(() => {
|
|
398
|
+
var _a;
|
|
399
|
+
(_a = clientRef.current) == null ? void 0 : _a.setShopperId(shopperId);
|
|
400
|
+
}, [shopperId]);
|
|
302
401
|
useEffect(() => {
|
|
303
402
|
return () => {
|
|
304
403
|
var _a;
|
|
@@ -333,7 +432,7 @@ function useSearch() {
|
|
|
333
432
|
setLoading(true);
|
|
334
433
|
setError(null);
|
|
335
434
|
try {
|
|
336
|
-
const res = await client.api.
|
|
435
|
+
const res = await client.api.searchVector(query, limit);
|
|
337
436
|
setResults((_b = res.results) != null ? _b : []);
|
|
338
437
|
} catch (e) {
|
|
339
438
|
setError((_c = e.message) != null ? _c : "Search failed");
|
|
@@ -399,8 +498,47 @@ function usePageIngest(product) {
|
|
|
399
498
|
}, [(_a = product == null ? void 0 : product.url) != null ? _a : product == null ? void 0 : product.name]);
|
|
400
499
|
}
|
|
401
500
|
|
|
501
|
+
// src/hooks/useChat.ts
|
|
502
|
+
import { useState as useState3, useCallback as useCallback3, useRef as useRef5 } from "react";
|
|
503
|
+
function useChat() {
|
|
504
|
+
const client = useHuskelContext();
|
|
505
|
+
const [messages, setMessages] = useState3([]);
|
|
506
|
+
const [sources, setSources] = useState3([]);
|
|
507
|
+
const [loading, setLoading] = useState3(false);
|
|
508
|
+
const [error, setError] = useState3(null);
|
|
509
|
+
const abortRef = useRef5(null);
|
|
510
|
+
const send = useCallback3(async (query) => {
|
|
511
|
+
var _a, _b, _c;
|
|
512
|
+
if (!query.trim() || loading) return;
|
|
513
|
+
(_a = abortRef.current) == null ? void 0 : _a.abort();
|
|
514
|
+
abortRef.current = new AbortController();
|
|
515
|
+
const userMsg = { role: "user", content: query };
|
|
516
|
+
setMessages((prev) => [...prev, userMsg]);
|
|
517
|
+
setLoading(true);
|
|
518
|
+
setError(null);
|
|
519
|
+
try {
|
|
520
|
+
const history = messages.map((m) => ({ role: m.role, content: m.content }));
|
|
521
|
+
const res = await client.api.chat(query, history);
|
|
522
|
+
const assistantMsg = { role: "assistant", content: res.answer };
|
|
523
|
+
setMessages((prev) => [...prev, assistantMsg]);
|
|
524
|
+
setSources((_b = res.sources) != null ? _b : []);
|
|
525
|
+
} catch (e) {
|
|
526
|
+
setError((_c = e == null ? void 0 : e.message) != null ? _c : "Chat request failed");
|
|
527
|
+
setMessages((prev) => prev.slice(0, -1));
|
|
528
|
+
} finally {
|
|
529
|
+
setLoading(false);
|
|
530
|
+
}
|
|
531
|
+
}, [client, messages, loading]);
|
|
532
|
+
const reset = useCallback3(() => {
|
|
533
|
+
setMessages([]);
|
|
534
|
+
setSources([]);
|
|
535
|
+
setError(null);
|
|
536
|
+
}, []);
|
|
537
|
+
return { messages, sources, loading, error, send, reset };
|
|
538
|
+
}
|
|
539
|
+
|
|
402
540
|
// src/components/SearchBar.tsx
|
|
403
|
-
import { useState as
|
|
541
|
+
import { useState as useState4, useEffect as useEffect3, useRef as useRef6 } from "react";
|
|
404
542
|
import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
405
543
|
var S = `
|
|
406
544
|
.hsk-wrap{position:relative;width:100%;font-family:inherit}
|
|
@@ -424,11 +562,11 @@ function SearchBar({
|
|
|
424
562
|
dropdownClassName,
|
|
425
563
|
renderResult
|
|
426
564
|
}) {
|
|
427
|
-
const [query, setQuery] =
|
|
428
|
-
const [open, setOpen] =
|
|
565
|
+
const [query, setQuery] = useState4("");
|
|
566
|
+
const [open, setOpen] = useState4(false);
|
|
429
567
|
const { results, loading, search, clear } = useSearch();
|
|
430
|
-
const timer =
|
|
431
|
-
const wrap =
|
|
568
|
+
const timer = useRef6();
|
|
569
|
+
const wrap = useRef6(null);
|
|
432
570
|
useEffect3(() => {
|
|
433
571
|
clearTimeout(timer.current);
|
|
434
572
|
if (!query.trim()) {
|
|
@@ -497,7 +635,7 @@ function SearchBar({
|
|
|
497
635
|
}
|
|
498
636
|
|
|
499
637
|
// src/components/Sparkle.tsx
|
|
500
|
-
import { useState as
|
|
638
|
+
import { useState as useState5 } from "react";
|
|
501
639
|
import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
502
640
|
var S2 = `
|
|
503
641
|
.hsk-sparkle{display:inline-flex;align-items:center;gap:5px;padding:4px 10px;font-size:12px;font-weight:600;background:#f47c3c;color:#fff;border:none;border-radius:20px;cursor:pointer;transition:opacity .2s,transform .15s}
|
|
@@ -506,11 +644,11 @@ var S2 = `
|
|
|
506
644
|
`;
|
|
507
645
|
function Sparkle({ productName, limit = 5, onResult, className }) {
|
|
508
646
|
const client = useHuskelContext();
|
|
509
|
-
const [loading, setLoading] =
|
|
647
|
+
const [loading, setLoading] = useState5(false);
|
|
510
648
|
const handleClick = async () => {
|
|
511
649
|
setLoading(true);
|
|
512
650
|
try {
|
|
513
|
-
const res = await client.api.
|
|
651
|
+
const res = await client.api.searchVector(productName, limit);
|
|
514
652
|
onResult == null ? void 0 : onResult(res.results);
|
|
515
653
|
} catch (e) {
|
|
516
654
|
console.error("[Huskel Sparkle]", e);
|
|
@@ -526,7 +664,152 @@ function Sparkle({ productName, limit = 5, onResult, className }) {
|
|
|
526
664
|
] })
|
|
527
665
|
] });
|
|
528
666
|
}
|
|
667
|
+
|
|
668
|
+
// src/components/ChatWidget.tsx
|
|
669
|
+
import { useState as useState6, useRef as useRef7, useEffect as useEffect4 } from "react";
|
|
670
|
+
import { Fragment as Fragment3, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
671
|
+
var S3 = `
|
|
672
|
+
.hsk-chat-widget{display:flex;flex-direction:column;height:100%;min-height:320px;font-family:inherit;background:#0f0f10;border:1px solid #2a2a2d;border-radius:12px;overflow:hidden}
|
|
673
|
+
.hsk-chat-header{display:flex;align-items:center;gap:10px;padding:14px 16px;border-bottom:1px solid #1e1e1f;background:#111112;flex-shrink:0}
|
|
674
|
+
.hsk-chat-title{font-size:14px;font-weight:600;color:#f3f3f2}
|
|
675
|
+
.hsk-chat-badge{font-size:10px;font-weight:700;letter-spacing:0.08em;text-transform:uppercase;color:#ff6a33;background:#ff6a3315;border:1px solid #ff6a3330;padding:2px 8px;border-radius:20px}
|
|
676
|
+
.hsk-chat-messages{flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:12px;scroll-behavior:smooth}
|
|
677
|
+
.hsk-chat-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:8px;color:#555;font-size:13px;text-align:center;padding:24px}
|
|
678
|
+
.hsk-chat-empty-icon{font-size:28px;margin-bottom:4px}
|
|
679
|
+
.hsk-msg-row{display:flex;gap:8px;align-items:flex-start}
|
|
680
|
+
.hsk-msg-row.user{flex-direction:row-reverse}
|
|
681
|
+
.hsk-msg-avatar{width:28px;height:28px;border-radius:50%;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:700}
|
|
682
|
+
.hsk-msg-avatar.ai{background:#ff6a3320;border:1px solid #ff6a3340;color:#ff6a33}
|
|
683
|
+
.hsk-msg-avatar.user{background:#2a2a2d;color:#9a9aa1}
|
|
684
|
+
.hsk-msg-bubble{max-width:78%;padding:10px 14px;border-radius:12px;font-size:13px;line-height:1.6}
|
|
685
|
+
.hsk-msg-bubble.ai{background:#171718;border:1px solid #2a2a2d;color:#e8e8e7;border-radius:4px 12px 12px 12px}
|
|
686
|
+
.hsk-msg-bubble.user{background:#ff6a33;color:#fff;border-radius:12px 4px 12px 12px}
|
|
687
|
+
.hsk-sources{margin-top:10px;display:flex;flex-direction:column;gap:6px}
|
|
688
|
+
.hsk-source-card{display:flex;align-items:center;gap:10px;padding:8px 10px;background:#1a1a1b;border:1px solid #252527;border-radius:8px;cursor:pointer;transition:border-color 0.15s}
|
|
689
|
+
.hsk-source-card:hover{border-color:#ff6a3360}
|
|
690
|
+
.hsk-source-img{width:36px;height:36px;object-fit:cover;border-radius:4px;background:#fff}
|
|
691
|
+
.hsk-source-name{font-size:12px;font-weight:500;color:#e8e8e7;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
692
|
+
.hsk-source-price{font-size:11px;color:#ff6a33;font-weight:700;margin-top:2px}
|
|
693
|
+
.hsk-typing{display:flex;gap:4px;align-items:center;padding:10px 14px;background:#171718;border:1px solid #2a2a2d;border-radius:4px 12px 12px 12px;width:fit-content}
|
|
694
|
+
.hsk-typing-dot{width:6px;height:6px;background:#ff6a33;border-radius:50%;animation:hsk-chat-bounce 1.2s infinite}
|
|
695
|
+
.hsk-typing-dot:nth-child(2){animation-delay:0.2s}
|
|
696
|
+
.hsk-typing-dot:nth-child(3){animation-delay:0.4s}
|
|
697
|
+
@keyframes hsk-chat-bounce{0%,100%{opacity:0.3;transform:translateY(0)}50%{opacity:1;transform:translateY(-4px)}}
|
|
698
|
+
.hsk-chat-input-area{display:flex;align-items:center;gap:8px;padding:12px 14px;border-top:1px solid #1e1e1f;background:#111112;flex-shrink:0}
|
|
699
|
+
.hsk-chat-input{flex:1;background:#1a1a1b;border:1px solid #2a2a2d;border-radius:8px;padding:9px 14px;font-size:13px;color:#f3f3f2;outline:none;font-family:inherit;transition:border-color 0.2s;resize:none;min-height:38px;max-height:120px;line-height:1.5}
|
|
700
|
+
.hsk-chat-input::placeholder{color:#555}
|
|
701
|
+
.hsk-chat-input:focus{border-color:#ff6a33}
|
|
702
|
+
.hsk-chat-send{width:34px;height:34px;border-radius:8px;background:#ff6a33;border:none;color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:16px;transition:opacity 0.15s,transform 0.1s}
|
|
703
|
+
.hsk-chat-send:hover{opacity:0.88}
|
|
704
|
+
.hsk-chat-send:active{transform:scale(0.93)}
|
|
705
|
+
.hsk-chat-send:disabled{opacity:0.4;cursor:not-allowed}
|
|
706
|
+
.hsk-chat-reset{font-size:11px;color:#555;cursor:pointer;padding:0 4px;transition:color 0.15s;background:none;border:none;font-family:inherit}
|
|
707
|
+
.hsk-chat-reset:hover{color:#ff6a33}
|
|
708
|
+
`;
|
|
709
|
+
function SourceCard({ source, onSelect }) {
|
|
710
|
+
var _a;
|
|
711
|
+
return /* @__PURE__ */ jsxs3("div", { className: "hsk-source-card", onClick: () => onSelect == null ? void 0 : onSelect(source), children: [
|
|
712
|
+
source.image && /* @__PURE__ */ jsx4("img", { src: source.image, alt: source.name, className: "hsk-source-img" }),
|
|
713
|
+
/* @__PURE__ */ jsxs3("div", { style: { flex: 1, minWidth: 0 }, children: [
|
|
714
|
+
/* @__PURE__ */ jsx4("div", { className: "hsk-source-name", children: source.name }),
|
|
715
|
+
source.price && /* @__PURE__ */ jsxs3("div", { className: "hsk-source-price", children: [
|
|
716
|
+
(_a = source.currency) != null ? _a : "KES",
|
|
717
|
+
" ",
|
|
718
|
+
source.price
|
|
719
|
+
] })
|
|
720
|
+
] })
|
|
721
|
+
] });
|
|
722
|
+
}
|
|
723
|
+
function ChatWidget({ placeholder = "Ask about anything in our store\u2026", title = "AI Shopping Assistant", className, onSelectSource }) {
|
|
724
|
+
const { messages, sources, loading, error, send, reset } = useChat();
|
|
725
|
+
const [input, setInput] = useState6("");
|
|
726
|
+
const bottomRef = useRef7(null);
|
|
727
|
+
const textareaRef = useRef7(null);
|
|
728
|
+
useEffect4(() => {
|
|
729
|
+
var _a;
|
|
730
|
+
(_a = bottomRef.current) == null ? void 0 : _a.scrollIntoView({ behavior: "smooth" });
|
|
731
|
+
}, [messages, loading]);
|
|
732
|
+
const handleSend = async () => {
|
|
733
|
+
const q = input.trim();
|
|
734
|
+
if (!q || loading) return;
|
|
735
|
+
setInput("");
|
|
736
|
+
if (textareaRef.current) textareaRef.current.style.height = "auto";
|
|
737
|
+
await send(q);
|
|
738
|
+
};
|
|
739
|
+
const handleKey = (e) => {
|
|
740
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
741
|
+
e.preventDefault();
|
|
742
|
+
handleSend();
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
const handleInput = (e) => {
|
|
746
|
+
setInput(e.target.value);
|
|
747
|
+
const t = e.target;
|
|
748
|
+
t.style.height = "auto";
|
|
749
|
+
t.style.height = Math.min(t.scrollHeight, 120) + "px";
|
|
750
|
+
};
|
|
751
|
+
return /* @__PURE__ */ jsxs3(Fragment3, { children: [
|
|
752
|
+
/* @__PURE__ */ jsx4("style", { children: S3 }),
|
|
753
|
+
/* @__PURE__ */ jsxs3("div", { className: `hsk-chat-widget ${className != null ? className : ""}`, children: [
|
|
754
|
+
/* @__PURE__ */ jsxs3("div", { className: "hsk-chat-header", children: [
|
|
755
|
+
/* @__PURE__ */ jsx4("span", { style: { fontSize: 16, color: "#ff6a33" }, children: "\u2726" }),
|
|
756
|
+
/* @__PURE__ */ jsx4("span", { className: "hsk-chat-title", children: title }),
|
|
757
|
+
/* @__PURE__ */ jsx4("span", { className: "hsk-chat-badge", children: "AI" }),
|
|
758
|
+
messages.length > 0 && /* @__PURE__ */ jsx4("button", { className: "hsk-chat-reset", onClick: reset, style: { marginLeft: "auto" }, children: "Clear" })
|
|
759
|
+
] }),
|
|
760
|
+
/* @__PURE__ */ jsxs3("div", { className: "hsk-chat-messages", children: [
|
|
761
|
+
messages.length === 0 ? /* @__PURE__ */ jsxs3("div", { className: "hsk-chat-empty", children: [
|
|
762
|
+
/* @__PURE__ */ jsx4("div", { className: "hsk-chat-empty-icon", children: "\u2726" }),
|
|
763
|
+
/* @__PURE__ */ jsx4("div", { children: "Ask me anything about our products" }),
|
|
764
|
+
/* @__PURE__ */ jsx4("div", { style: { fontSize: 12, color: "#444", marginTop: 4 }, children: '"Find me headphones under KSh 5,000" \xB7 "Gift ideas for a chef"' })
|
|
765
|
+
] }) : messages.map((msg, idx) => /* @__PURE__ */ jsxs3("div", { children: [
|
|
766
|
+
/* @__PURE__ */ jsxs3("div", { className: `hsk-msg-row ${msg.role}`, children: [
|
|
767
|
+
/* @__PURE__ */ jsx4("div", { className: `hsk-msg-avatar ${msg.role === "assistant" ? "ai" : "user"}`, children: msg.role === "assistant" ? "\u2726" : "\u2191" }),
|
|
768
|
+
/* @__PURE__ */ jsx4("div", { className: `hsk-msg-bubble ${msg.role === "assistant" ? "ai" : "user"}`, children: msg.content })
|
|
769
|
+
] }),
|
|
770
|
+
msg.role === "assistant" && idx === messages.length - 1 && sources.length > 0 && /* @__PURE__ */ jsx4("div", { style: { marginLeft: 36 }, children: /* @__PURE__ */ jsx4("div", { className: "hsk-sources", children: sources.map((src, si) => /* @__PURE__ */ jsx4(SourceCard, { source: src, onSelect: onSelectSource }, si)) }) })
|
|
771
|
+
] }, idx)),
|
|
772
|
+
loading && /* @__PURE__ */ jsxs3("div", { className: "hsk-msg-row", children: [
|
|
773
|
+
/* @__PURE__ */ jsx4("div", { className: "hsk-msg-avatar ai", children: "\u2726" }),
|
|
774
|
+
/* @__PURE__ */ jsxs3("div", { className: "hsk-typing", children: [
|
|
775
|
+
/* @__PURE__ */ jsx4("div", { className: "hsk-typing-dot" }),
|
|
776
|
+
/* @__PURE__ */ jsx4("div", { className: "hsk-typing-dot" }),
|
|
777
|
+
/* @__PURE__ */ jsx4("div", { className: "hsk-typing-dot" })
|
|
778
|
+
] })
|
|
779
|
+
] }),
|
|
780
|
+
error && /* @__PURE__ */ jsx4("div", { style: { fontSize: 12, color: "#ef4444", textAlign: "center", padding: 8 }, children: error }),
|
|
781
|
+
/* @__PURE__ */ jsx4("div", { ref: bottomRef })
|
|
782
|
+
] }),
|
|
783
|
+
/* @__PURE__ */ jsxs3("div", { className: "hsk-chat-input-area", children: [
|
|
784
|
+
/* @__PURE__ */ jsx4(
|
|
785
|
+
"textarea",
|
|
786
|
+
{
|
|
787
|
+
ref: textareaRef,
|
|
788
|
+
className: "hsk-chat-input",
|
|
789
|
+
value: input,
|
|
790
|
+
onChange: handleInput,
|
|
791
|
+
onKeyDown: handleKey,
|
|
792
|
+
placeholder,
|
|
793
|
+
rows: 1,
|
|
794
|
+
disabled: loading
|
|
795
|
+
}
|
|
796
|
+
),
|
|
797
|
+
/* @__PURE__ */ jsx4(
|
|
798
|
+
"button",
|
|
799
|
+
{
|
|
800
|
+
className: "hsk-chat-send",
|
|
801
|
+
onClick: handleSend,
|
|
802
|
+
disabled: !input.trim() || loading,
|
|
803
|
+
"aria-label": "Send",
|
|
804
|
+
children: "\u2191"
|
|
805
|
+
}
|
|
806
|
+
)
|
|
807
|
+
] })
|
|
808
|
+
] })
|
|
809
|
+
] });
|
|
810
|
+
}
|
|
529
811
|
export {
|
|
812
|
+
ChatWidget,
|
|
530
813
|
HuskelAPI,
|
|
531
814
|
HuskelClient,
|
|
532
815
|
HuskelProvider,
|
|
@@ -534,6 +817,7 @@ export {
|
|
|
534
817
|
Sparkle,
|
|
535
818
|
getHuskelClient,
|
|
536
819
|
initHuskel,
|
|
820
|
+
useChat,
|
|
537
821
|
useHuskel,
|
|
538
822
|
useIngest,
|
|
539
823
|
usePageIngest,
|