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