@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 CHANGED
@@ -53,6 +53,7 @@ interface HuskelConfig {
53
53
  siteId?: string;
54
54
  apiUrl?: string;
55
55
  apiToken?: string;
56
+ shopperId?: string;
56
57
  }
57
58
  interface SearchRequest {
58
59
  query: string;
@@ -82,11 +83,21 @@ declare class HuskelAPI {
82
83
  private apiUrl;
83
84
  private siteId;
84
85
  private apiToken;
85
- constructor(apiUrl: string, siteId: string, apiToken: string);
86
+ private getShopperId?;
87
+ private getSessionId?;
88
+ constructor(apiUrl: string, siteId: string, apiToken: string, getShopperId?: (() => string | undefined) | undefined, getSessionId?: (() => string | undefined) | undefined);
86
89
  private post;
87
90
  ingest(product: Product): Promise<IngestResponse>;
88
91
  ingestBatch(products: Product[]): Promise<IngestResponse>;
89
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
+ }>;
90
101
  }
91
102
 
92
103
  declare class HuskelClient {
@@ -95,7 +106,17 @@ declare class HuskelClient {
95
106
  private ingestTimer;
96
107
  private ingestedUrls;
97
108
  private onlineHandler;
109
+ private shopperId?;
110
+ private sessionId;
111
+ private static INGEST_CACHE_KEY;
112
+ private static INGEST_CACHE_TTL;
113
+ private loadIngestedCache;
114
+ private saveIngestedCache;
98
115
  constructor(config: HuskelConfig);
116
+ setShopperId(id: string | undefined): void;
117
+ getShopperId(): string | undefined;
118
+ getSessionId(): string;
119
+ private initSession;
99
120
  destroy(): void;
100
121
  queueIngest(rawProduct: RawProductInput): Promise<void>;
101
122
  queueIngestBatch(rawProducts: RawProductInput[]): Promise<void>;
@@ -149,6 +170,29 @@ declare function useIngest(): UseIngestReturn;
149
170
  */
150
171
  declare function usePageIngest(product: RawProductInput | null | undefined): void;
151
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
+
152
196
  interface SearchBarProps {
153
197
  placeholder?: string;
154
198
  limit?: number;
@@ -169,9 +213,18 @@ interface SparkleProps {
169
213
  }
170
214
  declare function Sparkle({ productName, limit, onResult, className }: SparkleProps): react_jsx_runtime.JSX.Element;
171
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
+
172
225
  interface HuskelProviderProps extends HuskelConfig {
173
226
  children: React.ReactNode;
174
227
  }
175
- declare function HuskelProvider({ siteId, apiUrl, apiToken, children }: HuskelProviderProps): react_jsx_runtime.JSX.Element;
228
+ declare function HuskelProvider({ siteId, apiUrl, apiToken, shopperId, children }: HuskelProviderProps): react_jsx_runtime.JSX.Element;
176
229
 
177
- 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
@@ -53,6 +53,7 @@ interface HuskelConfig {
53
53
  siteId?: string;
54
54
  apiUrl?: string;
55
55
  apiToken?: string;
56
+ shopperId?: string;
56
57
  }
57
58
  interface SearchRequest {
58
59
  query: string;
@@ -82,11 +83,21 @@ declare class HuskelAPI {
82
83
  private apiUrl;
83
84
  private siteId;
84
85
  private apiToken;
85
- constructor(apiUrl: string, siteId: string, apiToken: string);
86
+ private getShopperId?;
87
+ private getSessionId?;
88
+ constructor(apiUrl: string, siteId: string, apiToken: string, getShopperId?: (() => string | undefined) | undefined, getSessionId?: (() => string | undefined) | undefined);
86
89
  private post;
87
90
  ingest(product: Product): Promise<IngestResponse>;
88
91
  ingestBatch(products: Product[]): Promise<IngestResponse>;
89
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
+ }>;
90
101
  }
91
102
 
92
103
  declare class HuskelClient {
@@ -95,7 +106,17 @@ declare class HuskelClient {
95
106
  private ingestTimer;
96
107
  private ingestedUrls;
97
108
  private onlineHandler;
109
+ private shopperId?;
110
+ private sessionId;
111
+ private static INGEST_CACHE_KEY;
112
+ private static INGEST_CACHE_TTL;
113
+ private loadIngestedCache;
114
+ private saveIngestedCache;
98
115
  constructor(config: HuskelConfig);
116
+ setShopperId(id: string | undefined): void;
117
+ getShopperId(): string | undefined;
118
+ getSessionId(): string;
119
+ private initSession;
99
120
  destroy(): void;
100
121
  queueIngest(rawProduct: RawProductInput): Promise<void>;
101
122
  queueIngestBatch(rawProducts: RawProductInput[]): Promise<void>;
@@ -149,6 +170,29 @@ declare function useIngest(): UseIngestReturn;
149
170
  */
150
171
  declare function usePageIngest(product: RawProductInput | null | undefined): void;
151
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
+
152
196
  interface SearchBarProps {
153
197
  placeholder?: string;
154
198
  limit?: number;
@@ -169,9 +213,18 @@ interface SparkleProps {
169
213
  }
170
214
  declare function Sparkle({ productName, limit, onResult, className }: SparkleProps): react_jsx_runtime.JSX.Element;
171
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
+
172
225
  interface HuskelProviderProps extends HuskelConfig {
173
226
  children: React.ReactNode;
174
227
  }
175
- declare function HuskelProvider({ siteId, apiUrl, apiToken, children }: HuskelProviderProps): react_jsx_runtime.JSX.Element;
228
+ declare function HuskelProvider({ siteId, apiUrl, apiToken, shopperId, children }: HuskelProviderProps): react_jsx_runtime.JSX.Element;
176
229
 
177
- 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,
@@ -65,21 +67,33 @@ async function sleep(ms) {
65
67
  return new Promise((r) => setTimeout(r, ms));
66
68
  }
67
69
  var HuskelAPI = class {
68
- constructor(apiUrl, siteId, apiToken) {
70
+ constructor(apiUrl, siteId, apiToken, getShopperId, getSessionId) {
69
71
  this.apiUrl = apiUrl;
70
72
  this.siteId = siteId;
71
73
  this.apiToken = apiToken;
74
+ this.getShopperId = getShopperId;
75
+ this.getSessionId = getSessionId;
72
76
  }
73
77
  async post(path, body, attempt = 0) {
78
+ var _a, _b;
74
79
  const url = `${this.apiUrl}${path}`;
75
80
  try {
81
+ const headers = {
82
+ "Content-Type": "application/json",
83
+ "X-Huskel-Token": this.apiToken,
84
+ "X-Huskel-Site": this.siteId
85
+ };
86
+ const shopperId = (_a = this.getShopperId) == null ? void 0 : _a.call(this);
87
+ if (shopperId) {
88
+ headers["X-Huskel-Shopper-Id"] = shopperId;
89
+ }
90
+ const sessionId = (_b = this.getSessionId) == null ? void 0 : _b.call(this);
91
+ if (sessionId) {
92
+ headers["X-Huskel-Session-Id"] = sessionId;
93
+ }
76
94
  const res = await fetch(url, {
77
95
  method: "POST",
78
- headers: {
79
- "Content-Type": "application/json",
80
- "X-Huskel-Token": this.apiToken,
81
- "X-Huskel-Site": this.siteId
82
- },
96
+ headers,
83
97
  body: JSON.stringify(body)
84
98
  });
85
99
  if (!res.ok) {
@@ -122,6 +136,15 @@ var HuskelAPI = class {
122
136
  log("info", "search query", query);
123
137
  return this.post("/search", { query, siteId: this.siteId, limit });
124
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
+ }
125
148
  };
126
149
 
127
150
  // src/client.ts
@@ -205,19 +228,39 @@ function mapRawProduct(input) {
205
228
  slug
206
229
  };
207
230
  }
208
- var HuskelClient = class {
231
+ function generateUUID() {
232
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
233
+ return crypto.randomUUID();
234
+ }
235
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
236
+ const r = Math.random() * 16 | 0;
237
+ const v = c === "x" ? r : r & 3 | 8;
238
+ return v.toString(16);
239
+ });
240
+ }
241
+ var _HuskelClient = class _HuskelClient {
209
242
  constructor(config) {
210
243
  this.ingestQueue = [];
211
244
  this.ingestTimer = null;
212
245
  this.ingestedUrls = /* @__PURE__ */ new Set();
213
246
  this.onlineHandler = null;
247
+ this.sessionId = "";
214
248
  const siteId = config.siteId || getEnvVar("NEXT_PUBLIC_HUSKEL_SITE_ID") || "";
215
249
  const apiUrl = config.apiUrl || getEnvVar("NEXT_PUBLIC_HUSKEL_API_URL") || "";
216
250
  const apiToken = config.apiToken || getEnvVar("NEXT_PUBLIC_HUSKEL_API_TOKEN") || "";
217
251
  if (!siteId) console.error('[Huskel] Missing siteId. Set it via <HuskelProvider siteId="..."> or NEXT_PUBLIC_HUSKEL_SITE_ID.');
218
252
  if (!apiUrl) console.error('[Huskel] Missing apiUrl. Set it via <HuskelProvider apiUrl="..."> or NEXT_PUBLIC_HUSKEL_API_URL.');
219
253
  if (!apiToken) console.error('[Huskel] Missing apiToken. Set it via <HuskelProvider apiToken="..."> or NEXT_PUBLIC_HUSKEL_API_TOKEN.');
220
- this.api = new HuskelAPI(apiUrl, siteId, apiToken);
254
+ this.shopperId = config.shopperId;
255
+ this.initSession();
256
+ this.loadIngestedCache();
257
+ this.api = new HuskelAPI(
258
+ apiUrl,
259
+ siteId,
260
+ apiToken,
261
+ () => this.shopperId,
262
+ () => this.sessionId
263
+ );
221
264
  instance = this;
222
265
  if (typeof window !== "undefined") {
223
266
  this.onlineHandler = () => {
@@ -227,6 +270,55 @@ var HuskelClient = class {
227
270
  window.addEventListener("online", this.onlineHandler);
228
271
  }
229
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
+ }
298
+ setShopperId(id) {
299
+ this.shopperId = id;
300
+ }
301
+ getShopperId() {
302
+ return this.shopperId;
303
+ }
304
+ getSessionId() {
305
+ return this.sessionId;
306
+ }
307
+ initSession() {
308
+ if (typeof window !== "undefined" && window.sessionStorage) {
309
+ try {
310
+ let sid = window.sessionStorage.getItem("huskel_session_id");
311
+ if (!sid) {
312
+ sid = generateUUID();
313
+ window.sessionStorage.setItem("huskel_session_id", sid);
314
+ }
315
+ this.sessionId = sid;
316
+ return;
317
+ } catch (e) {
318
+ }
319
+ }
320
+ this.sessionId = generateUUID();
321
+ }
230
322
  destroy() {
231
323
  if (typeof window !== "undefined" && this.onlineHandler) {
232
324
  window.removeEventListener("online", this.onlineHandler);
@@ -245,6 +337,7 @@ var HuskelClient = class {
245
337
  return;
246
338
  }
247
339
  this.ingestedUrls.add(product.url);
340
+ this.saveIngestedCache();
248
341
  this.ingestQueue.push(product);
249
342
  this.scheduleFlush();
250
343
  }
@@ -259,6 +352,7 @@ var HuskelClient = class {
259
352
  this.ingestQueue.push(product);
260
353
  });
261
354
  if (this.ingestQueue.length > 0) {
355
+ this.saveIngestedCache();
262
356
  this.scheduleFlush();
263
357
  }
264
358
  }
@@ -290,6 +384,9 @@ var HuskelClient = class {
290
384
  }
291
385
  }
292
386
  };
387
+ _HuskelClient.INGEST_CACHE_KEY = "huskel_ingested_v1";
388
+ _HuskelClient.INGEST_CACHE_TTL = 24 * 60 * 60 * 1e3;
389
+ var HuskelClient = _HuskelClient;
293
390
  var instance = null;
294
391
  function initHuskel(config) {
295
392
  instance = new HuskelClient(config);
@@ -327,11 +424,15 @@ var import_react3 = require("react");
327
424
  var import_react2 = require("react");
328
425
  var import_jsx_runtime = require("react/jsx-runtime");
329
426
  var HuskelContext = (0, import_react2.createContext)(null);
330
- function HuskelProvider({ siteId, apiUrl, apiToken, children }) {
427
+ function HuskelProvider({ siteId, apiUrl, apiToken, shopperId, children }) {
331
428
  const clientRef = (0, import_react2.useRef)(null);
332
429
  if (!clientRef.current) {
333
- clientRef.current = new HuskelClient({ siteId, apiUrl, apiToken });
430
+ clientRef.current = new HuskelClient({ siteId, apiUrl, apiToken, shopperId });
334
431
  }
432
+ (0, import_react2.useEffect)(() => {
433
+ var _a;
434
+ (_a = clientRef.current) == null ? void 0 : _a.setShopperId(shopperId);
435
+ }, [shopperId]);
335
436
  (0, import_react2.useEffect)(() => {
336
437
  return () => {
337
438
  var _a;
@@ -366,7 +467,7 @@ function useSearch() {
366
467
  setLoading(true);
367
468
  setError(null);
368
469
  try {
369
- const res = await client.api.search(query, limit);
470
+ const res = await client.api.searchVector(query, limit);
370
471
  setResults((_b = res.results) != null ? _b : []);
371
472
  } catch (e) {
372
473
  setError((_c = e.message) != null ? _c : "Search failed");
@@ -432,8 +533,47 @@ function usePageIngest(product) {
432
533
  }, [(_a = product == null ? void 0 : product.url) != null ? _a : product == null ? void 0 : product.name]);
433
534
  }
434
535
 
435
- // src/components/SearchBar.tsx
536
+ // src/hooks/useChat.ts
436
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");
437
577
  var import_jsx_runtime2 = require("react/jsx-runtime");
438
578
  var S = `
439
579
  .hsk-wrap{position:relative;width:100%;font-family:inherit}
@@ -457,12 +597,12 @@ function SearchBar({
457
597
  dropdownClassName,
458
598
  renderResult
459
599
  }) {
460
- const [query, setQuery] = (0, import_react6.useState)("");
461
- const [open, setOpen] = (0, import_react6.useState)(false);
600
+ const [query, setQuery] = (0, import_react7.useState)("");
601
+ const [open, setOpen] = (0, import_react7.useState)(false);
462
602
  const { results, loading, search, clear } = useSearch();
463
- const timer = (0, import_react6.useRef)();
464
- const wrap = (0, import_react6.useRef)(null);
465
- (0, import_react6.useEffect)(() => {
603
+ const timer = (0, import_react7.useRef)();
604
+ const wrap = (0, import_react7.useRef)(null);
605
+ (0, import_react7.useEffect)(() => {
466
606
  clearTimeout(timer.current);
467
607
  if (!query.trim()) {
468
608
  clear();
@@ -475,7 +615,7 @@ function SearchBar({
475
615
  }, debounceMs);
476
616
  return () => clearTimeout(timer.current);
477
617
  }, [query, search, clear, limit, debounceMs]);
478
- (0, import_react6.useEffect)(() => {
618
+ (0, import_react7.useEffect)(() => {
479
619
  const handler = (e) => {
480
620
  if (wrap.current && !wrap.current.contains(e.target)) setOpen(false);
481
621
  };
@@ -530,7 +670,7 @@ function SearchBar({
530
670
  }
531
671
 
532
672
  // src/components/Sparkle.tsx
533
- var import_react7 = require("react");
673
+ var import_react8 = require("react");
534
674
  var import_jsx_runtime3 = require("react/jsx-runtime");
535
675
  var S2 = `
536
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}
@@ -539,11 +679,11 @@ var S2 = `
539
679
  `;
540
680
  function Sparkle({ productName, limit = 5, onResult, className }) {
541
681
  const client = useHuskelContext();
542
- const [loading, setLoading] = (0, import_react7.useState)(false);
682
+ const [loading, setLoading] = (0, import_react8.useState)(false);
543
683
  const handleClick = async () => {
544
684
  setLoading(true);
545
685
  try {
546
- const res = await client.api.search(productName, limit);
686
+ const res = await client.api.searchVector(productName, limit);
547
687
  onResult == null ? void 0 : onResult(res.results);
548
688
  } catch (e) {
549
689
  console.error("[Huskel Sparkle]", e);
@@ -559,8 +699,153 @@ function Sparkle({ productName, limit = 5, onResult, className }) {
559
699
  ] })
560
700
  ] });
561
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
+ }
562
846
  // Annotate the CommonJS export names for ESM import in node:
563
847
  0 && (module.exports = {
848
+ ChatWidget,
564
849
  HuskelAPI,
565
850
  HuskelClient,
566
851
  HuskelProvider,
@@ -568,6 +853,7 @@ function Sparkle({ productName, limit = 5, onResult, className }) {
568
853
  Sparkle,
569
854
  getHuskelClient,
570
855
  initHuskel,
856
+ useChat,
571
857
  useHuskel,
572
858
  useIngest,
573
859
  usePageIngest,