GenCN UI

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>
  );
}

Component API