From dc01ddc09f98a447213460c7381ba14850516d1e Mon Sep 17 00:00:00 2001 From: mblanke Date: Sun, 15 Feb 2026 08:51:38 -0500 Subject: [PATCH] feat: add live RAG pipeline widget - New RAGWidget with live stats (files/chunks/failed), status badge, progress bar - API proxy route /api/rag -> rag-api /status + /ingest-progress - Replace 3 Grafana RAG panels with single live widget - 5s auto-polling with error handling --- src/app/api/rag/route.ts | 34 +++++++++ src/app/page.tsx | 7 +- src/components/RAGWidget.tsx | 131 +++++++++++++++++++++++++++++++++++ 3 files changed, 167 insertions(+), 5 deletions(-) create mode 100644 src/app/api/rag/route.ts create mode 100644 src/components/RAGWidget.tsx diff --git a/src/app/api/rag/route.ts b/src/app/api/rag/route.ts new file mode 100644 index 0000000..2c79948 --- /dev/null +++ b/src/app/api/rag/route.ts @@ -0,0 +1,34 @@ +import { NextResponse } from "next/server"; + +const RAG_API = process.env.RAG_API_URL || "http://localhost:8099"; + +export async function GET() { + try { + const [statusRes, progressRes] = await Promise.all([ + fetch(`${RAG_API}/status`, { next: { revalidate: 0 } }), + fetch(`${RAG_API}/ingest-progress`, { next: { revalidate: 0 } }), + ]); + + const status = await statusRes.json(); + const progress = await progressRes.json(); + + return NextResponse.json({ + processed: status.processed ?? 0, + failed: status.failed ?? 0, + totalChunks: status.total_chunks ?? 0, + ingest: { + running: progress.running ?? false, + total: progress.total ?? 0, + done: progress.done ?? 0, + failed: progress.failed ?? 0, + currentFile: progress.current_file ?? "", + skipped: progress.skipped ?? 0, + }, + }); + } catch { + return NextResponse.json( + { error: "Failed to fetch RAG status" }, + { status: 500 } + ); + } +} diff --git a/src/app/page.tsx b/src/app/page.tsx index f3603ec..4a8089d 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -9,6 +9,7 @@ import NetworkWidget from "@/components/NetworkWidget"; import SynologyWidget from "@/components/SynologyWidget"; import ServerStatsWidget from "@/components/ServerStatsWidget"; import GPUStatsWidget from "@/components/GPUStatsWidget"; +import RAGWidget from "@/components/RAGWidget"; import { Container } from "@/types"; export default function Home() { @@ -145,11 +146,7 @@ export default function Home() { dashboardUid="docker-containers" panelId={8} /> -
- - - -
+ diff --git a/src/components/RAGWidget.tsx b/src/components/RAGWidget.tsx new file mode 100644 index 0000000..a7af9ac --- /dev/null +++ b/src/components/RAGWidget.tsx @@ -0,0 +1,131 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Database, RefreshCw, CheckCircle, XCircle, FileText, Layers } from "lucide-react"; + +interface RAGData { + processed: number; + failed: number; + totalChunks: number; + ingest: { + running: boolean; + total: number; + done: number; + failed: number; + currentFile: string; + skipped: number; + }; +} + +export default function RAGWidget() { + const [data, setData] = useState(null); + const [error, setError] = useState(false); + + useEffect(() => { + const fetchData = async () => { + try { + const res = await fetch("/api/rag"); + if (!res.ok) throw new Error(); + setData(await res.json()); + setError(false); + } catch { + setError(true); + } + }; + fetchData(); + const interval = setInterval(fetchData, 5000); + return () => clearInterval(interval); + }, []); + + if (error) { + return ( +
+
+ + RAG API unreachable +
+
+ ); + } + + if (!data) { + return ( +
+
+
+ ); + } + + const { processed, failed, totalChunks, ingest } = data; + const pct = ingest.total > 0 ? Math.round(((ingest.done + ingest.failed) / ingest.total) * 100) : 0; + const shortFile = ingest.currentFile ? ingest.currentFile.split("/").pop()?.slice(0, 45) : ""; + + return ( +
+
+
+ +

RAG Pipeline

+
+ {ingest.running && ( + + + Ingesting + + )} + {!ingest.running && ( + + + Idle + + )} +
+ + {/* Stats row */} +
+
+
+ +
+
{processed.toLocaleString()}
+
Files
+
+
+
+ +
+
{totalChunks.toLocaleString()}
+
Chunks
+
+
+
+ +
+
{failed}
+
Failed
+
+
+ + {/* Progress bar (shows when ingesting) */} + {ingest.running && ingest.total > 0 && ( +
+
+ {ingest.done + ingest.failed}/{ingest.total} + {pct}% +
+
+
+
+ {shortFile && ( +
+ {shortFile} +
+ )} +
+ )} +
+ ); +}