@retrivora-ai/rag-engine 1.9.6 → 1.9.8

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.
Files changed (62) hide show
  1. package/README.md +130 -113
  2. package/dist/{ILLMProvider-DNhyOYoK.d.mts → ILLMProvider-B8ROITYK.d.mts} +59 -8
  3. package/dist/{ILLMProvider-DNhyOYoK.d.ts → ILLMProvider-B8ROITYK.d.ts} +59 -8
  4. package/dist/handlers/index.d.mts +2 -2
  5. package/dist/handlers/index.d.ts +2 -2
  6. package/dist/handlers/index.js +2376 -489
  7. package/dist/handlers/index.mjs +2372 -489
  8. package/dist/{index-CjQdL0cX.d.ts → index-B8PbEFSY.d.mts} +17 -3
  9. package/dist/{index-CHL1jdYm.d.mts → index-BCPKUAVL.d.ts} +33 -41
  10. package/dist/{index-Hgbwl9X4.d.ts → index-CrxCy36A.d.mts} +33 -41
  11. package/dist/{index-C9v7-tWd.d.mts → index-DNvoi-sV.d.ts} +17 -3
  12. package/dist/index.css +695 -203
  13. package/dist/index.d.mts +37 -7
  14. package/dist/index.d.ts +37 -7
  15. package/dist/index.js +1197 -286
  16. package/dist/index.mjs +1221 -297
  17. package/dist/server.d.mts +62 -6
  18. package/dist/server.d.ts +62 -6
  19. package/dist/server.js +2526 -574
  20. package/dist/server.mjs +2517 -573
  21. package/package.json +12 -10
  22. package/src/app/constants.tsx +207 -212
  23. package/src/app/layout.tsx +4 -28
  24. package/src/app/types.ts +17 -17
  25. package/src/components/AmbientBackground.tsx +10 -10
  26. package/src/components/ArchitectureCard.tsx +43 -7
  27. package/src/components/ArchitectureCardsSection.tsx +37 -4
  28. package/src/components/ChatWidget.tsx +4 -1
  29. package/src/components/ChatWindow.tsx +9 -2
  30. package/src/components/CodeViewer.tsx +19 -14
  31. package/src/components/DocViewer.tsx +75 -15
  32. package/src/components/Documentation.tsx +111 -28
  33. package/src/components/Hero.tsx +103 -20
  34. package/src/components/Lifecycle.tsx +65 -25
  35. package/src/components/MarkdownComponents.tsx +44 -1
  36. package/src/components/MessageBubble.tsx +162 -50
  37. package/src/components/constants.tsx +279 -0
  38. package/src/config/RagConfig.ts +56 -10
  39. package/src/config/constants.ts +5 -0
  40. package/src/config/serverConfig.ts +15 -0
  41. package/src/core/ConfigResolver.ts +30 -25
  42. package/src/core/DatabaseStorage.ts +469 -0
  43. package/src/core/LicenseVerifier.ts +154 -0
  44. package/src/core/MultiAgentCoordinator.ts +239 -0
  45. package/src/core/Pipeline.ts +148 -16
  46. package/src/core/ProviderRegistry.ts +5 -5
  47. package/src/core/Retrivora.ts +52 -6
  48. package/src/core/VectorPlugin.ts +74 -11
  49. package/src/core/mcp.ts +261 -0
  50. package/src/exceptions/index.ts +52 -0
  51. package/src/handlers/index.ts +504 -47
  52. package/src/hooks/useRagChat.ts +100 -43
  53. package/src/hooks/useStoredMessages.ts +15 -4
  54. package/src/index.ts +7 -0
  55. package/src/llm/LLMFactory.ts +15 -6
  56. package/src/llm/providers/GroqProvider.ts +176 -0
  57. package/src/llm/providers/QwenProvider.ts +191 -0
  58. package/src/server.ts +23 -13
  59. package/src/types/chat.ts +16 -0
  60. package/src/types/props.ts +50 -1
  61. package/.env.example +0 -80
  62. package/LICENSE.txt +0 -21
