Modal Selector

PreviousNext

AI-focused model selector with preview, tooltips, and capability indicators built-in

Docs
paceuiui

Preview

Loading preview…
ai/modal-selector.tsx
"use client";

import { BrainIcon, CodeIcon, EyeIcon, FileSearch2Icon, SpeechIcon, Wand2Icon } from "lucide-react";
import { ComponentProps, ReactNode, useMemo, useState } from "react";

import { Swap } from "@/components/gsap/swap";
import { Button } from "@/components/ui/button";
import {
    DropdownMenu,
    DropdownMenuContent,
    DropdownMenuItem,
    DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";

type ModalCapability = "vision" | "thinking" | "code" | "speech" | "search" | "creativity";

export type ModalOption = {
    value: string;
    image: string;
    name: string;
    darkInvertImage?: boolean;
    description: string;
    capabilities: ModalCapability[];
};

type ModalSelectorProps = ComponentProps<typeof Button> & {
    showCapabilities?: boolean;
    showDescription?: boolean;
};

const modals: ModalOption[] = [
    {
        value: "openai-gpt-4o",
        image: "https://unpkg.com/@lobehub/icons-static-svg@latest/icons/openai.svg",
        name: "gpt-4o",
        darkInvertImage: true,
        description: "High-speed language understanding and generation",
        capabilities: ["vision", "thinking", "code", "speech"],
    },
    {
        value: "google-gemini-1.5",
        image: "https://unpkg.com/@lobehub/icons-static-svg@latest/icons/gemini-color.svg",
        name: "gemini-1.5",
        description: "Advanced multimodal processing for text and images",
        capabilities: ["vision", "thinking", "speech"],
    },
    {
        value: "anthropic-claude-3-opus",
        image: "https://unpkg.com/@lobehub/icons-static-svg@latest/icons/claude-color.svg",
        name: "claude-3-opus",
        description: "Deep contextual reasoning for complex tasks",
        capabilities: ["thinking", "code"],
    },
    {
        value: "xai-grok-1",
        image: "https://unpkg.com/@lobehub/icons-static-svg@latest/icons/grok.svg",
        name: "grok-1",
        darkInvertImage: true,
        description: "Conversational AI from xAI for real-time interactions",
        capabilities: ["thinking", "speech"],
    },
    {
        value: "qwen-1.5",
        image: "https://unpkg.com/@lobehub/icons-static-svg@latest/icons/qwen-color.svg",
        name: "qwen-1.5",
        description: "Efficient large language model by Alibaba Cloud",
        capabilities: ["thinking", "code"],
    },
    {
        value: "openai-gpt-4-turbo",
        image: "https://unpkg.com/@lobehub/icons-static-svg@latest/icons/openai.svg",
        name: "gpt-4-turbo",
        darkInvertImage: true,
        description: "Optimized for fast and cost-effective workflows",
        capabilities: ["thinking", "code", "speech"],
    },
    {
        value: "google-gemini-ultra",
        image: "https://unpkg.com/@lobehub/icons-static-svg@latest/icons/gemini-color.svg",
        name: "gemini-ultra",
        description: "Cutting-edge reasoning and creativity features",
        capabilities: ["vision", "thinking", "creativity"],
    },
    {
        value: "deepseek-llm",
        image: "https://unpkg.com/@lobehub/icons-static-svg@latest/icons/deepseek-color.svg",
        name: "deepseek-llm",
        description: "Multilingual model for global use cases",
        capabilities: ["thinking", "code"],
    },
    {
        value: "anthropic-claude-3-sonnet",
        image: "https://unpkg.com/@lobehub/icons-static-svg@latest/icons/claude-color.svg",
        name: "claude-3-sonnet",
        description: "Balanced model for efficiency and safety",
        capabilities: ["thinking"],
    },
    {
        value: "stability-sdxl",
        image: "https://unpkg.com/@lobehub/icons-static-svg@latest/icons/stability-color.svg",
        name: "stability-sdxl",
        description: "Text-to-image generation at photorealistic quality",
        capabilities: ["vision", "creativity"],
    },
    {
        value: "microsoft-copilot",
        image: "https://unpkg.com/@lobehub/icons-static-svg@latest/icons/microsoft-color.svg",
        name: "copilot",
        description: "Integrated productivity assistant from Microsoft",
        capabilities: ["thinking", "code", "speech"],
    },
    {
        value: "midjourney-v6",
        image: "https://unpkg.com/@lobehub/icons-static-svg@latest/icons/midjourney.svg",
        name: "midjourney-v6",
        darkInvertImage: true,
        description: "AI art generation for creative visuals",
        capabilities: ["vision", "creativity"],
    },
    {
        value: "perplexity-answers",
        image: "https://unpkg.com/@lobehub/icons-static-svg@latest/icons/perplexity-color.svg",
        name: "perplexity-answers",
        description: "AI-powered search and question answering",
        capabilities: ["search", "thinking"],
    },
    {
        value: "cohere-command-r",
        image: "https://unpkg.com/@lobehub/icons-static-svg@latest/icons/cohere-color.svg",
        name: "command-r",
        description: "Powerful retrieval-augmented generation model",
        capabilities: ["thinking", "search"],
    },
];

const CapabilityBadge = ({ capability }: { capability: ModalCapability }) => {
    const styles: Record<ModalCapability, { color: string; text: string; icon: ReactNode }> = {
        thinking: {
            color: "bg-purple-500/10 hover:bg-purple-500/20 text-purple-500",
            text: "Thinking",
            icon: <BrainIcon className="size-4.5" />,
        },
        code: {
            color: "bg-cyan-500/10 hover:bg-cyan-500/20 text-cyan-500",
            text: "Code",
            icon: <CodeIcon className="size-4.5" />,
        },
        vision: {
            color: "bg-green-500/10 hover:bg-green-500/20 text-green-500",
            text: "Vision",
            icon: <EyeIcon className="size-4.5" />,
        },
        creativity: {
            color: "bg-orange-500/10 hover:bg-orange-500/20 text-orange-500",
            text: "Creativity",
            icon: <Wand2Icon className="size-4.5" />,
        },
        search: {
            color: "bg-teal-500/10 hover:bg-teal-500/20 text-teal-500",
            text: "Search",
            icon: <FileSearch2Icon className="size-4.5" />,
        },
        speech: {
            color: "bg-pink-500/10 hover:bg-pink-500/20 text-pink-500",
            text: "Speech",
            icon: <SpeechIcon className="size-4.5" />,
        },
    };

    const { color, text, icon } = styles[capability];

    return (
        <Tooltip>
            <TooltipTrigger asChild>
                <div className={cn("rounded p-1", color)}>{icon}</div>
            </TooltipTrigger>
            <TooltipContent>
                <p>{text}</p>
            </TooltipContent>
        </Tooltip>
    );
};

export const ModalSelector = ({
    showDescription = true,
    showCapabilities = true,
    variant = "outline",
    className,
    ...props
}: ModalSelectorProps) => {
    const [selectedModal, setSelectedModal] = useState(modals[0].value);

    const selectedModalItem = useMemo(() => {
        return modals.find((item) => item.value === selectedModal) ?? modals[0];
    }, [selectedModal]);

    return (
        <TooltipProvider>
            <DropdownMenu>
                <DropdownMenuTrigger asChild>
                    <Button
                        {...props}
                        variant={variant}
                        className={cn("cursor-pointer overflow-hidden shadow-none select-none", className)}>
                        <Swap state={selectedModalItem} effects={["blur", "slideDown", "opacity"]}>
                            {(state) => (
                                <div className="flex items-center gap-2.5">
                                    <img
                                        src={state.image}
                                        className={cn("min-w-4.5", { "dark:invert": state.darkInvertImage })}
                                        alt={`${state.name} logo`}
                                    />
                                    <p className="text-base overflow-ellipsis">{state.name}</p>
                                    {showCapabilities && (
                                        <div className="flex items-center gap-1.5">
                                            {state.capabilities.map((capability) => (
                                                <CapabilityBadge capability={capability} key={capability} />
                                            ))}
                                        </div>
                                    )}
                                </div>
                            )}
                        </Swap>
                    </Button>
                </DropdownMenuTrigger>
                <DropdownMenuContent className="w-80 shadow-xs transition-all hover:shadow-md" align="start">
                    <ScrollArea className="h-80">
                        <div className="space-y-0.5">
                            {modals.map((item) => (
                                <DropdownMenuItem
                                    key={item.value}
                                    onClick={() => setSelectedModal(item.value)}
                                    className={cn("group relative cursor-pointer gap-3", {
                                        "bg-accent": selectedModal === item.value,
                                    })}>
                                    <img
                                        src={item.image}
                                        alt={`${item.name} logo`}
                                        className={cn("size-6", {
                                            "dark:invert": item.darkInvertImage,
                                        })}
                                    />
                                    <div className="grow">
                                        <div className="flex items-center gap-2">
                                            <p className="grow text-base/none font-medium">{item.name}</p>
                                        </div>
                                        {showDescription && (
                                            <p className="text-muted-foreground mt-0.5 line-clamp-1">
                                                {item.description}
                                            </p>
                                        )}
                                    </div>
                                    {showCapabilities && (
                                        <div className="absolute end-0 flex translate-x-5 scale-90 items-center gap-1.5 rounded bg-inherit px-2.5 py-1 opacity-0 transition-all duration-300 group-hover:translate-x-0 group-hover:scale-100 group-hover:opacity-100">
                                            {item.capabilities.map((capability) => (
                                                <CapabilityBadge capability={capability} key={capability} />
                                            ))}
                                        </div>
                                    )}
                                </DropdownMenuItem>
                            ))}
                        </div>
                    </ScrollArea>
                </DropdownMenuContent>
            </DropdownMenu>
        </TooltipProvider>
    );
};

Installation

npx shadcn@latest add @paceui/modal-selector

Usage

import { ModalSelector } from "@/components/ui/modal-selector"
<ModalSelector />