"use client"; import { useAtomValue } from "jotai"; import { AlertCircle, Dot, Edit3, Info, 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 { 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 */}
{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.

)} {/* 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 && }
); }