@@ -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+[^>]*)?>\s*([\s\S]*?)\s*<\/prose>/gi, '$1')
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 /"?(?:view|type|chartType)"?\s*:\s*"?(?:chart|carousel|pie|bar|line|product_carousel)"?/i.test(structuredContent.payload);
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
- // 1. Structural fixes
154
- raw = raw.replace(/([^\n])\n\|/g, '$1\n\n|') // Ensure blank line before table
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-lg ${isUser
191
- ? 'text-white'
192
- : 'bg-slate-100 dark:bg-white/10 text-slate-700 dark:text-white/80'
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 ? <User className={`${isCompact ? 'w-3.5 h-3.5' : 'w-4 h-4'} text-white`} /> : <Bot className={`${isCompact ? 'w-3.5 h-3.5' : 'w-4 h-4'}`} />}
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-sm dark:shadow-lg ${isUser
212
- ? 'text-white rounded-tr-sm'
213
- : 'bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 text-slate-800 dark:text-white/90 rounded-tl-sm'
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 && !isStreaming && message.content && (
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-slate-500/5 hover:bg-slate-500/10 border-slate-200 dark:border-white/10 text-slate-400 dark:text-white/30 hover:text-slate-600 dark:hover:text-white/60'
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 ? "Copied!" : "Copy response"}
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 className="inline-flex items-center gap-1.5 ml-2 text-emerald-500 animate-pulse align-middle text-xs font-medium">
261
- <HourglassLoader
262
- size={18}
263
- primaryColor={primaryColor}
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 — show eagerly during streaming if sources are available */}
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 and Trace */}
435
+ {/* Footer actions: Sources, Trace, Timestamp & Feedback */}
346
436
  {!isUser && (
347
- <div className="flex items-center gap-4 mt-1 w-full flex-wrap">
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' : ''} used
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 Trace' : 'Trace'}
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
 
@@ -0,0 +1,279 @@
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
+ Cpu,
21
+ Cloud,
22
+ } from 'lucide-react';
23
+ import { ArchitectureCardProps, Snippet, PipelineStep, ProviderPill } from '../types';
24
+
25
+ export const LANDING_PAGE_CONTENT = {
26
+ hero: {
27
+ badge: 'Dynamic & Universal Retrivora AI',
28
+ title: 'The Universal Bridge for Generative AI',
29
+ subtitle: 'Retrivora AI is a vendor-agnostic RAG engine that connects any Document to any LLM and Vector Database in minutes.',
30
+ },
31
+ lifecycle: {
32
+ title: 'The RAG Lifecycle',
33
+ },
34
+ guide: {
35
+ title: 'Interactive Guide',
36
+ subtitle: 'Universal RAG integration for your Next.js applications in minutes.',
37
+ }
38
+ };
39
+
40
+ export const VECTOR_DATABASES: ProviderPill[] = [
41
+ { Icon: Zap, label: 'Pinecone' },
42
+ { Icon: Database, label: 'PostgreSQL' },
43
+ { Icon: Layers, label: 'MongoDB' },
44
+ { Icon: Box, label: 'Qdrant' },
45
+ { Icon: Search, label: 'Milvus' },
46
+ { Icon: Package, label: 'ChromaDB' },
47
+ { Icon: Activity, label: 'Redis' },
48
+ { Icon: Hexagon, label: 'Weaviate' },
49
+ ];
50
+
51
+ export const AI_MODELS: ProviderPill[] = [
52
+ { Icon: Rabbit, label: 'Ollama' },
53
+ { Icon: Sparkles, label: 'OpenAI' },
54
+ { Icon: Bot, label: 'Anthropic' },
55
+ { Icon: Brain, label: 'Gemini' },
56
+ { Icon: Cpu, label: 'Groq' },
57
+ { Icon: Cloud, label: 'Qwen' },
58
+ { Icon: Code, label: 'Copilot' },
59
+ { Icon: Terminal, label: 'LiteLLM' },
60
+ ];
61
+
62
+ export const PIPELINE_STEPS: PipelineStep[] = [
63
+ {
64
+ step: '01',
65
+ Icon: FileText,
66
+ title: 'Ingest',
67
+ desc: 'Universal support for PDF, DOCX, CSV, and JSON. Documents are parsed, chunked, and embedded.',
68
+ colors: { from: '#10b981', to: '#14b8a6' },
69
+ },
70
+ {
71
+ step: '02',
72
+ Icon: Search,
73
+ title: 'Retrieve',
74
+ desc: 'Queries are converted to vectors. Semantic search finds context across any configured Vector DB.',
75
+ colors: { from: '#14b8a6', to: '#06b6d4' },
76
+ },
77
+ {
78
+ step: '03',
79
+ Icon: Puzzle,
80
+ title: 'Augment',
81
+ desc: 'Retrieved context is injected into the LLM prompt with namespaced project isolation.',
82
+ colors: { from: '#06b6d4', to: '#3b82f6' },
83
+ },
84
+ {
85
+ step: '04',
86
+ Icon: MessageSquare,
87
+ title: 'Generate',
88
+ desc: 'Providers like OpenAI, Anthropic, or Gemini produce grounded, source-cited responses.',
89
+ colors: { from: '#3b82f6', to: '#6366f1' },
90
+ },
91
+ ];
92
+
93
+ export const SNIPPETS: Snippet[] = [
94
+ {
95
+ id: 'pipeline',
96
+ title: 'Retrivora SDK',
97
+ language: 'typescript',
98
+ description: 'Create one server-side SDK instance from config; clients never know which providers are used.',
99
+ code: `import { Retrivora } from '@retrivora-ai/rag-engine/server';
100
+
101
+ const ai = new Retrivora({
102
+ projectId: 'support-app',
103
+ llm: {
104
+ provider: 'openai',
105
+ model: 'gpt-5',
106
+ apiKey: process.env.OPENAI_API_KEY,
107
+ },
108
+ embedding: {
109
+ provider: 'openai',
110
+ model: 'text-embedding-3-small',
111
+ apiKey: process.env.OPENAI_API_KEY,
112
+ },
113
+ vectorDatabase: {
114
+ provider: 'pinecone',
115
+ indexName: 'support-docs',
116
+ options: { apiKey: process.env.PINECONE_API_KEY },
117
+ },
118
+ retrieval: { strategy: 'hybrid', topK: 5 },
119
+ workflow: { type: 'agentic-rag' },
120
+ });
121
+
122
+ await ai.ingest([{
123
+ docId: 'doc-1',
124
+ content: 'Retrivora AI simplifies RAG...',
125
+ metadata: { source: 'docs' }
126
+ }]);
127
+
128
+ const { reply, sources } = await ai.ask('How does Retrivora work?');`,
129
+ },
130
+ {
131
+ id: 'handlers',
132
+ title: 'Next.js Routes',
133
+ language: 'typescript',
134
+ description: 'Keep secrets and provider code in App Router handlers, then expose simple API endpoints to React.',
135
+ code: `// app/api/chat/route.ts
136
+ import { Retrivora, createStreamHandler } from '@retrivora-ai/rag-engine/server';
137
+
138
+ const ai = new Retrivora({
139
+ projectId: 'support-app',
140
+ llm: { provider: 'openai', model: 'gpt-5', apiKey: process.env.OPENAI_API_KEY },
141
+ embedding: { provider: 'openai', model: 'text-embedding-3-small', apiKey: process.env.OPENAI_API_KEY },
142
+ vectorDatabase: {
143
+ provider: 'pinecone',
144
+ indexName: 'support-docs',
145
+ options: { apiKey: process.env.PINECONE_API_KEY },
146
+ },
147
+ });
148
+
149
+ export const POST = createStreamHandler(ai.config);
150
+
151
+ // app/api/upload/route.ts
152
+ // export const POST = createUploadHandler(ai.config);`,
153
+ },
154
+ {
155
+ id: 'ui',
156
+ title: 'React Client UI',
157
+ language: 'typescript',
158
+ description: 'Import browser-safe components in Client Components; API keys stay on the server.',
159
+ code: `'use client';
160
+
161
+ import { ConfigProvider, ChatWidget } from '@retrivora-ai/rag-engine';
162
+
163
+ export default function Layout({ children }) {
164
+ return (
165
+ <ConfigProvider
166
+ config={{
167
+ projectId: 'support-app',
168
+ ui: {
169
+ title: 'AI Assistant',
170
+ showSources: true,
171
+ allowUpload: true,
172
+ },
173
+ }}
174
+ >
175
+ {children}
176
+ <ChatWidget position="bottom-right" />
177
+ </ConfigProvider>
178
+ );
179
+ }`,
180
+ },
181
+ {
182
+ id: 'config',
183
+ title: 'Headless Hook',
184
+ language: 'typescript',
185
+ description: 'Build your own chat surface with the same server-backed route.',
186
+ code: `'use client';
187
+
188
+ import React from 'react';
189
+ import { useRagChat } from '@retrivora-ai/rag-engine';
190
+
191
+ export function AskBox() {
192
+ const [input, setInput] = React.useState('');
193
+ const { messages, send, isLoading } = useRagChat('support-app', {
194
+ apiUrl: '/api/chat',
195
+ });
196
+
197
+ return (
198
+ <form onSubmit={(event) => {
199
+ event.preventDefault();
200
+ send(input);
201
+ setInput('');
202
+ }}>
203
+ <input value={input} onChange={(event) => setInput(event.target.value)} />
204
+ <button disabled={isLoading}>Ask</button>
205
+
206
+ {messages.map((message) => (
207
+ <p key={message.id}>{message.content}</p>
208
+ ))}
209
+ </form>
210
+ );
211
+ }`,
212
+ },
213
+ {
214
+ id: 'env',
215
+ title: 'Environment',
216
+ language: 'bash',
217
+ description: 'The exact environment variable structure required for auto-configuration.',
218
+ code: `# Project Identity
219
+ RAG_PROJECT_ID=my-project
220
+
221
+ # Vector DB
222
+ VECTOR_DB_PROVIDER=pinecone
223
+ PINECONE_API_KEY=pcsk_...
224
+ VECTOR_DB_INDEX=products
225
+
226
+ # AI Services
227
+ LLM_PROVIDER=openai
228
+ LLM_MODEL=gpt-4o
229
+ OPENAI_API_KEY=sk-...`,
230
+ },
231
+ ];
232
+
233
+ export const ARCHITECTURE_CARDS: ArchitectureCardProps[] = [
234
+ {
235
+ icon: <Database className="text-indigo-600" />,
236
+ title: 'Vector Store',
237
+ description: 'Universal support for Pinecone, PGVector, MongoDB, Milvus, Qdrant, and more.',
238
+ badge: 'Vector DB',
239
+ badgeColor: 'bg-indigo-50 dark:bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 border-indigo-200 dark:border-indigo-500/20',
240
+ },
241
+ {
242
+ icon: <Zap className="text-amber-500" />,
243
+ title: 'Embeddings',
244
+ description: 'Seamlessly switch between OpenAI, Ollama, or custom embedding providers.',
245
+ badge: 'Models',
246
+ badgeColor: 'bg-amber-50 dark:bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-200 dark:border-amber-500/20',
247
+ },
248
+ {
249
+ icon: <Bot className="text-emerald-500" />,
250
+ title: 'LLM Orchestration',
251
+ description: 'Optimized inference across OpenAI, Anthropic, Gemini, and local LLMs.',
252
+ badge: 'Inference',
253
+ badgeColor: 'bg-emerald-50 dark:bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-200 dark:border-emerald-500/20',
254
+ },
255
+ ];
256
+
257
+ export const QUICK_START_STEPS = [
258
+ 'Install @retrivora-ai/rag-engine',
259
+ 'Configure your .env.local',
260
+ 'Mount the API handlers',
261
+ 'Drop the ChatWidget'
262
+ ];
263
+
264
+ export const API_ENDPOINTS = ['chat', 'health', 'ingest', 'upload'];
265
+
266
+ export const CHAT_SUGGESTIONS = [
267
+ 'What can you help me with?',
268
+ 'Summarise the key topics',
269
+ 'Show me an example'
270
+ ];
271
+
272
+ export const BORDER_RADIUS_MAP: Record<string, string> = {
273
+ none: 'rounded-none',
274
+ sm: 'rounded-sm',
275
+ md: 'rounded-md',
276
+ lg: 'rounded-lg',
277
+ xl: 'rounded-xl',
278
+ full: 'rounded-3xl',
279
+ };