From a5562d794257e81b7d80d7fef0875cf771da9456 Mon Sep 17 00:00:00 2001 From: msukkari Date: Mon, 28 Jul 2025 13:42:20 -0700 Subject: [PATCH 1/4] new demo card ui --- .../homepage/askSourcebotDemoCards.tsx | 264 +++++++----------- .../src/components/searchScopeInfoCard.tsx | 41 +++ .../components/chatBox/chatBoxToolbar.tsx | 5 +- 3 files changed, 138 insertions(+), 172 deletions(-) create mode 100644 packages/web/src/components/searchScopeInfoCard.tsx diff --git a/packages/web/src/app/[domain]/components/homepage/askSourcebotDemoCards.tsx b/packages/web/src/app/[domain]/components/homepage/askSourcebotDemoCards.tsx index e8bb264cb..743a1fc66 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, DemoSearchContext } 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 [selectedFilterContext, setSelectedFilterContext] = 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 getContextIcon = (context: DemoSearchContext, 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 ; + return ; } if (context.codeHostType) { const codeHostIcon = getCodeHostIcon(context.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 */} +
+ {/* Example Searches Row */}
-
-
- -

Search Contexts

+
+
+ +

Community Ask Results

-

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

+ {/* Search Context Filter */} +
+
+
+ +
+ +
+
+
+ Search Context:
-

Check out these featured ask results from the community

+ { + setSelectedFilterContext(null); + }} + > + All + + {demoExamples.searchContexts.map((context) => ( + { + setSelectedFilterContext(context.id); + }} + > + {getContextIcon(context, 12, selectedFilterContext === context.id)} + {context.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} - - ))} -
-
-

- {example.title} -

-

- {example.description} -

