@retrivora-ai/rag-engine 1.1.3 → 1.1.5
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/{chunk-JBZEUFW3.mjs → chunk-OCDCJUNE.mjs} +32 -31
- package/dist/handlers/index.js +32 -31
- package/dist/handlers/index.mjs +1 -1
- package/dist/index.js +223 -95
- package/dist/index.mjs +199 -71
- package/dist/server.js +32 -31
- package/dist/server.mjs +1 -1
- package/package.json +1 -1
- package/src/app/globals.css +9 -0
- package/src/components/MessageBubble.tsx +84 -1
- package/src/components/ProductCard.tsx +90 -0
- package/src/components/ProductCarousel.tsx +59 -0
- package/src/config/serverConfig.ts +5 -1
package/dist/server.js
CHANGED
|
@@ -1736,7 +1736,7 @@ function getRagConfig(baseConfig, env = process.env) {
|
|
|
1736
1736
|
return getEnvConfig(env, baseConfig);
|
|
1737
1737
|
}
|
|
1738
1738
|
function getEnvConfig(env = process.env, base) {
|
|
1739
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _A, _B, _C, _D, _E, _F, _G, _H, _I, _J, _K, _L, _M, _N, _O, _P, _Q, _R, _S, _T, _U, _V, _W, _X, _Y, _Z, __, _$, _aa, _ba, _ca, _da, _ea, _fa, _ga, _ha, _ia, _ja, _ka, _la, _ma, _na, _oa, _pa;
|
|
1739
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _A, _B, _C, _D, _E, _F, _G, _H, _I, _J, _K, _L, _M, _N, _O, _P, _Q, _R, _S, _T, _U, _V, _W, _X, _Y, _Z, __, _$, _aa, _ba, _ca, _da, _ea, _fa, _ga, _ha, _ia, _ja, _ka, _la, _ma, _na, _oa, _pa, _qa, _ra, _sa, _ta, _ua, _va;
|
|
1740
1740
|
const projectId = (_c = (_b = (_a = readString(env, "RAG_PROJECT_ID")) != null ? _a : readString(env, "NEXT_PUBLIC_PROJECT_ID")) != null ? _b : base == null ? void 0 : base.projectId) != null ? _c : "__default__";
|
|
1741
1741
|
const vectorProvider = readEnum(env, "VECTOR_DB_PROVIDER", "pinecone", VECTOR_DB_PROVIDERS);
|
|
1742
1742
|
const llmProvider = readEnum(env, "LLM_PROVIDER", "openai", LLM_PROVIDERS);
|
|
@@ -1745,34 +1745,35 @@ function getEnvConfig(env = process.env, base) {
|
|
|
1745
1745
|
const vectorDbOptions = {};
|
|
1746
1746
|
if (vectorProvider === "pinecone") {
|
|
1747
1747
|
vectorDbOptions.apiKey = (_g = (_f = readString(env, "PINECONE_API_KEY")) != null ? _f : (_e = (_d = base == null ? void 0 : base.vectorDb) == null ? void 0 : _d.options) == null ? void 0 : _e.apiKey) != null ? _g : "";
|
|
1748
|
+
vectorDbOptions.indexName = (_l = (_i = readString(env, "PINECONE_INDEX")) != null ? _i : (_h = base == null ? void 0 : base.vectorDb) == null ? void 0 : _h.indexName) != null ? _l : (_k = (_j = base == null ? void 0 : base.vectorDb) == null ? void 0 : _j.options) == null ? void 0 : _k.indexName;
|
|
1748
1749
|
} else if (vectorProvider === "pgvector" || vectorProvider === "postgresql") {
|
|
1749
|
-
vectorDbOptions.connectionString = (
|
|
1750
|
+
vectorDbOptions.connectionString = (_p = (_o = readString(env, "PGVECTOR_CONNECTION_STRING")) != null ? _o : (_n = (_m = base == null ? void 0 : base.vectorDb) == null ? void 0 : _m.options) == null ? void 0 : _n.connectionString) != null ? _p : "";
|
|
1750
1751
|
vectorDbOptions.dimensions = embeddingDimensions;
|
|
1751
1752
|
} else if (vectorProvider === "mongodb") {
|
|
1752
|
-
vectorDbOptions.uri = (
|
|
1753
|
-
vectorDbOptions.database = (
|
|
1754
|
-
vectorDbOptions.collection = (
|
|
1755
|
-
vectorDbOptions.indexName = (
|
|
1753
|
+
vectorDbOptions.uri = (_t = (_s = readString(env, "MONGODB_URI")) != null ? _s : (_r = (_q = base == null ? void 0 : base.vectorDb) == null ? void 0 : _q.options) == null ? void 0 : _r.uri) != null ? _t : "";
|
|
1754
|
+
vectorDbOptions.database = (_x = (_w = readString(env, "MONGODB_DB")) != null ? _w : (_v = (_u = base == null ? void 0 : base.vectorDb) == null ? void 0 : _u.options) == null ? void 0 : _v.database) != null ? _x : "";
|
|
1755
|
+
vectorDbOptions.collection = (_B = (_A = readString(env, "MONGODB_COLLECTION")) != null ? _A : (_z = (_y = base == null ? void 0 : base.vectorDb) == null ? void 0 : _y.options) == null ? void 0 : _z.collection) != null ? _B : "";
|
|
1756
|
+
vectorDbOptions.indexName = (_H = (_G = (_D = readString(env, "MONGODB_INDEX_NAME")) != null ? _D : (_C = base == null ? void 0 : base.vectorDb) == null ? void 0 : _C.indexName) != null ? _G : (_F = (_E = base == null ? void 0 : base.vectorDb) == null ? void 0 : _E.options) == null ? void 0 : _F.indexName) != null ? _H : "vector_index";
|
|
1756
1757
|
} else if (vectorProvider === "qdrant") {
|
|
1757
|
-
vectorDbOptions.baseUrl = (
|
|
1758
|
-
vectorDbOptions.apiKey = (
|
|
1758
|
+
vectorDbOptions.baseUrl = (_L = (_K = readString(env, "QDRANT_URL")) != null ? _K : (_J = (_I = base == null ? void 0 : base.vectorDb) == null ? void 0 : _I.options) == null ? void 0 : _J.baseUrl) != null ? _L : "http://localhost:6333";
|
|
1759
|
+
vectorDbOptions.apiKey = (_O = readString(env, "QDRANT_API_KEY")) != null ? _O : (_N = (_M = base == null ? void 0 : base.vectorDb) == null ? void 0 : _M.options) == null ? void 0 : _N.apiKey;
|
|
1759
1760
|
vectorDbOptions.dimensions = embeddingDimensions;
|
|
1760
1761
|
} else if (vectorProvider === "milvus") {
|
|
1761
|
-
vectorDbOptions.baseUrl = (
|
|
1762
|
+
vectorDbOptions.baseUrl = (_P = readString(env, "MILVUS_URL")) != null ? _P : "http://localhost:19530";
|
|
1762
1763
|
vectorDbOptions.apiKey = readString(env, "MILVUS_API_KEY");
|
|
1763
1764
|
} else if (vectorProvider === "chromadb") {
|
|
1764
|
-
vectorDbOptions.baseUrl = (
|
|
1765
|
+
vectorDbOptions.baseUrl = (_Q = readString(env, "CHROMADB_URL")) != null ? _Q : "http://localhost:8000";
|
|
1765
1766
|
} else if (vectorProvider === "weaviate") {
|
|
1766
|
-
vectorDbOptions.baseUrl = (
|
|
1767
|
+
vectorDbOptions.baseUrl = (_R = readString(env, "WEAVIATE_URL")) != null ? _R : "http://localhost:8080";
|
|
1767
1768
|
vectorDbOptions.apiKey = readString(env, "WEAVIATE_API_KEY");
|
|
1768
1769
|
} else if (vectorProvider === "redis") {
|
|
1769
|
-
vectorDbOptions.baseUrl = (
|
|
1770
|
+
vectorDbOptions.baseUrl = (_S = readString(env, "REDIS_URL")) != null ? _S : "";
|
|
1770
1771
|
vectorDbOptions.apiKey = readString(env, "REDIS_API_KEY");
|
|
1771
1772
|
} else if (vectorProvider === "rest") {
|
|
1772
|
-
vectorDbOptions.baseUrl = (
|
|
1773
|
+
vectorDbOptions.baseUrl = (_T = readString(env, "VECTOR_DB_REST_URL")) != null ? _T : "";
|
|
1773
1774
|
vectorDbOptions.headers = readString(env, "VECTOR_DB_REST_API_KEY") ? { "api-key": readString(env, "VECTOR_DB_REST_API_KEY") } : {};
|
|
1774
1775
|
} else if (vectorProvider === "universal_rest") {
|
|
1775
|
-
vectorDbOptions.baseUrl = (
|
|
1776
|
+
vectorDbOptions.baseUrl = (_V = (_U = readString(env, "VECTOR_BASE_URL")) != null ? _U : readString(env, "VECTOR_DB_REST_URL")) != null ? _V : "";
|
|
1776
1777
|
vectorDbOptions.profile = readString(env, "VECTOR_UNIVERSAL_PROFILE");
|
|
1777
1778
|
vectorDbOptions.headers = readString(env, "VECTOR_DB_REST_API_KEY") ? { Authorization: `Bearer ${readString(env, "VECTOR_DB_REST_API_KEY")}` } : {};
|
|
1778
1779
|
}
|
|
@@ -1789,20 +1790,20 @@ function getEnvConfig(env = process.env, base) {
|
|
|
1789
1790
|
openai: readString(env, "OPENAI_API_KEY"),
|
|
1790
1791
|
gemini: readString(env, "GEMINI_API_KEY"),
|
|
1791
1792
|
ollama: void 0,
|
|
1792
|
-
universal_rest: (
|
|
1793
|
-
custom: (
|
|
1793
|
+
universal_rest: (_W = readString(env, "EMBEDDING_API_KEY")) != null ? _W : readString(env, "OPENAI_API_KEY"),
|
|
1794
|
+
custom: (_X = readString(env, "EMBEDDING_API_KEY")) != null ? _X : readString(env, "OPENAI_API_KEY")
|
|
1794
1795
|
};
|
|
1795
1796
|
return {
|
|
1796
1797
|
projectId,
|
|
1797
1798
|
vectorDb: {
|
|
1798
1799
|
provider: vectorProvider,
|
|
1799
|
-
indexName: (
|
|
1800
|
+
indexName: (_Z = (_Y = readString(env, "VECTOR_DB_INDEX")) != null ? _Y : vectorDbOptions.indexName) != null ? _Z : "rag-index",
|
|
1800
1801
|
options: vectorDbOptions
|
|
1801
1802
|
},
|
|
1802
1803
|
llm: {
|
|
1803
1804
|
provider: llmProvider,
|
|
1804
|
-
model: (
|
|
1805
|
-
apiKey: (
|
|
1805
|
+
model: (__ = readString(env, "LLM_MODEL")) != null ? __ : "gpt-4o",
|
|
1806
|
+
apiKey: (_$ = llmApiKeyByProvider[llmProvider]) != null ? _$ : "",
|
|
1806
1807
|
baseUrl: readString(env, "LLM_BASE_URL"),
|
|
1807
1808
|
systemPrompt: readString(env, "LLM_SYSTEM_PROMPT"),
|
|
1808
1809
|
maxTokens: readNumber(env, "LLM_MAX_TOKENS", 1024),
|
|
@@ -1813,7 +1814,7 @@ function getEnvConfig(env = process.env, base) {
|
|
|
1813
1814
|
},
|
|
1814
1815
|
embedding: {
|
|
1815
1816
|
provider: embeddingProvider,
|
|
1816
|
-
model: (
|
|
1817
|
+
model: (_aa = readString(env, "EMBEDDING_MODEL")) != null ? _aa : "text-embedding-3-small",
|
|
1817
1818
|
apiKey: embeddingApiKeyByProvider[embeddingProvider],
|
|
1818
1819
|
baseUrl: readString(env, "EMBEDDING_BASE_URL"),
|
|
1819
1820
|
dimensions: embeddingDimensions,
|
|
@@ -1824,17 +1825,17 @@ function getEnvConfig(env = process.env, base) {
|
|
|
1824
1825
|
}
|
|
1825
1826
|
},
|
|
1826
1827
|
ui: {
|
|
1827
|
-
title: (
|
|
1828
|
-
subtitle: (
|
|
1829
|
-
primaryColor: (
|
|
1830
|
-
accentColor: (
|
|
1831
|
-
logoUrl: (
|
|
1832
|
-
placeholder: (
|
|
1833
|
-
showSources: ((
|
|
1834
|
-
welcomeMessage: (
|
|
1835
|
-
visualStyle: (
|
|
1836
|
-
borderRadius: (
|
|
1837
|
-
allowUpload: ((
|
|
1828
|
+
title: (_ca = (_ba = readString(env, "NEXT_PUBLIC_UI_TITLE")) != null ? _ba : readString(env, "UI_TITLE")) != null ? _ca : "AI Assistant",
|
|
1829
|
+
subtitle: (_ea = (_da = readString(env, "NEXT_PUBLIC_UI_SUBTITLE")) != null ? _da : readString(env, "UI_SUBTITLE")) != null ? _ea : "Powered by RAG",
|
|
1830
|
+
primaryColor: (_ga = (_fa = readString(env, "NEXT_PUBLIC_PRIMARY_COLOR")) != null ? _fa : readString(env, "UI_PRIMARY_COLOR")) != null ? _ga : "#10b981",
|
|
1831
|
+
accentColor: (_ia = (_ha = readString(env, "NEXT_PUBLIC_ACCENT_COLOR")) != null ? _ha : readString(env, "UI_ACCENT_COLOR")) != null ? _ia : "#3b82f6",
|
|
1832
|
+
logoUrl: (_ja = readString(env, "NEXT_PUBLIC_LOGO_URL")) != null ? _ja : readString(env, "UI_LOGO_URL"),
|
|
1833
|
+
placeholder: (_la = (_ka = readString(env, "NEXT_PUBLIC_PLACEHOLDER")) != null ? _ka : readString(env, "UI_PLACEHOLDER")) != null ? _la : "Ask me anything\u2026",
|
|
1834
|
+
showSources: ((_na = (_ma = readString(env, "NEXT_PUBLIC_SHOW_SOURCES")) != null ? _ma : readString(env, "UI_SHOW_SOURCES")) != null ? _na : "true") !== "false",
|
|
1835
|
+
welcomeMessage: (_pa = (_oa = readString(env, "NEXT_PUBLIC_WELCOME_MESSAGE")) != null ? _oa : readString(env, "UI_WELCOME_MESSAGE")) != null ? _pa : "Hello! I'm your AI assistant. Ask me anything about your documents.",
|
|
1836
|
+
visualStyle: (_ra = (_qa = readString(env, "NEXT_PUBLIC_UI_VISUAL_STYLE")) != null ? _qa : readString(env, "UI_VISUAL_STYLE")) != null ? _ra : "glass",
|
|
1837
|
+
borderRadius: (_ta = (_sa = readString(env, "NEXT_PUBLIC_UI_BORDER_RADIUS")) != null ? _sa : readString(env, "UI_BORDER_RADIUS")) != null ? _ta : "xl",
|
|
1838
|
+
allowUpload: ((_va = (_ua = readString(env, "NEXT_PUBLIC_ALLOW_UPLOAD")) != null ? _ua : readString(env, "UI_ALLOW_UPLOAD")) != null ? _va : "false") === "true"
|
|
1838
1839
|
},
|
|
1839
1840
|
rag: {
|
|
1840
1841
|
topK: readNumber(env, "RAG_TOP_K", 5),
|
package/dist/server.mjs
CHANGED
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.5",
|
|
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
|
@@ -65,6 +65,15 @@ body {
|
|
|
65
65
|
background: rgba(255, 255, 255, 0.2);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
.scrollbar-hide::-webkit-scrollbar {
|
|
69
|
+
display: none;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.scrollbar-hide {
|
|
73
|
+
-ms-overflow-style: none;
|
|
74
|
+
scrollbar-width: none;
|
|
75
|
+
}
|
|
76
|
+
|
|
68
77
|
/* ─── Prose Overrides (markdown in chat) ───────────────────── */
|
|
69
78
|
.prose-invert {
|
|
70
79
|
color: inherit;
|
|
@@ -7,6 +7,8 @@ import remarkGfm from 'remark-gfm';
|
|
|
7
7
|
import { ChatMessage } from '../llm/ILLMProvider';
|
|
8
8
|
import { VectorMatch } from '../types';
|
|
9
9
|
import { SourceCard } from './SourceCard';
|
|
10
|
+
import { ProductCarousel } from './ProductCarousel';
|
|
11
|
+
import { Product } from './ProductCard';
|
|
10
12
|
|
|
11
13
|
interface MessageBubbleProps {
|
|
12
14
|
message: ChatMessage;
|
|
@@ -26,6 +28,81 @@ export function MessageBubble({
|
|
|
26
28
|
const isUser = message.role === 'user';
|
|
27
29
|
const [showSources, setShowSources] = React.useState(false);
|
|
28
30
|
|
|
31
|
+
// Helper to extract image from metadata or object
|
|
32
|
+
const resolveImage = (data: Record<string, unknown>): string | undefined => {
|
|
33
|
+
if (!data) return undefined;
|
|
34
|
+
|
|
35
|
+
// Check single string fields
|
|
36
|
+
const singleFields = ['image', 'img', 'thumbnail'];
|
|
37
|
+
for (const field of singleFields) {
|
|
38
|
+
const val = data[field];
|
|
39
|
+
if (typeof val === 'string' && val) return val;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Check images array
|
|
43
|
+
if (Array.isArray(data.images) && data.images.length > 0) {
|
|
44
|
+
if (typeof data.images[0] === 'string') return data.images[0];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return undefined;
|
|
48
|
+
};
|
|
49
|
+
|
|
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
|
+
// Extract products from content (structured JSON)
|
|
73
|
+
const { productsFromContent, cleanContent } = React.useMemo(() => {
|
|
74
|
+
if (isUser) return { productsFromContent: [], cleanContent: message.content };
|
|
75
|
+
|
|
76
|
+
const jsonRegex = /```json\s*([\s\S]*?)\s*```/g;
|
|
77
|
+
const products: Product[] = [];
|
|
78
|
+
let content = message.content;
|
|
79
|
+
|
|
80
|
+
// Only process if it looks like it might contain product data
|
|
81
|
+
if (content.includes('"type": "products"') || content.includes('"type":"products"')) {
|
|
82
|
+
const matches = Array.from(content.matchAll(jsonRegex));
|
|
83
|
+
for (const m of matches) {
|
|
84
|
+
try {
|
|
85
|
+
const data = JSON.parse(m[1]);
|
|
86
|
+
if (data.type === 'products' && Array.isArray(data.items)) {
|
|
87
|
+
const formattedItems = data.items.map((item: Record<string, unknown>) => ({
|
|
88
|
+
...item,
|
|
89
|
+
image: (item.image as string) || resolveImage(item)
|
|
90
|
+
})) as Product[];
|
|
91
|
+
products.push(...formattedItems);
|
|
92
|
+
content = content.replace(m[0], '');
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
// Skip invalid JSON
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { productsFromContent: products, cleanContent: content.trim() };
|
|
101
|
+
}, [message.content, isUser]);
|
|
102
|
+
|
|
103
|
+
const allProducts = [...productsFromSources, ...productsFromContent];
|
|
104
|
+
const hasProducts = allProducts.length > 0;
|
|
105
|
+
|
|
29
106
|
return (
|
|
30
107
|
<div className={`flex gap-3 ${isUser ? 'flex-row-reverse' : 'flex-row'} items-start`}>
|
|
31
108
|
{/* Avatar */}
|
|
@@ -64,8 +141,14 @@ export function MessageBubble({
|
|
|
64
141
|
) : (
|
|
65
142
|
<div className={`prose prose-sm max-w-none ${isUser ? 'prose-invert' : 'dark:prose-invert'}`}>
|
|
66
143
|
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
|
67
|
-
{message.content}
|
|
144
|
+
{cleanContent || message.content}
|
|
68
145
|
</ReactMarkdown>
|
|
146
|
+
|
|
147
|
+
{/* Product Carousel */}
|
|
148
|
+
{hasProducts && (
|
|
149
|
+
<ProductCarousel products={allProducts} primaryColor={primaryColor} />
|
|
150
|
+
)}
|
|
151
|
+
|
|
69
152
|
{isStreaming && message.content && (
|
|
70
153
|
<span className="inline-block w-1.5 h-3.5 ml-1 bg-emerald-500 animate-pulse align-middle" />
|
|
71
154
|
)}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import Image from 'next/image';
|
|
5
|
+
import { ShoppingCart, ExternalLink } from 'lucide-react';
|
|
6
|
+
|
|
7
|
+
export interface Product {
|
|
8
|
+
id: string | number;
|
|
9
|
+
name: string;
|
|
10
|
+
brand?: string;
|
|
11
|
+
price?: string | number;
|
|
12
|
+
image?: string;
|
|
13
|
+
link?: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ProductCardProps {
|
|
18
|
+
product: Product;
|
|
19
|
+
primaryColor?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function ProductCard({ product, primaryColor = '#6366f1' }: ProductCardProps) {
|
|
23
|
+
return (
|
|
24
|
+
<div className="flex-shrink-0 w-64 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
|
+
{/* Product Image */}
|
|
26
|
+
<div className="relative h-40 w-full bg-slate-100 dark:bg-white/5 flex items-center justify-center overflow-hidden">
|
|
27
|
+
{product.image ? (
|
|
28
|
+
<Image
|
|
29
|
+
src={product.image}
|
|
30
|
+
alt={product.name}
|
|
31
|
+
fill
|
|
32
|
+
unoptimized
|
|
33
|
+
className="object-cover group-hover:scale-105 transition-transform duration-500"
|
|
34
|
+
/>
|
|
35
|
+
) : (
|
|
36
|
+
<div className="text-slate-400 dark:text-white/20">
|
|
37
|
+
<ShoppingCart className="w-12 h-12" />
|
|
38
|
+
</div>
|
|
39
|
+
)}
|
|
40
|
+
|
|
41
|
+
{/* Brand Badge */}
|
|
42
|
+
{product.brand && (
|
|
43
|
+
<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">
|
|
44
|
+
{product.brand}
|
|
45
|
+
</div>
|
|
46
|
+
)}
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
{/* Product Info */}
|
|
50
|
+
<div className="p-4 flex flex-col gap-2">
|
|
51
|
+
<div className="flex justify-between items-start gap-2">
|
|
52
|
+
<h3 className="text-sm font-semibold text-slate-800 dark:text-white/90 line-clamp-1">
|
|
53
|
+
{product.name}
|
|
54
|
+
</h3>
|
|
55
|
+
{product.price && (
|
|
56
|
+
<span className="text-sm font-bold" style={{ color: primaryColor }}>
|
|
57
|
+
{typeof product.price === 'number' ? `$${product.price}` : product.price}
|
|
58
|
+
</span>
|
|
59
|
+
)}
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
{product.description && (
|
|
63
|
+
<p className="text-[11px] text-slate-500 dark:text-white/50 line-clamp-2 leading-relaxed">
|
|
64
|
+
{product.description}
|
|
65
|
+
</p>
|
|
66
|
+
)}
|
|
67
|
+
|
|
68
|
+
<div className="mt-2 flex gap-2">
|
|
69
|
+
<button
|
|
70
|
+
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
|
+
style={{ background: primaryColor }}
|
|
72
|
+
>
|
|
73
|
+
<ShoppingCart className="w-3 h-3" />
|
|
74
|
+
Add to Cart
|
|
75
|
+
</button>
|
|
76
|
+
{product.link && (
|
|
77
|
+
<a
|
|
78
|
+
href={product.link}
|
|
79
|
+
target="_blank"
|
|
80
|
+
rel="noopener noreferrer"
|
|
81
|
+
className="p-2 rounded-lg bg-slate-100 dark:bg-white/10 text-slate-600 dark:text-white/70 hover:bg-slate-200 dark:hover:bg-white/20 transition-colors"
|
|
82
|
+
>
|
|
83
|
+
<ExternalLink className="w-3.5 h-3.5" />
|
|
84
|
+
</a>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useRef } from 'react';
|
|
4
|
+
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
|
5
|
+
import { Product, ProductCard } from './ProductCard';
|
|
6
|
+
|
|
7
|
+
interface ProductCarouselProps {
|
|
8
|
+
products: Product[];
|
|
9
|
+
primaryColor?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function ProductCarousel({ products, primaryColor = '#6366f1' }: ProductCarouselProps) {
|
|
13
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
14
|
+
|
|
15
|
+
const scroll = (direction: 'left' | 'right') => {
|
|
16
|
+
if (scrollRef.current) {
|
|
17
|
+
const scrollAmount = direction === 'left' ? -280 : 280;
|
|
18
|
+
scrollRef.current.scrollBy({ left: scrollAmount, behavior: 'smooth' });
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
if (!products || products.length === 0) return null;
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div className="relative w-full my-4 group/carousel">
|
|
26
|
+
{/* Scroll Buttons */}
|
|
27
|
+
<div className="absolute -left-3 top-1/2 -translate-y-1/2 z-10 opacity-0 group-hover/carousel:opacity-100 transition-opacity">
|
|
28
|
+
<button
|
|
29
|
+
onClick={() => scroll('left')}
|
|
30
|
+
className="p-1.5 rounded-full bg-white dark:bg-slate-800 shadow-lg 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-colors"
|
|
31
|
+
>
|
|
32
|
+
<ChevronLeft className="w-4 h-4" />
|
|
33
|
+
</button>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div className="absolute -right-3 top-1/2 -translate-y-1/2 z-10 opacity-0 group-hover/carousel:opacity-100 transition-opacity">
|
|
37
|
+
<button
|
|
38
|
+
onClick={() => scroll('right')}
|
|
39
|
+
className="p-1.5 rounded-full bg-white dark:bg-slate-800 shadow-lg 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-colors"
|
|
40
|
+
>
|
|
41
|
+
<ChevronRight className="w-4 h-4" />
|
|
42
|
+
</button>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
{/* Carousel Container */}
|
|
46
|
+
<div
|
|
47
|
+
ref={scrollRef}
|
|
48
|
+
className="flex gap-4 overflow-x-auto pb-4 px-1 scrollbar-hide snap-x snap-mandatory"
|
|
49
|
+
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
|
|
50
|
+
>
|
|
51
|
+
{products.map((product) => (
|
|
52
|
+
<div key={product.id} className="snap-start">
|
|
53
|
+
<ProductCard product={product} primaryColor={primaryColor} />
|
|
54
|
+
</div>
|
|
55
|
+
))}
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -74,6 +74,7 @@ export function getEnvConfig(env: Record<string, string | undefined> = process.e
|
|
|
74
74
|
|
|
75
75
|
if (vectorProvider === 'pinecone') {
|
|
76
76
|
vectorDbOptions.apiKey = readString(env, 'PINECONE_API_KEY') ?? (base?.vectorDb?.options?.apiKey as string) ?? '';
|
|
77
|
+
vectorDbOptions.indexName = readString(env, 'PINECONE_INDEX') ?? (base?.vectorDb?.indexName as string) ?? (base?.vectorDb?.options?.indexName as string);
|
|
77
78
|
} else if (vectorProvider === 'pgvector' || vectorProvider === 'postgresql') {
|
|
78
79
|
vectorDbOptions.connectionString = readString(env, 'PGVECTOR_CONNECTION_STRING') ?? (base?.vectorDb?.options?.connectionString as string) ?? '';
|
|
79
80
|
vectorDbOptions.dimensions = embeddingDimensions;
|
|
@@ -133,7 +134,10 @@ export function getEnvConfig(env: Record<string, string | undefined> = process.e
|
|
|
133
134
|
projectId,
|
|
134
135
|
vectorDb: {
|
|
135
136
|
provider: vectorProvider,
|
|
136
|
-
indexName:
|
|
137
|
+
indexName:
|
|
138
|
+
readString(env, 'VECTOR_DB_INDEX') ??
|
|
139
|
+
(vectorDbOptions.indexName as string) ??
|
|
140
|
+
'rag-index',
|
|
137
141
|
options: vectorDbOptions,
|
|
138
142
|
},
|
|
139
143
|
llm: {
|