/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
import { useQuery, useQueryClient } from '@tanstack/react-query'
import {
Layers3,
Loader2,
Pencil,
Plus,
RefreshCcw,
Trash2,
} from 'lucide-react'
import { useEffect, useMemo, useState, type ReactNode } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { ConfirmDialog } from '@/components/confirm-dialog'
import { StaticDataTable } from '@/components/data-table/static/static-data-table'
import { StaticRowActions } from '@/components/data-table/static/static-row-actions'
import { Button } from '@/components/design-system/button'
import { Dialog } from '@/components/dialog'
import { StatusBadge } from '@/components/status-badge'
import { TableId } from '@/components/table-id'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { Badge } from '@/components/ui/badge'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import {
Empty,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from '@/components/ui/empty'
import { useIsMobile } from '@/hooks/use-mobile'
import { cn } from '@/lib/utils'
import { deletePrefillGroup, getPrefillGroups } from '../../api'
import { prefillGroupsQueryKeys } from '../../lib'
import type { PrefillGroup } from '../../types'
import {
PREFILL_GROUP_TYPE_META,
parseEndpointKeys,
parseStringItems,
} from '../prefill-group-shared'
type PrefillGroupManagementDialogProps = {
open: boolean
onOpenChange: (open: boolean) => void
onCreateGroup: () => void
onEditGroup: (group: PrefillGroup) => void
}
export function PrefillGroupManagementDialog({
open,
onOpenChange,
onCreateGroup,
onEditGroup,
}: PrefillGroupManagementDialogProps) {
const { t } = useTranslation()
const queryClient = useQueryClient()
const isMobile = useIsMobile()
const [deleteState, setDeleteState] = useState<{
open: boolean
group: PrefillGroup | null
}>({ open: false, group: null })
const [isDeleting, setIsDeleting] = useState(false)
const {
data,
isLoading,
isFetching,
error,
refetch: refetchGroups,
} = useQuery({
queryKey: prefillGroupsQueryKeys.list(),
queryFn: () => getPrefillGroups(),
enabled: open,
})
const groups = useMemo(() => data?.data ?? [], [data?.data])
const sortedGroups = useMemo(
() =>
[...groups].sort((a, b) => {
if (a.type === b.type) {
return a.name.localeCompare(b.name)
}
return a.type.localeCompare(b.type)
}),
[groups]
)
const normalizedGroups = useMemo(
() =>
sortedGroups.map((group) => {
const meta = PREFILL_GROUP_TYPE_META[group.type] || {
label: group.type,
badge: 'neutral' as const,
}
const parsedItems =
group.type === 'endpoint'
? parseEndpointKeys(group.items)
: parseStringItems(group.items)
return { group, meta, parsedItems }
}),
[sortedGroups]
)
useEffect(() => {
if (!open) {
setDeleteState({ open: false, group: null })
setIsDeleting(false)
}
}, [open])
const handleDeleteClick = (group: PrefillGroup) => {
setDeleteState({ open: true, group })
}
const handleDeleteConfirm = async () => {
if (!deleteState.group) return
setIsDeleting(true)
try {
const response = await deletePrefillGroup(deleteState.group.id)
if (response.success) {
toast.success(t('Deleted "{{name}}"', { name: deleteState.group.name }))
queryClient.invalidateQueries({
queryKey: prefillGroupsQueryKeys.lists(),
})
setDeleteState({ open: false, group: null })
} else {
toast.error(response.message || t('Failed to delete group'))
}
} catch (err: unknown) {
toast.error((err as Error)?.message || t('Failed to delete group'))
} finally {
setIsDeleting(false)
}
}
let groupsContent: ReactNode
if (isLoading) {
groupsContent = (
{t('Fetching prefill groups...')}
)
} else if (normalizedGroups.length === 0) {
groupsContent = (
{t('No prefill groups yet')}
{t(
'Create your first group to reuse model, tag, or endpoint selections anywhere in the dashboard.'
)}
{t('Prefill groups help you keep complex configurations in sync.')}
)
} else if (isMobile) {
groupsContent = (