+ {demoExamples.searchExamples + .filter((example) => { + if (selectedFilterContext === null) return true; + return example.searchContext.includes(selectedFilterContext); + }) + .map((example) => { + const searchContexts = demoExamples.searchContexts.filter((context) => example.searchContext.includes(context.id)) + return ( + handleExampleClick(example)} + > + +
+
+ {searchContexts.map((context) => ( + + {getContextIcon(context, 12)} + {context.displayName} + + ))} +
+
+

+ {example.title} +

+

+ {example.description} +

+
-
- - - ) - })} + + + ) + })}
diff --git a/packages/web/src/components/searchScopeInfoCard.tsx b/packages/web/src/components/searchScopeInfoCard.tsx new file mode 100644 index 000000000..840835486 --- /dev/null +++ b/packages/web/src/components/searchScopeInfoCard.tsx @@ -0,0 +1,41 @@ +import Image from "next/image"; +import { LibraryBigIcon, Code, Layers } from "lucide-react"; +import { cn, getCodeHostIcon } from "@/lib/utils"; + +export const SearchScopeInfoCard = () => { + return ( +
+
+ +

Search Context

+
+
+ When asking Sourcebot a question, you can select one or more search contexts to constrain its scope. There + are two different types of search contexts: +
+
+ {(() => { + const githubIcon = getCodeHostIcon("github"); + return githubIcon ? ( + GitHub icon + ) : ( + + ); + })()} + Repository: A single repository, indicated by the code host icon. +
+
+ + Set: A set of repositories, indicated by the library icon. +
+
+
+
+ ); +}; \ No newline at end of file diff --git a/packages/web/src/features/chat/components/chatBox/chatBoxToolbar.tsx b/packages/web/src/features/chat/components/chatBox/chatBoxToolbar.tsx index 8744e9ac3..e32daaa58 100644 --- a/packages/web/src/features/chat/components/chatBox/chatBoxToolbar.tsx +++ b/packages/web/src/features/chat/components/chatBox/chatBoxToolbar.tsx @@ -13,6 +13,7 @@ import { ReactEditor, useSlate } from "slate-react"; import { useSelectedLanguageModel } from "../../useSelectedLanguageModel"; import { LanguageModelSelector } from "./languageModelSelector"; import { ContextSelector, type ContextItem } from "./contextSelector"; +import { SearchScopeInfoCard } from "@/components/searchScopeInfoCard"; export interface ChatBoxToolbarProps { languageModels: LanguageModelInfo[]; @@ -88,8 +89,8 @@ export const ChatBoxToolbar = ({ onOpenChanged={onContextSelectorOpenChanged} /> - - Search contexts and repositories to scope conversation to. + + {languageModels.length > 0 && ( From f9e0500ef83e0d8037f11888aa8fdcdb73408509 Mon Sep 17 00:00:00 2001 From: msukkari Date: Mon, 28 Jul 2025 15:11:51 -0700 Subject: [PATCH 2/4] rename search context to search scope --- .../chat/[id]/components/chatThreadPanel.tsx | 32 +++++----- .../[domain]/chat/components/newChatPanel.tsx | 4 +- .../components/homepage/agenticSearch.tsx | 8 +-- .../homepage/askSourcebotDemoCards.tsx | 52 +++++++-------- .../web/src/app/api/(server)/chat/route.ts | 20 +++--- .../web/src/components/atMentionInfoCard.tsx | 15 +++++ .../src/components/searchScopeInfoCard.tsx | 12 ++-- .../chat/components/chatBox/chatBox.tsx | 18 +++--- .../components/chatBox/chatBoxToolbar.tsx | 29 ++------- ...xtSelector.tsx => searchScopeSelector.tsx} | 64 +++++++++---------- .../chat/components/chatThread/chatThread.tsx | 22 +++---- packages/web/src/features/chat/types.ts | 6 +- .../features/chat/useCreateNewChatThread.ts | 12 ++-- packages/web/src/features/chat/utils.ts | 4 +- packages/web/src/types.ts | 19 ++---- 15 files changed, 153 insertions(+), 164 deletions(-) create mode 100644 packages/web/src/components/atMentionInfoCard.tsx rename packages/web/src/features/chat/components/chatBox/{contextSelector.tsx => searchScopeSelector.tsx} (86%) 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 48824d762..3653c0495 100644 --- a/packages/web/src/app/[domain]/chat/[id]/components/chatThreadPanel.tsx +++ b/packages/web/src/app/[domain]/chat/[id]/components/chatThreadPanel.tsx @@ -8,7 +8,7 @@ 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'; +import { SearchScopeItem } from '@/features/chat/components/chatBox/searchScopeSelector'; interface ChatThreadPanelProps { languageModels: LanguageModelInfo[]; @@ -37,9 +37,9 @@ export const ChatThreadPanel = ({ // 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 defaultSelectedReposets = lastUserMessage?.metadata?.selectedReposets ?? []; - const [selectedItems, setSelectedItems] = useState([ + const [selectedItems, setSelectedItems] = useState([ ...defaultSelectedRepos.map(repoName => { const repoInfo = repos.find(r => r.repoName === repoName); return { @@ -49,13 +49,13 @@ export const ChatThreadPanel = ({ codeHostType: repoInfo?.codeHostType || '' }; }), - ...defaultSelectedContexts.map(contextName => { - const context = searchContexts.find(c => c.name === contextName); + ...defaultSelectedReposets.map(reposetName => { + const reposet = searchContexts.find(c => c.name === reposetName); return { - type: 'context' as const, - value: contextName, - name: contextName, - repoCount: context?.repoNames.length || 0 + type: 'reposet' as const, + value: reposetName, + name: reposetName, + repoCount: reposet?.repoNames.length || 0 }; }) ]); @@ -67,7 +67,7 @@ export const ChatThreadPanel = ({ } try { - const { inputMessage, selectedRepos, selectedContexts } = JSON.parse(setChatState) as SetChatStatePayload; + const { inputMessage, selectedRepos, selectedReposets } = JSON.parse(setChatState) as SetChatStatePayload; setInputMessage(inputMessage); setSelectedItems([ ...selectedRepos.map(repoName => { @@ -79,13 +79,13 @@ export const ChatThreadPanel = ({ codeHostType: repoInfo?.codeHostType || '' }; }), - ...selectedContexts.map(contextName => { - const context = searchContexts.find(c => c.name === contextName); + ...selectedReposets.map(reposetName => { + const reposet = searchContexts.find(c => c.name === reposetName); return { - type: 'context' as const, - value: contextName, - name: contextName, - repoCount: context?.repoNames.length || 0 + type: 'reposet' as const, + value: reposetName, + name: reposetName, + repoCount: reposet?.repoNames.length || 0 }; }) ]); diff --git a/packages/web/src/app/[domain]/chat/components/newChatPanel.tsx b/packages/web/src/app/[domain]/chat/components/newChatPanel.tsx index a2addcd41..190aff7c5 100644 --- a/packages/web/src/app/[domain]/chat/components/newChatPanel.tsx +++ b/packages/web/src/app/[domain]/chat/components/newChatPanel.tsx @@ -10,7 +10,7 @@ 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"; +import { SearchScopeItem } from "@/features/chat/components/chatBox/searchScopeSelector"; interface NewChatPanelProps { languageModels: LanguageModelInfo[]; @@ -25,7 +25,7 @@ export const NewChatPanel = ({ searchContexts, order, }: NewChatPanelProps) => { - const [selectedItems, setSelectedItems] = useLocalStorage("selectedContextItems", [], { initializeWithValue: false }); + const [selectedItems, setSelectedItems] = useLocalStorage("selectedContextItems", [], { initializeWithValue: false }); const { createNewChatThread, isLoading } = useCreateNewChatThread(); const [isContextSelectorOpen, setIsContextSelectorOpen] = useState(false); diff --git a/packages/web/src/app/[domain]/components/homepage/agenticSearch.tsx b/packages/web/src/app/[domain]/components/homepage/agenticSearch.tsx index c8bd472f7..f3ccddfc5 100644 --- a/packages/web/src/app/[domain]/components/homepage/agenticSearch.tsx +++ b/packages/web/src/app/[domain]/components/homepage/agenticSearch.tsx @@ -9,7 +9,7 @@ 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 { SearchScopeItem } from "@/features/chat/components/chatBox/searchScopeSelector"; import { DemoExamples } from "@/types"; import { AskSourcebotDemoCards } from "./askSourcebotDemoCards"; @@ -34,7 +34,7 @@ export const AgenticSearch = ({ demoExamples, }: AgenticSearchProps) => { const { createNewChatThread, isLoading } = useCreateNewChatThread(); - const [selectedItems, setSelectedItems] = useLocalStorage("selectedContextItems", [], { initializeWithValue: false }); + const [selectedItems, setSelectedItems] = useLocalStorage("selectedContextItems", [], { initializeWithValue: false }); const [isContextSelectorOpen, setIsContextSelectorOpen] = useState(false); return ( @@ -74,10 +74,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 743a1fc66..61f64a145 100644 --- a/packages/web/src/app/[domain]/components/homepage/askSourcebotDemoCards.tsx +++ b/packages/web/src/app/[domain]/components/homepage/askSourcebotDemoCards.tsx @@ -6,7 +6,7 @@ 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 { DemoExamples, DemoSearchExample, DemoSearchContext } from "@/types"; +import { DemoExamples, DemoSearchExample, DemoSearchScope } from "@/types"; import { cn, getCodeHostIcon } from "@/lib/utils"; import useCaptureEvent from "@/hooks/useCaptureEvent"; import { SearchScopeInfoCard } from "@/components/searchScopeInfoCard"; @@ -19,7 +19,7 @@ export const AskSourcebotDemoCards = ({ demoExamples, }: AskSourcebotDemoCardsProps) => { const captureEvent = useCaptureEvent(); - const [selectedFilterContext, setSelectedFilterContext] = useState(null); + const [selectedFilterSearchScope, setSelectedFilterSearchScope] = useState(null); const handleExampleClick = (example: DemoSearchExample) => { captureEvent('wa_demo_search_example_card_pressed', { @@ -32,16 +32,16 @@ export const AskSourcebotDemoCards = ({ } } - const getContextIcon = (context: DemoSearchContext, size: number = 20, isSelected: boolean = false) => { + 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") { + 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) @@ -53,7 +53,7 @@ export const AskSourcebotDemoCards = ({ return ( {`${context.codeHostType}
- {/* Search Context Filter */} + {/* Search Scope Filter */}
@@ -102,30 +102,30 @@ export const AskSourcebotDemoCards = ({
- Search Context: + Search Scope:
{ - setSelectedFilterContext(null); + setSelectedFilterSearchScope(null); }} > All - {demoExamples.searchContexts.map((context) => ( + {demoExamples.searchScopes.map((searchScope) => ( { - setSelectedFilterContext(context.id); + setSelectedFilterSearchScope(searchScope.id); }} > - {getContextIcon(context, 12, selectedFilterContext === context.id)} - {context.displayName} + {getSearchScopeIcon(searchScope, 12, selectedFilterSearchScope === searchScope.id)} + {searchScope.displayName} ))}
@@ -133,11 +133,11 @@ export const AskSourcebotDemoCards = ({
{demoExamples.searchExamples .filter((example) => { - if (selectedFilterContext === null) return true; - return example.searchContext.includes(selectedFilterContext); + if (selectedFilterSearchScope === null) return true; + return example.searchScopes.includes(selectedFilterSearchScope); }) .map((example) => { - const searchContexts = demoExamples.searchContexts.filter((context) => example.searchContext.includes(context.id)) + const searchScopes = demoExamples.searchScopes.filter((searchScope) => example.searchScopes.includes(searchScope.id)) return (
- {searchContexts.map((context) => ( - - {getContextIcon(context, 12)} - {context.displayName} + {searchScopes.map((searchScope) => ( + + {getSearchScopeIcon(searchScope, 12)} + {searchScope.displayName} ))}
diff --git a/packages/web/src/app/api/(server)/chat/route.ts b/packages/web/src/app/api/(server)/chat/route.ts index c52b9e189..ac040c063 100644 --- a/packages/web/src/app/api/(server)/chat/route.ts +++ b/packages/web/src/app/api/(server)/chat/route.ts @@ -64,12 +64,12 @@ export async function POST(req: Request) { return serviceErrorResponse(schemaValidationError(parsed.error)); } - const { messages, id, selectedRepos, selectedContexts, languageModelId } = parsed.data; + const { messages, id, selectedRepos, selectedReposets, languageModelId } = parsed.data; const response = await chatHandler({ messages, id, selectedRepos, - selectedContexts, + selectedReposets, languageModelId, }, domain); @@ -94,11 +94,11 @@ interface ChatHandlerProps { messages: SBChatMessage[]; id: string; selectedRepos: string[]; - selectedContexts?: string[]; + selectedReposets?: string[]; languageModelId: string; } -const chatHandler = ({ messages, id, selectedRepos, selectedContexts, languageModelId }: ChatHandlerProps, domain: string) => sew(async () => +const chatHandler = ({ messages, id, selectedRepos, selectedReposets, languageModelId }: ChatHandlerProps, domain: string) => sew(async () => withAuth((userId) => withOrgMembership(userId, domain, async ({ org }) => { const chat = await prisma.chat.findUnique({ @@ -190,23 +190,23 @@ const chatHandler = ({ messages, id, selectedRepos, selectedContexts, languageMo // Expand search contexts to repos let expandedRepos = [...selectedRepos]; - if (selectedContexts && selectedContexts.length > 0) { - const searchContexts = await prisma.searchContext.findMany({ + if (selectedReposets && selectedReposets.length > 0) { + const searchReposets = await prisma.searchContext.findMany({ where: { orgId: org.id, - name: { in: selectedContexts } + name: { in: selectedReposets } }, include: { repos: true } }); - const contextRepos = searchContexts.flatMap(context => - context.repos.map(repo => repo.name) + const reposetRepos = searchReposets.flatMap(reposet => + reposet.repos.map(repo => repo.name) ); // Combine and deduplicate repos - expandedRepos = Array.from(new Set([...selectedRepos, ...contextRepos])); + expandedRepos = Array.from(new Set([...selectedRepos, ...reposetRepos])); } const researchStream = await createAgentStream({ diff --git a/packages/web/src/components/atMentionInfoCard.tsx b/packages/web/src/components/atMentionInfoCard.tsx new file mode 100644 index 000000000..74424074e --- /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 index 840835486..633dcbbb3 100644 --- a/packages/web/src/components/searchScopeInfoCard.tsx +++ b/packages/web/src/components/searchScopeInfoCard.tsx @@ -1,17 +1,17 @@ import Image from "next/image"; -import { LibraryBigIcon, Code, Layers } from "lucide-react"; +import { LibraryBigIcon, Code, ScanSearchIcon } from "lucide-react"; import { cn, getCodeHostIcon } from "@/lib/utils"; export const SearchScopeInfoCard = () => { return (
- -

Search Context

+ +

Search Scope

- When asking Sourcebot a question, you can select one or more search contexts to constrain its scope. There - are two different types of search contexts: + When asking Sourcebot a question, you can select one or more scopes to constrain the search. + There are two different types of search scopes:
{(() => { @@ -32,7 +32,7 @@ export const SearchScopeInfoCard = () => {
- Set: A set of repositories, indicated by the library icon. + Reposet: A set of repositories, indicated by the library icon.
diff --git a/packages/web/src/features/chat/components/chatBox/chatBox.tsx b/packages/web/src/features/chat/components/chatBox/chatBox.tsx index c33cbdf1b..170aa62f5 100644 --- a/packages/web/src/features/chat/components/chatBox/chatBox.tsx +++ b/packages/web/src/features/chat/components/chatBox/chatBox.tsx @@ -18,7 +18,7 @@ import { Suggestion } from "./types"; import { useSuggestionModeAndQuery } from "./useSuggestionModeAndQuery"; import { useSuggestionsData } from "./useSuggestionsData"; import { useToast } from "@/components/hooks/use-toast"; -import { ContextItem } from "./contextSelector"; +import { SearchScopeItem } from "./searchScopeSelector"; import { SearchContextQuery } from "@/lib/types"; interface ChatBoxProps { @@ -29,7 +29,7 @@ interface ChatBoxProps { isRedirecting?: boolean; isGenerating?: boolean; languageModels: LanguageModelInfo[]; - selectedItems: ContextItem[]; + selectedItems: SearchScopeItem[]; searchContexts: SearchContextQuery[]; onContextSelectorOpenChanged: (isOpen: boolean) => void; } @@ -58,10 +58,10 @@ export const ChatBox = ({ 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; } } @@ -162,7 +162,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 +284,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 e32daaa58..beb034e61 100644 --- a/packages/web/src/features/chat/components/chatBox/chatBoxToolbar.tsx +++ b/packages/web/src/features/chat/components/chatBox/chatBoxToolbar.tsx @@ -1,6 +1,5 @@ '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"; @@ -8,19 +7,19 @@ import { LanguageModelInfo } 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, type SearchScopeItem } 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; + selectedItems: SearchScopeItem[]; + onSelectedItemsChange: (items: SearchScopeItem[]) => void; isContextSelectorOpen: boolean; onContextSelectorOpenChanged: (isOpen: boolean) => void; } @@ -41,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, }); @@ -67,19 +57,14 @@ export const ChatBoxToolbar = ({ - - - - Add context + + - { +interface SearchScopeSelectorProps extends React.ButtonHTMLAttributes { repos: RepositoryQuery[]; searchContexts: SearchContextQuery[]; - selectedItems: ContextItem[]; - onSelectedItemsChange: (items: ContextItem[]) => void; + selectedItems: SearchScopeItem[]; + onSelectedItemsChange: (items: SearchScopeItem[]) => void; className?: string; isOpen: boolean; onOpenChanged: (isOpen: boolean) => void; } -export const ContextSelector = React.forwardRef< +export const SearchScopeSelector = React.forwardRef< HTMLButtonElement, - ContextSelectorProps + SearchScopeSelectorProps >( ( { @@ -87,7 +87,7 @@ export const ContextSelector = React.forwardRef< } }; - const toggleItem = (item: ContextItem) => { + const toggleItem = (item: SearchScopeItem) => { // Store current scroll position before state update if (scrollContainerRef.current) { scrollPosition.current = scrollContainerRef.current.scrollTop; @@ -99,13 +99,13 @@ export const ContextSelector = React.forwardRef< const isDemoMode = process.env.NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT === "demo"; - let newSelectedItems: ContextItem[]; + let newSelectedItems: SearchScopeItem[]; if (isSelected) { newSelectedItems = selectedItems.filter( (selected) => !(selected.type === item.type && selected.value === item.value) ); } else { - // Limit selected context to 1 in demo mode + // Limit selected search scope to 1 in demo mode if (isDemoMode) { newSelectedItems = [item]; } else { @@ -124,26 +124,26 @@ export const ContextSelector = React.forwardRef< onOpenChanged(!isOpen); }; - const allItems = React.useMemo(() => { - const contextItems: ContextItem[] = searchContexts.map(context => ({ - type: 'context' as const, + const allSearchScopeItems = React.useMemo(() => { + const repoSetSearchScopeItems: RepoSetSearchScopeItem[] = 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: RepoSearchScopeItem[] = 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( @@ -154,19 +154,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, selectedItems]); // 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 === 0 ? `Search scopes` : selectedItems.length === 1 ? selectedItems[0].name : `${selectedItems.length} selected` } @@ -205,13 +205,13 @@ export const ContextSelector = React.forwardRef< > No results found. - {sortedItems.map(({ item, isSelected }) => { + {sortedSearchScopeItems.map(({ item, isSelected }) => { return (
- {item.type === 'context' ? ( + {item.type === 'reposet' ? ( ) : ( // Render code host icon for repos @@ -253,7 +253,7 @@ export const ContextSelector = React.forwardRef< {item.name} - {item.type === 'context' && ( + {item.type === 'reposet' && ( void; + selectedItems: SearchScopeItem[]; + onSelectedItemsChange: (items: SearchScopeItem[]) => void; isChatReadonly: boolean; } @@ -62,10 +62,10 @@ export const ChatThread = ({ const router = useRouter(); const [isContextSelectorOpen, setIsContextSelectorOpen] = useState(false); - const { selectedRepos, selectedContexts } = useMemo(() => { + const { selectedRepos, selectedReposets } = 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 }; + const reposets = selectedItems.filter(item => item.type === 'reposet').map(item => item.value); + return { selectedRepos: repos, selectedReposets: reposets }; }, [selectedItems]); // Initial state is from attachments that exist in in the chat history. @@ -123,11 +123,11 @@ export const ChatThread = ({ _sendMessage(message, { body: { selectedRepos, - selectedContexts, + selectedReposets, languageModelId: selectedLanguageModel.model, } satisfies AdditionalChatRequestParams, - }); - }, [_sendMessage, selectedLanguageModel, toast, selectedRepos, selectedContexts]); + }); + }, [_sendMessage, selectedLanguageModel, toast, selectedRepos, selectedReposets]); const messagePairs = useMessagePairs(messages); @@ -243,13 +243,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), selectedRepos, selectedReposets); sendMessage(message); setIsAutoScrollEnabled(true); resetEditor(editor); - }, [sendMessage, selectedRepos, selectedContexts]); + }, [sendMessage, selectedRepos, selectedReposets]); return ( <> diff --git a/packages/web/src/features/chat/types.ts b/packages/web/src/features/chat/types.ts index b4c6bc8d5..24d709074 100644 --- a/packages/web/src/features/chat/types.ts +++ b/packages/web/src/features/chat/types.ts @@ -51,7 +51,7 @@ export const sbChatMessageMetadataSchema = z.object({ userId: z.string(), })).optional(), selectedRepos: z.array(z.string()).optional(), - selectedContexts: z.array(z.string()).optional(), + selectedReposets: z.array(z.string()).optional(), traceId: z.string().optional(), }); @@ -140,7 +140,7 @@ export const SET_CHAT_STATE_QUERY_PARAM = 'setChatState'; export type SetChatStatePayload = { inputMessage: CreateUIMessage; selectedRepos: string[]; - selectedContexts: string[]; + selectedReposets: string[]; } @@ -158,6 +158,6 @@ export type LanguageModelInfo = { export const additionalChatRequestParamsSchema = z.object({ languageModelId: z.string(), selectedRepos: z.array(z.string()), - selectedContexts: z.array(z.string()), + selectedReposets: z.array(z.string()), }); 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 54aaf14d2..3c1084461 100644 --- a/packages/web/src/features/chat/useCreateNewChatThread.ts +++ b/packages/web/src/features/chat/useCreateNewChatThread.ts @@ -11,7 +11,7 @@ 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 { SearchScopeItem } from "./components/chatBox/searchScopeSelector"; export const useCreateNewChatThread = () => { const domain = useDomain(); @@ -19,15 +19,15 @@ export const useCreateNewChatThread = () => { const { toast } = useToast(); const router = useRouter(); - const createNewChatThread = useCallback(async (children: Descendant[], selectedItems: ContextItem[]) => { + const createNewChatThread = useCallback(async (children: Descendant[], selectedItems: SearchScopeItem[]) => { const text = slateContentToString(children); const mentions = getAllMentionElements(children); - // Extract repos and contexts from selectedItems + // Extract repos and reposets 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 selectedReposets = selectedItems.filter(item => item.type === 'reposet').map(item => item.value); - const inputMessage = createUIMessage(text, mentions.map((mention) => mention.data), selectedRepos, selectedContexts); + const inputMessage = createUIMessage(text, mentions.map((mention) => mention.data), selectedRepos, selectedReposets); setIsLoading(true); const response = await createChat(domain); @@ -43,7 +43,7 @@ export const useCreateNewChatThread = () => { [SET_CHAT_STATE_QUERY_PARAM, JSON.stringify({ inputMessage, selectedRepos, - selectedContexts, + selectedReposets, } satisfies SetChatStatePayload)], ); diff --git a/packages/web/src/features/chat/utils.ts b/packages/web/src/features/chat/utils.ts index e24956644..36330ad3e 100644 --- a/packages/web/src/features/chat/utils.ts +++ b/packages/web/src/features/chat/utils.ts @@ -172,7 +172,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[], selectedRepos: string[], selectedReposets: string[]): CreateUIMessage => { // Converts applicable mentions into sources. const sources: Source[] = mentions .map((mention) => { @@ -206,7 +206,7 @@ export const createUIMessage = (text: string, mentions: MentionData[], selectedR ], metadata: { selectedRepos, - selectedContexts, + selectedReposets, }, } } diff --git a/packages/web/src/types.ts b/packages/web/src/types.ts index 5b55b9dbf..2ceb5d30e 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 From 3b2dd5179543dee64ee90ac5ba5f7a1cdcf31a8b Mon Sep 17 00:00:00 2001 From: msukkari Date: Mon, 28 Jul 2025 16:35:21 -0700 Subject: [PATCH 3/4] rename everything to use search scope --- .../chat/[id]/components/chatThreadPanel.tsx | 59 ++--------- .../[domain]/chat/components/newChatPanel.tsx | 15 ++- .../components/homepage/agenticSearch.tsx | 13 ++- .../web/src/app/api/(server)/chat/route.ts | 55 ++++++----- packages/web/src/features/chat/agent.ts | 14 +-- .../chat/components/chatBox/chatBox.tsx | 13 ++- .../components/chatBox/chatBoxToolbar.tsx | 16 +-- .../chatBox/searchScopeSelector.tsx | 97 +++++-------------- .../chat/components/chatThread/chatThread.tsx | 34 +++---- .../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 +- 14 files changed, 200 insertions(+), 225 deletions(-) create mode 100644 packages/web/src/features/chat/components/searchScopeIcon.tsx 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 3653c0495..92f3ab6e3 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 { SearchScopeItem } from '@/features/chat/components/chatBox/searchScopeSelector'; 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 defaultSelectedReposets = lastUserMessage?.metadata?.selectedReposets ?? []; + 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 || '' - }; - }), - ...defaultSelectedReposets.map(reposetName => { - const reposet = searchContexts.find(c => c.name === reposetName); - return { - type: 'reposet' as const, - value: reposetName, - name: reposetName, - repoCount: reposet?.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, selectedReposets } = 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 || '' - }; - }), - ...selectedReposets.map(reposetName => { - const reposet = searchContexts.find(c => c.name === reposetName); - return { - type: 'reposet' as const, - value: reposetName, - name: reposetName, - repoCount: reposet?.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 190aff7c5..91ae3f960 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 { SearchScopeItem } from "@/features/chat/components/chatBox/searchScopeSelector"; 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 f3ccddfc5..259c738b6 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 { SearchScopeItem } from "@/features/chat/components/chatBox/searchScopeSelector"; 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} /> diff --git a/packages/web/src/app/api/(server)/chat/route.ts b/packages/web/src/app/api/(server)/chat/route.ts index ac040c063..543255996 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, selectedReposets, languageModelId } = parsed.data; + const { messages, id, selectedSearchScopes, languageModelId } = parsed.data; const response = await chatHandler({ messages, id, - selectedRepos, - selectedReposets, + selectedSearchScopes, languageModelId, }, domain); @@ -93,12 +92,11 @@ const mergeStreamAsync = async (stream: StreamTextResult, writer: UIMe interface ChatHandlerProps { messages: SBChatMessage[]; id: string; - selectedRepos: string[]; - selectedReposets?: string[]; + selectedSearchScopes: SearchScope[]; languageModelId: string; } -const chatHandler = ({ messages, id, selectedRepos, selectedReposets, 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, selectedReposets, languageMo const startTime = new Date(); - // Expand search contexts to repos - let expandedRepos = [...selectedRepos]; - if (selectedReposets && selectedReposets.length > 0) { - const searchReposets = await prisma.searchContext.findMany({ - where: { - orgId: org.id, - name: { in: selectedReposets } - }, - 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 reposetRepos = searchReposets.flatMap(reposet => - reposet.repos.map(repo => repo.name) - ); + } - // Combine and deduplicate repos - expandedRepos = Array.from(new Set([...selectedRepos, ...reposetRepos])); - } + return []; + })); + const expandedRepos = expandedReposArrays.flat(); const researchStream = await createAgentStream({ model, @@ -215,7 +217,7 @@ const chatHandler = ({ messages, id, selectedRepos, selectedReposets, 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, selectedReposets, languageMo totalOutputTokens: totalUsage.outputTokens, totalResponseTimeMs: new Date().getTime() - startTime.getTime(), modelName: languageModelConfig.displayName ?? languageModelConfig.model, + selectedSearchScopes, traceId, } }) diff --git a/packages/web/src/features/chat/agent.ts b/packages/web/src/features/chat/agent.ts index 7f3ef93de..d443b99b7 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 170aa62f5..f5cfc974d 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 { SearchScopeItem } from "./searchScopeSelector"; import { SearchContextQuery } from "@/lib/types"; interface ChatBoxProps { @@ -29,7 +28,7 @@ interface ChatBoxProps { isRedirecting?: boolean; isGenerating?: boolean; languageModels: LanguageModelInfo[]; - selectedItems: SearchScopeItem[]; + selectedSearchScopes: SearchScope[]; searchContexts: SearchContextQuery[]; onContextSelectorOpenChanged: (isOpen: boolean) => void; } @@ -42,7 +41,7 @@ export const ChatBox = ({ isRedirecting, isGenerating, languageModels, - selectedItems, + selectedSearchScopes, searchContexts, onContextSelectorOpenChanged, }: ChatBoxProps) => { @@ -53,7 +52,7 @@ export const ChatBox = ({ const { suggestions, isLoading } = useSuggestionsData({ suggestionMode, suggestionQuery, - selectedRepos: selectedItems.map((item) => { + selectedRepos: selectedSearchScopes.map((item) => { if (item.type === 'repo') { return [item.value]; } @@ -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, ]) diff --git a/packages/web/src/features/chat/components/chatBox/chatBoxToolbar.tsx b/packages/web/src/features/chat/components/chatBox/chatBoxToolbar.tsx index beb034e61..eba67729f 100644 --- a/packages/web/src/features/chat/components/chatBox/chatBoxToolbar.tsx +++ b/packages/web/src/features/chat/components/chatBox/chatBoxToolbar.tsx @@ -3,14 +3,14 @@ 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 { ReactEditor, useSlate } from "slate-react"; import { useSelectedLanguageModel } from "../../useSelectedLanguageModel"; import { LanguageModelSelector } from "./languageModelSelector"; -import { SearchScopeSelector, type SearchScopeItem } from "./searchScopeSelector"; +import { SearchScopeSelector } from "./searchScopeSelector"; import { SearchScopeInfoCard } from "@/components/searchScopeInfoCard"; import { AtMentionInfoCard } from "@/components/atMentionInfoCard"; @@ -18,8 +18,8 @@ export interface ChatBoxToolbarProps { languageModels: LanguageModelInfo[]; repos: RepositoryQuery[]; searchContexts: SearchContextQuery[]; - selectedItems: SearchScopeItem[]; - onSelectedItemsChange: (items: SearchScopeItem[]) => 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) => { @@ -68,8 +68,8 @@ export const ChatBoxToolbar = ({ className="bg-inherit w-fit h-6 min-h-6" repos={repos} searchContexts={searchContexts} - selectedItems={selectedItems} - onSelectedItemsChange={onSelectedItemsChange} + selectedSearchScopes={selectedSearchScopes} + onSelectedSearchScopesChange={onSelectedSearchScopesChange} isOpen={isContextSelectorOpen} onOpenChanged={onContextSelectorOpenChanged} /> diff --git a/packages/web/src/features/chat/components/chatBox/searchScopeSelector.tsx b/packages/web/src/features/chat/components/chatBox/searchScopeSelector.tsx index 77887a536..a1c8f5955 100644 --- a/packages/web/src/features/chat/components/chatBox/searchScopeSelector.tsx +++ b/packages/web/src/features/chat/components/chatBox/searchScopeSelector.tsx @@ -4,13 +4,10 @@ import * as React from "react"; import { CheckIcon, ChevronDown, - FolderIcon, ScanSearchIcon, - LibraryBigIcon, } 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,28 +25,14 @@ import { CommandList, CommandSeparator, } from "@/components/ui/command"; - -export type RepoSearchScopeItem = { - type: 'repo'; - value: string; - name: string; - codeHostType: string; -} - -export type RepoSetSearchScopeItem = { - type: 'reposet'; - value: string; - name: string; - repoCount: number; -} - -export type SearchScopeItem = RepoSearchScopeItem | RepoSetSearchScopeItem; +import { RepoSetSearchScope, RepoSearchScope, SearchScope } from "../../types"; +import { SearchScopeIcon } from "../searchScopeIcon"; interface SearchScopeSelectorProps extends React.ButtonHTMLAttributes { repos: RepositoryQuery[]; searchContexts: SearchContextQuery[]; - selectedItems: SearchScopeItem[]; - onSelectedItemsChange: (items: SearchScopeItem[]) => void; + selectedSearchScopes: SearchScope[]; + onSelectedSearchScopesChange: (items: SearchScope[]) => void; className?: string; isOpen: boolean; onOpenChanged: (isOpen: boolean) => void; @@ -63,9 +46,9 @@ export const SearchScopeSelector = React.forwardRef< { repos, searchContexts, - onSelectedItemsChange, className, - selectedItems, + selectedSearchScopes, + onSelectedSearchScopesChange, isOpen, onOpenChanged, ...props @@ -81,43 +64,33 @@ export const SearchScopeSelector = 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: SearchScopeItem) => { + 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: SearchScopeItem[]; - if (isSelected) { - newSelectedItems = selectedItems.filter( + const newSelectedItems = isSelected ? + selectedSearchScopes.filter( (selected) => !(selected.type === item.type && selected.value === item.value) - ); - } else { - // Limit selected search scope 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 = () => { @@ -125,14 +98,14 @@ export const SearchScopeSelector = React.forwardRef< }; const allSearchScopeItems = React.useMemo(() => { - const repoSetSearchScopeItems: RepoSetSearchScopeItem[] = searchContexts.map(context => ({ + const repoSetSearchScopeItems: RepoSetSearchScope[] = searchContexts.map(context => ({ type: 'reposet' as const, value: context.name, name: context.name, repoCount: context.repoNames.length })); - const repoSearchScopeItems: RepoSearchScopeItem[] = 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, @@ -146,7 +119,7 @@ export const SearchScopeSelector = React.forwardRef< return allSearchScopeItems .map((item) => ({ item, - isSelected: selectedItems.some( + isSelected: selectedSearchScopes.some( (selected) => selected.type === item.type && selected.value === item.value ) })) @@ -159,7 +132,7 @@ export const SearchScopeSelector = React.forwardRef< if (a.item.type === 'repo' && b.item.type === 'reposet') return 1; return 0; }) - }, [allSearchScopeItems, selectedItems]); + }, [allSearchScopeItems, selectedSearchScopes]); // Restore scroll position after re-render React.useEffect(() => { @@ -189,9 +162,9 @@ export const SearchScopeSelector = React.forwardRef< className={cn("text-sm text-muted-foreground mx-1 font-medium")} > { - selectedItems.length === 0 ? `Search scopes` : - selectedItems.length === 1 ? selectedItems[0].name : - `${selectedItems.length} selected` + selectedSearchScopes.length === 0 ? `Search scopes` : + selectedSearchScopes.length === 1 ? selectedSearchScopes[0].name : + `${selectedSearchScopes.length} selected` } @@ -229,25 +202,7 @@ export const SearchScopeSelector = React.forwardRef<
- {item.type === 'reposet' ? ( - - ) : ( - // Render code host icon for repos - (() => { - const codeHostIcon = item.codeHostType ? getCodeHostIcon(item.codeHostType) : null; - return codeHostIcon ? ( - {`${item.codeHostType} - ) : ( - - ); - })() - )} +
@@ -269,7 +224,7 @@ export const SearchScopeSelector = React.forwardRef< })} - {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, selectedReposets } = useMemo(() => { - const repos = selectedItems.filter(item => item.type === 'repo').map(item => item.value); - const reposets = selectedItems.filter(item => item.type === 'reposet').map(item => item.value); - return { selectedRepos: repos, selectedReposets: reposets }; - }, [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, - selectedReposets, + selectedSearchScopes, languageModelId: selectedLanguageModel.model, } satisfies AdditionalChatRequestParams, }); - }, [_sendMessage, selectedLanguageModel, toast, selectedRepos, selectedReposets]); + }, [_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, selectedReposets); + const message = createUIMessage(text, mentions.map(({ data }) => data), selectedSearchScopes); sendMessage(message); setIsAutoScrollEnabled(true); resetEditor(editor); - }, [sendMessage, selectedRepos, selectedReposets]); + }, [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 7bfe49a08..ab3b676c2 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 000000000..933471f4e --- /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 24d709074..18aa55a2c 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(), - selectedReposets: 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[]; - selectedReposets: 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()), - selectedReposets: 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 3c1084461..f0de7e1ad 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 { SearchScopeItem } from "./components/chatBox/searchScopeSelector"; +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: SearchScopeItem[]) => { + const createNewChatThread = useCallback(async (children: Descendant[], selectedSearchScopes: SearchScope[]) => { const text = slateContentToString(children); const mentions = getAllMentionElements(children); - // Extract repos and reposets from selectedItems - const selectedRepos = selectedItems.filter(item => item.type === 'repo').map(item => item.value); - const selectedReposets = selectedItems.filter(item => item.type === 'reposet').map(item => item.value); - - const inputMessage = createUIMessage(text, mentions.map((mention) => mention.data), selectedRepos, selectedReposets); + 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, - selectedReposets, + selectedSearchScopes, } satisfies SetChatStatePayload)], ); diff --git a/packages/web/src/features/chat/utils.ts b/packages/web/src/features/chat/utils.ts index 36330ad3e..d84835efd 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[], selectedReposets: 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, - selectedReposets, + selectedSearchScopes, }, } } From f0c856121c707c4505257ab0bf705164849977cd Mon Sep 17 00:00:00 2001 From: msukkari Date: Mon, 28 Jul 2025 16:40:22 -0700 Subject: [PATCH 4/4] add changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba12ac523..3397298e4 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)