feat: Add RAG dashboard and API integration for document management

This commit is contained in:
2026-06-14 10:26:17 +08:00
parent 99ca2c838b
commit 19abb59608
4 changed files with 906 additions and 11 deletions
+30 -11
View File
@@ -14,6 +14,7 @@ import {
type UserSessionData,
} from "./api";
import AdminDashboard from "./Admin";
import RagDashboard from "./RagDashboard";
/* ------------------------------------------------------------------ */
/* Types */
@@ -226,6 +227,7 @@ export default function App() {
/* ── Admin state ──────────────────────────────────────────────────── */
const [showAdmin, setShowAdmin] = useState(false);
const [showRag, setShowRag] = useState(false);
/* ── Session sidebar state ───────────────────────────────────────── */
const [userSessions, setUserSessions] = useState<UserSessionData[]>([]);
@@ -419,7 +421,11 @@ export default function App() {
/* ── Admin UI ────────────────────────────────────────────────────── */
if (showAdmin && authenticated.role === "admin") {
return <AdminDashboard onBack={() => setShowAdmin(false)} />;
return <AdminDashboard onBack={() => { setShowAdmin(false); setShowRag(false); }} />;
}
if (showRag && authenticated.role === "admin") {
return <RagDashboard onBack={() => { setShowRag(false); setShowAdmin(false); }} />;
}
/* ── Chat UI ─────────────────────────────────────────────────────── */
@@ -472,16 +478,29 @@ export default function App() {
{/* Admin button — only for admin role */}
{authenticated.role === "admin" && (
<button
onClick={() => setShowAdmin(true)}
className="rounded-lg border border-gray-700 px-3 py-1.5 text-sm text-purple-400 transition hover:border-purple-500 hover:text-purple-300"
title="Admin dashboard"
>
<svg className="mr-1 inline-block h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
Admin
</button>
<>
<button
onClick={() => setShowAdmin(true)}
className="rounded-lg border border-gray-700 px-3 py-1.5 text-sm text-purple-400 transition hover:border-purple-500 hover:text-purple-300"
title="Admin dashboard"
>
<svg className="mr-1 inline-block h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
Admin
</button>
<button
onClick={() => setShowRag(true)}
className="rounded-lg border border-gray-700 px-3 py-1.5 text-sm text-emerald-400 transition hover:border-emerald-500 hover:text-emerald-300"
title="RAG Dashboard"
>
<svg className="mr-1 inline-block h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 21h10a2 2 0 002-2V9a2 2 0 00-1-1.73l-5-3a2 2 0 00-2 0l-5 3A2 2 0 005 9v10a2 2 0 002 2z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 17v-5m0 0l-2-2m2 2l2-2" />
</svg>
RAG
</button>
</>
)}
{/* Logout button */}
+658
View File
@@ -0,0 +1,658 @@
import { useState, useEffect, useRef } from "react";
import {
listRagCollections,
getRagCollectionInfo,
deleteRagCollection,
listRagDocuments,
deleteRagDocument,
retryRagDocument,
uploadRagDocument,
listRagSyncLogs,
type RAGCollectionInfo,
type RAGTrackedDocument,
type RAGSyncLog,
} from "./api";
/* ──────────────────────────────────────────────────────────────────────── */
/* Props */
/* ──────────────────────────────────────────────────────────────────────── */
interface RagProps {
onBack: () => void;
}
const STATUS_COLORS: Record<string, string> = {
done: "bg-emerald-900/50 text-emerald-400",
processing: "bg-amber-900/50 text-amber-400",
error: "bg-red-900/50 text-red-400",
running: "bg-blue-900/50 text-blue-400",
cancelled: "bg-gray-800 text-gray-400",
partial: "bg-amber-900/50 text-amber-400",
};
function statusBadge(status: string) {
const c = STATUS_COLORS[status] ?? "bg-gray-800 text-gray-400";
return `rounded-full px-2.5 py-0.5 text-xs font-medium ${c}`;
}
function formatSize(bytes: number): string {
if (bytes === 0) return "0 B";
const k = 1024;
const sizes = ["B", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;
}
/* ──────────────────────────────────────────────────────────────────────── */
/* Main Dashboard */
/* ──────────────────────────────────────────────────────────────────────── */
export default function RagDashboard({ onBack }: RagProps) {
const [tab, setTab] = useState<"overview" | "documents" | "collections">("overview");
const [collections, setCollections] = useState<string[]>([]);
const [collectionsInfo, setCollectionsInfo] = useState<Map<string, RAGCollectionInfo>>(new Map());
const [documents, setDocuments] = useState<RAGTrackedDocument[]>([]);
const [syncLogs, setSyncLogs] = useState<RAGSyncLog[]>([]);
const [busy, setBusy] = useState(true);
const [error, setError] = useState("");
/* ── Load all data ───────────────────────────────────────────────────── */
const loadAll = async () => {
setBusy(true);
setError("");
try {
const [colNames, docs, logs] = await Promise.all([
listRagCollections(),
listRagDocuments(),
listRagSyncLogs(),
]);
setCollections(colNames);
setDocuments(docs.items);
setSyncLogs(logs.items);
// Fetch info for each collection
const infoMap = new Map<string, RAGCollectionInfo>();
await Promise.all(
colNames.map(async (name) => {
try {
infoMap.set(name, await getRagCollectionInfo(name));
} catch {
/* skip */
}
}),
);
setCollectionsInfo(infoMap);
} catch (err: unknown) {
setError(err instanceof Error ? err.message : "Failed to load RAG data");
} finally {
setBusy(false);
}
};
useEffect(() => {
loadAll();
}, []);
/* ── Derived stats ──────────────────────────────────────────────────── */
const totalVectors = Array.from(collectionsInfo.values()).reduce(
(sum, c) => sum + c.total_vectors,
0,
);
const totalDocuments = documents.length;
const doneDocuments = documents.filter((d) => d.status === "done").length;
const errorDocuments = documents.filter((d) => d.status === "error").length;
/* ── Handlers ───────────────────────────────────────────────────────── */
const handleDeleteCollection = async (name: string) => {
if (!confirm(`Delete collection "${name}" and all its data?`)) return;
try {
await deleteRagCollection(name);
await loadAll();
} catch (err: unknown) {
alert(err instanceof Error ? err.message : "Delete failed");
}
};
const handleDeleteDocument = async (docId: string) => {
if (!confirm("Delete this document and all its vectors?")) return;
try {
await deleteRagDocument(docId);
await loadAll();
} catch (err: unknown) {
alert(err instanceof Error ? err.message : "Delete failed");
}
};
const handleRetryDocument = async (docId: string) => {
try {
await retryRagDocument(docId);
await loadAll();
} catch (err: unknown) {
alert(err instanceof Error ? err.message : "Retry failed");
}
};
/* ── Render ─────────────────────────────────────────────────────────── */
return (
<div className="mx-auto flex h-dvh max-w-6xl flex-col">
{/* Header */}
<header className="flex items-center gap-3 border-b border-gray-800 px-6 py-4">
<button
onClick={onBack}
className="rounded-lg border border-gray-700 p-2 text-gray-400 transition hover:border-indigo-500 hover:text-indigo-400"
title="Back to chat"
>
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
</button>
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-gradient-to-br from-emerald-600 to-teal-500 text-sm font-bold text-white shadow-lg shadow-emerald-500/25">
R
</div>
<div className="flex-1">
<h1 className="text-lg font-semibold leading-tight tracking-tight">
RAG Dashboard
</h1>
<p className="text-xs text-gray-500">
{totalVectors.toLocaleString()} vectors · {totalDocuments} documents
</p>
</div>
<button
onClick={loadAll}
disabled={busy}
className="rounded-lg border border-gray-700 px-3 py-1.5 text-sm text-gray-400 transition hover:border-indigo-500 hover:text-indigo-400 disabled:opacity-40"
>
Refresh
</button>
</header>
{/* Tabs */}
<div className="flex gap-1 border-b border-gray-800 px-6 pt-3">
{(["overview", "documents", "collections"] as const).map((t) => (
<button
key={t}
onClick={() => setTab(t)}
className={`rounded-t-lg px-4 py-2 text-sm font-medium capitalize transition ${
tab === t
? "border border-b-0 border-gray-700 bg-gray-900 text-gray-100"
: "text-gray-500 hover:text-gray-300"
}`}
>
{t}
{t === "documents" && (
<span className="ml-1.5 rounded-full bg-gray-800 px-1.5 py-0.5 text-xs">
{totalDocuments}
</span>
)}
{t === "collections" && (
<span className="ml-1.5 rounded-full bg-gray-800 px-1.5 py-0.5 text-xs">
{collections.length}
</span>
)}
</button>
))}
</div>
{/* Body */}
<div className="flex-1 overflow-y-auto px-6 py-6">
{error && (
<div className="mb-4 rounded-lg bg-red-900/40 px-4 py-3 text-sm text-red-400">
{error}
</div>
)}
{busy && tab === "overview" ? (
<div className="flex items-center justify-center py-20">
<p className="text-sm text-gray-500">Loading</p>
</div>
) : tab === "overview" ? (
<OverviewTab
collections={collections}
collectionsInfo={collectionsInfo}
totalDocuments={totalDocuments}
doneDocuments={doneDocuments}
errorDocuments={errorDocuments}
syncLogs={syncLogs}
/>
) : tab === "documents" ? (
<DocumentsTab
documents={documents}
onDelete={handleDeleteDocument}
onRetry={handleRetryDocument}
onUpload={loadAll}
/>
) : (
<CollectionsTab
collections={collections}
collectionsInfo={collectionsInfo}
onDelete={handleDeleteCollection}
/>
)}
</div>
</div>
);
}
/* ──────────────────────────────────────────────────────────────────────── */
/* Overview Tab */
/* ──────────────────────────────────────────────────────────────────────── */
function OverviewTab({
collections,
collectionsInfo,
totalDocuments,
doneDocuments,
errorDocuments,
syncLogs,
}: {
collections: string[];
collectionsInfo: Map<string, RAGCollectionInfo>;
totalDocuments: number;
doneDocuments: number;
errorDocuments: number;
syncLogs: RAGSyncLog[];
}) {
const totalVectors = Array.from(collectionsInfo.values()).reduce(
(sum, c) => sum + c.total_vectors,
0,
);
const latestSync = syncLogs[0] ?? null;
const cards = [
{
label: "Collections",
value: collections.length,
color: "from-emerald-500 to-teal-600",
},
{
label: "Total Vectors",
value: totalVectors.toLocaleString(),
color: "from-indigo-500 to-purple-600",
},
{
label: "Documents",
value: totalDocuments,
color: "from-amber-500 to-orange-600",
},
{
label: "Ingested",
value: doneDocuments,
color: "from-emerald-500 to-teal-600",
},
];
return (
<div className="space-y-8">
{/* Stat cards */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{cards.map((card) => (
<div
key={card.label}
className="rounded-xl border border-gray-800 bg-gray-900/60 p-5"
>
<p className="text-sm text-gray-500">{card.label}</p>
<p className="mt-1 text-3xl font-bold text-gray-100">
{card.value}
</p>
<div
className={`mt-3 h-1 w-full rounded-full bg-gradient-to-r ${card.color}`}
/>
</div>
))}
</div>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
{/* Document status breakdown */}
<div className="rounded-xl border border-gray-800 bg-gray-900/60 p-5">
<h3 className="mb-3 text-sm font-semibold text-gray-300">
Document Status
</h3>
<div className="space-y-2">
{[
{ label: "Done", count: doneDocuments, color: "from-emerald-500 to-teal-500" },
{ label: "Error", count: errorDocuments, color: "from-red-500 to-rose-500" },
{ label: "Processing", count: totalDocuments - doneDocuments - errorDocuments, color: "from-amber-500 to-orange-500" },
].map(({ label, count, color }) => (
<div key={label} className="flex items-center gap-3">
<span className="w-24 text-sm text-gray-400">{label}</span>
<div className="flex-1">
<div className="h-2 rounded-full bg-gray-800">
<div
className={`h-2 rounded-full bg-gradient-to-r ${color}`}
style={{
width: `${totalDocuments > 0 ? (count / totalDocuments) * 100 : 0}%`,
}}
/>
</div>
</div>
<span className="w-8 text-right text-sm text-gray-400">
{count}
</span>
</div>
))}
</div>
</div>
{/* Collection details */}
<div className="rounded-xl border border-gray-800 bg-gray-900/60 p-5">
<h3 className="mb-3 text-sm font-semibold text-gray-300">
Collections
</h3>
{collections.length === 0 ? (
<p className="py-4 text-center text-sm text-gray-500">
No collections yet.
</p>
) : (
<div className="space-y-3">
{collections.slice(0, 10).map((name) => {
const info = collectionsInfo.get(name);
return (
<div
key={name}
className="flex items-center justify-between rounded-lg bg-gray-800/50 px-3 py-2"
>
<div>
<p className="text-sm text-gray-200">{name}</p>
{info && (
<p className="text-xs text-gray-500">
{info.total_vectors.toLocaleString()} vectors · {info.dim} dims
</p>
)}
</div>
</div>
);
})}
</div>
)}
</div>
</div>
{/* Latest sync */}
{latestSync && (
<div className="rounded-xl border border-gray-800 bg-gray-900/60 p-5">
<h3 className="mb-3 text-sm font-semibold text-gray-300">
Latest Sync
</h3>
<div className="space-y-1 text-sm">
<p className="text-gray-400">
Collection:{" "}
<span className="text-gray-200">{latestSync.collection_name}</span>
</p>
<p className="text-gray-400">
Status:{" "}
<span className={statusBadge(latestSync.status)}>
{latestSync.status}
</span>
</p>
<p className="text-gray-400">
Files:{" "}
<span className="text-gray-200">
{latestSync.ingested} ingested · {latestSync.failed} failed ·{" "}
{latestSync.skipped} skipped
</span>
</p>
{latestSync.error_message && (
<p className="text-red-400">Error: {latestSync.error_message}</p>
)}
{latestSync.completed_at && (
<p className="text-xs text-gray-500">
Completed: {new Date(latestSync.completed_at).toLocaleString()}
</p>
)}
</div>
</div>
)}
</div>
);
}
/* ──────────────────────────────────────────────────────────────────────── */
/* Documents Tab */
/* ──────────────────────────────────────────────────────────────────────── */
function DocumentsTab({
documents,
onDelete,
onRetry,
onUpload,
}: {
documents: RAGTrackedDocument[];
onDelete: (id: string) => void;
onRetry: (id: string) => void;
onUpload: () => void;
}) {
const fileRef = useRef<HTMLInputElement>(null);
const [uploadBusy, setUploadBusy] = useState(false);
const [uploadError, setUploadError] = useState("");
const [collectionFilter, setCollectionFilter] = useState("");
const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
setUploadBusy(true);
setUploadError("");
try {
const result = await uploadRagDocument(file, collectionFilter || undefined);
alert(`Uploaded: ${result.filename} (${result.status})`);
onUpload();
} catch (err: unknown) {
setUploadError(err instanceof Error ? err.message : "Upload failed");
} finally {
setUploadBusy(false);
if (fileRef.current) fileRef.current.value = "";
}
};
const filtered = collectionFilter
? documents.filter((d) => d.collection_name === collectionFilter)
: documents;
const collections = [...new Set(documents.map((d) => d.collection_name))];
return (
<div className="space-y-6">
{/* Upload section */}
<div className="rounded-xl border border-gray-800 bg-gray-900/60 p-5">
<h3 className="mb-3 text-sm font-semibold text-gray-300">
Import Document
</h3>
<div className="flex flex-wrap items-center gap-3">
<input
type="file"
ref={fileRef}
onChange={handleUpload}
accept=".pdf,.docx,.txt,.md"
className="block w-full max-w-xs text-sm text-gray-400 file:mr-3 file:cursor-pointer file:rounded-lg file:border-0 file:bg-indigo-600 file:px-3 file:py-1.5 file:text-sm file:font-medium file:text-white hover:file:bg-indigo-500"
/>
<select
value={collectionFilter}
onChange={(e) => setCollectionFilter(e.target.value)}
className="rounded-lg border border-gray-700 bg-gray-800 px-3 py-1.5 text-sm text-gray-200 outline-none focus:border-indigo-500"
>
<option value="">Default collection</option>
{collections.map((c) => (
<option key={c} value={c}>
{c}
</option>
))}
</select>
{uploadBusy && (
<span className="text-sm text-amber-400">Uploading</span>
)}
</div>
{uploadError && (
<p className="mt-2 text-sm text-red-400">{uploadError}</p>
)}
</div>
{/* Documents table */}
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">
<thead>
<tr className="border-b border-gray-800 text-xs uppercase tracking-wider text-gray-500">
<th className="pb-3 pr-4">Filename</th>
<th className="pb-3 pr-4">Collection</th>
<th className="pb-3 pr-4">Size</th>
<th className="pb-3 pr-4">Status</th>
<th className="pb-3 pr-4">Chunks</th>
<th className="pb-3 pr-4">Created</th>
<th className="pb-3" />
</tr>
</thead>
<tbody>
{filtered.length === 0 ? (
<tr>
<td colSpan={7} className="py-8 text-center text-gray-500">
No documents yet. Upload a PDF, DOCX, TXT, or MD file above.
</td>
</tr>
) : (
filtered.map((doc) => (
<tr
key={doc.id}
className="border-b border-gray-800/50 transition hover:bg-gray-800/30"
>
<td className="max-w-48 py-3 pr-4">
<p className="truncate text-sm text-gray-200">
{doc.filename}
</p>
</td>
<td className="py-3 pr-4">
<span className="text-xs text-gray-400">
{doc.collection_name}
</span>
</td>
<td className="py-3 pr-4 text-gray-400">
{formatSize(doc.filesize)}
</td>
<td className="py-3 pr-4">
<span className={statusBadge(doc.status)}>
{doc.status}
</span>
</td>
<td className="py-3 pr-4 text-gray-400">
{doc.chunk_count}
</td>
<td className="py-3 pr-4 text-xs text-gray-500">
{doc.created_at
? new Date(doc.created_at).toLocaleDateString()
: "—"}
</td>
<td className="py-3">
<div className="flex gap-1">
{doc.status === "error" && (
<button
onClick={() => onRetry(doc.id)}
className="rounded-lg px-2 py-1 text-xs text-amber-400 transition hover:bg-amber-900/30"
title="Retry ingestion"
>
Retry
</button>
)}
<button
onClick={() => onDelete(doc.id)}
className="rounded-lg px-2 py-1 text-xs text-red-400 transition hover:bg-red-900/30"
title="Delete document"
>
Delete
</button>
</div>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
);
}
/* ──────────────────────────────────────────────────────────────────────── */
/* Collections Tab */
/* ──────────────────────────────────────────────────────────────────────── */
function CollectionsTab({
collections,
collectionsInfo,
onDelete,
}: {
collections: string[];
collectionsInfo: Map<string, RAGCollectionInfo>;
onDelete: (name: string) => void;
}) {
return (
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">
<thead>
<tr className="border-b border-gray-800 text-xs uppercase tracking-wider text-gray-500">
<th className="pb-3 pr-4">Name</th>
<th className="pb-3 pr-4">Vectors</th>
<th className="pb-3 pr-4">Dimensions</th>
<th className="pb-3 pr-4">Status</th>
<th className="pb-3" />
</tr>
</thead>
<tbody>
{collections.length === 0 ? (
<tr>
<td colSpan={5} className="py-8 text-center text-gray-500">
No collections found in Milvus.
</td>
</tr>
) : (
collections.map((name) => {
const info = collectionsInfo.get(name);
return (
<tr
key={name}
className="border-b border-gray-800/50 transition hover:bg-gray-800/30"
>
<td className="py-3 pr-4">
<p className="font-medium text-gray-200">{name}</p>
</td>
<td className="py-3 pr-4 text-gray-400">
{info
? info.total_vectors.toLocaleString()
: "…"}
</td>
<td className="py-3 pr-4 text-gray-400">
{info ? info.dim : "…"}
</td>
<td className="py-3 pr-4">
{info && (
<span
className={`rounded-full px-2.5 py-0.5 text-xs font-medium ${
info.indexing_status === "complete"
? "bg-emerald-900/50 text-emerald-400"
: "bg-amber-900/50 text-amber-400"
}`}
>
{info.indexing_status}
</span>
)}
</td>
<td className="py-3">
<button
onClick={() => onDelete(name)}
className="rounded-lg px-2 py-1 text-xs text-red-400 transition hover:bg-red-900/30"
title="Delete collection"
>
Delete
</button>
</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
);
}
+217
View File
@@ -322,3 +322,220 @@ export async function adminDeleteSession(sessionId: string): Promise<void> {
throw new Error(body?.detail ?? `HTTP ${res.status}`);
}
}
/* ── RAG API ─────────────────────────────────────────────────────── */
export interface RAGCollectionInfo {
name: string;
total_vectors: number;
dim: number;
indexing_status: string;
}
export interface RAGCollectionList {
items: string[];
}
export interface RAGSearchResult {
content: string;
score: number;
metadata: Record<string, unknown>;
parent_doc_id: string;
}
export interface RAGSearchResponse {
results: RAGSearchResult[];
}
export interface RAGTrackedDocument {
id: string;
collection_name: string;
filename: string;
filesize: number;
filetype: string;
status: string;
error_message: string | null;
vector_document_id: string | null;
chunk_count: number;
has_file: boolean;
created_at: string | null;
completed_at: string | null;
}
export interface RAGTrackedDocumentList {
items: RAGTrackedDocument[];
total: number;
}
export interface RAGIngestResponse {
id: string;
status: string;
filename: string;
collection: string;
message: string;
}
export interface RAGSyncLog {
id: string;
source: string;
collection_name: string;
status: string;
mode: string;
total_files: number;
ingested: number;
updated: number;
skipped: number;
failed: number;
error_message: string | null;
started_at: string | null;
completed_at: string | null;
}
export interface RAGSyncLogList {
items: RAGSyncLog[];
total: number;
}
export interface RAGMessageResponse {
message: string;
}
/** List Milvus collections. */
export async function listRagCollections(): Promise<string[]> {
const res = await fetch(`${API_BASE}/rag/collections`, {
headers: { ...authHeaders() },
});
if (!res.ok) {
const body = await res.json().catch(() => null);
throw new Error(body?.detail ?? `HTTP ${res.status}`);
}
const data: RAGCollectionList = await res.json();
return data.items;
}
/** Get collection info. */
export async function getRagCollectionInfo(
name: string,
): Promise<RAGCollectionInfo> {
const res = await fetch(`${API_BASE}/rag/collections/${encodeURIComponent(name)}`, {
headers: { ...authHeaders() },
});
if (!res.ok) {
const body = await res.json().catch(() => null);
throw new Error(body?.detail ?? `HTTP ${res.status}`);
}
return res.json();
}
/** Delete a collection. */
export async function deleteRagCollection(name: string): Promise<string> {
const res = await fetch(`${API_BASE}/rag/collections/${encodeURIComponent(name)}`, {
method: "DELETE",
headers: { ...authHeaders() },
});
if (!res.ok) {
const body = await res.json().catch(() => null);
throw new Error(body?.detail ?? `HTTP ${res.status}`);
}
const data: RAGMessageResponse = await res.json();
return data.message;
}
/** List tracked documents. */
export async function listRagDocuments(
collectionName?: string,
): Promise<RAGTrackedDocumentList> {
const params = collectionName ? `?collection_name=${encodeURIComponent(collectionName)}` : "";
const res = await fetch(`${API_BASE}/rag/documents${params}`, {
headers: { ...authHeaders() },
});
if (!res.ok) {
const body = await res.json().catch(() => null);
throw new Error(body?.detail ?? `HTTP ${res.status}`);
}
return res.json();
}
/** Delete a tracked document. */
export async function deleteRagDocument(docId: string): Promise<string> {
const res = await fetch(`${API_BASE}/rag/documents/${docId}`, {
method: "DELETE",
headers: { ...authHeaders() },
});
if (!res.ok) {
const body = await res.json().catch(() => null);
throw new Error(body?.detail ?? `HTTP ${res.status}`);
}
const data: RAGMessageResponse = await res.json();
return data.message;
}
/** Retry ingestion for a failed document. */
export async function retryRagDocument(docId: string): Promise<{ id: string; status: string; message: string }> {
const res = await fetch(`${API_BASE}/rag/documents/${docId}/retry`, {
method: "POST",
headers: { ...authHeaders() },
});
if (!res.ok) {
const body = await res.json().catch(() => null);
throw new Error(body?.detail ?? `HTTP ${res.status}`);
}
return res.json();
}
/** Upload a file for RAG ingestion. */
export async function uploadRagDocument(
file: File,
collectionName = "documents",
replace = true,
): Promise<RAGIngestResponse> {
const formData = new FormData();
formData.append("file", file);
const url = `${API_BASE}/rag/upload/${encodeURIComponent(collectionName)}?replace=${replace}`;
const res = await fetch(url, {
method: "POST",
headers: { ...authHeaders() }, // no Content-Type for FormData
body: formData,
});
if (!res.ok) {
const body = await res.json().catch(() => null);
throw new Error(body?.detail ?? `HTTP ${res.status}`);
}
return res.json();
}
/** List sync logs. */
export async function listRagSyncLogs(
collectionName?: string,
limit = 20,
): Promise<RAGSyncLogList> {
const params = new URLSearchParams();
if (collectionName) params.set("collection_name", collectionName);
params.set("limit", String(limit));
const res = await fetch(`${API_BASE}/rag/sync-logs?${params}`, {
headers: { ...authHeaders() },
});
if (!res.ok) {
const body = await res.json().catch(() => null);
throw new Error(body?.detail ?? `HTTP ${res.status}`);
}
return res.json();
}
/** Search across RAG collections. */
export async function searchRag(
query: string,
collectionName = "documents",
limit = 5,
): Promise<RAGSearchResponse> {
const res = await fetch(`${API_BASE}/rag/search`, {
method: "POST",
headers: { "Content-Type": "application/json", ...authHeaders() },
body: JSON.stringify({ query, collection_name: collectionName, limit }),
});
if (!res.ok) {
const body = await res.json().catch(() => null);
throw new Error(body?.detail ?? `HTTP ${res.status}`);
}
return res.json();
}
+1
View File
@@ -0,0 +1 @@
{"root":["./src/Admin.tsx","./src/App.tsx","./src/RagDashboard.tsx","./src/api.ts","./src/main.tsx","./src/vite-env.d.ts"],"version":"5.9.3"}