Update setup/settings flow for runtime

This commit is contained in:
Will Chen
2025-04-12 23:53:38 -07:00
parent 3074634c88
commit 6a4f538879
7 changed files with 498 additions and 154 deletions

View File

@@ -8,9 +8,9 @@ import { RuntimeMode } from "@/lib/schemas";
function formatRuntimeMode(runtimeMode: RuntimeMode | undefined) { function formatRuntimeMode(runtimeMode: RuntimeMode | undefined) {
switch (runtimeMode) { switch (runtimeMode) {
case "web-sandbox": case "web-sandbox":
return "Sandbox"; return "Sandboxed";
case "local-node": case "local-node":
return "Local"; return "Full Access";
default: default:
return runtimeMode; return runtimeMode;
} }
@@ -33,7 +33,7 @@ export const TitleBar = () => {
<div className="pl-24"></div> <div className="pl-24"></div>
<div className="hidden @md:block text-sm font-medium">{displayText}</div> <div className="hidden @md:block text-sm font-medium">{displayText}</div>
<div className="text-sm font-medium pl-4"> <div className="text-sm font-medium pl-4">
{formatRuntimeMode(settings?.runtimeMode)} runtime {formatRuntimeMode(settings?.runtimeMode)} mode
</div> </div>
<div className="flex-1 text-center text-sm font-medium">Dyad</div> <div className="flex-1 text-center text-sm font-medium">Dyad</div>
</div> </div>

View File

@@ -1,21 +1,48 @@
import { useState } from "react"; import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { IpcClient } from "@/ipc/ipc_client"; import { IpcClient } from "@/ipc/ipc_client";
import { useSettings } from "@/hooks/useSettings"; // Assuming useSettings provides a refresh function import { useSettings } from "@/hooks/useSettings"; // Assuming useSettings provides a refresh function
import { RuntimeMode } from "@/lib/schemas"; import { RuntimeMode } from "@/lib/schemas";
import { ExternalLink } from "lucide-react";
interface SetupRuntimeFlowProps { interface SetupRuntimeFlowProps {
onRuntimeSelected: (mode: RuntimeMode) => Promise<void>; hideIntroText?: boolean;
} }
export function SetupRuntimeFlow({ onRuntimeSelected }: SetupRuntimeFlowProps) { export function SetupRuntimeFlow({ hideIntroText }: SetupRuntimeFlowProps) {
const [isLoading, setIsLoading] = useState<RuntimeMode | null>(null); 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) => { const handleSelect = async (mode: RuntimeMode) => {
if (isLoading) return; // Prevent double clicks
setIsLoading(mode); setIsLoading(mode);
try { try {
await onRuntimeSelected(mode); await updateSettings({ runtimeMode: mode });
// No need to setIsLoading(null) as the component will unmount on success // Component likely unmounts on success
} catch (error) { } catch (error) {
console.error("Failed to set runtime mode:", error); console.error("Failed to set runtime mode:", error);
alert( alert(
@@ -27,18 +54,45 @@ export function SetupRuntimeFlow({ onRuntimeSelected }: SetupRuntimeFlowProps) {
} }
}; };
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 ( return (
<div className="flex flex-col items-center justify-center max-w-2xl m-auto p-8"> <div className="flex flex-col items-center justify-center max-w-2xl m-auto p-6">
<h1 className="text-4xl font-bold mb-4 text-center">Welcome to Dyad</h1> {!hideIntroText && (
<p className="text-lg text-gray-600 dark:text-gray-400 mb-10 text-center"> <>
You can start building apps with AI in a moment, but first pick how you <h1 className="text-4xl font-bold mb-2 text-center">
want to run these apps. You can always change your mind later. 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 />
Dont worry you can change this later anytime.
</p> </p>
</>
)}
<div className="w-full space-y-4"> <div className="w-full space-y-4">
<Button <Button
variant="outline" variant="outline"
className="relative bg-(--background-lightest) w-full justify-start p-6 h-auto text-left relative" className="relative bg-(--background-lightest) w-full justify-start p-4 h-auto text-left relative"
onClick={() => handleSelect("web-sandbox")} onClick={() => handleSelect("web-sandbox")}
disabled={!!isLoading} disabled={!!isLoading}
> >
@@ -65,27 +119,29 @@ export function SetupRuntimeFlow({ onRuntimeSelected }: SetupRuntimeFlowProps) {
</svg> </svg>
)} )}
<div> <div>
<p className="font-medium text-base">Sandboxed Runtime</p> <p className="font-medium text-base">Sandboxed Mode</p>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1"> <div className="text-sm text-gray-500 dark:text-gray-400 mt-1">
<div> <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"> <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 Recommended for beginners
</span> </span>
Code will run inside a browser sandbox. <p>Apps run in a protected environment within your browser.</p>
<p>Does not support advanced apps.</p>
</div>
</div> </div>
</p>
</div> </div>
</Button> </Button>
<Button <Button
variant="outline" variant="outline"
className="bg-(--background-lightest) w-full justify-start p-6 h-auto text-left relative" className="bg-(--background-lightest) w-full justify-start p-4 h-auto text-left relative flex flex-col items-start"
onClick={() => handleSelect("local-node")} onClick={handleLocalNodeClick}
disabled={!!isLoading} disabled={isLoading === "web-sandbox" || isLoading === "local-node"}
style={{ height: "auto" }} // Ensure height adjusts
> >
{isLoading === "local-node" && ( {isLoading === "check" || isLoading === "local-node" ? (
<svg <svg
className="animate-spin h-5 w-5 mr-3 absolute right-4 top-1/2 -translate-y-1/2" className="animate-spin h-5 w-5 mr-3 absolute right-4 top-6" // Adjust position
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -104,17 +160,97 @@ export function SetupRuntimeFlow({ onRuntimeSelected }: SetupRuntimeFlowProps) {
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" 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> ></path>
</svg> </svg>
)} ) : null}
<div> <div className="w-full">
<p className="font-medium text-base">Local Node.js Runtime</p> <p className="font-medium text-base">Full Access Mode</p>
<div className="text-sm text-gray-500 dark:text-gray-400 mt-1"> <div className="text-sm text-gray-500 dark:text-gray-400 mt-1 w-full">
<p> <p>
Code will run using Node.js on your computer and have full <span className="absolute top-4 right-2 bg-blue-100 text-blue-800 text-xs font-medium px-2 py-0.5 rounded-full">
system access. Best for power users
</span>
<p>
Apps run directly on your computer with full access to your
system.
</p> </p>
<p className=" mt-2 text-wrap wrap-break-word text-red-600 dark:text-red-400 bg-red-100 dark:bg-red-900 px-2 py-1 rounded-md"> <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 Warning: this will run AI-generated code directly on your
computer, which could put your system at risk. computer, which could put your computer at risk.
</p> </p>
</div> </div>
</div> </div>

View File

@@ -209,7 +209,59 @@ async function executeAppLocalNode({
}); });
} }
function checkCommandExists(command: string): Promise<string | null> {
return new Promise((resolve) => {
let output = "";
const process = spawn(command, ["--version"], {
shell: true,
stdio: ["ignore", "pipe", "pipe"], // ignore stdin, pipe stdout/stderr
});
process.stdout?.on("data", (data) => {
output += data.toString();
});
process.stderr?.on("data", (data) => {
// Log stderr but don't treat it as a failure unless the exit code is non-zero
console.warn(
`Stderr from "${command} --version": ${data.toString().trim()}`
);
});
process.on("error", (error) => {
console.error(`Error executing command "${command}":`, error.message);
resolve(null); // Command execution failed
});
process.on("close", (code) => {
if (code === 0) {
resolve(output.trim()); // Command succeeded, return trimmed output
} else {
console.error(
`Command "${command} --version" failed with code ${code}`
);
resolve(null); // Command failed
}
});
});
}
export function registerAppHandlers() { export function registerAppHandlers() {
ipcMain.handle(
"nodejs-status",
async (): Promise<{
nodeVersion: string | null;
npmVersion: string | null;
}> => {
// Run checks in parallel
const [nodeVersion, npmVersion] = await Promise.all([
checkCommandExists("node"),
checkCommandExists("npm"),
]);
return { nodeVersion, npmVersion };
}
);
ipcMain.handle( ipcMain.handle(
"get-app-sandbox-config", "get-app-sandbox-config",
async (_, { appId }: { appId: number }): Promise<SandboxConfig> => { async (_, { appId }: { appId: number }): Promise<SandboxConfig> => {

View File

@@ -488,4 +488,21 @@ export class IpcClient {
throw error; throw error;
} }
} }
// Check Node.js and npm status
public async getNodejsStatus(): Promise<{
nodeVersion: string | null;
npmVersion: string | null;
}> {
try {
const result = await this.ipcRenderer.invoke("nodejs-status");
return result as {
nodeVersion: string | null;
npmVersion: string | null;
};
} catch (error) {
showError(error);
throw error;
}
}
} }

View File

@@ -20,7 +20,7 @@ export default function HomePage() {
const search = useSearch({ from: "/" }); const search = useSearch({ from: "/" });
const setSelectedAppId = useSetAtom(selectedAppIdAtom); const setSelectedAppId = useSetAtom(selectedAppIdAtom);
const { refreshApps } = useLoadApps(); const { refreshApps } = useLoadApps();
const { settings, isAnyProviderSetup, updateSettings } = useSettings(); const { settings, isAnyProviderSetup } = useSettings();
const setIsPreviewOpen = useSetAtom(isPreviewOpenAtom); const setIsPreviewOpen = useSetAtom(isPreviewOpenAtom);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const { streamMessage } = useStreamChat(); const { streamMessage } = useStreamChat();
@@ -35,10 +35,6 @@ export default function HomePage() {
} }
}, [appId, navigate]); }, [appId, navigate]);
const handleSetRuntimeMode = async (mode: RuntimeMode) => {
await updateSettings({ runtimeMode: mode });
};
const handleSubmit = async () => { const handleSubmit = async () => {
if (!inputValue.trim()) return; if (!inputValue.trim()) return;
@@ -90,7 +86,7 @@ export default function HomePage() {
// Runtime Setup Flow // Runtime Setup Flow
// Render this only if runtimeMode is not set in settings // Render this only if runtimeMode is not set in settings
if (settings?.runtimeMode === "unset") { if (settings?.runtimeMode === "unset") {
return <SetupRuntimeFlow onRuntimeSelected={handleSetRuntimeMode} />; return <SetupRuntimeFlow />;
} }
// Main Home Page Content (Rendered only if runtimeMode is set) // Main Home Page Content (Rendered only if runtimeMode is set)

View File

@@ -1,4 +1,4 @@
import { useState } from "react"; import { useState, useEffect } from "react";
import { useTheme } from "../contexts/ThemeContext"; import { useTheme } from "../contexts/ThemeContext";
import { ProviderSettingsGrid } from "@/components/ProviderSettings"; import { ProviderSettingsGrid } from "@/components/ProviderSettings";
import ConfirmationDialog from "@/components/ConfirmationDialog"; import ConfirmationDialog from "@/components/ConfirmationDialog";
@@ -6,96 +6,8 @@ import { IpcClient } from "@/ipc/ipc_client";
import { showSuccess, showError } from "@/lib/toast"; import { showSuccess, showError } from "@/lib/toast";
import { useSettings } from "@/hooks/useSettings"; import { useSettings } from "@/hooks/useSettings";
import { RuntimeMode } from "@/lib/schemas"; import { RuntimeMode } from "@/lib/schemas";
import { Button } from "@/components/ui/button";
// Helper component for runtime option buttons import { ExternalLink } from "lucide-react";
function RuntimeOptionButton({
title,
description,
badge,
warning,
isSelected,
isLoading,
onClick,
disabled,
}: {
title: string;
description: string;
badge?: string;
warning?: string;
isSelected: boolean;
isLoading: boolean;
onClick: () => void;
disabled: boolean;
}) {
return (
<button
onClick={onClick}
disabled={disabled || isSelected}
className={`w-full justify-start p-6 h-auto text-left relative rounded-lg border transition-colors duration-150 group
${
isSelected
? "border-blue-500 dark:border-blue-400 bg-blue-50 dark:bg-gray-700 ring-2 ring-blue-300 dark:ring-blue-600"
: "bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/50"
}
${
disabled && !isSelected
? "opacity-50 cursor-not-allowed"
: "cursor-pointer"
}
`}
>
{isLoading && (
<div className="absolute right-4 top-1/2 -translate-y-1/2">
{/* Inline SVG Spinner */}
<svg
className="animate-spin h-5 w-5 text-gray-500 dark:text-gray-400"
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>
)}
{badge && (
<span className="absolute top-2 right-2 bg-blue-100 text-blue-800 text-xs font-medium px-2 py-0.5 rounded-full group-hover:bg-blue-200 dark:group-hover:bg-blue-900">
{badge}
</span>
)}
<div>
<p
className={`font-medium text-base ${
isSelected
? "text-blue-800 dark:text-blue-100"
: "text-gray-900 dark:text-white"
}`}
>
{title}
</p>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
{description}
</p>
{warning && (
<p className="mt-2 text-wrap break-word text-red-600 dark:text-red-400 bg-red-100 dark:bg-red-900 px-2 py-1 rounded-md text-xs">
{warning}
</p>
)}
</div>
</button>
);
}
export default function SettingsPage() { export default function SettingsPage() {
const { theme, setTheme } = useTheme(); const { theme, setTheme } = useTheme();
@@ -107,7 +19,28 @@ export default function SettingsPage() {
} = useSettings(); } = useSettings();
const [isResetDialogOpen, setIsResetDialogOpen] = useState(false); const [isResetDialogOpen, setIsResetDialogOpen] = useState(false);
const [isResetting, setIsResetting] = useState(false); const [isResetting, setIsResetting] = useState(false);
const [isUpdatingRuntime, setIsUpdatingRuntime] = useState(false); const [isUpdatingRuntime, setIsUpdatingRuntime] = 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);
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);
setNodeVersion(null);
setNpmVersion(null);
}
};
checkNode();
}, []);
const handleResetEverything = async () => { const handleResetEverything = async () => {
setIsResetting(true); setIsResetting(true);
@@ -132,7 +65,10 @@ export default function SettingsPage() {
const handleRuntimeChange = async (newMode: RuntimeMode) => { const handleRuntimeChange = async (newMode: RuntimeMode) => {
if (newMode === settings?.runtimeMode || isUpdatingRuntime) return; if (newMode === settings?.runtimeMode || isUpdatingRuntime) return;
setIsUpdatingRuntime(true); setIsUpdatingRuntime(newMode);
setShowNodeInstallPrompt(false);
setDownloadClicked(false);
try { try {
await updateSettings({ runtimeMode: newMode }); await updateSettings({ runtimeMode: newMode });
showSuccess("Runtime mode updated successfully."); showSuccess("Runtime mode updated successfully.");
@@ -144,7 +80,34 @@ export default function SettingsPage() {
: "Failed to update runtime mode." : "Failed to update runtime mode."
); );
} finally { } finally {
setIsUpdatingRuntime(false); setIsUpdatingRuntime(null);
}
};
const handleLocalNodeClick = async () => {
if (isUpdatingRuntime) return;
if (nodeVersion && npmVersion) {
handleRuntimeChange("local-node");
return;
}
setIsUpdatingRuntime("check");
try {
const status = await IpcClient.getInstance().getNodejsStatus();
setNodeVersion(status.nodeVersion);
setNpmVersion(status.npmVersion);
if (status.nodeVersion && status.npmVersion) {
handleRuntimeChange("local-node");
} else {
setShowNodeInstallPrompt(true);
setIsUpdatingRuntime(null);
}
} catch (error) {
console.error("Failed to check Node.js status on click:", error);
setShowNodeInstallPrompt(true);
setIsUpdatingRuntime(null);
showError("Could not verify Node.js installation.");
} }
}; };
@@ -201,8 +164,8 @@ export default function SettingsPage() {
Runtime Environment Runtime Environment
</h2> </h2>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4"> <p className="text-sm text-gray-500 dark:text-gray-400 mb-4">
Choose how app code is executed. This affects performance and Choose how app code is executed. This affects performance,
security. security, and capabilities.
</p> </p>
{settingsLoading ? ( {settingsLoading ? (
@@ -235,28 +198,207 @@ export default function SettingsPage() {
</p> </p>
) : ( ) : (
<div className="space-y-4"> <div className="space-y-4">
<RuntimeOptionButton <Button
title="Sandboxed Runtime" variant={
description="Code runs inside a browser sandbox. Safer, limited system access." currentRuntimeMode === "web-sandbox" ? "default" : "outline"
badge="Recommended for beginners" }
isSelected={currentRuntimeMode === "web-sandbox"} className={`disabled:opacity-90 w-full justify-start p-4 h-auto text-left relative group ${
isLoading={ currentRuntimeMode === "web-sandbox"
? "border-blue-500 dark:border-blue-400 bg-blue-50 dark:bg-gray-700 ring-2 ring-blue-300 dark:ring-blue-600"
: "bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/50"
} ${
isUpdatingRuntime && currentRuntimeMode !== "web-sandbox" isUpdatingRuntime && currentRuntimeMode !== "web-sandbox"
} ? "opacity-50 cursor-not-allowed"
: "cursor-pointer"
}`}
onClick={() => handleRuntimeChange("web-sandbox")} onClick={() => handleRuntimeChange("web-sandbox")}
disabled={isUpdatingRuntime} disabled={
/> isUpdatingRuntime !== null ||
<RuntimeOptionButton currentRuntimeMode === "web-sandbox"
title="Local Node.js Runtime"
description="Code runs using Node.js on your computer. Full system access."
warning="Warning: Running AI-generated code directly on your computer can be risky. Only use code from trusted sources."
isSelected={currentRuntimeMode === "local-node"}
isLoading={
isUpdatingRuntime && currentRuntimeMode !== "local-node"
} }
onClick={() => handleRuntimeChange("local-node")} >
disabled={isUpdatingRuntime} {isUpdatingRuntime === "web-sandbox" && (
/> <svg
className="animate-spin h-5 w-5 absolute right-4 top-1/2 -translate-y-1/2 text-gray-500 dark:text-gray-400"
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>
)}
<span className="absolute top-2 right-2 bg-blue-100 text-blue-800 text-xs font-medium px-2 py-0.5 rounded-full group-hover:bg-blue-200 dark:group-hover:bg-blue-900">
Recommended for beginners
</span>
<div>
<p
className={`font-medium text-base ${
currentRuntimeMode === "web-sandbox"
? "text-blue-800 dark:text-blue-100"
: "text-gray-900 dark:text-white"
}`}
>
Sandboxed Mode
</p>
<div className="text-sm text-gray-500 dark:text-gray-400 mt-1">
<p>
Apps run in a protected environment within your browser.
</p>
<p>Does not support advanced apps.</p>
</div>
</div>
</Button>
<Button
variant={
currentRuntimeMode === "local-node" ? "default" : "outline"
}
className={`disabled:opacity-90 w-full justify-start p-4 h-auto text-left relative group flex flex-col items-start ${
currentRuntimeMode === "local-node"
? "border-blue-500 dark:border-blue-400 bg-blue-50 dark:bg-gray-700 ring-2 ring-blue-300 dark:ring-blue-600"
: "bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/50"
} ${
isUpdatingRuntime && currentRuntimeMode !== "local-node"
? "opacity-50 cursor-not-allowed"
: "cursor-pointer"
}`}
onClick={handleLocalNodeClick}
disabled={
isUpdatingRuntime !== null ||
currentRuntimeMode === "local-node"
}
>
{(isUpdatingRuntime === "check" ||
isUpdatingRuntime === "local-node") && (
<svg
className="animate-spin h-5 w-5 absolute right-4 top-6 text-gray-500 dark:text-gray-400"
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>
)}
<span className="absolute top-2 right-2 bg-blue-100 text-blue-800 text-xs font-medium px-2 py-0.5 rounded-full group-hover:bg-blue-200 dark:group-hover:bg-blue-900">
Best for power users
</span>
<div className="w-full">
<p
className={`font-medium text-base ${
currentRuntimeMode === "local-node"
? "text-blue-800 dark:text-blue-100"
: "text-gray-900 dark:text-white"
}`}
>
Full Access Mode
</p>
<div className="text-sm text-gray-500 dark:text-gray-400 mt-1 w-full">
<p>
Apps run directly on your computer with full access to
your system.
</p>
<p>
Supports advanced apps that require server-side
capabilities.
</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 text-xs">
This mode requires Node.js and npm to be installed
and accessible in your system's PATH.
</p>
{!downloadClicked ? (
<Button
variant="link"
size="sm"
className="p-0 h-auto mt-2 text-blue-600 dark:text-blue-400 hover:underline flex items-center"
onClick={(e) => {
e.stopPropagation();
IpcClient.getInstance().openExternalUrl(
"https://nodejs.org/en/download"
);
setDownloadClicked(true);
}}
>
Download Node.js{" "}
<ExternalLink className="w-3 h-3 ml-1" />
</Button>
) : (
<p className="mt-2 text-xs text-gray-600 dark:text-gray-400">
Node.js download page opened.
</p>
)}
<Button
variant="default"
size="sm"
className="mt-3 w-full"
onClick={(e) => {
e.stopPropagation();
handleRuntimeChange("local-node");
}}
disabled={isUpdatingRuntime === "local-node"}
>
{isUpdatingRuntime === "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 have 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 text-xs">
Warning: This mode runs AI-generated code directly on
your computer, which can be risky. Only use code from
trusted sources or review it carefully.
</p>
</div>
</div>
</Button>
</div> </div>
)} )}
</div> </div>

View File

@@ -31,6 +31,7 @@ const validInvokeChannels = [
"get-env-vars", "get-env-vars",
"open-external-url", "open-external-url",
"reset-all", "reset-all",
"nodejs-status",
] as const; ] as const;
// Add valid receive channels // Add valid receive channels