settings-dialog

PreviousNext
Docs
platecomponent

Preview

Loading preview…
registry/components/editor/settings-dialog.tsx
'use client';

/* DEMO ONLY, DO NOT USE IN PRODUCTION */

import * as React from 'react';

import { CopilotPlugin } from '@platejs/ai/react';
import {
  Check,
  ChevronsUpDown,
  ExternalLinkIcon,
  Eye,
  EyeOff,
  Settings,
  Wand2Icon,
} from 'lucide-react';
import { useEditorRef } from 'platejs/react';

import { Button } from '@/components/ui/button';
import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
} from '@/components/ui/command';
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from '@/components/ui/popover';
import { cn } from '@/lib/utils';

import { aiChatPlugin } from './plugins/ai-kit';

type Model = {
  label: string;
  value: string;
};

export const models: Model[] = [
  // OpenAI Models
  { label: 'GPT-3.5 Turbo', value: 'openai/gpt-3.5-turbo' },
  { label: 'GPT-3.5 Turbo Instruct', value: 'openai/gpt-3.5-turbo-instruct' },
  { label: 'GPT-4 Turbo', value: 'openai/gpt-4-turbo' },
  { label: 'GPT-4.1', value: 'openai/gpt-4.1' },
  { label: 'GPT-4.1 Mini', value: 'openai/gpt-4.1-mini' },
  { label: 'GPT-4.1 Nano', value: 'openai/gpt-4.1-nano' },
  { label: 'GPT-4o', value: 'openai/gpt-4o' },
  { label: 'GPT-4o Mini', value: 'openai/gpt-4o-mini' },
  { label: 'GPT-5', value: 'openai/gpt-5' },
  { label: 'GPT-5 Codex', value: 'openai/gpt-5-codex' },
  { label: 'GPT-5 Mini', value: 'openai/gpt-5-mini' },
  { label: 'GPT-5 Nano', value: 'openai/gpt-5-nano' },
  { label: 'GPT-OSS 120B', value: 'openai/gpt-oss-120b' },
  { label: 'GPT-OSS 20B', value: 'openai/gpt-oss-20b' },
  { label: 'O1', value: 'openai/o1' },
  { label: 'O3', value: 'openai/o3' },
  { label: 'O3 Mini', value: 'openai/o3-mini' },
  { label: 'O4 Mini', value: 'openai/o4-mini' },

  // Google Models
  { label: 'Gemini 2.0 Flash', value: 'google/gemini-2.0-flash' },
  { label: 'Gemini 2.0 Flash Lite', value: 'google/gemini-2.0-flash-lite' },
  { label: 'Gemini 2.5 Flash', value: 'google/gemini-2.5-flash' },
  {
    label: 'Gemini 2.5 Flash Image Preview',
    value: 'google/gemini-2.5-flash-image-preview',
  },
  { label: 'Gemini 2.5 Flash Lite', value: 'google/gemini-2.5-flash-lite' },
  { label: 'Gemini 2.5 Pro', value: 'google/gemini-2.5-pro' },
  { label: 'Gemma 2 9B', value: 'google/gemma-2-9b' },

  // Alibaba Models
  { label: 'Qwen 3 14B', value: 'alibaba/qwen-3-14b' },
  { label: 'Qwen 3 235B', value: 'alibaba/qwen-3-235b' },
  { label: 'Qwen 3 30B', value: 'alibaba/qwen-3-30b' },
  { label: 'Qwen 3 32B', value: 'alibaba/qwen-3-32b' },
  { label: 'Qwen3 Coder', value: 'alibaba/qwen3-coder' },
  { label: 'Qwen3 Coder Plus', value: 'alibaba/qwen3-coder-plus' },
  { label: 'Qwen3 Max', value: 'alibaba/qwen3-max' },
  { label: 'Qwen3 Max Preview', value: 'alibaba/qwen3-max-preview' },
  {
    label: 'Qwen3 Next 80B A3B Instruct',
    value: 'alibaba/qwen3-next-80b-a3b-instruct',
  },
  {
    label: 'Qwen3 Next 80B A3B Thinking',
    value: 'alibaba/qwen3-next-80b-a3b-thinking',
  },
  { label: 'Qwen3 VL Instruct', value: 'alibaba/qwen3-vl-instruct' },
  { label: 'Qwen3 VL Thinking', value: 'alibaba/qwen3-vl-thinking' },

  // Amazon Models
  { label: 'Nova Lite', value: 'amazon/nova-lite' },
  { label: 'Nova Micro', value: 'amazon/nova-micro' },
  { label: 'Nova Pro', value: 'amazon/nova-pro' },

  // Anthropic Models
  { label: 'Claude 3 Haiku', value: 'anthropic/claude-3-haiku' },
  { label: 'Claude 3 Opus', value: 'anthropic/claude-3-opus' },
  { label: 'Claude 3.5 Haiku', value: 'anthropic/claude-3.5-haiku' },
  { label: 'Claude 3.5 Sonnet', value: 'anthropic/claude-3.5-sonnet' },
  { label: 'Claude 3.7 Sonnet', value: 'anthropic/claude-3.7-sonnet' },
  { label: 'Claude Opus 4', value: 'anthropic/claude-opus-4' },
  { label: 'Claude Opus 4.1', value: 'anthropic/claude-opus-4.1' },
  { label: 'Claude Sonnet 4', value: 'anthropic/claude-sonnet-4' },

  // Cohere Models
  { label: 'Command A', value: 'cohere/command-a' },
  { label: 'Command R', value: 'cohere/command-r' },
  { label: 'Command R Plus', value: 'cohere/command-r-plus' },

  // DeepSeek Models
  { label: 'DeepSeek R1', value: 'deepseek/deepseek-r1' },
  {
    label: 'DeepSeek R1 Distill Llama 70B',
    value: 'deepseek/deepseek-r1-distill-llama-70b',
  },
  { label: 'DeepSeek V3', value: 'deepseek/deepseek-v3' },
  { label: 'DeepSeek V3.1', value: 'deepseek/deepseek-v3.1' },
  { label: 'DeepSeek V3.1 Base', value: 'deepseek/deepseek-v3.1-base' },
  { label: 'DeepSeek V3.1 Terminus', value: 'deepseek/deepseek-v3.1-terminus' },
  { label: 'DeepSeek V3.2 Exp', value: 'deepseek/deepseek-v3.2-exp' },
  {
    label: 'DeepSeek V3.2 Exp Thinking',
    value: 'deepseek/deepseek-v3.2-exp-thinking',
  },

  // Inception Models
  { label: 'Mercury Coder Small', value: 'inception/mercury-coder-small' },

  // Meituan Models
  { label: 'LongCat Flash Chat', value: 'meituan/longcat-flash-chat' },
  { label: 'LongCat Flash Thinking', value: 'meituan/longcat-flash-thinking' },

  // Meta Models
  { label: 'Llama 3 70B', value: 'meta/llama-3-70b' },
  { label: 'Llama 3 8B', value: 'meta/llama-3-8b' },
  { label: 'Llama 3.1 70B', value: 'meta/llama-3.1-70b' },
  { label: 'Llama 3.1 8B', value: 'meta/llama-3.1-8b' },
  { label: 'Llama 3.2 11B', value: 'meta/llama-3.2-11b' },
  { label: 'Llama 3.2 1B', value: 'meta/llama-3.2-1b' },
  { label: 'Llama 3.2 3B', value: 'meta/llama-3.2-3b' },
  { label: 'Llama 3.2 90B', value: 'meta/llama-3.2-90b' },
  { label: 'Llama 3.3 70B', value: 'meta/llama-3.3-70b' },
  { label: 'Llama 4 Maverick', value: 'meta/llama-4-maverick' },
  { label: 'Llama 4 Scout', value: 'meta/llama-4-scout' },

  // Mistral Models
  { label: 'Codestral', value: 'mistral/codestral' },
  { label: 'Devstral Small', value: 'mistral/devstral-small' },
  { label: 'Magistral Medium', value: 'mistral/magistral-medium' },
  { label: 'Magistral Small', value: 'mistral/magistral-small' },
  { label: 'Ministral 3B', value: 'mistral/ministral-3b' },
  { label: 'Ministral 8B', value: 'mistral/ministral-8b' },
  { label: 'Mistral Large', value: 'mistral/mistral-large' },
  { label: 'Mistral Medium', value: 'mistral/mistral-medium' },
  { label: 'Mistral Small', value: 'mistral/mistral-small' },
  { label: 'Mixtral 8x22B Instruct', value: 'mistral/mixtral-8x22b-instruct' },
  { label: 'Pixtral 12B', value: 'mistral/pixtral-12b' },
  { label: 'Pixtral Large', value: 'mistral/pixtral-large' },

  // MoonshotAI Models
  { label: 'Kimi K2', value: 'moonshotai/kimi-k2' },
  { label: 'Kimi K2 0905', value: 'moonshotai/kimi-k2-0905' },
  { label: 'Kimi K2 Turbo', value: 'moonshotai/kimi-k2-turbo' },

  // Morph Models
  { label: 'Morph V3 Fast', value: 'morph/morph-v3-fast' },
  { label: 'Morph V3 Large', value: 'morph/morph-v3-large' },

  // Perplexity Models
  { label: 'Sonar', value: 'perplexity/sonar' },
  { label: 'Sonar Pro', value: 'perplexity/sonar-pro' },
  { label: 'Sonar Reasoning', value: 'perplexity/sonar-reasoning' },
  { label: 'Sonar Reasoning Pro', value: 'perplexity/sonar-reasoning-pro' },

  // Stealth Models
  { label: 'Sonoma Dusk Alpha', value: 'stealth/sonoma-dusk-alpha' },
  { label: 'Sonoma Sky Alpha', value: 'stealth/sonoma-sky-alpha' },

  // Vercel Models
  { label: 'v0 1.0 MD', value: 'vercel/v0-1.0-md' },
  { label: 'v0 1.5 MD', value: 'vercel/v0-1.5-md' },

  // xAI Models
  { label: 'Grok 2', value: 'xai/grok-2' },
  { label: 'Grok 2 Vision', value: 'xai/grok-2-vision' },
  { label: 'Grok 3', value: 'xai/grok-3' },
  { label: 'Grok 3 Fast', value: 'xai/grok-3-fast' },
  { label: 'Grok 3 Mini', value: 'xai/grok-3-mini' },
  { label: 'Grok 3 Mini Fast', value: 'xai/grok-3-mini-fast' },
  { label: 'Grok 4', value: 'xai/grok-4' },
  { label: 'Grok Code Fast 1', value: 'xai/grok-code-fast-1' },
  {
    label: 'Grok 4 Fast Non-Reasoning',
    value: 'xai/grok-4-fast-non-reasoning',
  },
  { label: 'Grok 4 Fast Reasoning', value: 'xai/grok-4-fast-reasoning' },

  // ZAI Models
  { label: 'GLM 4.5', value: 'zai/glm-4.5' },
  { label: 'GLM 4.5 Air', value: 'zai/glm-4.5-air' },
  { label: 'GLM 4.5V', value: 'zai/glm-4.5v' },
];

export function SettingsDialog() {
  const editor = useEditorRef();

  const [tempModel, setTempModel] = React.useState(models[7]);
  const [tempKeys, setTempKeys] = React.useState<Record<string, string>>({
    aiGatewayApiKey: '',
    uploadthing: '',
  });
  const [showKey, setShowKey] = React.useState<Record<string, boolean>>({});
  const [open, setOpen] = React.useState(false);
  const [openModel, setOpenModel] = React.useState(false);

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();

    // Update AI chat options
    const chatOptions = editor.getOptions(aiChatPlugin).chatOptions ?? {};

    editor.setOption(aiChatPlugin, 'chatOptions', {
      ...chatOptions,
      body: {
        ...chatOptions.body,
        apiKey: tempKeys.aiGatewayApiKey,
        model: tempModel.value,
      },
    });

    setOpen(false);

    // Update AI complete options
    const completeOptions =
      editor.getOptions(CopilotPlugin).completeOptions ?? {};
    editor.setOption(CopilotPlugin, 'completeOptions', {
      ...completeOptions,
      body: {
        ...completeOptions.body,
        apiKey: tempKeys.aiGatewayApiKey,
        model: tempModel.value,
      },
    });
  };

  const toggleKeyVisibility = (key: string) => {
    setShowKey((prev) => ({ ...prev, [key]: !prev[key] }));
  };

  const renderApiKeyInput = (service: string, label: string) => (
    <div className="group relative">
      <div className="flex items-center justify-between">
        <label
          className="-translate-y-1/2 absolute top-1/2 block cursor-text px-1 text-muted-foreground/70 text-sm transition-all group-focus-within:pointer-events-none group-focus-within:top-0 group-focus-within:cursor-default group-focus-within:font-medium group-focus-within:text-foreground group-focus-within:text-xs has-[+input:not(:placeholder-shown)]:pointer-events-none has-[+input:not(:placeholder-shown)]:top-0 has-[+input:not(:placeholder-shown)]:cursor-default has-[+input:not(:placeholder-shown)]:font-medium has-[+input:not(:placeholder-shown)]:text-foreground has-[+input:not(:placeholder-shown)]:text-xs"
          htmlFor={label}
        >
          <span className="inline-flex bg-background px-2">{label}</span>
        </label>
        <Button
          asChild
          size="icon"
          variant="ghost"
          className="absolute top-0 right-[28px] h-full"
        >
          <a
            className="flex items-center"
            href={
              service === 'aiGatewayApiKey'
                ? 'https://vercel.com/docs/ai-gateway'
                : 'https://uploadthing.com/dashboard'
            }
            rel="noopener noreferrer"
            target="_blank"
          >
            <ExternalLinkIcon className="size-4" />
            <span className="sr-only">Get {label}</span>
          </a>
        </Button>
      </div>

      <Input
        id={label}
        className="pr-10"
        value={tempKeys[service]}
        onChange={(e) =>
          setTempKeys((prev) => ({ ...prev, [service]: e.target.value }))
        }
        placeholder=""
        data-1p-ignore
        type={showKey[service] ? 'text' : 'password'}
      />
      <Button
        size="icon"
        variant="ghost"
        className="absolute top-0 right-0 h-full"
        onClick={() => toggleKeyVisibility(service)}
        type="button"
      >
        {showKey[service] ? (
          <EyeOff className="size-4" />
        ) : (
          <Eye className="size-4" />
        )}
        <span className="sr-only">
          {showKey[service] ? 'Hide' : 'Show'} {label}
        </span>
      </Button>
    </div>
  );

  return (
    <Dialog open={open} onOpenChange={setOpen}>
      <DialogTrigger asChild>
        <Button
          size="icon"
          variant="default"
          className={cn(
            'group fixed right-4 bottom-4 z-50 size-10 overflow-hidden',
            'rounded-full shadow-md hover:shadow-lg'
          )}
          // data-block-hide
        >
          <Settings className="size-4" />
        </Button>
      </DialogTrigger>
      <DialogContent>
        <DialogHeader>
          <DialogTitle className="text-xl">Settings</DialogTitle>
          <DialogDescription>
            Configure your API keys and preferences.
          </DialogDescription>
        </DialogHeader>

        <form className="space-y-10" onSubmit={handleSubmit}>
          {/* AI Settings Group */}
          <div className="space-y-4">
            <div className="flex items-center gap-2">
              <div className="size-8 rounded-full bg-purple-100 p-2 dark:bg-purple-900">
                <Wand2Icon className="size-4 text-purple-600 dark:text-purple-400" />
              </div>
              <h4 className="font-semibold">AI</h4>
            </div>

            <div className="space-y-4">
              {renderApiKeyInput('aiGatewayApiKey', 'AI Gateway API Key')}

              <div className="group relative">
                <label
                  className="-translate-y-1/2 absolute start-1 top-0 z-10 block bg-background px-2 font-medium text-foreground text-xs group-has-disabled:opacity-50"
                  htmlFor="select-model"
                >
                  Model
                </label>
                <Popover open={openModel} onOpenChange={setOpenModel}>
                  <PopoverTrigger id="select-model" asChild>
                    <Button
                      size="lg"
                      variant="outline"
                      className="w-full justify-between"
                      aria-expanded={openModel}
                      role="combobox"
                    >
                      <code>{tempModel.label}</code>
                      <ChevronsUpDown className="ml-2 size-4 shrink-0 opacity-50" />
                    </Button>
                  </PopoverTrigger>
                  <PopoverContent className="w-full p-0">
                    <Command>
                      <CommandInput placeholder="Search model..." />
                      <CommandEmpty>No model found.</CommandEmpty>
                      <CommandList>
                        <CommandGroup>
                          {models.map((m) => (
                            <CommandItem
                              key={m.value}
                              value={m.value}
                              onSelect={() => {
                                setTempModel(m);
                                setOpenModel(false);
                              }}
                            >
                              <Check
                                className={cn(
                                  'mr-2 size-4',
                                  tempModel.value === m.value
                                    ? 'opacity-100'
                                    : 'opacity-0'
                                )}
                              />
                              <code>{m.label}</code>
                            </CommandItem>
                          ))}
                        </CommandGroup>
                      </CommandList>
                    </Command>
                  </PopoverContent>
                </Popover>
              </div>
            </div>
          </div>

          {/* Upload Settings Group */}
          {/* <div className="space-y-4">
            <div className="flex items-center gap-2">
              <div className="size-8 rounded-full bg-red-100 p-2 dark:bg-red-900">
                <Upload className="size-4 text-red-600 dark:text-red-400" />
              </div>
              <h4 className="font-semibold">Upload</h4>
            </div>

            <div className="space-y-4">
              {renderApiKeyInput('uploadthing', 'Uploadthing API key')}
            </div>
          </div> */}

          <Button size="lg" className="w-full" type="submit">
            Save changes
          </Button>
        </form>

        <p className="text-muted-foreground text-sm">
          Not stored anywhere. Used only for current session requests.
        </p>
      </DialogContent>
    </Dialog>
  );
}

Installation

npx shadcn@latest add @plate/settings-dialog

Usage

import { SettingsDialog } from "@/components/settings-dialog"
<SettingsDialog />