GenUI Status
Status component for Chrome AI APIs showing availability and download progress for Summarizer, Writer, Rewriter, and Language Detector models.
Preview
Loading preview...
Installation
npx shadcn@latest add https://gencn-ui.encatch.com/r/genui-status.json'use client'; import * as React from 'react'; import { CheckCircle2, XCircle, Download, Loader2, AlertCircle, Zap } 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 { cn } from '@/lib/utils'; import { useGenUI } from '@/registry/new-york/gencn-ui/items/shared/genui-provider'; export interface GenUIStatusProps { /** * 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'; } export function GenUIStatus({ className, showErrors = true, showProgress = true, variant = 'card', }: GenUIStatusProps) { const { isSupported, isWriterSupported, isRewriterSupported, isLanguageDetectorSupported, availability, writerAvailability, rewriterAvailability, languageDetectorAvailability, downloadProgress, writerDownloadProgress, rewriterDownloadProgress, languageDetectorDownloadProgress, isDownloading, isWriterDownloading, isRewriterDownloading, isLanguageDetectorDownloading, error, checkAvailability, checkWriterAvailability, checkRewriterAvailability, checkLanguageDetectorAvailability, resetError, } = useGenUI(); const [isChecking, setIsChecking] = React.useState(false); const [isCheckingWriter, setIsCheckingWriter] = React.useState(false); const [isCheckingRewriter, setIsCheckingRewriter] = React.useState(false); const [isCheckingLanguageDetector, setIsCheckingLanguageDetector] = React.useState(false); const handleCheckAvailability = React.useCallback(async () => { setIsChecking(true); try { await checkAvailability(); } finally { setIsChecking(false); } }, [checkAvailability]); const handleCheckWriterAvailability = React.useCallback(async () => { setIsCheckingWriter(true); try { await checkWriterAvailability(); } finally { setIsCheckingWriter(false); } }, [checkWriterAvailability]); const handleCheckRewriterAvailability = React.useCallback(async () => { setIsCheckingRewriter(true); try { await checkRewriterAvailability(); } finally { setIsCheckingRewriter(false); } }, [checkRewriterAvailability]); const handleCheckLanguageDetectorAvailability = React.useCallback(async () => { setIsCheckingLanguageDetector(true); try { await checkLanguageDetectorAvailability(); } finally { setIsCheckingLanguageDetector(false); } }, [checkLanguageDetectorAvailability]); // Determine status display for Summarizer const getSummarizerStatusDisplay = () => { if (isSupported === null) { return { icon: Loader2, label: 'Checking Support', variant: 'secondary' as const, color: 'text-muted-foreground', }; } if (!isSupported) { return { icon: XCircle, label: 'Not Supported', variant: 'destructive' as const, color: 'text-destructive', }; } if (availability === null) { return { icon: AlertCircle, label: 'Unknown', variant: 'secondary' as const, color: 'text-muted-foreground', }; } if (availability === 'unavailable') { return { icon: XCircle, label: 'Unavailable', variant: 'destructive' as const, color: 'text-destructive', }; } if (availability === 'downloadable') { return { icon: Download, label: 'Downloading', variant: 'default' as const, color: 'text-primary', }; } if (availability === 'available') { return { icon: CheckCircle2, label: 'Available', variant: 'default' as const, color: 'text-green-600 dark:text-green-400', }; } return { icon: AlertCircle, label: 'Unknown', variant: 'secondary' as const, color: 'text-muted-foreground', }; }; // Determine status display for Writer const getWriterStatusDisplay = () => { if (isWriterSupported === null) { return { icon: Loader2, label: 'Checking Support', variant: 'secondary' as const, color: 'text-muted-foreground', }; } if (!isWriterSupported) { return { icon: XCircle, label: 'Not Supported', variant: 'destructive' as const, color: 'text-destructive', }; } if (writerAvailability === null) { return { icon: AlertCircle, label: 'Unknown', variant: 'secondary' as const, color: 'text-muted-foreground', }; } if (writerAvailability === 'unavailable') { return { icon: XCircle, label: 'Unavailable', variant: 'destructive' as const, color: 'text-destructive', }; } if (writerAvailability === 'downloadable') { return { icon: Download, label: 'Downloading', variant: 'default' as const, color: 'text-primary', }; } if (writerAvailability === 'available') { return { icon: CheckCircle2, label: 'Available', variant: 'default' as const, color: 'text-green-600 dark:text-green-400', }; } return { icon: AlertCircle, label: 'Unknown', variant: 'secondary' as const, color: 'text-muted-foreground', }; }; // Determine status display for Language Detector const getLanguageDetectorStatusDisplay = () => { if (isLanguageDetectorSupported === null) { return { icon: Loader2, label: 'Checking Support', variant: 'secondary' as const, color: 'text-muted-foreground', }; } if (!isLanguageDetectorSupported) { return { icon: XCircle, label: 'Not Supported', variant: 'destructive' as const, color: 'text-destructive', }; } if (languageDetectorAvailability === null) { return { icon: AlertCircle, label: 'Unknown', variant: 'secondary' as const, color: 'text-muted-foreground', }; } if (languageDetectorAvailability === 'unavailable') { return { icon: XCircle, label: 'Unavailable', variant: 'destructive' as const, color: 'text-destructive', }; } if (languageDetectorAvailability === 'downloadable') { return { icon: Download, label: 'Downloading', variant: 'default' as const, color: 'text-primary', }; } if (languageDetectorAvailability === 'available') { return { icon: CheckCircle2, label: 'Available', variant: 'default' as const, color: 'text-green-600 dark:text-green-400', }; } return { icon: AlertCircle, label: 'Unknown', variant: 'secondary' as const, color: 'text-muted-foreground', }; }; const summarizerStatus = getSummarizerStatusDisplay(); const writerStatus = getWriterStatusDisplay(); const getRewriterStatusDisplay = () => { if (isRewriterSupported === null) { return { icon: Loader2, label: 'Checking Support', variant: 'secondary' as const, color: 'text-muted-foreground', }; } if (!isRewriterSupported) { return { icon: XCircle, label: 'Not Supported', variant: 'destructive' as const, color: 'text-destructive', }; } if (rewriterAvailability === null) { return { icon: AlertCircle, label: 'Unknown', variant: 'secondary' as const, color: 'text-muted-foreground', }; } if (rewriterAvailability === 'unavailable') { return { icon: XCircle, label: 'Unavailable', variant: 'destructive' as const, color: 'text-destructive', }; } if (rewriterAvailability === 'downloadable') { return { icon: Download, label: 'Downloading', variant: 'default' as const, color: 'text-primary', }; } if (rewriterAvailability === 'available') { return { icon: CheckCircle2, label: 'Available', variant: 'default' as const, color: 'text-green-600 dark:text-green-400', }; } return { icon: AlertCircle, label: 'Unknown', variant: 'secondary' as const, color: 'text-muted-foreground', }; }; const rewriterStatus = getRewriterStatusDisplay(); const RewriterIcon = rewriterStatus.icon; const languageDetectorStatus = getLanguageDetectorStatusDisplay(); const SummarizerIcon = summarizerStatus.icon; const WriterIcon = writerStatus.icon; const LanguageDetectorIcon = languageDetectorStatus.icon; // Overall status (at least one is supported/available) const overallSupported = isSupported === true || isWriterSupported === true || isRewriterSupported === true || isLanguageDetectorSupported === true; const overallAvailable = availability === 'available' || writerAvailability === 'available' || rewriterAvailability === 'available' || languageDetectorAvailability === 'available'; const overallDownloading = isDownloading || isWriterDownloading || isRewriterDownloading || isLanguageDetectorDownloading; const overallStatus = overallAvailable ? { icon: CheckCircle2, label: 'Available', variant: 'default' as const, color: 'text-green-600 dark:text-green-400' } : overallDownloading ? { icon: Download, label: 'Downloading', variant: 'default' as const, color: 'text-primary' } : overallSupported ? { icon: AlertCircle, label: 'Checking', variant: 'secondary' as const, color: 'text-muted-foreground' } : { icon: XCircle, label: 'Not Supported', variant: 'destructive' as const, color: 'text-destructive' }; const OverallStatusIcon = overallStatus.icon; // Popover variant - Icon button with popover content if (variant === 'popover') { return ( <Popover> <PopoverTrigger asChild> <Button variant="outline" size="sm" className={cn('relative gap-2', className)} aria-label="Chrome AI Status" > <OverallStatusIcon className={cn( 'size-4 shrink-0', overallStatus.color, (isSupported === null || isWriterSupported === null || isLanguageDetectorSupported === null) && 'animate-spin' )} /> <span className="whitespace-nowrap">AI Status</span> {overallDownloading && ( <span className="absolute -top-1 -right-1 size-2 bg-primary rounded-full animate-pulse" /> )} </Button> </PopoverTrigger> <PopoverContent className="w-80" align="end"> <div className="space-y-4"> {/* Header */} <div className="flex items-center gap-2"> <Zap className="size-4 text-primary" /> <h3 className="font-semibold text-sm">Chrome AI Status</h3> </div> {/* Status Summary */} <div className="space-y-3"> {/* Summarizer Status */} <div className="space-y-2 border-b pb-3"> <div className="flex items-center justify-between text-sm font-medium"> <span>Summarizer Model</span> <Badge variant={summarizerStatus.variant} className="gap-1.5"> <SummarizerIcon className={cn( 'size-3', summarizerStatus.color, isSupported === null && 'animate-spin' )} /> {summarizerStatus.label} </Badge> </div> {isSupported && ( <div className="flex items-center justify-between text-xs text-muted-foreground ml-1"> <span>Support</span> <div className="flex items-center gap-1.5"> {isSupported === null ? ( <> <Spinner className="size-3" /> <span>Checking...</span> </> ) : isSupported ? ( <> <CheckCircle2 className="size-3 text-green-600 dark:text-green-400" /> <span>Supported</span> </> ) : ( <> <XCircle className="size-3 text-destructive" /> <span>Not Supported</span> </> )} </div> </div> )} {isDownloading && showProgress && ( <div className="space-y-1.5 ml-1"> <div className="flex items-center justify-between text-xs"> <span className="text-muted-foreground">Download</span> <span className="font-medium">{Math.round(downloadProgress)}%</span> </div> <Progress value={downloadProgress} className="h-1.5" /> </div> )} {isSupported && availability === null && ( <Button onClick={handleCheckAvailability} disabled={isChecking} variant="ghost" size="sm" className="h-7 text-xs w-full mt-1" > {isChecking ? ( <> <Spinner className="size-3 mr-1.5" /> Checking... </> ) : ( <> <AlertCircle className="size-3 mr-1.5" /> Check Availability </> )} </Button> )} </div> {/* Rewriter Status */} <div className="space-y-2 border-b pb-3"> <div className="flex items-center justify-between text-sm font-medium"> <span>Rewriter Model</span> <Badge variant={rewriterStatus.variant} className="gap-1.5"> <RewriterIcon className={cn( 'size-3', rewriterStatus.color, isRewriterSupported === null && 'animate-spin' )} /> {rewriterStatus.label} </Badge> </div> {isRewriterSupported !== null && ( <div className="flex items-center justify-between text-xs text-muted-foreground ml-1"> <span>Support</span> <div className="flex items-center gap-1.5"> {isRewriterSupported === null ? ( <> <Spinner className="size-3" /> <span>Checking...</span> </> ) : isRewriterSupported ? ( <> <CheckCircle2 className="size-3 text-green-600 dark:text-green-400" /> <span>Supported</span> </> ) : ( <> <XCircle className="size-3 text-destructive" /> <span>Not Supported</span> </> )} </div> </div> )} {isRewriterDownloading && showProgress && ( <div className="space-y-1.5 ml-1"> <div className="flex items-center justify-between text-xs"> <span className="text-muted-foreground">Download</span> <span className="font-medium">{Math.round(rewriterDownloadProgress)}%</span> </div> <Progress value={rewriterDownloadProgress} className="h-1.5" /> </div> )} {isRewriterSupported && rewriterAvailability === null && ( <Button onClick={handleCheckRewriterAvailability} disabled={isCheckingRewriter} variant="ghost" size="sm" className="h-7 text-xs w-full mt-1" > {isCheckingRewriter ? ( <> <Spinner className="size-3 mr-1.5" /> Checking... </> ) : ( <> <AlertCircle className="size-3 mr-1.5" /> Check Availability </> )} </Button> )} </div> {/* Writer Status */} <div className="space-y-2 border-b pb-3"> <div className="flex items-center justify-between text-sm font-medium"> <span>Writer Model</span> <Badge variant={writerStatus.variant} className="gap-1.5"> <WriterIcon className={cn( 'size-3', writerStatus.color, isWriterSupported === null && 'animate-spin' )} /> {writerStatus.label} </Badge> </div> {isWriterSupported !== null && ( <div className="flex items-center justify-between text-xs text-muted-foreground ml-1"> <span>Support</span> <div className="flex items-center gap-1.5"> {isWriterSupported === null ? ( <> <Spinner className="size-3" /> <span>Checking...</span> </> ) : isWriterSupported ? ( <> <CheckCircle2 className="size-3 text-green-600 dark:text-green-400" /> <span>Supported</span> </> ) : ( <> <XCircle className="size-3 text-destructive" /> <span>Not Supported</span> </> )} </div> </div> )} {isWriterDownloading && showProgress && ( <div className="space-y-1.5 ml-1"> <div className="flex items-center justify-between text-xs"> <span className="text-muted-foreground">Download</span> <span className="font-medium">{Math.round(writerDownloadProgress)}%</span> </div> <Progress value={writerDownloadProgress} className="h-1.5" /> </div> )} {isWriterSupported && writerAvailability === null && ( <Button onClick={handleCheckWriterAvailability} disabled={isCheckingWriter} variant="ghost" size="sm" className="h-7 text-xs w-full mt-1" > {isCheckingWriter ? ( <> <Spinner className="size-3 mr-1.5" /> Checking... </> ) : ( <> <AlertCircle className="size-3 mr-1.5" /> Check Availability </> )} </Button> )} </div> {/* Language Detector Status */} <div className="space-y-2"> <div className="flex items-center justify-between text-sm font-medium"> <span>Language Detector</span> <Badge variant={languageDetectorStatus.variant} className="gap-1.5"> <LanguageDetectorIcon className={cn( 'size-3', languageDetectorStatus.color, isLanguageDetectorSupported === null && 'animate-spin' )} /> {languageDetectorStatus.label} </Badge> </div> {isLanguageDetectorSupported !== null && ( <div className="flex items-center justify-between text-xs text-muted-foreground ml-1"> <span>Support</span> <div className="flex items-center gap-1.5"> {isLanguageDetectorSupported === null ? ( <> <Spinner className="size-3" /> <span>Checking...</span> </> ) : isLanguageDetectorSupported ? ( <> <CheckCircle2 className="size-3 text-green-600 dark:text-green-400" /> <span>Supported</span> </> ) : ( <> <XCircle className="size-3 text-destructive" /> <span>Not Supported</span> </> )} </div> </div> )} {isLanguageDetectorDownloading && showProgress && ( <div className="space-y-1.5 ml-1"> <div className="flex items-center justify-between text-xs"> <span className="text-muted-foreground">Download</span> <span className="font-medium">{Math.round(languageDetectorDownloadProgress)}%</span> </div> <Progress value={languageDetectorDownloadProgress} className="h-1.5" /> </div> )} {isLanguageDetectorSupported && languageDetectorAvailability === null && ( <Button onClick={handleCheckLanguageDetectorAvailability} disabled={isCheckingLanguageDetector} variant="ghost" size="sm" className="h-7 text-xs w-full mt-1" > {isCheckingLanguageDetector ? ( <> <Spinner className="size-3 mr-1.5" /> Checking... </> ) : ( <> <AlertCircle className="size-3 mr-1.5" /> Check Availability </> )} </Button> )} </div> </div> {/* Error Display */} {error && showErrors && ( <Alert variant="destructive" className="py-2"> <AlertCircle className="size-4" /> <AlertTitle className="text-xs">Error</AlertTitle> <AlertDescription className="text-xs flex items-center justify-between"> <span className="line-clamp-2">{error}</span> <button onClick={resetError} className="text-xs underline hover:no-underline ml-2 shrink-0" > Dismiss </button> </AlertDescription> </Alert> )} </div> </PopoverContent> </Popover> ); } // Compact variant if (variant === 'compact') { return ( <div className={cn('flex flex-col gap-2', className)}> <div className="flex items-center gap-2"> <SummarizerIcon className={cn('size-4', summarizerStatus.color, isSupported === null && 'animate-spin')} /> <span className="text-sm font-medium">Summarizer: {summarizerStatus.label}</span> {isDownloading && showProgress && ( <div className="flex items-center gap-2 ml-auto"> <span className="text-xs text-muted-foreground">{Math.round(downloadProgress)}%</span> <Progress value={downloadProgress} className="w-20 h-1.5" /> </div> )} </div> <div className="flex items-center gap-2"> <WriterIcon className={cn('size-4', writerStatus.color, isWriterSupported === null && 'animate-spin')} /> <span className="text-sm font-medium">Writer: {writerStatus.label}</span> {isWriterDownloading && showProgress && ( <div className="flex items-center gap-2 ml-auto"> <span className="text-xs text-muted-foreground">{Math.round(writerDownloadProgress)}%</span> <Progress value={writerDownloadProgress} className="w-20 h-1.5" /> </div> )} </div> <div className="flex items-center gap-2"> <RewriterIcon className={cn('size-4', rewriterStatus.color, isRewriterSupported === null && 'animate-spin')} /> <span className="text-sm font-medium">Rewriter: {rewriterStatus.label}</span> {isRewriterDownloading && showProgress && ( <div className="flex items-center gap-2 ml-auto"> <span className="text-xs text-muted-foreground">{Math.round(rewriterDownloadProgress)}%</span> <Progress value={rewriterDownloadProgress} className="w-20 h-1.5" /> </div> )} </div> <div className="flex items-center gap-2"> <LanguageDetectorIcon className={cn('size-4', languageDetectorStatus.color, isLanguageDetectorSupported === null && 'animate-spin')} /> <span className="text-sm font-medium">Language Detector: {languageDetectorStatus.label}</span> {isLanguageDetectorDownloading && showProgress && ( <div className="flex items-center gap-2 ml-auto"> <span className="text-xs text-muted-foreground">{Math.round(languageDetectorDownloadProgress)}%</span> <Progress value={languageDetectorDownloadProgress} className="w-20 h-1.5" /> </div> )} </div> </div> ); } // Card variant (default) return ( <Card className={className}> <CardHeader> <div className="flex items-center justify-between"> <div className="flex items-center gap-2"> <Zap className="size-5 text-primary" /> <CardTitle>Chrome AI Status</CardTitle> </div> <Badge variant={overallStatus.variant} className="gap-1.5"> <OverallStatusIcon className={cn( 'size-3', overallStatus.color, (isSupported === null || isWriterSupported === null || isLanguageDetectorSupported === null) && 'animate-spin' )} /> {overallStatus.label} </Badge> </div> <CardDescription> Status of Chrome AI APIs (Summarizer, Writer & Language Detector) availability and model downloads </CardDescription> </CardHeader> <CardContent className="space-y-4"> {/* Summarizer Status */} <div className="space-y-3 border-b pb-4"> <div className="flex items-center justify-between"> <h4 className="text-sm font-semibold">Summarizer Model</h4> <Badge variant={summarizerStatus.variant} className="gap-1.5"> <SummarizerIcon className={cn( 'size-3', summarizerStatus.color, isSupported === null && 'animate-spin' )} /> {summarizerStatus.label} </Badge> </div> {/* Summarizer Browser Support */} <div className="flex items-center justify-between text-sm"> <span className="text-muted-foreground">Browser Support</span> <div className="flex items-center gap-2"> {isSupported === null ? ( <> <Spinner className="size-3" /> <span className="text-muted-foreground">Checking...</span> </> ) : isSupported ? ( <> <CheckCircle2 className="size-4 text-green-600 dark:text-green-400" /> <span className="text-green-600 dark:text-green-400">Supported</span> </> ) : ( <> <XCircle className="size-4 text-destructive" /> <span className="text-destructive">Not Supported</span> </> )} </div> </div> {/* Summarizer Model Availability */} {isSupported && ( <div className="flex items-center justify-between text-sm"> <span className="text-muted-foreground">Model Availability</span> <div className="flex items-center gap-2"> {availability === null ? ( <> <Spinner className="size-3" /> <span className="text-muted-foreground">Unknown</span> </> ) : availability === 'available' ? ( <> <CheckCircle2 className="size-4 text-green-600 dark:text-green-400" /> <span className="text-green-600 dark:text-green-400">Available</span> </> ) : availability === 'downloadable' ? ( <> <Download className="size-4 text-primary animate-pulse" /> <span className="text-primary">Downloadable</span> </> ) : ( <> <XCircle className="size-4 text-destructive" /> <span className="text-destructive">Unavailable</span> </> )} </div> </div> )} {/* Summarizer Download Progress */} {isDownloading && showProgress && ( <div className="space-y-2"> <div className="flex items-center justify-between text-sm"> <span className="text-muted-foreground">Download Progress</span> <span className="font-medium">{Math.round(downloadProgress)}%</span> </div> <Progress value={downloadProgress} className="h-2" /> </div> )} {/* Summarizer Actions */} {isSupported && availability === null && ( <div className="pt-1"> <Button onClick={handleCheckAvailability} disabled={isChecking} variant="outline" size="sm" className="w-full" > {isChecking ? ( <> <Spinner className="size-3 mr-2" /> Checking... </> ) : ( <> <AlertCircle className="size-3 mr-2" /> Check Summarizer Availability </> )} </Button> </div> )} </div> {/* Writer Status */} <div className="space-y-3 border-b pb-4"> <div className="flex items-center justify-between"> <h4 className="text-sm font-semibold">Writer Model</h4> <Badge variant={writerStatus.variant} className="gap-1.5"> <WriterIcon className={cn( 'size-3', writerStatus.color, isWriterSupported === null && 'animate-spin' )} /> {writerStatus.label} </Badge> </div> {/* Writer Browser Support */} <div className="flex items-center justify-between text-sm"> <span className="text-muted-foreground">Browser Support</span> <div className="flex items-center gap-2"> {isWriterSupported === null ? ( <> <Spinner className="size-3" /> <span className="text-muted-foreground">Checking...</span> </> ) : isWriterSupported ? ( <> <CheckCircle2 className="size-4 text-green-600 dark:text-green-400" /> <span className="text-green-600 dark:text-green-400">Supported</span> </> ) : ( <> <XCircle className="size-4 text-destructive" /> <span className="text-destructive">Not Supported</span> </> )} </div> </div> {/* Writer Model Availability */} {isWriterSupported && ( <div className="flex items-center justify-between text-sm"> <span className="text-muted-foreground">Model Availability</span> <div className="flex items-center gap-2"> {writerAvailability === null ? ( <> <Spinner className="size-3" /> <span className="text-muted-foreground">Unknown</span> </> ) : writerAvailability === 'available' ? ( <> <CheckCircle2 className="size-4 text-green-600 dark:text-green-400" /> <span className="text-green-600 dark:text-green-400">Available</span> </> ) : writerAvailability === 'downloadable' ? ( <> <Download className="size-4 text-primary animate-pulse" /> <span className="text-primary">Downloadable</span> </> ) : ( <> <XCircle className="size-4 text-destructive" /> <span className="text-destructive">Unavailable</span> </> )} </div> </div> )} {/* Writer Download Progress */} {isWriterDownloading && showProgress && ( <div className="space-y-2"> <div className="flex items-center justify-between text-sm"> <span className="text-muted-foreground">Download Progress</span> <span className="font-medium">{Math.round(writerDownloadProgress)}%</span> </div> <Progress value={writerDownloadProgress} className="h-2" /> </div> )} {/* Writer Actions */} {isWriterSupported && writerAvailability === null && ( <div className="pt-1"> <Button onClick={handleCheckWriterAvailability} disabled={isCheckingWriter} variant="outline" size="sm" className="w-full" > {isCheckingWriter ? ( <> <Spinner className="size-3 mr-2" /> Checking... </> ) : ( <> <AlertCircle className="size-3 mr-2" /> Check Writer Availability </> )} </Button> </div> )} </div> {/* Rewriter Status */} <div className="space-y-3 border-b pb-4"> <div className="flex items-center justify-between"> <h4 className="text-sm font-semibold">Rewriter Model</h4> <Badge variant={rewriterStatus.variant} className="gap-1.5"> <RewriterIcon className={cn( 'size-3', rewriterStatus.color, isRewriterSupported === null && 'animate-spin' )} /> {rewriterStatus.label} </Badge> </div> {/* Rewriter Browser Support */} <div className="flex items-center justify-between text-sm"> <span className="text-muted-foreground">Browser Support</span> <div className="flex items-center gap-2"> {isRewriterSupported === null ? ( <> <Spinner className="size-3" /> <span className="text-muted-foreground">Checking...</span> </> ) : isRewriterSupported ? ( <> <CheckCircle2 className="size-4 text-green-600 dark:text-green-400" /> <span className="text-green-600 dark:text-green-400">Supported</span> </> ) : ( <> <XCircle className="size-4 text-destructive" /> <span className="text-destructive">Not Supported</span> </> )} </div> </div> {/* Rewriter Model Availability */} {isRewriterSupported && ( <div className="flex items-center justify-between text-sm"> <span className="text-muted-foreground">Model Availability</span> <div className="flex items-center gap-2"> {rewriterAvailability === null ? ( <> <Spinner className="size-3" /> <span className="text-muted-foreground">Unknown</span> </> ) : rewriterAvailability === 'available' ? ( <> <CheckCircle2 className="size-4 text-green-600 dark:text-green-400" /> <span className="text-green-600 dark:text-green-400">Available</span> </> ) : rewriterAvailability === 'downloadable' ? ( <> <Download className="size-4 text-primary animate-pulse" /> <span className="text-primary">Downloadable</span> </> ) : ( <> <XCircle className="size-4 text-destructive" /> <span className="text-destructive">Unavailable</span> </> )} </div> </div> )} {/* Rewriter Download Progress */} {isRewriterDownloading && showProgress && ( <div className="space-y-2"> <div className="flex items-center justify-between text-sm"> <span className="text-muted-foreground">Download Progress</span> <span className="font-medium">{Math.round(rewriterDownloadProgress)}%</span> </div> <Progress value={rewriterDownloadProgress} className="h-2" /> </div> )} {/* Rewriter Actions */} {isRewriterSupported && rewriterAvailability === null && ( <div className="pt-1"> <Button onClick={handleCheckRewriterAvailability} disabled={isCheckingRewriter} variant="outline" size="sm" className="w-full" > {isCheckingRewriter ? ( <> <Spinner className="size-3 mr-2" /> Checking... </> ) : ( <> <AlertCircle className="size-3 mr-2" /> Check Rewriter Availability </> )} </Button> </div> )} </div> {/* Language Detector Status */} <div className="space-y-3"> <div className="flex items-center justify-between"> <h4 className="text-sm font-semibold">Language Detector</h4> <Badge variant={languageDetectorStatus.variant} className="gap-1.5"> <LanguageDetectorIcon className={cn( 'size-3', languageDetectorStatus.color, isLanguageDetectorSupported === null && 'animate-spin' )} /> {languageDetectorStatus.label} </Badge> </div> {/* Language Detector Browser Support */} <div className="flex items-center justify-between text-sm"> <span className="text-muted-foreground">Browser Support</span> <div className="flex items-center gap-2"> {isLanguageDetectorSupported === null ? ( <> <Spinner className="size-3" /> <span className="text-muted-foreground">Checking...</span> </> ) : isLanguageDetectorSupported ? ( <> <CheckCircle2 className="size-4 text-green-600 dark:text-green-400" /> <span className="text-green-600 dark:text-green-400">Supported</span> </> ) : ( <> <XCircle className="size-4 text-destructive" /> <span className="text-destructive">Not Supported</span> </> )} </div> </div> {/* Language Detector Model Availability */} {isLanguageDetectorSupported && ( <div className="flex items-center justify-between text-sm"> <span className="text-muted-foreground">Model Availability</span> <div className="flex items-center gap-2"> {languageDetectorAvailability === null ? ( <> <Spinner className="size-3" /> <span className="text-muted-foreground">Unknown</span> </> ) : languageDetectorAvailability === 'available' ? ( <> <CheckCircle2 className="size-4 text-green-600 dark:text-green-400" /> <span className="text-green-600 dark:text-green-400">Available</span> </> ) : languageDetectorAvailability === 'downloadable' ? ( <> <Download className="size-4 text-primary animate-pulse" /> <span className="text-primary">Downloadable</span> </> ) : ( <> <XCircle className="size-4 text-destructive" /> <span className="text-destructive">Unavailable</span> </> )} </div> </div> )} {/* Language Detector Download Progress */} {isLanguageDetectorDownloading && showProgress && ( <div className="space-y-2"> <div className="flex items-center justify-between text-sm"> <span className="text-muted-foreground">Download Progress</span> <span className="font-medium">{Math.round(languageDetectorDownloadProgress)}%</span> </div> <Progress value={languageDetectorDownloadProgress} className="h-2" /> </div> )} {/* Language Detector Actions */} {isLanguageDetectorSupported && languageDetectorAvailability === null && ( <div className="pt-1"> <Button onClick={handleCheckLanguageDetectorAvailability} disabled={isCheckingLanguageDetector} variant="outline" size="sm" className="w-full" > {isCheckingLanguageDetector ? ( <> <Spinner className="size-3 mr-2" /> Checking... </> ) : ( <> <AlertCircle className="size-3 mr-2" /> Check Language Detector Availability </> )} </Button> </div> )} </div> {/* Error Display */} {error && showErrors && ( <Alert variant="destructive" className="mt-4"> <AlertCircle className="size-4" /> <AlertTitle>Error</AlertTitle> <AlertDescription className="flex items-center justify-between"> <span>{error}</span> <button onClick={resetError} className="text-xs underline hover:no-underline ml-2" > Dismiss </button> </AlertDescription> </Alert> )} </CardContent> </Card> ); }
Props
Prop
Type
Dependencies
- react
- genui-provider
- card
- badge
- progress
- spinner
- alert
- popover
- button