Remove runtime mode selection & have unified setup flow for node.js + API access
This commit is contained in:
@@ -1,39 +1,229 @@
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import { ChevronRight, GiftIcon, Sparkles } from "lucide-react";
|
||||
import {
|
||||
ChevronRight,
|
||||
GiftIcon,
|
||||
Sparkles,
|
||||
CheckCircle,
|
||||
AlertCircle,
|
||||
XCircle,
|
||||
Loader2,
|
||||
} from "lucide-react";
|
||||
import { providerSettingsRoute } from "@/routes/settings/providers/$provider";
|
||||
import { useSettings } from "@/hooks/useSettings";
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { IpcClient } from "@/ipc/ipc_client";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { showError } from "@/lib/toast";
|
||||
|
||||
export function SetupBanner() {
|
||||
const navigate = useNavigate();
|
||||
const { isAnyProviderSetup } = useSettings();
|
||||
const [nodeVersion, setNodeVersion] = useState<string | null>(null);
|
||||
const [nodeCheckError, setNodeCheckError] = useState<boolean>(false);
|
||||
const [nodeInstallError, setNodeInstallError] = useState<string | null>(null);
|
||||
const [nodeInstallLoading, setNodeInstallLoading] = useState<boolean>(false);
|
||||
const checkNode = useCallback(async () => {
|
||||
try {
|
||||
setNodeCheckError(false);
|
||||
const status = await IpcClient.getInstance().getNodejsStatus();
|
||||
setNodeVersion(status.nodeVersion);
|
||||
} catch (error) {
|
||||
console.error("Failed to check Node.js status:", error);
|
||||
setNodeVersion(null);
|
||||
setNodeCheckError(true);
|
||||
}
|
||||
}, [setNodeVersion, setNodeCheckError]);
|
||||
|
||||
const handleSetupClick = () => {
|
||||
useEffect(() => {
|
||||
checkNode();
|
||||
}, [checkNode]);
|
||||
|
||||
const handleAiSetupClick = () => {
|
||||
navigate({
|
||||
to: providerSettingsRoute.id,
|
||||
params: { provider: "google" },
|
||||
});
|
||||
};
|
||||
|
||||
const handleNodeInstallClick = async () => {
|
||||
setNodeInstallLoading(true);
|
||||
try {
|
||||
const result = await IpcClient.getInstance().installNode();
|
||||
if (!result.success) {
|
||||
showError(result.errorMessage);
|
||||
setNodeInstallError(result.errorMessage || "Unknown error");
|
||||
} else {
|
||||
setNodeVersion(result.version);
|
||||
}
|
||||
} catch (error) {
|
||||
showError("Failed to install Node.js. " + (error as Error).message);
|
||||
setNodeInstallError(
|
||||
"Failed to install Node.js. " + (error as Error).message
|
||||
);
|
||||
} finally {
|
||||
setNodeInstallLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const isNodeSetupComplete = !!nodeVersion;
|
||||
const isAiProviderSetup = isAnyProviderSetup();
|
||||
|
||||
const itemsNeedAction: string[] = [];
|
||||
if (!isNodeSetupComplete) itemsNeedAction.push("node-setup");
|
||||
if (isNodeSetupComplete && !isAiProviderSetup)
|
||||
itemsNeedAction.push("ai-setup");
|
||||
|
||||
if (itemsNeedAction.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const bannerClasses = cn(
|
||||
"w-full mb-8 border rounded-xl shadow-sm overflow-hidden",
|
||||
"border-zinc-200 dark:border-zinc-700"
|
||||
);
|
||||
|
||||
const getStatusIcon = (isComplete: boolean, hasError: boolean = false) => {
|
||||
if (hasError) {
|
||||
return <XCircle className="w-5 h-5 text-red-500" />;
|
||||
}
|
||||
return isComplete ? (
|
||||
<CheckCircle className="w-5 h-5 text-green-600 dark:text-green-500" />
|
||||
) : (
|
||||
<AlertCircle className="w-5 h-5 text-yellow-600 dark:text-yellow-500" />
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full mb-8 p-4 bg-blue-50 dark:bg-blue-900/30 border border-blue-200 dark:border-blue-700 rounded-xl shadow-sm cursor-pointer hover:bg-blue-100 dark:hover:bg-blue-900/40 transition-colors"
|
||||
onClick={handleSetupClick}
|
||||
>
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="bg-blue-100 dark:bg-blue-800 p-2 rounded-full">
|
||||
<Sparkles className="w-5 h-5 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium text-blue-800 dark:text-blue-300">
|
||||
Setup your AI API access
|
||||
</h3>
|
||||
<p className="text-sm text-blue-600 dark:text-blue-400 flex items-center gap-1">
|
||||
<GiftIcon className="w-3.5 h-3.5" />
|
||||
Use Google Gemini for free
|
||||
<div className={bannerClasses}>
|
||||
<Accordion
|
||||
type="multiple"
|
||||
className="w-full"
|
||||
defaultValue={itemsNeedAction}
|
||||
>
|
||||
<AccordionItem
|
||||
value="node-setup"
|
||||
className={cn(
|
||||
nodeCheckError
|
||||
? "bg-red-50 dark:bg-red-900/30"
|
||||
: isNodeSetupComplete
|
||||
? "bg-green-50 dark:bg-green-900/30"
|
||||
: "bg-yellow-50 dark:bg-yellow-900/30"
|
||||
)}
|
||||
>
|
||||
<AccordionTrigger className="px-4 py-3 transition-colors w-full hover:no-underline">
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="flex items-center gap-3">
|
||||
{getStatusIcon(isNodeSetupComplete, nodeCheckError)}
|
||||
<span className="font-medium text-sm">
|
||||
1. Check Node.js Runtime
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-4 pt-2 pb-4 bg-white dark:bg-zinc-900 border-t border-inherit">
|
||||
{nodeInstallError && (
|
||||
<p className="text-sm text-red-600 dark:text-red-400">
|
||||
{nodeInstallError}
|
||||
</p>
|
||||
)}
|
||||
{nodeCheckError ? (
|
||||
<p className="text-sm text-red-600 dark:text-red-400">
|
||||
Error checking Node.js status. Please ensure node.js are
|
||||
installed correctly and accessible in your system's PATH.
|
||||
</p>
|
||||
) : isNodeSetupComplete ? (
|
||||
<p className="text-sm">Node.js ({nodeVersion}) installed.</p>
|
||||
) : (
|
||||
<div>
|
||||
<p className="text-sm mb-3">
|
||||
Node.js is required to run apps locally.
|
||||
</p>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleNodeInstallClick}
|
||||
disabled={nodeInstallLoading}
|
||||
>
|
||||
{nodeInstallLoading ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Installing...
|
||||
</>
|
||||
) : (
|
||||
"Install Node.js Runtime"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem
|
||||
value="ai-setup"
|
||||
disabled={!isNodeSetupComplete}
|
||||
className={cn(
|
||||
isAiProviderSetup
|
||||
? "bg-green-50 dark:bg-green-900/30"
|
||||
: "bg-yellow-50 dark:bg-yellow-900/30",
|
||||
!isNodeSetupComplete ? "opacity-60" : ""
|
||||
)}
|
||||
>
|
||||
<AccordionTrigger
|
||||
className={cn(
|
||||
"px-4 py-3 transition-colors w-full hover:no-underline",
|
||||
!isNodeSetupComplete ? "cursor-not-allowed" : ""
|
||||
)}
|
||||
onClick={(e) => !isNodeSetupComplete && e.preventDefault()}
|
||||
>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="flex items-center gap-3">
|
||||
{getStatusIcon(isAiProviderSetup)}
|
||||
<span className="font-medium text-sm">
|
||||
2. Setup AI Model Access
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-4 pt-2 pb-4 bg-white dark:bg-zinc-900 border-t border-inherit">
|
||||
<p className="text-sm mb-3">
|
||||
Connect your preferred AI provider to start generating code.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRight className="w-5 h-5 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<div
|
||||
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"
|
||||
onClick={handleAiSetupClick}
|
||||
role="button"
|
||||
tabIndex={isNodeSetupComplete ? 0 : -1}
|
||||
onKeyDown={(e) =>
|
||||
isNodeSetupComplete && e.key === "Enter" && handleAiSetupClick()
|
||||
}
|
||||
>
|
||||
<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" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-sm text-blue-800 dark:text-blue-300">
|
||||
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" />
|
||||
Use Google Gemini for free
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRight className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,261 +0,0 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { IpcClient } from "@/ipc/ipc_client";
|
||||
import { useSettings } from "@/hooks/useSettings"; // Assuming useSettings provides a refresh function
|
||||
import { RuntimeMode } from "@/lib/schemas";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
|
||||
interface SetupRuntimeFlowProps {
|
||||
hideIntroText?: boolean;
|
||||
}
|
||||
|
||||
export function SetupRuntimeFlow({ hideIntroText }: SetupRuntimeFlowProps) {
|
||||
const [isLoading, setIsLoading] = useState<RuntimeMode | "check" | null>(
|
||||
null
|
||||
);
|
||||
const [showNodeInstallPrompt, setShowNodeInstallPrompt] = useState(false);
|
||||
const [nodeVersion, setNodeVersion] = useState<string | null>(null);
|
||||
const [npmVersion, setNpmVersion] = useState<string | null>(null);
|
||||
const [downloadClicked, setDownloadClicked] = useState(false);
|
||||
const { updateSettings } = useSettings();
|
||||
|
||||
// Pre-check Node.js status on component mount (optional but good UX)
|
||||
useEffect(() => {
|
||||
const checkNode = async () => {
|
||||
try {
|
||||
const status = await IpcClient.getInstance().getNodejsStatus();
|
||||
setNodeVersion(status.nodeVersion);
|
||||
setNpmVersion(status.npmVersion);
|
||||
} catch (error) {
|
||||
console.error("Failed to check Node.js status:", error);
|
||||
// Assume not installed if check fails
|
||||
setNodeVersion(null);
|
||||
setNpmVersion(null);
|
||||
}
|
||||
};
|
||||
checkNode();
|
||||
}, []);
|
||||
|
||||
const handleSelect = async (mode: RuntimeMode) => {
|
||||
if (isLoading) return; // Prevent double clicks
|
||||
|
||||
setIsLoading(mode);
|
||||
try {
|
||||
await updateSettings({ runtimeMode: mode });
|
||||
// Component likely unmounts on success
|
||||
} catch (error) {
|
||||
console.error("Failed to set runtime mode:", error);
|
||||
alert(
|
||||
`Error setting runtime mode: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`
|
||||
);
|
||||
setIsLoading(null); // Reset loading state on error
|
||||
}
|
||||
};
|
||||
|
||||
const handleLocalNodeClick = async () => {
|
||||
if (isLoading) return;
|
||||
|
||||
setIsLoading("check");
|
||||
try {
|
||||
if (nodeVersion && npmVersion) {
|
||||
// Node and npm found, proceed directly
|
||||
handleSelect("local-node");
|
||||
} else {
|
||||
// Node or npm not found, show prompt
|
||||
setShowNodeInstallPrompt(true);
|
||||
setIsLoading(null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to check Node.js status on click:", error);
|
||||
// Show prompt if check fails
|
||||
setShowNodeInstallPrompt(true);
|
||||
setIsLoading(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center max-w-2xl m-auto p-6">
|
||||
{!hideIntroText && (
|
||||
<>
|
||||
<h1 className="text-4xl font-bold mb-2 text-center">
|
||||
Welcome to Dyad
|
||||
</h1>
|
||||
<p className="text-lg text-gray-600 dark:text-gray-400 mb-6 text-center">
|
||||
Before you start building, choose how your apps will run. <br />
|
||||
Don’t worry — you can change this later anytime.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="w-full space-y-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="relative bg-(--background-lightest) w-full justify-start p-4 h-auto text-left relative"
|
||||
onClick={() => handleSelect("web-sandbox")}
|
||||
disabled={!!isLoading}
|
||||
>
|
||||
{isLoading === "web-sandbox" && (
|
||||
<svg
|
||||
className="animate-spin h-5 w-5 mr-3 absolute right-4 top-1/2 -translate-y-1/2"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
></circle>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
)}
|
||||
<div>
|
||||
<p className="font-medium text-base">Sandboxed Mode</p>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
<div>
|
||||
<span className="absolute top-4 right-2 bg-blue-100 text-blue-800 text-xs font-medium px-2 py-0.5 rounded-full">
|
||||
Recommended for beginners
|
||||
</span>
|
||||
<p>Apps run in a protected environment within your browser.</p>
|
||||
<p>Does not support advanced apps.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
className="bg-(--background-lightest) w-full justify-start p-4 h-auto text-left relative flex flex-col items-start"
|
||||
onClick={handleLocalNodeClick}
|
||||
disabled={isLoading === "web-sandbox" || isLoading === "local-node"}
|
||||
style={{ height: "auto" }} // Ensure height adjusts
|
||||
>
|
||||
{isLoading === "check" || isLoading === "local-node" ? (
|
||||
<svg
|
||||
className="animate-spin h-5 w-5 mr-3 absolute right-4 top-6" // Adjust position
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
></circle>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
) : null}
|
||||
<div className="w-full">
|
||||
<p className="font-medium text-base">Full Access Mode</p>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400 mt-1 w-full">
|
||||
<p>
|
||||
<span className="absolute top-4 right-2 bg-blue-100 text-blue-800 text-xs font-medium px-2 py-0.5 rounded-full">
|
||||
Best for power users
|
||||
</span>
|
||||
<p>
|
||||
Apps run directly on your computer with full access to your
|
||||
system.
|
||||
</p>
|
||||
<p>
|
||||
Supports advanced apps that require server-side capabilities.
|
||||
</p>
|
||||
</p>
|
||||
|
||||
{showNodeInstallPrompt && (
|
||||
<div className="mt-4 p-4 border border-yellow-300 bg-yellow-50 dark:bg-yellow-900/30 rounded-md text-yellow-800 dark:text-yellow-200 w-full">
|
||||
<p className="font-semibold">Install Node.js</p>
|
||||
<p className="mt-1">
|
||||
This mode requires Node.js to be installed.
|
||||
</p>
|
||||
{downloadClicked ? (
|
||||
<div
|
||||
className="text-blue-400 cursor-pointer flex items-center"
|
||||
onClick={() => {
|
||||
IpcClient.getInstance().openExternalUrl(
|
||||
"https://nodejs.org/en/download#:~:text=Or%20get%20a%20prebuilt%20Node.js"
|
||||
);
|
||||
setDownloadClicked(true);
|
||||
}}
|
||||
>
|
||||
Download Node.js
|
||||
<ExternalLink className="w-3 h-3 ml-1" />
|
||||
</div>
|
||||
) : null}
|
||||
{!downloadClicked ? (
|
||||
<Button
|
||||
size="sm"
|
||||
className=" mt-3 w-full inline-flex items-center justify-center"
|
||||
onClick={() => {
|
||||
IpcClient.getInstance().openExternalUrl(
|
||||
"https://nodejs.org/en/download#:~:text=Or%20get%20a%20prebuilt%20Node.js"
|
||||
);
|
||||
setDownloadClicked(true);
|
||||
}}
|
||||
>
|
||||
Download Node.js <ExternalLink className="w-3 h-3 ml-1" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
size="sm"
|
||||
className="mt-3 w-full"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation(); // Prevent outer button click
|
||||
handleSelect("local-node"); // Proceed with selection
|
||||
}}
|
||||
disabled={isLoading === "local-node"} // Disable while processing selection
|
||||
>
|
||||
{isLoading === "local-node" ? (
|
||||
<svg
|
||||
className="animate-spin h-4 w-4 mr-2"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
></circle>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
) : null}
|
||||
Continue - I installed Node.js
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="mt-4 text-wrap break-words text-red-600 dark:text-red-400 bg-red-100 dark:bg-red-900/50 px-2 py-1 rounded-md">
|
||||
Warning: this will run AI-generated code directly on your
|
||||
computer, which could put your computer at risk.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user