GenCN UI

Available

Conditionally render components based on AI API availability (local LLM only).

The Available components check if specific AI APIs are available locally and conditionally render children or fallback based on availability. This is useful for gracefully handling cases where AI features may not be available on all devices or browsers.

Each API has its own component:

  • GencnUIRewriterAvailable - Checks Rewriter API availability
  • GencnUIWriterAvailable - Checks Writer API availability
  • GencnUIProofreaderAvailable - Checks Proofreader API availability
  • GencnUILanguageModelAvailable - Checks LanguageModel (Prompt API) availability
  • GencnUITranslatorAvailable - Checks Translator API availability (requires language pair)
  • GencnUISummarizerAvailable - Checks Summarizer API availability
  • GencnUILanguageDetectorAvailable - Checks Language Detector API availability
  • GencnUIDetectLanguageAvailable - Convenience wrapper for GencnUIDetectLanguage component

Try out the component below to see how it works in practice.

Loading preview...
"use client";

import { useState, useMemo } from "react";
import {
  GencnUIRewriterAvailable,
  GencnUIWriterAvailable,
  GencnUIProofreaderAvailable,
  GencnUILanguageModelAvailable,
  GencnUITranslatorAvailable,
  GencnUISummarizerAvailable,
  GencnUILanguageDetectorAvailable,
  type MostSignificantStatus,
} from "@/registry/new-york/gencn-ui/items/available/gencn-ui-available";
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";

type APIType =
  | "rewriter"
  | "writer"
  | "proofreader"
  | "languageModel"
  | "translator"
  | "summarizer"
  | "languageDetector";

const apiTypes: APIType[] = [
  "rewriter",
  "writer",
  "proofreader",
  "languageModel",
  "translator",
  "summarizer",
  "languageDetector",
];

// Map API types to their corresponding components
const apiComponents = {
  rewriter: GencnUIRewriterAvailable,
  writer: GencnUIWriterAvailable,
  proofreader: GencnUIProofreaderAvailable,
  languageModel: GencnUILanguageModelAvailable,
  translator: GencnUITranslatorAvailable,
  summarizer: GencnUISummarizerAvailable,
  languageDetector: GencnUILanguageDetectorAvailable,
} as const;

type NonTranslatorAPIType = Exclude<APIType, "translator">;
type NonTranslatorComponent =
  | typeof GencnUIRewriterAvailable
  | typeof GencnUIWriterAvailable
  | typeof GencnUIProofreaderAvailable
  | typeof GencnUILanguageModelAvailable
  | typeof GencnUISummarizerAvailable
  | typeof GencnUILanguageDetectorAvailable;

