From fcb17839c18f63b0c5b1013e3a6f49e03d22a93e Mon Sep 17 00:00:00 2001 From: aliang Date: Sat, 28 Dec 2024 00:31:45 +0800 Subject: [PATCH] feat(ui): add repository select in Answer Engine (#3619) * feat(ui): add repository select in Answer Engine * update: code query * [autofix.ci] apply automated fixes * update * update: rename * update * update * update: rename * update: rename * [autofix.ci] apply automated fixes * update --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- ee/tabby-ui/app/(home)/page.tsx | 21 +- .../components/assistant-message-section.tsx | 25 ++- ee/tabby-ui/app/search/components/header.tsx | 2 +- .../app/search/components/search-context.ts | 32 +++ ee/tabby-ui/app/search/components/search.tsx | 103 ++++----- ee/tabby-ui/app/search/components/types.ts | 24 ++ .../components/user-message-section.tsx | 3 +- ee/tabby-ui/components/chat/chat.tsx | 18 +- .../components/prompt-editor/index.tsx | 14 -- .../index.tsx} | 211 ++++++------------ .../textarea-search/model-select.tsx | 93 ++++++++ .../textarea-search/repo-select.tsx | 169 ++++++++++++++ ee/tabby-ui/lib/hooks/use-models.tsx | 8 +- ee/tabby-ui/lib/hooks/use-repositories.ts | 31 +++ ee/tabby-ui/lib/hooks/use-thread-run.ts | 13 +- ee/tabby-ui/lib/stores/chat-actions.ts | 4 + ee/tabby-ui/lib/stores/chat-store.ts | 2 + ee/tabby-ui/lib/tabby/query.ts | 15 ++ 18 files changed, 532 insertions(+), 256 deletions(-) create mode 100644 ee/tabby-ui/app/search/components/search-context.ts create mode 100644 ee/tabby-ui/app/search/components/types.ts rename ee/tabby-ui/components/{textarea-search.tsx => textarea-search/index.tsx} (65%) create mode 100644 ee/tabby-ui/components/textarea-search/model-select.tsx create mode 100644 ee/tabby-ui/components/textarea-search/repo-select.tsx create mode 100644 ee/tabby-ui/lib/hooks/use-repositories.ts diff --git a/ee/tabby-ui/app/(home)/page.tsx b/ee/tabby-ui/app/(home)/page.tsx index 50253a0cf196..f6c01e67a503 100644 --- a/ee/tabby-ui/app/(home)/page.tsx +++ b/ee/tabby-ui/app/(home)/page.tsx @@ -10,12 +10,16 @@ import { useStore } from 'zustand' import { SESSION_STORAGE_KEY } from '@/lib/constants' import { useMe } from '@/lib/hooks/use-me' import { useSelectedModel } from '@/lib/hooks/use-models' +import { useSelectedRepository } from '@/lib/hooks/use-repositories' import { useIsChatEnabled, useIsFetchingServerInfo } from '@/lib/hooks/use-server-info' import { setThreadsPageNo } from '@/lib/stores/answer-engine-store' -import { updateSelectedModel } from '@/lib/stores/chat-actions' +import { + updateSelectedModel, + updateSelectedRepoSourceId +} from '@/lib/stores/chat-actions' import { clearHomeScrollPosition, setHomeScrollPosition, @@ -55,7 +59,8 @@ function MainPanel() { }) const scrollY = useStore(useScrollStore, state => state.homePage) - const { selectedModel, isModelLoading, models } = useSelectedModel() + const { selectedModel, isFetchingModels, models } = useSelectedModel() + const { selectedRepository, isFetchingRepositories } = useSelectedRepository() const showMainSection = !!data?.me || !isFetchingServerInfo @@ -85,6 +90,10 @@ function MainPanel() { updateSelectedModel(model) } + const onSelectedRepo = (sourceId: string | undefined) => { + updateSelectedRepoSourceId(sourceId) + } + const onSearch = (question: string, ctx?: ThreadRunContexts) => { setIsLoading(true) sessionStorage.setItem(SESSION_STORAGE_KEY.SEARCH_INITIAL_MSG, question) @@ -157,8 +166,12 @@ function MainPanel() { contextInfo={contextInfoData?.contextInfo} fetchingContextInfo={fetchingContextInfo} modelName={selectedModel} - onModelSelect={handleSelectModel} - isModelLoading={isModelLoading} + onSelectModel={handleSelectModel} + repoSourceId={selectedRepository?.sourceId} + onSelectRepo={onSelectedRepo} + isInitializingResources={ + isFetchingModels || isFetchingRepositories + } models={models} /> diff --git a/ee/tabby-ui/app/search/components/assistant-message-section.tsx b/ee/tabby-ui/app/search/components/assistant-message-section.tsx index 450ad9cca536..b4742febcbbe 100644 --- a/ee/tabby-ui/app/search/components/assistant-message-section.tsx +++ b/ee/tabby-ui/app/search/components/assistant-message-section.tsx @@ -77,7 +77,9 @@ import { DocDetailView } from '@/components/message-markdown/doc-detail-view' import { SiteFavicon } from '@/components/site-favicon' import { UserAvatar } from '@/components/user-avatar' -import { ConversationMessage, SearchContext, SOURCE_CARD_STYLE } from './search' +import { SOURCE_CARD_STYLE } from './search' +import { SearchContext } from './search-context' +import { ConversationMessage } from './types' export function AssistantMessageSection({ className, @@ -106,7 +108,8 @@ export function AssistantMessageSection({ fetchingContextInfo, onDeleteMessage, isThreadOwner, - onUpdateMessage + onUpdateMessage, + repositories } = useContext(SearchContext) const { supportsOnApplyInEditorV2 } = useContext(ChatContext) @@ -147,7 +150,15 @@ export function AssistantMessageSection({ const IconAnswer = isLoading ? IconSpinner : IconSparkles - const relevantCodeGitURL = message?.attachment?.code?.[0]?.gitUrl || '' + // match gitUrl for clientCode with codeSourceId + const clientCodeGitUrl = useMemo(() => { + if (!message.codeSourceId || !repositories?.length) return '' + + const target = repositories.find( + info => info.sourceId === message.codeSourceId + ) + return target?.gitUrl ?? '' + }, [message.codeSourceId, repositories]) const clientCodeContexts: RelevantCodeContext[] = useMemo(() => { if (!clientCode?.length) return [] @@ -158,11 +169,11 @@ export function AssistantMessageSection({ range: getRangeFromAttachmentCode(code), filepath: code.filepath || '', content: code.content, - git_url: relevantCodeGitURL + git_url: clientCodeGitUrl } }) ?? [] ) - }, [clientCode, relevantCodeGitURL]) + }, [clientCode, clientCodeGitUrl]) const serverCodeContexts: RelevantCodeContext[] = useMemo(() => { return ( @@ -185,9 +196,9 @@ export function AssistantMessageSection({ const messageAttachmentClientCode = useMemo(() => { return clientCode?.map(o => ({ ...o, - gitUrl: relevantCodeGitURL + gitUrl: clientCodeGitUrl })) - }, [clientCode, relevantCodeGitURL]) + }, [clientCode, clientCodeGitUrl]) const messageAttachmentDocs = message?.attachment?.doc const messageAttachmentCodeLen = diff --git a/ee/tabby-ui/app/search/components/header.tsx b/ee/tabby-ui/app/search/components/header.tsx index de8dac3b697a..acf1019edacb 100644 --- a/ee/tabby-ui/app/search/components/header.tsx +++ b/ee/tabby-ui/app/search/components/header.tsx @@ -32,7 +32,7 @@ import { ThemeToggle } from '@/components/theme-toggle' import { MyAvatar } from '@/components/user-avatar' import UserPanel from '@/components/user-panel' -import { SearchContext } from './search' +import { SearchContext } from './search-context' const deleteThreadMutation = graphql(/* GraphQL */ ` mutation DeleteThread($id: ID!) { diff --git a/ee/tabby-ui/app/search/components/search-context.ts b/ee/tabby-ui/app/search/components/search-context.ts new file mode 100644 index 000000000000..600c8e769648 --- /dev/null +++ b/ee/tabby-ui/app/search/components/search-context.ts @@ -0,0 +1,32 @@ +import { createContext } from 'react' + +import { + ContextInfo, + RepositorySourceListQuery +} from '@/lib/gql/generates/graphql' +import { ExtendedCombinedError } from '@/lib/types' + +import { ConversationMessage } from './types' + +type SearchContextValue = { + // flag for initialize the pathname + isPathnameInitialized: boolean + isLoading: boolean + onRegenerateResponse: (id: string) => void + onSubmitSearch: (question: string) => void + setDevPanelOpen: (v: boolean) => void + setConversationIdForDev: (v: string | undefined) => void + enableDeveloperMode: boolean + contextInfo: ContextInfo | undefined + fetchingContextInfo: boolean + onDeleteMessage: (id: string) => void + isThreadOwner: boolean + onUpdateMessage: ( + message: ConversationMessage + ) => Promise + repositories: RepositorySourceListQuery['repositoryList'] | undefined +} + +export const SearchContext = createContext( + {} as SearchContextValue +) diff --git a/ee/tabby-ui/app/search/components/search.tsx b/ee/tabby-ui/app/search/components/search.tsx index 96a934154112..8e7e3236f020 100644 --- a/ee/tabby-ui/app/search/components/search.tsx +++ b/ee/tabby-ui/app/search/components/search.tsx @@ -1,7 +1,6 @@ 'use client' import { - createContext, CSSProperties, Fragment, useEffect, @@ -27,12 +26,8 @@ import { useEnableDeveloperMode } from '@/lib/experiment-flags' import { graphql } from '@/lib/gql/generates' import { CodeQueryInput, - ContextInfo, DocQueryInput, InputMaybe, - Maybe, - Message, - MessageAttachmentClientCode, Role } from '@/lib/gql/generates/graphql' import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard' @@ -41,10 +36,14 @@ import { useDebounceValue } from '@/lib/hooks/use-debounce' import { useLatest } from '@/lib/hooks/use-latest' import { useMe } from '@/lib/hooks/use-me' import { useSelectedModel } from '@/lib/hooks/use-models' +import { useSelectedRepository } from '@/lib/hooks/use-repositories' import useRouterStuff from '@/lib/hooks/use-router-stuff' import { useIsChatEnabled } from '@/lib/hooks/use-server-info' import { useThreadRun } from '@/lib/hooks/use-thread-run' -import { updateSelectedModel } from '@/lib/stores/chat-actions' +import { + updateSelectedModel, + updateSelectedRepoSourceId +} from '@/lib/stores/chat-actions' import { clearHomeScrollPosition } from '@/lib/stores/scroll-store' import { useMutation } from '@/lib/tabby/gql' import { @@ -53,12 +52,7 @@ import { listThreads, setThreadPersistedMutation } from '@/lib/tabby/query' -import { - AttachmentCodeItem, - AttachmentDocItem, - ExtendedCombinedError, - ThreadRunContexts -} from '@/lib/types' +import { ExtendedCombinedError, ThreadRunContexts } from '@/lib/types' import { cn, getMentionsFromText, @@ -95,49 +89,10 @@ import { AssistantMessageSection } from './assistant-message-section' import { DevPanel } from './dev-panel' import { Header } from './header' import { MessagesSkeleton } from './messages-skeleton' +import { SearchContext } from './search-context' +import { ConversationMessage, ConversationPair } from './types' import { UserMessageSection } from './user-message-section' -export type ConversationMessage = Omit< - Message, - '__typename' | 'updatedAt' | 'createdAt' | 'attachment' | 'threadId' -> & { - threadId?: string - threadRelevantQuestions?: Maybe - error?: string - attachment?: { - clientCode?: Maybe> | undefined - code: Maybe> | undefined - doc: Maybe> | undefined - } -} - -type ConversationPair = { - question: ConversationMessage | null - answer: ConversationMessage | null -} - -type SearchContextValue = { - // flag for initialize the pathname - isPathnameInitialized: boolean - isLoading: boolean - onRegenerateResponse: (id: string) => void - onSubmitSearch: (question: string) => void - setDevPanelOpen: (v: boolean) => void - setConversationIdForDev: (v: string | undefined) => void - enableDeveloperMode: boolean - contextInfo: ContextInfo | undefined - fetchingContextInfo: boolean - onDeleteMessage: (id: string) => void - isThreadOwner: boolean - onUpdateMessage: ( - message: ConversationMessage - ) => Promise -} - -export const SearchContext = createContext( - {} as SearchContextValue -) - export const SOURCE_CARD_STYLE = { compress: 5.3, expand: 6.3 @@ -335,8 +290,9 @@ export function Search() { const isLoadingRef = useLatest(isLoading) - const { selectedModel, isModelLoading, models } = useSelectedModel() - + const { selectedModel, isFetchingModels, models } = useSelectedModel() + const { selectedRepository, isFetchingRepositories, repos } = + useSelectedRepository() const currentMessageForDev = useMemo(() => { return messages.find(item => item.id === messageIdForDev) }, [messageIdForDev, messages]) @@ -575,7 +531,7 @@ export function Search() { } const { sourceIdForCodeQuery, sourceIdsForDocQuery, searchPublic } = - getSourceInputs(ctx) + getSourceInputs(selectedRepository?.sourceId, ctx) const codeQuery: InputMaybe = sourceIdForCodeQuery ? { sourceId: sourceIdForCodeQuery, content: question } @@ -641,7 +597,10 @@ export function Search() { ) const { sourceIdForCodeQuery, sourceIdsForDocQuery, searchPublic } = - getSourceInputs(getThreadRunContextsFromMentions(mentions)) + getSourceInputs( + selectedRepository?.sourceId, + getThreadRunContextsFromMentions(mentions) + ) const codeQuery: InputMaybe = sourceIdForCodeQuery ? { sourceId: sourceIdForCodeQuery, content: newUserMessage.content } @@ -726,10 +685,14 @@ export function Search() { ) } - const onModelSelect = (model: string) => { + const onSelectModel = (model: string) => { updateSelectedModel(model) } + const onSelectedRepo = (sourceId: string | undefined) => { + updateSelectedRepoSourceId(sourceId) + } + const formatedThreadError: ExtendedCombinedError | undefined = useMemo(() => { if (!isReady || fetchingThread || !threadIdFromURL) return undefined if (threadError || !threadData?.threads?.edges?.length) { @@ -806,7 +769,8 @@ export function Search() { fetchingContextInfo, onDeleteMessage, isThreadOwner, - onUpdateMessage + onUpdateMessage, + repositories: repos }} >
@@ -944,8 +908,12 @@ export function Search() { contextInfo={contextInfoData?.contextInfo} fetchingContextInfo={fetchingContextInfo} modelName={selectedModel} - onModelSelect={onModelSelect} - isModelLoading={isModelLoading} + onSelectModel={onSelectModel} + repoSourceId={selectedRepository?.sourceId} + onSelectRepo={onSelectedRepo} + isInitializingResources={ + isFetchingModels || isFetchingRepositories + } models={models} />
@@ -1026,17 +994,24 @@ function ThreadMessagesErrorView({ ) } -function getSourceInputs(ctx: ThreadRunContexts | undefined) { +function getSourceInputs( + repositorySourceId: string | undefined, + ctx: ThreadRunContexts | undefined +) { let sourceIdsForDocQuery: string[] = [] let sourceIdForCodeQuery: string | undefined let searchPublic = false if (ctx) { sourceIdsForDocQuery = uniq( - compact([ctx?.codeSourceIds?.[0]].concat(ctx.docSourceIds)) + // Compatible with existing user messages + compact( + [repositorySourceId, ctx?.codeSourceIds?.[0]].concat(ctx.docSourceIds) + ) ) searchPublic = ctx.searchPublic ?? false - sourceIdForCodeQuery = ctx.codeSourceIds?.[0] ?? undefined + sourceIdForCodeQuery = + repositorySourceId || ctx.codeSourceIds?.[0] || undefined } return { sourceIdsForDocQuery, diff --git a/ee/tabby-ui/app/search/components/types.ts b/ee/tabby-ui/app/search/components/types.ts new file mode 100644 index 000000000000..04e7b82a2083 --- /dev/null +++ b/ee/tabby-ui/app/search/components/types.ts @@ -0,0 +1,24 @@ +import { + Maybe, + Message, + MessageAttachmentClientCode +} from '@/lib/gql/generates/graphql' +import { AttachmentCodeItem, AttachmentDocItem } from '@/lib/types' + +export type ConversationMessage = Omit< + Message, + '__typename' | 'updatedAt' | 'createdAt' | 'attachment' | 'threadId' +> & { + threadId?: string + threadRelevantQuestions?: Maybe + error?: string + attachment?: { + clientCode?: Maybe> | undefined + code: Maybe> | undefined + doc: Maybe> | undefined + } +} +export type ConversationPair = { + question: ConversationMessage | null + answer: ConversationMessage | null +} diff --git a/ee/tabby-ui/app/search/components/user-message-section.tsx b/ee/tabby-ui/app/search/components/user-message-section.tsx index cd2cd2bd4f70..9ccb396113ee 100644 --- a/ee/tabby-ui/app/search/components/user-message-section.tsx +++ b/ee/tabby-ui/app/search/components/user-message-section.tsx @@ -4,7 +4,8 @@ import { cn } from '@/lib/utils' import { ChatContext } from '@/components/chat/chat' import { MessageMarkdown } from '@/components/message-markdown' -import { ConversationMessage, SearchContext } from './search' +import { SearchContext } from './search-context' +import { ConversationMessage } from './types' interface QuestionBlockProps extends HTMLAttributes { message: ConversationMessage diff --git a/ee/tabby-ui/components/chat/chat.tsx b/ee/tabby-ui/components/chat/chat.tsx index 4294b43752d1..aa58e314fcb3 100644 --- a/ee/tabby-ui/components/chat/chat.tsx +++ b/ee/tabby-ui/components/chat/chat.tsx @@ -11,7 +11,6 @@ import type { import { useQuery } from 'urql' import { ERROR_CODE_NOT_FOUND } from '@/lib/constants' -import { graphql } from '@/lib/gql/generates' import { CodeQueryInput, CreateMessageInput, @@ -25,6 +24,7 @@ import { useLatest } from '@/lib/hooks/use-latest' import { useThreadRun } from '@/lib/hooks/use-thread-run' import { filename2prism } from '@/lib/language-utils' import { useChatStore } from '@/lib/stores/chat-store' +import { repositorySourceListQuery } from '@/lib/tabby/query' import { ExtendedCombinedError } from '@/lib/types' import { AssistantMessage, @@ -49,20 +49,6 @@ import { ChatScrollAnchor } from './chat-scroll-anchor' import { EmptyScreen } from './empty-screen' import { QuestionAnswerList } from './question-answer' -const repositoryListQuery = graphql(/* GraphQL */ ` - query RepositorySourceList { - repositoryList { - id - name - kind - gitUrl - sourceId - sourceName - sourceKind - } - } -`) - type ChatContextValue = { initialized: boolean threadId: string | undefined @@ -180,7 +166,7 @@ function ChatRenderer( const chatPanelRef = React.useRef(null) const [{ data: repositoryListData, fetching: fetchingRepos }] = useQuery({ - query: repositoryListQuery + query: repositorySourceListQuery }) const repos = repositoryListData?.repositoryList diff --git a/ee/tabby-ui/components/prompt-editor/index.tsx b/ee/tabby-ui/components/prompt-editor/index.tsx index 6937c559893a..8fd9c0424e97 100644 --- a/ee/tabby-ui/components/prompt-editor/index.tsx +++ b/ee/tabby-ui/components/prompt-editor/index.tsx @@ -161,20 +161,6 @@ export const PromptEditor = forwardRef( placement: placement === 'bottom' ? 'top-start' : 'bottom-start', disabled: !hasDocSources }) - }), - // for codebase mention - MentionExtension.configure({ - deleteTriggerWithBackspace: true, - HTMLAttributes: { - class: 'mention-code' - }, - suggestion: suggestion({ - category: 'code', - char: '#', - pluginKey: CodeMentionPluginKey, - placement: placement === 'bottom' ? 'top-start' : 'bottom-start', - disabled: !hasCodebaseSources - }) }) ], editorProps: { diff --git a/ee/tabby-ui/components/textarea-search.tsx b/ee/tabby-ui/components/textarea-search/index.tsx similarity index 65% rename from ee/tabby-ui/components/textarea-search.tsx rename to ee/tabby-ui/components/textarea-search/index.tsx index d0c3525a5a7b..5103d24f18a3 100644 --- a/ee/tabby-ui/components/textarea-search.tsx +++ b/ee/tabby-ui/components/textarea-search/index.tsx @@ -12,39 +12,30 @@ import { checkSourcesAvailability, cn, getMentionsFromText, - getThreadRunContextsFromMentions + getThreadRunContextsFromMentions, + isCodeSourceContext } from '@/lib/utils' -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuRadioGroup, - DropdownMenuRadioItem, - DropdownMenuTrigger -} from '@/components/ui/dropdown-menu' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' -import LoadingWrapper from './loading-wrapper' -import { PromptEditor, PromptEditorRef } from './prompt-editor' -import { Button } from './ui/button' -import { - IconArrowRight, - IconAtSign, - IconBox, - IconCheck, - IconHash, - IconSpinner -} from './ui/icons' -import { Separator } from './ui/separator' -import { Skeleton } from './ui/skeleton' +import LoadingWrapper from '../loading-wrapper' +import { PromptEditor, PromptEditorRef } from '../prompt-editor' +import { Button } from '../ui/button' +import { IconArrowRight, IconAtSign, IconSpinner } from '../ui/icons' +import { Separator } from '../ui/separator' +import { Skeleton } from '../ui/skeleton' +import { ModelSelect } from './model-select' +import { RepoSelect } from './repo-select' export default function TextAreaSearch({ onSearch, - onModelSelect, modelName, + onSelectModel, + repoSourceId, + onSelectRepo, className, placeholder, showBetaBadge, @@ -55,13 +46,17 @@ export default function TextAreaSearch({ isFollowup, contextInfo, fetchingContextInfo, - isModelLoading, + isInitializingResources, models }: { onSearch: (value: string, ctx: ThreadRunContexts) => void - onModelSelect: (v: string) => void - isModelLoading: boolean + onSelectModel: (v: string) => void + isInitializingResources: boolean + // selected model modelName: string | undefined + // selected repo + repoSourceId: string | undefined + onSelectRepo: (id: string | undefined) => void className?: string placeholder?: string showBetaBadge?: boolean @@ -88,14 +83,21 @@ export default function TextAreaSearch({ } const handleSelectModel = (v: string) => { - onModelSelect(v) + onSelectModel(v) + setTimeout(() => { + focusTextarea() + }) + } + + const handleSelectRepo = (id: string | undefined) => { + onSelectRepo(id) setTimeout(() => { focusTextarea() }) } const handleSubmit = (editor: Editor | undefined | null) => { - if (!editor || isLoading || isModelLoading) { + if (!editor || isLoading || isInitializingResources) { return } @@ -145,11 +147,17 @@ export default function TextAreaSearch({ .run() } - const { hasCodebaseSource, hasDocumentSource } = useMemo(() => { + const { hasDocumentSource } = useMemo(() => { return checkSourcesAvailability(contextInfo?.sources) }, [contextInfo?.sources]) + const repos = useMemo(() => { + return contextInfo?.sources.filter(x => isCodeSourceContext(x.sourceKind)) + }, [contextInfo?.sources]) + const showModelSelect = !!models?.length + const showRepoSelect = !!repos?.length + const showBottomBar = showModelSelect || showRepoSelect return (
- {isFollowup && showModelSelect && ( -
- + {isFollowup && showBottomBar && ( +
e.stopPropagation()} + > + {showRepoSelect && ( + + )} + {showRepoSelect && showModelSelect && ( + + )} + {showModelSelect && ( + + )}
)}
@@ -223,7 +245,7 @@ export default function TextAreaSearch({ 'bg-primary text-primary-foreground cursor-pointer': value.length > 0, '!bg-muted !text-primary !cursor-default': - isLoading || value.length === 0 || isModelLoading, + isLoading || value.length === 0 || isInitializingResources, 'mr-1.5': !showBetaBadge } )} @@ -248,7 +270,7 @@ export default function TextAreaSearch({ onClick={e => e.stopPropagation()} > @@ -256,25 +278,6 @@ export default function TextAreaSearch({
} > - {/* mention codebase */} - - - - - - Select a codebase to chat with - - - - {/* mention docs */} @@ -293,6 +296,15 @@ export default function TextAreaSearch({ + {/* select codebase */} + + + {/* model select */} {!!models?.length && ( <> @@ -311,85 +323,6 @@ export default function TextAreaSearch({ ) } -interface ModelSelectProps { - models: Maybe> | undefined - value: string | undefined - onChange: (v: string) => void - isInitializing?: boolean -} - -function ModelSelect({ - models, - value, - onChange, - isInitializing -}: ModelSelectProps) { - const onModelSelect = (v: string) => { - onChange(v) - } - - return ( - - - - } - > - {!!models?.length && ( - - - - - - - {models.map(model => { - const isSelected = model === value - return ( - { - onModelSelect(model) - e.stopPropagation() - }} - value={model} - key={model} - className="cursor-pointer py-2 pl-3" - > - - - {model} - - - ) - })} - - - - )} - - ) -} - function BetaBadge() { const { theme } = useCurrentTheme() return ( diff --git a/ee/tabby-ui/components/textarea-search/model-select.tsx b/ee/tabby-ui/components/textarea-search/model-select.tsx new file mode 100644 index 000000000000..2bc91edcef29 --- /dev/null +++ b/ee/tabby-ui/components/textarea-search/model-select.tsx @@ -0,0 +1,93 @@ +import { Maybe } from '@/lib/gql/generates/graphql' +import { cn } from '@/lib/utils' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuTrigger +} from '@/components/ui/dropdown-menu' + +import LoadingWrapper from '../loading-wrapper' +import { Button } from '../ui/button' +import { IconBox, IconCheck } from '../ui/icons' +import { Skeleton } from '../ui/skeleton' + +interface ModelSelectProps { + models: Maybe> | undefined + value: string | undefined + onChange: (v: string) => void + isInitializing?: boolean +} + +export function ModelSelect({ + models, + value, + onChange, + isInitializing +}: ModelSelectProps) { + const onSelectModel = (v: string) => { + onChange(v) + } + + return ( + + + + } + > + {!!models?.length && ( + + + + + + + {models.map(model => { + const isSelected = model === value + return ( + { + onSelectModel(model) + e.stopPropagation() + }} + value={model} + key={model} + className="cursor-pointer py-2 pl-3" + > + + + {model} + + + ) + })} + + + + )} + + ) +} diff --git a/ee/tabby-ui/components/textarea-search/repo-select.tsx b/ee/tabby-ui/components/textarea-search/repo-select.tsx new file mode 100644 index 000000000000..206d41d942bc --- /dev/null +++ b/ee/tabby-ui/components/textarea-search/repo-select.tsx @@ -0,0 +1,169 @@ +import { useRef, useState } from 'react' + +import { ContextInfo } from '@/lib/gql/generates/graphql' +import { cn } from '@/lib/utils' +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator +} from '@/components/ui/command' +import { IconFolderGit } from '@/components/ui/icons' +import { + Popover, + PopoverContent, + PopoverTrigger +} from '@/components/ui/popover' +import { SourceIcon } from '@/components/source-icon' + +import LoadingWrapper from '../loading-wrapper' +import { Button } from '../ui/button' +import { IconCheck } from '../ui/icons' +import { Skeleton } from '../ui/skeleton' + +interface RepoSelectProps { + repos: ContextInfo['sources'] | undefined + value: string | undefined + onChange: (v: string | undefined) => void + isInitializing?: boolean +} + +export function RepoSelect({ + repos, + value, + onChange, + isInitializing +}: RepoSelectProps) { + const [open, setOpen] = useState(false) + const commandListRef = useRef(null) + + const onSelectRepo = (v: string) => { + onChange(v) + } + + const scrollCommandListToTop = () => { + requestAnimationFrame(() => { + if (commandListRef.current) { + commandListRef.current.scrollTop = 0 + } + }) + } + + const onSearchChange = () => { + scrollCommandListToTop() + } + + const selectedRepo = value + ? repos?.find(repo => repo.sourceId === value) + : undefined + const selectedRepoName = selectedRepo?.sourceName + + // if there's no repo, hide the repo select + if (!isInitializing && !repos?.length) return null + + return ( + + + + } + > + + + + + + + + + No context found + + {repos?.map(repo => { + const isSelected = repo.sourceId === value + + return ( + { + onSelectRepo(repo.sourceId) + setOpen(false) + }} + title={repo.sourceName} + > + +
+ +
+ {repo.sourceName} +
+
+
+ ) + })} +
+
+ + + { + onChange(undefined) + setOpen(false) + }} + > + Clear + + +
+
+
+
+ ) +} diff --git a/ee/tabby-ui/lib/hooks/use-models.tsx b/ee/tabby-ui/lib/hooks/use-models.tsx index a2a02e5dc5e9..d5cb07805e07 100644 --- a/ee/tabby-ui/lib/hooks/use-models.tsx +++ b/ee/tabby-ui/lib/hooks/use-models.tsx @@ -32,21 +32,21 @@ export function useModels(): SWRResponse { } export function useSelectedModel() { - const { data: modelData, isLoading: isFetchingModel } = useModels() + const { data: modelData, isLoading } = useModels() const selectedModel = useStore(useChatStore, state => state.selectedModel) useEffect(() => { - if (!isFetchingModel) { + if (!isLoading) { // init model const validModel = getModelFromModelInfo(selectedModel, modelData?.chat) updateSelectedModel(validModel) } - }, [isFetchingModel]) + }, [isLoading]) return { // fetching model data or trying to get selected model from localstorage - isModelLoading: isFetchingModel, + isFetchingModels: isLoading, selectedModel, models: modelData?.chat } diff --git a/ee/tabby-ui/lib/hooks/use-repositories.ts b/ee/tabby-ui/lib/hooks/use-repositories.ts new file mode 100644 index 000000000000..78f492296608 --- /dev/null +++ b/ee/tabby-ui/lib/hooks/use-repositories.ts @@ -0,0 +1,31 @@ +'use client' + +import { useMemo } from 'react' +import { useQuery } from 'urql' + +import { useChatStore } from '@/lib/stores/chat-store' +import { repositorySourceListQuery } from '@/lib/tabby/query' + +export function useRepositorySources() { + return useQuery({ + query: repositorySourceListQuery + }) +} + +export function useSelectedRepository() { + const [{ data, fetching }] = useRepositorySources() + const repos = data?.repositoryList + const repoId = useChatStore(state => state.selectedRepoSourceId) + + const selectedRepository = useMemo(() => { + if (!repos?.length || !repoId) return undefined + + return repos.find(repo => repo.sourceId === repoId) + }, [repos, repoId]) + + return { + repos, + isFetchingRepositories: fetching, + selectedRepository + } +} diff --git a/ee/tabby-ui/lib/hooks/use-thread-run.ts b/ee/tabby-ui/lib/hooks/use-thread-run.ts index 5a834fa9bdf3..69c5d9e59233 100644 --- a/ee/tabby-ui/lib/hooks/use-thread-run.ts +++ b/ee/tabby-ui/lib/hooks/use-thread-run.ts @@ -38,6 +38,7 @@ const CreateThreadAndRunSubscription = graphql(/* GraphQL */ ` questions } ... on ThreadAssistantMessageAttachmentsCode { + codeSourceId hits { code { gitUrl @@ -116,6 +117,7 @@ const CreateThreadRunSubscription = graphql(/* GraphQL */ ` questions } ... on ThreadAssistantMessageAttachmentsCode { + codeSourceId hits { code { gitUrl @@ -190,13 +192,11 @@ const DeleteThreadMessagePairMutation = graphql(/* GraphQL */ ` ) } `) - -type ID = string - export interface AnswerStream { - threadId?: ID - userMessageId?: ID - assistantMessageId?: ID + threadId?: string + userMessageId?: string + assistantMessageId?: string + codeSourceId?: string relevantQuestions?: Array attachmentsCode?: ThreadAssistantMessageAttachmentCodeHits attachmentsDoc?: ThreadAssistantMessageAttachmentDocHits @@ -276,6 +276,7 @@ export function useThreadRun({ break case 'ThreadAssistantMessageAttachmentsCode': x.attachmentsCode = data.hits + x.codeSourceId = data.codeSourceId break case 'ThreadAssistantMessageAttachmentsDoc': x.attachmentsDoc = data.hits diff --git a/ee/tabby-ui/lib/stores/chat-actions.ts b/ee/tabby-ui/lib/stores/chat-actions.ts index eb88174aad17..a556bf1b789b 100644 --- a/ee/tabby-ui/lib/stores/chat-actions.ts +++ b/ee/tabby-ui/lib/stores/chat-actions.ts @@ -10,6 +10,10 @@ export const updateSelectedModel = (model: string | undefined) => { set(() => ({ selectedModel: model })) } +export const updateSelectedRepoSourceId = (sourceId: string | undefined) => { + set(() => ({ selectedRepoSourceId: sourceId })) +} + export const updateEnableActiveSelection = (enable: boolean) => { set(() => ({ enableActiveSelection: enable })) } diff --git a/ee/tabby-ui/lib/stores/chat-store.ts b/ee/tabby-ui/lib/stores/chat-store.ts index 1c09b032c6b7..0ddae5873d04 100644 --- a/ee/tabby-ui/lib/stores/chat-store.ts +++ b/ee/tabby-ui/lib/stores/chat-store.ts @@ -8,12 +8,14 @@ const excludeFromState = ['activeChatId'] export interface ChatState { activeChatId: string | undefined selectedModel: string | undefined + selectedRepoSourceId: string | undefined enableActiveSelection: boolean } const initialState: ChatState = { activeChatId: nanoid(), selectedModel: undefined, + selectedRepoSourceId: undefined, enableActiveSelection: true } diff --git a/ee/tabby-ui/lib/tabby/query.ts b/ee/tabby-ui/lib/tabby/query.ts index ddfc77f0a4fa..aede986173ac 100644 --- a/ee/tabby-ui/lib/tabby/query.ts +++ b/ee/tabby-ui/lib/tabby/query.ts @@ -397,6 +397,7 @@ export const listThreadMessages = graphql(/* GraphQL */ ` node { id threadId + codeSourceId role content attachment { @@ -473,3 +474,17 @@ export const notificationsQuery = graphql(/* GraphQL */ ` } } `) + +export const repositorySourceListQuery = graphql(/* GraphQL */ ` + query RepositorySourceList { + repositoryList { + id + name + kind + gitUrl + sourceId + sourceName + sourceKind + } + } +`)