SurfSense/surfsense_web/components/settings/model-config-manager.tsx

503 lines
18 KiB
TypeScript
Raw Normal View History

2025-06-09 15:50:15 -07:00
"use client";
2025-12-11 13:42:33 +02:00
import { useAtomValue } from "jotai";
2025-07-27 10:05:37 -07:00
import {
2025-07-27 10:41:15 -07:00
AlertCircle,
Bot,
Clock,
Edit3,
2025-12-23 01:16:25 -08:00
FileText,
MessageSquareQuote,
2025-07-27 10:41:15 -07:00
Plus,
RefreshCw,
2025-12-23 01:16:25 -08:00
Sparkles,
2025-07-27 10:41:15 -07:00
Trash2,
2025-12-23 01:16:25 -08:00
Wand2,
2025-07-27 10:41:15 -07:00
} from "lucide-react";
2025-09-30 21:53:10 -07:00
import { AnimatePresence, motion } from "motion/react";
2025-12-23 01:16:25 -08:00
import { useCallback, useState } from "react";
2025-12-11 13:42:33 +02:00
import {
2025-12-23 01:16:25 -08:00
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";
2025-07-27 10:41:15 -07:00
import { Alert, AlertDescription } from "@/components/ui/alert";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
2025-07-27 10:05:37 -07:00
import { Badge } from "@/components/ui/badge";
2025-07-27 10:41:15 -07:00
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
2025-07-27 10:05:37 -07:00
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Spinner } from "@/components/ui/spinner";
2026-01-26 23:32:30 -08:00
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
2025-12-23 01:16:25 -08:00
import type { NewLLMConfig } from "@/contracts/types/new-llm-config.types";
import { cn } from "@/lib/utils";
2025-06-09 15:50:15 -07:00
interface ModelConfigManagerProps {
searchSpaceId: number;
}
2025-12-23 01:16:25 -08:00
const container = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: {
staggerChildren: 0.05,
},
},
};
const item = {
hidden: { opacity: 0, y: 20 },
show: { opacity: 1, y: 0 },
};
export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) {
2025-12-23 01:16:25 -08:00
// Mutations
2025-12-11 13:42:33 +02:00
const {
2025-12-23 01:16:25 -08:00
mutateAsync: createConfig,
isPending: isCreating,
error: createError,
} = useAtomValue(createNewLLMConfigMutationAtom);
2025-12-11 13:42:33 +02:00
const {
2025-12-23 01:16:25 -08:00
mutateAsync: updateConfig,
isPending: isUpdating,
error: updateError,
} = useAtomValue(updateNewLLMConfigMutationAtom);
2025-12-11 13:42:33 +02:00
const {
2025-12-23 01:16:25 -08:00
mutateAsync: deleteConfig,
isPending: isDeleting,
error: deleteError,
} = useAtomValue(deleteNewLLMConfigMutationAtom);
// Queries
2025-12-11 13:42:33 +02:00
const {
2025-12-23 01:16:25 -08:00
data: configs,
isFetching: isLoading,
error: fetchError,
2025-12-11 13:42:33 +02:00
refetch: refreshConfigs,
2025-12-23 01:16:25 -08:00
} = useAtomValue(newLLMConfigsAtom);
const { data: globalConfigs = [] } = useAtomValue(globalNewLLMConfigsAtom);
// Local state
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [editingConfig, setEditingConfig] = useState<NewLLMConfig | null>(null);
const [configToDelete, setConfigToDelete] = useState<NewLLMConfig | null>(null);
const isSubmitting = isCreating || isUpdating;
const errors = [createError, updateError, deleteError, fetchError].filter(Boolean) as Error[];
const handleFormSubmit = useCallback(
async (formData: LLMConfigFormData) => {
try {
if (editingConfig) {
const { search_space_id, ...updateData } = formData;
2025-12-23 01:16:25 -08:00
await updateConfig({
id: editingConfig.id,
data: updateData,
2025-12-23 01:16:25 -08:00
});
} else {
await createConfig(formData);
}
setIsDialogOpen(false);
setEditingConfig(null);
} catch {
// Error handled by mutation
}
},
[editingConfig, createConfig, updateConfig]
);
2025-07-27 10:05:37 -07:00
2025-12-23 01:16:25 -08:00
const handleDelete = async () => {
if (!configToDelete) return;
try {
await deleteConfig({ id: configToDelete.id });
setConfigToDelete(null);
} catch {
// Error handled by mutation
2025-07-27 10:05:37 -07:00
}
};
2025-12-23 01:16:25 -08:00
const openEditDialog = (config: NewLLMConfig) => {
setEditingConfig(config);
setIsDialogOpen(true);
};
2025-12-23 01:16:25 -08:00
const openNewDialog = () => {
setEditingConfig(null);
setIsDialogOpen(true);
2025-07-27 10:05:37 -07:00
};
2025-12-23 01:16:25 -08:00
const closeDialog = () => {
setIsDialogOpen(false);
setEditingConfig(null);
2025-07-27 10:05:37 -07:00
};
return (
<div className="space-y-4 md:space-y-6">
2025-07-27 10:05:37 -07:00
{/* Header */}
<div className="flex flex-col space-y-4 sm:flex-row sm:items-center sm:justify-between sm:space-y-0">
<div className="flex items-center space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => refreshConfigs()}
2025-12-23 01:16:25 -08:00
disabled={isLoading}
className="flex items-center gap-2 text-xs md:text-sm h-8 md:h-9"
2025-07-27 10:05:37 -07:00
>
<RefreshCw className={cn("h-3 w-3 md:h-4 md:w-4", isLoading && "animate-spin")} />
2025-07-27 10:05:37 -07:00
Refresh
</Button>
</div>
</div>
2025-12-23 01:16:25 -08:00
{/* Error Alerts */}
<AnimatePresence>
{errors.length > 0 &&
errors.map((err) => (
2025-12-23 01:16:25 -08:00
<motion.div
key={err?.message ?? `error-${Date.now()}-${Math.random()}`}
2025-12-23 01:16:25 -08:00
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
>
<Alert variant="destructive" className="py-3 md:py-4">
<AlertCircle className="h-3 w-3 md:h-4 md:w-4 shrink-0" />
<AlertDescription className="text-xs md:text-sm">
{err?.message ?? "Something went wrong"}
</AlertDescription>
2025-12-23 01:16:25 -08:00
</Alert>
</motion.div>
))}
</AnimatePresence>
{/* Global Configs Info */}
{globalConfigs.length > 0 && (
<motion.div initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }}>
<Alert className="border-blue-500/30 bg-blue-500/5 py-3 md:py-4">
<Sparkles className="h-3 w-3 md:h-4 md:w-4 text-blue-600 dark:text-blue-400 shrink-0" />
<AlertDescription className="text-blue-800 dark:text-blue-200 text-xs md:text-sm">
2025-12-23 01:16:25 -08:00
<span className="font-medium">{globalConfigs.length} global configuration(s)</span>{" "}
available from your administrator. These are pre-configured and ready to use.{" "}
<span className="text-blue-600 dark:text-blue-300">
Global configs: {globalConfigs.map((g) => g.name).join(", ")}
</span>
</AlertDescription>
</Alert>
</motion.div>
2025-11-14 21:53:46 -08:00
)}
2025-07-27 10:05:37 -07:00
{/* Loading State */}
2025-12-23 01:16:25 -08:00
{isLoading && (
2025-07-27 10:05:37 -07:00
<Card>
<CardContent className="flex items-center justify-center py-10 md:py-16">
<div className="flex flex-col items-center gap-2 md:gap-3">
<Spinner size="md" className="md:h-8 md:w-8 text-muted-foreground" />
<span className="text-xs md:text-sm text-muted-foreground">
Loading configurations...
</span>
2025-07-27 10:05:37 -07:00
</div>
</CardContent>
</Card>
)}
2025-12-23 01:16:25 -08:00
{/* Configurations List */}
{!isLoading && (
<div className="space-y-4 md:space-y-6">
2025-07-27 10:05:37 -07:00
<div className="flex flex-col space-y-4 sm:flex-row sm:items-center sm:justify-between sm:space-y-0">
<h3 className="text-lg md:text-xl font-semibold tracking-tight">Your Configurations</h3>
<Button
onClick={openNewDialog}
className="flex items-center gap-2 text-xs md:text-sm h-8 md:h-9"
>
<Plus className="h-3 w-3 md:h-4 md:w-4" />
2025-07-27 10:05:37 -07:00
Add Configuration
</Button>
</div>
2025-12-23 01:16:25 -08:00
{configs?.length === 0 ? (
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }}>
<Card className="border-dashed border-2 border-muted-foreground/25">
<CardContent className="flex flex-col items-center justify-center py-10 md:py-16 text-center">
<div className="rounded-full bg-gradient-to-br from-violet-500/10 to-purple-500/10 p-4 md:p-6 mb-4 md:mb-6">
<Wand2 className="h-8 w-8 md:h-12 md:w-12 text-violet-600 dark:text-violet-400" />
2025-12-23 01:16:25 -08:00
</div>
<div className="space-y-2 mb-4 md:mb-6">
<h3 className="text-lg md:text-xl font-semibold">No Configurations Yet</h3>
<p className="text-xs md:text-sm text-muted-foreground max-w-sm">
2025-12-23 01:16:25 -08:00
Create your first AI configuration to customize how your agent responds
</p>
</div>
<Button
onClick={openNewDialog}
size="lg"
className="gap-2 text-xs md:text-sm h-9 md:h-10"
>
<Plus className="h-3 w-3 md:h-4 md:w-4" />
2025-12-23 01:16:25 -08:00
Create First Configuration
</Button>
</CardContent>
</Card>
</motion.div>
2025-07-27 10:05:37 -07:00
) : (
2025-12-23 01:16:25 -08:00
<motion.div variants={container} initial="hidden" animate="show" className="grid gap-4">
<AnimatePresence mode="popLayout">
{configs?.map((config) => {
2025-07-27 10:05:37 -07:00
return (
<motion.div
key={config.id}
2025-12-23 01:16:25 -08:00
variants={item}
layout
exit={{ opacity: 0, scale: 0.95 }}
2025-07-27 10:05:37 -07:00
>
2025-12-23 01:16:25 -08:00
<Card className="group overflow-hidden hover:shadow-lg transition-all duration-300 border-muted-foreground/10 hover:border-violet-500/30">
<CardContent className="p-0">
<div className="flex">
{/* Left accent bar */}
<div className="w-1 md:w-1.5 transition-colors bg-gradient-to-b from-violet-500/50 to-purple-500/50 group-hover:from-violet-500 group-hover:to-purple-500" />
<div className="flex-1 p-3 md:p-5">
<div className="flex items-start justify-between gap-2 md:gap-4">
{/* Main content */}
<div className="flex items-start gap-2 md:gap-4 flex-1 min-w-0">
<div className="flex h-10 w-10 md:h-12 md:w-12 items-center justify-center rounded-lg md:rounded-xl bg-gradient-to-br from-violet-500/10 to-purple-500/10 group-hover:from-violet-500/20 group-hover:to-purple-500/20 transition-colors shrink-0">
<Bot className="h-5 w-5 md:h-6 md:w-6 text-violet-600 dark:text-violet-400" />
2025-07-27 10:05:37 -07:00
</div>
<div className="flex-1 min-w-0 space-y-2 md:space-y-3">
{/* Title row */}
<div className="flex items-center gap-1.5 md:gap-2 flex-wrap">
<h4 className="text-sm md:text-base font-semibold tracking-tight truncate">
{config.name}
</h4>
<div className="flex items-center gap-1 md:gap-1.5 flex-wrap">
<Badge
variant="secondary"
className="text-[9px] md:text-[10px] font-medium px-1.5 md:px-2 py-0.5 bg-violet-500/10 text-violet-700 dark:text-violet-300 border-violet-500/20"
>
{config.provider}
</Badge>
2025-12-23 01:16:25 -08:00
{config.citations_enabled && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<Badge
variant="outline"
className="text-[9px] md:text-[10px] px-1.5 md:px-2 py-0.5 border-emerald-500/30 text-emerald-700 dark:text-emerald-300"
2025-12-23 01:16:25 -08:00
>
<MessageSquareQuote className="h-2.5 w-2.5 md:h-3 md:w-3 mr-0.5 md:mr-1" />
2025-12-23 01:16:25 -08:00
Citations
</Badge>
</TooltipTrigger>
<TooltipContent>
Citations are enabled for this configuration
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
2025-12-23 01:16:25 -08:00
{!config.use_default_system_instructions &&
config.system_instructions && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<Badge
variant="outline"
className="text-[9px] md:text-[10px] px-1.5 md:px-2 py-0.5 border-blue-500/30 text-blue-700 dark:text-blue-300"
2025-12-23 01:16:25 -08:00
>
<FileText className="h-2.5 w-2.5 md:h-3 md:w-3 mr-0.5 md:mr-1" />
2025-12-23 01:16:25 -08:00
Custom
</Badge>
</TooltipTrigger>
<TooltipContent>
Using custom system instructions
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
</div>
{/* Model name */}
<code className="text-[10px] md:text-xs font-mono text-muted-foreground bg-muted/50 px-1.5 md:px-2 py-0.5 md:py-1 rounded-md inline-block">
2025-12-23 01:16:25 -08:00
{config.model_name}
</code>
{/* Description if any */}
{config.description && (
<p className="text-[10px] md:text-xs text-muted-foreground line-clamp-1">
2025-12-23 01:16:25 -08:00
{config.description}
</p>
)}
2025-07-27 10:05:37 -07:00
{/* Footer row */}
<div className="flex items-center gap-2 md:gap-4 pt-1">
<div className="flex items-center gap-1 md:gap-1.5 text-[10px] md:text-xs text-muted-foreground">
<Clock className="h-2.5 w-2.5 md:h-3 md:w-3" />
2025-12-23 01:16:25 -08:00
<span>
{new Date(config.created_at).toLocaleDateString()}
</span>
</div>
</div>
2025-07-27 10:05:37 -07:00
</div>
</div>
{/* Actions */}
<div className="flex items-center gap-0.5 md:gap-1 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
2025-12-23 01:16:25 -08:00
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
onClick={() => openEditDialog(config)}
className="h-7 w-7 md:h-8 md:w-8 p-0 text-muted-foreground hover:text-foreground"
2025-12-23 01:16:25 -08:00
>
<Edit3 className="h-3.5 w-3.5 md:h-4 md:w-4" />
2025-12-23 01:16:25 -08:00
</Button>
</TooltipTrigger>
<TooltipContent>Edit</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
onClick={() => setConfigToDelete(config)}
className="h-7 w-7 md:h-8 md:w-8 p-0 text-muted-foreground hover:text-destructive"
2025-12-23 01:16:25 -08:00
>
<Trash2 className="h-3.5 w-3.5 md:h-4 md:w-4" />
2025-12-23 01:16:25 -08:00
</Button>
</TooltipTrigger>
<TooltipContent>Delete</TooltipContent>
</Tooltip>
</TooltipProvider>
2025-07-27 10:05:37 -07:00
</div>
</div>
</div>
</div>
</CardContent>
</Card>
</motion.div>
);
})}
</AnimatePresence>
2025-12-23 01:16:25 -08:00
</motion.div>
2025-07-27 10:05:37 -07:00
)}
</div>
)}
{/* Add/Edit Configuration Dialog */}
2025-12-23 01:16:25 -08:00
<Dialog open={isDialogOpen} onOpenChange={(open) => !open && closeDialog()}>
2025-07-27 10:05:37 -07:00
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
2025-12-23 01:16:25 -08:00
{editingConfig ? (
<Edit3 className="w-5 h-5 text-violet-600" />
) : (
<Plus className="w-5 h-5 text-violet-600" />
)}
{editingConfig ? "Edit Configuration" : "Create New Configuration"}
2025-07-27 10:05:37 -07:00
</DialogTitle>
<DialogDescription>
{editingConfig
2025-12-23 01:16:25 -08:00
? "Update your AI model and prompt configuration"
: "Set up a new AI model with custom prompts and citation settings"}
2025-07-27 10:05:37 -07:00
</DialogDescription>
</DialogHeader>
2025-12-23 01:16:25 -08:00
<LLMConfigForm
key={editingConfig ? `edit-${editingConfig.id}` : "create"}
searchSpaceId={searchSpaceId}
initialData={
editingConfig
? {
name: editingConfig.name,
description: editingConfig.description || "",
provider: editingConfig.provider,
custom_provider: editingConfig.custom_provider || "",
model_name: editingConfig.model_name,
api_key: editingConfig.api_key,
api_base: editingConfig.api_base || "",
litellm_params: editingConfig.litellm_params || {},
system_instructions: editingConfig.system_instructions || "",
use_default_system_instructions: editingConfig.use_default_system_instructions,
citations_enabled: editingConfig.citations_enabled,
}
: {
citations_enabled: true,
use_default_system_instructions: true,
}
}
onSubmit={handleFormSubmit}
onCancel={closeDialog}
isSubmitting={isSubmitting}
mode={editingConfig ? "edit" : "create"}
showAdvanced={true}
compact={true}
/>
2025-07-27 10:05:37 -07:00
</DialogContent>
</Dialog>
{/* Delete Confirmation Dialog */}
<AlertDialog
open={!!configToDelete}
onOpenChange={(open) => !open && setConfigToDelete(null)}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="flex items-center gap-2">
<Trash2 className="h-5 w-5 text-destructive" />
Delete Configuration
</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete{" "}
<span className="font-semibold text-foreground">{configToDelete?.name}</span>? This
2025-12-23 01:16:25 -08:00
action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isDeleting}>Cancel</AlertDialogCancel>
<AlertDialogAction
2025-12-23 01:16:25 -08:00
onClick={handleDelete}
disabled={isDeleting}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{isDeleting ? (
<>
<Spinner size="sm" className="mr-2" />
Deleting
</>
) : (
<>
<Trash2 className="mr-2 h-4 w-4" />
Delete
</>
)}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
2025-07-27 10:05:37 -07:00
</div>
);
}