export function GencnUIAvailableExample() {
  const [statuses, setStatuses] = useState<Record<APIType, MostSignificantStatus | null>>({
    rewriter: null,
    writer: null,
    proofreader: null,
    languageModel: null,
    translator: null,
    summarizer: null,
    languageDetector: null,
  });

  // Memoize status change handlers for each API type to prevent infinite loops
  const statusHandlers = useMemo(() => {
    const handlers: Record<APIType, (status: MostSignificantStatus) => void> = {} as Record<
      APIType,
      (status: MostSignificantStatus) => void
    >;
    apiTypes.forEach((apiType) => {
      handlers[apiType] = (status: MostSignificantStatus) => {
        setStatuses((prev) => {
          // Only update if status actually changed to prevent unnecessary re-renders
          if (prev[apiType] === status) return prev;
          return { ...prev, [apiType]: status };
        });
      };
    });
    return handlers;
  }, []); // Empty deps - handlers are stable

  const getStatusBadgeColor = (status: MostSignificantStatus | null) => {
    if (!status) return "bg-muted text-muted-foreground";
    switch (status) {
      case "unsupported":
        return "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200";
      case "checking":
        return "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200";
      case "available":
        return "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200";
      case "downloadable":
        return "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200";
      case "unavailable":
        return "bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200";
      default:
        return "bg-muted text-muted-foreground";
    }
  };

  return (
    <div className="space-y-6">
      {apiTypes.map((apiType) => {
        const status = statuses[apiType];
        return (
          <Card key={apiType} className="shadow-none">
            <CardHeader>
              <div className="flex items-center gap-2">
                <CardTitle className="capitalize">{apiType}</CardTitle>
                {status && (
                  <span
                    className={`rounded-full px-2 py-0.5 text-xs font-medium ${getStatusBadgeColor(status)}`}
                  >
                    {status}
                  </span>
                )}
              </div>
              <CardDescription>
                Example using <code className="text-xs">{apiType}</code> API type
              </CardDescription>
            </CardHeader>
            <CardContent>
              {apiType === "translator" ? (
                <GencnUITranslatorAvailable
                  targetLanguage="es"
                  sourceLanguage="en"
                  fallback={
                    <p className="text-muted-foreground text-sm">
                      LLM Not available showing <b>fallback component</b>
                    </p>
                  }
                  loading={
                    <p className="text-muted-foreground text-sm">
                      Checking {apiType} API support...
                    </p>
                  }
                  onStatusChange={statusHandlers[apiType]}
                >
                  <div className="space-y-2">
                    <p className="text-sm">
                      Wrapped in{" "}
                      <code className="bg-muted rounded px-1 py-0.5 text-xs">
                        {apiType}
                      </code>{" "}
                      and is available
                    </p>
                    {status && (
                      <p className="text-xs text-muted-foreground">
                        Current status: <span className="font-medium">{status}</span>
                      </p>
                    )}
                  </div>
                </GencnUITranslatorAvailable>
              ) : (
                (() => {
                  const Component = apiComponents[apiType] as NonTranslatorComponent;
                  return (
                    <Component
                      fallback={
                        <p className="text-muted-foreground text-sm">
                          LLM Not available showing <b>fallback component</b>
                        </p>
                      }
                      loading={
                        <p className="text-muted-foreground text-sm">
                          Checking {apiType} API support...
                        </p>
                      }
                      onStatusChange={statusHandlers[apiType]}
                    >
                      <div className="space-y-2">
                        <p className="text-sm">
                          Wrapped in{" "}
                          <code className="bg-muted rounded px-1 py-0.5 text-xs">
                            {apiType}
                          </code>{" "}
                          and is available
                        </p>
                        {status && (
                          <p className="text-xs text-muted-foreground">
                            Current status: <span className="font-medium">{status}</span>
                          </p>
                        )}
                      </div>
                    </Component>
                  );
                })()
              )}
            </CardContent>
          </Card>
        );
      })}
    </div>
  );
}

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-available
"use client";

import {
  useState,
  useRef,
  useEffect,
  type ReactNode,
} from "react";
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 { useTranslator } from "@/registry/new-york/gencn-ui/items/shared/hooks/internal/use-gencn-ui-translator";
import {
  isLanguageDetectorSupported as libIsLanguageDetectorSupported,
  checkLanguageDetectorAvailability as libCheckLanguageDetectorAvailability,
} from "@/registry/new-york/gencn-ui/items/shared/lib/gencn-ui-language-detector";
import { useSummarizer } from "@/registry/new-york/gencn-ui/items/shared/hooks/use-gencn-ui-summarizer";

// Shared interface for individual API available components
interface IndividualAPIAvailableProps {
  /**
   * Component to render when API is not available
   */
  fallback: ReactNode;

  /**
   * Component to render while checking availability
   */
  loading: ReactNode;

  /**
   * Children to render when API is available
   */
  children: ReactNode;

  /**
   * Callback that receives the most significant status
   * Status priority: "unsupported" > "checking" > "available" > "downloadable" > "unavailable"
   */
  onStatusChange?: (status: MostSignificantStatus) => void;
}

// Special interface for translator component that requires language pair
interface TranslatorAvailableProps extends IndividualAPIAvailableProps {
  /**
   * Target language code (required for translator)
   */
  targetLanguage: string;

  /**
   * Source language code (optional, defaults to "auto" if not provided)
   */
  sourceLanguage?: string;
}

type AvailabilityStatus = "available" | "downloadable" | "unavailable" | null;

// Type for the most significant status
export type MostSignificantStatus = 
  | "unsupported" 
  | "checking" 
  | "available" 
  | "downloadable" 
  | "unavailable";

// Helper function to get the most significant status
const getMostSignificantStatus = (
  isSupported: boolean | null,
  availability: AvailabilityStatus
): MostSignificantStatus => {
  // Most significant: browser doesn't support the API
  if (isSupported === false) {
    return "unsupported";
  }

  // Still checking support or availability
  if (isSupported === null || availability === null) {
    return "checking";
  }

  // At this point, isSupported === true and availability is known
  // Return the availability status (available > downloadable > unavailable)
  return availability;
};

