@retrivora-ai/rag-engine 1.2.0 → 1.2.2
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/dist/DocumentChunker-BXOUMKoP.d.ts +93 -0
- package/dist/DocumentChunker-D1dg5iCi.d.mts +93 -0
- package/dist/{chunk-OCDCJUNE.mjs → chunk-GCPPRD2G.mjs} +70 -1
- package/dist/handlers/index.d.mts +2 -2
- package/dist/handlers/index.d.ts +2 -2
- package/dist/handlers/index.js +70 -0
- package/dist/handlers/index.mjs +3 -1
- package/dist/{index-CYgr00ot.d.ts → index-B67KQ9NN.d.ts} +14 -2
- package/dist/{RagConfig-FyMB_UG6.d.mts → index-Cti1u0y1.d.mts} +87 -86
- package/dist/{RagConfig-FyMB_UG6.d.ts → index-Cti1u0y1.d.ts} +87 -86
- package/dist/{index-D-lfcqlL.d.mts → index-kUXnRvuI.d.mts} +14 -2
- package/dist/index.d.mts +53 -89
- package/dist/index.d.ts +53 -89
- package/dist/index.js +139 -27
- package/dist/index.mjs +143 -27
- package/dist/server.d.mts +9 -4
- package/dist/server.d.ts +9 -4
- package/dist/server.js +51 -0
- package/dist/server.mjs +1 -1
- package/package.json +1 -1
- package/src/app/api/suggestions/route.ts +4 -0
- package/src/components/ChatWidget.tsx +1 -7
- package/src/components/ChatWindow.tsx +180 -25
- package/src/components/ConfigProvider.tsx +7 -33
- package/src/components/DocumentUpload.tsx +1 -8
- package/src/components/MessageBubble.tsx +1 -12
- package/src/components/ProductCard.tsx +1 -15
- package/src/components/ProductCarousel.tsx +2 -7
- package/src/components/SourceCard.tsx +1 -6
- package/src/config/RagConfig.ts +2 -0
- package/src/config/uiConstants.ts +23 -0
- package/src/core/Pipeline.ts +57 -1
- package/src/core/VectorPlugin.ts +8 -1
- package/src/handlers/index.ts +31 -1
- package/src/hooks/useRagChat.ts +1 -45
- package/src/hooks/useStoredMessages.ts +1 -1
- package/src/index.ts +20 -5
- package/src/llm/ILLMProvider.ts +1 -13
- package/src/llm/providers/AnthropicProvider.ts +2 -1
- package/src/llm/providers/GeminiProvider.ts +2 -1
- package/src/llm/providers/OllamaProvider.ts +2 -1
- package/src/llm/providers/OpenAIProvider.ts +2 -1
- package/src/llm/providers/UniversalLLMAdapter.ts +2 -1
- package/src/server.ts +2 -2
- package/src/types/chat.ts +53 -0
- package/src/types/index.ts +17 -0
- package/src/types/props.ts +79 -0
- package/dist/DocumentChunker-Dh9TvmGG.d.mts +0 -45
- package/dist/DocumentChunker-Dh9TvmGG.d.ts +0 -45
|
@@ -11,33 +11,43 @@ import {
|
|
|
11
11
|
RotateCcw,
|
|
12
12
|
Maximize2,
|
|
13
13
|
Minimize2,
|
|
14
|
+
Mic,
|
|
15
|
+
MicOff,
|
|
14
16
|
} from 'lucide-react';
|
|
15
17
|
import { MessageBubble } from './MessageBubble';
|
|
16
18
|
import { useConfig } from './ConfigProvider';
|
|
17
19
|
import { useRagChat } from '../hooks/useRagChat';
|
|
18
20
|
import { BORDER_RADIUS_MAP, CHAT_SUGGESTIONS } from '../config/uiConstants';
|
|
21
|
+
import { ChatWindowProps } from '../types';
|
|
19
22
|
|
|
20
|
-
interface
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
23
|
+
interface SpeechRecognitionEvent {
|
|
24
|
+
resultIndex: number;
|
|
25
|
+
results: {
|
|
26
|
+
length: number;
|
|
27
|
+
[index: number]: {
|
|
28
|
+
isFinal: boolean;
|
|
29
|
+
[index: number]: { transcript: string };
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface SpeechRecognitionErrorEvent {
|
|
35
|
+
error: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface ISpeechRecognition {
|
|
39
|
+
continuous: boolean;
|
|
40
|
+
interimResults: boolean;
|
|
41
|
+
onresult: ((event: SpeechRecognitionEvent) => void) | null;
|
|
42
|
+
onerror: ((event: SpeechRecognitionErrorEvent) => void) | null;
|
|
43
|
+
onend: (() => void) | null;
|
|
44
|
+
start: () => void;
|
|
45
|
+
stop: () => void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface WindowWithSpeech extends Window {
|
|
49
|
+
SpeechRecognition?: new () => ISpeechRecognition;
|
|
50
|
+
webkitSpeechRecognition?: new () => ISpeechRecognition;
|
|
41
51
|
}
|
|
42
52
|
|
|
43
53
|
export function ChatWindow({
|
|
@@ -61,6 +71,66 @@ export function ChatWindow({
|
|
|
61
71
|
namespace: projectId,
|
|
62
72
|
});
|
|
63
73
|
|
|
74
|
+
const [suggestions, setSuggestions] = useState<string[]>([]);
|
|
75
|
+
const [isSuggesting, setIsSuggesting] = useState(false);
|
|
76
|
+
|
|
77
|
+
const [isListening, setIsListening] = useState(false);
|
|
78
|
+
const recognitionRef = useRef<ISpeechRecognition | null>(null);
|
|
79
|
+
|
|
80
|
+
// Initialize SpeechRecognition
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (typeof window !== 'undefined') {
|
|
83
|
+
const win = window as unknown as WindowWithSpeech;
|
|
84
|
+
const SpeechRecognition = win.SpeechRecognition || win.webkitSpeechRecognition;
|
|
85
|
+
if (SpeechRecognition) {
|
|
86
|
+
const recognition = new SpeechRecognition();
|
|
87
|
+
recognition.continuous = true;
|
|
88
|
+
recognition.interimResults = true;
|
|
89
|
+
|
|
90
|
+
recognition.onresult = (event: SpeechRecognitionEvent) => {
|
|
91
|
+
let finalTranscript = '';
|
|
92
|
+
for (let i = event.resultIndex; i < event.results.length; ++i) {
|
|
93
|
+
if (event.results[i].isFinal) {
|
|
94
|
+
finalTranscript += event.results[i][0].transcript;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (finalTranscript) {
|
|
98
|
+
setInput((prev) => (prev ? prev + ' ' : '') + finalTranscript.trim());
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
recognition.onerror = (event: SpeechRecognitionErrorEvent) => {
|
|
103
|
+
console.error('Speech recognition error:', event.error);
|
|
104
|
+
setIsListening(false);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
recognition.onend = () => {
|
|
108
|
+
setIsListening(false);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
recognitionRef.current = recognition;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}, []);
|
|
115
|
+
|
|
116
|
+
const toggleListening = () => {
|
|
117
|
+
if (!recognitionRef.current) {
|
|
118
|
+
alert('Speech recognition is not supported in your browser.');
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (isListening) {
|
|
123
|
+
recognitionRef.current.stop();
|
|
124
|
+
} else {
|
|
125
|
+
try {
|
|
126
|
+
recognitionRef.current.start();
|
|
127
|
+
setIsListening(true);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
console.error('Failed to start recognition:', err);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
64
134
|
useEffect(() => {
|
|
65
135
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
66
136
|
setMounted(true);
|
|
@@ -76,10 +146,17 @@ export function ChatWindow({
|
|
|
76
146
|
const sendMessage = useCallback(async () => {
|
|
77
147
|
const text = input.trim();
|
|
78
148
|
if (!text || isLoading) return;
|
|
149
|
+
|
|
150
|
+
if (isListening) {
|
|
151
|
+
recognitionRef.current?.stop();
|
|
152
|
+
}
|
|
153
|
+
|
|
79
154
|
setInput('');
|
|
155
|
+
setSuggestions([]);
|
|
156
|
+
setIsSuggesting(false);
|
|
80
157
|
await send(text);
|
|
81
158
|
window.setTimeout(() => inputRef.current?.focus(), 50);
|
|
82
|
-
}, [input, isLoading, send]);
|
|
159
|
+
}, [input, isLoading, send, isListening]);
|
|
83
160
|
|
|
84
161
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
85
162
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
@@ -90,13 +167,43 @@ export function ChatWindow({
|
|
|
90
167
|
|
|
91
168
|
const clearHistory = () => {
|
|
92
169
|
clear();
|
|
170
|
+
setSuggestions([]);
|
|
171
|
+
setIsSuggesting(false);
|
|
93
172
|
};
|
|
94
173
|
|
|
95
174
|
const isEmpty = messages.length === 0;
|
|
96
175
|
|
|
97
|
-
|
|
176
|
+
const currentRadius = BORDER_RADIUS_MAP[ui.borderRadius || 'xl'];
|
|
98
177
|
const isGlass = ui.visualStyle !== 'solid';
|
|
99
178
|
|
|
179
|
+
// Auto-suggestions fetcher (debounced)
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
if (input.trim().length < 3) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const timer = setTimeout(async () => {
|
|
186
|
+
setIsSuggesting(true);
|
|
187
|
+
try {
|
|
188
|
+
const response = await fetch('/api/suggestions', {
|
|
189
|
+
method: 'POST',
|
|
190
|
+
headers: { 'Content-Type': 'application/json' },
|
|
191
|
+
body: JSON.stringify({ query: input, namespace: projectId }),
|
|
192
|
+
});
|
|
193
|
+
const data = await response.json();
|
|
194
|
+
if (data.suggestions) {
|
|
195
|
+
setSuggestions(data.suggestions);
|
|
196
|
+
}
|
|
197
|
+
} catch (err) {
|
|
198
|
+
console.error('Failed to fetch suggestions:', err);
|
|
199
|
+
} finally {
|
|
200
|
+
setIsSuggesting(false);
|
|
201
|
+
}
|
|
202
|
+
}, 800); // 800ms debounce to save costs/latency
|
|
203
|
+
|
|
204
|
+
return () => clearTimeout(timer);
|
|
205
|
+
}, [input, projectId]);
|
|
206
|
+
|
|
100
207
|
return (
|
|
101
208
|
<div
|
|
102
209
|
className={`relative flex flex-col border border-slate-200 dark:border-white/10 shadow-2xl transition-all duration-300 ${currentRadius} ${
|
|
@@ -244,7 +351,7 @@ export function ChatWindow({
|
|
|
244
351
|
{/* Typing indicator (only if assistant hasn't started replying yet) */}
|
|
245
352
|
{isLoading && messages[messages.length - 1]?.role !== 'assistant' && (
|
|
246
353
|
<MessageBubble
|
|
247
|
-
message={{ role: 'assistant', content: '' }}
|
|
354
|
+
message={{ id: 'loading', role: 'assistant', content: '', createdAt: new Date().toISOString() }}
|
|
248
355
|
isStreaming
|
|
249
356
|
primaryColor={ui.primaryColor}
|
|
250
357
|
accentColor={ui.accentColor}
|
|
@@ -265,11 +372,44 @@ export function ChatWindow({
|
|
|
265
372
|
|
|
266
373
|
{/* ── Input ── */}
|
|
267
374
|
<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]'}`}>
|
|
375
|
+
|
|
376
|
+
{/* Dynamic Suggestions */}
|
|
377
|
+
{(suggestions.length > 0 || isSuggesting) && !isLoading && (
|
|
378
|
+
<div className="mb-3 flex flex-wrap gap-2 animate-in fade-in slide-in-from-bottom-2 duration-300">
|
|
379
|
+
{suggestions.map((suggestion) => (
|
|
380
|
+
<button
|
|
381
|
+
key={suggestion}
|
|
382
|
+
onClick={() => {
|
|
383
|
+
setInput(suggestion);
|
|
384
|
+
setSuggestions([]);
|
|
385
|
+
inputRef.current?.focus();
|
|
386
|
+
}}
|
|
387
|
+
className="flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-[11px] font-medium bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 text-slate-600 dark:text-white/60 hover:text-slate-900 dark:hover:text-white/90 hover:border-slate-300 dark:hover:border-white/20 transition-all shadow-sm cursor-pointer group"
|
|
388
|
+
>
|
|
389
|
+
<Sparkles className="w-3 h-3 text-amber-400" />
|
|
390
|
+
{suggestion}
|
|
391
|
+
</button>
|
|
392
|
+
))}
|
|
393
|
+
{isSuggesting && suggestions.length === 0 && (
|
|
394
|
+
<div className="flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-[11px] font-medium text-slate-400 dark:text-white/30 animate-pulse">
|
|
395
|
+
<Sparkles className="w-3 h-3" />
|
|
396
|
+
Thinking...
|
|
397
|
+
</div>
|
|
398
|
+
)}
|
|
399
|
+
</div>
|
|
400
|
+
)}
|
|
401
|
+
|
|
268
402
|
<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'}`}>
|
|
269
403
|
<textarea
|
|
270
404
|
ref={inputRef}
|
|
271
405
|
value={input}
|
|
272
|
-
onChange={(e) =>
|
|
406
|
+
onChange={(e) => {
|
|
407
|
+
const val = e.target.value;
|
|
408
|
+
setInput(val);
|
|
409
|
+
if (val.trim().length < 3) {
|
|
410
|
+
setSuggestions([]);
|
|
411
|
+
}
|
|
412
|
+
}}
|
|
273
413
|
onKeyDown={handleKeyDown}
|
|
274
414
|
placeholder={ui.placeholder}
|
|
275
415
|
rows={1}
|
|
@@ -278,6 +418,21 @@ export function ChatWindow({
|
|
|
278
418
|
style={{ scrollbarWidth: 'none' }}
|
|
279
419
|
/>
|
|
280
420
|
|
|
421
|
+
{ui.enableVoiceInput !== false && (
|
|
422
|
+
<button
|
|
423
|
+
type="button"
|
|
424
|
+
onClick={toggleListening}
|
|
425
|
+
className={`flex-shrink-0 w-9 h-9 rounded-xl flex items-center justify-center transition-all hover:scale-105 active:scale-95 ${
|
|
426
|
+
isListening
|
|
427
|
+
? 'bg-rose-500 text-white animate-pulse shadow-md shadow-rose-500/20'
|
|
428
|
+
: 'text-slate-400 dark:text-white/40 hover:bg-slate-200 dark:hover:bg-white/10'
|
|
429
|
+
}`}
|
|
430
|
+
title={isListening ? 'Stop listening' : 'Start voice input'}
|
|
431
|
+
>
|
|
432
|
+
{isListening ? <MicOff className="w-4 h-4" /> : <Mic className="w-4 h-4" />}
|
|
433
|
+
</button>
|
|
434
|
+
)}
|
|
435
|
+
|
|
281
436
|
<button
|
|
282
437
|
onClick={sendMessage}
|
|
283
438
|
disabled={!input.trim() || isLoading}
|
|
@@ -7,47 +7,21 @@
|
|
|
7
7
|
* Only the UI-safe portions of RagConfig are exposed to the client.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import React, { createContext, useContext
|
|
10
|
+
import React, { createContext, useContext } from 'react';
|
|
11
11
|
import { UIConfig } from '../config/RagConfig';
|
|
12
12
|
import { mergeDefined } from '../utils/templateUtils';
|
|
13
|
+
import { ClientConfig, ConfigProviderProps } from '../types';
|
|
14
|
+
import { DEFAULT_CONFIG } from '../config/uiConstants';
|
|
13
15
|
|
|
14
|
-
|
|
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
|
-
allowUpload: true,
|
|
35
|
-
allowResize: true,
|
|
36
|
-
},
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
const ConfigContext = createContext<ClientConfig>(defaultConfig);
|
|
16
|
+
const ConfigContext = createContext<ClientConfig>(DEFAULT_CONFIG);
|
|
40
17
|
|
|
41
18
|
export function ConfigProvider({
|
|
42
19
|
config,
|
|
43
20
|
children,
|
|
44
|
-
}: {
|
|
45
|
-
config?: { projectId?: string; ui?: Partial<UIConfig> };
|
|
46
|
-
children: ReactNode;
|
|
47
|
-
}) {
|
|
21
|
+
}: ConfigProviderProps) {
|
|
48
22
|
const merged: ClientConfig = {
|
|
49
|
-
projectId: config?.projectId ||
|
|
50
|
-
ui: mergeDefined(
|
|
23
|
+
projectId: config?.projectId || DEFAULT_CONFIG.projectId,
|
|
24
|
+
ui: mergeDefined(DEFAULT_CONFIG.ui, config?.ui) as Required<UIConfig>,
|
|
51
25
|
};
|
|
52
26
|
|
|
53
27
|
return <ConfigContext.Provider value={merged}>{children}</ConfigContext.Provider>;
|
|
@@ -4,14 +4,7 @@ import React, { useState, useRef } from 'react';
|
|
|
4
4
|
import { Upload, File, X, CheckCircle, AlertCircle, Loader2 } from 'lucide-react';
|
|
5
5
|
import { useConfig } from './ConfigProvider';
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
/** Optional namespace for the upload */
|
|
9
|
-
namespace?: string;
|
|
10
|
-
/** Callback when upload completes */
|
|
11
|
-
onUploadComplete?: (results: unknown) => void;
|
|
12
|
-
/** Additional className */
|
|
13
|
-
className?: string;
|
|
14
|
-
}
|
|
7
|
+
import { DocumentUploadProps } from '../types';
|
|
15
8
|
|
|
16
9
|
interface FileState {
|
|
17
10
|
file: File;
|
|
@@ -4,20 +4,9 @@ import React from 'react';
|
|
|
4
4
|
import { Bot, User, ChevronDown, ChevronRight } from 'lucide-react';
|
|
5
5
|
import ReactMarkdown from 'react-markdown';
|
|
6
6
|
import remarkGfm from 'remark-gfm';
|
|
7
|
-
import {
|
|
8
|
-
import { VectorMatch } from '../types';
|
|
7
|
+
import { MessageBubbleProps, Product } from '../types';
|
|
9
8
|
import { SourceCard } from './SourceCard';
|
|
10
9
|
import { ProductCarousel } from './ProductCarousel';
|
|
11
|
-
import { Product } from './ProductCard';
|
|
12
|
-
|
|
13
|
-
interface MessageBubbleProps {
|
|
14
|
-
message: ChatMessage;
|
|
15
|
-
sources?: VectorMatch[];
|
|
16
|
-
isStreaming?: boolean;
|
|
17
|
-
primaryColor?: string;
|
|
18
|
-
accentColor?: string;
|
|
19
|
-
onAddToCart?: (product: Product) => void;
|
|
20
|
-
}
|
|
21
10
|
|
|
22
11
|
export function MessageBubble({
|
|
23
12
|
message,
|
|
@@ -4,21 +4,7 @@ import React from 'react';
|
|
|
4
4
|
import Image from 'next/image';
|
|
5
5
|
import { ShoppingCart, ExternalLink } from 'lucide-react';
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
id: string | number;
|
|
9
|
-
name: string;
|
|
10
|
-
brand?: string;
|
|
11
|
-
price?: string | number;
|
|
12
|
-
image?: string;
|
|
13
|
-
link?: string;
|
|
14
|
-
description?: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
interface ProductCardProps {
|
|
18
|
-
product: Product;
|
|
19
|
-
primaryColor?: string;
|
|
20
|
-
onAddToCart?: (product: Product) => void;
|
|
21
|
-
}
|
|
7
|
+
import { ProductCardProps } from '../types';
|
|
22
8
|
|
|
23
9
|
export function ProductCard({ product, primaryColor = '#6366f1', onAddToCart }: ProductCardProps) {
|
|
24
10
|
return (
|
|
@@ -2,13 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { useRef, useState, useEffect } from 'react';
|
|
4
4
|
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
interface ProductCarouselProps {
|
|
8
|
-
products: Product[];
|
|
9
|
-
primaryColor?: string;
|
|
10
|
-
onAddToCart?: (product: Product) => void;
|
|
11
|
-
}
|
|
5
|
+
import { ProductCard } from './ProductCard';
|
|
6
|
+
import { ProductCarouselProps } from '../types';
|
|
12
7
|
|
|
13
8
|
export function ProductCarousel({ products, primaryColor = '#6366f1', onAddToCart }: ProductCarouselProps) {
|
|
14
9
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
@@ -2,12 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import React from 'react';
|
|
4
4
|
import { ChevronDown, ChevronUp } from 'lucide-react';
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
interface SourceCardProps {
|
|
8
|
-
source: VectorMatch;
|
|
9
|
-
index: number;
|
|
10
|
-
}
|
|
5
|
+
import { SourceCardProps } from '../types';
|
|
11
6
|
|
|
12
7
|
export function SourceCard({ source, index }: SourceCardProps) {
|
|
13
8
|
const [expanded, setExpanded] = React.useState(false);
|
package/src/config/RagConfig.ts
CHANGED
|
@@ -149,6 +149,8 @@ export interface UIConfig {
|
|
|
149
149
|
allowUpload?: boolean;
|
|
150
150
|
/** Whether to allow manual resizing of the chat window. Defaults to true. */
|
|
151
151
|
allowResize?: boolean;
|
|
152
|
+
/** Whether to enable voice search input. Defaults to true. */
|
|
153
|
+
enableVoiceInput?: boolean;
|
|
152
154
|
}
|
|
153
155
|
|
|
154
156
|
// ---------------------------------------------------------------------------
|
|
@@ -5,6 +5,29 @@
|
|
|
5
5
|
* is consumed via npm without Next.js path-alias configuration.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { ClientConfig } from '../types';
|
|
9
|
+
|
|
10
|
+
export const DEFAULT_CONFIG: ClientConfig = {
|
|
11
|
+
projectId: 'default',
|
|
12
|
+
ui: {
|
|
13
|
+
title: 'AI Assistant',
|
|
14
|
+
subtitle: 'Powered by RAG',
|
|
15
|
+
primaryColor: '#6366f1',
|
|
16
|
+
accentColor: '#8b5cf6',
|
|
17
|
+
logoUrl: '',
|
|
18
|
+
placeholder: 'Ask me anything…',
|
|
19
|
+
showSources: true,
|
|
20
|
+
welcomeMessage: "Hello! I'm your AI assistant. Ask me anything about your documents.",
|
|
21
|
+
showWidget: true,
|
|
22
|
+
poweredBy: 'RAG',
|
|
23
|
+
visualStyle: 'glass',
|
|
24
|
+
borderRadius: 'xl',
|
|
25
|
+
allowUpload: true,
|
|
26
|
+
allowResize: true,
|
|
27
|
+
enableVoiceInput: true,
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
8
31
|
/** Maps the `borderRadius` config value to a Tailwind class. */
|
|
9
32
|
export const BORDER_RADIUS_MAP: Record<string, string> = {
|
|
10
33
|
none: 'rounded-none',
|
package/src/core/Pipeline.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BaseVectorProvider } from '../providers/vectordb/BaseVectorProvider';
|
|
2
2
|
import { BaseGraphProvider } from '../providers/graphdb/BaseGraphProvider';
|
|
3
|
-
import { ILLMProvider
|
|
3
|
+
import { ILLMProvider } from '../llm/ILLMProvider';
|
|
4
|
+
import { ChatMessage } from '../types';
|
|
4
5
|
import { DocumentChunker, Chunk } from '../rag/DocumentChunker';
|
|
5
6
|
import { EntityExtractor } from '../rag/EntityExtractor';
|
|
6
7
|
import { Reranker } from '../rag/Reranker';
|
|
@@ -423,4 +424,59 @@ Optimized Search Query:`;
|
|
|
423
424
|
|
|
424
425
|
return rewrite.trim() || question;
|
|
425
426
|
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Generate 3-5 short, relevant questions based on the vector database content.
|
|
430
|
+
*/
|
|
431
|
+
async getSuggestions(query: string, namespace?: string): Promise<string[]> {
|
|
432
|
+
if (!query || query.trim().length < 3) {
|
|
433
|
+
return [];
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
await this.initialize();
|
|
437
|
+
const ns = namespace ?? this.config.projectId;
|
|
438
|
+
|
|
439
|
+
try {
|
|
440
|
+
// 1. Retrieve relevant context (top 3 matches)
|
|
441
|
+
const { sources } = await this.retrieve(query, { namespace: ns, topK: 3 });
|
|
442
|
+
|
|
443
|
+
if (sources.length === 0) {
|
|
444
|
+
return [];
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// 2. Generate suggestions using LLM
|
|
448
|
+
const context = sources.map((s) => s.content).join('\n\n---\n\n');
|
|
449
|
+
const prompt = `
|
|
450
|
+
Based on the following snippets from a document, what are 3 short, relevant questions a user might ask?
|
|
451
|
+
Focus on questions that can be answered by the context.
|
|
452
|
+
Keep each question under 10 words and make them very specific to the content.
|
|
453
|
+
Return ONLY a JSON array of strings like ["Question 1", "Question 2", "Question 3"].
|
|
454
|
+
|
|
455
|
+
Context:
|
|
456
|
+
${context}
|
|
457
|
+
|
|
458
|
+
Suggestions:`;
|
|
459
|
+
|
|
460
|
+
const response = await this.llmProvider.chat(
|
|
461
|
+
[
|
|
462
|
+
{ role: 'system', content: 'You are a helpful assistant that generates search suggestions.' },
|
|
463
|
+
{ role: 'user', content: prompt }
|
|
464
|
+
],
|
|
465
|
+
''
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
// Simple parsing of JSON array from LLM response
|
|
469
|
+
const match = response.match(/\[[\s\S]*\]/);
|
|
470
|
+
if (match) {
|
|
471
|
+
const suggestions = JSON.parse(match[0]);
|
|
472
|
+
if (Array.isArray(suggestions)) {
|
|
473
|
+
return suggestions.map(s => String(s)).slice(0, 3);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
} catch (error) {
|
|
477
|
+
console.error('[Pipeline] Failed to generate suggestions:', error);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return [];
|
|
481
|
+
}
|
|
426
482
|
}
|
package/src/core/VectorPlugin.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { ConfigValidator } from './ConfigValidator';
|
|
|
4
4
|
import { Pipeline } from './Pipeline';
|
|
5
5
|
import { ProviderHealthCheck, HealthCheckResult } from './ProviderHealthCheck';
|
|
6
6
|
import { IngestDocument, ChatResponse } from '../types';
|
|
7
|
-
import { ChatMessage } from '../
|
|
7
|
+
import { ChatMessage } from '../types';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* VectorPlugin — main orchestrator class.
|
|
@@ -81,4 +81,11 @@ export class VectorPlugin {
|
|
|
81
81
|
return this.pipeline.ingest(documents, namespace);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Get auto-suggestions based on a query prefix.
|
|
86
|
+
*/
|
|
87
|
+
async getSuggestions(query: string, namespace?: string): Promise<string[]> {
|
|
88
|
+
await this.validationPromise;
|
|
89
|
+
return this.pipeline.getSuggestions(query, namespace);
|
|
90
|
+
}
|
|
84
91
|
}
|
package/src/handlers/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
2
|
import { VectorPlugin } from '../core/VectorPlugin';
|
|
3
3
|
import { RagConfig } from '../config/RagConfig';
|
|
4
|
-
import { ChatMessage } from '../
|
|
4
|
+
import { ChatMessage } from '../types';
|
|
5
5
|
import { DocumentParser } from '../utils/DocumentParser';
|
|
6
6
|
|
|
7
7
|
// ─── SSE helpers ──────────────────────────────────────────────────────────────
|
|
@@ -261,5 +261,35 @@ export function createUploadHandler(configOrPlugin?: Partial<RagConfig> | Vector
|
|
|
261
261
|
};
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
+
/**
|
|
265
|
+
* createSuggestionsHandler — factory for the auto-suggestions endpoint.
|
|
266
|
+
*/
|
|
267
|
+
export function createSuggestionsHandler(configOrPlugin?: Partial<RagConfig> | VectorPlugin) {
|
|
268
|
+
const plugin =
|
|
269
|
+
configOrPlugin instanceof VectorPlugin
|
|
270
|
+
? configOrPlugin
|
|
271
|
+
: new VectorPlugin(configOrPlugin);
|
|
272
|
+
|
|
273
|
+
return async function POST(req: NextRequest) {
|
|
274
|
+
try {
|
|
275
|
+
const body = await req.json();
|
|
276
|
+
const { query, namespace } = body as {
|
|
277
|
+
query: string;
|
|
278
|
+
namespace?: string;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
if (typeof query !== 'string') {
|
|
282
|
+
return NextResponse.json({ error: 'query is required' }, { status: 400 });
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const suggestions = await plugin.getSuggestions(query, namespace);
|
|
286
|
+
return NextResponse.json({ suggestions });
|
|
287
|
+
} catch (err) {
|
|
288
|
+
const message = err instanceof Error ? err.message : 'Internal server error';
|
|
289
|
+
return NextResponse.json({ error: message }, { status: 500 });
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
264
294
|
// Re-export SSE helper so host apps can use it in custom handlers
|
|
265
295
|
export { sseFrame, sseTextFrame, sseMetaFrame, sseErrorFrame };
|
package/src/hooks/useRagChat.ts
CHANGED
|
@@ -19,53 +19,9 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
22
|
-
import { VectorMatch } from '../types';
|
|
22
|
+
import { VectorMatch, RagMessage, UseRagChatOptions, UseRagChatReturn } from '../types';
|
|
23
23
|
import { useStoredMessages } from './useStoredMessages';
|
|
24
24
|
|
|
25
|
-
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
26
|
-
|
|
27
|
-
export type MessageRole = 'user' | 'assistant';
|
|
28
|
-
|
|
29
|
-
export interface RagMessage {
|
|
30
|
-
id: string;
|
|
31
|
-
role: MessageRole;
|
|
32
|
-
content: string;
|
|
33
|
-
/** Retrieved source chunks (assistant messages only) */
|
|
34
|
-
sources?: VectorMatch[];
|
|
35
|
-
/** ISO timestamp */
|
|
36
|
-
createdAt: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface UseRagChatOptions {
|
|
40
|
-
/** Override the chat API endpoint (default: /api/chat) */
|
|
41
|
-
apiUrl?: string;
|
|
42
|
-
/** Override project namespace */
|
|
43
|
-
namespace?: string;
|
|
44
|
-
/** Persist chat history to localStorage (default: true) */
|
|
45
|
-
persist?: boolean;
|
|
46
|
-
/** Called after each successful assistant reply */
|
|
47
|
-
onReply?: (message: RagMessage) => void;
|
|
48
|
-
/** Called on error */
|
|
49
|
-
onError?: (error: string) => void;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export interface UseRagChatReturn {
|
|
53
|
-
/** All messages in the current conversation */
|
|
54
|
-
messages: RagMessage[];
|
|
55
|
-
/** Whether a response is in flight */
|
|
56
|
-
isLoading: boolean;
|
|
57
|
-
/** Current error message, or null */
|
|
58
|
-
error: string | null;
|
|
59
|
-
/** Send a user message */
|
|
60
|
-
send: (text: string) => Promise<void>;
|
|
61
|
-
/** Clear the conversation (and localStorage if persist=true) */
|
|
62
|
-
clear: () => void;
|
|
63
|
-
/** Retry the last failed send */
|
|
64
|
-
retry: () => Promise<void>;
|
|
65
|
-
/** Programmatically set the conversation (e.g. to restore from a DB) */
|
|
66
|
-
setMessages: React.Dispatch<React.SetStateAction<RagMessage[]>>;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
25
|
// ─── SSE Frame Parser ──────────────────────────────────────────────────────────
|
|
70
26
|
|
|
71
27
|
interface SseTextFrame { type: 'text'; text: string }
|