Skip to content

Commit

Permalink
refactor(chat): enhance at-mention handling and improve file item pro…
Browse files Browse the repository at this point in the history
…cessing
  • Loading branch information
Sma1lboy committed Jan 13, 2025
1 parent c6b2413 commit b0eac5f
Show file tree
Hide file tree
Showing 6 changed files with 198 additions and 21 deletions.
51 changes: 41 additions & 10 deletions ee/tabby-ui/components/chat/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,11 @@ import {
import { ChatPanel, ChatPanelRef } from './chat-panel'
import { ChatScrollAnchor } from './chat-scroll-anchor'
import { EmptyScreen } from './empty-screen'
import { FILEITEM_REGEX } from './form-editor/utils'
import { FileItem } from './form-editor/types'
import {
FILEITEM_REGEX,
replaceAtMentionPlaceHolderWithAt
} from './form-editor/utils'
import { QuestionAnswerList } from './question-answer'

type ChatContextValue = {
Expand Down Expand Up @@ -278,7 +282,8 @@ function ChatRenderer(
// delete message pair
const nextQaPairs = qaPairs.filter(o => o.user.id !== userMessageId)
setQaPairs(nextQaPairs)
setInput(userMessage.message)
// FIXME: put this transformer to somewhere else, both case in message markdown and edit could be cover by same method
setInput(replaceAtMentionPlaceHolderWithAt(userMessage.message))
if (userMessage.activeContext) {
openInEditor(getFileLocationFromContext(userMessage.activeContext))
}
Expand Down Expand Up @@ -487,7 +492,13 @@ function ChatRenderer(

setQaPairs(nextQaPairs)

sendUserMessage(...generateRequestPayload(newUserMessage))
// FIXME: we don't need to passing placeholder to backend
sendUserMessage(
...generateRequestPayload({
...newUserMessage,
message: replaceAtMentionPlaceHolderWithAt(userMessage.message)
})
)
}
)

Expand All @@ -508,26 +519,46 @@ function ChatRenderer(
}

const handleSubmit = async (value: string) => {
const fileItems: any[] = []
const fileItems: FileItem[] = []
let newValue = value

let match
while ((match = FILEITEM_REGEX.exec(value)) !== null) {
try {
const parsedItem = JSON.parse(match[1])
fileItems.push(parsedItem)

const replacement = `@${
const labelName =
parsedItem.label.split('/').pop() || parsedItem.label || 'unknown'
}`
newValue = newValue.replace(match[0], replacement)
newValue = newValue.replace(match[0], `@${labelName}`)
} catch (error) {
continue
}
}

// read all at file and push to relevant context, which will request to backend server later
let fileContents: Context[] = []
if (readFileContent && fileItems.length > 0) {
fileContents = await Promise.all(
fileItems.map(async item => {
const content = await readFileContent({ filepath: item.filepath })
return {
filepath:
'filepath' in item.filepath
? item.filepath.filepath
: item.filepath.uri,
content: content ?? '',
git_url:
'git_url' in item.filepath
? (item.filepath.git_url as string)
: '',
kind: 'file'
}
})
)
}

sendUserChat({
message: value,
relevantContext: relevantContext
relevantContext: [...fileContents, ...relevantContext]
})

setRelevantContext([])
Expand Down
10 changes: 4 additions & 6 deletions ee/tabby-ui/components/chat/form-editor/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ListFileItem } from 'tabby-chat-panel/index'

/**
* PromptProps defines the props for the PromptForm component.
*/
Expand Down Expand Up @@ -31,15 +33,11 @@ export interface PromptFormRef {
input: string
}

// TODO: move this into chat-panel in next iterate
/**
* Represents a file item inside the workspace.
* (You can add more properties if needed)
*/
export interface FileItem {
label: string
id?: string
// ... any other fields that you might have
}
export type FileItem = ListFileItem

/**
* Represents a file source item for mention suggestions.
Expand Down
107 changes: 103 additions & 4 deletions ee/tabby-ui/components/chat/form-editor/utils.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Mention from '@tiptap/extension-mention'
import { NodeViewWrapper, ReactNodeViewRenderer } from '@tiptap/react'
import { Filepath } from 'tabby-chat-panel/index'

import { cn } from '@/lib/utils'

Expand Down Expand Up @@ -53,16 +54,17 @@ export function shortenLabel(label: string, suffixLength = 15): string {
*/
export const MentionComponent = ({ node }: { node: any }) => {
return (
<NodeViewWrapper className="inline">
<NodeViewWrapper className="inline-block align-middle -my-1">
<span
className={cn(
'inline-flex items-center rounded bg-muted px-1.5 py-0.5 text-sm font-medium text-white',
'ring-1 ring-inset ring-muted'
'bg-muted prose inline-flex items-center rounded px-1.5 py-0.5 text-sm font-medium text-white',
'ring-muted ring-1 ring-inset',
'relative top-[0.1em]'
)}
data-category={node.attrs.category}
>
<FileItemIcon />
<span>{node.attrs.name}</span>
<span className="relative top-[-0.5px]">{node.attrs.name}</span>
</span>
</NodeViewWrapper>
)
Expand Down Expand Up @@ -193,3 +195,100 @@ export const MentionList = ({
</div>
)
}

// Some utils function help to extract place holder
export function replaceAtMentionPlaceHolderWithAt(value: string) {
// eslint-disable-next-line no-console
console.log('before value: ', value)

let newValue = value

let match
while ((match = FILEITEM_REGEX.exec(value)) !== null) {
try {
const parsedItem = JSON.parse(match[1])
const labelName =
parsedItem.label.split('/').pop() || parsedItem.label || 'unknown'
newValue = newValue.replace(match[0], `@${labelName}`)
} catch (error) {
continue
}
}

// eslint-disable-next-line no-console
console.log('new value:', newValue)
return newValue
}

interface ReplaceResult {
newValue: string
fileItems: FileItem[]
}

export const FILEITEM_AT_REGEX = /\[\[fileItemAt: (\d+)\]\]/g

// Some utils function help to extract place holder
// replace at mention JSON placeholder to something like [[fileItemAt: id]] which ad is string
// return a list of FileItem with unique id
// also return string already replaced
// example:
// [[fileItem:{"label":"src/CodeActions.ts","filepath":{"kind":"git","filepath":"clients/vscode/src/CodeActions.ts",
// "gitUrl":"[email protected]:Sma1lboy/tabby.git"}}]] explain this [[fileItem:{"label":"src/CodeActions.ts",
// "filepath":{"kind":"git","filepath":"clients/vscode/src/CodeActions.ts","gitUrl":"[email protected]:Sma1lboy/tabby.git"}}]]
// will replaced as [[fileItemAt: idx0]] explain this [[fileItemAt: idx1]]
// and with [fileItem1, fileItem2]
export function replaceAtMentionPlaceHolderWithAtPlaceHolder(
value: string
): ReplaceResult {
// eslint-disable-next-line no-console
console.log('before value: ', value)

let newValue = value
let match
const fileItems: FileItem[] = []
let idx = 0

while ((match = FILEITEM_REGEX.exec(value)) !== null) {
try {
const parsedItem = JSON.parse(match[1])

fileItems.push({
...parsedItem
})

newValue = newValue.replace(match[0], `[[fileItemAt: ${idx}]]`)

idx++
} catch (error) {
continue
}
}

// eslint-disable-next-line no-console
console.log('new value:', newValue)
return {
newValue,
fileItems
}
}

// utils function
export function getFilepathStringByChatPanelFilePath(
filepath: Filepath
): string {
return 'filepath' in filepath ? filepath.filepath : filepath.uri
}

export function getLastSegmentFromPath(filepath: string): string {
if (!filepath) {
return 'unknown'
}

const normalizedPath = filepath.replace(/\\/g, '/')

const cleanPath = normalizedPath.replace(/\/+$/, '')
const segments = cleanPath.split('/')
const lastSegment = segments[segments.length - 1]

return lastSegment || 'unknown'
}
17 changes: 17 additions & 0 deletions ee/tabby-ui/components/chat/prompt-form.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
.ProseMirror {
> * + * {
margin-top: 0.75em;
}

&:focus {
outline: none !important;
}

.ProseMirror-selectednode {
outline: none !important;
}

::selection {
background: transparent;
}
}
7 changes: 6 additions & 1 deletion ee/tabby-ui/components/chat/prompt-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import Placeholder from '@tiptap/extension-placeholder'
import Text from '@tiptap/extension-text'
import { EditorContent, useEditor } from '@tiptap/react'

import './prompt-form.css'

import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import { IconArrowElbow, IconEdit } from '@/components/ui/icons'
import { Popover, PopoverAnchor, PopoverContent } from '@/components/ui/popover'
Expand Down Expand Up @@ -283,7 +286,9 @@ function PromptFormRenderer(
{/* TipTap editor content */}
<EditorContent
editor={editor}
className="prose overflow-hidden break-words text-white focus:outline-none"
className={cn(
'prose overflow-hidden break-words text-white focus:outline-none'
)}
/>
</div>
{anchorElement}
Expand Down
27 changes: 27 additions & 0 deletions ee/tabby-ui/components/message-markdown/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ import {
MARKDOWN_SOURCE_REGEX
} from '@/lib/constants/regex'

import {
FILEITEM_AT_REGEX,
getFilepathStringByChatPanelFilePath,
getLastSegmentFromPath,
replaceAtMentionPlaceHolderWithAtPlaceHolder
} from '../chat/form-editor/utils'
import { Mention } from '../mention-tag'
import { Skeleton } from '../ui/skeleton'
import { CodeElement } from './code'
Expand Down Expand Up @@ -96,6 +102,11 @@ export function MessageMarkdown({
activeSelection,
...rest
}: MessageMarkdownProps) {
// deal with some unresolved file items
const msgRes = replaceAtMentionPlaceHolderWithAtPlaceHolder(message)
message = msgRes.newValue
const fileItems = msgRes.fileItems

const [symbolPositionMap, setSymbolLocationMap] = useState<
Map<string, SymbolInfo | undefined>
>(new Map())
Expand Down Expand Up @@ -163,6 +174,14 @@ export function MessageMarkdown({
return { sourceId, className }
})

processMatches(FILEITEM_AT_REGEX, AtMentionTag, (match: string) => {
return {
label: getLastSegmentFromPath(
getFilepathStringByChatPanelFilePath(fileItems[+match[1]].filepath)
)
}
})

addTextNode(text.slice(lastIndex))

return elements
Expand Down Expand Up @@ -377,6 +396,14 @@ function SourceTag({
)
}

function AtMentionTag({ label }: { label: string | undefined }) {
return (
<span className="bg-muted/50 hover:bg-muted text-muted-foreground prose inline-flex items-center rounded px-1.5 py-0.5 text-sm font-medium text-white">
@{label}
</span>
)
}

function RelevantDocumentBadge({
relevantDocument,
citationIndex
Expand Down

0 comments on commit b0eac5f

Please sign in to comment.