Skip to content

Commit

Permalink
feat(tabby-ui): add mention functionality in tabby chat ui (#3607)
Browse files Browse the repository at this point in the history
* feat(chat): add mention functionality with category selection and styling

* feat(chat): implement mention functionality with category support and editor integration

* feat(chat): add input management methods to PromptFormRef interface

* refactor(chat): remove unused FileList, CategoryMenu, mention components, and types

* refactor(chat): Revert Webviewhelper to a previous version

* refactor(chat): remove unused mention components and styles

* refactor(chat): remove PopoverMentionList component and its associated styles

* [autofix.ci] apply automated fixes

* refactor(chat): enhance at-mention handling and improve file item processing

* refactor(chat): implement file mention functionality and enhance mention rendering

* chore: fix some potential normalize issue

* refactor(chat): update chatInputRef type to use PromptFormRef for improved type safety, fix cannot focus bug

* update

* [autofix.ci] apply automated fixes

* update

* fix(chat): update fileItemToSourceItem to handle filepath extraction correctly

* update

* [autofix.ci] apply automated fixes

* update

* updae

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: liangfung <[email protected]>
  • Loading branch information
3 people authored Jan 17, 2025
1 parent 130b2c1 commit af33713
Show file tree
Hide file tree
Showing 23 changed files with 1,486 additions and 572 deletions.
5 changes: 4 additions & 1 deletion ee/tabby-ui/app/(home)/components/thread-feeds.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
} from '@/components/ui/pagination'
import { Separator } from '@/components/ui/separator'
import { Skeleton } from '@/components/ui/skeleton'
import { replaceAtMentionPlaceHolderWithAt } from '@/components/chat/form-editor/utils'
import LoadingWrapper from '@/components/loading-wrapper'
import { Mention } from '@/components/mention-tag'
import { UserAvatar } from '@/components/user-avatar'
Expand Down Expand Up @@ -329,7 +330,9 @@ function ThreadItem({ data }: ThreadItemProps) {
<ThreadTitleWithMentions
className="break-anywhere truncate text-lg font-medium"
sources={sources}
message={threadMessages?.[0]['node']['content']}
message={replaceAtMentionPlaceHolderWithAt(
threadMessages?.[0]?.['node']['content'] ?? ''
)}
/>
</LoadingWrapper>
</div>
Expand Down
23 changes: 22 additions & 1 deletion ee/tabby-ui/app/chat/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { MemoizedReactMarkdown } from '@/components/markdown'
import './page.css'

import { saveFetcherOptions } from '@/lib/tabby/token-management'
import { PromptFormRef } from '@/components/chat/form-editor/types'

const convertToHSLColor = (style: string) => {
return Color(style)
Expand Down Expand Up @@ -64,7 +65,7 @@ export default function ChatPage() {
const chatRef = useRef<ChatRef>(null)
const { width } = useWindowSize()
const prevWidthRef = useRef(width)
const chatInputRef = useRef<HTMLTextAreaElement>(null)
const chatInputRef = useRef<PromptFormRef>(null)

const searchParams = useSearchParams()
const client = searchParams.get('client') as ClientType
Expand All @@ -83,6 +84,9 @@ export default function ChatPage() {
supportsStoreAndFetchSessionState,
setSupportsStoreAndFetchSessionState
] = useState(false)
const [supportsListFileInWorkspace, setSupportProvideFileAtInfo] =
useState(false)
const [supportsReadFileContent, setSupportsReadFileContent] = useState(false)

const executeCommand = (command: ChatCommand) => {
if (chatRef.current) {
Expand Down Expand Up @@ -248,6 +252,13 @@ export default function ChatPage() {
server
?.hasCapability('readWorkspaceGitRepositories')
.then(setSupportsReadWorkspaceGitRepoInfo)
server
?.hasCapability('listFileInWorkspace')
.then(setSupportProvideFileAtInfo)
server
?.hasCapability('readFileContent')
.then(setSupportsReadFileContent)

Promise.all([
server?.hasCapability('fetchSessionState'),
server?.hasCapability('storeSessionState')
Expand Down Expand Up @@ -453,6 +464,16 @@ export default function ChatPage() {
storeSessionState={
supportsStoreAndFetchSessionState ? storeSessionState : undefined
}
listFileInWorkspace={
isInEditor && supportsListFileInWorkspace
? server?.listFileInWorkspace
: undefined
}
readFileContent={
isInEditor && supportsReadFileContent
? server?.readFileContent
: undefined
}
/>
</ErrorBoundary>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,7 @@ export function AssistantMessageSection({
setDevPanelOpen(true)
}}
highlightIndex={relevantCodeHighlightIndex}
supportsOpenInEditor={false}
/>
)}

Expand Down
94 changes: 73 additions & 21 deletions ee/tabby-ui/components/chat/chat-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { RefObject, useMemo, useState } from 'react'
import slugify from '@sindresorhus/slugify'
import { Content } from '@tiptap/core'
import { useWindowSize } from '@uidotdev/usehooks'
import type { UseChatHelpers } from 'ai/react'
import { AnimatePresence, motion } from 'framer-motion'
Expand All @@ -8,13 +9,16 @@ import { toast } from 'sonner'

import { SLUG_TITLE_MAX_LENGTH } from '@/lib/constants'
import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard'
import { useLatest } from '@/lib/hooks/use-latest'
import { updateEnableActiveSelection } from '@/lib/stores/chat-actions'
import { useChatStore } from '@/lib/stores/chat-store'
import { useMutation } from '@/lib/tabby/gql'
import { setThreadPersistedMutation } from '@/lib/tabby/query'
import type { Context } from '@/lib/types'
import type { Context, FileContext } from '@/lib/types'
import {
cn,
convertEditorContext,
getFileLocationFromContext,
getTitleFromMessages,
resolveFileNameForDisplay
} from '@/lib/utils'
Expand All @@ -24,6 +28,7 @@ import {
IconCheck,
IconEye,
IconEyeOff,
IconFile,
IconFileText,
IconRefresh,
IconRemove,
Expand All @@ -32,55 +37,57 @@ import {
IconTrash
} from '@/components/ui/icons'
import { ButtonScrollToBottom } from '@/components/button-scroll-to-bottom'
import { PromptForm, PromptFormRef } from '@/components/chat/prompt-form'
import { PromptForm } from '@/components/chat/prompt-form'
import { FooterText } from '@/components/footer'

import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip'
import { ChatContext } from './chat'
import { PromptFormRef } from './form-editor/types'
import { isSameEntireFileContextFromMention } from './form-editor/utils'
import { RepoSelect } from './repo-select'

export interface ChatPanelProps
extends Pick<UseChatHelpers, 'stop' | 'input' | 'setInput'> {
export interface ChatPanelProps extends Pick<UseChatHelpers, 'stop' | 'input'> {
setInput: (v: string) => void
id?: string
className?: string
onSubmit: (content: string) => Promise<any>
reload: () => void
chatMaxWidthClass: string
chatInputRef: RefObject<HTMLTextAreaElement>
chatInputRef: RefObject<PromptFormRef>
}

export interface ChatPanelRef {
focus: () => void
setInput: (input: Content) => void
input: string
}

function ChatPanelRenderer(
{
stop,
reload,
input,
setInput,
className,
onSubmit,
chatMaxWidthClass,
chatInputRef
}: ChatPanelProps,
ref: React.Ref<ChatPanelRef>
) {
const promptFormRef = React.useRef<PromptFormRef>(null)
const {
threadId,
container,
onClearMessages,
qaPairs,
isLoading,
relevantContext,
removeRelevantContext,
activeSelection,
onCopyContent,
selectedRepoId,
setSelectedRepoId,
repos,
initialized
initialized,
setRelevantContext,
openInEditor
} = React.useContext(ChatContext)
const enableActiveSelection = useChatStore(
state => state.enableActiveSelection
Expand Down Expand Up @@ -135,6 +142,39 @@ function ChatPanelRenderer(
}
}

const removeRelevantContext = useLatest((idx: number) => {
const editor = chatInputRef.current?.editor
if (!editor) {
return
}

const { state, view } = editor
const { tr } = state
const positionsToDelete: any[] = []

const currentContext: FileContext = relevantContext[idx]
state.doc.descendants((node, pos) => {
if (node.type.name === 'mention' && node.attrs.category === 'file') {
const fileContext = convertEditorContext({
filepath: node.attrs.fileItem.filepath,
content: '',
kind: 'file'
})
if (isSameEntireFileContextFromMention(fileContext, currentContext)) {
positionsToDelete.push({ from: pos, to: pos + node.nodeSize })
}
}
})

setRelevantContext(prev => prev.filter((item, index) => index !== idx))
positionsToDelete.reverse().forEach(({ from, to }) => {
tr.delete(from, to)
})

view.dispatch(tr)
editor.commands.focus()
})

const onSelectRepo = (sourceId: string | undefined) => {
setSelectedRepoId(sourceId)

Expand All @@ -148,11 +188,15 @@ function ChatPanelRenderer(
() => {
return {
focus: () => {
promptFormRef.current?.focus()
}
chatInputRef.current?.focus()
},
setInput: str => {
chatInputRef.current?.setInput(str)
},
input: chatInputRef.current?.input ?? ''
}
},
[]
[chatInputRef]
)

return (
Expand Down Expand Up @@ -236,7 +280,10 @@ function ChatPanelRenderer(
</Tooltip>
)}
</div>
<div className="border-t bg-background px-4 py-2 shadow-lg sm:space-y-4 sm:rounded-t-xl sm:border md:py-4">
<div
id="chat-panel-container"
className="border-t bg-background px-4 py-2 shadow-lg sm:space-y-4 sm:rounded-t-xl sm:border md:py-4"
>
<div className="flex flex-wrap gap-2">
<AnimatePresence presenceAffectsLayout>
<RepoSelect
Expand Down Expand Up @@ -304,14 +351,23 @@ function ChatPanelRenderer(
>
<Badge
variant="outline"
className="inline-flex h-7 flex-nowrap items-center gap-1 overflow-hidden rounded-md pr-0 text-sm font-semibold"
className={cn(
'inline-flex h-7 cursor-pointer flex-nowrap items-center gap-1 overflow-hidden rounded-md pr-0 text-sm font-semibold'
)}
onClick={() => {
openInEditor(getFileLocationFromContext(item))
}}
>
<IconFile className="shrink-0" />
<ContextLabel context={item} />
<Button
size="icon"
variant="ghost"
className="h-7 w-7 shrink-0 rounded-l-none hover:bg-muted/50"
onClick={removeRelevantContext.bind(null, idx)}
onClick={e => {
e.stopPropagation()
removeRelevantContext.current(idx)
}}
>
<IconRemove />
</Button>
Expand All @@ -322,13 +378,9 @@ function ChatPanelRenderer(
</AnimatePresence>
</div>
<PromptForm
ref={promptFormRef}
ref={chatInputRef}
onSubmit={onSubmit}
input={input}
setInput={setInput}
isLoading={isLoading}
chatInputRef={chatInputRef}
isInitializing={!initialized}
/>
<FooterText className="hidden sm:block" />
</div>
Expand Down
Loading

0 comments on commit af33713

Please sign in to comment.