Spaces:
Runtime error
Runtime error
| 'use client'; | |
| import * as React from 'react'; | |
| import { Settings, Eye, EyeOff, Check, Trash2, Key } from 'lucide-react'; | |
| import { Button } from '@/components/ui/button'; | |
| import { Input } from '@/components/ui/input'; | |
| import { Label } from '@/components/ui/label'; | |
| import { | |
| Sheet, | |
| SheetContent, | |
| SheetDescription, | |
| SheetHeader, | |
| SheetTitle, | |
| SheetTrigger, | |
| } from '@/components/ui/sheet'; | |
| import { useApiKeys } from '@/contexts/api-keys-context'; | |
| import { PROVIDER_CONFIGS, type ImageProvider } from '@/lib/api-config'; | |
| import { Icons } from './icons'; | |
| import { cn } from '@/lib/utils'; | |
| interface ApiKeyInputProps { | |
| provider: ImageProvider; | |
| label: string; | |
| placeholder: string; | |
| icon: React.ComponentType<{ className?: string }>; | |
| } | |
| function ApiKeyInput({ provider, label, placeholder, icon: Icon }: ApiKeyInputProps) { | |
| const { getApiKey, setApiKey, clearApiKey, hasKey } = useApiKeys(); | |
| const [showKey, setShowKey] = React.useState(false); | |
| const [localValue, setLocalValue] = React.useState(''); | |
| const [isSaved, setIsSaved] = React.useState(false); | |
| // Sync local value with context on mount and when context changes | |
| React.useEffect(() => { | |
| const contextValue = getApiKey(provider); | |
| setLocalValue(contextValue); | |
| setIsSaved(contextValue.length > 0); | |
| }, [getApiKey, provider]); | |
| const handleSave = () => { | |
| setApiKey(provider, localValue); | |
| setIsSaved(true); | |
| setTimeout(() => setIsSaved(false), 2000); | |
| }; | |
| const handleClear = () => { | |
| setLocalValue(''); | |
| clearApiKey(provider); | |
| }; | |
| const handleKeyDown = (e: React.KeyboardEvent) => { | |
| if (e.key === 'Enter') { | |
| handleSave(); | |
| } | |
| }; | |
| const isConfigured = hasKey(provider); | |
| return ( | |
| <div className="space-y-2"> | |
| <div className="flex items-center gap-2"> | |
| <Icon className="h-4 w-4 text-muted-foreground" /> | |
| <Label htmlFor={`api-key-${provider}`} className="text-sm font-medium"> | |
| {label} | |
| </Label> | |
| {isConfigured && ( | |
| <span className="ml-auto flex items-center gap-1 text-xs text-green-600 dark:text-green-400"> | |
| <Check className="h-3 w-3" /> | |
| Configured | |
| </span> | |
| )} | |
| </div> | |
| <div className="flex gap-2"> | |
| <div className="relative flex-1"> | |
| <Input | |
| id={`api-key-${provider}`} | |
| type={showKey ? 'text' : 'password'} | |
| value={localValue} | |
| onChange={(e) => setLocalValue(e.target.value)} | |
| onKeyDown={handleKeyDown} | |
| placeholder={placeholder} | |
| className="pr-10 font-mono text-sm" | |
| /> | |
| <Button | |
| type="button" | |
| variant="ghost" | |
| size="icon" | |
| className="absolute right-1 top-1/2 h-7 w-7 -translate-y-1/2" | |
| onClick={() => setShowKey(!showKey)} | |
| > | |
| {showKey ? ( | |
| <EyeOff className="h-4 w-4 text-muted-foreground" /> | |
| ) : ( | |
| <Eye className="h-4 w-4 text-muted-foreground" /> | |
| )} | |
| </Button> | |
| </div> | |
| <Button | |
| type="button" | |
| variant="outline" | |
| size="icon" | |
| onClick={handleSave} | |
| disabled={localValue === getApiKey(provider)} | |
| className={cn(isSaved && 'bg-green-100 dark:bg-green-900')} | |
| > | |
| <Check className={cn('h-4 w-4', isSaved && 'text-green-600')} /> | |
| </Button> | |
| <Button | |
| type="button" | |
| variant="outline" | |
| size="icon" | |
| onClick={handleClear} | |
| disabled={!localValue} | |
| > | |
| <Trash2 className="h-4 w-4 text-muted-foreground" /> | |
| </Button> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| export function SettingsPanel() { | |
| const { isOpen, setIsOpen, hasKey } = useApiKeys(); | |
| const configuredCount = [ | |
| hasKey('openai'), | |
| hasKey('google'), | |
| hasKey('qwen'), | |
| ].filter(Boolean).length; | |
| return ( | |
| <Sheet open={isOpen} onOpenChange={setIsOpen}> | |
| <SheetTrigger asChild> | |
| <Button variant="ghost" size="icon" className="relative"> | |
| <Settings className="h-5 w-5" /> | |
| {configuredCount > 0 && ( | |
| <span className="absolute -right-1 -top-1 flex h-4 w-4 items-center justify-center rounded-full bg-primary text-[10px] font-medium text-primary-foreground"> | |
| {configuredCount} | |
| </span> | |
| )} | |
| <span className="sr-only">Settings</span> | |
| </Button> | |
| </SheetTrigger> | |
| <SheetContent side="left" className="w-[400px] sm:w-[450px]"> | |
| <SheetHeader> | |
| <SheetTitle className="flex items-center gap-2"> | |
| <Key className="h-5 w-5" /> | |
| API Keys | |
| </SheetTitle> | |
| <SheetDescription> | |
| Configure your API keys for image generation. Keys are stored locally in your browser and never sent to our servers. | |
| </SheetDescription> | |
| </SheetHeader> | |
| <div className="mt-6 space-y-6"> | |
| <ApiKeyInput | |
| provider="openai" | |
| label="OpenAI API Key" | |
| placeholder="sk-..." | |
| icon={Icons.openai} | |
| /> | |
| <ApiKeyInput | |
| provider="google" | |
| label="Google Gemini API Key" | |
| placeholder="AIza..." | |
| icon={Icons.google} | |
| /> | |
| <ApiKeyInput | |
| provider="qwen" | |
| label="Qwen (DashScope) API Key" | |
| placeholder="sk-..." | |
| icon={Icons.qwen} | |
| /> | |
| <div className="rounded-lg border bg-muted/50 p-4"> | |
| <h4 className="text-sm font-medium">How to get API keys:</h4> | |
| <ul className="mt-2 space-y-1 text-xs text-muted-foreground"> | |
| <li> | |
| <strong>OpenAI:</strong>{' '} | |
| <a | |
| href="https://platform.openai.com/api-keys" | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className="text-primary hover:underline" | |
| > | |
| platform.openai.com/api-keys | |
| </a> | |
| </li> | |
| <li> | |
| <strong>Google:</strong>{' '} | |
| <a | |
| href="https://aistudio.google.com/apikey" | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className="text-primary hover:underline" | |
| > | |
| aistudio.google.com/apikey | |
| </a> | |
| </li> | |
| <li> | |
| <strong>Qwen:</strong>{' '} | |
| <a | |
| href="https://dashscope.console.aliyun.com/" | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className="text-primary hover:underline" | |
| > | |
| dashscope.console.aliyun.com | |
| </a> | |
| </li> | |
| </ul> | |
| </div> | |
| </div> | |
| </SheetContent> | |
| </Sheet> | |
| ); | |
| } | |