"use client"; import { useEffect, useRef, useState } from "react"; import { Check, ChevronUp, Eye, EyeOff, Loader2 } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "@/components/ui/command"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { cn } from "@/lib/utils"; import { getApiErrorMessage, getApiUrl } from "@/utils/api"; import { notify } from "@/components/ui/sonner"; export interface OpenAICompatibleImageFieldsProps { baseUrl: string; apiKey: string; model: string; onBaseUrlChange: (value: string) => void; onApiKeyChange: (value: string) => void; onModelChange: (value: string) => void; /** Settings page: report model list state so the parent can render the yellow banner below the card (like TextProvider). */ onModelListMetaChange?: (meta: { modelsChecked: boolean; modelCount: number }) => void; /** * `textProviderSettings` — same column widths, field order, inputs, pill button, and model row * as {@link TextProvider} when LLM is Custom (settings page only). * `stacked` — full-width onboarding / LLM selection layout (CustomConfig-style blocks). */ layout?: "stacked" | "textProviderSettings"; } /** * Image provider "Custom" (OpenAI-compatible). Styling matches Text settings Custom or onboarding stacked layout. */ export default function OpenAICompatibleImageFields({ baseUrl, apiKey, model, onBaseUrlChange, onApiKeyChange, onModelChange, onModelListMetaChange, layout = "stacked", }: OpenAICompatibleImageFieldsProps) { const [models, setModels] = useState([]); const [modelsLoading, setModelsLoading] = useState(false); const [modelsChecked, setModelsChecked] = useState(false); const [openModelSelect, setOpenModelSelect] = useState(false); const [showApiKey, setShowApiKey] = useState(false); const skipUrlKeyResetOnce = useRef(true); const urlKey = `${baseUrl}|${apiKey}`; useEffect(() => { if (skipUrlKeyResetOnce.current) { skipUrlKeyResetOnce.current = false; return; } setModels([]); setModelsChecked(false); onModelChange(""); // eslint-disable-next-line react-hooks/exhaustive-deps }, [urlKey]); const fetchModels = async () => { if (!baseUrl.trim()) return; setModelsLoading(true); try { const response = await fetch(getApiUrl("/api/v1/ppt/openai/models/available"), { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ url: baseUrl.trim(), api_key: apiKey, }), }); if (response.ok) { const data = await response.json(); setModels(Array.isArray(data) ? data : []); setModelsChecked(true); } else { const message = await getApiErrorMessage( response, "The server could not list models. Check your API key or endpoint and try again." ); console.error("Failed to fetch models"); setModels([]); setModelsChecked(true); notify.error( "Could not load models", message ); } } catch (error) { console.error("Error fetching models:", error); notify.error( "Could not load models", "Something went wrong while contacting the provider. Check your network and try again." ); setModels([]); setModelsChecked(true); } finally { setModelsLoading(false); } }; useEffect(() => { if (layout !== "textProviderSettings" || !onModelListMetaChange) return; onModelListMetaChange({ modelsChecked, modelCount: models.length }); }, [layout, modelsChecked, models.length, onModelListMetaChange]); if (layout === "textProviderSettings") { return (
onApiKeyChange(e.target.value)} className="w-full rounded-lg border border-gray-300 px-2 py-3 outline-none transition-colors focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20" placeholder="Key for your image endpoint" />
onBaseUrlChange(e.target.value)} className="mt-2 w-full rounded-lg border border-gray-300 px-2 py-3 outline-none transition-colors focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20" placeholder="Base URL (include /v1)" />
{(!modelsChecked || (modelsChecked && models.length === 0)) && ( )}
{modelsChecked && models.length > 0 ? (
No model found. {models.map((m) => ( { onModelChange(m); setOpenModelSelect(false); }} >
{m}
))}
) : null}
); } /* ----- stacked (onboarding / ImageSelectionConfig) ----- */ return (

Use an endpoint that supports OpenAI-style{" "} /v1/images/generations. Include{" "} /v1 in the URL.

onBaseUrlChange(e.target.value)} />
onApiKeyChange(e.target.value)} />
{(!modelsChecked || (modelsChecked && models.length === 0)) && (
)} {modelsChecked && models.length === 0 && (

No models found. Please make sure your API key is valid and has access to models.

)} {modelsChecked && models.length === 0 && (
onModelChange(e.target.value)} />
)} {modelsChecked && models.length > 0 && (

Important: Choose a model your server exposes for image generation.

No model found. {models.map((m, index) => ( { onModelChange(value); setOpenModelSelect(false); }} > {m} ))}
)}
); }