@retrivora-ai/rag-engine 1.9.8 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -28
- package/dist/{ILLMProvider-B8ROITYK.d.mts → ILLMProvider-DMxLyTdq.d.mts} +2 -2
- package/dist/{ILLMProvider-B8ROITYK.d.ts → ILLMProvider-DMxLyTdq.d.ts} +2 -2
- package/dist/handlers/index.d.mts +2 -2
- package/dist/handlers/index.d.ts +2 -2
- package/dist/handlers/index.js +248 -74
- package/dist/handlers/index.mjs +254 -74
- package/dist/{index-DNvoi-sV.d.ts → index-CfkqZd2Y.d.ts} +1 -1
- package/dist/{index-BCPKUAVL.d.ts → index-DHsFLJXQ.d.mts} +2 -31
- package/dist/{index-CrxCy36A.d.mts → index-tzwc7UhY.d.ts} +2 -31
- package/dist/{index-B8PbEFSY.d.mts → index-xygonxpW.d.mts} +1 -1
- package/dist/index.css +485 -557
- package/dist/index.d.mts +16 -10
- package/dist/index.d.ts +16 -10
- package/dist/index.js +36 -768
- package/dist/index.mjs +32 -782
- package/dist/server.d.mts +8 -5
- package/dist/server.d.ts +8 -5
- package/dist/server.js +249 -74
- package/dist/server.mjs +255 -74
- package/package.json +3 -2
- package/src/app/page.tsx +2 -0
- package/src/components/ChatWindow.tsx +14 -3
- package/src/components/constants.tsx +224 -0
- package/src/config/RagConfig.ts +2 -2
- package/src/config/serverConfig.ts +14 -1
- package/src/core/ConfigResolver.ts +32 -6
- package/src/core/LicenseVerifier.ts +106 -0
- package/src/core/Pipeline.ts +11 -8
- package/src/core/Retrivora.ts +1 -0
- package/src/handlers/index.ts +84 -48
- package/src/index.ts +3 -5
- package/src/components/AmbientBackground.tsx +0 -29
- package/src/components/ArchitectureCard.tsx +0 -53
- package/src/components/ArchitectureCardsSection.tsx +0 -48
- package/src/components/DocViewer.tsx +0 -97
- package/src/components/Documentation.tsx +0 -141
- package/src/components/Hero.tsx +0 -142
- package/src/components/Lifecycle.tsx +0 -77
- package/src/components/Navbar.tsx +0 -55
package/src/handlers/index.ts
CHANGED
|
@@ -145,6 +145,62 @@ function getOrCreatePlugin(configOrPlugin: Partial<RagConfig> | VectorPlugin | u
|
|
|
145
145
|
return _g[cacheKey];
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
// ─── Telemetry Reporter Helper ───────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Asynchronously sends telemetry payload to telemetryUrl when telemetry is enabled
|
|
152
|
+
* or when a licenseKey is set. Non-blocking.
|
|
153
|
+
*/
|
|
154
|
+
function reportTelemetry(
|
|
155
|
+
req: NextRequest,
|
|
156
|
+
plugin: VectorPlugin,
|
|
157
|
+
action: string,
|
|
158
|
+
status: 'success' | 'error',
|
|
159
|
+
details: string,
|
|
160
|
+
trace?: unknown
|
|
161
|
+
) {
|
|
162
|
+
try {
|
|
163
|
+
const config = plugin.getConfig();
|
|
164
|
+
const licenseKey = config.licenseKey || process.env.RAG_LICENSE_KEY || process.env.RETRIVORA_LICENSE_KEY || process.env.NEXT_PUBLIC_RETRIVORA_LICENSE_KEY;
|
|
165
|
+
const telemetryConfig = config.telemetry;
|
|
166
|
+
|
|
167
|
+
const enabled = telemetryConfig?.enabled ?? Boolean(licenseKey);
|
|
168
|
+
if (!enabled) return;
|
|
169
|
+
|
|
170
|
+
const telemetryUrl = telemetryConfig?.url || process.env.TELEMETRY_URL || process.env.NEXT_PUBLIC_TELEMETRY_URL || '/api/telemetry';
|
|
171
|
+
const host = req.headers.get('host') || 'localhost';
|
|
172
|
+
|
|
173
|
+
let absoluteUrl = telemetryUrl;
|
|
174
|
+
if (!telemetryUrl.startsWith('http')) {
|
|
175
|
+
const proto = req.headers.get('x-forwarded-proto') || 'http';
|
|
176
|
+
absoluteUrl = `${proto}://${host}${telemetryUrl}`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const projectId = config.projectId || 'default';
|
|
180
|
+
|
|
181
|
+
fetch(absoluteUrl, {
|
|
182
|
+
method: 'POST',
|
|
183
|
+
headers: {
|
|
184
|
+
'Content-Type': 'application/json',
|
|
185
|
+
'x-forwarded-for': req.headers.get('x-forwarded-for') || '',
|
|
186
|
+
'x-real-ip': req.headers.get('x-real-ip') || '',
|
|
187
|
+
},
|
|
188
|
+
body: JSON.stringify({
|
|
189
|
+
trace,
|
|
190
|
+
licenseKey,
|
|
191
|
+
projectId,
|
|
192
|
+
action,
|
|
193
|
+
status,
|
|
194
|
+
details,
|
|
195
|
+
}),
|
|
196
|
+
}).catch(err => {
|
|
197
|
+
console.warn('[Retrivora Telemetry] Async report warning:', err.message);
|
|
198
|
+
});
|
|
199
|
+
} catch {
|
|
200
|
+
/* non-blocking */
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
148
204
|
// ─── SSE helpers ──────────────────────────────────────────────────────────────
|
|
149
205
|
|
|
150
206
|
/**
|
|
@@ -194,17 +250,6 @@ const SSE_HEADERS: Record<string, string> = {
|
|
|
194
250
|
|
|
195
251
|
/**
|
|
196
252
|
* createChatHandler — factory that returns a Next.js App Router POST handler.
|
|
197
|
-
*
|
|
198
|
-
* Accepts either a full/partial `RagConfig` **or** a pre-built `VectorPlugin`
|
|
199
|
-
* instance (preferred for singleton/multi-tenant setups).
|
|
200
|
-
*
|
|
201
|
-
* @example
|
|
202
|
-
* // Option A — pass config (plugin created once at module load time)
|
|
203
|
-
* export const POST = createChatHandler(getRagConfig());
|
|
204
|
-
*
|
|
205
|
-
* // Option B — pass a pre-built plugin (most flexible)
|
|
206
|
-
* const plugin = new VectorPlugin(getRagConfig());
|
|
207
|
-
* export const POST = createChatHandler(plugin);
|
|
208
253
|
*/
|
|
209
254
|
export function createChatHandler(
|
|
210
255
|
configOrPlugin?: Partial<RagConfig> | VectorPlugin,
|
|
@@ -261,6 +306,8 @@ export function createChatHandler(
|
|
|
261
306
|
trace: result.trace,
|
|
262
307
|
}).catch(err => console.warn('[createChatHandler] Failed to save assistant message:', err));
|
|
263
308
|
|
|
309
|
+
reportTelemetry(req, plugin, 'QUERY_GENERATION', 'success', `Tokens: ${result.trace?.tokens?.totalTokens || 0}, Latency: ${result.trace?.latency?.totalMs || 0}ms`, result.trace);
|
|
310
|
+
|
|
264
311
|
return NextResponse.json({
|
|
265
312
|
...result,
|
|
266
313
|
messageId: assistantMsgId,
|
|
@@ -268,6 +315,7 @@ export function createChatHandler(
|
|
|
268
315
|
});
|
|
269
316
|
} catch (err) {
|
|
270
317
|
const message = err instanceof Error ? err.message : 'Internal server error';
|
|
318
|
+
reportTelemetry(req, plugin, 'QUERY_GENERATION', 'error', message);
|
|
271
319
|
return NextResponse.json({ error: message }, { status: 500 });
|
|
272
320
|
}
|
|
273
321
|
};
|
|
@@ -275,25 +323,6 @@ export function createChatHandler(
|
|
|
275
323
|
|
|
276
324
|
/**
|
|
277
325
|
* createStreamHandler — factory for a streaming SSE Next.js App Router POST handler.
|
|
278
|
-
*
|
|
279
|
-
* Streams the assistant response as Server-Sent Events (SSE). Each frame is a
|
|
280
|
-
* JSON object with a `type` discriminant:
|
|
281
|
-
* - `{ type: "text", text: "..." }` — incremental text chunk
|
|
282
|
-
* - `{ type: "metadata", sources: [...] }` — retrieval metadata (last frame)
|
|
283
|
-
* - `{ type: "error", error: "..." }` — stream-level error
|
|
284
|
-
*
|
|
285
|
-
* @example
|
|
286
|
-
* // src/app/api/chat/route.ts
|
|
287
|
-
* export const POST = createStreamHandler(getRagConfig());
|
|
288
|
-
*
|
|
289
|
-
* // Client-side parsing in useRagChat.ts:
|
|
290
|
-
* // const lines = chunk.split('\n');
|
|
291
|
-
* // for (const line of lines) {
|
|
292
|
-
* // if (line.startsWith('data: ')) {
|
|
293
|
-
* // const frame = JSON.parse(line.slice(6));
|
|
294
|
-
* // if (frame.type === 'text') { ... }
|
|
295
|
-
* // }
|
|
296
|
-
* // }
|
|
297
326
|
*/
|
|
298
327
|
export function createStreamHandler(
|
|
299
328
|
configOrPlugin?: Partial<RagConfig> | VectorPlugin,
|
|
@@ -311,7 +340,6 @@ export function createStreamHandler(
|
|
|
311
340
|
const rateLimited = checkRateLimit(req);
|
|
312
341
|
if (rateLimited) return rateLimited;
|
|
313
342
|
|
|
314
|
-
// Parse body first so we can return a normal JSON 400 before opening the stream
|
|
315
343
|
let body: {
|
|
316
344
|
message?: string;
|
|
317
345
|
history?: ChatMessage[];
|
|
@@ -380,11 +408,9 @@ export function createStreamHandler(
|
|
|
380
408
|
// Retrieval metadata object — always the final frame
|
|
381
409
|
enqueue(sseMetaFrame(chunk));
|
|
382
410
|
|
|
383
|
-
// ── LLM-driven visualization decision ─────────────────────────
|
|
384
411
|
const responseChunk = chunk as ChatResponse;
|
|
385
412
|
const sources = responseChunk?.sources || [];
|
|
386
413
|
|
|
387
|
-
// Emit observability trace if present
|
|
388
414
|
if (responseChunk?.trace) {
|
|
389
415
|
enqueue(sseObservabilityFrame(responseChunk.trace));
|
|
390
416
|
}
|
|
@@ -392,9 +418,6 @@ export function createStreamHandler(
|
|
|
392
418
|
let uiTransformation = responseChunk?.ui_transformation;
|
|
393
419
|
if (sources.length > 0) {
|
|
394
420
|
try {
|
|
395
|
-
// The pipeline already computed ui_transformation inside askStream().
|
|
396
|
-
// We ONLY fall back to the cheap heuristic transform (no LLM call) when
|
|
397
|
-
// the pipeline didn't produce one — never call analyzeAndDecide() here.
|
|
398
421
|
uiTransformation =
|
|
399
422
|
responseChunk?.ui_transformation ??
|
|
400
423
|
UITransformer.transform(message, sources, plugin.getConfig());
|
|
@@ -423,14 +446,16 @@ export function createStreamHandler(
|
|
|
423
446
|
uiTransformation,
|
|
424
447
|
trace: responseChunk?.trace,
|
|
425
448
|
}).catch(err => console.warn('[createStreamHandler] Failed to save assistant message:', err));
|
|
449
|
+
|
|
450
|
+
reportTelemetry(req, plugin, 'QUERY_GENERATION', 'success', `Tokens: ${responseChunk?.trace?.tokens?.totalTokens || 0}, Latency: ${responseChunk?.trace?.latency?.totalMs || 0}ms`, responseChunk?.trace);
|
|
426
451
|
}
|
|
427
452
|
}
|
|
428
453
|
} catch (streamError) {
|
|
429
|
-
// Do not log or enqueue errors if the client intentionally cancelled/aborted
|
|
430
454
|
if (isActive) {
|
|
431
455
|
const errorMessage =
|
|
432
456
|
streamError instanceof Error ? streamError.message : String(streamError);
|
|
433
457
|
console.error('[createStreamHandler] Stream error:', streamError);
|
|
458
|
+
reportTelemetry(req, plugin, 'QUERY_GENERATION', 'error', errorMessage);
|
|
434
459
|
try {
|
|
435
460
|
enqueue(sseErrorFrame(errorMessage));
|
|
436
461
|
} catch { /* silent */ }
|
|
@@ -480,9 +505,11 @@ export function createIngestHandler(
|
|
|
480
505
|
}
|
|
481
506
|
|
|
482
507
|
const results = await plugin.ingest(documents, namespace);
|
|
508
|
+
reportTelemetry(req, plugin, 'DOCUMENT_INGESTION', 'success', `Ingested ${documents.length} document(s)`);
|
|
483
509
|
return NextResponse.json({ results });
|
|
484
510
|
} catch (err) {
|
|
485
511
|
const message = err instanceof Error ? err.message : 'Internal server error';
|
|
512
|
+
reportTelemetry(req, plugin, 'DOCUMENT_INGESTION', 'error', message);
|
|
486
513
|
return NextResponse.json({ error: message }, { status: 500 });
|
|
487
514
|
}
|
|
488
515
|
};
|
|
@@ -560,20 +587,15 @@ export function createUploadHandler(
|
|
|
560
587
|
for (const row of parsed.data) {
|
|
561
588
|
i++;
|
|
562
589
|
const rowData = row as Record<string, string>;
|
|
563
|
-
|
|
564
|
-
// Dynamically determine the grouping key (typically the first column like ID or Handle)
|
|
565
590
|
const groupingKey = Object.keys(rowData)[0];
|
|
566
591
|
|
|
567
|
-
// Forward-fill ALL empty columns for variants belonging to the same group
|
|
568
592
|
if (lastRowData && groupingKey && rowData[groupingKey] && rowData[groupingKey] === lastRowData[groupingKey]) {
|
|
569
593
|
for (const key of Object.keys(rowData)) {
|
|
570
|
-
// Forward-fill if the current row lacks the field but the previous row has it
|
|
571
594
|
if (!rowData[key] && lastRowData[key]) {
|
|
572
595
|
rowData[key] = lastRowData[key];
|
|
573
596
|
}
|
|
574
597
|
}
|
|
575
598
|
}
|
|
576
|
-
// Save a copy of the filled rowData for the next iteration
|
|
577
599
|
lastRowData = { ...rowData };
|
|
578
600
|
|
|
579
601
|
const contentParts: string[] = [];
|
|
@@ -615,9 +637,11 @@ export function createUploadHandler(
|
|
|
615
637
|
}
|
|
616
638
|
|
|
617
639
|
const results = await plugin.ingest(documents, namespace);
|
|
640
|
+
reportTelemetry(req, plugin, 'DOCUMENT_INGESTION', 'success', `Uploaded & ingested ${files.length} file(s)`);
|
|
618
641
|
return NextResponse.json({ message: 'Upload successful', results });
|
|
619
642
|
} catch (err) {
|
|
620
643
|
const message = err instanceof Error ? err.message : 'Upload failed';
|
|
644
|
+
reportTelemetry(req, plugin, 'DOCUMENT_INGESTION', 'error', message);
|
|
621
645
|
return NextResponse.json({ error: message }, { status: 500 });
|
|
622
646
|
}
|
|
623
647
|
};
|
|
@@ -625,6 +649,7 @@ export function createUploadHandler(
|
|
|
625
649
|
|
|
626
650
|
/**
|
|
627
651
|
* createSuggestionsHandler — factory for the auto-suggestions endpoint.
|
|
652
|
+
* Supports both POST (JSON body) and GET (URL search parameters).
|
|
628
653
|
*/
|
|
629
654
|
export function createSuggestionsHandler(
|
|
630
655
|
configOrPlugin?: Partial<RagConfig> | VectorPlugin,
|
|
@@ -633,25 +658,34 @@ export function createSuggestionsHandler(
|
|
|
633
658
|
const plugin = getOrCreatePlugin(configOrPlugin);
|
|
634
659
|
const onAuthorize = options?.onAuthorize;
|
|
635
660
|
|
|
636
|
-
return async function
|
|
661
|
+
return async function handler(req: NextRequest) {
|
|
637
662
|
const authResult = await checkAuth(req, onAuthorize);
|
|
638
663
|
if (authResult) return authResult as any;
|
|
639
664
|
|
|
640
665
|
try {
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
666
|
+
let query = '';
|
|
667
|
+
let namespace: string | undefined = undefined;
|
|
668
|
+
|
|
669
|
+
if (req.method === 'POST') {
|
|
670
|
+
const body = await req.json().catch(() => ({}));
|
|
671
|
+
query = body.query || '';
|
|
672
|
+
namespace = body.namespace;
|
|
673
|
+
} else {
|
|
674
|
+
const url = new URL(req.url);
|
|
675
|
+
query = url.searchParams.get('query') || '';
|
|
676
|
+
namespace = url.searchParams.get('namespace') || undefined;
|
|
677
|
+
}
|
|
646
678
|
|
|
647
679
|
if (typeof query !== 'string') {
|
|
648
680
|
return NextResponse.json({ error: 'query is required' }, { status: 400 });
|
|
649
681
|
}
|
|
650
682
|
|
|
651
683
|
const suggestions = await plugin.getSuggestions(query, namespace);
|
|
684
|
+
reportTelemetry(req, plugin, 'AUTO_SUGGESTIONS', 'success', `Fetched ${suggestions.length} suggestions for: ${query.slice(0, 30)}`);
|
|
652
685
|
return NextResponse.json({ suggestions });
|
|
653
686
|
} catch (err) {
|
|
654
687
|
const message = err instanceof Error ? err.message : 'Internal server error';
|
|
688
|
+
reportTelemetry(req, plugin, 'AUTO_SUGGESTIONS', 'error', message);
|
|
655
689
|
return NextResponse.json({ error: message }, { status: 500 });
|
|
656
690
|
}
|
|
657
691
|
};
|
|
@@ -828,6 +862,8 @@ export function createRagHandler(
|
|
|
828
862
|
switch (segment) {
|
|
829
863
|
case 'health':
|
|
830
864
|
return healthHandler(req);
|
|
865
|
+
case 'suggestions':
|
|
866
|
+
return suggestionsHandler(req);
|
|
831
867
|
case 'history':
|
|
832
868
|
return historyHandler(req);
|
|
833
869
|
case 'feedback':
|
package/src/index.ts
CHANGED
|
@@ -15,11 +15,9 @@ export { MessageBubble } from './components/MessageBubble';
|
|
|
15
15
|
export { SourceCard } from './components/SourceCard';
|
|
16
16
|
export { ObservabilityPanel } from './components/ObservabilityPanel';
|
|
17
17
|
export { ConfigProvider, useConfig } from './components/ConfigProvider';
|
|
18
|
-
export {
|
|
19
|
-
export {
|
|
20
|
-
export {
|
|
21
|
-
export { Documentation } from './components/Documentation';
|
|
22
|
-
export { AmbientBackground } from './components/AmbientBackground';
|
|
18
|
+
export { CodeViewer } from './components/CodeViewer';
|
|
19
|
+
export { ProductCard } from './components/ProductCard';
|
|
20
|
+
export { ProductCarousel } from './components/ProductCarousel';
|
|
23
21
|
|
|
24
22
|
// ── Headless Hooks ────────────────────────────────────────────
|
|
25
23
|
export { useRagChat } from './hooks/useRagChat';
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import React from 'react';
|
|
4
|
-
|
|
5
|
-
export function AmbientBackground() {
|
|
6
|
-
return (
|
|
7
|
-
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
|
8
|
-
{/* Soft indigo orb — top left */}
|
|
9
|
-
<div
|
|
10
|
-
className="absolute -top-24 -left-24 w-[600px] h-[600px] rounded-full opacity-25 blur-3xl"
|
|
11
|
-
style={{ background: 'radial-gradient(circle, #c7d2fe 0%, #a5b4fc 40%, transparent 70%)' }}
|
|
12
|
-
/>
|
|
13
|
-
{/* Soft violet orb — top right */}
|
|
14
|
-
<div
|
|
15
|
-
className="absolute -top-20 right-0 w-[500px] h-[500px] rounded-full opacity-20 blur-3xl"
|
|
16
|
-
style={{ background: 'radial-gradient(circle, #ddd6fe 0%, #c4b5fd 50%, transparent 70%)' }}
|
|
17
|
-
/>
|
|
18
|
-
{/* Subtle grid */}
|
|
19
|
-
<div
|
|
20
|
-
className="absolute inset-0 opacity-[0.025]"
|
|
21
|
-
style={{
|
|
22
|
-
backgroundImage:
|
|
23
|
-
'linear-gradient(#6366f1 1px, transparent 1px), linear-gradient(90deg, #6366f1 1px, transparent 1px)',
|
|
24
|
-
backgroundSize: '56px 56px',
|
|
25
|
-
}}
|
|
26
|
-
/>
|
|
27
|
-
</div>
|
|
28
|
-
);
|
|
29
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { ArchitectureCardProps } from '@/app/types';
|
|
3
|
-
import { ArrowRight } from 'lucide-react';
|
|
4
|
-
|
|
5
|
-
interface ExtendedArchitectureCardProps extends ArchitectureCardProps {
|
|
6
|
-
gradientFrom?: string;
|
|
7
|
-
gradientTo?: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function ArchitectureCard({ icon, title, description, badge, badgeColor, gradientFrom = '#6366f1', gradientTo = '#8b5cf6' }: ExtendedArchitectureCardProps) {
|
|
11
|
-
return (
|
|
12
|
-
<div className="group relative bg-white rounded-2xl border border-slate-200 p-6 transition-all duration-300 hover:-translate-y-1 hover:shadow-xl hover:shadow-slate-200/80 overflow-hidden cursor-default">
|
|
13
|
-
{/* Gradient top accent */}
|
|
14
|
-
<div
|
|
15
|
-
className="absolute top-0 left-0 right-0 h-0.5 rounded-t-2xl opacity-80 group-hover:opacity-100 transition-opacity"
|
|
16
|
-
style={{ background: `linear-gradient(90deg, ${gradientFrom}, ${gradientTo})` }}
|
|
17
|
-
/>
|
|
18
|
-
|
|
19
|
-
{/* Card top row */}
|
|
20
|
-
<div className="mb-5 flex items-center justify-between">
|
|
21
|
-
<div
|
|
22
|
-
className="w-12 h-12 rounded-xl flex items-center justify-center text-2xl transition-transform duration-300 group-hover:scale-110"
|
|
23
|
-
style={{
|
|
24
|
-
background: `linear-gradient(135deg, ${gradientFrom}18, ${gradientTo}10)`,
|
|
25
|
-
border: `1px solid ${gradientFrom}20`,
|
|
26
|
-
}}
|
|
27
|
-
>
|
|
28
|
-
{icon}
|
|
29
|
-
</div>
|
|
30
|
-
<span className={`rounded-full border px-3 py-1 text-[10px] font-bold uppercase tracking-wider ${badgeColor}`}>
|
|
31
|
-
{badge}
|
|
32
|
-
</span>
|
|
33
|
-
</div>
|
|
34
|
-
|
|
35
|
-
<h3 className="mb-2 font-bold text-slate-900 text-base">{title}</h3>
|
|
36
|
-
<p className="text-sm text-slate-500 leading-relaxed">{description}</p>
|
|
37
|
-
|
|
38
|
-
{/* "Learn more" on hover */}
|
|
39
|
-
<div
|
|
40
|
-
className="mt-5 flex items-center gap-1 text-[11px] font-semibold opacity-0 group-hover:opacity-100 transition-all duration-300 -translate-y-1 group-hover:translate-y-0"
|
|
41
|
-
style={{ color: gradientFrom }}
|
|
42
|
-
>
|
|
43
|
-
Learn more <ArrowRight className="h-3 w-3" />
|
|
44
|
-
</div>
|
|
45
|
-
|
|
46
|
-
{/* Subtle hover bg tint */}
|
|
47
|
-
<div
|
|
48
|
-
className="absolute inset-0 rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none"
|
|
49
|
-
style={{ background: `radial-gradient(ellipse at 50% 0%, ${gradientFrom}05 0%, transparent 70%)` }}
|
|
50
|
-
/>
|
|
51
|
-
</div>
|
|
52
|
-
);
|
|
53
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import React from 'react';
|
|
4
|
-
import Link from 'next/link';
|
|
5
|
-
import { ArchitectureCard } from '@/components/ArchitectureCard';
|
|
6
|
-
import { ARCHITECTURE_CARDS } from '@/app/constants';
|
|
7
|
-
import { ArrowRight } from 'lucide-react';
|
|
8
|
-
|
|
9
|
-
const CARD_GRADIENTS = [
|
|
10
|
-
{ from: '#6366f1', to: '#8b5cf6' },
|
|
11
|
-
{ from: '#f59e0b', to: '#f97316' },
|
|
12
|
-
{ from: '#10b981', to: '#06b6d4' },
|
|
13
|
-
];
|
|
14
|
-
|
|
15
|
-
export function ArchitectureCardsSection() {
|
|
16
|
-
return (
|
|
17
|
-
<div className="relative">
|
|
18
|
-
<div className="text-center mb-12">
|
|
19
|
-
<h2 className="text-3xl md:text-4xl font-black text-slate-900 mb-4">
|
|
20
|
-
Built for every layer of the stack
|
|
21
|
-
</h2>
|
|
22
|
-
<p className="text-slate-500 max-w-xl mx-auto text-base leading-relaxed">
|
|
23
|
-
Swap providers without rewriting a single line of business logic.
|
|
24
|
-
</p>
|
|
25
|
-
</div>
|
|
26
|
-
|
|
27
|
-
<div className="grid gap-5 sm:grid-cols-3 max-w-5xl mx-auto">
|
|
28
|
-
{ARCHITECTURE_CARDS.map((card, i) => (
|
|
29
|
-
<ArchitectureCard
|
|
30
|
-
key={i}
|
|
31
|
-
{...card}
|
|
32
|
-
gradientFrom={CARD_GRADIENTS[i]?.from}
|
|
33
|
-
gradientTo={CARD_GRADIENTS[i]?.to}
|
|
34
|
-
/>
|
|
35
|
-
))}
|
|
36
|
-
</div>
|
|
37
|
-
|
|
38
|
-
<div className="mt-10 text-center">
|
|
39
|
-
<Link
|
|
40
|
-
href="/features"
|
|
41
|
-
className="inline-flex items-center gap-2 px-6 py-3 rounded-xl text-sm font-semibold text-slate-600 hover:text-indigo-600 border border-slate-200 hover:border-indigo-200 bg-white hover:bg-indigo-50 transition-all shadow-sm"
|
|
42
|
-
>
|
|
43
|
-
Explore all features <ArrowRight className="h-4 w-4" />
|
|
44
|
-
</Link>
|
|
45
|
-
</div>
|
|
46
|
-
</div>
|
|
47
|
-
);
|
|
48
|
-
}
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import React from 'react';
|
|
4
|
-
import { Snippet } from '@/app/types';
|
|
5
|
-
import { SNIPPETS } from '@/app/constants';
|
|
6
|
-
import { CodeViewer } from './CodeViewer';
|
|
7
|
-
import { ArrowLeft, ArrowRight } from 'lucide-react';
|
|
8
|
-
|
|
9
|
-
export function DocViewer({ activeSnippet, setActiveSnippet }: { activeSnippet: Snippet; setActiveSnippet: (s: Snippet) => void }) {
|
|
10
|
-
const currentIndex = SNIPPETS.findIndex(s => s.id === activeSnippet.id);
|
|
11
|
-
const totalSteps = SNIPPETS.length;
|
|
12
|
-
const isFirst = currentIndex === 0;
|
|
13
|
-
const isLast = currentIndex === totalSteps - 1;
|
|
14
|
-
|
|
15
|
-
const handlePrev = () => { if (!isFirst) setActiveSnippet(SNIPPETS[currentIndex - 1]); };
|
|
16
|
-
const handleNext = () => { if (!isLast) setActiveSnippet(SNIPPETS[currentIndex + 1]); };
|
|
17
|
-
|
|
18
|
-
const progressPercent = Math.round(((currentIndex + 1) / totalSteps) * 100);
|
|
19
|
-
|
|
20
|
-
return (
|
|
21
|
-
<div className="flex flex-col h-full p-6 gap-5">
|
|
22
|
-
{/* Tab selector */}
|
|
23
|
-
<div className="flex flex-wrap gap-1.5 border-b border-slate-100 pb-5">
|
|
24
|
-
{SNIPPETS.map(snippet => (
|
|
25
|
-
<button
|
|
26
|
-
key={snippet.id}
|
|
27
|
-
onClick={() => setActiveSnippet(snippet)}
|
|
28
|
-
className={`px-4 py-1.5 rounded-lg text-[10px] font-bold uppercase tracking-wider transition-all border ${
|
|
29
|
-
activeSnippet.id === snippet.id
|
|
30
|
-
? 'bg-indigo-600 text-white border-indigo-600 shadow-sm scale-[1.03]'
|
|
31
|
-
: 'bg-slate-50 border-slate-200 text-slate-500 hover:text-indigo-600 hover:border-indigo-200 hover:bg-indigo-50'
|
|
32
|
-
}`}
|
|
33
|
-
>
|
|
34
|
-
{snippet.title}
|
|
35
|
-
</button>
|
|
36
|
-
))}
|
|
37
|
-
</div>
|
|
38
|
-
|
|
39
|
-
{/* Progress bar */}
|
|
40
|
-
<div className="bg-slate-100 rounded-full h-1.5 overflow-hidden">
|
|
41
|
-
<div
|
|
42
|
-
className="h-full rounded-full transition-all duration-500"
|
|
43
|
-
style={{
|
|
44
|
-
width: `${progressPercent}%`,
|
|
45
|
-
background: 'linear-gradient(90deg, #4f46e5, #7c3aed)',
|
|
46
|
-
boxShadow: '0 0 6px rgba(79, 102, 241, 0.4)',
|
|
47
|
-
}}
|
|
48
|
-
/>
|
|
49
|
-
</div>
|
|
50
|
-
|
|
51
|
-
{/* Content */}
|
|
52
|
-
<div className="flex-grow flex flex-col gap-5">
|
|
53
|
-
<div className="flex items-start justify-between gap-4 flex-wrap sm:flex-nowrap">
|
|
54
|
-
<div>
|
|
55
|
-
<h3 className="text-lg font-bold text-slate-900 mb-1.5 flex items-center gap-3">
|
|
56
|
-
{activeSnippet.title}
|
|
57
|
-
<span className="text-[9px] font-extrabold uppercase px-2 py-0.5 rounded bg-indigo-50 border border-indigo-100 text-indigo-600">
|
|
58
|
-
Step {currentIndex + 1} of {totalSteps}
|
|
59
|
-
</span>
|
|
60
|
-
</h3>
|
|
61
|
-
<p className="text-xs text-slate-500 leading-relaxed italic">{activeSnippet.description}</p>
|
|
62
|
-
</div>
|
|
63
|
-
<span className="text-xs font-mono font-bold text-indigo-600 self-center shrink-0">{progressPercent}% Complete</span>
|
|
64
|
-
</div>
|
|
65
|
-
|
|
66
|
-
<CodeViewer snippet={activeSnippet} />
|
|
67
|
-
|
|
68
|
-
{/* Navigation */}
|
|
69
|
-
<div className="flex items-center justify-between border-t border-slate-100 pt-5 mt-auto">
|
|
70
|
-
<button
|
|
71
|
-
onClick={handlePrev}
|
|
72
|
-
disabled={isFirst}
|
|
73
|
-
className={`flex items-center gap-2 px-5 py-2.5 rounded-xl text-xs font-bold transition-all border ${
|
|
74
|
-
isFirst
|
|
75
|
-
? 'opacity-35 cursor-not-allowed border-slate-100 text-slate-300 bg-slate-50'
|
|
76
|
-
: 'bg-white border-slate-200 text-slate-600 hover:border-slate-300 hover:bg-slate-50 active:scale-95'
|
|
77
|
-
}`}
|
|
78
|
-
>
|
|
79
|
-
<ArrowLeft size={13} /> Previous Step
|
|
80
|
-
</button>
|
|
81
|
-
<button
|
|
82
|
-
onClick={handleNext}
|
|
83
|
-
disabled={isLast}
|
|
84
|
-
className={`flex items-center gap-2 px-6 py-2.5 rounded-xl text-xs font-bold transition-all border ${
|
|
85
|
-
isLast
|
|
86
|
-
? 'opacity-35 cursor-not-allowed border-slate-100 text-slate-300 bg-slate-50'
|
|
87
|
-
: 'text-white border-transparent shadow-md active:scale-95 hover:-translate-y-0.5'
|
|
88
|
-
}`}
|
|
89
|
-
style={!isLast ? { background: 'linear-gradient(135deg, #4f46e5, #7c3aed)', boxShadow: '0 4px 14px -4px rgba(79,70,229,0.35)' } : {}}
|
|
90
|
-
>
|
|
91
|
-
{isLast ? 'Complete!' : 'Next Step'} <ArrowRight size={13} />
|
|
92
|
-
</button>
|
|
93
|
-
</div>
|
|
94
|
-
</div>
|
|
95
|
-
</div>
|
|
96
|
-
);
|
|
97
|
-
}
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import React from 'react';
|
|
4
|
-
import { FileText, Zap, Package, ExternalLink, Code } from 'lucide-react';
|
|
5
|
-
import { DocViewer } from '@/components/DocViewer';
|
|
6
|
-
import { LANDING_PAGE_CONTENT, QUICK_START_STEPS, ADVANCED_STEPS, API_ENDPOINTS, SNIPPETS } from '@/app/constants';
|
|
7
|
-
import { Snippet } from '@/app/types';
|
|
8
|
-
|
|
9
|
-
export function Documentation() {
|
|
10
|
-
const [activeSnippet, setActiveSnippet] = React.useState<Snippet>(SNIPPETS[0]);
|
|
11
|
-
|
|
12
|
-
return (
|
|
13
|
-
<div className="max-w-6xl mx-auto">
|
|
14
|
-
{/* Header */}
|
|
15
|
-
<div className="text-center mb-14">
|
|
16
|
-
<span className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-indigo-50 border border-indigo-100 text-[10px] font-bold uppercase tracking-widest text-indigo-600 mb-5">
|
|
17
|
-
<span className="w-1.5 h-1.5 rounded-full bg-indigo-500 animate-pulse" />
|
|
18
|
-
Developer Guide
|
|
19
|
-
</span>
|
|
20
|
-
<h2 className="text-3xl md:text-4xl font-black text-slate-900 mb-4">
|
|
21
|
-
{LANDING_PAGE_CONTENT.guide.title}
|
|
22
|
-
</h2>
|
|
23
|
-
<p className="text-slate-500 max-w-xl mx-auto text-base leading-relaxed">
|
|
24
|
-
{LANDING_PAGE_CONTENT.guide.subtitle}
|
|
25
|
-
</p>
|
|
26
|
-
</div>
|
|
27
|
-
|
|
28
|
-
{/* Grid */}
|
|
29
|
-
<div className="w-full grid gap-8 lg:grid-cols-[270px_1fr] items-start">
|
|
30
|
-
{/* Sidebar */}
|
|
31
|
-
<aside className="flex flex-col gap-5">
|
|
32
|
-
{/* Quick Start */}
|
|
33
|
-
<section>
|
|
34
|
-
<h2 className="mb-3 text-[10px] font-black text-slate-400 uppercase tracking-[0.3em] flex items-center gap-2">
|
|
35
|
-
<FileText size={12} className="text-indigo-500" /> Quick Start
|
|
36
|
-
</h2>
|
|
37
|
-
<ol className="space-y-1">
|
|
38
|
-
{QUICK_START_STEPS.map((step, i) => {
|
|
39
|
-
const isActive = activeSnippet.id === step.id;
|
|
40
|
-
return (
|
|
41
|
-
<li key={step.id}>
|
|
42
|
-
<button
|
|
43
|
-
onClick={() => { const s = SNIPPETS.find(s => s.id === step.id); if (s) setActiveSnippet(s); }}
|
|
44
|
-
className={`w-full flex items-start gap-3 p-3 rounded-xl border text-left transition-all active:scale-[0.98] ${
|
|
45
|
-
isActive
|
|
46
|
-
? 'bg-indigo-50 border-indigo-200 shadow-sm'
|
|
47
|
-
: 'bg-white border-slate-200 hover:border-slate-300 hover:bg-slate-50'
|
|
48
|
-
}`}
|
|
49
|
-
>
|
|
50
|
-
<span className={`flex h-6 w-6 shrink-0 items-center justify-center rounded-lg font-mono text-[10px] font-bold border transition-all ${
|
|
51
|
-
isActive ? 'bg-indigo-600 text-white border-indigo-600 shadow-sm' : 'bg-slate-100 text-slate-500 border-slate-200'
|
|
52
|
-
}`}>{i + 1}</span>
|
|
53
|
-
<div className="pt-0.5">
|
|
54
|
-
<span className={`text-xs font-semibold block ${isActive ? 'text-indigo-700 font-bold' : 'text-slate-700'}`}>{step.text}</span>
|
|
55
|
-
<span className="text-[9px] text-slate-400 block mt-0.5">Step {i + 1}</span>
|
|
56
|
-
</div>
|
|
57
|
-
</button>
|
|
58
|
-
</li>
|
|
59
|
-
);
|
|
60
|
-
})}
|
|
61
|
-
</ol>
|
|
62
|
-
</section>
|
|
63
|
-
|
|
64
|
-
{/* Advanced */}
|
|
65
|
-
<section>
|
|
66
|
-
<h2 className="mb-3 text-[10px] font-black text-slate-400 uppercase tracking-[0.3em] flex items-center gap-2">
|
|
67
|
-
<Code size={12} className="text-violet-500" /> Advanced Options
|
|
68
|
-
</h2>
|
|
69
|
-
<ol className="space-y-1">
|
|
70
|
-
{ADVANCED_STEPS.map((step, i) => {
|
|
71
|
-
const isActive = activeSnippet.id === step.id;
|
|
72
|
-
return (
|
|
73
|
-
<li key={step.id}>
|
|
74
|
-
<button
|
|
75
|
-
onClick={() => { const s = SNIPPETS.find(s => s.id === step.id); if (s) setActiveSnippet(s); }}
|
|
76
|
-
className={`w-full flex items-start gap-3 p-3 rounded-xl border text-left transition-all active:scale-[0.98] ${
|
|
77
|
-
isActive
|
|
78
|
-
? 'bg-violet-50 border-violet-200 shadow-sm'
|
|
79
|
-
: 'bg-white border-slate-200 hover:border-slate-300 hover:bg-slate-50'
|
|
80
|
-
}`}
|
|
81
|
-
>
|
|
82
|
-
<span className={`flex h-6 w-6 shrink-0 items-center justify-center rounded-lg font-mono text-[10px] font-bold border transition-all ${
|
|
83
|
-
isActive ? 'bg-violet-600 text-white border-violet-600 shadow-sm' : 'bg-slate-100 text-slate-500 border-slate-200'
|
|
84
|
-
}`}>{i + 5}</span>
|
|
85
|
-
<div className="pt-0.5">
|
|
86
|
-
<span className={`text-xs font-semibold block ${isActive ? 'text-violet-700 font-bold' : 'text-slate-700'}`}>{step.text}</span>
|
|
87
|
-
<span className="text-[9px] text-slate-400 block mt-0.5">Extension</span>
|
|
88
|
-
</div>
|
|
89
|
-
</button>
|
|
90
|
-
</li>
|
|
91
|
-
);
|
|
92
|
-
})}
|
|
93
|
-
</ol>
|
|
94
|
-
</section>
|
|
95
|
-
|
|
96
|
-
{/* API Endpoints */}
|
|
97
|
-
<section className="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
|
|
98
|
-
<h2 className="mb-4 text-[10px] font-black text-slate-400 uppercase tracking-[0.3em] flex items-center gap-2">
|
|
99
|
-
<Zap size={12} className="text-indigo-500" /> API Endpoints
|
|
100
|
-
</h2>
|
|
101
|
-
<div className="grid gap-2">
|
|
102
|
-
{API_ENDPOINTS.map((slug) => (
|
|
103
|
-
<div
|
|
104
|
-
key={slug}
|
|
105
|
-
className="flex items-center justify-between p-2.5 rounded-lg bg-slate-50 border border-slate-100 hover:border-indigo-200 hover:bg-indigo-50 transition-all group"
|
|
106
|
-
>
|
|
107
|
-
<span className="text-[10px] font-mono text-slate-500 group-hover:text-indigo-600 transition-colors">/api/{slug}</span>
|
|
108
|
-
<span className="text-[8px] font-bold text-indigo-600 bg-indigo-50 px-1.5 py-0.5 rounded border border-indigo-100">POST</span>
|
|
109
|
-
</div>
|
|
110
|
-
))}
|
|
111
|
-
</div>
|
|
112
|
-
</section>
|
|
113
|
-
</aside>
|
|
114
|
-
|
|
115
|
-
{/* Code Viewer */}
|
|
116
|
-
<section className="flex flex-col h-full min-h-[560px]">
|
|
117
|
-
<div className="rounded-2xl border border-slate-200 bg-white overflow-hidden h-full shadow-sm">
|
|
118
|
-
<DocViewer activeSnippet={activeSnippet} setActiveSnippet={setActiveSnippet} />
|
|
119
|
-
</div>
|
|
120
|
-
</section>
|
|
121
|
-
</div>
|
|
122
|
-
|
|
123
|
-
{/* CTA */}
|
|
124
|
-
<div className="mt-14 text-center">
|
|
125
|
-
<a
|
|
126
|
-
href="https://www.npmjs.com/package/@retrivora-ai/rag-engine"
|
|
127
|
-
target="_blank" rel="noreferrer"
|
|
128
|
-
className="inline-flex items-center gap-3 px-8 py-4 rounded-2xl text-white font-bold text-sm shadow-lg transition-all hover:-translate-y-0.5"
|
|
129
|
-
style={{
|
|
130
|
-
background: 'linear-gradient(135deg, #4f46e5, #7c3aed)',
|
|
131
|
-
boxShadow: '0 8px 24px -6px rgba(79, 70, 229, 0.35)',
|
|
132
|
-
}}
|
|
133
|
-
>
|
|
134
|
-
<Package className="w-5 h-5" />
|
|
135
|
-
Get Started on NPM
|
|
136
|
-
<ExternalLink className="w-4 h-4" />
|
|
137
|
-
</a>
|
|
138
|
-
</div>
|
|
139
|
-
</div>
|
|
140
|
-
);
|
|
141
|
-
}
|