@huskel/sdk 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -38,6 +38,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
38
38
  // src/index.ts
39
39
  var index_exports = {};
40
40
  __export(index_exports, {
41
+ AIChatButton: () => AIChatButton,
42
+ ChatWidget: () => ChatWidget,
41
43
  HuskelAPI: () => HuskelAPI,
42
44
  HuskelClient: () => HuskelClient,
43
45
  HuskelProvider: () => HuskelProvider,
@@ -45,6 +47,7 @@ __export(index_exports, {
45
47
  Sparkle: () => Sparkle,
46
48
  getHuskelClient: () => getHuskelClient,
47
49
  initHuskel: () => initHuskel,
50
+ useChat: () => useChat,
48
51
  useHuskel: () => useHuskel,
49
52
  useIngest: () => useIngest,
50
53
  usePageIngest: () => usePageIngest,
@@ -134,6 +137,19 @@ var HuskelAPI = class {
134
137
  log("info", "search query", query);
135
138
  return this.post("/search", { query, siteId: this.siteId, limit });
136
139
  }
140
+ // Pure vector search — no LLM, instant results. This is what the SearchBar uses.
141
+ async searchVector(query, limit = 10) {
142
+ return this.post("/search/vector", { query, siteId: this.siteId, limit });
143
+ }
144
+ // Autocomplete — pure in-memory Trie, <1ms, no Upstash call. Only true prefix matches.
145
+ async searchAutocomplete(query, limit = 8) {
146
+ return this.post("/search/autocomplete", { query, siteId: this.siteId, limit });
147
+ }
148
+ // LLM chat — conversational search with history context.
149
+ async chat(query, history = []) {
150
+ log("info", "chat query", query);
151
+ return this.post("/chat", { query, siteId: this.siteId, history });
152
+ }
137
153
  };
138
154
 
139
155
  // src/client.ts
@@ -227,7 +243,7 @@ function generateUUID() {
227
243
  return v.toString(16);
228
244
  });
229
245
  }
230
- var HuskelClient = class {
246
+ var _HuskelClient = class _HuskelClient {
231
247
  constructor(config) {
232
248
  this.ingestQueue = [];
233
249
  this.ingestTimer = null;
@@ -242,6 +258,7 @@ var HuskelClient = class {
242
258
  if (!apiToken) console.error('[Huskel] Missing apiToken. Set it via <HuskelProvider apiToken="..."> or NEXT_PUBLIC_HUSKEL_API_TOKEN.');
243
259
  this.shopperId = config.shopperId;
244
260
  this.initSession();
261
+ this.loadIngestedCache();
245
262
  this.api = new HuskelAPI(
246
263
  apiUrl,
247
264
  siteId,
@@ -258,6 +275,31 @@ var HuskelClient = class {
258
275
  window.addEventListener("online", this.onlineHandler);
259
276
  }
260
277
  }
278
+ // 24h
279
+ loadIngestedCache() {
280
+ if (typeof window === "undefined") return;
281
+ try {
282
+ const raw = localStorage.getItem(_HuskelClient.INGEST_CACHE_KEY);
283
+ if (!raw) return;
284
+ const { ts, urls } = JSON.parse(raw);
285
+ if (Date.now() - ts > _HuskelClient.INGEST_CACHE_TTL) {
286
+ localStorage.removeItem(_HuskelClient.INGEST_CACHE_KEY);
287
+ return;
288
+ }
289
+ this.ingestedUrls = new Set(urls);
290
+ } catch (e) {
291
+ }
292
+ }
293
+ saveIngestedCache() {
294
+ if (typeof window === "undefined") return;
295
+ try {
296
+ localStorage.setItem(
297
+ _HuskelClient.INGEST_CACHE_KEY,
298
+ JSON.stringify({ ts: Date.now(), urls: [...this.ingestedUrls] })
299
+ );
300
+ } catch (e) {
301
+ }
302
+ }
261
303
  setShopperId(id) {
262
304
  this.shopperId = id;
263
305
  }
@@ -300,6 +342,7 @@ var HuskelClient = class {
300
342
  return;
301
343
  }
302
344
  this.ingestedUrls.add(product.url);
345
+ this.saveIngestedCache();
303
346
  this.ingestQueue.push(product);
304
347
  this.scheduleFlush();
305
348
  }
@@ -314,6 +357,7 @@ var HuskelClient = class {
314
357
  this.ingestQueue.push(product);
315
358
  });
316
359
  if (this.ingestQueue.length > 0) {
360
+ this.saveIngestedCache();
317
361
  this.scheduleFlush();
318
362
  }
319
363
  }
@@ -345,6 +389,9 @@ var HuskelClient = class {
345
389
  }
346
390
  }
347
391
  };
392
+ _HuskelClient.INGEST_CACHE_KEY = "huskel_ingested_v1";
393
+ _HuskelClient.INGEST_CACHE_TTL = 24 * 60 * 60 * 1e3;
394
+ var HuskelClient = _HuskelClient;
348
395
  var instance = null;
