Update setup banner: navigate to settings#ai & Dyad Pro option (#1361)

<!-- This is an auto-generated description by cubic. -->

## Summary by cubic
Updated the setup banner to navigate to Settings and auto-scroll to the
AI Providers section, and added a Dyad Pro setup option to streamline
onboarding.

- **New Features**
  - Added “Setup Dyad Pro” card with logo; opens Dyad Pro signup.
- “Other providers” now jumps to Settings → Provider section with smooth
scroll.
  - OpenRouter setup card updated with new teal styling.

- **Refactors**
- Introduced useScrollAndNavigateTo hook to navigate, scroll, and set
active section.
- Centralized active section state via activeSettingsSectionAtom (used
by SettingsList).
  - SetupProviderCard supports a new “dyad” variant styling.

<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Will Chen
2025-09-23 16:06:49 -07:00
committed by GitHub
parent 2597d50529
commit 42a406e3ab
5 changed files with 113 additions and 27 deletions

View File

@@ -4,3 +4,6 @@ export const isPreviewOpenAtom = atom(true);
export const selectedFileAtom = atom<{ export const selectedFileAtom = atom<{
path: string; path: string;
} | null>(null); } | null>(null);
export const activeSettingsSectionAtom = atom<string | null>(
"general-settings",
);

View File

