@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.
Files changed (49) hide show
  1. package/dist/DocumentChunker-BXOUMKoP.d.ts +93 -0
  2. package/dist/DocumentChunker-D1dg5iCi.d.mts +93 -0
  3. package/dist/{chunk-OCDCJUNE.mjs → chunk-GCPPRD2G.mjs} +70 -1
  4. package/dist/handlers/index.d.mts +2 -2
  5. package/dist/handlers/index.d.ts +2 -2
  6. package/dist/handlers/index.js +70 -0
  7. package/dist/handlers/index.mjs +3 -1
  8. package/dist/{index-CYgr00ot.d.ts → index-B67KQ9NN.d.ts} +14 -2
  9. package/dist/{RagConfig-FyMB_UG6.d.mts → index-Cti1u0y1.d.mts} +87 -86
  10. package/dist/{RagConfig-FyMB_UG6.d.ts → index-Cti1u0y1.d.ts} +87 -86
  11. package/dist/{index-D-lfcqlL.d.mts → index-kUXnRvuI.d.mts} +14 -2
  12. package/dist/index.d.mts +53 -89
  13. package/dist/index.d.ts +53 -89
  14. package/dist/index.js +139 -27
  15. package/dist/index.mjs +143 -27
  16. package/dist/server.d.mts +9 -4
  17. package/dist/server.d.ts +9 -4
  18. package/dist/server.js +51 -0
  19. package/dist/server.mjs +1 -1
  20. package/package.json +1 -1
  21. package/src/app/api/suggestions/route.ts +4 -0
  22. package/src/components/ChatWidget.tsx +1 -7
  23. package/src/components/ChatWindow.tsx +180 -25
  24. package/src/components/ConfigProvider.tsx +7 -33
  25. package/src/components/DocumentUpload.tsx +1 -8
  26. package/src/components/MessageBubble.tsx +1 -12
  27. package/src/components/ProductCard.tsx +1 -15
  28. package/src/components/ProductCarousel.tsx +2 -7
  29. package/src/components/SourceCard.tsx +1 -6
  30. package/src/config/RagConfig.ts +2 -0
  31. package/src/config/uiConstants.ts +23 -0
  32. package/src/core/Pipeline.ts +57 -1
  33. package/src/core/VectorPlugin.ts +8 -1
  34. package/src/handlers/index.ts +31 -1
  35. package/src/hooks/useRagChat.ts +1 -45
  36. package/src/hooks/useStoredMessages.ts +1 -1
  37. package/src/index.ts +20 -5
  38. package/src/llm/ILLMProvider.ts +1 -13
  39. package/src/llm/providers/AnthropicProvider.ts +2 -1
  40. package/src/llm/providers/GeminiProvider.ts +2 -1
  41. package/src/llm/providers/OllamaProvider.ts +2 -1
  42. package/src/llm/providers/OpenAIProvider.ts +2 -1
  43. package/src/llm/providers/UniversalLLMAdapter.ts +2 -1
  44. package/src/server.ts +2 -2
  45. package/src/types/chat.ts +53 -0
  46. package/src/types/index.ts +17 -0
  47. package/src/types/props.ts +79 -0
  48. package/dist/DocumentChunker-Dh9TvmGG.d.mts +0 -45
  49. 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 ChatWindowProps {
21
- /** Additional className for the wrapper div */
22
- className?: string;
23
- /** Inline styles for the wrapper div */
24
- style?: React.CSSProperties;
25
- /** Called when the close button is clicked (for widget mode) */
26
- onClose?: () => void;
27
- /** Whether to show a close (X) button in the header */
28
- showClose?: boolean;
29
- /** Called when the user starts dragging the resize handle */
30
- onResizeStart?: (e: React.MouseEvent) => void;
31
- /** Called when the user clicks the reset size button */
32
- onResetResize?: () => void;
33
- /** Whether the window has been resized from its default */
34
- isResized?: boolean;
35
- /** Called when the user clicks the maximize/minimize button */
36
- onMaximize?: () => void;
37
- /** Whether the window is currently maximized */
38
- isMaximized?: boolean;
39
- /** Called when the user clicks 'Add to Cart' on a product */
40
- onAddToCart?: (product: any) => void;
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
- const currentRadius = BORDER_RADIUS_MAP[ui.borderRadius || 'xl'];
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) => setInput(e.target.value)}
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, ReactNode } from 'react';
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
- 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
- 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 || defaultConfig.projectId,
50
- ui: mergeDefined(defaultConfig.ui, config?.ui) as Required<UIConfig>,
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
- interface DocumentUploadProps {
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 { ChatMessage } from '../llm/ILLMProvider';
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
- export interface Product {
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 { Product, ProductCard } from './ProductCard';
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 { VectorMatch } from '../types';
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);
@@ -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',
@@ -1,6 +1,7 @@
1
1
  import { BaseVectorProvider } from '../providers/vectordb/BaseVectorProvider';
2
2
  import { BaseGraphProvider } from '../providers/graphdb/BaseGraphProvider';
3
- import { ILLMProvider, ChatMessage } from '../llm/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
  }
@@ -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 '../llm/ILLMProvider';
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
  }
@@ -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 '../llm/ILLMProvider';
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 };
@@ -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 }
@@ -4,7 +4,7 @@ import * as React from 'react';
4
4
 
5
5
  export interface StoredMessage {
6
6
  id: string;
7
- role: 'user' | 'assistant';
7
+ role: 'user' | 'assistant' | 'system';
8
8
  content: string;
9
9
  createdAt?: string;
10
10
  sources?: unknown;