feat(ask_sb): Add back search scope requirement and other UI changes (#411)

* Revert "Remove search scope constraint"

This reverts commit e69ac0d806.

* 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
This commit is contained in:
Michael Sukkarieh 2025-07-29 15:50:36 -07:00 committed by GitHub
parent 211ad8fb12
commit 53edd44462
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 210 additions and 122 deletions

View file

@ -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

View file

@ -51,6 +51,7 @@ export const NewChatPanel = ({
languageModels={languageModels}
selectedSearchScopes={selectedSearchScopes}
searchContexts={searchContexts}
onContextSelectorOpenChanged={setIsContextSelectorOpen}
/>
<div className="w-full flex flex-row items-center bg-accent rounded-b-md px-2">
<ChatBoxToolbar

View file

@ -13,6 +13,7 @@ import { DemoExamples } from "@/types";
import { AskSourcebotDemoCards } from "./askSourcebotDemoCards";
import { AgenticSearchTutorialDialog } from "./agenticSearchTutorialDialog";
import { setAgenticSearchTutorialDismissedCookie } from "@/actions";
import { RepositorySnapshot } from "./repositorySnapshot";
interface AgenticSearchProps {
searchModeSelectorProps: SearchModeSelectorProps;
@ -58,6 +59,7 @@ export const AgenticSearch = ({
languageModels={languageModels}
selectedSearchScopes={selectedSearchScopes}
searchContexts={searchContexts}
onContextSelectorOpenChanged={setIsContextSelectorOpen}
/>
<Separator />
<div className="relative">
@ -79,6 +81,16 @@ export const AgenticSearch = ({
</div>
</div>
<div className="mt-8">
<RepositorySnapshot
repos={repos}
/>
</div>
<div className="flex flex-col items-center w-fit gap-6">
<Separator className="mt-5 w-[700px]" />
</div>
{demoExamples && (
<AskSourcebotDemoCards
demoExamples={demoExamples}

View file

@ -66,110 +66,93 @@ export const AskSourcebotDemoCards = ({
}
return (
<>
{process.env.NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT === "demo" && (
<p className="text-sm text-muted-foreground text-center mt-6">
Interested in using Sourcebot on your code? Check out our{' '}
<a
href="https://docs.sourcebot.dev/docs/overview"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
onClick={() => captureEvent('wa_demo_docs_link_pressed', {})}
>
docs
</a>
</p>
)}
<div className="w-full mt-16 space-y-12 px-4 max-w-[1000px]">
{/* Example Searches Row */}
<div className="space-y-4">
<div className="text-center mb-6">
<div className="flex items-center justify-center gap-3 mb-4">
<Search className="h-7 w-7 text-muted-foreground" />
<h3 className="text-2xl font-bold">Community Ask Results</h3>
</div>
</div>
{/* Search Scope Filter */}
<div className="flex flex-wrap items-center justify-center gap-2 mb-6">
<div className="flex items-center gap-2 mr-2">
<div className="relative group">
<Info className="h-4 w-4 text-muted-foreground cursor-help" />
<div className="absolute bottom-6 left-1/2 transform -translate-x-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 z-10 pointer-events-none">
<SearchScopeInfoCard />
<div className="absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-border"></div>
</div>
</div>
<span className="text-sm font-medium text-muted-foreground">Search Scope:</span>
</div>
<Badge
variant={selectedFilterSearchScope === null ? "default" : "secondary"}
className={`cursor-pointer transition-all duration-200 hover:shadow-sm ${selectedFilterSearchScope === null ? "bg-primary text-primary-foreground" : "hover:bg-secondary/80"
}`}
onClick={() => {
setSelectedFilterSearchScope(null);
}}
>
All
</Badge>
{demoExamples.searchScopes.map((searchScope) => (
<Badge
key={searchScope.id}
variant={selectedFilterSearchScope === searchScope.id ? "default" : "secondary"}
className={`cursor-pointer transition-all duration-200 hover:shadow-sm flex items-center gap-1 ${selectedFilterSearchScope === searchScope.id ? "bg-primary text-primary-foreground" : "hover:bg-secondary/80"
}`}
onClick={() => {
setSelectedFilterSearchScope(searchScope.id);
}}
>
{getSearchScopeIcon(searchScope, 12, selectedFilterSearchScope === searchScope.id)}
{searchScope.displayName}
</Badge>
))}
</div>
<div className="flex flex-wrap justify-center gap-3">
{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 (
<Card
key={example.url}
className="cursor-pointer transition-all duration-200 hover:shadow-md hover:scale-105 hover:border-primary/50 group w-full max-w-[350px]"
onClick={() => handleExampleClick(example)}
>
<CardContent className="p-4">
<div className="space-y-3">
<div className="flex items-center justify-between">
{searchScopes.map((searchScope) => (
<Badge key={searchScope.value} variant="secondary" className="text-[10px] px-1.5 py-0.5 h-4 flex items-center gap-1">
{getSearchScopeIcon(searchScope, 12)}
{searchScope.displayName}
</Badge>
))}
</div>
<div className="space-y-1">
<h4 className="font-semibold text-sm group-hover:text-primary transition-colors line-clamp-2">
{example.title}
</h4>
<p className="text-xs text-muted-foreground line-clamp-3 leading-relaxed">
{example.description}
</p>
</div>
</div>
</CardContent>
</Card>
)
})}
<div className="w-full mt-8 space-y-12 px-4 max-w-[1200px]">
{/* Example Searches Row */}
<div className="space-y-4">
<div className="text-center mb-6">
<div className="flex items-center justify-center gap-3 mb-4">
<Search className="h-7 w-7 text-muted-foreground" />
<h3 className="text-2xl font-bold">Community Ask Results</h3>
</div>
</div>
{/* Search Scope Filter */}
<div className="flex flex-wrap items-center justify-center gap-2 mb-6">
<div className="flex items-center gap-2 mr-2">
<div className="relative group">
<Info className="h-4 w-4 text-muted-foreground cursor-help" />
<div className="absolute bottom-6 left-1/2 transform -translate-x-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 z-10 pointer-events-none">
<SearchScopeInfoCard />
<div className="absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-border"></div>
</div>
</div>
<span className="text-sm font-medium text-muted-foreground">Search Scope:</span>
</div>
<Badge
variant={selectedFilterSearchScope === null ? "default" : "secondary"}
className={`cursor-pointer transition-all duration-200 hover:shadow-sm ${selectedFilterSearchScope === null ? "bg-primary text-primary-foreground" : "hover:bg-secondary/80"
}`}
onClick={() => {
setSelectedFilterSearchScope(null);
}}
>
All
</Badge>
{demoExamples.searchScopes.map((searchScope) => (
<Badge
key={searchScope.id}
variant={selectedFilterSearchScope === searchScope.id ? "default" : "secondary"}
className={`cursor-pointer transition-all duration-200 hover:shadow-sm flex items-center gap-1 ${selectedFilterSearchScope === searchScope.id ? "bg-primary text-primary-foreground" : "hover:bg-secondary/80"
}`}
onClick={() => {
setSelectedFilterSearchScope(searchScope.id);
}}
>
{getSearchScopeIcon(searchScope, 12, selectedFilterSearchScope === searchScope.id)}
{searchScope.displayName}
</Badge>
))}
</div>
<div className="flex flex-wrap justify-center gap-3">
{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 (
<Card
key={example.url}
className="cursor-pointer transition-all duration-200 hover:shadow-md hover:scale-105 hover:border-primary/50 group w-full max-w-[350px]"
onClick={() => handleExampleClick(example)}
>
<CardContent className="p-4">
<div className="space-y-3">
<div className="flex items-center justify-between">
{searchScopes.map((searchScope) => (
<Badge key={searchScope.value} variant="secondary" className="text-[10px] px-1.5 py-0.5 h-4 flex items-center gap-1">
{getSearchScopeIcon(searchScope, 12)}
{searchScope.displayName}
</Badge>
))}
</div>
<div className="space-y-1">
<h4 className="font-semibold text-sm group-hover:text-primary transition-colors line-clamp-2">
{example.title}
</h4>
<p className="text-xs text-muted-foreground line-clamp-3 leading-relaxed">
{example.description}
</p>
</div>
</div>
</CardContent>
</Card>
)
})}
</div>
</div>
</>
</div>
);
};

View file

@ -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 (
<div className="flex flex-col items-center gap-3">
<span className="text-sm">
{`Search ${indexedRepos.length} `}
{`${indexedRepos.length} `}
<Link
href={`${domain}/repos`}
className="text-link hover:underline"
>
{indexedRepos.length > 1 ? 'repositories' : 'repository'}
</Link>
{` indexed`}
</span>
<RepositoryCarousel repos={indexedRepos} />
{process.env.NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT === "demo" && (
<p className="text-sm text-muted-foreground text-center">
Interested in using Sourcebot on your code? Check out our{' '}
<a
href="https://docs.sourcebot.dev/docs/overview"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
onClick={() => captureEvent('wa_demo_docs_link_pressed', {})}
>
docs
</a>
</p>
)}
</div>
)
}

View file

@ -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: <GitBranchIcon className="w-4 h-4" />,
},
{
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: <Brain className="w-4 h-4" />,
},
{
id: "authentication-system",
title: "Authentication System",

View file

@ -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<HTMLDivElement>(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 <Leaf {...props} />
}, []);
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
</Button>
) : (
<Button
variant={isSubmitDisabled ? "outline" : "default"}
size="sm"
className="w-6 h-6"
onClick={onSubmit}
disabled={isSubmitDisabled}
>
<ArrowUp className="w-4 h-4" />
</Button>
<Tooltip>
<TooltipTrigger asChild>
<div
onClick={() => {
// @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();
}
}}
>
<Button
variant={isSubmitDisabled ? "outline" : "default"}
size="sm"
className="w-6 h-6"
onClick={onSubmit}
disabled={isSubmitDisabled}
>
<ArrowUp className="w-4 h-4" />
</Button>
</div>
</TooltipTrigger>
{(isSubmitDisabled && isSubmitDisabledReason === "no-repos-selected") && (
<TooltipContent>
<div className="flex flex-row items-center">
<TriangleAlertIcon className="h-4 w-4 text-warning mr-1" />
<span className="text-destructive">You must select at least one search scope</span>
</div>
</TooltipContent>
)}
</Tooltip>
)}
</div>
{suggestionMode !== "none" && (

View file

@ -57,6 +57,7 @@ export const SearchScopeSelector = React.forwardRef<
) => {
const scrollContainerRef = React.useRef<HTMLDivElement>(null);
const scrollPosition = React.useRef<number>(0);
const [hasSearchInput, setHasSearchInput] = React.useState(false);
const handleInputKeyDown = (
event: React.KeyboardEvent<HTMLInputElement>
@ -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<
<CommandInput
placeholder="Search scopes..."
onKeyDown={handleInputKeyDown}
onValueChange={(value) => setHasSearchInput(!!value)}
/>
<CommandList ref={scrollContainerRef}>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup>
{!hasSearchInput && (
<div
onClick={handleSelectAll}
className="flex items-center px-2 py-1.5 text-sm text-muted-foreground hover:text-foreground cursor-pointer transition-colors"
>
<span className="text-xs">Select all</span>
</div>
)}
{sortedSearchScopeItems.map(({ item, isSelected }) => {
return (
<CommandItem

View file

@ -321,6 +321,7 @@ export const ChatThread = ({
languageModels={languageModels}
selectedSearchScopes={selectedSearchScopes}
searchContexts={searchContexts}
onContextSelectorOpenChanged={setIsContextSelectorOpen}
/>
<div className="w-full flex flex-row items-center bg-accent rounded-b-md px-2">
<ChatBoxToolbar

View file

@ -6,7 +6,7 @@ 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, ScanSearchIcon, Zap } from 'lucide-react';
import { Brain, ChevronDown, ChevronRight, Clock, Cpu, InfoIcon, Loader2, List, ScanSearchIcon, Zap } from 'lucide-react';
import { MarkdownRenderer } from './markdownRenderer';
import { FindSymbolDefinitionsToolComponent } from './tools/findSymbolDefinitionsToolComponent';
import { FindSymbolReferencesToolComponent } from './tools/findSymbolReferencesToolComponent';
@ -89,7 +89,7 @@ export const DetailsCard = ({
)}
{metadata?.modelName && (
<div className="flex items-center text-xs">
<Cpu className="w-3 h-3 mr-1 flex-shrink-0" />
<Brain className="w-3 h-3 mr-1 flex-shrink-0" />
{metadata?.modelName}
</div>
)}
@ -106,7 +106,7 @@ export const DetailsCard = ({
</div>
)}
<div className="flex items-center text-xs">
<Brain className="w-3 h-3 mr-1 flex-shrink-0" />
<List className="w-3 h-3 mr-1 flex-shrink-0" />
{`${thinkingSteps.length} step${thinkingSteps.length === 1 ? '' : 's'}`}
</div>
</>