import { useState, useRef, useCallback, useEffect } from "react"; import SectionCard from "./SectionCard"; import Toggle from "./Toggle"; import ApiKeyField from "./ApiKeyField"; import SaveToast from "./SaveToast"; import useSettings from "../../hooks/useSettings"; const PROVIDERS_MAP = { pexels: { id: "pexels", label: "Pexels", description: "Stock photos from Pexels. Works with default key or add your own.", apiKey: true, settingKey: "pexels_api_key", labelKey: "Pexels API Key (Optional)", helpText: "Pexels works with a default key. Leave empty to use default or provide your own.", helpUrl: "https://www.pexels.com/api/", placeholder: "Leave empty for default key", provider: "pexels", }, pixabay: { id: "pixabay", label: "Pixabay", description: "Images and videos from Pixabay. Works with default key or add your own.", apiKey: true, settingKey: "pixabay_api_key", labelKey: "Pixabay API Key (Optional)", helpText: "Pixabay works with a default key. Leave empty to use default or provide your own.", helpUrl: "https://pixabay.com/api/docs/", placeholder: "Leave empty for default key", provider: "pixabay", }, unsplash: { id: "unsplash", label: "Unsplash", description: "High-quality photos from Unsplash. Works with default key or add your own.", apiKey: true, settingKey: "unsplash_access_key", labelKey: "Unsplash Access Key (Optional)", helpText: "Unsplash works with a default key. Leave empty to use default or provide your own.", helpUrl: "https://unsplash.com/developers", placeholder: "Leave empty for default key", provider: "unsplash", }, openverse: { id: "openverse", label: "Openverse", description: "Search openly licensed images. No API key required." }, wikimedia: { id: "wikimedia", label: "Wikimedia", description: "Openly licensed images from Wikimedia Commons. No API key required.", }, giphy: { id: "giphy", label: "Giphy", description: "GIFs and stickers from GIPHY. Works with default key or add your own.", apiKey: true, settingKey: "giphy_api_key", labelKey: "Giphy API Key (Optional)", helpText: "Giphy works with a default key. Leave empty to use default or provide your own.", helpUrl: "https://developers.giphy.com/", placeholder: "Leave empty for default key", provider: "giphy", }, openai: { id: "openai", label: "OpenAI", description: "Generate images with GPT Image.", apiKey: true, settingKey: "openai_api_key", labelKey: "OpenAI API Key", helpText: "Get your API key from OpenAI.", helpUrl: "https://platform.openai.com/api-keys", placeholder: "sk-...", provider: "openai", }, gemini: { id: "gemini", label: "Google Gemini", description: "Generate images with Google's Imagen 4.0 AI.", apiKey: true, settingKey: "gemini_api_key", labelKey: "Gemini API Key", helpText: "Get your free API key from Google AI Studio.", helpUrl: "https://aistudio.google.com/app/apikey", placeholder: "AIza...", provider: "gemini", }, }; const GripIcon = () => ( ); const ArrowUpIcon = () => ( ); const ArrowDownIcon = () => ( ); export default function ImageSourcesSection() { const { settings, setSetting, providerEnabled, setProvider, providerOrder, setProviderOrder, saveSettings, testKey, saving, resetProviderSettings, } = useSettings(); const [saveMessage, setSaveMessage] = useState({ type: null, text: "" }); // Drag state const [draggedId, setDraggedId] = useState(null); const [dragOverId, setDragOverId] = useState(null); const dragCounter = useRef(0); const handleReset = () => { if (confirm("Reset image sources settings to default values? This will reset enabled providers and their order. This will not save automatically.")) { resetProviderSettings(); setSaveMessage({ type: "success", text: "Provider settings reset to defaults. Click 'Save Changes' to apply." }); } }; const handleSave = async () => { setSaveMessage({ type: null, text: "" }); const result = await saveSettings("sources"); setSaveMessage({ type: result.success ? "success" : "error", text: result.message, }); }; // Toast handles auto-dismiss // Move provider up/down in order const moveProvider = useCallback( (id, direction) => { setProviderOrder((prev) => { const idx = prev.indexOf(id); if (idx === -1) return prev; const newIdx = direction === "up" ? idx - 1 : idx + 1; if (newIdx < 0 || newIdx >= prev.length) return prev; const updated = [...prev]; [updated[idx], updated[newIdx]] = [updated[newIdx], updated[idx]]; return updated; }); }, [setProviderOrder] ); // Drag handlers const handleDragStart = useCallback((e, id) => { setDraggedId(id); e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("text/plain", id); // Make drag ghost semi-transparent if (e.target) { setTimeout(() => { e.target.style.opacity = "0.5"; }, 0); } }, []); const handleDragEnd = useCallback((e) => { e.target.style.opacity = "1"; setDraggedId(null); setDragOverId(null); dragCounter.current = 0; }, []); const handleDragEnter = useCallback((e, id) => { e.preventDefault(); dragCounter.current++; setDragOverId(id); }, []); const handleDragLeave = useCallback(() => { dragCounter.current--; if (dragCounter.current <= 0) { setDragOverId(null); dragCounter.current = 0; } }, []); const handleDragOver = useCallback((e) => { e.preventDefault(); e.dataTransfer.dropEffect = "move"; }, []); const handleDrop = useCallback( (e, targetId) => { e.preventDefault(); const sourceId = e.dataTransfer.getData("text/plain"); if (!sourceId || sourceId === targetId) { setDragOverId(null); dragCounter.current = 0; return; } setProviderOrder((prev) => { const sourceIdx = prev.indexOf(sourceId); const targetIdx = prev.indexOf(targetId); if (sourceIdx === -1 || targetIdx === -1) return prev; const updated = [...prev]; updated.splice(sourceIdx, 1); updated.splice(targetIdx, 0, sourceId); return updated; }); setDragOverId(null); dragCounter.current = 0; }, [setProviderOrder] ); // Build ordered providers list const orderedProviders = providerOrder .filter((id) => PROVIDERS_MAP[id]) .map((id) => PROVIDERS_MAP[id]); const globalEnabled = providerEnabled.global !== false; return (
{/* Global Search - fixed at top, not draggable */}

Search across all enabled providers at once. When disabled, the Global Search tab will be hidden from the Image Generator.

setProvider("global", on)} id="provider-global" ariaLabel="Global Search" />
{/* Image Providers section */}

