feat: add autocomplete suggestions feature in search page

This commit is contained in:
Yash-1511
2025-01-05 17:30:00 +05:30
parent 756f4a2a66
commit f306159a5a
2 changed files with 98 additions and 22 deletions

View File

@@ -173,7 +173,10 @@ export default function Search() {
const [searchResultsLoading, setSearchResultsLoading] = useState(false); const [searchResultsLoading, setSearchResultsLoading] = useState(false);
const [focusSearchResult, setFocusSearchResult] = useState<SearchResult | null>(null); const [focusSearchResult, setFocusSearchResult] = useState<SearchResult | null>(null);
const [exampleQuery, setExampleQuery] = useState(""); const [exampleQuery, setExampleQuery] = useState("");
const [fileSuggestions, setFileSuggestions] = useState<string[]>([]);
const [showSuggestions, setShowSuggestions] = useState(false);
const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null); const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const suggestionsTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const isMobileWidth = useIsMobileWidth(); const isMobileWidth = useIsMobileWidth();
@@ -205,29 +208,62 @@ export default function Search() {
}); });
} }
useEffect(() => { function getFileSuggestions(query: string) {
if (!searchQuery.trim()) { // Get suggestions only if query starts with "file:"
if (!query.toLowerCase().startsWith("file:")) {
setFileSuggestions([]);
setShowSuggestions(false);
return; return;
} }
setFocusSearchResult(null); const filePrefix = query.substring(5).trim(); // Remove "file:" prefix
const apiUrl = `/api/file-suggestions?q=${encodeURIComponent(filePrefix)}`;
fetch(apiUrl, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
})
.then((response) => response.json())
.then((data) => {
setFileSuggestions(data);
setShowSuggestions(true);
})
.catch((error) => {
console.error("Error:", error);
});
}
function handleSearchInputChange(value: string) {
setSearchQuery(value);
// Clear previous timeouts
if (searchTimeoutRef.current) { if (searchTimeoutRef.current) {
clearTimeout(searchTimeoutRef.current); clearTimeout(searchTimeoutRef.current);
} }
if (suggestionsTimeoutRef.current) {
clearTimeout(suggestionsTimeoutRef.current);
}
if (searchQuery.trim()) { // Get file suggestions immediately
suggestionsTimeoutRef.current = setTimeout(() => {
getFileSuggestions(value);
}, 100);
// Debounce search
if (value.trim()) {
searchTimeoutRef.current = setTimeout(() => { searchTimeoutRef.current = setTimeout(() => {
search(); search();
}, 750); // 1000 milliseconds = 1 second }, 750);
}
} }
return () => { function applySuggestion(suggestion: string) {
if (searchTimeoutRef.current) { setSearchQuery(`file:${suggestion}`);
clearTimeout(searchTimeoutRef.current); setShowSuggestions(false);
search();
} }
};
}, [searchQuery]);
return ( return (
<SidebarProvider> <SidebarProvider>
@@ -247,14 +283,38 @@ export default function Search() {
<div className="md:w-3/4 sm:w-full mx-auto pt-6 md:pt-8"> <div className="md:w-3/4 sm:w-full mx-auto pt-6 md:pt-8">
<div className="p-4 md:w-3/4 sm:w-full mx-auto"> <div className="p-4 md:w-3/4 sm: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">
<div className="relative flex-1">
<Input <Input
autoFocus={true} autoFocus={true}
className="border-none pl-4" className="border-none pl-4"
onChange={(e) => setSearchQuery(e.currentTarget.value)} onChange={(e) => handleSearchInputChange(e.currentTarget.value)}
onKeyDown={(e) => e.key === "Enter" && search()} onKeyDown={(e) => {
if (e.key === "Enter") {
if (showSuggestions && fileSuggestions.length > 0) {
applySuggestion(fileSuggestions[0]);
} else {
search();
}
}
}}
type="search" type="search"
placeholder="Search Documents" placeholder="Search Documents (type 'file:' for file suggestions)"
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>
<button <button
className="px-2 gap-2 inline-flex items-center rounded border-l border-gray-300 hover:text-gray-500" className="px-2 gap-2 inline-flex items-center rounded border-l border-gray-300 hover:text-gray-500"
onClick={() => search()} onClick={() => search()}

View File

@@ -757,3 +757,19 @@ def edit_job(
# Return modified automation information as a JSON response # Return modified automation information as a JSON response
return Response(content=json.dumps(automation_info), media_type="application/json", status_code=200) return Response(content=json.dumps(automation_info), media_type="application/json", status_code=200)
@api.get("/file-suggestions", response_class=Response)
@requires(["authenticated"])
def get_file_suggestions(request: Request, q: str = ""):
"""Get file suggestions for autocompletion based on the query prefix."""
user = request.user.object
file_list = EntryAdapters.get_all_filenames_by_source(user, "computer")
# Filter files based on query prefix
suggestions = [f for f in file_list if f.lower().startswith(q.lower())]
# Sort suggestions alphabetically and limit to top 10
suggestions = sorted(suggestions)[:10]
return Response(content=json.dumps(suggestions), media_type="application/json")