@retrivora-ai/rag-engine 1.2.1 → 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 (43) hide show
  1. package/dist/DocumentChunker-BXOUMKoP.d.ts +93 -0
  2. package/dist/DocumentChunker-D1dg5iCi.d.mts +93 -0
  3. package/dist/handlers/index.d.mts +2 -2
  4. package/dist/handlers/index.d.ts +2 -2
  5. package/dist/{index-DbtE8wLM.d.ts → index-B67KQ9NN.d.ts} +1 -1
  6. package/dist/{RagConfig-BOLOz0_O.d.mts → index-Cti1u0y1.d.mts} +86 -94
  7. package/dist/{RagConfig-BOLOz0_O.d.ts → index-Cti1u0y1.d.ts} +86 -94
  8. package/dist/{index-64BDupW3.d.mts → index-kUXnRvuI.d.mts} +1 -1
  9. package/dist/index.d.mts +52 -78
  10. package/dist/index.d.ts +52 -78
  11. package/dist/index.js +93 -27
  12. package/dist/index.mjs +97 -27
  13. package/dist/server.d.mts +5 -4
  14. package/dist/server.d.ts +5 -4
  15. package/package.json +1 -1
  16. package/src/components/ChatWidget.tsx +1 -8
  17. package/src/components/ChatWindow.tsx +119 -29
  18. package/src/components/ConfigProvider.tsx +7 -33
  19. package/src/components/DocumentUpload.tsx +1 -8
  20. package/src/components/MessageBubble.tsx +1 -12
  21. package/src/components/ProductCard.tsx +1 -7
  22. package/src/components/ProductCarousel.tsx +1 -7
  23. package/src/components/SourceCard.tsx +1 -6
  24. package/src/config/RagConfig.ts +2 -0
  25. package/src/config/uiConstants.ts +23 -0
  26. package/src/core/Pipeline.ts +2 -1
  27. package/src/core/VectorPlugin.ts +1 -1
  28. package/src/handlers/index.ts +1 -1
  29. package/src/hooks/useRagChat.ts +1 -45
  30. package/src/hooks/useStoredMessages.ts +1 -1
  31. package/src/index.ts +20 -5
  32. package/src/llm/ILLMProvider.ts +1 -13
  33. package/src/llm/providers/AnthropicProvider.ts +2 -1
  34. package/src/llm/providers/GeminiProvider.ts +2 -1
  35. package/src/llm/providers/OllamaProvider.ts +2 -1
  36. package/src/llm/providers/OpenAIProvider.ts +2 -1
  37. package/src/llm/providers/UniversalLLMAdapter.ts +2 -1
  38. package/src/server.ts +2 -2
  39. package/src/types/chat.ts +53 -0
  40. package/src/types/index.ts +3 -0
  41. package/src/types/props.ts +79 -0
  42. package/dist/DocumentChunker-Dh9TvmGG.d.mts +0 -45
  43. package/dist/DocumentChunker-Dh9TvmGG.d.ts +0 -45
package/dist/index.mjs CHANGED
@@ -21,7 +21,9 @@ import {
21
21
  TriangleAlert,
22
22
  RotateCcw,
23
23
  Maximize2,
24
- Minimize2
24
+ Minimize2,
25
+ Mic,
26
+ MicOff
25
27
  } from "lucide-react";
26
28
 
27
29
  // src/components/MessageBubble.tsx
