import { Button } from "@/src/components/ui/button"; import { Input } from "@/src/components/ui/input"; import { Textarea } from "@/src/components/ui/textarea"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/src/components/ui/select"; import { DatePicker } from "@/src/components/date-picker"; import { useState, useEffect, type Dispatch, type SetStateAction } from "react"; import useProjectIdFromURL from "@/src/hooks/useProjectIdFromURL"; import { api } from "@/src/utils/api"; import { Check, ChevronDown, ExternalLink, FilterIcon, Info, Plus, WandSparkles, X, } from "lucide-react"; import { Popover, PopoverContent, PopoverTrigger, } from "@/src/components/ui/popover"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/src/components/ui/tooltip"; import { MultiSelect } from "@/src/features/filters/components/multi-select"; import { type WipFilterState, type WipFilterCondition, type FilterState, type FilterCondition, type ColumnDefinition, filterOperators, singleFilter, } from "@langfuse/shared"; import { NonEmptyString } from "@langfuse/shared"; import { cn } from "@/src/utils/tailwind"; import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture"; import { InputCommand, InputCommandEmpty, InputCommandGroup, InputCommandInput, InputCommandItem, InputCommandList, } from "@/src/components/ui/input-command"; import { useQueryProject } from "@/src/features/projects/hooks"; import { useLangfuseCloudRegion } from "@/src/features/organizations/hooks"; // Has WipFilterState, passes all valid filters to parent onChange export function PopoverFilterBuilder({ columns, filterState, onChange, columnsWithCustomSelect = [], filterWithAI = false, buttonType = "default", }: { columns: ColumnDefinition[]; filterState: FilterState; onChange: | Dispatch> | ((newState: FilterState) => void); columnsWithCustomSelect?: string[]; filterWithAI?: boolean; buttonType?: "default" | "icon"; }) { const capture = usePostHogClientCapture(); const [wipFilterState, _setWipFilterState] = useState(filterState); const addNewFilter = () => { setWipFilterState((prev) => [ ...prev, { column: undefined, type: undefined, operator: undefined, value: undefined, key: undefined, }, ]); }; const getValidFilters = (state: WipFilterState): FilterCondition[] => { const valid = state.filter( (f) => singleFilter.safeParse(f).success, ) as FilterCondition[]; return valid; }; const setWipFilterState = ( state: ((prev: WipFilterState) => WipFilterState) | WipFilterState, ) => { _setWipFilterState((prev) => { const newState = state instanceof Function ? state(prev) : state; const validFilters = getValidFilters(newState); onChange(validFilters); return newState; }); }; return (
{ if (open) { capture("table:filter_builder_open"); } // Create empty filter when opening popover if (open && filterState.length === 0) addNewFilter(); // Discard all wip filters when closing popover if (!open) { capture("table:filter_builder_close", { filter: filterState, }); setWipFilterState(filterState); } }} > {buttonType === "default" ? ( ) : ( )} {filterState.length > 0 ? ( buttonType === "default" ? ( Clear all filters ) : ( Clear all filters ) ) : null}
); } export function InlineFilterState({ filterState, className, }: { filterState: FilterState; className?: string; }) { return filterState.map((filter, i) => { return ( {filter.column} {filter.type === "stringObject" || filter.type === "numberObject" ? `.${filter.key}` : ""}{" "} {filter.operator}{" "} {filter.type === "datetime" ? new Date(filter.value).toLocaleString() : filter.type === "stringOptions" || filter.type === "arrayOptions" ? filter.value.length > 2 ? `${filter.value.length} selected` : filter.value.join(", ") : filter.type === "number" || filter.type === "numberObject" ? filter.value : filter.type === "boolean" ? `${filter.value}` : `"${filter.value}"`} ); }); } export function InlineFilterBuilder({ columns, filterState, onChange, disabled, columnsWithCustomSelect, filterWithAI = false, }: { columns: ColumnDefinition[]; filterState: FilterState; onChange: | Dispatch> | ((newState: FilterState) => void); disabled?: boolean; columnsWithCustomSelect?: string[]; filterWithAI?: boolean; }) { const [wipFilterState, _setWipFilterState] = useState(filterState); // sync filter state, e.g. when we exclude default LF filters on score creation to reflect in UI // Only sync if we don't have any WIP (invalid) filters, to avoid overwriting user's work-in-progress useEffect(() => { const hasWipFilters = wipFilterState.some( (f) => !singleFilter.safeParse(f).success, ); // Don't sync if we have WIP filters - user is actively editing if (!hasWipFilters) { _setWipFilterState(filterState); } }, [filterState, wipFilterState]); const setWipFilterState = ( state: ((prev: WipFilterState) => WipFilterState) | WipFilterState, ) => { _setWipFilterState((prev) => { const newState = state instanceof Function ? state(prev) : state; const validFilters = newState.filter( (f) => singleFilter.safeParse(f).success, ) as FilterState; onChange(validFilters); return newState; }); }; return (
); } const getOperator = ( type: NonNullable, ): WipFilterCondition["operator"] => { return filterOperators[type]?.length > 0 ? filterOperators[type][0] : undefined; }; function FilterBuilderForm({ columns, filterState, onChange, disabled, columnsWithCustomSelect = [], filterWithAI = false, }: { columns: ColumnDefinition[]; filterState: WipFilterState; onChange: Dispatch>; disabled?: boolean; columnsWithCustomSelect?: string[]; filterWithAI?: boolean; }) { const { isLangfuseCloud } = useLangfuseCloudRegion(); const [showAiFilter, setShowAiFilter] = useState(false); const [aiPrompt, setAiPrompt] = useState(""); const [aiError, setAiError] = useState(null); const projectId = useProjectIdFromURL(); const { organization } = useQueryProject(); const createFilterMutation = api.naturalLanguageFilters.createCompletion.useMutation(); const handleFilterChange = (filter: WipFilterCondition, i: number) => { onChange((prev) => { const newState = [...prev]; newState[i] = filter; return newState; }); }; const addNewFilter = () => { onChange((prev) => [ ...prev, { column: undefined, operator: undefined, value: undefined, type: undefined, key: undefined, }, ]); }; const removeFilter = (i: number) => { onChange((prev) => { const newState = [...prev]; newState.splice(i, 1); return newState; }); }; const handleAiFilterSubmit = async () => { if (aiPrompt.trim() && !createFilterMutation.isPending && projectId) { setAiError(null); try { const result = await createFilterMutation.mutateAsync({ projectId, prompt: aiPrompt.trim(), }); if (result && Array.isArray(result.filters)) { if (result.filters.length === 0) { setAiError("Failed to generate filters, try again"); return; } // Set the filters from the API response onChange(result.filters as WipFilterState); setAiPrompt(""); setShowAiFilter(false); } else { console.error(result); setAiError("Invalid response format from API"); } } catch (error) { console.error("Error calling tRPC API:", error); setAiError( error instanceof Error ? error.message : "Failed to generate filters", ); } } }; return ( <> {/* AI Filter Section at the top */} {!disabled && isLangfuseCloud && filterWithAI && (
{showAiFilter && (