@retrivora-ai/rag-engine 1.1.8 → 1.2.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.d.mts CHANGED
@@ -6,8 +6,10 @@ export { C as Chunk, a as ChunkOptions } from './DocumentChunker-Dh9TvmGG.mjs';
6
6
  interface ChatWidgetProps {
7
7
  /** Position of the floating button. Defaults to bottom-right. */
8
8
  position?: 'bottom-right' | 'bottom-left';
9
+ /** Called when the user clicks 'Add to Cart' on a product */
10
+ onAddToCart?: (product: any) => void;
9
11
  }
10
- declare function ChatWidget({ position }: ChatWidgetProps): React$1.JSX.Element | null;
12
+ declare function ChatWidget({ position, onAddToCart }: ChatWidgetProps): React$1.JSX.Element | null;
11
13
 
12
14
  interface ChatWindowProps {
13
15
  /** Additional className for the wrapper div */
@@ -28,8 +30,10 @@ interface ChatWindowProps {
28
30
  onMaximize?: () => void;
29
31
  /** Whether the window is currently maximized */
30
32
  isMaximized?: boolean;
33
+ /** Called when the user clicks 'Add to Cart' on a product */
34
+ onAddToCart?: (product: any) => void;
31
35
  }
32
- declare function ChatWindow({ className, style, onClose, showClose, onResizeStart, onResetResize, isResized, onMaximize, isMaximized }: ChatWindowProps): React$1.JSX.Element;
36
+ declare function ChatWindow({ className, style, onClose, showClose, onResizeStart, onResetResize, isResized, onMaximize, isMaximized, onAddToCart }: ChatWindowProps): React$1.JSX.Element;
33
37
 
34
38
  interface DocumentUploadProps {
35
39
  /** Optional namespace for the upload */
@@ -41,14 +45,25 @@ interface DocumentUploadProps {
41
45
  }
42
46
  declare function DocumentUpload({ namespace, onUploadComplete, className }: DocumentUploadProps): React$1.JSX.Element;
43
47
 
48
+ interface Product {
49
+ id: string | number;
50
+ name: string;
51
+ brand?: string;
52
+ price?: string | number;
53
+ image?: string;
54
+ link?: string;
55
+ description?: string;
56
+ }
57
+
44
58
  interface MessageBubbleProps {
45
59
  message: ChatMessage;
46
60
  sources?: VectorMatch[];
47
61
  isStreaming?: boolean;
48
62
  primaryColor?: string;
49
63
  accentColor?: string;
64
+ onAddToCart?: (product: Product) => void;
50
65
  }
51
- declare function MessageBubble({ message, sources, isStreaming, primaryColor, accentColor, }: MessageBubbleProps): React$1.JSX.Element;
66
+ declare function MessageBubble({ message, sources, isStreaming, primaryColor, accentColor, onAddToCart, }: MessageBubbleProps): React$1.JSX.Element;
52
67
 
53
68
  interface SourceCardProps {
54
69
  source: VectorMatch;
package/dist/index.d.ts CHANGED
@@ -6,8 +6,10 @@ export { C as Chunk, a as ChunkOptions } from './DocumentChunker-Dh9TvmGG.js';
6
6
  interface ChatWidgetProps {
7
7
  /** Position of the floating button. Defaults to bottom-right. */
8
8
  position?: 'bottom-right' | 'bottom-left';
9
+ /** Called when the user clicks 'Add to Cart' on a product */
10
+ onAddToCart?: (product: any) => void;
9
11
  }
10
- declare function ChatWidget({ position }: ChatWidgetProps): React$1.JSX.Element | null;
12
+ declare function ChatWidget({ position, onAddToCart }: ChatWidgetProps): React$1.JSX.Element | null;
11
13
 
12
14
  interface ChatWindowProps {
13
15
  /** Additional className for the wrapper div */
@@ -28,8 +30,10 @@ interface ChatWindowProps {
28
30
  onMaximize?: () => void;
29
31
  /** Whether the window is currently maximized */
30
32
  isMaximized?: boolean;
33
+ /** Called when the user clicks 'Add to Cart' on a product */
34
+ onAddToCart?: (product: any) => void;
31
35
  }
32
- declare function ChatWindow({ className, style, onClose, showClose, onResizeStart, onResetResize, isResized, onMaximize, isMaximized }: ChatWindowProps): React$1.JSX.Element;
36
+ declare function ChatWindow({ className, style, onClose, showClose, onResizeStart, onResetResize, isResized, onMaximize, isMaximized, onAddToCart }: ChatWindowProps): React$1.JSX.Element;
33
37
 
34
38
  interface DocumentUploadProps {
35
39
  /** Optional namespace for the upload */
@@ -41,14 +45,25 @@ interface DocumentUploadProps {
41
45
  }
42
46
  declare function DocumentUpload({ namespace, onUploadComplete, className }: DocumentUploadProps): React$1.JSX.Element;
43
47
 
48
+ interface Product {
49
+ id: string | number;
50
+ name: string;
51
+ brand?: string;
52
+ price?: string | number;
53
+ image?: string;
54
+ link?: string;
55
+ description?: string;
56
+ }
57
+
44
58
  interface MessageBubbleProps {
45
59
  message: ChatMessage;
46
60
  sources?: VectorMatch[];
47
61
  isStreaming?: boolean;
48
62
  primaryColor?: string;
49
63
  accentColor?: string;
64
+ onAddToCart?: (product: Product) => void;
50
65
  }
51
- declare function MessageBubble({ message, sources, isStreaming, primaryColor, accentColor, }: MessageBubbleProps): React$1.JSX.Element;
66
+ declare function MessageBubble({ message, sources, isStreaming, primaryColor, accentColor, onAddToCart, }: MessageBubbleProps): React$1.JSX.Element;
52
67
 
53
68
  interface SourceCardProps {
54
69
  source: VectorMatch;
package/dist/index.js CHANGED
@@ -103,7 +103,7 @@ var import_lucide_react3 = require("lucide-react");
103
103
  var import_react2 = __toESM(require("react"));
104
104
  var import_image = __toESM(require("next/image"));
105
105
  var import_lucide_react2 = require("lucide-react");
106
- function ProductCard({ product, primaryColor = "#6366f1" }) {
106
+ function ProductCard({ product, primaryColor = "#6366f1", onAddToCart }) {
107
107
  return /* @__PURE__ */ import_react2.default.createElement("div", { className: "flex-shrink-0 w-full bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-xl overflow-hidden shadow-sm hover:shadow-md transition-all duration-300 group" }, /* @__PURE__ */ import_react2.default.createElement("div", { className: "relative h-40 w-full bg-slate-100 dark:bg-white/5 flex items-center justify-center overflow-hidden" }, product.image ? /* @__PURE__ */ import_react2.default.createElement(
108
108
  import_image.default,
109
109
  {
@@ -116,6 +116,7 @@ function ProductCard({ product, primaryColor = "#6366f1" }) {
116
116
  ) : /* @__PURE__ */ import_react2.default.createElement("div", { className: "text-slate-400 dark:text-white/20" }, /* @__PURE__ */ import_react2.default.createElement(import_lucide_react2.ShoppingCart, { className: "w-12 h-12" })), product.brand && /* @__PURE__ */ import_react2.default.createElement("div", { className: "absolute top-2 left-2 px-2 py-1 bg-white/90 dark:bg-black/60 backdrop-blur-md rounded-md text-[10px] font-bold uppercase tracking-wider text-slate-700 dark:text-white/90 shadow-sm" }, product.brand)), /* @__PURE__ */ import_react2.default.createElement("div", { className: "p-4 flex flex-col gap-2" }, /* @__PURE__ */ import_react2.default.createElement("div", { className: "flex justify-between items-start gap-2" }, /* @__PURE__ */ import_react2.default.createElement("h3", { className: "text-sm font-semibold text-slate-800 dark:text-white/90 line-clamp-1" }, product.name), product.price && /* @__PURE__ */ import_react2.default.createElement("span", { className: "text-sm font-bold", style: { color: primaryColor } }, typeof product.price === "number" ? `$${product.price}` : product.price)), product.description && /* @__PURE__ */ import_react2.default.createElement("p", { className: "text-[11px] text-slate-500 dark:text-white/50 line-clamp-2 leading-relaxed" }, product.description), /* @__PURE__ */ import_react2.default.createElement("div", { className: "mt-2 flex gap-2" }, /* @__PURE__ */ import_react2.default.createElement(
117
117
  "button",
118
118
  {
119
+ onClick: () => onAddToCart == null ? void 0 : onAddToCart(product),
119
120
  className: "flex-1 py-2 px-3 rounded-lg text-[11px] font-medium text-white shadow-sm hover:opacity-90 transition-opacity flex items-center justify-center gap-1.5",
120
121
  style: { background: primaryColor }
121
122
  },
@@ -134,8 +135,21 @@ function ProductCard({ product, primaryColor = "#6366f1" }) {
134
135
  }
135
136
 
136
137
  // src/components/ProductCarousel.tsx
137
- function ProductCarousel({ products, primaryColor = "#6366f1" }) {
138
+ function ProductCarousel({ products, primaryColor = "#6366f1", onAddToCart }) {
138
139
  const scrollRef = (0, import_react3.useRef)(null);
140
+ const [canScroll, setCanScroll] = (0, import_react3.useState)(false);
141
+ (0, import_react3.useEffect)(() => {
142
+ const checkScroll = () => {
143
+ if (scrollRef.current) {
144
+ const hasOverflow = scrollRef.current.scrollWidth > scrollRef.current.clientWidth;
145
+ setCanScroll(hasOverflow);
146
+ }
147
+ };
148
+ checkScroll();
149
+ const observer = new ResizeObserver(checkScroll);
150
+ if (scrollRef.current) observer.observe(scrollRef.current);
151
+ return () => observer.disconnect();
152
+ }, [products]);
139
153
  const scroll = (direction) => {
140
154
  if (scrollRef.current) {
141
155
  const firstCard = scrollRef.current.firstElementChild;
@@ -147,7 +161,10 @@ function ProductCarousel({ products, primaryColor = "#6366f1" }) {
147
161
  }
148
162
  };
149
163
  if (!products || products.length === 0) return null;
150
- return /* @__PURE__ */ import_react3.default.createElement("div", { className: "relative w-full my-4 group/carousel" }, /* @__PURE__ */ import_react3.default.createElement("div", { className: "absolute -left-4 top-1/2 -translate-y-1/2 z-20 opacity-100 md:opacity-0 md:group-hover/carousel:opacity-100 transition-opacity" }, /* @__PURE__ */ import_react3.default.createElement(
164
+ if (products.length === 1) {
165
+ return /* @__PURE__ */ import_react3.default.createElement("div", { className: "my-4 w-[85%] sm:w-[48%] md:w-[31%] min-w-[200px] max-w-[300px] animate-fade-in-up" }, /* @__PURE__ */ import_react3.default.createElement(ProductCard, { product: products[0], primaryColor, onAddToCart }));
166
+ }
167
+ return /* @__PURE__ */ import_react3.default.createElement("div", { className: "relative w-full my-4 group/carousel animate-fade-in-up" }, canScroll && /* @__PURE__ */ import_react3.default.createElement(import_react3.default.Fragment, null, /* @__PURE__ */ import_react3.default.createElement("div", { className: "absolute -left-4 top-1/2 -translate-y-1/2 z-20 opacity-100 md:opacity-0 md:group-hover/carousel:opacity-100 transition-opacity" }, /* @__PURE__ */ import_react3.default.createElement(
151
168
  "button",
152
169
  {
153
170
  onClick: (e) => {
@@ -169,7 +186,7 @@ function ProductCarousel({ products, primaryColor = "#6366f1" }) {
169
186
  "aria-label": "Scroll right"
170
187
  },
171
188
  /* @__PURE__ */ import_react3.default.createElement(import_lucide_react3.ChevronRight, { className: "w-5 h-5" })
172
- )), /* @__PURE__ */ import_react3.default.createElement(
189
+ ))), /* @__PURE__ */ import_react3.default.createElement(
173
190
  "div",
174
191
  {
175
192
  ref: scrollRef,
@@ -182,7 +199,7 @@ function ProductCarousel({ products, primaryColor = "#6366f1" }) {
182
199
  key: product.id,
183
200
  className: "snap-start flex-shrink-0 w-[85%] sm:w-[48%] md:w-[31%] min-w-[200px] max-w-[300px]"
184
201
  },
185
- /* @__PURE__ */ import_react3.default.createElement(ProductCard, { product, primaryColor })
202
+ /* @__PURE__ */ import_react3.default.createElement(ProductCard, { product, primaryColor, onAddToCart })
186
203
  ))
187
204
  ));
188
205
  }
@@ -193,7 +210,8 @@ function MessageBubble({
193
210
  sources,
194
211
  isStreaming = false,
195
212
  primaryColor = "#6366f1",
196
- accentColor = "#8b5cf6"
213
+ accentColor = "#8b5cf6",
214
+ onAddToCart
197
215
  }) {
198
216
  const isUser = message.role === "user";
199
217
  const [showSources, setShowSources] = import_react4.default.useState(false);
@@ -282,7 +300,14 @@ function MessageBubble({
282
300
  style: isUser ? { background: `linear-gradient(135deg, ${primaryColor}, ${accentColor})` } : {}
283
301
  },
284
302
  isStreaming && !message.content ? /* @__PURE__ */ import_react4.default.createElement("span", { className: "flex items-center gap-1 py-1" }, /* @__PURE__ */ import_react4.default.createElement("span", { className: "w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:0ms]" }), /* @__PURE__ */ import_react4.default.createElement("span", { className: "w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:150ms]" }), /* @__PURE__ */ import_react4.default.createElement("span", { className: "w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:300ms]" })) : /* @__PURE__ */ import_react4.default.createElement("div", { className: `prose prose-sm max-w-none ${isUser ? "prose-invert" : "dark:prose-invert"}` }, /* @__PURE__ */ import_react4.default.createElement(import_react_markdown.default, { remarkPlugins: [import_remark_gfm.default] }, cleanContent || message.content), isStreaming && message.content && /* @__PURE__ */ import_react4.default.createElement("span", { className: "inline-block w-1.5 h-3.5 ml-1 bg-emerald-500 animate-pulse align-middle" }))
285
- ), !isUser && hasProducts && /* @__PURE__ */ import_react4.default.createElement("div", { className: "w-full mt-1" }, /* @__PURE__ */ import_react4.default.createElement(ProductCarousel, { products: allProducts, primaryColor })), !isUser && sources && sources.length > 0 && /* @__PURE__ */ import_react4.default.createElement("div", { className: "w-full" }, /* @__PURE__ */ import_react4.default.createElement(
303
+ ), !isUser && hasProducts && /* @__PURE__ */ import_react4.default.createElement("div", { className: "w-full mt-1" }, /* @__PURE__ */ import_react4.default.createElement(
304
+ ProductCarousel,
305
+ {
306
+ products: allProducts,
307
+ primaryColor,
308
+ onAddToCart
309
+ }
310
+ )), !isUser && sources && sources.length > 0 && /* @__PURE__ */ import_react4.default.createElement("div", { className: "w-full" }, /* @__PURE__ */ import_react4.default.createElement(
286
311
  "button",
287
312
  {
288
313
  onClick: () => setShowSources((s) => !s),
@@ -563,7 +588,8 @@ function ChatWindow({
563
588
  onResetResize,
564
589
  isResized = false,
565
590
  onMaximize,
566
- isMaximized = false
591
+ isMaximized = false,
592
+ onAddToCart
567
593
  }) {
568
594
  var _a;
569
595
  const { ui, projectId } = useConfig();
@@ -703,7 +729,8 @@ function ChatWindow({
703
729
  sources: msg.sources,
704
730
  isStreaming: isLoading && index === messages.length - 1 && msg.role === "assistant",
705
731
  primaryColor: ui.primaryColor,
706
- accentColor: ui.accentColor
732
+ accentColor: ui.accentColor,
733
+ onAddToCart
707
734
  }
708
735
  )), isLoading && ((_a = messages[messages.length - 1]) == null ? void 0 : _a.role) !== "assistant" && /* @__PURE__ */ import_react7.default.createElement(
709
736
  MessageBubble,
@@ -711,7 +738,8 @@ function ChatWindow({
711
738
  message: { role: "assistant", content: "" },
712
739
  isStreaming: true,
713
740
  primaryColor: ui.primaryColor,
714
- accentColor: ui.accentColor
741
+ accentColor: ui.accentColor,
742
+ onAddToCart
715
743
  }
716
744
  ), error && /* @__PURE__ */ import_react7.default.createElement("div", { className: "flex items-center gap-2 text-rose-500 dark:text-rose-400 text-sm bg-rose-50 dark:bg-rose-400/10 border border-rose-200 dark:border-rose-400/20 rounded-xl px-4 py-3" }, /* @__PURE__ */ import_react7.default.createElement(import_lucide_react5.TriangleAlert, { className: "w-4 h-4 flex-shrink-0" }), /* @__PURE__ */ import_react7.default.createElement("span", null, error)), /* @__PURE__ */ import_react7.default.createElement("div", { ref: bottomRef })),
717
745
  /* @__PURE__ */ import_react7.default.createElement("div", { className: `px-4 pb-4 pt-2 border-t border-slate-200 dark:border-white/10 ${isGlass ? "bg-transparent" : "bg-white dark:bg-[#0f0f1a]"}` }, /* @__PURE__ */ import_react7.default.createElement("div", { className: `flex items-end gap-2 bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 p-2 focus-within:border-slate-300 dark:focus-within:border-white/20 transition-all ${ui.borderRadius === "full" ? "rounded-3xl" : "rounded-2xl"}` }, /* @__PURE__ */ import_react7.default.createElement(
@@ -745,7 +773,7 @@ function ChatWindow({
745
773
 
746
774
  // src/components/ChatWidget.tsx
747
775
  var DEFAULT_DIMENSIONS = { width: 380, height: 600 };
748
- function ChatWidget({ position = "bottom-right" }) {
776
+ function ChatWidget({ position = "bottom-right", onAddToCart }) {
749
777
  const { ui } = useConfig();
750
778
  const [isOpen, setIsOpen] = (0, import_react8.useState)(false);
751
779
  const [hasUnread, setHasUnread] = (0, import_react8.useState)(false);
@@ -822,7 +850,8 @@ function ChatWidget({ position = "bottom-right" }) {
822
850
  onResetResize: ui.allowResize !== false ? handleResetResize : void 0,
823
851
  isResized,
824
852
  onMaximize: ui.allowResize !== false ? handleMaximize : void 0,
825
- isMaximized
853
+ isMaximized,
854
+ onAddToCart
826
855
  }
827
856
  ),
828
857
  /* @__PURE__ */ import_react8.default.createElement(
package/dist/index.mjs CHANGED
@@ -7,11 +7,11 @@ import {
7
7
  } from "./chunk-X4TOT24V.mjs";
8
8
 
9
9
  // src/components/ChatWidget.tsx
10
- import React8, { useState as useState4 } from "react";
10
+ import React8, { useState as useState5 } from "react";
11
11
  import { MessageSquare, X as X2 } from "lucide-react";
12
12
 
13
13
  // src/components/ChatWindow.tsx
14
- import React7, { useState as useState3, useRef as useRef3, useEffect as useEffect3, useCallback as useCallback2 } from "react";
14
+ import React7, { useState as useState4, useRef as useRef3, useEffect as useEffect4, useCallback as useCallback2 } from "react";
15
15
  import {
16
16
  Bot as Bot2,
17
17
  Trash2,
@@ -54,14 +54,14 @@ function SourceCard({ source, index }) {
54
54
  }
55
55
 
56
56
  // src/components/ProductCarousel.tsx
57
- import React3, { useRef } from "react";
57
+ import React3, { useRef, useState, useEffect } from "react";
58
58
  import { ChevronLeft, ChevronRight } from "lucide-react";
59
59
 
60
60
  // src/components/ProductCard.tsx
61
61
  import React2 from "react";
62
62
  import Image from "next/image";
63
63
  import { ShoppingCart, ExternalLink } from "lucide-react";
64
- function ProductCard({ product, primaryColor = "#6366f1" }) {
64
+ function ProductCard({ product, primaryColor = "#6366f1", onAddToCart }) {
65
65
  return /* @__PURE__ */ React2.createElement("div", { className: "flex-shrink-0 w-full bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-xl overflow-hidden shadow-sm hover:shadow-md transition-all duration-300 group" }, /* @__PURE__ */ React2.createElement("div", { className: "relative h-40 w-full bg-slate-100 dark:bg-white/5 flex items-center justify-center overflow-hidden" }, product.image ? /* @__PURE__ */ React2.createElement(
66
66
  Image,
67
67
  {
@@ -74,6 +74,7 @@ function ProductCard({ product, primaryColor = "#6366f1" }) {
74
74
  ) : /* @__PURE__ */ React2.createElement("div", { className: "text-slate-400 dark:text-white/20" }, /* @__PURE__ */ React2.createElement(ShoppingCart, { className: "w-12 h-12" })), product.brand && /* @__PURE__ */ React2.createElement("div", { className: "absolute top-2 left-2 px-2 py-1 bg-white/90 dark:bg-black/60 backdrop-blur-md rounded-md text-[10px] font-bold uppercase tracking-wider text-slate-700 dark:text-white/90 shadow-sm" }, product.brand)), /* @__PURE__ */ React2.createElement("div", { className: "p-4 flex flex-col gap-2" }, /* @__PURE__ */ React2.createElement("div", { className: "flex justify-between items-start gap-2" }, /* @__PURE__ */ React2.createElement("h3", { className: "text-sm font-semibold text-slate-800 dark:text-white/90 line-clamp-1" }, product.name), product.price && /* @__PURE__ */ React2.createElement("span", { className: "text-sm font-bold", style: { color: primaryColor } }, typeof product.price === "number" ? `$${product.price}` : product.price)), product.description && /* @__PURE__ */ React2.createElement("p", { className: "text-[11px] text-slate-500 dark:text-white/50 line-clamp-2 leading-relaxed" }, product.description), /* @__PURE__ */ React2.createElement("div", { className: "mt-2 flex gap-2" }, /* @__PURE__ */ React2.createElement(
75
75
  "button",
76
76
  {
77
+ onClick: () => onAddToCart == null ? void 0 : onAddToCart(product),
77
78
  className: "flex-1 py-2 px-3 rounded-lg text-[11px] font-medium text-white shadow-sm hover:opacity-90 transition-opacity flex items-center justify-center gap-1.5",
78
79
  style: { background: primaryColor }
79
80
  },
@@ -92,8 +93,21 @@ function ProductCard({ product, primaryColor = "#6366f1" }) {
92
93
  }
93
94
 
94
95
  // src/components/ProductCarousel.tsx
95
- function ProductCarousel({ products, primaryColor = "#6366f1" }) {
96
+ function ProductCarousel({ products, primaryColor = "#6366f1", onAddToCart }) {
96
97
  const scrollRef = useRef(null);
98
+ const [canScroll, setCanScroll] = useState(false);
99
+ useEffect(() => {
100
+ const checkScroll = () => {
101
+ if (scrollRef.current) {
102
+ const hasOverflow = scrollRef.current.scrollWidth > scrollRef.current.clientWidth;
103
+ setCanScroll(hasOverflow);
104
+ }
105
+ };
106
+ checkScroll();
107
+ const observer = new ResizeObserver(checkScroll);
108
+ if (scrollRef.current) observer.observe(scrollRef.current);
109
+ return () => observer.disconnect();
110
+ }, [products]);
97
111
  const scroll = (direction) => {
98
112
  if (scrollRef.current) {
99
113
  const firstCard = scrollRef.current.firstElementChild;
@@ -105,7 +119,10 @@ function ProductCarousel({ products, primaryColor = "#6366f1" }) {
105
119
  }
106
120
  };
107
121
  if (!products || products.length === 0) return null;
108
- return /* @__PURE__ */ React3.createElement("div", { className: "relative w-full my-4 group/carousel" }, /* @__PURE__ */ React3.createElement("div", { className: "absolute -left-4 top-1/2 -translate-y-1/2 z-20 opacity-100 md:opacity-0 md:group-hover/carousel:opacity-100 transition-opacity" }, /* @__PURE__ */ React3.createElement(
122
+ if (products.length === 1) {
123
+ return /* @__PURE__ */ React3.createElement("div", { className: "my-4 w-[85%] sm:w-[48%] md:w-[31%] min-w-[200px] max-w-[300px] animate-fade-in-up" }, /* @__PURE__ */ React3.createElement(ProductCard, { product: products[0], primaryColor, onAddToCart }));
124
+ }
125
+ return /* @__PURE__ */ React3.createElement("div", { className: "relative w-full my-4 group/carousel animate-fade-in-up" }, canScroll && /* @__PURE__ */ React3.createElement(React3.Fragment, null, /* @__PURE__ */ React3.createElement("div", { className: "absolute -left-4 top-1/2 -translate-y-1/2 z-20 opacity-100 md:opacity-0 md:group-hover/carousel:opacity-100 transition-opacity" }, /* @__PURE__ */ React3.createElement(
109
126
  "button",
110
127
  {
111
128
  onClick: (e) => {
@@ -127,7 +144,7 @@ function ProductCarousel({ products, primaryColor = "#6366f1" }) {
127
144
  "aria-label": "Scroll right"
128
145
  },
129
146
  /* @__PURE__ */ React3.createElement(ChevronRight, { className: "w-5 h-5" })
130
- )), /* @__PURE__ */ React3.createElement(
147
+ ))), /* @__PURE__ */ React3.createElement(
131
148
  "div",
132
149
  {
133
150
  ref: scrollRef,
@@ -140,7 +157,7 @@ function ProductCarousel({ products, primaryColor = "#6366f1" }) {
140
157
  key: product.id,
141
158
  className: "snap-start flex-shrink-0 w-[85%] sm:w-[48%] md:w-[31%] min-w-[200px] max-w-[300px]"
142
159
  },
143
- /* @__PURE__ */ React3.createElement(ProductCard, { product, primaryColor })
160
+ /* @__PURE__ */ React3.createElement(ProductCard, { product, primaryColor, onAddToCart })
144
161
  ))
145
162
  ));
146
163
  }
@@ -151,7 +168,8 @@ function MessageBubble({
151
168
  sources,
152
169
  isStreaming = false,
153
170
  primaryColor = "#6366f1",
154
- accentColor = "#8b5cf6"
171
+ accentColor = "#8b5cf6",
172
+ onAddToCart
155
173
  }) {
156
174
  const isUser = message.role === "user";
157
175
  const [showSources, setShowSources] = React4.useState(false);
@@ -240,7 +258,14 @@ function MessageBubble({
240
258
  style: isUser ? { background: `linear-gradient(135deg, ${primaryColor}, ${accentColor})` } : {}
241
259
  },
242
260
  isStreaming && !message.content ? /* @__PURE__ */ React4.createElement("span", { className: "flex items-center gap-1 py-1" }, /* @__PURE__ */ React4.createElement("span", { className: "w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:0ms]" }), /* @__PURE__ */ React4.createElement("span", { className: "w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:150ms]" }), /* @__PURE__ */ React4.createElement("span", { className: "w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:300ms]" })) : /* @__PURE__ */ React4.createElement("div", { className: `prose prose-sm max-w-none ${isUser ? "prose-invert" : "dark:prose-invert"}` }, /* @__PURE__ */ React4.createElement(ReactMarkdown, { remarkPlugins: [remarkGfm] }, cleanContent || message.content), isStreaming && message.content && /* @__PURE__ */ React4.createElement("span", { className: "inline-block w-1.5 h-3.5 ml-1 bg-emerald-500 animate-pulse align-middle" }))
243
- ), !isUser && hasProducts && /* @__PURE__ */ React4.createElement("div", { className: "w-full mt-1" }, /* @__PURE__ */ React4.createElement(ProductCarousel, { products: allProducts, primaryColor })), !isUser && sources && sources.length > 0 && /* @__PURE__ */ React4.createElement("div", { className: "w-full" }, /* @__PURE__ */ React4.createElement(
261
+ ), !isUser && hasProducts && /* @__PURE__ */ React4.createElement("div", { className: "w-full mt-1" }, /* @__PURE__ */ React4.createElement(
262
+ ProductCarousel,
263
+ {
264
+ products: allProducts,
265
+ primaryColor,
266
+ onAddToCart
267
+ }
268
+ )), !isUser && sources && sources.length > 0 && /* @__PURE__ */ React4.createElement("div", { className: "w-full" }, /* @__PURE__ */ React4.createElement(
244
269
  "button",
245
270
  {
246
271
  onClick: () => setShowSources((s) => !s),
@@ -291,7 +316,7 @@ function useConfig() {
291
316
  }
292
317
 
293
318
  // src/hooks/useRagChat.ts
294
- import { useState as useState2, useCallback, useRef as useRef2, useEffect as useEffect2 } from "react";
319
+ import { useState as useState3, useCallback, useRef as useRef2, useEffect as useEffect3 } from "react";
295
320
 
296
321
  // src/hooks/useStoredMessages.ts
297
322
  import * as React6 from "react";
@@ -352,11 +377,11 @@ function useRagChat(projectId, options = {}) {
352
377
  } = options;
353
378
  const storageKey = `rag_chat_${projectId}`;
354
379
  const [messages, setMessages] = useStoredMessages(storageKey, persist);
355
- const [isLoading, setIsLoading] = useState2(false);
356
- const [error, setError] = useState2(null);
380
+ const [isLoading, setIsLoading] = useState3(false);
381
+ const [error, setError] = useState3(null);
357
382
  const lastInputRef = useRef2("");
358
383
  const messagesRef = useRef2(messages);
359
- useEffect2(() => {
384
+ useEffect3(() => {
360
385
  messagesRef.current = messages;
361
386
  }, [messages]);
362
387
  const sendMessage = useCallback(
@@ -505,21 +530,22 @@ function ChatWindow({
505
530
  onResetResize,
506
531
  isResized = false,
507
532
  onMaximize,
508
- isMaximized = false
533
+ isMaximized = false,
534
+ onAddToCart
509
535
  }) {
510
536
  var _a;
511
537
  const { ui, projectId } = useConfig();
512
- const [input, setInput] = useState3("");
513
- const [mounted, setMounted] = useState3(false);
538
+ const [input, setInput] = useState4("");
539
+ const [mounted, setMounted] = useState4(false);
514
540
  const bottomRef = useRef3(null);
515
541
  const inputRef = useRef3(null);
516
542
  const { messages, send, clear, isLoading, error } = useRagChat(projectId, {
517
543
  namespace: projectId
518
544
  });
519
- useEffect3(() => {
545
+ useEffect4(() => {
520
546
  setMounted(true);
521
547
  }, []);
522
- useEffect3(() => {
548
+ useEffect4(() => {
523
549
  var _a2;
524
550
  if (messages.length > 0 || isLoading) {
525
551
  (_a2 = bottomRef.current) == null ? void 0 : _a2.scrollIntoView({ behavior: "smooth" });
@@ -645,7 +671,8 @@ function ChatWindow({
645
671
  sources: msg.sources,
646
672
  isStreaming: isLoading && index === messages.length - 1 && msg.role === "assistant",
647
673
  primaryColor: ui.primaryColor,
648
- accentColor: ui.accentColor
674
+ accentColor: ui.accentColor,
675
+ onAddToCart
649
676
  }
650
677
  )), isLoading && ((_a = messages[messages.length - 1]) == null ? void 0 : _a.role) !== "assistant" && /* @__PURE__ */ React7.createElement(
651
678
  MessageBubble,
@@ -653,7 +680,8 @@ function ChatWindow({
653
680
  message: { role: "assistant", content: "" },
654
681
  isStreaming: true,
655
682
  primaryColor: ui.primaryColor,
656
- accentColor: ui.accentColor
683
+ accentColor: ui.accentColor,
684
+ onAddToCart
657
685
  }
658
686
  ), error && /* @__PURE__ */ React7.createElement("div", { className: "flex items-center gap-2 text-rose-500 dark:text-rose-400 text-sm bg-rose-50 dark:bg-rose-400/10 border border-rose-200 dark:border-rose-400/20 rounded-xl px-4 py-3" }, /* @__PURE__ */ React7.createElement(TriangleAlert, { className: "w-4 h-4 flex-shrink-0" }), /* @__PURE__ */ React7.createElement("span", null, error)), /* @__PURE__ */ React7.createElement("div", { ref: bottomRef })),
659
687
  /* @__PURE__ */ React7.createElement("div", { className: `px-4 pb-4 pt-2 border-t border-slate-200 dark:border-white/10 ${isGlass ? "bg-transparent" : "bg-white dark:bg-[#0f0f1a]"}` }, /* @__PURE__ */ React7.createElement("div", { className: `flex items-end gap-2 bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 p-2 focus-within:border-slate-300 dark:focus-within:border-white/20 transition-all ${ui.borderRadius === "full" ? "rounded-3xl" : "rounded-2xl"}` }, /* @__PURE__ */ React7.createElement(
@@ -687,14 +715,14 @@ function ChatWindow({
687
715
 
688
716
  // src/components/ChatWidget.tsx
689
717
  var DEFAULT_DIMENSIONS = { width: 380, height: 600 };
690
- function ChatWidget({ position = "bottom-right" }) {
718
+ function ChatWidget({ position = "bottom-right", onAddToCart }) {
691
719
  const { ui } = useConfig();
692
- const [isOpen, setIsOpen] = useState4(false);
693
- const [hasUnread, setHasUnread] = useState4(false);
694
- const [dimensions, setDimensions] = useState4(DEFAULT_DIMENSIONS);
695
- const [isResizing, setIsResizing] = useState4(false);
696
- const [isMaximized, setIsMaximized] = useState4(false);
697
- const [prevDimensions, setPrevDimensions] = useState4(DEFAULT_DIMENSIONS);
720
+ const [isOpen, setIsOpen] = useState5(false);
721
+ const [hasUnread, setHasUnread] = useState5(false);
722
+ const [dimensions, setDimensions] = useState5(DEFAULT_DIMENSIONS);
723
+ const [isResizing, setIsResizing] = useState5(false);
724
+ const [isMaximized, setIsMaximized] = useState5(false);
725
+ const [prevDimensions, setPrevDimensions] = useState5(DEFAULT_DIMENSIONS);
698
726
  if (ui.showWidget === false) return null;
699
727
  const positionClass = position === "bottom-left" ? "bottom-6 left-6" : "bottom-6 right-6";
700
728
  const windowPositionClass = position === "bottom-left" ? "bottom-20 left-6" : "bottom-20 right-6";
@@ -764,7 +792,8 @@ function ChatWidget({ position = "bottom-right" }) {
764
792
  onResetResize: ui.allowResize !== false ? handleResetResize : void 0,
765
793
  isResized,
766
794
  onMaximize: ui.allowResize !== false ? handleMaximize : void 0,
767
- isMaximized
795
+ isMaximized,
796
+ onAddToCart
768
797
  }
769
798
  ),
770
799
  /* @__PURE__ */ React8.createElement(
@@ -800,12 +829,12 @@ function ChatWidget({ position = "bottom-right" }) {
800
829
  }
801
830
 
802
831
  // src/components/DocumentUpload.tsx
803
- import React9, { useState as useState5, useRef as useRef4 } from "react";
832
+ import React9, { useState as useState6, useRef as useRef4 } from "react";
804
833
  import { Upload, File, X as X3, CheckCircle, AlertCircle, Loader2 } from "lucide-react";
805
834
  function DocumentUpload({ namespace, onUploadComplete, className = "" }) {
806
835
  const { ui } = useConfig();
807
- const [fileStates, setFileStates] = useState5([]);
808
- const [isDragging, setIsDragging] = useState5(false);
836
+ const [fileStates, setFileStates] = useState6([]);
837
+ const [isDragging, setIsDragging] = useState6(false);
809
838
  const fileInputRef = useRef4(null);
810
839
  const addFiles = (files) => {
811
840
  const newStates = files.map((file) => ({ file, status: "idle" }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retrivora-ai/rag-engine",
3
- "version": "1.1.8",
3
+ "version": "1.2.0",
4
4
  "description": "Retrivora AI is a plug-and-play AI engine for RAG chat experiences — generic vector DB + LLM provider, embeddable or standalone.",
5
5
  "author": "Abhinav Alkuchi",
6
6
  "license": "MIT",
@@ -8,11 +8,13 @@ import { useConfig } from './ConfigProvider';
8
8
  interface ChatWidgetProps {
9
9
  /** Position of the floating button. Defaults to bottom-right. */
10
10
  position?: 'bottom-right' | 'bottom-left';
11
+ /** Called when the user clicks 'Add to Cart' on a product */
12
+ onAddToCart?: (product: any) => void;
11
13
  }
12
14
 
13
15
  const DEFAULT_DIMENSIONS = { width: 380, height: 600 };
14
16
 
15
- export function ChatWidget({ position = 'bottom-right' }: ChatWidgetProps) {
17
+ export function ChatWidget({ position = 'bottom-right', onAddToCart }: ChatWidgetProps) {
16
18
  const { ui } = useConfig();
17
19
  const [isOpen, setIsOpen] = useState(false);
18
20
  const [hasUnread, setHasUnread] = useState(false);
@@ -111,6 +113,7 @@ export function ChatWidget({ position = 'bottom-right' }: ChatWidgetProps) {
111
113
  isResized={isResized}
112
114
  onMaximize={ui.allowResize !== false ? handleMaximize : undefined}
113
115
  isMaximized={isMaximized}
116
+ onAddToCart={onAddToCart}
114
117
  />
115
118
  {/* Pointer (Tail) */}
116
119
  <div
@@ -36,6 +36,8 @@ interface ChatWindowProps {
36
36
  onMaximize?: () => void;
37
37
  /** Whether the window is currently maximized */
38
38
  isMaximized?: boolean;
39
+ /** Called when the user clicks 'Add to Cart' on a product */
40
+ onAddToCart?: (product: any) => void;
39
41
  }
40
42
 
41
43
  export function ChatWindow({
@@ -47,7 +49,8 @@ export function ChatWindow({
47
49
  onResetResize,
48
50
  isResized = false,
49
51
  onMaximize,
50
- isMaximized = false
52
+ isMaximized = false,
53
+ onAddToCart
51
54
  }: ChatWindowProps) {
52
55
  const { ui, projectId } = useConfig();
53
56
  const [input, setInput] = useState('');
@@ -233,6 +236,7 @@ export function ChatWindow({
233
236
  isStreaming={isLoading && index === messages.length - 1 && msg.role === 'assistant'}
234
237
  primaryColor={ui.primaryColor}
235
238
  accentColor={ui.accentColor}
239
+ onAddToCart={onAddToCart}
236
240
  />
237
241
  ))
238
242
  )}
@@ -244,6 +248,7 @@ export function ChatWindow({
244
248
  isStreaming
245
249
  primaryColor={ui.primaryColor}
246
250
  accentColor={ui.accentColor}
251
+ onAddToCart={onAddToCart}
247
252
  />
248
253
  )}
249
254
 
@@ -16,6 +16,7 @@ interface MessageBubbleProps {
16
16
  isStreaming?: boolean;
17
17
  primaryColor?: string;
18
18
  accentColor?: string;
19
+ onAddToCart?: (product: Product) => void;
19
20
  }
20
21
 
21
22
  export function MessageBubble({
@@ -24,6 +25,7 @@ export function MessageBubble({
24
25
  isStreaming = false,
25
26
  primaryColor = '#6366f1',
26
27
  accentColor = '#8b5cf6',
28
+ onAddToCart,
27
29
  }: MessageBubbleProps) {
28
30
  const isUser = message.role === 'user';
29
31
  const [showSources, setShowSources] = React.useState(false);
@@ -31,19 +33,19 @@ export function MessageBubble({
31
33
  // Helper to extract image from metadata or object
32
34
  const resolveImage = (data: Record<string, unknown>): string | undefined => {
33
35
  if (!data) return undefined;
34
-
36
+
35
37
  // Check single string fields
36
38
  const singleFields = ['image', 'img', 'thumbnail'];
37
39
  for (const field of singleFields) {
38
40
  const val = data[field];
39
41
  if (typeof val === 'string' && val) return val;
40
42
  }
41
-
43
+
42
44
  // Check images array
43
45
  if (Array.isArray(data.images) && data.images.length > 0) {
44
46
  if (typeof data.images[0] === 'string') return data.images[0];
45
47
  }
46
-
48
+
47
49
  return undefined;
48
50
  };
49
51
 
@@ -72,7 +74,7 @@ export function MessageBubble({
72
74
  // Extract products from content (structured JSON)
73
75
  const { productsFromContent, cleanContent } = React.useMemo(() => {
74
76
  if (isUser) return { productsFromContent: [], cleanContent: message.content };
75
-
77
+
76
78
  const jsonRegex = /```json\s*([\s\S]*?)\s*```/g;
77
79
  const products: Product[] = [];
78
80
  let content = message.content;
@@ -133,9 +135,8 @@ export function MessageBubble({
133
135
  <div className={`flex gap-3 ${isUser ? 'flex-row-reverse' : 'flex-row'} items-start`}>
134
136
  {/* Avatar */}
135
137
  <div
136
- className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center shadow-lg ${
137
- isUser ? 'text-white' : 'bg-slate-100 dark:bg-white/10 text-slate-700 dark:text-white/80'
138
- }`}
138
+ className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center shadow-lg ${isUser ? 'text-white' : 'bg-slate-100 dark:bg-white/10 text-slate-700 dark:text-white/80'
139
+ }`}
139
140
  style={isUser ? { background: `linear-gradient(135deg, ${primaryColor}, ${accentColor})` } : {}}
140
141
  >
141
142
  {isUser
@@ -147,11 +148,10 @@ export function MessageBubble({
147
148
  <div className={`flex flex-col gap-1 max-w-[90%] ${isUser ? 'items-end' : 'items-start'}`}>
148
149
  {/* Bubble */}
149
150
  <div
150
- className={`relative px-4 py-3 rounded-2xl text-sm leading-relaxed shadow-sm dark:shadow-lg ${
151
- isUser
151
+ className={`relative px-4 py-3 rounded-2xl text-sm leading-relaxed shadow-sm dark:shadow-lg ${isUser
152
152
  ? 'text-white rounded-tr-sm'
153
153
  : 'bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 text-slate-800 dark:text-white/90 rounded-tl-sm'
154
- }`}
154
+ }`}
155
155
  style={
156
156
  isUser
157
157
  ? { background: `linear-gradient(135deg, ${primaryColor}, ${accentColor})` }
@@ -169,7 +169,7 @@ export function MessageBubble({
169
169
  <ReactMarkdown remarkPlugins={[remarkGfm]}>
170
170
  {cleanContent || message.content}
171
171
  </ReactMarkdown>
172
-
172
+
173
173
  {isStreaming && message.content && (
174
174
  <span className="inline-block w-1.5 h-3.5 ml-1 bg-emerald-500 animate-pulse align-middle" />
175
175
  )}
@@ -180,7 +180,11 @@ export function MessageBubble({
180
180
  {/* Product Carousel (Outside the bubble) */}
181
181
  {!isUser && hasProducts && (
182
182
  <div className="w-full mt-1">
183
- <ProductCarousel products={allProducts} primaryColor={primaryColor} />
183
+ <ProductCarousel
184
+ products={allProducts}
185
+ primaryColor={primaryColor}
186
+ onAddToCart={onAddToCart}
187
+ />
184
188
  </div>
185
189
  )}
186
190
 
@@ -17,9 +17,10 @@ export interface Product {
17
17
  interface ProductCardProps {
18
18
  product: Product;
19
19
  primaryColor?: string;
20
+ onAddToCart?: (product: Product) => void;
20
21
  }
21
22
 
22
- export function ProductCard({ product, primaryColor = '#6366f1' }: ProductCardProps) {
23
+ export function ProductCard({ product, primaryColor = '#6366f1', onAddToCart }: ProductCardProps) {
23
24
  return (
24
25
  <div className="flex-shrink-0 w-full bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-xl overflow-hidden shadow-sm hover:shadow-md transition-all duration-300 group">
25
26
  {/* Product Image */}
@@ -67,6 +68,7 @@ export function ProductCard({ product, primaryColor = '#6366f1' }: ProductCardPr
67
68
 
68
69
  <div className="mt-2 flex gap-2">
69
70
  <button
71
+ onClick={() => onAddToCart?.(product)}
70
72
  className="flex-1 py-2 px-3 rounded-lg text-[11px] font-medium text-white shadow-sm hover:opacity-90 transition-opacity flex items-center justify-center gap-1.5"
71
73
  style={{ background: primaryColor }}
72
74
  >
@@ -1,16 +1,36 @@
1
1
  'use client';
2
2
 
3
- import React, { useRef } from 'react';
3
+ import React, { useRef, useState, useEffect } from 'react';
4
4
  import { ChevronLeft, ChevronRight } from 'lucide-react';
5
5
  import { Product, ProductCard } from './ProductCard';
6
6
 
7
7
  interface ProductCarouselProps {
8
8
  products: Product[];
9
9
  primaryColor?: string;
10
+ onAddToCart?: (product: Product) => void;
10
11
  }
11
12
 
12
- export function ProductCarousel({ products, primaryColor = '#6366f1' }: ProductCarouselProps) {
13
+ export function ProductCarousel({ products, primaryColor = '#6366f1', onAddToCart }: ProductCarouselProps) {
13
14
  const scrollRef = useRef<HTMLDivElement>(null);
15
+ const [canScroll, setCanScroll] = useState(false);
16
+
17
+ useEffect(() => {
18
+ const checkScroll = () => {
19
+ if (scrollRef.current) {
20
+ const hasOverflow = scrollRef.current.scrollWidth > scrollRef.current.clientWidth;
21
+ setCanScroll(hasOverflow);
22
+ }
23
+ };
24
+
25
+ // Initial check
26
+ checkScroll();
27
+
28
+ // Set up ResizeObserver to handle chat window resizing
29
+ const observer = new ResizeObserver(checkScroll);
30
+ if (scrollRef.current) observer.observe(scrollRef.current);
31
+
32
+ return () => observer.disconnect();
33
+ }, [products]);
14
34
 
15
35
  const scroll = (direction: 'left' | 'right') => {
16
36
  if (scrollRef.current) {
@@ -26,28 +46,41 @@ export function ProductCarousel({ products, primaryColor = '#6366f1' }: ProductC
26
46
 
27
47
  if (!products || products.length === 0) return null;
28
48
 
29
- return (
30
- <div className="relative w-full my-4 group/carousel">
31
- {/* Scroll Buttons */}
32
- <div className="absolute -left-4 top-1/2 -translate-y-1/2 z-20 opacity-100 md:opacity-0 md:group-hover/carousel:opacity-100 transition-opacity">
33
- <button
34
- onClick={(e) => { e.stopPropagation(); scroll('left'); }}
35
- className="p-2 rounded-full bg-white dark:bg-slate-800 shadow-xl border border-slate-200 dark:border-white/10 text-slate-600 dark:text-white/80 hover:text-indigo-500 dark:hover:text-indigo-400 transition-all hover:scale-110 active:scale-95"
36
- aria-label="Scroll left"
37
- >
38
- <ChevronLeft className="w-5 h-5" />
39
- </button>
49
+ // Single product: No carousel needed
50
+ if (products.length === 1) {
51
+ return (
52
+ <div className="my-4 w-[85%] sm:w-[48%] md:w-[31%] min-w-[200px] max-w-[300px] animate-fade-in-up">
53
+ <ProductCard product={products[0]} primaryColor={primaryColor} onAddToCart={onAddToCart} />
40
54
  </div>
55
+ );
56
+ }
41
57
 
42
- <div className="absolute -right-4 top-1/2 -translate-y-1/2 z-20 opacity-100 md:opacity-0 md:group-hover/carousel:opacity-100 transition-opacity">
43
- <button
44
- onClick={(e) => { e.stopPropagation(); scroll('right'); }}
45
- className="p-2 rounded-full bg-white dark:bg-slate-800 shadow-xl border border-slate-200 dark:border-white/10 text-slate-600 dark:text-white/80 hover:text-indigo-500 dark:hover:text-indigo-400 transition-all hover:scale-110 active:scale-95"
46
- aria-label="Scroll right"
47
- >
48
- <ChevronRight className="w-5 h-5" />
49
- </button>
50
- </div>
58
+ return (
59
+ <div className="relative w-full my-4 group/carousel animate-fade-in-up">
60
+ {/* Scroll Buttons - Only show if there is overflow */}
61
+ {canScroll && (
62
+ <>
63
+ <div className="absolute -left-4 top-1/2 -translate-y-1/2 z-20 opacity-100 md:opacity-0 md:group-hover/carousel:opacity-100 transition-opacity">
64
+ <button
65
+ onClick={(e) => { e.stopPropagation(); scroll('left'); }}
66
+ className="p-2 rounded-full bg-white dark:bg-slate-800 shadow-xl border border-slate-200 dark:border-white/10 text-slate-600 dark:text-white/80 hover:text-indigo-500 dark:hover:text-indigo-400 transition-all hover:scale-110 active:scale-95"
67
+ aria-label="Scroll left"
68
+ >
69
+ <ChevronLeft className="w-5 h-5" />
70
+ </button>
71
+ </div>
72
+
73
+ <div className="absolute -right-4 top-1/2 -translate-y-1/2 z-20 opacity-100 md:opacity-0 md:group-hover/carousel:opacity-100 transition-opacity">
74
+ <button
75
+ onClick={(e) => { e.stopPropagation(); scroll('right'); }}
76
+ className="p-2 rounded-full bg-white dark:bg-slate-800 shadow-xl border border-slate-200 dark:border-white/10 text-slate-600 dark:text-white/80 hover:text-indigo-500 dark:hover:text-indigo-400 transition-all hover:scale-110 active:scale-95"
77
+ aria-label="Scroll right"
78
+ >
79
+ <ChevronRight className="w-5 h-5" />
80
+ </button>
81
+ </div>
82
+ </>
83
+ )}
51
84
 
52
85
  {/* Carousel Container */}
53
86
  <div
@@ -60,7 +93,7 @@ export function ProductCarousel({ products, primaryColor = '#6366f1' }: ProductC
60
93
  key={product.id}
61
94
  className="snap-start flex-shrink-0 w-[85%] sm:w-[48%] md:w-[31%] min-w-[200px] max-w-[300px]"
62
95
  >
63
- <ProductCard product={product} primaryColor={primaryColor} />
96
+ <ProductCard product={product} primaryColor={primaryColor} onAddToCart={onAddToCart} />
64
97
  </div>
65
98
  ))}
66
99
  </div>