@@ -1,7 +1,9 @@
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useNavigate } from "@tanstack/react-router"; import { useEffect } from "react";
import { useEffect, useState } from "react"; import { useScrollAndNavigateTo } from "@/hooks/useScrollAndNavigateTo";
import { useAtom } from "jotai";
import { activeSettingsSectionAtom } from "@/atoms/viewAtoms";
const SETTINGS_SECTIONS = [ const SETTINGS_SECTIONS = [
{ id: "general-settings", label: "General" }, { id: "general-settings", label: "General" },
@@ -16,10 +18,11 @@ const SETTINGS_SECTIONS = [
]; ];
export function SettingsList({ show }: { show: boolean }) { export function SettingsList({ show }: { show: boolean }) {
const navigate = useNavigate(); const [activeSection, setActiveSection] = useAtom(activeSettingsSectionAtom);
const [activeSection, setActiveSection] = useState<string | null>( const scrollAndNavigateTo = useScrollAndNavigateTo("/settings", {
"general-settings", behavior: "smooth",
); block: "start",
});
useEffect(() => { useEffect(() => {
const observer = new IntersectionObserver( const observer = new IntersectionObserver(
@@ -50,16 +53,7 @@ export function SettingsList({ show }: { show: boolean }) {
return null; return null;
} }
const handleScrollAndNavigateTo = async (id: string) => { const handleScrollAndNavigateTo = scrollAndNavigateTo;
await navigate({
to: "/settings",
});
const element = document.getElementById(id);
if (element) {
element.scrollIntoView({ behavior: "smooth", block: "start" });
setActiveSection(id);
}
};
return ( return (
<div className="flex flex-col h-full"> <div className="flex flex-col h-full">

View File

@@ -8,9 +8,10 @@ import {
XCircle, XCircle,
Loader2, Loader2,
Settings, Settings,
GlobeIcon,
} from "lucide-react"; } from "lucide-react";
import { providerSettingsRoute } from "@/routes/settings/providers/$provider"; import { providerSettingsRoute } from "@/routes/settings/providers/$provider";
import { settingsRoute } from "@/routes/settings";
import SetupProviderCard from "@/components/SetupProviderCard"; import SetupProviderCard from "@/components/SetupProviderCard";
import { useState, useEffect, useCallback } from "react"; import { useState, useEffect, useCallback } from "react";
@@ -26,6 +27,10 @@ import { cn } from "@/lib/utils";
import { NodeSystemInfo } from "@/ipc/ipc_types"; import { NodeSystemInfo } from "@/ipc/ipc_types";
import { usePostHog } from "posthog-js/react"; import { usePostHog } from "posthog-js/react";
import { useLanguageModelProviders } from "@/hooks/useLanguageModelProviders"; import { useLanguageModelProviders } from "@/hooks/useLanguageModelProviders";
import { useScrollAndNavigateTo } from "@/hooks/useScrollAndNavigateTo";
// @ts-ignore
import logo from "../../assets/logo.svg";
type NodeInstallStep = type NodeInstallStep =
| "install" | "install"
| "waiting-for-continue" | "waiting-for-continue"
@@ -59,6 +64,11 @@ export function SetupBanner() {
checkNode(); checkNode();
}, [checkNode]); }, [checkNode]);
const settingsScrollAndNavigateTo = useScrollAndNavigateTo("/settings", {
behavior: "smooth",
block: "start",
});
const handleGoogleSetupClick = () => { const handleGoogleSetupClick = () => {
posthog.capture("setup-flow:ai-provider-setup:google:click"); posthog.capture("setup-flow:ai-provider-setup:google:click");
navigate({ navigate({
@@ -74,12 +84,16 @@ export function SetupBanner() {
params: { provider: "openrouter" }, params: { provider: "openrouter" },
}); });
}; };
const handleDyadProSetupClick = () => {
posthog.capture("setup-flow:ai-provider-setup:dyad:click");
IpcClient.getInstance().openExternalUrl(
"https://www.dyad.sh/pro?utm_source=dyad-app&utm_medium=app&utm_campaign=setup-banner",
);
};
const handleOtherProvidersClick = () => { const handleOtherProvidersClick = () => {
posthog.capture("setup-flow:ai-provider-setup:other:click"); posthog.capture("setup-flow:ai-provider-setup:other:click");
navigate({ settingsScrollAndNavigateTo("provider-settings");
to: settingsRoute.id,
});
}; };
const handleNodeInstallClick = useCallback(async () => { const handleNodeInstallClick = useCallback(async () => {
@@ -255,7 +269,7 @@ export function SetupBanner() {
onClick={handleOpenRouterSetupClick} onClick={handleOpenRouterSetupClick}
tabIndex={isNodeSetupComplete ? 0 : -1} tabIndex={isNodeSetupComplete ? 0 : -1}
leadingIcon={ leadingIcon={
<Sparkles className="w-4 h-4 text-purple-600 dark:text-purple-400" /> <Sparkles className="w-4 h-4 text-teal-600 dark:text-teal-400" />
} }
title="Setup OpenRouter API Key" title="Setup OpenRouter API Key"
subtitle={ subtitle={
@@ -266,6 +280,23 @@ export function SetupBanner() {
} }
/> />
<SetupProviderCard
className="mt-2"
variant="dyad"
onClick={handleDyadProSetupClick}
tabIndex={isNodeSetupComplete ? 0 : -1}
leadingIcon={
<img src={logo} alt="Dyad Logo" className="w-6 h-6 mr-0.5" />
}
title="Setup Dyad Pro"
subtitle={
<>
<GlobeIcon className="w-3 h-3" />
Access all AI models with one plan
</>
}
/>
<div <div
className="mt-2 p-3 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-gray-700 rounded-lg cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800/70 transition-colors" className="mt-2 p-3 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-gray-700 rounded-lg cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800/70 transition-colors"
onClick={handleOtherProvidersClick} onClick={handleOtherProvidersClick}

View File

@@ -2,7 +2,7 @@ import { ChevronRight } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { ReactNode } from "react"; import { ReactNode } from "react";
type SetupProviderVariant = "google" | "openrouter"; type SetupProviderVariant = "google" | "openrouter" | "dyad";
export function SetupProviderCard({ export function SetupProviderCard({
variant, variant,
@@ -75,11 +75,20 @@ function getVariantStyles(variant: SetupProviderVariant) {
case "openrouter": case "openrouter":
return { return {
container: container:
"bg-purple-50 dark:bg-purple-900/50 border-purple-200 dark:border-purple-700 hover:bg-purple-100 dark:hover:bg-purple-900/70", "bg-teal-50 dark:bg-teal-900/50 border-teal-200 dark:border-teal-700 hover:bg-teal-100 dark:hover:bg-teal-900/70",
iconWrapper: "bg-purple-100 dark:bg-purple-800", iconWrapper: "bg-teal-100 dark:bg-teal-800",
titleColor: "text-purple-800 dark:text-purple-300", titleColor: "text-teal-800 dark:text-teal-300",
subtitleColor: "text-purple-600 dark:text-purple-400", subtitleColor: "text-teal-600 dark:text-teal-400",
chevronColor: "text-purple-600 dark:text-purple-400", chevronColor: "text-teal-600 dark:text-teal-400",
} as const;
case "dyad":
return {
container:
"bg-primary/10 border-primary/50 dark:bg-violet-800/50 dark:border-violet-700 hover:bg-violet-100 dark:hover:bg-violet-900/70",
iconWrapper: "bg-primary/5 dark:bg-violet-800",
titleColor: "text-violet-800 dark:text-violet-300",
subtitleColor: "text-violet-600 dark:text-violet-400",
chevronColor: "text-violet-600 dark:text-violet-400",
} as const; } as const;
} }
} }

View File

@@ -0,0 +1,49 @@
import { useCallback } from "react";
import { useNavigate } from "@tanstack/react-router";
import { useSetAtom } from "jotai";
import { activeSettingsSectionAtom } from "@/atoms/viewAtoms";
type ScrollOptions = {
behavior?: ScrollBehavior;
block?: ScrollLogicalPosition;
inline?: ScrollLogicalPosition;
onScrolled?: (id: string, element: HTMLElement) => void;
};
/**
* Returns an async function that navigates to the given route, then scrolls the element with the provided id into view.
*/
export function useScrollAndNavigateTo(
to: string = "/settings",
options?: ScrollOptions,
) {
const navigate = useNavigate();
const setActiveSection = useSetAtom(activeSettingsSectionAtom);
return useCallback(
async (id: string) => {
await navigate({ to });
const element = document.getElementById(id);
if (element) {
element.scrollIntoView({
behavior: options?.behavior ?? "smooth",
block: options?.block ?? "start",
inline: options?.inline,
});
setActiveSection(id);
options?.onScrolled?.(id, element);
return true;
}
return false;
},
[
navigate,
to,
options?.behavior,
options?.block,
options?.inline,
options?.onScrolled,
setActiveSection,
],
);
}