@retrivora-ai/rag-engine 1.1.7 → 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,19 +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
- for (const p of productsFromSources) {
258
- const identifier = p.id || p.name;
259
- if (!seenIds.has(identifier)) {
260
- seenIds.add(identifier);
261
- uniqueProducts.push(p);
262
- }
263
- }
264
- return uniqueProducts;
265
- }, [productsFromSources, productsFromContent]);
253
+ const allProducts = productsFromContent;
266
254
  const hasProducts = allProducts.length > 0;
267
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(
268
256
  "div",
@@ -278,7 +266,14 @@ function MessageBubble({
278
266
  style: isUser ? { background: `linear-gradient(135deg, ${primaryColor}, ${accentColor})` } : {}
279
267
  },
280
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" }))
281
- ), !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(
282
277
  "button",
283
278
  {
284
279
  onClick: () => setShowSources((s) => !s),
@@ -559,7 +554,8 @@ function ChatWindow({
559
554
  onResetResize,
560
555
  isResized = false,
561
556
  onMaximize,
562
- isMaximized = false
557
+ isMaximized = false,
558
+ onAddToCart
563
559
  }) {
564
560
  var _a;
565
561
  const { ui, projectId } = useConfig();
@@ -699,7 +695,8 @@ function ChatWindow({
699
695
  sources: msg.sources,
700
696
  isStreaming: isLoading && index === messages.length - 1 && msg.role === "assistant",
701
697
  primaryColor: ui.primaryColor,
702
- accentColor: ui.accentColor
698
+ accentColor: ui.accentColor,
699
+ onAddToCart
703
700
  }
704
701
  )), isLoading && ((_a = messages[messages.length - 1]) == null ? void 0 : _a.role) !== "assistant" && /* @__PURE__ */ import_react7.default.createElement(
705
702
  MessageBubble,
@@ -707,7 +704,8 @@ function ChatWindow({
707
704
  message: { role: "assistant", content: "" },
708
705
  isStreaming: true,
709
706
  primaryColor: ui.primaryColor,
710
- accentColor: ui.accentColor
707
+ accentColor: ui.accentColor,
708
+ onAddToCart
711
709
  }
712
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 })),
713
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(
@@ -741,7 +739,7 @@ function ChatWindow({
741
739
 
742
740
  // src/components/ChatWidget.tsx
743
741
  var DEFAULT_DIMENSIONS = { width: 380, height: 600 };
744
- function ChatWidget({ position = "bottom-right" }) {
742
+ function ChatWidget({ position = "bottom-right", onAddToCart }) {
745
743
  const { ui } = useConfig();
746
744
  const [isOpen, setIsOpen] = (0, import_react8.useState)(false);
747
745
  const [hasUnread, setHasUnread] = (0, import_react8.useState)(false);
@@ -818,7 +816,8 @@ function ChatWidget({ position = "bottom-right" }) {
818
816
  onResetResize: ui.allowResize !== false ? handleResetResize : void 0,
819
817
  isResized,
820
818
  onMaximize: ui.allowResize !== false ? handleMaximize : void 0,
821
- isMaximized
819
+ isMaximized,
820
+ onAddToCart
822
821
  }
823
822
  ),
824
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,19 +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
- for (const p of productsFromSources) {
216
- const identifier = p.id || p.name;
217
- if (!seenIds.has(identifier)) {
218
- seenIds.add(identifier);
219
- uniqueProducts.push(p);
220
- }
221
- }
222
- return uniqueProducts;
223
- }, [productsFromSources, productsFromContent]);
211
+ const allProducts = productsFromContent;
224
212
  const hasProducts = allProducts.length > 0;
225
213
  return /* @__PURE__ */ React4.createElement("div", { className: `flex gap-3 ${isUser ? "flex-row-reverse" : "flex-row"} items-start` }, /* @__PURE__ */ React4.createElement(
226
214
  "div",
@@ -236,7 +224,14 @@ function MessageBubble({
236
224
  style: isUser ? { background: `linear-gradient(135deg, ${primaryColor}, ${accentColor})` } : {}
237
225
  },
238
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" }))
239
- ), !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(
240
235
  "button",
241
236
  {
242
237
  onClick: () => setShowSources((s) => !s),
@@ -287,7 +282,7 @@ function useConfig() {
287
282
  }
288
283
 
289
284
  // src/hooks/useRagChat.ts
290
- 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";
291
286
 
292
287
  // src/hooks/useStoredMessages.ts
293
288
  import * as React6 from "react";
@@ -348,11 +343,11 @@ function useRagChat(projectId, options = {}) {
348
343
  } = options;
349
344
  const storageKey = `rag_chat_${projectId}`;
350
345
  const [messages, setMessages] = useStoredMessages(storageKey, persist);
351
- const [isLoading, setIsLoading] = useState2(false);
352
- const [error, setError] = useState2(null);
346
+ const [isLoading, setIsLoading] = useState3(false);
347
+ const [error, setError] = useState3(null);
353
348
  const lastInputRef = useRef2("");
354
349
  const messagesRef = useRef2(messages);
355
- useEffect2(() => {
350
+ useEffect3(() => {
356
351
  messagesRef.current = messages;
357
352
  }, [messages]);
358
353
  const sendMessage = useCallback(
@@ -501,21 +496,22 @@ function ChatWindow({
501
496
  onResetResize,
502
497
  isResized = false,
503
498
  onMaximize,
504
- isMaximized = false
499
+ isMaximized = false,
500
+ onAddToCart
505
501
  }) {
506
502
  var _a;
507
503
  const { ui, projectId } = useConfig();
508
- const [input, setInput] = useState3("");
509
- const [mounted, setMounted] = useState3(false);
504
+ const [input, setInput] = useState4("");
505
+ const [mounted, setMounted] = useState4(false);
510
506
  const bottomRef = useRef3(null);
511
507
  const inputRef = useRef3(null);
512
508
  const { messages, send, clear, isLoading, error } = useRagChat(projectId, {
513
509
  namespace: projectId
514
510
  });
515
- useEffect3(() => {
511
+ useEffect4(() => {
516
512
  setMounted(true);
517
513
  }, []);
518
- useEffect3(() => {
514
+ useEffect4(() => {
519
515
  var _a2;
520
516
  if (messages.length > 0 || isLoading) {
521
517
  (_a2 = bottomRef.current) == null ? void 0 : _a2.scrollIntoView({ behavior: "smooth" });
@@ -641,7 +637,8 @@ function ChatWindow({
641
637
  sources: msg.sources,
642
638
  isStreaming: isLoading && index === messages.length - 1 && msg.role === "assistant",
643
639
  primaryColor: ui.primaryColor,
644
- accentColor: ui.accentColor
640
+ accentColor: ui.accentColor,
641
+ onAddToCart
645
642
  }
646
643
  )), isLoading && ((_a = messages[messages.length - 1]) == null ? void 0 : _a.role) !== "assistant" && /* @__PURE__ */ React7.createElement(
647
644
  MessageBubble,
@@ -649,7 +646,8 @@ function ChatWindow({
649
646
  message: { role: "assistant", content: "" },
650
647
  isStreaming: true,
651
648
  primaryColor: ui.primaryColor,
652
- accentColor: ui.accentColor
649
+ accentColor: ui.accentColor,
650
+ onAddToCart
653
651
  }
654
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 })),
655
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(
@@ -683,14 +681,14 @@ function ChatWindow({
683
681
 
684
682
  // src/components/ChatWidget.tsx
685
683
  var DEFAULT_DIMENSIONS = { width: 380, height: 600 };
686
- function ChatWidget({ position = "bottom-right" }) {
684
+ function ChatWidget({ position = "bottom-right", onAddToCart }) {
687
685
  const { ui } = useConfig();
688
- const [isOpen, setIsOpen] = useState4(false);
689
- const [hasUnread, setHasUnread] = useState4(false);
690
- const [dimensions, setDimensions] = useState4(DEFAULT_DIMENSIONS);
691
- const [isResizing, setIsResizing] = useState4(false);
692
- const [isMaximized, setIsMaximized] = useState4(false);
693
- 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);
694
692
  if (ui.showWidget === false) return null;
695
693
  const positionClass = position === "bottom-left" ? "bottom-6 left-6" : "bottom-6 right-6";
696
694
  const windowPositionClass = position === "bottom-left" ? "bottom-20 left-6" : "bottom-20 right-6";
@@ -760,7 +758,8 @@ function ChatWidget({ position = "bottom-right" }) {
760
758
  onResetResize: ui.allowResize !== false ? handleResetResize : void 0,
761
759
  isResized,
762
760
  onMaximize: ui.allowResize !== false ? handleMaximize : void 0,
763
- isMaximized
761
+ isMaximized,
762
+ onAddToCart
764
763
  }
765
764
  ),
766
765
  /* @__PURE__ */ React8.createElement(
@@ -796,12 +795,12 @@ function ChatWidget({ position = "bottom-right" }) {
796
795
  }
797
796
 
798
797
  // src/components/DocumentUpload.tsx
799
- import React9, { useState as useState5, useRef as useRef4 } from "react";
798
+ import React9, { useState as useState6, useRef as useRef4 } from "react";
800
799
  import { Upload, File, X as X3, CheckCircle, AlertCircle, Loader2 } from "lucide-react";
801
800
  function DocumentUpload({ namespace, onUploadComplete, className = "" }) {
802
801
  const { ui } = useConfig();
803
- const [fileStates, setFileStates] = useState5([]);
804
- const [isDragging, setIsDragging] = useState5(false);
802
+ const [fileStates, setFileStates] = useState6([]);
803
+ const [isDragging, setIsDragging] = useState6(false);
805
804
  const fileInputRef = useRef4(null);
806
805
  const addFiles = (files) => {
807
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.7",
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",
@@ -75,6 +75,55 @@ body {
75
75
  }
76
76
 
77
77
  /* ─── Prose Overrides (markdown in chat) ───────────────────── */
78
+ .prose {
79
+ font-size: 0.875rem;
80
+ line-height: 1.5;
81
+ }
82
+
83
+ .prose strong {
84
+ font-weight: 700;
85
+ color: inherit;
86
+ }
87
+
88
+ .prose em {
89
+ font-style: italic;
90
+ }
91
+
92
+ .prose p {
93
+ margin: 0.5em 0;
94
+ }
95
+
96
+ .prose ul, .prose ol {
97
+ padding-left: 1.5em;
98
+ margin: 0.5em 0;
99
+ }
100
+
101
+ .prose li {
102
+ margin: 0.25em 0;
103
+ }
104
+
105
+ .prose table {
106
+ width: 100%;
107
+ border-collapse: collapse;
108
+ margin: 1em 0;
109
+ font-size: 0.8rem;
110
+ }
111
+
112
+ .prose th, .prose td {
113
+ border: 1px solid var(--border);
114
+ padding: 0.5rem;
115
+ text-align: left;
116
+ }
117
+
118
+ .prose th {
119
+ background: rgba(0, 0, 0, 0.05);
120
+ font-weight: 600;
121
+ }
122
+
123
+ .dark .prose th {
124
+ background: rgba(255, 255, 255, 0.05);
125
+ }
126
+
78
127
  .prose-invert {
79
128
  color: inherit;
80
129
  }
@@ -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,26 +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
- // This ensures we only show what the assistant actually "displayed" as a result.
106
- if (productsFromContent.length > 0) return productsFromContent;
107
-
108
- // Fallback: If no structured products, show unique products from sources.
109
- const uniqueProducts: Product[] = [];
110
- const seenIds = new Set();
111
-
112
- for (const p of productsFromSources) {
113
- const identifier = p.id || p.name;
114
- if (!seenIds.has(identifier)) {
115
- seenIds.add(identifier);
116
- uniqueProducts.push(p);
117
- }
118
- }
83
+ const allProducts = productsFromContent;
119
84
 
120
- return uniqueProducts;
121
- }, [productsFromSources, productsFromContent]);
122
85
 
86
+ // Only surface product cards if the LLM explicitly provided them via structured JSON
123
87
  const hasProducts = allProducts.length > 0;
124
88
 
125
89
  return (
@@ -173,7 +137,11 @@ export function MessageBubble({
173
137
  {/* Product Carousel (Outside the bubble) */}
174
138
  {!isUser && hasProducts && (
175
139
  <div className="w-full mt-1">
176
- <ProductCarousel products={allProducts} primaryColor={primaryColor} />
140
+ <ProductCarousel
141
+ products={allProducts}
142
+ primaryColor={primaryColor}
143
+ onAddToCart={onAddToCart}
144
+ />
177
145
  </div>
178
146
  )}
179
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>