feat(ask_sb): Add onboarding tutorial (#408)

This commit is contained in:
Brendan Kellam 2025-07-28 23:46:26 -07:00 committed by GitHub
parent 45416a41d1
commit 4343b3c3d5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 385 additions and 28 deletions

View file

@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add ability to include/exclude connection in search context. [#399](https://github.com/sourcebot-dev/sourcebot/pull/399) - 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) - Search context refactor to search scope and demo card UI changes. [#405](https://github.com/sourcebot-dev/sourcebot/pull/405)
- Add GitHub star toast. [#409](https://github.com/sourcebot-dev/sourcebot/pull/409) - Add GitHub star toast. [#409](https://github.com/sourcebot-dev/sourcebot/pull/409)
- Added a onboarding modal when first visiting the homepage when `ask` mode is selected. [#408](https://github.com/sourcebot-dev/sourcebot/pull/408)
### Fixed ### Fixed
- Fixed multiple writes race condition on config file watcher. [#398](https://github.com/sourcebot-dev/sourcebot/pull/398) - Fixed multiple writes race condition on config file watcher. [#398](https://github.com/sourcebot-dev/sourcebot/pull/398)

Binary file not shown.

After

Width:  |  Height:  |  Size: 414 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 KiB

View file

@ -25,7 +25,7 @@ import { auth } from "./auth";
import { getConnection } from "./data/connection"; import { getConnection } from "./data/connection";
import { IS_BILLING_ENABLED } from "./ee/features/billing/stripe"; import { IS_BILLING_ENABLED } from "./ee/features/billing/stripe";
import InviteUserEmail from "./emails/inviteUserEmail"; import InviteUserEmail from "./emails/inviteUserEmail";
import { MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME, SEARCH_MODE_COOKIE_NAME, SINGLE_TENANT_ORG_DOMAIN, SOURCEBOT_GUEST_USER_ID, SOURCEBOT_SUPPORT_EMAIL } from "./lib/constants"; import { AGENTIC_SEARCH_TUTORIAL_DISMISSED_COOKIE_NAME, MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME, SEARCH_MODE_COOKIE_NAME, SINGLE_TENANT_ORG_DOMAIN, SOURCEBOT_GUEST_USER_ID, SOURCEBOT_SUPPORT_EMAIL } from "./lib/constants";
import { orgDomainSchema, orgNameSchema, repositoryQuerySchema } from "./lib/schemas"; import { orgDomainSchema, orgNameSchema, repositoryQuerySchema } from "./lib/schemas";
import { TenancyMode, ApiKeyPayload } from "./lib/types"; import { TenancyMode, ApiKeyPayload } from "./lib/types";
import { decrementOrgSeatCount, getSubscriptionForOrg } from "./ee/features/billing/serverUtils"; import { decrementOrgSeatCount, getSubscriptionForOrg } from "./ee/features/billing/serverUtils";
@ -2015,6 +2015,13 @@ export async function setSearchModeCookie(searchMode: "precise" | "agentic") {
}); });
} }
export async function setAgenticSearchTutorialDismissedCookie(dismissed: boolean) {
const cookieStore = await cookies();
cookieStore.set(AGENTIC_SEARCH_TUTORIAL_DISMISSED_COOKIE_NAME, dismissed ? "true" : "false", {
httpOnly: false, // Allow client-side access
});
}
////// Helpers /////// ////// Helpers ///////
const parseConnectionConfig = (config: string) => { const parseConnectionConfig = (config: string) => {

View file

@ -6,11 +6,13 @@ import { ChatBoxToolbar } from "@/features/chat/components/chatBox/chatBoxToolba
import { LanguageModelInfo, SearchScope } from "@/features/chat/types"; import { LanguageModelInfo, SearchScope } from "@/features/chat/types";
import { useCreateNewChatThread } from "@/features/chat/useCreateNewChatThread"; import { useCreateNewChatThread } from "@/features/chat/useCreateNewChatThread";
import { RepositoryQuery, SearchContextQuery } from "@/lib/types"; import { RepositoryQuery, SearchContextQuery } from "@/lib/types";
import { useState } from "react"; import { useCallback, useState } from "react";
import { SearchModeSelector, SearchModeSelectorProps } from "./toolbar"; import { SearchModeSelector, SearchModeSelectorProps } from "./toolbar";
import { useLocalStorage } from "usehooks-ts"; import { useLocalStorage } from "usehooks-ts";
import { DemoExamples } from "@/types"; import { DemoExamples } from "@/types";
import { AskSourcebotDemoCards } from "./askSourcebotDemoCards"; import { AskSourcebotDemoCards } from "./askSourcebotDemoCards";
import { AgenticSearchTutorialDialog } from "./agenticSearchTutorialDialog";
import { setAgenticSearchTutorialDismissedCookie } from "@/actions";
interface AgenticSearchProps { interface AgenticSearchProps {
searchModeSelectorProps: SearchModeSelectorProps; searchModeSelectorProps: SearchModeSelectorProps;
@ -23,6 +25,7 @@ interface AgenticSearchProps {
name: string | null; name: string | null;
}[]; }[];
demoExamples: DemoExamples | undefined; demoExamples: DemoExamples | undefined;
isTutorialDismissed: boolean;
} }
export const AgenticSearch = ({ export const AgenticSearch = ({
@ -31,11 +34,18 @@ export const AgenticSearch = ({
repos, repos,
searchContexts, searchContexts,
demoExamples, demoExamples,
isTutorialDismissed,
}: AgenticSearchProps) => { }: AgenticSearchProps) => {
const { createNewChatThread, isLoading } = useCreateNewChatThread(); const { createNewChatThread, isLoading } = useCreateNewChatThread();
const [selectedSearchScopes, setSelectedSearchScopes] = useLocalStorage<SearchScope[]>("selectedSearchScopes", [], { initializeWithValue: false }); const [selectedSearchScopes, setSelectedSearchScopes] = useLocalStorage<SearchScope[]>("selectedSearchScopes", [], { initializeWithValue: false });
const [isContextSelectorOpen, setIsContextSelectorOpen] = useState(false); const [isContextSelectorOpen, setIsContextSelectorOpen] = useState(false);
const [isTutorialOpen, setIsTutorialOpen] = useState(!isTutorialDismissed);
const onTutorialDismissed = useCallback(() => {
setIsTutorialOpen(false);
setAgenticSearchTutorialDismissedCookie(true);
}, []);
return ( return (
<div className="flex flex-col items-center w-full"> <div className="flex flex-col items-center w-full">
<div className="mt-4 w-full border rounded-md shadow-sm max-w-[800px]"> <div className="mt-4 w-full border rounded-md shadow-sm max-w-[800px]">
@ -75,6 +85,12 @@ export const AgenticSearch = ({
demoExamples={demoExamples} demoExamples={demoExamples}
/> />
)} )}
{isTutorialOpen && (
<AgenticSearchTutorialDialog
onClose={onTutorialDismissed}
/>
)}
</div > </div >
) )
} }

View file

@ -0,0 +1,339 @@
"use client"
import { Button } from "@/components/ui/button"
import { Dialog, DialogContent } from "@/components/ui/dialog"
import { ModelProviderLogo } from "@/features/chat/components/chatBox/modelProviderLogo"
import { cn } from "@/lib/utils"
import mentionsDemo from "@/public/ask_sb_tutorial_at_mentions.png"
import citationsDemo from "@/public/ask_sb_tutorial_citations.png"
import searchScopeDemo from "@/public/ask_sb_tutorial_search_scope.png"
import logoDarkSmall from "@/public/sb_logo_dark_small.png"
import { useQuery } from "@tanstack/react-query"
import {
ArrowLeftRightIcon,
AtSignIcon,
BookMarkedIcon,
BookTextIcon,
ChevronLeft,
ChevronRight,
CircleCheckIcon,
FileIcon,
FolderIcon,
GitCommitHorizontalIcon,
LibraryBigIcon,
ScanSearchIcon,
StarIcon,
TicketIcon,
} from "lucide-react"
import Image from "next/image"
import Link from "next/link"
import { useState } from "react"
interface AgenticSearchTutorialDialogProps {
onClose: () => void
}
// Star button component that fetches GitHub star count
const GitHubStarButton = () => {
const { data: starCount, isLoading, isError } = useQuery({
queryKey: ['github-stars', 'sourcebot-dev/sourcebot'],
queryFn: async () => {
const response = await fetch('https://api.github.com/repos/sourcebot-dev/sourcebot')
if (!response.ok) {
throw new Error('Failed to fetch star count')
}
const data = await response.json()
return data.stargazers_count as number;
},
staleTime: 5 * 60 * 1000, // 5 minutes
gcTime: 30 * 60 * 1000, // 30 minutes
retry: 3,
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
})
const formatStarCount = (count: number) => {
if (count >= 1000) {
return `${(count / 1000).toFixed(1)}k`
}
return count.toString()
}
return (
<Button
variant="secondary"
size="lg"
className="flex items-center gap-2"
onClick={() => window.open('https://github.com/sourcebot-dev/sourcebot', '_blank')}
>
<StarIcon className="w-4 h-4" />
<span className="font-medium">
{
!isLoading && !isError && starCount ? `Star (${formatStarCount(starCount)})` : 'Star'
}
</span>
</Button>
)
}
const tutorialSteps = [
{
leftContent: (
<div className="flex flex-col h-full p-8 justify-between gap-4">
<div className="flex flex-col gap-6">
<h2 className="text-5xl font-bold leading-tight">
Ask Source<span className="text-[#851EE6]">bot.</span>
</h2>
<p className="text-lg">
Ask questions about your <span className="font-bold">entire codebase</span> in natural language.
Get back responses grounded in code with <span className="font-bold">inline citations</span>.
</p>
<p className="text-md text-muted-foreground">
Ask Sourcebot is an agentic search tool that can answer questions about your codebase by searching, reading files, navigating references, and more. Supports any <Link href="https://docs.sourcebot.dev/docs/configuration/language-model-providers" className="underline">compatible LLM.</Link>
</p>
</div>
<div className="space-y-3 mx-auto flex flex-wrap justify-center gap-4">
<div className="flex flex-wrap items-center gap-4">
<ModelProviderLogo provider="anthropic" />
<ModelProviderLogo provider="openai" />
<ModelProviderLogo provider="google-generative-ai" />
<ModelProviderLogo provider="amazon-bedrock" />
<ModelProviderLogo provider="azure" />
<ModelProviderLogo provider="deepseek" />
<ModelProviderLogo provider="mistral" />
<ModelProviderLogo provider="openrouter" />
<ModelProviderLogo provider="xai" />
</div>
</div>
</div>
),
rightContent: (
<video
src="https://storage.googleapis.com/sourcebot-assets/hero_final.mp4"
autoPlay
loop
muted
playsInline
className="w-full h-full object-cover"
/>
),
},
{
leftContent: (
<div className="flex flex-col h-full p-8 space-y-6">
<h2 className="text-3xl font-bold leading-tight flex items-center gap-2">
<ScanSearchIcon className="inline-block h-8 w-8 text-primary" />
Search Scopes
</h2>
<p className="text-lg">
{`When asking Sourcebot a question, you can select one or more scopes to focus the search.`}
</p>
<div className="flex flex-col mb-2 text-muted-foreground">
<p className="mb-4">There are two types of search scopes:</p>
<div className="flex gap-2 mb-2">
<BookMarkedIcon className="h-4 w-4 text-muted-foreground flex-shrink-0 mt-1" />
<span><strong>Repository</strong>: A single repository.</span>
</div>
<div className="flex gap-2">
<LibraryBigIcon className="h-4 w-4 text-muted-foreground flex-shrink-0 mt-1" />
<span><strong>Reposet</strong>: A collection of repositories (<Link href="https://docs.sourcebot.dev/docs/features/search/search-contexts" className="underline">configuration docs</Link>).</span>
</div>
</div>
</div>
),
rightContent: (
<Image
src={searchScopeDemo}
alt="Search scope demo"
className="w-full h-full object-cover"
/>
),
},
{
leftContent: (
<div className="flex flex-col h-full p-8 space-y-6">
<h2 className="text-3xl font-bold leading-tight flex items-center gap-2">
<AtSignIcon className="inline-block h-8 w-8 text-primary" />
Mentions
</h2>
<p className="text-lg">
@ mention specific <FileIcon className="inline-block h-4 w-4 mb-1 ml-0.5" /> files to add them to the {`model's`} context. Suggestions will be scoped to the selected search scopes.
</p>
<div className="flex flex-col">
<p className="mb-3 text-muted-foreground"><strong>Coming soon</strong></p>
<div className="space-y-2 text-muted-foreground">
<div className="flex gap-2">
<FolderIcon className="h-4 w-4 flex-shrink-0 mt-1" />
<span><strong>Directories</strong>: Include entire folders as context</span>
</div>
<div className="flex gap-2">
<GitCommitHorizontalIcon className="h-4 w-4 flex-shrink-0 mt-1" />
<span><strong>Commits</strong>: Reference specific git commits</span>
</div>
<div className="flex gap-2">
<BookTextIcon className="h-4 w-4 flex-shrink-0 mt-1" />
<span><strong>Docs</strong>: Link to external docs and wikis</span>
</div>
<div className="flex gap-2">
<TicketIcon className="h-4 w-4 flex-shrink-0 mt-1" />
<span><strong>Issues</strong>: GitHub issues, Jira tickets, and more</span>
</div>
</div>
</div>
</div>
),
rightContent: (
<Image
src={mentionsDemo}
alt="Mentions demo"
className="w-full h-full object-cover"
/>
),
},
{
leftContent: (
<div className="flex flex-col h-full p-8 space-y-6">
<h2 className="text-3xl font-bold leading-tight flex items-center gap-2">
<ArrowLeftRightIcon className="inline-block h-8 w-8 text-primary" />
Inline Citations
</h2>
<p className="text-lg">
{`Sourcebot searches your codebase and provides responses with clickable citations that link directly to relevant sections of code.`}
</p>
</div>
),
rightContent: (
<Image
src={citationsDemo}
alt="Citations demo"
className="w-full h-full object-cover"
/>
),
},
{
leftContent: (
<div className="flex flex-col h-full p-8 space-y-6">
<h2 className="text-3xl font-bold leading-tight flex items-center gap-2">
<CircleCheckIcon className="inline-block h-8 w-8 text-primary" />
You&apos;re all set!
</h2>
<p className="text-lg">
You can now ask Sourcebot any question about your codebase. Checkout the <Link href="https://docs.sourcebot.dev/docs/features/ask/overview" className="underline">docs</Link> for more information.
</p>
<p className="text-lg">
<span className="font-bold">Hit a bug?</span> Open up <Link href="https://github.com/sourcebot-dev/sourcebot/issues" className="underline">an issue</Link>.
</p>
<p className="text-lg">
<span className="font-bold">Feature request?</span> Open a <Link href="https://github.com/sourcebot-dev/sourcebot/discussions" className="underline">discussion</Link>.
</p>
<p className="text-lg">
<span className="font-bold">Anything else?</span> <Link href="https://www.sourcebot.dev/contact" className="underline">Contact us</Link>.
</p>
</div>
),
rightContent: (
<div className="flex flex-col h-full justify-center items-center gap-6 bg-[#020817]">
<Image
src={logoDarkSmall}
width={150}
height={150}
alt={"Sourcebot logo"}
priority={true}
/>
<GitHubStarButton />
</div>
),
},
]
export const AgenticSearchTutorialDialog = ({ onClose }: AgenticSearchTutorialDialogProps) => {
const [currentStep, setCurrentStep] = useState(0)
const nextStep = () => {
if (currentStep < tutorialSteps.length - 1) {
setCurrentStep(currentStep + 1)
}
}
const prevStep = () => {
if (currentStep > 0) {
setCurrentStep(currentStep - 1)
}
}
const isLastStep = currentStep === tutorialSteps.length - 1
const isFirstStep = currentStep === 0
const currentStepData = tutorialSteps[currentStep];
return (
<Dialog open={true} onOpenChange={onClose}>
<DialogContent
className="sm:max-w-[900px] p-0 flex flex-col h-[525px] overflow-hidden rounded-xl border-none bg-transparent"
closeButtonClassName="text-white"
>
<div className="relative flex h-full">
{/* Left Column (Text Content & Navigation) */}
<div className="flex-1 flex flex-col justify-between bg-background">
<div className="p-4 flex-1 overflow-y-auto">
{currentStepData.leftContent}
</div>
{/* Fixed bottom navigation for left column */}
<div className="border-t p-6 flex items-center justify-between">
{/* Left side: Previous button container */}
<div className="w-36 flex justify-start">
<Button
variant="ghost"
onClick={prevStep}
className={cn(
"flex items-center gap-2",
isFirstStep && "opacity-0 pointer-events-none"
)}
>
<ChevronLeft className="w-4 h-4" />
Previous
</Button>
</div>
{/* Center: Progress dots */}
<div className="flex gap-2">
{tutorialSteps.map((_, index) => (
<div
key={index}
className={cn(
"w-2 h-2 rounded-full transition-colors",
index === currentStep ? "bg-primary" : "bg-muted"
)}
/>
))}
</div>
{/* Right side: Next/Start/Get Started button container */}
<div className="w-36 flex justify-end">
{isLastStep ? (
<Button onClick={onClose}>
Get Started
</Button>
) : (
<Button onClick={nextStep}>
Next
<ChevronRight className="w-4 h-4" />
</Button>
)}
</div>
</div>
</div>
{/* Right Column (Image/Visual Content) */}
<div className="flex-1 flex flex-col justify-between">
<div className="flex-1 overflow-y-auto">{currentStepData.rightContent}</div>
</div>
</div>
</DialogContent>
</Dialog>
)
}

View file

@ -9,7 +9,7 @@ import { CardContent } from "@/components/ui/card";
import { DemoExamples, DemoSearchExample, DemoSearchScope } from "@/types"; import { DemoExamples, DemoSearchExample, DemoSearchScope } from "@/types";
import { cn, getCodeHostIcon } from "@/lib/utils"; import { cn, getCodeHostIcon } from "@/lib/utils";
import useCaptureEvent from "@/hooks/useCaptureEvent"; import useCaptureEvent from "@/hooks/useCaptureEvent";
import { SearchScopeInfoCard } from "@/components/searchScopeInfoCard"; import { SearchScopeInfoCard } from "@/features/chat/components/chatBox/searchScopeInfoCard";
interface AskSourcebotDemoCardsProps { interface AskSourcebotDemoCardsProps {
demoExamples: DemoExamples; demoExamples: DemoExamples;

View file

@ -23,6 +23,7 @@ interface HomepageProps {
}[]; }[];
initialSearchMode: SearchMode; initialSearchMode: SearchMode;
demoExamples: DemoExamples | undefined; demoExamples: DemoExamples | undefined;
isAgenticSearchTutorialDismissed: boolean;
} }
@ -33,6 +34,7 @@ export const Homepage = ({
chatHistory, chatHistory,
initialSearchMode, initialSearchMode,
demoExamples, demoExamples,
isAgenticSearchTutorialDismissed,
}: HomepageProps) => { }: HomepageProps) => {
const [searchMode, setSearchMode] = useState<SearchMode>(initialSearchMode); const [searchMode, setSearchMode] = useState<SearchMode>(initialSearchMode);
const isAgenticSearchEnabled = languageModels.length > 0; const isAgenticSearchEnabled = languageModels.length > 0;
@ -90,6 +92,7 @@ export const Homepage = ({
searchContexts={searchContexts} searchContexts={searchContexts}
chatHistory={chatHistory} chatHistory={chatHistory}
demoExamples={demoExamples} demoExamples={demoExamples}
isTutorialDismissed={isAgenticSearchTutorialDismissed}
/> />
</CustomSlateEditor> </CustomSlateEditor>
)} )}

