"use client"; import { RefreshCw, X } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { useCreateFilter } from "@/app/api/mutations/useCreateFilter"; import { useDeleteFilter } from "@/app/api/mutations/useDeleteFilter"; import { useUpdateFilter } from "@/app/api/mutations/useUpdateFilter"; import { useGetSearchAggregations } from "@/app/api/queries/useGetSearchAggregations"; import { type FilterColor, FilterIconPopover, type IconKey, } from "@/components/filter-icon-popover"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardFooter, CardHeader, CardTitle, } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { MultiSelect } from "@/components/ui/multi-select"; import { Slider } from "@/components/ui/slider"; import { Textarea } from "@/components/ui/textarea"; import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context"; interface FacetBucket { key: string; count: number; } interface AvailableFacets { data_sources: FacetBucket[]; document_types: FacetBucket[]; owners: FacetBucket[]; connector_types: FacetBucket[]; } export const filterAccentClasses: Record = { zinc: "bg-muted text-muted-foreground", pink: "bg-accent-pink text-accent-pink-foreground", purple: "bg-accent-purple text-accent-purple-foreground", indigo: "bg-accent-indigo text-accent-indigo-foreground", emerald: "bg-accent-emerald text-accent-emerald-foreground", amber: "bg-accent-amber text-accent-amber-foreground", red: "bg-accent-red text-accent-red-foreground", }; export function KnowledgeFilterPanel() { const { queryOverride, selectedFilter, parsedFilterData, setSelectedFilter, isPanelOpen, closePanelOnly, createMode, endCreateMode, } = useKnowledgeFilter(); const deleteFilterMutation = useDeleteFilter(); const updateFilterMutation = useUpdateFilter(); const createFilterMutation = useCreateFilter(); const [name, setName] = useState(""); const [description, setDescription] = useState(""); const [isSaving, setIsSaving] = useState(false); const [color, setColor] = useState("zinc"); const [iconKey, setIconKey] = useState("filter"); const [nameError, setNameError] = useState(null); const nameInputRef = useRef(null); // Filter configuration states (mirror search page exactly) const [query, setQuery] = useState(""); const [selectedFilters, setSelectedFilters] = useState({ data_sources: ["*"] as string[], // Default to wildcard document_types: ["*"] as string[], // Default to wildcard owners: ["*"] as string[], // Default to wildcard connector_types: ["*"] as string[], // Default to wildcard }); const [resultLimit, setResultLimit] = useState(10); const [scoreThreshold, setScoreThreshold] = useState(0); // Available facets (loaded from API) const [availableFacets, setAvailableFacets] = useState({ data_sources: [], document_types: [], owners: [], connector_types: [], }); // Load current filter data into controls when a filter is selected useEffect(() => { if (selectedFilter && parsedFilterData) { setQuery(parsedFilterData.query || ""); // Set the actual filter selections from the saved knowledge filter const filters = parsedFilterData.filters || {}; // Use the exact selections from the saved filter // Empty arrays mean "none selected" not "all selected" // Provide defaults for missing fields to handle API-created filters const processedFilters = { data_sources: filters.data_sources ?? ["*"], document_types: filters.document_types ?? ["*"], owners: filters.owners ?? ["*"], connector_types: filters.connector_types ?? ["*"], }; console.log("[DEBUG] Loading filter selections:", processedFilters); setSelectedFilters(processedFilters); setResultLimit(parsedFilterData.limit || 10); setScoreThreshold(parsedFilterData.scoreThreshold || 0); setName(selectedFilter.name); setDescription(selectedFilter.description || ""); setColor(parsedFilterData.color ?? "zinc"); setIconKey(parsedFilterData.icon ?? "filter"); } }, [selectedFilter, parsedFilterData]); // Initialize defaults when entering create mode useEffect(() => { if (createMode && parsedFilterData) { setQuery(parsedFilterData.query || ""); // Provide defaults for missing filter fields const filters = parsedFilterData.filters || {}; setSelectedFilters({ data_sources: filters.data_sources ?? ["*"], document_types: filters.document_types ?? ["*"], owners: filters.owners ?? ["*"], connector_types: filters.connector_types ?? ["*"], }); setResultLimit(parsedFilterData.limit || 10); setScoreThreshold(parsedFilterData.scoreThreshold || 0); setName(""); setDescription(""); setColor(parsedFilterData.color ?? "zinc"); setIconKey(parsedFilterData.icon ?? "filter"); } }, [createMode, parsedFilterData]); // Load available facets using search aggregations hook const { data: aggregations } = useGetSearchAggregations("*", 1, 0, { enabled: isPanelOpen, placeholderData: (prev) => prev, staleTime: 60_000, gcTime: 5 * 60_000, }); useEffect(() => { if (!aggregations) return; const facets = { data_sources: aggregations.data_sources?.buckets || [], document_types: aggregations.document_types?.buckets || [], owners: aggregations.owners?.buckets || [], connector_types: aggregations.connector_types?.buckets || [], }; setAvailableFacets(facets); }, [aggregations]); // Don't render if panel is closed or we don't have any data if (!isPanelOpen || !parsedFilterData) return null; const handleSaveConfiguration = async () => { if (!name.trim()) { setNameError("Name is required"); nameInputRef.current?.focus(); return; } const filterData = { query, filters: selectedFilters, limit: resultLimit, scoreThreshold, color, icon: iconKey, }; setIsSaving(true); try { if (createMode) { const result = await createFilterMutation.mutateAsync({ name: name.trim(), description: description.trim(), queryData: JSON.stringify(filterData), }); if (result.success && result.filter) { setSelectedFilter(result.filter); endCreateMode(); } } else if (selectedFilter) { const result = await updateFilterMutation.mutateAsync({ id: selectedFilter.id, name: name.trim(), description: description.trim(), queryData: JSON.stringify(filterData), }); if (result.success && result.filter) { setSelectedFilter(result.filter); } } } catch (error) { console.error("Error saving knowledge filter:", error); } finally { setIsSaving(false); } }; const formatDate = (dateString: string) => { return new Date(dateString).toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", }); }; const handleFilterChange = ( facetType: keyof typeof selectedFilters, newValues: string[], ) => { setSelectedFilters((prev) => ({ ...prev, [facetType]: newValues, })); }; const handleDeleteFilter = async () => { if (!selectedFilter) return; const result = await deleteFilterMutation.mutateAsync({ id: selectedFilter.id, }); if (result.success) { setSelectedFilter(null); closePanelOnly(); } }; return (
Knowledge Filter
{/* Filter Name and Description */}
{ const v = e.target.value; setName(v); if (nameError && v.trim()) { setNameError(null); } }} required placeholder="Filter name" ref={nameInputRef} aria-invalid={!!nameError} />
{!createMode && selectedFilter?.created_at && (
Created{" "} {formatDate(selectedFilter.created_at)}
)} {createMode && (
Created{" "} {formatDate(new Date().toISOString())}
)}