From 6662d20ee84d4ef40bb31dcd81c5b995032dc21d Mon Sep 17 00:00:00 2001 From: Michael Sukkarieh Date: Mon, 28 Jul 2025 18:12:21 -0700 Subject: [PATCH] Search scope refactor (#405) * new demo card ui * rename search context to search scope * rename everything to use search scope * add changelog entry --- CHANGELOG.md | 1 + .../chat/[id]/components/chatThreadPanel.tsx | 59 +--- .../[domain]/chat/components/newChatPanel.tsx | 15 +- .../components/homepage/agenticSearch.tsx | 17 +- .../homepage/askSourcebotDemoCards.tsx | 276 +++++++----------- .../web/src/app/api/(server)/chat/route.ts | 55 ++-- .../web/src/components/atMentionInfoCard.tsx | 15 + .../src/components/searchScopeInfoCard.tsx | 41 +++ packages/web/src/features/chat/agent.ts | 14 +- .../chat/components/chatBox/chatBox.tsx | 27 +- .../components/chatBox/chatBoxToolbar.tsx | 44 +-- ...xtSelector.tsx => searchScopeSelector.tsx} | 133 +++------ .../chat/components/chatThread/chatThread.tsx | 36 +-- .../components/chatThread/detailsCard.tsx | 26 +- .../chat/components/searchScopeIcon.tsx | 32 ++ packages/web/src/features/chat/types.ts | 31 +- .../features/chat/useCreateNewChatThread.ts | 14 +- packages/web/src/features/chat/utils.ts | 6 +- packages/web/src/types.ts | 19 +- 19 files changed, 396 insertions(+), 465 deletions(-) create mode 100644 packages/web/src/components/atMentionInfoCard.tsx create mode 100644 packages/web/src/components/searchScopeInfoCard.tsx rename packages/web/src/features/chat/components/chatBox/{contextSelector.tsx => searchScopeSelector.tsx} (64%) create mode 100644 packages/web/src/features/chat/components/searchScopeIcon.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index ba12ac52..3397298e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Add search context to ask sourcebot context selector. [#397](https://github.com/sourcebot-dev/sourcebot/pull/397) - Add ability to include/exclude connection in search context. [#399](https://github.com/sourcebot-dev/sourcebot/pull/399) +- Search context refactor to search scope and demo card UI changes. [#405](https://github.com/sourcebot-dev/sourcebot/pull/405) ### Fixed - Fixed multiple writes race condition on config file watcher. [#398](https://github.com/sourcebot-dev/sourcebot/pull/398) diff --git a/packages/web/src/app/[domain]/chat/[id]/components/chatThreadPanel.tsx b/packages/web/src/app/[domain]/chat/[id]/components/chatThreadPanel.tsx index 48824d76..92f3ab6e 100644 --- a/packages/web/src/app/[domain]/chat/[id]/components/chatThreadPanel.tsx +++ b/packages/web/src/app/[domain]/chat/[id]/components/chatThreadPanel.tsx @@ -2,13 +2,12 @@ import { ResizablePanel } from '@/components/ui/resizable'; import { ChatThread } from '@/features/chat/components/chatThread'; -import { LanguageModelInfo, SBChatMessage, SET_CHAT_STATE_QUERY_PARAM, SetChatStatePayload } from '@/features/chat/types'; +import { LanguageModelInfo, SBChatMessage, SearchScope, SET_CHAT_STATE_QUERY_PARAM, SetChatStatePayload } from '@/features/chat/types'; import { RepositoryQuery, SearchContextQuery } from '@/lib/types'; import { CreateUIMessage } from 'ai'; import { useRouter, useSearchParams } from 'next/navigation'; import { useEffect, useState } from 'react'; import { useChatId } from '../../useChatId'; -import { ContextItem } from '@/features/chat/components/chatBox/contextSelector'; interface ChatThreadPanelProps { languageModels: LanguageModelInfo[]; @@ -33,33 +32,12 @@ export const ChatThreadPanel = ({ const router = useRouter(); const searchParams = useSearchParams(); const [inputMessage, setInputMessage] = useState | undefined>(undefined); - + // Use the last user's last message to determine what repos and contexts we should select by default. const lastUserMessage = messages.findLast((message) => message.role === "user"); - const defaultSelectedRepos = lastUserMessage?.metadata?.selectedRepos ?? []; - const defaultSelectedContexts = lastUserMessage?.metadata?.selectedContexts ?? []; + const defaultSelectedSearchScopes = lastUserMessage?.metadata?.selectedSearchScopes ?? []; + const [selectedSearchScopes, setSelectedSearchScopes] = useState(defaultSelectedSearchScopes); - const [selectedItems, setSelectedItems] = useState([ - ...defaultSelectedRepos.map(repoName => { - const repoInfo = repos.find(r => r.repoName === repoName); - return { - type: 'repo' as const, - value: repoName, - name: repoInfo?.repoDisplayName || repoName.split('/').pop() || repoName, - codeHostType: repoInfo?.codeHostType || '' - }; - }), - ...defaultSelectedContexts.map(contextName => { - const context = searchContexts.find(c => c.name === contextName); - return { - type: 'context' as const, - value: contextName, - name: contextName, - repoCount: context?.repoNames.length || 0 - }; - }) - ]); - useEffect(() => { const setChatState = searchParams.get(SET_CHAT_STATE_QUERY_PARAM); if (!setChatState) { @@ -67,28 +45,9 @@ export const ChatThreadPanel = ({ } try { - const { inputMessage, selectedRepos, selectedContexts } = JSON.parse(setChatState) as SetChatStatePayload; + const { inputMessage, selectedSearchScopes } = JSON.parse(setChatState) as SetChatStatePayload; setInputMessage(inputMessage); - setSelectedItems([ - ...selectedRepos.map(repoName => { - const repoInfo = repos.find(r => r.repoName === repoName); - return { - type: 'repo' as const, - value: repoName, - name: repoInfo?.repoDisplayName || repoName.split('/').pop() || repoName, - codeHostType: repoInfo?.codeHostType || '' - }; - }), - ...selectedContexts.map(contextName => { - const context = searchContexts.find(c => c.name === contextName); - return { - type: 'context' as const, - value: contextName, - name: contextName, - repoCount: context?.repoNames.length || 0 - }; - }) - ]); + setSelectedSearchScopes(selectedSearchScopes); } catch { console.error('Invalid message in URL'); } @@ -97,7 +56,7 @@ export const ChatThreadPanel = ({ const newSearchParams = new URLSearchParams(searchParams.toString()); newSearchParams.delete(SET_CHAT_STATE_QUERY_PARAM); router.replace(`?${newSearchParams.toString()}`, { scroll: false }); - }, [searchParams, router, repos, searchContexts]); + }, [searchParams, router]); return ( diff --git a/packages/web/src/app/[domain]/chat/components/newChatPanel.tsx b/packages/web/src/app/[domain]/chat/components/newChatPanel.tsx index a2addcd4..91ae3f96 100644 --- a/packages/web/src/app/[domain]/chat/components/newChatPanel.tsx +++ b/packages/web/src/app/[domain]/chat/components/newChatPanel.tsx @@ -5,12 +5,11 @@ import { ChatBox } from "@/features/chat/components/chatBox"; import { ChatBoxToolbar } from "@/features/chat/components/chatBox/chatBoxToolbar"; import { CustomSlateEditor } from "@/features/chat/customSlateEditor"; import { useCreateNewChatThread } from "@/features/chat/useCreateNewChatThread"; -import { LanguageModelInfo } from "@/features/chat/types"; +import { LanguageModelInfo, SearchScope } from "@/features/chat/types"; import { RepositoryQuery, SearchContextQuery } from "@/lib/types"; import { useCallback, useState } from "react"; import { Descendant } from "slate"; import { useLocalStorage } from "usehooks-ts"; -import { ContextItem } from "@/features/chat/components/chatBox/contextSelector"; interface NewChatPanelProps { languageModels: LanguageModelInfo[]; @@ -25,13 +24,13 @@ export const NewChatPanel = ({ searchContexts, order, }: NewChatPanelProps) => { - const [selectedItems, setSelectedItems] = useLocalStorage("selectedContextItems", [], { initializeWithValue: false }); + const [selectedSearchScopes, setSelectedSearchScopes] = useLocalStorage("selectedSearchScopes", [], { initializeWithValue: false }); const { createNewChatThread, isLoading } = useCreateNewChatThread(); const [isContextSelectorOpen, setIsContextSelectorOpen] = useState(false); const onSubmit = useCallback((children: Descendant[]) => { - createNewChatThread(children, selectedItems); - }, [createNewChatThread, selectedItems]); + createNewChatThread(children, selectedSearchScopes); + }, [createNewChatThread, selectedSearchScopes]); return ( @@ -50,7 +49,7 @@ export const NewChatPanel = ({ preferredSuggestionsBoxPlacement="bottom-start" isRedirecting={isLoading} languageModels={languageModels} - selectedItems={selectedItems} + selectedSearchScopes={selectedSearchScopes} searchContexts={searchContexts} onContextSelectorOpenChanged={setIsContextSelectorOpen} /> @@ -59,8 +58,8 @@ export const NewChatPanel = ({ languageModels={languageModels} repos={repos} searchContexts={searchContexts} - selectedItems={selectedItems} - onSelectedItemsChange={setSelectedItems} + selectedSearchScopes={selectedSearchScopes} + onSelectedSearchScopesChange={setSelectedSearchScopes} isContextSelectorOpen={isContextSelectorOpen} onContextSelectorOpenChanged={setIsContextSelectorOpen} /> diff --git a/packages/web/src/app/[domain]/components/homepage/agenticSearch.tsx b/packages/web/src/app/[domain]/components/homepage/agenticSearch.tsx index c8bd472f..259c738b 100644 --- a/packages/web/src/app/[domain]/components/homepage/agenticSearch.tsx +++ b/packages/web/src/app/[domain]/components/homepage/agenticSearch.tsx @@ -3,13 +3,12 @@ import { Separator } from "@/components/ui/separator"; import { ChatBox } from "@/features/chat/components/chatBox"; import { ChatBoxToolbar } from "@/features/chat/components/chatBox/chatBoxToolbar"; -import { LanguageModelInfo } from "@/features/chat/types"; +import { LanguageModelInfo, SearchScope } from "@/features/chat/types"; import { useCreateNewChatThread } from "@/features/chat/useCreateNewChatThread"; import { RepositoryQuery, SearchContextQuery } from "@/lib/types"; import { useState } from "react"; import { SearchModeSelector, SearchModeSelectorProps } from "./toolbar"; import { useLocalStorage } from "usehooks-ts"; -import { ContextItem } from "@/features/chat/components/chatBox/contextSelector"; import { DemoExamples } from "@/types"; import { AskSourcebotDemoCards } from "./askSourcebotDemoCards"; @@ -34,7 +33,7 @@ export const AgenticSearch = ({ demoExamples, }: AgenticSearchProps) => { const { createNewChatThread, isLoading } = useCreateNewChatThread(); - const [selectedItems, setSelectedItems] = useLocalStorage("selectedContextItems", [], { initializeWithValue: false }); + const [selectedSearchScopes, setSelectedSearchScopes] = useLocalStorage("selectedSearchScopes", [], { initializeWithValue: false }); const [isContextSelectorOpen, setIsContextSelectorOpen] = useState(false); return ( @@ -42,12 +41,12 @@ export const AgenticSearch = ({
{ - createNewChatThread(children, selectedItems); + createNewChatThread(children, selectedSearchScopes); }} className="min-h-[50px]" isRedirecting={isLoading} languageModels={languageModels} - selectedItems={selectedItems} + selectedSearchScopes={selectedSearchScopes} searchContexts={searchContexts} onContextSelectorOpenChanged={setIsContextSelectorOpen} /> @@ -58,8 +57,8 @@ export const AgenticSearch = ({ languageModels={languageModels} repos={repos} searchContexts={searchContexts} - selectedItems={selectedItems} - onSelectedItemsChange={setSelectedItems} + selectedSearchScopes={selectedSearchScopes} + onSelectedSearchScopesChange={setSelectedSearchScopes} isContextSelectorOpen={isContextSelectorOpen} onContextSelectorOpenChanged={setIsContextSelectorOpen} /> @@ -74,10 +73,6 @@ export const AgenticSearch = ({ {demoExamples && ( )}
diff --git a/packages/web/src/app/[domain]/components/homepage/askSourcebotDemoCards.tsx b/packages/web/src/app/[domain]/components/homepage/askSourcebotDemoCards.tsx index e8bb264c..61f64a14 100644 --- a/packages/web/src/app/[domain]/components/homepage/askSourcebotDemoCards.tsx +++ b/packages/web/src/app/[domain]/components/homepage/askSourcebotDemoCards.tsx @@ -1,32 +1,25 @@ 'use client'; +import { useState } from "react"; import Image from "next/image"; -import { Search, LibraryBigIcon, Code, Layers } from "lucide-react"; +import { Search, LibraryBigIcon, Code, Info } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Card } from "@/components/ui/card"; import { CardContent } from "@/components/ui/card"; -import { ContextItem, RepoContextItem, SearchContextItem } from "@/features/chat/components/chatBox/contextSelector"; -import { DemoExamples, DemoSearchExample, DemoSearchContextExample, DemoSearchContext } from "@/types"; +import { DemoExamples, DemoSearchExample, DemoSearchScope } from "@/types"; import { cn, getCodeHostIcon } from "@/lib/utils"; -import { RepositoryQuery, SearchContextQuery } from "@/lib/types"; import useCaptureEvent from "@/hooks/useCaptureEvent"; +import { SearchScopeInfoCard } from "@/components/searchScopeInfoCard"; interface AskSourcebotDemoCardsProps { demoExamples: DemoExamples; - selectedItems: ContextItem[]; - setSelectedItems: (items: ContextItem[]) => void; - searchContexts: SearchContextQuery[]; - repos: RepositoryQuery[]; } export const AskSourcebotDemoCards = ({ demoExamples, - selectedItems, - setSelectedItems, - searchContexts, - repos, }: AskSourcebotDemoCardsProps) => { const captureEvent = useCaptureEvent(); + const [selectedFilterSearchScope, setSelectedFilterSearchScope] = useState(null); const handleExampleClick = (example: DemoSearchExample) => { captureEvent('wa_demo_search_example_card_pressed', { @@ -39,87 +32,37 @@ export const AskSourcebotDemoCards = ({ } } - const getContextIcon = (context: DemoSearchContext, size: number = 20) => { + const getSearchScopeIcon = (searchScope: DemoSearchScope, size: number = 20, isSelected: boolean = false) => { const sizeClass = size === 12 ? "h-3 w-3" : "h-5 w-5"; + const colorClass = isSelected ? "text-primary-foreground" : "text-muted-foreground"; - if (context.type === "set") { - return ; + if (searchScope.type === "reposet") { + return ; } - if (context.codeHostType) { - const codeHostIcon = getCodeHostIcon(context.codeHostType); + if (searchScope.codeHostType) { + const codeHostIcon = getCodeHostIcon(searchScope.codeHostType); if (codeHostIcon) { + // When selected, icons need to match the inverted badge colors + // In light mode selected: light icon on dark bg (invert) + // In dark mode selected: dark icon on light bg (no invert, override dark:invert) + const selectedIconClass = isSelected + ? "invert dark:invert-0" + : codeHostIcon.className; + return ( {`${context.codeHostType} ); } } - return ; - } - - const handleContextClick = (demoSearchContexts: DemoSearchContext[], contextExample: DemoSearchContextExample) => { - const context = demoSearchContexts.find((context) => context.id === contextExample.searchContext) - if (!context) { - console.error(`Search context ${contextExample.searchContext} not found on handleContextClick`); - return; - } - - captureEvent('wa_demo_search_context_card_pressed', { - contextType: context.type, - contextName: context.value, - contextDisplayName: context.displayName, - }); - - const isDemoMode = process.env.NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT === "demo"; - const isSelected = selectedItems.some((item) => item.value === context.value); - if (isSelected) { - setSelectedItems(selectedItems.filter((item) => item.value !== context.value)); - return; - } - - const getNewSelectedItem = (): ContextItem | null => { - if (context.type === "set") { - const searchContext = searchContexts.find((item) => item.name === context.value); - if (!searchContext) { - console.error(`Search context ${context.value} not found on handleContextClick`); - return null; - } - - return { - type: 'context', - value: context.value, - name: context.displayName, - repoCount: searchContext.repoNames.length - } as SearchContextItem; - } else { - const repo = repos.find((repo) => repo.repoName === context.value); - if (!repo) { - console.error(`Repo ${context.value} not found on handleContextClick`); - return null; - } - - return { - type: 'repo', - value: context.value, - name: context.displayName, - codeHostType: repo.codeHostType - } as RepoContextItem; - } - } - - const newSelectedItem = getNewSelectedItem(); - if (newSelectedItem) { - setSelectedItems(isDemoMode ? [newSelectedItem] : [...selectedItems, newSelectedItem]); - } else { - console.error(`No new selected item found on handleContextClick`); - } + return ; } return ( @@ -139,110 +82,91 @@ export const AskSourcebotDemoCards = ({

)} -
- {/* Search Context Row */} -
-
-
- -

Search Contexts

-
-

Select the context you want to ask questions about

-
-
- {demoExamples.searchContextExamples.map((contextExample) => { - const context = demoExamples.searchContexts.find((context) => context.id === contextExample.searchContext) - if (!context) { - console.error(`Search context ${contextExample.searchContext} not found on handleContextClick`); - return null; - } - - const isSelected = selectedItems.some( - (selected) => (selected.type === 'context' && selected.value === context.value) || - (selected.type === 'repo' && selected.value === context.value) - ); - - const searchContext = searchContexts.find((item) => item.name === context.value); - const numRepos = searchContext ? searchContext.repoNames.length : undefined; - return ( - handleContextClick(demoExamples.searchContexts, contextExample)} - > - -
-
- {getContextIcon(context)} -
-
-
-

- {context.displayName} -

- {numRepos && ( - - {numRepos} repos - - )} -
-

{contextExample.description}

-
-
-
-
- ) - })} -
-
- +
{/* Example Searches Row */}
-
-
- -

Community Ask Results

+
+
+ +

Community Ask Results

-

Check out these featured ask results from the community

+ + {/* Search Scope Filter */} +
+
+
+ +
+ +
+
+
+ Search Scope: +
+ { + setSelectedFilterSearchScope(null); + }} + > + All + + {demoExamples.searchScopes.map((searchScope) => ( + { + setSelectedFilterSearchScope(searchScope.id); + }} + > + {getSearchScopeIcon(searchScope, 12, selectedFilterSearchScope === searchScope.id)} + {searchScope.displayName} + + ))} +
+
- {demoExamples.searchExamples.map((example) => { - const searchContexts = demoExamples.searchContexts.filter((context) => example.searchContext.includes(context.id)) - return ( - handleExampleClick(example)} - > - -
-
- {searchContexts.map((context) => ( - - {getContextIcon(context, 12)} - {context.displayName} - - ))} + {demoExamples.searchExamples + .filter((example) => { + if (selectedFilterSearchScope === null) return true; + return example.searchScopes.includes(selectedFilterSearchScope); + }) + .map((example) => { + const searchScopes = demoExamples.searchScopes.filter((searchScope) => example.searchScopes.includes(searchScope.id)) + return ( + handleExampleClick(example)} + > + +
+
+ {searchScopes.map((searchScope) => ( + + {getSearchScopeIcon(searchScope, 12)} + {searchScope.displayName} + + ))} +
+
+

+ {example.title} +

+

+ {example.description} +

+
-
-

- {example.title} -

-

- {example.description} -

-
-
- - - ) - })} + + + ) + })}
diff --git a/packages/web/src/app/api/(server)/chat/route.ts b/packages/web/src/app/api/(server)/chat/route.ts index c52b9e18..54325599 100644 --- a/packages/web/src/app/api/(server)/chat/route.ts +++ b/packages/web/src/app/api/(server)/chat/route.ts @@ -2,7 +2,7 @@ import { sew, withAuth, withOrgMembership } from "@/actions"; import { env } from "@/env.mjs"; import { _getConfiguredLanguageModelsFull, updateChatMessages, updateChatName } from "@/features/chat/actions"; import { createAgentStream } from "@/features/chat/agent"; -import { additionalChatRequestParamsSchema, SBChatMessage } from "@/features/chat/types"; +import { additionalChatRequestParamsSchema, SBChatMessage, SearchScope } from "@/features/chat/types"; import { getAnswerPartFromAssistantMessage } from "@/features/chat/utils"; import { ErrorCode } from "@/lib/errorCodes"; import { notFound, schemaValidationError, serviceErrorResponse } from "@/lib/serviceError"; @@ -64,12 +64,11 @@ export async function POST(req: Request) { return serviceErrorResponse(schemaValidationError(parsed.error)); } - const { messages, id, selectedRepos, selectedContexts, languageModelId } = parsed.data; + const { messages, id, selectedSearchScopes, languageModelId } = parsed.data; const response = await chatHandler({ messages, id, - selectedRepos, - selectedContexts, + selectedSearchScopes, languageModelId, }, domain); @@ -93,12 +92,11 @@ const mergeStreamAsync = async (stream: StreamTextResult, writer: UIMe interface ChatHandlerProps { messages: SBChatMessage[]; id: string; - selectedRepos: string[]; - selectedContexts?: string[]; + selectedSearchScopes: SearchScope[]; languageModelId: string; } -const chatHandler = ({ messages, id, selectedRepos, selectedContexts, languageModelId }: ChatHandlerProps, domain: string) => sew(async () => +const chatHandler = ({ messages, id, selectedSearchScopes, languageModelId }: ChatHandlerProps, domain: string) => sew(async () => withAuth((userId) => withOrgMembership(userId, domain, async ({ org }) => { const chat = await prisma.chat.findUnique({ @@ -188,26 +186,30 @@ const chatHandler = ({ messages, id, selectedRepos, selectedContexts, languageMo const startTime = new Date(); - // Expand search contexts to repos - let expandedRepos = [...selectedRepos]; - if (selectedContexts && selectedContexts.length > 0) { - const searchContexts = await prisma.searchContext.findMany({ - where: { - orgId: org.id, - name: { in: selectedContexts } - }, - include: { - repos: true + const expandedReposArrays = await Promise.all(selectedSearchScopes.map(async (scope) => { + if (scope.type === 'repo') { + return [scope.value]; + } + + if (scope.type === 'reposet') { + const reposet = await prisma.searchContext.findFirst({ + where: { + orgId: org.id, + name: scope.value + }, + include: { + repos: true + } + }); + + if (reposet) { + return reposet.repos.map(repo => repo.name); } - }); + } - const contextRepos = searchContexts.flatMap(context => - context.repos.map(repo => repo.name) - ); - - // Combine and deduplicate repos - expandedRepos = Array.from(new Set([...selectedRepos, ...contextRepos])); - } + return []; + })); + const expandedRepos = expandedReposArrays.flat(); const researchStream = await createAgentStream({ model, @@ -215,7 +217,7 @@ const chatHandler = ({ messages, id, selectedRepos, selectedContexts, languageMo headers, inputMessages: messageHistory, inputSources: sources, - selectedRepos: expandedRepos, + searchScopeRepoNames: expandedRepos, onWriteSource: (source) => { writer.write({ type: 'data-source', @@ -241,6 +243,7 @@ const chatHandler = ({ messages, id, selectedRepos, selectedContexts, languageMo totalOutputTokens: totalUsage.outputTokens, totalResponseTimeMs: new Date().getTime() - startTime.getTime(), modelName: languageModelConfig.displayName ?? languageModelConfig.model, + selectedSearchScopes, traceId, } }) diff --git a/packages/web/src/components/atMentionInfoCard.tsx b/packages/web/src/components/atMentionInfoCard.tsx new file mode 100644 index 00000000..74424074 --- /dev/null +++ b/packages/web/src/components/atMentionInfoCard.tsx @@ -0,0 +1,15 @@ +import { AtSignIcon } from "lucide-react"; + +export const AtMentionInfoCard = () => { + return ( +
+
+ +

Mention

+
+
+ When asking Sourcebot a question, you can @ mention files to include them in the context of the search. +
+
+ ); +}; \ No newline at end of file diff --git a/packages/web/src/components/searchScopeInfoCard.tsx b/packages/web/src/components/searchScopeInfoCard.tsx new file mode 100644 index 00000000..633dcbbb --- /dev/null +++ b/packages/web/src/components/searchScopeInfoCard.tsx @@ -0,0 +1,41 @@ +import Image from "next/image"; +import { LibraryBigIcon, Code, ScanSearchIcon } from "lucide-react"; +import { cn, getCodeHostIcon } from "@/lib/utils"; + +export const SearchScopeInfoCard = () => { + return ( +
+
+ +

Search Scope

+
+
+ When asking Sourcebot a question, you can select one or more scopes to constrain the search. + There are two different types of search scopes: +
+
+ {(() => { + const githubIcon = getCodeHostIcon("github"); + return githubIcon ? ( + GitHub icon + ) : ( + + ); + })()} + Repository: A single repository, indicated by the code host icon. +
+
+ + Reposet: A set of repositories, indicated by the library icon. +
+
+
+
+ ); +}; \ No newline at end of file diff --git a/packages/web/src/features/chat/agent.ts b/packages/web/src/features/chat/agent.ts index 7f3ef93d..d443b99b 100644 --- a/packages/web/src/features/chat/agent.ts +++ b/packages/web/src/features/chat/agent.ts @@ -16,7 +16,7 @@ interface AgentOptions { model: LanguageModel; providerOptions?: ProviderOptions; headers?: Record; - selectedRepos: string[]; + searchScopeRepoNames: string[]; inputMessages: ModelMessage[]; inputSources: Source[]; onWriteSource: (source: Source) => void; @@ -35,12 +35,12 @@ export const createAgentStream = async ({ headers, inputMessages, inputSources, - selectedRepos, + searchScopeRepoNames, onWriteSource, traceId, }: AgentOptions) => { const baseSystemPrompt = createBaseSystemPrompt({ - selectedRepos, + searchScopeRepoNames, }); const stream = streamText({ @@ -50,7 +50,7 @@ export const createAgentStream = async ({ system: baseSystemPrompt, messages: inputMessages, tools: { - [toolNames.searchCode]: createCodeSearchTool(selectedRepos), + [toolNames.searchCode]: createCodeSearchTool(searchScopeRepoNames), [toolNames.readFiles]: readFilesTool, [toolNames.findSymbolReferences]: findSymbolReferencesTool, [toolNames.findSymbolDefinitions]: findSymbolDefinitionsTool, @@ -150,11 +150,11 @@ export const createAgentStream = async ({ } interface BaseSystemPromptOptions { - selectedRepos: string[]; + searchScopeRepoNames: string[]; } export const createBaseSystemPrompt = ({ - selectedRepos, + searchScopeRepoNames, }: BaseSystemPromptOptions) => { return ` You are a powerful agentic AI code assistant built into Sourcebot, the world's best code-intelligence platform. Your job is to help developers understand and navigate their large codebases. @@ -176,7 +176,7 @@ Your workflow has two distinct phases: The user has selected the following repositories for analysis: -${selectedRepos.map(repo => `- ${repo}`).join('\n')} +${searchScopeRepoNames.map(repo => `- ${repo}`).join('\n')} diff --git a/packages/web/src/features/chat/components/chatBox/chatBox.tsx b/packages/web/src/features/chat/components/chatBox/chatBox.tsx index c33cbdf1..f5cfc974 100644 --- a/packages/web/src/features/chat/components/chatBox/chatBox.tsx +++ b/packages/web/src/features/chat/components/chatBox/chatBox.tsx @@ -3,7 +3,7 @@ import { VscodeFileIcon } from "@/app/components/vscodeFileIcon"; import { Button } from "@/components/ui/button"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; -import { CustomEditor, LanguageModelInfo, MentionElement, RenderElementPropsFor } from "@/features/chat/types"; +import { CustomEditor, LanguageModelInfo, MentionElement, RenderElementPropsFor, SearchScope } from "@/features/chat/types"; import { insertMention, slateContentToString } from "@/features/chat/utils"; import { cn, IS_MAC } from "@/lib/utils"; import { computePosition, flip, offset, shift, VirtualElement } from "@floating-ui/react"; @@ -18,7 +18,6 @@ import { Suggestion } from "./types"; import { useSuggestionModeAndQuery } from "./useSuggestionModeAndQuery"; import { useSuggestionsData } from "./useSuggestionsData"; import { useToast } from "@/components/hooks/use-toast"; -import { ContextItem } from "./contextSelector"; import { SearchContextQuery } from "@/lib/types"; interface ChatBoxProps { @@ -29,7 +28,7 @@ interface ChatBoxProps { isRedirecting?: boolean; isGenerating?: boolean; languageModels: LanguageModelInfo[]; - selectedItems: ContextItem[]; + selectedSearchScopes: SearchScope[]; searchContexts: SearchContextQuery[]; onContextSelectorOpenChanged: (isOpen: boolean) => void; } @@ -42,7 +41,7 @@ export const ChatBox = ({ isRedirecting, isGenerating, languageModels, - selectedItems, + selectedSearchScopes, searchContexts, onContextSelectorOpenChanged, }: ChatBoxProps) => { @@ -53,15 +52,15 @@ export const ChatBox = ({ const { suggestions, isLoading } = useSuggestionsData({ suggestionMode, suggestionQuery, - selectedRepos: selectedItems.map((item) => { + selectedRepos: selectedSearchScopes.map((item) => { if (item.type === 'repo') { return [item.value]; } - if (item.type === 'context') { - const context = searchContexts.find((context) => context.name === item.value); - if (context) { - return context.repoNames; + if (item.type === 'reposet') { + const reposet = searchContexts.find((reposet) => reposet.name === item.value); + if (reposet) { + return reposet.repoNames; } } @@ -130,7 +129,7 @@ export const ChatBox = ({ } } - if (selectedItems.length === 0) { + if (selectedSearchScopes.length === 0) { return { isSubmitDisabled: true, isSubmitDisabledReason: "no-repos-selected", @@ -154,7 +153,7 @@ export const ChatBox = ({ editor.children, isRedirecting, isGenerating, - selectedItems.length, + selectedSearchScopes.length, selectedLanguageModel, ]) @@ -162,7 +161,7 @@ export const ChatBox = ({ if (isSubmitDisabled) { if (isSubmitDisabledReason === "no-repos-selected") { toast({ - description: "⚠️ You must select at least one search context", + description: "⚠️ You must select at least one search scope", variant: "destructive", }); onContextSelectorOpenChanged(true); @@ -284,7 +283,7 @@ export const ChatBox = ({ >
- You must select at least one search context + You must select at least one search scope
)} diff --git a/packages/web/src/features/chat/components/chatBox/chatBoxToolbar.tsx b/packages/web/src/features/chat/components/chatBox/chatBoxToolbar.tsx index 8744e9ac..eba67729 100644 --- a/packages/web/src/features/chat/components/chatBox/chatBoxToolbar.tsx +++ b/packages/web/src/features/chat/components/chatBox/chatBoxToolbar.tsx @@ -1,25 +1,25 @@ 'use client'; -import { KeyboardShortcutHint } from "@/app/components/keyboardShortcutHint"; import { Button } from "@/components/ui/button"; import { Separator } from "@/components/ui/separator"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; -import { LanguageModelInfo } from "@/features/chat/types"; +import { LanguageModelInfo, SearchScope } from "@/features/chat/types"; import { RepositoryQuery, SearchContextQuery } from "@/lib/types"; import { AtSignIcon } from "lucide-react"; import { useCallback } from "react"; -import { useHotkeys } from "react-hotkeys-hook"; import { ReactEditor, useSlate } from "slate-react"; import { useSelectedLanguageModel } from "../../useSelectedLanguageModel"; import { LanguageModelSelector } from "./languageModelSelector"; -import { ContextSelector, type ContextItem } from "./contextSelector"; +import { SearchScopeSelector } from "./searchScopeSelector"; +import { SearchScopeInfoCard } from "@/components/searchScopeInfoCard"; +import { AtMentionInfoCard } from "@/components/atMentionInfoCard"; export interface ChatBoxToolbarProps { languageModels: LanguageModelInfo[]; repos: RepositoryQuery[]; searchContexts: SearchContextQuery[]; - selectedItems: ContextItem[]; - onSelectedItemsChange: (items: ContextItem[]) => void; + selectedSearchScopes: SearchScope[]; + onSelectedSearchScopesChange: (items: SearchScope[]) => void; isContextSelectorOpen: boolean; onContextSelectorOpenChanged: (isOpen: boolean) => void; } @@ -28,8 +28,8 @@ export const ChatBoxToolbar = ({ languageModels, repos, searchContexts, - selectedItems, - onSelectedItemsChange, + selectedSearchScopes, + onSelectedSearchScopesChange, isContextSelectorOpen, onContextSelectorOpenChanged, }: ChatBoxToolbarProps) => { @@ -40,15 +40,6 @@ export const ChatBoxToolbar = ({ ReactEditor.focus(editor); }, [editor]); - useHotkeys("alt+mod+p", (e) => { - e.preventDefault(); - onAddContext(); - }, { - enableOnFormTags: true, - enableOnContentEditable: true, - description: "Add context", - }); - const { selectedLanguageModel, setSelectedLanguageModel } = useSelectedLanguageModel({ initialLanguageModel: languageModels.length > 0 ? languageModels[0] : undefined, }); @@ -66,30 +57,25 @@ export const ChatBoxToolbar = ({ - - - - Add context + + - - - Search contexts and repositories to scope conversation to. + + {languageModels.length > 0 && ( diff --git a/packages/web/src/features/chat/components/chatBox/contextSelector.tsx b/packages/web/src/features/chat/components/chatBox/searchScopeSelector.tsx similarity index 64% rename from packages/web/src/features/chat/components/chatBox/contextSelector.tsx rename to packages/web/src/features/chat/components/chatBox/searchScopeSelector.tsx index 103b0195..a1c8f595 100644 --- a/packages/web/src/features/chat/components/chatBox/contextSelector.tsx +++ b/packages/web/src/features/chat/components/chatBox/searchScopeSelector.tsx @@ -4,13 +4,10 @@ import * as React from "react"; import { CheckIcon, ChevronDown, - FolderIcon, - LayersIcon, - LibraryBigIcon, + ScanSearchIcon, } from "lucide-react"; -import Image from "next/image"; -import { cn, getCodeHostIcon } from "@/lib/utils"; +import { cn } from "@/lib/utils"; import { RepositoryQuery, SearchContextQuery } from "@/lib/types"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; @@ -28,44 +25,30 @@ import { CommandList, CommandSeparator, } from "@/components/ui/command"; +import { RepoSetSearchScope, RepoSearchScope, SearchScope } from "../../types"; +import { SearchScopeIcon } from "../searchScopeIcon"; -export type RepoContextItem = { - type: 'repo'; - value: string; - name: string; - codeHostType: string; -} - -export type SearchContextItem = { - type: 'context'; - value: string; - name: string; - repoCount: number; -} - -export type ContextItem = RepoContextItem | SearchContextItem; - -interface ContextSelectorProps extends React.ButtonHTMLAttributes { +interface SearchScopeSelectorProps extends React.ButtonHTMLAttributes { repos: RepositoryQuery[]; searchContexts: SearchContextQuery[]; - selectedItems: ContextItem[]; - onSelectedItemsChange: (items: ContextItem[]) => void; + selectedSearchScopes: SearchScope[]; + onSelectedSearchScopesChange: (items: SearchScope[]) => void; className?: string; isOpen: boolean; onOpenChanged: (isOpen: boolean) => void; } -export const ContextSelector = React.forwardRef< +export const SearchScopeSelector = React.forwardRef< HTMLButtonElement, - ContextSelectorProps + SearchScopeSelectorProps >( ( { repos, searchContexts, - onSelectedItemsChange, className, - selectedItems, + selectedSearchScopes, + onSelectedSearchScopesChange, isOpen, onOpenChanged, ...props @@ -81,72 +64,62 @@ export const ContextSelector = React.forwardRef< if (event.key === "Enter") { onOpenChanged(true); } else if (event.key === "Backspace" && !event.currentTarget.value) { - const newSelectedItems = [...selectedItems]; + const newSelectedItems = [...selectedSearchScopes]; newSelectedItems.pop(); - onSelectedItemsChange(newSelectedItems); + onSelectedSearchScopesChange(newSelectedItems); } }; - const toggleItem = (item: ContextItem) => { + const toggleItem = (item: SearchScope) => { // Store current scroll position before state update if (scrollContainerRef.current) { scrollPosition.current = scrollContainerRef.current.scrollTop; } - const isSelected = selectedItems.some( + const isSelected = selectedSearchScopes.some( (selected) => selected.type === item.type && selected.value === item.value ); - const isDemoMode = process.env.NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT === "demo"; - - let newSelectedItems: ContextItem[]; - if (isSelected) { - newSelectedItems = selectedItems.filter( + const newSelectedItems = isSelected ? + selectedSearchScopes.filter( (selected) => !(selected.type === item.type && selected.value === item.value) - ); - } else { - // Limit selected context to 1 in demo mode - if (isDemoMode) { - newSelectedItems = [item]; - } else { - newSelectedItems = [...selectedItems, item]; - } - } + ) : + [...selectedSearchScopes, item]; - onSelectedItemsChange(newSelectedItems); + onSelectedSearchScopesChange(newSelectedItems); }; const handleClear = () => { - onSelectedItemsChange([]); + onSelectedSearchScopesChange([]); }; const handleTogglePopover = () => { onOpenChanged(!isOpen); }; - const allItems = React.useMemo(() => { - const contextItems: ContextItem[] = searchContexts.map(context => ({ - type: 'context' as const, + const allSearchScopeItems = React.useMemo(() => { + const repoSetSearchScopeItems: RepoSetSearchScope[] = searchContexts.map(context => ({ + type: 'reposet' as const, value: context.name, name: context.name, repoCount: context.repoNames.length })); - const repoItems: ContextItem[] = repos.map(repo => ({ + const repoSearchScopeItems: RepoSearchScope[] = repos.map(repo => ({ type: 'repo' as const, value: repo.repoName, name: repo.repoDisplayName || repo.repoName.split('/').pop() || repo.repoName, codeHostType: repo.codeHostType, })); - return [...contextItems, ...repoItems]; + return [...repoSetSearchScopeItems, ...repoSearchScopeItems]; }, [repos, searchContexts]); - const sortedItems = React.useMemo(() => { - return allItems + const sortedSearchScopeItems = React.useMemo(() => { + return allSearchScopeItems .map((item) => ({ item, - isSelected: selectedItems.some( + isSelected: selectedSearchScopes.some( (selected) => selected.type === item.type && selected.value === item.value ) })) @@ -154,19 +127,19 @@ export const ContextSelector = React.forwardRef< // Selected items first if (a.isSelected && !b.isSelected) return -1; if (!a.isSelected && b.isSelected) return 1; - // Then contexts before repos - if (a.item.type === 'context' && b.item.type === 'repo') return -1; - if (a.item.type === 'repo' && b.item.type === 'context') return 1; + // Then reposets before repos + if (a.item.type === 'reposet' && b.item.type === 'repo') return -1; + if (a.item.type === 'repo' && b.item.type === 'reposet') return 1; return 0; }) - }, [allItems, selectedItems]); + }, [allSearchScopeItems, selectedSearchScopes]); // Restore scroll position after re-render React.useEffect(() => { if (scrollContainerRef.current && scrollPosition.current > 0) { scrollContainerRef.current.scrollTop = scrollPosition.current; } - }, [sortedItems]); + }, [sortedSearchScopeItems]); return (
- + { - selectedItems.length === 0 ? `Select context` : - selectedItems.length === 1 ? selectedItems[0].name : - `${selectedItems.length} selected` + selectedSearchScopes.length === 0 ? `Search scopes` : + selectedSearchScopes.length === 1 ? selectedSearchScopes[0].name : + `${selectedSearchScopes.length} selected` } @@ -205,13 +178,13 @@ export const ContextSelector = React.forwardRef< > No results found. - {sortedItems.map(({ item, isSelected }) => { + {sortedSearchScopeItems.map(({ item, isSelected }) => { return (
- {item.type === 'context' ? ( - - ) : ( - // Render code host icon for repos - (() => { - const codeHostIcon = item.codeHostType ? getCodeHostIcon(item.codeHostType) : null; - return codeHostIcon ? ( - {`${item.codeHostType} - ) : ( - - ); - })() - )} +
{item.name} - {item.type === 'context' && ( + {item.type === 'reposet' && ( - {selectedItems.length > 0 && ( + {selectedSearchScopes.length > 0 && ( <> void; + selectedSearchScopes: SearchScope[]; + onSelectedSearchScopesChange: (items: SearchScope[]) => void; isChatReadonly: boolean; } @@ -48,8 +47,8 @@ export const ChatThread = ({ languageModels, repos, searchContexts, - selectedItems, - onSelectedItemsChange, + selectedSearchScopes, + onSelectedSearchScopesChange, isChatReadonly, }: ChatThreadProps) => { const domain = useDomain(); @@ -62,12 +61,6 @@ export const ChatThread = ({ const router = useRouter(); const [isContextSelectorOpen, setIsContextSelectorOpen] = useState(false); - const { selectedRepos, selectedContexts } = useMemo(() => { - const repos = selectedItems.filter(item => item.type === 'repo').map(item => item.value); - const contexts = selectedItems.filter(item => item.type === 'context').map(item => item.value); - return { selectedRepos: repos, selectedContexts: contexts }; - }, [selectedItems]); - // Initial state is from attachments that exist in in the chat history. const [sources, setSources] = useState( initialMessages?.flatMap((message) => @@ -122,12 +115,11 @@ export const ChatThread = ({ _sendMessage(message, { body: { - selectedRepos, - selectedContexts, + selectedSearchScopes, languageModelId: selectedLanguageModel.model, } satisfies AdditionalChatRequestParams, - }); - }, [_sendMessage, selectedLanguageModel, toast, selectedRepos, selectedContexts]); + }); + }, [_sendMessage, selectedLanguageModel, toast, selectedSearchScopes]); const messagePairs = useMessagePairs(messages); @@ -243,13 +235,13 @@ export const ChatThread = ({ const text = slateContentToString(children); const mentions = getAllMentionElements(children); - const message = createUIMessage(text, mentions.map(({ data }) => data), selectedRepos, selectedContexts); + const message = createUIMessage(text, mentions.map(({ data }) => data), selectedSearchScopes); sendMessage(message); setIsAutoScrollEnabled(true); resetEditor(editor); - }, [sendMessage, selectedRepos, selectedContexts]); + }, [sendMessage, selectedSearchScopes]); return ( <> @@ -327,7 +319,7 @@ export const ChatThread = ({ isGenerating={status === "streaming" || status === "submitted"} onStop={stop} languageModels={languageModels} - selectedItems={selectedItems} + selectedSearchScopes={selectedSearchScopes} searchContexts={searchContexts} onContextSelectorOpenChanged={setIsContextSelectorOpen} /> @@ -336,8 +328,8 @@ export const ChatThread = ({ languageModels={languageModels} repos={repos} searchContexts={searchContexts} - selectedItems={selectedItems} - onSelectedItemsChange={onSelectedItemsChange} + selectedSearchScopes={selectedSearchScopes} + onSelectedSearchScopesChange={onSelectedSearchScopesChange} isContextSelectorOpen={isContextSelectorOpen} onContextSelectorOpenChanged={setIsContextSelectorOpen} /> diff --git a/packages/web/src/features/chat/components/chatThread/detailsCard.tsx b/packages/web/src/features/chat/components/chatThread/detailsCard.tsx index 7bfe49a0..ab3b676c 100644 --- a/packages/web/src/features/chat/components/chatThread/detailsCard.tsx +++ b/packages/web/src/features/chat/components/chatThread/detailsCard.tsx @@ -4,14 +4,16 @@ import { Card, CardContent } from '@/components/ui/card'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { Separator } from '@/components/ui/separator'; import { Skeleton } from '@/components/ui/skeleton'; +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { cn } from '@/lib/utils'; -import { Brain, ChevronDown, ChevronRight, Clock, Cpu, InfoIcon, Loader2, Zap } from 'lucide-react'; +import { Brain, ChevronDown, ChevronRight, Clock, Cpu, InfoIcon, Loader2, ScanSearchIcon, Zap } from 'lucide-react'; import { MarkdownRenderer } from './markdownRenderer'; import { FindSymbolDefinitionsToolComponent } from './tools/findSymbolDefinitionsToolComponent'; import { FindSymbolReferencesToolComponent } from './tools/findSymbolReferencesToolComponent'; import { ReadFilesToolComponent } from './tools/readFilesToolComponent'; import { SearchCodeToolComponent } from './tools/searchCodeToolComponent'; import { SBChatMessageMetadata, SBChatMessagePart } from '../../types'; +import { SearchScopeIcon } from '../searchScopeIcon'; interface DetailsCardProps { @@ -61,6 +63,28 @@ export const DetailsCard = ({ {!isStreaming && ( <> + {metadata?.selectedSearchScopes && ( + + +
+ + {metadata.selectedSearchScopes.length} search scope{metadata.selectedSearchScopes.length === 1 ? '' : 's'} +
+
+ +
+
+ {metadata.selectedSearchScopes.map((item) => ( +
+ + {item.name} +
+ ))} +
+
+
+
+ )} {metadata?.modelName && (
diff --git a/packages/web/src/features/chat/components/searchScopeIcon.tsx b/packages/web/src/features/chat/components/searchScopeIcon.tsx new file mode 100644 index 00000000..933471f4 --- /dev/null +++ b/packages/web/src/features/chat/components/searchScopeIcon.tsx @@ -0,0 +1,32 @@ +import { cn, getCodeHostIcon } from "@/lib/utils"; +import { FolderIcon, LibraryBigIcon } from "lucide-react"; +import Image from "next/image"; +import { SearchScope } from "../types"; + +interface SearchScopeIconProps { + searchScope: SearchScope; + className?: string; +} + +export const SearchScopeIcon = ({ searchScope, className = "h-4 w-4" }: SearchScopeIconProps) => { + if (searchScope.type === 'reposet') { + return ; + } else { + // Render code host icon for repos + const codeHostIcon = searchScope.codeHostType ? getCodeHostIcon(searchScope.codeHostType) : null; + if (codeHostIcon) { + const size = className.includes('h-3') ? 12 : 16; + return ( + {`${searchScope.codeHostType} + ); + } else { + return ; + } + } +}; \ No newline at end of file diff --git a/packages/web/src/features/chat/types.ts b/packages/web/src/features/chat/types.ts index b4c6bc8d..18aa55a2 100644 --- a/packages/web/src/features/chat/types.ts +++ b/packages/web/src/features/chat/types.ts @@ -39,6 +39,28 @@ export const referenceSchema = z.discriminatedUnion('type', [ ]); export type Reference = z.infer; +export const repoSearchScopeSchema = z.object({ + type: z.literal('repo'), + value: z.string(), + name: z.string(), + codeHostType: z.string(), +}); +export type RepoSearchScope = z.infer; + +export const repoSetSearchScopeSchema = z.object({ + type: z.literal('reposet'), + value: z.string(), + name: z.string(), + repoCount: z.number(), +}); +export type RepoSetSearchScope = z.infer; + +export const searchScopeSchema = z.discriminatedUnion('type', [ + repoSearchScopeSchema, + repoSetSearchScopeSchema, +]); +export type SearchScope = z.infer; + export const sbChatMessageMetadataSchema = z.object({ modelName: z.string().optional(), totalInputTokens: z.number().optional(), @@ -50,8 +72,7 @@ export const sbChatMessageMetadataSchema = z.object({ timestamp: z.string(), // ISO date string userId: z.string(), })).optional(), - selectedRepos: z.array(z.string()).optional(), - selectedContexts: z.array(z.string()).optional(), + selectedSearchScopes: z.array(searchScopeSchema).optional(), traceId: z.string().optional(), }); @@ -139,8 +160,7 @@ export const SET_CHAT_STATE_QUERY_PARAM = 'setChatState'; export type SetChatStatePayload = { inputMessage: CreateUIMessage; - selectedRepos: string[]; - selectedContexts: string[]; + selectedSearchScopes: SearchScope[]; } @@ -157,7 +177,6 @@ export type LanguageModelInfo = { // Additional request body data that we send along to the chat API. export const additionalChatRequestParamsSchema = z.object({ languageModelId: z.string(), - selectedRepos: z.array(z.string()), - selectedContexts: z.array(z.string()), + selectedSearchScopes: z.array(searchScopeSchema), }); export type AdditionalChatRequestParams = z.infer; \ No newline at end of file diff --git a/packages/web/src/features/chat/useCreateNewChatThread.ts b/packages/web/src/features/chat/useCreateNewChatThread.ts index 54aaf14d..f0de7e1a 100644 --- a/packages/web/src/features/chat/useCreateNewChatThread.ts +++ b/packages/web/src/features/chat/useCreateNewChatThread.ts @@ -10,8 +10,7 @@ import { useRouter } from "next/navigation"; import { createChat } from "./actions"; import { isServiceError } from "@/lib/utils"; import { createPathWithQueryParams } from "@/lib/utils"; -import { SET_CHAT_STATE_QUERY_PARAM, SetChatStatePayload } from "./types"; -import { ContextItem } from "./components/chatBox/contextSelector"; +import { SearchScope, SET_CHAT_STATE_QUERY_PARAM, SetChatStatePayload } from "./types"; export const useCreateNewChatThread = () => { const domain = useDomain(); @@ -19,15 +18,11 @@ export const useCreateNewChatThread = () => { const { toast } = useToast(); const router = useRouter(); - const createNewChatThread = useCallback(async (children: Descendant[], selectedItems: ContextItem[]) => { + const createNewChatThread = useCallback(async (children: Descendant[], selectedSearchScopes: SearchScope[]) => { const text = slateContentToString(children); const mentions = getAllMentionElements(children); - // Extract repos and contexts from selectedItems - const selectedRepos = selectedItems.filter(item => item.type === 'repo').map(item => item.value); - const selectedContexts = selectedItems.filter(item => item.type === 'context').map(item => item.value); - - const inputMessage = createUIMessage(text, mentions.map((mention) => mention.data), selectedRepos, selectedContexts); + const inputMessage = createUIMessage(text, mentions.map((mention) => mention.data), selectedSearchScopes); setIsLoading(true); const response = await createChat(domain); @@ -42,8 +37,7 @@ export const useCreateNewChatThread = () => { const url = createPathWithQueryParams(`/${domain}/chat/${response.id}`, [SET_CHAT_STATE_QUERY_PARAM, JSON.stringify({ inputMessage, - selectedRepos, - selectedContexts, + selectedSearchScopes, } satisfies SetChatStatePayload)], ); diff --git a/packages/web/src/features/chat/utils.ts b/packages/web/src/features/chat/utils.ts index e2495664..d84835ef 100644 --- a/packages/web/src/features/chat/utils.ts +++ b/packages/web/src/features/chat/utils.ts @@ -12,6 +12,7 @@ import { SBChatMessage, SBChatMessagePart, SBChatMessageToolTypes, + SearchScope, Source, } from "./types" @@ -172,7 +173,7 @@ export const addLineNumbers = (source: string, lineOffset = 1) => { return source.split('\n').map((line, index) => `${index + lineOffset}:${line}`).join('\n'); } -export const createUIMessage = (text: string, mentions: MentionData[], selectedRepos: string[], selectedContexts: string[]): CreateUIMessage => { +export const createUIMessage = (text: string, mentions: MentionData[], selectedSearchScopes: SearchScope[]): CreateUIMessage => { // Converts applicable mentions into sources. const sources: Source[] = mentions .map((mention) => { @@ -205,8 +206,7 @@ export const createUIMessage = (text: string, mentions: MentionData[], selectedR })) as UIMessagePart<{ source: Source }, SBChatMessageToolTypes>[], ], metadata: { - selectedRepos, - selectedContexts, + selectedSearchScopes, }, } } diff --git a/packages/web/src/types.ts b/packages/web/src/types.ts index 5b55b9db..2ceb5d30 100644 --- a/packages/web/src/types.ts +++ b/packages/web/src/types.ts @@ -4,11 +4,11 @@ export const orgMetadataSchema = z.object({ anonymousAccessEnabled: z.boolean().optional(), }) -export const demoSearchContextSchema = z.object({ +export const demoSearchScopeSchema = z.object({ id: z.number(), displayName: z.string(), value: z.string(), - type: z.enum(["repo", "set"]), + type: z.enum(["repo", "reposet"]), codeHostType: z.string().optional(), }) @@ -16,22 +16,15 @@ export const demoSearchExampleSchema = z.object({ title: z.string(), description: z.string(), url: z.string(), - searchContext: z.array(z.number()) -}) - -export const demoSearchContextExampleSchema = z.object({ - searchContext: z.number(), - description: z.string(), + searchScopes: z.array(z.number()) }) export const demoExamplesSchema = z.object({ - searchContexts: demoSearchContextSchema.array(), + searchScopes: demoSearchScopeSchema.array(), searchExamples: demoSearchExampleSchema.array(), - searchContextExamples: demoSearchContextExampleSchema.array(), }) export type OrgMetadata = z.infer; export type DemoExamples = z.infer; -export type DemoSearchContext = z.infer; -export type DemoSearchExample = z.infer; -export type DemoSearchContextExample = z.infer; \ No newline at end of file +export type DemoSearchScope = z.infer; +export type DemoSearchExample = z.infer; \ No newline at end of file