"use client"; import * as React from "react"; import { Bot, Calendar, FolderKanban, Folder as FolderIcon, Layers, Plus, Search, ShieldCheck, Tag, UserRound, X } from "lucide-react"; import { useApp, AUTO_MODEL_KEY } from "@/components/app/store"; import { useApi } from "@/lib/client/api"; import type { Project, FilterKey, FilterToken } from "@/lib/client/types"; import { addFilter, parseQuery, removeFilter } from "@/lib/search/query"; import { ResponsiveDialog } from "@/components/ui/sheet"; import { ProviderIcon } from "@/components/brand/provider-icon"; import { providerName } from "@/lib/client/providers"; import { Button } from "@/components/ui/button"; import { useIsMobile } from "@/lib/client/hooks"; import { cn } from "@/lib/utils"; type Picker = "model" | "provider" | "project" | "folder" | "date" | "role" | "is"; const PICKERS: { key: Picker; label: string; icon: React.ReactNode }[] = [ { key: "model", label: "Model", icon: }, { key: "project", label: "Project", icon: }, { key: "date", label: "Date", icon: }, { key: "role", label: "Role", icon: }, { key: "is", label: "Status", icon: }, { key: "provider", label: "Provider", icon: }, { key: "folder", label: "Folder", icon: }, ]; /** * Filter chips for the search query: active tokens (removable) + "add filter" chips that open a small sheet * (ResponsiveDialog) and append tokens through `addFilter()` — the input stays the single source of truth. */ export function FilterChips({ query, onChange, className, compact }: { query: string; onChange: (next: string) => void; className?: string; compact?: boolean }) { const [picker, setPicker] = React.useState(null); const parsed = React.useMemo(() => parseQuery(query), [query]); const activeKeys = new Set(parsed.tokens.map((t) => (t.key === "after" || t.key === "before" ? "date" : t.key))); const apply = (key: FilterKey, value: string) => onChange(addFilter(query, key, value) + " "); const applyMany = (pairs: [FilterKey, string][]) => { let next = query; for (const [k, v] of pairs) next = addFilter(next, k, v); onChange(next + " "); }; return ( <>
{parsed.tokens.map((t) => ( onChange(removeFilter(query, t))} compact={compact} /> ))} {PICKERS.filter((p) => !compact || !activeKeys.has(p.key) || p.key === "model" || p.key === "is").map((p) => ( ))}
setPicker(null)} onPick={apply} onPickMany={applyMany} /> ); } function TokenChip({ token, onRemove, compact }: { token: FilterToken; onRemove: () => void; compact?: boolean }) { return ( {token.key}: {token.value} ); } /* ------------------------------------------------------------------------------------------------ * Picker sheets * ---------------------------------------------------------------------------------------------- */ interface Option { value: string; label: React.ReactNode; hint?: React.ReactNode; icon?: React.ReactNode; } function FilterPicker({ picker, onClose, onPick, onPickMany }: { picker: Picker | null; onClose: () => void; onPick: (key: FilterKey, value: string) => void; onPickMany: (pairs: [FilterKey, string][]) => void }) { const { models, connectedProviders, favorites, labels, folders } = useApp(); const projectsQ = useApi<{ projects: Project[] }>(picker === "project" ? "/api/projects" : null, { shouldRetryOnError: false }); const [q, setQ] = React.useState(""); const isMobile = useIsMobile(); const [after, setAfter] = React.useState(""); const [before, setBefore] = React.useState(""); const close = () => { setQ(""); setAfter(""); setBefore(""); onClose(); }; const pick = (key: FilterKey, value: string) => { onPick(key, value); close(); }; const title = PICKERS.find((p) => p.key === picker)?.label ?? "Filter"; let options: Option[] = []; let key: FilterKey = "model"; let searchable = false; if (picker === "model") { key = "model"; searchable = true; const usable = models.filter((m) => connectedProviders.has(m.provider) && m.status !== "deprecated"); const sorted = [...usable].sort((a, b) => Number(favorites.has(b.key)) - Number(favorites.has(a.key)) || a.displayName.localeCompare(b.displayName)); options = sorted .filter((m) => !q || `${m.displayName} ${m.key} ${labels[m.key] ?? ""}`.toLowerCase().includes(q.toLowerCase())) .slice(0, 60) .map((m) => ({ value: m.id, label: labels[m.key] ?? m.displayName, hint: providerName(m.provider), icon: })); } else if (picker === "provider") { key = "provider"; options = Array.from(connectedProviders).map((p) => ({ value: p, label: providerName(p), icon: })); } else if (picker === "project") { key = "project"; searchable = true; const list = (projectsQ.data?.projects ?? []).filter((p) => !p.archived); options = list.filter((p) => !q || p.name.toLowerCase().includes(q.toLowerCase())).map((p) => ({ value: p.name, label: p.name, hint: p.description ?? undefined, icon: p.icon ? {p.icon} : })); } else if (picker === "folder") { key = "folder"; options = folders.map((f) => ({ value: f.name, label: f.name, icon: })); } else if (picker === "role") { key = "role"; options = [ { value: "user", label: "You", hint: "Messages you wrote", icon: }, { value: "assistant", label: "Assistant", hint: "Model replies", icon: }, ]; } else if (picker === "is") { key = "is"; options = [ { value: "pinned", label: "Pinned", icon: }, { value: "archived", label: "Archived", icon: }, { value: "shared", label: "Shared publicly", icon: }, ]; } const dateOptions: { label: string; pairs: [FilterKey, string][] }[] = [ { label: "Today", pairs: [["after", "today"]] }, { label: "Yesterday", pairs: [["after", "yesterday"], ["before", "today"]] }, { label: "Last 7 days", pairs: [["after", "7d"]] }, { label: "Last 30 days", pairs: [["after", "30d"]] }, { label: "This month", pairs: [["after", "month"]] }, ]; const rowCls = cn("flex w-full items-center gap-3 rounded-lg px-3 text-left transition-colors hover:bg-bg-muted active:bg-bg-muted [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-fg-muted", isMobile ? "min-h-[50px] text-[15px]" : "min-h-[40px] text-[13.5px]"); return ( !o && close()} title={`Filter by ${title.toLowerCase()}`} size="sm" snap="half" expandable flush desktopFlush> {picker === "date" ? (
    {dateOptions.map((d) => (
  • ))}

Custom range

) : (
{searchable ? (
setQ(e.target.value)} placeholder={`Filter ${title.toLowerCase()}s…`} className="h-11 w-full rounded-lg border border-border bg-bg-elevated pl-9 pr-3 text-[16px] text-fg outline-none placeholder:text-fg-subtle focus:border-accent sm:h-9 sm:text-sm" autoFocus={!isMobile} aria-label={`Filter ${title.toLowerCase()}s`} />
) : null} {picker === "project" && projectsQ.error ?

Projects are not available yet.

: null} {options.length === 0 && !(picker === "project" && projectsQ.error) ?

{picker === "project" && projectsQ.isLoading ? "Loading…" : "Nothing to pick from."}

: null}
    {options.map((o) => (
  • ))}
{picker === "model" && !q ? (

Tip: model:{AUTO_MODEL_KEY} is not a filter — pick a real model or type any part of a name.

) : null}
)}
); }