Add OpenRouter to setup banner (#1242)
<!-- This is an auto-generated description by cubic. --> ## Summary by cubic Added OpenRouter as a first-class option in the setup banner and introduced a reusable provider card component. This streamlines provider selection and adds E2E coverage for the setup flow. - **New Features** - Added SetupProviderCard and used it for Google and OpenRouter in SetupBanner. - Clicking Google or OpenRouter routes to the correct provider settings and logs PostHog events. - Kept “Other providers” link to Settings. - Added setup.spec.ts E2E test to verify Google, OpenRouter, and Other navigation; introduced test config flag showSetupScreen to control the OPENAI_API_KEY shortcut. <!-- End of auto-generated description by cubic. -->
This commit is contained in:
@@ -1002,6 +1002,7 @@ export class PageObject {
|
|||||||
|
|
||||||
interface ElectronConfig {
|
interface ElectronConfig {
|
||||||
preLaunchHook?: ({ userDataDir }: { userDataDir: string }) => Promise<void>;
|
preLaunchHook?: ({ userDataDir }: { userDataDir: string }) => Promise<void>;
|
||||||
|
showSetupScreen?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// From https://github.com/microsoft/playwright/issues/8208#issuecomment-1435475930
|
// From https://github.com/microsoft/playwright/issues/8208#issuecomment-1435475930
|
||||||
@@ -1064,8 +1065,10 @@ export const test = base.extend<{
|
|||||||
process.env.DYAD_ENGINE_URL = "http://localhost:3500/engine/v1";
|
process.env.DYAD_ENGINE_URL = "http://localhost:3500/engine/v1";
|
||||||
process.env.DYAD_GATEWAY_URL = "http://localhost:3500/gateway/v1";
|
process.env.DYAD_GATEWAY_URL = "http://localhost:3500/gateway/v1";
|
||||||
process.env.E2E_TEST_BUILD = "true";
|
process.env.E2E_TEST_BUILD = "true";
|
||||||
|
if (!electronConfig.showSetupScreen) {
|
||||||
// This is just a hack to avoid the AI setup screen.
|
// This is just a hack to avoid the AI setup screen.
|
||||||
process.env.OPENAI_API_KEY = "sk-test";
|
process.env.OPENAI_API_KEY = "sk-test";
|
||||||
|
}
|
||||||
const baseTmpDir = os.tmpdir();
|
const baseTmpDir = os.tmpdir();
|
||||||
const userDataDir = path.join(baseTmpDir, `dyad-e2e-tests-${Date.now()}`);
|
const userDataDir = path.join(baseTmpDir, `dyad-e2e-tests-${Date.now()}`);
|
||||||
if (electronConfig.preLaunchHook) {
|
if (electronConfig.preLaunchHook) {
|
||||||
|
|||||||
31
e2e-tests/setup.spec.ts
Normal file
31
e2e-tests/setup.spec.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { testWithConfig } from "./helpers/test_helper";
|
||||||
|
import { expect } from "@playwright/test";
|
||||||
|
|
||||||
|
const testSetup = testWithConfig({
|
||||||
|
showSetupScreen: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
testSetup("setup ai provider", async ({ po }) => {
|
||||||
|
await po.page
|
||||||
|
.getByRole("button", { name: "Setup Google Gemini API Key" })
|
||||||
|
.click();
|
||||||
|
await expect(
|
||||||
|
po.page.getByRole("heading", { name: "Configure Google" }),
|
||||||
|
).toBeVisible();
|
||||||
|
expect(po.page.url()).toEqual("file:///providers/google");
|
||||||
|
|
||||||
|
await po.page.getByRole("button", { name: "Go Back" }).click();
|
||||||
|
await po.page
|
||||||
|
.getByRole("button", { name: "Setup OpenRouter API Key Free" })
|
||||||
|
.click();
|
||||||
|
await expect(
|
||||||
|
po.page.getByRole("heading", { name: "Configure OpenRouter" }),
|
||||||
|
).toBeVisible();
|
||||||
|
expect(po.page.url()).toEqual("file:///providers/openrouter");
|
||||||
|
|
||||||
|
await po.page.getByRole("button", { name: "Go Back" }).click();
|
||||||
|
await po.page
|
||||||
|
.getByRole("button", { name: "Setup other AI providers" })
|
||||||
|
.click();
|
||||||
|
expect(po.page.url()).toEqual("file:///settings");
|
||||||
|
});
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
} 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 { settingsRoute } from "@/routes/settings";
|
||||||
|
import SetupProviderCard from "@/components/SetupProviderCard";
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
import { IpcClient } from "@/ipc/ipc_client";
|
import { IpcClient } from "@/ipc/ipc_client";
|
||||||
@@ -58,7 +59,7 @@ export function SetupBanner() {
|
|||||||
checkNode();
|
checkNode();
|
||||||
}, [checkNode]);
|
}, [checkNode]);
|
||||||
|
|
||||||
const handleAiSetupClick = () => {
|
const handleGoogleSetupClick = () => {
|
||||||
posthog.capture("setup-flow:ai-provider-setup:google:click");
|
posthog.capture("setup-flow:ai-provider-setup:google:click");
|
||||||
navigate({
|
navigate({
|
||||||
to: providerSettingsRoute.id,
|
to: providerSettingsRoute.id,
|
||||||
@@ -66,6 +67,14 @@ export function SetupBanner() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOpenRouterSetupClick = () => {
|
||||||
|
posthog.capture("setup-flow:ai-provider-setup:openrouter:click");
|
||||||
|
navigate({
|
||||||
|
to: providerSettingsRoute.id,
|
||||||
|
params: { provider: "openrouter" },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleOtherProvidersClick = () => {
|
const handleOtherProvidersClick = () => {
|
||||||
posthog.capture("setup-flow:ai-provider-setup:other:click");
|
posthog.capture("setup-flow:ai-provider-setup:other:click");
|
||||||
navigate({
|
navigate({
|
||||||
@@ -226,30 +235,38 @@ export function SetupBanner() {
|
|||||||
<p className="text-sm mb-3">
|
<p className="text-sm mb-3">
|
||||||
Connect your preferred AI provider to start generating code.
|
Connect your preferred AI provider to start generating code.
|
||||||
</p>
|
</p>
|
||||||
<div
|
<SetupProviderCard
|
||||||
className="p-3 bg-blue-50 dark:bg-blue-900/50 border border-blue-200 dark:border-blue-700 rounded-lg cursor-pointer hover:bg-blue-100 dark:hover:bg-blue-900/70 transition-colors"
|
variant="google"
|
||||||
onClick={handleAiSetupClick}
|
onClick={handleGoogleSetupClick}
|
||||||
role="button"
|
|
||||||
tabIndex={isNodeSetupComplete ? 0 : -1}
|
tabIndex={isNodeSetupComplete ? 0 : -1}
|
||||||
>
|
leadingIcon={
|
||||||
<div className="flex items-center justify-between gap-3">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="bg-blue-100 dark:bg-blue-800 p-1.5 rounded-full">
|
|
||||||
<Sparkles className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
<Sparkles className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||||
</div>
|
}
|
||||||
<div>
|
title="Setup Google Gemini API Key"
|
||||||
<h4 className="font-medium text-sm text-blue-800 dark:text-blue-300">
|
subtitle={
|
||||||
Setup Google Gemini API Key
|
<>
|
||||||
</h4>
|
|
||||||
<p className="text-xs text-blue-600 dark:text-blue-400 flex items-center gap-1">
|
|
||||||
<GiftIcon className="w-3 h-3" />
|
<GiftIcon className="w-3 h-3" />
|
||||||
Use Google Gemini for free
|
Use Google Gemini for free
|
||||||
</p>
|
</>
|
||||||
</div>
|
}
|
||||||
</div>
|
/>
|
||||||
<ChevronRight className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
|
||||||
</div>
|
<SetupProviderCard
|
||||||
</div>
|
className="mt-2"
|
||||||
|
variant="openrouter"
|
||||||
|
onClick={handleOpenRouterSetupClick}
|
||||||
|
tabIndex={isNodeSetupComplete ? 0 : -1}
|
||||||
|
leadingIcon={
|
||||||
|
<Sparkles className="w-4 h-4 text-purple-600 dark:text-purple-400" />
|
||||||
|
}
|
||||||
|
title="Setup OpenRouter API Key"
|
||||||
|
subtitle={
|
||||||
|
<>
|
||||||
|
<GiftIcon className="w-3 h-3" />
|
||||||
|
Free models available
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<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"
|
||||||
|
|||||||
87
src/components/SetupProviderCard.tsx
Normal file
87
src/components/SetupProviderCard.tsx
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { ChevronRight } from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
|
type SetupProviderVariant = "google" | "openrouter";
|
||||||
|
|
||||||
|
export function SetupProviderCard({
|
||||||
|
variant,
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
leadingIcon,
|
||||||
|
onClick,
|
||||||
|
tabIndex = 0,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
variant: SetupProviderVariant;
|
||||||
|
title: string;
|
||||||
|
subtitle?: ReactNode;
|
||||||
|
leadingIcon: ReactNode;
|
||||||
|
onClick: () => void;
|
||||||
|
tabIndex?: number;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const styles = getVariantStyles(variant);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"p-3 border rounded-lg cursor-pointer transition-colors",
|
||||||
|
styles.container,
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
onClick={onClick}
|
||||||
|
role="button"
|
||||||
|
tabIndex={tabIndex}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className={cn("p-1.5 rounded-full", styles.iconWrapper)}>
|
||||||
|
{leadingIcon}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className={cn("font-medium text-sm", styles.titleColor)}>
|
||||||
|
{title}
|
||||||
|
</h4>
|
||||||
|
{subtitle ? (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"text-xs flex items-center gap-1",
|
||||||
|
styles.subtitleColor,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{subtitle}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ChevronRight className={cn("w-4 h-4", styles.chevronColor)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVariantStyles(variant: SetupProviderVariant) {
|
||||||
|
switch (variant) {
|
||||||
|
case "google":
|
||||||
|
return {
|
||||||
|
container:
|
||||||
|
"bg-blue-50 dark:bg-blue-900/50 border-blue-200 dark:border-blue-700 hover:bg-blue-100 dark:hover:bg-blue-900/70",
|
||||||
|
iconWrapper: "bg-blue-100 dark:bg-blue-800",
|
||||||
|
titleColor: "text-blue-800 dark:text-blue-300",
|
||||||
|
subtitleColor: "text-blue-600 dark:text-blue-400",
|
||||||
|
chevronColor: "text-blue-600 dark:text-blue-400",
|
||||||
|
} as const;
|
||||||
|
case "openrouter":
|
||||||
|
return {
|
||||||
|
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",
|
||||||
|
iconWrapper: "bg-purple-100 dark:bg-purple-800",
|
||||||
|
titleColor: "text-purple-800 dark:text-purple-300",
|
||||||
|
subtitleColor: "text-purple-600 dark:text-purple-400",
|
||||||
|
chevronColor: "text-purple-600 dark:text-purple-400",
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SetupProviderCard;
|
||||||
Reference in New Issue
Block a user