@retrivora-ai/rag-engine 1.1.8 → 1.1.9

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);
@@ -209,24 +227,6 @@ function MessageBubble({
209
227
  }
210
228
  return void 0;
211
229
  };
212
- const productsFromSources = import_react4.default.useMemo(() => {
213
- if (isUser || !sources) return [];
214
- return sources.filter((s) => {
215
- const m = s.metadata || {};
216
- return m.price || m.image || m.img || m.thumbnail || m.images || m.brand || m.type === "product";
217
- }).map((s) => {
218
- const m = s.metadata || {};
219
- return {
220
- id: s.id,
221
- name: m.name || m.title || s.content.split("\n")[0] || "Unknown Product",
222
- brand: m.brand,
223
- price: m.price,
224
- image: resolveImage(m),
225
- link: m.link,
226
- description: s.content
227
- };
228
- });
229
- }, [sources, isUser]);
230
230
  const { productsFromContent, cleanContent } = import_react4.default.useMemo(() => {
231
231
  if (isUser) return { productsFromContent: [], cleanContent: message.content };
232
232
  const jsonRegex = /```json\s*([\s\S]*?)\s*```/g;
@@ -250,23 +250,7 @@ function MessageBubble({
250
250
  }
251
251
  return { productsFromContent: products, cleanContent: content.trim() };
252
252
  }, [message.content, isUser]);
253
- const allProducts = import_react4.default.useMemo(() => {
254
- if (productsFromContent.length > 0) return productsFromContent;
255
- const uniqueProducts = [];
256
- const seenIds = /* @__PURE__ */ new Set();
257
- const contentLower = message.content.toLowerCase();
258
- for (const p of productsFromSources) {
259
- const identifier = p.id || p.name;
260
- if (seenIds.has(identifier)) continue;
261
- const nameMatch = contentLower.includes(p.name.toLowerCase());
262
- const brandMatch = p.brand ? contentLower.includes(p.brand.toLowerCase()) : false;
263
- if (nameMatch || brandMatch) {
264
- seenIds.add(identifier);
265
- uniqueProducts.push(p);
266
- }
267
- }
268
- return uniqueProducts;
269
- }, [productsFromSources, productsFromContent, message.content]);
253
+ const allProducts = productsFromContent;
270
254
  const hasProducts = allProducts.length > 0;
271
255
  return /* @__PURE__ */ import_react4.default.createElement("div", { className: `flex gap-3 ${isUser ? "flex-row-reverse" : "flex-row"} items-start` }, /* @__PURE__ */ import_react4.default.createElement(
272
256
  "div",
@@ -282,7 +266,14 @@ function MessageBubble({
282
266
  style: isUser ? { background: `linear-gradient(135deg, ${primaryColor}, ${accentColor})` } : {}
283
267
  },
284
268
  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(
269
+ ), !isUser && hasProducts && /* @__PURE__ */ import_react4.default.createElement("div", { className: "w-full mt-1" }, /* @__PURE__ */ import_react4.default.createElement(
270
+ ProductCarousel,
271
+ {
272
+ products: allProducts,
273
+ primaryColor,
274
+ onAddToCart
275
+ }
276
+ )), !isUser && sources && sources.length > 0 && /* @__PURE__ */ import_react4.default.createElement("div", { className: "w-full" }, /* @__PURE__ */ import_react4.default.createElement(
286
277
  "button",
287
278
  {
288
279
  onClick: () => setShowSources((s) => !s),
@@ -563,7 +554,8 @@ function ChatWindow({
563
554
  onResetResize,
564
555
  isResized = false,
565
556
  onMaximize,
566
- isMaximized = false
557
+ isMaximized = false,
558
+ onAddToCart
567
559
  }) {
568
560
  var _a;
569
561
  const { ui, projectId } = useConfig();
@@ -703,7 +695,8 @@ function ChatWindow({
703
695
  sources: msg.sources,
704
696
  isStreaming: isLoading && index === messages.length - 1 && msg.role === "assistant",
705
697
  primaryColor: ui.primaryColor,
706
- accentColor: ui.accentColor
698
+ accentColor: ui.accentColor,
699
+ onAddToCart
707
700
  }
708
701
  )), isLoading && ((_a = messages[messages.length - 1]) == null ? void 0 : _a.role) !== "assistant" && /* @__PURE__ */ import_react7.default.createElement(
709
702
  MessageBubble,
@@ -711,7 +704,8 @@ function ChatWindow({
711
704
  message: { role: "assistant", content: "" },
712
705
  isStreaming: true,
713
706
  primaryColor: ui.primaryColor,
714
- accentColor: ui.accentColor
707
+ accentColor: ui.accentColor,
708
+ onAddToCart
715
709
  }
716
710
  ), 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
711
  /* @__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 +739,7 @@ function ChatWindow({
745
739
 
746
740
  // src/components/ChatWidget.tsx
747
741
  var DEFAULT_DIMENSIONS = { width: 380, height: 600 };
748
- function ChatWidget({ position = "bottom-right" }) {
742
+ function ChatWidget({ position = "bottom-right", onAddToCart }) {
749
743
  const { ui } = useConfig();
750
744
  const [isOpen, setIsOpen] = (0, import_react8.useState)(false);
751
745
  const [hasUnread, setHasUnread] = (0, import_react8.useState)(false);
@@ -822,7 +816,8 @@ function ChatWidget({ position = "bottom-right" }) {
822
816
  onResetResize: ui.allowResize !== false ? handleResetResize : void 0,
823
817
  isResized,
824
818
  onMaximize: ui.allowResize !== false ? handleMaximize : void 0,
825
- isMaximized
819
+ isMaximized,
820
+ onAddToCart
826
821
  }
827
822
  ),
828
823
  /* @__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);
@@ -167,24 +185,6 @@ function MessageBubble({
167
185
  }
168
186
  return void 0;
169
187
  };
170
- const productsFromSources = React4.useMemo(() => {
171
- if (isUser || !sources) return [];
172
- return sources.filter((s) => {
173
- const m = s.metadata || {};
174
- return m.price || m.image || m.img || m.thumbnail || m.images || m.brand || m.type === "product";
175
- }).map((s) => {
176
- const m = s.metadata || {};
177
- return {
178
- id: s.id,
179
- name: m.name || m.title || s.content.split("\n")[0] || "Unknown Product",
180
- brand: m.brand,
181
- price: m.price,
182
- image: resolveImage(m),
183
- link: m.link,
184
- description: s.content
185
- };
186
- });
187
- }, [sources, isUser]);
188
188
  const { productsFromContent, cleanContent } = React4.useMemo(() => {
189
189
  if (isUser) return { productsFromContent: [], cleanContent: message.content };
190
190
  const jsonRegex = /```json\s*([\s\S]*?)\s*```/g;
@@ -208,23 +208,7 @@ function MessageBubble({
208
208
  }
209
209
  return { productsFromContent: products, cleanContent: content.trim() };
210
210
  }, [message.content, isUser]);
211
- const allProducts = React4.useMemo(() => {
212
- if (productsFromContent.length > 0) return productsFromContent;
213
- const uniqueProducts = [];
214
- const seenIds = /* @__PURE__ */ new Set();
215
- const contentLower = message.content.toLowerCase();
216
- for (const p of productsFromSources) {
217
- const identifier = p.id || p.name;
218
- if (seenIds.has(identifier)) continue;
219
- const nameMatch = contentLower.includes(p.name.toLowerCase());
220
- const brandMatch = p.brand ? contentLower.includes(p.brand.toLowerCase()) : false;
221
- if (nameMatch || brandMatch) {
222
- seenIds.add(identifier);
223
- uniqueProducts.push(p);
224
- }
225
- }
226
- return uniqueProducts;
227
- }, [productsFromSources, productsFromContent, message.content]);
211
+ const allProducts = productsFromContent;
228
212
  const hasProducts = allProducts.length > 0;
229
213
  return /* @__PURE__ */ React4.createElement("div", { className: `flex gap-3 ${isUser ? "flex-row-reverse" : "flex-row"} items-start` }, /* @__PURE__ */ React4.createElement(
230
214
  "div",
@@ -240,7 +224,14 @@ function MessageBubble({
240
224
  style: isUser ? { background: `linear-gradient(135deg, ${primaryColor}, ${accentColor})` } : {}
241
225
  },
242
226
  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(
227
+ ), !isUser && hasProducts && /* @__PURE__ */ React4.createElement("div", { className: "w-full mt-1" }, /* @__PURE__ */ React4.createElement(
228
+ ProductCarousel,
229
+ {
230
+ products: allProducts,
231
+ primaryColor,
232
+ onAddToCart
233
+ }
234
+ )), !isUser && sources && sources.length > 0 && /* @__PURE__ */ React4.createElement("div", { className: "w-full" }, /* @__PURE__ */ React4.createElement(
244
235
  "button",
245
236
  {
246
237
  onClick: () => setShowSources((s) => !s),
@@ -291,7 +282,7 @@ function useConfig() {
291
282
  }
292
283
 
293
284
  // src/hooks/useRagChat.ts
294
- import { useState as useState2, useCallback, useRef as useRef2, useEffect as useEffect2 } from "react";
285
+ import { useState as useState3, useCallback, useRef as useRef2, useEffect as useEffect3 } from "react";
295
286
 
296
287
  // src/hooks/useStoredMessages.ts
297
288
  import * as React6 from "react";
@@ -352,11 +343,11 @@ function useRagChat(projectId, options = {}) {
352
343
  } = options;
353
344
  const storageKey = `rag_chat_${projectId}`;
354
345
  const [messages, setMessages] = useStoredMessages(storageKey, persist);
355
- const [isLoading, setIsLoading] = useState2(false);
356
- const [error, setError] = useState2(null);
346
+ const [isLoading, setIsLoading] = useState3(false);
347
+ const [error, setError] = useState3(null);
357
348
  const lastInputRef = useRef2("");
358
349
  const messagesRef = useRef2(messages);
359
- useEffect2(() => {
350
+ useEffect3(() => {
360
351
  messagesRef.current = messages;
361
352
  }, [messages]);
362
353
  const sendMessage = useCallback(
@@ -505,21 +496,22 @@ function ChatWindow({
505
496
  onResetResize,
506
497
  isResized = false,
507
498
  onMaximize,
508
- isMaximized = false
499
+ isMaximized = false,
500
+ onAddToCart
509
501
  }) {
510
502
  var _a;
511
503
  const { ui, projectId } = useConfig();
512
- const [input, setInput] = useState3("");
513
- const [mounted, setMounted] = useState3(false);
504
+ const [input, setInput] = useState4("");
505
+ const [mounted, setMounted] = useState4(false);
514
506
  const bottomRef = useRef3(null);
515
507
  const inputRef = useRef3(null);
516
508
  const { messages, send, clear, isLoading, error } = useRagChat(projectId, {
517
509
  namespace: projectId
518
510
  });
519
- useEffect3(() => {
511
+ useEffect4(() => {
520
512
  setMounted(true);
521
513
  }, []);
522
- useEffect3(() => {
514
+ useEffect4(() => {
523
515
  var _a2;
524
516
  if (messages.length > 0 || isLoading) {
525
517
  (_a2 = bottomRef.current) == null ? void 0 : _a2.scrollIntoView({ behavior: "smooth" });
@@ -645,7 +637,8 @@ function ChatWindow({
645
637
  sources: msg.sources,
646
638
  isStreaming: isLoading && index === messages.length - 1 && msg.role === "assistant",
647
639
  primaryColor: ui.primaryColor,
648
- accentColor: ui.accentColor
640
+ accentColor: ui.accentColor,
641
+ onAddToCart
649
642
  }
650
643
  )), isLoading && ((_a = messages[messages.length - 1]) == null ? void 0 : _a.role) !== "assistant" && /* @__PURE__ */ React7.createElement(
651
644
  MessageBubble,
@@ -653,7 +646,8 @@ function ChatWindow({
653
646
  message: { role: "assistant", content: "" },
654
647
  isStreaming: true,
655
648
  primaryColor: ui.primaryColor,
656
- accentColor: ui.accentColor
649
+ accentColor: ui.accentColor,
650
+ onAddToCart
657
651
  }
658
652
  ), 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
653
  /* @__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 +681,14 @@ function ChatWindow({
687
681
 
688
682
  // src/components/ChatWidget.tsx
689
683
  var DEFAULT_DIMENSIONS = { width: 380, height: 600 };
690
- function ChatWidget({ position = "bottom-right" }) {
684
+ function ChatWidget({ position = "bottom-right", onAddToCart }) {
691
685
  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);
686
+ const [isOpen, setIsOpen] = useState5(false);
687
+ const [hasUnread, setHasUnread] = useState5(false);
688
+ const [dimensions, setDimensions] = useState5(DEFAULT_DIMENSIONS);
689
+ const [isResizing, setIsResizing] = useState5(false);
690
+ const [isMaximized, setIsMaximized] = useState5(false);
691
+ const [prevDimensions, setPrevDimensions] = useState5(DEFAULT_DIMENSIONS);
698
692
  if (ui.showWidget === false) return null;
699
693
  const positionClass = position === "bottom-left" ? "bottom-6 left-6" : "bottom-6 right-6";
700
694
  const windowPositionClass = position === "bottom-left" ? "bottom-20 left-6" : "bottom-20 right-6";
@@ -764,7 +758,8 @@ function ChatWidget({ position = "bottom-right" }) {
764
758
  onResetResize: ui.allowResize !== false ? handleResetResize : void 0,
765
759
  isResized,
766
760
  onMaximize: ui.allowResize !== false ? handleMaximize : void 0,
767
- isMaximized
761
+ isMaximized,
762
+ onAddToCart
768
763
  }
769
764
  ),
770
765
  /* @__PURE__ */ React8.createElement(
@@ -800,12 +795,12 @@ function ChatWidget({ position = "bottom-right" }) {
800
795
  }
801
796
 
802
797
  // src/components/DocumentUpload.tsx
803
- import React9, { useState as useState5, useRef as useRef4 } from "react";
798
+ import React9, { useState as useState6, useRef as useRef4 } from "react";
804
799
  import { Upload, File, X as X3, CheckCircle, AlertCircle, Loader2 } from "lucide-react";
805
800
  function DocumentUpload({ namespace, onUploadComplete, className = "" }) {
806
801
  const { ui } = useConfig();
807
- const [fileStates, setFileStates] = useState5([]);
808
- const [isDragging, setIsDragging] = useState5(false);
802
+ const [fileStates, setFileStates] = useState6([]);
803
+ const [isDragging, setIsDragging] = useState6(false);
809
804
  const fileInputRef = useRef4(null);
810
805
  const addFiles = (files) => {
811
806
  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.1.9",
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);
@@ -47,28 +49,6 @@ export function MessageBubble({
47
49
  return undefined;
48
50
  };
49
51
 
50
- // Extract products from sources
51
- const productsFromSources = React.useMemo(() => {
52
- if (isUser || !sources) return [];
53
- return sources
54
- .filter(s => {
55
- const m = s.metadata || {};
56
- return m.price || m.image || m.img || m.thumbnail || m.images || m.brand || m.type === 'product';
57
- })
58
- .map(s => {
59
- const m = (s.metadata || {}) as Record<string, unknown>;
60
- return {
61
- id: s.id,
62
- name: (m.name || m.title || s.content.split('\n')[0] || 'Unknown Product') as string,
63
- brand: m.brand as string,
64
- price: m.price as string | number,
65
- image: resolveImage(m),
66
- link: m.link as string,
67
- description: s.content
68
- };
69
- });
70
- }, [sources, isUser]);
71
-
72
52
  // Extract products from content (structured JSON)
73
53
  const { productsFromContent, cleanContent } = React.useMemo(() => {
74
54
  if (isUser) return { productsFromContent: [], cleanContent: message.content };
@@ -100,33 +80,10 @@ export function MessageBubble({
100
80
  return { productsFromContent: products, cleanContent: content.trim() };
101
81
  }, [message.content, isUser]);
102
82
 
103
- const allProducts = React.useMemo(() => {
104
- // If the LLM explicitly provided products in a structured format, only show those.
105
- if (productsFromContent.length > 0) return productsFromContent;
106
-
107
- // Fallback: Show unique products from sources, but ONLY if they are mentioned in the message text.
108
- // This aligns the carousel with what the assistant is actually "displaying" in its response.
109
- const uniqueProducts: Product[] = [];
110
- const seenIds = new Set();
111
- const contentLower = message.content.toLowerCase();
112
-
113
- for (const p of productsFromSources) {
114
- const identifier = p.id || p.name;
115
- if (seenIds.has(identifier)) continue;
116
-
117
- // Check if the product name or brand is mentioned in the assistant's text
118
- const nameMatch = contentLower.includes(p.name.toLowerCase());
119
- const brandMatch = p.brand ? contentLower.includes(p.brand.toLowerCase()) : false;
120
-
121
- if (nameMatch || brandMatch) {
122
- seenIds.add(identifier);
123
- uniqueProducts.push(p);
124
- }
125
- }
83
+ const allProducts = productsFromContent;
126
84
 
127
- return uniqueProducts;
128
- }, [productsFromSources, productsFromContent, message.content]);
129
85
 
86
+ // Only surface product cards if the LLM explicitly provided them via structured JSON
130
87
  const hasProducts = allProducts.length > 0;
131
88
 
132
89
  return (
@@ -180,7 +137,11 @@ export function MessageBubble({
180
137
  {/* Product Carousel (Outside the bubble) */}
181
138
  {!isUser && hasProducts && (
182
139
  <div className="w-full mt-1">
183
- <ProductCarousel products={allProducts} primaryColor={primaryColor} />
140
+ <ProductCarousel
141
+ products={allProducts}
142
+ primaryColor={primaryColor}
143
+ onAddToCart={onAddToCart}
144
+ />
184
145
  </div>
185
146
  )}
186
147
 
@@ -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>