diff --git a/app/api/collections/[collection_id]/route.ts b/app/api/collections/[collection_id]/route.ts
new file mode 100644
index 0000000..5bebdb5
--- /dev/null
+++ b/app/api/collections/[collection_id]/route.ts
@@ -0,0 +1,86 @@
+import { NextResponse } from 'next/server';
+
+const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8000';
+
+// GET /api/collections/[collection_id] - Get a specific collection
+export async function GET(
+ request: Request,
+ { params }: { params: Promise<{ collection_id: string }> }
+) {
+ const { collection_id } = await params;
+ const apiKey = request.headers.get('X-API-KEY');
+
+ if (!apiKey) {
+ return NextResponse.json(
+ { error: 'Missing X-API-KEY header' },
+ { status: 401 }
+ );
+ }
+
+ try {
+ const response = await fetch(
+ `${backendUrl}/api/v1/collections/${collection_id}?include_docs=true`,
+ {
+ headers: {
+ 'X-API-KEY': apiKey,
+ },
+ }
+ );
+
+ const text = await response.text();
+ const data = text ? JSON.parse(text) : {};
+
+ if (!response.ok) {
+ return NextResponse.json(data, { status: response.status });
+ }
+
+ return NextResponse.json(data, { status: response.status });
+ } catch (error: any) {
+ return NextResponse.json(
+ { success: false, error: error.message, data: null },
+ { status: 500 }
+ );
+ }
+}
+
+// DELETE /api/collection/[collection_id] - Delete a collection
+export async function DELETE(
+ request: Request,
+ { params }: { params: Promise<{ collection_id: string }> }
+) {
+ const { collection_id } = await params;
+ const apiKey = request.headers.get('X-API-KEY');
+
+ if (!apiKey) {
+ return NextResponse.json(
+ { error: 'Missing X-API-KEY header' },
+ { status: 401 }
+ );
+ }
+
+ try {
+ const response = await fetch(
+ `${backendUrl}/api/v1/collections/${collection_id}`,
+ {
+ method: 'DELETE',
+ headers: {
+ 'X-API-KEY': apiKey,
+ },
+ }
+ );
+
+ const text = await response.text();
+ const data = text ? JSON.parse(text) : { success: true };
+
+ if (!response.ok) {
+ return NextResponse.json(data, { status: response.status });
+ }
+
+ return NextResponse.json(data, { status: response.status });
+ } catch (error: any) {
+ return NextResponse.json(
+ { success: false, error: error.message, data: null },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/collections/jobs/[job_id]/route.ts b/app/api/collections/jobs/[job_id]/route.ts
new file mode 100644
index 0000000..f5fc414
--- /dev/null
+++ b/app/api/collections/jobs/[job_id]/route.ts
@@ -0,0 +1,44 @@
+import { NextResponse } from 'next/server';
+
+const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8000';
+
+// GET /api/collections/jobs/[job_id] - Get collection job status
+export async function GET(
+ request: Request,
+ { params }: { params: Promise<{ job_id: string }> }
+) {
+ const { job_id } = await params;
+ const apiKey = request.headers.get('X-API-KEY');
+
+ if (!apiKey) {
+ return NextResponse.json(
+ { error: 'Missing X-API-KEY header' },
+ { status: 401 }
+ );
+ }
+
+ try {
+ const response = await fetch(
+ `${backendUrl}/api/v1/collections/jobs/${job_id}`,
+ {
+ headers: {
+ 'X-API-KEY': apiKey,
+ },
+ }
+ );
+
+ const text = await response.text();
+ const data = text ? JSON.parse(text) : {};
+
+ if (!response.ok) {
+ return NextResponse.json(data, { status: response.status });
+ }
+
+ return NextResponse.json(data, { status: response.status });
+ } catch (error: any) {
+ return NextResponse.json(
+ { success: false, error: error.message, data: null },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/collections/route.ts b/app/api/collections/route.ts
new file mode 100644
index 0000000..edade40
--- /dev/null
+++ b/app/api/collections/route.ts
@@ -0,0 +1,84 @@
+import { NextRequest, NextResponse } from 'next/server';
+
+
+export async function GET(request: Request) {
+ const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8000';
+ const apiKey = request.headers.get('X-API-KEY');
+
+ if (!apiKey) {
+ return NextResponse.json(
+ { error: 'Missing X-API-KEY header' },
+ { status: 401 }
+ );
+ }
+
+ try {
+ const response = await fetch(`${backendUrl}/api/v1/collections/`, {
+ headers: {
+ 'X-API-KEY': apiKey,
+ },
+ });
+
+ // Handle empty responses (204 No Content, etc.)
+ const text = await response.text();
+ const data = text ? JSON.parse(text) : [];
+
+ if (!response.ok) {
+ return NextResponse.json(data, { status: response.status });
+ }
+
+ return NextResponse.json(data, { status: response.status });
+ } catch (error: any) {
+ return NextResponse.json(
+ { success: false, error: error.message, data: null },
+ { status: 500 }
+ );
+ }
+}
+
+export async function POST(request: NextRequest) {
+ try {
+ // Get the API key from request headers
+ const apiKey = request.headers.get('X-API-KEY');
+
+ if (!apiKey) {
+ return NextResponse.json(
+ { error: 'Missing X-API-KEY header' },
+ { status: 401 }
+ );
+ }
+
+ // Get the JSON body from the request
+ const body = await request.json();
+
+ // Get backend URL from environment variable
+ const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8000';
+
+ // Forward the request to the actual backend
+ const response = await fetch(`${backendUrl}/api/v1/collections/`, {
+ method: 'POST',
+ body: JSON.stringify(body),
+ headers: {
+ 'X-API-KEY': apiKey,
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ // Handle empty responses (204 No Content, etc.)
+ const text = await response.text();
+ const data = text ? JSON.parse(text) : { success: true };
+
+ // Return the response with the same status code
+ if (!response.ok) {
+ return NextResponse.json(data, { status: response.status });
+ }
+
+ return NextResponse.json(data, { status: response.status });
+ } catch (error: any) {
+ console.error('Proxy error:', error);
+ return NextResponse.json(
+ { error: 'Failed to forward request to backend', details: error.message },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/components/Sidebar.tsx b/app/components/Sidebar.tsx
index 22f1d5d..c20a629 100644
--- a/app/components/Sidebar.tsx
+++ b/app/components/Sidebar.tsx
@@ -73,6 +73,7 @@ export default function Sidebar({ collapsed, activeRoute = '/evaluations' }: Sid
]
},
{ name: 'Documents', route: '/document' },
+ { name: 'Knowledge Base', route: '/knowledge-base' },
// { name: 'Model Testing', route: '/model-testing', comingSoon: true },
// { name: 'Guardrails', route: '/guardrails', comingSoon: true },
// { name: 'Redteaming', route: '/redteaming', comingSoon: true },
diff --git a/app/components/utils.ts b/app/components/utils.ts
index 6f14bbf..845c28f 100644
--- a/app/components/utils.ts
+++ b/app/components/utils.ts
@@ -5,14 +5,22 @@
/**
* Converts a date string to IST timezone and formats it
* @param dateString - Date string from backend (in IST format but without timezone info)
- * @returns Formatted date string in en-GB locale with 12-hour format
+ * @returns Formatted date string (e.g., "15 Jan 2024, 14:30")
*/
-export const formatDate = (dateString: string): string => {
- // Parse the date string and treat it as IST time
- const date = new Date(dateString);
- // Add 5.5 hours (IST offset) since the input is already in IST but parsed as UTC
- const istDate = new Date(date.getTime() + (5.5 * 60 * 60 * 1000));
- return istDate.toLocaleString('en-GB', { hour12: true });
+export const formatDate = (dateString?: string): string => {
+ if (!dateString) return 'N/A';
+ try {
+ const date = new Date(dateString);
+ const istDate = new Date(date.getTime() + (5.5 * 60 * 60 * 1000));
+ const day = istDate.getDate();
+ const month = istDate.toLocaleDateString('en-US', { month: 'short' });
+ const year = istDate.getFullYear();
+ const hours = String(istDate.getHours()).padStart(2, '0');
+ const minutes = String(istDate.getMinutes()).padStart(2, '0');
+ return `${day} ${month} ${year}, ${hours}:${minutes}`;
+ } catch {
+ return dateString;
+ }
};
/**
diff --git a/app/document/page.tsx b/app/document/page.tsx
index 89c051e..9fc44e1 100644
--- a/app/document/page.tsx
+++ b/app/document/page.tsx
@@ -4,6 +4,7 @@ import { useRouter } from 'next/navigation'
import { APIKey, STORAGE_KEY } from '../keystore/page';
import Sidebar from '../components/Sidebar';
import { useToast } from '../components/Toast';
+import { formatDate } from '../components/utils';
// Backend response interface
export interface Document {
@@ -367,22 +368,6 @@ function DocumentListing({
currentPage,
onPageChange,
}: DocumentListingProps) {
- const formatDate = (dateString?: string) => {
- if (!dateString) return 'N/A';
- try {
- const date = new Date(dateString);
- const istDate = new Date(date.getTime() + (5.5 * 60 * 60 * 1000));
- const day = istDate.getDate();
- const month = istDate.toLocaleDateString('en-US', { month: 'short' });
- const year = istDate.getFullYear();
- const hours = String(istDate.getHours()).padStart(2, '0');
- const minutes = String(istDate.getMinutes()).padStart(2, '0');
- return `${day} ${month} ${year}, ${hours}:${minutes}`;
- } catch {
- return dateString;
- }
- };
-
return (
@@ -594,23 +579,6 @@ function DocumentPreview({ document, isLoading }: DocumentPreviewProps) {
setImageLoadError(false);
}, [document?.id]);
- const formatDate = (dateString?: string) => {
- if (!dateString) return 'N/A';
- try {
- const date = new Date(dateString);
- // Add 5.5 hours (IST offset) since the input is already in IST but parsed as UTC
- const istDate = new Date(date.getTime() + (5.5 * 60 * 60 * 1000));
- const day = istDate.getDate();
- const month = istDate.toLocaleDateString('en-US', { month: 'short' });
- const year = istDate.getFullYear();
- const hours = String(istDate.getHours()).padStart(2, '0');
- const minutes = String(istDate.getMinutes()).padStart(2, '0');
- return `${day} ${month} ${year}, ${hours}:${minutes}`;
- } catch {
- return dateString;
- }
- };
-
const getFileExtension = (filename: string) => {
const parts = filename.split('.');
return parts.length > 1 ? parts[parts.length - 1].toLowerCase() : '';
diff --git a/app/knowledge-base/page.tsx b/app/knowledge-base/page.tsx
new file mode 100644
index 0000000..474e857
--- /dev/null
+++ b/app/knowledge-base/page.tsx
@@ -0,0 +1,1154 @@
+"use client"
+
+import { useState, useEffect, useRef } from 'react';
+import { colors } from '@/app/lib/colors';
+import { formatDate } from '@/app/components/utils';
+import Sidebar from '@/app/components/Sidebar';
+import { APIKey, STORAGE_KEY } from '../keystore/page';
+
+export interface Document {
+ id: string;
+ fname: string;
+ object_store_url: string;
+ signed_url?: string;
+ file_size?: number;
+ inserted_at?: string;
+ updated_at?: string;
+}
+
+export interface Collection {
+ id: string;
+ name?: string;
+ description?: string;
+ inserted_at: string;
+ updated_at: string;
+ status?: string;
+ job_id?: string;
+ documents?: Document[];
+}
+
+export default function KnowledgeBasePage() {
+ const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
+ const [collections, setCollections] = useState
([]);
+ const [availableDocuments, setAvailableDocuments] = useState([]);
+ const [selectedCollection, setSelectedCollection] = useState(null);
+ const [isLoading, setIsLoading] = useState(false);
+ const [isCreating, setIsCreating] = useState(false);
+ const [showCreateForm, setShowCreateForm] = useState(false);
+ const [showDocumentPicker, setShowDocumentPicker] = useState(false);
+ const [showConfirmCreate, setShowConfirmCreate] = useState(false);
+ const [showDocPreviewModal, setShowDocPreviewModal] = useState(false);
+ const [previewDoc, setPreviewDoc] = useState(null);
+ const [apiKey, setApiKey] = useState(null);
+
+ // Polling refs — persist across renders, no stale closures
+ const apiKeyRef = useRef(null);
+ const activeJobsRef = useRef