Update setup/settings flow for runtime
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
</p>
|
</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">
|
<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>
|
||||||
</p>
|
</div>
|
||||||
</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>
|
||||||
|
Supports advanced apps that require server-side capabilities.
|
||||||
|
</p>
|
||||||
</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">
|
|
||||||
|
{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>
|
||||||
|
|||||||
@@ -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> => {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user