diff --git a/packages/web/src/app/[domain]/components/homepage/askSourcebotDemoCards.tsx b/packages/web/src/app/[domain]/components/homepage/askSourcebotDemoCards.tsx index e8bb264c..743a1fc6 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 */} -
-
-
- -

Search Contexts

-
-

Select the context you want to ask questions about

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

- {context.displayName} -

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

{contextExample.description}

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

Community Ask Results

+
+
+ +

Community Ask Results

-

Check out these featured ask results from the community

+ + {/* Search Context Filter */} +
+
+
+ +
+ +
+
+
+ Search Context: +
+ { + 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} - - ))} + {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} +

+
-
-

- {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 00000000..84083548 --- /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 8744e9ac..e32daaa5 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 && (