diff --git a/ee/tabby-ui/components/prompt-form.tsx b/ee/tabby-ui/components/prompt-form.tsx index f930b54..165c4c9 100644 --- a/ee/tabby-ui/components/prompt-form.tsx +++ b/ee/tabby-ui/components/prompt-form.tsx @@ -1,17 +1,30 @@ import { UseChatHelpers } from 'ai/react' import * as React from 'react' -import Textarea from 'react-textarea-autosize' - +import useSWR from 'swr' import { Button, buttonVariants } from '@/components/ui/button' -import { IconArrowElbow, IconEdit, IconPlus } from '@/components/ui/icons' +import { + IconArrowElbow, + IconEdit, + IconSymbolFunction +} from '@/components/ui/icons' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' +import { + Combobox, + ComboboxAnchor, + ComboboxContent, + ComboboxOption, + ComboboxTextarea +} from '@/components/ui/combobox' +import { Popover, PopoverAnchor, PopoverContent } from '@/components/ui/popover' import { useEnterSubmit } from '@/lib/hooks/use-enter-submit' import { cn } from '@/lib/utils' -import { useRouter } from 'next/navigation' +import fetcher from '@/lib/tabby-fetcher' +import { debounce, has } from 'lodash-es' +import type { ISearchHit, SearchReponse } from '@/lib/types' export interface PromptProps extends Pick { @@ -26,63 +39,258 @@ export function PromptForm({ isLoading }: PromptProps) { const { formRef, onKeyDown } = useEnterSubmit() + const [queryCompletionUrl, setQueryCompletionUrl] = React.useState< + string | null + >(null) + const latestFetchKey = React.useRef('') const inputRef = React.useRef(null) - const router = useRouter() + // store the input selection for replacing inputValue + const prevInputSelectionEnd = React.useRef() + // for updating the input selection after replacing + const nextInputSelectionRange = React.useRef<[number, number]>() + const [options, setOptions] = React.useState([]) + const [selectedCompletionsMap, setSelectedCompletionsMap] = React.useState< + Record + >({}) - React.useEffect(() => { - if (inputRef.current) { - inputRef.current.focus() + useSWR(queryCompletionUrl, fetcher, { + revalidateOnFocus: false, + dedupingInterval: 0, + onSuccess: (data, key) => { + if (key !== latestFetchKey.current) return + setOptions(data?.hits ?? []) } + }) + + React.useLayoutEffect(() => { + if (nextInputSelectionRange.current?.length) { + inputRef.current?.setSelectionRange?.( + nextInputSelectionRange.current[0], + nextInputSelectionRange.current[1] + ) + nextInputSelectionRange.current = undefined + } + }) + + const handleSearchCompletion = React.useMemo(() => { + return debounce((e: React.ChangeEvent) => { + const value = e.target?.value ?? '' + const end = e.target?.selectionEnd ?? 0 + const queryNameMatches = getSearchCompletionQueryName(value, end) + const queryName = queryNameMatches?.[1] + if (queryName) { + const query = encodeURIComponent(`name:${queryName} kind:function`) + const url = `/v1beta/search?q=${query}` + latestFetchKey.current = url + setQueryCompletionUrl(url) + } else { + setOptions([]) + } + }, 200) }, []) + const handleCompletionSelect = (item: ISearchHit) => { + const selectionEnd = prevInputSelectionEnd.current ?? 0 + const queryNameMatches = getSearchCompletionQueryName(input, selectionEnd) + if (queryNameMatches) { + setSelectedCompletionsMap({ + ...selectedCompletionsMap, + [`@${item.doc?.name}`]: item + }) + const replaceString = `@${item?.doc?.name} ` + const prevInput = input + .substring(0, selectionEnd) + .replace(new RegExp(queryNameMatches[0]), '') + const nextSelectionEnd = prevInput.length + replaceString.length + // store the selection range and update it when layout + nextInputSelectionRange.current = [nextSelectionEnd, nextSelectionEnd] + // insert a space to break the search query + setInput(prevInput + replaceString + input.slice(selectionEnd)) + } + setOptions([]) + } + + const handlePromptSubmit: React.FormEventHandler< + HTMLFormElement + > = async e => { + e.preventDefault() + if (!input?.trim()) { + return + } + + let finalInput = input + // replace queryname to doc.body of selected completions + Object.keys(selectedCompletionsMap).forEach(key => { + const completion = selectedCompletionsMap[key] + if (!completion?.doc) return + finalInput = finalInput.replaceAll( + key, + `\n${'```'}${completion.doc?.language ?? ''}\n${ + completion.doc.body ?? '' + }\n${'```'}\n` + ) + }) + + setInput('') + await onSubmit(finalInput) + } + + const handleTextareaKeyDown = ( + e: React.KeyboardEvent, + isOpen: boolean + ) => { + if (isOpen && ['ArrowRight', 'ArrowLeft', 'Home', 'End'].includes(e.key)) { + setOptions([]) + } else { + onKeyDown(e) + } + } + return ( -
{ - e.preventDefault() - if (!input?.trim()) { - return - } - setInput('') - await onSubmit(input) - }} - ref={formRef} - > -
- - - -