diff --git a/core/config/usesFreeTrialApiKey.ts b/core/config/usesFreeTrialApiKey.ts index 6657b1edf00..0ffbf2db5b2 100644 --- a/core/config/usesFreeTrialApiKey.ts +++ b/core/config/usesFreeTrialApiKey.ts @@ -1,12 +1,12 @@ import { decodeSecretLocation, SecretType } from "@continuedev/config-yaml"; -import { BrowserSerializedContinueConfig } from ".."; +import { BrowserSerializedContinueConfig, ModelDescription } from ".."; /** - * Helper function to determine if the config uses a free trial API key + * Helper function to determine if the config uses an API key that relies on Continue credits (free trial or models add-on) * @param config The serialized config object * @returns true if the config is using any free trial models */ -export function usesFreeTrialApiKey( +export function usesCreditsBasedApiKey( config: BrowserSerializedContinueConfig | null, ): boolean { if (!config) { @@ -19,12 +19,7 @@ export function usesFreeTrialApiKey( // Check if any of the chat models use free-trial provider try { - const hasFreeTrial = allModels?.some( - (model) => - model.apiKeyLocation && - decodeSecretLocation(model.apiKeyLocation).secretType === - SecretType.FreeTrial, - ); + const hasFreeTrial = allModels?.some(modelUsesCreditsBasedApiKey); return hasFreeTrial; } catch (e) { @@ -33,3 +28,15 @@ export function usesFreeTrialApiKey( return false; } + +const modelUsesCreditsBasedApiKey = (model: ModelDescription) => { + if (!model.apiKeyLocation) { + return false; + } + + const secretType = decodeSecretLocation(model.apiKeyLocation).secretType; + + return ( + secretType === SecretType.FreeTrial || secretType === SecretType.ModelsAddOn + ); +}; diff --git a/core/config/usesFreeTrialApiKey.vitest.ts b/core/config/usesFreeTrialApiKey.vitest.ts index b92dd90a2b0..6d8318830d5 100644 --- a/core/config/usesFreeTrialApiKey.vitest.ts +++ b/core/config/usesFreeTrialApiKey.vitest.ts @@ -15,13 +15,13 @@ vi.mock("@continuedev/config-yaml", () => ({ })); describe("usesFreeTrialApiKey", () => { - let usesFreeTrialApiKey: typeof import("./usesFreeTrialApiKey").usesFreeTrialApiKey; + let usesFreeTrialApiKey: typeof import("./usesFreeTrialApiKey").usesCreditsBasedApiKey; let SecretType: typeof import("@continuedev/config-yaml").SecretType; beforeEach(async () => { mockDecodeSecretLocation.mockReset(); usesFreeTrialApiKey = (await import("./usesFreeTrialApiKey")) - .usesFreeTrialApiKey; + .usesCreditsBasedApiKey; SecretType = (await import("@continuedev/config-yaml")).SecretType; }); diff --git a/core/control-plane/client.ts b/core/control-plane/client.ts index 0a065080434..fff0d615b15 100644 --- a/core/control-plane/client.ts +++ b/core/control-plane/client.ts @@ -12,10 +12,10 @@ import fetch, { RequestInit, Response } from "node-fetch"; import { OrganizationDescription } from "../config/ProfileLifecycleManager.js"; import { + BaseSessionMetadata, IDE, ModelDescription, Session, - BaseSessionMetadata, } from "../index.js"; import { Logger } from "../util/Logger.js"; @@ -39,12 +39,11 @@ export interface ControlPlaneWorkspace { export interface ControlPlaneModelDescription extends ModelDescription {} -export interface FreeTrialStatus { +export interface CreditStatus { optedInToFreeTrial: boolean; - chatCount?: number; - autocompleteCount?: number; - chatLimit: number; - autocompleteLimit: number; + hasCredits: boolean; + creditBalance: number; + hasPurchasedCredits: boolean; } export const TRIAL_PROXY_URL = @@ -260,20 +259,20 @@ export class ControlPlaneClient { } } - public async getFreeTrialStatus(): Promise { + public async getCreditStatus(): Promise { if (!(await this.isSignedIn())) { return null; } try { - const resp = await this.requestAndHandleError("ide/free-trial-status", { + const resp = await this.requestAndHandleError("ide/credits", { method: "GET", }); - return (await resp.json()) as FreeTrialStatus; + return (await resp.json()) as CreditStatus; } catch (e) { // Capture control plane API failures to Sentry Logger.error(e, { - context: "control_plane_free_trial_status", + context: "control_plane_credit_status", }); return null; } diff --git a/core/core.ts b/core/core.ts index 0d1b5ffbae2..2587fc0afa5 100644 --- a/core/core.ts +++ b/core/core.ts @@ -483,14 +483,8 @@ export class Core { return await getControlPlaneEnv(this.ide.getIdeSettings()); }); - on("controlPlane/getFreeTrialStatus", async (msg) => { - return this.configHandler.controlPlaneClient.getFreeTrialStatus(); - }); - - on("controlPlane/getModelsAddOnUpgradeUrl", async (msg) => { - return this.configHandler.controlPlaneClient.getModelsAddOnCheckoutUrl( - msg.data.vsCodeUriScheme, - ); + on("controlPlane/getCreditStatus", async (msg) => { + return this.configHandler.controlPlaneClient.getCreditStatus(); }); on("mcp/reloadServer", async (msg) => { diff --git a/core/llm/streamChat.ts b/core/llm/streamChat.ts index d1a0d4fe0fc..d7de6c7f94d 100644 --- a/core/llm/streamChat.ts +++ b/core/llm/streamChat.ts @@ -1,11 +1,12 @@ import { fetchwithRequestOptions } from "@continuedev/fetch"; import { ChatMessage, IDE, PromptLog } from ".."; import { ConfigHandler } from "../config/ConfigHandler"; -import { usesFreeTrialApiKey } from "../config/usesFreeTrialApiKey"; +import { usesCreditsBasedApiKey } from "../config/usesFreeTrialApiKey"; import { FromCoreProtocol, ToCoreProtocol } from "../protocol"; import { IMessenger, Message } from "../protocol/messenger"; import { Telemetry } from "../util/posthog"; import { TTS } from "../util/tts"; +import { isOutOfStarterCredits } from "./utils/starterCredits"; export async function* llmStreamChat( configHandler: ConfigHandler, @@ -151,7 +152,7 @@ export async function* llmStreamChat( true, ); - void checkForFreeTrialExceeded(configHandler, messenger); + void checkForOutOfStarterCredits(configHandler, messenger); if (!next.done) { throw new Error("Will never happen"); @@ -182,24 +183,19 @@ export async function* llmStreamChat( } } -async function checkForFreeTrialExceeded( +async function checkForOutOfStarterCredits( configHandler: ConfigHandler, messenger: IMessenger, ) { - const { config } = await configHandler.getSerializedConfig(); - - // Only check if the user is using the free trial - if (config && !usesFreeTrialApiKey(config)) { - return; - } - try { - const freeTrialStatus = - await configHandler.controlPlaneClient.getFreeTrialStatus(); + const { config } = await configHandler.getSerializedConfig(); + const creditStatus = + await configHandler.controlPlaneClient.getCreditStatus(); + if ( - freeTrialStatus && - freeTrialStatus.chatCount && - freeTrialStatus.chatCount > freeTrialStatus.chatLimit + config && + creditStatus && + isOutOfStarterCredits(usesCreditsBasedApiKey(config), creditStatus) ) { void messenger.request("freeTrialExceeded", undefined); } diff --git a/core/llm/utils/starterCredits.ts b/core/llm/utils/starterCredits.ts new file mode 100644 index 00000000000..9f0d021d74b --- /dev/null +++ b/core/llm/utils/starterCredits.ts @@ -0,0 +1,12 @@ +import { CreditStatus } from "../../control-plane/client"; + +export function isOutOfStarterCredits( + usingModelsAddOnApiKey: boolean, + creditStatus: CreditStatus, +): boolean { + return ( + usingModelsAddOnApiKey && + !creditStatus.hasCredits && + !creditStatus.hasPurchasedCredits + ); +} diff --git a/core/protocol/core.ts b/core/protocol/core.ts index c559d9f6406..28e617f798f 100644 --- a/core/protocol/core.ts +++ b/core/protocol/core.ts @@ -48,10 +48,7 @@ import { ControlPlaneEnv, ControlPlaneSessionInfo, } from "../control-plane/AuthTypes"; -import { - FreeTrialStatus, - RemoteSessionMetadata, -} from "../control-plane/client"; +import { CreditStatus, RemoteSessionMetadata } from "../control-plane/client"; import { ProcessedItem } from "../nextEdit/NextEditPrefetchQueue"; import { NextEditOutcome } from "../nextEdit/types"; import { ContinueErrorReason } from "../util/errors"; @@ -327,11 +324,7 @@ export type ToCoreFromIdeOrWebviewProtocol = { "clipboardCache/add": [{ content: string }, void]; "controlPlane/openUrl": [{ path: string; orgSlug?: string }, void]; "controlPlane/getEnvironment": [undefined, ControlPlaneEnv]; - "controlPlane/getFreeTrialStatus": [undefined, FreeTrialStatus | null]; - "controlPlane/getModelsAddOnUpgradeUrl": [ - { vsCodeUriScheme?: string }, - { url: string } | null, - ]; + "controlPlane/getCreditStatus": [undefined, CreditStatus | null]; isItemTooBig: [{ item: ContextItemWithId }, boolean]; didChangeControlPlaneSessionInfo: [ { sessionInfo: ControlPlaneSessionInfo | undefined }, diff --git a/core/protocol/passThrough.ts b/core/protocol/passThrough.ts index 5a6f45eba0c..f77b7a05dd0 100644 --- a/core/protocol/passThrough.ts +++ b/core/protocol/passThrough.ts @@ -84,8 +84,7 @@ export const WEBVIEW_TO_CORE_PASS_THROUGH: (keyof ToCoreFromWebviewProtocol)[] = "tools/evaluatePolicy", "tools/preprocessArgs", "controlPlane/getEnvironment", - "controlPlane/getFreeTrialStatus", - "controlPlane/getModelsAddOnUpgradeUrl", + "controlPlane/getCreditStatus", "controlPlane/openUrl", "isItemTooBig", "process/markAsBackgrounded", diff --git a/extensions/cli/src/ui/FreeTrialTransitionUI.tsx b/extensions/cli/src/ui/FreeTrialTransitionUI.tsx index 0943d801558..4b3cbbeb743 100644 --- a/extensions/cli/src/ui/FreeTrialTransitionUI.tsx +++ b/extensions/cli/src/ui/FreeTrialTransitionUI.tsx @@ -176,7 +176,7 @@ const FreeTrialTransitionUI: React.FC = ({ if (selectedOption === 1) { // Option 1: Open models setup page setCurrentStep("processing"); - const modelsUrl = new URL("setup-models", env.appUrl).toString(); + const modelsUrl = new URL("settings/billing", env.appUrl).toString(); setWasModelsSetup(true); // Track that user went through models setup try { diff --git a/gui/src/components/FreeTrialButton.tsx b/gui/src/components/FreeTrialButton.tsx deleted file mode 100644 index c3373cf7b99..00000000000 --- a/gui/src/components/FreeTrialButton.tsx +++ /dev/null @@ -1,168 +0,0 @@ -import { ChevronDownIcon } from "@heroicons/react/24/outline"; -import { FreeTrialStatus } from "core/control-plane/client"; -import { useContext } from "react"; -import { Button, SecondaryButton, vscButtonBackground } from "."; -import { IdeMessengerContext } from "../context/IdeMessenger"; -import { fontSize } from "../util"; -import { setLocalStorage } from "../util/localStorage"; -import { Listbox, ListboxButton, ListboxOptions, Transition } from "./ui"; - -interface ProgressBarProps { - label: string; - current: number; - total: number; -} - -function ProgressBar({ label, current, total }: ProgressBarProps) { - const percentage = Math.min((current / total) * 100, 100); - - return ( -
-
- {label} - - {current}/{total} - -
-
-
-
-
- ); -} - -interface FreeTrialProgressBarsProps { - freeTrialStatus: FreeTrialStatus; -} - -function FreeTrialProgressBars({ - freeTrialStatus, -}: FreeTrialProgressBarsProps) { - // Use data from freeTrialStatus - const autocompleteUsage = { - current: freeTrialStatus.autocompleteCount, - total: freeTrialStatus.autocompleteLimit, - }; - const chatUsage = { - current: freeTrialStatus.chatCount, - total: freeTrialStatus.chatLimit, - }; - - return ( - <> - - - - - ); -} - -interface FreeTrialButtonProps { - freeTrialStatus?: FreeTrialStatus | null; -} - -export default function FreeTrialButton({ - freeTrialStatus, -}: FreeTrialButtonProps) { - const ideMessenger = useContext(IdeMessengerContext); - - const onExitFreeTrial = async () => { - setLocalStorage("hasExitedFreeTrial", true); - - await ideMessenger.request("controlPlane/openUrl", { - path: "setup-models", - orgSlug: undefined, - }); - }; - - return ( - -
- -
- Free trial usage -
-
- - - -
-

- Free trial of the Models Add-On -

- -
- - You are currently using a free trial the Models Add-On, which - allows you to use a variety of frontier models for a flat - monthly fee. Read more about usage limits and what models are - included{" "} - { - await ideMessenger.request("controlPlane/openUrl", { - path: "pricing", - orgSlug: undefined, - }); - }} - className="cursor-pointer text-blue-400 underline hover:text-blue-300" - > - here - - . - -
- - {!freeTrialStatus ? ( -
- - Loading trial usage... - -
- ) : ( - - )} - -
- - Exit trial - - -
-
-
-
-
-
- ); -} diff --git a/gui/src/components/Layout.tsx b/gui/src/components/Layout.tsx index 8045b72c89d..af5f3b785bc 100644 --- a/gui/src/components/Layout.tsx +++ b/gui/src/components/Layout.tsx @@ -14,7 +14,6 @@ import { setDialogMessage, setShowDialog } from "../redux/slices/uiSlice"; import { enterEdit, exitEdit } from "../redux/thunks/edit"; import { saveCurrentSession } from "../redux/thunks/session"; import { fontSize, isMetaEquivalentKeyPressed } from "../util"; -import { incrementFreeTrialCount } from "../util/freeTrial"; import { ROUTES } from "../util/navigation"; import { FatalErrorIndicator } from "./config/FatalErrorNotice"; import TextDialog from "./dialogs"; @@ -139,14 +138,6 @@ const Layout = () => { [location, navigate], ); - useWebviewListener( - "incrementFtc", - async () => { - incrementFreeTrialCount(); - }, - [], - ); - useWebviewListener( "setupLocalConfig", async () => { diff --git a/gui/src/components/OnboardingCard/components/OnboardingCardLanding.tsx b/gui/src/components/OnboardingCard/components/OnboardingCardLanding.tsx index 3fca53a7c2e..846aa70fd8c 100644 --- a/gui/src/components/OnboardingCard/components/OnboardingCardLanding.tsx +++ b/gui/src/components/OnboardingCard/components/OnboardingCardLanding.tsx @@ -2,11 +2,10 @@ import { useContext } from "react"; import { Button, SecondaryButton } from "../.."; import { useAuth } from "../../../context/Auth"; import { IdeMessengerContext } from "../../../context/IdeMessenger"; +import { useCreditStatus } from "../../../hooks/useCredits"; import { useAppDispatch, useAppSelector } from "../../../redux/hooks"; import { selectCurrentOrg } from "../../../redux/slices/profilesSlice"; import { selectFirstHubProfile } from "../../../redux/thunks/selectFirstHubProfile"; -import { hasPassedFTL } from "../../../util/freeTrial"; -import { ToolTip } from "../../gui/Tooltip"; import ContinueLogo from "../../svg/ContinueLogo"; import { useOnboardingCard } from "../hooks/useOnboardingCard"; @@ -38,15 +37,23 @@ export function OnboardingCardLanding({ }); } - function openPastFreeTrialOnboarding() { + function openBillingPage() { ideMessenger.post("controlPlane/openUrl", { - path: "setup-models", + path: "settings/billing", orgSlug: currentOrg?.slug, }); onboardingCard.close(isDialog); } - const pastFreeTrialLimit = hasPassedFTL(); + function openApiKeysPage() { + ideMessenger.post("controlPlane/openUrl", { + path: "setup-models/api-keys", + orgSlug: currentOrg?.slug, + }); + onboardingCard.close(isDialog); + } + + const { creditStatus, outOfStarterCredits } = useCreditStatus(); return (
@@ -54,39 +61,29 @@ export function OnboardingCardLanding({
- {pastFreeTrialLimit ? ( + {outOfStarterCredits ? ( <>

- You've reached the free trial limit. Visit the Continue Platform to - select a Coding Agent. + You've used all your starter credits! Click below to purchase + credits or configure API keys

+ + Set up API keys + ) : ( <>

- Log in to access a free trial of the -
- - - ideMessenger.post("controlPlane/openUrl", { - path: "pricing", - }) - } - > - Models Add-On - - + Log in to get up and running with starter credits

); } diff --git a/gui/src/components/OnboardingCard/components/OnboardingModelsAddOnTab.tsx b/gui/src/components/OnboardingCard/components/OnboardingModelsAddOnTab.tsx index 7110cd6b0ce..bc667ca80c3 100644 --- a/gui/src/components/OnboardingCard/components/OnboardingModelsAddOnTab.tsx +++ b/gui/src/components/OnboardingCard/components/OnboardingModelsAddOnTab.tsx @@ -30,30 +30,13 @@ export function OnboardingModelsAddOnTab() { return () => clearInterval(interval); }, [isPolling, isJetbrains, ideMessenger]); - function openPricingPage() { - ideMessenger.post("controlPlane/openUrl", { - path: "pricing", - }); - } - - async function handleUpgrade() { + async function openBillingSettings() { try { - const response = await ideMessenger.request( - "controlPlane/getModelsAddOnUpgradeUrl", - { - vsCodeUriScheme: getLocalStorage("vsCodeUriScheme"), - }, - ); - - if (response.status === "success" && response.content?.url) { - await ideMessenger.request("openUrl", response.content.url); - } else { - console.error("Failed to get upgrade URL"); - openPricingPage(); - } + await ideMessenger.request("controlPlane/openUrl", { + path: "settings/billing", + }); } catch (error) { console.error("Error during upgrade process:", error); - openPricingPage(); } finally { if (isJetbrains) { setIsPolling(true); @@ -63,6 +46,12 @@ export function OnboardingModelsAddOnTab() { } } + function openPricingPage() { + void ideMessenger.request("controlPlane/openUrl", { + path: "pricing", + }); + } + // Show polling UI for JetBrains after upgrade if (isPolling && isJetbrains) { return ( @@ -84,10 +73,6 @@ export function OnboardingModelsAddOnTab() {
-
- $20/month -
- Use a{" "} variety of frontier models {" "} - for a flat monthly fee + at cost. -
-

- Base Tier: 500 Chat/Edit and 25,000 - Autocomplete requests per month -

-

- Plus Tier (2.5x): 1,250 Chat/Edit - and 62,500 Autocomplete requests per month -

-

- Pro Tier (5x): 2,500 Chat/Edit and - 125,000 Autocomplete requests per month -

-
-
-
diff --git a/gui/src/components/StarterCreditsButton.tsx b/gui/src/components/StarterCreditsButton.tsx new file mode 100644 index 00000000000..51f58d16e14 --- /dev/null +++ b/gui/src/components/StarterCreditsButton.tsx @@ -0,0 +1,38 @@ +import { GiftIcon } from "@heroicons/react/24/outline"; +import { CreditStatus } from "core/control-plane/client"; +import StarterCreditsPopover from "./StarterCreditsPopover"; + +interface FreeTrialButtonProps { + creditStatus?: CreditStatus | null; + refreshCreditStatus?: () => Promise; +} + +/** + * @deprecated Use StarterCreditsPopover with a custom button/icon instead. + * This component is kept for backward compatibility. + */ +export default function StarterCreditsButton({ + creditStatus, + refreshCreditStatus, +}: FreeTrialButtonProps) { + return ( + + + + + + ); +} diff --git a/gui/src/components/StarterCreditsPopover.tsx b/gui/src/components/StarterCreditsPopover.tsx new file mode 100644 index 00000000000..c80777199e9 --- /dev/null +++ b/gui/src/components/StarterCreditsPopover.tsx @@ -0,0 +1,196 @@ +import { + ArrowPathIcon, + GiftIcon, + XMarkIcon, +} from "@heroicons/react/24/outline"; +import { CreditStatus } from "core/control-plane/client"; +import { useContext, useState } from "react"; +import { Button, SecondaryButton, vscButtonBackground } from "."; +import { useAuth } from "../context/Auth"; +import { IdeMessengerContext } from "../context/IdeMessenger"; +import { cn } from "../util/cn"; +import { setLocalStorage } from "../util/localStorage"; +import { ToolbarButtonWithTooltip } from "./StyledMarkdownPreview/StepContainerPreToolbar/ToolbarButtonWithTooltip"; +import { Listbox, ListboxButton, ListboxOptions, Transition } from "./ui"; + +interface ProgressBarProps { + label: string; + current: number; + total: number; +} + +function ProgressBar({ label, current, total }: ProgressBarProps) { + const percentage = Math.min((current / total) * 100, 100); + + return ( +
+
+ {label} + + ${(current / 100).toFixed(2)} / ${(total / 100).toFixed(2)} + +
+
+
+
+
+ ); +} + +interface CreditStatusProgressBarsProps { + creditStatus: CreditStatus; +} + +function CreditStatusProgressBar({ + creditStatus, +}: CreditStatusProgressBarsProps) { + const total = 50; + const usage = total - (creditStatus.creditBalance ?? 0); + + return ( + + ); +} + +interface StarterCreditsPopoverProps { + creditStatus?: CreditStatus | null; + refreshCreditStatus?: () => Promise; + children: React.ReactNode; +} + +export default function StarterCreditsPopover({ + creditStatus, + refreshCreditStatus, + children, +}: StarterCreditsPopoverProps) { + const ideMessenger = useContext(IdeMessengerContext); + const { refreshProfiles } = useAuth(); + const [isRefreshing, setIsRefreshing] = useState(false); + + const onSetupApiKeys = async () => { + await ideMessenger.request("controlPlane/openUrl", { + path: "setup-models/api-keys", + orgSlug: undefined, + }); + }; + + const onPurchaseCredits = async () => { + await ideMessenger.request("controlPlane/openUrl", { + path: "settings/billing", + orgSlug: undefined, + }); + }; + + const onHideStarterCreditsUsage = async () => { + // At this point the user doesn't want to see the credit usage UI, so we make sure this is gone right away + setLocalStorage("hasExitedFreeTrial", true); + }; + + const onRefresh = async () => { + if (isRefreshing) { + return; + } + + setIsRefreshing(true); + + const refreshCalls: Promise[] = [ + refreshProfiles("Manual refresh from starter credits button"), + ]; + + if (refreshCreditStatus) { + refreshCalls.push(refreshCreditStatus()); + } + + try { + await Promise.all(refreshCalls); + } finally { + setIsRefreshing(false); + } + }; + + return ( + + + {children} + + + + +
+
+ +

Starter credits

+
+
+ { + void onRefresh(); + }} + tooltipContent="Refresh credit usage" + > + + + + + +
+ +
+ + You are currently using starter credits for Continue, which + allows you to use a variety of frontier models at cost. Read + more{" "} + { + await ideMessenger.request("controlPlane/openUrl", { + path: "pricing", + orgSlug: undefined, + }); + ``; + }} + className="cursor-pointer text-blue-400 underline hover:text-blue-300" + > + here + + . + +
+ + {!creditStatus ? ( +
+ + Loading credit usage... + +
+ ) : ( + + )} + +
+ + Setup API Keys + + +
+
+
+
+
+ ); +} diff --git a/gui/src/components/loaders/FreeTrialProgressBar.tsx b/gui/src/components/loaders/FreeTrialProgressBar.tsx deleted file mode 100644 index 2b677bd9c74..00000000000 --- a/gui/src/components/loaders/FreeTrialProgressBar.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { ExclamationCircleIcon } from "@heroicons/react/24/outline"; -import { useDispatch } from "react-redux"; -import { useNavigate } from "react-router-dom"; -import { AddModelForm } from "../../forms/AddModelForm"; -import { setDialogMessage, setShowDialog } from "../../redux/slices/uiSlice"; -import { FREE_TRIAL_LIMIT_REQUESTS } from "../../util/freeTrial"; -import { ToolTip } from "../gui/Tooltip"; - -interface FreeTrialProgressBarProps { - completed: number; - total: number; -} - -function FreeTrialProgressBar({ completed, total }: FreeTrialProgressBarProps) { - const dispatch = useDispatch(); - const navigate = useNavigate(); - const fillPercentage = Math.min(100, Math.max(0, (completed / total) * 100)); - - function onClick() { - dispatch(setShowDialog(true)); - dispatch( - setDialogMessage( - { - dispatch(setShowDialog(false)); - navigate("/"); - }} - />, - ), - ); - } - - if (completed > total) { - return ( - -
- - Trial limit reached -
-
- ); - } - - return ( - -
- - Free trial requests - -
-
0.75 ? "bg-amber-500" : "bg-stone-500" - }`} - style={{ - width: `${fillPercentage}%`, - }} - /> -
- - {completed} / {total} - -
- - ); -} - -export default FreeTrialProgressBar; diff --git a/gui/src/components/mainInput/Lump/LumpToolbar/BlockSettingsTopToolbar.tsx b/gui/src/components/mainInput/Lump/LumpToolbar/BlockSettingsTopToolbar.tsx index 5cca2cdc172..3f5d66d88bf 100644 --- a/gui/src/components/mainInput/Lump/LumpToolbar/BlockSettingsTopToolbar.tsx +++ b/gui/src/components/mainInput/Lump/LumpToolbar/BlockSettingsTopToolbar.tsx @@ -1,10 +1,11 @@ import { CubeIcon, ExclamationTriangleIcon, + GiftIcon, PencilIcon, WrenchScrewdriverIcon, } from "@heroicons/react/24/outline"; -import { useContext, useEffect, useState } from "react"; +import { useContext } from "react"; import { useNavigate } from "react-router-dom"; import { IdeMessengerContext } from "../../../../context/IdeMessenger"; import { useAppDispatch, useAppSelector } from "../../../../redux/hooks"; @@ -13,14 +14,12 @@ import { selectToolCallsByStatus, } from "../../../../redux/selectors/selectToolCalls"; import { setSelectedProfile } from "../../../../redux/slices/profilesSlice"; -import FreeTrialButton from "../../../FreeTrialButton"; +import StarterCreditsPopover from "../../../StarterCreditsPopover"; import { ToolTip } from "../../../gui/Tooltip"; import HoverItem from "../../InputToolbar/HoverItem"; -import { usesFreeTrialApiKey } from "core/config/usesFreeTrialApiKey"; -import type { FreeTrialStatus } from "core/control-plane/client"; import { useAuth } from "../../../../context/Auth"; -import { getLocalStorage } from "../../../../util/localStorage"; +import { useCreditStatus } from "../../../../hooks/useCredits"; import { CONFIG_ROUTES } from "../../../../util/navigation"; import { AssistantAndOrgListbox } from "../../../AssistantAndOrgListbox"; @@ -30,7 +29,6 @@ export function BlockSettingsTopToolbar() { const { selectedProfile } = useAuth(); const configError = useAppSelector((store) => store.config.configError); - const config = useAppSelector((state) => state.config.config); const ideMessenger = useContext(IdeMessengerContext); const pendingToolCalls = useAppSelector(selectPendingToolCalls); @@ -40,40 +38,11 @@ export function BlockSettingsTopToolbar() { const hasActiveContent = pendingToolCalls.length > 0 || callingToolCalls.length > 0; - const [freeTrialStatus, setFreeTrialStatus] = - useState(null); - const hasExitedFreeTrial = getLocalStorage("hasExitedFreeTrial"); - const isUsingFreeTrial = usesFreeTrialApiKey(config) && !hasExitedFreeTrial; - - useEffect(() => { - const fetchFreeTrialStatus = () => { - ideMessenger - .request("controlPlane/getFreeTrialStatus", undefined) - .then((resp) => { - if (resp.status === "success") { - setFreeTrialStatus(resp.content); - } - }) - .catch(() => {}); - }; - - fetchFreeTrialStatus(); - - let intervalId: NodeJS.Timeout | null = null; - - if (isUsingFreeTrial) { - intervalId = setInterval(fetchFreeTrialStatus, 15000); - } - - return () => { - if (intervalId) { - clearInterval(intervalId); - } - }; - }, [ideMessenger, isUsingFreeTrial]); - const shouldShowError = configError && configError?.length > 0; + const { creditStatus, isUsingFreeTrial, refreshCreditStatus } = + useCreditStatus(); + const handleRulesClick = () => { if (selectedProfile) { dispatch(setSelectedProfile(selectedProfile.id)); @@ -132,6 +101,19 @@ export function BlockSettingsTopToolbar() { {!hasActiveContent && (
+ {isUsingFreeTrial && ( + + + + + + + + )} + @@ -153,16 +135,9 @@ export function BlockSettingsTopToolbar() { )}
- +
- {isUsingFreeTrial ? ( - - ) : ( - - )} +
diff --git a/gui/src/context/MockIdeMessenger.ts b/gui/src/context/MockIdeMessenger.ts index e2e8a8ec9ee..e3ab3fb58dc 100644 --- a/gui/src/context/MockIdeMessenger.ts +++ b/gui/src/context/MockIdeMessenger.ts @@ -33,12 +33,11 @@ const DEFAULT_MOCK_CORE_RESPONSES: MockResponses = { contents: "Current file contents", path: "file:///Users/user/workspace1/current_file.py", }, - "controlPlane/getFreeTrialStatus": { - autocompleteLimit: 1000, + "controlPlane/getCreditStatus": { optedInToFreeTrial: false, - chatLimit: 1000, - autocompleteCount: 0, - chatCount: 0, + creditBalance: 0, + hasCredits: false, + hasPurchasedCredits: false, }, getWorkspaceDirs: [ "file:///Users/user/workspace1", diff --git a/gui/src/forms/AddModelForm.tsx b/gui/src/forms/AddModelForm.tsx index 5de78133ccf..61a7142992e 100644 --- a/gui/src/forms/AddModelForm.tsx +++ b/gui/src/forms/AddModelForm.tsx @@ -134,22 +134,6 @@ export function AddModelForm({

Add Chat model

- {/* TODO sync free trial limit with hub */} - {/* {!hideFreeTrialLimitMessage && hasPassedFTL() && ( -

- You've reached the free trial limit of {FREE_TRIAL_LIMIT_REQUESTS}{" "} - free inputs. To keep using Continue, you can either use your own - API key, or use a local LLM. To read more about the options, see - our{" "} - ideMessenger.post("openUrl", CONTINUE_SETUP_URL)} - > - documentation - - . -

- )} */} -
diff --git a/gui/src/hooks/useCredits.ts b/gui/src/hooks/useCredits.ts new file mode 100644 index 00000000000..8f2a4bb45b7 --- /dev/null +++ b/gui/src/hooks/useCredits.ts @@ -0,0 +1,59 @@ +import { usesCreditsBasedApiKey } from "core/config/usesFreeTrialApiKey"; +import { CreditStatus } from "core/control-plane/client"; +import { isOutOfStarterCredits } from "core/llm/utils/starterCredits"; +import { useCallback, useContext, useEffect, useState } from "react"; +import { IdeMessengerContext } from "../context/IdeMessenger"; +import { useAppSelector } from "../redux/hooks"; +import { getLocalStorage } from "../util/localStorage"; + +export function useCreditStatus() { + const config = useAppSelector((state) => state.config.config); + const ideMessenger = useContext(IdeMessengerContext); + const [creditStatus, setCreditStatus] = useState(null); + + const hasExitedFreeTrial = getLocalStorage("hasExitedFreeTrial"); + const usingCreditsBasedApiKey = usesCreditsBasedApiKey(config); + const isUsingFreeTrial = usingCreditsBasedApiKey && !hasExitedFreeTrial; + const outOfStarterCredits = creditStatus + ? isOutOfStarterCredits(usingCreditsBasedApiKey, creditStatus) + : false; + + const refreshCreditStatus = useCallback(async () => { + try { + const resp = await ideMessenger.request( + "controlPlane/getCreditStatus", + undefined, + ); + if (resp.status === "success") { + setCreditStatus(resp.content); + } + } catch (error) { + console.error("Failed to refresh credit status", error); + } + }, [ideMessenger]); + + useEffect(() => { + void refreshCreditStatus(); + + let intervalId: NodeJS.Timeout | null = null; + + if (isUsingFreeTrial) { + intervalId = setInterval(() => { + void refreshCreditStatus(); + }, 15000); + } + + return () => { + if (intervalId) { + clearInterval(intervalId); + } + }; + }, [isUsingFreeTrial, refreshCreditStatus]); + + return { + creditStatus, + outOfStarterCredits, + isUsingFreeTrial, + refreshCreditStatus, + }; +} diff --git a/gui/src/pages/gui/Chat.tsx b/gui/src/pages/gui/Chat.tsx index 871188cee84..060f0a68d9a 100644 --- a/gui/src/pages/gui/Chat.tsx +++ b/gui/src/pages/gui/Chat.tsx @@ -198,30 +198,6 @@ export function Chat() { return; } - // TODO - hook up with hub to detect free trial progress - // if (model.provider === "free-trial") { - // const newCount = incrementFreeTrialCount(); - - // if (newCount === FREE_TRIAL_LIMIT_REQUESTS) { - // posthog?.capture("ftc_reached"); - // } - // if (newCount >= FREE_TRIAL_LIMIT_REQUESTS) { - // // Show this message whether using platform or not - // // So that something happens if in new chat - // void ideMessenger.ide.showToast( - // "error", - // "You've reached the free trial limit. Please configure a model to continue.", - // ); - - // // If history, show the dialog, which will automatically close if there is not history - // if (history.length) { - // dispatch(setDialogMessage()); - // dispatch(setShowDialog(true)); - // } - // return; - // } - // } - if (isCurrentlyInEdit) { void dispatch( streamEditThunk({ diff --git a/gui/src/pages/gui/ModelsAddOnLimitDialog.tsx b/gui/src/pages/gui/ModelsAddOnLimitDialog.tsx deleted file mode 100644 index 3f38623f8cf..00000000000 --- a/gui/src/pages/gui/ModelsAddOnLimitDialog.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { DISCORD_LINK, GITHUB_LINK } from "core/util/constants"; -import { useContext } from "react"; -import { SecondaryButton } from "../../components"; -import { DiscordIcon } from "../../components/svg/DiscordIcon"; -import { GithubIcon } from "../../components/svg/GithubIcon"; -import { IdeMessengerContext } from "../../context/IdeMessenger"; - -export function ModelsAddOnLimitDialog() { - const ideMessenger = useContext(IdeMessengerContext); - - return ( -
-

Models Add-On Limit Reached

- -
- - You have reached the monthly limit for chat requests with the Models - Add-On. This limit exists to avoid abuse, but if this happened from - normal usage we encourage you to contact us on GitHub or Discord - -
- { - ideMessenger.post("openUrl", GITHUB_LINK); - }} - > - - Github - - { - ideMessenger.post("openUrl", DISCORD_LINK); - }} - > - - Discord - -
-
-
- ); -} diff --git a/gui/src/pages/gui/OutOfCreditsDialog.tsx b/gui/src/pages/gui/OutOfCreditsDialog.tsx new file mode 100644 index 00000000000..ac10bd9d535 --- /dev/null +++ b/gui/src/pages/gui/OutOfCreditsDialog.tsx @@ -0,0 +1,34 @@ +import { CreditCardIcon } from "@heroicons/react/24/outline"; +import { useContext } from "react"; +import { SecondaryButton } from "../../components"; +import { IdeMessengerContext } from "../../context/IdeMessenger"; + +export function OutOfCreditsDialog() { + const ideMessenger = useContext(IdeMessengerContext); + + return ( +
+

You're out of credits!

+ +
+ + To purchase more or set up auto top-up, click below to visit the + billing page: + +
+ { + ideMessenger.post("controlPlane/openUrl", { + path: "/settings/billing", + }); + }} + > + + Purchase Credits + +
+
+
+ ); +} diff --git a/gui/src/pages/gui/StreamError.tsx b/gui/src/pages/gui/StreamError.tsx index c3f5ad0e62e..e06cf8d7576 100644 --- a/gui/src/pages/gui/StreamError.tsx +++ b/gui/src/pages/gui/StreamError.tsx @@ -22,7 +22,7 @@ import { setDialogMessage, setShowDialog } from "../../redux/slices/uiSlice"; import { streamResponseThunk } from "../../redux/thunks/streamResponse"; import { isLocalProfile } from "../../util"; import { analyzeError } from "../../util/errorAnalysis"; -import { ModelsAddOnLimitDialog } from "./ModelsAddOnLimitDialog"; +import { OutOfCreditsDialog } from "./OutOfCreditsDialog"; interface StreamErrorProps { error: unknown; @@ -121,10 +121,8 @@ const StreamErrorDialog = ({ error }: StreamErrorProps) => { ); - if ( - parsedError === "You have exceeded the chat limit for the Models Add-On." - ) { - return ; + if (parsedError.includes("You're out of credits!")) { + return ; } let errorContent = ( diff --git a/gui/src/util/freeTrial.ts b/gui/src/util/freeTrial.ts deleted file mode 100644 index 1961fb358f0..00000000000 --- a/gui/src/util/freeTrial.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { getLocalStorage, setLocalStorage } from "./localStorage"; - -export const FREE_TRIAL_LIMIT_REQUESTS = 50; - -export function hasPassedFTL(): boolean { - return (getLocalStorage("ftc") ?? 0) > FREE_TRIAL_LIMIT_REQUESTS; -} - -export function incrementFreeTrialCount(): number { - const u = getLocalStorage("ftc") ?? 0; - setLocalStorage("ftc", u + 1); - return u + 1; -} diff --git a/gui/src/util/localStorage.ts b/gui/src/util/localStorage.ts index de98714ec55..182cf486dc0 100644 --- a/gui/src/util/localStorage.ts +++ b/gui/src/util/localStorage.ts @@ -8,7 +8,6 @@ type LocalStorageTypes = { hasDismissedOnboardingCard: boolean; ide: "vscode" | "jetbrains"; vsCodeUriScheme: string; - ftc: number; fontSize: number; [key: `inputHistory_${string}`]: JSONContent[]; extensionVersion: string;