@retrivora-ai/rag-engine 1.9.7 → 1.9.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/README.md +136 -136
- package/dist/{ILLMProvider-Bhk6zJOK.d.mts → ILLMProvider-DMxLyTdq.d.mts} +59 -4
- package/dist/{ILLMProvider-Bhk6zJOK.d.ts → ILLMProvider-DMxLyTdq.d.ts} +59 -4
- package/dist/handlers/index.d.mts +2 -2
- package/dist/handlers/index.d.ts +2 -2
- package/dist/handlers/index.js +2327 -474
- package/dist/handlers/index.mjs +2329 -473
- package/dist/{index-B9J_XEh0.d.ts → index-CfkqZd2Y.d.ts} +12 -2
- package/dist/{index-C3SVtPYg.d.mts → index-DXd29KMq.d.mts} +27 -52
- package/dist/{index-Bu7T6xgr.d.ts → index-D_bOdJML.d.ts} +27 -52
- package/dist/{index-BJ4cd-t5.d.mts → index-xygonxpW.d.mts} +12 -2
- package/dist/index.css +783 -550
- package/dist/index.d.mts +35 -7
- package/dist/index.d.ts +35 -7
- package/dist/index.js +419 -282
- package/dist/index.mjs +426 -292
- package/dist/server.d.mts +65 -6
- package/dist/server.d.ts +65 -6
- package/dist/server.js +2185 -317
- package/dist/server.mjs +2185 -317
- package/package.json +13 -8
- package/src/app/constants.tsx +37 -7
- package/src/app/page.tsx +2 -0
- package/src/components/ChatWidget.tsx +2 -1
- package/src/components/ChatWindow.tsx +4 -1
- package/src/components/CodeViewer.tsx +19 -14
- package/src/components/MarkdownComponents.tsx +44 -1
- package/src/components/MessageBubble.tsx +162 -50
- package/src/components/constants.tsx +228 -0
- package/src/config/RagConfig.ts +48 -2
- package/src/config/constants.ts +4 -0
- package/src/config/serverConfig.ts +15 -0
- package/src/core/ConfigResolver.ts +38 -6
- package/src/core/DatabaseStorage.ts +469 -0
- package/src/core/LicenseVerifier.ts +260 -0
- package/src/core/MultiAgentCoordinator.ts +239 -0
- package/src/core/Pipeline.ts +151 -18
- package/src/core/Retrivora.ts +7 -0
- package/src/core/VectorPlugin.ts +12 -3
- package/src/core/mcp.ts +261 -0
- package/src/handlers/index.ts +449 -63
- package/src/hooks/useRagChat.ts +96 -42
- package/src/hooks/useStoredMessages.ts +15 -4
- package/src/index.ts +3 -0
- package/src/llm/LLMFactory.ts +9 -1
- package/src/llm/providers/GroqProvider.ts +176 -0
- package/src/llm/providers/QwenProvider.ts +191 -0
- package/src/server.ts +7 -0
- package/src/types/chat.ts +14 -0
- package/src/types/props.ts +12 -0
- package/.env.example +0 -80
- package/LICENSE.txt +0 -21
- package/src/components/AmbientBackground.tsx +0 -29
- package/src/components/ArchitectureCard.tsx +0 -17
- package/src/components/ArchitectureCardsSection.tsx +0 -15
- package/src/components/DocViewer.tsx +0 -103
- package/src/components/Documentation.tsx +0 -121
- package/src/components/Hero.tsx +0 -59
- package/src/components/Lifecycle.tsx +0 -37
- package/src/components/Navbar.tsx +0 -55
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import React from 'react';
|
|
4
|
-
import { Bot, User, ChevronDown, ChevronRight, Activity, Copy, Check } from 'lucide-react';
|
|
4
|
+
import { Bot, User, ChevronDown, ChevronRight, Activity, Copy, Check, ThumbsUp, ThumbsDown, Clock } from 'lucide-react';
|
|
5
5
|
import ReactMarkdown from 'react-markdown';
|
|
6
6
|
import { MessageBubbleProps, Product, UITransformationResponse } from '../types';
|
|
7
7
|
import { SourceCard } from './SourceCard';
|
|
@@ -39,12 +39,111 @@ function normalizePlusSeparatedLists(raw: string): string {
|
|
|
39
39
|
|
|
40
40
|
function stripLeakedAnswerWrappers(raw: string): string {
|
|
41
41
|
return raw
|
|
42
|
-
.replace(/<prose\s+answer(?:\s+in\s+[^>]*)
|
|
42
|
+
.replace(/<prose\s+answer(?:\s+in\s+[^>]*)?>[\s\S]*?<\/prose>/gi, '$1')
|
|
43
43
|
.replace(/<\/?prose(?:\s+answer)?[^>]*>/gi, '')
|
|
44
44
|
.replace(/\n{3,}/g, '\n\n')
|
|
45
45
|
.trim();
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
function formatTime(isoString?: string): string {
|
|
49
|
+
if (!isoString) return '';
|
|
50
|
+
try {
|
|
51
|
+
const d = new Date(isoString);
|
|
52
|
+
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
53
|
+
} catch {
|
|
54
|
+
return '';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ─── Feedback Buttons ─────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
interface FeedbackBarProps {
|
|
61
|
+
messageId: string;
|
|
62
|
+
onFeedback: (messageId: string, rating: 'thumbs_up' | 'thumbs_down') => void;
|
|
63
|
+
primaryColor: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function FeedbackBar({ messageId, onFeedback, primaryColor }: FeedbackBarProps) {
|
|
67
|
+
const [rating, setRating] = React.useState<'thumbs_up' | 'thumbs_down' | null>(null);
|
|
68
|
+
const [submitting, setSubmitting] = React.useState(false);
|
|
69
|
+
|
|
70
|
+
const handleRate = async (r: 'thumbs_up' | 'thumbs_down') => {
|
|
71
|
+
if (rating !== null || submitting) return;
|
|
72
|
+
setSubmitting(true);
|
|
73
|
+
setRating(r);
|
|
74
|
+
try {
|
|
75
|
+
await onFeedback(messageId, r);
|
|
76
|
+
} catch {
|
|
77
|
+
// Rating already set optimistically — keep it
|
|
78
|
+
} finally {
|
|
79
|
+
setSubmitting(false);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const isLocked = rating !== null;
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<div className="flex items-center gap-1">
|
|
87
|
+
{/* Divider */}
|
|
88
|
+
<span className="h-3 w-px bg-slate-200 dark:bg-white/10 mx-1" />
|
|
89
|
+
|
|
90
|
+
{/* Thumbs Up */}
|
|
91
|
+
<button
|
|
92
|
+
onClick={() => handleRate('thumbs_up')}
|
|
93
|
+
disabled={isLocked}
|
|
94
|
+
title={rating === 'thumbs_up' ? 'You liked this' : 'Good response'}
|
|
95
|
+
className={`group relative flex items-center justify-center w-6 h-6 rounded-lg transition-all duration-200 cursor-pointer border ${
|
|
96
|
+
rating === 'thumbs_up'
|
|
97
|
+
? 'bg-emerald-50 dark:bg-emerald-500/10 border-emerald-200 dark:border-emerald-500/25 text-emerald-600 dark:text-emerald-400 scale-110'
|
|
98
|
+
: rating === 'thumbs_down'
|
|
99
|
+
? 'opacity-30 border-transparent text-slate-300 dark:text-white/20 cursor-not-allowed'
|
|
100
|
+
: 'border-transparent text-slate-300 dark:text-white/20 hover:text-emerald-500 dark:hover:text-emerald-400 hover:bg-emerald-50 dark:hover:bg-emerald-500/10 hover:border-emerald-200 dark:hover:border-emerald-500/20 hover:scale-110'
|
|
101
|
+
}`}
|
|
102
|
+
>
|
|
103
|
+
<ThumbsUp className="w-3 h-3" />
|
|
104
|
+
{/* Tooltip */}
|
|
105
|
+
{!isLocked && (
|
|
106
|
+
<span className="absolute -top-8 left-1/2 -translate-x-1/2 px-2 py-1 text-[10px] font-medium bg-slate-800 dark:bg-slate-700 text-white rounded-lg opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none whitespace-nowrap shadow-lg z-10">
|
|
107
|
+
Good response
|
|
108
|
+
</span>
|
|
109
|
+
)}
|
|
110
|
+
</button>
|
|
111
|
+
|
|
112
|
+
{/* Thumbs Down */}
|
|
113
|
+
<button
|
|
114
|
+
onClick={() => handleRate('thumbs_down')}
|
|
115
|
+
disabled={isLocked}
|
|
116
|
+
title={rating === 'thumbs_down' ? 'You disliked this' : 'Bad response'}
|
|
117
|
+
className={`group relative flex items-center justify-center w-6 h-6 rounded-lg transition-all duration-200 cursor-pointer border ${
|
|
118
|
+
rating === 'thumbs_down'
|
|
119
|
+
? 'bg-rose-50 dark:bg-rose-500/10 border-rose-200 dark:border-rose-500/25 text-rose-600 dark:text-rose-400 scale-110'
|
|
120
|
+
: rating === 'thumbs_up'
|
|
121
|
+
? 'opacity-30 border-transparent text-slate-300 dark:text-white/20 cursor-not-allowed'
|
|
122
|
+
: 'border-transparent text-slate-300 dark:text-white/20 hover:text-rose-500 dark:hover:text-rose-400 hover:bg-rose-50 dark:hover:bg-rose-500/10 hover:border-rose-200 dark:hover:border-rose-500/20 hover:scale-110'
|
|
123
|
+
}`}
|
|
124
|
+
>
|
|
125
|
+
<ThumbsDown className="w-3 h-3" />
|
|
126
|
+
{!isLocked && (
|
|
127
|
+
<span className="absolute -top-8 left-1/2 -translate-x-1/2 px-2 py-1 text-[10px] font-medium bg-slate-800 dark:bg-slate-700 text-white rounded-lg opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none whitespace-nowrap shadow-lg z-10">
|
|
128
|
+
Bad response
|
|
129
|
+
</span>
|
|
130
|
+
)}
|
|
131
|
+
</button>
|
|
132
|
+
|
|
133
|
+
{/* Confirmation pill */}
|
|
134
|
+
{isLocked && (
|
|
135
|
+
<span className={`ml-1 text-[10px] font-semibold px-2 py-0.5 rounded-full border transition-all duration-300 ${
|
|
136
|
+
rating === 'thumbs_up'
|
|
137
|
+
? 'text-emerald-600 dark:text-emerald-400 bg-emerald-50 dark:bg-emerald-500/10 border-emerald-200 dark:border-emerald-500/20'
|
|
138
|
+
: 'text-rose-600 dark:text-rose-400 bg-rose-50 dark:bg-rose-500/10 border-rose-200 dark:border-rose-500/20'
|
|
139
|
+
}`}>
|
|
140
|
+
{rating === 'thumbs_up' ? 'Helpful' : 'Not helpful'}
|
|
141
|
+
</span>
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
48
147
|
// ─── Main component ───────────────────────────────────────────────────────────
|
|
49
148
|
|
|
50
149
|
export function MessageBubble({
|
|
@@ -55,6 +154,7 @@ export function MessageBubble({
|
|
|
55
154
|
accentColor = '#8b5cf6',
|
|
56
155
|
onAddToCart,
|
|
57
156
|
viewportSize = 'large',
|
|
157
|
+
onFeedback,
|
|
58
158
|
}: MessageBubbleProps) {
|
|
59
159
|
const isUser = message.role === 'user';
|
|
60
160
|
const isCompact = viewportSize === 'compact';
|
|
@@ -85,7 +185,7 @@ export function MessageBubble({
|
|
|
85
185
|
if (type && !['text', 'table'].includes(type)) return true;
|
|
86
186
|
}
|
|
87
187
|
if (structuredContent.payload) {
|
|
88
|
-
return
|
|
188
|
+
return /\"?(?:view|type|chartType)\"?\s*:\s*\"?(?:chart|carousel|pie|bar|line|product_carousel)\"?/i.test(structuredContent.payload);
|
|
89
189
|
}
|
|
90
190
|
return false;
|
|
91
191
|
}, [message.uiTransformation, structuredContent.payload]);
|
|
@@ -129,18 +229,10 @@ export function MessageBubble({
|
|
|
129
229
|
const processedMarkdown = React.useMemo<string>(() => {
|
|
130
230
|
let raw = structuredContent.payload ? structuredContent.text : (cleanContent || message.content);
|
|
131
231
|
|
|
132
|
-
// 0. Pre-sanitize: strip $ from non-monetary whole integers.
|
|
133
|
-
// Real prices always have a decimal (e.g. $257.00). A bare $168 or $9680811891595 is NOT a price.
|
|
134
232
|
raw = raw.replace(/\$\s*(\d+)(?!\.\d)/g, '$1');
|
|
135
233
|
raw = normalizePlusSeparatedLists(raw);
|
|
136
234
|
raw = stripLeakedAnswerWrappers(raw);
|
|
137
|
-
|
|
138
|
-
// 0a. Strip bold (**...**) from inline field labels.
|
|
139
|
-
// The LLM wraps field names like **Stock** — 168 or **EAN** — 9680811891595 in bold.
|
|
140
|
-
// We want to strip the ** so it renders as plain text, not bold.
|
|
141
235
|
raw = raw.replace(/\*\*([^*\n]+)\*\*(\s*[—\-–:]\s*)/g, '$1$2');
|
|
142
|
-
|
|
143
|
-
// 0b. Remove duplicate lines (LLM sometimes repeats fields like Stock, EAN twice)
|
|
144
236
|
raw = raw
|
|
145
237
|
.split('\n')
|
|
146
238
|
.filter((line, idx, arr) => {
|
|
@@ -148,13 +240,12 @@ export function MessageBubble({
|
|
|
148
240
|
return !trimmed || arr.findIndex(l => l.trim() === trimmed) === idx;
|
|
149
241
|
})
|
|
150
242
|
.join('\n');
|
|
243
|
+
|
|
151
244
|
const hasStructuredUiBlock = Boolean(structuredContent.payload) || /```(?:ui|json|chart)\s*[\s\S]*?"(?:view|chartType|type|data|items|rows|products|results)"\s*:/i.test(raw);
|
|
152
245
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
.replace(/(\|[^\n]*\|)\n\n+(?=\|)/g, '$1\n'); // Remove blank lines between rows
|
|
246
|
+
raw = raw.replace(/([^\n])\n\|/g, '$1\n\n|')
|
|
247
|
+
.replace(/(\|[^\n]*\|)\n\n+(?=\|)/g, '$1\n');
|
|
156
248
|
|
|
157
|
-
// 2. HEALING: Wrap naked JSON blocks
|
|
158
249
|
if (!isStreaming) {
|
|
159
250
|
raw = raw.replace(/(?:^|\n)\s*\{[\s\S]*\}\s*(?:\n|$)/g, (match) => {
|
|
160
251
|
if (match.includes('```')) return match;
|
|
@@ -166,7 +257,6 @@ export function MessageBubble({
|
|
|
166
257
|
});
|
|
167
258
|
}
|
|
168
259
|
|
|
169
|
-
// 2b. Suppress duplicated markdown tables if a UI block is present
|
|
170
260
|
if (hasStructuredUiBlock) {
|
|
171
261
|
raw = stripStructuredUiLabels(stripMarkdownTables(raw))
|
|
172
262
|
.replace(/\n{3,}/g, '\n\n')
|
|
@@ -182,18 +272,25 @@ export function MessageBubble({
|
|
|
182
272
|
[primaryColor, accentColor, isStreaming, onAddToCart, viewportSize],
|
|
183
273
|
);
|
|
184
274
|
|
|
275
|
+
const timeLabel = formatTime(message.createdAt);
|
|
276
|
+
const isCompleted = !isStreaming && message.content;
|
|
277
|
+
|
|
185
278
|
// ── Render ─────────────────────────────────────────────────────────────────
|
|
186
279
|
return (
|
|
187
280
|
<div className={`flex ${isCompact ? 'gap-2' : 'gap-3'} ${isUser ? 'flex-row-reverse' : 'flex-row'} items-start`}>
|
|
188
281
|
{/* Avatar */}
|
|
189
282
|
<div
|
|
190
|
-
className={`flex-shrink-0 ${isCompact ? 'w-7 h-7' : 'w-8 h-8'} rounded-full flex items-center justify-center shadow-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
283
|
+
className={`flex-shrink-0 ${isCompact ? 'w-7 h-7' : 'w-8 h-8'} rounded-full flex items-center justify-center shadow-md transition-all duration-300 ${
|
|
284
|
+
isUser
|
|
285
|
+
? 'text-white'
|
|
286
|
+
: 'bg-gradient-to-br from-slate-100 to-slate-200 dark:from-white/10 dark:to-white/5 text-slate-600 dark:text-white/70 ring-1 ring-slate-200/80 dark:ring-white/10'
|
|
287
|
+
} ${isStreaming && !isUser ? 'ring-2 ring-offset-1 ring-offset-transparent animate-pulse' : ''}`}
|
|
194
288
|
style={isUser ? { background: `linear-gradient(135deg, ${primaryColor}, ${accentColor})` } : {}}
|
|
195
289
|
>
|
|
196
|
-
{isUser
|
|
290
|
+
{isUser
|
|
291
|
+
? <User className={`${isCompact ? 'w-3.5 h-3.5' : 'w-4 h-4'} text-white`} />
|
|
292
|
+
: <Bot className={`${isCompact ? 'w-3.5 h-3.5' : 'w-4 h-4'}`} />
|
|
293
|
+
}
|
|
197
294
|
</div>
|
|
198
295
|
|
|
199
296
|
<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'}`}>
|
|
@@ -208,28 +305,30 @@ export function MessageBubble({
|
|
|
208
305
|
|
|
209
306
|
{/* Bubble */}
|
|
210
307
|
<div
|
|
211
|
-
className={`relative group ${isCompact ? 'px-3 py-2.5 text-[13px]' : 'px-4 py-3 text-sm'} rounded-2xl leading-relaxed shadow
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
308
|
+
className={`relative group ${isCompact ? 'px-3 py-2.5 text-[13px]' : 'px-4 py-3 text-sm'} rounded-2xl leading-relaxed transition-shadow duration-200 ${
|
|
309
|
+
isUser
|
|
310
|
+
? 'text-white rounded-tr-sm shadow-md hover:shadow-lg'
|
|
311
|
+
: 'bg-white dark:bg-white/5 border border-slate-200/80 dark:border-white/10 text-slate-800 dark:text-white/90 rounded-tl-sm shadow-sm hover:shadow-md hover:border-slate-300/60 dark:hover:border-white/15'
|
|
312
|
+
}`}
|
|
215
313
|
style={
|
|
216
314
|
isUser ? { background: `linear-gradient(135deg, ${primaryColor}, ${accentColor})` } : {}
|
|
217
315
|
}
|
|
218
316
|
>
|
|
219
317
|
{/* Floating Copy Button for completed assistant messages */}
|
|
220
|
-
{!isUser &&
|
|
318
|
+
{!isUser && isCompleted && (
|
|
221
319
|
<button
|
|
222
320
|
onClick={handleCopy}
|
|
223
321
|
className={`absolute top-2 right-2 p-1.5 rounded-lg border transition-all duration-200 cursor-pointer ${
|
|
224
322
|
copied
|
|
225
323
|
? 'bg-emerald-500/10 border-emerald-500/20 text-emerald-500 dark:text-emerald-400'
|
|
226
|
-
: 'bg-
|
|
324
|
+
: 'bg-white/60 dark:bg-white/5 hover:bg-white dark:hover:bg-white/10 border-slate-200/60 dark:border-white/10 text-slate-300 dark:text-white/20 hover:text-slate-600 dark:hover:text-white/60 hover:border-slate-300 dark:hover:border-white/20'
|
|
227
325
|
} opacity-0 group-hover:opacity-100 backdrop-blur-sm shadow-sm scale-95 group-hover:scale-100`}
|
|
228
|
-
title={copied ?
|
|
326
|
+
title={copied ? 'Copied!' : 'Copy response'}
|
|
229
327
|
>
|
|
230
328
|
{copied ? <Check className="w-3.5 h-3.5" /> : <Copy className="w-3.5 h-3.5" />}
|
|
231
329
|
</button>
|
|
232
330
|
)}
|
|
331
|
+
|
|
233
332
|
{isStreaming && !message.content ? (
|
|
234
333
|
<span className="flex items-center gap-1 py-1">
|
|
235
334
|
<span className="w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:0ms]" />
|
|
@@ -255,28 +354,20 @@ export function MessageBubble({
|
|
|
255
354
|
</ReactMarkdown>
|
|
256
355
|
)}
|
|
257
356
|
|
|
258
|
-
{/* Fix: Always show streaming cursor when isStreaming is true */}
|
|
259
357
|
{isStreaming && (
|
|
260
|
-
<span
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
accentColor={accentColor}
|
|
265
|
-
glow={false} />
|
|
266
|
-
Thinking...
|
|
267
|
-
</span>
|
|
358
|
+
<span
|
|
359
|
+
className="inline-block w-2.5 h-4 ml-1.5 animate-[pulse_1.2s_infinite] align-middle rounded-sm"
|
|
360
|
+
style={{ backgroundColor: primaryColor }}
|
|
361
|
+
/>
|
|
268
362
|
)}
|
|
269
363
|
</div>
|
|
270
364
|
)}
|
|
271
365
|
</div>
|
|
272
366
|
|
|
273
|
-
{/* Auto-generated visualization
|
|
367
|
+
{/* Auto-generated visualization */}
|
|
274
368
|
{(() => {
|
|
275
369
|
if (isUser || structuredContent.payload) return null;
|
|
276
370
|
|
|
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
371
|
if (isStreaming && !message.uiTransformation && sources && sources.length > 0) {
|
|
281
372
|
const hasProductSource = sources.some(s =>
|
|
282
373
|
s.metadata && Object.keys(s.metadata).some(k =>
|
|
@@ -301,7 +392,7 @@ export function MessageBubble({
|
|
|
301
392
|
if (!message.uiTransformation) return null;
|
|
302
393
|
const ui = message.uiTransformation as UITransformationResponse;
|
|
303
394
|
const textContent = (ui.data as { content?: string })?.content ?? '';
|
|
304
|
-
|
|
395
|
+
|
|
305
396
|
const isNegativeOrFallbackText =
|
|
306
397
|
textContent.toLowerCase().includes('cannot answer') ||
|
|
307
398
|
textContent.toLowerCase().includes('no relevant data') ||
|
|
@@ -315,7 +406,7 @@ export function MessageBubble({
|
|
|
315
406
|
!['text', 'table'].includes(ui.type) ||
|
|
316
407
|
(ui.type === 'text' && !hasRichUI && allProducts.length === 0 && textContent &&
|
|
317
408
|
!processedMarkdown.toLowerCase().includes(textContent.trim().toLowerCase())));
|
|
318
|
-
|
|
409
|
+
|
|
319
410
|
if (!shouldShow) return null;
|
|
320
411
|
return (
|
|
321
412
|
<div className="w-full mt-3">
|
|
@@ -330,7 +421,6 @@ export function MessageBubble({
|
|
|
330
421
|
);
|
|
331
422
|
})()}
|
|
332
423
|
|
|
333
|
-
|
|
334
424
|
{/* Product Carousel */}
|
|
335
425
|
{!isUser && !hasRichUI && !shouldSuppressProductCarousel && allProducts.length > 0 && (
|
|
336
426
|
<div className="w-full mt-1">
|
|
@@ -342,28 +432,50 @@ export function MessageBubble({
|
|
|
342
432
|
</div>
|
|
343
433
|
)}
|
|
344
434
|
|
|
345
|
-
{/* Footer actions: Sources
|
|
435
|
+
{/* Footer actions: Sources, Trace, Timestamp & Feedback */}
|
|
346
436
|
{!isUser && (
|
|
347
|
-
<div className=
|
|
437
|
+
<div className={`flex items-center gap-3 mt-1.5 w-full flex-wrap ${isCompact ? 'gap-2' : 'gap-3'}`}>
|
|
438
|
+
{/* Sources toggle */}
|
|
348
439
|
{sources && sources.length > 0 && (
|
|
349
440
|
<button
|
|
350
441
|
onClick={() => setShowSources(s => !s)}
|
|
351
|
-
className="text-[11px] text-indigo-400 hover:text-indigo-300 transition-colors flex items-center gap-1"
|
|
442
|
+
className="text-[11px] text-indigo-400 hover:text-indigo-300 dark:text-indigo-400/70 dark:hover:text-indigo-300 transition-colors flex items-center gap-1 font-medium"
|
|
352
443
|
>
|
|
353
444
|
{showSources ? <ChevronDown className="w-3 h-3" /> : <ChevronRight className="w-3 h-3" />}
|
|
354
|
-
{sources.length} source{sources.length !== 1 ? 's' : ''}
|
|
445
|
+
{sources.length} source{sources.length !== 1 ? 's' : ''}
|
|
355
446
|
</button>
|
|
356
447
|
)}
|
|
357
448
|
|
|
449
|
+
{/* Trace toggle */}
|
|
358
450
|
{message.trace && (
|
|
359
451
|
<button
|
|
360
452
|
onClick={() => setShowTrace(t => !t)}
|
|
361
|
-
className="text-[11px] text-emerald-500 hover:text-emerald-400 transition-colors flex items-center gap-1"
|
|
453
|
+
className="text-[11px] text-emerald-500/70 hover:text-emerald-400 dark:text-emerald-500/50 dark:hover:text-emerald-400 transition-colors flex items-center gap-1 font-medium"
|
|
362
454
|
>
|
|
363
455
|
<Activity className="w-3 h-3" />
|
|
364
|
-
{showTrace ? 'Hide
|
|
456
|
+
{showTrace ? 'Hide trace' : 'Trace'}
|
|
365
457
|
</button>
|
|
366
458
|
)}
|
|
459
|
+
|
|
460
|
+
{/* Spacer */}
|
|
461
|
+
<div className="flex-1" />
|
|
462
|
+
|
|
463
|
+
{/* Timestamp */}
|
|
464
|
+
{timeLabel && isCompleted && (
|
|
465
|
+
<span className="flex items-center gap-1 text-[10px] text-slate-300 dark:text-white/20 font-medium select-none">
|
|
466
|
+
<Clock className="w-2.5 h-2.5" />
|
|
467
|
+
{timeLabel}
|
|
468
|
+
</span>
|
|
469
|
+
)}
|
|
470
|
+
|
|
471
|
+
{/* Feedback thumbs — only for completed, non-user messages with an ID */}
|
|
472
|
+
{isCompleted && onFeedback && message.id && (
|
|
473
|
+
<FeedbackBar
|
|
474
|
+
messageId={message.id}
|
|
475
|
+
onFeedback={onFeedback}
|
|
476
|
+
primaryColor={primaryColor}
|
|
477
|
+
/>
|
|
478
|
+
)}
|
|
367
479
|
</div>
|
|
368
480
|
)}
|
|
369
481
|
|
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
FileText,
|
|
18
18
|
Puzzle,
|
|
19
19
|
MessageSquare,
|
|
20
|
+
Cpu,
|
|
21
|
+
Cloud,
|
|
20
22
|
} from 'lucide-react';
|
|
21
23
|
import { ArchitectureCardProps, Snippet, PipelineStep, ProviderPill } from '../types';
|
|
22
24
|
|
|
@@ -51,6 +53,8 @@ export const AI_MODELS: ProviderPill[] = [
|
|
|
51
53
|
{ Icon: Sparkles, label: 'OpenAI' },
|
|
52
54
|
{ Icon: Bot, label: 'Anthropic' },
|
|
53
55
|
{ Icon: Brain, label: 'Gemini' },
|
|
56
|
+
{ Icon: Cpu, label: 'Groq' },
|
|
57
|
+
{ Icon: Cloud, label: 'Qwen' },
|
|
54
58
|
{ Icon: Code, label: 'Copilot' },
|
|
55
59
|
{ Icon: Terminal, label: 'LiteLLM' },
|
|
56
60
|
];
|
|
@@ -273,3 +277,227 @@ export const BORDER_RADIUS_MAP: Record<string, string> = {
|
|
|
273
277
|
xl: 'rounded-xl',
|
|
274
278
|
full: 'rounded-3xl',
|
|
275
279
|
};
|
|
280
|
+
|
|
281
|
+
export interface RagPatternStep {
|
|
282
|
+
label: string;
|
|
283
|
+
type: 'input' | 'process' | 'database' | 'model' | 'output';
|
|
284
|
+
color: string;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export interface RagPattern {
|
|
288
|
+
id: string;
|
|
289
|
+
name: string;
|
|
290
|
+
tagline: string;
|
|
291
|
+
description: string;
|
|
292
|
+
pros: string[];
|
|
293
|
+
cons: string[];
|
|
294
|
+
snippet: string;
|
|
295
|
+
color: string;
|
|
296
|
+
flowSteps: RagPatternStep[];
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export const RAG_PATTERNS: RagPattern[] = [
|
|
300
|
+
{
|
|
301
|
+
id: 'naive',
|
|
302
|
+
name: 'Naive RAG',
|
|
303
|
+
tagline: 'Standard semantic search pipeline',
|
|
304
|
+
description: 'The foundation pattern: splits documents into text chunks, embeddings are stored in a vector database, and relevant context is fetched via direct query similarity to augment the model prompt.',
|
|
305
|
+
pros: ['Very low latency', 'Low resource cost', 'Simple implementation'],
|
|
306
|
+
cons: ['Struggles with complex multi-hop queries', 'No ranking optimization', 'Can return irrelevant context chunks'],
|
|
307
|
+
color: '#d97706', // Amber/Brown
|
|
308
|
+
snippet: `import { Retrivora } from '@retrivora-ai/rag-engine/server';
|
|
309
|
+
|
|
310
|
+
const ai = new Retrivora({
|
|
311
|
+
projectId: 'naive-rag-demo',
|
|
312
|
+
vectorDatabase: {
|
|
313
|
+
provider: 'pinecone',
|
|
314
|
+
indexName: 'knowledge-base',
|
|
315
|
+
},
|
|
316
|
+
workflow: {
|
|
317
|
+
type: 'simple-rag'
|
|
318
|
+
}
|
|
319
|
+
});`,
|
|
320
|
+
flowSteps: [
|
|
321
|
+
{ label: 'Documents', type: 'input', color: 'bg-amber-100 text-amber-800 dark:bg-amber-950/40 dark:text-amber-300' },
|
|
322
|
+
{ label: 'Chunking', type: 'process', color: 'bg-amber-50 text-amber-700 dark:bg-amber-900/20 dark:text-amber-400' },
|
|
323
|
+
{ label: 'Embedding Model', type: 'model', color: 'bg-amber-100 text-amber-900 dark:bg-amber-800/30 dark:text-amber-200' },
|
|
324
|
+
{ label: 'Vector Store', type: 'database', color: 'bg-amber-200 text-amber-950 dark:bg-amber-700/40 dark:text-amber-100' },
|
|
325
|
+
{ label: 'Context Prompt', type: 'process', color: 'bg-amber-50 text-amber-700 dark:bg-amber-900/20 dark:text-amber-400' },
|
|
326
|
+
{ label: 'LLM Generator', type: 'model', color: 'bg-indigo-100 text-indigo-900 dark:bg-indigo-800/30 dark:text-indigo-200' },
|
|
327
|
+
]
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
id: 'retrieve-and-rerank',
|
|
331
|
+
name: 'Retrieve-and-rerank',
|
|
332
|
+
tagline: 'High-precision contextual relevance',
|
|
333
|
+
description: 'Enhances standard RAG by fetching a larger pool of vector candidates and feeding them through a cross-encoder Reranker model. The reranker scores context chunks dynamically relative to query intent, filtering noise.',
|
|
334
|
+
pros: ['Extremely high context precision', 'Better generation accuracy', 'Mitigates hallucination rates'],
|
|
335
|
+
cons: ['Adds slight latency bottleneck (~100-300ms)', 'Higher computational requirements'],
|
|
336
|
+
color: '#2563eb', // Blue
|
|
337
|
+
snippet: `import { Retrivora } from '@retrivora-ai/rag-engine/server';
|
|
338
|
+
|
|
339
|
+
const ai = new Retrivora({
|
|
340
|
+
projectId: 'rerank-rag-demo',
|
|
341
|
+
vectorDatabase: {
|
|
342
|
+
provider: 'pinecone',
|
|
343
|
+
indexName: 'knowledge-base',
|
|
344
|
+
},
|
|
345
|
+
retrieval: {
|
|
346
|
+
strategy: 'contextual-compression',
|
|
347
|
+
useReranking: true, // Enables cross-encoder re-evaluation
|
|
348
|
+
topK: 5,
|
|
349
|
+
}
|
|
350
|
+
});`,
|
|
351
|
+
flowSteps: [
|
|
352
|
+
{ label: 'Query Input', type: 'input', color: 'bg-blue-100 text-blue-800 dark:bg-blue-950/40 dark:text-blue-300' },
|
|
353
|
+
{ label: 'Vector Retrieval', type: 'database', color: 'bg-blue-200 text-blue-950 dark:bg-blue-700/40 dark:text-blue-100' },
|
|
354
|
+
{ label: 'Reranker Model', type: 'model', color: 'bg-indigo-100 text-indigo-950 dark:bg-indigo-700/40 dark:text-indigo-100' },
|
|
355
|
+
{ label: 'Filtered Context', type: 'process', color: 'bg-blue-50 text-blue-700 dark:bg-blue-900/20 dark:text-blue-400' },
|
|
356
|
+
{ label: 'Generative Model', type: 'model', color: 'bg-violet-100 text-violet-900 dark:bg-violet-800/30 dark:text-violet-200' },
|
|
357
|
+
]
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
id: 'multimodal',
|
|
361
|
+
name: 'Multimodal RAG',
|
|
362
|
+
tagline: 'Search and reason across text and media',
|
|
363
|
+
description: 'Ingests both unstructured text documents and visual/audio/structured assets. It queries multi-modal embeddings, feeding retrieved visuals and matched context fragments to multimodal LLMs (like Gemini or Claude) to support diagrams, tables, and media outputs.',
|
|
364
|
+
pros: ['Handles images, PDFs, charts, audio', 'Deeper context retrieval', 'Generates visual answers'],
|
|
365
|
+
cons: ['Multimodal embeddings are computationally expensive', 'Higher token payload sizes'],
|
|
366
|
+
color: '#7c3aed', // Purple
|
|
367
|
+
snippet: `import { Retrivora } from '@retrivora-ai/rag-engine/server';
|
|
368
|
+
|
|
369
|
+
const ai = new Retrivora({
|
|
370
|
+
projectId: 'multimodal-rag-demo',
|
|
371
|
+
llm: {
|
|
372
|
+
provider: 'gemini',
|
|
373
|
+
model: 'gemini-2.5-flash', // Multimodal reasoning model
|
|
374
|
+
},
|
|
375
|
+
embedding: {
|
|
376
|
+
provider: 'gemini',
|
|
377
|
+
model: 'text-embedding-004',
|
|
378
|
+
},
|
|
379
|
+
workflow: {
|
|
380
|
+
type: 'multimodal-rag'
|
|
381
|
+
}
|
|
382
|
+
});`,
|
|
383
|
+
flowSteps: [
|
|
384
|
+
{ label: 'Multi-modal Docs', type: 'input', color: 'bg-purple-100 text-purple-800 dark:bg-purple-950/40 dark:text-purple-300' },
|
|
385
|
+
{ label: 'Multi-modal Embed', type: 'model', color: 'bg-purple-50 text-purple-700 dark:bg-purple-900/20 dark:text-purple-400' },
|
|
386
|
+
{ label: 'Vector Store', type: 'database', color: 'bg-purple-200 text-purple-950 dark:bg-purple-700/40 dark:text-purple-100' },
|
|
387
|
+
{ label: 'Media Context', type: 'process', color: 'bg-purple-100 text-purple-950 dark:bg-purple-800/40 dark:text-purple-100' },
|
|
388
|
+
{ label: 'Multimodal LLM', type: 'model', color: 'bg-violet-100 text-violet-900 dark:bg-violet-800/30 dark:text-violet-200' },
|
|
389
|
+
]
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
id: 'graph',
|
|
393
|
+
name: 'Graph RAG',
|
|
394
|
+
tagline: 'Entity-relationship semantic reasoning',
|
|
395
|
+
description: 'Builds a knowledge graph structure from raw documents using an LLM extractor. Entity-to-entity and entity-to-document relationships are resolved, allowing queries to retrieve structured semantic contexts (nodes & relationships) to resolve structural, relational, and hierarchy-based questions.',
|
|
396
|
+
pros: ['Excellent for tracing connections', 'Resolves hierarchy and relationships', 'Grounded fact validation'],
|
|
397
|
+
cons: ['Heavy write-time pipeline ingestion cost', 'Requires graph database setup'],
|
|
398
|
+
color: '#059669', // Emerald
|
|
399
|
+
snippet: `import { Retrivora } from '@retrivora-ai/rag-engine/server';
|
|
400
|
+
|
|
401
|
+
const ai = new Retrivora({
|
|
402
|
+
projectId: 'graph-rag-demo',
|
|
403
|
+
graphDb: {
|
|
404
|
+
provider: 'neo4j',
|
|
405
|
+
options: { uri: 'neo4j://localhost:7687' },
|
|
406
|
+
},
|
|
407
|
+
workflow: {
|
|
408
|
+
type: 'graph-rag'
|
|
409
|
+
}
|
|
410
|
+
});`,
|
|
411
|
+
flowSteps: [
|
|
412
|
+
{ label: 'Documents', type: 'input', color: 'bg-emerald-100 text-emerald-800 dark:bg-emerald-950/40 dark:text-emerald-300' },
|
|
413
|
+
{ label: 'LLM Extractor', type: 'model', color: 'bg-emerald-50 text-emerald-700 dark:bg-emerald-900/20 dark:text-emerald-400' },
|
|
414
|
+
{ label: 'Graph Database', type: 'database', color: 'bg-emerald-200 text-emerald-950 dark:bg-emerald-700/40 dark:text-emerald-100' },
|
|
415
|
+
{ label: 'Relation Search', type: 'process', color: 'bg-emerald-100 text-emerald-950 dark:bg-emerald-800/40 dark:text-emerald-100' },
|
|
416
|
+
{ label: 'LLM Reasoner', type: 'model', color: 'bg-indigo-100 text-indigo-900 dark:bg-indigo-800/30 dark:text-indigo-200' },
|
|
417
|
+
]
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
id: 'hybrid',
|
|
421
|
+
name: 'Hybrid RAG',
|
|
422
|
+
tagline: 'Best of both worlds: Vector + Graph',
|
|
423
|
+
description: 'Queries both a vector database for semantic match proximity and a graph database for topological linkages. It joins vector search results and sub-graph entity contexts into a unified prompt, resolving complex interconnected schemas.',
|
|
424
|
+
pros: ['Unparalleled query coverage', 'Combines semantic proximity and graph relationships', 'Robust grounding'],
|
|
425
|
+
cons: ['Most complex RAG configuration', 'Double database query overhead'],
|
|
426
|
+
color: '#d97706', // Orange
|
|
427
|
+
snippet: `import { Retrivora } from '@retrivora-ai/rag-engine/server';
|
|
428
|
+
|
|
429
|
+
const ai = new Retrivora({
|
|
430
|
+
projectId: 'hybrid-rag-demo',
|
|
431
|
+
vectorDatabase: { provider: 'mongodb', indexName: 'default' },
|
|
432
|
+
graphDb: { provider: 'neo4j', options: { uri: 'neo4j://...' } },
|
|
433
|
+
workflow: {
|
|
434
|
+
type: 'hybrid-rag'
|
|
435
|
+
}
|
|
436
|
+
});`,
|
|
437
|
+
flowSteps: [
|
|
438
|
+
{ label: 'Query', type: 'input', color: 'bg-amber-100 text-amber-800 dark:bg-amber-950/40 dark:text-amber-300' },
|
|
439
|
+
{ label: 'Vector Store', type: 'database', color: 'bg-orange-100 text-orange-950 dark:bg-orange-700/40 dark:text-orange-100' },
|
|
440
|
+
{ label: 'Graph Database', type: 'database', color: 'bg-emerald-100 text-emerald-950 dark:bg-emerald-700/40 dark:text-emerald-100' },
|
|
441
|
+
{ label: 'Unified Context', type: 'process', color: 'bg-amber-50 text-amber-700 dark:bg-amber-900/20 dark:text-amber-400' },
|
|
442
|
+
{ label: 'LLM Reasoner', type: 'model', color: 'bg-indigo-100 text-indigo-900 dark:bg-indigo-800/30 dark:text-indigo-200' },
|
|
443
|
+
]
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
id: 'agentic-router',
|
|
447
|
+
name: 'Agentic RAG (Router)',
|
|
448
|
+
tagline: 'Dynamic intent-based query routing',
|
|
449
|
+
description: 'Uses an intelligent fast router agent to analyze the intent of the incoming query. The router dynamically decides whether to fetch data from the Vector DB, query a Graph DB, fallback to web search, or trigger external API tools.',
|
|
450
|
+
pros: ['Highly adaptive workflow', 'Saves tokens on simple queries', 'Automatically invokes external API tools'],
|
|
451
|
+
cons: ['Adds slight routing classification latency', 'Dependent on router accuracy'],
|
|
452
|
+
color: '#0891b2', // Cyan
|
|
453
|
+
snippet: `import { Retrivora } from '@retrivora-ai/rag-engine/server';
|
|
454
|
+
|
|
455
|
+
const ai = new Retrivora({
|
|
456
|
+
projectId: 'agentic-router-demo',
|
|
457
|
+
workflow: {
|
|
458
|
+
type: 'agentic-router'
|
|
459
|
+
}
|
|
460
|
+
});`,
|
|
461
|
+
flowSteps: [
|
|
462
|
+
{ label: 'User Query', type: 'input', color: 'bg-cyan-100 text-cyan-800 dark:bg-cyan-950/40 dark:text-cyan-300' },
|
|
463
|
+
{ label: 'Router Agent', type: 'model', color: 'bg-indigo-100 text-indigo-900 dark:bg-indigo-800/30 dark:text-indigo-200' },
|
|
464
|
+
{ label: 'Vector / Graph / Tools', type: 'database', color: 'bg-cyan-200 text-cyan-950 dark:bg-cyan-700/40 dark:text-cyan-100' },
|
|
465
|
+
{ label: 'Selected Context', type: 'process', color: 'bg-cyan-50 text-cyan-700 dark:bg-cyan-900/20 dark:text-cyan-400' },
|
|
466
|
+
{ label: 'LLM Generator', type: 'model', color: 'bg-violet-100 text-violet-900 dark:bg-violet-800/30 dark:text-violet-200' },
|
|
467
|
+
]
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
id: 'multi-agent',
|
|
471
|
+
name: 'Agent RAG (Multi-Agent)',
|
|
472
|
+
tagline: 'Collaborative swarm-based problem solving',
|
|
473
|
+
description: 'Deploys a supervisor agent that coordinates a group of specialized worker agents (e.g., search agent, tool executor agent, web crawler agent). Agents collaborate, pass information, check each other\'s facts, and compile a final answer.',
|
|
474
|
+
pros: ['Excellent for complex research tasks', 'Self-correcting query pipelines', 'Multi-step action sequences'],
|
|
475
|
+
cons: ['High latency (requires multiple LLM cycles)', 'Higher cost per query', 'Complex state synchronization'],
|
|
476
|
+
color: '#ec4899', // Pink
|
|
477
|
+
snippet: `import { Retrivora } from '@retrivora-ai/rag-engine/server';
|
|
478
|
+
|
|
479
|
+
const ai = new Retrivora({
|
|
480
|
+
projectId: 'multi-agent-demo',
|
|
481
|
+
workflow: {
|
|
482
|
+
type: 'multi-agent'
|
|
483
|
+
},
|
|
484
|
+
mcpServers: [
|
|
485
|
+
{
|
|
486
|
+
name: 'search-engine',
|
|
487
|
+
transport: 'stdio',
|
|
488
|
+
command: 'npx',
|
|
489
|
+
args: ['-y', '@modelcontextprotocol/server-web-search']
|
|
490
|
+
}
|
|
491
|
+
]
|
|
492
|
+
});`,
|
|
493
|
+
flowSteps: [
|
|
494
|
+
{ label: 'Goal Query', type: 'input', color: 'bg-pink-100 text-pink-800 dark:bg-pink-950/40 dark:text-pink-300' },
|
|
495
|
+
{ label: 'Supervisor Agent', type: 'model', color: 'bg-pink-200 text-pink-950 dark:bg-pink-800/30 dark:text-pink-100' },
|
|
496
|
+
{ label: 'Specialist Agents', type: 'model', color: 'bg-indigo-100 text-indigo-900 dark:bg-indigo-900/30 dark:text-indigo-200' },
|
|
497
|
+
{ label: 'Tools / Vector DBs', type: 'database', color: 'bg-pink-200 text-pink-950 dark:bg-pink-700/40 dark:text-pink-100' },
|
|
498
|
+
{ label: 'Consensus Context', type: 'process', color: 'bg-pink-50 text-pink-700 dark:bg-pink-900/20 dark:text-pink-400' },
|
|
499
|
+
{ label: 'Final Output', type: 'output', color: 'bg-indigo-600 text-white shadow-sm' },
|
|
500
|
+
]
|
|
501
|
+
}
|
|
502
|
+
];
|
|
503
|
+
|