AI Status
Status component for Chrome AI APIs showing availability and download progress for Summarizer, Writer, Rewriter, and Language Detector models.
View the component below to see the availability status and download progress for Chrome AI APIs including Summarizer, Writer, Rewriter, and Language Detector models.
Loading preview...
"use client"; import { GencnUIStatus } from "@/registry/new-york/gencn-ui/items/llm-status/gencn-ui-status"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; export function GencnUIStatusExample() { return ( <Tabs defaultValue="popover" className="w-full"> <TabsList> <TabsTrigger value="popover">Popover</TabsTrigger> <TabsTrigger value="card">Card</TabsTrigger> <TabsTrigger value="compact">Compact</TabsTrigger> </TabsList> <TabsContent value="popover"> <GencnUIStatus variant="popover" /> </TabsContent> <TabsContent value="card"> <GencnUIStatus variant="card" /> </TabsContent> <TabsContent value="compact"> <GencnUIStatus variant="compact" /> </TabsContent> </Tabs> ); }
Server API
No server API required - this component uses client-side APIs only.
Installation
Setup Required: Make sure you have configured components.json first. See
the installation guide for setup instructions.
npx shadcn@latest add @gencn-ui/gencn-ui-status"use client"; import * as React from "react"; import { CheckCircle2, XCircle, Download, Loader2, AlertCircle, Zap, ChevronDown, } from "lucide-react"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Progress } from "@/components/ui/progress"; import { Spinner } from "@/components/ui/spinner"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { Button } from "@/components/ui/button"; import { Separator } from "@/components/ui/separator"; import { cn } from "@/lib/utils"; import { useRewriter } from "@/registry/new-york/gencn-ui/items/shared/hooks/internal/use-gencn-ui-rewriter"; import { useWriter } from "@/registry/new-york/gencn-ui/items/shared/hooks/internal/use-gencn-ui-writer"; import { useProofreader } from "@/registry/new-york/gencn-ui/items/shared/hooks/internal/use-gencn-ui-proofreader"; import { useLanguageModel } from "@/registry/new-york/gencn-ui/items/shared/hooks/internal/use-gencn-ui-language-model"; import { useLanguageDetector } from "@/registry/new-york/gencn-ui/items/shared/hooks/internal/use-gencn-ui-language-detector"; import { useSummarizer } from "@/registry/new-york/gencn-ui/items/shared/hooks/use-gencn-ui-summarizer"; import type { AvailabilityStatus } from "@/registry/new-york/gencn-ui/items/shared/gencn-ui-types"; import { pub, sub, type SummarizerDownloadPayload, type WriterDownloadPayload, type RewriterDownloadPayload, type ProofreaderDownloadPayload, type LanguageModelDownloadPayload, } from "@/registry/new-york/gencn-ui/items/shared/lib/gencn-ui-pubsub"; import type { SummarizerDownload, WriterDownload, RewriterDownload, ProofreaderDownload, LanguageModelDownload, } from "@/registry/new-york/gencn-ui/items/shared/gencn-ui-types"; export interface GencnUIStatusProps { /** * Custom className for the container */ className?: string; /** * Show detailed error messages */ showErrors?: boolean; /** * Show download progress */ showProgress?: boolean; /** * Display variant * - "card": Full card display (default) * - "popover": Icon button with popover * - "compact": Compact inline display */ variant?: "card" | "popover" | "compact"; } type StatusDisplay = { icon: React.ComponentType<{ className?: string }>; label: string; variant: "default" | "secondary" | "destructive"; color: string; }; function getStatusDisplay( isSupported: boolean | null, availability: AvailabilityStatus ): StatusDisplay { if (isSupported === null) { return { icon: Loader2, label: "Checking Support", variant: "secondary", color: "text-muted-foreground", }; } if (!isSupported) { return { icon: XCircle, label: "Not Supported", variant: "destructive", color: "text-destructive", }; } if (availability === null) { return { icon: AlertCircle, label: "Unknown", variant: "secondary", color: "text-muted-foreground", }; } if (availability === "unavailable") { return { icon: XCircle, label: "Unavailable", variant: "destructive", color: "text-destructive", }; } if (availability === "downloadable") { return { icon: Download, label: "Not Installed", variant: "default", color: "text-primary", }; } if (availability === "available") { return { icon: CheckCircle2, label: "Available", variant: "default", color: "text-green-600 dark:text-green-400", }; } return { icon: AlertCircle, label: "Unknown", variant: "secondary", color: "text-muted-foreground", }; } export function GencnUIStatus({ className, showErrors = true, showProgress = true, variant = "card", }: GencnUIStatusProps) { // Use hooks for all features const summarizer = useSummarizer(); const writer = useWriter(); const rewriter = useRewriter(); const proofreader = useProofreader(); const languageModel = useLanguageModel(); const languageDetector = useLanguageDetector(); // Local state for downloads (subscribed from events) const [summarizerDownload, setSummarizerDownload] = React.useState<SummarizerDownload | null>(null); const [writerDownload, setWriterDownload] = React.useState<WriterDownload | null>(null); const [rewriterDownload, setRewriterDownload] = React.useState<RewriterDownload | null>(null); const [proofreaderDownload, setProofreaderDownload] = React.useState<ProofreaderDownload | null>(null); const [languageModelDownload, setLanguageModelDownload] = React.useState<LanguageModelDownload | null>(null); // Subscribe to download events React.useEffect(() => { // Summarizer events const unsubSummarizerRegister = sub("summarizer:register", (event) => { const payload = event.detail as SummarizerDownloadPayload; setSummarizerDownload(payload.download); }); const unsubSummarizerUpdate = sub("summarizer:update", (event) => { const payload = event.detail as SummarizerDownloadPayload; setSummarizerDownload(payload.download); }); const unsubSummarizerCancel = sub("summarizer:cancel", () => { setSummarizerDownload(null); }); const unsubSummarizerDelete = sub("summarizer:delete", () => { setSummarizerDownload(null); }); // Writer events const unsubWriterRegister = sub("writer:register", (event) => { const payload = event.detail as WriterDownloadPayload; setWriterDownload(payload.download); }); const unsubWriterUpdate = sub("writer:update", (event) => { const payload = event.detail as WriterDownloadPayload; setWriterDownload(payload.download); }); const unsubWriterCancel = sub("writer:cancel", () => { setWriterDownload(null); }); const unsubWriterDelete = sub("writer:delete", () => { setWriterDownload(null); }); // Rewriter events const unsubRewriterRegister = sub("rewriter:register", (event) => { const payload = event.detail as RewriterDownloadPayload; setRewriterDownload(payload.download); }); const unsubRewriterUpdate = sub("rewriter:update", (event) => { const payload = event.detail as RewriterDownloadPayload; setRewriterDownload(payload.download); }); const unsubRewriterCancel = sub("rewriter:cancel", () => { setRewriterDownload(null); }); const unsubRewriterDelete = sub("rewriter:delete", () => { setRewriterDownload(null); }); // Proofreader events const unsubProofreaderRegister = sub("proofreader:register", (event) => { const payload = event.detail as ProofreaderDownloadPayload; setProofreaderDownload(payload.download); }); const unsubProofreaderUpdate = sub("proofreader:update", (event) => { const payload = event.detail as ProofreaderDownloadPayload; setProofreaderDownload(payload.download); }); const unsubProofreaderCancel = sub("proofreader:cancel", () => { setProofreaderDownload(null); }); const unsubProofreaderDelete = sub("proofreader:delete", () => { setProofreaderDownload(null); }); // LanguageModel events const unsubLanguageModelRegister = sub("languageModel:register", (event) => { const payload = event.detail as LanguageModelDownloadPayload; setLanguageModelDownload(payload.download); }); const unsubLanguageModelUpdate = sub("languageModel:update", (event) => { const payload = event.detail as LanguageModelDownloadPayload; setLanguageModelDownload(payload.download); }); const unsubLanguageModelCancel = sub("languageModel:cancel", () => { setLanguageModelDownload(null); }); const unsubLanguageModelDelete = sub("languageModel:delete", () => { setLanguageModelDownload(null); }); return () => { unsubSummarizerRegister(); unsubSummarizerUpdate(); unsubSummarizerCancel(); unsubSummarizerDelete(); unsubWriterRegister(); unsubWriterUpdate(); unsubWriterCancel(); unsubWriterDelete(); unsubRewriterRegister(); unsubRewriterUpdate(); unsubRewriterCancel(); unsubRewriterDelete(); unsubProofreaderRegister(); unsubProofreaderUpdate(); unsubProofreaderCancel(); unsubProofreaderDelete(); unsubLanguageModelRegister(); unsubLanguageModelUpdate(); unsubLanguageModelCancel(); unsubLanguageModelDelete(); }; }, []); // Download registration and start functions const registerSummarizerDownload = React.useCallback(() => { if (!summarizer.isSupported) { return; } if (summarizerDownload && (summarizerDownload.status === "downloadable" || summarizerDownload.status === "downloading")) { return; } pub("summarizer:register", { download: { status: "downloadable", progress: 0, }, }); }, [summarizer.isSupported, summarizerDownload]); const startSummarizerDownload = React.useCallback(async () => { if (summarizerDownload?.status === "downloadable" || summarizerDownload?.status === "error") { pub("summarizer:start", { download: summarizerDownload }); } }, [summarizerDownload]); const registerWriterDownload = React.useCallback(() => { if (!writer.isSupported) { return; } if (writerDownload && (writerDownload.status === "downloadable" || writerDownload.status === "downloading")) { return; } pub("writer:register", { download: { status: "downloadable", progress: 0, }, }); }, [writer.isSupported, writerDownload]); const startWriterDownload = React.useCallback(async () => { if (writerDownload?.status === "downloadable" || writerDownload?.status === "error") { pub("writer:start", { download: writerDownload }); } }, [writerDownload]); const registerRewriterDownload = React.useCallback(() => { if (!rewriter.isSupported) { return; } if (rewriterDownload && (rewriterDownload.status === "downloadable" || rewriterDownload.status === "downloading")) { return; } pub("rewriter:register", { download: { status: "downloadable", progress: 0, }, }); }, [rewriter.isSupported, rewriterDownload]); const startRewriterDownload = React.useCallback(async () => { if (rewriterDownload?.status === "downloadable" || rewriterDownload?.status === "error") { pub("rewriter:start", { download: rewriterDownload }); } }, [rewriterDownload]); const registerProofreaderDownload = React.useCallback(() => { if (!proofreader.isSupported) { return; } if (proofreaderDownload && (proofreaderDownload.status === "downloadable" || proofreaderDownload.status === "downloading")) { return; } pub("proofreader:register", { download: { status: "downloadable", progress: 0, }, }); }, [proofreader.isSupported, proofreaderDownload]); const startProofreaderDownload = React.useCallback(async () => { if (proofreaderDownload?.status === "downloadable" || proofreaderDownload?.status === "error") { pub("proofreader:start", { download: proofreaderDownload }); } }, [proofreaderDownload]); const registerLanguageModelDownload = React.useCallback(() => { if (!languageModel.isSupported) { return; } if (languageModelDownload && (languageModelDownload.status === "downloadable" || languageModelDownload.status === "downloading")) { return; } pub("languageModel:register", { download: { status: "downloadable", progress: 0, }, }); }, [languageModel.isSupported, languageModelDownload]); const startLanguageModelDownload = React.useCallback(async () => { if (languageModelDownload?.status === "downloadable" || languageModelDownload?.status === "error") { pub("languageModel:start", { download: languageModelDownload }); } }, [languageModelDownload]); // State for manual availability checks (only for hooks that support it) const [isCheckingLanguageDetector, setIsCheckingLanguageDetector] = React.useState(false); const [isCheckingLanguageModel, setIsCheckingLanguageModel] = React.useState(false); // Handlers for availability checks (only for hooks that expose checkAvailability) const handleCheckLanguageDetectorAvailability = React.useCallback(async () => { setIsCheckingLanguageDetector(true); try { await languageDetector.checkAvailability(); } finally { setIsCheckingLanguageDetector(false); } }, [languageDetector]); const handleCheckLanguageModelAvailability = React.useCallback(async () => { setIsCheckingLanguageModel(true); try { await languageModel.checkAvailability(); } finally { setIsCheckingLanguageModel(false); } }, [languageModel]); // Download handlers const handleDownloadSummarizer = React.useCallback(async () => { try { if (!summarizerDownload) { registerSummarizerDownload(); setTimeout(() => { startSummarizerDownload(); }, 0); } else if ( summarizerDownload.status === "downloadable" || summarizerDownload.status === "error" ) { await startSummarizerDownload(); } } catch (err) { console.error("[GenUI Status] Error downloading Summarizer:", err); } }, [summarizerDownload, registerSummarizerDownload, startSummarizerDownload]); const handleDownloadWriter = React.useCallback(async () => { try { if (!writerDownload) { registerWriterDownload(); setTimeout(() => { startWriterDownload(); }, 0); } else if ( writerDownload.status === "downloadable" || writerDownload.status === "error" ) { await startWriterDownload(); } } catch (err) { console.error("[GenUI Status] Error downloading Writer:", err); } }, [writerDownload, registerWriterDownload, startWriterDownload]); const handleDownloadRewriter = React.useCallback(async () => { try { if (!rewriterDownload) { registerRewriterDownload(); setTimeout(() => { startRewriterDownload(); }, 0); } else if ( rewriterDownload.status === "downloadable" || rewriterDownload.status === "error" ) { await startRewriterDownload(); } } catch (err) { console.error("[GenUI Status] Error downloading Rewriter:", err); } }, [rewriterDownload, registerRewriterDownload, startRewriterDownload]); const handleDownloadLanguageDetector = React.useCallback(async () => { try { await languageDetector.checkAvailability(); } catch (err) { console.error("[GenUI Status] Error checking LanguageDetector:", err); } }, [languageDetector]); const handleDownloadProofreader = React.useCallback(async () => { try { if (!proofreaderDownload) { registerProofreaderDownload(); setTimeout(() => { startProofreaderDownload(); }, 0); } else if ( proofreaderDownload.status === "downloadable" || proofreaderDownload.status === "error" ) { await startProofreaderDownload(); } } catch (err) { console.error("[GenUI Status] Error downloading Proofreader:", err); } }, [ proofreaderDownload, registerProofreaderDownload, startProofreaderDownload, ]); const handleDownloadLanguageModel = React.useCallback(async () => { try { if (!languageModelDownload) { registerLanguageModelDownload(); setTimeout(() => { startLanguageModelDownload(); }, 0); } else if ( languageModelDownload.status === "downloadable" || languageModelDownload.status === "error" ) { await startLanguageModelDownload(); } // Re-check availability after download setTimeout(async () => { await languageModel.checkAvailability(); }, 2000); } catch (err) { console.error("[GenUI Status] Error downloading LanguageModel:", err); } }, [ languageModelDownload, registerLanguageModelDownload, startLanguageModelDownload, languageModel, ]); // Get status displays const summarizerStatus = getStatusDisplay( summarizer.isSupported, summarizer.availability ); const writerStatus = getStatusDisplay(writer.isSupported, writer.availability); const rewriterStatus = getStatusDisplay( rewriter.isSupported, rewriter.availability ); const proofreaderStatus = getStatusDisplay( proofreader.isSupported, proofreader.availability ); const languageModelStatus = getStatusDisplay( languageModel.isSupported, languageModel.availability ); const languageDetectorStatus = getStatusDisplay( languageDetector.isSupported, languageDetector.availability ); // Icon components const SummarizerIcon = summarizerStatus.icon; const WriterIcon = writerStatus.icon; const RewriterIcon = rewriterStatus.icon; const ProofreaderIcon = proofreaderStatus.icon; const LanguageModelIcon = languageModelStatus.icon; const LanguageDetectorIcon = languageDetectorStatus.icon; // Overall status calculation const overallSupported = summarizer.isSupported === true || writer.isSupported === true || rewriter.isSupported === true || proofreader.isSupported === true || languageModel.isSupported === true || languageDetector.isSupported === true; const overallAvailable = summarizer.availability === "available" || writer.availability === "available" || rewriter.availability === "available" || proofreader.availability === "available" || languageModel.availability === "available" || languageDetector.availability === "available"; const overallDownloading = summarizerDownload?.status === "downloading" || writerDownload?.status === "downloading" || rewriterDownload?.status === "downloading" || proofreaderDownload?.status === "downloading" || languageModelDownload?.status === "downloading"; const overallStatus: StatusDisplay = overallAvailable ? { icon: CheckCircle2, label: "Available", variant: "default", color: "text-green-600 dark:text-green-400", } : overallDownloading ? { icon: Loader2, label: "Downloading", variant: "default", color: "text-primary", } : overallSupported ? { icon: AlertCircle, label: "Checking", variant: "secondary", color: "text-muted-foreground", } : { icon: XCircle, label: "Not Supported", variant: "destructive", color: "text-destructive", }; const OverallStatusIcon = overallStatus.icon; const isAnyChecking = summarizer.isSupported === null || writer.isSupported === null || rewriter.isSupported === null || proofreader.isSupported === null || languageModel.isSupported === null || languageDetector.isSupported === null; // Helper to render status row in compact/popover view const renderStatusRow = ( label: string, isSupported: boolean | null, availability: AvailabilityStatus, statusDisplay: StatusDisplay, Icon: React.ComponentType<{ className?: string }>, downloadStatus?: { status: string; progress: number } | null, onDownload?: () => void, onCheck?: () => void, isChecking?: boolean ) => ( <> <div className="flex flex-col gap-1"> <div className="flex items-center gap-2"> <span className="text-sm font-medium">{label}:</span> <div className="ml-auto"> {downloadStatus?.status === "downloading" && showProgress ? ( <div className="flex items-center gap-2"> <span className="text-muted-foreground text-xs"> {Math.round(downloadStatus.progress)}% </span> <Progress value={downloadStatus.progress} className="h-1.5 w-20" /> </div> ) : isSupported && availability === "downloadable" && downloadStatus?.status !== "downloading" ? ( <Button onClick={onDownload} variant="outline" size="sm" className="h-7 text-xs" > <Download className="mr-1.5 size-3" /> Download </Button> ) : isSupported && availability === "available" ? ( <CheckCircle2 className="size-4 text-green-600 dark:text-green-400" /> ) : !isSupported || availability === "unavailable" ? ( <XCircle className="text-destructive size-4" /> ) : isSupported === null ? ( <Loader2 className="text-muted-foreground size-4 animate-spin" /> ) : availability === null && onCheck ? ( <Button onClick={onCheck} disabled={isChecking} variant="ghost" size="sm" className="h-7 text-xs" > {isChecking ? ( <Spinner className="mr-1.5 size-3" /> ) : ( <AlertCircle className="mr-1.5 size-3" /> )} Check </Button> ) : ( <AlertCircle className="text-muted-foreground size-4" /> )} </div> </div> <span className="text-xs font-medium">{statusDisplay.label}</span> </div> <Separator /> </> ); // Reusable status list content const renderStatusList = () => ( <div className="flex flex-col gap-2"> {renderStatusRow( "Summarizer", summarizer.isSupported, summarizer.availability, summarizerStatus, SummarizerIcon, summarizerDownload, handleDownloadSummarizer )} {renderStatusRow( "Writer", writer.isSupported, writer.availability, writerStatus, WriterIcon, writerDownload, handleDownloadWriter )} {renderStatusRow( "Rewriter", rewriter.isSupported, rewriter.availability, rewriterStatus, RewriterIcon, rewriterDownload, handleDownloadRewriter )} {renderStatusRow( "Language Detector", languageDetector.isSupported, languageDetector.availability, languageDetectorStatus, LanguageDetectorIcon, undefined, handleDownloadLanguageDetector, handleCheckLanguageDetectorAvailability, isCheckingLanguageDetector )} {renderStatusRow( "Proofreader", proofreader.isSupported, proofreader.availability, proofreaderStatus, ProofreaderIcon, proofreaderDownload, handleDownloadProofreader )} {renderStatusRow( "Prompt API", languageModel.isSupported, languageModel.availability, languageModelStatus, LanguageModelIcon, languageModelDownload, handleDownloadLanguageModel, handleCheckLanguageModelAvailability, isCheckingLanguageModel )} </div> ); // Popover variant if (variant === "popover") { return ( <Popover> <PopoverTrigger asChild> <Button variant="outline" size="sm" className={cn("relative justify-start gap-2", className)} aria-label="Chrome AI Status" > {overallDownloading ? ( <Spinner className="size-4 shrink-0" /> ) : ( <OverallStatusIcon className={cn( "size-4 shrink-0", overallStatus.color, isAnyChecking && "animate-spin" )} /> )} <span className="text-foreground whitespace-nowrap"> Chrome AI Status </span> {overallDownloading && ( <span className="bg-primary absolute -top-1 -right-1 size-2 animate-pulse rounded-full" /> )} <ChevronDown className="ml-auto size-4 shrink-0" /> </Button> </PopoverTrigger> <PopoverContent align="end">{renderStatusList()}</PopoverContent> </Popover> ); } // Compact variant if (variant === "compact") { return ( <div className={cn("text-foreground flex flex-col gap-2", className)}> {renderStatusList()} </div> ); } // Card variant (default) const renderSupport = (supported: boolean | null) => { if (supported === null) { return ( <div className="flex items-center"> <Spinner className="size-3" /> </div> ); } return supported ? ( <div className="flex items-center"> <CheckCircle2 className="size-3.5 text-green-600 dark:text-green-400" /> </div> ) : ( <div className="flex items-center"> <XCircle className="text-destructive size-3.5" /> </div> ); }; const renderAvailability = (avail: AvailabilityStatus) => { if (avail === null) { return ( <div className="flex items-center gap-1.5"> <Spinner className="size-3" /> <span className="text-muted-foreground text-xs">Unknown</span> </div> ); } if (avail === "available") { return ( <div className="flex items-center gap-1.5"> <span className="text-xs text-green-600 dark:text-green-400"> Available </span> </div> ); } if (avail === "downloadable") { return ( <div className="flex items-center gap-1.5"> <span className="text-primary text-xs">Not Installed</span> </div> ); } return ( <div className="flex items-center gap-1.5"> <XCircle className="text-destructive size-3.5" /> <span className="text-destructive text-xs">Unavailable</span> </div> ); }; const renderModelRow = ( name: string, Icon: React.ComponentType<{ className?: string }>, statusColor: string, isSupported: boolean | null, availability: AvailabilityStatus, downloadStatus?: { status: string; progress: number } | null, onDownload?: () => void, onCheck?: () => void, isChecking?: boolean ) => ( <div className="grid grid-cols-3 items-center gap-4 border-b py-3"> <div className="flex items-center gap-2"> <Icon className={cn("size-4", statusColor)} /> <span className="text-sm font-medium">{name}</span> </div> <div className="flex items-center gap-3"> {renderSupport(isSupported)} {isSupported ? renderAvailability(availability) : <span className="text-muted-foreground text-xs">ā</span>} </div> <div> {downloadStatus?.status === "downloading" && showProgress ? ( <div className="flex items-center gap-2"> <Progress value={downloadStatus.progress} className="h-1.5 flex-1" /> <span className="text-muted-foreground w-10 text-xs"> {Math.round(downloadStatus.progress)}% </span> </div> ) : isSupported && availability === "downloadable" && downloadStatus?.status !== "downloading" ? ( <Button onClick={onDownload} variant="outline" size="sm" className="h-7 text-xs" > <Download className="mr-1.5 size-3" /> Download </Button> ) : isSupported && availability === null && onCheck ? ( <Button onClick={onCheck} disabled={isChecking} variant="ghost" size="sm" className="h-7 text-xs" > {isChecking ? ( <Spinner className="mr-1.5 size-3" /> ) : ( <AlertCircle className="mr-1.5 size-3" /> )} Check </Button> ) : null} </div> </div> ); return ( <Card className={className}> <CardHeader> <div className="flex items-center justify-between"> <div className="flex items-center gap-2"> <Zap className="text-primary size-5" /> <CardTitle>Chrome AI Status</CardTitle> </div> <Badge variant={overallStatus.variant} className="gap-1.5"> <OverallStatusIcon className={cn( "size-3", overallStatus.color, isAnyChecking && "animate-spin" )} /> {overallStatus.label} </Badge> </div> <CardDescription> Status of Chrome AI APIs (Summarizer, Writer, Rewriter, Proofreader, Language Detector & Prompt API) availability and model downloads </CardDescription> </CardHeader> <CardContent> <div className="overflow-x-auto"> <div className="min-w-[500px]"> <div className="mb-2 grid grid-cols-3 gap-4 border-b pb-3 text-sm font-semibold"> <div>Model</div> <div>Support & Availability</div> <div>Status</div> </div> {renderModelRow( "Summarizer", SummarizerIcon, summarizerStatus.color, summarizer.isSupported, summarizer.availability, summarizerDownload, handleDownloadSummarizer )} {renderModelRow( "Writer", WriterIcon, writerStatus.color, writer.isSupported, writer.availability, writerDownload, handleDownloadWriter )} {renderModelRow( "Rewriter", RewriterIcon, rewriterStatus.color, rewriter.isSupported, rewriter.availability, rewriterDownload, handleDownloadRewriter )} {renderModelRow( "Language Detector", LanguageDetectorIcon, languageDetectorStatus.color, languageDetector.isSupported, languageDetector.availability, undefined, handleDownloadLanguageDetector, handleCheckLanguageDetectorAvailability, isCheckingLanguageDetector )} {renderModelRow( "Proofreader", ProofreaderIcon, proofreaderStatus.color, proofreader.isSupported, proofreader.availability, proofreaderDownload, handleDownloadProofreader )} {renderModelRow( "Prompt API", LanguageModelIcon, languageModelStatus.color, languageModel.isSupported, languageModel.availability, languageModelDownload, handleDownloadLanguageModel, handleCheckLanguageModelAvailability, isCheckingLanguageModel )} </div> </div> {languageModel.error && showErrors && ( <Alert variant="destructive" className="mt-4"> <AlertCircle className="size-4" /> <AlertTitle>Prompt API Error</AlertTitle> <AlertDescription className="text-xs"> {languageModel.error} </AlertDescription> </Alert> )} </CardContent> </Card> ); }