// Helper function to check if API is available
const isAPIAvailable = (
  isSupported: boolean | null,
  availability: AvailabilityStatus
): boolean => {
  // First check if browser supports the API
  if (isSupported === false) {
    return false;
  }

  // If we're still checking support, return false
  if (isSupported === null) {
    return false;
  }

  // At this point, isSupported === true, so browser supports it
  // Check local availability only
  if (availability === null) {
    // Still checking availability
    return false;
  }

  // If locally available or downloadable, it's available
  if (availability === "available" || availability === "downloadable") {
    return true;
  }

  // If unavailable locally, return false
  return false;
};

// Individual API Available Component Hook
function useAPIAvailability(
  isSupported: boolean | null,
  availability: AvailabilityStatus,
  onStatusChange?: (status: MostSignificantStatus) => void
) {
  const [isChecking, setIsChecking] = useState(true);
  const [isAvailable, setIsAvailable] = useState(false);
  const timeoutRef = useRef<NodeJS.Timeout | null>(null);

  useEffect(() => {
    let cancelled = false;

    const checkAvailability = () => {
      const status = getMostSignificantStatus(isSupported, availability);
      
      // Notify callback of status change
      if (!cancelled && onStatusChange) {
        onStatusChange(status);
      }

      // If explicitly not supported, we're done
      if (isSupported === false) {
        if (!cancelled) {
          setIsAvailable(false);
          setIsChecking(false);
        }
        return;
      }

      // If we have availability info, check it synchronously
      if (availability !== null) {
        if (!cancelled) {
          setIsAvailable(isAPIAvailable(isSupported, availability));
          setIsChecking(false);
        }
        return;
      }

      // If isSupported is null, we're still checking support
      // If isSupported is true but availability is null, we need to check availability
      if (isSupported === null || (isSupported === true && availability === null)) {
        // Add a timeout to stop checking after a reasonable time (2 seconds)
        if (timeoutRef.current) {
          clearTimeout(timeoutRef.current);
        }
        timeoutRef.current = setTimeout(() => {
          if (!cancelled) {
            // If still null after timeout, assume not supported
            setIsAvailable(false);
            setIsChecking(false);
            if (onStatusChange) {
              onStatusChange("unsupported");
            }
          }
          timeoutRef.current = null;
        }, 2000);

        if (!cancelled) {
          setIsChecking(true);
          setIsAvailable(false);
        }
      }
    };

    checkAvailability();

    return () => {
      cancelled = true;
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current);
        timeoutRef.current = null;
      }
    };
  }, [isSupported, availability, onStatusChange]);

  return { isChecking, isAvailable };
}

/**
 * GencnUIRewriterAvailable component checks if Rewriter API is available locally
 * and conditionally renders children or fallback based on availability.
 */
export function GencnUIRewriterAvailable({
  fallback,
  loading,
  children,
  onStatusChange,
}: IndividualAPIAvailableProps) {
  const rewriter = useRewriter();
  const { isChecking, isAvailable } = useAPIAvailability(
    rewriter.isSupported,
    rewriter.availability,
    onStatusChange
  );

  if (isChecking) {
    return <>{loading}</>;
  }

  return <>{isAvailable ? children : fallback}</>;
}

/**
 * GencnUIWriterAvailable component checks if Writer API is available locally
 * and conditionally renders children or fallback based on availability.
 */
export function GencnUIWriterAvailable({
  fallback,
  loading,
  children,
  onStatusChange,
}: IndividualAPIAvailableProps) {
  const writer = useWriter();
  const { isChecking, isAvailable } = useAPIAvailability(
    writer.isSupported,
    writer.availability,
    onStatusChange
  );

  if (isChecking) {
    return <>{loading}</>;
  }

  return <>{isAvailable ? children : fallback}</>;
}

/**
 * GencnUIProofreaderAvailable component checks if Proofreader API is available locally
 * and conditionally renders children or fallback based on availability.
 */
export function GencnUIProofreaderAvailable({
  fallback,
  loading,
  children,
  onStatusChange,
}: IndividualAPIAvailableProps) {
  const proofreader = useProofreader();
  const { isChecking, isAvailable } = useAPIAvailability(
    proofreader.isSupported,
    proofreader.availability,
    onStatusChange
  );

  if (isChecking) {
    return <>{loading}</>;
  }

  return <>{isAvailable ? children : fallback}</>;
}

/**
 * GencnUILanguageModelAvailable component checks if LanguageModel (Prompt API) is available locally
 * and conditionally renders children or fallback based on availability.
 */
