Skip to content

Add "Explore Hub" onboarding card #4596

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Mar 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions core/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1003,6 +1003,10 @@ export class Core {
});

on("didChangeActiveTextEditor", async ({ data: { filepath } }) => {
this.messenger.send("didChangeActiveTextEditor", {
filepath,
});

try {
const ignore = shouldIgnore(filepath, this.ide);
if (!ignore) {
Expand Down
1 change: 1 addition & 0 deletions core/protocol/passThrough.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,5 @@ export const CORE_TO_WEBVIEW_PASS_THROUGH: (keyof ToWebviewFromCoreProtocol)[] =
"getWebviewHistoryLength",
"getCurrentSessionId",
"docs/suggestions",
"didChangeActiveTextEditor",
];
1 change: 1 addition & 0 deletions core/protocol/webview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export type ToWebviewFromIdeOrCoreProtocol = {
},
void,
];
didChangeActiveTextEditor: [{ filepath: string }, void];
isContinueInputFocused: [undefined, boolean];
addContextItem: [
{
Expand Down
61 changes: 61 additions & 0 deletions gui/src/components/ExploreHubCard/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { ChevronRightIcon } from "@heroicons/react/24/outline";
import { useContext } from "react";
import { Button, ButtonSubtext } from "..";
import { IdeMessengerContext } from "../../context/IdeMessenger";
import { useAppDispatch, useAppSelector } from "../../redux/hooks";
import { setIsExploreDialogOpen } from "../../redux/slices/uiSlice";
import { LocalStorageKey, setLocalStorage } from "../../util/localStorage";
import { ReusableCard } from "../ReusableCard";

export function ExploreHubCard() {
const dispatch = useAppDispatch();
const isOpen = useAppSelector((state) => state.ui.isExploreDialogOpen);
const ideMessenger = useContext(IdeMessengerContext);

if (!isOpen) return null;

return (
<ReusableCard
showCloseButton={true}
onClose={() => {
setLocalStorage(LocalStorageKey.IsExploreDialogOpen, false);
setLocalStorage(LocalStorageKey.HasDismissedExploreDialog, true);
return dispatch(setIsExploreDialogOpen(false));
}}
>
<div className="flex flex-col items-center gap-1 p-4 text-center">
<h2 className="text-2xl font-semibold">Create Your Own Assistant</h2>

<p className="max-w-lg text-base leading-relaxed">
Discover and remix popular assistants, or create your own from scratch
</p>

<Button
className="w-full"
onClick={() => {
ideMessenger.request("controlPlane/openUrl", {
path: "/explore/assistants",
orgSlug: undefined,
});
}}
>
Explore Assistants
</Button>

<ButtonSubtext
onClick={() => {
ideMessenger.request("controlPlane/openUrl", {
path: "/new?type=assistant",
orgSlug: undefined,
});
}}
>
<div className="mt-4 flex cursor-pointer items-center justify-center gap-1">
<span>Or, create your own assistant from scratch</span>
<ChevronRightIcon className="h-3 w-3" />
</div>
</ButtonSubtext>
</div>
</ReusableCard>
);
}
35 changes: 10 additions & 25 deletions gui/src/components/OnboardingCard/OnboardingCard.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,9 @@
import * as Tabs from "./tabs";
import { TabTitle, OnboardingCardTabs } from "./components/OnboardingCardTabs";
import { XMarkIcon } from "@heroicons/react/24/outline";
import styled from "styled-components";
import { CloseButton, defaultBorderRadius, vscInputBackground } from "../";
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 { useAppSelector } from "../../redux/hooks";

const StyledCard = styled.div`
margin: auto;
border-radius: ${defaultBorderRadius};
background-color: ${vscInputBackground};
box-shadow:
0 20px 25px -5px rgb(0 0 0 / 0.1),
0 8px 10px -6px rgb(0 0 0 / 0.1);
`;
import * as Tabs from "./tabs";

export interface OnboardingCardState {
show?: boolean;
Expand Down Expand Up @@ -47,20 +36,16 @@ export function OnboardingCard({ isDialog }: OnboardingCardProps) {
}

return (
<StyledCard
className="xs:py-4 xs:px-4 relative px-2 py-3"
data-testid="onboarding-card"
<ReusableCard
showCloseButton={!isDialog && !!config.models.length}
onClose={() => onboardingCard.close()}
testId="onboarding-card"
>
<OnboardingCardTabs
activeTab={onboardingCard.activeTab || "Best"}
onTabClick={onboardingCard.setActiveTab}
/>
{!isDialog && !!config.models.length && (
<CloseButton onClick={() => onboardingCard.close()}>
<XMarkIcon className="mt-1.5 hidden h-5 w-5 hover:brightness-125 sm:flex" />
</CloseButton>
)}
<div className="content py-4">{renderTabContent()}</div>
</StyledCard>
{renderTabContent()}
</ReusableCard>
);
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,12 @@
import { XMarkIcon } from "@heroicons/react/24/outline";
import { useState } from "react";
import styled from "styled-components";
import { CloseButton, defaultBorderRadius, vscInputBackground } from "../..";
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 { useOnboardingCard } from "../hooks";
import OnboardingLocalTab from "../tabs/OnboardingLocalTab";
import MainTab from "./tabs/main";
import { useAppSelector } from "../../../redux/hooks";

const StyledCard = styled.div`
margin: auto;
border-radius: ${defaultBorderRadius};
background-color: ${vscInputBackground};
box-shadow:
0 20px 25px -5px rgb(0 0 0 / 0.1),
0 8px 10px -6px rgb(0 0 0 / 0.1);
`;

export interface OnboardingCardState {
show?: boolean;
Expand All @@ -31,44 +20,38 @@ interface OnboardingCardProps {
export function PlatformOnboardingCard({ isDialog }: OnboardingCardProps) {
const onboardingCard = useOnboardingCard();
const config = useAppSelector((store) => store.config.config);
const [currentTab, setCurrentTab] = useState<"main" | "local">("main");

if (getLocalStorage("onboardingStatus") === undefined) {
setLocalStorage("onboardingStatus", "Started");
}

const [currentTab, setCurrentTab] = useState<"main" | "local">("main");

return (
<StyledCard className="xs:py-4 xs:px-4 relative px-2 py-3">
{!isDialog && !!config.models.length && (
<CloseButton onClick={() => onboardingCard.close()}>
<XMarkIcon className="mt-1.5 hidden h-5 w-5 hover:brightness-125 sm:flex" />
</CloseButton>
)}
<div className="content py-4">
<div className="flex h-full w-full items-center justify-center">
{currentTab === "main" ? (
<MainTab
onRemainLocal={() => setCurrentTab("local")}
isDialog={isDialog}
/>
) : (
<div className="mt-4 flex flex-col">
<Alert type="info">
By choosing this option, Continue will be configured by a local{" "}
<code>config.yaml</code> file. If you're just looking to use
Ollama and still want to manage your configuration through
Continue, click{" "}
<a href="#" onClick={() => setCurrentTab("main")}>
here
</a>
</Alert>

<OnboardingLocalTab isDialog={isDialog} />
</div>
)}
</div>
<ReusableCard
showCloseButton={!isDialog && !!config.models.length}
onClose={() => onboardingCard.close()}
>
<div className="flex h-full w-full items-center justify-center">
{currentTab === "main" ? (
<MainTab
onRemainLocal={() => setCurrentTab("local")}
isDialog={isDialog}
/>
) : (
<div className="mt-4 flex flex-col">
<Alert type="info">
By choosing this option, Continue will be configured by a local{" "}
<code>config.yaml</code> file. If you're just looking to use
Ollama and still want to manage your configuration through
Continue, click{" "}
<a href="#" onClick={() => setCurrentTab("main")}>
here
</a>
</Alert>
<OnboardingLocalTab isDialog={isDialog} />
</div>
)}
</div>
</StyledCard>
</ReusableCard>
);
}
42 changes: 42 additions & 0 deletions gui/src/components/ReusableCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { XMarkIcon } from "@heroicons/react/24/outline";
import styled from "styled-components";
import { CloseButton, defaultBorderRadius, vscInputBackground } from ".";

const StyledCard = styled.div`
margin: auto;
border-radius: ${defaultBorderRadius};
background-color: ${vscInputBackground};
box-shadow:
0 20px 25px -5px rgb(0 0 0 / 0.1),
0 8px 10px -6px rgb(0 0 0 / 0.1);
`;

interface ReusableCardProps {
children: React.ReactNode;
showCloseButton?: boolean;
onClose?: () => void;
className?: string;
testId?: string;
}

export function ReusableCard({
children,
showCloseButton,
onClose,
className = "",
testId,
}: ReusableCardProps) {
return (
<StyledCard
className={`xs:py-4 xs:px-4 relative px-2 py-3 ${className}`}
data-testid={testId}
>
{showCloseButton && (
<CloseButton onClick={onClose}>
<XMarkIcon className="mt-1.5 hidden h-5 w-5 hover:brightness-125 sm:flex" />
</CloseButton>
)}
<div className="content py-4">{children}</div>
</StyledCard>
);
}
66 changes: 14 additions & 52 deletions gui/src/pages/gui/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
import CodeToEditCard from "../../components/CodeToEditCard";
import FeedbackDialog from "../../components/dialogs/FeedbackDialog";
import FreeTrialOverDialog from "../../components/dialogs/FreeTrialOverDialog";
import { ExploreHubCard } from "../../components/ExploreHubCard";
import { useFindWidget } from "../../components/find/FindWidget";
import TimelineItem from "../../components/gui/TimelineItem";
import ChatIndexingPeeks from "../../components/indexing/ChatIndexingPeeks";
Expand Down Expand Up @@ -76,9 +77,11 @@ import {
import getMultifileEditPrompt from "../../util/getMultifileEditPrompt";
import { getLocalStorage, setLocalStorage } from "../../util/localStorage";
import ConfigErrorIndicator from "./ConfigError";
import { ExploreDialogWatcher } from "./ExploreDialogWatcher";
import { ToolCallDiv } from "./ToolCallDiv";
import { ToolCallButtons } from "./ToolCallDiv/ToolCallButtonsDiv";
import ToolOutput from "./ToolCallDiv/ToolOutput";
import { useAutoScroll } from "./useAutoScroll";

const StopButton = styled.div`
background-color: ${vscBackground};
Expand Down Expand Up @@ -136,58 +139,6 @@ function fallbackRender({ error, resetErrorBoundary }: any) {
);
}

const useAutoScroll = (
ref: React.RefObject<HTMLDivElement>,
history: unknown[],
) => {
const [userHasScrolled, setUserHasScrolled] = useState(false);

useEffect(() => {
if (history.length) {
setUserHasScrolled(false);
}
}, [history.length]);

useEffect(() => {
if (!ref.current || history.length === 0) return;

const handleScroll = () => {
const elem = ref.current;
if (!elem) return;

const isAtBottom =
Math.abs(elem.scrollHeight - elem.scrollTop - elem.clientHeight) < 1;

/**
* We stop auto scrolling if a user manually scrolled up.
* We resume auto scrolling if a user manually scrolled to the bottom.
*/
setUserHasScrolled(!isAtBottom);
};

const resizeObserver = new ResizeObserver(() => {
const elem = ref.current;
if (!elem || userHasScrolled) return;
elem.scrollTop = elem.scrollHeight;
});

ref.current.addEventListener("scroll", handleScroll);

// Observe the container
resizeObserver.observe(ref.current);

// Observe all immediate children
Array.from(ref.current.children).forEach((child) => {
resizeObserver.observe(child);
});

return () => {
resizeObserver.disconnect();
ref.current?.removeEventListener("scroll", handleScroll);
};
}, [ref, history.length, userHasScrolled]);
};

export function Chat() {
const posthog = usePostHog();
const dispatch = useAppDispatch();
Expand Down Expand Up @@ -227,6 +178,9 @@ export function Chat() {
);
const lastSessionId = useAppSelector((state) => state.session.lastSessionId);
const useHub = useAppSelector(selectUseHub);
const hasDismissedExploreDialog = useAppSelector(
(state) => state.ui.hasDismissedExploreDialog,
);

useEffect(() => {
// Cmd + Backspace to delete current step
Expand Down Expand Up @@ -570,6 +524,8 @@ export function Chat() {
/>
)}

{!hasDismissedExploreDialog && <ExploreDialogWatcher />}

{history.length === 0 && (
<>
{onboardingCard.show && (
Expand All @@ -587,6 +543,12 @@ export function Chat() {
<TutorialCard onClose={closeTutorialCard} />
</div>
)}

{!onboardingCard.show && showTutorialCard === false && (
<div className="mx-2 mt-10">
<ExploreHubCard />
</div>
)}
</>
)}
</div>
Expand Down
Loading
Loading