"use client"; import { useAtomValue } from "jotai"; import { AlertCircle, Dot, Edit3, Info, RefreshCw, Trash2 } from "lucide-react"; import { useMemo, useState } from "react"; import { membersAtom, myAccessAtom } from "@/atoms/members/members-query.atoms"; import { deleteVisionLLMConfigMutationAtom } from "@/atoms/vision-llm-config/vision-llm-config-mutation.atoms"; import { globalVisionLLMConfigsAtom, visionLLMConfigsAtom, } from "@/atoms/vision-llm-config/vision-llm-config-query.atoms"; import { VisionConfigDialog } from "@/components/shared/vision-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 { VisionLLMConfig } 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 VisionModelManagerProps { 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 VisionModelManager({ searchSpaceId }: VisionModelManagerProps) { const isDesktop = useMediaQuery("(min-width: 768px)"); const { mutateAsync: deleteConfig, isPending: isDeleting, error: deleteError, } = useAtomValue(deleteVisionLLMConfigMutationAtom); const { data: userConfigs, isFetching: configsLoading, error: fetchError, refetch: refreshConfigs, } = useAtomValue(visionLLMConfigsAtom); const { data: globalConfigs = [], isFetching: globalLoading } = useAtomValue( globalVisionLLMConfigsAtom ); 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 = useMemo(() => { if (!access) return false; if (access.is_owner) return true; return access.permissions?.includes("vision_configs:create") ?? false; }, [access]); const canDelete = useMemo(() => { if (!access) return false; if (access.is_owner) return true; return access.permissions?.includes("vision_configs:delete") ?? false; }, [access]); 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: VisionLLMConfig) => { 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 (
{canCreate && ( )}
{errors.map((err) => (
{err?.message}
))} {access && !isLoading && isReadOnly && (
You have read-only access to vision model 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 ")}{" "} vision model configurations {!canDelete && ", but cannot delete them"}.
)} {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 vision{" "} {globalConfigs.filter((g) => !("is_auto_mode" in g && g.is_auto_mode)).length === 1 ? "model" : "models"} {" "} available from your administrator.

)} {isLoading && (
{["skeleton-a", "skeleton-b", "skeleton-c"].map((key) => (
))}
)} {!isLoading && (
{(userConfigs?.length ?? 0) === 0 ? (

No Vision Models Yet

{canCreate ? "Add your own vision-capable model (GPT-4o, Claude, Gemini, etc.)" : "No vision 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}
)}
); })}
)}
)} { setIsDialogOpen(open); if (!open) setEditingConfig(null); }} config={editingConfig} isGlobal={false} searchSpaceId={searchSpaceId} mode={editingConfig ? "edit" : "create"} /> !open && setConfigToDelete(null)} > Delete Vision Model Are you sure you want to delete{" "} {configToDelete?.name}? Cancel Delete {isDeleting && }
); }