feat: adding an auto completion component in chat playground (#702)
* feat: Adding an auto-completion component * Update ee/tabby-ui/components/prompt-form.tsx Co-authored-by: Meng Zhang <meng@tabbyml.com> * fix: prepare for submitting prompt * fix: Generate code block templates * fix: select completion and replace with doc name --------- Co-authored-by: Meng Zhang <meng@tabbyml.com>refactor-extract-code
parent
6bea2a4a78
commit
e7f874b8da
|
|
@ -1,17 +1,30 @@
|
||||||
import { UseChatHelpers } from 'ai/react'
|
import { UseChatHelpers } from 'ai/react'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import Textarea from 'react-textarea-autosize'
|
import useSWR from 'swr'
|
||||||
|
|
||||||
import { Button, buttonVariants } from '@/components/ui/button'
|
import { Button, buttonVariants } from '@/components/ui/button'
|
||||||
import { IconArrowElbow, IconEdit, IconPlus } from '@/components/ui/icons'
|
import {
|
||||||
|
IconArrowElbow,
|
||||||
|
IconEdit,
|
||||||
|
IconSymbolFunction
|
||||||
|
} from '@/components/ui/icons'
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipTrigger
|
TooltipTrigger
|
||||||
} from '@/components/ui/tooltip'
|
} 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 { useEnterSubmit } from '@/lib/hooks/use-enter-submit'
|
||||||
import { cn } from '@/lib/utils'
|
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
|
export interface PromptProps
|
||||||
extends Pick<UseChatHelpers, 'input' | 'setInput'> {
|
extends Pick<UseChatHelpers, 'input' | 'setInput'> {
|
||||||
|
|
@ -26,63 +39,258 @@ export function PromptForm({
|
||||||
isLoading
|
isLoading
|
||||||
}: PromptProps) {
|
}: PromptProps) {
|
||||||
const { formRef, onKeyDown } = useEnterSubmit()
|
const { formRef, onKeyDown } = useEnterSubmit()
|
||||||
|
const [queryCompletionUrl, setQueryCompletionUrl] = React.useState<
|
||||||
|
string | null
|
||||||
|
>(null)
|
||||||
|
const latestFetchKey = React.useRef('')
|
||||||
const inputRef = React.useRef<HTMLTextAreaElement>(null)
|
const inputRef = React.useRef<HTMLTextAreaElement>(null)
|
||||||
const router = useRouter()
|
// 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 [options, setOptions] = React.useState<SearchReponse['hits']>([])
|
||||||
|
const [selectedCompletionsMap, setSelectedCompletionsMap] = React.useState<
|
||||||
|
Record<string, ISearchHit>
|
||||||
|
>({})
|
||||||
|
|
||||||
React.useEffect(() => {
|
useSWR<SearchReponse>(queryCompletionUrl, fetcher, {
|
||||||
if (inputRef.current) {
|
revalidateOnFocus: false,
|
||||||
inputRef.current.focus()
|
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<HTMLTextAreaElement>) => {
|
||||||
|
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<HTMLTextAreaElement>,
|
||||||
|
isOpen: boolean
|
||||||
|
) => {
|
||||||
|
if (isOpen && ['ArrowRight', 'ArrowLeft', 'Home', 'End'].includes(e.key)) {
|
||||||
|
setOptions([])
|
||||||
|
} else {
|
||||||
|
onKeyDown(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form onSubmit={handlePromptSubmit} ref={formRef}>
|
||||||
onSubmit={async e => {
|
<Combobox
|
||||||
e.preventDefault()
|
inputRef={inputRef}
|
||||||
if (!input?.trim()) {
|
options={options}
|
||||||
return
|
onSelect={handleCompletionSelect}
|
||||||
}
|
>
|
||||||
setInput('')
|
{({ open, highlightedIndex }) => {
|
||||||
await onSubmit(input)
|
const highlightedOption = options?.[highlightedIndex]
|
||||||
}}
|
|
||||||
ref={formRef}
|
return (
|
||||||
>
|
<>
|
||||||
<div className="relative flex max-h-60 w-full grow flex-col overflow-hidden bg-background px-8 sm:rounded-md sm:border sm:px-12">
|
<ComboboxAnchor>
|
||||||
<span
|
<div className="bg-background relative flex max-h-60 w-full grow flex-col overflow-hidden px-8 sm:rounded-md sm:border sm:px-12">
|
||||||
className={cn(
|
<span
|
||||||
buttonVariants({ size: 'sm', variant: 'ghost' }),
|
className={cn(
|
||||||
'absolute left-0 top-4 h-8 w-8 rounded-full bg-background p-0 hover:bg-background sm:left-4'
|
buttonVariants({ size: 'sm', variant: 'ghost' }),
|
||||||
)}
|
'bg-background hover:bg-background absolute left-0 top-4 h-8 w-8 rounded-full p-0 sm:left-4'
|
||||||
>
|
)}
|
||||||
<IconEdit />
|
>
|
||||||
</span>
|
<IconEdit />
|
||||||
<Textarea
|
</span>
|
||||||
ref={inputRef}
|
<ComboboxTextarea
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onKeyDown={onKeyDown}
|
rows={1}
|
||||||
rows={1}
|
placeholder="Ask a question."
|
||||||
value={input}
|
spellCheck={false}
|
||||||
onChange={e => setInput(e.target.value)}
|
className="min-h-[60px] w-full resize-none bg-transparent px-4 py-[1.3rem] focus-within:outline-none sm:text-sm"
|
||||||
placeholder="Ask a question."
|
value={input}
|
||||||
spellCheck={false}
|
ref={inputRef}
|
||||||
className="min-h-[60px] w-full resize-none bg-transparent px-4 py-[1.3rem] focus-within:outline-none sm:text-sm"
|
onChange={e => {
|
||||||
/>
|
if (has(e, 'target.value')) {
|
||||||
<div className="absolute right-0 top-4 sm:right-4">
|
prevInputSelectionEnd.current = e.target.selectionEnd
|
||||||
<Tooltip>
|
setInput(e.target.value)
|
||||||
<TooltipTrigger asChild>
|
handleSearchCompletion(e)
|
||||||
<Button
|
} else {
|
||||||
type="submit"
|
prevInputSelectionEnd.current = undefined
|
||||||
size="icon"
|
}
|
||||||
disabled={isLoading || input === ''}
|
}}
|
||||||
|
onKeyDown={e => handleTextareaKeyDown(e, open)}
|
||||||
|
/>
|
||||||
|
<div className="absolute right-0 top-4 sm:right-4">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
size="icon"
|
||||||
|
disabled={isLoading || input === ''}
|
||||||
|
>
|
||||||
|
<IconArrowElbow />
|
||||||
|
<span className="sr-only">Send message</span>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Send message</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ComboboxAnchor>
|
||||||
|
<ComboboxContent
|
||||||
|
align="start"
|
||||||
|
side="top"
|
||||||
|
onOpenAutoFocus={e => e.preventDefault()}
|
||||||
|
className="w-[60vw] md:w-[430px]"
|
||||||
>
|
>
|
||||||
<IconArrowElbow />
|
<Popover open={open && !!highlightedOption}>
|
||||||
<span className="sr-only">Send message</span>
|
<PopoverAnchor asChild>
|
||||||
</Button>
|
<div className="max-h-[300px] overflow-y-scroll">
|
||||||
</TooltipTrigger>
|
{open &&
|
||||||
<TooltipContent>Send message</TooltipContent>
|
!!options?.length &&
|
||||||
</Tooltip>
|
options.map((item, index) => (
|
||||||
</div>
|
<ComboboxOption
|
||||||
</div>
|
item={item}
|
||||||
|
index={index}
|
||||||
|
key={item?.id}
|
||||||
|
>
|
||||||
|
<div className="flex w-full items-center justify-between gap-8 overflow-x-hidden">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<IconForCompletionKind kind={item?.doc?.kind} />
|
||||||
|
<div className="max-w-[200px] truncate">
|
||||||
|
{item?.doc?.name}(...)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-muted-foreground flex-1 truncate text-right text-sm">
|
||||||
|
{item?.doc?.body}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ComboboxOption>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</PopoverAnchor>
|
||||||
|
<PopoverContent
|
||||||
|
asChild
|
||||||
|
align="start"
|
||||||
|
side="right"
|
||||||
|
alignOffset={-4}
|
||||||
|
onOpenAutoFocus={e => e.preventDefault()}
|
||||||
|
onKeyDownCapture={e => e.preventDefault()}
|
||||||
|
className="rounded-none"
|
||||||
|
collisionPadding={{ bottom: 120 }}
|
||||||
|
>
|
||||||
|
<div className="flex max-h-[70vh] w-[20vw] flex-col overflow-y-auto px-2 md:w-[240px] lg:w-[340px]">
|
||||||
|
<div className="mb-2">
|
||||||
|
{highlightedOption?.doc?.kind
|
||||||
|
? `(${highlightedOption?.doc?.kind}) `
|
||||||
|
: ''}
|
||||||
|
{highlightedOption?.doc?.name}
|
||||||
|
</div>
|
||||||
|
<div className="text-muted-foreground flex-1 whitespace-pre-wrap break-all">
|
||||||
|
{highlightedOption?.doc?.body}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</ComboboxContent>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</Combobox>
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the name of the completion query from a given string@.
|
||||||
|
* @param {string} val - The input string to search for the completion query name.
|
||||||
|
* @param {number | undefined} selectionEnd - The index at which the selection ends in the input string.
|
||||||
|
* @return {string | undefined} - The name of the completion query if found, otherwise undefined.
|
||||||
|
*/
|
||||||
|
export function getSearchCompletionQueryName(
|
||||||
|
val: string,
|
||||||
|
selectionEnd: number | undefined
|
||||||
|
): RegExpExecArray | null {
|
||||||
|
const queryString = val.substring(0, selectionEnd)
|
||||||
|
const matches = /@(\w+)$/.exec(queryString)
|
||||||
|
return matches
|
||||||
|
}
|
||||||
|
|
||||||
|
function IconForCompletionKind({
|
||||||
|
kind,
|
||||||
|
...rest
|
||||||
|
}: { kind: string | undefined } & React.ComponentProps<'svg'>) {
|
||||||
|
switch (kind) {
|
||||||
|
case 'function':
|
||||||
|
return <IconSymbolFunction {...rest} />
|
||||||
|
default:
|
||||||
|
return <IconSymbolFunction {...rest} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,245 @@
|
||||||
|
import * as React from 'react'
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverAnchor,
|
||||||
|
PopoverPortal,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverClose
|
||||||
|
} from '@/components/ui/popover'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { useCombobox } from 'downshift'
|
||||||
|
import type {
|
||||||
|
UseComboboxReturnValue,
|
||||||
|
UseComboboxState,
|
||||||
|
UseComboboxStateChangeOptions
|
||||||
|
} from 'downshift'
|
||||||
|
import Textarea from 'react-textarea-autosize'
|
||||||
|
import { isNil, omitBy } from 'lodash-es'
|
||||||
|
|
||||||
|
interface ComboboxContextValue<T = any> extends UseComboboxReturnValue<T> {
|
||||||
|
open: boolean
|
||||||
|
inputRef: React.RefObject<HTMLInputElement | HTMLTextAreaElement>
|
||||||
|
anchorRef: React.RefObject<HTMLElement>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ComboboxContext = React.createContext({} as ComboboxContextValue)
|
||||||
|
|
||||||
|
export const ComboboxClose = PopoverClose
|
||||||
|
export const ComboboxAnchor = React.forwardRef<
|
||||||
|
React.ElementRef<typeof PopoverAnchor>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof PopoverAnchor>
|
||||||
|
>((props, forwardRef) => {
|
||||||
|
return <PopoverAnchor {...props} ref={forwardRef} />
|
||||||
|
})
|
||||||
|
ComboboxAnchor.displayName = 'ComboboxAnchor'
|
||||||
|
|
||||||
|
export const ComboboxTextarea = React.forwardRef<
|
||||||
|
React.ElementRef<typeof Textarea>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof Textarea>
|
||||||
|
>((props, forwardRef) => {
|
||||||
|
const { getInputProps, open } = React.useContext(ComboboxContext)
|
||||||
|
const { onKeyDown, onChange, onInput, onBlur, onClick, ...rest } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Textarea
|
||||||
|
{...getInputProps(
|
||||||
|
omitBy(
|
||||||
|
{
|
||||||
|
onKeyDown: (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
|
if (e.key === 'Enter' && open) {
|
||||||
|
e.preventDefault()
|
||||||
|
} else {
|
||||||
|
onKeyDown?.(e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onChange,
|
||||||
|
onInput,
|
||||||
|
onBlur,
|
||||||
|
onClick,
|
||||||
|
ref: forwardRef
|
||||||
|
},
|
||||||
|
isNil
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
ComboboxTextarea.displayName = 'ComboboxTextarea'
|
||||||
|
|
||||||
|
export const ComboboxContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof PopoverContent>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof PopoverContent> & {
|
||||||
|
popupMatchAnchorWidth?: boolean
|
||||||
|
}
|
||||||
|
>(({ children, style, popupMatchAnchorWidth, ...rest }, forwardRef) => {
|
||||||
|
const { getMenuProps, anchorRef } = React.useContext(ComboboxContext)
|
||||||
|
const popupWidth = React.useRef<number | undefined>(undefined)
|
||||||
|
|
||||||
|
React.useLayoutEffect(() => {
|
||||||
|
if (popupMatchAnchorWidth) {
|
||||||
|
const anchor = anchorRef.current
|
||||||
|
if (anchor) {
|
||||||
|
const rect = anchor.getBoundingClientRect()
|
||||||
|
popupWidth.current = rect.width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PopoverPortal>
|
||||||
|
<PopoverContent
|
||||||
|
align="start"
|
||||||
|
onOpenAutoFocus={e => e.preventDefault()}
|
||||||
|
style={{
|
||||||
|
width: popupWidth.current,
|
||||||
|
...style
|
||||||
|
}}
|
||||||
|
{...getMenuProps({ ref: forwardRef }, { suppressRefError: true })}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</PopoverContent>
|
||||||
|
</PopoverPortal>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
ComboboxContent.displayName = 'ComboboxContent'
|
||||||
|
|
||||||
|
interface ComboboxOptionProps<T = any> {
|
||||||
|
item: T
|
||||||
|
index: number
|
||||||
|
className?: string
|
||||||
|
disabled?: boolean
|
||||||
|
children?:
|
||||||
|
| React.ReactNode
|
||||||
|
| React.ReactNode[]
|
||||||
|
| ((p: { selected: boolean; highlighted: boolean }) => React.ReactNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ComboboxOption = React.forwardRef<
|
||||||
|
React.RefObject<HTMLDivElement>,
|
||||||
|
ComboboxOptionProps
|
||||||
|
>(({ item, index, className, children, disabled, ...rest }, forwardRef) => {
|
||||||
|
const { highlightedIndex, selectedItem, getItemProps } =
|
||||||
|
React.useContext(ComboboxContext)
|
||||||
|
const highlighted = highlightedIndex === index
|
||||||
|
const selected = selectedItem === item
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ComboboxClose key={item.id} asChild>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none',
|
||||||
|
highlighted && 'text-accent-foreground bg-accent',
|
||||||
|
selected && 'font-bold',
|
||||||
|
disabled && 'pointer-events-none opacity-50',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...getItemProps({
|
||||||
|
item,
|
||||||
|
index,
|
||||||
|
onMouseLeave: e => e.preventDefault(),
|
||||||
|
onMouseOut: e => e.preventDefault()
|
||||||
|
})}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{typeof children === 'function'
|
||||||
|
? children({ highlighted, selected })
|
||||||
|
: children}
|
||||||
|
</div>
|
||||||
|
</ComboboxClose>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
ComboboxOption.displayName = 'ComboboxOption'
|
||||||
|
|
||||||
|
interface ComboboxProps<T> {
|
||||||
|
options: T[] | undefined
|
||||||
|
onSelect?: (data: T) => void
|
||||||
|
inputRef?: React.RefObject<HTMLTextAreaElement | HTMLInputElement>
|
||||||
|
children?:
|
||||||
|
| React.ReactNode
|
||||||
|
| React.ReactNode[]
|
||||||
|
| ((contextValue: ComboboxContextValue) => React.ReactNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Combobox<T extends { id: number }>({
|
||||||
|
options,
|
||||||
|
onSelect,
|
||||||
|
inputRef: propsInputRef,
|
||||||
|
children
|
||||||
|
}: ComboboxProps<T>) {
|
||||||
|
const [manualOpen, setManualOpen] = React.useState(false)
|
||||||
|
const internalInputRef = React.useRef<HTMLTextAreaElement | HTMLInputElement>(
|
||||||
|
null
|
||||||
|
)
|
||||||
|
const inputRef = propsInputRef ?? internalInputRef
|
||||||
|
const anchorRef = React.useRef<HTMLElement>(null)
|
||||||
|
|
||||||
|
const stateReducer = React.useCallback(
|
||||||
|
(
|
||||||
|
state: UseComboboxState<T>,
|
||||||
|
actionAndChanges: UseComboboxStateChangeOptions<T>
|
||||||
|
) => {
|
||||||
|
const { type, changes } = actionAndChanges
|
||||||
|
switch (type) {
|
||||||
|
case useCombobox.stateChangeTypes.MenuMouseLeave:
|
||||||
|
return {
|
||||||
|
...changes,
|
||||||
|
highlightedIndex: state.highlightedIndex
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return changes
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const comboboxValue = useCombobox({
|
||||||
|
items: options ?? [],
|
||||||
|
isOpen: manualOpen,
|
||||||
|
onSelectedItemChange({ selectedItem }) {
|
||||||
|
if (selectedItem) {
|
||||||
|
onSelect?.(selectedItem)
|
||||||
|
setManualOpen(false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onIsOpenChange: ({ isOpen }) => {
|
||||||
|
if (!isOpen) {
|
||||||
|
setManualOpen(false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
stateReducer
|
||||||
|
})
|
||||||
|
|
||||||
|
const { setHighlightedIndex, highlightedIndex } = comboboxValue
|
||||||
|
const open = manualOpen && !!options?.length
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (open && !!options.length && highlightedIndex === -1) {
|
||||||
|
setHighlightedIndex(0)
|
||||||
|
}
|
||||||
|
if (open && !options.length) {
|
||||||
|
setManualOpen(false)
|
||||||
|
}
|
||||||
|
}, [open, options])
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (options?.length) {
|
||||||
|
setManualOpen(true)
|
||||||
|
} else {
|
||||||
|
setManualOpen(false)
|
||||||
|
}
|
||||||
|
}, [options])
|
||||||
|
|
||||||
|
const contextValue = React.useMemo(() => {
|
||||||
|
return { ...comboboxValue, open, inputRef, anchorRef }
|
||||||
|
}, [comboboxValue, open, inputRef, anchorRef])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ComboboxContext.Provider value={contextValue}>
|
||||||
|
<Popover open={open}>
|
||||||
|
{typeof children === 'function' ? children(contextValue) : children}
|
||||||
|
</Popover>
|
||||||
|
</ComboboxContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -554,6 +554,24 @@ function IconNotice({ className, ...props }: React.ComponentProps<'svg'>) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function IconSymbolFunction({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<'svg'>) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="currentColor"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d="M13.51 4l-5-3h-1l-5 3-.49.86v6l.49.85 5 3h1l5-3 .49-.85v-6L13.51 4zm-6 9.56l-4.5-2.7V5.7l4.5 2.45v5.41zM3.27 4.7l4.74-2.84 4.74 2.84-4.74 2.59L3.27 4.7zm9.74 6.16l-4.5 2.7V8.15l4.5-2.45v5.16z" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
IconEdit,
|
IconEdit,
|
||||||
IconNextChat,
|
IconNextChat,
|
||||||
|
|
@ -583,5 +601,6 @@ export {
|
||||||
IconExternalLink,
|
IconExternalLink,
|
||||||
IconChevronUpDown,
|
IconChevronUpDown,
|
||||||
IconSlack,
|
IconSlack,
|
||||||
IconNotice
|
IconNotice,
|
||||||
|
IconSymbolFunction
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import * as React from 'react'
|
||||||
|
import * as PopoverPrimitive from '@radix-ui/react-popover'
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const Popover = PopoverPrimitive.Root
|
||||||
|
|
||||||
|
const PopoverTrigger = PopoverPrimitive.Trigger
|
||||||
|
|
||||||
|
const PopoverClose = PopoverPrimitive.Close
|
||||||
|
|
||||||
|
const PopoverPortal = PopoverPrimitive.Portal
|
||||||
|
|
||||||
|
const PopoverAnchor = PopoverPrimitive.Anchor
|
||||||
|
|
||||||
|
const PopoverContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||||
|
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||||
|
<PopoverPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow animate-in data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
||||||
|
|
||||||
|
export {
|
||||||
|
Popover,
|
||||||
|
PopoverTrigger,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverClose,
|
||||||
|
PopoverPortal,
|
||||||
|
PopoverAnchor
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './common'
|
||||||
|
export * from './search'
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
export type ISearchHit = {
|
||||||
|
id: number
|
||||||
|
doc?: {
|
||||||
|
body?: string
|
||||||
|
name?: string
|
||||||
|
filepath?: string
|
||||||
|
git_url?: string
|
||||||
|
kind?: string
|
||||||
|
language?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export type SearchReponse = {
|
||||||
|
hits?: Array<ISearchHit>
|
||||||
|
num_hits?: number
|
||||||
|
}
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
"@radix-ui/react-dialog": "1.0.4",
|
"@radix-ui/react-dialog": "1.0.4",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.0.5",
|
"@radix-ui/react-dropdown-menu": "^2.0.5",
|
||||||
"@radix-ui/react-label": "^2.0.2",
|
"@radix-ui/react-label": "^2.0.2",
|
||||||
|
"@radix-ui/react-popover": "^1.0.7",
|
||||||
"@radix-ui/react-select": "^1.2.2",
|
"@radix-ui/react-select": "^1.2.2",
|
||||||
"@radix-ui/react-separator": "^1.0.3",
|
"@radix-ui/react-separator": "^1.0.3",
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
|
|
@ -30,7 +31,9 @@
|
||||||
"class-variance-authority": "^0.4.0",
|
"class-variance-authority": "^0.4.0",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
"compare-versions": "^6.1.0",
|
"compare-versions": "^6.1.0",
|
||||||
|
"downshift": "^8.2.2",
|
||||||
"focus-trap-react": "^10.1.1",
|
"focus-trap-react": "^10.1.1",
|
||||||
|
"lodash-es": "^4.17.21",
|
||||||
"nanoid": "^4.0.2",
|
"nanoid": "^4.0.2",
|
||||||
"next": "^13.4.7",
|
"next": "^13.4.7",
|
||||||
"next-auth": "0.0.0-manual.83c4ebd1",
|
"next-auth": "0.0.0-manual.83c4ebd1",
|
||||||
|
|
@ -49,6 +52,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/typography": "^0.5.9",
|
"@tailwindcss/typography": "^0.5.9",
|
||||||
|
"@types/lodash-es": "^4.17.10",
|
||||||
"@types/node": "^17.0.12",
|
"@types/node": "^17.0.12",
|
||||||
"@types/react": "^18.0.22",
|
"@types/react": "^18.0.22",
|
||||||
"@types/react-dom": "^18.0.7",
|
"@types/react-dom": "^18.0.7",
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,13 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime "^0.14.0"
|
regenerator-runtime "^0.14.0"
|
||||||
|
|
||||||
|
"@babel/runtime@^7.22.15":
|
||||||
|
version "7.23.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.2.tgz#062b0ac103261d68a966c4c7baf2ae3e62ec3885"
|
||||||
|
integrity sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==
|
||||||
|
dependencies:
|
||||||
|
regenerator-runtime "^0.14.0"
|
||||||
|
|
||||||
"@eslint-community/eslint-utils@^4.2.0":
|
"@eslint-community/eslint-utils@^4.2.0":
|
||||||
version "4.4.0"
|
version "4.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59"
|
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59"
|
||||||
|
|
@ -418,6 +425,28 @@
|
||||||
aria-hidden "^1.1.1"
|
aria-hidden "^1.1.1"
|
||||||
react-remove-scroll "2.5.5"
|
react-remove-scroll "2.5.5"
|
||||||
|
|
||||||
|
"@radix-ui/react-popover@^1.0.7":
|
||||||
|
version "1.0.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.0.7.tgz#23eb7e3327330cb75ec7b4092d685398c1654e3c"
|
||||||
|
integrity sha512-shtvVnlsxT6faMnK/a7n0wptwBD23xc1Z5mdrtKLwVEfsEMXodS0r5s0/g5P0hX//EKYZS2sxUjqfzlg52ZSnQ==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.13.10"
|
||||||
|
"@radix-ui/primitive" "1.0.1"
|
||||||
|
"@radix-ui/react-compose-refs" "1.0.1"
|
||||||
|
"@radix-ui/react-context" "1.0.1"
|
||||||
|
"@radix-ui/react-dismissable-layer" "1.0.5"
|
||||||
|
"@radix-ui/react-focus-guards" "1.0.1"
|
||||||
|
"@radix-ui/react-focus-scope" "1.0.4"
|
||||||
|
"@radix-ui/react-id" "1.0.1"
|
||||||
|
"@radix-ui/react-popper" "1.1.3"
|
||||||
|
"@radix-ui/react-portal" "1.0.4"
|
||||||
|
"@radix-ui/react-presence" "1.0.1"
|
||||||
|
"@radix-ui/react-primitive" "1.0.3"
|
||||||
|
"@radix-ui/react-slot" "1.0.2"
|
||||||
|
"@radix-ui/react-use-controllable-state" "1.0.1"
|
||||||
|
aria-hidden "^1.1.1"
|
||||||
|
react-remove-scroll "2.5.5"
|
||||||
|
|
||||||
"@radix-ui/react-popper@1.1.2":
|
"@radix-ui/react-popper@1.1.2":
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.1.2.tgz#4c0b96fcd188dc1f334e02dba2d538973ad842e9"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.1.2.tgz#4c0b96fcd188dc1f334e02dba2d538973ad842e9"
|
||||||
|
|
@ -705,6 +734,18 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/katex/-/katex-0.16.3.tgz#a341c89705145b7dd8e2a133b282a133eabe6076"
|
resolved "https://registry.yarnpkg.com/@types/katex/-/katex-0.16.3.tgz#a341c89705145b7dd8e2a133b282a133eabe6076"
|
||||||
integrity sha512-CeVMX9EhVUW8MWnei05eIRks4D5Wscw/W9Byz1s3PA+yJvcdvq9SaDjiUKvRvEgjpdTyJMjQA43ae4KTwsvOPg==
|
integrity sha512-CeVMX9EhVUW8MWnei05eIRks4D5Wscw/W9Byz1s3PA+yJvcdvq9SaDjiUKvRvEgjpdTyJMjQA43ae4KTwsvOPg==
|
||||||
|
|
||||||
|
"@types/lodash-es@^4.17.10":
|
||||||
|
version "4.17.10"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/lodash-es/-/lodash-es-4.17.10.tgz#1b36a76ca9eda20c0263e19bbe1a3adb1b317707"
|
||||||
|
integrity sha512-YJP+w/2khSBwbUSFdGsSqmDvmnN3cCKoPOL7Zjle6s30ZtemkkqhjVfFqGwPN7ASil5VyjE2GtyU/yqYY6mC0A==
|
||||||
|
dependencies:
|
||||||
|
"@types/lodash" "*"
|
||||||
|
|
||||||
|
"@types/lodash@*":
|
||||||
|
version "4.14.200"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.200.tgz#435b6035c7eba9cdf1e039af8212c9e9281e7149"
|
||||||
|
integrity sha512-YI/M/4HRImtNf3pJgbF+W6FrXovqj+T+/HpENLTooK9PnkacBsDpeP3IpHab40CClUfhNmdM2WTNP2sa2dni5Q==
|
||||||
|
|
||||||
"@types/mdast@^3.0.0":
|
"@types/mdast@^3.0.0":
|
||||||
version "3.0.13"
|
version "3.0.13"
|
||||||
resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.13.tgz#b7ba6e52d0faeb9c493e32c205f3831022be4e1b"
|
resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.13.tgz#b7ba6e52d0faeb9c493e32c205f3831022be4e1b"
|
||||||
|
|
@ -1269,6 +1310,11 @@ compare-versions@^6.1.0:
|
||||||
resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-6.1.0.tgz#3f2131e3ae93577df111dba133e6db876ffe127a"
|
resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-6.1.0.tgz#3f2131e3ae93577df111dba133e6db876ffe127a"
|
||||||
integrity sha512-LNZQXhqUvqUTotpZ00qLSaify3b4VFD588aRr8MKFw4CMUr98ytzCW5wDH5qx/DEY5kCDXcbcRuCqL0szEf2tg==
|
integrity sha512-LNZQXhqUvqUTotpZ00qLSaify3b4VFD588aRr8MKFw4CMUr98ytzCW5wDH5qx/DEY5kCDXcbcRuCqL0szEf2tg==
|
||||||
|
|
||||||
|
compute-scroll-into-view@^3.0.3:
|
||||||
|
version "3.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/compute-scroll-into-view/-/compute-scroll-into-view-3.1.0.tgz#753f11d972596558d8fe7c6bcbc8497690ab4c87"
|
||||||
|
integrity sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg==
|
||||||
|
|
||||||
concat-map@0.0.1:
|
concat-map@0.0.1:
|
||||||
version "0.0.1"
|
version "0.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||||
|
|
@ -1435,6 +1481,17 @@ doctrine@^3.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
esutils "^2.0.2"
|
esutils "^2.0.2"
|
||||||
|
|
||||||
|
downshift@^8.2.2:
|
||||||
|
version "8.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/downshift/-/downshift-8.2.2.tgz#bc4bf0024ad9b70bf2c493b58cca9c652fb1d555"
|
||||||
|
integrity sha512-UmJHlNTzmFN3i427Hh9f1OXMnkhgSB/J+urC9ywabvwuftm0nB0/Utsb89OtDq+2UqyScQV4Ro7EM2PEV80N5w==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.22.15"
|
||||||
|
compute-scroll-into-view "^3.0.3"
|
||||||
|
prop-types "^15.8.1"
|
||||||
|
react-is "^18.2.0"
|
||||||
|
tslib "^2.6.2"
|
||||||
|
|
||||||
electron-to-chromium@^1.4.535:
|
electron-to-chromium@^1.4.535:
|
||||||
version "1.4.537"
|
version "1.4.537"
|
||||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.537.tgz#aac4101db53066be1e49baedd000a26bc754adc9"
|
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.537.tgz#aac4101db53066be1e49baedd000a26bc754adc9"
|
||||||
|
|
@ -2586,6 +2643,11 @@ locate-path@^6.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
p-locate "^5.0.0"
|
p-locate "^5.0.0"
|
||||||
|
|
||||||
|
lodash-es@^4.17.21:
|
||||||
|
version "4.17.21"
|
||||||
|
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
|
||||||
|
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
|
||||||
|
|
||||||
lodash.castarray@^4.4.0:
|
lodash.castarray@^4.4.0:
|
||||||
version "4.4.0"
|
version "4.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/lodash.castarray/-/lodash.castarray-4.4.0.tgz#c02513515e309daddd4c24c60cfddcf5976d9115"
|
resolved "https://registry.yarnpkg.com/lodash.castarray/-/lodash.castarray-4.4.0.tgz#c02513515e309daddd4c24c60cfddcf5976d9115"
|
||||||
|
|
@ -3586,7 +3648,7 @@ react-is@^16.13.1:
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||||
|
|
||||||
react-is@^18.0.0:
|
react-is@^18.0.0, react-is@^18.2.0:
|
||||||
version "18.2.0"
|
version "18.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
|
||||||
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==
|
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==
|
||||||
|
|
@ -4179,7 +4241,7 @@ tslib@^1.8.1:
|
||||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
||||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||||
|
|
||||||
tslib@^2.0.0, tslib@^2.1.0, tslib@^2.4.0:
|
tslib@^2.0.0, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.6.2:
|
||||||
version "2.6.2"
|
version "2.6.2"
|
||||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
|
||||||
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
|
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue