From e33adda6fb5f32ce433b7c4055e8c6b0809be670 Mon Sep 17 00:00:00 2001 From: Cole Goldsmith Date: Thu, 18 Sep 2025 16:33:16 -0500 Subject: [PATCH 01/10] start creating a new filters component --- frontend/components/knowledge-filter-list.tsx | 340 ++++++++++++++++++ frontend/components/navigation.tsx | 12 + frontend/package-lock.json | 9 + frontend/package.json | 1 + .../api/queries/useGetFiltersSearchQuery.ts | 63 ++++ frontend/src/app/globals.css | 2 +- frontend/tailwind.config.ts | 2 + 7 files changed, 428 insertions(+), 1 deletion(-) create mode 100644 frontend/components/knowledge-filter-list.tsx create mode 100644 frontend/src/app/api/queries/useGetFiltersSearchQuery.ts diff --git a/frontend/components/knowledge-filter-list.tsx b/frontend/components/knowledge-filter-list.tsx new file mode 100644 index 00000000..5815391a --- /dev/null +++ b/frontend/components/knowledge-filter-list.tsx @@ -0,0 +1,340 @@ +"use client"; + +import { useState, useEffect, useRef } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Card, CardContent } from "@/components/ui/card"; + +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { + ChevronDown, + Filter, + Search, + X, + Loader2, + Plus, + Save, +} from "lucide-react"; +import { cn } from "@/lib/utils"; +import { + useGetFiltersSearchQuery, + type KnowledgeFilter, +} from "@/src/app/api/queries/useGetFiltersSearchQuery"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; + +interface ParsedQueryData { + query: string; + filters: { + data_sources: string[]; + document_types: string[]; + owners: string[]; + }; + limit: number; + scoreThreshold: number; +} + +interface KnowledgeFilterListProps { + selectedFilter: KnowledgeFilter | null; + onFilterSelect: (filter: KnowledgeFilter | null) => void; +} + +export function KnowledgeFilterList({ + selectedFilter, + onFilterSelect, +}: KnowledgeFilterListProps) { + const [searchQuery, setSearchQuery] = useState(""); + const [showCreateModal, setShowCreateModal] = useState(false); + const [createName, setCreateName] = useState(""); + const [createDescription, setCreateDescription] = useState(""); + const [creating, setCreating] = useState(false); + + const { + data, + isFetching: loading, + refetch, + } = useGetFiltersSearchQuery(searchQuery, 20, { enabled: true }); + const filters: KnowledgeFilter[] = (data ?? []) as KnowledgeFilter[]; + + const deleteFilter = async (filterId: string, e: React.MouseEvent) => { + e.stopPropagation(); + + try { + const response = await fetch(`/api/knowledge-filter/${filterId}`, { + method: "DELETE", + }); + + if (response.ok) { + // If this was the selected filter, clear selection + if (selectedFilter?.id === filterId) { + onFilterSelect(null); + } + // Refresh list + refetch(); + } else { + console.error("Failed to delete knowledge filter"); + } + } catch (error) { + console.error("Error deleting knowledge filter:", error); + } + }; + + const handleFilterSelect = (filter: KnowledgeFilter) => { + onFilterSelect(filter); + }; + + const handleClearFilter = () => { + onFilterSelect(null); + }; + + const handleCreateNew = () => { + setShowCreateModal(true); + }; + + const handleCreateFilter = async () => { + if (!createName.trim()) return; + + setCreating(true); + try { + // Create a basic filter with wildcards (match everything by default) + const defaultFilterData = { + query: "", + filters: { + data_sources: ["*"], + document_types: ["*"], + owners: ["*"], + }, + limit: 10, + scoreThreshold: 0, + }; + + const response = await fetch("/api/knowledge-filter", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: createName.trim(), + description: createDescription.trim(), + queryData: JSON.stringify(defaultFilterData), + }), + }); + + const result = await response.json(); + if (response.ok && result.success) { + // Create the new filter object + const newFilter: KnowledgeFilter = { + id: result.filter.id, + name: createName.trim(), + description: createDescription.trim(), + query_data: JSON.stringify(defaultFilterData), + owner: result.filter.owner, + created_at: result.filter.created_at, + updated_at: result.filter.updated_at, + }; + + // Select the new filter + onFilterSelect(newFilter); + + // Close modal and reset form + setShowCreateModal(false); + setCreateName(""); + setCreateDescription(""); + // Refresh list to include newly created filter + refetch(); + } else { + console.error("Failed to create knowledge filter:", result.error); + } + } catch (error) { + console.error("Error creating knowledge filter:", error); + } finally { + setCreating(false); + } + }; + + const handleCancelCreate = () => { + setShowCreateModal(false); + setCreateName(""); + setCreateDescription(""); + }; + + const parseQueryData = (queryData: string): ParsedQueryData => { + return JSON.parse(queryData) as ParsedQueryData; + }; + + const getFilterSummary = (filter: KnowledgeFilter): string => { + try { + const parsed = JSON.parse(filter.query_data) as ParsedQueryData; + const parts = []; + + if (parsed.query) parts.push(`"${parsed.query}"`); + if (parsed.filters.data_sources.length > 0) + parts.push(`${parsed.filters.data_sources.length} sources`); + if (parsed.filters.document_types.length > 0) + parts.push(`${parsed.filters.document_types.length} types`); + if (parsed.filters.owners.length > 0) + parts.push(`${parsed.filters.owners.length} owners`); + + return parts.join(" • ") || "No filters"; + } catch { + return "Invalid filter"; + } + }; + + return ( + <> +
+
+
+ Knowledge Filters +
+ +
+ {loading ? ( +
+ + + Loading... + +
+ ) : filters.length === 0 ? ( +
+ {searchQuery ? "No filters found" : "No saved filters"} +
+ ) : ( + filters.map((filter) => ( +
handleFilterSelect(filter)} + className={cn( + "flex items-center gap-3 px-3 py-2 w-full rounded-lg hover:bg-accent hover:text-accent-foreground cursor-pointer group transition-colors", + selectedFilter?.id === filter.id && + "bg-accent text-accent-foreground" + )} + > +
+
+ +
+ {filter.name} +
+
+ {filter.description && ( +
+ {filter.description} +
+ )} +
+
+ {new Date(filter.created_at).toLocaleDateString(undefined, { + month: "short", + day: "numeric", + year: "numeric", + })} +
+ + {(() => { + const count = parseQueryData(filter.query_data).filters + .data_sources.length; + return `${count} ${count === 1 ? "source" : "sources"}`; + })()} + +
+
+ +
+ )) + )} +
+ {/* Create Filter Dialog */} + + + + Create New Knowledge Filter + + Save a reusable filter to quickly scope searches across your + knowledge base. + + +
+
+
+ + setCreateName(e.target.value)} + className="mt-1" + /> +
+
+ +