Merge pull request #44 from nix-community/feat/autocomplete-suggestions

autocomplete: init
This commit is contained in:
Johannes Kirschbauer 2023-09-21 11:15:15 +02:00 committed by GitHub
commit 5913852836
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 126 additions and 65 deletions

View File

@ -1,50 +1,62 @@
import { Box } from "@mui/system";
import { useEffect, useMemo } from "react";
import { PageState } from "../../models/internals";
import { PageState, normalizePath } from "../../models/internals";
import { byType, pipe } from "../../queries";
import { DocItem } from "../../models/nix";
import { BasicList, BasicListItem } from "../basicList";
import FunctionItem from "../functionItem/functionItem";
import { SetPageStateVariable } from "../pageContext";
import { useMiniSearch } from "react-minisearch";
interface FunctionsProps {
pageState: PageState;
setPageStateVariable: SetPageStateVariable;
}
export const commonSearchOptions = {
// allow 22% levenshtein distance (e.g. 2.2 of 10 characters don't match)
fuzzy: 0.22,
// prefer to show builtins first
boostDocument: (id: string, term: string) => {
let boost = 1;
boost *= id.includes("builtins") ? 10 : 1;
boost *= id.includes(term) ? 100 : 1;
return boost;
},
boost: {
id: 1000,
name: 100,
category: 10,
example: 0.5,
fn_type: 10,
description: 1,
},
};
export function NixFunctions(props: FunctionsProps) {
const { pageState, setPageStateVariable } = props;
const { data, selected, term, filter } = pageState;
const setSelected = setPageStateVariable<string | null>("selected");
const { search, searchResults, rawResults } = useMiniSearch<DocItem>(data, {
const minisearch = useMiniSearch<DocItem>(data, {
fields: ["id", "name", "category", "description", "example", "fn_type"],
searchOptions: {
// allow 22% levenshtein distance (e.g. 2.2 of 10 characters don't match)
fuzzy: 0.22,
// prefer to show builtins first
boostDocument: (id, term) => {
let boost = 1;
boost *= id.includes("builtins") ? 10 : 1;
boost *= id.includes(term) ? 10 : 1;
return boost;
},
boost: {
id: 1000,
name: 100,
category: 10,
example: 0.5,
fn_type: 10,
description: 1,
},
},
searchOptions: commonSearchOptions,
idField: "id",
tokenize: (text: string, fieldName): string[] => {
let normalizedId = normalizePath(text);
const tokens = [];
if (fieldName === "id") {
tokens.push(text);
tokens.push(normalizedId);
tokens.push(...normalizedId.split("."));
}
//split the text into words
const wordTokens = text.split(/\W/);
const containsUpper = (w: string) => Boolean(w.match(/[A-Z]/)?.length);
const tokens = [
tokens.push(
text,
// include the words itself if they contain upperCharacters
// mapAttrs -> mapAttrs
...wordTokens.filter(containsUpper),
@ -56,15 +68,17 @@ export function NixFunctions(props: FunctionsProps) {
.flat(),
// just include lowercase words without further tokenizing
// map -> map
...wordTokens.filter((w) => !containsUpper(w)),
];
return tokens;
...wordTokens.filter((w) => !containsUpper(w))
);
return tokens.filter(Boolean);
},
});
const { search, searchResults, rawResults } = minisearch;
//initial site-load is safe to call
useEffect(() => {
search(term);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@ -103,7 +117,7 @@ export function NixFunctions(props: FunctionsProps) {
return (
<Box sx={{ ml: { xs: 0, md: 2 } }}>
<BasicList items={preRenderedItems} search={search} />
<BasicList items={preRenderedItems} minisearch={minisearch} />
</Box>
);
}

View File

@ -8,8 +8,9 @@ import { usePageContext } from "../pageContext";
import { useMobile } from "../layout/layout";
import { EmptyRecordsPlaceholder } from "../emptyRecordsPlaceholder";
import { FunctionOfTheDay } from "../functionOfTheDay";
import { Query, SearchOptions } from "minisearch";
import { ViewMode } from "../../models/internals";
import { UseMiniSearch } from "react-minisearch";
import { DocItem } from "../../models/nix";
export type BasicListItem = {
item: React.ReactNode;
@ -17,11 +18,12 @@ export type BasicListItem = {
};
export type BasicListProps = BasicDataViewProps & {
selected?: string | null;
search: (query: Query, options?: SearchOptions) => void;
minisearch: UseMiniSearch<DocItem>;
};
export function BasicList(props: BasicListProps) {
const { items, search } = props;
const { items, minisearch } = props;
const { search, suggestions, autoSuggest, clearSuggestions } = minisearch;
const { pageState, setPageStateVariable, resetQueries } = usePageContext();
const isMobile = useMobile();
const { page, itemsPerPage, FOTD: showFunctionOfTheDay, data } = pageState;
@ -72,6 +74,9 @@ export function BasicList(props: BasicListProps) {
handleClear={handleClear}
placeholder="search nix functions"
handleSearch={handleSearch}
suggestions={suggestions || []}
autoSuggest={autoSuggest}
clearSuggestions={clearSuggestions}
/>
{showFunctionOfTheDay && (
<FunctionOfTheDay

View File

@ -3,12 +3,13 @@ import Paper from "@mui/material/Paper";
import InputBase from "@mui/material/InputBase";
import IconButton from "@mui/material/IconButton";
import ClearIcon from "@mui/icons-material/Clear";
import { Box, debounce, Grid, Typography } from "@mui/material";
import { Autocomplete, Box, debounce, Grid, Typography } from "@mui/material";
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
import { NixType, nixTypes } from "../../models/nix";
import SearchIcon from "@mui/icons-material/Search";
import { usePageContext } from "../pageContext";
import { SelectOption } from "../selectOption";
import { SearchOptions, Suggestion } from "minisearch";
export type Filter = { from: NixType; to: NixType };
@ -17,10 +18,21 @@ export interface SearchInputProps {
handleClear: () => void;
handleFilter: (filter: Filter | ((curr: Filter) => Filter)) => void;
placeholder: string;
suggestions: Suggestion[];
autoSuggest: (query: string, options?: SearchOptions) => void;
clearSuggestions: () => void;
}
export function SearchInput(props: SearchInputProps) {
const { handleSearch, placeholder, handleFilter, handleClear } = props;
const {
handleSearch,
placeholder,
handleFilter,
handleClear,
suggestions,
autoSuggest,
clearSuggestions,
} = props;
const { pageState } = usePageContext();
const { filter, term } = pageState;
const [_term, _setTerm] = useState(term);
@ -37,14 +49,28 @@ export function SearchInput(props: SearchInputProps) {
const _handleClear = () => {
_setTerm("");
handleClear();
clearSuggestions();
};
const handleType = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
_setTerm(e.target.value);
autoSuggest(e.target.value, {
fuzzy: 0.25,
fields: ["id", "name", "category"],
});
debouncedSubmit(e.target.value);
};
const autoCompleteOptions = useMemo(() => {
const options = suggestions
.slice(0, 5)
.map((s) => s.terms)
.flat();
const sorted = options.sort((a, b) => -b.localeCompare(a));
return [...new Set(sorted)];
}, [suggestions]);
return (
<>
<Paper
@ -65,19 +91,39 @@ export function SearchInput(props: SearchInputProps) {
<IconButton aria-label="clear-button" onClick={_handleClear}>
<ClearIcon />
</IconButton>
<InputBase
autoFocus
sx={{
ml: 1,
flex: 1,
backgroundColor: "paper.main",
p: 1,
<Autocomplete
// disablePortal
// id="combo-box-demo"
options={autoCompleteOptions}
sx={{ width: "100%" }}
onChange={(e, value) => {
handleType({
target: { value: value || "" },
} as React.ChangeEvent<HTMLInputElement>);
}}
placeholder={placeholder}
inputProps={{ "aria-label": "search-input" }}
value={_term}
onChange={(e) => handleType(e)}
renderInput={(params) => (
// <InputBase {...params} {...params.InputProps} />
<InputBase
autoFocus
sx={{
ml: 1,
flex: 1,
backgroundColor: "paper.main",
p: 1,
}}
placeholder={placeholder}
// inputProps={{ "aria-label": "search-input" }}
value={_term}
onChange={(e) => handleType(e)}
{...params}
{...params.InputProps}
endAdornment={undefined}
/>
)}
/>
<IconButton
sx={{
p: 1,

View File

@ -40,7 +40,6 @@ export const normalizePath = (id: string) => {
const start = substr.slice(0, 1);
const end = substr.slice(1);
console.log({ start, end });
return start.toUpperCase() + end;
})
.join("");

View File

@ -2,26 +2,23 @@ import { MetaData, NixType } from "../models/nix";
import { getTypes } from "./lib";
export const byType =
({ to, from }: { to: NixType; from: NixType }) =>
(data: MetaData): MetaData => {
if (to === "any" && from === "any") {
return data;
} else {
return data.filter(
// TODO: Implement proper type matching
({ name, fn_type }) => {
if (fn_type) {
const parsedType = getTypes(name, fn_type);
// if(name === "derivation"){
// console.log({name,parsedType,fn_type});
// }
return (
parsedType.args.includes(from) && parsedType.types.includes(to)
);
} else {
return to === "any" && from === "any";
({ to, from }: { to: NixType; from: NixType }) =>
(data: MetaData): MetaData => {
if (to === "any" && from === "any") {
return data;
} else {
return data.filter(
// TODO: Implement proper type matching
({ name, fn_type }) => {
if (fn_type) {
const parsedType = getTypes(name, fn_type);
return (
parsedType.args.includes(from) && parsedType.types.includes(to)
);
} else {
return to === "any" && from === "any";
}
}
}
);
}
};
);
}
};