View file

@ -10,7 +10,7 @@ import { UpgradeToast } from "./components/upgradeToast";
import { ServiceErrorException } from "@/lib/serviceError"; import { ServiceErrorException } from "@/lib/serviceError";
import { auth } from "@/auth"; import { auth } from "@/auth";
import { cookies } from "next/headers"; import { cookies } from "next/headers";
import { SEARCH_MODE_COOKIE_NAME } from "@/lib/constants"; import { AGENTIC_SEARCH_TUTORIAL_DISMISSED_COOKIE_NAME, SEARCH_MODE_COOKIE_NAME } from "@/lib/constants";
import { env } from "@/env.mjs"; import { env } from "@/env.mjs";
import { loadJsonFile } from "@sourcebot/shared"; import { loadJsonFile } from "@sourcebot/shared";
import { DemoExamples, demoExamplesSchema } from "@/types"; import { DemoExamples, demoExamplesSchema } from "@/types";
@ -51,6 +51,8 @@ export default async function Home({ params: { domain } }: { params: { domain: s
searchModeCookie?.value === "precise" searchModeCookie?.value === "precise"
) ? searchModeCookie.value : models.length > 0 ? "agentic" : "precise"; ) ? searchModeCookie.value : models.length > 0 ? "agentic" : "precise";
const isAgenticSearchTutorialDismissed = cookieStore.get(AGENTIC_SEARCH_TUTORIAL_DISMISSED_COOKIE_NAME)?.value === "true";
const demoExamples = env.SOURCEBOT_DEMO_EXAMPLES_PATH ? await (async () => { const demoExamples = env.SOURCEBOT_DEMO_EXAMPLES_PATH ? await (async () => {
try { try {
return await loadJsonFile<DemoExamples>(env.SOURCEBOT_DEMO_EXAMPLES_PATH!, demoExamplesSchema); return await loadJsonFile<DemoExamples>(env.SOURCEBOT_DEMO_EXAMPLES_PATH!, demoExamplesSchema);
@ -74,6 +76,7 @@ export default async function Home({ params: { domain } }: { params: { domain: s
chatHistory={chatHistory} chatHistory={chatHistory}
initialSearchMode={initialSearchMode} initialSearchMode={initialSearchMode}
demoExamples={demoExamples} demoExamples={demoExamples}
isAgenticSearchTutorialDismissed={isAgenticSearchTutorialDismissed}
/> />
<Footer /> <Footer />
</div> </div>

View file

@ -31,8 +31,10 @@ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef< const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>, React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
>(({ className, children, ...props }, ref) => ( closeButtonClassName?: string
}
>(({ className, children, closeButtonClassName, ...props }, ref) => (
<DialogPortal> <DialogPortal>
<DialogOverlay /> <DialogOverlay />
<DialogPrimitive.Content <DialogPrimitive.Content
@ -45,7 +47,7 @@ const DialogContent = React.forwardRef<
> >
{children} {children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"> <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" /> <X className={cn("h-4 w-4", closeButtonClassName)} />
<span className="sr-only">Close</span> <span className="sr-only">Close</span>
</DialogPrimitive.Close> </DialogPrimitive.Close>
</DialogPrimitive.Content> </DialogPrimitive.Content>

View file

@ -283,7 +283,7 @@ export const ChatBox = ({
> >
<Editable <Editable
className="w-full focus-visible:outline-none focus-visible:ring-0 bg-background text-base disabled:cursor-not-allowed disabled:opacity-50 md:text-sm" className="w-full focus-visible:outline-none focus-visible:ring-0 bg-background text-base disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
placeholder="Ask a question about the selected search scopes. @mention files to refine your query." placeholder="Ask a question about your code. @mention files or select search scopes to refine your query."
renderElement={renderElement} renderElement={renderElement}
renderLeaf={renderLeaf} renderLeaf={renderLeaf}
onKeyDown={onKeyDown} onKeyDown={onKeyDown}

View file

@ -11,8 +11,8 @@ import { ReactEditor, useSlate } from "slate-react";
import { useSelectedLanguageModel } from "../../useSelectedLanguageModel"; import { useSelectedLanguageModel } from "../../useSelectedLanguageModel";
import { LanguageModelSelector } from "./languageModelSelector"; import { LanguageModelSelector } from "./languageModelSelector";
import { SearchScopeSelector } from "./searchScopeSelector"; import { SearchScopeSelector } from "./searchScopeSelector";
import { SearchScopeInfoCard } from "@/components/searchScopeInfoCard"; import { SearchScopeInfoCard } from "@/features/chat/components/chatBox/searchScopeInfoCard";
import { AtMentionInfoCard } from "@/components/atMentionInfoCard"; import { AtMentionInfoCard } from "@/features/chat/components/chatBox/atMentionInfoCard";
export interface ChatBoxToolbarProps { export interface ChatBoxToolbarProps {
languageModels: LanguageModelInfo[]; languageModels: LanguageModelInfo[];

View file

@ -1,6 +1,4 @@
import Image from "next/image"; import { BookMarkedIcon, LibraryBigIcon, ScanSearchIcon } from "lucide-react";
import { LibraryBigIcon, Code, ScanSearchIcon } from "lucide-react";
import { cn, getCodeHostIcon } from "@/lib/utils";
export const SearchScopeInfoCard = () => { export const SearchScopeInfoCard = () => {
return ( return (
@ -10,24 +8,11 @@ export const SearchScopeInfoCard = () => {
<h4 className="text-sm font-semibold text-popover-foreground">Search Scope</h4> <h4 className="text-sm font-semibold text-popover-foreground">Search Scope</h4>
</div> </div>
<div className="text-sm text-popover-foreground leading-relaxed"> <div className="text-sm text-popover-foreground leading-relaxed">
When asking Sourcebot a question, you can select one or more scopes to constrain the search. When asking Sourcebot a question, you can select one or more scopes to focus the search.
There are two different types of search scopes: There are two different types of search scopes:
<div className="mt-3 space-y-2"> <div className="mt-3 space-y-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{(() => { <BookMarkedIcon className="h-4 w-4 text-muted-foreground flex-shrink-0" />
const githubIcon = getCodeHostIcon("github");
return githubIcon ? (
<Image
src={githubIcon.src}
alt="GitHub icon"
width={16}
height={16}
className={cn("h-4 w-4 flex-shrink-0", githubIcon.className)}
/>
) : (
<Code className="h-4 w-4 text-muted-foreground flex-shrink-0" />
);
})()}
<span><strong>Repository</strong>: A single repository, indicated by the code host icon.</span> <span><strong>Repository</strong>: A single repository, indicated by the code host icon.</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">

View file

@ -24,6 +24,7 @@ export const TEAM_FEATURES = [
export const MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME = 'sb.mobile-unsupported-splash-screen-dismissed'; export const MOBILE_UNSUPPORTED_SPLASH_SCREEN_DISMISSED_COOKIE_NAME = 'sb.mobile-unsupported-splash-screen-dismissed';
export const SEARCH_MODE_COOKIE_NAME = 'sb.search-mode'; export const SEARCH_MODE_COOKIE_NAME = 'sb.search-mode';
export const AGENTIC_SEARCH_TUTORIAL_DISMISSED_COOKIE_NAME = 'sb.agentic-search-tutorial-dismissed';
// NOTE: changing SOURCEBOT_GUEST_USER_ID may break backwards compatibility since this value is used // NOTE: changing SOURCEBOT_GUEST_USER_ID may break backwards compatibility since this value is used
// to detect old guest users in the DB. If you change this value ensure it doesn't break upgrade flows // to detect old guest users in the DB. If you change this value ensure it doesn't break upgrade flows