diff --git a/studio/frontend/src/app/routes/__root.tsx b/studio/frontend/src/app/routes/__root.tsx index 47bff815e6..69f9c26fb0 100644 --- a/studio/frontend/src/app/routes/__root.tsx +++ b/studio/frontend/src/app/routes/__root.tsx @@ -5,6 +5,7 @@ import { AppSidebar } from "@/components/app-sidebar"; import { Navbar } from "@/components/navbar"; import { fetchDeviceType, usePlatformStore } from "@/config/env"; import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"; +import { IngestionToastStack } from "@/features/rag/components/ingestion-toast-stack"; import { SettingsDialog, useSettingsDialogStore } from "@/features/settings"; import { useTrainingUnloadGuard } from "@/features/training/hooks/use-training-unload-guard"; import { useSidebarPin } from "@/hooks/use-sidebar-pin"; @@ -114,6 +115,7 @@ function RootLayout() { return ( + {hideNavbar ? (
diff --git a/studio/frontend/src/features/rag/components/ingestion-toast-stack.tsx b/studio/frontend/src/features/rag/components/ingestion-toast-stack.tsx new file mode 100644 index 0000000000..ad824c7111 --- /dev/null +++ b/studio/frontend/src/features/rag/components/ingestion-toast-stack.tsx @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 + +import { Button } from "@/components/ui/button"; +import { Cancel01Icon } from "@hugeicons/core-free-icons"; +import { HugeiconsIcon } from "@hugeicons/react"; +import { AnimatePresence, motion, useReducedMotion } from "motion/react"; +import { useEffect, useState } from "react"; +import { useRagStore } from "../stores/rag-store"; +import { IngestionProgress } from "./ingestion-progress"; + +/** + * Floating progress stack for in-flight RAG ingestion jobs. + * + * Watches the rag-store `jobs` map (populated by `subscribeJob`) and + * renders one card per job that's neither completed nor errored. On + * completion the card stays for a few seconds with a success state + * before fading out, so users notice the run finished even if they + * weren't watching the per-doc progress chips. + * + * Mounted once at the app root (`__root.tsx`) so it follows users + * across pages. + */ + +const DISMISS_DELAY_MS = 4000; + +export function IngestionToastStack() { + const jobs = useRagStore((s) => s.jobs); + const reduced = useReducedMotion(); + // Per-job timeout handles so terminal toasts auto-dismiss. + const [dismissedJobs, setDismissedJobs] = useState>( + () => new Set(), + ); + + // Schedule auto-dismiss for jobs that have reached a terminal state. + useEffect(() => { + const timers: ReturnType[] = []; + for (const [jobId, event] of Object.entries(jobs)) { + if ( + (event.type === "complete" || event.type === "error") && + !dismissedJobs.has(jobId) + ) { + timers.push( + setTimeout(() => { + setDismissedJobs((prev) => { + const next = new Set(prev); + next.add(jobId); + return next; + }); + }, DISMISS_DELAY_MS), + ); + } + } + return () => timers.forEach(clearTimeout); + }, [jobs, dismissedJobs]); + + const visible = Object.entries(jobs).filter( + ([jobId]) => !dismissedJobs.has(jobId), + ); + if (visible.length === 0) return null; + + return ( +
+ + {visible.map(([jobId, event]) => { + const isTerminal = + event.type === "complete" || event.type === "error"; + const isError = event.type === "error"; + return ( + +
+
+ + {isError + ? "Ingestion failed" + : event.type === "complete" + ? "Indexed" + : "Indexing document"} + + +
+ +
+
+ ); + })} +
+
+ ); +}