From b86bc6f727e292c2d3a8fb1511646c36e2a2531f Mon Sep 17 00:00:00 2001 From: Patrick Erichsen Date: Tue, 3 Jun 2025 20:36:25 -0700 Subject: [PATCH] feat: refactor onboarding card --- .continue/rules/gui-link-opening.md | 10 + core/protocol/core.ts | 7 +- gui/src/components/AddModelButtonSubtext.tsx | 2 +- gui/src/components/Layout.tsx | 13 +- .../OnboardingCard/OnboardingCard.tsx | 66 ++++--- ...ubmitButton.tsx => ApiKeySubmitButton.tsx} | 13 +- .../components/BestExperienceConfigForm.tsx | 171 ------------------ .../components/OllamaModelDownload.tsx | 2 +- .../components/OnboardingApiKeyTab.tsx | 13 ++ .../components/OnboardingCardLanding.tsx | 101 +++++++++++ .../components/OnboardingCardTabs.tsx | 59 ++---- ...ngLocalTab.tsx => OnboardingOllamaTab.tsx} | 19 +- .../components/ProviderAlert.tsx | 36 ---- .../OnboardingCard/components/index.ts | 9 + .../OnboardingCard/hooks/useOnboardingCard.ts | 23 ++- .../platform/PlatformOnboardingCard.tsx | 66 ------- .../OnboardingCard/platform/tabs/main.tsx | 108 ----------- .../OnboardingCard/tabs/OnboardingBestTab.tsx | 22 --- .../tabs/OnboardingQuickstartTab.tsx | 31 ---- .../components/OnboardingCard/tabs/index.ts | 3 - gui/src/components/OnboardingCard/utils.ts | 8 +- .../dialogs/FreeTrialOverDialog.tsx | 4 +- gui/src/components/index.ts | 7 - .../LumpToolbar/BlockSettingsTopToolbar.tsx | 4 +- .../components/modelSelection/ModelSelect.tsx | 16 +- .../modelSelection/ModelSelectionListbox.tsx | 72 ++++---- gui/src/forms/AddModelForm.tsx | 16 +- gui/src/pages/config/HelpCenterSection.tsx | 7 +- gui/src/pages/gui/Chat.tsx | 3 +- gui/src/pages/gui/EmptyChatBody.tsx | 8 +- 30 files changed, 302 insertions(+), 617 deletions(-) create mode 100644 .continue/rules/gui-link-opening.md rename gui/src/components/OnboardingCard/components/{QuickStartSubmitButton.tsx => ApiKeySubmitButton.tsx} (84%) delete mode 100644 gui/src/components/OnboardingCard/components/BestExperienceConfigForm.tsx create mode 100644 gui/src/components/OnboardingCard/components/OnboardingApiKeyTab.tsx create mode 100644 gui/src/components/OnboardingCard/components/OnboardingCardLanding.tsx rename gui/src/components/OnboardingCard/components/{OnboardingLocalTab.tsx => OnboardingOllamaTab.tsx} (91%) delete mode 100644 gui/src/components/OnboardingCard/components/ProviderAlert.tsx create mode 100644 gui/src/components/OnboardingCard/components/index.ts delete mode 100644 gui/src/components/OnboardingCard/platform/PlatformOnboardingCard.tsx delete mode 100644 gui/src/components/OnboardingCard/platform/tabs/main.tsx delete mode 100644 gui/src/components/OnboardingCard/tabs/OnboardingBestTab.tsx delete mode 100644 gui/src/components/OnboardingCard/tabs/OnboardingQuickstartTab.tsx delete mode 100644 gui/src/components/OnboardingCard/tabs/index.ts diff --git a/.continue/rules/gui-link-opening.md b/.continue/rules/gui-link-opening.md new file mode 100644 index 000000000..db791e67a --- /dev/null +++ b/.continue/rules/gui-link-opening.md @@ -0,0 +1,10 @@ +--- +globs: gui/**/* +description: Ensures consistent URL opening behavior in GUI components using the + IDE messenger pattern +alwaysApply: false +--- + +# GUI Link Opening + +When adding functionality to open external links in GUI components, use `ideMessenger.post("openUrl", url)` where `ideMessenger` is obtained from `useContext(IdeMessengerContext)` diff --git a/core/protocol/core.ts b/core/protocol/core.ts index 142764d19..4d899b92c 100644 --- a/core/protocol/core.ts +++ b/core/protocol/core.ts @@ -35,7 +35,10 @@ import { import { SerializedOrgWithProfiles } from "../config/ProfileLifecycleManager"; import { ControlPlaneSessionInfo } from "../control-plane/AuthTypes"; -export type OnboardingModes = "Local" | "Best" | "Custom" | "Quickstart"; +export enum OnboardingModes { + API_KEYS = "API Keys", + OLLAMA = "Ollama", +} export interface ListHistoryOptions { offset?: number; @@ -197,7 +200,7 @@ export type ToCoreFromIdeOrWebviewProtocol = { { contextItems: ContextItem[]; errorMessage?: string }, ]; "clipboardCache/add": [{ content: string }, void]; - "controlPlane/openUrl": [{ path: string; orgSlug: string | undefined }, void]; + "controlPlane/openUrl": [{ path: string; orgSlug?: string }, void]; isItemTooBig: [{ item: ContextItemWithId }, boolean]; didChangeControlPlaneSessionInfo: [ { sessionInfo: ControlPlaneSessionInfo | undefined }, diff --git a/gui/src/components/AddModelButtonSubtext.tsx b/gui/src/components/AddModelButtonSubtext.tsx index aa7ef99b5..ed4bbda14 100644 --- a/gui/src/components/AddModelButtonSubtext.tsx +++ b/gui/src/components/AddModelButtonSubtext.tsx @@ -9,7 +9,7 @@ function AddModelButtonSubtext() { This will update your{" "} ideMessenger.post("config/openProfile", { profileId: undefined, diff --git a/gui/src/components/Layout.tsx b/gui/src/components/Layout.tsx index b5e4ebda6..b9a81d63d 100644 --- a/gui/src/components/Layout.tsx +++ b/gui/src/components/Layout.tsx @@ -1,3 +1,4 @@ +import { OnboardingModes } from "core/protocol/core"; import { useContext, useEffect } from "react"; import { Outlet, useLocation, useNavigate } from "react-router-dom"; import styled from "styled-components"; @@ -13,13 +14,14 @@ import { enterEdit, exitEdit } from "../redux/thunks/edit"; import { saveCurrentSession } from "../redux/thunks/session"; import { fontSize, isMetaEquivalentKeyPressed } from "../util"; import { incrementFreeTrialCount } from "../util/freeTrial"; +import { getLocalStorage } from "../util/localStorage"; import { ROUTES } from "../util/navigation"; import { FatalErrorIndicator } from "./config/FatalErrorNotice"; import TextDialog from "./dialogs"; import Footer from "./Footer"; import { LumpProvider } from "./mainInput/Lump/LumpContext"; import { useMainEditor } from "./mainInput/TipTapEditor"; -import { isNewUserOnboarding, useOnboardingCard } from "./OnboardingCard"; +import { useOnboardingCard } from "./OnboardingCard"; import OSRContextMenu from "./OSRContextMenu"; import PostHogPageView from "./PosthogPageView"; @@ -131,7 +133,7 @@ const Layout = () => { useWebviewListener( "openOnboardingCard", async () => { - onboardingCard.open("Best"); + onboardingCard.open(OnboardingModes.API_KEYS); }, [], ); @@ -139,7 +141,7 @@ const Layout = () => { useWebviewListener( "setupLocalConfig", async () => { - onboardingCard.open("Local"); + onboardingCard.open(OnboardingModes.OLLAMA); }, [], ); @@ -179,7 +181,6 @@ const Layout = () => { if (isMetaEquivalentKeyPressed(event) && event.code === "KeyC") { const selection = window.getSelection()?.toString(); if (selection) { - // Copy to clipboard setTimeout(() => { void navigator.clipboard.writeText(selection); }, 100); @@ -196,10 +197,10 @@ const Layout = () => { useEffect(() => { if ( - isNewUserOnboarding() && + !getLocalStorage("onboardingStatus") && (location.pathname === "/" || location.pathname === "/index.html") ) { - onboardingCard.open("Quickstart"); + onboardingCard.open(); } }, [location]); diff --git a/gui/src/components/OnboardingCard/OnboardingCard.tsx b/gui/src/components/OnboardingCard/OnboardingCard.tsx index aa8be2a29..89e89a0aa 100644 --- a/gui/src/components/OnboardingCard/OnboardingCard.tsx +++ b/gui/src/components/OnboardingCard/OnboardingCard.tsx @@ -1,13 +1,18 @@ +import { OnboardingModes } from "core/protocol/core"; import { useAppSelector } from "../../redux/hooks"; import { getLocalStorage, setLocalStorage } from "../../util/localStorage"; import { ReusableCard } from "../ReusableCard"; -import { OnboardingCardTabs, TabTitle } from "./components/OnboardingCardTabs"; -import { useOnboardingCard } from "./hooks/useOnboardingCard"; -import * as Tabs from "./tabs"; +import { + OnboardingApiKeyTab, + OnboardingCardLanding, + OnboardingCardTabs, + OnboardingOllamaTab, +} from "./components"; +import { useOnboardingCard } from "./hooks"; export interface OnboardingCardState { show?: boolean; - activeTab?: TabTitle; + activeTab?: OnboardingModes; } interface OnboardingCardProps { @@ -15,37 +20,48 @@ interface OnboardingCardProps { } export function OnboardingCard({ isDialog }: OnboardingCardProps) { - const onboardingCard = useOnboardingCard(); + const { activeTab, close, setActiveTab } = useOnboardingCard(); const config = useAppSelector((store) => store.config.config); - function renderTabContent() { - switch (onboardingCard.activeTab) { - case "Quickstart": - return ; - case "Best": - return ; - case "Local": - return ; - default: - return ; - } - } - if (getLocalStorage("onboardingStatus") === undefined) { setLocalStorage("onboardingStatus", "Started"); } + function renderTabContent() { + switch (activeTab) { + case OnboardingModes.API_KEYS: + return ; + case OnboardingModes.OLLAMA: + return ; + default: + return ; + } + } + + if (activeTab) { + return ( + + + {renderTabContent()} + + ); + } + return ( onboardingCard.close()} - testId="onboarding-card" + onClose={close} > - - {renderTabContent()} +
+ setActiveTab(OnboardingModes.API_KEYS)} + isDialog={isDialog} + /> +
); } diff --git a/gui/src/components/OnboardingCard/components/QuickStartSubmitButton.tsx b/gui/src/components/OnboardingCard/components/ApiKeySubmitButton.tsx similarity index 84% rename from gui/src/components/OnboardingCard/components/QuickStartSubmitButton.tsx rename to gui/src/components/OnboardingCard/components/ApiKeySubmitButton.tsx index 07688fe6b..54f56f583 100644 --- a/gui/src/components/OnboardingCard/components/QuickStartSubmitButton.tsx +++ b/gui/src/components/OnboardingCard/components/ApiKeySubmitButton.tsx @@ -1,3 +1,4 @@ +import { OnboardingModes } from "core/protocol/core"; import { useContext } from "react"; import { useDispatch } from "react-redux"; import { Button, ButtonSubtext } from "../.."; @@ -7,15 +8,17 @@ import { isJetBrains } from "../../../util"; import { useSubmitOnboarding } from "../hooks"; import JetBrainsFetchGitHubTokenDialog from "./JetBrainsFetchGitHubTokenDialog"; -interface QuickstartSubmitButtonProps { +interface ApiKeySubmitButtonProps { isDialog?: boolean; } -function QuickstartSubmitButton({ isDialog }: QuickstartSubmitButtonProps) { +function ApiKeySubmitButton({ isDialog }: ApiKeySubmitButtonProps) { const ideMessenger = useContext(IdeMessengerContext); const dispatch = useDispatch(); - - const { submitOnboarding } = useSubmitOnboarding("Quickstart", isDialog); + const { submitOnboarding } = useSubmitOnboarding( + OnboardingModes.API_KEYS, + isDialog, + ); function onComplete() { submitOnboarding(); @@ -66,4 +69,4 @@ function QuickstartSubmitButton({ isDialog }: QuickstartSubmitButtonProps) { ); } -export default QuickstartSubmitButton; +export default ApiKeySubmitButton; diff --git a/gui/src/components/OnboardingCard/components/BestExperienceConfigForm.tsx b/gui/src/components/OnboardingCard/components/BestExperienceConfigForm.tsx deleted file mode 100644 index 0929aad8b..000000000 --- a/gui/src/components/OnboardingCard/components/BestExperienceConfigForm.tsx +++ /dev/null @@ -1,171 +0,0 @@ -import { CubeIcon } from "@heroicons/react/24/outline"; -import { FormEventHandler, useContext, useState } from "react"; -import { Button, Input, InputSubtext, lightGray } from "../.."; -import { useAuth } from "../../../context/Auth"; -import { IdeMessengerContext } from "../../../context/IdeMessenger"; -import { models } from "../../../pages/AddNewModel/configs/models"; -import { providers } from "../../../pages/AddNewModel/configs/providers"; -import { useAppDispatch } from "../../../redux/hooks"; -import { updateSelectedModelByRole } from "../../../redux/thunks"; -import AddModelButtonSubtext from "../../AddModelButtonSubtext"; - -const { anthropic, mistral } = providers; -const chatProvider = anthropic!; -const autocompleteProvider = mistral!; - -const { - claude35Sonnet: chatModel, - claude35Haiku: repoMapModel, - codestral: autocompleteModel, -} = models; - -interface BestExperienceConfigFormProps { - onComplete: () => void; -} - -function BestExperienceConfigForm({ - onComplete, -}: BestExperienceConfigFormProps) { - const dispatch = useAppDispatch(); - const { selectedProfile } = useAuth(); - - const ideMessenger = useContext(IdeMessengerContext); - - const [autocompleteApiKey, setAutocompleteApiKey] = useState(""); - const [chatApiKey, setChatApiKey] = useState(""); - - const handleSubmit: FormEventHandler = async (e) => { - e.preventDefault(); - - const chatModelConfig = { - model: chatModel.params.model, - provider: chatProvider.provider, - underlyingProviderName: chatProvider.provider, - apiKey: chatApiKey, - title: chatModel.params.title, - }; - - const repoMapConfig = { - model: repoMapModel.params.model, - provider: chatProvider.provider, - underlyingProviderName: chatProvider.provider, - apiKey: chatApiKey, - title: repoMapModel.params.title, - }; - - const autocompleteModelConfig = { - title: autocompleteModel.params.title, - provider: autocompleteProvider.provider, - underlyingProviderName: autocompleteProvider.provider, - model: autocompleteModel.params.model, - apiKey: autocompleteApiKey, - }; - - ideMessenger.post("config/addModel", { model: chatModelConfig }); - ideMessenger.post("config/addModel", { - model: repoMapConfig, - role: "repoMapFileSelection", - }); - - dispatch( - updateSelectedModelByRole({ - selectedProfile, - role: "chat", - modelTitle: chatModelConfig.title, - }), - ); - - if (!!autocompleteApiKey) { - await ideMessenger.request("addAutocompleteModel", { - model: autocompleteModelConfig, - }); - } - - onComplete(); - }; - - return ( -
-
-
-
- -
- - - {chatModel.title}{" "} - by Anthropic - -
-
- -
- setChatApiKey(e.target.value)} - data-testid="best-chat-api-key-input" - /> - - - Click here - {" "} - to create an Anthropic API key - -
-
- -
-
- -
- - - {autocompleteModel.title}{" "} - by Mistral - -
-
- -
- setAutocompleteApiKey(e.target.value)} - data-testid="best-autocomplete-api-key-input" - /> - - - Click here - {" "} - to create a Mistral API key - -
-
- -
- - -
-
-
- ); -} - -export default BestExperienceConfigForm; diff --git a/gui/src/components/OnboardingCard/components/OllamaModelDownload.tsx b/gui/src/components/OnboardingCard/components/OllamaModelDownload.tsx index c98ca6794..abce87d86 100644 --- a/gui/src/components/OnboardingCard/components/OllamaModelDownload.tsx +++ b/gui/src/components/OnboardingCard/components/OllamaModelDownload.tsx @@ -27,7 +27,7 @@ function OllamaModelDownload({ return (
-

{title}

+

{title}

{hasDownloaded ? ( ) : ( diff --git a/gui/src/components/OnboardingCard/components/OnboardingApiKeyTab.tsx b/gui/src/components/OnboardingCard/components/OnboardingApiKeyTab.tsx new file mode 100644 index 000000000..54ed617be --- /dev/null +++ b/gui/src/components/OnboardingCard/components/OnboardingApiKeyTab.tsx @@ -0,0 +1,13 @@ +import Alert from "../../gui/Alert"; + +interface OnboardingApiKeyTabProps { + isDialog?: boolean; +} + +export function OnboardingApiKeyTab({ isDialog }: OnboardingApiKeyTabProps) { + return ( +
+ Configure Continue to a variety of model providers +
+ ); +} diff --git a/gui/src/components/OnboardingCard/components/OnboardingCardLanding.tsx b/gui/src/components/OnboardingCard/components/OnboardingCardLanding.tsx new file mode 100644 index 000000000..d6e6b8a19 --- /dev/null +++ b/gui/src/components/OnboardingCard/components/OnboardingCardLanding.tsx @@ -0,0 +1,101 @@ +import { ChevronRightIcon } from "@heroicons/react/24/outline"; +import { useContext } from "react"; +import { Button, ButtonSubtext } from "../.."; +import { useAuth } from "../../../context/Auth"; +import { IdeMessengerContext } from "../../../context/IdeMessenger"; +import { selectCurrentOrg } from "../../../redux"; +import { useAppSelector } from "../../../redux/hooks"; +import { hasPassedFTL } from "../../../util/freeTrial"; +import ContinueLogo from "../../gui/ContinueLogo"; +import { ToolTip } from "../../gui/Tooltip"; +import { useOnboardingCard } from "../hooks"; + +export function OnboardingCardLanding({ + onSelectConfigure, + isDialog, +}: { + onSelectConfigure: () => void; + isDialog?: boolean; +}) { + const ideMessenger = useContext(IdeMessengerContext); + const onboardingCard = useOnboardingCard(); + const auth = useAuth(); + const currentOrg = useAppSelector(selectCurrentOrg); + + function onGetStarted() { + void auth.login(true).then((success) => { + if (success) { + onboardingCard.close(isDialog); + } + }); + } + + function openPastFreeTrialOnboarding() { + ideMessenger.post("controlPlane/openUrl", { + path: "setup-models", + orgSlug: currentOrg?.slug, + }); + onboardingCard.close(isDialog); + } + + const pastFreeTrialLimit = hasPassedFTL(); + + return ( +
+
+ +
+ + {pastFreeTrialLimit ? ( + <> +

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

+ + + ) : ( + <> +

+ Log in to access a free trial of the +
+ + ideMessenger.post("controlPlane/openUrl", { + path: "pricing", + }) + } + > + Models Add-On + + + Free trial includes 50 Chat requests and 2,000 autocomplete + requests + +

+ + + + )} + + +
+ Or, configure your own models + +
+
+
+ ); +} diff --git a/gui/src/components/OnboardingCard/components/OnboardingCardTabs.tsx b/gui/src/components/OnboardingCard/components/OnboardingCardTabs.tsx index b1543ea58..26db153ed 100644 --- a/gui/src/components/OnboardingCard/components/OnboardingCardTabs.tsx +++ b/gui/src/components/OnboardingCard/components/OnboardingCardTabs.tsx @@ -1,33 +1,12 @@ +import { OnboardingModes } from "core/protocol/core"; import styled from "styled-components"; import { vscForeground } from "../.."; -import { hasPassedFTL } from "../../../util/freeTrial"; interface OnboardingCardTabsProps { - activeTab: TabTitle; - onTabClick: (tabName: TabTitle) => void; + activeTab: OnboardingModes; + onTabClick: (tabName: OnboardingModes) => void; } -export type TabTitle = "Quickstart" | "Best" | "Local" | "ExistingUserHubIntro"; - -export const TabTitles: { [k in TabTitle]: { md: string; default: string } } = { - Quickstart: { - md: "Quickstart", - default: "Quickstart", - }, - Best: { - md: "Best", - default: "Best experience", - }, - Local: { - md: "Local", - default: "Local with Ollama", - }, - ExistingUserHubIntro: { - md: "Try out hub.continue.dev", - default: "Try out hub.continue.dev", - }, -}; - const StyledSelect = styled.select` width: 100%; padding: 0.5rem; @@ -73,27 +52,21 @@ export function OnboardingCardTabs({ activeTab, onTabClick, }: OnboardingCardTabsProps) { + console.log({ keys: Object.values(OnboardingModes) }); return (
- {Object.entries(TabTitles).map(([tabType, titles]) => { - if (hasPassedFTL() && tabType === "Quickstart") { - return undefined; - } - + {Object.values(OnboardingModes).map((tabTitle) => { return ( onTabClick(tabType as TabTitle)} - data-testid={`onboarding-tab-${tabType}`} + key={tabTitle} + isActive={activeTab === tabTitle} + onClick={() => onTabClick(tabTitle as OnboardingModes)} + data-testid={`onboarding-tab-${tabTitle}`} > -

- {titles.default} -

-

{titles.md}

+

{tabTitle}

); })} @@ -102,16 +75,12 @@ export function OnboardingCardTabs({
onTabClick(e.target.value as TabTitle)} + onChange={(e) => onTabClick(e.target.value as OnboardingModes)} > - {Object.entries(TabTitles).map(([tabType, titles]) => { - if (hasPassedFTL() && tabType === "Quickstart") { - return null; - } - + {Object.values(OnboardingModes).map((tabTitle) => { return ( - ); })} diff --git a/gui/src/components/OnboardingCard/components/OnboardingLocalTab.tsx b/gui/src/components/OnboardingCard/components/OnboardingOllamaTab.tsx similarity index 91% rename from gui/src/components/OnboardingCard/components/OnboardingLocalTab.tsx rename to gui/src/components/OnboardingCard/components/OnboardingOllamaTab.tsx index b0cc6f179..e447fe530 100644 --- a/gui/src/components/OnboardingCard/components/OnboardingLocalTab.tsx +++ b/gui/src/components/OnboardingCard/components/OnboardingOllamaTab.tsx @@ -5,6 +5,7 @@ import { LOCAL_ONBOARDING_FIM_MODEL, LOCAL_ONBOARDING_PROVIDER_TITLE, } from "core/config/onboarding"; +import { OnboardingModes } from "core/protocol/core"; import { useContext, useEffect, useState } from "react"; import { Button, ButtonSubtext } from "../.."; import { useAuth } from "../../../context/Auth"; @@ -12,20 +13,24 @@ import { IdeMessengerContext } from "../../../context/IdeMessenger"; import { useAppDispatch } from "../../../redux/hooks"; import { setDialogMessage, setShowDialog } from "../../../redux/slices/uiSlice"; import { updateSelectedModelByRole } from "../../../redux/thunks"; +import Alert from "../../gui/Alert"; import { useSubmitOnboarding } from "../hooks"; import OllamaModelDownload from "./OllamaModelDownload"; import { OllamaStatus } from "./OllamaStatus"; const OLLAMA_CHECK_INTERVAL_MS = 3000; -interface OnboardingLocalTabProps { +interface OnboardingOllamaTabProps { isDialog?: boolean; } -function OnboardingLocalTab({ isDialog }: OnboardingLocalTabProps) { +export function OnboardingOllamaTab({ isDialog }: OnboardingOllamaTabProps) { const dispatch = useAppDispatch(); const ideMessenger = useContext(IdeMessengerContext); - const { submitOnboarding } = useSubmitOnboarding("Local", isDialog); + const { submitOnboarding } = useSubmitOnboarding( + OnboardingModes.OLLAMA, + isDialog, + ); const [hasLoadedChatModel, setHasLoadedChatModel] = useState(false); const [downloadedOllamaModels, setDownloadedOllamaModels] = useState< string[] @@ -133,10 +138,10 @@ function OnboardingLocalTab({ isDialog }: OnboardingLocalTabProps) { return (
+ Configure Continue to run models locally using Ollama +
-

- Install Ollama -

+

Install Ollama

@@ -175,5 +180,3 @@ function OnboardingLocalTab({ isDialog }: OnboardingLocalTabProps) {
); } - -export default OnboardingLocalTab; diff --git a/gui/src/components/OnboardingCard/components/ProviderAlert.tsx b/gui/src/components/OnboardingCard/components/ProviderAlert.tsx deleted file mode 100644 index 13e2eae11..000000000 --- a/gui/src/components/OnboardingCard/components/ProviderAlert.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { useDispatch } from "react-redux"; -import AddModelForm from "../../../forms/AddModelForm"; -import { setDialogMessage, setShowDialog } from "../../../redux/slices/uiSlice"; -import Alert from "../../gui/Alert"; -import { useSubmitOnboarding } from "../hooks"; - -function ProviderAlert() { - const dispatch = useDispatch(); - const { submitOnboarding } = useSubmitOnboarding("Custom"); - - function onClick() { - dispatch(setShowDialog(true)); - dispatch(setDialogMessage()); - } - - return ( -
- -

- Prefer to use a different provider like OpenAI? -

-

- - Click here - {" "} - to add a Chat model from OpenAI, Gemini, and more -

-
-
- ); -} - -export default ProviderAlert; diff --git a/gui/src/components/OnboardingCard/components/index.ts b/gui/src/components/OnboardingCard/components/index.ts new file mode 100644 index 000000000..bf7efbfd5 --- /dev/null +++ b/gui/src/components/OnboardingCard/components/index.ts @@ -0,0 +1,9 @@ +export * from "./ApiKeySubmitButton"; +export * from "./JetBrainsFetchGitHubTokenDialog"; +export * from "./OllamaCompletedStep"; +export * from "./OllamaModelDownload"; +export * from "./OllamaStatus"; +export * from "./OnboardingApiKeyTab"; +export * from "./OnboardingCardLanding"; +export * from "./OnboardingCardTabs"; +export * from "./OnboardingOllamaTab"; diff --git a/gui/src/components/OnboardingCard/hooks/useOnboardingCard.ts b/gui/src/components/OnboardingCard/hooks/useOnboardingCard.ts index 9622754c4..388ba8353 100644 --- a/gui/src/components/OnboardingCard/hooks/useOnboardingCard.ts +++ b/gui/src/components/OnboardingCard/hooks/useOnboardingCard.ts @@ -1,21 +1,19 @@ -import { useDispatch } from "react-redux"; +import { OnboardingModes } from "core/protocol/core"; import { useNavigate } from "react-router-dom"; -import { TabTitle } from "../components/OnboardingCardTabs"; +import { OnboardingCardState } from ".."; +import { useAppDispatch, useAppSelector } from "../../../redux/hooks"; import { setDialogMessage, setOnboardingCard, setShowDialog, } from "../../../redux/slices/uiSlice"; -import { OnboardingCardState } from ".."; import { getLocalStorage, setLocalStorage } from "../../../util/localStorage"; -import { useAppDispatch, useAppSelector } from "../../../redux/hooks"; -import { saveCurrentSession } from "../../../redux/thunks/session"; export interface UseOnboardingCard { show: OnboardingCardState["show"]; activeTab: OnboardingCardState["activeTab"]; - setActiveTab: (tab: TabTitle) => void; - open: (tab: TabTitle) => void; + setActiveTab: (tab: OnboardingModes) => void; + open: (tab?: OnboardingModes) => void; close: (isDialog?: boolean) => void; } @@ -40,9 +38,14 @@ export function useOnboardingCard(): UseOnboardingCard { show = onboardingStatus !== "Completed" && !hasDismissedOnboardingCard; } - async function open(tab: TabTitle) { + async function open(tab?: OnboardingModes) { navigate("/"); - dispatch(setOnboardingCard({ show: true, activeTab: tab })); + dispatch( + setOnboardingCard({ + show: true, + activeTab: tab ?? OnboardingModes.API_KEYS, + }), + ); } function close(isDialog = false) { @@ -54,7 +57,7 @@ export function useOnboardingCard(): UseOnboardingCard { } } - function setActiveTab(tab: TabTitle) { + function setActiveTab(tab: OnboardingModes) { dispatch(setOnboardingCard({ show: true, activeTab: tab })); } diff --git a/gui/src/components/OnboardingCard/platform/PlatformOnboardingCard.tsx b/gui/src/components/OnboardingCard/platform/PlatformOnboardingCard.tsx deleted file mode 100644 index 883ea66d1..000000000 --- a/gui/src/components/OnboardingCard/platform/PlatformOnboardingCard.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { useState } from "react"; -import { useAuth } from "../../../context/Auth"; -import { useAppSelector } from "../../../redux/hooks"; -import { getLocalStorage, setLocalStorage } from "../../../util/localStorage"; -import Alert from "../../gui/Alert"; -import { ReusableCard } from "../../ReusableCard"; -import { TabTitle } from "../components/OnboardingCardTabs"; -import OnboardingLocalTab from "../components/OnboardingLocalTab"; -import { useOnboardingCard } from "../hooks"; -import MainTab from "./tabs/main"; - -export interface OnboardingCardState { - show?: boolean; - activeTab?: TabTitle; -} - -interface OnboardingCardProps { - isDialog: boolean; -} - -export function PlatformOnboardingCard({ isDialog }: OnboardingCardProps) { - const onboardingCard = useOnboardingCard(); - const config = useAppSelector((store) => store.config.config); - const auth = useAuth(); - const [currentTab, setCurrentTab] = useState<"main" | "local">("main"); - - if (getLocalStorage("onboardingStatus") === undefined) { - setLocalStorage("onboardingStatus", "Started"); - } - - function onGetStarted() { - auth.login(true).then((success) => { - if (success) { - onboardingCard.close(isDialog); - } - }); - } - - return ( - onboardingCard.close()} - > -
- {currentTab === "main" ? ( - setCurrentTab("local")} - isDialog={isDialog} - /> - ) : ( -
- - By choosing this option, Continue will be configured by a local - config.yaml file. If you're just looking to use Ollama and still - want to manage your configuration through Continue, click{" "} - - here - - - -
- )} -
-
- ); -} diff --git a/gui/src/components/OnboardingCard/platform/tabs/main.tsx b/gui/src/components/OnboardingCard/platform/tabs/main.tsx deleted file mode 100644 index 9bad6a50f..000000000 --- a/gui/src/components/OnboardingCard/platform/tabs/main.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { ChevronRightIcon } from "@heroicons/react/24/outline"; -import { useContext } from "react"; -import { ButtonSubtext } from "../../.."; -import { useAuth } from "../../../../context/Auth"; -import { IdeMessengerContext } from "../../../../context/IdeMessenger"; -import { selectCurrentOrg } from "../../../../redux"; -import { useAppSelector } from "../../../../redux/hooks"; -import { hasPassedFTL } from "../../../../util/freeTrial"; -import ContinueLogo from "../../../gui/ContinueLogo"; -import { Button } from "../../../ui/Button"; -import { useOnboardingCard } from "../../hooks"; - -export default function MainTab({ - onRemainLocal, - isDialog, -}: { - onRemainLocal: () => void; - isDialog: boolean; -}) { - const ideMessenger = useContext(IdeMessengerContext); - const onboardingCard = useOnboardingCard(); - const auth = useAuth(); - const currentOrg = useAppSelector(selectCurrentOrg); - - function onGetStarted() { - void auth.login(true).then((success) => { - if (success) { - onboardingCard.close(isDialog); - } - }); - } - - function openPastFreeTrialOnboarding() { - ideMessenger.post("controlPlane/openUrl", { - path: "setup-models", - orgSlug: currentOrg?.slug, - }); - onboardingCard.close(isDialog); - } - - const pastFreeTrialLimit = hasPassedFTL(); - - return ( -
-
- -
- - {pastFreeTrialLimit ? ( - <> -

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

- - - ) : onboardingCard.activeTab === "ExistingUserHubIntro" ? ( - <> -

- You can now browse and create custom AI code assistants at{" "} - hub.continue.dev -

- - - - ) : ( - <> -

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

- - - - )} - - {onboardingCard.activeTab === "ExistingUserHubIntro" ? ( - onboardingCard.close(isDialog)}> -
- Or, use Continue as usual - -
-
- ) : ( - -
- Or, remain local - -
-
- )} -
- ); -} diff --git a/gui/src/components/OnboardingCard/tabs/OnboardingBestTab.tsx b/gui/src/components/OnboardingCard/tabs/OnboardingBestTab.tsx deleted file mode 100644 index 17d2ead00..000000000 --- a/gui/src/components/OnboardingCard/tabs/OnboardingBestTab.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import BestExperienceConfigForm from "../components/BestExperienceConfigForm"; -import ProviderAlert from "../components/ProviderAlert"; -import { useSubmitOnboarding } from "../hooks"; - -interface OnboardingBestTabProps { - isDialog?: boolean; -} - -function OnboardingBestTab({ isDialog }: OnboardingBestTabProps) { - const { submitOnboarding } = useSubmitOnboarding("Best", isDialog); - - return ( -
-
- -
- -
- ); -} - -export default OnboardingBestTab; diff --git a/gui/src/components/OnboardingCard/tabs/OnboardingQuickstartTab.tsx b/gui/src/components/OnboardingCard/tabs/OnboardingQuickstartTab.tsx deleted file mode 100644 index f291cf7c8..000000000 --- a/gui/src/components/OnboardingCard/tabs/OnboardingQuickstartTab.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import ContinueLogo from "../../gui/ContinueLogo"; -import QuickStartSubmitButton from "../components/QuickStartSubmitButton"; - -interface OnboardingQuickstartTabProps { - isDialog?: boolean; -} - -function OnboardingQuickstartTab({ isDialog }: OnboardingQuickstartTabProps) { - return ( -
-
-
- -
- -

- Quickly get up and running using our API keys. After this trial, we'll - help you set up your own models. -

- -

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

- - -
-
- ); -} - -export default OnboardingQuickstartTab; diff --git a/gui/src/components/OnboardingCard/tabs/index.ts b/gui/src/components/OnboardingCard/tabs/index.ts deleted file mode 100644 index 1e6aae37d..000000000 --- a/gui/src/components/OnboardingCard/tabs/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { default as Local } from "../components/OnboardingLocalTab"; -export { default as Best } from "./OnboardingBestTab"; -export { default as Quickstart } from "./OnboardingQuickstartTab"; diff --git a/gui/src/components/OnboardingCard/utils.ts b/gui/src/components/OnboardingCard/utils.ts index 4d2b8ef2c..45822ead4 100644 --- a/gui/src/components/OnboardingCard/utils.ts +++ b/gui/src/components/OnboardingCard/utils.ts @@ -1,14 +1,10 @@ +import { OnboardingModes } from "core/protocol/core"; import { getLocalStorage, setLocalStorage } from "../../util/localStorage"; import { OnboardingCardState } from "./OnboardingCard"; -// Note that there is no "NotStarted" status since the -// local storage value is null until onboarding begins export type OnboardingStatus = "Started" | "Completed"; -// If there is no value in local storage for "onboardingStatus", -// it implies that the user has not begun or completed onboarding. export function isNewUserOnboarding() { - // We used to use "onboardingComplete", but switched to "onboardingStatus" const onboardingCompleteLegacyValue = localStorage.getItem("onboardingComplete"); @@ -24,7 +20,7 @@ export function isNewUserOnboarding() { export const defaultOnboardingCardState: OnboardingCardState = { show: false, - activeTab: "Quickstart", + activeTab: OnboardingModes.API_KEYS, }; export enum OllamaConnectionStatuses { diff --git a/gui/src/components/dialogs/FreeTrialOverDialog.tsx b/gui/src/components/dialogs/FreeTrialOverDialog.tsx index a200b2f15..8f66075bc 100644 --- a/gui/src/components/dialogs/FreeTrialOverDialog.tsx +++ b/gui/src/components/dialogs/FreeTrialOverDialog.tsx @@ -2,7 +2,7 @@ import { useEffect } from "react"; import { useDispatch } from "react-redux"; import { useAppSelector } from "../../redux/hooks"; import { setDialogMessage, setShowDialog } from "../../redux/slices/uiSlice"; -import { PlatformOnboardingCard } from "../OnboardingCard/platform/PlatformOnboardingCard"; +import { OnboardingCard } from "../OnboardingCard"; function FreeTrialOverDialog() { const dispatch = useDispatch(); @@ -17,7 +17,7 @@ function FreeTrialOverDialog() { return (
- +
); } diff --git a/gui/src/components/index.ts b/gui/src/components/index.ts index 217c9203d..a13b7643a 100644 --- a/gui/src/components/index.ts +++ b/gui/src/components/index.ts @@ -85,13 +85,6 @@ export const GhostButton = styled.button` } `; -export const InputSubtext = styled.span` - font-size: 0.75rem; - line-height: 1rem; - color: ${lightGray}; - margin-top: 0.25rem; -`; - export const ButtonSubtext = styled.span` display: block; margin-top: 0; diff --git a/gui/src/components/mainInput/Lump/LumpToolbar/BlockSettingsTopToolbar.tsx b/gui/src/components/mainInput/Lump/LumpToolbar/BlockSettingsTopToolbar.tsx index 8fe721155..a9bf99d71 100644 --- a/gui/src/components/mainInput/Lump/LumpToolbar/BlockSettingsTopToolbar.tsx +++ b/gui/src/components/mainInput/Lump/LumpToolbar/BlockSettingsTopToolbar.tsx @@ -99,7 +99,7 @@ function BlockSettingsToolbarIcon( ? "bg-error" : "bg-badge" : undefined - } relative flex select-none items-center rounded-full px-[3px] transition-all duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/50 sm:px-1 ${props.className || ""}`} + } relative flex select-none items-center rounded-full px-[3px] py-0.5 transition-all duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/50 sm:px-1 ${props.className || ""}`} >
-
+
- {isSelected && } - {hovered && ( - - )} + +
@@ -251,7 +251,7 @@ function ModelSelect() { onClick={onClickAddModel} value={"addModel" as any} > -
+
Add Chat model
diff --git a/gui/src/components/modelSelection/ModelSelectionListbox.tsx b/gui/src/components/modelSelection/ModelSelectionListbox.tsx index 3db8ce9c5..aa61e18f5 100644 --- a/gui/src/components/modelSelection/ModelSelectionListbox.tsx +++ b/gui/src/components/modelSelection/ModelSelectionListbox.tsx @@ -22,6 +22,7 @@ import { Transition, } from "../../components/ui"; import { DisplayInfo } from "../../pages/AddNewModel/configs/models"; + export const StyledListbox = styled(Listbox)` background-color: ${vscBackground}; `; @@ -82,6 +83,7 @@ export const StyledListboxOptions = styled(ListboxOptions)` outline: none; } `; + export const StyledListboxOption = styled(ListboxOption)<{ selected: boolean; }>` @@ -93,6 +95,7 @@ export const StyledListboxOption = styled(ListboxOption)<{ display: flex; gap: 8px; align-items: center; + justify-content: space-between; &:hover { background-color: ${vscListActiveBackground}; @@ -128,7 +131,7 @@ function ModelSelectionListbox({ @@ -146,28 +149,27 @@ function ModelSelectionListbox({ {({ selected }) => ( <> - {option.title === "Autodetect" ? ( - - ) : ( - window.vscMediaUrl && - option.icon && ( - - ) - )} - {option.title} - +
+ {option.title === "Autodetect" ? ( + + ) : ( + window.vscMediaUrl && + option.icon && ( + + ) + )} + {option.title} +
{selected && ( - - +
)} diff --git a/gui/src/pages/config/HelpCenterSection.tsx b/gui/src/pages/config/HelpCenterSection.tsx index e49a17780..004e5b78a 100644 --- a/gui/src/pages/config/HelpCenterSection.tsx +++ b/gui/src/pages/config/HelpCenterSection.tsx @@ -81,7 +81,12 @@ export function HelpCenterSection() { generateTitle: true, }), ); - dispatch(setOnboardingCard({ show: true, activeTab: "Best" })); + dispatch( + setOnboardingCard({ + show: true, + activeTab: undefined, + }), + ); ideMessenger.post("showTutorial", undefined); }} /> diff --git a/gui/src/pages/gui/Chat.tsx b/gui/src/pages/gui/Chat.tsx index c3e8ac512..eb6259f07 100644 --- a/gui/src/pages/gui/Chat.tsx +++ b/gui/src/pages/gui/Chat.tsx @@ -54,6 +54,7 @@ import { incrementFreeTrialCount, } from "../../util/freeTrial"; +import { OnboardingModes } from "core/protocol/core"; import { getLocalStorage, setLocalStorage } from "../../util/localStorage"; import { EmptyChatBody } from "./EmptyChatBody"; import { ExploreDialogWatcher } from "./ExploreDialogWatcher"; @@ -207,7 +208,7 @@ export function Chat() { // 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"); + onboardingCard.open(OnboardingModes.API_KEYS); // If history, show the dialog, which will automatically close if there is not history if (history.length) { diff --git a/gui/src/pages/gui/EmptyChatBody.tsx b/gui/src/pages/gui/EmptyChatBody.tsx index ac8ba7631..54185f597 100644 --- a/gui/src/pages/gui/EmptyChatBody.tsx +++ b/gui/src/pages/gui/EmptyChatBody.tsx @@ -1,7 +1,6 @@ import { ConversationStarterCards } from "../../components/ConversationStarters"; import { ExploreHubCard } from "../../components/ExploreHubCard"; import { OnboardingCard } from "../../components/OnboardingCard"; -import { PlatformOnboardingCard } from "../../components/OnboardingCard/platform/PlatformOnboardingCard"; export interface EmptyChatBodyProps { showOnboardingCard?: boolean; @@ -11,12 +10,7 @@ export function EmptyChatBody({ showOnboardingCard }: EmptyChatBodyProps) { if (showOnboardingCard) { return (
- {true ? ( - // For now we are excluding local onboarding options other than ollama - - ) : ( - - )} +
); }