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