Add a file filter combobox for easier file filter selection

This commit is contained in:
sabaimran
2025-01-20 14:39:29 -08:00
parent d36f235da5
commit 9f18d6494f

View File

@@ -23,7 +23,6 @@ import {
MagnifyingGlass, MagnifyingGlass,
NoteBlank, NoteBlank,
NotionLogo, NotionLogo,
Eye,
Trash, Trash,
DotsThreeVertical, DotsThreeVertical,
Waveform, Waveform,
@@ -31,6 +30,8 @@ import {
Download, Download,
Brain, Brain,
Check, Check,
BoxArrowDown,
Funnel,
} from "@phosphor-icons/react"; } from "@phosphor-icons/react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import Link from "next/link"; import Link from "next/link";
@@ -65,8 +66,23 @@ import {
PaginationPrevious, PaginationPrevious,
} from "@/components/ui/pagination"; } from "@/components/ui/pagination";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
import { uploadDataForIndexing } from "../common/chatFunctions"; import { uploadDataForIndexing } from "../common/chatFunctions";
import { Progress } from "@/components/ui/progress"; import { Progress } from "@/components/ui/progress";
import { cn } from "@/lib/utils";
interface AdditionalData { interface AdditionalData {
file: string; file: string;
source: string; source: string;
@@ -461,19 +477,93 @@ const UploadFiles: React.FC<{
); );
}; };
interface FileFilterComboBoxProps {
allFiles: string[];
onChooseFile: (file: string) => void;
isMobileWidth: boolean;
explicitFile?: string;
}
export function FileFilterComboBox(props: FileFilterComboBoxProps) {
const [open, setOpen] = useState(false)
const [value, setValue] = useState(props.explicitFile || "")
useEffect(() => {
if (props.explicitFile) {
setValue(props.explicitFile);
}
}, [props.explicitFile]);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className={`justify-between" ${props.isMobileWidth ? "w-full" : "w-[200px]"}`}
>
{value
? (
props.isMobileWidth ?
"✔️"
: "Selected"
)
: (
props.isMobileWidth ?
" "
: "Select file"
)
}
<Funnel className="opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0">
<Command>
<CommandInput placeholder="Search framework..." />
<CommandList>
<CommandEmpty>No framework found.</CommandEmpty>
<CommandGroup>
{props.allFiles.map((file) => (
<CommandItem
key={file}
value={file}
onSelect={(currentValue) => {
setValue(currentValue === value ? "" : currentValue)
setOpen(false)
props.onChooseFile(currentValue)
}}
>
{file}
<Check
className={cn(
"ml-auto",
value === file ? "opacity-100" : "opacity-0"
)}
/>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
)
}
export default function Search() { export default function Search() {
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const [searchResults, setSearchResults] = useState<SearchResult[] | null>(null); const [searchResults, setSearchResults] = useState<SearchResult[] | null>(null);
const [searchResultsLoading, setSearchResultsLoading] = useState(false); const [searchResultsLoading, setSearchResultsLoading] = useState(false);
const searchInputRef = useRef<HTMLInputElement>(null);
const [focusSearchResult, setFocusSearchResult] = useState<SearchResult | null>(null); const [focusSearchResult, setFocusSearchResult] = useState<SearchResult | null>(null);
const [files, setFiles] = useState<FileObject[]>([]); const [files, setFiles] = useState<FileObject[]>([]);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [fileObjectsLoading, setFileObjectsLoading] = useState(true); const [fileObjectsLoading, setFileObjectsLoading] = useState(true);
const [fileSuggestions, setFileSuggestions] = useState<string[]>([]);
const [allFiles, setAllFiles] = useState<string[]>([]); const [allFiles, setAllFiles] = useState<string[]>([]);
const [showSuggestions, setShowSuggestions] = useState(false);
const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null); const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const [selectedFile, setSelectedFile] = useState<string | null>(null); const [selectedFile, setSelectedFile] = useState<string | null>(null);
const [selectedFileFilter, setSelectedFileFilter] = useState<string | undefined>(undefined);
const [selectedFileFullText, setSelectedFileFullText] = useState<string | null>(null); const [selectedFileFullText, setSelectedFileFullText] = useState<string | null>(null);
const [isDeleting, setIsDeleting] = useState(false); const [isDeleting, setIsDeleting] = useState(false);
const [uploadedFiles, setUploadedFiles] = useState<string[]>([]); const [uploadedFiles, setUploadedFiles] = useState<string[]>([]);
@@ -501,30 +591,27 @@ export default function Search() {
}); });
}, []); }, []);
function getFileSuggestions(query: string) { useEffect(() => {
const fileFilterMatch = query.match(/file:([^"\s]*|"[^"]*")?/); // Replace the file: filter with the selected suggestion
if (!fileFilterMatch) { const fileFilterMatch = searchQuery.match(/file:([^"\s]*|"[^"]*")?/);
setFileSuggestions([]);
setShowSuggestions(false); if (fileFilterMatch) {
return; const extractedFileFilter = fileFilterMatch[1];
// Strip the double quotes
const extractedFileFilterValue = extractedFileFilter?.replace(/"/g, "");
if (extractedFileFilterValue) {
setSelectedFileFilter(extractedFileFilterValue);
}
} }
}, [searchQuery]);
const filePrefix = fileFilterMatch[1]?.replace(/^"|"$/g, '').trim() || '';
const filteredSuggestions = allFiles
.filter(file => file.toLowerCase().includes(filePrefix.toLowerCase()))
.sort()
.slice(0, 10);
setFileSuggestions(filteredSuggestions);
setShowSuggestions(true);
}
function handleSearchInputChange(value: string) { function handleSearchInputChange(value: string) {
setSearchQuery(value); setSearchQuery(value);
if (!value.trim()) { if (!value.trim()) {
setSearchResults(null); setSearchResults(null);
setShowSuggestions(false);
return; return;
} }
@@ -533,9 +620,6 @@ export default function Search() {
clearTimeout(searchTimeoutRef.current); clearTimeout(searchTimeoutRef.current);
} }
// Get file suggestions immediately
getFileSuggestions(value);
// Debounce search // Debounce search
if (value.trim()) { if (value.trim()) {
searchTimeoutRef.current = setTimeout(() => { searchTimeoutRef.current = setTimeout(() => {
@@ -545,10 +629,10 @@ export default function Search() {
} }
function applySuggestion(suggestion: string) { function applySuggestion(suggestion: string) {
// Replace the file: filter with the selected suggestion // Append the file: filter with the selected suggestion
const newQuery = searchQuery.replace(/file:([^"\s]*|"[^"]*")?/, `file:"${suggestion}"`); const newQuery = `file:"${suggestion}" ${searchQuery}`;
setSearchQuery(newQuery); setSearchQuery(newQuery);
setShowSuggestions(false); searchInputRef.current?.focus();
search(); search();
} }
@@ -687,49 +771,36 @@ export default function Search() {
</header> </header>
<div> <div>
<div className={`${styles.searchLayout}`}> <div className={`${styles.searchLayout}`}>
<div className="md:w-5/6 sm:w-full mx-auto pt-6 md:pt-8"> <div className="w-full md:w-5/6 mx-auto pt-6 md:pt-8">
<div className="p-4 w-full mx-auto"> <div className="p-4 w-full mx-auto">
<div className="flex justify-between items-center border-2 border-muted p-1 gap-1 rounded-lg"> <div className="flex justify-between items-center border-2 border-muted p-1 gap-1 rounded-lg flex-col md:flex-row">
<div className="relative flex-1"> <div className="relative flex-1 w-full">
<Input <Input
autoFocus={true} autoFocus={true}
className="border-none pl-4 focus-visible:ring-transparent focus-visible:ring-offset-transparent" className="border-none pl-4 focus-visible:ring-transparent focus-visible:ring-offset-transparent"
onChange={(e) => handleSearchInputChange(e.currentTarget.value)} onChange={(e) => handleSearchInputChange(e.currentTarget.value)}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Enter") { if (e.key === "Enter") {
if (showSuggestions && fileSuggestions.length > 0) { search();
applySuggestion(fileSuggestions[0]);
} else {
search();
}
} }
}} }}
ref={searchInputRef}
type="search" type="search"
placeholder="Search Documents (type 'file:' for file suggestions)" placeholder="Search Documents"
value={searchQuery} value={searchQuery}
/> />
{showSuggestions && fileSuggestions.length > 0 && (
<div className="absolute z-10 w-full mt-1 bg-background border rounded-md shadow-lg">
{fileSuggestions.map((suggestion, index) => (
<div
key={index}
className="px-4 py-2 hover:bg-muted cursor-pointer"
onClick={() => applySuggestion(suggestion)}
>
{suggestion}
</div>
))}
</div>
)}
</div> </div>
<Button <div id="actions" className="flex gap-2">
className="px-2 gap-2 inline-flex rounded-none items-center border-l border-gray-300 hover:text-gray-500" <FileFilterComboBox allFiles={allFiles} onChooseFile={(file) => applySuggestion(file)} isMobileWidth={isMobileWidth} explicitFile={selectedFileFilter} />
variant={"ghost"} <Button
onClick={() => search()} className="px-2 gap-2 inline-flex rounded-none items-center border-l border-gray-300 hover:text-gray-500"
> variant={"ghost"}
<MagnifyingGlass className="h-4 w-4" /> onClick={() => search()}
<span>Find</span> >
</Button> <MagnifyingGlass className="h-4 w-4" />
<span>Find</span>
</Button>
</div>
</div> </div>
{searchResults === null && ( {searchResults === null && (
<UploadFiles setUploadedFiles={setUploadedFiles} /> <UploadFiles setUploadedFiles={setUploadedFiles} />