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