From 8916a6ab089aa542af28dc6d74f1b7dcc680d930 Mon Sep 17 00:00:00 2001 From: Jackson Chen <541898146chen@gmail.com> Date: Sat, 16 Nov 2024 14:22:57 -0600 Subject: [PATCH] refactor: Update code to use KeywordInfo type for onRenderLsp event handler --- clients/tabby-chat-panel/src/index.ts | 18 ++-- ee/tabby-ui/components/chat/chat.tsx | 17 +++- .../components/chat/question-answer.tsx | 1 + .../components/message-markdown/index.tsx | 90 ++++++++++++++++--- 4 files changed, 102 insertions(+), 24 deletions(-) diff --git a/clients/tabby-chat-panel/src/index.ts b/clients/tabby-chat-panel/src/index.ts index b0af3f8eadf5..cb2677c5c80b 100644 --- a/clients/tabby-chat-panel/src/index.ts +++ b/clients/tabby-chat-panel/src/index.ts @@ -53,7 +53,14 @@ export interface ServerApi { updateTheme: (style: string, themeClass: string) => void updateActiveSelection: (context: Context | null) => void } - +export interface KeywordInfo { + sourceFile: string + sourceLine: number + sourceChar: number + targetFile: string + targetLine: number + targetChar: number +} export interface ClientApi { navigate: (context: Context, opts?: NavigateOpts) => void refresh: () => Promise @@ -71,14 +78,7 @@ export interface ClientApi { onKeyboardEvent: (type: 'keydown' | 'keyup' | 'keypress', event: KeyboardEventInit) => void onRenderLsp: (filepaths: string[], keywords: string[]) => Promise> } diff --git a/ee/tabby-ui/components/chat/chat.tsx b/ee/tabby-ui/components/chat/chat.tsx index 2979fe4d40a8..bca6740c2e71 100644 --- a/ee/tabby-ui/components/chat/chat.tsx +++ b/ee/tabby-ui/components/chat/chat.tsx @@ -1,6 +1,11 @@ import React, { RefObject } from 'react' import { compact, findIndex, isEqual, some, uniqWith } from 'lodash-es' -import type { Context, FileContext, NavigateOpts } from 'tabby-chat-panel' +import type { + Context, + FileContext, + KeywordInfo, + NavigateOpts +} from 'tabby-chat-panel' import { ERROR_CODE_NOT_FOUND } from '@/lib/constants' import { @@ -46,7 +51,10 @@ type ChatContextValue = { content: string, opts?: { languageId: string; smart: boolean } ) => void -onRenderLsp?: (filepaths: string[], keywords: string[]) => void + onRenderLsp?: ( + filepaths: string[], + keywords: string[] + ) => Promise> relevantContext: Context[] activeSelection: Context | null removeRelevantContext: (index: number) => void @@ -85,7 +93,10 @@ interface ChatProps extends React.ComponentProps<'div'> { content: string, opts?: { languageId: string; smart: boolean } ) => void - onRenderLsp?: (filepaths: string[], keywords: string[]) => void + onRenderLsp?: ( + filepaths: string[], + keywords: string[] + ) => Promise> chatInputRef: RefObject } diff --git a/ee/tabby-ui/components/chat/question-answer.tsx b/ee/tabby-ui/components/chat/question-answer.tsx index 3ac7515b2b5b..2913937348d7 100644 --- a/ee/tabby-ui/components/chat/question-answer.tsx +++ b/ee/tabby-ui/components/chat/question-answer.tsx @@ -390,6 +390,7 @@ function AssistantMessageCard(props: AssistantMessageCardProps) { onCodeCitationMouseLeave={onCodeCitationMouseLeave} canWrapLongLines={!isLoading} onRenderLsp={onRenderLsp} + onNavigateToContext={onNavigateToContext} /> {!!message.error && } diff --git a/ee/tabby-ui/components/message-markdown/index.tsx b/ee/tabby-ui/components/message-markdown/index.tsx index ef32ba4742cb..e8f968bce7ab 100644 --- a/ee/tabby-ui/components/message-markdown/index.tsx +++ b/ee/tabby-ui/components/message-markdown/index.tsx @@ -33,6 +33,8 @@ import { MemoizedReactMarkdown } from '@/components/markdown' import './style.css' +import { Context, KeywordInfo, NavigateOpts } from 'tabby-chat-panel/index' + import { MARKDOWN_CITATION_REGEX, MARKDOWN_SOURCE_REGEX @@ -75,7 +77,13 @@ export interface MessageMarkdownProps { content: string, opts?: { languageId: string; smart: boolean } ) => void - onRenderLsp?: (filepaths: string[], keywords: string[]) => void + onRenderLsp?: ( + filepaths: string[], + keywords: string[] + ) => Promise> + onNavigateToContext?: + | ((context: Context, opts?: NavigateOpts) => void) + | undefined onCodeCitationClick?: (code: MessageAttachmentCode) => void onCodeCitationMouseEnter?: (index: number) => void onCodeCitationMouseLeave?: (index: number) => void @@ -98,6 +106,8 @@ type MessageMarkdownContextValue = { contextInfo: ContextInfo | undefined fetchingContextInfo: boolean canWrapLongLines: boolean + keywordMap: KeywordMapType + onNavigateToContext?: (context: Context, opts?: NavigateOpts) => void } const MessageMarkdownContext = createContext( @@ -116,6 +126,7 @@ export function MessageMarkdown({ className, canWrapLongLines, onRenderLsp, + onNavigateToContext, ...rest }: MessageMarkdownProps) { const messageAttachments: MessageAttachments = useMemo(() => { @@ -180,21 +191,26 @@ export function MessageMarkdown({ return elements } + const [keywordMap, setKeywordMap] = useState({}) // rendering keywords useEffect(() => { if (message && canWrapLongLines && onRenderLsp) { - // eslint-disable-next-line no-console - console.log('docs', attachmentDocs?.map(doc => doc.link).join(',')) - // eslint-disable-next-line no-console - console.log('code', attachmentCode?.map(code => code.filepath).join(',')) + setKeywordMap({}) // TODO: remove htis const inlineCodeRegex = /`([^`]+)`/g const matches = message.match(inlineCodeRegex) if (matches) { - const inlineCodes = matches.map(match => match.replace(/`/g, '').trim()) - onRenderLsp(inlineCodes, [attachmentCode ? attachmentCode?.map(code => code.filepath) : ...[]]) + const inlineCodes = Array.from( + new Set(matches.map(match => match.replace(/`/g, '').trim())) + ) + onRenderLsp( + attachmentCode ? attachmentCode?.map(code => code.filepath) : [], + inlineCodes + ).then(res => { + setKeywordMap(res) + }) } } - }, [message, canWrapLongLines, onRenderLsp]) + }, [message, canWrapLongLines, onRenderLsp, attachmentCode]) return ( {childrenItem} })} @@ -246,21 +263,66 @@ export function MessageMarkdown({ return
  • {children}
  • }, code({ node, inline, className, children, ...props }) { + const { keywordMap, onNavigateToContext, canWrapLongLines } = + // eslint-disable-next-line react-hooks/rules-of-hooks + useContext(MessageMarkdownContext) + if (children.length) { if (children[0] == '▍') { return ( ) } - children[0] = (children[0] as string).replace('`▍`', '▍') } const match = /language-(\w+)/.exec(className || '') if (inline) { + const keyword = children[0]?.toString() + if (!keyword) { + return ( + + {children} + + ) + } + + const info = keywordMap[keyword] + + const isClickable = Boolean( + info && onNavigateToContext && canWrapLongLines + ) + return ( - + { + if (!isClickable) return + if (onNavigateToContext) { + onNavigateToContext( + { + filepath: info.targetFile, + content: '', + git_url: '', + kind: 'file', + range: { + start: info.targetLine, + end: info.targetLine + 1 + } + }, + { openInEditor: true } + ) + } + }} + title={info ? `${info.targetFile}:${info.targetLine}` : ''} + {...props} + > {children} ) @@ -504,3 +566,7 @@ export function SiteFavicon({ ) } + +interface KeywordMapType { + [key: string]: KeywordInfo +}