@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 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 HuskelClient = class {
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.search(query, limit);
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/components/SearchBar.tsx
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, import_react6.useState)("");
520
- 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);
521
602
  const { results, loading, search, clear } = useSearch();
522
- const timer = (0, import_react6.useRef)();
523
- const wrap = (0, import_react6.useRef)(null);
524
- (0, import_react6.useEffect)(() => {
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, import_react6.useEffect)(() => {
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 import_react7 = require("react");
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, import_react7.useState)(false);
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.search(productName, limit);
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,