@@ -281,7 +283,9 @@ function MessageBubble({
281
283
 
282
284
  // src/components/ConfigProvider.tsx
283
285
  import React5, { createContext, useContext } from "react";
284
- var defaultConfig = {
286
+
287
+ // src/config/uiConstants.ts
288
+ var DEFAULT_CONFIG = {
285
289
  projectId: "default",
286
290
  ui: {
287
291
  title: "AI Assistant",
@@ -297,17 +301,33 @@ var defaultConfig = {
297
301
  visualStyle: "glass",
298
302
  borderRadius: "xl",
299
303
  allowUpload: true,
300
- allowResize: true
304
+ allowResize: true,
305
+ enableVoiceInput: true
301
306
  }
302
307
  };
303
- var ConfigContext = createContext(defaultConfig);
308
+ var BORDER_RADIUS_MAP = {
309
+ none: "rounded-none",
310
+ sm: "rounded-sm",
311
+ md: "rounded-md",
312
+ lg: "rounded-lg",
313
+ xl: "rounded-xl",
314
+ full: "rounded-3xl"
315
+ };
316
+ var CHAT_SUGGESTIONS = [
317
+ "What can you help me with?",
318
+ "Summarise the key topics",
319
+ "Show me an example"
320
+ ];
321
+
322
+ // src/components/ConfigProvider.tsx
323
+ var ConfigContext = createContext(DEFAULT_CONFIG);
304
324
  function ConfigProvider({
305
325
  config,
306
326
  children
307
327
  }) {
308
328
  const merged = {
309
- projectId: (config == null ? void 0 : config.projectId) || defaultConfig.projectId,
310
- ui: mergeDefined(defaultConfig.ui, config == null ? void 0 : config.ui)
329
+ projectId: (config == null ? void 0 : config.projectId) || DEFAULT_CONFIG.projectId,
330
+ ui: mergeDefined(DEFAULT_CONFIG.ui, config == null ? void 0 : config.ui)
311
331
  };
312
332
  return /* @__PURE__ */ React5.createElement(ConfigContext.Provider, { value: merged }, children);
313
333
  }
@@ -505,21 +525,6 @@ function useRagChat(projectId, options = {}) {
505
525
  };
506
526
  }
507
527
 
508
- // src/config/uiConstants.ts
509
- var BORDER_RADIUS_MAP = {
510
- none: "rounded-none",
511
- sm: "rounded-sm",
512
- md: "rounded-md",
513
- lg: "rounded-lg",
514
- xl: "rounded-xl",
515
- full: "rounded-3xl"
516
- };
517
- var CHAT_SUGGESTIONS = [
518
- "What can you help me with?",
519
- "Summarise the key topics",
520
- "Show me an example"
521
- ];
522
-
523
528
  // src/components/ChatWindow.tsx
524
529
  function ChatWindow({
525
530
  className = "",
@@ -542,6 +547,56 @@ function ChatWindow({
542
547
  const { messages, send, clear, isLoading, error } = useRagChat(projectId, {
543
548
  namespace: projectId
544
549
  });
550
+ const [suggestions, setSuggestions] = useState4([]);
551
+ const [isSuggesting, setIsSuggesting] = useState4(false);
552
+ const [isListening, setIsListening] = useState4(false);
553
+ const recognitionRef = useRef3(null);
554
+ useEffect4(() => {
555
+ if (typeof window !== "undefined") {
556
+ const win = window;
557
+ const SpeechRecognition = win.SpeechRecognition || win.webkitSpeechRecognition;
558
+ if (SpeechRecognition) {
559
+ const recognition = new SpeechRecognition();
560
+ recognition.continuous = true;
561
+ recognition.interimResults = true;
562
+ recognition.onresult = (event) => {
563
+ let finalTranscript = "";
564
+ for (let i = event.resultIndex; i < event.results.length; ++i) {
565
+ if (event.results[i].isFinal) {
566
+ finalTranscript += event.results[i][0].transcript;
567
+ }
568
+ }
569
+ if (finalTranscript) {
570
+ setInput((prev) => (prev ? prev + " " : "") + finalTranscript.trim());
571
+ }
572
+ };
573
+ recognition.onerror = (event) => {
574
+ console.error("Speech recognition error:", event.error);
575
+ setIsListening(false);
576
+ };
577
+ recognition.onend = () => {
578
+ setIsListening(false);
579
+ };
580
+ recognitionRef.current = recognition;
581
+ }
582
+ }
583
+ }, []);
584
+ const toggleListening = () => {
585
+ if (!recognitionRef.current) {
586
+ alert("Speech recognition is not supported in your browser.");
587
+ return;
588
+ }
589
+ if (isListening) {
590
+ recognitionRef.current.stop();
591
+ } else {
592
+ try {
593
+ recognitionRef.current.start();
594
+ setIsListening(true);
595
+ } catch (err) {
596
+ console.error("Failed to start recognition:", err);
597
+ }
598
+ }
599
+ };
545
600
  useEffect4(() => {
546
601
  setMounted(true);
547
602
  }, []);
@@ -552,15 +607,21 @@ function ChatWindow({
552
607
  }
553
608
  }, [messages, isLoading]);
554
609
  const sendMessage = useCallback2(async () => {
610
+ var _a2;
555
611
  const text = input.trim();
556
612
  if (!text || isLoading) return;
613
+ if (isListening) {
614
+ (_a2 = recognitionRef.current) == null ? void 0 : _a2.stop();
615
+ }
557
616
  setInput("");
617
+ setSuggestions([]);
618
+ setIsSuggesting(false);
558
619
  await send(text);
559
620
  window.setTimeout(() => {
560
- var _a2;
561
- return (_a2 = inputRef.current) == null ? void 0 : _a2.focus();
621
+ var _a3;
622
+ return (_a3 = inputRef.current) == null ? void 0 : _a3.focus();
562
623
  }, 50);
563
- }, [input, isLoading, send]);
624
+ }, [input, isLoading, send, isListening]);
564
625
  const handleKeyDown = (e) => {
565
626
  if (e.key === "Enter" && !e.shiftKey) {
566
627
  e.preventDefault();
@@ -569,12 +630,12 @@ function ChatWindow({
569
630
  };
570
631
  const clearHistory = () => {
571
632
  clear();
633
+ setSuggestions([]);
634
+ setIsSuggesting(false);
572
635
  };
573
636
  const isEmpty = messages.length === 0;
574
637
  const currentRadius = BORDER_RADIUS_MAP[ui.borderRadius || "xl"];
575
638
  const isGlass = ui.visualStyle !== "solid";
576
- const [suggestions, setSuggestions] = useState4([]);
577
- const [isSuggesting, setIsSuggesting] = useState4(false);
578
639
  useEffect4(() => {
579
640
  if (input.trim().length < 3) {
580
641
  return;
@@ -703,7 +764,7 @@ function ChatWindow({
703
764
  )), isLoading && ((_a = messages[messages.length - 1]) == null ? void 0 : _a.role) !== "assistant" && /* @__PURE__ */ React7.createElement(
704
765
  MessageBubble,
705
766
  {
706
- message: { role: "assistant", content: "" },
767
+ message: { id: "loading", role: "assistant", content: "", createdAt: (/* @__PURE__ */ new Date()).toISOString() },
707
768
  isStreaming: true,
708
769
  primaryColor: ui.primaryColor,
709
770
  accentColor: ui.accentColor,
@@ -743,6 +804,15 @@ function ChatWindow({
743
804
  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",
744
805
  style: { scrollbarWidth: "none" }
745
806
  }
807
+ ), ui.enableVoiceInput !== false && /* @__PURE__ */ React7.createElement(
808
+ "button",
809
+ {
810
+ type: "button",
811
+ onClick: toggleListening,
812
+ className: `flex-shrink-0 w-9 h-9 rounded-xl flex items-center justify-center transition-all hover:scale-105 active:scale-95 ${isListening ? "bg-rose-500 text-white animate-pulse shadow-md shadow-rose-500/20" : "text-slate-400 dark:text-white/40 hover:bg-slate-200 dark:hover:bg-white/10"}`,
813
+ title: isListening ? "Stop listening" : "Start voice input"
814
+ },
815
+ isListening ? /* @__PURE__ */ React7.createElement(MicOff, { className: "w-4 h-4" }) : /* @__PURE__ */ React7.createElement(Mic, { className: "w-4 h-4" })
746
816
  ), /* @__PURE__ */ React7.createElement(
747
817
  "button",
748
818
  {
package/dist/server.d.mts CHANGED
@@ -1,7 +1,8 @@
1
- import { i as VectorDBConfig, L as LLMConfig, c as EmbeddingConfig, g as RagConfig, e as IngestDocument, C as ChatMessage, b as ChatResponse, k as RetrievalResult, h as UpsertDocument, V as VectorMatch, G as GraphDBConfig, l as GraphNode, m as Edge, n as GraphSearchResult, I as ILLMProvider, j as VectorDBProvider, f as LLMProvider, d as EmbeddingProvider, R as RAGConfig, U as UIConfig, a as ChatOptions, E as EmbedOptions } from './RagConfig-BOLOz0_O.mjs';
2
- export { C as Chunk, a as ChunkOptions, D as DocumentChunker } from './DocumentChunker-Dh9TvmGG.mjs';
3
- import { H as HealthCheckResult, I as IProviderValidator, a as IProviderHealthChecker } from './index-64BDupW3.mjs';
4
- export { C as ConfigValidator, V as ValidationError, b as VectorPlugin, c as createChatHandler, d as createHealthHandler, e as createIngestHandler, f as createStreamHandler, g as createUploadHandler, s as sseErrorFrame, h as sseFrame, i as sseMetaFrame, j as sseTextFrame } from './index-64BDupW3.mjs';
1
+ import { j as VectorDBConfig, L as LLMConfig, E as EmbeddingConfig, h as RagConfig, I as IngestDocument, C as ChatMessage, d as ChatResponse, l as RetrievalResult, i as UpsertDocument, V as VectorMatch, G as GraphDBConfig, m as GraphNode, n as Edge, o as GraphSearchResult, k as VectorDBProvider, f as LLMProvider, e as EmbeddingProvider, g as RAGConfig, U as UIConfig, c as ChatOptions } from './index-Cti1u0y1.mjs';
2
+ import { I as ILLMProvider, E as EmbedOptions } from './DocumentChunker-D1dg5iCi.mjs';
3
+ export { C as Chunk, a as ChunkOptions, D as DocumentChunker } from './DocumentChunker-D1dg5iCi.mjs';
4
+ import { H as HealthCheckResult, I as IProviderValidator, a as IProviderHealthChecker } from './index-kUXnRvuI.mjs';
5
+ export { C as ConfigValidator, V as ValidationError, b as VectorPlugin, c as createChatHandler, d as createHealthHandler, e as createIngestHandler, f as createStreamHandler, g as createUploadHandler, s as sseErrorFrame, h as sseFrame, i as sseMetaFrame, j as sseTextFrame } from './index-kUXnRvuI.mjs';
5
6
  import 'next/server';
6
7
 
7
8
  /**
package/dist/server.d.ts CHANGED
@@ -1,7 +1,8 @@
1
- import { i as VectorDBConfig, L as LLMConfig, c as EmbeddingConfig, g as RagConfig, e as IngestDocument, C as ChatMessage, b as ChatResponse, k as RetrievalResult, h as UpsertDocument, V as VectorMatch, G as GraphDBConfig, l as GraphNode, m as Edge, n as GraphSearchResult, I as ILLMProvider, j as VectorDBProvider, f as LLMProvider, d as EmbeddingProvider, R as RAGConfig, U as UIConfig, a as ChatOptions, E as EmbedOptions } from './RagConfig-BOLOz0_O.js';
2
- export { C as Chunk, a as ChunkOptions, D as DocumentChunker } from './DocumentChunker-Dh9TvmGG.js';
3
- import { H as HealthCheckResult, I as IProviderValidator, a as IProviderHealthChecker } from './index-DbtE8wLM.js';
4
- export { C as ConfigValidator, V as ValidationError, b as VectorPlugin, c as createChatHandler, d as createHealthHandler, e as createIngestHandler, f as createStreamHandler, g as createUploadHandler, s as sseErrorFrame, h as sseFrame, i as sseMetaFrame, j as sseTextFrame } from './index-DbtE8wLM.js';
1
+ import { j as VectorDBConfig, L as LLMConfig, E as EmbeddingConfig, h as RagConfig, I as IngestDocument, C as ChatMessage, d as ChatResponse, l as RetrievalResult, i as UpsertDocument, V as VectorMatch, G as GraphDBConfig, m as GraphNode, n as Edge, o as GraphSearchResult, k as VectorDBProvider, f as LLMProvider, e as EmbeddingProvider, g as RAGConfig, U as UIConfig, c as ChatOptions } from './index-Cti1u0y1.js';
2
+ import { I as ILLMProvider, E as EmbedOptions } from './DocumentChunker-BXOUMKoP.js';
3
+ export { C as Chunk, a as ChunkOptions, D as DocumentChunker } from './DocumentChunker-BXOUMKoP.js';
4
+ import { H as HealthCheckResult, I as IProviderValidator, a as IProviderHealthChecker } from './index-B67KQ9NN.js';
5
+ export { C as ConfigValidator, V as ValidationError, b as VectorPlugin, c as createChatHandler, d as createHealthHandler, e as createIngestHandler, f as createStreamHandler, g as createUploadHandler, s as sseErrorFrame, h as sseFrame, i as sseMetaFrame, j as sseTextFrame } from './index-B67KQ9NN.js';
5
6
  import 'next/server';
6
7
 
7
8
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retrivora-ai/rag-engine",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "Retrivora AI is a plug-and-play AI engine for RAG chat experiences — generic vector DB + LLM provider, embeddable or standalone.",
5
5
  "author": "Abhinav Alkuchi",
6
6
  "license": "MIT",
@@ -4,14 +4,7 @@ import React, { useState } from 'react';
4
4
  import { MessageSquare, X } from 'lucide-react';
5
5
  import { ChatWindow } from './ChatWindow';
6
6
  import { useConfig } from './ConfigProvider';
7
- import { Product } from '../types';
8
-
9
- interface ChatWidgetProps {
10
- /** Position of the floating button. Defaults to bottom-right. */
11
- position?: 'bottom-right' | 'bottom-left';
12
- /** Called when the user clicks 'Add to Cart' on a product */
13
- onAddToCart?: (product: Product) => void;
14
- }
7
+ import { ChatWidgetProps } from '../types';
15
8
 
16
9
  const DEFAULT_DIMENSIONS = { width: 380, height: 600 };
17
10
 
@@ -11,34 +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';
19
- import { Product } from '../types';
20
-
21
- interface ChatWindowProps {
22
- /** Additional className for the wrapper div */
23
- className?: string;
24
- /** Inline styles for the wrapper div */
25
- style?: React.CSSProperties;
26
- /** Called when the close button is clicked (for widget mode) */
27
- onClose?: () => void;
28
- /** Whether to show a close (X) button in the header */
29
- showClose?: boolean;
30
- /** Called when the user starts dragging the resize handle */
31
- onResizeStart?: (e: React.MouseEvent) => void;
32
- /** Called when the user clicks the reset size button */
33
- onResetResize?: () => void;
34
- /** Whether the window has been resized from its default */
35
- isResized?: boolean;
36
- /** Called when the user clicks the maximize/minimize button */
37
- onMaximize?: () => void;
38
- /** Whether the window is currently maximized */
39
- isMaximized?: boolean;
40
- /** Called when the user clicks 'Add to Cart' on a product */
41
- onAddToCart?: (product: Product) => void;
21
+ import { ChatWindowProps } from '../types';
22
+
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;
42
51
  }
43
52
 
44
53
  export function ChatWindow({
@@ -62,6 +71,66 @@ export function ChatWindow({
62
71
  namespace: projectId,
63
72
  });
64
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
+
65
134
  useEffect(() => {
66
135
  // eslint-disable-next-line react-hooks/set-state-in-effect
67
136
  setMounted(true);
@@ -77,10 +146,17 @@ export function ChatWindow({
77
146
  const sendMessage = useCallback(async () => {
78
147
  const text = input.trim();
79
148
  if (!text || isLoading) return;
149
+
150
+ if (isListening) {
151
+ recognitionRef.current?.stop();
152
+ }
153
+
80
154
  setInput('');
155
+ setSuggestions([]);
156
+ setIsSuggesting(false);
81
157
  await send(text);
82
158
  window.setTimeout(() => inputRef.current?.focus(), 50);
83
- }, [input, isLoading, send]);
159
+ }, [input, isLoading, send, isListening]);
84
160
 
85
161
  const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
86
162
  if (e.key === 'Enter' && !e.shiftKey) {
@@ -91,16 +167,15 @@ export function ChatWindow({
91
167
 
92
168
  const clearHistory = () => {
93
169
  clear();
170
+ setSuggestions([]);
171
+ setIsSuggesting(false);
94
172
  };
95
173
 
96
174
  const isEmpty = messages.length === 0;
97
175
 
98
- const currentRadius = BORDER_RADIUS_MAP[ui.borderRadius || 'xl'];
176
+ const currentRadius = BORDER_RADIUS_MAP[ui.borderRadius || 'xl'];
99
177
  const isGlass = ui.visualStyle !== 'solid';
100
178
 
101
- const [suggestions, setSuggestions] = useState<string[]>([]);
102
- const [isSuggesting, setIsSuggesting] = useState(false);
103
-
104
179
  // Auto-suggestions fetcher (debounced)
105
180
  useEffect(() => {
106
181
  if (input.trim().length < 3) {
@@ -276,7 +351,7 @@ export function ChatWindow({
276
351
  {/* Typing indicator (only if assistant hasn't started replying yet) */}
277
352
  {isLoading && messages[messages.length - 1]?.role !== 'assistant' && (
278
353
  <MessageBubble
279
- message={{ role: 'assistant', content: '' }}
354
+ message={{ id: 'loading', role: 'assistant', content: '', createdAt: new Date().toISOString() }}
280
355
  isStreaming
281
356
  primaryColor={ui.primaryColor}
282
357
  accentColor={ui.accentColor}
@@ -343,6 +418,21 @@ export function ChatWindow({
343
418
  style={{ scrollbarWidth: 'none' }}
344
419
  />
345
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
+
346
436
  <button
347
437
  onClick={sendMessage}
348
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 '../types';
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,13 +4,7 @@ import React from 'react';
4
4
  import Image from 'next/image';
5
5
  import { ShoppingCart, ExternalLink } from 'lucide-react';
6
6
 
7
- import { Product } from '../types';
8
-
9
- interface ProductCardProps {
10
- product: Product;
11
- primaryColor?: string;
12
- onAddToCart?: (product: Product) => void;
13
- }
7
+ import { ProductCardProps } from '../types';
14
8
 
15
9
  export function ProductCard({ product, primaryColor = '#6366f1', onAddToCart }: ProductCardProps) {
16
10
  return (
@@ -3,13 +3,7 @@
3
3
  import React, { useRef, useState, useEffect } from 'react';
4
4
  import { ChevronLeft, ChevronRight } from 'lucide-react';
5
5
  import { ProductCard } from './ProductCard';
6
- import { Product } from '../types';
7
-
8
- interface ProductCarouselProps {
9
- products: Product[];
10
- primaryColor?: string;
11
- onAddToCart?: (product: Product) => void;
12
- }
6
+ import { ProductCarouselProps } from '../types';
13
7
 
14
8
  export function ProductCarousel({ products, primaryColor = '#6366f1', onAddToCart }: ProductCarouselProps) {
15
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';
@@ -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.