@retrivora-ai/rag-engine 0.1.0

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 (61) hide show
  1. package/.env.example +77 -0
  2. package/README.md +149 -0
  3. package/dist/DocumentChunker-BUrIrcPk.d.mts +43 -0
  4. package/dist/DocumentChunker-BUrIrcPk.d.ts +43 -0
  5. package/dist/RAGPipeline-BmkIv1HD.d.mts +298 -0
  6. package/dist/RAGPipeline-BmkIv1HD.d.ts +298 -0
  7. package/dist/chunk-NCG2JKXB.mjs +1254 -0
  8. package/dist/chunk-ZPXLQR5Q.mjs +67 -0
  9. package/dist/handlers/index.d.mts +68 -0
  10. package/dist/handlers/index.d.ts +68 -0
  11. package/dist/handlers/index.js +1319 -0
  12. package/dist/handlers/index.mjs +13 -0
  13. package/dist/index.d.mts +93 -0
  14. package/dist/index.d.ts +93 -0
  15. package/dist/index.js +612 -0
  16. package/dist/index.mjs +551 -0
  17. package/dist/server.d.mts +211 -0
  18. package/dist/server.d.ts +211 -0
  19. package/dist/server.js +1457 -0
  20. package/dist/server.mjs +148 -0
  21. package/package.json +90 -0
  22. package/src/app/api/chat/route.ts +4 -0
  23. package/src/app/api/health/route.ts +4 -0
  24. package/src/app/api/ingest/route.ts +26 -0
  25. package/src/app/favicon.ico +0 -0
  26. package/src/app/globals.css +163 -0
  27. package/src/app/layout.tsx +35 -0
  28. package/src/app/page.tsx +506 -0
  29. package/src/components/ChatWidget.tsx +91 -0
  30. package/src/components/ChatWindow.tsx +248 -0
  31. package/src/components/ConfigProvider.tsx +56 -0
  32. package/src/components/MessageBubble.tsx +99 -0
  33. package/src/components/SourceCard.tsx +66 -0
  34. package/src/components/ThemeProvider.tsx +8 -0
  35. package/src/components/ThemeToggle.tsx +29 -0
  36. package/src/config/RagConfig.ts +159 -0
  37. package/src/config/UniversalProfiles.ts +83 -0
  38. package/src/config/serverConfig.ts +142 -0
  39. package/src/handlers/index.ts +202 -0
  40. package/src/hooks/useHydrated.ts +13 -0
  41. package/src/hooks/useRagChat.ts +167 -0
  42. package/src/hooks/useStoredMessages.ts +53 -0
  43. package/src/index.ts +27 -0
  44. package/src/llm/ILLMProvider.ts +60 -0
  45. package/src/llm/LLMFactory.ts +54 -0
  46. package/src/llm/providers/AnthropicProvider.ts +87 -0
  47. package/src/llm/providers/OllamaProvider.ts +102 -0
  48. package/src/llm/providers/OpenAIProvider.ts +92 -0
  49. package/src/llm/providers/UniversalLLMAdapter.ts +154 -0
  50. package/src/rag/DocumentChunker.ts +105 -0
  51. package/src/rag/RAGPipeline.ts +196 -0
  52. package/src/server.ts +30 -0
  53. package/src/types/pdf-parse.d.ts +13 -0
  54. package/src/utils/DocumentParser.ts +75 -0
  55. package/src/utils/templateUtils.ts +78 -0
  56. package/src/vectordb/IVectorDB.ts +75 -0
  57. package/src/vectordb/VectorDBFactory.ts +41 -0
  58. package/src/vectordb/adapters/MongoDbAdapter.ts +175 -0
  59. package/src/vectordb/adapters/PgVectorAdapter.ts +159 -0
  60. package/src/vectordb/adapters/PineconeAdapter.ts +115 -0
  61. package/src/vectordb/adapters/UniversalVectorDBAdapter.ts +177 -0
