@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.
- package/.env.example +77 -0
- package/README.md +149 -0
- package/dist/DocumentChunker-BUrIrcPk.d.mts +43 -0
- package/dist/DocumentChunker-BUrIrcPk.d.ts +43 -0
- package/dist/RAGPipeline-BmkIv1HD.d.mts +298 -0
- package/dist/RAGPipeline-BmkIv1HD.d.ts +298 -0
- package/dist/chunk-NCG2JKXB.mjs +1254 -0
- package/dist/chunk-ZPXLQR5Q.mjs +67 -0
- package/dist/handlers/index.d.mts +68 -0
- package/dist/handlers/index.d.ts +68 -0
- package/dist/handlers/index.js +1319 -0
- package/dist/handlers/index.mjs +13 -0
- package/dist/index.d.mts +93 -0
- package/dist/index.d.ts +93 -0
- package/dist/index.js +612 -0
- package/dist/index.mjs +551 -0
- package/dist/server.d.mts +211 -0
- package/dist/server.d.ts +211 -0
- package/dist/server.js +1457 -0
- package/dist/server.mjs +148 -0
- package/package.json +90 -0
- package/src/app/api/chat/route.ts +4 -0
- package/src/app/api/health/route.ts +4 -0
- package/src/app/api/ingest/route.ts +26 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +163 -0
- package/src/app/layout.tsx +35 -0
- package/src/app/page.tsx +506 -0
- package/src/components/ChatWidget.tsx +91 -0
- package/src/components/ChatWindow.tsx +248 -0
- package/src/components/ConfigProvider.tsx +56 -0
- package/src/components/MessageBubble.tsx +99 -0
- package/src/components/SourceCard.tsx +66 -0
- package/src/components/ThemeProvider.tsx +8 -0
- package/src/components/ThemeToggle.tsx +29 -0
- package/src/config/RagConfig.ts +159 -0
- package/src/config/UniversalProfiles.ts +83 -0
- package/src/config/serverConfig.ts +142 -0
- package/src/handlers/index.ts +202 -0
- package/src/hooks/useHydrated.ts +13 -0
- package/src/hooks/useRagChat.ts +167 -0
- package/src/hooks/useStoredMessages.ts +53 -0
- package/src/index.ts +27 -0
- package/src/llm/ILLMProvider.ts +60 -0
- package/src/llm/LLMFactory.ts +54 -0
- package/src/llm/providers/AnthropicProvider.ts +87 -0
- package/src/llm/providers/OllamaProvider.ts +102 -0
- package/src/llm/providers/OpenAIProvider.ts +92 -0
- package/src/llm/providers/UniversalLLMAdapter.ts +154 -0
- package/src/rag/DocumentChunker.ts +105 -0
- package/src/rag/RAGPipeline.ts +196 -0
- package/src/server.ts +30 -0
- package/src/types/pdf-parse.d.ts +13 -0
- package/src/utils/DocumentParser.ts +75 -0
- package/src/utils/templateUtils.ts +78 -0
- package/src/vectordb/IVectorDB.ts +75 -0
- package/src/vectordb/VectorDBFactory.ts +41 -0
- package/src/vectordb/adapters/MongoDbAdapter.ts +175 -0
- package/src/vectordb/adapters/PgVectorAdapter.ts +159 -0
- package/src/vectordb/adapters/PineconeAdapter.ts +115 -0
- 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
|
+
}
|