export function GencnUILanguageModelAvailable({
  fallback,
  loading,
  children,
  onStatusChange,
}: IndividualAPIAvailableProps) {
  const languageModel = useLanguageModel();
  const { isChecking, isAvailable } = useAPIAvailability(
    languageModel.isSupported,
    languageModel.availability,
    onStatusChange
  );

  if (isChecking) {
    return <>{loading}</>;
  }

  return <>{isAvailable ? children : fallback}</>;
}

/**
 * GencnUITranslatorAvailable component checks if Translator API is available locally
 * for a specific language pair and conditionally renders children or fallback based on availability.
 */
export function GencnUITranslatorAvailable({
  fallback,
  loading,
  children,
  targetLanguage,
  sourceLanguage,
  onStatusChange,
}: TranslatorAvailableProps) {
  const translator = useTranslator();
  const [pairAvailability, setPairAvailability] = useState<AvailabilityStatus>(null);

  // Check language pair availability when targetLanguage or sourceLanguage changes
  useEffect(() => {
    // Only check if translator is supported and we have a target language
    if (!translator.isSupported || !targetLanguage) {
      setPairAvailability(null);
      return;
    }

    const sourceLang = sourceLanguage || "auto";
    translator.checkLanguagePairAvailability(sourceLang, targetLanguage)
      .then((avail) => {
        setPairAvailability(avail);
      })
      .catch(() => {
        // Check if still supported before setting to null
        if (translator.isSupported) {
          setPairAvailability(null);
        }
      });
  }, [translator.isSupported, translator.checkLanguagePairAvailability, targetLanguage, sourceLanguage]);

  // Use language pair availability if available, otherwise fall back to general availability
  const availability = pairAvailability !== null ? pairAvailability : translator.availability;
  
  // Convert isSupported from boolean to boolean | null for consistency
  const isSupported: boolean | null = translator.isSupported ? true : false;

  const { isChecking, isAvailable } = useAPIAvailability(isSupported, availability, onStatusChange);

  if (isChecking) {
    return <>{loading}</>;
  }

  return <>{isAvailable ? children : fallback}</>;
}

/**
 * GencnUISummarizerAvailable component checks if Summarizer API is available locally
 * and conditionally renders children or fallback based on availability.
 */
export function GencnUISummarizerAvailable({
  fallback,
  loading,
  children,
  onStatusChange,
}: IndividualAPIAvailableProps) {
  const summarizer = useSummarizer();
  const { isChecking, isAvailable } = useAPIAvailability(
    summarizer.isSupported,
    summarizer.availability,
    onStatusChange
  );

  if (isChecking) {
    return <>{loading}</>;
  }

  return <>{isAvailable ? children : fallback}</>;
}

/**
 * GencnUILanguageDetectorAvailable component checks if Language Detector API is available locally
 * and conditionally renders children or fallback based on availability.
 */
export function GencnUILanguageDetectorAvailable({
  fallback,
  loading,
  children,
  onStatusChange,
}: IndividualAPIAvailableProps) {
  const [isSupported, setIsSupported] = useState<boolean | null>(null);
  const [availability, setAvailability] = useState<AvailabilityStatus>(null);

  // Check language detector support and availability
  useEffect(() => {
    const supported = libIsLanguageDetectorSupported();
    setIsSupported(supported);
    if (supported) {
      libCheckLanguageDetectorAvailability()
        .then((avail) => {
          setAvailability(avail);
        })
        .catch(() => {
          setAvailability(null);
        });
    }
  }, []);

  const { isChecking, isAvailable } = useAPIAvailability(isSupported, availability, onStatusChange);

  if (isChecking) {
    return <>{loading}</>;
  }

  return <>{isAvailable ? children : fallback}</>;
}

/**
 * GencnUIDetectLanguageAvailable component checks if Language Detector API is available locally
 * and conditionally renders GencnUIDetectLanguage component or fallback based on availability.
 * This is a convenience wrapper around GencnUILanguageDetectorAvailable for the GencnUIDetectLanguage component.
 */
export function GencnUIDetectLanguageAvailable({
  fallback,
  loading,
  children,
  onStatusChange,
}: IndividualAPIAvailableProps) {
  return (
    <GencnUILanguageDetectorAvailable fallback={fallback} loading={loading} onStatusChange={onStatusChange}>
      {children}
    </GencnUILanguageDetectorAvailable>
  );
}

Component API