-
Notifications
You must be signed in to change notification settings - Fork 9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Extract search #303
Extract search #303
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,5 +12,6 @@ jobs: | |
- name: lint | ||
run: | | ||
npm ci | ||
npm run build --prefix stampy-search | ||
npm run lint | ||
npm run test |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,3 +9,6 @@ node_modules | |
|
||
.DS_store | ||
wrangler.toml | ||
|
||
stampy-search/dist | ||
stampy-search/example/stampySearch.min.js |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,36 +1,69 @@ | ||
import {useState, useEffect, useRef, MutableRefObject, FocusEvent} from 'react' | ||
import {useState, useEffect, useCallback, useRef, MutableRefObject, FocusEvent} from 'react' | ||
import debounce from 'lodash/debounce' | ||
import {Searcher, Question as QuestionType, SearchResult} from 'stampy-search' | ||
import {AddQuestion} from '~/routes/questions/add' | ||
import {Action, ActionType} from '~/routes/questions/actions' | ||
import {MagnifyingGlass, Edit} from '~/components/icons-generated' | ||
import {useSearch, Question as QuestionType, SearchResult} from '~/hooks/search' | ||
import AutoHeight from 'react-auto-height' | ||
import Dialog from '~/components/dialog' | ||
|
||
type Props = { | ||
onSiteAnswersRef: MutableRefObject<QuestionType[]> | ||
initialQuery?: string | ||
openQuestionTitles: string[] | ||
onSelect: (pageid: string, title: string) => void | ||
} | ||
|
||
const empty: [] = [] | ||
|
||
export default function Search({onSiteAnswersRef, openQuestionTitles, onSelect}: Props) { | ||
const [showResults, setShowResults] = useState(false) | ||
export default function Search({ | ||
onSiteAnswersRef, | ||
initialQuery, | ||
openQuestionTitles, | ||
onSelect, | ||
}: Props) { | ||
const [showResults, setShowResults] = useState(initialQuery !== undefined) | ||
const [showMore, setShowMore] = useState(false) | ||
const searchInputRef = useRef('') | ||
|
||
const {search, arePendingSearches, results} = useSearch(onSiteAnswersRef) | ||
const [arePendingSearches, setPendingSearches] = useState(false) | ||
const [results, setResults] = useState([] as SearchResult[]) | ||
const [searcher, setSearcher] = useState<Searcher>() | ||
|
||
useEffect(() => { | ||
setSearcher( | ||
new Searcher({ | ||
getAllQuestions: () => onSiteAnswersRef.current, | ||
onResolveCallback: (query?: string, res?: SearchResult[] | null) => { | ||
if (res) { | ||
setPendingSearches(false) | ||
setResults(res) | ||
} | ||
}, | ||
}) | ||
) | ||
}, [onSiteAnswersRef]) | ||
|
||
const searchFn = (rawValue: string) => { | ||
const value = rawValue.trim() | ||
if (value === searchInputRef.current) return | ||
const searchFn = useCallback( | ||
(rawValue: string) => { | ||
const value = rawValue.trim() | ||
if (value === searchInputRef.current) return | ||
|
||
searchInputRef.current = value | ||
searchInputRef.current = value | ||
|
||
search(value) | ||
logSearch(value) | ||
} | ||
setPendingSearches(true) | ||
searcher && searcher.searchLive(value) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is probably mostly for TS reasons, but it shouldn't be a valid option to NOT perform the search at this step, if it "always works" it should throw an exception after future changes that make it suddenly "not work" |
||
logSearch(value) | ||
}, | ||
[searcher] | ||
) | ||
|
||
// Show the url query if defined | ||
useEffect(() => { | ||
if (initialQuery && searcher) { | ||
initialQuery && searchFn(initialQuery) | ||
} | ||
}, [initialQuery, searcher, searchFn]) | ||
|
||
const handleChange = debounce(searchFn, 100) | ||
|
||
|
@@ -63,6 +96,7 @@ export default function Search({onSiteAnswersRef, openQuestionTitles, onSelect}: | |
name="searchbar" | ||
placeholder="Search for more questions here..." | ||
autoComplete="off" | ||
defaultValue={searchInputRef.current} | ||
onChange={(e) => handleChange(e.currentTarget.value)} | ||
onKeyDown={(e) => e.key === 'Enter' && searchFn(e.currentTarget.value)} | ||
/> | ||
|
@@ -107,12 +141,13 @@ export default function Search({onSiteAnswersRef, openQuestionTitles, onSelect}: | |
</div> | ||
</AutoHeight> | ||
</div> | ||
{showMore && ( | ||
{showMore && searcher && ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the searcher gets created as soon as the component is loaded. It's not properly configured then, as the worker still has to start up, but it's around, so this will be displayed as soon as the component gets loaded |
||
<ShowMoreSuggestions | ||
onClose={() => { | ||
setShowMore(false) | ||
setHide(true) | ||
}} | ||
searcher={searcher} | ||
question={searchInputRef.current} | ||
relatedQuestions={results.map(({title}) => title)} | ||
/> | ||
|
@@ -161,33 +196,25 @@ const ShowMoreSuggestions = ({ | |
question, | ||
relatedQuestions, | ||
onClose, | ||
searcher, | ||
}: { | ||
question: string | ||
relatedQuestions: string[] | ||
onClose: (e: unknown) => void | ||
searcher: Searcher | ||
}) => { | ||
const [extraQuestions, setExtraQuestions] = useState<SearchResult[]>(empty) | ||
const [error, setError] = useState<string>() | ||
|
||
useEffect(() => { | ||
const getResults = async (question: string) => { | ||
try { | ||
const result = await fetch(`/questions/search?question=${encodeURIComponent(question)}`) | ||
|
||
if (result.status == 200) { | ||
const questions = await result.json() | ||
setExtraQuestions(questions) // don't set on API errors | ||
} else { | ||
console.error(await result.text()) | ||
setError('Error while searching for similar questions') | ||
} | ||
} catch (e) { | ||
searcher | ||
.searchUnpublished(question, 5) | ||
.then((res: SearchResult[] | null) => res && setExtraQuestions(res)) | ||
.catch((e: any) => { | ||
console.error(e) | ||
setError(e instanceof Error ? e.message : '') | ||
} | ||
} | ||
getResults(question) | ||
}, [question]) | ||
setError(e) | ||
}) | ||
}, [question, searcher]) | ||
|
||
if (extraQuestions === empty) { | ||
return ( | ||
|
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks like a very useful exercise to find out how reusable is current architecture, and I'd be happy for this code to live on a branch or fork for experimental deployments.
However, I am not able to confirm this is the right step for the master branch and production deployment, where we should aim for more maintainability and simplified architecture. Adding more features on top of previous hacks does not sound sustainable to me.
For 3rd party users of this feature, creating new class instances to spin of workers sensitive to CORS and special API calls while having to provide your own
getAllQuestions
, then dealing with optional-results success callbacks without error handling and quite complex semantics ... that doesn't sound very simple to use / modular / ...If there is someone else who wants to take responsibility for long term maintenance of this repo with occational firefighting and debugging if the site stops working, I'd be happy to hand over the "architecture oversight".
If not, I will rather focus on figuring out how to include stampy search on 3rd party sites using some simpler API, without this new PoC feature 🤔 Ideally something drop-in replacable with API from https://nlp.stampy.ai/ (or whatever is / will be the current url / name for that feature)