Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/shared/src/entitlements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ const ACTIVE_ONLINE_LICENSE_STATUSES: LicenseStatus[] = [
'past_due',
];

// @WARNING: when adding a new entitlement to this list, make sure
// lighthouse/lambda/entitlements.ts is also updated && deployed
// prior to rolling a new Sourcebot version.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const ALL_ENTITLEMENTS = [
"search-contexts",
Expand All @@ -37,6 +40,7 @@ const ALL_ENTITLEMENTS = [
"chat-sharing",
"org-management",
"oauth",
"ask"
] as const;
export type Entitlement = (typeof ALL_ENTITLEMENTS)[number];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,17 @@ import { HomeView } from "@/hooks/useHomeView";
import { NotificationDot } from "../../../components/notificationDot";
import { useMemo } from "react";
import Link from "next/link";
import { useEntitlements } from "@/features/entitlements/useEntitlements";
import { Entitlement } from "@sourcebot/shared";
import { UpgradeBadge } from "../upsell/upgradeBadge";

interface NavItem {
title: string;
href: string;
icon: LucideIcon;
key: string;
requiresAuth?: boolean;
requiredEntitlement?: Entitlement;
}

interface NavProps {
Expand All @@ -26,8 +30,13 @@ interface NavProps {
homeView: HomeView;
}

export function Nav({ isSettingsNotificationVisible, isSignedIn, homeView }: NavProps) {
export function Nav({
isSettingsNotificationVisible,
isSignedIn,
homeView
}: NavProps) {
const pathname = usePathname();
const entitlements = useEntitlements();

const baseItems = useMemo((): NavItem[] => {

Expand All @@ -42,7 +51,8 @@ export function Nav({ isSettingsNotificationVisible, isSignedIn, homeView }: Nav
title: "Ask",
href: "/chat",
icon: MessageCircleIcon,
key: "chat"
key: "chat",
requiredEntitlement: "ask"
}

return [
Expand Down Expand Up @@ -101,6 +111,10 @@ export function Nav({ isSettingsNotificationVisible, isSignedIn, homeView }: Nav
{baseItems.filter((item) => !item.requiresAuth || isSignedIn).map((item) => {
const showNotification =
(item.key === "settings" && isSettingsNotificationVisible);

const showUpgradeBadge =
(item.requiredEntitlement && !entitlements.includes(item.requiredEntitlement));

return (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild isActive={isActive(item.href)} tooltip={item.title}>
Expand All @@ -109,6 +123,7 @@ export function Nav({ isSettingsNotificationVisible, isSignedIn, homeView }: Nav
>
<item.icon />
<span>{item.title}</span>
{showUpgradeBadge && <UpgradeBadge />}
{showNotification && <NotificationDot className="ml-1.5" />}
</Link>
</SidebarMenuButton>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar";
import { useEntitlements } from "@/features/entitlements/useEntitlements";
import { Entitlement } from "@sourcebot/shared";
import {
ChartAreaIcon,
KeyRoundIcon,
Expand All @@ -23,6 +25,7 @@ import {
} from "lucide-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { UpgradeBadge } from "../upsell/upgradeBadge";

const iconMap = {
"link": LinkIcon,
Expand All @@ -44,6 +47,7 @@ export type NavItem = {
title: React.ReactNode;
icon?: NavIconName;
isNotificationDotVisible?: boolean;
requiredEntitlement?: Entitlement;
};

export type NavGroup = {
Expand All @@ -57,6 +61,7 @@ interface NavProps {

export function Nav({ groups }: NavProps) {
const pathname = usePathname();
const entitlements = useEntitlements();

return (
<>
Expand All @@ -69,13 +74,18 @@ export function Nav({ groups }: NavProps) {
const isActive = item.hrefRegex
? new RegExp(item.hrefRegex).test(pathname)
: pathname === item.href;

const showUpgradeBadge =
(item.requiredEntitlement && !entitlements.includes(item.requiredEntitlement));

const Icon = item.icon ? iconMap[item.icon] : undefined;
return (
<SidebarMenuItem key={item.href}>
<SidebarMenuButton asChild isActive={isActive}>
<Link href={item.href}>
{Icon && <Icon />}
<span>{item.title}</span>
{showUpgradeBadge && <UpgradeBadge />}
{item.isNotificationDotVisible && <NotificationDot className="ml-1.5" />}
</Link>
</SidebarMenuButton>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import { useKeymapType } from "@/hooks/useKeymapType";
import { KeymapType } from "@/lib/types";
import { cn } from "@/lib/utils";
import {
ArrowLeftToLineIcon, ArrowRightToLineIcon, ArrowUpCircle, ChevronsUpDown, CodeIcon,
ArrowLeftToLineIcon, ArrowRightToLineIcon, ChevronsUpDown, CodeIcon,
Laptop, LogIn, LogOut, Moon, SettingsIcon, Sun, UserIcon
} from "lucide-react";
import { Session } from "next-auth";
Expand All @@ -44,7 +44,7 @@ import { KeyboardShortcutHint } from "@/app/components/keyboardShortcutHint";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { Separator } from "@/components/ui/separator";
import { WhatsNewSidebarButton } from "./whatsNewSidebarButton";
import { UpsellBadge } from "./upsellBadge";
import { UpgradeButton } from "./upsell/upgradeButton";

interface SidebarBaseProps {
session: Session | null;
Expand Down Expand Up @@ -89,7 +89,7 @@ export function SidebarBase({ session, collapsible = "icon", headerContent, chil
{children}
</SidebarContent>
<SidebarFooter className="border-t border-sidebar-border">
{!isValidLicenseActive && isOwner && <UpsellBadge />}
{!isValidLicenseActive && isOwner && <UpgradeButton />}
{collapsible !== "none" && <CollapseSidebarButton />}
<WhatsNewSidebarButton />
{session ? (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Badge } from "@/components/ui/badge"

export const UpgradeBadge = () => {
return (
<Badge
className="bg-purple-500/20 text-purple-400 border-purple-500/30 text-[10px] px-1.5 py-0 rounded-md leading-normal tracking-wide"
>
Upgrade
</Badge>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { UpsellDialog } from "./upsellDialog";
import { useOffers } from "@/ee/features/lighthouse/useOffers";
import { Skeleton } from "@/components/ui/skeleton";

export const UpsellBadge = () => {
export const UpgradeButton = () => {
const [isDialogOpen, setIsDialogOpen] = useState(false);

const { data: offers, isPending, isError } = useOffers();
Expand All @@ -21,14 +21,14 @@ export const UpsellBadge = () => {
return null;
}

const label = offers.trial.eligible ? "Try Enterprise" : "Free plan";
const label = offers.trial.eligible ? "Try Enterprise" : "Upgrade to Enterprise";
return (
<>
<div className="group-data-[state=collapsed]:hidden px-2 pt-1">
<button
type="button"
onClick={() => setIsDialogOpen(true)}
className="inline-flex items-center gap-1.5 rounded-full border border-border px-2.5 py-1 text-xs text-muted-foreground text-nowrap transition-colors hover:border-foreground hover:text-foreground"
className="inline-flex font-semibold items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs text-muted-foreground text-nowrap transition-colors bg-purple-500/20 text-purple-400 border-purple-500/30 hover:bg-purple-500/10"
>
<ArrowUpCircle className="h-3.5 w-3.5" />
{label}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { SearchModeSelector } from "@/app/(app)/components/searchModeSelector";
import { Separator } from "@/components/ui/separator";
import { ChatBox } from "@/features/chat/components/chatBox";
import { ChatBoxToolbar } from "@/features/chat/components/chatBox/chatBoxToolbar";
import { LoginModal } from "@/app/components/loginModal";
import { NotConfiguredErrorBanner } from "@/features/chat/components/notConfiguredErrorBanner";
import { LanguageModelInfo, RepoSearchScope } from "@/features/chat/types";
import { useCreateNewChatThread } from "@/features/chat/useCreateNewChatThread";
Expand All @@ -29,7 +28,7 @@ export const LandingPage = ({
repoId,
isAuthenticated,
}: LandingPageProps) => {
const { createNewChatThread, isLoading, loginWall } = useCreateNewChatThread({ isAuthenticated });
const { createNewChatThread, isLoading } = useCreateNewChatThread();
const [isContextSelectorOpen, setIsContextSelectorOpen] = useState(false);
const isChatBoxDisabled = languageModels.length === 0;

Expand Down Expand Up @@ -77,6 +76,8 @@ export const LandingPage = ({
selectedSearchScopes={selectedSearchScopes}
searchContexts={[]}
isDisabled={isChatBoxDisabled}
isAuthenticated={isAuthenticated}
isLoginWallEnabled={true}
/>
<Separator />
<div className="relative">
Expand All @@ -103,13 +104,6 @@ export const LandingPage = ({
)}
</div>
</div>

<LoginModal
isOpen={loginWall.isOpen}
onOpenChange={loginWall.onOpenChange}
providers={loginWall.providers}
callbackUrl={typeof window !== 'undefined' ? window.location.href : ''}
/>
</div>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ interface ChatThreadPanelProps {
messages: SBChatMessage[];
isOwner: boolean;
isAuthenticated: boolean;
isLoginWallEnabled: boolean;
chatName?: string;
}

Expand All @@ -26,6 +27,7 @@ export const ChatThreadPanel = ({
messages,
isOwner,
isAuthenticated,
isLoginWallEnabled,
chatName,
}: ChatThreadPanelProps) => {
// @note: we are guaranteed to have a chatId because this component will only be
Expand Down Expand Up @@ -74,6 +76,7 @@ export const ChatThreadPanel = ({
onSelectedSearchScopesChange={setSelectedSearchScopes}
isOwner={isOwner}
isAuthenticated={isAuthenticated}
isLoginWallEnabled={isLoginWallEnabled}
chatName={chatName}
/>
</div>
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/(app)/chat/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ export default async function Page(props: PageProps) {
messages={messages}
isOwner={isOwner}
isAuthenticated={!!session}
isLoginWallEnabled={env.EXPERIMENT_ASK_GH_ENABLED === 'true'}
chatName={name ?? undefined}
/>
</div>
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/app/(app)/chat/chatLandingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export async function ChatLandingPage() {
repos={allRepos}
searchContexts={searchContexts}
isAuthenticated={!!session}
isLoginWallEnabled={env.EXPERIMENT_ASK_GH_ENABLED === 'true'}
/>
</CustomSlateEditor>

Expand Down
16 changes: 6 additions & 10 deletions packages/web/src/app/(app)/chat/components/landingPageChatBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,23 @@ import { useLocalStorage } from "usehooks-ts";
import { SELECTED_SEARCH_SCOPES_LOCAL_STORAGE_KEY } from "@/features/chat/constants";
import { SearchModeSelector } from "../../components/searchModeSelector";
import { NotConfiguredErrorBanner } from "@/features/chat/components/notConfiguredErrorBanner";
import { LoginModal } from "@/app/components/loginModal";

interface LandingPageChatBox {
languageModels: LanguageModelInfo[];
repos: RepositoryQuery[];
searchContexts: SearchContextQuery[];
isAuthenticated: boolean;
isLoginWallEnabled: boolean;
}

export const LandingPageChatBox = ({
languageModels,
repos,
searchContexts,
isAuthenticated,
isLoginWallEnabled,
}: LandingPageChatBox) => {
const { createNewChatThread, isLoading, loginWall } = useCreateNewChatThread({ isAuthenticated });
const { createNewChatThread, isLoading } = useCreateNewChatThread();
const [selectedSearchScopes, setSelectedSearchScopes] = useLocalStorage<SearchScope[]>(SELECTED_SEARCH_SCOPES_LOCAL_STORAGE_KEY, [], { initializeWithValue: false });
const [isContextSelectorOpen, setIsContextSelectorOpen] = useState(false);
const isChatBoxDisabled = languageModels.length === 0;
Expand All @@ -36,14 +37,16 @@ export const LandingPageChatBox = ({
<div className="border rounded-md w-full shadow-sm">
<ChatBox
onSubmit={(children) => {
createNewChatThread(children, selectedSearchScopes);
createNewChatThread(children);
}}
className="min-h-[50px]"
isRedirecting={isLoading}
languageModels={languageModels}
selectedSearchScopes={selectedSearchScopes}
searchContexts={searchContexts}
isDisabled={isChatBoxDisabled}
isAuthenticated={isAuthenticated}
isLoginWallEnabled={isLoginWallEnabled}
/>
<Separator />
<div className="relative">
Expand All @@ -68,13 +71,6 @@ export const LandingPageChatBox = ({
{isChatBoxDisabled && (
<NotConfiguredErrorBanner className="mt-4" />
)}

<LoginModal
isOpen={loginWall.isOpen}
onOpenChange={loginWall.onOpenChange}
providers={loginWall.providers}
callbackUrl={typeof window !== 'undefined' ? window.location.href : ''}
/>
</div >
)
}
1 change: 1 addition & 0 deletions packages/web/src/app/(app)/settings/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export const getSidebarNavGroups = async () =>
title: "Analytics",
href: `/settings/analytics`,
icon: "chart-area" as const,
requiredEntitlement: 'analytics'
},
{
title: "License",
Expand Down
Loading