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:
@@ -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",
|
||||||
|
);
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
49
src/hooks/useScrollAndNavigateTo.ts
Normal file
49
src/hooks/useScrollAndNavigateTo.ts
Normal 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,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user