diff --git a/frontend/components/knowledge-filter-list.tsx b/frontend/components/knowledge-filter-list.tsx index 1393a50a..2d54c237 100644 --- a/frontend/components/knowledge-filter-list.tsx +++ b/frontend/components/knowledge-filter-list.tsx @@ -6,7 +6,7 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; -import { Filter, X, Loader2, Plus, Save } from "lucide-react"; +import { Filter, Loader2, Plus, Save } from "lucide-react"; import { cn } from "@/lib/utils"; import { useGetFiltersSearchQuery, @@ -41,7 +41,7 @@ export function KnowledgeFilterList({ selectedFilter, onFilterSelect, }: KnowledgeFilterListProps) { - const [searchQuery, setSearchQuery] = useState(""); + const [searchQuery] = useState(""); const [showCreateModal, setShowCreateModal] = useState(false); const [createName, setCreateName] = useState(""); const [createDescription, setCreateDescription] = useState(""); @@ -60,10 +60,6 @@ export function KnowledgeFilterList({ onFilterSelect(filter); }; - // const handleClearFilter = () => { - // onFilterSelect(null); - // }; - const handleCreateNew = () => { setShowCreateModal(true); }; @@ -176,8 +172,10 @@ export function KnowledgeFilterList({ {(() => { - const count = parseQueryData(filter.query_data).filters - .data_sources.length; + const dataSources = + parseQueryData(filter.query_data).filters.data_sources; + if (dataSources[0] === "*") return "All sources"; + const count = dataSources.length; return `${count} ${count === 1 ? "source" : "sources"}`; })()} diff --git a/frontend/components/knowledge-filter-panel.tsx b/frontend/components/knowledge-filter-panel.tsx index 6673b337..1bdc662f 100644 --- a/frontend/components/knowledge-filter-panel.tsx +++ b/frontend/components/knowledge-filter-panel.tsx @@ -1,12 +1,11 @@ "use client"; import { useState, useEffect } from "react"; -import { X, Edit3, Save, Settings, RefreshCw } from "lucide-react"; +import { X, Edit3, Save, RefreshCw } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Card, CardContent, - CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; @@ -17,6 +16,8 @@ import { MultiSelect } from "@/components/ui/multi-select"; import { Slider } from "@/components/ui/slider"; import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context"; import { useDeleteFilter } from "@/app/api/mutations/useDeleteFilter"; +import { useUpdateFilter } from "@/app/api/mutations/useUpdateFilter"; +import { useGetSearchAggregations } from "@/src/app/api/queries/useGetSearchAggregations"; interface FacetBucket { key: string; @@ -39,6 +40,7 @@ export function KnowledgeFilterPanel() { closePanelOnly, } = useKnowledgeFilter(); const deleteFilterMutation = useDeleteFilter(); + const updateFilterMutation = useUpdateFilter(); // Edit mode states const [isEditingMeta, setIsEditingMeta] = useState(false); @@ -92,49 +94,24 @@ export function KnowledgeFilterPanel() { } }, [selectedFilter, parsedFilterData]); - // Load available facets from API + // 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 (isPanelOpen) { - loadAvailableFacets(); - } - }, [isPanelOpen]); - - const loadAvailableFacets = async () => { - console.log("[DEBUG] Loading available facets..."); - try { - // Do a search to get facets (similar to search page) - const response = await fetch("/api/search", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - query: "*", // Use wildcard like search page to get all documents/facets - limit: 1, - scoreThreshold: 0, - // Omit filters entirely to get all available facets - }), - }); - - const result = await response.json(); - console.log("[DEBUG] Search API response:", result); - - if (response.ok && result.aggregations) { - const facets = { - data_sources: result.aggregations.data_sources?.buckets || [], - document_types: result.aggregations.document_types?.buckets || [], - owners: result.aggregations.owners?.buckets || [], - connector_types: result.aggregations.connector_types?.buckets || [], - }; - console.log("[DEBUG] Setting facets:", facets); - setAvailableFacets(facets); - } else { - console.log("[DEBUG] No aggregations in response or response not ok"); - } - } catch (error) { - console.error("Failed to load available facets:", error); - } - }; + 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 no filter selected if (!isPanelOpen || !selectedFilter || !parsedFilterData) return null; @@ -173,29 +150,14 @@ export function KnowledgeFilterPanel() { setIsSaving(true); try { - const response = await fetch( - `/api/knowledge-filter/${selectedFilter.id}`, - { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - name: editingName.trim(), - description: editingDescription.trim(), - }), - } - ); + const result = await updateFilterMutation.mutateAsync({ + id: selectedFilter.id, + name: editingName.trim(), + description: editingDescription.trim(), + }); - const result = await response.json(); - if (response.ok && result.success) { - const updatedFilter = { - ...selectedFilter, - name: editingName.trim(), - description: editingDescription.trim(), - updated_at: new Date().toISOString(), - }; - setSelectedFilter(updatedFilter); + if (result.success && result.filter) { + setSelectedFilter(result.filter); setIsEditingMeta(false); } } catch (error) { @@ -215,28 +177,13 @@ export function KnowledgeFilterPanel() { setIsSaving(true); try { - const response = await fetch( - `/api/knowledge-filter/${selectedFilter.id}`, - { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - queryData: JSON.stringify(filterData), - }), - } - ); + const result = await updateFilterMutation.mutateAsync({ + id: selectedFilter.id, + queryData: JSON.stringify(filterData), + }); - const result = await response.json(); - if (response.ok && result.success) { - // Update the filter in context - const updatedFilter = { - ...selectedFilter, - query_data: JSON.stringify(filterData), - updated_at: new Date().toISOString(), - }; - setSelectedFilter(updatedFilter); + if (result.success && result.filter) { + setSelectedFilter(result.filter); } } catch (error) { console.error("Error updating filter configuration:", error); diff --git a/frontend/src/app/api/mutations/useUpdateFilter.ts b/frontend/src/app/api/mutations/useUpdateFilter.ts new file mode 100644 index 00000000..b611e658 --- /dev/null +++ b/frontend/src/app/api/mutations/useUpdateFilter.ts @@ -0,0 +1,52 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { KnowledgeFilter } from "../queries/useGetFiltersSearchQuery"; + +export interface UpdateFilterRequest { + id: string; + name?: string; + description?: string; + queryData?: string; // stringified ParsedQueryData +} + +export interface UpdateFilterResponse { + success: boolean; + filter: KnowledgeFilter; + message?: string; +} + +async function updateFilter(data: UpdateFilterRequest): Promise { + // Build a body with only provided fields + const body: Record = {}; + if (typeof data.name !== "undefined") body.name = data.name; + if (typeof data.description !== "undefined") body.description = data.description; + if (typeof data.queryData !== "undefined") body.queryData = data.queryData; + + const response = await fetch(`/api/knowledge-filter/${data.id}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); + + const json = await response.json().catch(() => ({})); + + if (!response.ok) { + const errorMessage = (json && (json.error as string)) || "Failed to update knowledge filter"; + throw new Error(errorMessage); + } + + return json as UpdateFilterResponse; +} + +export const useUpdateFilter = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: updateFilter, + onSuccess: () => { + // Refresh any knowledge filter lists/searches + queryClient.invalidateQueries({ queryKey: ["knowledge-filters"], exact: false }); + }, + }); +}; diff --git a/frontend/src/app/api/queries/useGetSearchAggregations.ts b/frontend/src/app/api/queries/useGetSearchAggregations.ts new file mode 100644 index 00000000..fcf65f06 --- /dev/null +++ b/frontend/src/app/api/queries/useGetSearchAggregations.ts @@ -0,0 +1,47 @@ +import { useQuery, useQueryClient, type UseQueryOptions } from "@tanstack/react-query"; + +export interface FacetBucket { + key: string; + count: number; +} + +export interface SearchAggregations { + data_sources?: { buckets: FacetBucket[] }; + document_types?: { buckets: FacetBucket[] }; + owners?: { buckets: FacetBucket[] }; + connector_types?: { buckets: FacetBucket[] }; +} + +type Options = Omit, "queryKey" | "queryFn">; + +export const useGetSearchAggregations = ( + query: string, + limit: number, + scoreThreshold: number, + options?: Options +) => { + const queryClient = useQueryClient(); + + async function fetchAggregations(): Promise { + const response = await fetch("/api/search", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ query, limit, scoreThreshold }), + }); + + const json = await response.json().catch(() => ({})); + + if (!response.ok) { + throw new Error((json && json.error) || "Failed to load search aggregations"); + } + + return (json.aggregations || {}) as SearchAggregations; + } + + return useQuery({ + queryKey: ["search-aggregations", query, limit, scoreThreshold], + queryFn: fetchAggregations, + placeholderData: prev => prev, + ...options, + }, queryClient); +}; diff --git a/frontend/src/app/knowledge/page.tsx b/frontend/src/app/knowledge/page.tsx index ef54621c..cbbcd758 100644 --- a/frontend/src/app/knowledge/page.tsx +++ b/frontend/src/app/knowledge/page.tsx @@ -5,19 +5,12 @@ import { Cloud, Filter, HardDrive, - Loader2, Search, Trash2, X, } from "lucide-react"; import { AgGridReact, CustomCellRendererProps } from "ag-grid-react"; -import { - type FormEvent, - useCallback, - useEffect, - useState, - useRef, -} from "react"; +import { useCallback, useEffect, useState, useRef, ChangeEvent } from "react"; import { useRouter } from "next/navigation"; import { SiGoogledrive } from "react-icons/si"; import { TbBrandOnedrive } from "react-icons/tb"; @@ -62,8 +55,6 @@ function SearchPage() { const { isMenuOpen } = useTask(); const { selectedFilter, setSelectedFilter, parsedFilterData, isPanelOpen } = useKnowledgeFilter(); - const [query, setQuery] = useState(""); - const [queryInputText, setQueryInputText] = useState(""); const [selectedRows, setSelectedRows] = useState([]); const [showBulkDeleteDialog, setShowBulkDeleteDialog] = useState(false); @@ -73,26 +64,11 @@ function SearchPage() { data = [], isFetching, refetch: refetchSearch, - } = useGetSearchQuery(query, parsedFilterData); + } = useGetSearchQuery(parsedFilterData?.query || "", parsedFilterData); - // Update query when global filter changes - useEffect(() => { - if (parsedFilterData?.query) { - setQueryInputText(parsedFilterData.query); - } - }, [parsedFilterData]); - - const handleSearch = useCallback( - (e?: FormEvent) => { - if (e) e.preventDefault(); - if (query.trim() === queryInputText.trim()) { - refetchSearch(); - return; - } - setQuery(queryInputText); - }, - [queryInputText, refetchSearch, query] - ); + const handleTableSearch = (e: ChangeEvent) => { + gridRef.current?.api.setGridOption("quickFilterText", e.target.value); + }; const fileResults = data as File[]; @@ -224,6 +200,10 @@ function SearchPage() { } }; + useEffect(() => { + refetchSearch(); + }, [selectedFilter, refetchSearch]); + return (
-
-
+ +
{selectedFilter?.name && (
@@ -266,12 +246,10 @@ function SearchPage() { id="search-query" type="text" placeholder="Search your documents..." - onChange={(e) => setQueryInputText(e.target.value)} - value={queryInputText} - defaultValue={parsedFilterData?.query} + onChange={handleTableSearch} />
- + */} {/* //TODO: Implement sync button */} {/* */} - + {selectedRows.length > 0 && ( + + )}