diff --git a/docs/swagger.json b/docs/swagger.json index 1fb82f4d18..25f9be0306 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -11987,7 +11987,105 @@ "description": "Ok", "content": { "application/json": { - "schema": {} + "schema": { + "properties": { + "total": { + "type": "number", + "format": "double" + }, + "tax": { + "type": "number", + "format": "double", + "nullable": true + }, + "subtotal": { + "type": "number", + "format": "double" + }, + "discount": { + "properties": { + "coupon": { + "properties": { + "percent_off": { + "type": "number", + "format": "double", + "nullable": true + }, + "name": { + "type": "string", + "nullable": true + } + }, + "required": [ + "percent_off", + "name" + ], + "type": "object" + } + }, + "required": [ + "coupon" + ], + "type": "object", + "nullable": true + }, + "lines": { + "properties": { + "data": { + "items": { + "properties": { + "description": { + "type": "string", + "nullable": true + }, + "amount": { + "type": "number", + "format": "double", + "nullable": true + }, + "id": { + "type": "string", + "nullable": true + } + }, + "required": [ + "description", + "amount", + "id" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "data" + ], + "type": "object", + "nullable": true + }, + "next_payment_attempt": { + "type": "number", + "format": "double", + "nullable": true + }, + "currency": { + "type": "string", + "nullable": true + } + }, + "required": [ + "total", + "tax", + "subtotal", + "discount", + "lines", + "next_payment_attempt", + "currency" + ], + "type": "object", + "nullable": true + } } } } @@ -12060,7 +12158,79 @@ "description": "Ok", "content": { "application/json": { - "schema": {} + "schema": { + "properties": { + "items": { + "items": { + "properties": { + "price": { + "properties": { + "product": { + "properties": { + "name": { + "type": "string", + "nullable": true + } + }, + "required": [ + "name" + ], + "type": "object", + "nullable": true + } + }, + "required": [ + "product" + ], + "type": "object" + }, + "quantity": { + "type": "number", + "format": "double" + } + }, + "required": [ + "price" + ], + "type": "object" + }, + "type": "array" + }, + "trial_end": { + "type": "number", + "format": "double", + "nullable": true + }, + "id": { + "type": "string" + }, + "current_period_start": { + "type": "number", + "format": "double" + }, + "current_period_end": { + "type": "number", + "format": "double" + }, + "cancel_at_period_end": { + "type": "boolean" + }, + "status": { + "type": "string" + } + }, + "required": [ + "items", + "trial_end", + "id", + "current_period_start", + "current_period_end", + "cancel_at_period_end", + "status" + ], + "type": "object", + "nullable": true + } } } } diff --git a/helicone-node/api/generatedTypes/public.ts b/helicone-node/api/generatedTypes/public.ts index f9ace06250..62460ba01e 100644 --- a/helicone-node/api/generatedTypes/public.ts +++ b/helicone-node/api/generatedTypes/public.ts @@ -4322,7 +4322,32 @@ export interface operations { /** @description Ok */ 200: { content: { - "application/json": unknown; + "application/json": ({ + /** Format: double */ + total: number; + /** Format: double */ + tax: number | null; + /** Format: double */ + subtotal: number; + discount: ({ + coupon: { + /** Format: double */ + percent_off: number | null; + name: string | null; + }; + }) | null; + lines: ({ + data: ({ + description: string | null; + /** Format: double */ + amount: number | null; + id: string | null; + })[]; + }) | null; + /** Format: double */ + next_payment_attempt: number | null; + currency: string | null; + }) | null; }; }; }; @@ -4350,7 +4375,26 @@ export interface operations { /** @description Ok */ 200: { content: { - "application/json": unknown; + "application/json": ({ + items: ({ + price: { + product: ({ + name: string | null; + }) | null; + }; + /** Format: double */ + quantity?: number; + })[]; + /** Format: double */ + trial_end: number | null; + id: string; + /** Format: double */ + current_period_start: number; + /** Format: double */ + current_period_end: number; + cancel_at_period_end: boolean; + status: string; + }) | null; }; }; }; diff --git a/valhalla/jawn/src/controllers/private/organizationController.ts b/valhalla/jawn/src/controllers/private/organizationController.ts index 7eeb93395d..fc8500ce08 100644 --- a/valhalla/jawn/src/controllers/private/organizationController.ts +++ b/valhalla/jawn/src/controllers/private/organizationController.ts @@ -93,7 +93,7 @@ export class OrganizationController extends Controller { @Request() request: JawnAuthenticatedRequest ): Promise> { const organizationManager = new OrganizationManager(request.authParams); - const memberCount = await organizationManager.getMemberCount(); + const memberCount = await organizationManager.getMemberCount(true); if (memberCount.error || !memberCount.data) { return err(memberCount.error ?? "Error getting member count"); } @@ -281,7 +281,7 @@ export class OrganizationController extends Controller { const stripeManager = new StripeManager(request.authParams); const organizationManager = new OrganizationManager(request.authParams); - const memberCount = await organizationManager.getMemberCount(); + const memberCount = await organizationManager.getMemberCount(true); if (memberCount.error || !memberCount.data) { return err(memberCount.error ?? "Error getting member count"); } diff --git a/valhalla/jawn/src/controllers/public/stripeController.ts b/valhalla/jawn/src/controllers/public/stripeController.ts index 4606ae8710..0a6b7a869d 100644 --- a/valhalla/jawn/src/controllers/public/stripeController.ts +++ b/valhalla/jawn/src/controllers/public/stripeController.ts @@ -13,6 +13,7 @@ import { import Stripe from "stripe"; import { JawnAuthenticatedRequest } from "../../types/request"; import { StripeManager } from "../../managers/stripe/StripeManager"; +import { Result } from "../../lib/shared/result"; export interface UpgradeToProRequest { addons?: { @@ -148,7 +149,26 @@ export class StripeController extends Controller { @Get("/subscription/preview-invoice") public async previewInvoice( @Request() request: JawnAuthenticatedRequest - ): Promise { + ): Promise<{ + currency: string | null; + next_payment_attempt: number | null; + lines: { + data: { + id: string | null; + amount: number | null; + description: string | null; + }[]; + } | null; + discount: { + coupon: { + name: string | null; + percent_off: number | null; + }; + } | null; + subtotal: number; + tax: number | null; + total: number; + } | null> { const stripeManager = new StripeManager(request.authParams); const result = await stripeManager.getUpcomingInvoice(); @@ -157,7 +177,15 @@ export class StripeController extends Controller { throw new Error(result.error); } - return result.data; + return { + currency: result.data?.currency ?? null, + next_payment_attempt: result.data?.next_payment_attempt ?? null, + lines: result.data?.lines ?? null, + discount: result.data?.discount ?? null, + subtotal: result.data?.subtotal ?? 0, + tax: result.data?.tax ?? null, + total: result.data?.total ?? 0, + }; } @Post("/subscription/cancel-subscription") @@ -184,7 +212,22 @@ export class StripeController extends Controller { @Get("/subscription") public async getSubscription( @Request() request: JawnAuthenticatedRequest - ): Promise { + ): Promise<{ + status: string; + cancel_at_period_end: boolean; + current_period_end: number; + current_period_start: number; + id: string; + trial_end: number | null; + items: { + quantity?: number; + price: { + product: { + name: string | null; + } | null; + }; + }[]; + } | null> { const stripeManager = new StripeManager(request.authParams); const result = await stripeManager.getSubscription(); @@ -193,7 +236,24 @@ export class StripeController extends Controller { throw new Error(result.error); } - return result.data; + if (!result.data) return null; + + return { + status: result.data.status, + cancel_at_period_end: result.data.cancel_at_period_end, + current_period_end: result.data.current_period_end, + current_period_start: result.data.current_period_start, + id: result.data.id, + trial_end: result.data.trial_end, + items: result.data.items.data.map((item) => ({ + quantity: item.quantity, + price: { + product: { + name: ((item.price.product as any)?.name ?? null) as string | null, + }, + }, + })), + }; } @Post("/webhook") diff --git a/valhalla/jawn/src/managers/organization/OrganizationManager.ts b/valhalla/jawn/src/managers/organization/OrganizationManager.ts index 625e43d724..93333fd3d0 100644 --- a/valhalla/jawn/src/managers/organization/OrganizationManager.ts +++ b/valhalla/jawn/src/managers/organization/OrganizationManager.ts @@ -358,7 +358,9 @@ export class OrganizationManager extends BaseManager { return ok(layout); } - async getMemberCount(): Promise> { + async getMemberCount( + filterHeliconeEmails: boolean = false + ): Promise> { const { data: members, error: membersError } = await this.organizationStore.getOrganizationMembers( this.authParams.organizationId @@ -367,7 +369,12 @@ export class OrganizationManager extends BaseManager { if (membersError !== null) { return err(membersError); } - return ok(members.length); + return ok( + members.filter( + (member) => + !filterHeliconeEmails || !member.email.endsWith("@helicone.ai") + ).length + ); } async getOrganizationMembers( diff --git a/valhalla/jawn/src/managers/stripe/StripeManager.ts b/valhalla/jawn/src/managers/stripe/StripeManager.ts index 2cf7396a11..7655541b54 100644 --- a/valhalla/jawn/src/managers/stripe/StripeManager.ts +++ b/valhalla/jawn/src/managers/stripe/StripeManager.ts @@ -8,24 +8,16 @@ import { dbExecute } from "../../lib/shared/db/dbExecute"; import { clickhouseDb } from "../../lib/db/ClickhouseWrapper"; import { buildFilterWithAuthClickHouse } from "../../lib/shared/filters/filters"; import { UpgradeToProRequest } from "../../controllers/public/stripeController"; +import { OrganizationManager } from "../organization/OrganizationManager"; -const proProductPrices = - ENVIRONMENT === "production" - ? { - "request-volume": process.env.PRICE_PROD_REQUEST_VOLUME_ID!, - "pro-users": process.env.PRICE_PROD_PRO_USERS_ID!, - prompts: process.env.PRICE_PROD_PROMPTS_ID!, - alerts: process.env.PRICE_PROD_ALERTS_ID!, - } - : { - // TEST PRODUCTS - "request-volume": "price_1P0zwNFeVmeixR9wkrT3DYdi", - "pro-users": "price_1PxwrxFeVmeixR9wUhWdnEu6", - prompts: "price_1PyozaFeVmeixR9wqQoIV2Ur", - alerts: "price_1PySmZFeVmeixR9wKEemD7jP", - }; +const proProductPrices = { + "request-volume": process.env.PRICE_PROD_REQUEST_VOLUME_ID!, //(This is just growth) + "pro-users": process.env.PRICE_PROD_PRO_USERS_ID!, + prompts: process.env.PRICE_PROD_PROMPTS_ID!, + alerts: process.env.PRICE_PROD_ALERTS_ID!, +}; -const EARLY_ADOPTER_COUPON = "0IPsIob0"; +const EARLY_ADOPTER_COUPON = "9ca5IeEs"; // WlDg28Kf | prod: 9ca5IeEs export class StripeManager extends BaseManager { private stripe: Stripe; @@ -54,10 +46,6 @@ export class StripeManager extends BaseManager { return err("User does not have an email"); } - const getStripeCustomer = await this.stripe.customers.list({ - email: user.data?.user?.email ?? "", - }); - console.log(getStripeCustomer); const customer = await this.stripe.customers.create({ email: user.data.user.email, }); @@ -210,22 +198,13 @@ WHERE (${builtFilter.filter})`, } } private async getOrgMemberCount(): Promise> { - const members = await supabaseServer.client - .from("organization_member") - .select("*", { count: "exact" }) - .eq("organization", this.authParams.organizationId); - - if (members.error) { - console.log(members.error); - return err("Error getting organization members"); - } - - return ok(members.count!); + const organizationManager = new OrganizationManager(this.authParams); + return await organizationManager.getMemberCount(true); } private shouldApplyCoupon(): boolean { const currentDate = new Date(); - const cutoffDate = new Date("2023-10-10"); + const cutoffDate = new Date("2024-10-15"); return currentDate < cutoffDate; } @@ -279,11 +258,12 @@ WHERE (${builtFilter.filter})`, tier: "pro-20240913", }, }, - allow_promotion_codes: true, }; if (this.shouldApplyCoupon()) { sessionParams.discounts = [{ coupon: EARLY_ADOPTER_COUPON }]; + } else { + sessionParams.allow_promotion_codes = true; } const session = await this.stripe.checkout.sessions.create(sessionParams); diff --git a/valhalla/jawn/src/tsoa-build/private/swagger.json b/valhalla/jawn/src/tsoa-build/private/swagger.json index 36c27c2b24..96affae78b 100644 --- a/valhalla/jawn/src/tsoa-build/private/swagger.json +++ b/valhalla/jawn/src/tsoa-build/private/swagger.json @@ -11449,7 +11449,105 @@ "description": "Ok", "content": { "application/json": { - "schema": {} + "schema": { + "properties": { + "total": { + "type": "number", + "format": "double" + }, + "tax": { + "type": "number", + "format": "double", + "nullable": true + }, + "subtotal": { + "type": "number", + "format": "double" + }, + "discount": { + "properties": { + "coupon": { + "properties": { + "percent_off": { + "type": "number", + "format": "double", + "nullable": true + }, + "name": { + "type": "string", + "nullable": true + } + }, + "required": [ + "percent_off", + "name" + ], + "type": "object" + } + }, + "required": [ + "coupon" + ], + "type": "object", + "nullable": true + }, + "lines": { + "properties": { + "data": { + "items": { + "properties": { + "description": { + "type": "string", + "nullable": true + }, + "amount": { + "type": "number", + "format": "double", + "nullable": true + }, + "id": { + "type": "string", + "nullable": true + } + }, + "required": [ + "description", + "amount", + "id" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "data" + ], + "type": "object", + "nullable": true + }, + "next_payment_attempt": { + "type": "number", + "format": "double", + "nullable": true + }, + "currency": { + "type": "string", + "nullable": true + } + }, + "required": [ + "total", + "tax", + "subtotal", + "discount", + "lines", + "next_payment_attempt", + "currency" + ], + "type": "object", + "nullable": true + } } } } @@ -11522,7 +11620,79 @@ "description": "Ok", "content": { "application/json": { - "schema": {} + "schema": { + "properties": { + "items": { + "items": { + "properties": { + "price": { + "properties": { + "product": { + "properties": { + "name": { + "type": "string", + "nullable": true + } + }, + "required": [ + "name" + ], + "type": "object", + "nullable": true + } + }, + "required": [ + "product" + ], + "type": "object" + }, + "quantity": { + "type": "number", + "format": "double" + } + }, + "required": [ + "price" + ], + "type": "object" + }, + "type": "array" + }, + "trial_end": { + "type": "number", + "format": "double", + "nullable": true + }, + "id": { + "type": "string" + }, + "current_period_start": { + "type": "number", + "format": "double" + }, + "current_period_end": { + "type": "number", + "format": "double" + }, + "cancel_at_period_end": { + "type": "boolean" + }, + "status": { + "type": "string" + } + }, + "required": [ + "items", + "trial_end", + "id", + "current_period_start", + "current_period_end", + "cancel_at_period_end", + "status" + ], + "type": "object", + "nullable": true + } } } } diff --git a/valhalla/jawn/src/tsoa-build/public/swagger.json b/valhalla/jawn/src/tsoa-build/public/swagger.json index 1fb82f4d18..25f9be0306 100644 --- a/valhalla/jawn/src/tsoa-build/public/swagger.json +++ b/valhalla/jawn/src/tsoa-build/public/swagger.json @@ -11987,7 +11987,105 @@ "description": "Ok", "content": { "application/json": { - "schema": {} + "schema": { + "properties": { + "total": { + "type": "number", + "format": "double" + }, + "tax": { + "type": "number", + "format": "double", + "nullable": true + }, + "subtotal": { + "type": "number", + "format": "double" + }, + "discount": { + "properties": { + "coupon": { + "properties": { + "percent_off": { + "type": "number", + "format": "double", + "nullable": true + }, + "name": { + "type": "string", + "nullable": true + } + }, + "required": [ + "percent_off", + "name" + ], + "type": "object" + } + }, + "required": [ + "coupon" + ], + "type": "object", + "nullable": true + }, + "lines": { + "properties": { + "data": { + "items": { + "properties": { + "description": { + "type": "string", + "nullable": true + }, + "amount": { + "type": "number", + "format": "double", + "nullable": true + }, + "id": { + "type": "string", + "nullable": true + } + }, + "required": [ + "description", + "amount", + "id" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "data" + ], + "type": "object", + "nullable": true + }, + "next_payment_attempt": { + "type": "number", + "format": "double", + "nullable": true + }, + "currency": { + "type": "string", + "nullable": true + } + }, + "required": [ + "total", + "tax", + "subtotal", + "discount", + "lines", + "next_payment_attempt", + "currency" + ], + "type": "object", + "nullable": true + } } } } @@ -12060,7 +12158,79 @@ "description": "Ok", "content": { "application/json": { - "schema": {} + "schema": { + "properties": { + "items": { + "items": { + "properties": { + "price": { + "properties": { + "product": { + "properties": { + "name": { + "type": "string", + "nullable": true + } + }, + "required": [ + "name" + ], + "type": "object", + "nullable": true + } + }, + "required": [ + "product" + ], + "type": "object" + }, + "quantity": { + "type": "number", + "format": "double" + } + }, + "required": [ + "price" + ], + "type": "object" + }, + "type": "array" + }, + "trial_end": { + "type": "number", + "format": "double", + "nullable": true + }, + "id": { + "type": "string" + }, + "current_period_start": { + "type": "number", + "format": "double" + }, + "current_period_end": { + "type": "number", + "format": "double" + }, + "cancel_at_period_end": { + "type": "boolean" + }, + "status": { + "type": "string" + } + }, + "required": [ + "items", + "trial_end", + "id", + "current_period_start", + "current_period_end", + "cancel_at_period_end", + "status" + ], + "type": "object", + "nullable": true + } } } } diff --git a/web/components/layout/auth/DesktopSidebar.tsx b/web/components/layout/auth/DesktopSidebar.tsx index 2ce107ec93..8454967f77 100644 --- a/web/components/layout/auth/DesktopSidebar.tsx +++ b/web/components/layout/auth/DesktopSidebar.tsx @@ -1,4 +1,4 @@ -import { Button, buttonVariants } from "@/components/ui/button"; +import { Button } from "@/components/ui/button"; import { Tooltip, TooltipContent, @@ -11,17 +11,21 @@ import { ChevronLeftIcon, ChevronRightIcon, CloudArrowUpIcon, + Cog6ToothIcon, QuestionMarkCircleIcon, } from "@heroicons/react/24/outline"; import Link from "next/link"; import { useRouter } from "next/router"; import { useOrg } from "../organizationContext"; import OrgDropdown from "../orgDropdown"; +import NavItem from "./NavItem"; +import { InfoBox } from "@/components/ui/helicone/infoBox"; +import { useMemo } from "react"; -interface NavigationItem { +export interface NavigationItem { name: string; href: string; - icon: React.ComponentType>; + icon: React.ComponentType> | null; current: boolean; featured?: boolean; subItems?: NavigationItem[]; @@ -29,15 +33,11 @@ interface NavigationItem { interface SidebarProps { NAVIGATION: NavigationItem[]; - setReferOpen: (open: boolean) => void; + setOpen: (open: boolean) => void; } -const DesktopSidebar = ({ - NAVIGATION, - setReferOpen, - setOpen, -}: SidebarProps) => { +const DesktopSidebar = ({ NAVIGATION }: SidebarProps) => { const org = useOrg(); const tier = org?.currentOrg?.tier; const router = useRouter(); @@ -59,111 +59,60 @@ const DesktopSidebar = ({ : [...prev, name] ); }; + const largeWith = useMemo( + () => cn(isCollapsed ? "w-16" : "w-52"), + [isCollapsed] + ); - const renderNavItem = (link: NavigationItem, isSubItem = false) => { - const hasSubItems = link.subItems && link.subItems.length > 0; + const NAVIGATION_ITEMS = useMemo(() => { + if (isCollapsed) { + return NAVIGATION.flatMap((item) => { + if (item.subItems && expandedItems.includes(item.name)) { + return [ + item, + ...item.subItems.filter((subItem) => subItem.icon !== null), + ]; + } + return [item]; + }).filter((item) => item.icon !== null); + } - return ( -
- {isCollapsed ? ( - - - toggleExpand(link.name) : undefined - } - className={cn( - buttonVariants({ - variant: "ghost", - size: "icon", - }), - "h-9 w-9", - link.current && "bg-accent hover:bg-accent" - )} - > - - {link.name} - - - - {link.name} - {link.featured && ( - New - )} - - - ) : ( -
- toggleExpand(link.name) : undefined} - className={cn( - buttonVariants({ - variant: link.current ? "secondary" : "ghost", - size: "sm", - }), - "justify-start w-full", - hasSubItems && "flex items-center justify-between" - )} - > -
- - {link.name} -
- {hasSubItems && ( - - )} - -
- )} - {hasSubItems && expandedItems.includes(link.name) && !isCollapsed && ( -
- {link.subItems!.map((subItem) => renderNavItem(subItem, true))} -
- )} -
- ); - }; + return NAVIGATION.map((item) => { + if (item.subItems) { + return { + ...item, + subItems: item.subItems.map((subItem) => ({ + ...subItem, + href: subItem.href, + })), + }; + } + return item; + }); + }, [NAVIGATION, isCollapsed, expandedItems]); return ( <>
- {!isCollapsed && } -
+
+ {!isCollapsed && } +
+
-
{((!isCollapsed && org?.currentOrg?.organization_type === "reseller") || @@ -209,11 +157,36 @@ const DesktopSidebar = ({ className="group flex flex-col gap-4 py-2 data-[collapsed=true]:py-2 " >
- + {!isCollapsed && ( +
+ <>} className="flex flex-col"> +
+ + Early Adopter Exclusive: $120 Credit for the year.
+
+ + Switch to Pro and get $10/mo credit for 1 year, as a thank + you for your early support! + +
+ +
+
+ )}
{isCollapsed ? ( <> @@ -271,7 +244,7 @@ const DesktopSidebar = ({ + )}
- - {tier === "free" && + {/* {tier === "free" && org?.currentOrg?.organization_type !== "customer" && ( -
- +
+ + +
- )} + )} */}
diff --git a/web/components/layout/auth/MobileNavigation.tsx b/web/components/layout/auth/MobileNavigation.tsx index 07908b1b0b..787487f274 100644 --- a/web/components/layout/auth/MobileNavigation.tsx +++ b/web/components/layout/auth/MobileNavigation.tsx @@ -18,24 +18,14 @@ import { } from "@heroicons/react/24/outline"; import { clsx } from "../../shared/clsx"; import ThemedDropdown from "../../shared/themed/themedDropdown"; +import { NavigationItem } from "./DesktopSidebar"; interface MobileNavigationProps { - NAVIGATION: { - name: string; - href: string; - icon: React.ComponentType>; - current: boolean; - featured?: boolean; - }[]; - setReferOpen: (open: boolean) => void; + NAVIGATION: NavigationItem[]; setOpen: (open: boolean) => void; } -const MobileNavigation = ({ - NAVIGATION, - setReferOpen, - setOpen, -}: MobileNavigationProps) => { +const MobileNavigation = ({ NAVIGATION, setOpen }: MobileNavigationProps) => { const [sidebarOpen, setSidebarOpen] = useState(false); const router = useRouter(); const supabaseClient = useSupabaseClient(); @@ -198,12 +188,14 @@ const MobileNavigation = ({ )} onClick={() => setSidebarOpen(false)} > - + {link.icon && ( + + )} {link.name} {link.featured && ( diff --git a/web/components/layout/auth/NavItem.tsx b/web/components/layout/auth/NavItem.tsx new file mode 100644 index 0000000000..cc9463073e --- /dev/null +++ b/web/components/layout/auth/NavItem.tsx @@ -0,0 +1,142 @@ +import { buttonVariants } from "@/components/ui/button"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { cn } from "@/lib/utils"; +import { ChevronRightIcon } from "@heroicons/react/24/outline"; +import Link from "next/link"; +import { useRouter } from "next/router"; + +interface NavigationItem { + name: string; + href: string; + icon: React.ComponentType> | null; + current: boolean; + featured?: boolean; + subItems?: NavigationItem[]; +} + +interface NavItemProps { + link: NavigationItem; + isCollapsed: boolean; + isSubItem?: boolean; + expandedItems: string[]; + toggleExpand: (name: string) => void; +} + +const NavItem: React.FC = ({ + link, + isCollapsed, + isSubItem = false, + expandedItems, + toggleExpand, +}) => { + const router = useRouter(); + const hasSubItems = link.subItems && link.subItems.length > 0; + + if (isCollapsed) { + return ( + + + { + if (hasSubItems) { + e.preventDefault(); + router.push(link.subItems![0].href); + } + }} + className={cn( + buttonVariants({ + variant: "ghost", + size: "icon", + }), + "h-9 w-9", + link.current && "bg-accent hover:bg-accent" + )} + > + {link.icon && ( + + )} + {link.name} + + + + {link.name} + {link.featured && ( + New + )} + + + ); + } + + return ( +
+ toggleExpand(link.name) : undefined} + className={cn( + hasSubItems + ? "flex items-center gap-1 font-medium text-gray-400 text-xs mt-[10px] text-[11px]" + : cn( + buttonVariants({ + variant: link.current ? "secondary" : "ghost", + size: "xs", + }), + "h-6", + "justify-start w-full", + "text-sm font-medium my-[2px] text-[12px]" + ), + "" + )} + > +
+ {link.icon && ( + + )} + {link.name} +
+ {hasSubItems && ( + + )} + + {hasSubItems && expandedItems.includes(link.name) && ( +
+ {link.subItems!.map((subItem) => ( + + ))} +
+ )} +
+ ); +}; + +export default NavItem; diff --git a/web/components/layout/auth/Sidebar.tsx b/web/components/layout/auth/Sidebar.tsx index 4b0738cbe1..b7fe9d7a49 100644 --- a/web/components/layout/auth/Sidebar.tsx +++ b/web/components/layout/auth/Sidebar.tsx @@ -2,14 +2,11 @@ import { ArchiveBoxIcon, - BeakerIcon, BellIcon, - ChartBarIcon, + BuildingLibraryIcon, CircleStackIcon, - CodeBracketIcon, - Cog6ToothIcon, - DocumentTextIcon, HomeIcon, + LockClosedIcon, ShieldCheckIcon, SparklesIcon, TableCellsIcon, @@ -19,24 +16,23 @@ import { import { useUser } from "@supabase/auth-helpers-react"; import { useRouter } from "next/router"; import { useMemo } from "react"; -import { GoRepoForked } from "react-icons/go"; -import DesktopSidebar from "./DesktopSidebar"; +import DesktopSidebar, { NavigationItem } from "./DesktopSidebar"; import { PiGraphLight } from "react-icons/pi"; import MobileNavigation from "./MobileNavigation"; +import { useOrg } from "../organizationContext"; +import { NotepadText, TestTube2, Webhook } from "lucide-react"; interface SidebarProps { - tier: string; - setReferOpen: (open: boolean) => void; setOpen: (open: boolean) => void; } -const Sidebar = ({ tier, setReferOpen, setOpen }: SidebarProps) => { +const Sidebar = ({ setOpen }: SidebarProps) => { const router = useRouter(); const { pathname } = router; const user = useUser(); - - const NAVIGATION = useMemo( + const org = useOrg(); + const NAVIGATION: NavigationItem[] = useMemo( () => [ { name: "Dashboard", @@ -50,111 +46,185 @@ const Sidebar = ({ tier, setReferOpen, setOpen }: SidebarProps) => { icon: TableCellsIcon, current: pathname.includes("/requests"), }, + { - name: "Datasets", - href: "/datasets", - icon: CircleStackIcon, - current: pathname.includes("/datasets"), - }, - ...(!user?.email?.includes("@helicone.ai") - ? [] - : [ - { - name: "Evals", - href: "/evals", - icon: ChartBarIcon, - current: pathname.includes("/evals"), - }, - { - name: "Connections", - href: "/connections", - icon: GoRepoForked, - current: pathname.includes("/connections"), - }, - ]), - { - name: "Sessions", - href: "/sessions", - icon: PiGraphLight, - current: pathname.includes("/sessions"), - }, - { - name: "Prompts", - href: "/prompts", - icon: DocumentTextIcon, - current: pathname.includes("/prompts"), - }, - { - name: "Users", - href: "/users", - icon: UsersIcon, - current: pathname.includes("/users"), - }, - { - name: "Alerts", - href: "/alerts", - icon: BellIcon, - current: pathname.includes("/alerts"), - }, - { - name: "Fine-Tune", - href: "/fine-tune", - icon: SparklesIcon, - current: pathname.includes("/fine-tune"), - }, - { - name: "Properties", - href: "/properties", - icon: TagIcon, - current: pathname.includes("/properties"), - }, - { - name: "Cache", - href: "/cache", - icon: ArchiveBoxIcon, - current: pathname.includes("/cache"), - }, - { - name: "Rate Limits", - href: "/rate-limit", - icon: ShieldCheckIcon, - current: pathname.includes("/rate-limit"), + name: "Segments", + href: "/segments", + icon: null, + current: false, + subItems: [ + { + name: "Sessions", + href: "/sessions", + icon: PiGraphLight, + current: pathname.includes("/sessions"), + }, + { + name: "Properties", + href: "/properties", + icon: TagIcon, + current: pathname.includes("/properties"), + }, + + { + name: "Users", + href: "/users", + icon: UsersIcon, + current: pathname.includes("/users"), + }, + ], }, { - name: "Playground", - href: "/playground", - icon: BeakerIcon, - current: pathname.includes("/playground"), + name: "Improve", + href: "/improve", + icon: null, + current: false, + subItems: [ + { + name: "Prompts", + href: "/prompts", + icon: NotepadText, + current: pathname.includes("/prompts"), + }, + { + name: "Playground", + href: "/playground", + icon: TestTube2, + current: pathname.includes("/playground"), + }, + + ...(!user?.email?.includes("@helicone.ai") + ? [] + : [ + { + name: "Evals", + href: "/evals", + icon: SparklesIcon, + current: pathname.includes("/evals"), + }, + ]), + { + name: "Datasets", + href: "/datasets", + icon: CircleStackIcon, + current: pathname.includes("/datasets"), + }, + ], }, + { name: "Developer", href: "/developer", - icon: CodeBracketIcon, + icon: null, current: pathname.includes("/developer"), + subItems: [ + { + name: "Cache", + href: "/cache", + icon: ArchiveBoxIcon, + current: pathname.includes("/cache"), + }, + { + name: "Rate Limits", + href: "/rate-limit", + icon: ShieldCheckIcon, + current: pathname === "/rate-limit", + }, + { + name: "Alerts", + href: "/alerts", + icon: BellIcon, + current: pathname.includes("/alerts"), + }, + { + name: "Webhooks", + href: "/webhooks", + icon: Webhook, + current: pathname.includes("/webhooks"), + }, + { + name: "Vault", + href: "/vault", + icon: LockClosedIcon, + current: pathname.includes("/vault"), + }, + ], }, - { - name: "Settings", - href: "/settings", - icon: Cog6ToothIcon, - current: pathname.includes("/settings"), - }, + ...(org?.currentOrg?.tier === "enterprise" + ? [ + { + name: "Enterprise", + href: "/enterprise", + icon: BuildingLibraryIcon, + current: pathname.includes("/enterprise"), + subItems: [ + { + name: "Webhooks", + href: "/enterprise/webhooks", + icon: null, + current: false, + }, + { + name: "Vault", + href: "/enterprise/vault", + icon: null, + current: false, + }, + ], + }, + ] + : []), + // { + // name: "Settings", + // href: "/settings", + // icon: Cog6ToothIcon, + // current: false, + // subItems: [ + // { + // name: "Organization", + // href: "/settings/organization", + // icon: null, + // current: false, + // }, + // { + // name: "API Keys", + // href: "/settings/api-keys", + // icon: null, + // current: false, + // }, + // ...(!user?.email?.includes("@helicone.ai") + // ? [] + // : [ + // { + // name: "Connections", + // href: "/settings/connections", + // icon: null, + // current: pathname.includes("/settings/connections"), + // }, + // ]), + // { + // name: "Members", + // href: "/settings/members", + // icon: null, + // current: false, + // }, + // { + // name: "Billing", + // href: "/settings/billing", + // icon: null, + // current: pathname.includes("/settings/billing"), + // }, + // ], + // }, ], [pathname, user?.email] ); return ( <> - + - + ); }; diff --git a/web/components/layout/auth/authLayout.tsx b/web/components/layout/auth/authLayout.tsx index 249acbb487..c975608b7c 100644 --- a/web/components/layout/auth/authLayout.tsx +++ b/web/components/layout/auth/authLayout.tsx @@ -7,7 +7,6 @@ import { useOrg } from "../organizationContext"; import UpgradeProModal from "../../shared/upgradeProModal"; import { useAlertBanners } from "../../../services/hooks/admin"; -import ReferralModal from "../../shared/referralModal"; import MetaData from "../public/authMetaData"; import DemoModal from "./DemoModal"; @@ -24,8 +23,7 @@ const AuthLayout = (props: AuthLayoutProps) => { const router = useRouter(); const { pathname } = router; const org = useOrg(); - const tier = org?.currentOrg?.tier; - const [referOpen, setReferOpen] = useState(false); + const [open, setOpen] = useState(false); const currentPage = useMemo(() => { @@ -50,11 +48,7 @@ const AuthLayout = (props: AuthLayoutProps) => {
- +
@@ -63,7 +57,7 @@ const AuthLayout = (props: AuthLayoutProps) => {
- + ); diff --git a/web/components/layout/common/button.tsx b/web/components/layout/common/button.tsx index 993e0335d4..4521e18502 100644 --- a/web/components/layout/common/button.tsx +++ b/web/components/layout/common/button.tsx @@ -1,20 +1,23 @@ import React from "react"; interface GenericButtonProps { - onClick: () => void; + onClick: (event: React.MouseEvent) => void; icon?: React.ReactNode; text: string; count?: number; + disabled?: boolean; + className?: string; } export const GenericButton = React.forwardRef< HTMLButtonElement, GenericButtonProps ->(({ onClick, icon, text, count }, ref) => ( +>(({ onClick, icon, text, count, disabled, className }, ref) => ( @@ -219,9 +218,6 @@ export default function OrgDropdown({ setReferOpen }: OrgDropdownProps) { createNewOrgHandler()}> Create New Org - setAddOpen(true)}> - Invite Members - User Settings (({ children, featureName, enabled = true }, ref) => { + const [isDialogOpen, setIsDialogOpen] = useState(false); + const org = useOrg(); + + const customDescription = useMemo( + () => descriptions?.[featureName as keyof typeof descriptions], + [featureName] + ); + + const hasAccess = React.useMemo(() => { + return ( + enabled && + (org?.currentOrg?.tier === "pro-20240913" || + org?.currentOrg?.tier === "growth" || + (org?.currentOrg?.stripe_metadata as { addons?: { prompts?: boolean } }) + ?.addons?.prompts) + ); + }, [org?.currentOrg?.tier, org?.currentOrg?.stripe_metadata, enabled]); + + const handleClick = useCallback( + (e: React.MouseEvent) => { + if (!hasAccess) { + e.preventDefault(); + e.stopPropagation(); + setIsDialogOpen(true); + } else if (children.props.onClick) { + children.props.onClick(e); + } + }, + [hasAccess, children.props, setIsDialogOpen] + ); + + const isPro = useMemo(() => { + return org?.currentOrg?.tier === "pro-20240913"; + }, [org?.currentOrg?.tier]); + return ( + <> + {React.cloneElement(children, { + ref, + onClick: handleClick, + className: !hasAccess + ? `${children.props.className || ""} ` + : children.props.className, + })} + + + + + Need more requests? + +

+ {customDescription || + "The Free plan only comes with 10,000 requests per month, but getting more is easy."} +

+
+
+

Free

+ {!isPro && ( + + Current plan + + )} +
    +
  • + + 10k free requests/month +
  • +
  • + + Access to Dashboard +
  • +
  • + + Free, truly +
  • +
+
+
+

Pro

+ {isPro && ( + + Current plan + + )} + $20/user +

Everything in Free, plus:

+
    +
  • + + Limitless requests (first 100k free) +
  • +
  • + + Access to all features +
  • +
  • + + Standard support +
  • +
+ + See all features → + + + +
+
+

+ Don't worry, we are still processing all your incoming + requests. You will be able to see them when you upgrade to Pro. +

+
+
+ + ); +}); + +ProFeatureWrapper.displayName = "ProFeatureWrapper"; diff --git a/web/components/shared/helicone/FeatureUpgradeCard.tsx b/web/components/shared/helicone/FeatureUpgradeCard.tsx new file mode 100644 index 0000000000..bbbafbfc9e --- /dev/null +++ b/web/components/shared/helicone/FeatureUpgradeCard.tsx @@ -0,0 +1,136 @@ +import React, { useState } from "react"; +import Link from "next/link"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { InfoBox } from "@/components/ui/helicone/infoBox"; +import { ChevronDownIcon, ChevronUpIcon, CheckIcon } from "lucide-react"; + +interface FeatureUpgradeCardProps { + title: string; + description: string; + infoBoxText: string; + videoSrc?: string; + youtubeVideo?: string; + documentationLink: string; +} + +export const FeatureUpgradeCard: React.FC = ({ + title, + description, + infoBoxText, + videoSrc, + youtubeVideo, + documentationLink, +}) => { + const [isPlanComparisonVisible, setIsPlanComparisonVisible] = useState(false); + + return ( + + + {title} +

+ {description} +

+
+ + +

{infoBoxText}

+
+ {videoSrc && ( +
+ +
+ )} + {youtubeVideo && ( + + )} +
+ + + {isPlanComparisonVisible && ( +
+

Plan Comparison

+
+
+

Free

+ + Current plan + +
    +
  • + + 10k free requests/month +
  • +
  • + + Access to Dashboard +
  • +
  • + + Free, truly +
  • +
+
+
+

Pro

+ $20/user +

Everything in Free, plus:

+
    +
  • + + Limitless requests (first 100k free) +
  • +
  • + + Access to all features +
  • +
  • + + Standard support +
  • +
+ + See all features → + +
+
+
+ )} +
+ + +
+
+
+ ); +}; diff --git a/web/components/shared/themed/themedTimeFilter.tsx b/web/components/shared/themed/themedTimeFilter.tsx index f328119a7e..d8294be07f 100644 --- a/web/components/shared/themed/themedTimeFilter.tsx +++ b/web/components/shared/themed/themedTimeFilter.tsx @@ -6,6 +6,7 @@ import useNotification from "../notification/useNotification"; import useSearchParams from "../utils/useSearchParams"; import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@tremor/react"; import { TimeFilter } from "../../templates/dashboard/dashboardPage"; +import { ProFeatureWrapper } from "../ProBlockerComponents/ProFeatureWrapper"; interface ThemedTimeFilterProps { timeFilterOptions: { key: string; value: string }[]; @@ -257,30 +258,57 @@ const ThemedTimeFilter = (props: ThemedTimeFilterProps) => { )} - {timeFilterOptions.map((option, idx) => ( - - ))} + {timeFilterOptions.map((option, idx) => + ["3M", "All"].includes(option.value) ? ( + + + + ) : ( + + ) + )} ); }; diff --git a/web/components/templates/alerts/alertsPage.tsx b/web/components/templates/alerts/alertsPage.tsx index 2dc9bd27d8..63f276328d 100644 --- a/web/components/templates/alerts/alertsPage.tsx +++ b/web/components/templates/alerts/alertsPage.tsx @@ -12,6 +12,8 @@ import { getUSDate } from "../../shared/utils/utils"; import { Tooltip } from "@mui/material"; import EditAlertModal from "./editAlertModal"; import { useGetOrgSlackChannels } from "@/services/hooks/organizations"; +import { ProFeatureWrapper } from "@/components/shared/ProBlockerComponents/ProFeatureWrapper"; +import { Col } from "@/components/layout/common"; interface AlertsPageProps { user: User; @@ -70,26 +72,51 @@ const AlertsPage = (props: AlertsPageProps) => { organization

- + + + + + +
    {alerts.length === 0 ? ( - + + ) : ( { const [open, setOpen] = useState(false); const [openUpgradeModal, setOpenUpgradeModal] = useState(false); - const hasCache = chMetrics.totalCacheHits.data?.data - ? +chMetrics.totalCacheHits.data?.data > 0 - : true; + const hasCache = useMemo(() => { + return chMetrics.totalCacheHits.data?.data !== undefined && + chMetrics.totalCacheHits.data?.data !== null + ? +chMetrics.totalCacheHits.data?.data > 0 + : true; + }, [chMetrics.totalCacheHits.data?.data]); const metrics = [ { @@ -129,10 +137,17 @@ const CachePage = (props: CachePageProps) => { cacheDist.sort((a: any, b: any) => a.name.localeCompare(b.name)); + const org = useOrg(); + const isPro = org?.currentOrg?.tier !== "free"; + return ( <> + Cache Beta + + } actions={ { } /> - {!hasCache ? ( -
    -
    - + {!isPro ? ( +
    + +
    + ) : !hasCache ? ( +
    +
    +
    + +
    +

    - No cache data available + {!isPro + ? "Upgrade to Pro to start using Cache" + : "No Cache Data Found"}

    - Please view our documentation to learn how to enable cache for - your requests. + View our documentation to learn how to use caching.

    -
    +
    View Docs
    + + {isPro && ( +
    + Or + +
    +

    + TS/JS Quick Start +

    + +
    +
    + )}
    ) : ( diff --git a/web/components/templates/connections/connectionsPage.tsx b/web/components/templates/connections/connectionsPage.tsx index 0a63a9d0bf..8fc9578f5c 100644 --- a/web/components/templates/connections/connectionsPage.tsx +++ b/web/components/templates/connections/connectionsPage.tsx @@ -27,19 +27,19 @@ const ConnectionsPage: React.FC = () => { const allItems: Integration[] = useMemo( () => [ - { title: "OpenAI", type: "provider" }, - { title: "Anthropic", type: "provider" }, - { title: "Together AI", type: "provider" }, - { title: "OpenRouter", type: "provider" }, - { title: "Fireworks", type: "provider" }, - { title: "Azure", type: "provider" }, - { title: "Groq", type: "provider" }, - { title: "Deepinfra", type: "provider" }, - { title: "Anyscale", type: "provider" }, - { title: "Cloudflare", type: "provider" }, - { title: "LemonFox", type: "provider" }, - { title: "Perplexity", type: "provider" }, - { title: "Mistral", type: "provider" }, + // { title: "OpenAI", type: "provider" }, + // { title: "Anthropic", type: "provider" }, + // { title: "Together AI", type: "provider" }, + // { title: "OpenRouter", type: "provider" }, + // { title: "Fireworks", type: "provider" }, + // { title: "Azure", type: "provider" }, + // { title: "Groq", type: "provider" }, + // { title: "Deepinfra", type: "provider" }, + // { title: "Anyscale", type: "provider" }, + // { title: "Cloudflare", type: "provider" }, + // { title: "LemonFox", type: "provider" }, + // { title: "Perplexity", type: "provider" }, + // { title: "Mistral", type: "provider" }, { title: "OpenPipe", type: "fine-tuning", @@ -48,12 +48,12 @@ const ConnectionsPage: React.FC = () => { (integration) => integration.integration_name === "open_pipe" )?.active ?? false, }, - { title: "PostHog", type: "destination", enabled: false }, - { title: "Datadog", type: "destination", enabled: false }, - { title: "Pillar", type: "gateway", enabled: true }, - { title: "NotDiamond", type: "gateway", enabled: false }, - { title: "Diffy", type: "other-provider", enabled: false }, - { title: "Lytix", type: "destination", enabled: false }, + // { title: "PostHog", type: "destination", enabled: false }, + // { title: "Datadog", type: "destination", enabled: false }, + // { title: "Pillar", type: "gateway", enabled: true }, + // { title: "NotDiamond", type: "gateway", enabled: false }, + // { title: "Diffy", type: "other-provider", enabled: false }, + // { title: "Lytix", type: "destination", enabled: false }, ], [integrations] ); @@ -80,7 +80,7 @@ const ConnectionsPage: React.FC = () => { return (
    -

    Integrations

    +

    Connections

    Explore and connect with various integrations to enhance your Helicone experience. diff --git a/web/components/templates/datasets/datasetsPage.tsx b/web/components/templates/datasets/datasetsPage.tsx index 07d22b5453..0a8501dc3c 100644 --- a/web/components/templates/datasets/datasetsPage.tsx +++ b/web/components/templates/datasets/datasetsPage.tsx @@ -1,13 +1,9 @@ -import { - BookOpenIcon, - CircleStackIcon as DatabaseIcon, -} from "@heroicons/react/24/outline"; -import Link from "next/link"; import { useRouter } from "next/router"; import { useGetHeliconeDatasets } from "../../../services/hooks/dataset/heliconeDataset"; import { SortDirection } from "../../../services/lib/sorts/users/sorts"; import AuthHeader from "../../shared/authHeader"; import ThemedTable from "../../shared/themed/table/themedTable"; +import { FeatureUpgradeCard } from "@/components/shared/helicone/FeatureUpgradeCard"; interface DatasetsPageProps { currentPage: number; @@ -31,50 +27,14 @@ const DatasetsPage = (props: DatasetsPageProps) => { <> {!isLoading && datasets.length === 0 ? ( -

    -
    - -

    - No Datasets -

    - -

    - Create your first dataset to get started. Here's a quick - tutorial: -

    -
    - -
    -
    - - - View Docs - - - - Create Dataset - -
    -
    +
    +
    ) : ( ; -}[] = [ - { - id: 0, - title: "Keys", - icon: KeyIcon, - }, - { - id: 1, - title: "Webhooks", - icon: GlobeAltIcon, - }, - { - id: 2, - title: "Vault", - icon: LockClosedIcon, - }, -]; - -const DeveloperPage = (props: DeveloperPageProps) => { - const { defaultIndex = 0 } = props; - - const orgContext = useOrg(); - - const user = useUser(); - - const router = useRouter(); - - const { hasFlag } = useFeatureFlags( - "webhook_beta", - orgContext?.currentOrg?.id || "" - ); - - const tier = orgContext?.currentOrg?.tier; - - const isPaidPlan = tier !== "free"; + children: React.ReactNode; +} +const DeveloperPage: React.FC = ({ title, children }) => { return ( <> - - - - {tabs.map((tab) => ( - { - router.push( - { - query: { tab: tab.id }, - }, - undefined, - { shallow: true } - ); - }} - > - {tab.title} - - ))} - - - - - - -
    - {hasFlag ? ( - - ) : ( -
    -
    - -

    - We'd love to learn more about your use case -

    -

    - Please get in touch with us to discuss our webhook - feature. -

    -
    - - Contact Us - -
    -
    -
    - )} -
    -
    - - {isPaidPlan ? ( - - ) : ( -
    -
    - -

    - We'd love to learn more about your use case -

    -

    - Please get in touch with us to discuss our vault feature. -

    -
    - - Contact Us - -
    -
    -
    - )} -
    -
    -
    + + {children} ); }; diff --git a/web/components/templates/home/components/platform.tsx b/web/components/templates/home/components/platform.tsx index b1113a13b4..cb6a769e9b 100644 --- a/web/components/templates/home/components/platform.tsx +++ b/web/components/templates/home/components/platform.tsx @@ -119,6 +119,7 @@ export default function Platform() {
    + {/* eslint-disable-next-line @next/next/no-img-element */} tab.key === activeTab)?.src || diff --git a/web/components/templates/organization/deleteOrgModal.tsx b/web/components/templates/organization/deleteOrgModal.tsx index 32b3120aab..50b1563d98 100644 --- a/web/components/templates/organization/deleteOrgModal.tsx +++ b/web/components/templates/organization/deleteOrgModal.tsx @@ -1,9 +1,7 @@ -import { useSupabaseClient } from "@supabase/auth-helpers-react"; import { clsx } from "../../shared/clsx"; import { useOrg } from "../../layout/organizationContext"; import useNotification from "../../shared/notification/useNotification"; import ThemedModal from "../../shared/themed/themedModal"; -import { Database } from "../../../supabase/database.types"; import { useRouter } from "next/router"; import { useState } from "react"; import { getJawnClient } from "../../../lib/clients/jawn"; @@ -22,7 +20,6 @@ export const DeleteOrgModal = (props: DeleteOrgModalProps) => { const orgContext = useOrg(); const router = useRouter(); const jawn = getJawnClient(orgId); - const supabaseClient = useSupabaseClient(); const [confirmOrgName, setConfirmOrgName] = useState(""); return ( @@ -65,6 +62,17 @@ export const DeleteOrgModal = (props: DeleteOrgModalProps) => { + + +
    {isLoading || isOrgOwnerLoading ? ( diff --git a/web/components/templates/organization/plan/EnterprisePlanCard.tsx b/web/components/templates/organization/plan/EnterprisePlanCard.tsx new file mode 100644 index 0000000000..6233565ca6 --- /dev/null +++ b/web/components/templates/organization/plan/EnterprisePlanCard.tsx @@ -0,0 +1,70 @@ +import { Col } from "@/components/layout/common"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import Link from "next/link"; +import { PlanFeatureCard } from "./PlanFeatureCard"; + +export const EnterprisePlanCard = () => { + return ( +
    + + + + Enterprise{" "} + + Current plan + + + + Your custom Enterprise plan tailored for your organization{"'"}s + needs. + + + + +

    + For detailed information about your Enterprise plan, including + custom features, limits, and support options, please contact your + account manager. +

    + + + + +
    +
    + +
    + + window.open( + "https://cal.com/team/helicone/helicone-discovery", + "_blank" + ) + } + /> + + + window.open( + "https://docs.helicone.ai/advanced-usage/enterprise-features", + "_blank" + ) + } + /> +
    +
    + ); +}; diff --git a/web/components/templates/organization/plan/InvoiceSheet.tsx b/web/components/templates/organization/plan/InvoiceSheet.tsx new file mode 100644 index 0000000000..ef19eb06e0 --- /dev/null +++ b/web/components/templates/organization/plan/InvoiceSheet.tsx @@ -0,0 +1,118 @@ +import { Button } from "@/components/ui/button"; +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, + SheetTrigger, +} from "@/components/ui/sheet"; +import { useState } from "react"; +import { useOrg } from "@/components/layout/organizationContext"; +import { useQuery } from "@tanstack/react-query"; +import { getJawnClient } from "@/lib/clients/jawn"; + +export const InvoiceSheet: React.FC = () => { + const [isSheetOpen, setIsSheetOpen] = useState(false); + const org = useOrg(); + + const upcomingInvoice = useQuery({ + queryKey: ["upcoming-invoice", org?.currentOrg?.id], + queryFn: async (query) => { + const orgId = query.queryKey[1] as string; + const jawn = getJawnClient(orgId); + const invoice = await jawn.GET("/v1/stripe/subscription/preview-invoice"); + return invoice; + }, + }); + + const formatCurrency = (amount: number) => { + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: upcomingInvoice.data?.data?.currency || "USD", + }).format(amount / 100); + }; + + return ( + + + + + + + Upcoming Invoice + + Details of your next billing cycle + + +
    + {upcomingInvoice.isLoading ? ( +

    Loading invoice details...

    + ) : upcomingInvoice.error ? ( +

    Error loading invoice. Please try again.

    + ) : upcomingInvoice.data?.data ? ( + <> +
    + Total Due: + + {formatCurrency(upcomingInvoice.data.data.total)} + +
    +
    + Due Date:{" "} + {new Date( + upcomingInvoice.data.data.next_payment_attempt! * 1000 + ).toLocaleDateString()} +
    +
    +

    Line Items:

    +
    + {upcomingInvoice.data.data.lines?.data.map( + (item: any, index: number) => ( +
    + {item.description} + {formatCurrency(item.amount)} +
    + ) + )} +
    +
    + {upcomingInvoice.data.data.discount && ( +
    +

    Discount:

    +
    + {upcomingInvoice.data.data.discount.coupon.name} - + {upcomingInvoice.data.data.discount.coupon.percent_off}% off +
    +
    + )} +
    +
    + Subtotal: + + {formatCurrency(upcomingInvoice.data.data.subtotal)} + +
    +
    + Tax: + + {formatCurrency(upcomingInvoice.data.data.tax ?? 0)} + +
    +
    + Total: + {formatCurrency(upcomingInvoice.data.data.total)} +
    +
    + + ) : ( +

    No invoice data available.

    + )} +
    +
    +
    + ); +}; diff --git a/web/components/templates/organization/plan/MigrateGrowthToPro.tsx b/web/components/templates/organization/plan/MigrateGrowthToPro.tsx new file mode 100644 index 0000000000..2104086e6b --- /dev/null +++ b/web/components/templates/organization/plan/MigrateGrowthToPro.tsx @@ -0,0 +1,216 @@ +import { Col } from "@/components/layout/common"; +import { useOrg } from "@/components/layout/organizationContext"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { getJawnClient } from "@/lib/clients/jawn"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import Link from "next/link"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { useState } from "react"; +import { CalendarIcon } from "lucide-react"; +import { PlanFeatureCard } from "./PlanFeatureCard"; +import { InfoBox } from "@/components/ui/helicone/infoBox"; + +export const MigrateGrowthToPro = () => { + const org = useOrg(); + const [isUpgradeDialogOpen, setIsUpgradeDialogOpen] = useState(false); + const [isCancelDialogOpen, setIsCancelDialogOpen] = useState(false); + + const subscription = useQuery({ + queryKey: ["subscription", org?.currentOrg?.id], + queryFn: async (query) => { + const orgId = query.queryKey[1] as string; + const jawn = getJawnClient(orgId); + const subscription = await jawn.GET("/v1/stripe/subscription"); + return subscription; + }, + }); + + const upgradeExistingCustomerToPro = useMutation({ + mutationFn: async () => { + const jawn = getJawnClient(org?.currentOrg?.id); + const result = await jawn.POST("/v1/stripe/subscription/migrate-to-pro"); + return result; + }, + }); + + const cancelSubscription = useMutation({ + mutationFn: async () => { + const jawn = getJawnClient(org?.currentOrg?.id); + const result = await jawn.POST( + "/v1/stripe/subscription/cancel-subscription" + ); + return result; + }, + }); + + const handleUpgrade = async () => { + const result = await upgradeExistingCustomerToPro.mutateAsync(); + setIsUpgradeDialogOpen(false); + subscription.refetch(); + window.location.reload(); + }; + + const handleCancel = async () => { + await cancelSubscription.mutateAsync(); + setIsCancelDialogOpen(false); + subscription.refetch(); + }; + + const isSubscriptionEnding = subscription.data?.data?.cancel_at_period_end; + + const getBillingCycleDates = () => { + if ( + subscription.data?.data?.current_period_start && + subscription.data?.data?.current_period_end + ) { + const startDate = new Date( + subscription.data.data.current_period_start * 1000 + ); + const endDate = new Date( + subscription.data.data.current_period_end * 1000 + ); + return `Next billing period: ${startDate.toLocaleDateString()} - ${endDate.toLocaleDateString()}`; + } + return "N/A"; + }; + + return ( +
    + + + + Growth{" "} + + Deprecating soon + + + + We are discontinuing the Growth plan soon. Please{" "} + upgrade to Pro to keep 100k requests every month and access + all features or downgrade to Free plan. + + + + <>} variant="warning"> +

    Growth plan will be discontinued on October 15th, 2024

    +
    +
    + + {getBillingCycleDates()} +
    + + + + {isSubscriptionEnding && ( +

    + Your subscription is already set to cancel at the end of the + current billing period. +

    + )} + + View old billing page + + + View pricing page + + +
    +
    + +
    + + + +
    + + + + + Upgrade to Pro Plan + + You are about to upgrade to the Pro plan. This will give you + access to all Pro features. + + + + + + + + + + + + + Cancel Subscription + + Are you sure you want to cancel your subscription? You will lose + access to all Growth plan features at the end of your current + billing period. + + + + + + + + +
    + ); +}; diff --git a/web/components/templates/organization/plan/PlanFeatureCard.tsx b/web/components/templates/organization/plan/PlanFeatureCard.tsx new file mode 100644 index 0000000000..ee9504ca8a --- /dev/null +++ b/web/components/templates/organization/plan/PlanFeatureCard.tsx @@ -0,0 +1,34 @@ +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; + +interface PlanFeatureCardProps { + title: string; + description: string; + buttonText: string; + onButtonClick?: () => void; +} + +export const PlanFeatureCard: React.FC = ({ + title, + description, + buttonText, + onButtonClick, +}) => ( + + + {title} + {description} + + + + + +); diff --git a/web/components/templates/organization/plan/UnknownTierCard.tsx b/web/components/templates/organization/plan/UnknownTierCard.tsx new file mode 100644 index 0000000000..8268738017 --- /dev/null +++ b/web/components/templates/organization/plan/UnknownTierCard.tsx @@ -0,0 +1,46 @@ +import { Col } from "@/components/layout/common"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import Link from "next/link"; + +interface UnknownTierCardProps { + tier: string; +} + +export const UnknownTierCard: React.FC = ({ tier }) => { + return ( +
    + + + Unknown Plan + + We couldn't recognize your current plan. We've sent + migration instructions to your email. + + + + +
    + Your current plan: {tier} +
    + + + + +

    + If you're still having trouble, please email our support + team at {"support@helicone.ai"} for assistance. +

    + + +
    +
    +
    + ); +}; diff --git a/web/components/templates/organization/plan/billingPage.tsx b/web/components/templates/organization/plan/billingPage.tsx new file mode 100644 index 0000000000..3a34070f72 --- /dev/null +++ b/web/components/templates/organization/plan/billingPage.tsx @@ -0,0 +1,46 @@ +import { useOrg } from "@/components/layout/organizationContext"; +import AuthHeader from "@/components/shared/authHeader"; +import { Col } from "@/components/layout/common"; +import { FreePlanCard } from "./freeBillingPage"; +import { ProPlanCard } from "./proBillingPage"; +import { MigrateGrowthToPro } from "./MigrateGrowthToPro"; +import { UnknownTierCard } from "./UnknownTierCard"; +import { EnterprisePlanCard } from "./EnterprisePlanCard"; + +interface OrgPlanPageProps {} + +const BillingPlanPage = (props: OrgPlanPageProps) => { + const org = useOrg(); + + const knownTiers = ["free", "pro-20240913", "growth", "enterprise"]; + + return ( + <> + Billing
    } + /> + + {org?.currentOrg?.tier === "growth" && } + {org?.currentOrg?.tier === "free" && } + {org?.currentOrg?.tier === "pro-20240913" && } + {org?.currentOrg?.tier && + !knownTiers.includes(org?.currentOrg?.tier) && ( + + )} + {org?.currentOrg?.tier === "enterprise" && } + {/* +
    + Looking for something else? +
    +

    Contact us

    +

    + Observability needs, support, or just want to say hi? +

    + +
    */} + + + ); +}; + +export default BillingPlanPage; diff --git a/web/components/templates/organization/plan/freeBillingPage.tsx b/web/components/templates/organization/plan/freeBillingPage.tsx new file mode 100644 index 0000000000..5e2c7f8f2a --- /dev/null +++ b/web/components/templates/organization/plan/freeBillingPage.tsx @@ -0,0 +1,250 @@ +import { Col } from "@/components/layout/common"; +import { useOrg } from "@/components/layout/organizationContext"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Progress } from "@/components/ui/progress"; +import { getJawnClient } from "@/lib/clients/jawn"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { CalendarIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"; +import { useState } from "react"; + +export const FreePlanCard = () => { + const org = useOrg(); + const freeUsage = useQuery({ + queryKey: ["free-usage", org?.currentOrg?.id], + queryFn: async (query) => { + const orgId = query.queryKey[1] as string; + const jawn = getJawnClient(orgId); + const invoice = await jawn.GET("/v1/stripe/subscription/free/usage"); + return invoice; + }, + }); + + const subscription = useQuery({ + queryKey: ["subscription", org?.currentOrg?.id], + queryFn: async (query) => { + const orgId = query.queryKey[1] as string; + const jawn = getJawnClient(orgId); + const subscription = await jawn.GET("/v1/stripe/subscription"); + return subscription; + }, + }); + + const upgradeToPro = useMutation({ + mutationFn: async () => { + const jawn = getJawnClient(org?.currentOrg?.id); + const endpoint = + subscription.data?.data?.status === "canceled" + ? "/v1/stripe/subscription/existing-customer/upgrade-to-pro" + : "/v1/stripe/subscription/new-customer/upgrade-to-pro"; + const result = await jawn.POST(endpoint, { + body: {}, + }); + return result; + }, + }); + + const isOverUsage = freeUsage.data?.data && freeUsage.data?.data >= 100_000; + + const getBillingCycleDates = () => { + const now = new Date(); + const currentYear = now.getFullYear(); + const currentMonth = now.getMonth(); + const startDate = new Date(currentYear, currentMonth, 1); + const endDate = new Date(currentYear, currentMonth + 1, 0); + + const formatDate = (date: Date) => + date.toLocaleDateString("en-US", { + month: "long", + day: "numeric", + year: "numeric", + }); + + return `${formatDate(startDate)} → ${formatDate(endDate)}`; + }; + + const [isComparisonOpen, setIsComparisonOpen] = useState(false); + + return ( +
    + + + + Free{" "} + + Current plan + + + + Here's a summary of your request usage this month. + + + +
    +
    + Requests used +
    +
    + {freeUsage.data?.data?.toLocaleString()} / 10,000 +
    +
    + +
    + + {getBillingCycleDates()} +
    + + + + 14 days free trial + + +
    + + + The Pro plan covers everything in Free, and: + + {isComparisonOpen && ( +
    + {proFeatures.map((feature, index) => ( + + ))} +
    + )} +
    +
    +
    + +
    + + + + Learn about our Enterprise plan + + + Built for companies looking to scale. Includes everything in Pro, + plus unlimited requests, prompts, experiments and more. + + + + + + + + + + + Looking for something else? + + + Need support, have a unique use case or want to say hi? + + + + + + +
    +
    + ); +}; + +const ComparisonItem = ({ + title, + description, +}: { + title: string; + description: string; +}) => ( +
    +
    +

    {title}

    +

    + {description} +

    +
    +
    +); + +const proFeatures = [ + { + title: "100k requests", + description: "Higher limit compared to 10k.", + }, + { + title: "3 month log retention", + description: "Longer log retention compared to 1 month.", + }, + { + title: "Access to Playground", + description: "Test your prompts with different models.", + }, + { + title: "Access to Caching", + description: "Cache frequent responses to save costs and time.", + }, + { + title: "Access to Rate Limits", + description: "Limit your user's usage.", + }, + { + title: "Access to Sessions", + description: "Trace agent workflow and conversations.", + }, + { + title: "Access to User Tracking", + description: "Keep track of your users.", + }, + { + title: "Access to Datasets", + description: "Collect historical requests for training and finetuning.", + }, + { + title: "API Access", + description: "Access to 60 calls/min using our expansive API.", + }, + { + title: "SOC-2 Type II Compliance", + description: "Safety and privacy.", + }, + { + title: "Prompts & Experiments", + description: "Collect historical requests for training and finetuning.", + }, + { + title: "Alerts (Slack + Email)", + description: "Access to 60 calls/min using our expansive API.", + }, +]; diff --git a/web/components/templates/organization/plan/proBillingPage.tsx b/web/components/templates/organization/plan/proBillingPage.tsx new file mode 100644 index 0000000000..24960735d5 --- /dev/null +++ b/web/components/templates/organization/plan/proBillingPage.tsx @@ -0,0 +1,371 @@ +import { Col } from "@/components/layout/common"; +import { useOrg } from "@/components/layout/organizationContext"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { getJawnClient } from "@/lib/clients/jawn"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import Link from "next/link"; +import { InvoiceSheet } from "./InvoiceSheet"; +import { Switch } from "@/components/ui/switch"; +import { Label } from "@/components/ui/label"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { useState } from "react"; +import { CalendarIcon } from "lucide-react"; +import { PlanFeatureCard } from "./PlanFeatureCard"; +import { InfoBox } from "@/components/ui/helicone/infoBox"; +import { useCallback } from "react"; + +export const ProPlanCard = () => { + const org = useOrg(); + const [isAlertsDialogOpen, setIsAlertsDialogOpen] = useState(false); + const [isPromptsDialogOpen, setIsPromptsDialogOpen] = useState(false); + const [isEnablingAlerts, setIsEnablingAlerts] = useState(false); + const [isEnablingPrompts, setIsEnablingPrompts] = useState(false); + + const subscription = useQuery({ + queryKey: ["subscription", org?.currentOrg?.id], + queryFn: async (query) => { + const orgId = query.queryKey[1] as string; + const jawn = getJawnClient(orgId); + const subscription = await jawn.GET("/v1/stripe/subscription"); + return subscription; + }, + }); + + const manageSubscriptionPaymentLink = useMutation({ + mutationFn: async () => { + const jawn = getJawnClient(org?.currentOrg?.id); + const result = await jawn.POST( + "/v1/stripe/subscription/manage-subscription" + ); + return result; + }, + }); + + const reactivateSubscription = useMutation({ + mutationFn: async () => { + const jawn = getJawnClient(org?.currentOrg?.id); + const result = await jawn.POST( + "/v1/stripe/subscription/undo-cancel-subscription" + ); + return result; + }, + }); + + const addProductToSubscription = useMutation({ + mutationFn: async (productType: "alerts" | "prompts") => { + const jawn = getJawnClient(org?.currentOrg?.id); + const result = await jawn.POST( + "/v1/stripe/subscription/add-ons/{productType}", + { + params: { + path: { + productType, + }, + }, + } + ); + return result; + }, + }); + + const deleteProductFromSubscription = useMutation({ + mutationFn: async (productType: "alerts" | "prompts") => { + const jawn = getJawnClient(org?.currentOrg?.id); + const result = await jawn.DELETE( + "/v1/stripe/subscription/add-ons/{productType}", + { + params: { + path: { + productType, + }, + }, + } + ); + return result; + }, + }); + + const isTrialActive = + subscription.data?.data?.trial_end && + new Date(subscription.data.data.trial_end * 1000) > new Date() && + (!subscription.data?.data?.current_period_start || + new Date(subscription.data.data.trial_end * 1000) > + new Date(subscription.data.data.current_period_start * 1000)); + + const isSubscriptionEnding = subscription.data?.data?.cancel_at_period_end; + + const hasAlerts = subscription.data?.data?.items?.some( + (item: any) => item.price.product?.name === "Alerts" && item.quantity > 0 + ); + const hasPrompts = subscription.data?.data?.items?.some( + (item: any) => item.price.product?.name === "Prompts" && item.quantity > 0 + ); + + const handleAlertsToggle = () => { + setIsEnablingAlerts(!hasAlerts); + setIsAlertsDialogOpen(true); + }; + + const handlePromptsToggle = () => { + setIsEnablingPrompts(!hasPrompts); + setIsPromptsDialogOpen(true); + }; + + const confirmAlertsChange = async () => { + if (isEnablingAlerts) { + await addProductToSubscription.mutateAsync("alerts"); + } else { + await deleteProductFromSubscription.mutateAsync("alerts"); + } + setIsAlertsDialogOpen(false); + subscription.refetch(); + }; + + const confirmPromptsChange = async () => { + if (isEnablingPrompts) { + await addProductToSubscription.mutateAsync("prompts"); + } else { + await deleteProductFromSubscription.mutateAsync("prompts"); + } + setIsPromptsDialogOpen(false); + subscription.refetch(); + }; + + const getBillingCycleDates = () => { + if ( + subscription.data?.data?.current_period_start && + subscription.data?.data?.current_period_end + ) { + const startDate = new Date( + subscription.data.data.current_period_start * 1000 + ); + const endDate = new Date( + subscription.data.data.current_period_end * 1000 + ); + return `${startDate.toLocaleDateString()} - ${endDate.toLocaleDateString()}`; + } + return "N/A"; + }; + + const getDialogDescription = useCallback( + (isEnabling: boolean, feature: string, price: string) => { + let description = isEnabling + ? `You are about to enable ${feature}. This will add ${price}/mo to your subscription.` + : `You are about to disable ${feature}. This will remove ${price}/mo from your subscription.`; + + if (isTrialActive && isEnabling) { + description += + " You will not be charged for this feature while on your trial."; + } + + return description; + }, + [isTrialActive] + ); + + return ( +
    + + + + Pro{" "} + + Current plan + + + + Here's a summary of your subscription. + + + + {isTrialActive && ( + <>}> +

    + Your trial ends on:{" "} + {new Date( + subscription.data!.data!.trial_end! * 1000 + ).toLocaleDateString()} +

    +
    + )} + {subscription.data?.data?.current_period_start && + subscription.data?.data?.current_period_end && ( +
    +

    + Current billing period:{" "} + {new Date( + subscription.data.data.current_period_start * 1000 + ).toLocaleDateString()}{" "} + -{" "} + {new Date( + subscription.data.data.current_period_end * 1000 + ).toLocaleDateString()} +

    + {isSubscriptionEnding && ( +

    + Your subscription will end on:{" "} + {new Date( + subscription.data.data.current_period_end * 1000 + ).toLocaleDateString()} +

    + )} +
    + )} +
    + + {getBillingCycleDates()} +
    + +
    +
    + + +
    + {isTrialActive && ( + + Included in trial (enable to start using) + + )} +
    +
    +
    + + +
    + {isTrialActive && ( + + Included in trial (enable to start using) + + )} +
    + + + {isSubscriptionEnding ? ( + + ) : ( + + )} + + + View pricing page + + +
    +
    + +
    + + + +
    + + + + + + {isEnablingAlerts ? "Enable Alerts" : "Disable Alerts"} + + + {getDialogDescription(isEnablingAlerts, "Alerts", "$15")} + + + + + + + + + + + + + + {isEnablingPrompts ? "Enable Prompts" : "Disable Prompts"} + + + {getDialogDescription(isEnablingPrompts, "Prompts", "$30")} + + + + + + + + +
    + ); +}; diff --git a/web/components/templates/pricing/pricingCompare.tsx b/web/components/templates/pricing/pricingCompare.tsx new file mode 100644 index 0000000000..cf9edd7a82 --- /dev/null +++ b/web/components/templates/pricing/pricingCompare.tsx @@ -0,0 +1,65 @@ +import { CheckIcon } from "lucide-react"; +import { UpgradeToProCTA } from "./upgradeToProCTA"; + +export const PricingCompare = ({ + featureName = "", +}: { + featureName: string; +}) => { + return ( + <> +

    + The Free plan only comes with 10,000 requests per month, but getting + more is easy. +

    +
    +
    +

    Free

    + + Current plan + +
      +
    • + + 10k free requests/month +
    • +
    • + + Access to Dashboard +
    • +
    • + + Free, truly +
    • +
    +
    +
    +

    Pro

    + $20/user +

    Everything in Free, plus:

    +
      +
    • + + Limitless requests (first 100k free) +
    • +
    • + + Access to all features +
    • +
    • + + Standard support +
    • +
    + + See all features → + + +
    +
    + + ); +}; diff --git a/web/components/templates/pricing/upgradeToProCTA.tsx b/web/components/templates/pricing/upgradeToProCTA.tsx new file mode 100644 index 0000000000..62a4872a67 --- /dev/null +++ b/web/components/templates/pricing/upgradeToProCTA.tsx @@ -0,0 +1,117 @@ +import { useOrg } from "@/components/layout/organizationContext"; +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; +import { getJawnClient } from "@/lib/clients/jawn"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { useMemo, useState } from "react"; + +export const UpgradeToProCTA = ({ + defaultPrompts = false, + defaultAlerts = false, + showAddons = false, +}) => { + const org = useOrg(); + const subscription = useQuery({ + queryKey: ["subscription", org?.currentOrg?.id], + queryFn: async (query) => { + const orgId = query.queryKey[1] as string; + const jawn = getJawnClient(orgId); + const subscription = await jawn.GET("/v1/stripe/subscription"); + return subscription; + }, + }); + const [prompts, setPrompts] = useState(defaultPrompts); + const [alerts, setAlerts] = useState(defaultAlerts); + + const upgradeToPro = useMutation({ + mutationFn: async () => { + const jawn = getJawnClient(org?.currentOrg?.id); + const endpoint = + subscription.data?.data?.status === "canceled" + ? "/v1/stripe/subscription/existing-customer/upgrade-to-pro" + : "/v1/stripe/subscription/new-customer/upgrade-to-pro"; + const result = await jawn.POST(endpoint, { + body: { + addons: { + prompts, + alerts, + }, + }, + }); + return result; + }, + }); + + const isPro = useMemo(() => { + return org?.currentOrg?.tier === "pro-20240913"; + }, [org?.currentOrg?.tier]); + + return ( +
    + {showAddons && ( +
    +

    Add-ons

    +
    +
    + setPrompts(checked)} + /> +
    + +

    + + $30/mo +

    +
    +
    +
    + setAlerts(checked)} + /> +
    + +

    + + $15/mo +

    +
    +
    +
    +
    + )} + + +
    + ); +}; diff --git a/web/components/templates/prompts/CreatePromptDialog.tsx b/web/components/templates/prompts/CreatePromptDialog.tsx new file mode 100644 index 0000000000..3c8e051582 --- /dev/null +++ b/web/components/templates/prompts/CreatePromptDialog.tsx @@ -0,0 +1,106 @@ +import { ProFeatureWrapper } from "@/components/shared/ProBlockerComponents/ProFeatureWrapper"; +import { DocumentPlusIcon } from "@heroicons/react/24/outline"; +import React, { useCallback, useRef, useState } from "react"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "../../ui/dialog"; +import HcButton from "../../ui/hcButton"; +import { Label } from "../../ui/label"; +import { Switch } from "../../ui/switch"; +import { MODEL_LIST } from "../playground/new/modelList"; +import { DiffHighlight } from "../welcome/diffHighlight"; + +interface CreatePromptDialogProps { + hasAccess: boolean; + onCreatePrompt: (name: string, model: string, content: string) => void; +} + +const CreatePromptDialog: React.FC = ({ + hasAccess, + onCreatePrompt, +}) => { + const [imNotTechnical, setImNotTechnical] = useState(false); + const [newPromptName, setNewPromptName] = useState(""); + const [newPromptModel, setNewPromptModel] = useState(MODEL_LIST[0].value); + const [newPromptContent, setNewPromptContent] = useState(""); + const [promptVariables, setPromptVariables] = useState([]); + const newPromptInputRef = useRef(null); + + const extractVariables = useCallback((content: string) => { + const regex = /\{\{([^}]+)\}\}/g; + const matches = content.match(regex); + return matches ? matches.map((match) => match.slice(2, -2).trim()) : []; + }, []); + + return ( + + + + + + + + + Create a new prompt +
    + + +
    +
    +
    + {imNotTechnical ? ( + <>{/* ... (rest of the non-technical UI) ... */} + ) : ( + <> +

    TS/JS Quick Start

    + + + )} +
    +
    +
    + ); +}; + +export default CreatePromptDialog; diff --git a/web/components/templates/prompts/promptsPage.tsx b/web/components/templates/prompts/promptsPage.tsx index 48f6f8577b..f4d15ea267 100644 --- a/web/components/templates/prompts/promptsPage.tsx +++ b/web/components/templates/prompts/promptsPage.tsx @@ -1,5 +1,9 @@ +import { useOrg } from "@/components/layout/organizationContext"; +import { ProFeatureWrapper } from "@/components/shared/ProBlockerComponents/ProFeatureWrapper"; +import { InfoBox } from "@/components/ui/helicone/infoBox"; +import { MagnifyingGlassIcon } from "@heroicons/react/20/solid"; import { - BookOpenIcon, + ChevronDownIcon, DocumentPlusIcon, DocumentTextIcon, EyeIcon, @@ -7,16 +11,27 @@ import { Square2StackIcon, TableCellsIcon, } from "@heroicons/react/24/outline"; -import { Divider, TextInput } from "@tremor/react"; +import { TextInput } from "@tremor/react"; import Link from "next/link"; import { useRouter } from "next/router"; -import { Fragment, useRef, useState, useCallback } from "react"; +import { useCallback, useMemo, useRef, useState } from "react"; +import { useJawnClient } from "../../../lib/clients/jawnHook"; import { usePrompts } from "../../../services/hooks/prompts/prompts"; -import { DiffHighlight } from "../welcome/diffHighlight"; -import PromptCard from "./promptCard"; +import AuthHeader from "../../shared/authHeader"; +import LoadingAnimation from "../../shared/loadingAnimation"; +import useNotification from "../../shared/notification/useNotification"; import { SimpleTable } from "../../shared/table/simpleTable"; -import HcButton from "../../ui/hcButton"; -import { MagnifyingGlassIcon } from "@heroicons/react/20/solid"; +import ThemedTabs from "../../shared/themed/themedTabs"; +import useSearchParams from "../../shared/utils/useSearchParams"; +import { Badge } from "../../ui/badge"; +import { Button } from "../../ui/button"; +import { + Card, + CardContent, + CardFooter, + CardHeader, + CardTitle, +} from "../../ui/card"; import { Dialog, DialogContent, @@ -24,20 +39,9 @@ import { DialogTitle, DialogTrigger, } from "../../ui/dialog"; -import PromptDelete from "./promptDelete"; -import LoadingAnimation from "../../shared/loadingAnimation"; -import PromptUsageChart from "./promptUsageChart"; -import ThemedTabs from "../../shared/themed/themedTabs"; -import useSearchParams from "../../shared/utils/useSearchParams"; -import AuthHeader from "../../shared/authHeader"; import HcBadge from "../../ui/hcBadge"; -import { Switch } from "../../ui/switch"; +import HcButton from "../../ui/hcButton"; import { Label } from "../../ui/label"; -import { Button } from "../../ui/button"; -import { useJawnClient } from "../../../lib/clients/jawnHook"; -import useNotification from "../../shared/notification/useNotification"; -import { Textarea } from "../../ui/textarea"; -import { Badge } from "../../ui/badge"; import { Select, SelectContent, @@ -45,8 +49,15 @@ import { SelectTrigger, SelectValue, } from "../../ui/select"; -import { MODEL_LIST } from "../playground/new/modelList"; +import { Switch } from "../../ui/switch"; +import { Textarea } from "../../ui/textarea"; import { Tooltip, TooltipContent, TooltipTrigger } from "../../ui/tooltip"; +import { MODEL_LIST } from "../playground/new/modelList"; +import { PricingCompare } from "../pricing/pricingCompare"; +import { DiffHighlight } from "../welcome/diffHighlight"; +import PromptCard from "./promptCard"; +import PromptDelete from "./promptDelete"; +import PromptUsageChart from "./promptUsageChart"; interface PromptsPageProps { defaultIndex: number; @@ -129,6 +140,22 @@ const PromptsPage = (props: PromptsPageProps) => { router.push(`/prompts/${res.data.data?.id}`); } }; + const org = useOrg(); + + const hasAccess = useMemo(() => { + return ( + org?.currentOrg?.tier === "growth" || + (org?.currentOrg?.tier === "pro-20240913" && + (org?.currentOrg?.stripe_metadata as { addons?: { prompts?: boolean } }) + ?.addons?.prompts) + ); + }, [org?.currentOrg?.tier, org?.currentOrg?.stripe_metadata]); + + const hasLimitedAccess = useMemo(() => { + return !hasAccess && (prompts?.length ?? 0) > 0; + }, [hasAccess, prompts?.length]); + + const [showPricingCompare, setShowPricingCompare] = useState(false); return ( <> @@ -137,6 +164,18 @@ const PromptsPage = (props: PromptsPageProps) => { title={
    Prompts + {hasLimitedAccess && ( + +

    + Need to create new prompts? + + + +

    +
    + )}
    } /> @@ -148,148 +187,167 @@ const PromptsPage = (props: PromptsPageProps) => {
    ) : ( <> -
    -
    -
    - setSearchName(value)} - placeholder="Search prompts..." - /> -
    - - - +
    +
    + setSearchName(value)} + placeholder="Search prompts..." /> - - - - Create a new prompt -
    - + + + + {hasAccess ? ( + - -
    -
    -
    - {imNotTechnical ? ( - <> -
    -
    - - - setNewPromptName(e.target.value) - } - ref={newPromptInputRef} - /> -
    -
    - - -
    -
    - -