"use client"; import * as React from "react"; import Link from "next/link"; import { usePathname, useRouter } from "next/navigation"; import { ArrowRight, Sparkles } from "lucide-react"; import { useApp } from "@/components/app/store"; import { useApi } from "@/lib/client/api"; import type { PublicConnection } from "@/lib/client/types"; const SS_REDIRECTED = "polyllm:onboarding-redirected"; const LS_DONE_PREFIX = "polyllm:onboarding-done:"; /** * First-run gate. Mounted by the chat empty state (and safe anywhere inside ). * * A user who has never completed onboarding and has no provider connected is sent to * `/app/onboarding` once per browser session; afterwards this renders a small "Finish setup" link * so nobody is trapped in a redirect loop. Completion is stored server-side * (`users.onboarding_completed_at`) by the onboarding page. */ export function Onboarding() { const { user } = useApp(); const router = useRouter(); const pathname = usePathname(); // Same SWR key as the store → deduped, but gives us `isLoading` so we never redirect on an empty first render. const providers = useApi<{ connections: PublicConnection[] }>("/api/providers"); const [redirected, setRedirected] = React.useState(null); const eligible = user.onboardingCompletedAt === null && !providers.isLoading && (providers.data?.connections.length ?? 0) === 0; React.useEffect(() => { if (!eligible || pathname.startsWith("/app/onboarding")) return; let doneLocally = false; let already = false; try { doneLocally = window.localStorage.getItem(LS_DONE_PREFIX + user.id) === "1"; already = window.sessionStorage.getItem(SS_REDIRECTED) === "1"; } catch { /* storage unavailable */ } if (doneLocally) return; if (already) { // eslint-disable-next-line react-hooks/set-state-in-effect setRedirected(true); return; } try { window.sessionStorage.setItem(SS_REDIRECTED, "1"); } catch { /* ignore */ } router.replace("/app/onboarding"); }, [eligible, pathname, router, user.id]); if (!eligible || !redirected) return null; return ( Finish setting up PolyLLM ); }