fix: select completion and replace with doc name
parent
1014de1b12
commit
62d53fe4d5
|
|
@ -25,7 +25,6 @@ import { cn } from '@/lib/utils'
|
||||||
import fetcher from '@/lib/tabby-fetcher'
|
import fetcher from '@/lib/tabby-fetcher'
|
||||||
import { debounce, has } from 'lodash-es'
|
import { debounce, has } from 'lodash-es'
|
||||||
import type { ISearchHit, SearchReponse } from '@/lib/types'
|
import type { ISearchHit, SearchReponse } from '@/lib/types'
|
||||||
import { lightfair } from 'react-syntax-highlighter/dist/esm/styles/hljs'
|
|
||||||
|
|
||||||
export interface PromptProps
|
export interface PromptProps
|
||||||
extends Pick<UseChatHelpers, 'input' | 'setInput'> {
|
extends Pick<UseChatHelpers, 'input' | 'setInput'> {
|
||||||
|
|
@ -45,6 +44,9 @@ export function PromptForm({
|
||||||
>(null)
|
>(null)
|
||||||
const latestFetchKey = React.useRef('')
|
const latestFetchKey = React.useRef('')
|
||||||
const inputRef = React.useRef<HTMLTextAreaElement>(null)
|
const inputRef = React.useRef<HTMLTextAreaElement>(null)
|
||||||
|
// store the input selection for replacing inputValue
|
||||||
|
const prevInputSelectionEnd = React.useRef<number>()
|
||||||
|
// for updating the input selection after replacing
|
||||||
const nextInputSelectionRange = React.useRef<[number, number]>()
|
const nextInputSelectionRange = React.useRef<[number, number]>()
|
||||||
const [options, setOptions] = React.useState<SearchReponse['hits']>([])
|
const [options, setOptions] = React.useState<SearchReponse['hits']>([])
|
||||||
const [selectedCompletionsMap, setSelectedCompletionsMap] = React.useState<
|
const [selectedCompletionsMap, setSelectedCompletionsMap] = React.useState<
|
||||||
|
|
@ -53,10 +55,9 @@ export function PromptForm({
|
||||||
|
|
||||||
useSWR<SearchReponse>(queryCompletionUrl, fetcher, {
|
useSWR<SearchReponse>(queryCompletionUrl, fetcher, {
|
||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
dedupingInterval: 500,
|
dedupingInterval: 0,
|
||||||
onSuccess: (data, key) => {
|
onSuccess: (data, key) => {
|
||||||
if (key !== latestFetchKey.current) return
|
if (key !== latestFetchKey.current) return
|
||||||
|
|
||||||
setOptions(data?.hits ?? [])
|
setOptions(data?.hits ?? [])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -88,21 +89,23 @@ export function PromptForm({
|
||||||
}, 200)
|
}, 200)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleCompletionSelect = (
|
const handleCompletionSelect = (item: ISearchHit) => {
|
||||||
inputRef: React.RefObject<HTMLTextAreaElement | HTMLInputElement>,
|
const selectionEnd = prevInputSelectionEnd.current ?? 0
|
||||||
item: ISearchHit
|
|
||||||
) => {
|
|
||||||
const selectionEnd = inputRef.current?.selectionEnd ?? 0
|
|
||||||
const queryNameMatches = getSearchCompletionQueryName(input, selectionEnd)
|
const queryNameMatches = getSearchCompletionQueryName(input, selectionEnd)
|
||||||
if (queryNameMatches) {
|
if (queryNameMatches) {
|
||||||
setSelectedCompletionsMap({
|
setSelectedCompletionsMap({
|
||||||
...selectedCompletionsMap,
|
...selectedCompletionsMap,
|
||||||
[queryNameMatches[0]]: item
|
[`@${item.doc?.name}`]: item
|
||||||
})
|
})
|
||||||
// insert a space to break the search query
|
const replaceString = `@${item?.doc?.name} `
|
||||||
setInput(input.slice(0, selectionEnd) + ' ' + input.slice(selectionEnd))
|
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
|
// store the selection range and update it when layout
|
||||||
nextInputSelectionRange.current = [selectionEnd + 1, selectionEnd + 1]
|
nextInputSelectionRange.current = [nextSelectionEnd, nextSelectionEnd]
|
||||||
|
// insert a space to break the search query
|
||||||
|
setInput(prevInput + replaceString + input.slice(selectionEnd))
|
||||||
}
|
}
|
||||||
setOptions([])
|
setOptions([])
|
||||||
}
|
}
|
||||||
|
|
@ -132,6 +135,17 @@ export function PromptForm({
|
||||||
await onSubmit(finalInput)
|
await onSubmit(finalInput)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleTextareaKeyDown = (
|
||||||
|
e: React.KeyboardEvent<HTMLTextAreaElement>,
|
||||||
|
isOpen: boolean
|
||||||
|
) => {
|
||||||
|
if (isOpen && ['ArrowRight', 'ArrowLeft', 'Home', 'End'].includes(e.key)) {
|
||||||
|
setOptions([])
|
||||||
|
} else {
|
||||||
|
onKeyDown(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handlePromptSubmit} ref={formRef}>
|
<form onSubmit={handlePromptSubmit} ref={formRef}>
|
||||||
<Combobox
|
<Combobox
|
||||||
|
|
@ -164,11 +178,14 @@ export function PromptForm({
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
if (has(e, 'target.value')) {
|
if (has(e, 'target.value')) {
|
||||||
|
prevInputSelectionEnd.current = e.target.selectionEnd
|
||||||
setInput(e.target.value)
|
setInput(e.target.value)
|
||||||
handleSearchCompletion(e)
|
handleSearchCompletion(e)
|
||||||
|
} else {
|
||||||
|
prevInputSelectionEnd.current = undefined
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={e => handleTextareaKeyDown(e, open)}
|
||||||
/>
|
/>
|
||||||
<div className="absolute right-0 top-4 sm:right-4">
|
<div className="absolute right-0 top-4 sm:right-4">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
|
|
@ -189,6 +206,7 @@ export function PromptForm({
|
||||||
</ComboboxAnchor>
|
</ComboboxAnchor>
|
||||||
<ComboboxContent
|
<ComboboxContent
|
||||||
align="start"
|
align="start"
|
||||||
|
side="top"
|
||||||
onOpenAutoFocus={e => e.preventDefault()}
|
onOpenAutoFocus={e => e.preventDefault()}
|
||||||
className="w-[60vw] md:w-[430px]"
|
className="w-[60vw] md:w-[430px]"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ export const ComboboxTextarea = React.forwardRef<
|
||||||
onKeyDown: (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
onKeyDown: (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
if (e.key === 'Enter' && open) {
|
if (e.key === 'Enter' && open) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
} else if (!open) {
|
} else {
|
||||||
onKeyDown?.(e)
|
onKeyDown?.(e)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -154,10 +154,7 @@ ComboboxOption.displayName = 'ComboboxOption'
|
||||||
|
|
||||||
interface ComboboxProps<T> {
|
interface ComboboxProps<T> {
|
||||||
options: T[] | undefined
|
options: T[] | undefined
|
||||||
onSelect?: (
|
onSelect?: (data: T) => void
|
||||||
ref: React.RefObject<HTMLTextAreaElement | HTMLInputElement>,
|
|
||||||
data: T
|
|
||||||
) => void
|
|
||||||
inputRef?: React.RefObject<HTMLTextAreaElement | HTMLInputElement>
|
inputRef?: React.RefObject<HTMLTextAreaElement | HTMLInputElement>
|
||||||
children?:
|
children?:
|
||||||
| React.ReactNode
|
| React.ReactNode
|
||||||
|
|
@ -202,7 +199,7 @@ export function Combobox<T extends { id: number }>({
|
||||||
isOpen: manualOpen,
|
isOpen: manualOpen,
|
||||||
onSelectedItemChange({ selectedItem }) {
|
onSelectedItemChange({ selectedItem }) {
|
||||||
if (selectedItem) {
|
if (selectedItem) {
|
||||||
onSelect?.(inputRef, selectedItem)
|
onSelect?.(selectedItem)
|
||||||
setManualOpen(false)
|
setManualOpen(false)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue