From 53edd44462684f9de01c1227a75dee686a467363 Mon Sep 17 00:00:00 2001 From: Michael Sukkarieh Date: Tue, 29 Jul 2025 15:50:36 -0700 Subject: [PATCH] feat(ask_sb): Add back search scope requirement and other UI changes (#411) * Revert "Remove search scope constraint" This reverts commit e69ac0d806960271c3184af00022683eb59b5533. * add llm section to onboard final page * add select all button * add repo snapshot to agentic search and other ui nits * refactor demo repo index cta into repo snapshop * changelog --- CHANGELOG.md | 1 + .../[domain]/chat/components/newChatPanel.tsx | 1 + .../components/homepage/agenticSearch.tsx | 12 ++ .../homepage/askSourcebotDemoCards.tsx | 187 ++++++++---------- .../homepage/repositorySnapshot.tsx | 18 +- packages/web/src/app/onboard/page.tsx | 9 +- .../chat/components/chatBox/chatBox.tsx | 83 ++++++-- .../chatBox/searchScopeSelector.tsx | 14 ++ .../chat/components/chatThread/chatThread.tsx | 1 + .../components/chatThread/detailsCard.tsx | 6 +- 10 files changed, 210 insertions(+), 122 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea7e39eb..f80f4a22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bumped next version. [#406](https://github.com/sourcebot-dev/sourcebot/pull/406) - [ask sb] Improved search code tool with filter options. [#400](https://github.com/sourcebot-dev/sourcebot/pull/400) - [ask sb] Removed search scope constraint. [#400](https://github.com/sourcebot-dev/sourcebot/pull/400) +- [ask sb] Add back search scope requirement and other UI changes. [#411](https://github.com/sourcebot-dev/sourcebot/pull/411) ## [4.6.0] - 2025-07-25 diff --git a/packages/web/src/app/[domain]/chat/components/newChatPanel.tsx b/packages/web/src/app/[domain]/chat/components/newChatPanel.tsx index 699e63bd..91ae3f96 100644 --- a/packages/web/src/app/[domain]/chat/components/newChatPanel.tsx +++ b/packages/web/src/app/[domain]/chat/components/newChatPanel.tsx @@ -51,6 +51,7 @@ export const NewChatPanel = ({ languageModels={languageModels} selectedSearchScopes={selectedSearchScopes} searchContexts={searchContexts} + onContextSelectorOpenChanged={setIsContextSelectorOpen} />
@@ -79,6 +81,16 @@ export const AgenticSearch = ({
+
+ +
+ +
+ +
+ {demoExamples && ( - {process.env.NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT === "demo" && ( -

- Interested in using Sourcebot on your code? Check out our{' '} - captureEvent('wa_demo_docs_link_pressed', {})} - > - docs - -

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

Community Ask Results

-
-
- - {/* 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 - .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 Searches Row */} +
+
+
+ +

Community Ask Results

+ + {/* 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 + .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} +

+
+
+
+
+ ) + })} +
- +
); }; \ No newline at end of file diff --git a/packages/web/src/app/[domain]/components/homepage/repositorySnapshot.tsx b/packages/web/src/app/[domain]/components/homepage/repositorySnapshot.tsx index c867fdd0..6248a659 100644 --- a/packages/web/src/app/[domain]/components/homepage/repositorySnapshot.tsx +++ b/packages/web/src/app/[domain]/components/homepage/repositorySnapshot.tsx @@ -16,6 +16,7 @@ import { import { RepoIndexingStatus } from "@sourcebot/db"; import { SymbolIcon } from "@radix-ui/react-icons"; import { RepositoryQuery } from "@/lib/types"; +import { captureEvent } from "@/hooks/useCaptureEvent"; interface RepositorySnapshotProps { repos: RepositoryQuery[]; @@ -68,15 +69,30 @@ export function RepositorySnapshot({ return (
- {`Search ${indexedRepos.length} `} + {`${indexedRepos.length} `} {indexedRepos.length > 1 ? 'repositories' : 'repository'} + {` indexed`} + {process.env.NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT === "demo" && ( +

+ Interested in using Sourcebot on your code? Check out our{' '} + captureEvent('wa_demo_docs_link_pressed', {})} + > + docs + +

+ )}
) } diff --git a/packages/web/src/app/onboard/page.tsx b/packages/web/src/app/onboard/page.tsx index fbc746d9..5fd4a49b 100644 --- a/packages/web/src/app/onboard/page.tsx +++ b/packages/web/src/app/onboard/page.tsx @@ -14,7 +14,7 @@ import { prisma } from "@/prisma"; import { OrgRole } from "@sourcebot/db"; import { LogoutEscapeHatch } from "@/app/components/logoutEscapeHatch"; import { redirect } from "next/navigation"; -import { BetweenHorizontalStart, GitBranchIcon, LockIcon } from "lucide-react"; +import { BetweenHorizontalStart, Brain, GitBranchIcon, LockIcon } from "lucide-react"; import { hasEntitlement } from "@sourcebot/shared"; import { env } from "@/env.mjs"; import { GcpIapAuth } from "@/app/[domain]/components/gcpIapAuth"; @@ -87,6 +87,13 @@ export default async function Onboarding({ searchParams }: OnboardingProps) { href: "https://docs.sourcebot.dev/docs/connections/overview", icon: , }, + { + id: "language-models", + title: "Language Models", + description: "Learn how to configure your language model providers to start using Ask Sourcebot", + href: "https://docs.sourcebot.dev/docs/configuration/language-model-providers", + icon: , + }, { id: "authentication-system", title: "Authentication System", diff --git a/packages/web/src/features/chat/components/chatBox/chatBox.tsx b/packages/web/src/features/chat/components/chatBox/chatBox.tsx index 5d539bcf..934199f2 100644 --- a/packages/web/src/features/chat/components/chatBox/chatBox.tsx +++ b/packages/web/src/features/chat/components/chatBox/chatBox.tsx @@ -5,10 +5,9 @@ import { Button } from "@/components/ui/button"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { CustomEditor, LanguageModelInfo, MentionElement, RenderElementPropsFor, SearchScope } from "@/features/chat/types"; import { insertMention, slateContentToString } from "@/features/chat/utils"; -import { SearchContextQuery } from "@/lib/types"; import { cn, IS_MAC } from "@/lib/utils"; import { computePosition, flip, offset, shift, VirtualElement } from "@floating-ui/react"; -import { ArrowUp, Loader2, StopCircleIcon } from "lucide-react"; +import { ArrowUp, Loader2, StopCircleIcon, TriangleAlertIcon } from "lucide-react"; import { Fragment, KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useHotkeys } from "react-hotkeys-hook"; import { Descendant, insertText } from "slate"; @@ -18,6 +17,8 @@ import { SuggestionBox } from "./suggestionsBox"; import { Suggestion } from "./types"; import { useSuggestionModeAndQuery } from "./useSuggestionModeAndQuery"; import { useSuggestionsData } from "./useSuggestionsData"; +import { useToast } from "@/components/hooks/use-toast"; +import { SearchContextQuery } from "@/lib/types"; interface ChatBoxProps { onSubmit: (children: Descendant[], editor: CustomEditor) => void; @@ -29,6 +30,7 @@ interface ChatBoxProps { languageModels: LanguageModelInfo[]; selectedSearchScopes: SearchScope[]; searchContexts: SearchContextQuery[]; + onContextSelectorOpenChanged: (isOpen: boolean) => void; } export const ChatBox = ({ @@ -41,6 +43,7 @@ export const ChatBox = ({ languageModels, selectedSearchScopes, searchContexts, + onContextSelectorOpenChanged, }: ChatBoxProps) => { const suggestionsBoxRef = useRef(null); const [index, setIndex] = useState(0); @@ -67,6 +70,7 @@ export const ChatBox = ({ const { selectedLanguageModel } = useSelectedLanguageModel({ initialLanguageModel: languageModels.length > 0 ? languageModels[0] : undefined, }); + const { toast } = useToast(); // Reset the index when the suggestion mode changes. useEffect(() => { @@ -97,9 +101,9 @@ export const ChatBox = ({ return }, []); - const { isSubmitDisabled } = useMemo((): { + const { isSubmitDisabled, isSubmitDisabledReason } = useMemo((): { isSubmitDisabled: true, - isSubmitDisabledReason: "empty" | "redirecting" | "generating" | "no-language-model-selected" + isSubmitDisabledReason: "empty" | "redirecting" | "generating" | "no-repos-selected" | "no-language-model-selected" } | { isSubmitDisabled: false, isSubmitDisabledReason: undefined, @@ -125,6 +129,13 @@ export const ChatBox = ({ } } + if (selectedSearchScopes.length === 0) { + return { + isSubmitDisabled: true, + isSubmitDisabledReason: "no-repos-selected", + } + } + if (selectedLanguageModel === undefined) { return { @@ -138,11 +149,29 @@ export const ChatBox = ({ isSubmitDisabledReason: undefined, } - }, [editor.children, isRedirecting, isGenerating, selectedLanguageModel]) + }, [ + editor.children, + isRedirecting, + isGenerating, + selectedSearchScopes.length, + selectedLanguageModel, + ]) const onSubmit = useCallback(() => { + if (isSubmitDisabled) { + if (isSubmitDisabledReason === "no-repos-selected") { + toast({ + description: "⚠️ You must select at least one search scope", + variant: "destructive", + }); + onContextSelectorOpenChanged(true); + } + + return; + } + _onSubmit(editor.children, editor); - }, [_onSubmit, editor]); + }, [_onSubmit, editor, isSubmitDisabled, isSubmitDisabledReason, toast, onContextSelectorOpenChanged]); const onInsertSuggestion = useCallback((suggestion: Suggestion) => { switch (suggestion.type) { @@ -281,15 +310,39 @@ export const ChatBox = ({ Stop ) : ( - + + +
{ + // @hack: When submission is disabled, we still want to issue + // a warning to the user as to why the submission is disabled. + // onSubmit on the Button will not be called because of the + // disabled prop, hence the call here. + if (isSubmitDisabled) { + onSubmit(); + } + }} + > + +
+
+ {(isSubmitDisabled && isSubmitDisabledReason === "no-repos-selected") && ( + +
+ + You must select at least one search scope +
+
+ )} +
)}
{suggestionMode !== "none" && ( diff --git a/packages/web/src/features/chat/components/chatBox/searchScopeSelector.tsx b/packages/web/src/features/chat/components/chatBox/searchScopeSelector.tsx index a1c8f595..63a44cef 100644 --- a/packages/web/src/features/chat/components/chatBox/searchScopeSelector.tsx +++ b/packages/web/src/features/chat/components/chatBox/searchScopeSelector.tsx @@ -57,6 +57,7 @@ export const SearchScopeSelector = React.forwardRef< ) => { const scrollContainerRef = React.useRef(null); const scrollPosition = React.useRef(0); + const [hasSearchInput, setHasSearchInput] = React.useState(false); const handleInputKeyDown = ( event: React.KeyboardEvent @@ -93,6 +94,10 @@ export const SearchScopeSelector = React.forwardRef< onSelectedSearchScopesChange([]); }; + const handleSelectAll = () => { + onSelectedSearchScopesChange(allSearchScopeItems); + }; + const handleTogglePopover = () => { onOpenChanged(!isOpen); }; @@ -180,10 +185,19 @@ export const SearchScopeSelector = React.forwardRef< setHasSearchInput(!!value)} /> No results found. + {!hasSearchInput && ( +
+ Select all +
+ )} {sortedSearchScopeItems.map(({ item, isSelected }) => { return (
- + {metadata?.modelName}
)} @@ -106,7 +106,7 @@ export const DetailsCard = ({
)}
- + {`${thinkingSteps.length} step${thinkingSteps.length === 1 ? '' : 's'}`}