@huskel/sdk 0.3.2 → 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 +45 -1
- package/dist/index.d.ts +45 -1
- package/dist/index.js +239 -12
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +235 -10
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -90,6 +90,14 @@ declare class HuskelAPI {
|
|
|
90
90
|
ingest(product: Product): Promise<IngestResponse>;
|
|
91
91
|
ingestBatch(products: Product[]): Promise<IngestResponse>;
|
|
92
92
|
search(query: string, limit?: number): Promise<SearchResponse>;
|
|
93
|
+
searchVector(query: string, limit?: number): Promise<SearchResponse>;
|
|
94
|
+
chat(query: string, history?: Array<{
|
|
95
|
+
role: 'user' | 'assistant';
|
|
96
|
+
content: string;
|
|
97
|
+
}>): Promise<{
|
|
98
|
+
answer: string;
|
|
99
|
+
sources: any[];
|
|
100
|
+
}>;
|
|
93
101
|
}
|
|
94
102
|
|
|
95
103
|
declare class HuskelClient {
|
|
@@ -100,6 +108,10 @@ declare class HuskelClient {
|
|
|
100
108
|
private onlineHandler;
|
|
101
109
|
private shopperId?;
|
|
102
110
|
private sessionId;
|
|
111
|
+
private static INGEST_CACHE_KEY;
|
|
112
|
+
private static INGEST_CACHE_TTL;
|
|
113
|
+
private loadIngestedCache;
|
|
114
|
+
private saveIngestedCache;
|
|
103
115
|
constructor(config: HuskelConfig);
|
|
104
116
|
setShopperId(id: string | undefined): void;
|
|
105
117
|
getShopperId(): string | undefined;
|
|
@@ -158,6 +170,29 @@ declare function useIngest(): UseIngestReturn;
|
|
|
158
170
|
*/
|
|
159
171
|
declare function usePageIngest(product: RawProductInput | null | undefined): void;
|
|
160
172
|
|
|
173
|
+
interface ChatMessage {
|
|
174
|
+
role: 'user' | 'assistant';
|
|
175
|
+
content: string;
|
|
176
|
+
}
|
|
177
|
+
interface ChatSource {
|
|
178
|
+
id?: string;
|
|
179
|
+
name: string;
|
|
180
|
+
price?: string;
|
|
181
|
+
currency?: string;
|
|
182
|
+
category?: string;
|
|
183
|
+
url?: string;
|
|
184
|
+
image?: string;
|
|
185
|
+
}
|
|
186
|
+
interface UseChatReturn {
|
|
187
|
+
messages: ChatMessage[];
|
|
188
|
+
sources: ChatSource[];
|
|
189
|
+
loading: boolean;
|
|
190
|
+
error: string | null;
|
|
191
|
+
send: (query: string) => Promise<void>;
|
|
192
|
+
reset: () => void;
|
|
193
|
+
}
|
|
194
|
+
declare function useChat(): UseChatReturn;
|
|
195
|
+
|
|
161
196
|
interface SearchBarProps {
|
|
162
197
|
placeholder?: string;
|
|
163
198
|
limit?: number;
|
|
@@ -178,9 +213,18 @@ interface SparkleProps {
|
|
|
178
213
|
}
|
|
179
214
|
declare function Sparkle({ productName, limit, onResult, className }: SparkleProps): react_jsx_runtime.JSX.Element;
|
|
180
215
|
|
|
216
|
+
interface ChatWidgetProps {
|
|
217
|
+
placeholder?: string;
|
|
218
|
+
title?: string;
|
|
219
|
+
className?: string;
|
|
220
|
+
/** Called when user clicks a product link */
|
|
221
|
+
onSelectSource?: (source: ChatSource) => void;
|
|
222
|
+
}
|
|
223
|
+
declare function ChatWidget({ placeholder, title, className, onSelectSource }: ChatWidgetProps): react_jsx_runtime.JSX.Element;
|
|
224
|
+
|
|
181
225
|
interface HuskelProviderProps extends HuskelConfig {
|
|
182
226
|
children: React.ReactNode;
|
|
183
227
|
}
|
|
184
228
|
declare function HuskelProvider({ siteId, apiUrl, apiToken, shopperId, children }: HuskelProviderProps): react_jsx_runtime.JSX.Element;
|
|
185
229
|
|
|
186
|
-
export { HuskelAPI, HuskelClient, type HuskelConfig, type HuskelError, HuskelProvider, type IngestResponse, type Product, type RawProductInput, SearchBar, type SearchRequest, type SearchResponse, type SearchResult, Sparkle, getHuskelClient, initHuskel, useHuskel, useIngest, usePageIngest, useSearch };
|
|
230
|
+
export { type ChatMessage, type ChatSource, ChatWidget, HuskelAPI, HuskelClient, type HuskelConfig, type HuskelError, HuskelProvider, type IngestResponse, type Product, type RawProductInput, SearchBar, type SearchRequest, type SearchResponse, type SearchResult, Sparkle, getHuskelClient, initHuskel, useChat, useHuskel, useIngest, usePageIngest, useSearch };
|
package/dist/index.d.ts
CHANGED
|
@@ -90,6 +90,14 @@ declare class HuskelAPI {
|
|
|
90
90
|
ingest(product: Product): Promise<IngestResponse>;
|
|
91
91
|
ingestBatch(products: Product[]): Promise<IngestResponse>;
|
|
92
92
|
search(query: string, limit?: number): Promise<SearchResponse>;
|
|
93
|
+
searchVector(query: string, limit?: number): Promise<SearchResponse>;
|
|
94
|
+
chat(query: string, history?: Array<{
|
|
95
|
+
role: 'user' | 'assistant';
|
|
96
|
+
content: string;
|
|
97
|
+
}>): Promise<{
|
|
98
|
+
answer: string;
|
|
99
|
+
sources: any[];
|
|
100
|
+
}>;
|
|
93
101
|
}
|
|
94
102
|
|
|
95
103
|
declare class HuskelClient {
|
|
@@ -100,6 +108,10 @@ declare class HuskelClient {
|
|
|
100
108
|
private onlineHandler;
|
|
101
109
|
private shopperId?;
|
|
102
110
|
private sessionId;
|
|
111
|
+
private static INGEST_CACHE_KEY;
|
|
112
|
+
private static INGEST_CACHE_TTL;
|
|
113
|
+
private loadIngestedCache;
|
|
114
|
+
private saveIngestedCache;
|
|
103
115
|
constructor(config: HuskelConfig);
|
|
104
116
|
setShopperId(id: string | undefined): void;
|
|
105
117
|
getShopperId(): string | undefined;
|
|
@@ -158,6 +170,29 @@ declare function useIngest(): UseIngestReturn;
|
|
|
158
170
|
*/
|
|
159
171
|
declare function usePageIngest(product: RawProductInput | null | undefined): void;
|
|
160
172
|
|
|
173
|
+
interface ChatMessage {
|
|
174
|
+
role: 'user' | 'assistant';
|
|
175
|
+
content: string;
|
|
176
|
+
}
|
|
177
|
+
interface ChatSource {
|
|
178
|
+
id?: string;
|
|
179
|
+
name: string;
|
|
180
|
+
price?: string;
|
|
181
|
+
currency?: string;
|
|
182
|
+
category?: string;
|
|
183
|
+
url?: string;
|
|
184
|
+
image?: string;
|
|
185
|
+
}
|
|
186
|
+
interface UseChatReturn {
|
|
187
|
+
messages: ChatMessage[];
|
|
188
|
+
sources: ChatSource[];
|
|
189
|
+
loading: boolean;
|
|
190
|
+
error: string | null;
|
|
191
|
+
send: (query: string) => Promise<void>;
|
|
192
|
+
reset: () => void;
|
|
193
|
+
}
|
|
194
|
+
declare function useChat(): UseChatReturn;
|
|
195
|
+
|
|
161
196
|
interface SearchBarProps {
|
|
162
197
|
placeholder?: string;
|
|
163
198
|
limit?: number;
|
|
@@ -178,9 +213,18 @@ interface SparkleProps {
|
|
|
178
213
|
}
|
|
179
214
|
declare function Sparkle({ productName, limit, onResult, className }: SparkleProps): react_jsx_runtime.JSX.Element;
|
|
180
215
|
|
|
216
|
+
interface ChatWidgetProps {
|
|
217
|
+
placeholder?: string;
|
|
218
|
+
title?: string;
|
|
219
|
+
className?: string;
|
|
220
|
+
/** Called when user clicks a product link */
|
|
221
|
+
onSelectSource?: (source: ChatSource) => void;
|
|
222
|
+
}
|
|
223
|
+
declare function ChatWidget({ placeholder, title, className, onSelectSource }: ChatWidgetProps): react_jsx_runtime.JSX.Element;
|
|
224
|
+
|
|
181
225
|
interface HuskelProviderProps extends HuskelConfig {
|
|
182
226
|
children: React.ReactNode;
|
|
183
227
|
}
|
|
184
228
|
declare function HuskelProvider({ siteId, apiUrl, apiToken, shopperId, children }: HuskelProviderProps): react_jsx_runtime.JSX.Element;
|
|
185
229
|
|
|
186
|
-
export { HuskelAPI, HuskelClient, type HuskelConfig, type HuskelError, HuskelProvider, type IngestResponse, type Product, type RawProductInput, SearchBar, type SearchRequest, type SearchResponse, type SearchResult, Sparkle, getHuskelClient, initHuskel, useHuskel, useIngest, usePageIngest, useSearch };
|
|
230
|
+
export { type ChatMessage, type ChatSource, ChatWidget, HuskelAPI, HuskelClient, type HuskelConfig, type HuskelError, HuskelProvider, type IngestResponse, type Product, type RawProductInput, SearchBar, type SearchRequest, type SearchResponse, type SearchResult, Sparkle, getHuskelClient, initHuskel, useChat, useHuskel, useIngest, usePageIngest, useSearch };
|
package/dist/index.js
CHANGED
|
@@ -38,6 +38,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
38
38
|
// src/index.ts
|
|
39
39
|
var index_exports = {};
|
|
40
40
|
__export(index_exports, {
|
|
41
|
+
ChatWidget: () => ChatWidget,
|
|
41
42
|
HuskelAPI: () => HuskelAPI,
|
|
42
43
|
HuskelClient: () => HuskelClient,
|
|
43
44
|
HuskelProvider: () => HuskelProvider,
|
|
@@ -45,6 +46,7 @@ __export(index_exports, {
|
|
|
45
46
|
Sparkle: () => Sparkle,
|
|
46
47
|
getHuskelClient: () => getHuskelClient,
|
|
47
48
|
initHuskel: () => initHuskel,
|
|
49
|
+
useChat: () => useChat,
|
|
48
50
|
useHuskel: () => useHuskel,
|
|
49
51
|
useIngest: () => useIngest,
|
|
50
52
|
usePageIngest: () => usePageIngest,
|
|
@@ -134,6 +136,15 @@ var HuskelAPI = class {
|
|
|
134
136
|
log("info", "search query", query);
|
|
135
137
|
return this.post("/search", { query, siteId: this.siteId, limit });
|
|
136
138
|
}
|
|
139
|
+
// Pure vector search — no LLM, instant results. This is what the SearchBar uses.
|
|
140
|
+
async searchVector(query, limit = 10) {
|
|
141
|
+
return this.post("/search/vector", { query, siteId: this.siteId, limit });
|
|
142
|
+
}
|
|
143
|
+
// LLM chat — conversational search with history context.
|
|
144
|
+
async chat(query, history = []) {
|
|
145
|
+
log("info", "chat query", query);
|
|
146
|
+
return this.post("/chat", { query, siteId: this.siteId, history });
|
|
147
|
+
}
|
|
137
148
|
};
|
|
138
149
|
|
|
139
150
|
// src/client.ts
|
|
@@ -227,7 +238,7 @@ function generateUUID() {
|
|
|
227
238
|
return v.toString(16);
|
|
228
239
|
});
|
|
229
240
|
}
|
|
230
|
-
var
|
|
241
|
+
var _HuskelClient = class _HuskelClient {
|
|
231
242
|
constructor(config) {
|
|
232
243
|
this.ingestQueue = [];
|
|
233
244
|
this.ingestTimer = null;
|
|
@@ -242,6 +253,7 @@ var HuskelClient = class {
|
|
|
242
253
|
if (!apiToken) console.error('[Huskel] Missing apiToken. Set it via <HuskelProvider apiToken="..."> or NEXT_PUBLIC_HUSKEL_API_TOKEN.');
|
|
243
254
|
this.shopperId = config.shopperId;
|
|
244
255
|
this.initSession();
|
|
256
|
+
this.loadIngestedCache();
|
|
245
257
|
this.api = new HuskelAPI(
|
|
246
258
|
apiUrl,
|
|
247
259
|
siteId,
|
|
@@ -258,6 +270,31 @@ var HuskelClient = class {
|
|
|
258
270
|
window.addEventListener("online", this.onlineHandler);
|
|
259
271
|
}
|
|
260
272
|
}
|
|
273
|
+
// 24h
|
|
274
|
+
loadIngestedCache() {
|
|
275
|
+
if (typeof window === "undefined") return;
|
|
276
|
+
try {
|
|
277
|
+
const raw = localStorage.getItem(_HuskelClient.INGEST_CACHE_KEY);
|
|
278
|
+
if (!raw) return;
|
|
279
|
+
const { ts, urls } = JSON.parse(raw);
|
|
280
|
+
if (Date.now() - ts > _HuskelClient.INGEST_CACHE_TTL) {
|
|
281
|
+
localStorage.removeItem(_HuskelClient.INGEST_CACHE_KEY);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
this.ingestedUrls = new Set(urls);
|
|
285
|
+
} catch (e) {
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
saveIngestedCache() {
|
|
289
|
+
if (typeof window === "undefined") return;
|
|
290
|
+
try {
|
|
291
|
+
localStorage.setItem(
|
|
292
|
+
_HuskelClient.INGEST_CACHE_KEY,
|
|
293
|
+
JSON.stringify({ ts: Date.now(), urls: [...this.ingestedUrls] })
|
|
294
|
+
);
|
|
295
|
+
} catch (e) {
|
|
296
|
+
}
|
|
297
|
+
}
|
|
261
298
|
setShopperId(id) {
|
|
262
299
|
this.shopperId = id;
|
|
263
300
|
}
|
|
@@ -300,6 +337,7 @@ var HuskelClient = class {
|
|
|
300
337
|
return;
|
|
301
338
|
}
|
|
302
339
|
this.ingestedUrls.add(product.url);
|
|
340
|
+
this.saveIngestedCache();
|
|
303
341
|
this.ingestQueue.push(product);
|
|
304
342
|
this.scheduleFlush();
|
|
305
343
|
}
|
|
@@ -314,6 +352,7 @@ var HuskelClient = class {
|
|
|
314
352
|
this.ingestQueue.push(product);
|
|
315
353
|
});
|
|
316
354
|
if (this.ingestQueue.length > 0) {
|
|
355
|
+
this.saveIngestedCache();
|
|
317
356
|
this.scheduleFlush();
|
|
318
357
|
}
|
|
319
358
|
}
|
|
@@ -345,6 +384,9 @@ var HuskelClient = class {
|
|
|
345
384
|
}
|
|
346
385
|
}
|
|
347
386
|
};
|
|
387
|
+
_HuskelClient.INGEST_CACHE_KEY = "huskel_ingested_v1";
|
|
388
|
+
_HuskelClient.INGEST_CACHE_TTL = 24 * 60 * 60 * 1e3;
|
|
389
|
+
var HuskelClient = _HuskelClient;
|
|
348
390
|
var instance = null;
|
|
349
391
|
function initHuskel(config) {
|
|
350
392
|
instance = new HuskelClient(config);
|
|
@@ -425,7 +467,7 @@ function useSearch() {
|
|
|
425
467
|
setLoading(true);
|
|
426
468
|
setError(null);
|
|
427
469
|
try {
|
|
428
|
-
const res = await client.api.
|
|
470
|
+
const res = await client.api.searchVector(query, limit);
|
|
429
471
|
setResults((_b = res.results) != null ? _b : []);
|
|
430
472
|
} catch (e) {
|
|
431
473
|
setError((_c = e.message) != null ? _c : "Search failed");
|
|
@@ -491,8 +533,47 @@ function usePageIngest(product) {
|
|
|
491
533
|
}, [(_a = product == null ? void 0 : product.url) != null ? _a : product == null ? void 0 : product.name]);
|
|
492
534
|
}
|
|
493
535
|
|
|
494
|
-
// src/
|
|
536
|
+
// src/hooks/useChat.ts
|
|
495
537
|
var import_react6 = require("react");
|
|
538
|
+
function useChat() {
|
|
539
|
+
const client = useHuskelContext();
|
|
540
|
+
const [messages, setMessages] = (0, import_react6.useState)([]);
|
|
541
|
+
const [sources, setSources] = (0, import_react6.useState)([]);
|
|
542
|
+
const [loading, setLoading] = (0, import_react6.useState)(false);
|
|
543
|
+
const [error, setError] = (0, import_react6.useState)(null);
|
|
544
|
+
const abortRef = (0, import_react6.useRef)(null);
|
|
545
|
+
const send = (0, import_react6.useCallback)(async (query) => {
|
|
546
|
+
var _a, _b, _c;
|
|
547
|
+
if (!query.trim() || loading) return;
|
|
548
|
+
(_a = abortRef.current) == null ? void 0 : _a.abort();
|
|
549
|
+
abortRef.current = new AbortController();
|
|
550
|
+
const userMsg = { role: "user", content: query };
|
|
551
|
+
setMessages((prev) => [...prev, userMsg]);
|
|
552
|
+
setLoading(true);
|
|
553
|
+
setError(null);
|
|
554
|
+
try {
|
|
555
|
+
const history = messages.map((m) => ({ role: m.role, content: m.content }));
|
|
556
|
+
const res = await client.api.chat(query, history);
|
|
557
|
+
const assistantMsg = { role: "assistant", content: res.answer };
|
|
558
|
+
setMessages((prev) => [...prev, assistantMsg]);
|
|
559
|
+
setSources((_b = res.sources) != null ? _b : []);
|
|
560
|
+
} catch (e) {
|
|
561
|
+
setError((_c = e == null ? void 0 : e.message) != null ? _c : "Chat request failed");
|
|
562
|
+
setMessages((prev) => prev.slice(0, -1));
|
|
563
|
+
} finally {
|
|
564
|
+
setLoading(false);
|
|
565
|
+
}
|
|
566
|
+
}, [client, messages, loading]);
|
|
567
|
+
const reset = (0, import_react6.useCallback)(() => {
|
|
568
|
+
setMessages([]);
|
|
569
|
+
setSources([]);
|
|
570
|
+
setError(null);
|
|
571
|
+
}, []);
|
|
572
|
+
return { messages, sources, loading, error, send, reset };
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// src/components/SearchBar.tsx
|
|
576
|
+
var import_react7 = require("react");
|
|
496
577
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
497
578
|
var S = `
|
|
498
579
|
.hsk-wrap{position:relative;width:100%;font-family:inherit}
|
|
@@ -516,12 +597,12 @@ function SearchBar({
|
|
|
516
597
|
dropdownClassName,
|
|
517
598
|
renderResult
|
|
518
599
|
}) {
|
|
519
|
-
const [query, setQuery] = (0,
|
|
520
|
-
const [open, setOpen] = (0,
|
|
600
|
+
const [query, setQuery] = (0, import_react7.useState)("");
|
|
601
|
+
const [open, setOpen] = (0, import_react7.useState)(false);
|
|
521
602
|
const { results, loading, search, clear } = useSearch();
|
|
522
|
-
const timer = (0,
|
|
523
|
-
const wrap = (0,
|
|
524
|
-
(0,
|
|
603
|
+
const timer = (0, import_react7.useRef)();
|
|
604
|
+
const wrap = (0, import_react7.useRef)(null);
|
|
605
|
+
(0, import_react7.useEffect)(() => {
|
|
525
606
|
clearTimeout(timer.current);
|
|
526
607
|
if (!query.trim()) {
|
|
527
608
|
clear();
|
|
@@ -534,7 +615,7 @@ function SearchBar({
|
|
|
534
615
|
}, debounceMs);
|
|
535
616
|
return () => clearTimeout(timer.current);
|
|
536
617
|
}, [query, search, clear, limit, debounceMs]);
|
|
537
|
-
(0,
|
|
618
|
+
(0, import_react7.useEffect)(() => {
|
|
538
619
|
const handler = (e) => {
|
|
539
620
|
if (wrap.current && !wrap.current.contains(e.target)) setOpen(false);
|
|
540
621
|
};
|
|
@@ -589,7 +670,7 @@ function SearchBar({
|
|
|
589
670
|
}
|
|
590
671
|
|
|
591
672
|
// src/components/Sparkle.tsx
|
|
592
|
-
var
|
|
673
|
+
var import_react8 = require("react");
|
|
593
674
|
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
594
675
|
var S2 = `
|
|
595
676
|
.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}
|
|
@@ -598,11 +679,11 @@ var S2 = `
|
|
|
598
679
|
`;
|
|
599
680
|
function Sparkle({ productName, limit = 5, onResult, className }) {
|
|
600
681
|
const client = useHuskelContext();
|
|
601
|
-
const [loading, setLoading] = (0,
|
|
682
|
+
const [loading, setLoading] = (0, import_react8.useState)(false);
|
|
602
683
|
const handleClick = async () => {
|
|
603
684
|
setLoading(true);
|
|
604
685
|
try {
|
|
605
|
-
const res = await client.api.
|
|
686
|
+
const res = await client.api.searchVector(productName, limit);
|
|
606
687
|
onResult == null ? void 0 : onResult(res.results);
|
|
607
688
|
} catch (e) {
|
|
608
689
|
console.error("[Huskel Sparkle]", e);
|
|
@@ -618,8 +699,153 @@ function Sparkle({ productName, limit = 5, onResult, className }) {
|
|
|
618
699
|
] })
|
|
619
700
|
] });
|
|
620
701
|
}
|
|
702
|
+
|
|
703
|
+
// src/components/ChatWidget.tsx
|
|
704
|
+
var import_react9 = require("react");
|
|
705
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
706
|
+
var S3 = `
|
|
707
|
+
.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}
|
|
708
|
+
.hsk-chat-header{display:flex;align-items:center;gap:10px;padding:14px 16px;border-bottom:1px solid #1e1e1f;background:#111112;flex-shrink:0}
|
|
709
|
+
.hsk-chat-title{font-size:14px;font-weight:600;color:#f3f3f2}
|
|
710
|
+
.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}
|
|
711
|
+
.hsk-chat-messages{flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:12px;scroll-behavior:smooth}
|
|
712
|
+
.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}
|
|
713
|
+
.hsk-chat-empty-icon{font-size:28px;margin-bottom:4px}
|
|
714
|
+
.hsk-msg-row{display:flex;gap:8px;align-items:flex-start}
|
|
715
|
+
.hsk-msg-row.user{flex-direction:row-reverse}
|
|
716
|
+
.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}
|
|
717
|
+
.hsk-msg-avatar.ai{background:#ff6a3320;border:1px solid #ff6a3340;color:#ff6a33}
|
|
718
|
+
.hsk-msg-avatar.user{background:#2a2a2d;color:#9a9aa1}
|
|
719
|
+
.hsk-msg-bubble{max-width:78%;padding:10px 14px;border-radius:12px;font-size:13px;line-height:1.6}
|
|
720
|
+
.hsk-msg-bubble.ai{background:#171718;border:1px solid #2a2a2d;color:#e8e8e7;border-radius:4px 12px 12px 12px}
|
|
721
|
+
.hsk-msg-bubble.user{background:#ff6a33;color:#fff;border-radius:12px 4px 12px 12px}
|
|
722
|
+
.hsk-sources{margin-top:10px;display:flex;flex-direction:column;gap:6px}
|
|
723
|
+
.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}
|
|
724
|
+
.hsk-source-card:hover{border-color:#ff6a3360}
|
|
725
|
+
.hsk-source-img{width:36px;height:36px;object-fit:cover;border-radius:4px;background:#fff}
|
|
726
|
+
.hsk-source-name{font-size:12px;font-weight:500;color:#e8e8e7;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
727
|
+
.hsk-source-price{font-size:11px;color:#ff6a33;font-weight:700;margin-top:2px}
|
|
728
|
+
.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}
|
|
729
|
+
.hsk-typing-dot{width:6px;height:6px;background:#ff6a33;border-radius:50%;animation:hsk-chat-bounce 1.2s infinite}
|
|
730
|
+
.hsk-typing-dot:nth-child(2){animation-delay:0.2s}
|
|
731
|
+
.hsk-typing-dot:nth-child(3){animation-delay:0.4s}
|
|
732
|
+
@keyframes hsk-chat-bounce{0%,100%{opacity:0.3;transform:translateY(0)}50%{opacity:1;transform:translateY(-4px)}}
|
|
733
|
+
.hsk-chat-input-area{display:flex;align-items:center;gap:8px;padding:12px 14px;border-top:1px solid #1e1e1f;background:#111112;flex-shrink:0}
|
|
734
|
+
.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}
|
|
735
|
+
.hsk-chat-input::placeholder{color:#555}
|
|
736
|
+
.hsk-chat-input:focus{border-color:#ff6a33}
|
|
737
|
+
.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}
|
|
738
|
+
.hsk-chat-send:hover{opacity:0.88}
|
|
739
|
+
.hsk-chat-send:active{transform:scale(0.93)}
|
|
740
|
+
.hsk-chat-send:disabled{opacity:0.4;cursor:not-allowed}
|
|
741
|
+
.hsk-chat-reset{font-size:11px;color:#555;cursor:pointer;padding:0 4px;transition:color 0.15s;background:none;border:none;font-family:inherit}
|
|
742
|
+
.hsk-chat-reset:hover{color:#ff6a33}
|
|
743
|
+
`;
|
|
744
|
+
function SourceCard({ source, onSelect }) {
|
|
745
|
+
var _a;
|
|
746
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "hsk-source-card", onClick: () => onSelect == null ? void 0 : onSelect(source), children: [
|
|
747
|
+
source.image && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("img", { src: source.image, alt: source.name, className: "hsk-source-img" }),
|
|
748
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { flex: 1, minWidth: 0 }, children: [
|
|
749
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "hsk-source-name", children: source.name }),
|
|
750
|
+
source.price && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "hsk-source-price", children: [
|
|
751
|
+
(_a = source.currency) != null ? _a : "KES",
|
|
752
|
+
" ",
|
|
753
|
+
source.price
|
|
754
|
+
] })
|
|
755
|
+
] })
|
|
756
|
+
] });
|
|
757
|
+
}
|
|
758
|
+
function ChatWidget({ placeholder = "Ask about anything in our store\u2026", title = "AI Shopping Assistant", className, onSelectSource }) {
|
|
759
|
+
const { messages, sources, loading, error, send, reset } = useChat();
|
|
760
|
+
const [input, setInput] = (0, import_react9.useState)("");
|
|
761
|
+
const bottomRef = (0, import_react9.useRef)(null);
|
|
762
|
+
const textareaRef = (0, import_react9.useRef)(null);
|
|
763
|
+
(0, import_react9.useEffect)(() => {
|
|
764
|
+
var _a;
|
|
765
|
+
(_a = bottomRef.current) == null ? void 0 : _a.scrollIntoView({ behavior: "smooth" });
|
|
766
|
+
}, [messages, loading]);
|
|
767
|
+
const handleSend = async () => {
|
|
768
|
+
const q = input.trim();
|
|
769
|
+
if (!q || loading) return;
|
|
770
|
+
setInput("");
|
|
771
|
+
if (textareaRef.current) textareaRef.current.style.height = "auto";
|
|
772
|
+
await send(q);
|
|
773
|
+
};
|
|
774
|
+
const handleKey = (e) => {
|
|
775
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
776
|
+
e.preventDefault();
|
|
777
|
+
handleSend();
|
|
778
|
+
}
|
|
779
|
+
};
|
|
780
|
+
const handleInput = (e) => {
|
|
781
|
+
setInput(e.target.value);
|
|
782
|
+
const t = e.target;
|
|
783
|
+
t.style.height = "auto";
|
|
784
|
+
t.style.height = Math.min(t.scrollHeight, 120) + "px";
|
|
785
|
+
};
|
|
786
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
|
|
787
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("style", { children: S3 }),
|
|
788
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: `hsk-chat-widget ${className != null ? className : ""}`, children: [
|
|
789
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "hsk-chat-header", children: [
|
|
790
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { fontSize: 16, color: "#ff6a33" }, children: "\u2726" }),
|
|
791
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "hsk-chat-title", children: title }),
|
|
792
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "hsk-chat-badge", children: "AI" }),
|
|
793
|
+
messages.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: "hsk-chat-reset", onClick: reset, style: { marginLeft: "auto" }, children: "Clear" })
|
|
794
|
+
] }),
|
|
795
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "hsk-chat-messages", children: [
|
|
796
|
+
messages.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "hsk-chat-empty", children: [
|
|
797
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "hsk-chat-empty-icon", children: "\u2726" }),
|
|
798
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { children: "Ask me anything about our products" }),
|
|
799
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontSize: 12, color: "#444", marginTop: 4 }, children: '"Find me headphones under KSh 5,000" \xB7 "Gift ideas for a chef"' })
|
|
800
|
+
] }) : messages.map((msg, idx) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { children: [
|
|
801
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: `hsk-msg-row ${msg.role}`, children: [
|
|
802
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: `hsk-msg-avatar ${msg.role === "assistant" ? "ai" : "user"}`, children: msg.role === "assistant" ? "\u2726" : "\u2191" }),
|
|
803
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: `hsk-msg-bubble ${msg.role === "assistant" ? "ai" : "user"}`, children: msg.content })
|
|
804
|
+
] }),
|
|
805
|
+
msg.role === "assistant" && idx === messages.length - 1 && sources.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { marginLeft: 36 }, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "hsk-sources", children: sources.map((src, si) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(SourceCard, { source: src, onSelect: onSelectSource }, si)) }) })
|
|
806
|
+
] }, idx)),
|
|
807
|
+
loading && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "hsk-msg-row", children: [
|
|
808
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "hsk-msg-avatar ai", children: "\u2726" }),
|
|
809
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "hsk-typing", children: [
|
|
810
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "hsk-typing-dot" }),
|
|
811
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "hsk-typing-dot" }),
|
|
812
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "hsk-typing-dot" })
|
|
813
|
+
] })
|
|
814
|
+
] }),
|
|
815
|
+
error && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontSize: 12, color: "#ef4444", textAlign: "center", padding: 8 }, children: error }),
|
|
816
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { ref: bottomRef })
|
|
817
|
+
] }),
|
|
818
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "hsk-chat-input-area", children: [
|
|
819
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
820
|
+
"textarea",
|
|
821
|
+
{
|
|
822
|
+
ref: textareaRef,
|
|
823
|
+
className: "hsk-chat-input",
|
|
824
|
+
value: input,
|
|
825
|
+
onChange: handleInput,
|
|
826
|
+
onKeyDown: handleKey,
|
|
827
|
+
placeholder,
|
|
828
|
+
rows: 1,
|
|
829
|
+
disabled: loading
|
|
830
|
+
}
|
|
831
|
+
),
|
|
832
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
833
|
+
"button",
|
|
834
|
+
{
|
|
835
|
+
className: "hsk-chat-send",
|
|
836
|
+
onClick: handleSend,
|
|
837
|
+
disabled: !input.trim() || loading,
|
|
838
|
+
"aria-label": "Send",
|
|
839
|
+
children: "\u2191"
|
|
840
|
+
}
|
|
841
|
+
)
|
|
842
|
+
] })
|
|
843
|
+
] })
|
|
844
|
+
] });
|
|
845
|
+
}
|
|
621
846
|
// Annotate the CommonJS export names for ESM import in node:
|
|
622
847
|
0 && (module.exports = {
|
|
848
|
+
ChatWidget,
|
|
623
849
|
HuskelAPI,
|
|
624
850
|
HuskelClient,
|
|
625
851
|
HuskelProvider,
|
|
@@ -627,6 +853,7 @@ function Sparkle({ productName, limit = 5, onResult, className }) {
|
|
|
627
853
|
Sparkle,
|
|
628
854
|
getHuskelClient,
|
|
629
855
|
initHuskel,
|
|
856
|
+
useChat,
|
|
630
857
|
useHuskel,
|
|
631
858
|
useIngest,
|
|
632
859
|
usePageIngest,
|