@@ -0,0 +1,248 @@
1
+ 'use client';
2
+
3
+ import React, { useState, useRef, useEffect, useCallback } from 'react';
4
+ import {
5
+ Bot,
6
+ Trash2,
7
+ X,
8
+ Sparkles,
9
+ ArrowUp,
10
+ TriangleAlert,
11
+ } from 'lucide-react';
12
+ import { MessageBubble } from './MessageBubble';
13
+ import { useConfig } from './ConfigProvider';
14
+ import { useRagChat } from '@/hooks/useRagChat';
15
+
16
+ interface ChatWindowProps {
17
+ /** Additional className for the wrapper div */
18
+ className?: string;
19
+ /** Inline styles for the wrapper div */
20
+ style?: React.CSSProperties;
21
+ /** Called when the close button is clicked (for widget mode) */
22
+ onClose?: () => void;
23
+ /** Whether to show a close (X) button in the header */
24
+ showClose?: boolean;
25
+ }
26
+
27
+ export function ChatWindow({ className = '', style, onClose, showClose = false }: ChatWindowProps) {
28
+ const { ui, projectId } = useConfig();
29
+ const [input, setInput] = useState('');
30
+ const [mounted, setMounted] = useState(false);
31
+ const bottomRef = useRef<HTMLDivElement>(null);
32
+ const inputRef = useRef<HTMLTextAreaElement>(null);
33
+ const { messages, send, clear, isLoading, error } = useRagChat(projectId, {
34
+ namespace: projectId,
35
+ });
36
+
37
+ useEffect(() => {
38
+ // eslint-disable-next-line react-hooks/set-state-in-effect
39
+ setMounted(true);
40
+ }, []);
41
+
42
+ // Scroll to bottom on new message (but not on initial mount with empty messages)
43
+ useEffect(() => {
44
+ if (messages.length > 0 || isLoading) {
45
+ bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
46
+ }
47
+ }, [messages, isLoading]);
48
+
49
+ const sendMessage = useCallback(async () => {
50
+ const text = input.trim();
51
+ if (!text || isLoading) return;
52
+ setInput('');
53
+ await send(text);
54
+ window.setTimeout(() => inputRef.current?.focus(), 50);
55
+ }, [input, isLoading, send]);
56
+
57
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
58
+ if (e.key === 'Enter' && !e.shiftKey) {
59
+ e.preventDefault();
60
+ sendMessage();
61
+ }
62
+ };
63
+
64
+ const clearHistory = () => {
65
+ clear();
66
+ };
67
+
68
+ const isEmpty = messages.length === 0;
69
+
70
+ const borderRadiusMap = {
71
+ none: 'rounded-none',
72
+ sm: 'rounded-sm',
73
+ md: 'rounded-md',
74
+ lg: 'rounded-lg',
75
+ xl: 'rounded-xl',
76
+ full: 'rounded-3xl',
77
+ };
78
+
79
+ const currentRadius = borderRadiusMap[ui.borderRadius || 'xl'];
80
+ const isGlass = ui.visualStyle !== 'solid';
81
+
82
+ return (
83
+ <div
84
+ className={`flex flex-col border border-slate-200 dark:border-white/10 shadow-2xl transition-all duration-300 ${currentRadius} ${
85
+ isGlass
86
+ ? 'bg-white/90 dark:bg-[#0f0f1a]/90 backdrop-blur-xl'
87
+ : 'bg-white dark:bg-[#0f0f1a]'
88
+ } ${className}`}
89
+ style={{ '--primary': ui.primaryColor, '--accent': ui.accentColor, ...style } as React.CSSProperties}
90
+ >
91
+ {/* ── Header ── */}
92
+ <div
93
+ className="flex items-center justify-between px-5 py-4 border-b border-slate-200 dark:border-white/10"
94
+ style={{ background: `linear-gradient(135deg, ${ui.primaryColor}15, ${ui.accentColor}10)` }}
95
+ >
96
+ <div className="flex items-center gap-3">
97
+ {ui.logoUrl ? (
98
+ // eslint-disable-next-line @next/next/no-img-element
99
+ <img src={ui.logoUrl} alt="logo" className="w-8 h-8 rounded-lg object-cover" />
100
+ ) : (
101
+ <div
102
+ className={`w-9 h-9 flex items-center justify-center shadow-md ${ui.borderRadius === 'full' ? 'rounded-full' : 'rounded-xl'}`}
103
+ style={{ background: `linear-gradient(135deg, ${ui.primaryColor}, ${ui.accentColor})` }}
104
+ >
105
+ <Bot className="w-5 h-5 text-white" />
106
+ </div>
107
+ )}
108
+ <div>
109
+ <h2 className="text-slate-900 dark:text-white font-semibold text-sm leading-tight">{ui.title}</h2>
110
+ {ui.poweredBy && (
111
+ <p className="text-slate-500 dark:text-white/40 text-[11px] leading-tight">{`Powered by ${ui.poweredBy}`}</p>
112
+ )}
113
+ </div>
114
+ </div>
115
+
116
+ <div className="flex items-center gap-2">
117
+ {/* Online indicator */}
118
+ <span className="flex items-center gap-1 text-[10px] text-emerald-500 dark:text-emerald-400">
119
+ <span className="w-1.5 h-1.5 rounded-full bg-emerald-500 dark:bg-emerald-400 animate-pulse" />
120
+ Online
121
+ </span>
122
+
123
+ {mounted && messages.length > 0 && (
124
+ <button
125
+ onClick={clearHistory}
126
+ title="Clear conversation"
127
+ className="w-7 h-7 rounded-lg flex items-center justify-center text-slate-400 hover:text-slate-600 dark:text-white/40 dark:hover:text-white/70 hover:bg-slate-100 dark:hover:bg-white/10 transition-all cursor-pointer"
128
+ >
129
+ <Trash2 className="w-3.5 h-3.5" />
130
+ </button>
131
+ )}
132
+
133
+ {showClose && onClose && (
134
+ <button
135
+ onClick={onClose}
136
+ title="Close chat"
137
+ className="w-7 h-7 rounded-lg flex items-center justify-center text-slate-400 hover:text-slate-600 dark:text-white/40 dark:hover:text-white/70 hover:bg-slate-100 dark:hover:bg-white/10 transition-all cursor-pointer"
138
+ >
139
+ <X className="w-4 h-4" />
140
+ </button>
141
+ )}
142
+ </div>
143
+ </div>
144
+
145
+ <div className="flex-1 overflow-y-auto px-4 py-4 space-y-5 scrollbar-thin scrollbar-thumb-slate-200 dark:scrollbar-thumb-white/10 scrollbar-track-transparent min-h-0 bg-slate-50 dark:bg-transparent">
146
+ {!mounted || isEmpty ? (
147
+ /* Welcome state */
148
+ <div className="h-full flex flex-col items-center justify-center text-center gap-4 px-6 py-12">
149
+ <div
150
+ className={`w-16 h-16 flex items-center justify-center shadow-lg ${ui.borderRadius === 'full' ? 'rounded-full' : 'rounded-2xl'}`}
151
+ style={{ background: `linear-gradient(135deg, ${ui.primaryColor}, ${ui.accentColor})` }}
152
+ >
153
+ <Sparkles className="w-8 h-8 text-white" />
154
+ </div>
155
+ <div>
156
+ <p className="text-slate-800 dark:text-white/80 font-medium leading-relaxed">{ui.welcomeMessage}</p>
157
+ <p className="text-slate-500 dark:text-white/30 text-sm mt-2">
158
+ Ask a question to get started
159
+ </p>
160
+ </div>
161
+
162
+ {/* Suggested prompts */}
163
+ <div className="flex flex-wrap gap-2 justify-center mt-2">
164
+ {['What can you help me with?', 'Summarise the key topics', 'Show me an example'].map(
165
+ (suggestion) => (
166
+ <button
167
+ key={suggestion}
168
+ onClick={() => {
169
+ setInput(suggestion);
170
+ inputRef.current?.focus();
171
+ }}
172
+ className="px-3 py-1.5 rounded-full text-xs border border-slate-200 dark:border-white/10 text-slate-600 dark:text-white/50 hover:text-slate-900 dark:hover:text-white/80 hover:border-slate-300 dark:hover:border-white/20 hover:bg-slate-100 dark:hover:bg-white/5 transition-all"
173
+ >
174
+ {suggestion}
175
+ </button>
176
+ )
177
+ )}
178
+ </div>
179
+ </div>
180
+ ) : (
181
+ messages.map((msg) => (
182
+ <MessageBubble
183
+ key={msg.id}
184
+ message={msg}
185
+ sources={msg.sources}
186
+ primaryColor={ui.primaryColor}
187
+ accentColor={ui.accentColor}
188
+ />
189
+ ))
190
+ )}
191
+
192
+ {/* Typing indicator */}
193
+ {isLoading && (
194
+ <MessageBubble
195
+ message={{ role: 'assistant', content: '' }}
196
+ isStreaming
197
+ primaryColor={ui.primaryColor}
198
+ accentColor={ui.accentColor}
199
+ />
200
+ )}
201
+
202
+ {/* Error */}
203
+ {error && (
204
+ <div className="flex items-center gap-2 text-rose-500 dark:text-rose-400 text-sm bg-rose-50 dark:bg-rose-400/10 border border-rose-200 dark:border-rose-400/20 rounded-xl px-4 py-3">
205
+ <TriangleAlert className="w-4 h-4 flex-shrink-0" />
206
+ <span>{error}</span>
207
+ </div>
208
+ )}
209
+
210
+ <div ref={bottomRef} />
211
+ </div>
212
+
213
+ {/* ── Input ── */}
214
+ <div className={`px-4 pb-4 pt-2 border-t border-slate-200 dark:border-white/10 ${isGlass ? 'bg-transparent' : 'bg-white dark:bg-[#0f0f1a]'}`}>
215
+ <div className={`flex items-end gap-2 bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 p-2 focus-within:border-slate-300 dark:focus-within:border-white/20 transition-all ${ui.borderRadius === 'full' ? 'rounded-3xl' : 'rounded-2xl'}`}>
216
+ <textarea
217
+ ref={inputRef}
218
+ value={input}
219
+ onChange={(e) => setInput(e.target.value)}
220
+ onKeyDown={handleKeyDown}
221
+ placeholder={ui.placeholder}
222
+ rows={1}
223
+ disabled={isLoading}
224
+ className="flex-1 bg-transparent text-slate-900 dark:text-white/90 placeholder-slate-400 dark:placeholder-white/30 text-sm resize-none outline-none py-1.5 px-2 max-h-32 leading-relaxed disabled:opacity-50"
225
+ style={{ scrollbarWidth: 'none' }}
226
+ />
227
+
228
+ <button
229
+ onClick={sendMessage}
230
+ disabled={!input.trim() || isLoading}
231
+ className="flex-shrink-0 w-9 h-9 rounded-xl flex items-center justify-center transition-all disabled:opacity-30 disabled:cursor-not-allowed hover:scale-105 active:scale-95 shadow-md"
232
+ style={{
233
+ background:
234
+ input.trim() && !isLoading
235
+ ? `linear-gradient(135deg, ${ui.primaryColor}, ${ui.accentColor})`
236
+ : 'rgba(148, 163, 184, 0.2)', // slate-400/20 for light mode disabled
237
+ }}
238
+ >
239
+ <ArrowUp className="w-4 h-4 text-white" />
240
+ </button>
241
+ </div>
242
+ <p className="text-center text-[10px] text-slate-400 dark:text-white/20 mt-2">
243
+ Press Enter to send · Shift+Enter for new line
244
+ </p>
245
+ </div>
246
+ </div>
247
+ );
248
+ }
@@ -0,0 +1,56 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * ConfigProvider — React Context that makes RagConfig (UI subset) available
5
+ * to all child components without prop drilling.
6
+ *
7
+ * Only the UI-safe portions of RagConfig are exposed to the client.
8
+ */
9
+
10
+ import React, { createContext, useContext, ReactNode } from 'react';
11
+ import { UIConfig } from '@/config/RagConfig';
12
+ import { mergeDefined } from '@/utils/templateUtils';
13
+
14
+ export interface ClientConfig {
15
+ projectId: string;
16
+ ui: Required<UIConfig>;
17
+ }
18
+
19
+ const defaultConfig: ClientConfig = {
20
+ projectId: 'default',
21
+ ui: {
22
+ title: 'AI Assistant',
23
+ subtitle: 'Powered by RAG',
24
+ primaryColor: '#6366f1',
25
+ accentColor: '#8b5cf6',
26
+ logoUrl: '',
27
+ placeholder: 'Ask me anything…',
28
+ showSources: true,
29
+ welcomeMessage: "Hello! I'm your AI assistant. Ask me anything about your documents.",
30
+ showWidget: true,
31
+ poweredBy: 'RAG',
32
+ visualStyle: 'glass',
33
+ borderRadius: 'xl',
34
+ },
35
+ };
36
+
37
+ const ConfigContext = createContext<ClientConfig>(defaultConfig);
38
+
39
+ export function ConfigProvider({
40
+ config,
41
+ children,
42
+ }: {
43
+ config?: { projectId?: string; ui?: Partial<UIConfig> };
44
+ children: ReactNode;
45
+ }) {
46
+ const merged: ClientConfig = {
47
+ projectId: config?.projectId || defaultConfig.projectId,
48
+ ui: mergeDefined(defaultConfig.ui, config?.ui) as Required<UIConfig>,
49
+ };
50
+
51
+ return <ConfigContext.Provider value={merged}>{children}</ConfigContext.Provider>;
52
+ }
53
+
54
+ export function useConfig(): ClientConfig {
55
+ return useContext(ConfigContext);
56
+ }
@@ -0,0 +1,99 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import { Bot, User, ChevronDown, ChevronRight } from 'lucide-react';
5
+ import ReactMarkdown from 'react-markdown';
6
+ import remarkGfm from 'remark-gfm';
7
+ import { ChatMessage } from '@/llm/ILLMProvider';
8
+ import { VectorMatch } from '@/vectordb/IVectorDB';
9
+ import { SourceCard } from './SourceCard';
10
+
11
+ interface MessageBubbleProps {
12
+ message: ChatMessage;
13
+ sources?: VectorMatch[];
14
+ isStreaming?: boolean;
15
+ primaryColor?: string;
16
+ accentColor?: string;
17
+ }
18
+
19
+ export function MessageBubble({
20
+ message,
21
+ sources,
22
+ isStreaming = false,
23
+ primaryColor = '#6366f1',
24
+ accentColor = '#8b5cf6',
25
+ }: MessageBubbleProps) {
26
+ const isUser = message.role === 'user';
27
+ const [showSources, setShowSources] = React.useState(false);
28
+
29
+ return (
30
+ <div className={`flex gap-3 ${isUser ? 'flex-row-reverse' : 'flex-row'} items-start`}>
31
+ {/* Avatar */}
32
+ <div
33
+ className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center shadow-lg ${
34
+ isUser ? 'text-white' : 'bg-slate-100 dark:bg-white/10 text-slate-700 dark:text-white/80'
35
+ }`}
36
+ style={isUser ? { background: `linear-gradient(135deg, ${primaryColor}, ${accentColor})` } : {}}
37
+ >
38
+ {isUser
39
+ ? <User className="w-4 h-4 text-white" />
40
+ : <Bot className="w-4 h-4" />
41
+ }
42
+ </div>
43
+
44
+ <div className={`flex flex-col gap-1 max-w-[75%] ${isUser ? 'items-end' : 'items-start'}`}>
45
+ {/* Bubble */}
46
+ <div
47
+ className={`relative px-4 py-3 rounded-2xl text-sm leading-relaxed shadow-sm dark:shadow-lg ${
48
+ isUser
49
+ ? 'text-white rounded-tr-sm'
50
+ : 'bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 text-slate-800 dark:text-white/90 rounded-tl-sm'
51
+ }`}
52
+ style={
53
+ isUser
54
+ ? { background: `linear-gradient(135deg, ${primaryColor}, ${accentColor})` }
55
+ : {}
56
+ }
57
+ >
58
+ {isStreaming ? (
59
+ <span className="flex items-center gap-1">
60
+ <span className="w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:0ms]" />
61
+ <span className="w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:150ms]" />
62
+ <span className="w-1.5 h-1.5 rounded-full bg-slate-400 dark:bg-white/60 animate-bounce [animation-delay:300ms]" />
63
+ </span>
64
+ ) : (
65
+ <div className={`prose prose-sm max-w-none ${isUser ? 'prose-invert' : 'dark:prose-invert'}`}>
66
+ <ReactMarkdown remarkPlugins={[remarkGfm]}>
67
+ {message.content}
68
+ </ReactMarkdown>
69
+ </div>
70
+ )}
71
+ </div>
72
+
73
+ {/* Sources toggle */}
74
+ {!isUser && sources && sources.length > 0 && (
75
+ <div className="w-full">
76
+ <button
77
+ onClick={() => setShowSources((s) => !s)}
78
+ className="text-[11px] text-indigo-400 hover:text-indigo-300 transition-colors flex items-center gap-1 mt-1"
79
+ >
80
+ {showSources
81
+ ? <ChevronDown className="w-3 h-3" />
82
+ : <ChevronRight className="w-3 h-3" />
83
+ }
84
+ {sources.length} source{sources.length !== 1 ? 's' : ''} used
85
+ </button>
86
+
87
+ {showSources && (
88
+ <div className="mt-2 flex flex-col gap-2">
89
+ {sources.map((src, i) => (
90
+ <SourceCard key={src.id} source={src} index={i} />
91
+ ))}
92
+ </div>
93
+ )}
94
+ </div>
95
+ )}
96
+ </div>
97
+ </div>
98
+ );
99
+ }
@@ -0,0 +1,66 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import { ChevronDown, ChevronUp } from 'lucide-react';
5
+ import { VectorMatch } from '@/vectordb/IVectorDB';
6
+
7
+ interface SourceCardProps {
8
+ source: VectorMatch;
9
+ index: number;
10
+ }
11
+
12
+ export function SourceCard({ source, index }: SourceCardProps) {
13
+ const [expanded, setExpanded] = React.useState(false);
14
+ const scorePercent = Math.round(source.score * 100);
15
+
16
+ const scoreColor =
17
+ source.score >= 0.8
18
+ ? 'text-emerald-400'
19
+ : source.score >= 0.6
20
+ ? 'text-amber-400'
21
+ : 'text-rose-400';
22
+
23
+ const scoreBg =
24
+ source.score >= 0.8
25
+ ? 'bg-emerald-50 border-emerald-100 dark:bg-emerald-400/10 dark:border-emerald-400/20'
26
+ : source.score >= 0.6
27
+ ? 'bg-amber-50 border-amber-100 dark:bg-amber-400/10 dark:border-amber-400/20'
28
+ : 'bg-rose-50 border-rose-100 dark:bg-rose-400/10 dark:border-rose-400/20';
29
+
30
+ const preview = source.content.slice(0, 120);
31
+ const hasMore = source.content.length > 120;
32
+
33
+ return (
34
+ <div
35
+ className={`rounded-xl border backdrop-blur-sm p-3 transition-all duration-200 hover:scale-[1.01] cursor-pointer ${scoreBg}`}
36
+ onClick={() => setExpanded((e) => !e)}
37
+ >
38
+ <div className="flex items-start justify-between gap-2">
39
+ <div className="flex items-center gap-2 min-w-0">
40
+ <span className="flex-shrink-0 w-5 h-5 rounded-full bg-slate-200 dark:bg-white/10 flex items-center justify-center text-[10px] font-bold text-slate-500 dark:text-white/60">
41
+ {index + 1}
42
+ </span>
43
+ <span className="text-xs text-slate-500 dark:text-white/50 truncate font-mono">
44
+ {(source.metadata?.docId as string) ?? source.id}
45
+ </span>
46
+ </div>
47
+ <span className={`flex-shrink-0 text-xs font-semibold ${scoreColor} tabular-nums`}>
48
+ {scorePercent}%
49
+ </span>
50
+ </div>
51
+
52
+ <p className="mt-2 text-xs text-slate-600 dark:text-white/70 leading-relaxed">
53
+ {expanded ? source.content : `${preview}${hasMore && !expanded ? '…' : ''}`}
54
+ </p>
55
+
56
+ {hasMore && (
57
+ <button className="mt-1 text-[10px] text-indigo-400 hover:text-indigo-300 transition-colors flex items-center gap-0.5">
58
+ {expanded
59
+ ? <><ChevronUp className="w-3 h-3" /> Show less</>
60
+ : <><ChevronDown className="w-3 h-3" /> Show more</>
61
+ }
62
+ </button>
63
+ )}
64
+ </div>
65
+ );
66
+ }
@@ -0,0 +1,8 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { ThemeProvider as NextThemesProvider } from 'next-themes';
5
+
6
+ export function ThemeProvider({ children, ...props }: React.ComponentProps<typeof NextThemesProvider>) {
7
+ return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
8
+ }
@@ -0,0 +1,29 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { Moon, Sun } from 'lucide-react';
5
+ import { useTheme } from 'next-themes';
6
+ import { useHydrated } from '@/hooks/useHydrated';
7
+
8
+ export function ThemeToggle() {
9
+ const { theme, setTheme } = useTheme();
10
+ const mounted = useHydrated();
11
+
12
+ if (!mounted) {
13
+ return <div className="w-8 h-8 rounded-lg" />; // Placeholder to avoid layout shift
14
+ }
15
+
16
+ return (
17
+ <button
18
+ onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
19
+ className="w-8 h-8 rounded-lg flex items-center justify-center text-slate-500 hover:text-slate-900 dark:text-white/40 dark:hover:text-white/70 hover:bg-slate-100 dark:hover:bg-white/10 transition-all"
20
+ aria-label="Toggle theme"
21
+ >
22
+ {theme === 'dark' ? (
23
+ <Sun className="w-4 h-4" />
24
+ ) : (
25
+ <Moon className="w-4 h-4" />
26
+ )}
27
+ </button>
28
+ );
29
+ }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Master configuration interface for Retrivora AI.
3
+ * Each consuming project provides one RagConfig object to drive
4
+ * vector DB selection, LLM selection, embedding, and UI branding.
5
+ */
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Vector DB
9
+ // ---------------------------------------------------------------------------
10
+
11
+ export type VectorDBProvider = 'pinecone' | 'pgvector' | 'mongodb' | 'rest' | 'universal_rest' | 'custom';
12
+
13
+ export interface VectorDBConfig {
14
+ /** Which vector database to use */
15
+ provider: VectorDBProvider;
16
+ /** The index / table name to query */
17
+ indexName: string;
18
+ /** Provider-specific options (API keys, connection strings, etc.)
19
+ *
20
+ * For 'universal_rest', the following options are supported:
21
+ * - baseUrl: string
22
+ * - headers?: Record<string, string>
23
+ * - queryPath?: string
24
+ * - upsertPath?: string
25
+ * - queryPayloadTemplate?: string (e.g. '{"vector": {{vector}}, "limit": {{topK}}}')
26
+ * - upsertPayloadTemplate?: string (e.g. '{"id": "{{id}}", "vector": {{vector}}}')
27
+ * - responseExtractPath?: string (e.g. 'data.results')
28
+ * - idPath?: string (e.g. '_id')
29
+ * - scorePath?: string (e.g. 'similarity')
30
+ * - contentPath?: string (e.g. 'text')
31
+ */
32
+ options: Record<string, unknown>;
33
+ }
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // LLM Provider
37
+ // ---------------------------------------------------------------------------
38
+
39
+ export type LLMProvider = 'openai' | 'anthropic' | 'ollama' | 'rest' | 'universal_rest' | 'custom';
40
+
41
+ export interface LLMConfig {
42
+ /** Which LLM provider to use */
43
+ provider: LLMProvider;
44
+ /** Model name, e.g. "gpt-4o", "claude-3-opus-20240229", "llama3" */
45
+ model: string;
46
+ /** API key for cloud providers */
47
+ apiKey?: string;
48
+ /** Base URL — required for Ollama or self-hosted endpoints */
49
+ baseUrl?: string;
50
+ /** Custom system prompt injected before every chat */
51
+ systemPrompt?: string;
52
+ /** Max tokens in the LLM response */
53
+ maxTokens?: number;
54
+ /** Sampling temperature (0–1) */
55
+ temperature?: number;
56
+ /** Provider-specific options
57
+ *
58
+ * For 'universal_rest', the following options are supported:
59
+ * - headers?: Record<string, string>
60
+ * - chatPath?: string
61
+ * - chatPayloadTemplate?: string (e.g. '{"prompt": "{{prompt}}", "max_tokens": {{maxTokens}}}')
62
+ * - responseExtractPath?: string (e.g. 'choices[0].text' or 'response')
63
+ */
64
+ options?: Record<string, unknown>;
65
+ }
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // Embedding
69
+ // ---------------------------------------------------------------------------
70
+
71
+ export type EmbeddingProvider = 'openai' | 'ollama' | 'rest' | 'universal_rest' | 'custom';
72
+
73
+ export interface EmbeddingConfig {
74
+ /** Which embedding provider to use */
75
+ provider: EmbeddingProvider;
76
+ /** Model name, e.g. "text-embedding-ada-002", "nomic-embed-text" */
77
+ model: string;
78
+ /** API key (if needed) */
79
+ apiKey?: string;
80
+ /** Base URL for custom / Ollama embedding endpoints */
81
+ baseUrl?: string;
82
+ /** Output vector dimension — must match the index dimension */
83
+ dimensions?: number;
84
+ /** Provider-specific options for custom adapters */
85
+ options?: Record<string, unknown>;
86
+ }
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // UI Branding
90
+ // ---------------------------------------------------------------------------
91
+
92
+ export interface UIConfig {
93
+ /** Title shown in the chat header */
94
+ title?: string;
95
+ /** Subtitle / description below the title */
96
+ subtitle?: string;
97
+ /** Primary brand colour (CSS colour string) */
98
+ primaryColor?: string;
99
+ /** Accent colour for user message bubbles */
100
+ accentColor?: string;
101
+ /** URL to a logo image */
102
+ logoUrl?: string;
103
+ /** Input placeholder text */
104
+ placeholder?: string;
105
+ /** Show source cards after each answer */
106
+ showSources?: boolean;
107
+ /** Welcome message shown on first load */
108
+ welcomeMessage?: string;
109
+ /** Whether to show the floating chat widget. Defaults to true. */
110
+ showWidget?: boolean;
111
+ /** Custom 'Powered by' text shown in the header */
112
+ poweredBy?: string;
113
+ /** Visual style: 'glass' (default) or 'solid' */
114
+ visualStyle?: 'glass' | 'solid';
115
+ /** Border radius: 'none', 'sm', 'md', 'lg', 'xl', 'full' */
116
+ borderRadius?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | 'full';
117
+ }
118
+
119
+ // ---------------------------------------------------------------------------
120
+ // RAG Pipeline Tuning
121
+ // ---------------------------------------------------------------------------
122
+
123
+ export interface RAGConfig {
124
+ /** Number of top-K chunks retrieved per query */
125
+ topK?: number;
126
+ /** Minimum similarity score to include a chunk (0–1) */
127
+ scoreThreshold?: number;
128
+ /** Target chunk size in tokens */
129
+ chunkSize?: number;
130
+ /** Overlap between adjacent chunks in tokens */
131
+ chunkOverlap?: number;
132
+ }
133
+
134
+ // ---------------------------------------------------------------------------
135
+ // Root Config
136
+ // ---------------------------------------------------------------------------
137
+
138
+ export interface RagConfig {
139
+ /**
140
+ * Unique identifier for the consuming project.
141
+ * Used as the vector DB namespace to achieve multi-tenancy.
142
+ */
143
+ projectId: string;
144
+
145
+ /** Vector database configuration */
146
+ vectorDb: VectorDBConfig;
147
+
148
+ /** LLM configuration */
149
+ llm: LLMConfig;
150
+
151
+ /** Embedding configuration */
152
+ embedding: EmbeddingConfig;
153
+
154
+ /** Optional UI branding overrides */
155
+ ui?: UIConfig;
156
+
157
+ /** Optional RAG pipeline tuning knobs */
158
+ rag?: RAGConfig;
159
+ }