Merge pull request #171 from langflow-ai/feat/filters-sweep

Feat/filters sweep
This commit is contained in:
Cole Goldsmith 2025-10-02 10:52:31 -05:00 committed by GitHub
commit 331f737c00
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 210 additions and 222 deletions

View file

@ -133,7 +133,7 @@ export function FilterIconPopover({
type="button" type="button"
onClick={() => onColorChange(c)} onClick={() => onColorChange(c)}
className={cn( className={cn(
"flex items-center justify-center h-6 w-6 rounded-sm transition-colors text-primary", "flex items-center justify-center h-6 w-6 rounded-sm transition-colors text-white",
colorSwatchClasses[c] colorSwatchClasses[c]
)} )}
aria-label={c} aria-label={c}

View file

@ -66,7 +66,7 @@ export function KnowledgeFilterList({
return ( return (
<> <>
<div className="flex flex-col gap-1 px-3 !mb-12 mt-0 h-full overflow-y-auto"> <div className="flex flex-col gap-2 px-3 !mb-12 mt-0 h-full overflow-y-auto">
<div className="flex items-center w-full justify-between pl-3"> <div className="flex items-center w-full justify-between pl-3">
<div className="text-sm font-medium text-muted-foreground"> <div className="text-sm font-medium text-muted-foreground">
Knowledge Filters Knowledge Filters
@ -76,7 +76,7 @@ export function KnowledgeFilterList({
size="sm" size="sm"
onClick={handleCreateNew} onClick={handleCreateNew}
title="Create New Filter" title="Create New Filter"
className="h-8 px-3 text-muted-foreground" className="!h-8 w-8 px-0 text-muted-foreground"
> >
<Plus className="h-3 w-3" /> <Plus className="h-3 w-3" />
</Button> </Button>
@ -106,12 +106,14 @@ export function KnowledgeFilterList({
<div className="flex flex-col gap-1 flex-1 min-w-0"> <div className="flex flex-col gap-1 flex-1 min-w-0">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{(() => { {(() => {
const parsed = parseQueryData(filter.query_data) as ParsedQueryData; const parsed = parseQueryData(
filter.query_data
) as ParsedQueryData;
const Icon = iconKeyToComponent(parsed.icon); const Icon = iconKeyToComponent(parsed.icon);
return ( return (
<div <div
className={cn( className={cn(
"flex items-center justify-center w-5 h-5 rounded transition-colors", "flex items-center justify-center w-5 h-5 rounded flex-shrink-0 transition-colors",
filterAccentClasses[parsed.color], filterAccentClasses[parsed.color],
parsed.color === "zinc" && parsed.color === "zinc" &&
"group-hover:bg-background group-[.active]:bg-background" "group-hover:bg-background group-[.active]:bg-background"

View file

@ -1,9 +1,15 @@
"use client"; "use client";
import { useState, useEffect } from "react"; import { useState, useEffect, useRef } from "react";
import { X, Save, RefreshCw } from "lucide-react"; import { X, RefreshCw } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import {
Card,
CardContent,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
@ -61,6 +67,8 @@ export function KnowledgeFilterPanel() {
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const [color, setColor] = useState<FilterColor>("zinc"); const [color, setColor] = useState<FilterColor>("zinc");
const [iconKey, setIconKey] = useState<IconKey>("filter"); const [iconKey, setIconKey] = useState<IconKey>("filter");
const [nameError, setNameError] = useState<string | null>(null);
const nameInputRef = useRef<HTMLInputElement>(null);
// Filter configuration states (mirror search page exactly) // Filter configuration states (mirror search page exactly)
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
@ -146,27 +154,12 @@ export function KnowledgeFilterPanel() {
// Don't render if panel is closed or we don't have any data // Don't render if panel is closed or we don't have any data
if (!isPanelOpen || !parsedFilterData) return null; if (!isPanelOpen || !parsedFilterData) return null;
const selectAllFilters = () => {
// Use wildcards instead of listing all specific items
setSelectedFilters({
data_sources: ["*"],
document_types: ["*"],
owners: ["*"],
connector_types: ["*"],
});
};
const clearAllFilters = () => {
setSelectedFilters({
data_sources: [],
document_types: [],
owners: [],
connector_types: [],
});
};
const handleSaveConfiguration = async () => { const handleSaveConfiguration = async () => {
if (!name.trim()) return; if (!name.trim()) {
setNameError("Name is required");
nameInputRef.current?.focus();
return;
}
const filterData = { const filterData = {
query, query,
filters: selectedFilters, filters: selectedFilters,
@ -238,8 +231,8 @@ export function KnowledgeFilterPanel() {
}; };
return ( return (
<div className="fixed right-0 top-14 bottom-0 w-80 bg-background border-l border-border/40 z-40 overflow-y-auto"> <div className="fixed right-0 top-14 bottom-0 w-80 bg-background border-l z-40 overflow-y-auto">
<Card className="h-full rounded-none border-0 shadow-lg"> <Card className="h-full rounded-none border-0 shadow-lg flex flex-col">
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<CardTitle className="text-lg flex items-center gap-2"> <CardTitle className="text-lg flex items-center gap-2">
@ -248,7 +241,10 @@ export function KnowledgeFilterPanel() {
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={closePanelOnly} onClick={() => {
setSelectedFilter(null);
closePanelOnly();
}}
className="h-8 w-8 p-0" className="h-8 w-8 p-0"
> >
<X className="h-4 w-4" /> <X className="h-4 w-4" />
@ -260,7 +256,10 @@ export function KnowledgeFilterPanel() {
{/* Filter Name and Description */} {/* Filter Name and Description */}
<div className="space-y-3"> <div className="space-y-3">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="filter-name">Filter name</Label> <Label htmlFor="filter-name" className="gap-1">
Filter name
<span className="text-destructive">*</span>
</Label>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FilterIconPopover <FilterIconPopover
color={color} color={color}
@ -271,8 +270,17 @@ export function KnowledgeFilterPanel() {
<Input <Input
id="filter-name" id="filter-name"
value={name} value={name}
onChange={(e) => setName(e.target.value)} onChange={(e) => {
const v = e.target.value;
setName(v);
if (nameError && v.trim()) {
setNameError(null);
}
}}
required
placeholder="Filter name" placeholder="Filter name"
ref={nameInputRef}
aria-invalid={!!nameError}
/> />
</div> </div>
</div> </div>
@ -282,13 +290,19 @@ export function KnowledgeFilterPanel() {
{formatDate(selectedFilter.created_at)} {formatDate(selectedFilter.created_at)}
</div> </div>
)} )}
{createMode && (
<div className="space-y-2 text-xs text-right text-muted-foreground">
<span className="text-placeholder-foreground">Created</span>{" "}
{formatDate(new Date().toISOString())}
</div>
)}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="filter-description">Description</Label> <Label htmlFor="filter-description">Description</Label>
<Textarea <Textarea
id="filter-description" id="filter-description"
value={description} value={description}
onChange={(e) => setDescription(e.target.value)} onChange={(e) => setDescription(e.target.value)}
placeholder="Optional description" placeholder="Provide a brief description of your knowledge filter..."
rows={3} rows={3}
/> />
</div> </div>
@ -301,17 +315,17 @@ export function KnowledgeFilterPanel() {
</Label> </Label>
<Textarea <Textarea
id="search-query" id="search-query"
placeholder="e.g., 'financial reports from Q4'" placeholder="Enter your search query..."
value={query} value={query}
className="font-mono placeholder:font-mono"
onChange={(e) => setQuery(e.target.value)} onChange={(e) => setQuery(e.target.value)}
rows={3} rows={2}
/> />
</div> </div>
{/* Filter Dropdowns */} {/* Filter Dropdowns */}
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-sm font-medium">Data Sources</Label>
<MultiSelect <MultiSelect
options={(availableFacets.data_sources || []).map((bucket) => ({ options={(availableFacets.data_sources || []).map((bucket) => ({
value: bucket.key, value: bucket.key,
@ -322,13 +336,12 @@ export function KnowledgeFilterPanel() {
onValueChange={(values) => onValueChange={(values) =>
handleFilterChange("data_sources", values) handleFilterChange("data_sources", values)
} }
placeholder="Select data sources..." placeholder="Select sources..."
allOptionLabel="All Data Sources" allOptionLabel="All sources"
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-sm font-medium">Document Types</Label>
<MultiSelect <MultiSelect
options={(availableFacets.document_types || []).map( options={(availableFacets.document_types || []).map(
(bucket) => ({ (bucket) => ({
@ -341,13 +354,12 @@ export function KnowledgeFilterPanel() {
onValueChange={(values) => onValueChange={(values) =>
handleFilterChange("document_types", values) handleFilterChange("document_types", values)
} }
placeholder="Select document types..." placeholder="Select types..."
allOptionLabel="All Document Types" allOptionLabel="All types"
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-sm font-medium">Owners</Label>
<MultiSelect <MultiSelect
options={(availableFacets.owners || []).map((bucket) => ({ options={(availableFacets.owners || []).map((bucket) => ({
value: bucket.key, value: bucket.key,
@ -362,7 +374,6 @@ export function KnowledgeFilterPanel() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-sm font-medium">Sources</Label>
<MultiSelect <MultiSelect
options={(availableFacets.connector_types || []).map( options={(availableFacets.connector_types || []).map(
(bucket) => ({ (bucket) => ({
@ -375,33 +386,13 @@ export function KnowledgeFilterPanel() {
onValueChange={(values) => onValueChange={(values) =>
handleFilterChange("connector_types", values) handleFilterChange("connector_types", values)
} }
placeholder="Select sources..." placeholder="Select connectors..."
allOptionLabel="All Sources" allOptionLabel="All connectors"
/> />
</div> </div>
{/* All/None buttons */}
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={selectAllFilters}
className="h-auto px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground hover:bg-muted/50 border-border/50"
>
All
</Button>
<Button
variant="outline"
size="sm"
onClick={clearAllFilters}
className="h-auto px-3 py-1.5 text-xs text-muted-foreground hover:text-foreground hover:bg-muted/50 border-border/50"
>
None
</Button>
</div>
{/* Result Limit Control - exactly like search page */} {/* Result Limit Control - exactly like search page */}
<div className="space-y-4 pt-4 border-t border-border/50"> <div className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Label className="text-sm font-medium text-nowrap"> <Label className="text-sm font-medium text-nowrap">
@ -462,39 +453,45 @@ export function KnowledgeFilterPanel() {
/> />
</div> </div>
</div> </div>
{/* Save Configuration Button */}
<div className="flex flex-col gap-3 pt-4 border-t border-border/50">
<Button
onClick={handleSaveConfiguration}
disabled={isSaving}
className="w-full"
size="sm"
>
{isSaving ? (
<>
<RefreshCw className="h-3 w-3 mr-2 animate-spin" />
Saving...
</>
) : (
<>
<Save className="h-3 w-3 mr-2" />
Save Configuration
</>
)}
</Button>
{!createMode && (
<Button
variant="destructive"
className="w-full"
onClick={handleDeleteFilter}
>
Delete Filter
</Button>
)}
</div>
</div> </div>
</CardContent> </CardContent>
<CardFooter className="mt-auto align-bottom justify-end gap-2">
{/* Save Configuration Button */}
{createMode && (
<Button
onClick={closePanelOnly}
disabled={isSaving}
variant="outline"
size="sm"
>
Cancel
</Button>
)}
{!createMode && (
<Button
variant="destructive"
size="sm"
onClick={handleDeleteFilter}
disabled={isSaving}
>
Delete Filter
</Button>
)}
<Button
onClick={handleSaveConfiguration}
disabled={isSaving}
size="sm"
className="relative"
>
{isSaving && (
<>
<RefreshCw className="h-3 w-3 animate-spin" />
Saving...
</>
)}
{!isSaving && (createMode ? "Create Filter" : "Update Filter")}
</Button>
</CardFooter>
</Card> </Card>
</div> </div>
); );

View file

@ -14,7 +14,7 @@ const buttonVariants = cva(
"border border-input hover:bg-muted hover:text-accent-foreground disabled:bg-muted disabled:!border-none", "border border-input hover:bg-muted hover:text-accent-foreground disabled:bg-muted disabled:!border-none",
primary: primary:
"border bg-background text-secondary-foreground hover:bg-muted hover:shadow-sm", "border bg-background text-secondary-foreground hover:bg-muted hover:shadow-sm",
warning: "bg-warning text-secondary hover:bg-warning/90", warning: "bg-warning text-warning-foreground hover:bg-warning/90",
secondary: secondary:
"border border-muted bg-muted text-secondary-foreground hover:bg-secondary-foreground/5", "border border-muted bg-muted text-secondary-foreground hover:bg-secondary-foreground/5",
ghost: ghost:

View file

@ -1,39 +1,39 @@
"use client" "use client";
import * as React from "react" import * as React from "react";
import { ChevronDown, Check } from "lucide-react" import { ChevronDown, Check } from "lucide-react";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button";
import { import {
Command, Command,
CommandEmpty, CommandEmpty,
CommandGroup, CommandGroup,
CommandInput, CommandInput,
CommandItem, CommandItem,
} from "@/components/ui/command" } from "@/components/ui/command";
import { import {
Popover, Popover,
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
} from "@/components/ui/popover" } from "@/components/ui/popover";
import { ScrollArea } from "@/components/ui/scroll-area" import { ScrollArea } from "@/components/ui/scroll-area";
interface Option { interface Option {
value: string value: string;
label: string label: string;
count?: number count?: number;
} }
interface MultiSelectProps { interface MultiSelectProps {
options: Option[] options: Option[];
value: string[] value: string[];
onValueChange: (value: string[]) => void onValueChange: (value: string[]) => void;
placeholder?: string placeholder?: string;
className?: string className?: string;
maxSelection?: number maxSelection?: number;
searchPlaceholder?: string searchPlaceholder?: string;
showAllOption?: boolean showAllOption?: boolean;
allOptionLabel?: string allOptionLabel?: string;
} }
export function MultiSelect({ export function MultiSelect({
@ -43,60 +43,61 @@ export function MultiSelect({
placeholder = "Select items...", placeholder = "Select items...",
className, className,
maxSelection, maxSelection,
searchPlaceholder = "Search...", searchPlaceholder = "Search options...",
showAllOption = true, showAllOption = true,
allOptionLabel = "All" allOptionLabel = "All",
}: MultiSelectProps) { }: MultiSelectProps) {
const [open, setOpen] = React.useState(false) const [open, setOpen] = React.useState(false);
const [searchValue, setSearchValue] = React.useState("") const [searchValue, setSearchValue] = React.useState("");
const isAllSelected = value.includes("*") const isAllSelected = value.includes("*");
const filteredOptions = options.filter(option => const filteredOptions = options.filter((option) =>
option.label.toLowerCase().includes(searchValue.toLowerCase()) option.label.toLowerCase().includes(searchValue.toLowerCase())
) );
const handleSelect = (optionValue: string) => { const handleSelect = (optionValue: string) => {
if (optionValue === "*") { if (optionValue === "*") {
// Toggle "All" selection // Toggle "All" selection
if (isAllSelected) { if (isAllSelected) {
onValueChange([]) onValueChange([]);
} else { } else {
onValueChange(["*"]) onValueChange(["*"]);
} }
} else { } else {
let newValue: string[] let newValue: string[];
if (value.includes(optionValue)) { if (value.includes(optionValue)) {
// Remove the item // Remove the item
newValue = value.filter(v => v !== optionValue && v !== "*") newValue = value.filter((v) => v !== optionValue && v !== "*");
} else { } else {
// Add the item and remove "All" if present // Add the item and remove "All" if present
newValue = [...value.filter(v => v !== "*"), optionValue] newValue = [...value.filter((v) => v !== "*"), optionValue];
// Check max selection limit // Check max selection limit
if (maxSelection && newValue.length > maxSelection) { if (maxSelection && newValue.length > maxSelection) {
return return;
} }
} }
onValueChange(newValue) onValueChange(newValue);
} }
} };
const getDisplayText = () => { const getDisplayText = () => {
if (isAllSelected) { if (isAllSelected) {
return allOptionLabel return allOptionLabel;
} }
if (value.length === 0) { if (value.length === 0) {
return placeholder return placeholder;
} }
// Extract the noun from placeholder (e.g., "Select data sources..." -> "data sources") // Extract the noun from placeholder (e.g., "Select data sources..." -> "data sources")
const noun = placeholder.toLowerCase().replace('select ', '').replace('...', '') const noun = placeholder
return `${value.length} ${noun}` .toLowerCase()
} .replace("select ", "")
.replace("...", "");
return `${value.length} ${noun}`;
};
return ( return (
<Popover open={open} onOpenChange={setOpen}> <Popover open={open} onOpenChange={setOpen}>
@ -106,17 +107,15 @@ export function MultiSelect({
role="combobox" role="combobox"
aria-expanded={open} aria-expanded={open}
className={cn( className={cn(
"w-full justify-between min-h-[40px] h-auto text-left", "w-full justify-between h-8 py-0 text-left",
className className
)} )}
> >
<span className="text-foreground text-sm"> <span className="text-foreground text-sm">{getDisplayText()}</span>
{getDisplayText()}
</span>
<ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> <ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-full p-0" align="start"> <PopoverContent className="p-0" align="start">
<Command> <Command>
<CommandInput <CommandInput
placeholder={searchPlaceholder} placeholder={searchPlaceholder}
@ -132,16 +131,13 @@ export function MultiSelect({
onSelect={() => handleSelect("*")} onSelect={() => handleSelect("*")}
className="cursor-pointer" className="cursor-pointer"
> >
<span className="flex-1">{allOptionLabel}</span>
<Check <Check
className={cn( className={cn(
"mr-2 h-4 w-4", "mr-2 h-4 w-4",
isAllSelected ? "opacity-100" : "opacity-0" isAllSelected ? "opacity-100" : "opacity-0"
)} )}
/> />
<span className="flex-1">{allOptionLabel}</span>
<span className="text-xs text-blue-500 bg-blue-500/10 px-1.5 py-0.5 rounded ml-2">
*
</span>
</CommandItem> </CommandItem>
)} )}
{filteredOptions.map((option) => ( {filteredOptions.map((option) => (
@ -149,20 +145,19 @@ export function MultiSelect({
key={option.value} key={option.value}
onSelect={() => handleSelect(option.value)} onSelect={() => handleSelect(option.value)}
className="cursor-pointer" className="cursor-pointer"
disabled={isAllSelected}
> >
<Check
className={cn(
"mr-2 h-4 w-4",
value.includes(option.value) ? "opacity-100" : "opacity-0"
)}
/>
<span className="flex-1">{option.label}</span> <span className="flex-1">{option.label}</span>
{option.count !== undefined && ( {option.count !== undefined && (
<span className="text-xs text-muted-foreground bg-muted/50 px-1.5 py-0.5 rounded ml-2"> <span className="text-xs text-muted-foreground bg-muted/50 px-1.5 py-0.5 rounded ml-2">
{option.count} {option.count}
</span> </span>
)} )}
<Check
className={cn(
"mr-2 h-4 w-4",
value.includes(option.value) ? "opacity-100" : "opacity-0"
)}
/>
</CommandItem> </CommandItem>
))} ))}
</ScrollArea> </ScrollArea>
@ -170,5 +165,5 @@ export function MultiSelect({
</Command> </Command>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
) );
} }

View file

@ -17,10 +17,10 @@ const Slider = React.forwardRef<
)} )}
{...props} {...props}
> >
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-muted/40"> <SliderPrimitive.Track className="relative h-[5px] w-full grow overflow-hidden rounded-full bg-muted">
<SliderPrimitive.Range className="absolute h-full bg-primary" /> <SliderPrimitive.Range className="absolute h-full bg-muted-foreground" />
</SliderPrimitive.Track> </SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" /> <SliderPrimitive.Thumb className="block h-4 w-4 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root> </SliderPrimitive.Root>
)) ))
Slider.displayName = SliderPrimitive.Root.displayName Slider.displayName = SliderPrimitive.Root.displayName

View file

@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
<textarea <textarea
data-slot="textarea" data-slot="textarea"
className={cn( className={cn(
"primary-input placeholder:font-mono placeholder:text-placeholder-foreground min-h-fit", "primary-input placeholder:text-placeholder-foreground min-h-fit",
className className
)} )}
{...props} {...props}

View file

@ -28,7 +28,6 @@
"@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8", "@radix-ui/react-tooltip": "^1.2.8",
"@tailwindcss/forms": "^0.5.10", "@tailwindcss/forms": "^0.5.10",
"@tailwindcss/line-clamp": "^0.4.4",
"@tailwindcss/typography": "^0.5.16", "@tailwindcss/typography": "^0.5.16",
"@tanstack/react-query": "^5.86.0", "@tanstack/react-query": "^5.86.0",
"ag-grid-community": "^34.2.0", "ag-grid-community": "^34.2.0",
@ -2318,14 +2317,6 @@
"tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1" "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1"
} }
}, },
"node_modules/@tailwindcss/line-clamp": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/@tailwindcss/line-clamp/-/line-clamp-0.4.4.tgz",
"integrity": "sha512-5U6SY5z8N42VtrCrKlsTAA35gy2VSyYtHWCsg1H87NU1SXnEfekTVlrga9fzUDrrHcGi2Lb5KenUWb4lRQT5/g==",
"peerDependencies": {
"tailwindcss": ">=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1"
}
},
"node_modules/@tailwindcss/typography": { "node_modules/@tailwindcss/typography": {
"version": "0.5.16", "version": "0.5.16",
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz",

View file

@ -29,7 +29,6 @@
"@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8", "@radix-ui/react-tooltip": "^1.2.8",
"@tailwindcss/forms": "^0.5.10", "@tailwindcss/forms": "^0.5.10",
"@tailwindcss/line-clamp": "^0.4.4",
"@tailwindcss/typography": "^0.5.16", "@tailwindcss/typography": "^0.5.16",
"@tanstack/react-query": "^5.86.0", "@tanstack/react-query": "^5.86.0",
"ag-grid-community": "^34.2.0", "ag-grid-community": "^34.2.0",

View file

@ -25,27 +25,28 @@
--muted-foreground: 240 4% 46%; --muted-foreground: 240 4% 46%;
--accent: 240 5% 96%; --accent: 240 5% 96%;
--accent-foreground: 0 0% 0%; --accent-foreground: 0 0% 0%;
--destructive: 0 72% 51%;
--destructive-foreground: 0 0% 100%;
--warning: 48, 96%, 89%;
--warning-foreground: 26, 90%, 37%;
--border: 240 4.8% 95.9%; --border: 240 4.8% 95.9%;
--input: 240 6% 90%; --input: 240 6% 90%;
--ring: 0 0% 0%; --ring: 0 0% 0%;
--placeholder-foreground: 240 5% 65%; --placeholder-foreground: 240 5% 65%;
--accent-amber: 48, 96%, 89%; /* amber-100 #fef3c7 */ --accent-amber: 48 96% 89%; /* amber-100 #fef3c7 */
--accent-amber-foreground: 26, 90%, 37%; /* amber-700 #b45309 */ --accent-amber-foreground: 26 90% 37%; /* amber-700 #b45309 */
--accent-emerald: 149, 80%, 90%; /* emerald-100 #d1fae5 */ --accent-emerald: 149 80% 90%; /* emerald-100 #d1fae5 */
--accent-emerald-foreground: 161, 94%, 30%; /* emerald-600 #059669 */ --accent-emerald-foreground: 161 94% 30%; /* emerald-600 #059669 */
--accent-red: 0, 93%, 94%; /* red-100 #fee2e2 */ --accent-red: 0 93% 94%; /* red-100 #fee2e2 */
--accent-red-foreground: 0, 72%, 51%; /* red-600 #dc2626 */ --accent-red-foreground: 0 72% 51%; /* red-600 #dc2626 */
--accent-indigo: 226, 100%, 94%; /* indigo-100 #e0e7ff */ --accent-indigo: 226 100% 94%; /* indigo-100 #e0e7ff */
--accent-indigo-foreground: 243, 75%, 59%; /* indigo-600 #4f46e5 */ --accent-indigo-foreground: 243 75% 59%; /* indigo-600 #4f46e5 */
--accent-pink: 326, 78%, 95%; /* pink-100 #fce7f3 */ --accent-pink: 326 78% 95%; /* pink-100 #fce7f3 */
--accent-pink-foreground: 333, 71%, 51%; /* pink-600 #db2777 */ --accent-pink-foreground: 333 71% 51%; /* pink-600 #db2777 */
--accent-purple: 269, 100%, 95%; /* purple-100 #f3e8ff */ --accent-purple: 269 100% 95%; /* purple-100 #f3e8ff */
--accent-purple-foreground: 271, 81%, 56%; /* purple-600 #7c3aed */ --accent-purple-foreground: 271 81% 56%; /* purple-600 #7c3aed */
--destructive: 0 72% 51%; /* red-600 #dc2626 */
--destructive-foreground: 0 0% 100%;
--warning: 26 90% 37%; /* amber-700 #b45309 */
--warning-foreground: 0 0% 100%;
/* Component Colors */ /* Component Colors */
--component-icon: #d8598a; --component-icon: #d8598a;
@ -71,27 +72,28 @@
--muted-foreground: 240 5% 65%; --muted-foreground: 240 5% 65%;
--accent: 240 4% 16%; --accent: 240 4% 16%;
--accent-foreground: 0 0% 100%; --accent-foreground: 0 0% 100%;
--destructive: 0 84% 60%;
--destructive-foreground: 0 0% 100%;
--warning: 22, 78%, 26%;
--warning-foreground: 46, 97%, 65%;
--border: 240 3.7% 15.9%; --border: 240 3.7% 15.9%;
--input: 240 5% 34%; --input: 240 5% 34%;
--ring: 0 0% 100%; --ring: 0 0% 100%;
--placeholder-foreground: 240 4% 46%; --placeholder-foreground: 240 4% 46%;
--accent-amber: 22, 78%, 26%; /* amber-900 #78350f */ --accent-amber: 22 78% 26%; /* amber-900 #78350f */
--accent-amber-foreground: 46, 97%, 65%; /* amber-300 #fcd34d */ --accent-amber-foreground: 46 97% 65%; /* amber-300 #fcd34d */
--accent-emerald: 164, 86%, 16%; /* emerald-900 #064e3b */ --accent-emerald: 164 86% 16%; /* emerald-900 #064e3b */
--accent-emerald-foreground: 158, 64%, 52%; /* emerald-400 #34d399 */ --accent-emerald-foreground: 158 64% 52%; /* emerald-400 #34d399 */
--accent-red: 0, 63%, 31%; /* red-900 #7f1d1d */ --accent-red: 0 63% 31%; /* red-900 #7f1d1d */
--accent-red-foreground: 0, 91%, 71%; /* red-400 #f87171 */ --accent-red-foreground: 0 91% 71%; /* red-400 #f87171 */
--accent-indigo: 242, 47%, 34%; /* indigo-900 #312e81 */ --accent-indigo: 242 47% 34%; /* indigo-900 #312e81 */
--accent-indigo-foreground: 234, 89%, 74%; /* indigo-400 #818cf8 */ --accent-indigo-foreground: 234 89% 74%; /* indigo-400 #818cf8 */
--accent-pink: 336, 69%, 30%; /* pink-900 #831843 */ --accent-pink: 336 69% 30%; /* pink-900 #831843 */
--accent-pink-foreground: 329, 86%, 70%; /* pink-400 #f472b6 */ --accent-pink-foreground: 329 86% 70%; /* pink-400 #f472b6 */
--accent-purple: 274, 66%, 32%; /* purple-900 #4c1d95 */ --accent-purple: 274 66% 32%; /* purple-900 #4c1d95 */
--accent-purple-foreground: 270, 95%, 75%; /* purple-400 #a78bfa */ --accent-purple-foreground: 270 95% 75%; /* purple-400 #a78bfa */
--destructive: 0 84% 60%; /* red-500 #ef4444 */
--destructive-foreground: 0 0% 100%;
--warning: 46 97% 65%; /* amber-300 #fcd34d */
--warning-foreground: 0 0% 0%;
} }
* { * {

View file

@ -64,7 +64,7 @@ function SearchPage() {
}; };
// Convert TaskFiles to File format and merge with backend results // Convert TaskFiles to File format and merge with backend results
const taskFilesAsFiles: File[] = taskFiles.map(taskFile => { const taskFilesAsFiles: File[] = taskFiles.map((taskFile) => {
return { return {
filename: taskFile.filename, filename: taskFile.filename,
mimetype: taskFile.mimetype, mimetype: taskFile.mimetype,
@ -77,11 +77,11 @@ function SearchPage() {
const backendFiles = data as File[]; const backendFiles = data as File[];
const filteredTaskFiles = taskFilesAsFiles.filter(taskFile => { const filteredTaskFiles = taskFilesAsFiles.filter((taskFile) => {
return ( return (
taskFile.status !== "active" && taskFile.status !== "active" &&
!backendFiles.some( !backendFiles.some(
backendFile => backendFile.filename === taskFile.filename (backendFile) => backendFile.filename === taskFile.filename
) )
); );
}); });
@ -123,7 +123,7 @@ function SearchPage() {
{ {
field: "size", field: "size",
headerName: "Size", headerName: "Size",
valueFormatter: params => valueFormatter: (params) =>
params.value ? `${Math.round(params.value / 1024)} KB` : "-", params.value ? `${Math.round(params.value / 1024)} KB` : "-",
}, },
{ {
@ -133,13 +133,13 @@ function SearchPage() {
{ {
field: "owner", field: "owner",
headerName: "Owner", headerName: "Owner",
valueFormatter: params => valueFormatter: (params) =>
params.data?.owner_name || params.data?.owner_email || "—", params.data?.owner_name || params.data?.owner_email || "—",
}, },
{ {
field: "chunkCount", field: "chunkCount",
headerName: "Chunks", headerName: "Chunks",
valueFormatter: params => params.data?.chunkCount?.toString() || "-", valueFormatter: (params) => params.data?.chunkCount?.toString() || "-",
}, },
{ {
field: "avgScore", field: "avgScore",
@ -201,7 +201,7 @@ function SearchPage() {
try { try {
// Delete each file individually since the API expects one filename at a time // Delete each file individually since the API expects one filename at a time
const deletePromises = selectedRows.map(row => const deletePromises = selectedRows.map((row) =>
deleteDocumentMutation.mutateAsync({ filename: row.filename }) deleteDocumentMutation.mutateAsync({ filename: row.filename })
); );
@ -255,7 +255,7 @@ function SearchPage() {
<div className="primary-input min-h-10 !flex items-center flex-nowrap focus-within:border-foreground transition-colors !p-[0.3rem]"> <div className="primary-input min-h-10 !flex items-center flex-nowrap focus-within:border-foreground transition-colors !p-[0.3rem]">
{selectedFilter?.name && ( {selectedFilter?.name && (
<div <div
className={`flex items-center gap-1 h-full px-1.5 py-0.5 rounded max-w-[300px] ${ className={`flex items-center gap-1 h-full px-1.5 py-0.5 mr-1 rounded max-w-[25%] ${
filterAccentClasses[parsedFilterData?.color || "zinc"] filterAccentClasses[parsedFilterData?.color || "zinc"]
}`} }`}
> >
@ -267,8 +267,12 @@ function SearchPage() {
/> />
</div> </div>
)} )}
<Search
className="h-4 w-4 ml-1 flex-shrink-0 text-placeholder-foreground"
strokeWidth={1.5}
/>
<input <input
className="bg-transparent w-full h-full ml-2 focus:outline-none focus-visible:outline-none placeholder:font-mono" className="bg-transparent w-full h-full ml-2 focus:outline-none focus-visible:outline-none font-mono placeholder:font-mono"
name="search-query" name="search-query"
id="search-query" id="search-query"
type="text" type="text"
@ -318,7 +322,7 @@ function SearchPage() {
rowSelection="multiple" rowSelection="multiple"
rowMultiSelectWithClick={false} rowMultiSelectWithClick={false}
suppressRowClickSelection={true} suppressRowClickSelection={true}
getRowId={params => params.data.filename} getRowId={(params) => params.data.filename}
domLayout="normal" domLayout="normal"
onSelectionChanged={onSelectionChanged} onSelectionChanged={onSelectionChanged}
noRowsOverlayComponent={() => ( noRowsOverlayComponent={() => (
@ -346,7 +350,7 @@ function SearchPage() {
}? This will remove all chunks and data associated with these documents. This action cannot be undone. }? This will remove all chunks and data associated with these documents. This action cannot be undone.
Documents to be deleted: Documents to be deleted:
${selectedRows.map(row => `${row.filename}`).join("\n")}`} ${selectedRows.map((row) => `${row.filename}`).join("\n")}`}
confirmText="Delete All" confirmText="Delete All"
onConfirm={handleBulkDelete} onConfirm={handleBulkDelete}
isLoading={deleteDocumentMutation.isPending} isLoading={deleteDocumentMutation.isPending}

View file

@ -4,7 +4,6 @@ import tailwindcssTypography from "@tailwindcss/typography";
import { fontFamily } from "tailwindcss/defaultTheme"; import { fontFamily } from "tailwindcss/defaultTheme";
import plugin from "tailwindcss/plugin"; import plugin from "tailwindcss/plugin";
import tailwindcssAnimate from "tailwindcss-animate"; import tailwindcssAnimate from "tailwindcss-animate";
import tailwindcssLineClamp from "@tailwindcss/line-clamp";
const config = { const config = {
darkMode: ["class"], darkMode: ["class"],
@ -167,7 +166,6 @@ const config = {
}, },
plugins: [ plugins: [
tailwindcssAnimate, tailwindcssAnimate,
tailwindcssLineClamp,
tailwindcssForms({ tailwindcssForms({
strategy: "class", strategy: "class",
}), }),