|
|
@ -1,16 +1,15 @@ |
|
|
|
import { |
|
|
|
import { |
|
|
|
|
|
|
|
type InputHTMLAttributes, |
|
|
|
useEffect, |
|
|
|
useEffect, |
|
|
|
useMemo, |
|
|
|
useMemo, |
|
|
|
useRef, |
|
|
|
useRef, |
|
|
|
useState, |
|
|
|
useState, |
|
|
|
type InputHTMLAttributes, |
|
|
|
|
|
|
|
} from 'react'; |
|
|
|
} from 'react'; |
|
|
|
import { cn } from '../../lib/classname'; |
|
|
|
import { cn } from '../../lib/classname'; |
|
|
|
import { useOutsideClick } from '../../hooks/use-outside-click'; |
|
|
|
import { useOutsideClick } from '../../hooks/use-outside-click'; |
|
|
|
import { useDebounceValue } from '../../hooks/use-debounce'; |
|
|
|
import { useDebounceValue } from '../../hooks/use-debounce'; |
|
|
|
import { httpGet } from '../../lib/http'; |
|
|
|
import { httpGet } from '../../lib/http'; |
|
|
|
import { useToast } from '../../hooks/use-toast'; |
|
|
|
import { useToast } from '../../hooks/use-toast'; |
|
|
|
import { Loader2 } from 'lucide-react'; |
|
|
|
|
|
|
|
import { Spinner } from '../ReactIcons/Spinner.tsx'; |
|
|
|
import { Spinner } from '../ReactIcons/Spinner.tsx'; |
|
|
|
import type { PageType } from '../CommandMenu/CommandMenu.tsx'; |
|
|
|
import type { PageType } from '../CommandMenu/CommandMenu.tsx'; |
|
|
|
|
|
|
|
|
|
|
@ -24,7 +23,7 @@ type GetTopAIRoadmapTermResponse = { |
|
|
|
type AITermSuggestionInputProps = { |
|
|
|
type AITermSuggestionInputProps = { |
|
|
|
value: string; |
|
|
|
value: string; |
|
|
|
onValueChange: (value: string) => void; |
|
|
|
onValueChange: (value: string) => void; |
|
|
|
onSelect?: (roadmapId: string) => void; |
|
|
|
onSelect?: (roadmapId: string, roadmapTitle: string) => void; |
|
|
|
inputClassName?: string; |
|
|
|
inputClassName?: string; |
|
|
|
wrapperClassName?: string; |
|
|
|
wrapperClassName?: string; |
|
|
|
placeholder?: string; |
|
|
|
placeholder?: string; |
|
|
@ -62,7 +61,7 @@ export function AITermSuggestionInput(props: AITermSuggestionInputProps) { |
|
|
|
useState<GetTopAIRoadmapTermResponse>([]); |
|
|
|
useState<GetTopAIRoadmapTermResponse>([]); |
|
|
|
const [searchedText, setSearchedText] = useState(defaultValue); |
|
|
|
const [searchedText, setSearchedText] = useState(defaultValue); |
|
|
|
const [activeCounter, setActiveCounter] = useState(0); |
|
|
|
const [activeCounter, setActiveCounter] = useState(0); |
|
|
|
const debouncedSearchValue = useDebounceValue(searchedText, 500); |
|
|
|
const debouncedSearchValue = useDebounceValue(searchedText, 300); |
|
|
|
|
|
|
|
|
|
|
|
const loadTopAIRoadmapTerm = async () => { |
|
|
|
const loadTopAIRoadmapTerm = async () => { |
|
|
|
const trimmedValue = debouncedSearchValue.trim(); |
|
|
|
const trimmedValue = debouncedSearchValue.trim(); |
|
|
@ -133,16 +132,21 @@ export function AITermSuggestionInput(props: AITermSuggestionInputProps) { |
|
|
|
|
|
|
|
|
|
|
|
setIsActive(true); |
|
|
|
setIsActive(true); |
|
|
|
setIsLoading(true); |
|
|
|
setIsLoading(true); |
|
|
|
loadTopAIRoadmapTerm().then((results) => { |
|
|
|
loadTopAIRoadmapTerm() |
|
|
|
|
|
|
|
.then((results) => { |
|
|
|
const normalizedSearchText = debouncedSearchValue.trim().toLowerCase(); |
|
|
|
const normalizedSearchText = debouncedSearchValue.trim().toLowerCase(); |
|
|
|
const matchingOfficialRoadmaps = officialRoadmaps.filter((roadmap) => { |
|
|
|
const matchingOfficialRoadmaps = officialRoadmaps.filter((roadmap) => { |
|
|
|
return roadmap.title.toLowerCase().indexOf(normalizedSearchText) !== -1; |
|
|
|
return ( |
|
|
|
|
|
|
|
roadmap.title.toLowerCase().indexOf(normalizedSearchText) !== -1 |
|
|
|
|
|
|
|
); |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
setSearchResults( |
|
|
|
setSearchResults( |
|
|
|
[...matchingOfficialRoadmaps, ...results]?.slice(0, 5) || [], |
|
|
|
[...matchingOfficialRoadmaps, ...results]?.slice(0, 5) || [], |
|
|
|
); |
|
|
|
); |
|
|
|
setActiveCounter(0); |
|
|
|
setActiveCounter(0); |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
.finally(() => { |
|
|
|
setIsLoading(false); |
|
|
|
setIsLoading(false); |
|
|
|
}); |
|
|
|
}); |
|
|
|
}, [debouncedSearchValue]); |
|
|
|
}, [debouncedSearchValue]); |
|
|
@ -158,6 +162,8 @@ export function AITermSuggestionInput(props: AITermSuggestionInputProps) { |
|
|
|
setIsActive(false); |
|
|
|
setIsActive(false); |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const isFinishedTyping = debouncedSearchValue === searchedText; |
|
|
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
return ( |
|
|
|
<div className={cn('relative', wrapperClassName)}> |
|
|
|
<div className={cn('relative', wrapperClassName)}> |
|
|
|
<input |
|
|
|
<input |
|
|
@ -196,21 +202,23 @@ export function AITermSuggestionInput(props: AITermSuggestionInputProps) { |
|
|
|
setSearchedText(''); |
|
|
|
setSearchedText(''); |
|
|
|
setIsActive(false); |
|
|
|
setIsActive(false); |
|
|
|
} else if (e.key === 'Enter') { |
|
|
|
} else if (e.key === 'Enter') { |
|
|
|
if (searchResults.length > 0) { |
|
|
|
if (!searchResults.length || !isFinishedTyping) { |
|
|
|
|
|
|
|
return; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
e.preventDefault(); |
|
|
|
e.preventDefault(); |
|
|
|
const activeData = searchResults[activeCounter]; |
|
|
|
const activeData = searchResults[activeCounter]; |
|
|
|
if (activeData) { |
|
|
|
if (activeData) { |
|
|
|
if (activeData.isOfficial) { |
|
|
|
if (activeData.isOfficial) { |
|
|
|
window.location.href = `/${activeData._id}`; |
|
|
|
window.open(`/${activeData._id}`, '_blank')?.focus(); |
|
|
|
return; |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
onValueChange(activeData.term); |
|
|
|
onValueChange(activeData.term); |
|
|
|
onSelect?.(activeData._id); |
|
|
|
onSelect?.(activeData._id, activeData.title); |
|
|
|
setIsActive(false); |
|
|
|
setIsActive(false); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
}} |
|
|
|
}} |
|
|
|
/> |
|
|
|
/> |
|
|
|
|
|
|
|
|
|
|
@ -223,9 +231,12 @@ export function AITermSuggestionInput(props: AITermSuggestionInputProps) { |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
)} |
|
|
|
)} |
|
|
|
|
|
|
|
|
|
|
|
{isActive && searchResults.length > 0 && searchedText.length > 0 && ( |
|
|
|
{isActive && |
|
|
|
|
|
|
|
isFinishedTyping && |
|
|
|
|
|
|
|
searchResults.length > 0 && |
|
|
|
|
|
|
|
searchedText.length > 0 && ( |
|
|
|
<div |
|
|
|
<div |
|
|
|
className="absolute top-full z-50 mt-1 w-full rounded-md border bg-white px-2 py-2 shadow" |
|
|
|
className="absolute top-full z-50 mt-1 w-full rounded-md border bg-white p-1 shadow" |
|
|
|
ref={dropdownRef} |
|
|
|
ref={dropdownRef} |
|
|
|
> |
|
|
|
> |
|
|
|
<div className="flex flex-col"> |
|
|
|
<div className="flex flex-col"> |
|
|
@ -246,7 +257,7 @@ export function AITermSuggestionInput(props: AITermSuggestionInputProps) { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
onValueChange(result?.term); |
|
|
|
onValueChange(result?.term); |
|
|
|
onSelect?.(result._id); |
|
|
|
onSelect?.(result._id, result.title); |
|
|
|
setSearchedText(''); |
|
|
|
setSearchedText(''); |
|
|
|
setIsActive(false); |
|
|
|
setIsActive(false); |
|
|
|
}} |
|
|
|
}} |
|
|
@ -259,7 +270,7 @@ export function AITermSuggestionInput(props: AITermSuggestionInputProps) { |
|
|
|
: 'bg-blue-400 text-blue-50', |
|
|
|
: 'bg-blue-400 text-blue-50', |
|
|
|
)} |
|
|
|
)} |
|
|
|
> |
|
|
|
> |
|
|
|
{result.isOfficial ? 'Official' : 'Generated'} |
|
|
|
{result.isOfficial ? 'Official' : 'AI Generated'} |
|
|
|
</span> |
|
|
|
</span> |
|
|
|
{result?.title || result?.term} |
|
|
|
{result?.title || result?.term} |
|
|
|
</button> |
|
|
|
</button> |
|
|
|