@huskel/sdk 0.4.2 → 0.4.6

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
@@ -63,7 +63,15 @@ var HuskelAPI = class {
63
63
  });
64
64
  if (!res.ok) {
65
65
  const text = await res.text();
66
- const err = { status: res.status, message: text };
66
+ let message = text;
67
+ try {
68
+ const parsed = JSON.parse(text);
69
+ if (parsed && typeof parsed.error === "string") {
70
+ message = parsed.error;
71
+ }
72
+ } catch (e) {
73
+ }
74
+ const err = { status: res.status, message };
67
75
  if (res.status >= 400 && res.status < 500) {
68
76
  log("error", `${path} failed [${res.status}]`, text);
69
77
  throw err;
@@ -114,6 +122,52 @@ var HuskelAPI = class {
114
122
  log("info", "chat query", query);
115
123
  return this.post("/chat", { query, siteId: this.siteId, history });
116
124
  }
125
+ // --- Cart System ---
126
+ buildHeaders() {
127
+ var _a, _b;
128
+ const headers = {
129
+ "Content-Type": "application/json",
130
+ "X-Huskel-Token": this.apiToken,
131
+ "X-Huskel-Site": this.siteId
132
+ };
133
+ const shopperId = (_a = this.getShopperId) == null ? void 0 : _a.call(this);
134
+ if (shopperId) headers["X-Huskel-Shopper-Id"] = shopperId;
135
+ const sessionId = (_b = this.getSessionId) == null ? void 0 : _b.call(this);
136
+ if (sessionId) headers["X-Huskel-Session-Id"] = sessionId;
137
+ return headers;
138
+ }
139
+ async getCart() {
140
+ const res = await fetch(`${this.apiUrl}/cart?siteId=${this.siteId}`, {
141
+ headers: this.buildHeaders()
142
+ });
143
+ if (!res.ok) throw new Error("Failed to fetch cart");
144
+ return res.json();
145
+ }
146
+ async clearCart() {
147
+ const res = await fetch(`${this.apiUrl}/cart?siteId=${this.siteId}`, {
148
+ method: "DELETE",
149
+ headers: this.buildHeaders()
150
+ });
151
+ if (!res.ok) throw new Error("Failed to clear cart");
152
+ return res.json();
153
+ }
154
+ async checkoutCart() {
155
+ const res = await fetch(`${this.apiUrl}/cart/checkout`, {
156
+ method: "POST",
157
+ headers: this.buildHeaders(),
158
+ body: JSON.stringify({ siteId: this.siteId })
159
+ });
160
+ if (!res.ok) throw new Error("Failed to checkout cart");
161
+ return res.json();
162
+ }
163
+ async getCheckoutConfig() {
164
+ const res = await fetch(`${this.apiUrl}/checkout/config?site_id=${this.siteId}`, {
165
+ method: "GET",
166
+ headers: this.buildHeaders()
167
+ });
168
+ if (!res.ok) throw new Error("Failed to fetch checkout config");
169
+ return res.json();
170
+ }
117
171
  };
118
172
 
119
173
  // src/client.ts
@@ -221,13 +275,14 @@ var _HuskelClient = class _HuskelClient {
221
275
  if (!apiUrl) console.error('[Huskel] Missing apiUrl. Set it via <HuskelProvider apiUrl="..."> or NEXT_PUBLIC_HUSKEL_API_URL.');
222
276
  if (!apiToken) console.error('[Huskel] Missing apiToken. Set it via <HuskelProvider apiToken="..."> or NEXT_PUBLIC_HUSKEL_API_TOKEN.');
223
277
  this.shopperId = config.shopperId;
278
+ this.onCheckout = config.onCheckout;
224
279
  this.initSession();
225
280
  this.loadIngestedCache();
226
281
  this.api = new HuskelAPI(
227
282
  apiUrl,
228
283
  siteId,
229
284
  apiToken,
230
- () => this.shopperId,
285
+ () => this.getShopperId(),
231
286
  () => this.sessionId
232
287
  );
233
288
  instance = this;
@@ -264,11 +319,14 @@ var _HuskelClient = class _HuskelClient {
264
319
  } catch (e) {
265
320
  }
266
321
  }
322
+ reRegister() {
323
+ instance = this;
324
+ }
267
325
  setShopperId(id) {
268
326
  this.shopperId = id;
269
327
  }
270
328
  getShopperId() {
271
- return this.shopperId;
329
+ return this.shopperId || "guest_" + this.sessionId;
272
330
  }
273
331
  getSessionId() {
274
332
  return this.sessionId;
@@ -397,11 +455,17 @@ function HuskelProvider({ siteId, apiUrl, apiToken, shopperId, children }) {
397
455
  const clientRef = useRef2(null);
398
456
  if (!clientRef.current) {
399
457
  clientRef.current = new HuskelClient({ siteId, apiUrl, apiToken, shopperId });
458
+ } else {
459
+ clientRef.current.reRegister();
400
460
  }
401
461
  useEffect(() => {
402
462
  var _a;
403
463
  (_a = clientRef.current) == null ? void 0 : _a.setShopperId(shopperId);
404
464
  }, [shopperId]);
465
+ useEffect(() => {
466
+ var _a;
467
+ (_a = clientRef.current) == null ? void 0 : _a.reRegister();
468
+ }, []);
405
469
  useEffect(() => {
406
470
  return () => {
407
471
  var _a;
@@ -433,6 +497,7 @@ function useSearch() {
433
497
  return;
434
498
  }
435
499
  const gen = ++genRef.current;
500
+ setLoading(true);
436
501
  setError(null);
437
502
  try {
438
503
  const res = await client.api.searchAutocomplete(query, limit);
@@ -441,7 +506,15 @@ function useSearch() {
441
506
  }
442
507
  } catch (e) {
443
508
  if (gen === genRef.current) {
444
- setError((_b = e.message) != null ? _b : "Search failed");
509
+ let msg = (_b = e == null ? void 0 : e.message) != null ? _b : "Search failed";
510
+ try {
511
+ const parsed = JSON.parse(msg);
512
+ if (parsed && parsed.error) {
513
+ msg = parsed.error;
514
+ }
515
+ } catch (e2) {
516
+ }
517
+ setError(msg);
445
518
  }
446
519
  } finally {
447
520
  if (gen === genRef.current) setLoading(false);
@@ -516,13 +589,13 @@ function useChat() {
516
589
  const [loading, setLoading] = useState3(false);
517
590
  const [error, setError] = useState3(null);
518
591
  const abortRef = useRef5(null);
519
- const send = useCallback3(async (query) => {
520
- var _a, _b, _c;
592
+ const send = useCallback3(async (query, displayQuery) => {
593
+ var _a, _b, _c, _d, _e;
521
594
  if (!query.trim() || loading) return;
522
595
  (_a = abortRef.current) == null ? void 0 : _a.abort();
523
596
  abortRef.current = new AbortController();
524
597
  const signal = abortRef.current.signal;
525
- const userMsg = { role: "user", content: query };
598
+ const userMsg = { role: "user", content: displayQuery != null ? displayQuery : query };
526
599
  setMessages((prev) => [...prev, userMsg]);
527
600
  setLoading(true);
528
601
  setError(null);
@@ -548,9 +621,30 @@ function useChat() {
548
621
  }
549
622
  if (signal.aborted) return;
550
623
  setSources((_b = res.sources) != null ? _b : []);
624
+ if (((_c = res.action) == null ? void 0 : _c.type) === "add_to_cart" || res.checkout) {
625
+ if (typeof window !== "undefined") {
626
+ window.dispatchEvent(new CustomEvent("huskel:cart_updated", { detail: res.checkout }));
627
+ }
628
+ }
629
+ if (((_d = res.action) == null ? void 0 : _d.type) === "checkout") {
630
+ if (typeof window !== "undefined") {
631
+ window.dispatchEvent(new CustomEvent("huskel:trigger_checkout", { detail: res.checkout }));
632
+ }
633
+ }
634
+ if (res.checkout && client.onCheckout) {
635
+ client.onCheckout(res.checkout);
636
+ }
551
637
  } catch (e) {
552
638
  if (signal.aborted) return;
553
- setError((_c = e == null ? void 0 : e.message) != null ? _c : "Chat request failed");
639
+ let msg = (_e = e == null ? void 0 : e.message) != null ? _e : "Chat request failed";
640
+ try {
641
+ const parsed = JSON.parse(msg);
642
+ if (parsed && parsed.error) {
643
+ msg = parsed.error;
644
+ }
645
+ } catch (e2) {
646
+ }
647
+ setError(msg);
554
648
  setMessages((prev) => prev.slice(0, -1));
555
649
  } finally {
556
650
  if (!signal.aborted) {
@@ -569,9 +663,54 @@ function useChat() {
569
663
  return { messages, sources, loading, error, send, reset };
570
664
  }
571
665
 
666
+ // src/hooks/useCart.ts
667
+ import { useState as useState4, useEffect as useEffect3, useCallback as useCallback4 } from "react";
668
+ function useCart() {
669
+ const client = useHuskelContext();
670
+ const [cart, setCart] = useState4(null);
671
+ const [loading, setLoading] = useState4(false);
672
+ const shopperId = client.getShopperId();
673
+ const fetchCart = useCallback4(async () => {
674
+ if (!shopperId) return;
675
+ setLoading(true);
676
+ try {
677
+ const res = await client.api.getCart();
678
+ setCart(res);
679
+ } catch (e) {
680
+ console.error("[Huskel] Failed to fetch cart", e);
681
+ } finally {
682
+ setLoading(false);
683
+ }
684
+ }, [client, shopperId]);
685
+ useEffect3(() => {
686
+ fetchCart();
687
+ const handleCartUpdate = (e) => {
688
+ if (e.detail) {
689
+ setCart(e.detail);
690
+ } else {
691
+ fetchCart();
692
+ }
693
+ };
694
+ if (typeof window !== "undefined") {
695
+ window.addEventListener("huskel:cart_updated", handleCartUpdate);
696
+ return () => window.removeEventListener("huskel:cart_updated", handleCartUpdate);
697
+ }
698
+ }, [fetchCart, shopperId]);
699
+ return { cart, loading, fetchCart };
700
+ }
701
+
702
+ // src/components/SearchBar.tsx
703
+ import { useState as useState5, useEffect as useEffect4, useRef as useRef6 } from "react";
704
+
705
+ // src/utils/cn.ts
706
+ import { clsx } from "clsx";
707
+ import { twMerge } from "tailwind-merge";
708
+ function cn(...inputs) {
709
+ return twMerge(clsx(inputs));
710
+ }
711
+
572
712
  // src/components/SearchBar.tsx
573
- import { useState as useState4, useEffect as useEffect3, useRef as useRef6 } from "react";
574
- import { jsx as jsx2, jsxs } from "react/jsx-runtime";
713
+ import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
575
714
  var SearchIcon = () => /* @__PURE__ */ jsxs("svg", { width: "15", height: "15", viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", children: [
576
715
  /* @__PURE__ */ jsx2("circle", { cx: "8.5", cy: "8.5", r: "5.5" }),
577
716
  /* @__PURE__ */ jsx2("line", { x1: "13", y1: "13", x2: "18", y2: "18" })
@@ -579,7 +718,7 @@ var SearchIcon = () => /* @__PURE__ */ jsxs("svg", { width: "15", height: "15",
579
718
  function SearchBar({
580
719
  placeholder = "Search products\u2026",
581
720
  limit = 10,
582
- debounceMs = 80,
721
+ debounceMs = 300,
583
722
  onSelect,
584
723
  className,
585
724
  inputClassName,
@@ -588,25 +727,35 @@ function SearchBar({
588
727
  theme,
589
728
  classNames = {}
590
729
  }) {
591
- const [query, setQuery] = useState4("");
592
- const [open, setOpen] = useState4(false);
730
+ const [query, setQuery] = useState5("");
731
+ const [open, setOpen] = useState5(false);
732
+ const [isDebouncing, setIsDebouncing] = useState5(false);
593
733
  const { results, loading, search, clear } = useSearch();
734
+ const client = useHuskelContext();
594
735
  const timer = useRef6();
595
736
  const wrap = useRef6(null);
596
- useEffect3(() => {
737
+ const ignoreNextQueryChange = useRef6(false);
738
+ useEffect4(() => {
739
+ if (ignoreNextQueryChange.current) {
740
+ ignoreNextQueryChange.current = false;
741
+ return;
742
+ }
597
743
  clearTimeout(timer.current);
598
744
  if (!query.trim()) {
599
745
  clear();
600
746
  setOpen(false);
747
+ setIsDebouncing(false);
601
748
  return;
602
749
  }
603
750
  setOpen(true);
751
+ setIsDebouncing(true);
604
752
  timer.current = setTimeout(() => {
753
+ setIsDebouncing(false);
605
754
  search(query, limit);
606
755
  }, debounceMs);
607
756
  return () => clearTimeout(timer.current);
608
757
  }, [query]);
609
- useEffect3(() => {
758
+ useEffect4(() => {
610
759
  const h = (e) => {
611
760
  if (wrap.current && !wrap.current.contains(e.target)) setOpen(false);
612
761
  };
@@ -614,75 +763,220 @@ function SearchBar({
614
763
  return () => document.removeEventListener("mousedown", h);
615
764
  }, []);
616
765
  const handleSelect = (r) => {
766
+ if (query.trim()) {
767
+ client.api.searchVector(query, 1).catch(() => {
768
+ });
769
+ }
770
+ ignoreNextQueryChange.current = true;
617
771
  setOpen(false);
618
772
  setQuery(r.product.name);
619
773
  onSelect == null ? void 0 : onSelect(r);
620
774
  };
775
+ const handleCommitSearch = () => {
776
+ if (!query.trim()) return;
777
+ client.api.searchVector(query, 1).catch(() => {
778
+ });
779
+ if (results.length > 0) {
780
+ handleSelect(results[0]);
781
+ }
782
+ };
621
783
  const showDrop = open && query.trim().length > 0;
622
- const customStyles = __spreadValues(__spreadValues(__spreadValues(__spreadValues({}, (theme == null ? void 0 : theme.primaryColor) && { "--hsk-primary": theme.primaryColor }), (theme == null ? void 0 : theme.backgroundColor) && { "--hsk-bg": theme.backgroundColor }), (theme == null ? void 0 : theme.textColor) && { "--hsk-text": theme.textColor }), (theme == null ? void 0 : theme.fontFamily) && { "--hsk-font": theme.fontFamily });
623
- return /* @__PURE__ */ jsxs("div", { className: `hsk-sb-wrap ${classNames.root || ""} ${className || ""}`, ref: wrap, style: customStyles, children: [
784
+ const customStyles = __spreadValues(__spreadValues(__spreadValues(__spreadValues(__spreadValues({}, (theme == null ? void 0 : theme.primaryColor) && { "--hsk-primary": theme.primaryColor }), (theme == null ? void 0 : theme.backgroundColor) && { "--hsk-bg": theme.backgroundColor }), (theme == null ? void 0 : theme.textColor) && { "--hsk-text": theme.textColor }), (theme == null ? void 0 : theme.fontFamily) && { "--hsk-font": theme.fontFamily }), (theme == null ? void 0 : theme.borderRadius) && { "--hsk-border-radius": theme.borderRadius });
785
+ return /* @__PURE__ */ jsxs("div", { className: cn("hsk-sb-wrap", classNames.root, className), ref: wrap, style: customStyles, children: [
624
786
  /* @__PURE__ */ jsx2("span", { className: "hsk-sb-icon", children: /* @__PURE__ */ jsx2(SearchIcon, {}) }),
625
787
  /* @__PURE__ */ jsx2(
626
788
  "input",
627
789
  {
628
- className: `hsk-sb-input ${classNames.input || ""} ${inputClassName || ""}`,
790
+ className: cn("hsk-sb-input", classNames.input, inputClassName),
629
791
  type: "text",
630
792
  value: query,
631
793
  placeholder,
632
794
  onChange: (e) => setQuery(e.target.value),
633
795
  onFocus: () => results.length > 0 && query.trim() && setOpen(true),
796
+ onKeyDown: (e) => {
797
+ if (e.key === "Enter") {
798
+ handleCommitSearch();
799
+ }
800
+ },
634
801
  autoComplete: "off",
635
802
  spellCheck: false
636
803
  }
637
804
  ),
638
- showDrop && /* @__PURE__ */ jsxs("div", { className: `hsk-sb-drop ${classNames.dropdown || ""} ${dropdownClassName || ""}`, style: { position: "absolute" }, children: [
639
- loading && /* @__PURE__ */ jsx2("div", { className: "hsk-sb-loading-bar" }),
640
- results.length === 0 && !loading && /* @__PURE__ */ jsxs("div", { className: "hsk-sb-empty", children: [
641
- "No results for \u201C",
642
- query,
643
- "\u201D"
644
- ] }),
645
- results.map((r, i) => {
646
- var _a;
647
- return renderResult ? /* @__PURE__ */ jsx2(
648
- "div",
649
- {
650
- onClick: () => handleSelect(r),
651
- className: "hsk-sb-fade",
652
- style: { animationDelay: `${i * 18}ms` },
653
- children: renderResult(r)
654
- },
655
- r.id
656
- ) : /* @__PURE__ */ jsxs(
657
- "div",
658
- {
659
- className: `hsk-sb-row hsk-sb-fade ${classNames.row || ""}`,
660
- style: { animationDelay: `${i * 18}ms` },
661
- onClick: () => handleSelect(r),
662
- children: [
663
- /* @__PURE__ */ jsx2("span", { className: "hsk-sb-row-icon", children: /* @__PURE__ */ jsx2(SearchIcon, {}) }),
664
- /* @__PURE__ */ jsxs("div", { className: "hsk-sb-row-body", children: [
665
- /* @__PURE__ */ jsx2("div", { className: "hsk-sb-row-title", children: r.product.name }),
666
- (r.product.category || r.product.brand) && /* @__PURE__ */ jsx2("div", { className: "hsk-sb-row-sub", children: (_a = r.product.category) != null ? _a : r.product.brand })
667
- ] })
668
- ]
669
- },
670
- r.id
671
- );
672
- })
805
+ showDrop && /* @__PURE__ */ jsxs("div", { className: cn("hsk-sb-drop", classNames.dropdown, dropdownClassName), style: { position: "absolute" }, children: [
806
+ (loading || isDebouncing) && /* @__PURE__ */ jsx2("div", { className: "hsk-sb-loading-bar" }),
807
+ (loading || isDebouncing) && results.length === 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
808
+ /* @__PURE__ */ jsxs("div", { className: "hsk-sb-skeleton-row", children: [
809
+ /* @__PURE__ */ jsx2("span", { className: "hsk-sb-skeleton-icon" }),
810
+ /* @__PURE__ */ jsxs("div", { className: "hsk-sb-row-body", children: [
811
+ /* @__PURE__ */ jsx2("div", { className: "hsk-sb-skeleton-text1" }),
812
+ /* @__PURE__ */ jsx2("div", { className: "hsk-sb-skeleton-text2" })
813
+ ] })
814
+ ] }),
815
+ /* @__PURE__ */ jsxs("div", { className: "hsk-sb-skeleton-row", children: [
816
+ /* @__PURE__ */ jsx2("span", { className: "hsk-sb-skeleton-icon" }),
817
+ /* @__PURE__ */ jsxs("div", { className: "hsk-sb-row-body", children: [
818
+ /* @__PURE__ */ jsx2("div", { className: "hsk-sb-skeleton-text1", style: { width: "45%" } }),
819
+ /* @__PURE__ */ jsx2("div", { className: "hsk-sb-skeleton-text2", style: { width: "25%" } })
820
+ ] })
821
+ ] })
822
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
823
+ results.length === 0 && !loading && !isDebouncing && /* @__PURE__ */ jsxs("div", { className: "hsk-sb-empty", children: [
824
+ "No results for \u201C",
825
+ query,
826
+ "\u201D"
827
+ ] }),
828
+ results.map((r, i) => {
829
+ var _a;
830
+ return renderResult ? /* @__PURE__ */ jsx2(
831
+ "div",
832
+ {
833
+ onClick: () => handleSelect(r),
834
+ className: "hsk-sb-fade",
835
+ style: { animationDelay: `${i * 18}ms` },
836
+ children: renderResult(r)
837
+ },
838
+ r.id
839
+ ) : /* @__PURE__ */ jsxs(
840
+ "div",
841
+ {
842
+ className: cn("hsk-sb-row hsk-sb-fade", classNames.row),
843
+ style: { animationDelay: `${i * 18}ms` },
844
+ onClick: () => handleSelect(r),
845
+ children: [
846
+ /* @__PURE__ */ jsx2("span", { className: "hsk-sb-row-icon", children: /* @__PURE__ */ jsx2(SearchIcon, {}) }),
847
+ /* @__PURE__ */ jsxs("div", { className: "hsk-sb-row-body", children: [
848
+ /* @__PURE__ */ jsx2("div", { className: "hsk-sb-row-title", children: r.product.name }),
849
+ (r.product.category || r.product.brand) && /* @__PURE__ */ jsx2("div", { className: "hsk-sb-row-sub", children: (_a = r.product.category) != null ? _a : r.product.brand })
850
+ ] })
851
+ ]
852
+ },
853
+ r.id
854
+ );
855
+ })
856
+ ] })
673
857
  ] })
674
858
  ] });
675
859
  }
676
860
 
677
861
  // src/components/Sparkle.tsx
678
- import { useState as useState5, useEffect as useEffect4, useRef as useRef7 } from "react";
862
+ import { useState as useState6, useEffect as useEffect5, useRef as useRef7 } from "react";
679
863
  import { createPortal } from "react-dom";
680
- import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
681
- var SparkleIcon = ({ className }) => /* @__PURE__ */ jsx3("svg", { className, width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx3("path", { d: "m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z" }) });
864
+
865
+ // src/utils/markdown.tsx
866
+ import { Fragment as Fragment2, jsx as jsx3 } from "react/jsx-runtime";
867
+ var parseInline = (text, keyPrefix) => {
868
+ const tokenRegex = /(\[[^\]]+\]\([^)]+\)|\*\*[^*]+\*\*|`[^`]+`)/g;
869
+ const parts = text.split(tokenRegex);
870
+ return parts.map((part, index) => {
871
+ if (!part) return null;
872
+ const key = `${keyPrefix}-inline-${index}`;
873
+ if (part.startsWith("`") && part.endsWith("`")) {
874
+ return /* @__PURE__ */ jsx3("code", { className: "hsk-markdown-code", children: part.slice(1, -1) }, key);
875
+ }
876
+ if (part.startsWith("**") && part.endsWith("**")) {
877
+ return /* @__PURE__ */ jsx3("strong", { children: parseInline(part.slice(2, -2), key) }, key);
878
+ }
879
+ const linkMatch = part.match(/^\[([^\]]+)\]\(([^)]+)\)$/);
880
+ if (linkMatch) {
881
+ const url = linkMatch[2];
882
+ const isSafeUrl = /^(https?|mailto|tel):/i.test(url) || url.startsWith("/");
883
+ if (isSafeUrl) {
884
+ return /* @__PURE__ */ jsx3("a", { href: url, target: "_blank", rel: "noopener noreferrer", className: "hsk-markdown-link", children: parseInline(linkMatch[1], key) }, key);
885
+ }
886
+ return /* @__PURE__ */ jsx3("span", { children: parseInline(linkMatch[1], key) }, key);
887
+ }
888
+ return part;
889
+ });
890
+ };
891
+ function renderMarkdown(content) {
892
+ const lines = content.split("\n");
893
+ const elements = [];
894
+ let i = 0;
895
+ while (i < lines.length) {
896
+ const line = lines[i];
897
+ const key = `md-line-${i}`;
898
+ if (!line.trim()) {
899
+ i++;
900
+ continue;
901
+ }
902
+ const headerMatch = line.match(/^(#{1,3})\s+(.*)/);
903
+ if (headerMatch) {
904
+ const level = headerMatch[1].length;
905
+ const Tag = `h${level + 3}`;
906
+ elements.push(/* @__PURE__ */ jsx3(Tag, { className: `hsk-markdown-h${level}`, children: parseInline(headerMatch[2], key) }, key));
907
+ i++;
908
+ continue;
909
+ }
910
+ if (line.match(/^[-*]\s+/)) {
911
+ const listItems = [];
912
+ while (i < lines.length && lines[i].match(/^[-*]\s+/)) {
913
+ const itemText = lines[i].replace(/^[-*]\s+/, "");
914
+ listItems.push(/* @__PURE__ */ jsx3("li", { children: parseInline(itemText, `li-${i}`) }, `li-${i}`));
915
+ i++;
916
+ }
917
+ elements.push(/* @__PURE__ */ jsx3("ul", { className: "hsk-markdown-list", children: listItems }, `ul-${key}`));
918
+ continue;
919
+ }
920
+ if (line.trim().startsWith("|")) {
921
+ const tableRows = [];
922
+ let isHeader = true;
923
+ while (i < lines.length && lines[i].trim().startsWith("|")) {
924
+ const rowLine = lines[i].trim();
925
+ if (rowLine.match(/^\|[-:| ]+\|$/)) {
926
+ i++;
927
+ isHeader = false;
928
+ continue;
929
+ }
930
+ const cells = rowLine.split("|").slice(1, -1).map((c) => c.trim());
931
+ const Tag = isHeader ? "th" : "td";
932
+ tableRows.push(
933
+ /* @__PURE__ */ jsx3("tr", { children: cells.map((cell, cIdx) => /* @__PURE__ */ jsx3(Tag, { children: parseInline(cell, `td-${i}-${cIdx}`) }, `td-${i}-${cIdx}`)) }, `tr-${i}`)
934
+ );
935
+ i++;
936
+ }
937
+ elements.push(
938
+ /* @__PURE__ */ jsx3("div", { className: "hsk-table-wrapper", children: /* @__PURE__ */ jsx3("table", { className: "hsk-markdown-table", children: /* @__PURE__ */ jsx3("tbody", { children: tableRows }) }) }, `table-wrapper-${key}`)
939
+ );
940
+ continue;
941
+ }
942
+ elements.push(
943
+ /* @__PURE__ */ jsx3("p", { className: "hsk-markdown-p", children: parseInline(line, key) }, key)
944
+ );
945
+ i++;
946
+ }
947
+ return /* @__PURE__ */ jsx3(Fragment2, { children: elements });
948
+ }
949
+
950
+ // src/components/Sparkle.tsx
951
+ import { Fragment as Fragment3, jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
952
+ var SparkleIcon = ({ className }) => /* @__PURE__ */ jsx4("svg", { className, width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx4("path", { d: "m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z" }) });
682
953
  var CloseIcon = () => /* @__PURE__ */ jsxs2("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: [
683
- /* @__PURE__ */ jsx3("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
684
- /* @__PURE__ */ jsx3("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
954
+ /* @__PURE__ */ jsx4("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
955
+ /* @__PURE__ */ jsx4("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
956
+ ] });
957
+ var ArrowUpIcon = () => /* @__PURE__ */ jsxs2("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
958
+ /* @__PURE__ */ jsx4("path", { d: "m5 12 7-7 7 7" }),
959
+ /* @__PURE__ */ jsx4("path", { d: "M12 19V5" })
685
960
  ] });
961
+ var getFriendlyError = (err) => {
962
+ let str = "";
963
+ if (typeof err === "string") str = err;
964
+ else if (err && typeof err === "object" && err.message) str = err.message;
965
+ else try {
966
+ str = JSON.stringify(err);
967
+ } catch (e) {
968
+ str = String(err);
969
+ }
970
+ if (str.toLowerCase().includes("token limit")) {
971
+ return "You've reached your usage limit. Please update your billing limits in your dashboard to continue.";
972
+ }
973
+ try {
974
+ const parsed = JSON.parse(str);
975
+ return parsed.error || parsed.message || str;
976
+ } catch (e) {
977
+ return str;
978
+ }
979
+ };
686
980
  function SparkleModal({
687
981
  productName,
688
982
  limit,
@@ -692,26 +986,42 @@ function SparkleModal({
692
986
  onNavigate,
693
987
  onResult,
694
988
  theme,
695
- classNames = {}
989
+ classNames = {},
990
+ product: initialProduct
696
991
  }) {
697
- const { results, loading, search } = useSearch();
698
- const initiated = useRef7(false);
699
- useEffect4(() => {
700
- if (!initiated.current) {
701
- initiated.current = true;
702
- search(productName, limit);
992
+ var _a, _b, _c;
993
+ const client = useHuskelContext();
994
+ const [fetchedProduct, setFetchedProduct] = useState6(null);
995
+ const displayProduct = initialProduct || fetchedProduct;
996
+ const { results, loading: searchLoading, search } = useSearch();
997
+ const { messages, sources, loading: chatLoading, error: chatError, send } = useChat();
998
+ const [chatInput, setChatInput] = useState6("");
999
+ const chatBottomRef = useRef7(null);
1000
+ const chatTextareaRef = useRef7(null);
1001
+ useEffect5(() => {
1002
+ if (!initialProduct && !fetchedProduct) {
1003
+ client.api.searchVector(productName, 1).then((res) => {
1004
+ if (res.results && res.results.length > 0) {
1005
+ setFetchedProduct(res.results[0].product);
1006
+ }
1007
+ }).catch((err) => console.error("[Huskel] Failed to fetch product details", err));
703
1008
  }
704
- }, []);
705
- useEffect4(() => {
1009
+ search(productName, limit);
1010
+ }, [productName, initialProduct, fetchedProduct, client, limit, search]);
1011
+ useEffect5(() => {
706
1012
  if (results.length > 0) onResult == null ? void 0 : onResult(results);
707
- }, [results]);
708
- useEffect4(() => {
1013
+ }, [results, onResult]);
1014
+ useEffect5(() => {
709
1015
  const h = (e) => {
710
1016
  if (e.key === "Escape") onClose();
711
1017
  };
712
1018
  document.addEventListener("keydown", h);
713
1019
  return () => document.removeEventListener("keydown", h);
714
- }, []);
1020
+ }, [onClose]);
1021
+ useEffect5(() => {
1022
+ var _a2;
1023
+ (_a2 = chatBottomRef.current) == null ? void 0 : _a2.scrollIntoView({ behavior: "smooth" });
1024
+ }, [messages, chatLoading]);
715
1025
  const blurVal = typeof backdropBlur === "number" ? `${backdropBlur}px` : backdropBlur != null ? backdropBlur : "16px";
716
1026
  const bg = backdropColor != null ? backdropColor : void 0;
717
1027
  const handleNav = (r) => {
@@ -721,83 +1031,209 @@ function SparkleModal({
721
1031
  if (r.product.url) window.location.href = r.product.url;
722
1032
  }
723
1033
  };
724
- const customStyles = __spreadValues(__spreadValues(__spreadValues(__spreadValues({}, (theme == null ? void 0 : theme.primaryColor) && { "--hsk-primary": theme.primaryColor }), (theme == null ? void 0 : theme.backgroundColor) && { "--hsk-bg": theme.backgroundColor }), (theme == null ? void 0 : theme.textColor) && { "--hsk-text": theme.textColor }), (theme == null ? void 0 : theme.fontFamily) && { "--hsk-font": theme.fontFamily });
725
- return /* @__PURE__ */ jsx3(
1034
+ const handleSend = async (text) => {
1035
+ const q = (text != null ? text : chatInput).trim();
1036
+ if (!q || chatLoading) return;
1037
+ setChatInput("");
1038
+ if (chatTextareaRef.current) {
1039
+ chatTextareaRef.current.style.height = "auto";
1040
+ }
1041
+ if (messages.length === 0 && displayProduct) {
1042
+ const contextQuery = `[Context: Shopper is viewing "${displayProduct.name}". Price: ${displayProduct.price}. Description: ${displayProduct.description || ""}]
1043
+
1044
+ Question: ${q}`;
1045
+ await send(contextQuery, q);
1046
+ } else {
1047
+ await send(q);
1048
+ }
1049
+ };
1050
+ const handleKeyDown = (e) => {
1051
+ if (e.key === "Enter" && !e.shiftKey) {
1052
+ e.preventDefault();
1053
+ handleSend();
1054
+ }
1055
+ };
1056
+ const handleInput = (e) => {
1057
+ setChatInput(e.target.value);
1058
+ const t = e.target;
1059
+ t.style.height = "auto";
1060
+ t.style.height = `${Math.min(t.scrollHeight, 140)}px`;
1061
+ };
1062
+ const customStyles = __spreadValues(__spreadValues(__spreadValues(__spreadValues(__spreadValues({}, (theme == null ? void 0 : theme.primaryColor) && { "--hsk-primary": theme.primaryColor }), (theme == null ? void 0 : theme.backgroundColor) && { "--hsk-bg": theme.backgroundColor }), (theme == null ? void 0 : theme.textColor) && { "--hsk-text": theme.textColor }), (theme == null ? void 0 : theme.fontFamily) && { "--hsk-font": theme.fontFamily }), (theme == null ? void 0 : theme.borderRadius) && { "--hsk-border-radius": theme.borderRadius });
1063
+ const displayMessages = messages.length === 0 && displayProduct ? [
1064
+ {
1065
+ role: "assistant",
1066
+ content: `Hi! I can help you with **${displayProduct.name}**. Ask me about its specifications, features, compare it with other options, or find alternatives!`
1067
+ }
1068
+ ] : messages;
1069
+ return /* @__PURE__ */ jsx4(
726
1070
  "div",
727
1071
  {
728
- className: `hsk-sp-backdrop ${classNames.backdrop || ""}`,
1072
+ className: cn("hsk-sp-backdrop", classNames.backdrop),
729
1073
  onClick: onClose,
730
1074
  style: __spreadValues({
731
1075
  backdropFilter: `blur(${blurVal})`,
732
1076
  WebkitBackdropFilter: `blur(${blurVal})`,
733
1077
  background: bg != null ? bg : void 0
734
1078
  }, customStyles),
735
- children: /* @__PURE__ */ jsxs2("div", { className: `hsk-sp-card ${classNames.card || ""}`, onClick: (e) => e.stopPropagation(), children: [
1079
+ children: /* @__PURE__ */ jsxs2("div", { className: cn("hsk-sp-card hsk-sp-fullscreen", classNames.card), onClick: (e) => e.stopPropagation(), children: [
736
1080
  /* @__PURE__ */ jsxs2("div", { className: "hsk-sp-header", children: [
737
- /* @__PURE__ */ jsx3("span", { className: "hsk-sp-header-icon", style: { display: "flex", alignItems: "center" }, children: /* @__PURE__ */ jsx3(SparkleIcon, {}) }),
1081
+ /* @__PURE__ */ jsx4("span", { className: "hsk-sp-header-icon", style: { display: "flex", alignItems: "center" }, children: /* @__PURE__ */ jsx4(SparkleIcon, {}) }),
738
1082
  /* @__PURE__ */ jsxs2("div", { className: "hsk-sp-header-body", children: [
739
- /* @__PURE__ */ jsxs2("div", { className: "hsk-sp-header-title", children: [
740
- "Similar to \u201C",
741
- productName,
742
- "\u201D"
743
- ] }),
744
- /* @__PURE__ */ jsx3("div", { className: "hsk-sp-header-sub", children: "AI vector similarity \xB7 instant results" })
1083
+ /* @__PURE__ */ jsx4("div", { className: "hsk-sp-header-title", children: (displayProduct == null ? void 0 : displayProduct.name) || productName }),
1084
+ /* @__PURE__ */ jsx4("div", { className: "hsk-sp-header-sub", children: "Ask questions, compare specs, or check similar products" })
745
1085
  ] }),
746
- /* @__PURE__ */ jsx3("button", { className: "hsk-sp-close", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ jsx3(CloseIcon, {}) })
1086
+ /* @__PURE__ */ jsx4("button", { className: "hsk-sp-close", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ jsx4(CloseIcon, {}) })
747
1087
  ] }),
748
- loading && /* @__PURE__ */ jsx3("div", { className: "hsk-sp-bar" }),
749
- /* @__PURE__ */ jsxs2("div", { className: "hsk-sp-results", children: [
750
- !loading && results.length === 0 && /* @__PURE__ */ jsx3("div", { className: "hsk-sp-empty", children: "No similar products found." }),
751
- results.map((r, i) => {
752
- var _a, _b, _c;
753
- const price = parseFloat(((_a = r.product.price) == null ? void 0 : _a.replace(/[^0-9.]/g, "")) || "0");
754
- const currency = (_b = r.product.currency) != null ? _b : "KES";
755
- return /* @__PURE__ */ jsxs2(
756
- "div",
757
- {
758
- className: `hsk-sp-item ${classNames.item || ""}`,
759
- style: { animationDelay: `${i * 55}ms` },
760
- children: [
761
- /* @__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}" }) }),
762
- /* @__PURE__ */ jsxs2("div", { className: "hsk-sp-item-body", children: [
763
- r.product.category && /* @__PURE__ */ jsx3("div", { className: "hsk-sp-item-cat", children: r.product.category }),
764
- /* @__PURE__ */ jsx3("div", { className: "hsk-sp-item-name", children: r.product.name }),
765
- /* @__PURE__ */ jsxs2("div", { className: "hsk-sp-item-price-row", children: [
766
- /* @__PURE__ */ jsx3("span", { className: "hsk-sp-item-currency", children: currency }),
767
- /* @__PURE__ */ jsx3("span", { className: "hsk-sp-item-price", children: price.toLocaleString() })
1088
+ searchLoading && /* @__PURE__ */ jsx4("div", { className: "hsk-sp-bar" }),
1089
+ /* @__PURE__ */ jsxs2("div", { className: "hsk-sp-body", children: [
1090
+ /* @__PURE__ */ jsxs2("div", { className: "hsk-sp-details-pane", children: [
1091
+ displayProduct && /* @__PURE__ */ jsxs2("div", { className: "hsk-sp-product-profile-container", children: [
1092
+ /* @__PURE__ */ jsxs2("div", { className: "hsk-sp-product-profile", children: [
1093
+ /* @__PURE__ */ jsx4("div", { className: "hsk-sp-details-imgwrap", children: ((_a = displayProduct.images) == null ? void 0 : _a[0]) ? /* @__PURE__ */ jsx4("img", { src: displayProduct.images[0], alt: displayProduct.name }) : /* @__PURE__ */ jsx4("span", { className: "hsk-sp-img-placeholder", children: "\u{1F6CD}" }) }),
1094
+ /* @__PURE__ */ jsxs2("div", { className: "hsk-sp-details-meta", children: [
1095
+ displayProduct.brand && /* @__PURE__ */ jsx4("span", { className: "hsk-sp-item-brand", children: displayProduct.brand }),
1096
+ displayProduct.category && /* @__PURE__ */ jsx4("span", { className: "hsk-sp-item-cat", children: displayProduct.category }),
1097
+ /* @__PURE__ */ jsx4("h2", { className: "hsk-sp-details-name", children: displayProduct.name }),
1098
+ /* @__PURE__ */ jsxs2("div", { className: "hsk-sp-item-price-row", children: [
1099
+ /* @__PURE__ */ jsx4("span", { className: "hsk-sp-item-currency", children: (_b = displayProduct.currency) != null ? _b : "KES" }),
1100
+ /* @__PURE__ */ jsx4("span", { className: "hsk-sp-item-price", children: parseFloat(((_c = displayProduct.price) == null ? void 0 : _c.replace(/[^0-9.]/g, "")) || "0").toLocaleString() }),
1101
+ displayProduct.originalPrice && /* @__PURE__ */ jsx4("span", { className: "hsk-sp-item-original-price", children: parseFloat(displayProduct.originalPrice.replace(/[^0-9.]/g, "") || "0").toLocaleString() }),
1102
+ displayProduct.discount && /* @__PURE__ */ jsxs2("span", { className: "hsk-sp-item-discount", children: [
1103
+ "(",
1104
+ displayProduct.discount,
1105
+ ")"
1106
+ ] })
1107
+ ] }),
1108
+ /* @__PURE__ */ jsxs2("div", { className: "hsk-sp-item-meta-badges", children: [
1109
+ displayProduct.rating && /* @__PURE__ */ jsxs2("span", { className: "hsk-sp-meta-badge hsk-sp-meta-badge-rating", children: [
1110
+ "\u2605 ",
1111
+ parseFloat(displayProduct.rating.toString()).toFixed(1),
1112
+ " ",
1113
+ displayProduct.reviewCount ? `(${displayProduct.reviewCount})` : ""
768
1114
  ] }),
769
- /* @__PURE__ */ jsxs2("div", { className: "hsk-sp-actions", children: [
770
- /* @__PURE__ */ jsx3(
771
- "button",
772
- {
773
- className: "hsk-sp-action hsk-sp-action-primary",
774
- onClick: () => handleNav(r),
775
- children: "View Product"
776
- }
777
- ),
778
- /* @__PURE__ */ jsx3(
779
- "button",
780
- {
781
- className: "hsk-sp-action hsk-sp-action-secondary",
782
- onClick: () => onClose(),
783
- children: "Add to Cart"
784
- }
785
- )
1115
+ displayProduct.availability && /* @__PURE__ */ jsx4("span", { className: `hsk-sp-meta-badge hsk-sp-meta-badge-avail ${displayProduct.availability.toLowerCase().includes("in") ? "in-stock" : "out-stock"}`, children: displayProduct.availability }),
1116
+ displayProduct.stock && !displayProduct.availability && /* @__PURE__ */ jsxs2("span", { className: "hsk-sp-meta-badge hsk-sp-meta-badge-stock", children: [
1117
+ "Stock: ",
1118
+ displayProduct.stock
786
1119
  ] })
787
1120
  ] })
788
- ]
789
- },
790
- r.id
791
- );
792
- })
793
- ] }),
794
- /* @__PURE__ */ jsxs2("div", { className: "hsk-sp-footer", children: [
795
- /* @__PURE__ */ jsxs2("span", { className: "hsk-sp-badge", style: { display: "inline-flex", alignItems: "center", gap: "4px" }, children: [
796
- /* @__PURE__ */ jsx3(SparkleIcon, {}),
797
- " Huskel AI"
1121
+ ] })
1122
+ ] }),
1123
+ displayProduct.specs && Object.keys(displayProduct.specs).length > 0 && /* @__PURE__ */ jsx4("div", { className: "hsk-sp-specs-horizontal", children: Object.entries(displayProduct.specs).map(([key, val]) => /* @__PURE__ */ jsxs2("div", { className: "hsk-sp-spec-item-horizontal", children: [
1124
+ /* @__PURE__ */ jsxs2("span", { className: "hsk-sp-spec-label-horizontal", children: [
1125
+ key,
1126
+ ":"
1127
+ ] }),
1128
+ /* @__PURE__ */ jsx4("span", { className: "hsk-sp-spec-value-horizontal", title: val, children: val })
1129
+ ] }, key)) }),
1130
+ displayProduct.description && /* @__PURE__ */ jsxs2("div", { className: "hsk-sp-details-desc", children: [
1131
+ /* @__PURE__ */ jsx4("h4", { children: "Description" }),
1132
+ /* @__PURE__ */ jsx4("p", { children: displayProduct.description })
1133
+ ] })
1134
+ ] }),
1135
+ /* @__PURE__ */ jsxs2("div", { className: "hsk-sp-similar-section", children: [
1136
+ /* @__PURE__ */ jsx4("h3", { children: "Similar Products" }),
1137
+ /* @__PURE__ */ jsx4("div", { className: "hsk-sp-results", children: (() => {
1138
+ const similarProducts = results.filter(
1139
+ (r) => {
1140
+ var _a2;
1141
+ const isSameName = r.product.name.toLowerCase() === ((_a2 = displayProduct == null ? void 0 : displayProduct.name) == null ? void 0 : _a2.toLowerCase());
1142
+ const isSameSlug = r.product.slug && (displayProduct == null ? void 0 : displayProduct.slug) && r.product.slug.toLowerCase() === displayProduct.slug.toLowerCase();
1143
+ return !isSameName && !isSameSlug;
1144
+ }
1145
+ );
1146
+ if (!searchLoading && similarProducts.length === 0) {
1147
+ return /* @__PURE__ */ jsx4("div", { className: "hsk-sp-empty", children: "No similar products found." });
1148
+ }
1149
+ return similarProducts.map((r, i) => {
1150
+ var _a2, _b2, _c2;
1151
+ const price = parseFloat(((_a2 = r.product.price) == null ? void 0 : _a2.replace(/[^0-9.]/g, "")) || "0");
1152
+ const currency = (_b2 = r.product.currency) != null ? _b2 : "KES";
1153
+ return /* @__PURE__ */ jsxs2(
1154
+ "div",
1155
+ {
1156
+ className: cn("hsk-sp-item", classNames.item),
1157
+ style: { animationDelay: `${i * 55}ms` },
1158
+ children: [
1159
+ /* @__PURE__ */ jsx4("div", { className: "hsk-sp-img-wrap", children: ((_c2 = r.product.images) == null ? void 0 : _c2[0]) ? /* @__PURE__ */ jsx4("img", { src: r.product.images[0], alt: r.product.name }) : /* @__PURE__ */ jsx4("span", { className: "hsk-sp-img-placeholder", children: "\u{1F6CD}" }) }),
1160
+ /* @__PURE__ */ jsxs2("div", { className: "hsk-sp-item-body", children: [
1161
+ /* @__PURE__ */ jsxs2("div", { children: [
1162
+ r.product.category && /* @__PURE__ */ jsx4("div", { className: "hsk-sp-item-cat", children: r.product.category }),
1163
+ /* @__PURE__ */ jsx4("div", { className: "hsk-sp-item-name", title: r.product.name, children: r.product.name })
1164
+ ] }),
1165
+ /* @__PURE__ */ jsxs2("div", { className: "hsk-sp-item-price-row", children: [
1166
+ /* @__PURE__ */ jsx4("span", { className: "hsk-sp-item-currency", children: currency }),
1167
+ /* @__PURE__ */ jsx4("span", { className: "hsk-sp-item-price", children: price.toLocaleString() })
1168
+ ] }),
1169
+ /* @__PURE__ */ jsx4("div", { className: "hsk-sp-actions", children: /* @__PURE__ */ jsx4(
1170
+ "button",
1171
+ {
1172
+ className: "hsk-sp-action hsk-sp-action-primary",
1173
+ onClick: () => handleNav(r),
1174
+ children: "View"
1175
+ }
1176
+ ) })
1177
+ ] })
1178
+ ]
1179
+ },
1180
+ r.id
1181
+ );
1182
+ });
1183
+ })() })
1184
+ ] })
798
1185
  ] }),
799
- /* @__PURE__ */ jsx3("span", { className: "hsk-sp-esc", children: "Esc to close" })
800
- ] })
1186
+ /* @__PURE__ */ jsxs2("div", { className: "hsk-sp-chat-pane", children: [
1187
+ /* @__PURE__ */ jsxs2("div", { className: "hsk-cb-msgs", children: [
1188
+ displayMessages.map((msg, idx) => {
1189
+ const isUser = msg.role === "user";
1190
+ return /* @__PURE__ */ jsx4("div", { className: "hsk-cb-msg-group", children: isUser ? /* @__PURE__ */ jsx4("div", { className: "hsk-cb-user-msg", children: /* @__PURE__ */ jsx4("div", { className: "hsk-cb-user-bubble", children: msg.content }) }) : /* @__PURE__ */ jsxs2("div", { className: "hsk-cb-ai-msg", children: [
1191
+ /* @__PURE__ */ jsx4("div", { className: "hsk-cb-ai-icon", style: { display: "flex", alignItems: "center" }, children: /* @__PURE__ */ jsx4(SparkleIcon, {}) }),
1192
+ /* @__PURE__ */ jsx4("div", { className: "hsk-cb-ai-body", children: /* @__PURE__ */ jsx4("div", { className: "hsk-cb-ai-text", children: renderMarkdown(msg.content) }) })
1193
+ ] }) }, idx);
1194
+ }),
1195
+ chatLoading && /* @__PURE__ */ jsxs2("div", { className: "hsk-cb-typing-row", children: [
1196
+ /* @__PURE__ */ jsx4("div", { className: "hsk-cb-ai-icon", style: { display: "flex", alignItems: "center" }, children: /* @__PURE__ */ jsx4(SparkleIcon, {}) }),
1197
+ /* @__PURE__ */ jsxs2("div", { className: "hsk-cb-typing", children: [
1198
+ /* @__PURE__ */ jsx4("div", { className: "hsk-cb-dot" }),
1199
+ /* @__PURE__ */ jsx4("div", { className: "hsk-cb-dot" }),
1200
+ /* @__PURE__ */ jsx4("div", { className: "hsk-cb-dot" })
1201
+ ] })
1202
+ ] }),
1203
+ chatError && /* @__PURE__ */ jsx4("div", { className: "hsk-cb-error", children: getFriendlyError(chatError) }),
1204
+ /* @__PURE__ */ jsx4("div", { ref: chatBottomRef, style: { height: 1 } })
1205
+ ] }),
1206
+ /* @__PURE__ */ jsxs2("div", { className: "hsk-cb-input-wrap", children: [
1207
+ /* @__PURE__ */ jsxs2("div", { className: "hsk-cb-input-box", children: [
1208
+ /* @__PURE__ */ jsx4(
1209
+ "textarea",
1210
+ {
1211
+ ref: chatTextareaRef,
1212
+ className: "hsk-cb-textarea",
1213
+ value: chatInput,
1214
+ onChange: handleInput,
1215
+ onKeyDown: handleKeyDown,
1216
+ placeholder: "Ask about this product, specs, or comparison...",
1217
+ rows: 1,
1218
+ disabled: chatLoading
1219
+ }
1220
+ ),
1221
+ /* @__PURE__ */ jsx4(
1222
+ "button",
1223
+ {
1224
+ className: "hsk-cb-send",
1225
+ onClick: () => handleSend(),
1226
+ disabled: !chatInput.trim() || chatLoading,
1227
+ "aria-label": "Send message",
1228
+ children: /* @__PURE__ */ jsx4(ArrowUpIcon, {})
1229
+ }
1230
+ )
1231
+ ] }),
1232
+ /* @__PURE__ */ jsx4("div", { className: "hsk-cb-hint", children: "Huskel AI \xB7 instant product knowledge" })
1233
+ ] })
1234
+ ] })
1235
+ ] }),
1236
+ /* @__PURE__ */ jsx4("div", { className: "hsk-sp-footer", children: /* @__PURE__ */ jsx4("span", { className: "hsk-sp-esc", children: "Esc to close" }) })
801
1237
  ] })
802
1238
  }
803
1239
  );
@@ -811,39 +1247,41 @@ function Sparkle({
811
1247
  className,
812
1248
  onNavigate,
813
1249
  theme,
814
- classNames = {}
1250
+ classNames = {},
1251
+ product
815
1252
  }) {
816
- const [open, setOpen] = useState5(false);
817
- const [mounted, setMounted] = useState5(false);
818
- useEffect4(() => {
1253
+ const [open, setOpen] = useState6(false);
1254
+ const [mounted, setMounted] = useState6(false);
1255
+ useEffect5(() => {
819
1256
  setMounted(true);
820
1257
  }, []);
821
- const customStyles = __spreadValues(__spreadValues(__spreadValues(__spreadValues({}, (theme == null ? void 0 : theme.primaryColor) && { "--hsk-primary": theme.primaryColor }), (theme == null ? void 0 : theme.backgroundColor) && { "--hsk-bg": theme.backgroundColor }), (theme == null ? void 0 : theme.textColor) && { "--hsk-text": theme.textColor }), (theme == null ? void 0 : theme.fontFamily) && { "--hsk-font": theme.fontFamily });
822
- return /* @__PURE__ */ jsxs2(Fragment, { children: [
823
- /* @__PURE__ */ jsx3(
1258
+ const customStyles = __spreadValues(__spreadValues(__spreadValues(__spreadValues(__spreadValues({}, (theme == null ? void 0 : theme.primaryColor) && { "--hsk-primary": theme.primaryColor }), (theme == null ? void 0 : theme.backgroundColor) && { "--hsk-bg": theme.backgroundColor }), (theme == null ? void 0 : theme.textColor) && { "--hsk-text": theme.textColor }), (theme == null ? void 0 : theme.fontFamily) && { "--hsk-font": theme.fontFamily }), (theme == null ? void 0 : theme.borderRadius) && { "--hsk-border-radius": theme.borderRadius });
1259
+ return /* @__PURE__ */ jsxs2(Fragment3, { children: [
1260
+ /* @__PURE__ */ jsx4(
824
1261
  "button",
825
1262
  {
826
- className: `hsk-sp-btn ${classNames.button || ""} ${className || ""}`,
1263
+ className: cn("hsk-sp-btn", classNames.button, className),
827
1264
  onClick: () => setOpen(true),
828
1265
  style: customStyles,
829
1266
  title: "Find similar products",
830
1267
  "aria-label": "Find similar products",
831
- children: /* @__PURE__ */ jsx3(SparkleIcon, {})
1268
+ children: /* @__PURE__ */ jsx4(SparkleIcon, {})
832
1269
  }
833
1270
  ),
834
1271
  open && mounted && createPortal(
835
- /* @__PURE__ */ jsx3(
1272
+ /* @__PURE__ */ jsx4(
836
1273
  SparkleModal,
837
1274
  {
838
1275
  productName,
839
1276
  limit,
1277
+ onResult,
840
1278
  backdropColor,
841
1279
  backdropBlur,
842
1280
  onClose: () => setOpen(false),
843
- onResult,
844
1281
  onNavigate,
845
1282
  theme,
846
- classNames
1283
+ classNames,
1284
+ product
847
1285
  }
848
1286
  ),
849
1287
  document.body
@@ -852,97 +1290,10 @@ function Sparkle({
852
1290
  }
853
1291
 
854
1292
  // src/components/ChatWidget.tsx
855
- import { useState as useState6, useRef as useRef8, useEffect as useEffect5 } from "react";
856
-
857
- // src/utils/markdown.tsx
858
- import { Fragment as Fragment2, jsx as jsx4 } from "react/jsx-runtime";
859
- var parseInline = (text, keyPrefix) => {
860
- const tokenRegex = /(\[[^\]]+\]\([^)]+\)|\*\*[^*]+\*\*|`[^`]+`)/g;
861
- const parts = text.split(tokenRegex);
862
- return parts.map((part, index) => {
863
- if (!part) return null;
864
- const key = `${keyPrefix}-inline-${index}`;
865
- if (part.startsWith("`") && part.endsWith("`")) {
866
- return /* @__PURE__ */ jsx4("code", { className: "hsk-markdown-code", children: part.slice(1, -1) }, key);
867
- }
868
- if (part.startsWith("**") && part.endsWith("**")) {
869
- return /* @__PURE__ */ jsx4("strong", { children: parseInline(part.slice(2, -2), key) }, key);
870
- }
871
- const linkMatch = part.match(/^\[([^\]]+)\]\(([^)]+)\)$/);
872
- if (linkMatch) {
873
- const url = linkMatch[2];
874
- const isSafeUrl = /^(https?|mailto|tel):/i.test(url) || url.startsWith("/");
875
- if (isSafeUrl) {
876
- return /* @__PURE__ */ jsx4("a", { href: url, target: "_blank", rel: "noopener noreferrer", className: "hsk-markdown-link", children: parseInline(linkMatch[1], key) }, key);
877
- }
878
- return /* @__PURE__ */ jsx4("span", { children: parseInline(linkMatch[1], key) }, key);
879
- }
880
- return part;
881
- });
882
- };
883
- function renderMarkdown(content) {
884
- const lines = content.split("\n");
885
- const elements = [];
886
- let i = 0;
887
- while (i < lines.length) {
888
- const line = lines[i];
889
- const key = `md-line-${i}`;
890
- if (!line.trim()) {
891
- i++;
892
- continue;
893
- }
894
- const headerMatch = line.match(/^(#{1,3})\s+(.*)/);
895
- if (headerMatch) {
896
- const level = headerMatch[1].length;
897
- const Tag = `h${level + 3}`;
898
- elements.push(/* @__PURE__ */ jsx4(Tag, { className: `hsk-markdown-h${level}`, children: parseInline(headerMatch[2], key) }, key));
899
- i++;
900
- continue;
901
- }
902
- if (line.match(/^[-*]\s+/)) {
903
- const listItems = [];
904
- while (i < lines.length && lines[i].match(/^[-*]\s+/)) {
905
- const itemText = lines[i].replace(/^[-*]\s+/, "");
906
- listItems.push(/* @__PURE__ */ jsx4("li", { children: parseInline(itemText, `li-${i}`) }, `li-${i}`));
907
- i++;
908
- }
909
- elements.push(/* @__PURE__ */ jsx4("ul", { className: "hsk-markdown-list", children: listItems }, `ul-${key}`));
910
- continue;
911
- }
912
- if (line.trim().startsWith("|")) {
913
- const tableRows = [];
914
- let isHeader = true;
915
- while (i < lines.length && lines[i].trim().startsWith("|")) {
916
- const rowLine = lines[i].trim();
917
- if (rowLine.match(/^\|[-:| ]+\|$/)) {
918
- i++;
919
- isHeader = false;
920
- continue;
921
- }
922
- const cells = rowLine.split("|").slice(1, -1).map((c) => c.trim());
923
- const Tag = isHeader ? "th" : "td";
924
- tableRows.push(
925
- /* @__PURE__ */ jsx4("tr", { children: cells.map((cell, cIdx) => /* @__PURE__ */ jsx4(Tag, { children: parseInline(cell, `td-${i}-${cIdx}`) }, `td-${i}-${cIdx}`)) }, `tr-${i}`)
926
- );
927
- i++;
928
- }
929
- elements.push(
930
- /* @__PURE__ */ jsx4("div", { className: "hsk-table-wrapper", children: /* @__PURE__ */ jsx4("table", { className: "hsk-markdown-table", children: /* @__PURE__ */ jsx4("tbody", { children: tableRows }) }) }, `table-wrapper-${key}`)
931
- );
932
- continue;
933
- }
934
- elements.push(
935
- /* @__PURE__ */ jsx4("p", { className: "hsk-markdown-p", children: parseInline(line, key) }, key)
936
- );
937
- i++;
938
- }
939
- return /* @__PURE__ */ jsx4(Fragment2, { children: elements });
940
- }
941
-
942
- // src/components/ChatWidget.tsx
1293
+ import { useState as useState7, useRef as useRef8, useEffect as useEffect6 } from "react";
943
1294
  import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
944
1295
  var SparkleIcon2 = () => /* @__PURE__ */ jsx5("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx5("path", { d: "m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z" }) });
945
- var ArrowUpIcon = () => /* @__PURE__ */ jsxs3("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
1296
+ var ArrowUpIcon2 = () => /* @__PURE__ */ jsxs3("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
946
1297
  /* @__PURE__ */ jsx5("path", { d: "m5 12 7-7 7 7" }),
947
1298
  /* @__PURE__ */ jsx5("path", { d: "M12 19V5" })
948
1299
  ] });
@@ -972,10 +1323,10 @@ function ChatWidget({
972
1323
  onSelectSource
973
1324
  }) {
974
1325
  const { messages, sources, loading, error, send, reset } = useChat();
975
- const [input, setInput] = useState6("");
1326
+ const [input, setInput] = useState7("");
976
1327
  const bottomRef = useRef8(null);
977
1328
  const textareaRef = useRef8(null);
978
- useEffect5(() => {
1329
+ useEffect6(() => {
979
1330
  var _a;
980
1331
  (_a = bottomRef.current) == null ? void 0 : _a.scrollIntoView({ behavior: "smooth" });
981
1332
  }, [messages, loading]);
@@ -998,14 +1349,14 @@ function ChatWidget({
998
1349
  t.style.height = "auto";
999
1350
  t.style.height = Math.min(t.scrollHeight, 120) + "px";
1000
1351
  };
1001
- const customStyles = __spreadValues(__spreadValues(__spreadValues(__spreadValues({}, (theme == null ? void 0 : theme.primaryColor) && { "--hsk-primary": theme.primaryColor }), (theme == null ? void 0 : theme.backgroundColor) && { "--hsk-bg": theme.backgroundColor }), (theme == null ? void 0 : theme.textColor) && { "--hsk-text": theme.textColor }), (theme == null ? void 0 : theme.fontFamily) && { "--hsk-font": theme.fontFamily });
1352
+ const customStyles = __spreadValues(__spreadValues(__spreadValues(__spreadValues(__spreadValues({}, (theme == null ? void 0 : theme.primaryColor) && { "--hsk-primary": theme.primaryColor }), (theme == null ? void 0 : theme.backgroundColor) && { "--hsk-bg": theme.backgroundColor }), (theme == null ? void 0 : theme.textColor) && { "--hsk-text": theme.textColor }), (theme == null ? void 0 : theme.fontFamily) && { "--hsk-font": theme.fontFamily }), (theme == null ? void 0 : theme.borderRadius) && { "--hsk-border-radius": theme.borderRadius });
1002
1353
  return /* @__PURE__ */ jsxs3(
1003
1354
  "div",
1004
1355
  {
1005
- className: `hsk-chat-widget ${classNames.root || ""} ${className || ""}`,
1356
+ className: cn("hsk-chat-widget", classNames.root, className),
1006
1357
  style: customStyles,
1007
1358
  children: [
1008
- /* @__PURE__ */ jsxs3("div", { className: `hsk-chat-header ${classNames.header || ""}`, children: [
1359
+ /* @__PURE__ */ jsxs3("div", { className: cn("hsk-chat-header", classNames.header), children: [
1009
1360
  /* @__PURE__ */ jsx5("span", { className: "hsk-chat-header-icon", children: /* @__PURE__ */ jsx5(SparkleIcon2, {}) }),
1010
1361
  /* @__PURE__ */ jsx5("span", { className: "hsk-chat-title", children: title }),
1011
1362
  /* @__PURE__ */ jsx5("span", { className: "hsk-chat-badge", children: "AI" }),
@@ -1018,8 +1369,8 @@ function ChatWidget({
1018
1369
  /* @__PURE__ */ jsx5("div", { className: "hsk-chat-empty-suggestions", children: emptyStateSuggestions })
1019
1370
  ] }) : messages.map((msg, idx) => /* @__PURE__ */ jsxs3("div", { children: [
1020
1371
  /* @__PURE__ */ jsxs3("div", { className: `hsk-msg-row ${msg.role}`, children: [
1021
- /* @__PURE__ */ jsx5("div", { className: `hsk-msg-avatar ${msg.role === "assistant" ? "ai" : "user"}`, children: msg.role === "assistant" ? /* @__PURE__ */ jsx5(SparkleIcon2, {}) : "U" }),
1022
- /* @__PURE__ */ jsx5("div", { className: `hsk-msg-bubble ${msg.role} ${classNames.messageBubble || ""}`, children: renderMarkdown(msg.content) })
1372
+ /* @__PURE__ */ jsx5("div", { className: cn("hsk-msg-avatar", msg.role === "assistant" ? "ai" : "user"), children: msg.role === "assistant" ? /* @__PURE__ */ jsx5(SparkleIcon2, {}) : "U" }),
1373
+ /* @__PURE__ */ jsx5("div", { className: cn("hsk-msg-bubble", msg.role, classNames.messageBubble), children: renderMarkdown(msg.content) })
1023
1374
  ] }),
1024
1375
  msg.role === "assistant" && idx === messages.length - 1 && sources.length > 0 && /* @__PURE__ */ jsx5("div", { className: "hsk-sources-container", children: /* @__PURE__ */ jsx5("div", { className: "hsk-sources", children: sources.map((src, si) => /* @__PURE__ */ jsx5(SourceCard, { source: src, defaultCurrency, onSelect: onSelectSource }, si)) }) })
1025
1376
  ] }, idx)),
@@ -1031,7 +1382,14 @@ function ChatWidget({
1031
1382
  /* @__PURE__ */ jsx5("div", { className: "hsk-typing-dot" })
1032
1383
  ] })
1033
1384
  ] }),
1034
- error && /* @__PURE__ */ jsx5("div", { className: "hsk-chat-error", children: error }),
1385
+ error && /* @__PURE__ */ jsx5("div", { className: "hsk-chat-error", children: (() => {
1386
+ try {
1387
+ const parsed = JSON.parse(error);
1388
+ return parsed.error || parsed.message || error;
1389
+ } catch (e) {
1390
+ return error;
1391
+ }
1392
+ })() }),
1035
1393
  /* @__PURE__ */ jsx5("div", { ref: bottomRef })
1036
1394
  ] }),
1037
1395
  /* @__PURE__ */ jsxs3("div", { className: "hsk-chat-input-area", children: [
@@ -1039,7 +1397,7 @@ function ChatWidget({
1039
1397
  "textarea",
1040
1398
  {
1041
1399
  ref: textareaRef,
1042
- className: `hsk-chat-input ${classNames.input || ""}`,
1400
+ className: cn("hsk-chat-input", classNames.input),
1043
1401
  value: input,
1044
1402
  onChange: handleInput,
1045
1403
  onKeyDown: handleKey,
@@ -1055,7 +1413,7 @@ function ChatWidget({
1055
1413
  onClick: handleSend,
1056
1414
  disabled: !input.trim() || loading,
1057
1415
  "aria-label": "Send message",
1058
- children: /* @__PURE__ */ jsx5(ArrowUpIcon, {})
1416
+ children: /* @__PURE__ */ jsx5(ArrowUpIcon2, {})
1059
1417
  }
1060
1418
  )
1061
1419
  ] })
@@ -1065,11 +1423,11 @@ function ChatWidget({
1065
1423
  }
1066
1424
 
1067
1425
  // src/components/AIChatButton.tsx
1068
- import { useState as useState7, useEffect as useEffect6, useRef as useRef9, useCallback as useCallback4 } from "react";
1426
+ import { useState as useState8, useEffect as useEffect7, useRef as useRef9, useCallback as useCallback5 } from "react";
1069
1427
  import { createPortal as createPortal2 } from "react-dom";
1070
- import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
1428
+ import { Fragment as Fragment4, jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
1071
1429
  var SparkleIcon3 = ({ className }) => /* @__PURE__ */ jsx6("svg", { className, width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx6("path", { d: "m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z" }) });
1072
- var ArrowUpIcon2 = () => /* @__PURE__ */ jsxs4("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
1430
+ var ArrowUpIcon3 = () => /* @__PURE__ */ jsxs4("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
1073
1431
  /* @__PURE__ */ jsx6("path", { d: "m5 12 7-7 7 7" }),
1074
1432
  /* @__PURE__ */ jsx6("path", { d: "M12 19V5" })
1075
1433
  ] });
@@ -1086,14 +1444,14 @@ var DEFAULT_CHIPS = [
1086
1444
  ];
1087
1445
  function SourcesCarousel({ sources, defaultCurrency, onSelectSource }) {
1088
1446
  const railRef = useRef9(null);
1089
- const [showNext, setShowNext] = useState7(false);
1090
- const measure = useCallback4(() => {
1447
+ const [showNext, setShowNext] = useState8(false);
1448
+ const measure = useCallback5(() => {
1091
1449
  const el = railRef.current;
1092
1450
  if (!el) return;
1093
1451
  const atEnd = el.scrollLeft + el.clientWidth >= el.scrollWidth - 8;
1094
1452
  setShowNext(el.scrollWidth > el.clientWidth + 4 && !atEnd);
1095
1453
  }, []);
1096
- useEffect6(() => {
1454
+ useEffect7(() => {
1097
1455
  measure();
1098
1456
  const el = railRef.current;
1099
1457
  if (!el) return;
@@ -1133,7 +1491,7 @@ function SourcesCarousel({ sources, defaultCurrency, onSelectSource }) {
1133
1491
  si
1134
1492
  );
1135
1493
  }) }),
1136
- showNext && /* @__PURE__ */ jsxs4(Fragment3, { children: [
1494
+ showNext && /* @__PURE__ */ jsxs4(Fragment4, { children: [
1137
1495
  /* @__PURE__ */ jsx6(
1138
1496
  "div",
1139
1497
  {
@@ -1146,7 +1504,7 @@ function SourcesCarousel({ sources, defaultCurrency, onSelectSource }) {
1146
1504
  ] });
1147
1505
  }
1148
1506
  function ChatModal({
1149
- title = "AI Shopping Assistant",
1507
+ title = "Shopping Assistant",
1150
1508
  placeholder = "Ask me anything \u2014 gifts, budget, use case\u2026",
1151
1509
  backdropColor,
1152
1510
  backdropBlur,
@@ -1159,15 +1517,15 @@ function ChatModal({
1159
1517
  }) {
1160
1518
  var _a, _b;
1161
1519
  const { messages, sources, loading, error, send, reset } = useChat();
1162
- const [input, setInput] = useState7("");
1163
- const [selectedProduct, setSelectedProduct] = useState7(null);
1520
+ const [input, setInput] = useState8("");
1521
+ const [selectedProduct, setSelectedProduct] = useState8(null);
1164
1522
  const bottomRef = useRef9(null);
1165
1523
  const textareaRef = useRef9(null);
1166
- useEffect6(() => {
1524
+ useEffect7(() => {
1167
1525
  var _a2;
1168
1526
  (_a2 = bottomRef.current) == null ? void 0 : _a2.scrollIntoView({ behavior: "smooth" });
1169
1527
  }, [messages, loading, selectedProduct]);
1170
- useEffect6(() => {
1528
+ useEffect7(() => {
1171
1529
  const h = (e) => {
1172
1530
  if (e.key === "Escape") onClose();
1173
1531
  };
@@ -1204,24 +1562,21 @@ function ChatModal({
1204
1562
  t.style.height = `${Math.min(t.scrollHeight, 140)}px`;
1205
1563
  };
1206
1564
  const blurVal = typeof backdropBlur === "number" ? `${backdropBlur}px` : backdropBlur != null ? backdropBlur : "20px";
1207
- const customStyles = __spreadValues(__spreadValues(__spreadValues(__spreadValues({}, (theme == null ? void 0 : theme.primaryColor) && { "--hsk-primary": theme.primaryColor }), (theme == null ? void 0 : theme.backgroundColor) && { "--hsk-bg": theme.backgroundColor }), (theme == null ? void 0 : theme.textColor) && { "--hsk-text": theme.textColor }), (theme == null ? void 0 : theme.fontFamily) && { "--hsk-font": theme.fontFamily });
1565
+ const customStyles = __spreadValues(__spreadValues(__spreadValues(__spreadValues(__spreadValues({}, (theme == null ? void 0 : theme.primaryColor) && { "--hsk-primary": theme.primaryColor }), (theme == null ? void 0 : theme.backgroundColor) && { "--hsk-bg": theme.backgroundColor }), (theme == null ? void 0 : theme.textColor) && { "--hsk-text": theme.textColor }), (theme == null ? void 0 : theme.fontFamily) && { "--hsk-font": theme.fontFamily }), (theme == null ? void 0 : theme.borderRadius) && { "--hsk-border-radius": theme.borderRadius });
1208
1566
  return /* @__PURE__ */ jsx6(
1209
1567
  "div",
1210
1568
  {
1211
- className: `hsk-cb-overlay ${classNames.overlay || ""}`,
1569
+ className: cn("hsk-cb-overlay", classNames.overlay),
1212
1570
  onClick: onClose,
1213
1571
  style: __spreadValues(__spreadValues({
1214
1572
  backdropFilter: `blur(${blurVal})`,
1215
1573
  WebkitBackdropFilter: `blur(${blurVal})`
1216
1574
  }, backdropColor ? { background: backdropColor } : {}), customStyles),
1217
- children: /* @__PURE__ */ jsxs4("div", { className: `hsk-cb-panel ${classNames.panel || ""}`, onClick: (e) => e.stopPropagation(), children: [
1575
+ children: /* @__PURE__ */ jsxs4("div", { className: cn("hsk-cb-panel", classNames.panel), onClick: (e) => e.stopPropagation(), children: [
1218
1576
  /* @__PURE__ */ jsxs4("div", { className: "hsk-cb-topbar", children: [
1219
1577
  /* @__PURE__ */ jsxs4("div", { className: "hsk-cb-topbar-left", children: [
1220
1578
  /* @__PURE__ */ jsx6("span", { className: "hsk-cb-topbar-icon", style: { display: "flex", alignItems: "center" }, children: /* @__PURE__ */ jsx6(SparkleIcon3, {}) }),
1221
- /* @__PURE__ */ jsxs4("div", { children: [
1222
- /* @__PURE__ */ jsx6("div", { className: "hsk-cb-topbar-title", children: title }),
1223
- /* @__PURE__ */ jsx6("div", { className: "hsk-cb-topbar-sub", children: "Powered by Huskel AI \xB7 searches the whole catalogue" })
1224
- ] })
1579
+ /* @__PURE__ */ jsx6("div", { children: /* @__PURE__ */ jsx6("div", { className: "hsk-cb-topbar-title", children: title }) })
1225
1580
  ] }),
1226
1581
  /* @__PURE__ */ jsxs4("div", { className: "hsk-cb-topbar-actions", children: [
1227
1582
  messages.length > 0 && /* @__PURE__ */ jsx6("button", { className: "hsk-cb-topbar-btn", onClick: reset, children: "Clear chat" }),
@@ -1231,8 +1586,7 @@ function ChatModal({
1231
1586
  /* @__PURE__ */ jsxs4("div", { className: "hsk-cb-msgs", children: [
1232
1587
  messages.length === 0 ? /* @__PURE__ */ jsxs4("div", { className: "hsk-cb-empty", children: [
1233
1588
  /* @__PURE__ */ jsx6("div", { className: "hsk-cb-empty-icon", style: { display: "flex", alignItems: "center" }, children: /* @__PURE__ */ jsx6(SparkleIcon3, {}) }),
1234
- /* @__PURE__ */ jsx6("div", { className: "hsk-cb-empty-title", children: "What can I help you find?" }),
1235
- /* @__PURE__ */ jsx6("div", { className: "hsk-cb-empty-sub", children: "Ask about products, budgets, gift ideas, specs \u2014 I'll search the entire catalogue for you." }),
1589
+ /* @__PURE__ */ jsx6("div", { className: "hsk-cb-empty-title", children: "Find exactly what you need" }),
1236
1590
  /* @__PURE__ */ jsx6("div", { className: "hsk-cb-chips", children: chips.map((chip) => /* @__PURE__ */ jsx6(
1237
1591
  "button",
1238
1592
  {
@@ -1295,7 +1649,7 @@ function ChatModal({
1295
1649
  "textarea",
1296
1650
  {
1297
1651
  ref: textareaRef,
1298
- className: `hsk-cb-textarea ${classNames.input || ""}`,
1652
+ className: cn("hsk-cb-textarea", classNames.input),
1299
1653
  value: input,
1300
1654
  onChange: handleInput,
1301
1655
  onKeyDown: handleKeyDown,
@@ -1308,11 +1662,11 @@ function ChatModal({
1308
1662
  /* @__PURE__ */ jsx6(
1309
1663
  "button",
1310
1664
  {
1311
- className: `hsk-cb-send ${classNames.sendButton || ""}`,
1665
+ className: cn("hsk-cb-send", classNames.sendButton),
1312
1666
  onClick: () => handleSend(),
1313
1667
  disabled: !input.trim() || loading,
1314
1668
  "aria-label": "Send message",
1315
- children: /* @__PURE__ */ jsx6(ArrowUpIcon2, {})
1669
+ children: /* @__PURE__ */ jsx6(ArrowUpIcon3, {})
1316
1670
  }
1317
1671
  )
1318
1672
  ] }),
@@ -1335,17 +1689,17 @@ function AIChatButton({
1335
1689
  theme,
1336
1690
  classNames = {}
1337
1691
  }) {
1338
- const [open, setOpen] = useState7(false);
1339
- const [mounted, setMounted] = useState7(false);
1340
- useEffect6(() => {
1692
+ const [open, setOpen] = useState8(false);
1693
+ const [mounted, setMounted] = useState8(false);
1694
+ useEffect7(() => {
1341
1695
  setMounted(true);
1342
1696
  }, []);
1343
- const customStyles = __spreadValues(__spreadValues(__spreadValues(__spreadValues({}, (theme == null ? void 0 : theme.primaryColor) && { "--hsk-primary": theme.primaryColor }), (theme == null ? void 0 : theme.backgroundColor) && { "--hsk-bg": theme.backgroundColor }), (theme == null ? void 0 : theme.textColor) && { "--hsk-text": theme.textColor }), (theme == null ? void 0 : theme.fontFamily) && { "--hsk-font": theme.fontFamily });
1344
- return /* @__PURE__ */ jsxs4(Fragment3, { children: [
1697
+ const customStyles = __spreadValues(__spreadValues(__spreadValues(__spreadValues(__spreadValues({}, (theme == null ? void 0 : theme.primaryColor) && { "--hsk-primary": theme.primaryColor }), (theme == null ? void 0 : theme.backgroundColor) && { "--hsk-bg": theme.backgroundColor }), (theme == null ? void 0 : theme.textColor) && { "--hsk-text": theme.textColor }), (theme == null ? void 0 : theme.fontFamily) && { "--hsk-font": theme.fontFamily }), (theme == null ? void 0 : theme.borderRadius) && { "--hsk-border-radius": theme.borderRadius });
1698
+ return /* @__PURE__ */ jsxs4(Fragment4, { children: [
1345
1699
  /* @__PURE__ */ jsxs4(
1346
1700
  "button",
1347
1701
  {
1348
- className: `hsk-cb-btn ${classNames.button || ""} ${className || ""}`,
1702
+ className: cn("hsk-cb-btn", classNames.button, className),
1349
1703
  onClick: () => setOpen(true),
1350
1704
  style: customStyles,
1351
1705
  "aria-label": "Open AI chat",
@@ -1375,8 +1729,260 @@ function AIChatButton({
1375
1729
  )
1376
1730
  ] });
1377
1731
  }
1732
+
1733
+ // src/components/CartBadge.tsx
1734
+ import { jsx as jsx7 } from "react/jsx-runtime";
1735
+ function CartBadge({ className }) {
1736
+ const { cart } = useCart();
1737
+ if (!cart || cart.item_count === 0) return null;
1738
+ return /* @__PURE__ */ jsx7("span", { className: cn("hsk-cart-badge", className), children: cart.item_count });
1739
+ }
1740
+
1741
+ // src/components/CartDrawer.tsx
1742
+ import { useState as useState10, useEffect as useEffect9 } from "react";
1743
+ import { createPortal as createPortal4 } from "react-dom";
1744
+
1745
+ // src/components/CheckoutModal.tsx
1746
+ import { useState as useState9, useEffect as useEffect8 } from "react";
1747
+ import { createPortal as createPortal3 } from "react-dom";
1748
+ import { Fragment as Fragment5, jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
1749
+ function CheckoutModal({
1750
+ onClose,
1751
+ theme,
1752
+ customStyles,
1753
+ hskThemeAttr
1754
+ }) {
1755
+ var _a, _b, _c, _d;
1756
+ const { cart, loading: cartLoading } = useCart();
1757
+ const client = useHuskelContext();
1758
+ const [config, setConfig] = useState9(null);
1759
+ const [loading, setLoading] = useState9(true);
1760
+ const [checkingOut, setCheckingOut] = useState9(false);
1761
+ const [paymentSuccess, setPaymentSuccess] = useState9(false);
1762
+ useEffect8(() => {
1763
+ client.api.getCheckoutConfig().then((res) => setConfig(res.payment_methods)).catch((e) => console.error("[Huskel] Failed to fetch checkout config", e)).finally(() => setLoading(false));
1764
+ }, [client]);
1765
+ const handlePay = async (method) => {
1766
+ setCheckingOut(true);
1767
+ setTimeout(async () => {
1768
+ try {
1769
+ const payload = await client.api.checkoutCart();
1770
+ if (client.onCheckout) {
1771
+ client.onCheckout(payload);
1772
+ }
1773
+ setPaymentSuccess(true);
1774
+ setTimeout(() => {
1775
+ onClose();
1776
+ }, 3e3);
1777
+ } catch (e) {
1778
+ console.error("[Huskel] Checkout failed", e);
1779
+ setCheckingOut(false);
1780
+ }
1781
+ }, 1500);
1782
+ };
1783
+ const hasPaymentMethods = config && Object.values(config).some((m) => m.enabled);
1784
+ return createPortal3(
1785
+ /* @__PURE__ */ jsx8(
1786
+ "div",
1787
+ {
1788
+ className: "hsk-cart-backdrop",
1789
+ style: __spreadProps(__spreadValues({}, customStyles), { zIndex: 999999 }),
1790
+ "data-hsk-theme": hskThemeAttr,
1791
+ onClick: onClose,
1792
+ children: /* @__PURE__ */ jsxs5(
1793
+ "div",
1794
+ {
1795
+ className: "hsk-checkout-modal",
1796
+ style: customStyles,
1797
+ "data-hsk-theme": hskThemeAttr,
1798
+ onClick: (e) => e.stopPropagation(),
1799
+ children: [
1800
+ /* @__PURE__ */ jsxs5("div", { className: "hsk-checkout-header", children: [
1801
+ /* @__PURE__ */ jsx8("h2", { children: "Secure Checkout" }),
1802
+ /* @__PURE__ */ jsx8("button", { onClick: onClose, className: "hsk-close-btn", children: "\xD7" })
1803
+ ] }),
1804
+ /* @__PURE__ */ jsx8("div", { className: "hsk-checkout-content", children: paymentSuccess ? /* @__PURE__ */ jsxs5("div", { className: "hsk-checkout-success", children: [
1805
+ /* @__PURE__ */ jsxs5("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: "hsk-success-icon", children: [
1806
+ /* @__PURE__ */ jsx8("path", { d: "M22 11.08V12a10 10 0 1 1-5.93-9.14" }),
1807
+ /* @__PURE__ */ jsx8("polyline", { points: "22 4 12 14.01 9 11.01" })
1808
+ ] }),
1809
+ /* @__PURE__ */ jsx8("h3", { children: "Payment Successful!" }),
1810
+ /* @__PURE__ */ jsx8("p", { children: "Thank you for your order." })
1811
+ ] }) : /* @__PURE__ */ jsxs5("div", { className: "hsk-checkout-split", children: [
1812
+ /* @__PURE__ */ jsxs5("div", { className: "hsk-checkout-summary", children: [
1813
+ /* @__PURE__ */ jsx8("h3", { children: "Order Summary" }),
1814
+ cartLoading || !cart ? /* @__PURE__ */ jsx8("p", { className: "hsk-cart-loading", children: "Loading order..." }) : /* @__PURE__ */ jsxs5(Fragment5, { children: [
1815
+ /* @__PURE__ */ jsx8("ul", { className: "hsk-checkout-items", children: cart.items.map((item) => /* @__PURE__ */ jsxs5("li", { children: [
1816
+ /* @__PURE__ */ jsxs5("span", { children: [
1817
+ item.quantity,
1818
+ "x ",
1819
+ item.name
1820
+ ] }),
1821
+ /* @__PURE__ */ jsxs5("span", { children: [
1822
+ item.currency,
1823
+ " ",
1824
+ (item.price_numeric * item.quantity).toLocaleString(void 0, { minimumFractionDigits: 2 })
1825
+ ] })
1826
+ ] }, item.id)) }),
1827
+ /* @__PURE__ */ jsxs5("div", { className: "hsk-checkout-total", children: [
1828
+ /* @__PURE__ */ jsx8("span", { children: "Total" }),
1829
+ /* @__PURE__ */ jsxs5("span", { children: [
1830
+ cart.currency,
1831
+ " ",
1832
+ cart.total.toLocaleString(void 0, { minimumFractionDigits: 2 })
1833
+ ] })
1834
+ ] })
1835
+ ] })
1836
+ ] }),
1837
+ /* @__PURE__ */ jsxs5("div", { className: "hsk-checkout-payment", children: [
1838
+ /* @__PURE__ */ jsx8("h3", { children: "Payment Method" }),
1839
+ loading ? /* @__PURE__ */ jsx8("p", { className: "hsk-cart-loading", children: "Loading secure payment methods..." }) : !hasPaymentMethods ? /* @__PURE__ */ jsx8("p", { className: "hsk-checkout-error", children: "No payment methods are currently available for this store." }) : /* @__PURE__ */ jsxs5("div", { className: "hsk-payment-options", children: [
1840
+ ((_a = config == null ? void 0 : config.mpesa) == null ? void 0 : _a.enabled) && /* @__PURE__ */ jsx8("button", { onClick: () => handlePay("mpesa"), disabled: checkingOut, className: "hsk-pay-btn hsk-pay-mpesa", children: checkingOut ? "Processing..." : "Pay with M-Pesa" }),
1841
+ ((_b = config == null ? void 0 : config.equity) == null ? void 0 : _b.enabled) && /* @__PURE__ */ jsx8("button", { onClick: () => handlePay("equity"), disabled: checkingOut, className: "hsk-pay-btn hsk-pay-equity", children: checkingOut ? "Processing..." : "Pay with Equity Bank" }),
1842
+ ((_c = config == null ? void 0 : config.stripe) == null ? void 0 : _c.enabled) && /* @__PURE__ */ jsx8("button", { onClick: () => handlePay("stripe"), disabled: checkingOut, className: "hsk-pay-btn hsk-pay-stripe", children: checkingOut ? "Processing..." : "Pay with Card (Stripe)" }),
1843
+ ((_d = config == null ? void 0 : config.paypal) == null ? void 0 : _d.enabled) && /* @__PURE__ */ jsx8("button", { onClick: () => handlePay("paypal"), disabled: checkingOut, className: "hsk-pay-btn hsk-pay-paypal", children: checkingOut ? "Processing..." : "Pay with PayPal" })
1844
+ ] })
1845
+ ] })
1846
+ ] }) })
1847
+ ]
1848
+ }
1849
+ )
1850
+ }
1851
+ ),
1852
+ document.body
1853
+ );
1854
+ }
1855
+
1856
+ // src/components/CartDrawer.tsx
1857
+ import { Fragment as Fragment6, jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
1858
+ function CartDrawer({
1859
+ trigger,
1860
+ className,
1861
+ theme
1862
+ }) {
1863
+ const { cart, loading } = useCart();
1864
+ const [open, setOpen] = useState10(false);
1865
+ const [showCheckout, setShowCheckout] = useState10(false);
1866
+ const [mounted, setMounted] = useState10(false);
1867
+ const client = useHuskelContext();
1868
+ useEffect9(() => {
1869
+ setMounted(true);
1870
+ const handleTriggerCheckout = () => {
1871
+ setShowCheckout(true);
1872
+ setOpen(false);
1873
+ };
1874
+ window.addEventListener("huskel:trigger_checkout", handleTriggerCheckout);
1875
+ return () => {
1876
+ window.removeEventListener("huskel:trigger_checkout", handleTriggerCheckout);
1877
+ };
1878
+ }, []);
1879
+ useEffect9(() => {
1880
+ if (open) {
1881
+ document.body.style.overflow = "hidden";
1882
+ } else {
1883
+ document.body.style.overflow = "";
1884
+ }
1885
+ return () => {
1886
+ document.body.style.overflow = "";
1887
+ };
1888
+ }, [open]);
1889
+ const handleCheckout = async () => {
1890
+ if (!cart || cart.items.length === 0) return;
1891
+ setShowCheckout(true);
1892
+ };
1893
+ const isStringTheme = typeof theme === "string";
1894
+ const hskThemeAttr = isStringTheme ? theme : void 0;
1895
+ const customStyles = !isStringTheme && theme ? __spreadValues(__spreadValues(__spreadValues(__spreadValues(__spreadValues({}, (theme == null ? void 0 : theme.primaryColor) && { "--hsk-primary": theme.primaryColor, "--hsk-primary-color": theme.primaryColor }), (theme == null ? void 0 : theme.backgroundColor) && { "--hsk-bg": theme.backgroundColor }), (theme == null ? void 0 : theme.textColor) && { "--hsk-text": theme.textColor }), (theme == null ? void 0 : theme.fontFamily) && { "--hsk-font": theme.fontFamily }), (theme == null ? void 0 : theme.borderRadius) && { "--hsk-border-radius": theme.borderRadius }) : void 0;
1896
+ return /* @__PURE__ */ jsxs6(Fragment6, { children: [
1897
+ /* @__PURE__ */ jsx9("div", { onClick: () => setOpen(true), style: { display: "inline-block" }, children: trigger || /* @__PURE__ */ jsxs6(
1898
+ "button",
1899
+ {
1900
+ className: cn("hsk-cart-trigger", className),
1901
+ style: customStyles,
1902
+ "data-hsk-theme": hskThemeAttr,
1903
+ "aria-label": "Open cart",
1904
+ children: [
1905
+ /* @__PURE__ */ jsxs6("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
1906
+ /* @__PURE__ */ jsx9("circle", { cx: "9", cy: "21", r: "1" }),
1907
+ /* @__PURE__ */ jsx9("circle", { cx: "20", cy: "21", r: "1" }),
1908
+ /* @__PURE__ */ jsx9("path", { d: "M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6" })
1909
+ ] }),
1910
+ cart && cart.item_count > 0 ? /* @__PURE__ */ jsx9("span", { className: "hsk-cart-trigger-badge", children: cart.item_count }) : null
1911
+ ]
1912
+ }
1913
+ ) }),
1914
+ open && mounted && createPortal4(
1915
+ /* @__PURE__ */ jsx9(
1916
+ "div",
1917
+ {
1918
+ className: "hsk-cart-backdrop",
1919
+ style: customStyles,
1920
+ "data-hsk-theme": hskThemeAttr,
1921
+ onClick: () => setOpen(false),
1922
+ children: /* @__PURE__ */ jsxs6(
1923
+ "div",
1924
+ {
1925
+ className: "hsk-cart-bottom-sheet",
1926
+ style: customStyles,
1927
+ "data-hsk-theme": hskThemeAttr,
1928
+ onClick: (e) => e.stopPropagation(),
1929
+ children: [
1930
+ /* @__PURE__ */ jsx9("div", { className: "hsk-cart-sheet-handle" }),
1931
+ /* @__PURE__ */ jsxs6("div", { className: "hsk-cart-sheet-header", children: [
1932
+ /* @__PURE__ */ jsx9("h2", { children: "Your Cart" }),
1933
+ /* @__PURE__ */ jsx9("button", { onClick: () => setOpen(false), className: "hsk-close-btn", children: "\xD7" })
1934
+ ] }),
1935
+ /* @__PURE__ */ jsx9("div", { className: "hsk-cart-sheet-content", children: loading && !cart ? /* @__PURE__ */ jsx9("div", { className: "hsk-cart-loading", children: "Loading cart..." }) : !cart || cart.items.length === 0 ? /* @__PURE__ */ jsx9("div", { className: "hsk-cart-empty", children: "Your cart is empty." }) : /* @__PURE__ */ jsx9("ul", { className: "hsk-cart-items", children: cart.items.map((item) => /* @__PURE__ */ jsxs6("li", { className: "hsk-cart-item", children: [
1936
+ item.image && /* @__PURE__ */ jsx9("img", { src: item.image, alt: item.name, className: "hsk-cart-item-img" }),
1937
+ /* @__PURE__ */ jsxs6("div", { className: "hsk-cart-item-info", children: [
1938
+ /* @__PURE__ */ jsx9("span", { className: "hsk-cart-item-name", children: item.name }),
1939
+ /* @__PURE__ */ jsxs6("span", { className: "hsk-cart-item-price", children: [
1940
+ item.currency,
1941
+ " ",
1942
+ item.price_numeric.toLocaleString(void 0, { minimumFractionDigits: 2 })
1943
+ ] })
1944
+ ] }),
1945
+ /* @__PURE__ */ jsxs6("div", { className: "hsk-cart-item-qty", children: [
1946
+ "x",
1947
+ item.quantity
1948
+ ] })
1949
+ ] }, item.id)) }) }),
1950
+ cart && cart.items.length > 0 && /* @__PURE__ */ jsxs6("div", { className: "hsk-cart-sheet-footer", children: [
1951
+ /* @__PURE__ */ jsxs6("div", { className: "hsk-cart-total", children: [
1952
+ /* @__PURE__ */ jsx9("span", { children: "Total" }),
1953
+ /* @__PURE__ */ jsxs6("span", { children: [
1954
+ cart.currency,
1955
+ " ",
1956
+ cart.total.toLocaleString(void 0, { minimumFractionDigits: 2 })
1957
+ ] })
1958
+ ] }),
1959
+ /* @__PURE__ */ jsx9("button", { onClick: handleCheckout, className: "hsk-checkout-btn", children: "Checkout securely" })
1960
+ ] })
1961
+ ]
1962
+ }
1963
+ )
1964
+ }
1965
+ ),
1966
+ document.body
1967
+ ),
1968
+ showCheckout && mounted && /* @__PURE__ */ jsx9(
1969
+ CheckoutModal,
1970
+ {
1971
+ onClose: () => {
1972
+ setShowCheckout(false);
1973
+ setOpen(false);
1974
+ },
1975
+ theme: isStringTheme ? theme : void 0,
1976
+ customStyles,
1977
+ hskThemeAttr
1978
+ }
1979
+ )
1980
+ ] });
1981
+ }
1378
1982
  export {
1379
1983
  AIChatButton,
1984
+ CartBadge,
1985
+ CartDrawer,
1380
1986
  ChatWidget,
1381
1987
  HuskelAPI,
1382
1988
  HuskelClient,
@@ -1385,6 +1991,7 @@ export {
1385
1991
  Sparkle,
1386
1992
  getHuskelClient,
1387
1993
  initHuskel,
1994
+ useCart,
1388
1995
  useChat,
1389
1996
  useHuskel,
1390
1997
  useIngest,