"use client"; import { useAtomValue } from "jotai"; import { AlertCircle, Dot, Info, Pencil, RefreshCw, Trash2 } from "lucide-react"; import { useMemo, useState } from "react"; import { deleteImageGenConfigMutationAtom } from "@/atoms/image-gen-config/image-gen-config-mutation.atoms"; import { globalImageGenConfigsAtom, imageGenConfigsAtom, } from "@/atoms/image-gen-config/image-gen-config-query.atoms"; import { membersAtom, myAccessAtom } from "@/atoms/members/members-query.atoms"; import { ImageConfigDialog } from "@/components/shared/image-config-dialog"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Skeleton } from "@/components/ui/skeleton"; import { Spinner } from "@/components/ui/spinner"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import type { ImageGenerationConfig } from "@/contracts/types/new-llm-config.types"; import { useMediaQuery } from "@/hooks/use-media-query"; import { getProviderIcon } from "@/lib/provider-icons"; import { cn } from "@/lib/utils"; interface ImageModelManagerProps { searchSpaceId: number; } function getInitials(name: string): string { const parts = name.trim().split(/\s+/); if (parts.length >= 2) { return (parts[0][0] + parts[1][0]).toUpperCase(); } return name.slice(0, 2).toUpperCase(); } export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) { const isDesktop = useMediaQuery("(min-width: 768px)"); const { mutateAsync: deleteConfig, isPending: isDeleting, error: deleteError, } = useAtomValue(deleteImageGenConfigMutationAtom); const { data: userConfigs, isFetching: configsLoading, error: fetchError, refetch: refreshConfigs, } = useAtomValue(imageGenConfigsAtom); const { data: globalConfigs = [], isFetching: globalLoading } = useAtomValue(globalImageGenConfigsAtom); const { data: members } = useAtomValue(membersAtom); const memberMap = useMemo(() => { const map = new Map(); if (members) { for (const m of members) { map.set(m.user_id, { name: m.user_display_name || m.user_email || "Unknown", email: m.user_email || undefined, avatarUrl: m.user_avatar_url || undefined, }); } } return map; }, [members]); const { data: access } = useAtomValue(myAccessAtom); const canCreate = !!access && (access.is_owner || (access.permissions?.includes("image_generations:create") ?? false)); const canDelete = !!access && (access.is_owner || (access.permissions?.includes("image_generations:delete") ?? false)); const canUpdate = canCreate; const isReadOnly = !canCreate && !canDelete; const [isDialogOpen, setIsDialogOpen] = useState(false); const [editingConfig, setEditingConfig] = useState(null); const [configToDelete, setConfigToDelete] = useState(null); const isLoading = configsLoading || globalLoading; const errors = [deleteError, fetchError].filter(Boolean) as Error[]; const openEditDialog = (config: ImageGenerationConfig) => { setEditingConfig(config); setIsDialogOpen(true); }; const openNewDialog = () => { setEditingConfig(null); setIsDialogOpen(true); }; const handleDelete = async () => { if (!configToDelete) return; try { await deleteConfig({ id: configToDelete.id, name: configToDelete.name }); setConfigToDelete(null); } catch { // Error handled by mutation } }; return (
{/* Header actions */}
{canCreate && ( )}
{/* Errors */} {errors.map((err) => (
{err?.message}
))} {/* Read-only / Limited permissions notice */} {access && !isLoading && isReadOnly && (
You have read-only access to image generation configurations. Contact a space owner to request additional permissions.
)} {access && !isLoading && !isReadOnly && (!canCreate || !canDelete) && (
You can{" "} {[canCreate && "create and edit", canDelete && "delete"] .filter(Boolean) .join(" and ")}{" "} image model configurations {!canDelete && ", but cannot delete them"}.
)} {/* Global info */} {globalConfigs.filter((g) => !("is_auto_mode" in g && g.is_auto_mode)).length > 0 && (

{globalConfigs.filter((g) => !("is_auto_mode" in g && g.is_auto_mode)).length}{" "} global image{" "} {globalConfigs.filter((g) => !("is_auto_mode" in g && g.is_auto_mode)).length === 1 ? "model" : "models"} {" "} available from your administrator. {(() => { const nonAuto = globalConfigs.filter( (g) => !("is_auto_mode" in g && g.is_auto_mode) ); const premium = nonAuto.filter( (g) => "billing_tier" in g && (g as { billing_tier?: string }).billing_tier === "premium" ).length; const free = nonAuto.length - premium; if (premium > 0 && free > 0) { return `${premium} premium, ${free} free.`; } if (premium > 0) { return `All ${premium} premium — debits your shared credit pool.`; } return `All ${free} free.`; })()}

)} {/* Global Image Models — read-only cards with per-model Free/Premium badges. Mirrors the badge palette used by the chat role selector (`llm-role-manager.tsx`) so the meaning is consistent across every model-configuration surface (chat / image / vision). */} {!isLoading && globalConfigs.filter((g) => !("is_auto_mode" in g && g.is_auto_mode)).length > 0 && (

Global Image Models

{globalConfigs .filter((g) => !("is_auto_mode" in g && g.is_auto_mode)) .map((cfg) => { const billingTier = ("billing_tier" in cfg && typeof (cfg as { billing_tier?: string }).billing_tier === "string" && (cfg as { billing_tier?: string }).billing_tier) || "free"; const isPremium = billingTier === "premium"; return (
{getProviderIcon(cfg.provider, { className: "size-4" })}

{cfg.name}

{isPremium ? ( Premium ) : ( Free )}
{cfg.description && (

{cfg.description}

)}
{cfg.model_name}
); })}
)} {/* Loading Skeleton */} {isLoading && (
{["skeleton-a", "skeleton-b", "skeleton-c"].map((key) => (
))}
)} {/* User Configs */} {!isLoading && (
{(userConfigs?.length ?? 0) === 0 ? (

No Image Models Yet

{canCreate ? "Add your own image generation model (DALL-E 3, GPT Image 1, etc.)" : "No image models have been added to this space yet. Contact a space owner to add one."}

) : (
{userConfigs?.map((config) => { const member = config.user_id ? memberMap.get(config.user_id) : null; return (
{/* Header: Icon + Name + Actions */}
{getProviderIcon(config.provider, { className: "size-4" })}

{config.name}

{config.description && (

{config.description}

)}
{(canUpdate || canDelete) && (
{canUpdate && ( Edit )} {canDelete && ( Delete )}
)}
{/* Footer: Date + Creator */}
{new Date(config.created_at).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric", })} {member && ( <>
{member.avatarUrl && ( )} {getInitials(member.name)} {member.name}
{member.email || member.name}
)}
); })}
)}
)} {/* Create/Edit Dialog — shared component */} { setIsDialogOpen(open); if (!open) setEditingConfig(null); }} config={editingConfig} isGlobal={false} searchSpaceId={searchSpaceId} mode={editingConfig ? "edit" : "create"} /> {/* Delete Confirmation */} !open && setConfigToDelete(null)} > Delete Image Model Are you sure you want to delete{" "} {configToDelete?.name}? Cancel Delete {isDeleting && }
); }