TypeScript 97.4%
SQL 1%
JavaScript 0.9%
CSS 0.6%
1"use client";2import * as React from "react";3import { Download, Share, SquarePlus, X } from "lucide-react";4import { Button } from "@/components/ui/button";5import { useLocalStorage, useMounted, useStandalone } from "@/lib/client/hooks";6import { cn } from "@/lib/utils";78interface BeforeInstallPromptEvent extends Event {9 prompt: () => Promise<void>;10 userChoice: Promise<{ outcome: "accepted" | "dismissed" }>;11}1213function isIOS() {14 if (typeof navigator === "undefined") return false;15 const ua = navigator.userAgent;16 return /iPhone|iPad|iPod/.test(ua) || (ua.includes("Mac") && "ontouchend" in document);17}1819/**20 * "Install PolyLLM" hint. Uses `beforeinstallprompt` where the browser supports it (Chromium,21 * Android) and shows the Share → Add to Home Screen tip on iOS Safari. Hidden when already22 * installed (standalone) or after the visitor dismisses it. Marketing only — Settings → Appearance23 * has its own hint.24 */25export function InstallHint({ className, compact }: { className?: string; compact?: boolean }) {26 const mounted = useMounted();27 const standalone = useStandalone();28 const [dismissed, setDismissed] = useLocalStorage<boolean>("polyllm:install-hint-dismissed", false);29 const [deferred, setDeferred] = React.useState<BeforeInstallPromptEvent | null>(null);30 const [installed, setInstalled] = React.useState(false);31 const [ios, setIos] = React.useState(false);3233 React.useEffect(() => {34 // eslint-disable-next-line react-hooks/set-state-in-effect35 setIos(isIOS());36 const onPrompt = (e: Event) => {37 e.preventDefault();38 setDeferred(e as BeforeInstallPromptEvent);39 };40 const onInstalled = () => setInstalled(true);41 window.addEventListener("beforeinstallprompt", onPrompt);42 window.addEventListener("appinstalled", onInstalled);43 return () => {44 window.removeEventListener("beforeinstallprompt", onPrompt);45 window.removeEventListener("appinstalled", onInstalled);46 };47 }, []);4849 if (!mounted || standalone || dismissed || installed) return null;50 if (!deferred && !ios) return null;5152 const install = async () => {53 if (!deferred) return;54 await deferred.prompt();55 const { outcome } = await deferred.userChoice;56 if (outcome === "accepted") setInstalled(true);57 setDeferred(null);58 };5960 return (61 <div role="note" className={cn("relative flex items-start gap-3 rounded-2xl bg-bg-elevated p-4 shadow-xs ring-1 ring-border", compact && "p-3", className)}>62 <span className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-accent-soft text-accent">{deferred ? <Download className="size-4.5" /> : <SquarePlus className="size-4.5" />}</span>63 <div className="min-w-0 flex-1">64 <p className="text-[14px] font-semibold tracking-tight">Install PolyLLM</p>65 {deferred ? (66 <p className="mt-0.5 text-[13px] leading-5 text-fg-muted">Add it to your home screen or dock: it opens full-screen, remembers your session and feels like a native app.</p>67 ) : (68 <p className="mt-0.5 text-[13px] leading-5 text-fg-muted">69 In Safari, tap <Share className="mx-0.5 inline size-3.5 -translate-y-px" aria-label="Share" /> <span className="font-medium text-fg">Share</span>, then <span className="font-medium text-fg">Add to Home Screen</span>. PolyLLM opens full-screen with its own icon.70 </p>71 )}72 {deferred ? (73 <Button size="sm" className="mt-2.5 h-9 sm:h-8" onClick={install}>74 <Download /> Install75 </Button>76 ) : null}77 </div>78 <button type="button" onClick={() => setDismissed(true)} className="tap -mr-1 -mt-1 rounded-md p-1.5 text-fg-subtle transition-colors hover:bg-bg-muted hover:text-fg" aria-label="Dismiss install hint">79 <X className="size-4" />80 </button>81 </div>82 );83}84