@retrivora-ai/rag-engine 1.9.3 → 1.9.7
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/README.md +59 -7
- package/dist/{ILLMProvider-Bw2A28nU.d.mts → ILLMProvider-Bhk6zJOK.d.mts} +43 -7
- package/dist/{ILLMProvider-Bw2A28nU.d.ts → ILLMProvider-Bhk6zJOK.d.ts} +43 -7
- package/dist/handlers/index.d.mts +2 -2
- package/dist/handlers/index.d.ts +2 -2
- package/dist/handlers/index.js +737 -237
- package/dist/handlers/index.mjs +736 -237
- package/dist/index-B9J_XEh0.d.ts +187 -0
- package/dist/index-BJ4cd-t5.d.mts +187 -0
- package/dist/{index-B70ZLkfG.d.mts → index-Bu7T6xgr.d.ts} +20 -3
- package/dist/{index-DVu-mkAM.d.ts → index-C3SVtPYg.d.mts} +20 -3
- package/dist/index.css +237 -10
- package/dist/index.d.mts +13 -5
- package/dist/index.d.ts +13 -5
- package/dist/index.js +365 -164
- package/dist/index.mjs +350 -158
- package/dist/server.d.mts +15 -94
- package/dist/server.d.ts +15 -94
- package/dist/server.js +850 -239
- package/dist/server.mjs +839 -238
- package/package.json +2 -4
- package/src/app/api/chat/route.ts +2 -2
- package/src/app/api/health/route.ts +3 -2
- package/src/app/api/ingest/route.ts +3 -2
- package/src/app/api/suggestions/route.ts +3 -2
- package/src/app/api/upload/route.ts +3 -2
- package/src/app/constants.tsx +168 -148
- package/src/app/layout.tsx +5 -18
- package/src/app/types.ts +17 -17
- package/src/components/ChatWidget.tsx +3 -1
- package/src/components/ChatWindow.tsx +5 -1
- package/src/components/DocViewer.tsx +71 -5
- package/src/components/Documentation.tsx +74 -11
- package/src/components/MessageBubble.tsx +39 -2
- package/src/components/ThinkingBlock.tsx +75 -0
- package/src/components/VisualizationRenderer.tsx +27 -1
- package/src/components/constants.tsx +275 -0
- package/src/config/RagConfig.ts +47 -0
- package/src/config/constants.ts +1 -0
- package/src/config/serverConfig.ts +25 -0
- package/src/core/ConfigResolver.ts +73 -22
- package/src/core/ConfigValidator.ts +2 -1
- package/src/core/Pipeline.ts +226 -68
- package/src/core/ProviderRegistry.ts +16 -7
- package/src/core/QueryProcessor.ts +38 -4
- package/src/core/Retrivora.ts +91 -0
- package/src/core/VectorPlugin.ts +62 -8
- package/src/exceptions/index.ts +111 -0
- package/src/handlers/index.ts +73 -0
- package/src/hooks/useRagChat.ts +30 -5
- package/src/index.ts +27 -1
- package/src/lib/plugin.ts +24 -0
- package/src/llm/LLMFactory.ts +8 -4
- package/src/llm/providers/AnthropicProvider.ts +70 -20
- package/src/llm/providers/GeminiProvider.ts +14 -15
- package/src/llm/providers/OllamaProvider.ts +13 -16
- package/src/llm/providers/OpenAIProvider.ts +9 -14
- package/src/llm/providers/UniversalLLMAdapter.ts +5 -5
- package/src/llm/utils.ts +46 -0
- package/src/providers/vectordb/MongoDBProvider.ts +9 -4
- package/src/providers/vectordb/MultiTablePostgresProvider.ts +45 -13
- package/src/rag/EntityExtractor.ts +2 -2
- package/src/rag/Reranker.ts +9 -16
- package/src/server.ts +30 -1
- package/src/types/chat.ts +9 -0
- package/src/types/props.ts +38 -1
- package/src/utils/UITransformer.ts +73 -4
- package/dist/DocumentChunker-Dh9TvmGG.d.mts +0 -45
- package/dist/DocumentChunker-Dh9TvmGG.d.ts +0 -45
|
@@ -4,8 +4,28 @@ import React from 'react';
|
|
|
4
4
|
import { Snippet } from '@/app/types';
|
|
5
5
|
import { SNIPPETS } from '@/app/constants';
|
|
6
6
|
import { CodeViewer } from './CodeViewer';
|
|
7
|
+
import { ArrowLeft, ArrowRight } from 'lucide-react';
|
|
7
8
|
|
|
8
9
|
export function DocViewer({ activeSnippet, setActiveSnippet }: { activeSnippet: Snippet, setActiveSnippet: (s: Snippet) => void }) {
|
|
10
|
+
const currentIndex = SNIPPETS.findIndex((s) => s.id === activeSnippet.id);
|
|
11
|
+
const totalSteps = SNIPPETS.length;
|
|
12
|
+
const isFirst = currentIndex === 0;
|
|
13
|
+
const isLast = currentIndex === totalSteps - 1;
|
|
14
|
+
|
|
15
|
+
const handlePrev = () => {
|
|
16
|
+
if (!isFirst) {
|
|
17
|
+
setActiveSnippet(SNIPPETS[currentIndex - 1]);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const handleNext = () => {
|
|
22
|
+
if (!isLast) {
|
|
23
|
+
setActiveSnippet(SNIPPETS[currentIndex + 1]);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const progressPercent = Math.round(((currentIndex + 1) / totalSteps) * 100);
|
|
28
|
+
|
|
9
29
|
return (
|
|
10
30
|
<>
|
|
11
31
|
<div className="mb-6 flex flex-wrap gap-2 border-b border-slate-100 dark:border-white/5 pb-6">
|
|
@@ -23,14 +43,60 @@ export function DocViewer({ activeSnippet, setActiveSnippet }: { activeSnippet:
|
|
|
23
43
|
))}
|
|
24
44
|
</div>
|
|
25
45
|
|
|
46
|
+
{/* Visual Progress Bar */}
|
|
47
|
+
<div className="mb-6 bg-slate-100 dark:bg-white/5 rounded-full h-2.5 overflow-hidden border border-slate-200/50 dark:border-white/5 relative">
|
|
48
|
+
<div
|
|
49
|
+
className="h-full bg-gradient-to-r from-indigo-500 via-indigo-600 to-violet-600 transition-all duration-500 rounded-full shadow-[0_0_8px_rgba(99,102,241,0.5)]"
|
|
50
|
+
style={{ width: `${progressPercent}%` }}
|
|
51
|
+
/>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
26
54
|
<div className="flex-grow flex flex-col gap-6 animate-in fade-in slide-in-from-bottom-2 duration-500">
|
|
27
|
-
<div className="max-w-2xl">
|
|
28
|
-
<
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
55
|
+
<div className="max-w-2xl flex items-start justify-between gap-4 flex-wrap sm:flex-nowrap">
|
|
56
|
+
<div>
|
|
57
|
+
<h3 className="text-xl font-bold text-slate-900 dark:text-white mb-2 flex items-center gap-3">
|
|
58
|
+
{activeSnippet.title}
|
|
59
|
+
<span className="text-[10px] font-extrabold uppercase px-2 py-0.5 rounded bg-indigo-50 dark:bg-indigo-500/10 border border-indigo-100 dark:border-indigo-500/20 text-indigo-600 dark:text-indigo-400">
|
|
60
|
+
Step {currentIndex + 1} of {totalSteps}
|
|
61
|
+
</span>
|
|
62
|
+
</h3>
|
|
63
|
+
<p className="text-sm text-slate-500 dark:text-white/40 leading-relaxed font-medium italic">
|
|
64
|
+
{activeSnippet.description}
|
|
65
|
+
</p>
|
|
66
|
+
</div>
|
|
67
|
+
<span className="text-xs font-mono font-bold text-indigo-600 dark:text-indigo-400 self-center shrink-0">
|
|
68
|
+
{progressPercent}% Complete
|
|
69
|
+
</span>
|
|
32
70
|
</div>
|
|
71
|
+
|
|
33
72
|
<CodeViewer snippet={activeSnippet} />
|
|
73
|
+
|
|
74
|
+
{/* Next/Prev Navigation */}
|
|
75
|
+
<div className="flex items-center justify-between border-t border-slate-200 dark:border-white/10 pt-6 mt-4">
|
|
76
|
+
<button
|
|
77
|
+
onClick={handlePrev}
|
|
78
|
+
disabled={isFirst}
|
|
79
|
+
className={`flex items-center gap-2 px-5 py-2.5 rounded-xl text-xs font-bold transition-all border ${
|
|
80
|
+
isFirst
|
|
81
|
+
? 'opacity-40 cursor-not-allowed border-slate-200 dark:border-white/5 text-slate-400 dark:text-white/20 bg-slate-50 dark:bg-transparent'
|
|
82
|
+
: 'bg-white dark:bg-white/5 border-slate-200 dark:border-white/10 text-slate-700 dark:text-white/70 hover:border-slate-300 dark:hover:border-white/20 active:scale-95'
|
|
83
|
+
}`}
|
|
84
|
+
>
|
|
85
|
+
<ArrowLeft size={14} /> Previous Step
|
|
86
|
+
</button>
|
|
87
|
+
|
|
88
|
+
<button
|
|
89
|
+
onClick={handleNext}
|
|
90
|
+
disabled={isLast}
|
|
91
|
+
className={`flex items-center gap-2 px-6 py-2.5 rounded-xl text-xs font-bold transition-all border ${
|
|
92
|
+
isLast
|
|
93
|
+
? 'opacity-40 cursor-not-allowed border-slate-200 dark:border-white/5 text-slate-400 dark:text-white/20 bg-slate-50 dark:bg-transparent'
|
|
94
|
+
: 'bg-indigo-600 text-white border-indigo-600 hover:bg-indigo-700 hover:border-indigo-700 active:scale-95 shadow-md shadow-indigo-500/10'
|
|
95
|
+
}`}
|
|
96
|
+
>
|
|
97
|
+
{isLast ? 'Complete!' : 'Next Step'} <ArrowRight size={14} />
|
|
98
|
+
</button>
|
|
99
|
+
</div>
|
|
34
100
|
</div>
|
|
35
101
|
</>
|
|
36
102
|
);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import React from 'react';
|
|
4
|
-
import { FileText, Zap, Package, ExternalLink } from 'lucide-react';
|
|
4
|
+
import { FileText, Zap, Package, ExternalLink, Code } from 'lucide-react';
|
|
5
5
|
import { DocViewer } from '@/components/DocViewer';
|
|
6
|
-
import { LANDING_PAGE_CONTENT, QUICK_START_STEPS, API_ENDPOINTS, SNIPPETS } from '@/app/constants';
|
|
6
|
+
import { LANDING_PAGE_CONTENT, QUICK_START_STEPS, ADVANCED_STEPS, API_ENDPOINTS, SNIPPETS } from '@/app/constants';
|
|
7
7
|
import { Snippet } from '@/app/types';
|
|
8
8
|
|
|
9
9
|
export function Documentation() {
|
|
@@ -16,20 +16,83 @@ export function Documentation() {
|
|
|
16
16
|
<p className="text-slate-600 dark:text-white/40 max-w-2xl mx-auto">{LANDING_PAGE_CONTENT.guide.subtitle}</p>
|
|
17
17
|
</div>
|
|
18
18
|
<div className="relative z-10 w-full grid gap-16 lg:grid-cols-[280px_1fr] items-start">
|
|
19
|
-
<aside className="flex flex-col gap-
|
|
19
|
+
<aside className="flex flex-col gap-8">
|
|
20
20
|
<section>
|
|
21
|
-
<h2 className="mb-
|
|
21
|
+
<h2 className="mb-4 text-[10px] font-black text-slate-400 dark:text-white/30 uppercase tracking-[0.3em] flex items-center gap-3">
|
|
22
22
|
<FileText size={14} className="text-indigo-500" /> Quick Start
|
|
23
23
|
</h2>
|
|
24
|
-
<ol className="space-y-
|
|
25
|
-
{QUICK_START_STEPS.map((step, i) =>
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
<
|
|
29
|
-
|
|
30
|
-
|
|
24
|
+
<ol className="space-y-2.5">
|
|
25
|
+
{QUICK_START_STEPS.map((step, i) => {
|
|
26
|
+
const isActive = activeSnippet.id === step.id;
|
|
27
|
+
return (
|
|
28
|
+
<li key={step.id}>
|
|
29
|
+
<button
|
|
30
|
+
onClick={() => {
|
|
31
|
+
const snip = SNIPPETS.find((s) => s.id === step.id);
|
|
32
|
+
if (snip) setActiveSnippet(snip);
|
|
33
|
+
}}
|
|
34
|
+
className={`w-full flex items-start gap-4 p-3 rounded-xl border text-left transition-all active:scale-[0.98] ${
|
|
35
|
+
isActive
|
|
36
|
+
? 'bg-indigo-500/10 dark:bg-indigo-500/10 border-indigo-200 dark:border-indigo-500/30'
|
|
37
|
+
: 'bg-white dark:bg-white/3 border-slate-200 dark:border-white/5 hover:border-slate-300 dark:hover:border-white/10 hover:bg-slate-50 dark:hover:bg-white/5 shadow-sm'
|
|
38
|
+
}`}
|
|
39
|
+
>
|
|
40
|
+
<span className={`flex h-6 w-6 shrink-0 items-center justify-center rounded-lg font-mono text-[10px] font-bold border transition-all ${
|
|
41
|
+
isActive
|
|
42
|
+
? 'bg-indigo-600 text-white border-indigo-600 shadow-md shadow-indigo-500/20'
|
|
43
|
+
: 'bg-slate-100 dark:bg-white/5 text-slate-600 dark:text-white/40 border-slate-200 dark:border-white/10'
|
|
44
|
+
}`}>{i + 1}</span>
|
|
45
|
+
<div className="pt-0.5">
|
|
46
|
+
<span className={`text-xs font-semibold block transition-colors ${
|
|
47
|
+
isActive ? 'text-indigo-600 dark:text-indigo-400 font-bold' : 'text-slate-700 dark:text-white/70 font-medium'
|
|
48
|
+
}`}>{step.text}</span>
|
|
49
|
+
<span className="text-[9px] text-slate-400 dark:text-white/30 block mt-0.5">Step {i + 1}</span>
|
|
50
|
+
</div>
|
|
51
|
+
</button>
|
|
52
|
+
</li>
|
|
53
|
+
);
|
|
54
|
+
})}
|
|
55
|
+
</ol>
|
|
56
|
+
</section>
|
|
57
|
+
|
|
58
|
+
<section>
|
|
59
|
+
<h2 className="mb-4 text-[10px] font-black text-slate-400 dark:text-white/30 uppercase tracking-[0.3em] flex items-center gap-3">
|
|
60
|
+
<Code size={14} className="text-violet-500" /> Advanced Options
|
|
61
|
+
</h2>
|
|
62
|
+
<ol className="space-y-2.5">
|
|
63
|
+
{ADVANCED_STEPS.map((step, i) => {
|
|
64
|
+
const isActive = activeSnippet.id === step.id;
|
|
65
|
+
return (
|
|
66
|
+
<li key={step.id}>
|
|
67
|
+
<button
|
|
68
|
+
onClick={() => {
|
|
69
|
+
const snip = SNIPPETS.find((s) => s.id === step.id);
|
|
70
|
+
if (snip) setActiveSnippet(snip);
|
|
71
|
+
}}
|
|
72
|
+
className={`w-full flex items-start gap-4 p-3 rounded-xl border text-left transition-all active:scale-[0.98] ${
|
|
73
|
+
isActive
|
|
74
|
+
? 'bg-violet-500/10 dark:bg-violet-500/10 border-violet-200 dark:border-violet-500/30'
|
|
75
|
+
: 'bg-white dark:bg-white/3 border-slate-200 dark:border-white/5 hover:border-slate-300 dark:hover:border-white/10 hover:bg-slate-50 dark:hover:bg-white/5 shadow-sm'
|
|
76
|
+
}`}
|
|
77
|
+
>
|
|
78
|
+
<span className={`flex h-6 w-6 shrink-0 items-center justify-center rounded-lg font-mono text-[10px] font-bold border transition-all ${
|
|
79
|
+
isActive
|
|
80
|
+
? 'bg-violet-600 text-white border-violet-600 shadow-md shadow-violet-500/20'
|
|
81
|
+
: 'bg-slate-100 dark:bg-white/5 text-slate-600 dark:text-white/40 border-slate-200 dark:border-white/10'
|
|
82
|
+
}`}>{i + 5}</span>
|
|
83
|
+
<div className="pt-0.5">
|
|
84
|
+
<span className={`text-xs font-semibold block transition-colors ${
|
|
85
|
+
isActive ? 'text-violet-600 dark:text-violet-400 font-bold' : 'text-slate-700 dark:text-white/70 font-medium'
|
|
86
|
+
}`}>{step.text}</span>
|
|
87
|
+
<span className="text-[9px] text-slate-400 dark:text-white/30 block mt-0.5">Extension</span>
|
|
88
|
+
</div>
|
|
89
|
+
</button>
|
|
90
|
+
</li>
|
|
91
|
+
);
|
|
92
|
+
})}
|
|
31
93
|
</ol>
|
|
32
94
|
</section>
|
|
95
|
+
|
|
33
96
|
<section className="rounded-2xl border border-slate-200 dark:border-white/10 bg-white dark:bg-white/3 p-6 shadow-sm">
|
|
34
97
|
<h2 className="mb-6 text-[10px] font-black text-slate-400 dark:text-white/30 uppercase tracking-[0.3em] flex items-center gap-3">
|
|
35
98
|
<Zap size={14} className="text-indigo-500" /> API Endpoints
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
} from '../utils/ProductExtractor';
|
|
22
22
|
import { createMarkdownComponents } from './MarkdownComponents';
|
|
23
23
|
import { UIDispatcher } from './UIDispatcher';
|
|
24
|
+
import { ThinkingBlock } from './ThinkingBlock';
|
|
24
25
|
|
|
25
26
|
function normalizePlusSeparatedLists(raw: string): string {
|
|
26
27
|
return raw
|
|
@@ -196,6 +197,15 @@ export function MessageBubble({
|
|
|
196
197
|
</div>
|
|
197
198
|
|
|
198
199
|
<div className={`flex flex-col gap-1 min-w-0 ${isCompact ? 'max-w-[94%]' : isMedium ? 'max-w-[92%]' : 'max-w-[90%]'} ${isUser ? 'items-end' : 'items-start'}`}>
|
|
200
|
+
{/* Thinking Steps */}
|
|
201
|
+
{!isUser && message.thinking && (
|
|
202
|
+
<ThinkingBlock
|
|
203
|
+
thinking={message.thinking}
|
|
204
|
+
thinkingMs={message.thinkingMs}
|
|
205
|
+
isStreaming={isStreaming && !message.content}
|
|
206
|
+
/>
|
|
207
|
+
)}
|
|
208
|
+
|
|
199
209
|
{/* Bubble */}
|
|
200
210
|
<div
|
|
201
211
|
className={`relative group ${isCompact ? 'px-3 py-2.5 text-[13px]' : 'px-4 py-3 text-sm'} rounded-2xl leading-relaxed shadow-sm dark:shadow-lg ${isUser
|
|
@@ -260,9 +270,35 @@ export function MessageBubble({
|
|
|
260
270
|
)}
|
|
261
271
|
</div>
|
|
262
272
|
|
|
263
|
-
{/* Auto-generated visualization
|
|
273
|
+
{/* Auto-generated visualization — show eagerly during streaming if sources are available */}
|
|
264
274
|
{(() => {
|
|
265
|
-
if (isUser || structuredContent.payload
|
|
275
|
+
if (isUser || structuredContent.payload) return null;
|
|
276
|
+
|
|
277
|
+
// During streaming: if we have sources but no uiTransformation yet,
|
|
278
|
+
// eagerly render a skeleton carousel for product queries so the user
|
|
279
|
+
// sees something immediately instead of waiting for the metadata frame.
|
|
280
|
+
if (isStreaming && !message.uiTransformation && sources && sources.length > 0) {
|
|
281
|
+
const hasProductSource = sources.some(s =>
|
|
282
|
+
s.metadata && Object.keys(s.metadata).some(k =>
|
|
283
|
+
['name','title','price','brand','image','product','sku','category'].includes(k.toLowerCase())
|
|
284
|
+
)
|
|
285
|
+
);
|
|
286
|
+
if (hasProductSource) {
|
|
287
|
+
return (
|
|
288
|
+
<div className="w-full mt-3">
|
|
289
|
+
<VisualizationRenderer
|
|
290
|
+
data={{ type: 'product_carousel', title: 'Recommended Products', description: 'Loading...', data: [] }}
|
|
291
|
+
primaryColor={primaryColor}
|
|
292
|
+
onAddToCart={onAddToCart}
|
|
293
|
+
className="rounded-3xl border border-slate-200/60 dark:border-white/10 bg-white/90 dark:bg-slate-900/80 p-4"
|
|
294
|
+
isStreaming={true}
|
|
295
|
+
/>
|
|
296
|
+
</div>
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (!message.uiTransformation) return null;
|
|
266
302
|
const ui = message.uiTransformation as UITransformationResponse;
|
|
267
303
|
const textContent = (ui.data as { content?: string })?.content ?? '';
|
|
268
304
|
|
|
@@ -294,6 +330,7 @@ export function MessageBubble({
|
|
|
294
330
|
);
|
|
295
331
|
})()}
|
|
296
332
|
|
|
333
|
+
|
|
297
334
|
{/* Product Carousel */}
|
|
298
335
|
{!isUser && !hasRichUI && !shouldSuppressProductCarousel && allProducts.length > 0 && (
|
|
299
336
|
<div className="w-full mt-1">
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
4
|
+
import { Brain, ChevronDown, ChevronRight, Loader2 } from 'lucide-react';
|
|
5
|
+
|
|
6
|
+
interface ThinkingBlockProps {
|
|
7
|
+
thinking: string;
|
|
8
|
+
thinkingMs?: number;
|
|
9
|
+
isStreaming?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function ThinkingBlock({ thinking, thinkingMs, isStreaming = false }: ThinkingBlockProps) {
|
|
13
|
+
const [isExpanded, setIsExpanded] = useState(true);
|
|
14
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
15
|
+
|
|
16
|
+
// Collapse by default when streaming finishes
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (!isStreaming) {
|
|
19
|
+
setIsExpanded(false);
|
|
20
|
+
} else {
|
|
21
|
+
setIsExpanded(true);
|
|
22
|
+
}
|
|
23
|
+
}, [isStreaming]);
|
|
24
|
+
|
|
25
|
+
// Auto-scroll thinking container while streaming
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (isStreaming && isExpanded && containerRef.current) {
|
|
28
|
+
containerRef.current.scrollTop = containerRef.current.scrollHeight;
|
|
29
|
+
}
|
|
30
|
+
}, [thinking, isStreaming, isExpanded]);
|
|
31
|
+
|
|
32
|
+
const durationSec = thinkingMs ? (thinkingMs / 1000).toFixed(1) : null;
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className="w-full max-w-2xl my-1.5 border border-slate-200 dark:border-white/10 rounded-xl bg-slate-50/50 dark:bg-white/5 overflow-hidden transition-all duration-200">
|
|
36
|
+
{/* Header */}
|
|
37
|
+
<button
|
|
38
|
+
onClick={() => setIsExpanded(!isExpanded)}
|
|
39
|
+
className="w-full px-4 py-2.5 flex items-center justify-between text-xs font-medium text-slate-500 dark:text-white/40 hover:bg-slate-100 dark:hover:bg-white/10 transition-colors cursor-pointer select-none"
|
|
40
|
+
>
|
|
41
|
+
<div className="flex items-center gap-2">
|
|
42
|
+
{isStreaming ? (
|
|
43
|
+
<Loader2 className="w-3.5 h-3.5 animate-spin text-slate-400 dark:text-white/30" />
|
|
44
|
+
) : (
|
|
45
|
+
<Brain className="w-3.5 h-3.5 text-slate-400 dark:text-white/40" />
|
|
46
|
+
)}
|
|
47
|
+
<span>
|
|
48
|
+
{isStreaming
|
|
49
|
+
? 'Thinking...'
|
|
50
|
+
: durationSec
|
|
51
|
+
? `Thought for ${durationSec}s`
|
|
52
|
+
: 'Thinking process'}
|
|
53
|
+
</span>
|
|
54
|
+
</div>
|
|
55
|
+
<div>
|
|
56
|
+
{isExpanded ? (
|
|
57
|
+
<ChevronDown className="w-3.5 h-3.5 text-slate-400 dark:text-white/30" />
|
|
58
|
+
) : (
|
|
59
|
+
<ChevronRight className="w-3.5 h-3.5 text-slate-400 dark:text-white/30" />
|
|
60
|
+
)}
|
|
61
|
+
</div>
|
|
62
|
+
</button>
|
|
63
|
+
|
|
64
|
+
{/* Body */}
|
|
65
|
+
{isExpanded && (
|
|
66
|
+
<div
|
|
67
|
+
ref={containerRef}
|
|
68
|
+
className="px-4 pb-3 pt-1 border-t border-slate-200 dark:border-white/10 text-xs font-mono leading-relaxed text-slate-600 dark:text-white/60 bg-slate-50/20 dark:bg-transparent max-h-60 overflow-y-auto whitespace-pre-wrap select-text break-words"
|
|
69
|
+
>
|
|
70
|
+
{thinking}
|
|
71
|
+
</div>
|
|
72
|
+
)}
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -183,7 +183,10 @@ function renderVisualization(
|
|
|
183
183
|
return <TableView data={data as TableData} isStreaming={isStreaming} />;
|
|
184
184
|
case 'product_carousel':
|
|
185
185
|
case 'carousel':
|
|
186
|
+
// Show skeleton while streaming so the user sees an indicator immediately
|
|
187
|
+
if (isStreaming) return <CarouselSkeleton />;
|
|
186
188
|
return <CarouselView data={data as CarouselProduct[]} onAddToCart={onAddToCart} primaryColor={primaryColor} />;
|
|
189
|
+
|
|
187
190
|
case 'text':
|
|
188
191
|
default:
|
|
189
192
|
return <TextView data={data} />;
|
|
@@ -194,7 +197,30 @@ function renderVisualization(
|
|
|
194
197
|
}
|
|
195
198
|
}
|
|
196
199
|
|
|
197
|
-
// ───
|
|
200
|
+
// ─── Carousel Skeleton ───────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
function CarouselSkeleton() {
|
|
203
|
+
return (
|
|
204
|
+
<div className="w-full py-2">
|
|
205
|
+
<div className="mb-3 h-4 w-36 rounded-full bg-slate-200 animate-pulse" />
|
|
206
|
+
<div className="flex gap-3 overflow-hidden">
|
|
207
|
+
{Array.from({ length: 4 }).map((_, i) => (
|
|
208
|
+
<div
|
|
209
|
+
key={i}
|
|
210
|
+
className="flex-shrink-0 w-44 rounded-2xl border border-slate-200 bg-white shadow-sm overflow-hidden animate-pulse"
|
|
211
|
+
>
|
|
212
|
+
<div className="h-28 bg-slate-100" />
|
|
213
|
+
<div className="p-3 space-y-2">
|
|
214
|
+
<div className="h-3 w-28 rounded-full bg-slate-200" />
|
|
215
|
+
<div className="h-3 w-16 rounded-full bg-slate-100" />
|
|
216
|
+
<div className="h-3 w-20 rounded-full bg-emerald-100" />
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
))}
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
198
224
|
|
|
199
225
|
function ChartSkeleton({ type }: { type: 'pie' | 'bar' | 'table' }) {
|
|
200
226
|
if (type === 'pie') {
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Zap,
|
|
4
|
+
Database,
|
|
5
|
+
Layers,
|
|
6
|
+
Search,
|
|
7
|
+
Box,
|
|
8
|
+
Package,
|
|
9
|
+
Activity,
|
|
10
|
+
Hexagon,
|
|
11
|
+
Sparkles,
|
|
12
|
+
Bot,
|
|
13
|
+
Brain,
|
|
14
|
+
Rabbit,
|
|
15
|
+
Code,
|
|
16
|
+
Terminal,
|
|
17
|
+
FileText,
|
|
18
|
+
Puzzle,
|
|
19
|
+
MessageSquare,
|
|
20
|
+
} from 'lucide-react';
|
|
21
|
+
import { ArchitectureCardProps, Snippet, PipelineStep, ProviderPill } from '../types';
|
|
22
|
+
|
|
23
|
+
export const LANDING_PAGE_CONTENT = {
|
|
24
|
+
hero: {
|
|
25
|
+
badge: 'Dynamic & Universal Retrivora AI',
|
|
26
|
+
title: 'The Universal Bridge for Generative AI',
|
|
27
|
+
subtitle: 'Retrivora AI is a vendor-agnostic RAG engine that connects any Document to any LLM and Vector Database in minutes.',
|
|
28
|
+
},
|
|
29
|
+
lifecycle: {
|
|
30
|
+
title: 'The RAG Lifecycle',
|
|
31
|
+
},
|
|
32
|
+
guide: {
|
|
33
|
+
title: 'Interactive Guide',
|
|
34
|
+
subtitle: 'Universal RAG integration for your Next.js applications in minutes.',
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const VECTOR_DATABASES: ProviderPill[] = [
|
|
39
|
+
{ Icon: Zap, label: 'Pinecone' },
|
|
40
|
+
{ Icon: Database, label: 'PostgreSQL' },
|
|
41
|
+
{ Icon: Layers, label: 'MongoDB' },
|
|
42
|
+
{ Icon: Box, label: 'Qdrant' },
|
|
43
|
+
{ Icon: Search, label: 'Milvus' },
|
|
44
|
+
{ Icon: Package, label: 'ChromaDB' },
|
|
45
|
+
{ Icon: Activity, label: 'Redis' },
|
|
46
|
+
{ Icon: Hexagon, label: 'Weaviate' },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
export const AI_MODELS: ProviderPill[] = [
|
|
50
|
+
{ Icon: Rabbit, label: 'Ollama' },
|
|
51
|
+
{ Icon: Sparkles, label: 'OpenAI' },
|
|
52
|
+
{ Icon: Bot, label: 'Anthropic' },
|
|
53
|
+
{ Icon: Brain, label: 'Gemini' },
|
|
54
|
+
{ Icon: Code, label: 'Copilot' },
|
|
55
|
+
{ Icon: Terminal, label: 'LiteLLM' },
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
export const PIPELINE_STEPS: PipelineStep[] = [
|
|
59
|
+
{
|
|
60
|
+
step: '01',
|
|
61
|
+
Icon: FileText,
|
|
62
|
+
title: 'Ingest',
|
|
63
|
+
desc: 'Universal support for PDF, DOCX, CSV, and JSON. Documents are parsed, chunked, and embedded.',
|
|
64
|
+
colors: { from: '#10b981', to: '#14b8a6' },
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
step: '02',
|
|
68
|
+
Icon: Search,
|
|
69
|
+
title: 'Retrieve',
|
|
70
|
+
desc: 'Queries are converted to vectors. Semantic search finds context across any configured Vector DB.',
|
|
71
|
+
colors: { from: '#14b8a6', to: '#06b6d4' },
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
step: '03',
|
|
75
|
+
Icon: Puzzle,
|
|
76
|
+
title: 'Augment',
|
|
77
|
+
desc: 'Retrieved context is injected into the LLM prompt with namespaced project isolation.',
|
|
78
|
+
colors: { from: '#06b6d4', to: '#3b82f6' },
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
step: '04',
|
|
82
|
+
Icon: MessageSquare,
|
|
83
|
+
title: 'Generate',
|
|
84
|
+
desc: 'Providers like OpenAI, Anthropic, or Gemini produce grounded, source-cited responses.',
|
|
85
|
+
colors: { from: '#3b82f6', to: '#6366f1' },
|
|
86
|
+
},
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
export const SNIPPETS: Snippet[] = [
|
|
90
|
+
{
|
|
91
|
+
id: 'pipeline',
|
|
92
|
+
title: 'Retrivora SDK',
|
|
93
|
+
language: 'typescript',
|
|
94
|
+
description: 'Create one server-side SDK instance from config; clients never know which providers are used.',
|
|
95
|
+
code: `import { Retrivora } from '@retrivora-ai/rag-engine/server';
|
|
96
|
+
|
|
97
|
+
const ai = new Retrivora({
|
|
98
|
+
projectId: 'support-app',
|
|
99
|
+
llm: {
|
|
100
|
+
provider: 'openai',
|
|
101
|
+
model: 'gpt-5',
|
|
102
|
+
apiKey: process.env.OPENAI_API_KEY,
|
|
103
|
+
},
|
|
104
|
+
embedding: {
|
|
105
|
+
provider: 'openai',
|
|
106
|
+
model: 'text-embedding-3-small',
|
|
107
|
+
apiKey: process.env.OPENAI_API_KEY,
|
|
108
|
+
},
|
|
109
|
+
vectorDatabase: {
|
|
110
|
+
provider: 'pinecone',
|
|
111
|
+
indexName: 'support-docs',
|
|
112
|
+
options: { apiKey: process.env.PINECONE_API_KEY },
|
|
113
|
+
},
|
|
114
|
+
retrieval: { strategy: 'hybrid', topK: 5 },
|
|
115
|
+
workflow: { type: 'agentic-rag' },
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
await ai.ingest([{
|
|
119
|
+
docId: 'doc-1',
|
|
120
|
+
content: 'Retrivora AI simplifies RAG...',
|
|
121
|
+
metadata: { source: 'docs' }
|
|
122
|
+
}]);
|
|
123
|
+
|
|
124
|
+
const { reply, sources } = await ai.ask('How does Retrivora work?');`,
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
id: 'handlers',
|
|
128
|
+
title: 'Next.js Routes',
|
|
129
|
+
language: 'typescript',
|
|
130
|
+
description: 'Keep secrets and provider code in App Router handlers, then expose simple API endpoints to React.',
|
|
131
|
+
code: `// app/api/chat/route.ts
|
|
132
|
+
import { Retrivora, createStreamHandler } from '@retrivora-ai/rag-engine/server';
|
|
133
|
+
|
|
134
|
+
const ai = new Retrivora({
|
|
135
|
+
projectId: 'support-app',
|
|
136
|
+
llm: { provider: 'openai', model: 'gpt-5', apiKey: process.env.OPENAI_API_KEY },
|
|
137
|
+
embedding: { provider: 'openai', model: 'text-embedding-3-small', apiKey: process.env.OPENAI_API_KEY },
|
|
138
|
+
vectorDatabase: {
|
|
139
|
+
provider: 'pinecone',
|
|
140
|
+
indexName: 'support-docs',
|
|
141
|
+
options: { apiKey: process.env.PINECONE_API_KEY },
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
export const POST = createStreamHandler(ai.config);
|
|
146
|
+
|
|
147
|
+
// app/api/upload/route.ts
|
|
148
|
+
// export const POST = createUploadHandler(ai.config);`,
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
id: 'ui',
|
|
152
|
+
title: 'React Client UI',
|
|
153
|
+
language: 'typescript',
|
|
154
|
+
description: 'Import browser-safe components in Client Components; API keys stay on the server.',
|
|
155
|
+
code: `'use client';
|
|
156
|
+
|
|
157
|
+
import { ConfigProvider, ChatWidget } from '@retrivora-ai/rag-engine';
|
|
158
|
+
|
|
159
|
+
export default function Layout({ children }) {
|
|
160
|
+
return (
|
|
161
|
+
<ConfigProvider
|
|
162
|
+
config={{
|
|
163
|
+
projectId: 'support-app',
|
|
164
|
+
ui: {
|
|
165
|
+
title: 'AI Assistant',
|
|
166
|
+
showSources: true,
|
|
167
|
+
allowUpload: true,
|
|
168
|
+
},
|
|
169
|
+
}}
|
|
170
|
+
>
|
|
171
|
+
{children}
|
|
172
|
+
<ChatWidget position="bottom-right" />
|
|
173
|
+
</ConfigProvider>
|
|
174
|
+
);
|
|
175
|
+
}`,
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
id: 'config',
|
|
179
|
+
title: 'Headless Hook',
|
|
180
|
+
language: 'typescript',
|
|
181
|
+
description: 'Build your own chat surface with the same server-backed route.',
|
|
182
|
+
code: `'use client';
|
|
183
|
+
|
|
184
|
+
import React from 'react';
|
|
185
|
+
import { useRagChat } from '@retrivora-ai/rag-engine';
|
|
186
|
+
|
|
187
|
+
export function AskBox() {
|
|
188
|
+
const [input, setInput] = React.useState('');
|
|
189
|
+
const { messages, send, isLoading } = useRagChat('support-app', {
|
|
190
|
+
apiUrl: '/api/chat',
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<form onSubmit={(event) => {
|
|
195
|
+
event.preventDefault();
|
|
196
|
+
send(input);
|
|
197
|
+
setInput('');
|
|
198
|
+
}}>
|
|
199
|
+
<input value={input} onChange={(event) => setInput(event.target.value)} />
|
|
200
|
+
<button disabled={isLoading}>Ask</button>
|
|
201
|
+
|
|
202
|
+
{messages.map((message) => (
|
|
203
|
+
<p key={message.id}>{message.content}</p>
|
|
204
|
+
))}
|
|
205
|
+
</form>
|
|
206
|
+
);
|
|
207
|
+
}`,
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
id: 'env',
|
|
211
|
+
title: 'Environment',
|
|
212
|
+
language: 'bash',
|
|
213
|
+
description: 'The exact environment variable structure required for auto-configuration.',
|
|
214
|
+
code: `# Project Identity
|
|
215
|
+
RAG_PROJECT_ID=my-project
|
|
216
|
+
|
|
217
|
+
# Vector DB
|
|
218
|
+
VECTOR_DB_PROVIDER=pinecone
|
|
219
|
+
PINECONE_API_KEY=pcsk_...
|
|
220
|
+
VECTOR_DB_INDEX=products
|
|
221
|
+
|
|
222
|
+
# AI Services
|
|
223
|
+
LLM_PROVIDER=openai
|
|
224
|
+
LLM_MODEL=gpt-4o
|
|
225
|
+
OPENAI_API_KEY=sk-...`,
|
|
226
|
+
},
|
|
227
|
+
];
|
|
228
|
+
|
|
229
|
+
export const ARCHITECTURE_CARDS: ArchitectureCardProps[] = [
|
|
230
|
+
{
|
|
231
|
+
icon: <Database className="text-indigo-600" />,
|
|
232
|
+
title: 'Vector Store',
|
|
233
|
+
description: 'Universal support for Pinecone, PGVector, MongoDB, Milvus, Qdrant, and more.',
|
|
234
|
+
badge: 'Vector DB',
|
|
235
|
+
badgeColor: 'bg-indigo-50 dark:bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 border-indigo-200 dark:border-indigo-500/20',
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
icon: <Zap className="text-amber-500" />,
|
|
239
|
+
title: 'Embeddings',
|
|
240
|
+
description: 'Seamlessly switch between OpenAI, Ollama, or custom embedding providers.',
|
|
241
|
+
badge: 'Models',
|
|
242
|
+
badgeColor: 'bg-amber-50 dark:bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-200 dark:border-amber-500/20',
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
icon: <Bot className="text-emerald-500" />,
|
|
246
|
+
title: 'LLM Orchestration',
|
|
247
|
+
description: 'Optimized inference across OpenAI, Anthropic, Gemini, and local LLMs.',
|
|
248
|
+
badge: 'Inference',
|
|
249
|
+
badgeColor: 'bg-emerald-50 dark:bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-200 dark:border-emerald-500/20',
|
|
250
|
+
},
|
|
251
|
+
];
|
|
252
|
+
|
|
253
|
+
export const QUICK_START_STEPS = [
|
|
254
|
+
'Install @retrivora-ai/rag-engine',
|
|
255
|
+
'Configure your .env.local',
|
|
256
|
+
'Mount the API handlers',
|
|
257
|
+
'Drop the ChatWidget'
|
|
258
|
+
];
|
|
259
|
+
|
|
260
|
+
export const API_ENDPOINTS = ['chat', 'health', 'ingest', 'upload'];
|
|
261
|
+
|
|
262
|
+
export const CHAT_SUGGESTIONS = [
|
|
263
|
+
'What can you help me with?',
|
|
264
|
+
'Summarise the key topics',
|
|
265
|
+
'Show me an example'
|
|
266
|
+
];
|
|
267
|
+
|
|
268
|
+
export const BORDER_RADIUS_MAP: Record<string, string> = {
|
|
269
|
+
none: 'rounded-none',
|
|
270
|
+
sm: 'rounded-sm',
|
|
271
|
+
md: 'rounded-md',
|
|
272
|
+
lg: 'rounded-lg',
|
|
273
|
+
xl: 'rounded-xl',
|
|
274
|
+
full: 'rounded-3xl',
|
|
275
|
+
};
|