mirror of
https://github.com/mblanke/Dashboard.git
synced 2026-03-01 12:10:20 -05:00
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
This commit is contained in:
34
src/app/api/rag/route.ts
Normal file
34
src/app/api/rag/route.ts
Normal file
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import NetworkWidget from "@/components/NetworkWidget";
|
|||||||
import SynologyWidget from "@/components/SynologyWidget";
|
import SynologyWidget from "@/components/SynologyWidget";
|
||||||
import ServerStatsWidget from "@/components/ServerStatsWidget";
|
import ServerStatsWidget from "@/components/ServerStatsWidget";
|
||||||
import GPUStatsWidget from "@/components/GPUStatsWidget";
|
import GPUStatsWidget from "@/components/GPUStatsWidget";
|
||||||
|
import RAGWidget from "@/components/RAGWidget";
|
||||||
import { Container } from "@/types";
|
import { Container } from "@/types";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
@@ -145,11 +146,7 @@ export default function Home() {
|
|||||||
dashboardUid="docker-containers"
|
dashboardUid="docker-containers"
|
||||||
panelId={8}
|
panelId={8}
|
||||||
/>
|
/>
|
||||||
<div className="grid grid-cols-3 gap-2">
|
<RAGWidget />
|
||||||
<GrafanaWidget title="Files Processed" dashboardUid="rag-pipeline" panelId={1} height={120} />
|
|
||||||
<GrafanaWidget title="Failed Files" dashboardUid="rag-pipeline" panelId={2} height={120} />
|
|
||||||
<GrafanaWidget title="Total Chunks" dashboardUid="rag-pipeline" panelId={3} height={120} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
131
src/components/RAGWidget.tsx
Normal file
131
src/components/RAGWidget.tsx
Normal file
@@ -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<RAGData | null>(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 (
|
||||||
|
<div className="bg-gray-800/50 rounded-xl border border-gray-700 p-4">
|
||||||
|
<div className="flex items-center gap-2 text-red-400 text-sm">
|
||||||
|
<XCircle className="w-4 h-4" />
|
||||||
|
<span>RAG API unreachable</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return (
|
||||||
|
<div className="bg-gray-800/50 rounded-xl border border-gray-700 p-4 animate-pulse">
|
||||||
|
<div className="h-20 bg-gray-700/50 rounded" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="bg-gray-800/50 rounded-xl border border-gray-700 p-4">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Database className="w-5 h-5 text-purple-400" />
|
||||||
|
<h3 className="text-sm font-semibold text-white">RAG Pipeline</h3>
|
||||||
|
</div>
|
||||||
|
{ingest.running && (
|
||||||
|
<span className="flex items-center gap-1.5 text-xs text-amber-400 bg-amber-400/10 px-2 py-0.5 rounded-full">
|
||||||
|
<RefreshCw className="w-3 h-3 animate-spin" />
|
||||||
|
Ingesting
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{!ingest.running && (
|
||||||
|
<span className="flex items-center gap-1.5 text-xs text-green-400 bg-green-400/10 px-2 py-0.5 rounded-full">
|
||||||
|
<CheckCircle className="w-3 h-3" />
|
||||||
|
Idle
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats row */}
|
||||||
|
<div className="grid grid-cols-3 gap-3 mb-3">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="flex items-center justify-center gap-1 text-green-400 mb-0.5">
|
||||||
|
<FileText className="w-3.5 h-3.5" />
|
||||||
|
</div>
|
||||||
|
<div className="text-lg font-bold text-white">{processed.toLocaleString()}</div>
|
||||||
|
<div className="text-[10px] text-gray-500 uppercase tracking-wider">Files</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="flex items-center justify-center gap-1 text-blue-400 mb-0.5">
|
||||||
|
<Layers className="w-3.5 h-3.5" />
|
||||||
|
</div>
|
||||||
|
<div className="text-lg font-bold text-white">{totalChunks.toLocaleString()}</div>
|
||||||
|
<div className="text-[10px] text-gray-500 uppercase tracking-wider">Chunks</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="flex items-center justify-center gap-1 text-red-400 mb-0.5">
|
||||||
|
<XCircle className="w-3.5 h-3.5" />
|
||||||
|
</div>
|
||||||
|
<div className="text-lg font-bold text-white">{failed}</div>
|
||||||
|
<div className="text-[10px] text-gray-500 uppercase tracking-wider">Failed</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Progress bar (shows when ingesting) */}
|
||||||
|
{ingest.running && ingest.total > 0 && (
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<div className="flex justify-between text-xs text-gray-400">
|
||||||
|
<span>{ingest.done + ingest.failed}/{ingest.total}</span>
|
||||||
|
<span>{pct}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="h-1.5 bg-gray-700 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="h-full rounded-full bg-gradient-to-r from-purple-500 to-blue-500 transition-all duration-500"
|
||||||
|
style={{ width: `${pct}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{shortFile && (
|
||||||
|
<div className="text-[10px] text-gray-500 truncate" title={ingest.currentFile}>
|
||||||
|
{shortFile}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user