@retrivora-ai/rag-engine 2.2.1 → 2.2.3

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.
@@ -1,9 +1,8 @@
1
1
  'use client';
2
2
 
3
3
  import React, { useState, useRef } from 'react';
4
- import { Upload, File, X, CheckCircle, AlertCircle, Loader2 } from 'lucide-react';
4
+ import { Upload, File, X, CheckCircle, AlertCircle, Loader2, CheckCircle2, Sparkles, Files } from 'lucide-react';
5
5
  import { useConfig } from './ConfigProvider';
6
-
7
6
  import { DocumentUploadProps } from '../types';
8
7
 
9
8
  interface FileState {
@@ -18,6 +17,14 @@ export function DocumentUpload({ namespace, uploadUrl, retrivoraApiBase, onUploa
18
17
  const [isDragging, setIsDragging] = useState(false);
19
18
  const fileInputRef = useRef<HTMLInputElement>(null);
20
19
 
20
+ // Success Modal State
21
+ const [showSuccessModal, setShowSuccessModal] = useState(false);
22
+ const [lastUploadSummary, setLastUploadSummary] = useState<{
23
+ totalFilesCount: number;
24
+ filesUploaded: string[];
25
+ namespaceUsed: string;
26
+ } | null>(null);
27
+
21
28
  const isFreeTier = !projectId || projectId.includes('free') || projectId.includes('demo');
22
29
 
23
30
  const MULTI_DIMENSION_MODELS: Record<string, number[]> = {
@@ -30,7 +37,6 @@ export function DocumentUpload({ namespace, uploadUrl, retrivoraApiBase, onUploa
30
37
  const currentModel = embedding?.model || 'unknown';
31
38
  const defaultDimension = isFreeTier ? 768 : (embedding?.dimensions || 1536);
32
39
 
33
- // Provide specific dimensions for known models, otherwise provide common ones
34
40
  let availableDimensions = isFreeTier
35
41
  ? [768]
36
42
  : Object.entries(MULTI_DIMENSION_MODELS).find(([k]) => currentModel.includes(k))?.[1];
@@ -41,7 +47,6 @@ export function DocumentUpload({ namespace, uploadUrl, retrivoraApiBase, onUploa
41
47
  : [...COMMON_DIMENSIONS, defaultDimension].sort((a, b) => a - b);
42
48
  }
43
49
 
44
- const isChangeable = !isFreeTier;
45
50
  const [dimension, setDimension] = useState(defaultDimension);
46
51
 
47
52
  const addFiles = (files: File[]) => {
@@ -80,12 +85,11 @@ export function DocumentUpload({ namespace, uploadUrl, retrivoraApiBase, onUploa
80
85
  const idleFiles = fileStates.filter(s => s.status === 'idle');
81
86
  if (idleFiles.length === 0) return;
82
87
 
83
- // Update status to uploading
84
88
  setFileStates(prev => prev.map(s => s.status === 'idle' ? { ...s, status: 'uploading' } : s));
85
89
 
86
90
  const formData = new FormData();
87
91
  idleFiles.forEach(s => formData.append('files', s.file));
88
- const targetNs = namespace || projectId;
92
+ const targetNs = namespace || projectId || 'default';
89
93
  if (targetNs) formData.append('namespace', targetNs);
90
94
  formData.append('dimension', dimension.toString());
91
95
 
@@ -100,7 +104,6 @@ export function DocumentUpload({ namespace, uploadUrl, retrivoraApiBase, onUploa
100
104
  body: formData,
101
105
  });
102
106
 
103
- // Fallback resolution: if primaryUrl returns 404 and is not '/api/upload', try '/api/upload'
104
107
  if (response.status === 404 && primaryUrl !== '/api/upload') {
105
108
  console.warn(`[Retrivora SDK] ⚠️ Upload endpoint "${primaryUrl}" returned 404. Retrying fallback to "/api/upload"...`);
106
109
  response = await fetch('/api/upload', {
@@ -116,6 +119,15 @@ export function DocumentUpload({ namespace, uploadUrl, retrivoraApiBase, onUploa
116
119
  console.log(`[Retrivora SDK] ✅ Upload response success:`, result);
117
120
 
118
121
  setFileStates(prev => prev.map(s => s.status === 'uploading' ? { ...s, status: 'success' } : s));
122
+
123
+ const updatedSuccessCount = fileStates.length;
124
+ setLastUploadSummary({
125
+ totalFilesCount: updatedSuccessCount,
126
+ filesUploaded: idleFiles.map(s => s.file.name),
127
+ namespaceUsed: targetNs,
128
+ });
129
+ setShowSuccessModal(true);
130
+
119
131
  if (onUploadComplete) onUploadComplete(result);
120
132
  } catch (err: any) {
121
133
  const message = err instanceof Error ? err.message : 'Upload failed';
@@ -128,127 +140,191 @@ export function DocumentUpload({ namespace, uploadUrl, retrivoraApiBase, onUploa
128
140
  const hasIdle = fileStates.some(s => s.status === 'idle');
129
141
 
130
142
  return (
131
- <div
132
- className={`p-6 border-2 border-dashed transition-all duration-300 rounded-2xl ${
133
- isDragging
134
- ? 'border-emerald-500 bg-emerald-50/50 dark:bg-emerald-500/5'
135
- : 'border-slate-200 dark:border-white/10 bg-white dark:bg-white/5'
136
- } ${className}`}
137
- onDragOver={onDragOver}
138
- onDragLeave={onDragLeave}
139
- onDrop={onDrop}
140
- >
141
- <div className="flex flex-col items-center justify-center text-center">
142
- <div
143
- className="w-12 h-12 rounded-full flex items-center justify-center mb-4 shadow-sm"
144
- style={{ background: `linear-gradient(135deg, ${ui.primaryColor}20, ${ui.accentColor}20)` }}
145
- >
146
- <Upload className="w-6 h-6" style={{ color: ui.primaryColor }} />
147
- </div>
148
- <h3 className="text-slate-900 dark:text-white font-semibold mb-1">Upload Documents</h3>
149
- <p className="text-slate-500 dark:text-white/40 text-sm mb-6 max-w-xs">
150
- Drag and drop PDF, DOCX, Excel (.xlsx, .xls), CSV, TXT, or JSON files to train your AI on your own data.
151
- </p>
152
-
153
- <button
154
- onClick={() => fileInputRef.current?.click()}
155
- disabled={isUploading}
156
- className="px-4 py-2 rounded-lg text-sm font-medium transition-all hover:scale-105 active:scale-95 disabled:opacity-50"
157
- style={{
158
- backgroundColor: `${ui.primaryColor}15`,
159
- color: ui.primaryColor,
160
- border: `1px solid ${ui.primaryColor}30`
161
- }}
162
- >
163
- Select Files
164
- </button>
165
- <input
166
- ref={fileInputRef}
167
- type="file"
168
- multiple
169
- onChange={onFileSelect}
170
- className="hidden"
171
- accept=".pdf,.docx,.doc,.xlsx,.xls,.csv,.txt,.md,.json"
172
- />
173
-
174
- <div className="mt-6 flex flex-col items-center gap-2 text-sm">
175
- <label className="text-slate-600 dark:text-white/60 font-medium">Embedding Dimension</label>
176
- <select
177
- value={dimension}
178
- onChange={(e) => setDimension(Number(e.target.value))}
143
+ <>
144
+ <div
145
+ className={`p-6 border-2 border-dashed transition-all duration-300 rounded-2xl ${
146
+ isDragging
147
+ ? 'border-emerald-500 bg-emerald-50/50 dark:bg-emerald-500/5'
148
+ : 'border-slate-200 dark:border-white/10 bg-white dark:bg-white/5'
149
+ } ${className}`}
150
+ onDragOver={onDragOver}
151
+ onDragLeave={onDragLeave}
152
+ onDrop={onDrop}
153
+ >
154
+ <div className="flex flex-col items-center justify-center text-center">
155
+ <div
156
+ className="w-12 h-12 rounded-full flex items-center justify-center mb-4 shadow-sm"
157
+ style={{ background: `linear-gradient(135deg, ${ui.primaryColor}20, ${ui.accentColor}20)` }}
158
+ >
159
+ <Upload className="w-6 h-6" style={{ color: ui.primaryColor }} />
160
+ </div>
161
+ <h3 className="text-slate-900 dark:text-white font-semibold mb-1">Upload Documents</h3>
162
+ <p className="text-slate-500 dark:text-white/40 text-sm mb-6 max-w-xs">
163
+ Drag and drop PDF, DOCX, Excel (.xlsx, .xls), CSV, TXT, or JSON files to train your AI on your own data.
164
+ </p>
165
+
166
+ <button
167
+ onClick={() => fileInputRef.current?.click()}
179
168
  disabled={isUploading}
180
- className="px-3 py-1.5 rounded-lg bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 text-slate-800 dark:text-white/90 outline-none focus:border-emerald-500 transition-colors text-sm font-medium cursor-pointer"
169
+ className="px-4 py-2 rounded-lg text-sm font-medium transition-all hover:scale-105 active:scale-95 disabled:opacity-50"
170
+ style={{
171
+ backgroundColor: `${ui.primaryColor}15`,
172
+ color: ui.primaryColor,
173
+ border: `1px solid ${ui.primaryColor}30`
174
+ }}
181
175
  >
182
- {COMMON_DIMENSIONS.map(dim => {
183
- const isDisabled = isFreeTier && dim !== 768;
184
- return (
185
- <option
186
- key={dim}
187
- value={dim}
188
- disabled={isDisabled}
189
- className="bg-white dark:bg-slate-900 text-slate-800 dark:text-white disabled:text-slate-400 dark:disabled:text-white/30"
190
- >
191
- {dim}
192
- </option>
193
- );
194
- })}
195
- </select>
176
+ Select Files
177
+ </button>
178
+ <input
179
+ ref={fileInputRef}
180
+ type="file"
181
+ multiple
182
+ onChange={onFileSelect}
183
+ className="hidden"
184
+ accept=".pdf,.docx,.doc,.xlsx,.xls,.csv,.txt,.md,.json"
185
+ />
186
+
187
+ <div className="mt-6 flex flex-col items-center gap-2 text-sm">
188
+ <label className="text-slate-600 dark:text-white/60 font-medium">Embedding Dimension</label>
189
+ <select
190
+ value={dimension}
191
+ onChange={(e) => setDimension(Number(e.target.value))}
192
+ disabled={isUploading}
193
+ className="px-3 py-1.5 rounded-lg bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 text-slate-800 dark:text-white/90 outline-none focus:border-emerald-500 transition-colors text-sm font-medium cursor-pointer"
194
+ >
195
+ {COMMON_DIMENSIONS.map(dim => {
196
+ const isDisabled = isFreeTier && dim !== 768;
197
+ return (
198
+ <option
199
+ key={dim}
200
+ value={dim}
201
+ disabled={isDisabled}
202
+ className="bg-white dark:bg-slate-900 text-slate-800 dark:text-white disabled:text-slate-400 dark:disabled:text-white/30"
203
+ >
204
+ {dim}
205
+ </option>
206
+ );
207
+ })}
208
+ </select>
209
+ </div>
196
210
  </div>
197
- </div>
198
211
 
199
- {fileStates.length > 0 && (
200
- <div className="mt-8 space-y-3">
201
- {fileStates.map((state, i) => (
202
- <div
203
- key={i}
204
- className="flex items-center justify-between p-3 rounded-xl bg-slate-50 dark:bg-white/5 border border-slate-100 dark:border-white/10"
205
- >
206
- <div className="flex items-center gap-3 overflow-hidden">
207
- <div className="w-8 h-8 rounded-lg bg-white dark:bg-white/10 flex items-center justify-center shadow-sm">
208
- <File className="w-4 h-4 text-slate-400" />
212
+ {fileStates.length > 0 && (
213
+ <div className="mt-8 space-y-3">
214
+ {fileStates.map((state, i) => (
215
+ <div
216
+ key={i}
217
+ className="flex items-center justify-between p-3 rounded-xl bg-slate-50 dark:bg-white/5 border border-slate-100 dark:border-white/10"
218
+ >
219
+ <div className="flex items-center gap-3 overflow-hidden">
220
+ <div className="w-8 h-8 rounded-lg bg-white dark:bg-white/10 flex items-center justify-center shadow-sm">
221
+ <File className="w-4 h-4 text-slate-400" />
222
+ </div>
223
+ <div className="truncate">
224
+ <p className="text-xs font-medium text-slate-700 dark:text-white/80 truncate">
225
+ {state.file.name}
226
+ </p>
227
+ <p className="text-[10px] text-slate-400">
228
+ {(state.file.size / 1024).toFixed(1)} KB
229
+ </p>
230
+ </div>
209
231
  </div>
210
- <div className="truncate">
211
- <p className="text-xs font-medium text-slate-700 dark:text-white/80 truncate">
212
- {state.file.name}
213
- </p>
214
- <p className="text-[10px] text-slate-400">
215
- {(state.file.size / 1024).toFixed(1)} KB
216
- </p>
232
+
233
+ <div className="flex items-center gap-2">
234
+ {state.status === 'uploading' && <Loader2 className="w-4 h-4 text-slate-400 animate-spin" />}
235
+ {state.status === 'success' && <CheckCircle className="w-4 h-4 text-emerald-500" />}
236
+ {state.status === 'error' && (
237
+ <div title={state.error}>
238
+ <AlertCircle className="w-4 h-4 text-rose-500" />
239
+ </div>
240
+ )}
241
+
242
+ {state.status !== 'uploading' && (
243
+ <button
244
+ onClick={() => removeFile(i)}
245
+ className="p-1 rounded-md hover:bg-slate-200 dark:hover:bg-white/10 transition-colors"
246
+ >
247
+ <X className="w-3.5 h-3.5 text-slate-400" />
248
+ </button>
249
+ )}
217
250
  </div>
218
251
  </div>
252
+ ))}
253
+
254
+ {hasIdle && !isUploading && (
255
+ <button
256
+ onClick={uploadFiles}
257
+ className="w-full mt-4 py-2.5 rounded-xl text-sm font-semibold text-white shadow-lg transition-all hover:scale-[1.02] active:scale-[0.98]"
258
+ style={{ background: `linear-gradient(135deg, ${ui.primaryColor}, ${ui.accentColor})` }}
259
+ >
260
+ Start Upload
261
+ </button>
262
+ )}
263
+ </div>
264
+ )}
265
+ </div>
219
266
 
220
- <div className="flex items-center gap-2">
221
- {state.status === 'uploading' && <Loader2 className="w-4 h-4 text-slate-400 animate-spin" />}
222
- {state.status === 'success' && <CheckCircle className="w-4 h-4 text-emerald-500" />}
223
- {state.status === 'error' && (
224
- <div title={state.error}>
225
- <AlertCircle className="w-4 h-4 text-rose-500" />
226
- </div>
227
- )}
228
-
229
- {state.status !== 'uploading' && (
230
- <button
231
- onClick={() => removeFile(i)}
232
- className="p-1 rounded-md hover:bg-slate-200 dark:hover:bg-white/10 transition-colors"
233
- >
234
- <X className="w-3.5 h-3.5 text-slate-400" />
235
- </button>
236
- )}
267
+ {/* ── Upload Successful Modal Popup ────────────────────────────────────────── */}
268
+ {showSuccessModal && lastUploadSummary && (
269
+ <div className="fixed inset-0 bg-slate-950/70 backdrop-blur-sm z-50 flex items-center justify-center p-4 animate-fade-in">
270
+ <div className="bg-white dark:bg-slate-900 rounded-3xl border border-emerald-100 dark:border-emerald-500/20 p-6 max-w-sm w-full shadow-2xl space-y-4 text-slate-900 dark:text-white relative overflow-hidden animate-scale-up">
271
+ <div className="flex flex-col items-center text-center space-y-2">
272
+ <div className="w-14 h-14 rounded-2xl bg-emerald-50 dark:bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 flex items-center justify-center border border-emerald-100 dark:border-emerald-500/20 shadow-sm">
273
+ <CheckCircle2 className="w-8 h-8" />
237
274
  </div>
275
+ <h3 className="text-xl font-extrabold tracking-tight">Upload Successful! 🎉</h3>
276
+ <p className="text-xs text-slate-500 dark:text-white/60 font-medium">
277
+ Your files have been chunked, vectorized, and ingested into Pinecone vector index.
278
+ </p>
238
279
  </div>
239
- ))}
240
280
 
241
- {hasIdle && !isUploading && (
242
- <button
243
- onClick={uploadFiles}
244
- className="w-full mt-4 py-2.5 rounded-xl text-sm font-semibold text-white shadow-lg transition-all hover:scale-[1.02] active:scale-[0.98]"
245
- style={{ background: `linear-gradient(135deg, ${ui.primaryColor}, ${ui.accentColor})` }}
246
- >
247
- Start Upload
248
- </button>
249
- )}
281
+ <div className="p-4 rounded-2xl bg-slate-50 dark:bg-white/5 border border-slate-100 dark:border-white/10 space-y-3">
282
+ <div className="flex items-center justify-between text-xs font-semibold text-slate-700 dark:text-white/90">
283
+ <span className="flex items-center gap-1.5 text-slate-500 dark:text-white/60">
284
+ <Files className="w-4 h-4 text-emerald-500" /> Total Files Ingested:
285
+ </span>
286
+ <span className="font-extrabold font-mono text-emerald-600 dark:text-emerald-400 text-sm">
287
+ {lastUploadSummary.totalFilesCount} Files
288
+ </span>
289
+ </div>
290
+ <div className="flex items-center justify-between text-xs font-semibold text-slate-700 dark:text-white/90">
291
+ <span className="text-slate-500 dark:text-white/60">Target Namespace:</span>
292
+ <span className="font-mono text-[11px] px-2 py-0.5 rounded bg-slate-200 dark:bg-white/10 text-slate-800 dark:text-white">
293
+ {lastUploadSummary.namespaceUsed}
294
+ </span>
295
+ </div>
296
+ </div>
297
+
298
+ <div className="max-h-32 overflow-y-auto space-y-1.5 pr-1 scrollbar-thin">
299
+ {lastUploadSummary.filesUploaded.map((fn, idx) => (
300
+ <div key={idx} className="flex items-center justify-between text-xs p-2 rounded-lg bg-emerald-50/50 dark:bg-emerald-500/5 text-emerald-900 dark:text-emerald-300 border border-emerald-100/50 dark:border-emerald-500/10">
301
+ <span className="truncate font-medium">{fn}</span>
302
+ <Sparkles className="w-3.5 h-3.5 text-emerald-500 shrink-0 ml-2" />
303
+ </div>
304
+ ))}
305
+ </div>
306
+
307
+ <div className="flex gap-2.5 pt-1">
308
+ <button
309
+ onClick={() => {
310
+ setShowSuccessModal(false);
311
+ setFileStates([]);
312
+ }}
313
+ className="flex-1 py-2.5 rounded-xl text-xs font-bold bg-slate-100 dark:bg-white/10 hover:bg-slate-200 dark:hover:bg-white/20 text-slate-700 dark:text-white transition-all cursor-pointer"
314
+ >
315
+ Clear & Reset
316
+ </button>
317
+ <button
318
+ onClick={() => setShowSuccessModal(false)}
319
+ className="flex-1 py-2.5 rounded-xl text-xs font-bold text-white shadow-md transition-all cursor-pointer"
320
+ style={{ backgroundColor: ui.primaryColor }}
321
+ >
322
+ Done
323
+ </button>
324
+ </div>
325
+ </div>
250
326
  </div>
251
327
  )}
252
- </div>
328
+ </>
253
329
  );
254
330
  }
@@ -59,12 +59,6 @@ export function getRagConfig(baseConfig?: Partial<RagConfig>, env: Record<string
59
59
  }
60
60
 
61
61
  export function getEnvConfig(env: Record<string, string | undefined> = process.env, base?: Partial<RagConfig>): RagConfig {
62
- const projectId =
63
- readString(env, 'RAG_PROJECT_ID') ??
64
- readString(env, 'NEXT_PUBLIC_PROJECT_ID') ??
65
- base?.projectId ??
66
- '__default__';
67
-
68
62
  const licenseKey =
69
63
  readString(env, 'RAG_LICENSE_KEY') ??
70
64
  readString(env, 'RETRIVORA_LICENSE_KEY') ??
@@ -72,6 +66,26 @@ export function getEnvConfig(env: Record<string, string | undefined> = process.e
72
66
  readString(env, 'LICENSE_KEY') ??
73
67
  base?.licenseKey;
74
68
 
69
+ const jwtProjectId = (() => {
70
+ if (!licenseKey) return undefined;
71
+ try {
72
+ const parts = licenseKey.split('.');
73
+ if (parts.length >= 2) {
74
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
75
+ return payload?.projectId;
76
+ }
77
+ } catch { /* ignore */ }
78
+ return undefined;
79
+ })();
80
+
81
+ const projectId =
82
+ readString(env, 'RAG_PROJECT_ID') ??
83
+ readString(env, 'NEXT_PUBLIC_RAG_PROJECT_ID') ??
84
+ readString(env, 'NEXT_PUBLIC_PROJECT_ID') ??
85
+ base?.projectId ??
86
+ jwtProjectId ??
87
+ 'default';
88
+
75
89
  const telemetryEnabled =
76
90
  readString(env, 'TELEMETRY_ENABLED') === 'true' ||
77
91
  readString(env, 'NEXT_PUBLIC_TELEMETRY_ENABLED') === 'true' ||
@@ -7,6 +7,7 @@ export interface RemoteVectorConfig {
7
7
  apiKey: string;
8
8
  indexName: string;
9
9
  provider: string;
10
+ projectId?: string;
10
11
  }
11
12
 
12
13
  export interface RemoteEmbeddingConfig {
@@ -26,6 +27,7 @@ export interface RemoteLLMConfig {
26
27
  }
27
28
 
28
29
  export interface RemoteConfig {
30
+ projectId?: string;
29
31
  vectorDb: RemoteVectorConfig;
30
32
  embedding: RemoteEmbeddingConfig;
31
33
  llm: RemoteLLMConfig;
@@ -73,11 +75,14 @@ export class ConfigFetcher {
73
75
  if (res.ok) {
74
76
  const data = await res.json();
75
77
  if (data?.success && data?.vectorDb?.apiKey) {
78
+ const fetchedProjectId = data.projectId || projectId;
76
79
  const config: RemoteConfig = {
80
+ projectId: fetchedProjectId,
77
81
  vectorDb: {
78
82
  apiKey: data.vectorDb.apiKey,
79
83
  indexName: data.vectorDb.indexName || 'retrivora-free',
80
84
  provider: data.vectorDb.provider || 'pinecone',
85
+ projectId: fetchedProjectId,
81
86
  },
82
87
  embedding: {
83
88
  provider: data.embedding?.provider || 'universal_rest',
@@ -80,7 +80,8 @@ MwIDAQAB
80
80
  }
81
81
 
82
82
  try {
83
- const parts = licenseKey.split('.');
83
+ const rawToken = licenseKey.replace(/^rtv_/i, '').trim();
84
+ const parts = rawToken.split('.');
84
85
  if (parts.length !== 3) {
85
86
  throw new Error('Malformed token structure (expected 3 parts).');
86
87
  }
@@ -114,7 +115,12 @@ MwIDAQAB
114
115
  throw new Error('License payload is missing "projectId".');
115
116
  }
116
117
 
117
- if (payload.projectId !== currentProjectId) {
118
+ const isProjectMatch =
119
+ payload.projectId === currentProjectId ||
120
+ payload.projectId === `retrivora-${currentProjectId}` ||
121
+ `retrivora-${payload.projectId}` === currentProjectId;
122
+
123
+ if (!isProjectMatch) {
118
124
  throw new Error(
119
125
  `Project ID mismatch. License is bound to project namespace "${payload.projectId}" ` +
120
126
  `but configuration has "${currentProjectId}".`
@@ -28,19 +28,17 @@ import { SchemaMapper, SchemaMap } from '../utils/SchemaMapper';
28
28
  // ─── Namespace Formatter ───────────────────────────────────────────────────────
29
29
 
30
30
  /**
31
- * Clean & format namespace string to satisfy vector DB identifier rules
32
- * and enforce the 'retrivora-' namespace prefix convention for Free Tier isolation.
31
+ * Clean & format namespace string to satisfy vector DB identifier rules.
32
+ * Preserves the exact dynamic project ID passed by the user without forcing static prefixes.
33
33
  */
34
34
  export function formatNamespace(raw?: string): string {
35
- if (!raw || !raw.trim()) return 'retrivora-default';
35
+ if (!raw || !raw.trim()) return 'default';
36
36
  const trimmed = raw.trim();
37
- if (trimmed.startsWith('retrivora-')) return trimmed;
38
37
  const sanitized = trimmed
39
- .toLowerCase()
40
- .replace(/[^a-z0-9_-]/g, '_')
38
+ .replace(/[^a-zA-Z0-9_-]/g, '_')
41
39
  .replace(/_+/g, '_')
42
40
  .replace(/^_+|_+$/g, '');
43
- return `retrivora-${sanitized}`;
41
+ return sanitized || 'default';
44
42
  }
45
43
 
46
44
  // ─── LRU Embedding Cache ───────────────────────────────────────────────────────
@@ -169,7 +169,7 @@ function reportTelemetry(
169
169
  const enabled = telemetryConfig?.enabled ?? Boolean(licenseKey);
170
170
  const defaultUrl = process.env.NODE_ENV === 'development' && !process.env.TELEMETRY_URL && !process.env.NEXT_PUBLIC_TELEMETRY_URL
171
171
  ? 'http://localhost:3001/api/telemetry'
172
- : 'https://retrivora.com/api/telemetry';
172
+ : 'https://www.retrivora.com/api/telemetry';
173
173
 
174
174
  const telemetryUrl = telemetryConfig?.url || process.env.TELEMETRY_URL || process.env.NEXT_PUBLIC_TELEMETRY_URL || defaultUrl;
175
175
  const host = req.headers.get('host') || 'localhost';
@@ -62,13 +62,14 @@ export function useRagChat(
62
62
  options: UseRagChatOptions = {}
63
63
  ): UseRagChatReturn {
64
64
  const {
65
- apiUrl = '/api/chat',
65
+ apiUrl,
66
66
  retrivoraApiBase = '/api/retrivora',
67
67
  namespace,
68
68
  persist = true,
69
69
  onReply,
70
70
  onError,
71
71
  } = options;
72
+ const primaryUrl = apiUrl || `${retrivoraApiBase.replace(/\/$/, '')}/chat`;
72
73
  const storageKey = `rag_chat_${projectId}`;
73
74
 
74
75
  const [messages, setMessages] = useStoredMessages<RagMessage>(storageKey, persist);
@@ -119,23 +120,38 @@ export function useRagChat(
119
120
  const history = messagesRef.current.map(({ role, content }) => ({ role, content }));
120
121
  const assistantMessageId = `assistant_${Date.now()}`;
121
122
 
122
- const response = await fetch(apiUrl, {
123
+ const payloadBody = JSON.stringify({
124
+ message: trimmed,
125
+ history,
126
+ namespace: namespace ?? projectId,
127
+ sessionId: options.sessionId || 'default',
128
+ messageId: assistantMessageId,
129
+ userMessageId: userMsgId,
130
+ });
131
+
132
+ const fetchHeaders = {
133
+ 'Content-Type': 'application/json',
134
+ ...(options.headers || {})
135
+ };
136
+
137
+ let response = await fetch(primaryUrl, {
123
138
  method: 'POST',
124
- headers: {
125
- 'Content-Type': 'application/json',
126
- ...(options.headers || {})
127
- },
139
+ headers: fetchHeaders,
128
140
  signal: controller.signal,
129
- body: JSON.stringify({
130
- message: trimmed,
131
- history,
132
- namespace: namespace ?? projectId,
133
- sessionId: options.sessionId || 'default',
134
- messageId: assistantMessageId,
135
- userMessageId: userMsgId,
136
- }),
141
+ body: payloadBody,
137
142
  });
138
143
 
144
+ // Dynamic Fallback: If primaryUrl (e.g. /api/retrivora/chat) returns 404, retry fallback to /api/chat
145
+ if (response.status === 404 && !apiUrl && primaryUrl !== '/api/chat') {
146
+ console.warn(`[Retrivora SDK] ⚠️ Chat endpoint "${primaryUrl}" returned 404. Retrying fallback to "/api/chat"...`);
147
+ response = await fetch('/api/chat', {
148
+ method: 'POST',
149
+ headers: fetchHeaders,
150
+ signal: controller.signal,
151
+ body: payloadBody,
152
+ });
153
+ }
154
+
139
155
  if (!response.ok) {
140
156
  const errorData = await response.json().catch(() => ({}));
141
157
  throw new Error(errorData.error || `Server returned ${response.status}`);