diff --git a/core/core.ts b/core/core.ts index c74f2e539d..88f4807534 100644 --- a/core/core.ts +++ b/core/core.ts @@ -55,6 +55,7 @@ import { import { usePlatform } from "./control-plane/flags"; import type { FromCoreProtocol, ToCoreProtocol } from "./protocol"; import type { IMessenger, Message } from "./protocol/messenger"; +import { controlPlaneEnv } from "./control-plane/env"; export class Core { // implements IMessenger @@ -294,6 +295,13 @@ export class Core { addContextProvider(msg.data); }); + on("controlPlane/openUrl", async (msg) => { + await this.messenger.request( + "openUrl", + `${controlPlaneEnv.APP_URL}${msg.data.path}`, + ); + }); + // Context providers on("context/addDocs", async (msg) => { void this.docsService.indexAndAdd(msg.data); diff --git a/core/protocol/core.ts b/core/protocol/core.ts index b48b113989..52cffb0279 100644 --- a/core/protocol/core.ts +++ b/core/protocol/core.ts @@ -191,4 +191,5 @@ export type ToCoreFromIdeOrWebviewProtocol = { { contextItems: ContextItem[] }, ]; "clipboardCache/add": [{ content: string }, void]; + "controlPlane/openUrl": [{ path: string }, void]; }; diff --git a/core/protocol/passThrough.ts b/core/protocol/passThrough.ts index a638eafb47..3d871c42aa 100644 --- a/core/protocol/passThrough.ts +++ b/core/protocol/passThrough.ts @@ -58,6 +58,7 @@ export const WEBVIEW_TO_CORE_PASS_THROUGH: (keyof ToCoreFromWebviewProtocol)[] = "profiles/switch", "didChangeSelectedProfile", "tools/call", + "controlPlane/openUrl", ]; // Message types to pass through from core to webview diff --git a/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/constants/MessageTypes.kt b/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/constants/MessageTypes.kt index 9c2b8c2dc2..66910bced9 100644 --- a/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/constants/MessageTypes.kt +++ b/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/constants/MessageTypes.kt @@ -116,6 +116,7 @@ class MessageTypes { "profiles/switch", "didChangeSelectedProfile", "tools/call", + "controlPlane/openUrl" ) } } \ No newline at end of file diff --git a/gui/src/components/Footer.tsx b/gui/src/components/Footer.tsx index 9b5a6840c9..b2efd221c9 100644 --- a/gui/src/components/Footer.tsx +++ b/gui/src/components/Footer.tsx @@ -1,6 +1,7 @@ import { useAppSelector } from "../redux/hooks"; import { selectDefaultModel } from "../redux/slices/configSlice"; import { FREE_TRIAL_LIMIT_REQUESTS } from "../util/freeTrial"; +import { getLocalStorage } from "../util/localStorage"; import FreeTrialProgressBar from "./loaders/FreeTrialProgressBar"; function Footer() { @@ -10,7 +11,7 @@ function Footer() { return ( diff --git a/gui/src/components/Layout.tsx b/gui/src/components/Layout.tsx index e8a0deb129..cadaa5830e 100644 --- a/gui/src/components/Layout.tsx +++ b/gui/src/components/Layout.tsx @@ -13,7 +13,6 @@ import { newSession, } from "../redux/slices/sessionSlice"; import { getFontSize, isMetaEquivalentKeyPressed } from "../util"; -import { getLocalStorage, setLocalStorage } from "../util/localStorage"; import { ROUTES } from "../util/navigation"; import TextDialog from "./dialogs"; import Footer from "./Footer"; @@ -23,6 +22,7 @@ import AccountDialog from "./AccountDialog"; import { AuthProvider } from "../context/Auth"; import { exitEditMode } from "../redux/thunks"; import { loadLastSession, saveCurrentSession } from "../redux/thunks/session"; +import { incrementFreeTrialCount } from "../util/freeTrial"; const LayoutTopDiv = styled(CustomScrollbarDiv)` height: 100%; @@ -150,12 +150,7 @@ const Layout = () => { useWebviewListener( "incrementFtc", async () => { - const u = getLocalStorage("ftc"); - if (u) { - setLocalStorage("ftc", u + 1); - } else { - setLocalStorage("ftc", 1); - } + incrementFreeTrialCount(); }, [], ); diff --git a/gui/src/components/OnboardingCard/OnboardingCard.tsx b/gui/src/components/OnboardingCard/OnboardingCard.tsx index 78caa4b5b2..eebbfb2bc9 100644 --- a/gui/src/components/OnboardingCard/OnboardingCard.tsx +++ b/gui/src/components/OnboardingCard/OnboardingCard.tsx @@ -20,7 +20,11 @@ export interface OnboardingCardState { activeTab?: TabTitle; } -export function OnboardingCard() { +interface OnboardingCardProps { + isDialog?: boolean; +} + +export function OnboardingCard({ isDialog }: OnboardingCardProps) { const onboardingCard = useOnboardingCard(); function renderTabContent() { @@ -49,9 +53,11 @@ export function OnboardingCard() { activeTab={onboardingCard.activeTab || "Best"} onTabClick={onboardingCard.setActiveTab} /> - - - + {!isDialog && ( + onboardingCard.close()}> + + + )}
{renderTabContent()}
); diff --git a/gui/src/components/OnboardingCard/components/QuickStartSubmitButton.tsx b/gui/src/components/OnboardingCard/components/QuickStartSubmitButton.tsx index 04eabf345c..00d0702c3c 100644 --- a/gui/src/components/OnboardingCard/components/QuickStartSubmitButton.tsx +++ b/gui/src/components/OnboardingCard/components/QuickStartSubmitButton.tsx @@ -9,11 +9,15 @@ import { useSubmitOnboarding } from "../hooks"; import JetBrainsFetchGitHubTokenDialog from "./JetBrainsFetchGitHubTokenDialog"; import { setDefaultModel } from "../../../redux/slices/configSlice"; -function QuickstartSubmitButton() { +interface QuickstartSubmitButtonProps { + isDialog?: boolean; +} + +function QuickstartSubmitButton({ isDialog }: QuickstartSubmitButtonProps) { const ideMessenger = useContext(IdeMessengerContext); const dispatch = useDispatch(); - const { submitOnboarding } = useSubmitOnboarding("Quickstart"); + const { submitOnboarding } = useSubmitOnboarding("Quickstart", isDialog); function onComplete() { submitOnboarding(); diff --git a/gui/src/components/OnboardingCard/hooks/useOnboardingCard.ts b/gui/src/components/OnboardingCard/hooks/useOnboardingCard.ts index ac6078818c..9622754c41 100644 --- a/gui/src/components/OnboardingCard/hooks/useOnboardingCard.ts +++ b/gui/src/components/OnboardingCard/hooks/useOnboardingCard.ts @@ -1,7 +1,11 @@ import { useDispatch } from "react-redux"; import { useNavigate } from "react-router-dom"; import { TabTitle } from "../components/OnboardingCardTabs"; -import { setOnboardingCard } from "../../../redux/slices/uiSlice"; +import { + setDialogMessage, + setOnboardingCard, + setShowDialog, +} from "../../../redux/slices/uiSlice"; import { OnboardingCardState } from ".."; import { getLocalStorage, setLocalStorage } from "../../../util/localStorage"; import { useAppDispatch, useAppSelector } from "../../../redux/hooks"; @@ -12,7 +16,7 @@ export interface UseOnboardingCard { activeTab: OnboardingCardState["activeTab"]; setActiveTab: (tab: TabTitle) => void; open: (tab: TabTitle) => void; - close: () => void; + close: (isDialog?: boolean) => void; } export function useOnboardingCard(): UseOnboardingCard { @@ -38,20 +42,16 @@ export function useOnboardingCard(): UseOnboardingCard { async function open(tab: TabTitle) { navigate("/"); - - // Used to clear the chat panel before showing onboarding card - dispatch( - saveCurrentSession({ - openNewSession: true, - }), - ); - dispatch(setOnboardingCard({ show: true, activeTab: tab })); } - function close() { + function close(isDialog = false) { setLocalStorage("hasDismissedOnboardingCard", true); dispatch(setOnboardingCard({ show: false })); + if (isDialog) { + dispatch(setDialogMessage(undefined)); + dispatch(setShowDialog(false)); + } } function setActiveTab(tab: TabTitle) { diff --git a/gui/src/components/OnboardingCard/hooks/useSubmitOnboarding.ts b/gui/src/components/OnboardingCard/hooks/useSubmitOnboarding.ts index a0407c171b..93b24ea91a 100644 --- a/gui/src/components/OnboardingCard/hooks/useSubmitOnboarding.ts +++ b/gui/src/components/OnboardingCard/hooks/useSubmitOnboarding.ts @@ -6,7 +6,7 @@ import { OnboardingModes } from "core/protocol/core"; import { useTutorialCard } from "../../../hooks/useTutorialCard"; import { useOnboardingCard } from "./useOnboardingCard"; -export function useSubmitOnboarding(mode: OnboardingModes) { +export function useSubmitOnboarding(mode: OnboardingModes, isDialog = false) { const posthog = usePostHog(); const ideMessenger = useContext(IdeMessengerContext); const { openTutorialCard } = useTutorialCard(); @@ -16,7 +16,7 @@ export function useSubmitOnboarding(mode: OnboardingModes) { const onboardingStatus = getLocalStorage("onboardingStatus"); // Always close the onboarding card and update config.json - closeOnboardingCard(); + closeOnboardingCard(isDialog); ideMessenger.post("completeOnboarding", { mode, }); diff --git a/gui/src/components/OnboardingCard/platform/PlatformOnboardingCard.tsx b/gui/src/components/OnboardingCard/platform/PlatformOnboardingCard.tsx index 360282e4a7..ede9295b3b 100644 --- a/gui/src/components/OnboardingCard/platform/PlatformOnboardingCard.tsx +++ b/gui/src/components/OnboardingCard/platform/PlatformOnboardingCard.tsx @@ -23,7 +23,11 @@ export interface OnboardingCardState { activeTab?: TabTitle; } -export function PlatformOnboardingCard() { +interface OnboardingCardProps { + isDialog: boolean; +} + +export function PlatformOnboardingCard({ isDialog }: OnboardingCardProps) { const onboardingCard = useOnboardingCard(); if (getLocalStorage("onboardingStatus") === undefined) { @@ -34,13 +38,18 @@ export function PlatformOnboardingCard() { return ( - - - + {!isDialog && ( + onboardingCard.close()}> + + + )}
{currentTab === "main" ? ( - setCurrentTab("local")} /> + setCurrentTab("local")} + isDialog={isDialog} + /> ) : (
@@ -53,7 +62,7 @@ export function PlatformOnboardingCard() { - +
)}
diff --git a/gui/src/components/OnboardingCard/platform/tabs/local.tsx b/gui/src/components/OnboardingCard/platform/tabs/local.tsx deleted file mode 100644 index ae30085ecc..0000000000 --- a/gui/src/components/OnboardingCard/platform/tabs/local.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { useAuth } from "../../../../context/Auth"; -import ContinueLogo from "../../../gui/ContinueLogo"; -import { useOnboardingCard } from "../../hooks"; - -export default function LocalTab() { - const onboardingCard = useOnboardingCard(); - const auth = useAuth(); - - return ( -
-
- -
- -

- Log in to quickly build your first custom AI code assistant -

- - {/*

- To prevent abuse, we'll ask you to sign in to GitHub. -

*/} - -
-
- ); -} diff --git a/gui/src/components/OnboardingCard/platform/tabs/main.tsx b/gui/src/components/OnboardingCard/platform/tabs/main.tsx index f6a14cb939..94e3c0de0c 100644 --- a/gui/src/components/OnboardingCard/platform/tabs/main.tsx +++ b/gui/src/components/OnboardingCard/platform/tabs/main.tsx @@ -3,47 +3,78 @@ import { Button, ButtonSubtext } from "../../.."; import { useAuth } from "../../../../context/Auth"; import ContinueLogo from "../../../gui/ContinueLogo"; import { useOnboardingCard } from "../../hooks"; +import { hasPassedFTL } from "../../../../util/freeTrial"; +import { useContext } from "react"; +import { IdeMessengerContext } from "../../../../context/IdeMessenger"; export default function MainTab({ onRemainLocal, + isDialog, }: { onRemainLocal: () => void; + isDialog: boolean; }) { + const ideMessenger = useContext(IdeMessengerContext); const onboardingCard = useOnboardingCard(); const auth = useAuth(); function onGetStarted() { auth.login(true).then((success) => { if (success) { - onboardingCard.close(); + onboardingCard.close(isDialog); } }); } + function openPastFreeTrialOnboarding() { + ideMessenger.post("controlPlane/openUrl", { + path: "setup-models", + }); + onboardingCard.close(isDialog); + } + + const pastFreeTrialLimit = hasPassedFTL(); + return (
-

- Log in to quickly build your first custom AI code assistant -

- -
- - -
- Or, remain local - -
-
-
+ {pastFreeTrialLimit ? ( + <> +

+ You've reached the free trial limit. Visit the Continue Platform to + select a Coding Assistant. +

+ + + ) : ( + <> +

+ Log in to quickly build your first custom AI code assistant +

+ + + + )} + + +
+ Or, remain local + +
+
); } diff --git a/gui/src/components/OnboardingCard/tabs/OnboardingBestTab.tsx b/gui/src/components/OnboardingCard/tabs/OnboardingBestTab.tsx index f4c02cefda..17d2ead001 100644 --- a/gui/src/components/OnboardingCard/tabs/OnboardingBestTab.tsx +++ b/gui/src/components/OnboardingCard/tabs/OnboardingBestTab.tsx @@ -2,8 +2,12 @@ import BestExperienceConfigForm from "../components/BestExperienceConfigForm"; import ProviderAlert from "../components/ProviderAlert"; import { useSubmitOnboarding } from "../hooks"; -function OnboardingBestTab() { - const { submitOnboarding } = useSubmitOnboarding("Best"); +interface OnboardingBestTabProps { + isDialog?: boolean; +} + +function OnboardingBestTab({ isDialog }: OnboardingBestTabProps) { + const { submitOnboarding } = useSubmitOnboarding("Best", isDialog); return (
diff --git a/gui/src/components/OnboardingCard/tabs/OnboardingLocalTab.tsx b/gui/src/components/OnboardingCard/tabs/OnboardingLocalTab.tsx index 71b4a13acf..76ba5bf5c8 100644 --- a/gui/src/components/OnboardingCard/tabs/OnboardingLocalTab.tsx +++ b/gui/src/components/OnboardingCard/tabs/OnboardingLocalTab.tsx @@ -14,14 +14,20 @@ import OllamaModelDownload from "../components/OllamaModelDownload"; import { OllamaStatus } from "../components/OllamaStatus"; import { useSubmitOnboarding } from "../hooks"; import { setDefaultModel } from "../../../redux/slices/configSlice"; +import { setDialogMessage, setShowDialog } from "../../../redux/slices/uiSlice"; const OLLAMA_CHECK_INTERVAL_MS = 3000; -function OnboardingLocalTab() { +interface OnboardingLocalTabProps { + isDialog?: boolean; +} + +function OnboardingLocalTab({ isDialog }: OnboardingLocalTabProps) { const dispatch = useDispatch(); const ideMessenger = useContext(IdeMessengerContext); const { submitOnboarding } = useSubmitOnboarding( hasPassedFTL() ? "LocalAfterFreeTrial" : "Local", + isDialog, ); const [hasLoadedChatModel, setHasLoadedChatModel] = useState(false); const [downloadedOllamaModels, setDownloadedOllamaModels] = useState< @@ -117,6 +123,11 @@ function OnboardingLocalTab() { onClick={() => { submitOnboarding(); + if (isDialog) { + dispatch(setDialogMessage(undefined)); + dispatch(setShowDialog(false)); + } + // Set the selected model to the local chat model dispatch( setDefaultModel({ diff --git a/gui/src/components/OnboardingCard/tabs/OnboardingQuickstartTab.tsx b/gui/src/components/OnboardingCard/tabs/OnboardingQuickstartTab.tsx index 326fd32e21..f291cf7c88 100644 --- a/gui/src/components/OnboardingCard/tabs/OnboardingQuickstartTab.tsx +++ b/gui/src/components/OnboardingCard/tabs/OnboardingQuickstartTab.tsx @@ -1,7 +1,11 @@ import ContinueLogo from "../../gui/ContinueLogo"; import QuickStartSubmitButton from "../components/QuickStartSubmitButton"; -function OnboardingQuickstartTab() { +interface OnboardingQuickstartTabProps { + isDialog?: boolean; +} + +function OnboardingQuickstartTab({ isDialog }: OnboardingQuickstartTabProps) { return (
@@ -18,7 +22,7 @@ function OnboardingQuickstartTab() { To prevent abuse, we'll ask you to sign in to GitHub.

- +
); diff --git a/gui/src/components/dialogs/FreeTrialOverDialog.tsx b/gui/src/components/dialogs/FreeTrialOverDialog.tsx new file mode 100644 index 0000000000..d8666b1b20 --- /dev/null +++ b/gui/src/components/dialogs/FreeTrialOverDialog.tsx @@ -0,0 +1,32 @@ +import { useDispatch } from "react-redux"; +import { setDialogMessage, setShowDialog } from "../../redux/slices/uiSlice"; +import { useAppSelector } from "../../redux/hooks"; +import { useEffect } from "react"; +import { selectUsePlatform } from "../../redux/selectors"; +import { PlatformOnboardingCard } from "../OnboardingCard/platform/PlatformOnboardingCard"; +import { OnboardingCard } from "../OnboardingCard"; + +function FreeTrialOverDialog() { + const dispatch = useDispatch(); + const history = useAppSelector((store) => store.session.history); + const usePlatform = useAppSelector(selectUsePlatform); + + useEffect(() => { + if (history.length === 0) { + dispatch(setShowDialog(false)); + dispatch(setDialogMessage(undefined)); + } + }, [history]); + + return ( +
+ {usePlatform ? ( + + ) : ( + + )} +
+ ); +} + +export default FreeTrialOverDialog; diff --git a/gui/src/components/dialogs/index.tsx b/gui/src/components/dialogs/index.tsx index 9e515d6cb0..1cf0567eeb 100644 --- a/gui/src/components/dialogs/index.tsx +++ b/gui/src/components/dialogs/index.tsx @@ -78,7 +78,7 @@ const TextDialog = (props: TextDialogProps) => { > - + {typeof props.message === "string" ? ( diff --git a/gui/src/components/modelSelection/platform/AssistantSelect.tsx b/gui/src/components/modelSelection/platform/AssistantSelect.tsx index 383c50f1ef..972dd27827 100644 --- a/gui/src/components/modelSelection/platform/AssistantSelect.tsx +++ b/gui/src/components/modelSelection/platform/AssistantSelect.tsx @@ -25,7 +25,9 @@ export function AssistantSelect(props: AssistantSelectProps) { const dispatch = useAppDispatch(); function onNewAssistant() { - ideMessenger.post("openUrl", "https://app-test.continue.dev/new"); + ideMessenger.post("controlPlane/openUrl", { + path: "new", + }); } return ( diff --git a/gui/src/pages/gui/Chat.tsx b/gui/src/pages/gui/Chat.tsx index 2d688a8b37..1bb5f57143 100644 --- a/gui/src/pages/gui/Chat.tsx +++ b/gui/src/pages/gui/Chat.tsx @@ -66,13 +66,17 @@ import { getMetaKeyLabel, isMetaEquivalentKeyPressed, } from "../../util"; -import { FREE_TRIAL_LIMIT_REQUESTS } from "../../util/freeTrial"; +import { + FREE_TRIAL_LIMIT_REQUESTS, + incrementFreeTrialCount, +} from "../../util/freeTrial"; import getMultifileEditPrompt from "../../util/getMultifileEditPrompt"; import { getLocalStorage, setLocalStorage } from "../../util/localStorage"; import ConfigErrorIndicator from "./ConfigError"; import { ToolCallDiv } from "./ToolCallDiv"; import { ToolCallButtons } from "./ToolCallDiv/ToolCallButtonsDiv"; import ToolOutput from "./ToolCallDiv/ToolOutput"; +import FreeTrialOverDialog from "../../components/dialogs/FreeTrialOverDialog"; const StopButton = styled.div` background-color: ${vscBackground}; @@ -247,21 +251,29 @@ export function Chat() { editorToClearOnSend?: Editor, ) => { if (defaultModel?.provider === "free-trial") { - const u = getLocalStorage("ftc"); - if (u) { - setLocalStorage("ftc", u + 1); - - if (u >= FREE_TRIAL_LIMIT_REQUESTS) { - onboardingCard.open("Best"); - posthog?.capture("ftc_reached"); - ideMessenger.ide.showToast( - "info", - "You've reached the free trial limit. Please configure a model to continue.", - ); - return; + 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 + ideMessenger.ide.showToast( + "error", + "You've reached the free trial limit. Please configure a model to continue.", + ); + + // Card in chat will only show if no history + // Also, note that platform card ignore the "Best", always opens to main tab + onboardingCard.open("Best"); + + // If history, show the dialog, which will automatically close if there is not history + if (history.length) { + dispatch(setDialogMessage()); + dispatch(setShowDialog(true)); } - } else { - setLocalStorage("ftc", 1); + return; } } @@ -543,9 +555,9 @@ export function Chat() { {onboardingCard.show && (
{usePlatform ? ( - + ) : ( - + )}
)} diff --git a/gui/src/util/freeTrial.ts b/gui/src/util/freeTrial.ts index 83e9ea510f..1961fb358f 100644 --- a/gui/src/util/freeTrial.ts +++ b/gui/src/util/freeTrial.ts @@ -1,11 +1,13 @@ -import { getLocalStorage } from "./localStorage"; +import { getLocalStorage, setLocalStorage } from "./localStorage"; export const FREE_TRIAL_LIMIT_REQUESTS = 50; -/** - * - * @returns {boolean} true if the user has passed the free trial limit, false otherwise. - */ 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; +}