Image Providers

{orderedProviders.map((provider, index) => { const enabled = providerEnabled[provider.id] !== false; const isDragging = draggedId === provider.id; const isDragOver = dragOverId === provider.id && draggedId !== provider.id; return (
handleDragStart(e, provider.id)} onDragEnd={handleDragEnd} onDragEnter={(e) => handleDragEnter(e, provider.id)} onDragLeave={handleDragLeave} onDragOver={handleDragOver} onDrop={(e) => handleDrop(e, provider.id)} className={`rounded-xl border p-5 transition-all bg-white ${ isDragging ? "opacity-50 border-gray-200 bg-gray-50/50" : isDragOver ? "border-gray-400 ring-2 ring-gray-100" : "border-gray-200 hover:border-gray-300" }`} style={{ cursor: "grab" }} >
{/* Drag handle + Order controls */}
{/* Position indicator */} {index + 1}
{/* Provider info */}

{provider.label}

{provider.description}

{/* Toggle */} setProvider(provider.id, on)} id={`provider-${provider.id}`} ariaLabel={`Enable ${provider.label}`} />
{provider.apiKey && enabled && (
setSetting(provider.settingKey, v)} onTest={testKey} provider={provider.provider} helpText={provider.helpText} helpUrl={provider.helpUrl} placeholder={provider.placeholder || ""} />
)}
); })}
setSaveMessage({ type: null, text: "" })} />
); }