"use client"; import { useAtomValue } from "jotai"; import { AlertCircle, Edit3, FileText, Info, MessageSquareQuote, Plus, RefreshCw, Trash2, Wand2, } from "lucide-react"; import { AnimatePresence, motion } from "motion/react"; import Image from "next/image"; import { useCallback, useMemo, useState } from "react"; import { membersAtom, myAccessAtom } from "@/atoms/members/members-query.atoms"; import { createNewLLMConfigMutationAtom, deleteNewLLMConfigMutationAtom, updateNewLLMConfigMutationAtom, } from "@/atoms/new-llm-config/new-llm-config-mutation.atoms"; import { globalNewLLMConfigsAtom, newLLMConfigsAtom, } from "@/atoms/new-llm-config/new-llm-config-query.atoms"; import { LLMConfigForm, type LLMConfigFormData } from "@/components/shared/llm-config-form"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Skeleton } from "@/components/ui/skeleton"; import { Spinner } from "@/components/ui/spinner"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import type { NewLLMConfig } from "@/contracts/types/new-llm-config.types"; import { getProviderIcon } from "@/lib/provider-icons"; import { cn } from "@/lib/utils"; interface ModelConfigManagerProps { searchSpaceId: number; } const container = { hidden: { opacity: 0 }, show: { opacity: 1, transition: { staggerChildren: 0.05, }, }, }; const item = { hidden: { opacity: 0, y: 20 }, show: { opacity: 1, y: 0 }, }; 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 ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) { // Mutations const { mutateAsync: createConfig, isPending: isCreating } = useAtomValue( createNewLLMConfigMutationAtom ); const { mutateAsync: updateConfig, isPending: isUpdating } = useAtomValue( updateNewLLMConfigMutationAtom ); const { mutateAsync: deleteConfig, isPending: isDeleting } = useAtomValue( deleteNewLLMConfigMutationAtom ); // Queries const { data: configs, isFetching: isLoading, error: fetchError, refetch: refreshConfigs, } = useAtomValue(newLLMConfigsAtom); const { data: globalConfigs = [] } = useAtomValue(globalNewLLMConfigsAtom); // Members for user resolution 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]); // Permissions const { data: access } = useAtomValue(myAccessAtom); const canCreate = useMemo(() => { if (!access) return false; if (access.is_owner) return true; return access.permissions?.includes("llm_configs:create") ?? false; }, [access]); const canUpdate = useMemo(() => { if (!access) return false; if (access.is_owner) return true; return access.permissions?.includes("llm_configs:update") ?? false; }, [access]); const canDelete = useMemo(() => { if (!access) return false; if (access.is_owner) return true; return access.permissions?.includes("llm_configs:delete") ?? false; }, [access]); const isReadOnly = !canCreate && !canUpdate && !canDelete; // Local state const [isDialogOpen, setIsDialogOpen] = useState(false); const [editingConfig, setEditingConfig] = useState(null); const [configToDelete, setConfigToDelete] = useState(null); const isSubmitting = isCreating || isUpdating; const handleFormSubmit = useCallback( async (formData: LLMConfigFormData) => { try { if (editingConfig) { const { search_space_id, ...updateData } = formData; await updateConfig({ id: editingConfig.id, data: updateData, }); } else { await createConfig(formData); } setIsDialogOpen(false); setEditingConfig(null); } catch { // Error is displayed inside the dialog by the form } }, [editingConfig, createConfig, updateConfig] ); const handleDelete = async () => { if (!configToDelete) return; try { await deleteConfig({ id: configToDelete.id }); setConfigToDelete(null); } catch { // Error handled by mutation state } }; const openEditDialog = (config: NewLLMConfig) => { setEditingConfig(config); setIsDialogOpen(true); }; const openNewDialog = () => { setEditingConfig(null); setIsDialogOpen(true); }; const closeDialog = () => { setIsDialogOpen(false); setEditingConfig(null); }; return (
{/* Header actions */}
{canCreate && ( )}
{/* Fetch Error Alert */} {fetchError && ( {fetchError?.message ?? "Failed to load configurations"} )} {/* Read-only / Limited permissions notice */} {access && !isLoading && isReadOnly && ( You have read-only access to LLM configurations. Contact a space owner to request additional permissions. )} {access && !isLoading && !isReadOnly && (!canCreate || !canUpdate || !canDelete) && ( You can{" "} {[canCreate && "create", canUpdate && "edit", canDelete && "delete"] .filter(Boolean) .join(" and ")}{" "} configurations {!canDelete && ", but cannot delete them"}. )} {/* Global Configs Info */} {globalConfigs.length > 0 && ( {globalConfigs.length} global configuration(s){" "} available from your administrator. These are pre-configured and ready to use.{" "} Global configs: {globalConfigs.map((g) => g.name).join(", ")} )} {/* Loading Skeleton */} {isLoading && (
{["skeleton-a", "skeleton-b", "skeleton-c"].map((key) => ( {/* Header */}
{/* Provider + Model */}
{/* Feature badges */}
{/* Footer */}
))}
)} {/* Configurations List */} {!isLoading && (
{configs?.length === 0 ? (

No Configurations Yet

{canCreate ? "Create your first AI configuration to customize how your agent responds" : "No AI configurations have been added to this space yet. Contact a space owner to add one."}

{canCreate && ( )}
) : ( {configs?.map((config) => { const member = config.user_id ? memberMap.get(config.user_id) : null; return ( {/* Header: Name + Actions */}

{config.name}

{config.description && (

{config.description}

)}
{(canUpdate || canDelete) && (
{canUpdate && ( Edit )} {canDelete && ( Delete )}
)}
{/* Provider + Model */}
{getProviderIcon(config.provider, { className: "size-3.5 shrink-0" })} {config.model_name}
{/* Feature badges */}
{config.citations_enabled && ( Citations )} {!config.use_default_system_instructions && config.system_instructions && ( Custom )}
{/* Footer: Date + Creator */}
{new Date(config.created_at).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric", })} {member && ( <> ยท
{member.avatarUrl ? ( {member.name} ) : (
{getInitials(member.name)}
)} {member.name}
{member.email || member.name}
)}
); })}
)}
)} {/* Add/Edit Configuration Dialog */} !open && closeDialog()}> e.preventDefault()} > {editingConfig ? "Edit Configuration" : "Create New Configuration"} {editingConfig ? "Update your AI model and prompt configuration" : "Set up a new AI model with custom prompts and citation settings"} {/* Delete Confirmation Dialog */} !open && setConfigToDelete(null)} > Delete Configuration Are you sure you want to delete{" "} {configToDelete?.name}? This action cannot be undone. Cancel {isDeleting ? ( <> Deleting ) : ( <> Delete )}
); }