diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/tables/table-search-bar.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/tables/table-search-bar.tsx deleted file mode 100644 index 04d3ec5b0..000000000 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/analytics/tables/table-search-bar.tsx +++ /dev/null @@ -1,361 +0,0 @@ -"use client"; - -import { SimpleTooltip } from "@/components/ui/simple-tooltip"; -import { cn } from "@/lib/utils"; -import { - ArrowElbowDownLeftIcon, - MagnifyingGlassIcon, - SparkleIcon, - SpinnerGapIcon, - XIcon, -} from "@phosphor-icons/react"; -import { - useCallback, - useEffect, - useMemo, - useRef, - useState, - type KeyboardEvent, -} from "react"; -import type { RowData } from "../shared"; -import type { QueryDataGridToolbarContext } from "./query-data-grid"; -import { looksLikeNaturalLanguageQuery } from "./search-bar-logic"; -import type { AiTableFilterChat } from "./use-ai-table-filter-chat"; - -const SEARCH_DEBOUNCE_MS = 250; - -type TableSearchBarProps = { - /** Extended toolbar context from QueryDataGrid (grid state + fetch state). */ - ctx: QueryDataGridToolbarContext, - /** - * The query currently driving the grid. When it changes, the grid resets - * its own state (incl. `quickSearch`), so the bar drops any typed-but-now - * -stale search text to stay in sync. - */ - queryKey: string, - /** Chat thread powering the AI filter fallback. */ - chat: AiTableFilterChat, - /** The validated AI row filter currently applied to the grid, or null. */ - activeFilterQuery: string | null, - /** True when the AI committed a query that failed shape validation and was - * therefore NOT applied. */ - filterRejected: boolean, - /** Sends a natural-language filter request to the AI. */ - onAiSubmit: (text: string) => void, - /** Clears the AI row filter (back to the plain table). */ - onClearFilter: () => void, -}; - -/** Compact removable chip showing an active AI query/filter. */ -function ActiveQueryChip({ - label, - query, - onClear, - clearLabel, -}: { - label: string, - query: string, - onClear: () => void, - clearLabel: string, -}) { - return ( - - {query} - - } - > - - - {label} - - - - ); -} - -/** - * Filter-first search bar for the analytics tables page. Drops into the - * DataGridToolbar in place of the built-in quick-search input. - * - * - Typing applies a debounced substring (ILIKE) filter over the table via - * `state.quickSearch` — columns, sort, and pagination stay untouched. - * - Input that a substring can't express (natural language, comparisons, or - * searches that came back empty) is routed to the AI on Enter; the AI - * responds with a row filter over the SAME table, shown as a removable - * chip. ⌘/Ctrl+Enter always asks the AI. - */ -export function TableSearchBar({ - ctx, - queryKey, - chat, - activeFilterQuery, - filterRejected, - onAiSubmit, - onClearFilter, -}: TableSearchBarProps) { - const [input, setInput] = useState(""); - // The notice (AI text reply / rejection / error) the user has dismissed — - // compared by content so a NEW notice shows again. - const [dismissedNotice, setDismissedNotice] = useState(null); - - const { onChange } = ctx; - const debounceRef = useRef | null>(null); - - // Grid query changed (AI filter/builder applied or cleared) → the grid - // just reset its quickSearch, so typed text left in the input would look - // applied when it isn't. Drop it and cancel any pending debounce. - const prevQueryKeyRef = useRef(queryKey); - useEffect(() => { - if (prevQueryKeyRef.current === queryKey) return; - prevQueryKeyRef.current = queryKey; - if (debounceRef.current != null) { - clearTimeout(debounceRef.current); - debounceRef.current = null; - } - setInput(""); - }, [queryKey]); - - const applyQuickSearch = useCallback( - (value: string) => { - onChange((s) => - s.quickSearch === value - ? s - : { - ...s, - quickSearch: value, - // Reset to the first page — the filtered result set may not - // reach the page the user had scrolled to. - pagination: { ...s.pagination, pageIndex: 0 }, - }, - ); - }, - [onChange], - ); - - // Each keystroke re-runs the ClickHouse query, so writes to - // `state.quickSearch` are debounced. Clearing skips the debounce — showing - // the unfiltered table again should feel instant. - const scheduleQuickSearch = useCallback( - (value: string) => { - if (debounceRef.current != null) clearTimeout(debounceRef.current); - const trimmed = value.trim(); - if (trimmed.length === 0) { - debounceRef.current = null; - applyQuickSearch(""); - return; - } - debounceRef.current = setTimeout(() => { - debounceRef.current = null; - applyQuickSearch(trimmed); - }, SEARCH_DEBOUNCE_MS); - }, - [applyQuickSearch], - ); - - useEffect(() => { - return () => { - if (debounceRef.current != null) clearTimeout(debounceRef.current); - }; - }, []); - - const flushQuickSearch = useCallback(() => { - if (debounceRef.current != null) { - clearTimeout(debounceRef.current); - debounceRef.current = null; - } - applyQuickSearch(input.trim()); - }, [applyQuickSearch, input]); - - const trimmedInput = input.trim(); - const isNaturalLanguage = useMemo( - () => looksLikeNaturalLanguageQuery(trimmedInput), - [trimmedInput], - ); - // The plain filter came back empty for exactly what's typed — a substring - // match can't help, so offer the AI even for plain-looking terms. - const zeroResults = - trimmedInput.length > 0 - && ctx.state.quickSearch === trimmedInput - && !ctx.isLoading - && !ctx.isRefetching - && ctx.rowCount === 0; - const wantsAi = trimmedInput.length > 0 && (isNaturalLanguage || zeroResults); - - const submitAi = useCallback(() => { - const text = input.trim(); - if (text.length === 0 || chat.isResponding) return; - if (debounceRef.current != null) { - clearTimeout(debounceRef.current); - debounceRef.current = null; - } - // The text is a question for the AI, not a substring to match. - applyQuickSearch(""); - setInput(""); - setDismissedNotice(null); - onAiSubmit(text); - }, [input, chat.isResponding, applyQuickSearch, onAiSubmit]); - - const handleKeyDown = useCallback( - (e: KeyboardEvent) => { - if (e.key !== "Enter" || e.shiftKey) return; - e.preventDefault(); - if (e.metaKey || e.ctrlKey) { - submitAi(); - return; - } - if (wantsAi) { - submitAi(); - } else { - flushQuickSearch(); - } - }, - [submitAi, flushQuickSearch, wantsAi], - ); - - const handleInputChange = useCallback( - (value: string) => { - setInput(value); - scheduleQuickSearch(value); - }, - [scheduleQuickSearch], - ); - - const clearInput = useCallback(() => { - setInput(""); - if (debounceRef.current != null) { - clearTimeout(debounceRef.current); - debounceRef.current = null; - } - applyQuickSearch(""); - }, [applyQuickSearch]); - - // Chip label: the natural-language request that produced the active filter. - const activeFilterLabel = useMemo(() => { - if (activeFilterQuery == null) return null; - for (let i = chat.messages.length - 1; i >= 0; i--) { - const msg = chat.messages[i]!; - if (msg.role !== "user") continue; - const part = msg.content.find((p) => p.type === "text"); - if (part?.type === "text" && part.text.trim().length > 0) { - return part.text.trim(); - } - } - return "AI filter"; - }, [activeFilterQuery, chat.messages]); - - const rejectionMessage = filterRejected - ? "The AI answered with a query that would change this table's columns, so it wasn't applied. Try rephrasing your request." - : null; - const notice = chat.error?.message ?? chat.assistantNote ?? rejectionMessage; - const noticeVisible = notice != null && notice !== dismissedNotice; - - const isAiActive = activeFilterQuery != null; - const placeholder = isAiActive - ? "Search within results or refine with AI…" - : "Search rows or ask AI…"; - - return ( - // No flex-1 on the outer wrapper — a flex-1 search would swallow the - // toolbar's leading cluster and push siblings (e.g. the row-count badge) - // to the far right. Width lives on the input shell instead. -
-
- {chat.isResponding ? ( - - ) : wantsAi || isAiActive ? ( - - ) : ( - - )} - handleInputChange(e.target.value)} - onKeyDown={handleKeyDown} - placeholder={placeholder} - className={cn( - "min-w-0 flex-1 bg-transparent text-xs outline-none", - "placeholder:text-muted-foreground/40", - )} - aria-label="Search rows or ask AI" - /> - {input.length > 0 && !wantsAi && ( - - )} - {wantsAi && !chat.isResponding && ( - - - - )} -
- - {activeFilterQuery != null && activeFilterLabel != null && ( - - )} - - {noticeVisible && ( -
- - {notice} - -
- )} -
- ); -}