diff --git a/frontend/components/knowledge-filter-dropdown.tsx b/frontend/components/knowledge-filter-dropdown.tsx new file mode 100644 index 00000000..d2c2fb27 --- /dev/null +++ b/frontend/components/knowledge-filter-dropdown.tsx @@ -0,0 +1,426 @@ +"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 { Badge } from "@/components/ui/badge" +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" + +interface KnowledgeFilter { + id: string + name: string + description: string + query_data: string + owner: string + created_at: string + updated_at: string +} + +interface ParsedQueryData { + query: string + filters: { + data_sources: string[] + document_types: string[] + owners: string[] + } + limit: number + scoreThreshold: number +} + +interface KnowledgeFilterDropdownProps { + selectedFilter: KnowledgeFilter | null + onFilterSelect: (filter: KnowledgeFilter | null) => void +} + +export function KnowledgeFilterDropdown({ selectedFilter, onFilterSelect }: KnowledgeFilterDropdownProps) { + const [isOpen, setIsOpen] = useState(false) + const [filters, setFilters] = useState([]) + const [loading, setLoading] = useState(false) + const [searchQuery, setSearchQuery] = useState("") + const [showCreateModal, setShowCreateModal] = useState(false) + const [createName, setCreateName] = useState("") + const [createDescription, setCreateDescription] = useState("") + const [creating, setCreating] = useState(false) + const dropdownRef = useRef(null) + + const loadFilters = async (query = "") => { + setLoading(true) + try { + const response = await fetch("/api/knowledge-filter/search", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + query, + limit: 20 // Limit for dropdown + }), + }) + + const result = await response.json() + if (response.ok && result.success) { + setFilters(result.filters) + } else { + console.error("Failed to load knowledge filters:", result.error) + setFilters([]) + } + } catch (error) { + console.error("Error loading knowledge filters:", error) + setFilters([]) + } finally { + setLoading(false) + } + } + + const deleteFilter = async (filterId: string, e: React.MouseEvent) => { + e.stopPropagation() + + try { + const response = await fetch(`/api/knowledge-filter/${filterId}`, { + method: "DELETE", + }) + + if (response.ok) { + // Remove from local state + setFilters(prev => prev.filter(f => f.id !== filterId)) + + // If this was the selected filter, clear selection + if (selectedFilter?.id === filterId) { + onFilterSelect(null) + } + } else { + console.error("Failed to delete knowledge filter") + } + } catch (error) { + console.error("Error deleting knowledge filter:", error) + } + } + + const handleFilterSelect = (filter: KnowledgeFilter) => { + onFilterSelect(filter) + setIsOpen(false) + } + + const handleClearFilter = () => { + onFilterSelect(null) + setIsOpen(false) + } + + const handleCreateNew = () => { + setIsOpen(false) + 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 + } + + // Add to local filters list + setFilters(prev => [newFilter, ...prev]) + + // Select the new filter + onFilterSelect(newFilter) + + // Close modal and reset form + setShowCreateModal(false) + setCreateName("") + setCreateDescription("") + } 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 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" + } + } + + useEffect(() => { + if (isOpen) { + loadFilters() + } + }, [isOpen]) + + useEffect(() => { + const timeoutId = setTimeout(() => { + if (isOpen) { + loadFilters(searchQuery) + } + }, 300) + + return () => clearTimeout(timeoutId) + }, [searchQuery, isOpen]) + + // Close dropdown when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsOpen(false) + } + } + + document.addEventListener("mousedown", handleClickOutside) + return () => document.removeEventListener("mousedown", handleClickOutside) + }, []) + + return ( +
+ + + {isOpen && ( + + + {/* Search Header */} +
+
+ + setSearchQuery(e.target.value)} + className="pl-9 h-8 text-sm" + /> +
+
+ + {/* Filter List */} +
+ {/* Clear filter option */} +
+
+ +
+
All Knowledge
+
No filters applied
+
+
+
+ + {loading ? ( +
+ + Loading... +
+ ) : filters.length === 0 ? ( +
+ {searchQuery ? "No filters found" : "No saved filters"} +
+ ) : ( + filters.map((filter) => ( +
handleFilterSelect(filter)} + className={cn( + "flex items-center gap-3 p-3 hover:bg-accent hover:text-accent-foreground cursor-pointer group transition-colors", + selectedFilter?.id === filter.id && "bg-accent text-accent-foreground" + )} + > +
+ +
+
{filter.name}
+
+ {getFilterSummary(filter)} +
+
+
+ +
+ )) + )} +
+ + {/* Create New Filter Option */} +
+
+ +
+
Create New Filter
+
Save current search as filter
+
+
+
+ + {/* Selected Filter Details */} + {selectedFilter && ( +
+
+ Selected: {selectedFilter.name} +
+ {selectedFilter.description && ( +
+ {selectedFilter.description} +
+ )} +
+ )} +
+
+ )} + + {/* Create Filter Modal */} + {showCreateModal && ( +
+
+

Create New Knowledge Filter

+ +
+
+ + setCreateName(e.target.value)} + className="mt-1" + /> +
+ +
+ +