TypeScript 97.4%
SQL 1%
JavaScript 0.9%
CSS 0.6%
1"use client";2import * as React from "react";3import Link from "next/link";4import { usePathname, useRouter } from "next/navigation";5import { ArrowRight, Sparkles } from "lucide-react";6import { useApp } from "@/components/app/store";7import { useApi } from "@/lib/client/api";8import type { PublicConnection } from "@/lib/client/types";910const SS_REDIRECTED = "polyllm:onboarding-redirected";11const LS_DONE_PREFIX = "polyllm:onboarding-done:";1213/**14 * First-run gate. Mounted by the chat empty state (and safe anywhere inside <AppProvider>).15 *16 * A user who has never completed onboarding and has no provider connected is sent to17 * `/app/onboarding` once per browser session; afterwards this renders a small "Finish setup" link18 * so nobody is trapped in a redirect loop. Completion is stored server-side19 * (`users.onboarding_completed_at`) by the onboarding page.20 */21export function Onboarding() {22 const { user } = useApp();23 const router = useRouter();24 const pathname = usePathname();25 // Same SWR key as the store → deduped, but gives us `isLoading` so we never redirect on an empty first render.26 const providers = useApi<{ connections: PublicConnection[] }>("/api/providers");27 const [redirected, setRedirected] = React.useState<boolean | null>(null);2829 const eligible = user.onboardingCompletedAt === null && !providers.isLoading && (providers.data?.connections.length ?? 0) === 0;3031 React.useEffect(() => {32 if (!eligible || pathname.startsWith("/app/onboarding")) return;33 let doneLocally = false;34 let already = false;35 try {36 doneLocally = window.localStorage.getItem(LS_DONE_PREFIX + user.id) === "1";37 already = window.sessionStorage.getItem(SS_REDIRECTED) === "1";38 } catch {39 /* storage unavailable */40 }41 if (doneLocally) return;42 if (already) {43 // eslint-disable-next-line react-hooks/set-state-in-effect44 setRedirected(true);45 return;46 }47 try {48 window.sessionStorage.setItem(SS_REDIRECTED, "1");49 } catch {50 /* ignore */51 }52 router.replace("/app/onboarding");53 }, [eligible, pathname, router, user.id]);5455 if (!eligible || !redirected) return null;56 return (57 <Link href="/app/onboarding" className="mb-5 inline-flex min-h-[40px] items-center gap-2 rounded-full bg-accent-soft px-3.5 text-[13px] font-medium text-accent transition-colors hover:bg-accent/20">58 <Sparkles className="size-3.5" aria-hidden />59 Finish setting up PolyLLM60 <ArrowRight className="size-3.5" aria-hidden />61 </Link>62 );63}64