@retrivora-ai/rag-engine 1.1.8 → 1.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +18 -3
- package/dist/index.d.ts +18 -3
- package/dist/index.js +42 -47
- package/dist/index.mjs +62 -67
- package/package.json +1 -1
- package/src/components/ChatWidget.tsx +4 -1
- package/src/components/ChatWindow.tsx +6 -1
- package/src/components/MessageBubble.tsx +9 -48
- 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);
|
|
@@ -209,24 +227,6 @@ function MessageBubble({
|
|
|
209
227
|
}
|
|
210
228
|
return void 0;
|
|
211
229
|
};
|
|
212
|
-
const productsFromSources = import_react4.default.useMemo(() => {
|
|
213
|
-
if (isUser || !sources) return [];
|
|
214
|
-
return sources.filter((s) => {
|
|
215
|
-
const m = s.metadata || {};
|
|
216
|
-
return m.price || m.image || m.img || m.thumbnail || m.images || m.brand || m.type === "product";
|
|
217
|
-
}).map((s) => {
|
|
218
|
-
const m = s.metadata || {};
|
|
219
|
-
return {
|
|
220
|
-
id: s.id,
|
|
221
|
-
name: m.name || m.title || s.content.split("\n")[0] || "Unknown Product",
|
|
222
|
-
brand: m.brand,
|
|
223
|
-
price: m.price,
|
|
224
|
-
image: resolveImage(m),
|
|
225
|
-
link: m.link,
|
|
226
|
-
description: s.content
|
|
227
|
-
};
|
|
228
|
-
});
|
|
229
|
-
}, [sources, isUser]);
|
|
230
230
|
const { productsFromContent, cleanContent } = import_react4.default.useMemo(() => {
|
|
231
231
|
if (isUser) return { productsFromContent: [], cleanContent: message.content };
|
|
232
232
|
const jsonRegex = /```json\s*([\s\S]*?)\s*```/g;
|
|
@@ -250,23 +250,7 @@ function MessageBubble({
|
|
|
250
250
|
}
|
|
251
251
|
return { productsFromContent: products, cleanContent: content.trim() };
|
|
252
252
|
}, [message.content, isUser]);
|
|
253
|
-
const allProducts =
|
|
254
|
-
if (productsFromContent.length > 0) return productsFromContent;
|
|
255
|
-
const uniqueProducts = [];
|
|
256
|
-
const seenIds = /* @__PURE__ */ new Set();
|
|
257
|
-
const contentLower = message.content.toLowerCase();
|
|
258
|
-
for (const p of productsFromSources) {
|
|
259
|
-
const identifier = p.id || p.name;
|
|
260
|
-
if (seenIds.has(identifier)) continue;
|
|
261
|
-
const nameMatch = contentLower.includes(p.name.toLowerCase());
|
|
262
|
-
const brandMatch = p.brand ? contentLower.includes(p.brand.toLowerCase()) : false;
|
|
263
|
-
if (nameMatch || brandMatch) {
|
|
264
|
-
seenIds.add(identifier);
|
|
265
|
-
uniqueProducts.push(p);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
return uniqueProducts;
|
|
269
|
-
}, [productsFromSources, productsFromContent, message.content]);
|
|
253
|
+
const allProducts = productsFromContent;
|
|
270
254
|
const hasProducts = allProducts.length > 0;
|
|
271
255
|
return /* @__PURE__ */ import_react4.default.createElement("div", { className: `flex gap-3 ${isUser ? "flex-row-reverse" : "flex-row"} items-start` }, /* @__PURE__ */ import_react4.default.createElement(
|
|
272
256
|
"div",
|
|
@@ -282,7 +266,14 @@ function MessageBubble({
|
|
|
282
266
|
style: isUser ? { background: `linear-gradient(135deg, ${primaryColor}, ${accentColor})` } : {}
|
|
283
267
|
},
|
|
284
268
|
isStreaming && !message.content ? /* @__PURE__ */ import_react4.default.createElement("span", { className: "flex items-center gap-1 py-1" }, /* @__PURE__ */ import_react4.default.createElement("span", { className: "w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:0ms]" }), /* @__PURE__ */ import_react4.default.createElement("span", { className: "w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:150ms]" }), /* @__PURE__ */ import_react4.default.createElement("span", { className: "w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:300ms]" })) : /* @__PURE__ */ import_react4.default.createElement("div", { className: `prose prose-sm max-w-none ${isUser ? "prose-invert" : "dark:prose-invert"}` }, /* @__PURE__ */ import_react4.default.createElement(import_react_markdown.default, { remarkPlugins: [import_remark_gfm.default] }, cleanContent || message.content), isStreaming && message.content && /* @__PURE__ */ import_react4.default.createElement("span", { className: "inline-block w-1.5 h-3.5 ml-1 bg-emerald-500 animate-pulse align-middle" }))
|
|
285
|
-
), !isUser && hasProducts && /* @__PURE__ */ import_react4.default.createElement("div", { className: "w-full mt-1" }, /* @__PURE__ */ import_react4.default.createElement(
|
|
269
|
+
), !isUser && hasProducts && /* @__PURE__ */ import_react4.default.createElement("div", { className: "w-full mt-1" }, /* @__PURE__ */ import_react4.default.createElement(
|
|
270
|
+
ProductCarousel,
|
|
271
|
+
{
|
|
272
|
+
products: allProducts,
|
|
273
|
+
primaryColor,
|
|
274
|
+
onAddToCart
|
|
275
|
+
}
|
|
276
|
+
)), !isUser && sources && sources.length > 0 && /* @__PURE__ */ import_react4.default.createElement("div", { className: "w-full" }, /* @__PURE__ */ import_react4.default.createElement(
|
|
286
277
|
"button",
|
|
287
278
|
{
|
|
288
279
|
onClick: () => setShowSources((s) => !s),
|
|
@@ -563,7 +554,8 @@ function ChatWindow({
|
|
|
563
554
|
onResetResize,
|
|
564
555
|
isResized = false,
|
|
565
556
|
onMaximize,
|
|
566
|
-
isMaximized = false
|
|
557
|
+
isMaximized = false,
|
|
558
|
+
onAddToCart
|
|
567
559
|
}) {
|
|
568
560
|
var _a;
|
|
569
561
|
const { ui, projectId } = useConfig();
|
|
@@ -703,7 +695,8 @@ function ChatWindow({
|
|
|
703
695
|
sources: msg.sources,
|
|
704
696
|
isStreaming: isLoading && index === messages.length - 1 && msg.role === "assistant",
|
|
705
697
|
primaryColor: ui.primaryColor,
|
|
706
|
-
accentColor: ui.accentColor
|
|
698
|
+
accentColor: ui.accentColor,
|
|
699
|
+
onAddToCart
|
|
707
700
|
}
|
|
708
701
|
)), isLoading && ((_a = messages[messages.length - 1]) == null ? void 0 : _a.role) !== "assistant" && /* @__PURE__ */ import_react7.default.createElement(
|
|
709
702
|
MessageBubble,
|
|
@@ -711,7 +704,8 @@ function ChatWindow({
|
|
|
711
704
|
message: { role: "assistant", content: "" },
|
|
712
705
|
isStreaming: true,
|
|
713
706
|
primaryColor: ui.primaryColor,
|
|
714
|
-
accentColor: ui.accentColor
|
|
707
|
+
accentColor: ui.accentColor,
|
|
708
|
+
onAddToCart
|
|
715
709
|
}
|
|
716
710
|
), error && /* @__PURE__ */ import_react7.default.createElement("div", { className: "flex items-center gap-2 text-rose-500 dark:text-rose-400 text-sm bg-rose-50 dark:bg-rose-400/10 border border-rose-200 dark:border-rose-400/20 rounded-xl px-4 py-3" }, /* @__PURE__ */ import_react7.default.createElement(import_lucide_react5.TriangleAlert, { className: "w-4 h-4 flex-shrink-0" }), /* @__PURE__ */ import_react7.default.createElement("span", null, error)), /* @__PURE__ */ import_react7.default.createElement("div", { ref: bottomRef })),
|
|
717
711
|
/* @__PURE__ */ import_react7.default.createElement("div", { className: `px-4 pb-4 pt-2 border-t border-slate-200 dark:border-white/10 ${isGlass ? "bg-transparent" : "bg-white dark:bg-[#0f0f1a]"}` }, /* @__PURE__ */ import_react7.default.createElement("div", { className: `flex items-end gap-2 bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 p-2 focus-within:border-slate-300 dark:focus-within:border-white/20 transition-all ${ui.borderRadius === "full" ? "rounded-3xl" : "rounded-2xl"}` }, /* @__PURE__ */ import_react7.default.createElement(
|
|
@@ -745,7 +739,7 @@ function ChatWindow({
|
|
|
745
739
|
|
|
746
740
|
// src/components/ChatWidget.tsx
|
|
747
741
|
var DEFAULT_DIMENSIONS = { width: 380, height: 600 };
|
|
748
|
-
function ChatWidget({ position = "bottom-right" }) {
|
|
742
|
+
function ChatWidget({ position = "bottom-right", onAddToCart }) {
|
|
749
743
|
const { ui } = useConfig();
|
|
750
744
|
const [isOpen, setIsOpen] = (0, import_react8.useState)(false);
|
|
751
745
|
const [hasUnread, setHasUnread] = (0, import_react8.useState)(false);
|
|
@@ -822,7 +816,8 @@ function ChatWidget({ position = "bottom-right" }) {
|
|
|
822
816
|
onResetResize: ui.allowResize !== false ? handleResetResize : void 0,
|
|
823
817
|
isResized,
|
|
824
818
|
onMaximize: ui.allowResize !== false ? handleMaximize : void 0,
|
|
825
|
-
isMaximized
|
|
819
|
+
isMaximized,
|
|
820
|
+
onAddToCart
|
|
826
821
|
}
|
|
827
822
|
),
|
|
828
823
|
/* @__PURE__ */ import_react8.default.createElement(
|
package/dist/index.mjs
CHANGED
|
@@ -7,11 +7,11 @@ import {
|
|
|
7
7
|
} from "./chunk-X4TOT24V.mjs";
|
|
8
8
|
|
|
9
9
|
// src/components/ChatWidget.tsx
|
|
10
|
-
import React8, { useState as
|
|
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);
|
|
@@ -167,24 +185,6 @@ function MessageBubble({
|
|
|
167
185
|
}
|
|
168
186
|
return void 0;
|
|
169
187
|
};
|
|
170
|
-
const productsFromSources = React4.useMemo(() => {
|
|
171
|
-
if (isUser || !sources) return [];
|
|
172
|
-
return sources.filter((s) => {
|
|
173
|
-
const m = s.metadata || {};
|
|
174
|
-
return m.price || m.image || m.img || m.thumbnail || m.images || m.brand || m.type === "product";
|
|
175
|
-
}).map((s) => {
|
|
176
|
-
const m = s.metadata || {};
|
|
177
|
-
return {
|
|
178
|
-
id: s.id,
|
|
179
|
-
name: m.name || m.title || s.content.split("\n")[0] || "Unknown Product",
|
|
180
|
-
brand: m.brand,
|
|
181
|
-
price: m.price,
|
|
182
|
-
image: resolveImage(m),
|
|
183
|
-
link: m.link,
|
|
184
|
-
description: s.content
|
|
185
|
-
};
|
|
186
|
-
});
|
|
187
|
-
}, [sources, isUser]);
|
|
188
188
|
const { productsFromContent, cleanContent } = React4.useMemo(() => {
|
|
189
189
|
if (isUser) return { productsFromContent: [], cleanContent: message.content };
|
|
190
190
|
const jsonRegex = /```json\s*([\s\S]*?)\s*```/g;
|
|
@@ -208,23 +208,7 @@ function MessageBubble({
|
|
|
208
208
|
}
|
|
209
209
|
return { productsFromContent: products, cleanContent: content.trim() };
|
|
210
210
|
}, [message.content, isUser]);
|
|
211
|
-
const allProducts =
|
|
212
|
-
if (productsFromContent.length > 0) return productsFromContent;
|
|
213
|
-
const uniqueProducts = [];
|
|
214
|
-
const seenIds = /* @__PURE__ */ new Set();
|
|
215
|
-
const contentLower = message.content.toLowerCase();
|
|
216
|
-
for (const p of productsFromSources) {
|
|
217
|
-
const identifier = p.id || p.name;
|
|
218
|
-
if (seenIds.has(identifier)) continue;
|
|
219
|
-
const nameMatch = contentLower.includes(p.name.toLowerCase());
|
|
220
|
-
const brandMatch = p.brand ? contentLower.includes(p.brand.toLowerCase()) : false;
|
|
221
|
-
if (nameMatch || brandMatch) {
|
|
222
|
-
seenIds.add(identifier);
|
|
223
|
-
uniqueProducts.push(p);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
return uniqueProducts;
|
|
227
|
-
}, [productsFromSources, productsFromContent, message.content]);
|
|
211
|
+
const allProducts = productsFromContent;
|
|
228
212
|
const hasProducts = allProducts.length > 0;
|
|
229
213
|
return /* @__PURE__ */ React4.createElement("div", { className: `flex gap-3 ${isUser ? "flex-row-reverse" : "flex-row"} items-start` }, /* @__PURE__ */ React4.createElement(
|
|
230
214
|
"div",
|
|
@@ -240,7 +224,14 @@ function MessageBubble({
|
|
|
240
224
|
style: isUser ? { background: `linear-gradient(135deg, ${primaryColor}, ${accentColor})` } : {}
|
|
241
225
|
},
|
|
242
226
|
isStreaming && !message.content ? /* @__PURE__ */ React4.createElement("span", { className: "flex items-center gap-1 py-1" }, /* @__PURE__ */ React4.createElement("span", { className: "w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:0ms]" }), /* @__PURE__ */ React4.createElement("span", { className: "w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:150ms]" }), /* @__PURE__ */ React4.createElement("span", { className: "w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:300ms]" })) : /* @__PURE__ */ React4.createElement("div", { className: `prose prose-sm max-w-none ${isUser ? "prose-invert" : "dark:prose-invert"}` }, /* @__PURE__ */ React4.createElement(ReactMarkdown, { remarkPlugins: [remarkGfm] }, cleanContent || message.content), isStreaming && message.content && /* @__PURE__ */ React4.createElement("span", { className: "inline-block w-1.5 h-3.5 ml-1 bg-emerald-500 animate-pulse align-middle" }))
|
|
243
|
-
), !isUser && hasProducts && /* @__PURE__ */ React4.createElement("div", { className: "w-full mt-1" }, /* @__PURE__ */ React4.createElement(
|
|
227
|
+
), !isUser && hasProducts && /* @__PURE__ */ React4.createElement("div", { className: "w-full mt-1" }, /* @__PURE__ */ React4.createElement(
|
|
228
|
+
ProductCarousel,
|
|
229
|
+
{
|
|
230
|
+
products: allProducts,
|
|
231
|
+
primaryColor,
|
|
232
|
+
onAddToCart
|
|
233
|
+
}
|
|
234
|
+
)), !isUser && sources && sources.length > 0 && /* @__PURE__ */ React4.createElement("div", { className: "w-full" }, /* @__PURE__ */ React4.createElement(
|
|
244
235
|
"button",
|
|
245
236
|
{
|
|
246
237
|
onClick: () => setShowSources((s) => !s),
|
|
@@ -291,7 +282,7 @@ function useConfig() {
|
|
|
291
282
|
}
|
|
292
283
|
|
|
293
284
|
// src/hooks/useRagChat.ts
|
|
294
|
-
import { useState as
|
|
285
|
+
import { useState as useState3, useCallback, useRef as useRef2, useEffect as useEffect3 } from "react";
|
|
295
286
|
|
|
296
287
|
// src/hooks/useStoredMessages.ts
|
|
297
288
|
import * as React6 from "react";
|
|
@@ -352,11 +343,11 @@ function useRagChat(projectId, options = {}) {
|
|
|
352
343
|
} = options;
|
|
353
344
|
const storageKey = `rag_chat_${projectId}`;
|
|
354
345
|
const [messages, setMessages] = useStoredMessages(storageKey, persist);
|
|
355
|
-
const [isLoading, setIsLoading] =
|
|
356
|
-
const [error, setError] =
|
|
346
|
+
const [isLoading, setIsLoading] = useState3(false);
|
|
347
|
+
const [error, setError] = useState3(null);
|
|
357
348
|
const lastInputRef = useRef2("");
|
|
358
349
|
const messagesRef = useRef2(messages);
|
|
359
|
-
|
|
350
|
+
useEffect3(() => {
|
|
360
351
|
messagesRef.current = messages;
|
|
361
352
|
}, [messages]);
|
|
362
353
|
const sendMessage = useCallback(
|
|
@@ -505,21 +496,22 @@ function ChatWindow({
|
|
|
505
496
|
onResetResize,
|
|
506
497
|
isResized = false,
|
|
507
498
|
onMaximize,
|
|
508
|
-
isMaximized = false
|
|
499
|
+
isMaximized = false,
|
|
500
|
+
onAddToCart
|
|
509
501
|
}) {
|
|
510
502
|
var _a;
|
|
511
503
|
const { ui, projectId } = useConfig();
|
|
512
|
-
const [input, setInput] =
|
|
513
|
-
const [mounted, setMounted] =
|
|
504
|
+
const [input, setInput] = useState4("");
|
|
505
|
+
const [mounted, setMounted] = useState4(false);
|
|
514
506
|
const bottomRef = useRef3(null);
|
|
515
507
|
const inputRef = useRef3(null);
|
|
516
508
|
const { messages, send, clear, isLoading, error } = useRagChat(projectId, {
|
|
517
509
|
namespace: projectId
|
|
518
510
|
});
|
|
519
|
-
|
|
511
|
+
useEffect4(() => {
|
|
520
512
|
setMounted(true);
|
|
521
513
|
}, []);
|
|
522
|
-
|
|
514
|
+
useEffect4(() => {
|
|
523
515
|
var _a2;
|
|
524
516
|
if (messages.length > 0 || isLoading) {
|
|
525
517
|
(_a2 = bottomRef.current) == null ? void 0 : _a2.scrollIntoView({ behavior: "smooth" });
|
|
@@ -645,7 +637,8 @@ function ChatWindow({
|
|
|
645
637
|
sources: msg.sources,
|
|
646
638
|
isStreaming: isLoading && index === messages.length - 1 && msg.role === "assistant",
|
|
647
639
|
primaryColor: ui.primaryColor,
|
|
648
|
-
accentColor: ui.accentColor
|
|
640
|
+
accentColor: ui.accentColor,
|
|
641
|
+
onAddToCart
|
|
649
642
|
}
|
|
650
643
|
)), isLoading && ((_a = messages[messages.length - 1]) == null ? void 0 : _a.role) !== "assistant" && /* @__PURE__ */ React7.createElement(
|
|
651
644
|
MessageBubble,
|
|
@@ -653,7 +646,8 @@ function ChatWindow({
|
|
|
653
646
|
message: { role: "assistant", content: "" },
|
|
654
647
|
isStreaming: true,
|
|
655
648
|
primaryColor: ui.primaryColor,
|
|
656
|
-
accentColor: ui.accentColor
|
|
649
|
+
accentColor: ui.accentColor,
|
|
650
|
+
onAddToCart
|
|
657
651
|
}
|
|
658
652
|
), error && /* @__PURE__ */ React7.createElement("div", { className: "flex items-center gap-2 text-rose-500 dark:text-rose-400 text-sm bg-rose-50 dark:bg-rose-400/10 border border-rose-200 dark:border-rose-400/20 rounded-xl px-4 py-3" }, /* @__PURE__ */ React7.createElement(TriangleAlert, { className: "w-4 h-4 flex-shrink-0" }), /* @__PURE__ */ React7.createElement("span", null, error)), /* @__PURE__ */ React7.createElement("div", { ref: bottomRef })),
|
|
659
653
|
/* @__PURE__ */ React7.createElement("div", { className: `px-4 pb-4 pt-2 border-t border-slate-200 dark:border-white/10 ${isGlass ? "bg-transparent" : "bg-white dark:bg-[#0f0f1a]"}` }, /* @__PURE__ */ React7.createElement("div", { className: `flex items-end gap-2 bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 p-2 focus-within:border-slate-300 dark:focus-within:border-white/20 transition-all ${ui.borderRadius === "full" ? "rounded-3xl" : "rounded-2xl"}` }, /* @__PURE__ */ React7.createElement(
|
|
@@ -687,14 +681,14 @@ function ChatWindow({
|
|
|
687
681
|
|
|
688
682
|
// src/components/ChatWidget.tsx
|
|
689
683
|
var DEFAULT_DIMENSIONS = { width: 380, height: 600 };
|
|
690
|
-
function ChatWidget({ position = "bottom-right" }) {
|
|
684
|
+
function ChatWidget({ position = "bottom-right", onAddToCart }) {
|
|
691
685
|
const { ui } = useConfig();
|
|
692
|
-
const [isOpen, setIsOpen] =
|
|
693
|
-
const [hasUnread, setHasUnread] =
|
|
694
|
-
const [dimensions, setDimensions] =
|
|
695
|
-
const [isResizing, setIsResizing] =
|
|
696
|
-
const [isMaximized, setIsMaximized] =
|
|
697
|
-
const [prevDimensions, setPrevDimensions] =
|
|
686
|
+
const [isOpen, setIsOpen] = useState5(false);
|
|
687
|
+
const [hasUnread, setHasUnread] = useState5(false);
|
|
688
|
+
const [dimensions, setDimensions] = useState5(DEFAULT_DIMENSIONS);
|
|
689
|
+
const [isResizing, setIsResizing] = useState5(false);
|
|
690
|
+
const [isMaximized, setIsMaximized] = useState5(false);
|
|
691
|
+
const [prevDimensions, setPrevDimensions] = useState5(DEFAULT_DIMENSIONS);
|
|
698
692
|
if (ui.showWidget === false) return null;
|
|
699
693
|
const positionClass = position === "bottom-left" ? "bottom-6 left-6" : "bottom-6 right-6";
|
|
700
694
|
const windowPositionClass = position === "bottom-left" ? "bottom-20 left-6" : "bottom-20 right-6";
|
|
@@ -764,7 +758,8 @@ function ChatWidget({ position = "bottom-right" }) {
|
|
|
764
758
|
onResetResize: ui.allowResize !== false ? handleResetResize : void 0,
|
|
765
759
|
isResized,
|
|
766
760
|
onMaximize: ui.allowResize !== false ? handleMaximize : void 0,
|
|
767
|
-
isMaximized
|
|
761
|
+
isMaximized,
|
|
762
|
+
onAddToCart
|
|
768
763
|
}
|
|
769
764
|
),
|
|
770
765
|
/* @__PURE__ */ React8.createElement(
|
|
@@ -800,12 +795,12 @@ function ChatWidget({ position = "bottom-right" }) {
|
|
|
800
795
|
}
|
|
801
796
|
|
|
802
797
|
// src/components/DocumentUpload.tsx
|
|
803
|
-
import React9, { useState as
|
|
798
|
+
import React9, { useState as useState6, useRef as useRef4 } from "react";
|
|
804
799
|
import { Upload, File, X as X3, CheckCircle, AlertCircle, Loader2 } from "lucide-react";
|
|
805
800
|
function DocumentUpload({ namespace, onUploadComplete, className = "" }) {
|
|
806
801
|
const { ui } = useConfig();
|
|
807
|
-
const [fileStates, setFileStates] =
|
|
808
|
-
const [isDragging, setIsDragging] =
|
|
802
|
+
const [fileStates, setFileStates] = useState6([]);
|
|
803
|
+
const [isDragging, setIsDragging] = useState6(false);
|
|
809
804
|
const fileInputRef = useRef4(null);
|
|
810
805
|
const addFiles = (files) => {
|
|
811
806
|
const newStates = files.map((file) => ({ file, status: "idle" }));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@retrivora-ai/rag-engine",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.9",
|
|
4
4
|
"description": "Retrivora AI is a plug-and-play AI engine for RAG chat experiences — generic vector DB + LLM provider, embeddable or standalone.",
|
|
5
5
|
"author": "Abhinav Alkuchi",
|
|
6
6
|
"license": "MIT",
|
|
@@ -8,11 +8,13 @@ import { useConfig } from './ConfigProvider';
|
|
|
8
8
|
interface ChatWidgetProps {
|
|
9
9
|
/** Position of the floating button. Defaults to bottom-right. */
|
|
10
10
|
position?: 'bottom-right' | 'bottom-left';
|
|
11
|
+
/** Called when the user clicks 'Add to Cart' on a product */
|
|
12
|
+
onAddToCart?: (product: any) => void;
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
const DEFAULT_DIMENSIONS = { width: 380, height: 600 };
|
|
14
16
|
|
|
15
|
-
export function ChatWidget({ position = 'bottom-right' }: ChatWidgetProps) {
|
|
17
|
+
export function ChatWidget({ position = 'bottom-right', onAddToCart }: ChatWidgetProps) {
|
|
16
18
|
const { ui } = useConfig();
|
|
17
19
|
const [isOpen, setIsOpen] = useState(false);
|
|
18
20
|
const [hasUnread, setHasUnread] = useState(false);
|
|
@@ -111,6 +113,7 @@ export function ChatWidget({ position = 'bottom-right' }: ChatWidgetProps) {
|
|
|
111
113
|
isResized={isResized}
|
|
112
114
|
onMaximize={ui.allowResize !== false ? handleMaximize : undefined}
|
|
113
115
|
isMaximized={isMaximized}
|
|
116
|
+
onAddToCart={onAddToCart}
|
|
114
117
|
/>
|
|
115
118
|
{/* Pointer (Tail) */}
|
|
116
119
|
<div
|
|
@@ -36,6 +36,8 @@ interface ChatWindowProps {
|
|
|
36
36
|
onMaximize?: () => void;
|
|
37
37
|
/** Whether the window is currently maximized */
|
|
38
38
|
isMaximized?: boolean;
|
|
39
|
+
/** Called when the user clicks 'Add to Cart' on a product */
|
|
40
|
+
onAddToCart?: (product: any) => void;
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
export function ChatWindow({
|
|
@@ -47,7 +49,8 @@ export function ChatWindow({
|
|
|
47
49
|
onResetResize,
|
|
48
50
|
isResized = false,
|
|
49
51
|
onMaximize,
|
|
50
|
-
isMaximized = false
|
|
52
|
+
isMaximized = false,
|
|
53
|
+
onAddToCart
|
|
51
54
|
}: ChatWindowProps) {
|
|
52
55
|
const { ui, projectId } = useConfig();
|
|
53
56
|
const [input, setInput] = useState('');
|
|
@@ -233,6 +236,7 @@ export function ChatWindow({
|
|
|
233
236
|
isStreaming={isLoading && index === messages.length - 1 && msg.role === 'assistant'}
|
|
234
237
|
primaryColor={ui.primaryColor}
|
|
235
238
|
accentColor={ui.accentColor}
|
|
239
|
+
onAddToCart={onAddToCart}
|
|
236
240
|
/>
|
|
237
241
|
))
|
|
238
242
|
)}
|
|
@@ -244,6 +248,7 @@ export function ChatWindow({
|
|
|
244
248
|
isStreaming
|
|
245
249
|
primaryColor={ui.primaryColor}
|
|
246
250
|
accentColor={ui.accentColor}
|
|
251
|
+
onAddToCart={onAddToCart}
|
|
247
252
|
/>
|
|
248
253
|
)}
|
|
249
254
|
|
|
@@ -16,6 +16,7 @@ interface MessageBubbleProps {
|
|
|
16
16
|
isStreaming?: boolean;
|
|
17
17
|
primaryColor?: string;
|
|
18
18
|
accentColor?: string;
|
|
19
|
+
onAddToCart?: (product: Product) => void;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
export function MessageBubble({
|
|
@@ -24,6 +25,7 @@ export function MessageBubble({
|
|
|
24
25
|
isStreaming = false,
|
|
25
26
|
primaryColor = '#6366f1',
|
|
26
27
|
accentColor = '#8b5cf6',
|
|
28
|
+
onAddToCart,
|
|
27
29
|
}: MessageBubbleProps) {
|
|
28
30
|
const isUser = message.role === 'user';
|
|
29
31
|
const [showSources, setShowSources] = React.useState(false);
|
|
@@ -47,28 +49,6 @@ export function MessageBubble({
|
|
|
47
49
|
return undefined;
|
|
48
50
|
};
|
|
49
51
|
|
|
50
|
-
// Extract products from sources
|
|
51
|
-
const productsFromSources = React.useMemo(() => {
|
|
52
|
-
if (isUser || !sources) return [];
|
|
53
|
-
return sources
|
|
54
|
-
.filter(s => {
|
|
55
|
-
const m = s.metadata || {};
|
|
56
|
-
return m.price || m.image || m.img || m.thumbnail || m.images || m.brand || m.type === 'product';
|
|
57
|
-
})
|
|
58
|
-
.map(s => {
|
|
59
|
-
const m = (s.metadata || {}) as Record<string, unknown>;
|
|
60
|
-
return {
|
|
61
|
-
id: s.id,
|
|
62
|
-
name: (m.name || m.title || s.content.split('\n')[0] || 'Unknown Product') as string,
|
|
63
|
-
brand: m.brand as string,
|
|
64
|
-
price: m.price as string | number,
|
|
65
|
-
image: resolveImage(m),
|
|
66
|
-
link: m.link as string,
|
|
67
|
-
description: s.content
|
|
68
|
-
};
|
|
69
|
-
});
|
|
70
|
-
}, [sources, isUser]);
|
|
71
|
-
|
|
72
52
|
// Extract products from content (structured JSON)
|
|
73
53
|
const { productsFromContent, cleanContent } = React.useMemo(() => {
|
|
74
54
|
if (isUser) return { productsFromContent: [], cleanContent: message.content };
|
|
@@ -100,33 +80,10 @@ export function MessageBubble({
|
|
|
100
80
|
return { productsFromContent: products, cleanContent: content.trim() };
|
|
101
81
|
}, [message.content, isUser]);
|
|
102
82
|
|
|
103
|
-
const allProducts =
|
|
104
|
-
// If the LLM explicitly provided products in a structured format, only show those.
|
|
105
|
-
if (productsFromContent.length > 0) return productsFromContent;
|
|
106
|
-
|
|
107
|
-
// Fallback: Show unique products from sources, but ONLY if they are mentioned in the message text.
|
|
108
|
-
// This aligns the carousel with what the assistant is actually "displaying" in its response.
|
|
109
|
-
const uniqueProducts: Product[] = [];
|
|
110
|
-
const seenIds = new Set();
|
|
111
|
-
const contentLower = message.content.toLowerCase();
|
|
112
|
-
|
|
113
|
-
for (const p of productsFromSources) {
|
|
114
|
-
const identifier = p.id || p.name;
|
|
115
|
-
if (seenIds.has(identifier)) continue;
|
|
116
|
-
|
|
117
|
-
// Check if the product name or brand is mentioned in the assistant's text
|
|
118
|
-
const nameMatch = contentLower.includes(p.name.toLowerCase());
|
|
119
|
-
const brandMatch = p.brand ? contentLower.includes(p.brand.toLowerCase()) : false;
|
|
120
|
-
|
|
121
|
-
if (nameMatch || brandMatch) {
|
|
122
|
-
seenIds.add(identifier);
|
|
123
|
-
uniqueProducts.push(p);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
83
|
+
const allProducts = productsFromContent;
|
|
126
84
|
|
|
127
|
-
return uniqueProducts;
|
|
128
|
-
}, [productsFromSources, productsFromContent, message.content]);
|
|
129
85
|
|
|
86
|
+
// Only surface product cards if the LLM explicitly provided them via structured JSON
|
|
130
87
|
const hasProducts = allProducts.length > 0;
|
|
131
88
|
|
|
132
89
|
return (
|
|
@@ -180,7 +137,11 @@ export function MessageBubble({
|
|
|
180
137
|
{/* Product Carousel (Outside the bubble) */}
|
|
181
138
|
{!isUser && hasProducts && (
|
|
182
139
|
<div className="w-full mt-1">
|
|
183
|
-
<ProductCarousel
|
|
140
|
+
<ProductCarousel
|
|
141
|
+
products={allProducts}
|
|
142
|
+
primaryColor={primaryColor}
|
|
143
|
+
onAddToCart={onAddToCart}
|
|
144
|
+
/>
|
|
184
145
|
</div>
|
|
185
146
|
)}
|
|
186
147
|
|
|
@@ -17,9 +17,10 @@ export interface Product {
|
|
|
17
17
|
interface ProductCardProps {
|
|
18
18
|
product: Product;
|
|
19
19
|
primaryColor?: string;
|
|
20
|
+
onAddToCart?: (product: Product) => void;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
export function ProductCard({ product, primaryColor = '#6366f1' }: ProductCardProps) {
|
|
23
|
+
export function ProductCard({ product, primaryColor = '#6366f1', onAddToCart }: ProductCardProps) {
|
|
23
24
|
return (
|
|
24
25
|
<div className="flex-shrink-0 w-full bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-xl overflow-hidden shadow-sm hover:shadow-md transition-all duration-300 group">
|
|
25
26
|
{/* Product Image */}
|
|
@@ -67,6 +68,7 @@ export function ProductCard({ product, primaryColor = '#6366f1' }: ProductCardPr
|
|
|
67
68
|
|
|
68
69
|
<div className="mt-2 flex gap-2">
|
|
69
70
|
<button
|
|
71
|
+
onClick={() => onAddToCart?.(product)}
|
|
70
72
|
className="flex-1 py-2 px-3 rounded-lg text-[11px] font-medium text-white shadow-sm hover:opacity-90 transition-opacity flex items-center justify-center gap-1.5"
|
|
71
73
|
style={{ background: primaryColor }}
|
|
72
74
|
>
|
|
@@ -1,16 +1,36 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import React, { useRef } from 'react';
|
|
3
|
+
import React, { useRef, useState, useEffect } from 'react';
|
|
4
4
|
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
|
5
5
|
import { Product, ProductCard } from './ProductCard';
|
|
6
6
|
|
|
7
7
|
interface ProductCarouselProps {
|
|
8
8
|
products: Product[];
|
|
9
9
|
primaryColor?: string;
|
|
10
|
+
onAddToCart?: (product: Product) => void;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
export function ProductCarousel({ products, primaryColor = '#6366f1' }: ProductCarouselProps) {
|
|
13
|
+
export function ProductCarousel({ products, primaryColor = '#6366f1', onAddToCart }: ProductCarouselProps) {
|
|
13
14
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
15
|
+
const [canScroll, setCanScroll] = useState(false);
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const checkScroll = () => {
|
|
19
|
+
if (scrollRef.current) {
|
|
20
|
+
const hasOverflow = scrollRef.current.scrollWidth > scrollRef.current.clientWidth;
|
|
21
|
+
setCanScroll(hasOverflow);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Initial check
|
|
26
|
+
checkScroll();
|
|
27
|
+
|
|
28
|
+
// Set up ResizeObserver to handle chat window resizing
|
|
29
|
+
const observer = new ResizeObserver(checkScroll);
|
|
30
|
+
if (scrollRef.current) observer.observe(scrollRef.current);
|
|
31
|
+
|
|
32
|
+
return () => observer.disconnect();
|
|
33
|
+
}, [products]);
|
|
14
34
|
|
|
15
35
|
const scroll = (direction: 'left' | 'right') => {
|
|
16
36
|
if (scrollRef.current) {
|
|
@@ -26,28 +46,41 @@ export function ProductCarousel({ products, primaryColor = '#6366f1' }: ProductC
|
|
|
26
46
|
|
|
27
47
|
if (!products || products.length === 0) return null;
|
|
28
48
|
|
|
29
|
-
|
|
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>
|