From c1467bcd827431beef61c0bf17e64fe2669d1564 Mon Sep 17 00:00:00 2001 From: bkellam Date: Wed, 15 Oct 2025 22:39:52 -0700 Subject: [PATCH] Various improvements and optimizations on the web side --- packages/web/src/actions.ts | 78 +++- .../components/pureTreePreviewPanel.tsx | 2 +- .../browse/hooks/useBrowseNavigation.ts | 34 +- .../[domain]/browse/hooks/useBrowsePath.ts | 3 +- .../src/app/[domain]/browse/hooks/utils.ts | 30 +- .../components/demoCards.tsx} | 6 +- .../components/landingPageChatBox.tsx} | 57 +-- .../[domain]/chat/components/newChatPanel.tsx | 72 ---- .../components/tutorialDialog.tsx} | 24 +- packages/web/src/app/[domain]/chat/layout.tsx | 5 + packages/web/src/app/[domain]/chat/page.tsx | 118 ++++-- .../[domain]/components/errorNavIndicator.tsx | 137 ------ .../[domain]/components/homepage/index.tsx | 102 ----- .../components/homepage/preciseSearch.tsx | 145 ------- .../homepage/repositoryCarousel.tsx | 105 ----- .../homepage/repositorySnapshot.tsx | 156 ------- .../index.tsx} | 125 ++++-- .../navigationMenu/progressIndicator.tsx | 118 ++++++ .../trialIndicator.tsx} | 2 +- .../app/[domain]/components/pathHeader.tsx | 2 +- .../components/progressNavIndicator.tsx | 73 ---- .../components/repositoryCarousel.tsx | 158 +++++++ .../toolbar.tsx => searchModeSelector.tsx} | 54 ++- .../components/warningNavIndicator.tsx | 79 ---- .../connections/[id]/components/repoList.tsx | 2 +- packages/web/src/app/[domain]/layout.tsx | 2 + packages/web/src/app/[domain]/page.tsx | 104 +---- .../web/src/app/[domain]/repos/columns.tsx | 2 +- .../search/components/searchLandingPage.tsx | 170 ++++++++ .../search/components/searchResultsPage.tsx | 372 +++++++++++++++++ .../searchResultsPanel/fileMatch.tsx | 2 +- packages/web/src/app/[domain]/search/page.tsx | 389 +----------------- .../web/src/app/[domain]/settings/layout.tsx | 4 + .../web/src/components/ui/navigation-menu.tsx | 2 +- .../components/exploreMenu/referenceList.tsx | 2 +- .../components/chatThread/tools/shared.tsx | 2 +- .../fileTree/components/pureFileTreePanel.tsx | 4 +- packages/web/src/lib/utils.ts | 13 + packages/web/tailwind.config.ts | 3 +- 39 files changed, 1200 insertions(+), 1558 deletions(-) rename packages/web/src/app/[domain]/{components/homepage/askSourcebotDemoCards.tsx => chat/components/demoCards.tsx} (98%) rename packages/web/src/app/[domain]/{components/homepage/agenticSearch.tsx => chat/components/landingPageChatBox.tsx} (59%) delete mode 100644 packages/web/src/app/[domain]/chat/components/newChatPanel.tsx rename packages/web/src/app/[domain]/{components/homepage/agenticSearchTutorialDialog.tsx => chat/components/tutorialDialog.tsx} (95%) delete mode 100644 packages/web/src/app/[domain]/components/errorNavIndicator.tsx delete mode 100644 packages/web/src/app/[domain]/components/homepage/index.tsx delete mode 100644 packages/web/src/app/[domain]/components/homepage/preciseSearch.tsx delete mode 100644 packages/web/src/app/[domain]/components/homepage/repositoryCarousel.tsx delete mode 100644 packages/web/src/app/[domain]/components/homepage/repositorySnapshot.tsx rename packages/web/src/app/[domain]/components/{navigationMenu.tsx => navigationMenu/index.tsx} (56%) create mode 100644 packages/web/src/app/[domain]/components/navigationMenu/progressIndicator.tsx rename packages/web/src/app/[domain]/components/{trialNavIndicator.tsx => navigationMenu/trialIndicator.tsx} (95%) delete mode 100644 packages/web/src/app/[domain]/components/progressNavIndicator.tsx create mode 100644 packages/web/src/app/[domain]/components/repositoryCarousel.tsx rename packages/web/src/app/[domain]/components/{homepage/toolbar.tsx => searchModeSelector.tsx} (83%) delete mode 100644 packages/web/src/app/[domain]/components/warningNavIndicator.tsx create mode 100644 packages/web/src/app/[domain]/search/components/searchLandingPage.tsx create mode 100644 packages/web/src/app/[domain]/search/components/searchResultsPage.tsx diff --git a/packages/web/src/actions.ts b/packages/web/src/actions.ts index 5b73922c..3bfcee35 100644 --- a/packages/web/src/actions.ts +++ b/packages/web/src/actions.ts @@ -10,7 +10,7 @@ import { prisma } from "@/prisma"; import { render } from "@react-email/components"; import * as Sentry from '@sentry/nextjs'; import { decrypt, encrypt, generateApiKey, getTokenFromConfig, hashSecret } from "@sourcebot/crypto"; -import { ApiKey, ConnectionSyncStatus, Org, OrgRole, Prisma, RepoIndexingStatus, StripeSubscriptionStatus } from "@sourcebot/db"; +import { ApiKey, ConnectionSyncStatus, Org, OrgRole, Prisma, RepoIndexingStatus, RepoJobStatus, RepoJobType, StripeSubscriptionStatus } from "@sourcebot/db"; import { createLogger } from "@sourcebot/logger"; import { azuredevopsSchema } from "@sourcebot/schemas/v3/azuredevops.schema"; import { bitbucketSchema } from "@sourcebot/schemas/v3/bitbucket.schema"; @@ -638,22 +638,20 @@ export const getConnectionInfo = async (connectionId: number, domain: string) => } }))); -export const getRepos = async (filter: { status?: RepoIndexingStatus[], connectionId?: number } = {}) => sew(() => +export const getRepos = async ({ + where, + take, +}: { + where?: Prisma.RepoWhereInput, + take?: number +} = {}) => sew(() => withOptionalAuthV2(async ({ org, prisma }) => { const repos = await prisma.repo.findMany({ where: { orgId: org.id, - ...(filter.status ? { - repoIndexingStatus: { in: filter.status } - } : {}), - ...(filter.connectionId ? { - connections: { - some: { - connectionId: filter.connectionId - } - } - } : {}), - } + ...where, + }, + take, }); return repos.map((repo) => repositoryQuerySchema.parse({ @@ -669,6 +667,60 @@ export const getRepos = async (filter: { status?: RepoIndexingStatus[], connecti })) })); +/** + * Returns a set of aggregated stats about the repos in the org + */ +export const getReposStats = async () => sew(() => + withOptionalAuthV2(async ({ org, prisma }) => { + const [ + // Total number of repos. + numberOfRepos, + // Number of repos with their first time indexing jobs either + // pending or in progress. + numberOfReposWithFirstTimeIndexingJobsInProgress, + // Number of repos that have been indexed at least once. + numberOfReposWithIndex, + ] = await Promise.all([ + prisma.repo.count({ + where: { + orgId: org.id, + } + }), + prisma.repo.count({ + where: { + orgId: org.id, + jobs: { + some: { + type: RepoJobType.INDEX, + status: { + in: [ + RepoJobStatus.PENDING, + RepoJobStatus.IN_PROGRESS, + ] + } + }, + }, + indexedAt: null, + } + }), + prisma.repo.count({ + where: { + orgId: org.id, + NOT: { + indexedAt: null, + } + } + }) + ]); + + return { + numberOfRepos, + numberOfReposWithFirstTimeIndexingJobsInProgress, + numberOfReposWithIndex, + }; + }) +) + export const getRepoInfoByName = async (repoName: string) => sew(() => withOptionalAuthV2(async ({ org, prisma }) => { // @note: repo names are represented by their remote url diff --git a/packages/web/src/app/[domain]/browse/[...path]/components/pureTreePreviewPanel.tsx b/packages/web/src/app/[domain]/browse/[...path]/components/pureTreePreviewPanel.tsx index cdb8f3ca..83c9528e 100644 --- a/packages/web/src/app/[domain]/browse/[...path]/components/pureTreePreviewPanel.tsx +++ b/packages/web/src/app/[domain]/browse/[...path]/components/pureTreePreviewPanel.tsx @@ -3,7 +3,7 @@ import { useRef } from "react"; import { FileTreeItem } from "@/features/fileTree/actions"; import { FileTreeItemComponent } from "@/features/fileTree/components/fileTreeItemComponent"; -import { getBrowsePath } from "../../hooks/useBrowseNavigation"; +import { getBrowsePath } from "../../hooks/utils"; import { ScrollArea } from "@/components/ui/scroll-area"; import { useBrowseParams } from "../../hooks/useBrowseParams"; import { useDomain } from "@/hooks/useDomain"; diff --git a/packages/web/src/app/[domain]/browse/hooks/useBrowseNavigation.ts b/packages/web/src/app/[domain]/browse/hooks/useBrowseNavigation.ts index 0d79170e..c798a64c 100644 --- a/packages/web/src/app/[domain]/browse/hooks/useBrowseNavigation.ts +++ b/packages/web/src/app/[domain]/browse/hooks/useBrowseNavigation.ts @@ -3,7 +3,8 @@ import { useRouter } from "next/navigation"; import { useDomain } from "@/hooks/useDomain"; import { useCallback } from "react"; -import { BrowseState, SET_BROWSE_STATE_QUERY_PARAM } from "../browseStateProvider"; +import { BrowseState } from "../browseStateProvider"; +import { getBrowsePath } from "./utils"; export type BrowseHighlightRange = { start: { lineNumber: number; column: number; }; @@ -25,37 +26,6 @@ export interface GetBrowsePathProps { domain: string; } -export const getBrowsePath = ({ - repoName, - revisionName = 'HEAD', - path, - pathType, - highlightRange, - setBrowseState, - domain, -}: GetBrowsePathProps) => { - const params = new URLSearchParams(); - - if (highlightRange) { - const { start, end } = highlightRange; - - if ('column' in start && 'column' in end) { - params.set(HIGHLIGHT_RANGE_QUERY_PARAM, `${start.lineNumber}:${start.column},${end.lineNumber}:${end.column}`); - } else { - params.set(HIGHLIGHT_RANGE_QUERY_PARAM, `${start.lineNumber},${end.lineNumber}`); - } - } - - if (setBrowseState) { - params.set(SET_BROWSE_STATE_QUERY_PARAM, JSON.stringify(setBrowseState)); - } - - const encodedPath = encodeURIComponent(path); - const browsePath = `/${domain}/browse/${repoName}@${revisionName}/-/${pathType}/${encodedPath}${params.size > 0 ? `?${params.toString()}` : ''}`; - return browsePath; -} - - export const useBrowseNavigation = () => { const router = useRouter(); const domain = useDomain(); diff --git a/packages/web/src/app/[domain]/browse/hooks/useBrowsePath.ts b/packages/web/src/app/[domain]/browse/hooks/useBrowsePath.ts index fcf29be8..d612a75c 100644 --- a/packages/web/src/app/[domain]/browse/hooks/useBrowsePath.ts +++ b/packages/web/src/app/[domain]/browse/hooks/useBrowsePath.ts @@ -1,7 +1,8 @@ 'use client'; import { useMemo } from "react"; -import { getBrowsePath, GetBrowsePathProps } from "./useBrowseNavigation"; +import { GetBrowsePathProps } from "./useBrowseNavigation"; +import { getBrowsePath } from "./utils"; import { useDomain } from "@/hooks/useDomain"; export const useBrowsePath = ({ diff --git a/packages/web/src/app/[domain]/browse/hooks/utils.ts b/packages/web/src/app/[domain]/browse/hooks/utils.ts index ba3214fb..5e10b6d8 100644 --- a/packages/web/src/app/[domain]/browse/hooks/utils.ts +++ b/packages/web/src/app/[domain]/browse/hooks/utils.ts @@ -1,3 +1,5 @@ +import { SET_BROWSE_STATE_QUERY_PARAM } from "../browseStateProvider"; +import { GetBrowsePathProps, HIGHLIGHT_RANGE_QUERY_PARAM } from "./useBrowseNavigation"; export const getBrowseParamsFromPathParam = (pathParam: string) => { const sentinelIndex = pathParam.search(/\/-\/(tree|blob)/); @@ -7,7 +9,7 @@ export const getBrowseParamsFromPathParam = (pathParam: string) => { const repoAndRevisionPart = decodeURIComponent(pathParam.substring(0, sentinelIndex)); const lastAtIndex = repoAndRevisionPart.lastIndexOf('@'); - + const repoName = lastAtIndex === -1 ? repoAndRevisionPart : repoAndRevisionPart.substring(0, lastAtIndex); const revisionName = lastAtIndex === -1 ? undefined : repoAndRevisionPart.substring(lastAtIndex + 1); @@ -40,4 +42,28 @@ export const getBrowseParamsFromPathParam = (pathParam: string) => { path, pathType, } -} \ No newline at end of file +}; + +export const getBrowsePath = ({ + repoName, revisionName = 'HEAD', path, pathType, highlightRange, setBrowseState, domain, +}: GetBrowsePathProps) => { + const params = new URLSearchParams(); + + if (highlightRange) { + const { start, end } = highlightRange; + + if ('column' in start && 'column' in end) { + params.set(HIGHLIGHT_RANGE_QUERY_PARAM, `${start.lineNumber}:${start.column},${end.lineNumber}:${end.column}`); + } else { + params.set(HIGHLIGHT_RANGE_QUERY_PARAM, `${start.lineNumber},${end.lineNumber}`); + } + } + + if (setBrowseState) { + params.set(SET_BROWSE_STATE_QUERY_PARAM, JSON.stringify(setBrowseState)); + } + + const encodedPath = encodeURIComponent(path); + const browsePath = `/${domain}/browse/${repoName}@${revisionName}/-/${pathType}/${encodedPath}${params.size > 0 ? `?${params.toString()}` : ''}`; + return browsePath; +}; diff --git a/packages/web/src/app/[domain]/components/homepage/askSourcebotDemoCards.tsx b/packages/web/src/app/[domain]/chat/components/demoCards.tsx similarity index 98% rename from packages/web/src/app/[domain]/components/homepage/askSourcebotDemoCards.tsx rename to packages/web/src/app/[domain]/chat/components/demoCards.tsx index 31037607..016d9605 100644 --- a/packages/web/src/app/[domain]/components/homepage/askSourcebotDemoCards.tsx +++ b/packages/web/src/app/[domain]/chat/components/demoCards.tsx @@ -11,13 +11,13 @@ import { cn, getCodeHostIcon } from "@/lib/utils"; import useCaptureEvent from "@/hooks/useCaptureEvent"; import { SearchScopeInfoCard } from "@/features/chat/components/chatBox/searchScopeInfoCard"; -interface AskSourcebotDemoCardsProps { +interface DemoCards { demoExamples: DemoExamples; } -export const AskSourcebotDemoCards = ({ +export const DemoCards = ({ demoExamples, -}: AskSourcebotDemoCardsProps) => { +}: DemoCards) => { const captureEvent = useCaptureEvent(); const [selectedFilterSearchScope, setSelectedFilterSearchScope] = useState(null); diff --git a/packages/web/src/app/[domain]/components/homepage/agenticSearch.tsx b/packages/web/src/app/[domain]/chat/components/landingPageChatBox.tsx similarity index 59% rename from packages/web/src/app/[domain]/components/homepage/agenticSearch.tsx rename to packages/web/src/app/[domain]/chat/components/landingPageChatBox.tsx index 327cd297..b2080df3 100644 --- a/packages/web/src/app/[domain]/components/homepage/agenticSearch.tsx +++ b/packages/web/src/app/[domain]/chat/components/landingPageChatBox.tsx @@ -6,47 +6,24 @@ import { ChatBoxToolbar } from "@/features/chat/components/chatBox/chatBoxToolba import { LanguageModelInfo, SearchScope } from "@/features/chat/types"; import { useCreateNewChatThread } from "@/features/chat/useCreateNewChatThread"; import { RepositoryQuery, SearchContextQuery } from "@/lib/types"; -import { useCallback, useState } from "react"; -import { SearchModeSelector, SearchModeSelectorProps } from "./toolbar"; +import { useState } from "react"; import { useLocalStorage } from "usehooks-ts"; -import { DemoExamples } from "@/types"; -import { AskSourcebotDemoCards } from "./askSourcebotDemoCards"; -import { AgenticSearchTutorialDialog } from "./agenticSearchTutorialDialog"; -import { setAgenticSearchTutorialDismissedCookie } from "@/actions"; -import { RepositorySnapshot } from "./repositorySnapshot"; +import { SearchModeSelector } from "../../components/searchModeSelector"; -interface AgenticSearchProps { - searchModeSelectorProps: SearchModeSelectorProps; +interface LandingPageChatBox { languageModels: LanguageModelInfo[]; repos: RepositoryQuery[]; searchContexts: SearchContextQuery[]; - chatHistory: { - id: string; - createdAt: Date; - name: string | null; - }[]; - demoExamples: DemoExamples | undefined; - isTutorialDismissed: boolean; } -export const AgenticSearch = ({ - searchModeSelectorProps, +export const LandingPageChatBox = ({ languageModels, repos, searchContexts, - demoExamples, - isTutorialDismissed, -}: AgenticSearchProps) => { +}: LandingPageChatBox) => { const { createNewChatThread, isLoading } = useCreateNewChatThread(); const [selectedSearchScopes, setSelectedSearchScopes] = useLocalStorage("selectedSearchScopes", [], { initializeWithValue: false }); const [isContextSelectorOpen, setIsContextSelectorOpen] = useState(false); - - const [isTutorialOpen, setIsTutorialOpen] = useState(!isTutorialDismissed); - const onTutorialDismissed = useCallback(() => { - setIsTutorialOpen(false); - setAgenticSearchTutorialDismissedCookie(true); - }, []); - return (
@@ -74,34 +51,12 @@ export const AgenticSearch = ({ onContextSelectorOpenChanged={setIsContextSelectorOpen} />
- -
- -
- -
- -
- - {demoExamples && ( - - )} - - {isTutorialOpen && ( - - )} ) } \ No newline at end of file diff --git a/packages/web/src/app/[domain]/chat/components/newChatPanel.tsx b/packages/web/src/app/[domain]/chat/components/newChatPanel.tsx deleted file mode 100644 index 91ae3f96..00000000 --- a/packages/web/src/app/[domain]/chat/components/newChatPanel.tsx +++ /dev/null @@ -1,72 +0,0 @@ -'use client'; - -import { ResizablePanel } from "@/components/ui/resizable"; -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, 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"; - -interface NewChatPanelProps { - languageModels: LanguageModelInfo[]; - repos: RepositoryQuery[]; - searchContexts: SearchContextQuery[]; - order: number; -} - -export const NewChatPanel = ({ - languageModels, - repos, - searchContexts, - order, -}: NewChatPanelProps) => { - const [selectedSearchScopes, setSelectedSearchScopes] = useLocalStorage("selectedSearchScopes", [], { initializeWithValue: false }); - const { createNewChatThread, isLoading } = useCreateNewChatThread(); - const [isContextSelectorOpen, setIsContextSelectorOpen] = useState(false); - - const onSubmit = useCallback((children: Descendant[]) => { - createNewChatThread(children, selectedSearchScopes); - }, [createNewChatThread, selectedSearchScopes]); - - - return ( - -
-

What can I help you understand?

-
- - -
- -
-
-
-
-
- ) -} \ No newline at end of file diff --git a/packages/web/src/app/[domain]/components/homepage/agenticSearchTutorialDialog.tsx b/packages/web/src/app/[domain]/chat/components/tutorialDialog.tsx similarity index 95% rename from packages/web/src/app/[domain]/components/homepage/agenticSearchTutorialDialog.tsx rename to packages/web/src/app/[domain]/chat/components/tutorialDialog.tsx index a44d6735..c5ddd85d 100644 --- a/packages/web/src/app/[domain]/components/homepage/agenticSearchTutorialDialog.tsx +++ b/packages/web/src/app/[domain]/chat/components/tutorialDialog.tsx @@ -1,7 +1,8 @@ "use client" +import { setAgenticSearchTutorialDismissedCookie } from "@/actions" import { Button } from "@/components/ui/button" -import { Dialog, DialogContent } from "@/components/ui/dialog" +import { Dialog, DialogContent, DialogTitle } 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" @@ -27,11 +28,9 @@ import { } from "lucide-react" import Image from "next/image" import Link from "next/link" -import { useState } from "react" +import { useCallback, useState } from "react" + -interface AgenticSearchTutorialDialogProps { - onClose: () => void -} // Star button component that fetches GitHub star count @@ -249,7 +248,17 @@ const tutorialSteps = [ }, ] -export const AgenticSearchTutorialDialog = ({ onClose }: AgenticSearchTutorialDialogProps) => { +interface TutorialDialogProps { + isOpen: boolean; +} + +export const TutorialDialog = ({ isOpen: _isOpen }: TutorialDialogProps) => { + const [isOpen, setIsOpen] = useState(_isOpen); + const onClose = useCallback(() => { + setIsOpen(false); + setAgenticSearchTutorialDismissedCookie(true); + }, []); + const [currentStep, setCurrentStep] = useState(0) const nextStep = () => { @@ -269,11 +278,12 @@ export const AgenticSearchTutorialDialog = ({ onClose }: AgenticSearchTutorialDi const currentStepData = tutorialSteps[currentStep]; return ( - + + Ask Sourcebot tutorial
{/* Left Column (Text Content & Navigation) */}
diff --git a/packages/web/src/app/[domain]/chat/layout.tsx b/packages/web/src/app/[domain]/chat/layout.tsx index e82ea91f..2968c748 100644 --- a/packages/web/src/app/[domain]/chat/layout.tsx +++ b/packages/web/src/app/[domain]/chat/layout.tsx @@ -1,10 +1,14 @@ +import { AGENTIC_SEARCH_TUTORIAL_DISMISSED_COOKIE_NAME } from '@/lib/constants'; import { NavigationGuardProvider } from 'next-navigation-guard'; +import { cookies } from 'next/headers'; +import { TutorialDialog } from './components/tutorialDialog'; interface LayoutProps { children: React.ReactNode; } export default async function Layout({ children }: LayoutProps) { + const isTutorialDismissed = (await cookies()).get(AGENTIC_SEARCH_TUTORIAL_DISMISSED_COOKIE_NAME)?.value === "true"; return ( // @note: we use a navigation guard here since we don't support resuming streams yet. @@ -13,6 +17,7 @@ export default async function Layout({ children }: LayoutProps) {
{children}
+ ) } \ No newline at end of file diff --git a/packages/web/src/app/[domain]/chat/page.tsx b/packages/web/src/app/[domain]/chat/page.tsx index 8bdddf9e..5b7afef2 100644 --- a/packages/web/src/app/[domain]/chat/page.tsx +++ b/packages/web/src/app/[domain]/chat/page.tsx @@ -1,13 +1,17 @@ -import { getRepos, getSearchContexts } from "@/actions"; -import { getUserChatHistory, getConfiguredLanguageModelsInfo } from "@/features/chat/actions"; +import { getRepos, getReposStats, getSearchContexts } from "@/actions"; +import { SourcebotLogo } from "@/app/components/sourcebotLogo"; +import { getConfiguredLanguageModelsInfo } from "@/features/chat/actions"; +import { CustomSlateEditor } from "@/features/chat/customSlateEditor"; import { ServiceErrorException } from "@/lib/serviceError"; -import { isServiceError } from "@/lib/utils"; -import { NewChatPanel } from "./components/newChatPanel"; -import { TopBar } from "../components/topBar"; -import { ResizablePanelGroup } from "@/components/ui/resizable"; -import { ChatSidePanel } from "./components/chatSidePanel"; -import { auth } from "@/auth"; -import { AnimatedResizableHandle } from "@/components/ui/animatedResizableHandle"; +import { isServiceError, measure } from "@/lib/utils"; +import { LandingPageChatBox } from "./components/landingPageChatBox"; +import { RepositoryCarousel } from "../components/repositoryCarousel"; +import { NavigationMenu } from "../components/navigationMenu"; +import { Separator } from "@/components/ui/separator"; +import { DemoCards } from "./components/demoCards"; +import { env } from "@/env.mjs"; +import { loadJsonFile } from "@sourcebot/shared"; +import { DemoExamples, demoExamplesSchema } from "@/types"; interface PageProps { params: Promise<{ @@ -18,47 +22,85 @@ interface PageProps { export default async function Page(props: PageProps) { const params = await props.params; const languageModels = await getConfiguredLanguageModelsInfo(); - const repos = await getRepos(); const searchContexts = await getSearchContexts(params.domain); - const session = await auth(); - const chatHistory = session ? await getUserChatHistory(params.domain) : []; + const allRepos = await getRepos(); - if (isServiceError(chatHistory)) { - throw new ServiceErrorException(chatHistory); - } + const carouselRepos = await getRepos({ + where: { + indexedAt: { + not: null, + }, + }, + take: 10, + }); - if (isServiceError(repos)) { - throw new ServiceErrorException(repos); + const repoStats = await getReposStats(); + + if (isServiceError(allRepos)) { + throw new ServiceErrorException(allRepos); } if (isServiceError(searchContexts)) { throw new ServiceErrorException(searchContexts); } - const indexedRepos = repos.filter((repo) => repo.indexedAt !== undefined); + if (isServiceError(carouselRepos)) { + throw new ServiceErrorException(carouselRepos); + } + + if (isServiceError(repoStats)) { + throw new ServiceErrorException(repoStats); + } + + const demoExamples = env.SOURCEBOT_DEMO_EXAMPLES_PATH ? await (async () => { + try { + return (await measure(() => loadJsonFile(env.SOURCEBOT_DEMO_EXAMPLES_PATH!, demoExamplesSchema), 'loadExamplesJsonFile')).data; + } catch (error) { + console.error('Failed to load demo examples:', error); + return undefined; + } + })() : undefined; return ( - <> - + - - - - - - + +
+
+ +
+ + + + +
+ +
+ + {demoExamples && ( + <> +
+ +
+ + + + )} + +
+
) } \ No newline at end of file diff --git a/packages/web/src/app/[domain]/components/errorNavIndicator.tsx b/packages/web/src/app/[domain]/components/errorNavIndicator.tsx deleted file mode 100644 index 4f7810aa..00000000 --- a/packages/web/src/app/[domain]/components/errorNavIndicator.tsx +++ /dev/null @@ -1,137 +0,0 @@ -"use client"; - -import Link from "next/link"; -import { HoverCard, HoverCardTrigger, HoverCardContent } from "@/components/ui/hover-card"; -import { CircleXIcon } from "lucide-react"; -import { useDomain } from "@/hooks/useDomain"; -import { unwrapServiceError } from "@/lib/utils"; -import useCaptureEvent from "@/hooks/useCaptureEvent"; -import { env } from "@/env.mjs"; -import { useQuery } from "@tanstack/react-query"; -import { ConnectionSyncStatus, RepoIndexingStatus } from "@sourcebot/db"; -import { getConnections } from "@/actions"; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; -import { getRepos } from "@/app/api/(client)/client"; - -export const ErrorNavIndicator = () => { - const domain = useDomain(); - const captureEvent = useCaptureEvent(); - - const { data: repos, isPending: isPendingRepos, isError: isErrorRepos } = useQuery({ - queryKey: ['repos', domain], - queryFn: () => unwrapServiceError(getRepos()), - select: (data) => data.filter(repo => repo.repoIndexingStatus === RepoIndexingStatus.FAILED), - refetchInterval: env.NEXT_PUBLIC_POLLING_INTERVAL_MS, - }); - - const { data: connections, isPending: isPendingConnections, isError: isErrorConnections } = useQuery({ - queryKey: ['connections', domain], - queryFn: () => unwrapServiceError(getConnections(domain)), - select: (data) => data.filter(connection => connection.syncStatus === ConnectionSyncStatus.FAILED), - refetchInterval: env.NEXT_PUBLIC_POLLING_INTERVAL_MS, - }); - - if (isPendingRepos || isErrorRepos || isPendingConnections || isErrorConnections) { - return null; - } - - if (repos.length === 0 && connections.length === 0) { - return null; - } - - return ( - - captureEvent('wa_error_nav_hover', {})}> - captureEvent('wa_error_nav_pressed', {})}> -
- - {repos.length + connections.length > 0 && ( - {repos.length + connections.length} - )} -
- -
- -
- {connections.length > 0 && ( -
-
-
-

Connection Sync Issues

-
-

- The following connections have failed to sync: -

-
- - {connections - .slice(0, 10) - .map(connection => ( - captureEvent('wa_error_nav_job_pressed', {})}> -
- - - {connection.name} - - - {connection.name} - - -
- - ))} -
- {connections.length > 10 && ( -
- And {connections.length - 10} more... -
- )} -
-
- )} - - {repos.length > 0 && ( -
-
-
-

Repository Indexing Issues

-
-

- The following repositories failed to index: -

-
- - {repos - .slice(0, 10) - .map(repo => ( -
- - - {repo.repoName} - - - {repo.repoName} - - -
- ))} -
- {repos.length > 10 && ( -
- And {repos.length - 10} more... -
- )} -
-
- )} -
-
-
- ); -}; diff --git a/packages/web/src/app/[domain]/components/homepage/index.tsx b/packages/web/src/app/[domain]/components/homepage/index.tsx deleted file mode 100644 index fa230288..00000000 --- a/packages/web/src/app/[domain]/components/homepage/index.tsx +++ /dev/null @@ -1,102 +0,0 @@ -'use client'; - -import { SourcebotLogo } from "@/app/components/sourcebotLogo"; -import { LanguageModelInfo } from "@/features/chat/types"; -import { RepositoryQuery, SearchContextQuery } from "@/lib/types"; -import { useHotkeys } from "react-hotkeys-hook"; -import { AgenticSearch } from "./agenticSearch"; -import { PreciseSearch } from "./preciseSearch"; -import { SearchMode } from "./toolbar"; -import { CustomSlateEditor } from "@/features/chat/customSlateEditor"; -import { setSearchModeCookie } from "@/actions"; -import { useCallback, useState } from "react"; -import { DemoExamples } from "@/types"; - -interface HomepageProps { - initialRepos: RepositoryQuery[]; - searchContexts: SearchContextQuery[]; - languageModels: LanguageModelInfo[]; - chatHistory: { - id: string; - createdAt: Date; - name: string | null; - }[]; - initialSearchMode: SearchMode; - demoExamples: DemoExamples | undefined; - isAgenticSearchTutorialDismissed: boolean; -} - - -export const Homepage = ({ - initialRepos, - searchContexts, - languageModels, - chatHistory, - initialSearchMode, - demoExamples, - isAgenticSearchTutorialDismissed, -}: HomepageProps) => { - const [searchMode, setSearchMode] = useState(initialSearchMode); - const isAgenticSearchEnabled = languageModels.length > 0; - - const onSearchModeChanged = useCallback(async (newMode: SearchMode) => { - setSearchMode(newMode); - await setSearchModeCookie(newMode); - }, [setSearchMode]); - - useHotkeys("mod+i", (e) => { - e.preventDefault(); - onSearchModeChanged("agentic"); - }, { - enableOnFormTags: true, - enableOnContentEditable: true, - description: "Switch to agentic search", - }); - - useHotkeys("mod+p", (e) => { - e.preventDefault(); - onSearchModeChanged("precise"); - }, { - enableOnFormTags: true, - enableOnContentEditable: true, - description: "Switch to precise search", - }); - - return ( -
-
- -
- - {searchMode === "precise" ? ( - - ) : ( - - - - )} -
- ) -} - diff --git a/packages/web/src/app/[domain]/components/homepage/preciseSearch.tsx b/packages/web/src/app/[domain]/components/homepage/preciseSearch.tsx deleted file mode 100644 index d5608940..00000000 --- a/packages/web/src/app/[domain]/components/homepage/preciseSearch.tsx +++ /dev/null @@ -1,145 +0,0 @@ -'use client'; - -import { Separator } from "@/components/ui/separator"; -import { SyntaxReferenceGuideHint } from "../syntaxReferenceGuideHint"; -import { RepositorySnapshot } from "./repositorySnapshot"; -import { RepositoryQuery } from "@/lib/types"; -import { useDomain } from "@/hooks/useDomain"; -import Link from "next/link"; -import { SearchBar } from "../searchBar/searchBar"; -import { SearchModeSelector, SearchModeSelectorProps } from "./toolbar"; - -interface PreciseSearchProps { - initialRepos: RepositoryQuery[]; - searchModeSelectorProps: SearchModeSelectorProps; -} - -export const PreciseSearch = ({ - initialRepos, - searchModeSelectorProps, -}: PreciseSearchProps) => { - const domain = useDomain(); - - return ( - <> -
- - -
- -
-
-
- -
-
- - How to search -
- - - test todo (both test and todo) - - - test or todo (either test or todo) - - - {`"exit boot"`} (exact match) - - - TODO case:yes (case sensitive) - - - - - file:README setup (by filename) - - - repo:torvalds/linux test (by repo) - - - lang:typescript (by language) - - - rev:HEAD (by branch or tag) - - - - - file:{`\\.py$`} {`(files that end in ".py")`} - - - sym:main {`(symbols named "main")`} - - - todo -lang:c (negate filter) - - - content:README (search content only) - - -
- -
- - ) -} - -const HowToSection = ({ title, children }: { title: string, children: React.ReactNode }) => { - return ( -
- {title} - {children} -
- ) - -} - -const Highlight = ({ children }: { children: React.ReactNode }) => { - return ( - - {children} - - ) -} - -const QueryExample = ({ children }: { children: React.ReactNode }) => { - return ( - - {children} - - ) -} - -const QueryExplanation = ({ children }: { children: React.ReactNode }) => { - return ( - - {children} - - ) -} - -const Query = ({ query, domain, children }: { query: string, domain: string, children: React.ReactNode }) => { - return ( - - {children} - - ) -} diff --git a/packages/web/src/app/[domain]/components/homepage/repositoryCarousel.tsx b/packages/web/src/app/[domain]/components/homepage/repositoryCarousel.tsx deleted file mode 100644 index 0a90e1a7..00000000 --- a/packages/web/src/app/[domain]/components/homepage/repositoryCarousel.tsx +++ /dev/null @@ -1,105 +0,0 @@ -'use client'; - -import { - Carousel, - CarouselContent, - CarouselItem, -} from "@/components/ui/carousel"; -import Autoscroll from "embla-carousel-auto-scroll"; -import { getCodeHostInfoForRepo } from "@/lib/utils"; -import Image from "next/image"; -import { FileIcon } from "@radix-ui/react-icons"; -import clsx from "clsx"; -import { RepositoryQuery } from "@/lib/types"; -import { getBrowsePath } from "../../browse/hooks/useBrowseNavigation"; -import Link from "next/link"; -import { useDomain } from "@/hooks/useDomain"; - -interface RepositoryCarouselProps { - repos: RepositoryQuery[]; -} - -export const RepositoryCarousel = ({ - repos, -}: RepositoryCarouselProps) => { - return ( - - - {repos.map((repo, index) => ( - - - - ))} - - - ) -}; - -interface RepositoryBadgeProps { - repo: RepositoryQuery; -} - -const RepositoryBadge = ({ - repo -}: RepositoryBadgeProps) => { - const domain = useDomain(); - const { repoIcon, displayName } = (() => { - const info = getCodeHostInfoForRepo({ - codeHostType: repo.codeHostType, - name: repo.repoName, - displayName: repo.repoDisplayName, - webUrl: repo.webUrl, - }); - - if (info) { - return { - repoIcon: {info.codeHostName}, - displayName: info.displayName, - } - } - - return { - repoIcon: , - displayName: repo.repoName, - } - })(); - - return ( - - {repoIcon} - - {displayName} - - - ) -} diff --git a/packages/web/src/app/[domain]/components/homepage/repositorySnapshot.tsx b/packages/web/src/app/[domain]/components/homepage/repositorySnapshot.tsx deleted file mode 100644 index d6845fa2..00000000 --- a/packages/web/src/app/[domain]/components/homepage/repositorySnapshot.tsx +++ /dev/null @@ -1,156 +0,0 @@ -"use client"; - -import Link from "next/link"; -import { RepositoryCarousel } from "./repositoryCarousel"; -import { useDomain } from "@/hooks/useDomain"; -import { useQuery } from "@tanstack/react-query"; -import { unwrapServiceError } from "@/lib/utils"; -import { getRepos } from "@/app/api/(client)/client"; -import { env } from "@/env.mjs"; -import { Skeleton } from "@/components/ui/skeleton"; -import { - Carousel, - CarouselContent, - CarouselItem, -} from "@/components/ui/carousel"; -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[]; -} - -const MAX_REPOS_TO_DISPLAY_IN_CAROUSEL = 15; - -export function RepositorySnapshot({ - repos: initialRepos, -}: RepositorySnapshotProps) { - const domain = useDomain(); - - const { data: repos, isPending, isError } = useQuery({ - queryKey: ['repos', domain], - queryFn: () => unwrapServiceError(getRepos()), - refetchInterval: env.NEXT_PUBLIC_POLLING_INTERVAL_MS, - placeholderData: initialRepos, - }); - - if (isPending || isError || !repos) { - return ( -
- -
- ) - } - - // Use `indexedAt` to determine if a repo has __ever__ been indexed. - // The repo indexing status only tells us the repo's current indexing status. - const indexedRepos = repos.filter((repo) => repo.indexedAt !== undefined); - - // If there are no indexed repos... - if (indexedRepos.length === 0) { - - // ... show a loading state if repos are being indexed now - if (repos.some((repo) => repo.repoIndexingStatus === RepoIndexingStatus.INDEXING || repo.repoIndexingStatus === RepoIndexingStatus.IN_INDEX_QUEUE)) { - return ( -
- - indexing in progress... -
- ) - - // ... otherwise, show the empty state. - } else { - return ( - - ) - } - } - - return ( -
- - {`${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 - -

- )} -
- ) -} - -function EmptyRepoState() { - return ( -
- No repositories found - -
-
- - <> - Create a{" "} - - connection - {" "} - to start indexing repositories - - -
-
-
- ) -} - -function RepoSkeleton() { - return ( -
- {/* Skeleton for "Search X repositories" text */} -
- {/* "Search X" */} - {/* "repositories" */} -
- - {/* Skeleton for repository carousel */} - - - {[1, 2, 3].map((_, index) => ( - -
- {/* Icon */} - {/* Repository name */} -
-
- ))} -
-
-
- ) -} diff --git a/packages/web/src/app/[domain]/components/navigationMenu.tsx b/packages/web/src/app/[domain]/components/navigationMenu/index.tsx similarity index 56% rename from packages/web/src/app/[domain]/components/navigationMenu.tsx rename to packages/web/src/app/[domain]/components/navigationMenu/index.tsx index b71b7ab7..6cab2a68 100644 --- a/packages/web/src/app/[domain]/components/navigationMenu.tsx +++ b/packages/web/src/app/[domain]/components/navigationMenu/index.tsx @@ -1,21 +1,26 @@ +import { getRepos, getReposStats } from "@/actions"; +import { SourcebotLogo } from "@/app/components/sourcebotLogo"; +import { auth } from "@/auth"; +import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { NavigationMenu as NavigationMenuBase, NavigationMenuItem, NavigationMenuLink, NavigationMenuList, navigationMenuTriggerStyle } from "@/components/ui/navigation-menu"; -import Link from "next/link"; import { Separator } from "@/components/ui/separator"; -import { SettingsDropdown } from "./settingsDropdown"; -import { GitHubLogoIcon, DiscordLogoIcon } from "@radix-ui/react-icons"; -import { redirect } from "next/navigation"; -import { OrgSelector } from "./orgSelector"; -import { ErrorNavIndicator } from "./errorNavIndicator"; -import { WarningNavIndicator } from "./warningNavIndicator"; -import { ProgressNavIndicator } from "./progressNavIndicator"; -import { SourcebotLogo } from "@/app/components/sourcebotLogo"; -import { TrialNavIndicator } from "./trialNavIndicator"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; +import { getSubscriptionInfo } from "@/ee/features/billing/actions"; import { IS_BILLING_ENABLED } from "@/ee/features/billing/stripe"; import { env } from "@/env.mjs"; -import { getSubscriptionInfo } from "@/ee/features/billing/actions"; -import { auth } from "@/auth"; -import WhatsNewIndicator from "./whatsNewIndicator"; +import { ServiceErrorException } from "@/lib/serviceError"; +import { cn, getShortenedNumberDisplayString, isServiceError } from "@/lib/utils"; +import { DiscordLogoIcon, GitHubLogoIcon } from "@radix-ui/react-icons"; +import { RepoJobStatus, RepoJobType } from "@sourcebot/db"; +import { BookMarkedIcon, CircleIcon, MessageCircleIcon, SearchIcon, SettingsIcon } from "lucide-react"; +import Link from "next/link"; +import { redirect } from "next/navigation"; +import { OrgSelector } from "../orgSelector"; +import { SettingsDropdown } from "../settingsDropdown"; +import WhatsNewIndicator from "../whatsNewIndicator"; +import { ProgressIndicator } from "./progressIndicator"; +import { TrialIndicator } from "./trialIndicator"; const SOURCEBOT_DISCORD_URL = "https://discord.gg/6Fhp27x7Pb"; const SOURCEBOT_GITHUB_URL = "https://github.com/sourcebot-dev/sourcebot"; @@ -31,6 +36,38 @@ export const NavigationMenu = async ({ const session = await auth(); const isAuthenticated = session?.user !== undefined; + const repoStats = await getReposStats(); + if (isServiceError(repoStats)) { + throw new ServiceErrorException(repoStats); + } + + const sampleRepos = await getRepos({ + where: { + jobs: { + some: { + type: RepoJobType.INDEX, + status: { + in: [ + RepoJobStatus.PENDING, + RepoJobStatus.IN_PROGRESS, + ] + } + }, + }, + indexedAt: null, + }, + take: 5, + }); + + if (isServiceError(sampleRepos)) { + throw new ServiceErrorException(sampleRepos); + } + + const { + numberOfRepos, + numberOfReposWithFirstTimeIndexingJobsInProgress, + } = repoStats; + return (
@@ -55,48 +92,55 @@ export const NavigationMenu = async ({ )} - + + Search - Repositories + + Ask + + + + + + Repositories + + {getShortenedNumberDisplayString(numberOfRepos)} + {numberOfReposWithFirstTimeIndexingJobsInProgress > 0 && ( + + )} + + + + +

{numberOfRepos} total {numberOfRepos === 1 ? 'repository' : 'repositories'}

+
+
+
{isAuthenticated && ( <> - {env.NEXT_PUBLIC_SOURCEBOT_CLOUD_ENVIRONMENT === undefined && ( - - - Agents - - - )} - - - Connections - - + Settings @@ -107,10 +151,11 @@ export const NavigationMenu = async ({
- - - - + +
{ @@ -145,7 +190,5 @@ export const NavigationMenu = async ({
- - ) } \ No newline at end of file diff --git a/packages/web/src/app/[domain]/components/navigationMenu/progressIndicator.tsx b/packages/web/src/app/[domain]/components/navigationMenu/progressIndicator.tsx new file mode 100644 index 00000000..cc5d5478 --- /dev/null +++ b/packages/web/src/app/[domain]/components/navigationMenu/progressIndicator.tsx @@ -0,0 +1,118 @@ +'use client'; + +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Separator } from "@/components/ui/separator"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; +import { useDomain } from "@/hooks/useDomain"; +import { RepositoryQuery } from "@/lib/types"; +import { getCodeHostInfoForRepo, getShortenedNumberDisplayString } from "@/lib/utils"; +import clsx from "clsx"; +import { FileIcon, Loader2Icon, RefreshCwIcon } from "lucide-react"; +import Image from "next/image"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { useMemo } from "react"; + +interface ProgressIndicatorProps { + numberOfReposWithFirstTimeIndexingJobsInProgress: number; + sampleRepos: RepositoryQuery[]; +} + +export const ProgressIndicator = ({ + numberOfReposWithFirstTimeIndexingJobsInProgress: numRepos, + sampleRepos, +}: ProgressIndicatorProps) => { + const domain = useDomain(); + const router = useRouter(); + + if (numRepos === 0) { + return null; + } + + const numReposString = getShortenedNumberDisplayString(numRepos); + + return ( + + + + + + {numReposString} + + + + +
+

{`Syncing ${numReposString} ${numRepos === 1 ? 'repository' : 'repositories'}`}

+ +
+ +
+ {sampleRepos.map((repo) => ( + + ))} +
+ {numRepos > sampleRepos.length && ( +
+ + {`View ${numRepos - sampleRepos.length} more`} + +
+ )} +
+
+ ) +} + +const RepoItem = ({ repo }: { repo: RepositoryQuery }) => { + + const { repoIcon, displayName } = useMemo(() => { + const info = getCodeHostInfoForRepo({ + name: repo.repoName, + codeHostType: repo.codeHostType, + displayName: repo.repoDisplayName, + webUrl: repo.webUrl, + }); + + if (info) { + return { + repoIcon: {info.codeHostName}, + displayName: info.displayName, + } + } + + return { + repoIcon: , + displayName: repo.repoName, + } + + + }, [repo.repoName, repo.codeHostType, repo.repoDisplayName, repo.webUrl]); + + + return ( + + {repoIcon} + + {displayName} + + + ) +} \ No newline at end of file diff --git a/packages/web/src/app/[domain]/components/trialNavIndicator.tsx b/packages/web/src/app/[domain]/components/navigationMenu/trialIndicator.tsx similarity index 95% rename from packages/web/src/app/[domain]/components/trialNavIndicator.tsx rename to packages/web/src/app/[domain]/components/navigationMenu/trialIndicator.tsx index 70b13ecd..f7af06fc 100644 --- a/packages/web/src/app/[domain]/components/trialNavIndicator.tsx +++ b/packages/web/src/app/[domain]/components/navigationMenu/trialIndicator.tsx @@ -13,7 +13,7 @@ interface Props { } | null | ServiceError; } -export const TrialNavIndicator = ({ subscription }: Props) => { +export const TrialIndicator = ({ subscription }: Props) => { const domain = useDomain(); const captureEvent = useCaptureEvent(); diff --git a/packages/web/src/app/[domain]/components/pathHeader.tsx b/packages/web/src/app/[domain]/components/pathHeader.tsx index 18806937..11b5bf1d 100644 --- a/packages/web/src/app/[domain]/components/pathHeader.tsx +++ b/packages/web/src/app/[domain]/components/pathHeader.tsx @@ -3,7 +3,7 @@ import { cn, getCodeHostInfoForRepo } from "@/lib/utils"; import { LaptopIcon } from "@radix-ui/react-icons"; import Image from "next/image"; -import { getBrowsePath } from "../browse/hooks/useBrowseNavigation"; +import { getBrowsePath } from "../browse/hooks/utils"; import { ChevronRight, MoreHorizontal } from "lucide-react"; import { useCallback, useState, useMemo, useRef, useEffect } from "react"; import { useToast } from "@/components/hooks/use-toast"; diff --git a/packages/web/src/app/[domain]/components/progressNavIndicator.tsx b/packages/web/src/app/[domain]/components/progressNavIndicator.tsx deleted file mode 100644 index f9e0d8cb..00000000 --- a/packages/web/src/app/[domain]/components/progressNavIndicator.tsx +++ /dev/null @@ -1,73 +0,0 @@ -"use client"; - -import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card"; -import useCaptureEvent from "@/hooks/useCaptureEvent"; -import { useDomain } from "@/hooks/useDomain"; -import { env } from "@/env.mjs"; -import { unwrapServiceError } from "@/lib/utils"; -import { RepoIndexingStatus } from "@prisma/client"; -import { useQuery } from "@tanstack/react-query"; -import { Loader2Icon } from "lucide-react"; -import Link from "next/link"; -import { getRepos } from "@/app/api/(client)/client"; - -export const ProgressNavIndicator = () => { - const domain = useDomain(); - const captureEvent = useCaptureEvent(); - - const { data: inProgressRepos, isPending, isError } = useQuery({ - queryKey: ['repos'], - queryFn: () => unwrapServiceError(getRepos()), - select: (data) => data.filter(repo => repo.repoIndexingStatus === RepoIndexingStatus.IN_INDEX_QUEUE || repo.repoIndexingStatus === RepoIndexingStatus.INDEXING), - refetchInterval: env.NEXT_PUBLIC_POLLING_INTERVAL_MS, - }); - - if (isPending || isError || inProgressRepos.length === 0) { - return null; - } - - return ( - captureEvent('wa_progress_nav_pressed', {})} - > - - captureEvent('wa_progress_nav_hover', {})}> -
- - {inProgressRepos.length} -
-
- -
-
-
-

Indexing in Progress

-
-

- The following repositories are currently being indexed: -

-
- { - inProgressRepos.slice(0, 10) - .map(item => ( -
- {item.repoName} -
- ) - )} - {inProgressRepos.length > 10 && ( -
- And {inProgressRepos.length - 10} more... -
- )} -
-
-
-
- - ); -}; \ No newline at end of file diff --git a/packages/web/src/app/[domain]/components/repositoryCarousel.tsx b/packages/web/src/app/[domain]/components/repositoryCarousel.tsx new file mode 100644 index 00000000..bd4f2d1a --- /dev/null +++ b/packages/web/src/app/[domain]/components/repositoryCarousel.tsx @@ -0,0 +1,158 @@ +'use client'; + +import { + Carousel, + CarouselContent, + CarouselItem, +} from "@/components/ui/carousel"; +import { captureEvent } from "@/hooks/useCaptureEvent"; +import { RepositoryQuery } from "@/lib/types"; +import { getCodeHostInfoForRepo } from "@/lib/utils"; +import { FileIcon } from "@radix-ui/react-icons"; +import clsx from "clsx"; +import Autoscroll from "embla-carousel-auto-scroll"; +import Image from "next/image"; +import Link from "next/link"; +import { getBrowsePath } from "../browse/hooks/utils"; +import { useDomain } from "@/hooks/useDomain"; + +interface RepositoryCarouselProps { + displayRepos: RepositoryQuery[]; + numberOfReposWithIndex: number; +} + +export function RepositoryCarousel({ + displayRepos, + numberOfReposWithIndex, +}: RepositoryCarouselProps) { + const domain = useDomain(); + + if (numberOfReposWithIndex === 0) { + return ( +
+ No repositories found + +
+
+ + <> + Create a{" "} + + connection + {" "} + to start indexing repositories + + +
+
+
+ ) + } + + return ( +
+ + {`${numberOfReposWithIndex} `} + + {numberOfReposWithIndex > 1 ? 'repositories' : 'repository'} + + {` indexed`} + + + + {displayRepos.map((repo, index) => ( + + + + ))} + + + {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 + +

+ )} +
+ ) +} + +interface RepositoryBadgeProps { + repo: RepositoryQuery; +} + +const RepositoryBadge = ({ + repo +}: RepositoryBadgeProps) => { + const domain = useDomain(); + const { repoIcon, displayName } = (() => { + const info = getCodeHostInfoForRepo({ + codeHostType: repo.codeHostType, + name: repo.repoName, + displayName: repo.repoDisplayName, + webUrl: repo.webUrl, + }); + + if (info) { + return { + repoIcon: {info.codeHostName}, + displayName: info.displayName, + } + } + + return { + repoIcon: , + displayName: repo.repoName, + } + })(); + + return ( + + {repoIcon} + + {displayName} + + + ) +} diff --git a/packages/web/src/app/[domain]/components/homepage/toolbar.tsx b/packages/web/src/app/[domain]/components/searchModeSelector.tsx similarity index 83% rename from packages/web/src/app/[domain]/components/homepage/toolbar.tsx rename to packages/web/src/app/[domain]/components/searchModeSelector.tsx index cc9c65e0..e615ac8c 100644 --- a/packages/web/src/app/[domain]/components/homepage/toolbar.tsx +++ b/packages/web/src/app/[domain]/components/searchModeSelector.tsx @@ -1,13 +1,16 @@ 'use client'; import { KeyboardShortcutHint } from "@/app/components/keyboardShortcutHint"; +import { useDomain } from "@/hooks/useDomain"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Separator } from "@/components/ui/separator"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; -import { MessageCircleIcon, SearchIcon, TriangleAlert } from "lucide-react"; +import { MessageCircleIcon, SearchIcon } from "lucide-react"; import Link from "next/link"; -import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { useCallback, useState } from "react"; +import { useHotkeys } from "react-hotkeys-hook"; export type SearchMode = "precise" | "agentic"; @@ -17,24 +20,47 @@ const AGENTIC_SEARCH_DOCS_URL = "https://docs.sourcebot.dev/docs/features/ask/ov export interface SearchModeSelectorProps { searchMode: SearchMode; - isAgenticSearchEnabled: boolean; - onSearchModeChange: (searchMode: SearchMode) => void; className?: string; } export const SearchModeSelector = ({ searchMode, - isAgenticSearchEnabled, - onSearchModeChange, className, }: SearchModeSelectorProps) => { + const domain = useDomain(); const [focusedSearchMode, setFocusedSearchMode] = useState(searchMode); + const router = useRouter(); + + const onSearchModeChanged = useCallback((value: SearchMode) => { + router.push(`/${domain}/${value === "precise" ? "search" : "chat"}`); + }, [domain, router]); + + useHotkeys("mod+i", (e) => { + e.preventDefault(); + onSearchModeChanged("agentic"); + }, { + enableOnFormTags: true, + enableOnContentEditable: true, + description: "Switch to agentic search", + }); + + useHotkeys("mod+p", (e) => { + e.preventDefault(); + onSearchModeChanged("precise"); + }, { + enableOnFormTags: true, + enableOnContentEditable: true, + description: "Switch to precise search", + }); + return (