349
396
  function initHuskel(config) {
350
397
  instance = new HuskelClient(config);
@@ -413,29 +460,34 @@ function useSearch() {
413
460
  const [results, setResults] = (0, import_react3.useState)([]);
414
461
  const [loading, setLoading] = (0, import_react3.useState)(false);
415
462
  const [error, setError] = (0, import_react3.useState)(null);
416
- const abortRef = (0, import_react3.useRef)(null);
417
- const search = (0, import_react3.useCallback)(async (query, limit = 10) => {
418
- var _a, _b, _c;
463
+ const genRef = (0, import_react3.useRef)(0);
464
+ const search = (0, import_react3.useCallback)(async (query, limit = 8) => {
465
+ var _a, _b;
419
466
  if (!query.trim()) {
420
467
  setResults([]);
468
+ setLoading(false);
421
469
  return;
422
470
  }
423
- (_a = abortRef.current) == null ? void 0 : _a.abort();
424
- abortRef.current = new AbortController();
425
- setLoading(true);
471
+ const gen = ++genRef.current;
426
472
  setError(null);
427
473
  try {
428
- const res = await client.api.search(query, limit);
429
- setResults((_b = res.results) != null ? _b : []);
474
+ const res = await client.api.searchAutocomplete(query, limit);
475
+ if (gen === genRef.current) {
476
+ setResults((_a = res.results) != null ? _a : []);
477
+ }
430
478
  } catch (e) {
431
- setError((_c = e.message) != null ? _c : "Search failed");
479
+ if (gen === genRef.current) {
480
+ setError((_b = e.message) != null ? _b : "Search failed");
481
+ }
432
482
  } finally {
433
- setLoading(false);
483
+ if (gen === genRef.current) setLoading(false);
434
484
  }
435
485
  }, [client]);
436
486
  const clear = (0, import_react3.useCallback)(() => {
487
+ genRef.current++;
437
488
  setResults([]);
438
489
  setError(null);
490
+ setLoading(false);
439
491
  }, []);
440
492
  return { results, loading, error, search, clear };
441
493
  }
@@ -491,135 +543,1602 @@ function usePageIngest(product) {
491
543
  }, [(_a = product == null ? void 0 : product.url) != null ? _a : product == null ? void 0 : product.name]);
492
544
  }
493
545
 
494
- // src/components/SearchBar.tsx
546
+ // src/hooks/useChat.ts
495
547
  var import_react6 = require("react");
548
+ function useChat() {
549
+ const client = useHuskelContext();
550
+ const [messages, setMessages] = (0, import_react6.useState)([]);
551
+ const [sources, setSources] = (0, import_react6.useState)([]);
552
+ const [loading, setLoading] = (0, import_react6.useState)(false);
553
+ const [error, setError] = (0, import_react6.useState)(null);
554
+ const abortRef = (0, import_react6.useRef)(null);
555
+ const send = (0, import_react6.useCallback)(async (query) => {
556
+ var _a, _b, _c;
557
+ if (!query.trim() || loading) return;
558
+ (_a = abortRef.current) == null ? void 0 : _a.abort();
559
+ abortRef.current = new AbortController();
560
+ const userMsg = { role: "user", content: query };
561
+ setMessages((prev) => [...prev, userMsg]);
562
+ setLoading(true);
563
+ setError(null);
564
+ try {
565
+ const history = messages.map((m) => ({ role: m.role, content: m.content }));
566
+ const res = await client.api.chat(query, history);
567
+ const assistantMsg = { role: "assistant", content: res.answer };
568
+ setMessages((prev) => [...prev, assistantMsg]);
569
+ setSources((_b = res.sources) != null ? _b : []);
570
+ } catch (e) {
571
+ setError((_c = e == null ? void 0 : e.message) != null ? _c : "Chat request failed");
572
+ setMessages((prev) => prev.slice(0, -1));
573
+ } finally {
574
+ setLoading(false);
575
+ }
576
+ }, [client, messages, loading]);
577
+ const reset = (0, import_react6.useCallback)(() => {
578
+ setMessages([]);
579
+ setSources([]);
580
+ setError(null);
581
+ }, []);
582
+ return { messages, sources, loading, error, send, reset };
583
+ }
584
+
585
+ // src/components/SearchBar.tsx
586
+ var import_react7 = require("react");
496
587
  var import_jsx_runtime2 = require("react/jsx-runtime");
497
- var S = `
498
- .hsk-wrap{position:relative;width:100%;font-family:inherit}
499
- .hsk-input{width:100%;padding:10px 16px;font-size:15px;border:1.5px solid #e2e2e2;border-radius:8px;outline:none;box-sizing:border-box;background:#fff;transition:border-color .2s}
500
- .hsk-input:focus{border-color:#f47c3c}
501
- .hsk-drop{position:absolute;top:calc(100% + 6px);left:0;right:0;background:#fff;border:1px solid #e2e2e2;border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,.1);z-index:9999;max-height:360px;overflow-y:auto}
502
- .hsk-item{display:flex;align-items:center;gap:12px;padding:10px 14px;cursor:pointer;transition:background .15s}
503
- .hsk-item:hover{background:#faf5f1}
504
- .hsk-item img{width:40px;height:40px;object-fit:cover;border-radius:4px}
505
- .hsk-item-name{font-size:14px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
506
- .hsk-item-price{font-size:13px;color:#f47c3c;margin-top:2px}
507
- .hsk-msg{padding:16px;text-align:center;font-size:14px;color:#888}
588
+ var CSS = `
589
+ /* \u2500\u2500 Light mode defaults (CSS custom properties) \u2500\u2500 */
590
+ .hsk-sb-wrap {
591
+ --hsk-bg: #fff;
592
+ --hsk-border: #d1d5db;
593
+ --hsk-text: #111;
594
+ --hsk-muted: #9ca3af;
595
+ --hsk-hover: #f3f4f6;
596
+ --hsk-drop-shadow: 0 8px 30px rgba(0,0,0,.12);
597
+ position: relative;
598
+ width: 100%;
599
+ font-family: inherit;
600
+ }
601
+
602
+ /* \u2500\u2500 Dark mode overrides at wrap level so ALL children inherit \u2500\u2500 */
603
+ @media (prefers-color-scheme: dark) {
604
+ .hsk-sb-wrap {
605
+ --hsk-bg: #1a1a1b;
606
+ --hsk-border: #2a2a2d;
607
+ --hsk-text: #f3f3f2;
608
+ --hsk-muted: #666;
609
+ --hsk-hover: #1e1e1f;
610
+ --hsk-drop-shadow: 0 12px 40px rgba(0,0,0,.5);
611
+ }
612
+ }
613
+
614
+ /* \u2500\u2500 Input \u2500\u2500 */
615
+ .hsk-sb-input {
616
+ width: 100%;
617
+ padding: 10px 16px 10px 40px;
618
+ font-size: 14px;
619
+ border-radius: 9999px;
620
+ border: 1.5px solid var(--hsk-border);
621
+ outline: none;
622
+ box-sizing: border-box;
623
+ background: var(--hsk-bg);
624
+ color: var(--hsk-text);
625
+ transition: border-color .15s, box-shadow .15s;
626
+ font-family: inherit;
627
+ }
628
+ .hsk-sb-input::placeholder { color: var(--hsk-muted); }
629
+ .hsk-sb-input:focus {
630
+ border-color: #ff6a33;
631
+ box-shadow: 0 0 0 3px rgba(255,106,51,.12);
632
+ }
633
+ .hsk-sb-icon {
634
+ position: absolute;
635
+ left: 14px;
636
+ top: 50%;
637
+ transform: translateY(-50%);
638
+ color: var(--hsk-muted);
639
+ pointer-events: none;
640
+ display: flex;
641
+ align-items: center;
642
+ }
643
+
644
+ /* \u2500\u2500 Dropdown \u2500\u2500 */
645
+ .hsk-sb-drop {
646
+ position: absolute;
647
+ top: calc(100% + 6px);
648
+ left: 0; right: 0;
649
+ background: var(--hsk-bg);
650
+ border: 1px solid var(--hsk-border);
651
+ border-radius: 12px;
652
+ box-shadow: var(--hsk-drop-shadow);
653
+ z-index: 9999;
654
+ overflow: hidden;
655
+ padding: 6px 0;
656
+ }
657
+
658
+ /* \u2500\u2500 Row \u2500\u2500 */
659
+ .hsk-sb-row {
660
+ display: flex;
661
+ align-items: center;
662
+ gap: 12px;
663
+ padding: 9px 16px;
664
+ cursor: pointer;
665
+ transition: background .1s;
666
+ }
667
+ .hsk-sb-row:hover { background: var(--hsk-hover); }
668
+ .hsk-sb-row-icon {
669
+ color: var(--hsk-muted);
670
+ flex-shrink: 0;
671
+ display: flex;
672
+ align-items: center;
673
+ }
674
+ .hsk-sb-row-body { flex: 1; min-width: 0; }
675
+ .hsk-sb-row-title {
676
+ font-size: 13px;
677
+ font-weight: 500;
678
+ color: var(--hsk-text);
679
+ white-space: nowrap;
680
+ overflow: hidden;
681
+ text-overflow: ellipsis;
682
+ line-height: 1.3;
683
+ }
684
+ .hsk-sb-row-sub {
685
+ font-size: 11px;
686
+ color: var(--hsk-muted);
687
+ margin-top: 2px;
688
+ white-space: nowrap;
689
+ overflow: hidden;
690
+ text-overflow: ellipsis;
691
+ }
692
+ .hsk-sb-empty {
693
+ padding: 14px 16px;
694
+ font-size: 13px;
695
+ color: var(--hsk-muted);
696
+ }
697
+
698
+ /* \u2500\u2500 Thin accent bar while loading (non-intrusive, no text) \u2500\u2500 */
699
+ .hsk-sb-loading-bar {
700
+ height: 2px;
701
+ background: linear-gradient(90deg, transparent, #ff6a33, transparent);
702
+ background-size: 200% 100%;
703
+ animation: hsk-sweep 0.9s linear infinite;
704
+ position: absolute;
705
+ top: 0; left: 0; right: 0;
706
+ }
707
+ @keyframes hsk-sweep {
708
+ 0% { background-position: 200% 0; }
709
+ 100% { background-position: -200% 0; }
710
+ }
711
+
712
+ /* \u2500\u2500 Staggered fade-in \u2500\u2500 */
713
+ .hsk-sb-fade { animation: hsk-fin .1s ease-out both; }
714
+ @keyframes hsk-fin { from { opacity:0; transform:translateY(3px); } to { opacity:1; transform:none; } }
508
715
  `;
716
+ var SearchIcon = () => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("svg", { width: "15", height: "15", viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", children: [
717
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "8.5", cy: "8.5", r: "5.5" }),
718
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "13", y1: "13", x2: "18", y2: "18" })
719
+ ] });
509
720
  function SearchBar({
510
- placeholder = "Search for what you want \u2014 how you want",
721
+ placeholder = "Search products\u2026",
511
722
  limit = 10,
512
- debounceMs = 300,
723
+ debounceMs = 80,
513
724
  onSelect,
514
725
  className,
515
726
  inputClassName,
516
727
  dropdownClassName,
517
728
  renderResult
518
729
  }) {
519
- const [query, setQuery] = (0, import_react6.useState)("");
520
- const [open, setOpen] = (0, import_react6.useState)(false);
730
+ const [query, setQuery] = (0, import_react7.useState)("");
731
+ const [open, setOpen] = (0, import_react7.useState)(false);
521
732
  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)(() => {
733
+ const timer = (0, import_react7.useRef)();
734
+ const wrap = (0, import_react7.useRef)(null);
735
+ (0, import_react7.useEffect)(() => {
525
736
  clearTimeout(timer.current);
526
737
  if (!query.trim()) {
527
738
  clear();
528
739
  setOpen(false);
529
740
  return;
530
741
  }
742
+ setOpen(true);
531
743
  timer.current = setTimeout(() => {
532
744
  search(query, limit);
533
- setOpen(true);
534
745
  }, debounceMs);
535
746
  return () => clearTimeout(timer.current);
536
- }, [query, search, clear, limit, debounceMs]);
537
- (0, import_react6.useEffect)(() => {
538
- const handler = (e) => {
747
+ }, [query]);
748
+ (0, import_react7.useEffect)(() => {
749
+ const h = (e) => {
539
750
  if (wrap.current && !wrap.current.contains(e.target)) setOpen(false);
540
751
  };
541
- document.addEventListener("mousedown", handler);
542
- return () => document.removeEventListener("mousedown", handler);
752
+ document.addEventListener("mousedown", h);
753
+ return () => document.removeEventListener("mousedown", h);
543
754
  }, []);
544
755
  const handleSelect = (r) => {
545
756
  setOpen(false);
546
757
  setQuery(r.product.name);
547
758
  onSelect == null ? void 0 : onSelect(r);
548
759
  };
760
+ const showDrop = open && query.trim().length > 0;
549
761
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
550
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("style", { children: S }),
551
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `hsk-wrap ${className != null ? className : ""}`, ref: wrap, children: [
762
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("style", { children: CSS }),
763
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `hsk-sb-wrap ${className != null ? className : ""}`, ref: wrap, children: [
764
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "hsk-sb-icon", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(SearchIcon, {}) }),
552
765
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
553
766
  "input",
554
767
  {
555
- className: `hsk-input ${inputClassName != null ? inputClassName : ""}`,
768
+ className: `hsk-sb-input ${inputClassName != null ? inputClassName : ""}`,
556
769
  type: "text",
557
770
  value: query,
558
771
  placeholder,
559
772
  onChange: (e) => setQuery(e.target.value),
560
- onFocus: () => results.length && setOpen(true)
773
+ onFocus: () => results.length > 0 && query.trim() && setOpen(true),
774
+ autoComplete: "off",
775
+ spellCheck: false
561
776
  }
562
777
  ),
563
- open && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `hsk-drop ${dropdownClassName != null ? dropdownClassName : ""}`, children: [
564
- loading && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "hsk-msg", children: "Searching\u2026" }),
565
- !loading && results.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "hsk-msg", children: [
566
- 'No results for "',
778
+ showDrop && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `hsk-sb-drop ${dropdownClassName != null ? dropdownClassName : ""}`, style: { position: "absolute" }, children: [
779
+ loading && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "hsk-sb-loading-bar" }),
780
+ results.length === 0 && !loading && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "hsk-sb-empty", children: [
781
+ "No results for \u201C",
567
782
  query,
568
- '"'
783
+ "\u201D"
569
784
  ] }),
570
- results.map(
571
- (r) => {
572
- var _a, _b;
573
- return renderResult ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { onClick: () => handleSelect(r), children: renderResult(r) }, r.id) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "hsk-item", onClick: () => handleSelect(r), children: [
574
- ((_a = r.product.images) == null ? void 0 : _a[0]) && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("img", { src: r.product.images[0], alt: r.product.name }),
575
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { children: [
576
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "hsk-item-name", children: r.product.name }),
577
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "hsk-item-price", children: [
578
- (_b = r.product.currency) != null ? _b : "KES",
579
- " ",
580
- r.product.price
785
+ results.map((r, i) => {
786
+ var _a;
787
+ return renderResult ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
788
+ "div",
789
+ {
790
+ onClick: () => handleSelect(r),
791
+ className: "hsk-sb-fade",
792
+ style: { animationDelay: `${i * 18}ms` },
793
+ children: renderResult(r)
794
+ },
795
+ r.id
796
+ ) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
797
+ "div",
798
+ {
799
+ className: "hsk-sb-row hsk-sb-fade",
800
+ style: { animationDelay: `${i * 18}ms` },
801
+ onClick: () => handleSelect(r),
802
+ children: [
803
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "hsk-sb-row-icon", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(SearchIcon, {}) }),
804
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "hsk-sb-row-body", children: [
805
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "hsk-sb-row-title", children: r.product.name }),
806
+ (r.product.category || r.product.brand) && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "hsk-sb-row-sub", children: (_a = r.product.category) != null ? _a : r.product.brand })
581
807
  ] })
582
- ] })
583
- ] }, r.id);
584
- }
585
- )
808
+ ]
809
+ },
810
+ r.id
811
+ );
812
+ })
586
813
  ] })
587
814
  ] })
588
815
  ] });
589
816
  }
590
817
 
591
818
  // src/components/Sparkle.tsx
592
- var import_react7 = require("react");
819
+ var import_react8 = require("react");
820
+ var import_react_dom = require("react-dom");
593
821
  var import_jsx_runtime3 = require("react/jsx-runtime");
594
- var S2 = `
595
- .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}
596
- .hsk-sparkle:hover{opacity:.88;transform:scale(1.04)}
597
- .hsk-sparkle:disabled{opacity:.5;cursor:not-allowed}
822
+ var CSS2 = `
823
+ /* \u2500\u2500 Trigger button \u2500\u2500 */
824
+ .hsk-sp-btn {
825
+ display: inline-flex;
826
+ align-items: center;
827
+ justify-content: center;
828
+ width: 32px;
829
+ height: 32px;
830
+ border-radius: 8px;
831
+ border: 1px solid var(--hsk-sp-border, rgba(255,106,51,.35));
832
+ background: var(--hsk-sp-bg, rgba(255,106,51,.08));
833
+ color: #ff6a33;
834
+ cursor: pointer;
835
+ font-size: 15px;
836
+ line-height: 1;
837
+ transition: background .15s, border-color .15s, transform .12s;
838
+ flex-shrink: 0;
839
+ padding: 0;
840
+ }
841
+ .hsk-sp-btn:hover {
842
+ background: rgba(255,106,51,.18);
843
+ border-color: rgba(255,106,51,.7);
844
+ transform: scale(1.1);
845
+ }
846
+ .hsk-sp-btn:active { transform: scale(.92); }
847
+
848
+ /* \u2500\u2500 Backdrop \u2500\u2500 */
849
+ .hsk-sp-backdrop {
850
+ position: fixed;
851
+ inset: 0;
852
+ z-index: 99998;
853
+ display: flex;
854
+ align-items: flex-start;
855
+ justify-content: center;
856
+ padding: clamp(48px, 10vh, 96px) 16px 40px;
857
+ animation: hsk-bd-in .2s ease-out both;
858
+ overflow-y: auto;
859
+ }
860
+ @keyframes hsk-bd-in { from { opacity:0; } to { opacity:1; } }
861
+
862
+ /* \u2500\u2500 Modal card \u2500\u2500 */
863
+ .hsk-sp-card {
864
+ width: 100%;
865
+ max-width: 600px;
866
+ border-radius: 18px;
867
+ overflow: hidden;
868
+ animation: hsk-card-in .24s cubic-bezier(.34,1.36,.64,1) both;
869
+ flex-shrink: 0;
870
+ /* light */
871
+ background: var(--hsk-modal-card-bg, #fff);
872
+ border: 1px solid var(--hsk-modal-card-border, rgba(0,0,0,.08));
873
+ box-shadow: 0 32px 80px rgba(0,0,0,.18), 0 2px 8px rgba(0,0,0,.06);
874
+ }
875
+ @keyframes hsk-card-in {
876
+ from { opacity:0; transform: scale(.96) translateY(-12px); }
877
+ to { opacity:1; transform: scale(1) translateY(0); }
878
+ }
879
+
880
+ /* \u2500\u2500 Header \u2500\u2500 */
881
+ .hsk-sp-header {
882
+ display: flex;
883
+ align-items: center;
884
+ gap: 10px;
885
+ padding: 18px 20px 14px;
886
+ border-bottom: 1px solid var(--hsk-modal-divide, rgba(0,0,0,.07));
887
+ }
888
+ .hsk-sp-header-icon { font-size: 18px; color: #ff6a33; flex-shrink: 0; }
889
+ .hsk-sp-header-body { flex: 1; min-width: 0; }
890
+ .hsk-sp-header-title {
891
+ font-size: 14px;
892
+ font-weight: 600;
893
+ color: var(--hsk-modal-text, #111);
894
+ white-space: nowrap;
895
+ overflow: hidden;
896
+ text-overflow: ellipsis;
897
+ }
898
+ .hsk-sp-header-sub {
899
+ font-size: 11px;
900
+ color: var(--hsk-modal-muted, #888);
901
+ margin-top: 2px;
902
+ }
903
+ .hsk-sp-close {
904
+ width: 30px; height: 30px;
905
+ border-radius: 8px;
906
+ border: 1px solid var(--hsk-modal-divide, rgba(0,0,0,.1));
907
+ background: none;
908
+ color: var(--hsk-modal-muted, #888);
909
+ cursor: pointer;
910
+ font-size: 18px;
911
+ display: flex; align-items: center; justify-content: center;
912
+ transition: all .15s;
913
+ flex-shrink: 0;
914
+ }
915
+ .hsk-sp-close:hover { border-color: #ff6a33; color: #ff6a33; }
916
+
917
+ /* \u2500\u2500 Thin loading accent bar \u2500\u2500 */
918
+ .hsk-sp-bar {
919
+ height: 2px;
920
+ background: linear-gradient(90deg, transparent 0%, #ff6a33 40%, #ffaa80 60%, transparent 100%);
921
+ background-size: 200% 100%;
922
+ animation: hsk-bar .9s linear infinite;
923
+ }
924
+ @keyframes hsk-bar { 0%{background-position:200% 0} 100%{background-position:-200% 0} }
925
+
926
+ /* \u2500\u2500 Results list \u2500\u2500 */
927
+ .hsk-sp-results { padding: 10px 12px; display: flex; flex-direction: column; gap: 8px; }
928
+ .hsk-sp-empty { padding: 40px; text-align: center; font-size: 13px; color: var(--hsk-modal-muted, #aaa); }
929
+
930
+ /* \u2500\u2500 Result card (toast-inspired: slides up from bottom) \u2500\u2500 */
931
+ .hsk-sp-item {
932
+ display: flex;
933
+ gap: 14px;
934
+ padding: 14px;
935
+ border-radius: 12px;
936
+ border: 1px solid var(--hsk-modal-item-border, rgba(0,0,0,.07));
937
+ background: var(--hsk-modal-item-bg, #f9f9f9);
938
+ animation: hsk-toast-up .28s cubic-bezier(.22,.68,0,1.2) both;
939
+ overflow: hidden;
940
+ }
941
+ @keyframes hsk-toast-up {
942
+ from { opacity:0; transform: translateY(18px) scale(.97); }
943
+ to { opacity:1; transform: translateY(0) scale(1); }
944
+ }
945
+
946
+ /* image */
947
+ .hsk-sp-img-wrap {
948
+ width: 72px; height: 72px;
949
+ border-radius: 10px;
950
+ background: #fff;
951
+ border: 1px solid var(--hsk-modal-divide, rgba(0,0,0,.07));
952
+ flex-shrink: 0;
953
+ overflow: hidden;
954
+ display: flex; align-items: center; justify-content: center;
955
+ padding: 6px;
956
+ }
957
+ .hsk-sp-img-wrap img { max-width: 100%; max-height: 100%; object-fit: contain; }
958
+ .hsk-sp-img-placeholder { font-size: 26px; }
959
+
960
+ /* body */
961
+ .hsk-sp-item-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
962
+ .hsk-sp-item-name {
963
+ font-size: 14px;
964
+ font-weight: 600;
965
+ color: var(--hsk-modal-text, #111);
966
+ line-height: 1.35;
967
+ overflow: hidden;
968
+ display: -webkit-box;
969
+ -webkit-line-clamp: 2;
970
+ -webkit-box-orient: vertical;
971
+ }
972
+ .hsk-sp-item-cat {
973
+ font-size: 11px;
974
+ font-weight: 600;
975
+ color: #ff6a33;
976
+ text-transform: uppercase;
977
+ letter-spacing: .05em;
978
+ }
979
+ .hsk-sp-item-price-row { display: flex; align-items: baseline; gap: 8px; margin-top: 2px; }
980
+ .hsk-sp-item-price {
981
+ font-size: 18px;
982
+ font-weight: 700;
983
+ color: var(--hsk-modal-text, #111);
984
+ }
985
+ .hsk-sp-item-currency { font-size: 12px; color: var(--hsk-modal-muted, #888); }
986
+
987
+ /* actions */
988
+ .hsk-sp-actions { display: flex; gap: 6px; margin-top: 8px; }
989
+ .hsk-sp-action {
990
+ flex: 1;
991
+ padding: 7px 10px;
992
+ border-radius: 8px;
993
+ font-size: 12px;
994
+ font-weight: 600;
995
+ cursor: pointer;
996
+ border: 1px solid transparent;
997
+ transition: all .15s;
998
+ text-align: center;
999
+ font-family: inherit;
1000
+ }
1001
+ .hsk-sp-action-primary {
1002
+ background: #ff6a33;
1003
+ color: #fff;
1004
+ border-color: #ff6a33;
1005
+ }
1006
+ .hsk-sp-action-primary:hover { background: #e55d2a; }
1007
+ .hsk-sp-action-secondary {
1008
+ background: var(--hsk-action-sec-bg, rgba(0,0,0,.06));
1009
+ color: var(--hsk-modal-muted, #666);
1010
+ border-color: var(--hsk-modal-divide, rgba(0,0,0,.1));
1011
+ }
1012
+ .hsk-sp-action-secondary:hover {
1013
+ background: var(--hsk-action-sec-bg-hover, rgba(0,0,0,.1));
1014
+ color: var(--hsk-modal-text, #333);
1015
+ }
1016
+
1017
+ /* \u2500\u2500 Footer \u2500\u2500 */
1018
+ .hsk-sp-footer {
1019
+ padding: 12px 20px;
1020
+ border-top: 1px solid var(--hsk-modal-divide, rgba(0,0,0,.07));
1021
+ display: flex;
1022
+ align-items: center;
1023
+ gap: 8px;
1024
+ }
1025
+ .hsk-sp-badge {
1026
+ font-size: 10px; font-weight: 700; letter-spacing: .07em; text-transform: uppercase;
1027
+ color: #ff6a33;
1028
+ background: rgba(255,106,51,.1);
1029
+ border: 1px solid rgba(255,106,51,.25);
1030
+ padding: 2px 8px;
1031
+ border-radius: 999px;
1032
+ }
1033
+ .hsk-sp-esc { font-size: 11px; color: var(--hsk-modal-muted, #bbb); margin-left: auto; }
1034
+
1035
+ /* \u2500\u2500 Dark mode \u2500\u2500 */
1036
+ @media (prefers-color-scheme: dark) {
1037
+ .hsk-sp-card {
1038
+ --hsk-modal-card-bg: #111112;
1039
+ --hsk-modal-card-border: rgba(255,255,255,.07);
1040
+ --hsk-modal-text: #f3f3f2;
1041
+ --hsk-modal-muted: #666;
1042
+ --hsk-modal-divide: rgba(255,255,255,.07);
1043
+ --hsk-modal-item-bg: #1a1a1b;
1044
+ --hsk-modal-item-border: rgba(255,255,255,.06);
1045
+ --hsk-action-sec-bg: rgba(255,255,255,.07);
1046
+ --hsk-action-sec-bg-hover: rgba(255,255,255,.12);
1047
+ }
1048
+ .hsk-sp-img-wrap { background: #242425; border-color: rgba(255,255,255,.08); }
1049
+ }
598
1050
  `;
599
- function Sparkle({ productName, limit = 5, onResult, className }) {
600
- const client = useHuskelContext();
601
- const [loading, setLoading] = (0, import_react7.useState)(false);
602
- const handleClick = async () => {
603
- setLoading(true);
604
- try {
605
- const res = await client.api.search(productName, limit);
606
- onResult == null ? void 0 : onResult(res.results);
607
- } catch (e) {
608
- console.error("[Huskel Sparkle]", e);
609
- } finally {
610
- setLoading(false);
1051
+ function SparkleModal({ productName, limit, backdropColor, backdropBlur, onClose, onNavigate, onResult }) {
1052
+ const { results, loading, search } = useSearch();
1053
+ const initiated = (0, import_react8.useRef)(false);
1054
+ (0, import_react8.useEffect)(() => {
1055
+ if (!initiated.current) {
1056
+ initiated.current = true;
1057
+ search(productName, limit);
1058
+ }
1059
+ }, []);
1060
+ (0, import_react8.useEffect)(() => {
1061
+ if (results.length > 0) onResult == null ? void 0 : onResult(results);
1062
+ }, [results]);
1063
+ (0, import_react8.useEffect)(() => {
1064
+ const h = (e) => {
1065
+ if (e.key === "Escape") onClose();
1066
+ };
1067
+ document.addEventListener("keydown", h);
1068
+ return () => document.removeEventListener("keydown", h);
1069
+ }, []);
1070
+ const blurVal = typeof backdropBlur === "number" ? `${backdropBlur}px` : backdropBlur != null ? backdropBlur : "16px";
1071
+ const bg = backdropColor != null ? backdropColor : void 0;
1072
+ const handleNav = (r) => {
1073
+ const prevent = onNavigate == null ? void 0 : onNavigate(r);
1074
+ if (prevent !== false) {
1075
+ onClose();
1076
+ if (r.product.url) window.location.href = r.product.url;
611
1077
  }
612
1078
  };
613
1079
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
614
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("style", { children: S2 }),
615
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("button", { className: `hsk-sparkle ${className != null ? className : ""}`, onClick: handleClick, disabled: loading, children: [
616
- "\u2726 ",
617
- loading ? "Finding\u2026" : "Similar"
1080
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("style", { children: CSS2 }),
1081
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1082
+ "div",
1083
+ {
1084
+ className: "hsk-sp-backdrop",
1085
+ onClick: onClose,
1086
+ style: {
1087
+ backdropFilter: `blur(${blurVal})`,
1088
+ WebkitBackdropFilter: `blur(${blurVal})`,
1089
+ background: bg != null ? bg : void 0
1090
+ /* CSS handles light/dark via @media if bg not forced */
1091
+ },
1092
+ children: [
1093
+ !bg && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("style", { children: `
1094
+ @media (prefers-color-scheme: dark) { .hsk-sp-backdrop { background: rgba(0,0,0,.80); } }
1095
+ @media (prefers-color-scheme: light) { .hsk-sp-backdrop { background: rgba(240,240,245,.70); } }
1096
+ ` }),
1097
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "hsk-sp-card", onClick: (e) => e.stopPropagation(), children: [
1098
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "hsk-sp-header", children: [
1099
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "hsk-sp-header-icon", children: "\u2726" }),
1100
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "hsk-sp-header-body", children: [
1101
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "hsk-sp-header-title", children: [
1102
+ "Similar to \u201C",
1103
+ productName,
1104
+ "\u201D"
1105
+ ] }),
1106
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "hsk-sp-header-sub", children: "AI vector similarity \xB7 instant results" })
1107
+ ] }),
1108
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { className: "hsk-sp-close", onClick: onClose, "aria-label": "Close", children: "\xD7" })
1109
+ ] }),
1110
+ loading && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "hsk-sp-bar" }),
1111
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "hsk-sp-results", children: [
1112
+ !loading && results.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "hsk-sp-empty", children: "No similar products found." }),
1113
+ results.map((r, i) => {
1114
+ var _a, _b, _c;
1115
+ const price = parseFloat(((_a = r.product.price) == null ? void 0 : _a.replace(/[^0-9.]/g, "")) || "0");
1116
+ const currency = (_b = r.product.currency) != null ? _b : "KES";
1117
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1118
+ "div",
1119
+ {
1120
+ className: "hsk-sp-item",
1121
+ style: { animationDelay: `${i * 55}ms` },
1122
+ children: [
1123
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "hsk-sp-img-wrap", children: ((_c = r.product.images) == null ? void 0 : _c[0]) ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("img", { src: r.product.images[0], alt: r.product.name }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "hsk-sp-img-placeholder", children: "\u{1F6CD}" }) }),
1124
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "hsk-sp-item-body", children: [
1125
+ r.product.category && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "hsk-sp-item-cat", children: r.product.category }),
1126
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "hsk-sp-item-name", children: r.product.name }),
1127
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "hsk-sp-item-price-row", children: [
1128
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "hsk-sp-item-currency", children: currency }),
1129
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "hsk-sp-item-price", children: price.toLocaleString() })
1130
+ ] }),
1131
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "hsk-sp-actions", children: [
1132
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1133
+ "button",
1134
+ {
1135
+ className: "hsk-sp-action hsk-sp-action-primary",
1136
+ onClick: () => handleNav(r),
1137
+ children: "View Product"
1138
+ }
1139
+ ),
1140
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1141
+ "button",
1142
+ {
1143
+ className: "hsk-sp-action hsk-sp-action-secondary",
1144
+ onClick: () => onClose(),
1145
+ children: "Add to Cart"
1146
+ }
1147
+ )
1148
+ ] })
1149
+ ] })
1150
+ ]
1151
+ },
1152
+ r.id
1153
+ );
1154
+ })
1155
+ ] }),
1156
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "hsk-sp-footer", children: [
1157
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "hsk-sp-badge", children: "\u2726 Huskel AI" }),
1158
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "hsk-sp-esc", children: "Esc to close" })
1159
+ ] })
1160
+ ] })
1161
+ ]
1162
+ }
1163
+ )
1164
+ ] });
1165
+ }
1166
+ function Sparkle({ productName, limit = 8, onResult, backdropColor, backdropBlur, className, onNavigate }) {
1167
+ const [open, setOpen] = (0, import_react8.useState)(false);
1168
+ const [mounted, setMounted] = (0, import_react8.useState)(false);
1169
+ (0, import_react8.useEffect)(() => {
1170
+ setMounted(true);
1171
+ }, []);
1172
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
1173
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("style", { children: CSS2 }),
1174
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1175
+ "button",
1176
+ {
1177
+ className: `hsk-sp-btn ${className != null ? className : ""}`,
1178
+ onClick: () => setOpen(true),
1179
+ title: "Find similar products",
1180
+ "aria-label": "Find similar products",
1181
+ children: "\u2726"
1182
+ }
1183
+ ),
1184
+ open && mounted && (0, import_react_dom.createPortal)(
1185
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1186
+ SparkleModal,
1187
+ {
1188
+ productName,
1189
+ limit,
1190
+ backdropColor,
1191
+ backdropBlur,
1192
+ onClose: () => setOpen(false),
1193
+ onResult,
1194
+ onNavigate
1195
+ }
1196
+ ),
1197
+ document.body
1198
+ )
1199
+ ] });
1200
+ }
1201
+
1202
+ // src/components/ChatWidget.tsx
1203
+ var import_react9 = require("react");
1204
+ var import_jsx_runtime4 = require("react/jsx-runtime");
1205
+ var S = `
1206
+ .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}
1207
+ .hsk-chat-header{display:flex;align-items:center;gap:10px;padding:14px 16px;border-bottom:1px solid #1e1e1f;background:#111112;flex-shrink:0}
1208
+ .hsk-chat-title{font-size:14px;font-weight:600;color:#f3f3f2}
1209
+ .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}
1210
+ .hsk-chat-messages{flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:12px;scroll-behavior:smooth}
1211
+ .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}
1212
+ .hsk-chat-empty-icon{font-size:28px;margin-bottom:4px}
1213
+ .hsk-msg-row{display:flex;gap:8px;align-items:flex-start}
1214
+ .hsk-msg-row.user{flex-direction:row-reverse}
1215
+ .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}
1216
+ .hsk-msg-avatar.ai{background:#ff6a3320;border:1px solid #ff6a3340;color:#ff6a33}
1217
+ .hsk-msg-avatar.user{background:#2a2a2d;color:#9a9aa1}
1218
+ .hsk-msg-bubble{max-width:78%;padding:10px 14px;border-radius:12px;font-size:13px;line-height:1.6}
1219
+ .hsk-msg-bubble.ai{background:#171718;border:1px solid #2a2a2d;color:#e8e8e7;border-radius:4px 12px 12px 12px}
1220
+ .hsk-msg-bubble.user{background:#ff6a33;color:#fff;border-radius:12px 4px 12px 12px}
1221
+ .hsk-sources{margin-top:10px;display:flex;flex-direction:column;gap:6px}
1222
+ .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}
1223
+ .hsk-source-card:hover{border-color:#ff6a3360}
1224
+ .hsk-source-img{width:36px;height:36px;object-fit:cover;border-radius:4px;background:#fff}
1225
+ .hsk-source-name{font-size:12px;font-weight:500;color:#e8e8e7;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
1226
+ .hsk-source-price{font-size:11px;color:#ff6a33;font-weight:700;margin-top:2px}
1227
+ .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}
1228
+ .hsk-typing-dot{width:6px;height:6px;background:#ff6a33;border-radius:50%;animation:hsk-chat-bounce 1.2s infinite}
1229
+ .hsk-typing-dot:nth-child(2){animation-delay:0.2s}
1230
+ .hsk-typing-dot:nth-child(3){animation-delay:0.4s}
1231
+ @keyframes hsk-chat-bounce{0%,100%{opacity:0.3;transform:translateY(0)}50%{opacity:1;transform:translateY(-4px)}}
1232
+ .hsk-chat-input-area{display:flex;align-items:center;gap:8px;padding:12px 14px;border-top:1px solid #1e1e1f;background:#111112;flex-shrink:0}
1233
+ .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}
1234
+ .hsk-chat-input::placeholder{color:#555}
1235
+ .hsk-chat-input:focus{border-color:#ff6a33}
1236
+ .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}
1237
+ .hsk-chat-send:hover{opacity:0.88}
1238
+ .hsk-chat-send:active{transform:scale(0.93)}
1239
+ .hsk-chat-send:disabled{opacity:0.4;cursor:not-allowed}
1240
+ .hsk-chat-reset{font-size:11px;color:#555;cursor:pointer;padding:0 4px;transition:color 0.15s;background:none;border:none;font-family:inherit}
1241
+ .hsk-chat-reset:hover{color:#ff6a33}
1242
+ `;
1243
+ function SourceCard({ source, onSelect }) {
1244
+ var _a;
1245
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "hsk-source-card", onClick: () => onSelect == null ? void 0 : onSelect(source), children: [
1246
+ source.image && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("img", { src: source.image, alt: source.name, className: "hsk-source-img" }),
1247
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { flex: 1, minWidth: 0 }, children: [
1248
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "hsk-source-name", children: source.name }),
1249
+ source.price && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "hsk-source-price", children: [
1250
+ (_a = source.currency) != null ? _a : "KES",
1251
+ " ",
1252
+ source.price
1253
+ ] })
1254
+ ] })
1255
+ ] });
1256
+ }
1257
+ function ChatWidget({ placeholder = "Ask about anything in our store\u2026", title = "AI Shopping Assistant", className, onSelectSource }) {
1258
+ const { messages, sources, loading, error, send, reset } = useChat();
1259
+ const [input, setInput] = (0, import_react9.useState)("");
1260
+ const bottomRef = (0, import_react9.useRef)(null);
1261
+ const textareaRef = (0, import_react9.useRef)(null);
1262
+ (0, import_react9.useEffect)(() => {
1263
+ var _a;
1264
+ (_a = bottomRef.current) == null ? void 0 : _a.scrollIntoView({ behavior: "smooth" });
1265
+ }, [messages, loading]);
1266
+ const handleSend = async () => {
1267
+ const q = input.trim();
1268
+ if (!q || loading) return;
1269
+ setInput("");
1270
+ if (textareaRef.current) textareaRef.current.style.height = "auto";
1271
+ await send(q);
1272
+ };
1273
+ const handleKey = (e) => {
1274
+ if (e.key === "Enter" && !e.shiftKey) {
1275
+ e.preventDefault();
1276
+ handleSend();
1277
+ }
1278
+ };
1279
+ const handleInput = (e) => {
1280
+ setInput(e.target.value);
1281
+ const t = e.target;
1282
+ t.style.height = "auto";
1283
+ t.style.height = Math.min(t.scrollHeight, 120) + "px";
1284
+ };
1285
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
1286
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("style", { children: S }),
1287
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: `hsk-chat-widget ${className != null ? className : ""}`, children: [
1288
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "hsk-chat-header", children: [
1289
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { fontSize: 16, color: "#ff6a33" }, children: "\u2726" }),
1290
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "hsk-chat-title", children: title }),
1291
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "hsk-chat-badge", children: "AI" }),
1292
+ messages.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: "hsk-chat-reset", onClick: reset, style: { marginLeft: "auto" }, children: "Clear" })
1293
+ ] }),
1294
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "hsk-chat-messages", children: [
1295
+ messages.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "hsk-chat-empty", children: [
1296
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "hsk-chat-empty-icon", children: "\u2726" }),
1297
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { children: "Ask me anything about our products" }),
1298
+ /* @__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"' })
1299
+ ] }) : messages.map((msg, idx) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { children: [
1300
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: `hsk-msg-row ${msg.role}`, children: [
1301
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: `hsk-msg-avatar ${msg.role === "assistant" ? "ai" : "user"}`, children: msg.role === "assistant" ? "\u2726" : "\u2191" }),
1302
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: `hsk-msg-bubble ${msg.role === "assistant" ? "ai" : "user"}`, children: msg.content })
1303
+ ] }),
1304
+ 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)) }) })
1305
+ ] }, idx)),
1306
+ loading && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "hsk-msg-row", children: [
1307
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "hsk-msg-avatar ai", children: "\u2726" }),
1308
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "hsk-typing", children: [
1309
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "hsk-typing-dot" }),
1310
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "hsk-typing-dot" }),
1311
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "hsk-typing-dot" })
1312
+ ] })
1313
+ ] }),
1314
+ error && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { fontSize: 12, color: "#ef4444", textAlign: "center", padding: 8 }, children: error }),
1315
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { ref: bottomRef })
1316
+ ] }),
1317
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "hsk-chat-input-area", children: [
1318
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1319
+ "textarea",
1320
+ {
1321
+ ref: textareaRef,
1322
+ className: "hsk-chat-input",
1323
+ value: input,
1324
+ onChange: handleInput,
1325
+ onKeyDown: handleKey,
1326
+ placeholder,
1327
+ rows: 1,
1328
+ disabled: loading
1329
+ }
1330
+ ),
1331
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1332
+ "button",
1333
+ {
1334
+ className: "hsk-chat-send",
1335
+ onClick: handleSend,
1336
+ disabled: !input.trim() || loading,
1337
+ "aria-label": "Send",
1338
+ children: "\u2191"
1339
+ }
1340
+ )
1341
+ ] })
618
1342
  ] })
619
1343
  ] });
620
1344
  }
1345
+
1346
+ // src/components/AIChatButton.tsx
1347
+ var import_react10 = require("react");
1348
+ var import_react_dom2 = require("react-dom");
1349
+ var import_jsx_runtime5 = require("react/jsx-runtime");
1350
+ var CSS3 = `
1351
+ /* \u2500\u2500 Trigger button \u2500\u2500 */
1352
+ .hsk-cb-btn {
1353
+ display: inline-flex;
1354
+ align-items: center;
1355
+ gap: 7px;
1356
+ padding: 8px 16px;
1357
+ border-radius: 9999px;
1358
+ border: 1px solid rgba(255,106,51,.4);
1359
+ background: rgba(255,106,51,.1);
1360
+ color: #ff6a33;
1361
+ font-size: 13px;
1362
+ font-weight: 600;
1363
+ cursor: pointer;
1364
+ transition: background .15s, border-color .15s, transform .12s, box-shadow .15s;
1365
+ font-family: inherit;
1366
+ white-space: nowrap;
1367
+ }
1368
+ .hsk-cb-btn:hover {
1369
+ background: rgba(255,106,51,.18);
1370
+ border-color: rgba(255,106,51,.7);
1371
+ box-shadow: 0 4px 16px rgba(255,106,51,.2);
1372
+ }
1373
+ .hsk-cb-btn:active { transform: scale(.95); }
1374
+ .hsk-cb-btn-icon { font-size: 15px; line-height: 1; }
1375
+
1376
+ /* \u2500\u2500 Full-screen overlay \u2500\u2500 */
1377
+ .hsk-cb-overlay {
1378
+ position: fixed;
1379
+ inset: 0;
1380
+ z-index: 99999;
1381
+ display: flex;
1382
+ flex-direction: column;
1383
+ animation: hsk-overlay-in .2s ease-out both;
1384
+ }
1385
+ @keyframes hsk-overlay-in {
1386
+ from { opacity: 0; }
1387
+ to { opacity: 1; }
1388
+ }
1389
+
1390
+ /* \u2500\u2500 Panel (Claude-style, centered column) \u2500\u2500 */
1391
+ .hsk-cb-panel {
1392
+ position: relative;
1393
+ display: flex;
1394
+ flex-direction: column;
1395
+ height: 100%;
1396
+ max-width: 780px;
1397
+ width: 100%;
1398
+ margin: 0 auto;
1399
+ animation: hsk-panel-in .28s cubic-bezier(.34,1.2,.64,1) both;
1400
+ }
1401
+ @keyframes hsk-panel-in {
1402
+ from { opacity: 0; transform: translateY(24px); }
1403
+ to { opacity: 1; transform: translateY(0); }
1404
+ }
1405
+
1406
+ /* \u2500\u2500 Top bar \u2500\u2500 */
1407
+ .hsk-cb-topbar {
1408
+ display: flex;
1409
+ align-items: center;
1410
+ justify-content: space-between;
1411
+ padding: 20px 28px 12px;
1412
+ flex-shrink: 0;
1413
+ }
1414
+ .hsk-cb-topbar-left {
1415
+ display: flex;
1416
+ align-items: center;
1417
+ gap: 10px;
1418
+ }
1419
+ .hsk-cb-topbar-icon {
1420
+ font-size: 22px;
1421
+ color: #ff6a33;
1422
+ line-height: 1;
1423
+ animation: hsk-sparkle-spin 6s linear infinite;
1424
+ }
1425
+ @keyframes hsk-sparkle-spin {
1426
+ 0%,100% { transform: rotate(0deg) scale(1); }
1427
+ 25% { transform: rotate(15deg) scale(1.1); }
1428
+ 75% { transform: rotate(-10deg) scale(.95); }
1429
+ }
1430
+ .hsk-cb-topbar-title {
1431
+ font-size: 16px;
1432
+ font-weight: 700;
1433
+ color: var(--hsk-chat-text, #111);
1434
+ letter-spacing: -.01em;
1435
+ }
1436
+ .hsk-cb-topbar-sub {
1437
+ font-size: 12px;
1438
+ color: var(--hsk-chat-muted, #888);
1439
+ margin-top: 2px;
1440
+ }
1441
+ .hsk-cb-topbar-actions {
1442
+ display: flex;
1443
+ align-items: center;
1444
+ gap: 8px;
1445
+ }
1446
+ .hsk-cb-topbar-btn {
1447
+ height: 34px;
1448
+ padding: 0 14px;
1449
+ border-radius: 8px;
1450
+ border: 1px solid var(--hsk-chat-divide, rgba(0,0,0,.1));
1451
+ background: none;
1452
+ color: var(--hsk-chat-muted, #888);
1453
+ font-size: 12px;
1454
+ font-weight: 500;
1455
+ cursor: pointer;
1456
+ transition: all .15s;
1457
+ font-family: inherit;
1458
+ }
1459
+ .hsk-cb-topbar-btn:hover {
1460
+ border-color: #ff6a33;
1461
+ color: #ff6a33;
1462
+ }
1463
+ .hsk-cb-close {
1464
+ width: 34px; height: 34px;
1465
+ border-radius: 8px;
1466
+ border: 1px solid var(--hsk-chat-divide, rgba(0,0,0,.1));
1467
+ background: none;
1468
+ color: var(--hsk-chat-muted, #888);
1469
+ cursor: pointer;
1470
+ font-size: 20px;
1471
+ display: flex; align-items: center; justify-content: center;
1472
+ transition: all .15s;
1473
+ flex-shrink: 0;
1474
+ font-family: inherit;
1475
+ line-height: 1;
1476
+ }
1477
+ .hsk-cb-close:hover { border-color: #ff6a33; color: #ff6a33; }
1478
+
1479
+ /* \u2500\u2500 Messages scroll area \u2500\u2500 */
1480
+ .hsk-cb-msgs {
1481
+ flex: 1;
1482
+ overflow-y: auto;
1483
+ padding: 8px 28px 0;
1484
+ display: flex;
1485
+ flex-direction: column;
1486
+ gap: 0;
1487
+ scroll-behavior: smooth;
1488
+ scrollbar-width: thin;
1489
+ scrollbar-color: var(--hsk-chat-divide, rgba(0,0,0,.1)) transparent;
1490
+ }
1491
+
1492
+ /* \u2500\u2500 Empty / welcome state \u2500\u2500 */
1493
+ .hsk-cb-empty {
1494
+ flex: 1;
1495
+ display: flex;
1496
+ flex-direction: column;
1497
+ align-items: center;
1498
+ justify-content: center;
1499
+ gap: 20px;
1500
+ padding: 60px 32px;
1501
+ text-align: center;
1502
+ }
1503
+ .hsk-cb-empty-icon {
1504
+ font-size: 48px;
1505
+ color: #ff6a33;
1506
+ animation: hsk-sparkle-spin 4s linear infinite;
1507
+ }
1508
+ .hsk-cb-empty-title {
1509
+ font-size: 26px;
1510
+ font-weight: 700;
1511
+ color: var(--hsk-chat-text, #111);
1512
+ letter-spacing: -.02em;
1513
+ }
1514
+ .hsk-cb-empty-sub {
1515
+ font-size: 14px;
1516
+ color: var(--hsk-chat-muted, #888);
1517
+ line-height: 1.7;
1518
+ max-width: 380px;
1519
+ }
1520
+ .hsk-cb-chips {
1521
+ display: flex;
1522
+ flex-wrap: wrap;
1523
+ gap: 8px;
1524
+ justify-content: center;
1525
+ margin-top: 4px;
1526
+ }
1527
+ .hsk-cb-chip {
1528
+ padding: 8px 16px;
1529
+ border-radius: 9999px;
1530
+ border: 1px solid var(--hsk-chat-divide, rgba(0,0,0,.1));
1531
+ background: var(--hsk-chat-source-bg, rgba(0,0,0,.03));
1532
+ color: var(--hsk-chat-text, #333);
1533
+ font-size: 13px;
1534
+ cursor: pointer;
1535
+ transition: all .15s;
1536
+ font-family: inherit;
1537
+ }
1538
+ .hsk-cb-chip:hover {
1539
+ border-color: #ff6a33;
1540
+ color: #ff6a33;
1541
+ background: rgba(255,106,51,.06);
1542
+ }
1543
+
1544
+ /* \u2500\u2500 Message rows \u2500\u2500 */
1545
+ .hsk-cb-msg-group {
1546
+ padding: 20px 0;
1547
+ border-bottom: 1px solid var(--hsk-chat-divide, rgba(0,0,0,.05));
1548
+ animation: hsk-msg-in .22s ease-out both;
1549
+ }
1550
+ .hsk-cb-msg-group:last-child { border-bottom: none; }
1551
+ @keyframes hsk-msg-in {
1552
+ from { opacity: 0; transform: translateY(10px); }
1553
+ to { opacity: 1; transform: translateY(0); }
1554
+ }
1555
+
1556
+ /* User message */
1557
+ .hsk-cb-user-msg {
1558
+ display: flex;
1559
+ justify-content: flex-end;
1560
+ margin-bottom: 20px;
1561
+ }
1562
+ .hsk-cb-user-bubble {
1563
+ background: #ff6a33;
1564
+ color: #fff;
1565
+ padding: 12px 20px;
1566
+ border-radius: 22px 22px 6px 22px;
1567
+ font-size: 15px;
1568
+ line-height: 1.6;
1569
+ max-width: 72%;
1570
+ font-weight: 500;
1571
+ }
1572
+
1573
+ /* AI message - no bubble, just clean text like Claude */
1574
+ .hsk-cb-ai-msg {
1575
+ display: flex;
1576
+ align-items: flex-start;
1577
+ gap: 14px;
1578
+ }
1579
+ .hsk-cb-ai-icon {
1580
+ width: 28px; height: 28px;
1581
+ border-radius: 50%;
1582
+ background: rgba(255,106,51,.12);
1583
+ border: 1px solid rgba(255,106,51,.25);
1584
+ color: #ff6a33;
1585
+ font-size: 13px;
1586
+ display: flex; align-items: center; justify-content: center;
1587
+ flex-shrink: 0;
1588
+ margin-top: 2px;
1589
+ }
1590
+ .hsk-cb-ai-body { flex: 1; min-width: 0; }
1591
+ .hsk-cb-ai-text {
1592
+ font-size: 15px;
1593
+ line-height: 1.75;
1594
+ color: var(--hsk-chat-text, #111);
1595
+ white-space: pre-wrap;
1596
+ }
1597
+
1598
+ /* \u2500\u2500 Sources horizontal carousel \u2500\u2500 */
1599
+ .hsk-cb-sources-wrap {
1600
+ position: relative;
1601
+ margin-top: 20px;
1602
+ }
1603
+ .hsk-cb-sources {
1604
+ display: flex;
1605
+ flex-direction: row;
1606
+ gap: 14px;
1607
+ overflow-x: auto;
1608
+ scroll-snap-type: x mandatory;
1609
+ scrollbar-width: none;
1610
+ -ms-overflow-style: none;
1611
+ padding-bottom: 4px;
1612
+ }
1613
+ .hsk-cb-sources::-webkit-scrollbar { display: none; }
1614
+ /* Feathered right edge */
1615
+ .hsk-cb-sources-fade {
1616
+ position: absolute;
1617
+ right: 0; top: 0; bottom: 4px;
1618
+ width: 90px;
1619
+ pointer-events: none;
1620
+ }
1621
+ /* Scroll-next pill */
1622
+ .hsk-cb-sources-next {
1623
+ position: absolute;
1624
+ right: 10px;
1625
+ top: 50%;
1626
+ transform: translateY(-50%);
1627
+ width: 30px; height: 30px;
1628
+ border-radius: 50%;
1629
+ border: 1px solid var(--hsk-chat-divide, rgba(0,0,0,.12));
1630
+ background: var(--hsk-chat-bg, #0e0e0f);
1631
+ color: var(--hsk-chat-text, #eee);
1632
+ cursor: pointer;
1633
+ font-size: 16px;
1634
+ display: flex; align-items: center; justify-content: center;
1635
+ box-shadow: 0 2px 12px rgba(0,0,0,.2);
1636
+ transition: all .15s;
1637
+ z-index: 3;
1638
+ font-family: inherit;
1639
+ line-height: 1;
1640
+ }
1641
+ .hsk-cb-sources-next:hover { border-color: #ff6a33; color: #ff6a33; }
1642
+ /* Card: no border, no bg, no radius \u2014 clean dark canvas */
1643
+ .hsk-cb-source {
1644
+ flex: 0 0 188px;
1645
+ scroll-snap-align: start;
1646
+ border-radius: 0;
1647
+ border: none;
1648
+ background: transparent;
1649
+ cursor: pointer;
1650
+ transition: transform .14s, opacity .14s;
1651
+ animation: hsk-card-in .26s ease-out both;
1652
+ overflow: visible;
1653
+ }
1654
+ @keyframes hsk-card-in {
1655
+ from { opacity: 0; transform: translateX(16px); }
1656
+ to { opacity: 1; transform: none; }
1657
+ }
1658
+ .hsk-cb-source:hover { transform: translateY(-3px); opacity: .92; }
1659
+ .hsk-cb-src-imgwrap {
1660
+ width: 188px;
1661
+ height: 188px;
1662
+ overflow: hidden;
1663
+ border-radius: 0;
1664
+ display: block;
1665
+ }
1666
+ .hsk-cb-src-imgwrap img {
1667
+ width: 100%; height: 100%;
1668
+ object-fit: cover;
1669
+ transition: transform .22s;
1670
+ display: block;
1671
+ }
1672
+ .hsk-cb-source:hover .hsk-cb-src-imgwrap img { transform: scale(1.05); }
1673
+ .hsk-cb-src-imgwrap-empty {
1674
+ width: 188px;
1675
+ height: 188px;
1676
+ background: var(--hsk-chat-divide, rgba(255,255,255,.06));
1677
+ display: flex; align-items: center; justify-content: center;
1678
+ color: var(--hsk-chat-muted, #555);
1679
+ font-size: 32px;
1680
+ }
1681
+ .hsk-cb-src-info {
1682
+ padding: 8px 2px 0;
1683
+ }
1684
+ .hsk-cb-src-name {
1685
+ font-size: 13px;
1686
+ font-weight: 600;
1687
+ color: var(--hsk-chat-text, #eee);
1688
+ line-height: 1.4;
1689
+ display: -webkit-box;
1690
+ -webkit-line-clamp: 2;
1691
+ -webkit-box-orient: vertical;
1692
+ overflow: hidden;
1693
+ }
1694
+ .hsk-cb-src-price {
1695
+ font-size: 13px;
1696
+ color: #ff6a33;
1697
+ font-weight: 700;
1698
+ margin-top: 3px;
1699
+ }
1700
+
1701
+ /* \u2500\u2500 Selected product inline card \u2500\u2500 */
1702
+ .hsk-cb-selected-product {
1703
+ display: flex;
1704
+ align-items: flex-start;
1705
+ gap: 14px;
1706
+ margin-top: 16px;
1707
+ padding: 14px;
1708
+ border: 1px solid var(--hsk-chat-divide, rgba(255,255,255,.08));
1709
+ border-left: 3px solid #ff6a33;
1710
+ background: var(--hsk-chat-source-bg, rgba(255,255,255,.03));
1711
+ cursor: pointer;
1712
+ transition: border-color .15s;
1713
+ animation: hsk-msg-in .2s ease-out both;
1714
+ }
1715
+ .hsk-cb-selected-product:hover { border-left-color: rgba(255,106,51,.6); }
1716
+ .hsk-cb-selected-img {
1717
+ width: 64px; height: 64px;
1718
+ object-fit: cover;
1719
+ flex-shrink: 0;
1720
+ }
1721
+ .hsk-cb-selected-info { flex: 1; min-width: 0; }
1722
+ .hsk-cb-selected-name {
1723
+ font-size: 13px; font-weight: 700;
1724
+ color: var(--hsk-chat-text, #eee);
1725
+ margin-bottom: 3px;
1726
+ }
1727
+ .hsk-cb-selected-price {
1728
+ font-size: 13px; color: #ff6a33; font-weight: 700;
1729
+ }
1730
+
1731
+ /* \u2500\u2500 Typing indicator \u2500\u2500 */
1732
+ .hsk-cb-typing-row {
1733
+ display: flex;
1734
+ align-items: flex-start;
1735
+ gap: 14px;
1736
+ padding: 20px 0;
1737
+ }
1738
+ .hsk-cb-typing {
1739
+ display: flex;
1740
+ gap: 5px;
1741
+ padding: 14px 18px;
1742
+ }
1743
+ .hsk-cb-dot {
1744
+ width: 7px; height: 7px;
1745
+ border-radius: 50%;
1746
+ background: var(--hsk-chat-muted, #ccc);
1747
+ animation: hsk-dot-pulse 1.2s ease-in-out infinite;
1748
+ }
1749
+ .hsk-cb-dot:nth-child(2) { animation-delay: .18s; }
1750
+ .hsk-cb-dot:nth-child(3) { animation-delay: .36s; }
1751
+ @keyframes hsk-dot-pulse {
1752
+ 0%,100% { opacity: .3; transform: scale(.75); }
1753
+ 50% { opacity: 1; transform: scale(1); }
1754
+ }
1755
+
1756
+ /* \u2500\u2500 Input area \u2500\u2500 */
1757
+ .hsk-cb-input-wrap {
1758
+ padding: 16px 28px 28px;
1759
+ flex-shrink: 0;
1760
+ }
1761
+ .hsk-cb-input-box {
1762
+ display: flex;
1763
+ align-items: flex-end;
1764
+ gap: 10px;
1765
+ background: var(--hsk-chat-input-bg, rgba(0,0,0,.04));
1766
+ border: 1.5px solid var(--hsk-chat-divide, rgba(0,0,0,.1));
1767
+ border-radius: 18px;
1768
+ padding: 14px 14px 14px 20px;
1769
+ transition: border-color .15s, box-shadow .15s;
1770
+ }
1771
+ .hsk-cb-input-box:focus-within {
1772
+ border-color: #ff6a33;
1773
+ box-shadow: 0 0 0 3px rgba(255,106,51,.1);
1774
+ }
1775
+ .hsk-cb-textarea {
1776
+ flex: 1;
1777
+ background: transparent;
1778
+ border: none;
1779
+ outline: none;
1780
+ resize: none;
1781
+ font-size: 15px;
1782
+ color: var(--hsk-chat-text, #111);
1783
+ min-height: 24px;
1784
+ max-height: 140px;
1785
+ line-height: 1.55;
1786
+ font-family: inherit;
1787
+ }
1788
+ .hsk-cb-textarea::placeholder { color: var(--hsk-chat-muted, #aaa); }
1789
+ .hsk-cb-send {
1790
+ width: 38px; height: 38px;
1791
+ border-radius: 10px;
1792
+ background: #ff6a33;
1793
+ border: none;
1794
+ color: #fff;
1795
+ cursor: pointer;
1796
+ font-size: 18px;
1797
+ display: flex; align-items: center; justify-content: center;
1798
+ flex-shrink: 0;
1799
+ transition: opacity .15s, transform .1s, background .15s;
1800
+ font-family: inherit;
1801
+ }
1802
+ .hsk-cb-send:hover { opacity: .88; }
1803
+ .hsk-cb-send:active { transform: scale(.9); }
1804
+ .hsk-cb-send:disabled { opacity: .3; cursor: not-allowed; background: var(--hsk-chat-muted, #ccc); }
1805
+ .hsk-cb-hint {
1806
+ text-align: center;
1807
+ font-size: 11px;
1808
+ color: var(--hsk-chat-muted, #bbb);
1809
+ margin-top: 10px;
1810
+ }
1811
+
1812
+ /* \u2500\u2500 Error \u2500\u2500 */
1813
+ .hsk-cb-error {
1814
+ margin: 8px 0;
1815
+ padding: 10px 14px;
1816
+ border-radius: 10px;
1817
+ background: rgba(239,68,68,.08);
1818
+ border: 1px solid rgba(239,68,68,.2);
1819
+ color: #ef4444;
1820
+ font-size: 13px;
1821
+ }
1822
+
1823
+ /* \u2500\u2500 Dark mode \u2500\u2500 */
1824
+ @media (prefers-color-scheme: dark) {
1825
+ .hsk-cb-overlay {
1826
+ --hsk-chat-bg: #0e0e0f;
1827
+ --hsk-chat-text: #f0efed;
1828
+ --hsk-chat-muted: #555;
1829
+ --hsk-chat-divide: rgba(255,255,255,.07);
1830
+ --hsk-chat-input-bg: rgba(255,255,255,.05);
1831
+ --hsk-chat-source-bg: rgba(255,255,255,.04);
1832
+ --hsk-fade-bg: #0e0e0f;
1833
+ }
1834
+ .hsk-cb-overlay {
1835
+ background: rgba(0,0,0,.92) !important;
1836
+ }
1837
+ }
1838
+ @media (prefers-color-scheme: light) {
1839
+ .hsk-cb-overlay {
1840
+ --hsk-chat-bg: #fafafa;
1841
+ --hsk-chat-text: #111;
1842
+ --hsk-chat-muted: #999;
1843
+ --hsk-chat-divide: rgba(0,0,0,.08);
1844
+ --hsk-chat-input-bg: rgba(0,0,0,.04);
1845
+ --hsk-chat-source-bg: rgba(0,0,0,.025);
1846
+ --hsk-fade-bg: #fafafa;
1847
+ }
1848
+ .hsk-cb-overlay {
1849
+ background: rgba(240,240,244,.88) !important;
1850
+ }
1851
+ }
1852
+ `;
1853
+ var CHIPS = [
1854
+ "Cheapest smartphone",
1855
+ "Smart TV under KSh 20,000",
1856
+ "Noise-cancelling headphones",
1857
+ "Best laptop for students"
1858
+ ];
1859
+ function SourcesCarousel({ sources, onSelectSource }) {
1860
+ const railRef = (0, import_react10.useRef)(null);
1861
+ const [showNext, setShowNext] = (0, import_react10.useState)(false);
1862
+ const measure = (0, import_react10.useCallback)(() => {
1863
+ const el = railRef.current;
1864
+ if (!el) return;
1865
+ const atEnd = el.scrollLeft + el.clientWidth >= el.scrollWidth - 8;
1866
+ setShowNext(el.scrollWidth > el.clientWidth + 4 && !atEnd);
1867
+ }, []);
1868
+ (0, import_react10.useEffect)(() => {
1869
+ measure();
1870
+ const el = railRef.current;
1871
+ if (!el) return;
1872
+ const ro = new ResizeObserver(measure);
1873
+ ro.observe(el);
1874
+ el.addEventListener("scroll", measure, { passive: true });
1875
+ return () => {
1876
+ ro.disconnect();
1877
+ el.removeEventListener("scroll", measure);
1878
+ };
1879
+ }, [measure, sources]);
1880
+ const scrollNext = () => {
1881
+ var _a;
1882
+ (_a = railRef.current) == null ? void 0 : _a.scrollBy({ left: 170, behavior: "smooth" });
1883
+ };
1884
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "hsk-cb-sources-wrap", children: [
1885
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "hsk-cb-sources", ref: railRef, children: sources.map((src, si) => {
1886
+ var _a;
1887
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
1888
+ "div",
1889
+ {
1890
+ className: "hsk-cb-source",
1891
+ style: { animationDelay: `${si * 50}ms` },
1892
+ onClick: () => onSelectSource == null ? void 0 : onSelectSource(src),
1893
+ children: [
1894
+ src.image ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "hsk-cb-src-imgwrap", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("img", { src: src.image, alt: src.name, loading: "lazy" }) }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "hsk-cb-src-imgwrap-empty", children: "\u2726" }),
1895
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "hsk-cb-src-info", children: [
1896
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "hsk-cb-src-name", children: src.name }),
1897
+ src.price && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "hsk-cb-src-price", children: [
1898
+ (_a = src.currency) != null ? _a : "KES",
1899
+ " ",
1900
+ parseFloat(src.price.replace(/[^0-9.]/g, "") || "0").toLocaleString()
1901
+ ] })
1902
+ ] })
1903
+ ]
1904
+ },
1905
+ si
1906
+ );
1907
+ }) }),
1908
+ showNext && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
1909
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1910
+ "div",
1911
+ {
1912
+ className: "hsk-cb-sources-fade",
1913
+ style: { background: "linear-gradient(to right, transparent, var(--hsk-fade-bg, #0e0e0f))" }
1914
+ }
1915
+ ),
1916
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { className: "hsk-cb-sources-next", onClick: scrollNext, "aria-label": "See more", children: "\u203A" })
1917
+ ] })
1918
+ ] });
1919
+ }
1920
+ function ChatModal({
1921
+ title = "AI Shopping Assistant",
1922
+ placeholder = "Ask me anything \u2014 gifts, budget, use case\u2026",
1923
+ backdropColor,
1924
+ backdropBlur,
1925
+ onClose,
1926
+ onSelectSource
1927
+ }) {
1928
+ var _a, _b;
1929
+ const { messages, sources, loading, error, send, reset } = useChat();
1930
+ const [input, setInput] = (0, import_react10.useState)("");
1931
+ const [selectedProduct, setSelectedProduct] = (0, import_react10.useState)(null);
1932
+ const bottomRef = (0, import_react10.useRef)(null);
1933
+ const textareaRef = (0, import_react10.useRef)(null);
1934
+ (0, import_react10.useEffect)(() => {
1935
+ var _a2;
1936
+ (_a2 = bottomRef.current) == null ? void 0 : _a2.scrollIntoView({ behavior: "smooth" });
1937
+ }, [messages, loading, selectedProduct]);
1938
+ (0, import_react10.useEffect)(() => {
1939
+ const h = (e) => {
1940
+ if (e.key === "Escape") onClose();
1941
+ };
1942
+ document.addEventListener("keydown", h);
1943
+ return () => document.removeEventListener("keydown", h);
1944
+ }, []);
1945
+ const handleSourceClick = (src) => {
1946
+ var _a2;
1947
+ setSelectedProduct(src);
1948
+ onSelectSource == null ? void 0 : onSelectSource(src);
1949
+ const q = `Tell me more about the ${src.name}${src.price ? ` (${(_a2 = src.currency) != null ? _a2 : "KES"} ${src.price})` : ""} \u2014 what are its key specs, who is it best for, and is it worth buying?`;
1950
+ send(q);
1951
+ };
1952
+ const handleSend = async (text) => {
1953
+ const q = (text != null ? text : input).trim();
1954
+ if (!q || loading) return;
1955
+ setSelectedProduct(null);
1956
+ setInput("");
1957
+ if (textareaRef.current) {
1958
+ textareaRef.current.style.height = "auto";
1959
+ }
1960
+ await send(q);
1961
+ };
1962
+ const handleKeyDown = (e) => {
1963
+ if (e.key === "Enter" && !e.shiftKey) {
1964
+ e.preventDefault();
1965
+ handleSend();
1966
+ }
1967
+ };
1968
+ const handleInput = (e) => {
1969
+ setInput(e.target.value);
1970
+ const t = e.target;
1971
+ t.style.height = "auto";
1972
+ t.style.height = `${Math.min(t.scrollHeight, 140)}px`;
1973
+ };
1974
+ const blurVal = typeof backdropBlur === "number" ? `${backdropBlur}px` : backdropBlur != null ? backdropBlur : "20px";
1975
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
1976
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("style", { children: CSS3 }),
1977
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1978
+ "div",
1979
+ {
1980
+ className: "hsk-cb-overlay",
1981
+ onClick: onClose,
1982
+ style: __spreadValues({
1983
+ backdropFilter: `blur(${blurVal})`,
1984
+ WebkitBackdropFilter: `blur(${blurVal})`
1985
+ }, backdropColor ? { background: backdropColor } : {}),
1986
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "hsk-cb-panel", onClick: (e) => e.stopPropagation(), children: [
1987
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "hsk-cb-topbar", children: [
1988
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "hsk-cb-topbar-left", children: [
1989
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "hsk-cb-topbar-icon", children: "\u2726" }),
1990
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { children: [
1991
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "hsk-cb-topbar-title", children: title }),
1992
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "hsk-cb-topbar-sub", children: "Powered by Huskel AI \xB7 searches the whole catalogue" })
1993
+ ] })
1994
+ ] }),
1995
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "hsk-cb-topbar-actions", children: [
1996
+ messages.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { className: "hsk-cb-topbar-btn", onClick: reset, children: "Clear chat" }),
1997
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { className: "hsk-cb-close", onClick: onClose, "aria-label": "Close", children: "\xD7" })
1998
+ ] })
1999
+ ] }),
2000
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "hsk-cb-msgs", children: [
2001
+ messages.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "hsk-cb-empty", children: [
2002
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "hsk-cb-empty-icon", children: "\u2726" }),
2003
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "hsk-cb-empty-title", children: "What can I help you find?" }),
2004
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "hsk-cb-empty-sub", children: "Ask about products, budgets, gift ideas, specs \u2014 I'll search the entire catalogue for you." }),
2005
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "hsk-cb-chips", children: CHIPS.map((chip) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2006
+ "button",
2007
+ {
2008
+ className: "hsk-cb-chip",
2009
+ onClick: () => handleSend(chip),
2010
+ children: chip
2011
+ },
2012
+ chip
2013
+ )) })
2014
+ ] }) : messages.map((msg, idx) => {
2015
+ const isLast = idx === messages.length - 1;
2016
+ const isUser = msg.role === "user";
2017
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "hsk-cb-msg-group", children: isUser ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "hsk-cb-user-msg", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "hsk-cb-user-bubble", children: msg.content }) }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "hsk-cb-ai-msg", children: [
2018
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "hsk-cb-ai-icon", children: "\u2726" }),
2019
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "hsk-cb-ai-body", children: [
2020
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "hsk-cb-ai-text", children: msg.content }),
2021
+ isLast && sources.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2022
+ SourcesCarousel,
2023
+ {
2024
+ sources,
2025
+ onSelectSource: handleSourceClick
2026
+ }
2027
+ )
2028
+ ] })
2029
+ ] }) }, idx);
2030
+ }),
2031
+ selectedProduct && loading && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2032
+ "div",
2033
+ {
2034
+ className: "hsk-cb-selected-product",
2035
+ onClick: () => selectedProduct.url && window.open(selectedProduct.url, "_blank"),
2036
+ children: [
2037
+ selectedProduct.image && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("img", { className: "hsk-cb-selected-img", src: selectedProduct.image, alt: selectedProduct.name }),
2038
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "hsk-cb-selected-info", children: [
2039
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "hsk-cb-selected-name", children: selectedProduct.name }),
2040
+ selectedProduct.price && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "hsk-cb-selected-price", children: [
2041
+ (_a = selectedProduct.currency) != null ? _a : "KES",
2042
+ " ",
2043
+ parseFloat(((_b = selectedProduct.price) != null ? _b : "").replace(/[^0-9.]/g, "") || "0").toLocaleString()
2044
+ ] })
2045
+ ] })
2046
+ ]
2047
+ }
2048
+ ),
2049
+ loading && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "hsk-cb-typing-row", children: [
2050
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "hsk-cb-ai-icon", children: "\u2726" }),
2051
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "hsk-cb-typing", children: [
2052
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "hsk-cb-dot" }),
2053
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "hsk-cb-dot" }),
2054
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "hsk-cb-dot" })
2055
+ ] })
2056
+ ] }),
2057
+ error && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "hsk-cb-error", children: error }),
2058
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { ref: bottomRef, style: { height: 1 } })
2059
+ ] }),
2060
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "hsk-cb-input-wrap", children: [
2061
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "hsk-cb-input-box", children: [
2062
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2063
+ "textarea",
2064
+ {
2065
+ ref: textareaRef,
2066
+ className: "hsk-cb-textarea",
2067
+ value: input,
2068
+ onChange: handleInput,
2069
+ onKeyDown: handleKeyDown,
2070
+ placeholder,
2071
+ rows: 1,
2072
+ disabled: loading,
2073
+ autoFocus: true
2074
+ }
2075
+ ),
2076
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2077
+ "button",
2078
+ {
2079
+ className: "hsk-cb-send",
2080
+ onClick: () => handleSend(),
2081
+ disabled: !input.trim() || loading,
2082
+ "aria-label": "Send",
2083
+ children: "\u2191"
2084
+ }
2085
+ )
2086
+ ] }),
2087
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "hsk-cb-hint", children: "Huskel AI \xB7 searches the whole catalogue in real time" })
2088
+ ] })
2089
+ ] })
2090
+ }
2091
+ )
2092
+ ] });
2093
+ }
2094
+ function AIChatButton({
2095
+ label,
2096
+ title,
2097
+ placeholder,
2098
+ backdropColor,
2099
+ backdropBlur,
2100
+ className,
2101
+ onSelectSource
2102
+ }) {
2103
+ const [open, setOpen] = (0, import_react10.useState)(false);
2104
+ const [mounted, setMounted] = (0, import_react10.useState)(false);
2105
+ (0, import_react10.useEffect)(() => {
2106
+ setMounted(true);
2107
+ }, []);
2108
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
2109
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("style", { children: CSS3 }),
2110
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2111
+ "button",
2112
+ {
2113
+ className: `hsk-cb-btn ${className != null ? className : ""}`,
2114
+ onClick: () => setOpen(true),
2115
+ "aria-label": "Open AI chat",
2116
+ children: [
2117
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "hsk-cb-btn-icon", children: "\u2726" }),
2118
+ label !== void 0 ? label : null
2119
+ ]
2120
+ }
2121
+ ),
2122
+ open && mounted && (0, import_react_dom2.createPortal)(
2123
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2124
+ ChatModal,
2125
+ {
2126
+ title,
2127
+ placeholder,
2128
+ backdropColor,
2129
+ backdropBlur,
2130
+ onClose: () => setOpen(false),
2131
+ onSelectSource
2132
+ }
2133
+ ),
2134
+ document.body
2135
+ )
2136
+ ] });
2137
+ }
621
2138
  // Annotate the CommonJS export names for ESM import in node:
622
2139
  0 && (module.exports = {
2140
+ AIChatButton,
2141
+ ChatWidget,
623
2142
  HuskelAPI,
624
2143
  HuskelClient,
625
2144
  HuskelProvider,
@@ -627,6 +2146,7 @@ function Sparkle({ productName, limit = 5, onResult, className }) {
627
2146
  Sparkle,
628
2147
  getHuskelClient,
629
2148
  initHuskel,
2149
+ useChat,
630
2150
  useHuskel,
631
2151
  useIngest,
632
2152
  usePageIngest,