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 { 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 { 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() {
|
export function SetupBanner() {
|
||||||
const navigate = useNavigate();
|
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({
|
navigate({
|
||||||
to: providerSettingsRoute.id,
|
to: providerSettingsRoute.id,
|
||||||
params: { provider: "google" },
|
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 (
|
return (
|
||||||
<div
|
<div className={bannerClasses}>
|
||||||
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"
|
<Accordion
|
||||||
onClick={handleSetupClick}
|
type="multiple"
|
||||||
>
|
className="w-full"
|
||||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
defaultValue={itemsNeedAction}
|
||||||
<div className="flex items-center gap-3">
|
>
|
||||||
<div className="bg-blue-100 dark:bg-blue-800 p-2 rounded-full">
|
<AccordionItem
|
||||||
<Sparkles className="w-5 h-5 text-blue-600 dark:text-blue-400" />
|
value="node-setup"
|
||||||
</div>
|
className={cn(
|
||||||
<div>
|
nodeCheckError
|
||||||
<h3 className="font-medium text-blue-800 dark:text-blue-300">
|
? "bg-red-50 dark:bg-red-900/30"
|
||||||
Setup your AI API access
|
: isNodeSetupComplete
|
||||||
</h3>
|
? "bg-green-50 dark:bg-green-900/30"
|
||||||
<p className="text-sm text-blue-600 dark:text-blue-400 flex items-center gap-1">
|
: "bg-yellow-50 dark:bg-yellow-900/30"
|
||||||
<GiftIcon className="w-3.5 h-3.5" />
|
)}
|
||||||
Use Google Gemini for free
|
>
|
||||||
|
<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>
|
</p>
|
||||||
</div>
|
<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"
|
||||||
<ChevronRight className="w-5 h-5 text-blue-600 dark:text-blue-400" />
|
onClick={handleAiSetupClick}
|
||||||
</div>
|
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>
|
</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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -209,43 +209,6 @@ 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
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper to kill process on a specific port (cross-platform, using kill-port)
|
// Helper to kill process on a specific port (cross-platform, using kill-port)
|
||||||
async function killProcessOnPort(port: number): Promise<void> {
|
async function killProcessOnPort(port: number): Promise<void> {
|
||||||
try {
|
try {
|
||||||
@@ -256,21 +219,6 @@ async function killProcessOnPort(port: number): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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> => {
|
||||||
|
|||||||
136
src/ipc/handlers/node_handlers.ts
Normal file
136
src/ipc/handlers/node_handlers.ts
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import { ipcMain } from "electron";
|
||||||
|
import { spawn } from "child_process";
|
||||||
|
import { platform } from "os";
|
||||||
|
import { InstallNodeResult } from "../ipc_types";
|
||||||
|
type ShellResult =
|
||||||
|
| {
|
||||||
|
success: true;
|
||||||
|
output: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
success: false;
|
||||||
|
errorMessage: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function runShell(command: string): Promise<ShellResult> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let stdout = "";
|
||||||
|
let stderr = "";
|
||||||
|
const process = spawn(command, {
|
||||||
|
shell: true,
|
||||||
|
stdio: ["ignore", "pipe", "pipe"], // ignore stdin, pipe stdout/stderr
|
||||||
|
});
|
||||||
|
|
||||||
|
process.stdout?.on("data", (data) => {
|
||||||
|
stdout += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
process.stderr?.on("data", (data) => {
|
||||||
|
stderr += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("error", (error) => {
|
||||||
|
console.error(`Error executing command "${command}":`, error.message);
|
||||||
|
resolve({ success: false, errorMessage: error.message });
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("close", (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
resolve({ success: true, output: stdout.trim() });
|
||||||
|
} else {
|
||||||
|
const errorMessage =
|
||||||
|
stderr.trim() || `Command failed with code ${code}`;
|
||||||
|
console.error(
|
||||||
|
`Command "${command}" failed with code ${code}: ${stderr.trim()}`
|
||||||
|
);
|
||||||
|
resolve({ success: false, errorMessage });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 registerNodeHandlers() {
|
||||||
|
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("install-node", async (): Promise<InstallNodeResult> => {
|
||||||
|
console.log("Installing Node.js...");
|
||||||
|
if (platform() === "win32") {
|
||||||
|
let result = await runShell("winget install Volta.Volta");
|
||||||
|
if (!result.success) {
|
||||||
|
return { success: false, errorMessage: result.errorMessage };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let result = await runShell("curl https://get.volta.sh | bash");
|
||||||
|
if (!result.success) {
|
||||||
|
return { success: false, errorMessage: result.errorMessage };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log("Installed Volta");
|
||||||
|
|
||||||
|
process.env.PATH = ["~/.volta/bin", process.env.PATH].join(":");
|
||||||
|
console.log("Updated PATH");
|
||||||
|
let result = await runShell("volta install node");
|
||||||
|
if (!result.success) {
|
||||||
|
return { success: false, errorMessage: result.errorMessage };
|
||||||
|
}
|
||||||
|
console.log("Installed Node.js (via Volta)");
|
||||||
|
|
||||||
|
result = await runShell("node --version");
|
||||||
|
if (!result.success) {
|
||||||
|
return { success: false, errorMessage: result.errorMessage };
|
||||||
|
}
|
||||||
|
console.log("Node.js is setup with version");
|
||||||
|
|
||||||
|
return { success: true, version: result.output };
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import type {
|
|||||||
ChatStreamParams,
|
ChatStreamParams,
|
||||||
CreateAppParams,
|
CreateAppParams,
|
||||||
CreateAppResult,
|
CreateAppResult,
|
||||||
|
InstallNodeResult,
|
||||||
ListAppsResponse,
|
ListAppsResponse,
|
||||||
SandboxConfig,
|
SandboxConfig,
|
||||||
Version,
|
Version,
|
||||||
@@ -520,6 +521,17 @@ export class IpcClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Install Node.js and npm
|
||||||
|
public async installNode(): Promise<InstallNodeResult> {
|
||||||
|
try {
|
||||||
|
const result = await this.ipcRenderer.invoke("install-node");
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
showError(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- GitHub Device Flow ---
|
// --- GitHub Device Flow ---
|
||||||
public startGithubDeviceFlow(appId: number | null): void {
|
public startGithubDeviceFlow(appId: number | null): void {
|
||||||
this.ipcRenderer.invoke("github:start-flow", { appId });
|
this.ipcRenderer.invoke("github:start-flow", { appId });
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { registerSettingsHandlers } from "./handlers/settings_handlers";
|
|||||||
import { registerShellHandlers } from "./handlers/shell_handler";
|
import { registerShellHandlers } from "./handlers/shell_handler";
|
||||||
import { registerDependencyHandlers } from "./handlers/dependency_handlers";
|
import { registerDependencyHandlers } from "./handlers/dependency_handlers";
|
||||||
import { registerGithubHandlers } from "./handlers/github_handlers";
|
import { registerGithubHandlers } from "./handlers/github_handlers";
|
||||||
|
import { registerNodeHandlers } from "./handlers/node_handlers";
|
||||||
|
|
||||||
export function registerIpcHandlers() {
|
export function registerIpcHandlers() {
|
||||||
// Register all IPC handlers by category
|
// Register all IPC handlers by category
|
||||||
@@ -15,4 +16,5 @@ export function registerIpcHandlers() {
|
|||||||
registerShellHandlers();
|
registerShellHandlers();
|
||||||
registerDependencyHandlers();
|
registerDependencyHandlers();
|
||||||
registerGithubHandlers();
|
registerGithubHandlers();
|
||||||
|
registerNodeHandlers();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,3 +65,13 @@ export interface SandboxConfig {
|
|||||||
dependencies: Record<string, string>;
|
dependencies: Record<string, string>;
|
||||||
entry: string;
|
entry: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type InstallNodeResult =
|
||||||
|
| {
|
||||||
|
success: true;
|
||||||
|
version: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
success: false;
|
||||||
|
errorMessage: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -86,9 +86,11 @@ export type GithubUser = z.infer<typeof GithubUserSchema>;
|
|||||||
export const UserSettingsSchema = z.object({
|
export const UserSettingsSchema = z.object({
|
||||||
selectedModel: LargeLanguageModelSchema,
|
selectedModel: LargeLanguageModelSchema,
|
||||||
providerSettings: z.record(z.string(), ProviderSettingSchema),
|
providerSettings: z.record(z.string(), ProviderSettingSchema),
|
||||||
runtimeMode: RuntimeModeSchema,
|
|
||||||
githubUser: GithubUserSchema.optional(),
|
githubUser: GithubUserSchema.optional(),
|
||||||
githubAccessToken: SecretSchema.optional(),
|
githubAccessToken: SecretSchema.optional(),
|
||||||
|
|
||||||
|
// DEPRECATED.
|
||||||
|
runtimeMode: RuntimeModeSchema.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ const DEFAULT_SETTINGS: UserSettings = {
|
|||||||
provider: "auto",
|
provider: "auto",
|
||||||
},
|
},
|
||||||
providerSettings: {},
|
providerSettings: {},
|
||||||
runtimeMode: "unset",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const SETTINGS_FILE = "user-settings.json";
|
const SETTINGS_FILE = "user-settings.json";
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ import { ChatInput } from "@/components/chat/ChatInput";
|
|||||||
import { isPreviewOpenAtom } from "@/atoms/viewAtoms";
|
import { isPreviewOpenAtom } from "@/atoms/viewAtoms";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useStreamChat } from "@/hooks/useStreamChat";
|
import { useStreamChat } from "@/hooks/useStreamChat";
|
||||||
import { SetupRuntimeFlow } from "@/components/SetupRuntimeFlow";
|
|
||||||
import { RuntimeMode } from "@/lib/schemas";
|
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const [inputValue, setInputValue] = useAtom(chatInputValueAtom);
|
const [inputValue, setInputValue] = useAtom(chatInputValueAtom);
|
||||||
@@ -83,12 +81,6 @@ export default function HomePage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Runtime Setup Flow
|
|
||||||
// Render this only if runtimeMode is not set in settings
|
|
||||||
if (settings?.runtimeMode === "unset") {
|
|
||||||
return <SetupRuntimeFlow />;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Main Home Page Content (Rendered only if runtimeMode is set)
|
// Main Home Page Content (Rendered only if runtimeMode is set)
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center max-w-3xl m-auto p-8">
|
<div className="flex flex-col items-center justify-center max-w-3xl m-auto p-8">
|
||||||
@@ -96,7 +88,7 @@ export default function HomePage() {
|
|||||||
Build your dream app
|
Build your dream app
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{!isAnyProviderSetup() && <SetupBanner />}
|
<SetupBanner />
|
||||||
|
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<ChatInput onSubmit={handleSubmit} />
|
<ChatInput onSubmit={handleSubmit} />
|
||||||
|
|||||||
@@ -4,44 +4,14 @@ import { ProviderSettingsGrid } from "@/components/ProviderSettings";
|
|||||||
import ConfirmationDialog from "@/components/ConfirmationDialog";
|
import ConfirmationDialog from "@/components/ConfirmationDialog";
|
||||||
import { IpcClient } from "@/ipc/ipc_client";
|
import { IpcClient } from "@/ipc/ipc_client";
|
||||||
import { showSuccess, showError } from "@/lib/toast";
|
import { showSuccess, showError } from "@/lib/toast";
|
||||||
import { useSettings } from "@/hooks/useSettings";
|
|
||||||
import { RuntimeMode } from "@/lib/schemas";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { ExternalLink } from "lucide-react";
|
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
const { theme, setTheme } = useTheme();
|
const { theme, setTheme } = useTheme();
|
||||||
const {
|
|
||||||
settings,
|
|
||||||
updateSettings,
|
|
||||||
loading: settingsLoading,
|
|
||||||
error: settingsError,
|
|
||||||
} = 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<
|
|
||||||
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 [appVersion, setAppVersion] = useState<string | null>(null);
|
const [appVersion, setAppVersion] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
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();
|
|
||||||
|
|
||||||
// Fetch app version
|
// Fetch app version
|
||||||
const fetchVersion = async () => {
|
const fetchVersion = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -75,59 +45,6 @@ export default function SettingsPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRuntimeChange = async (newMode: RuntimeMode) => {
|
|
||||||
if (newMode === settings?.runtimeMode || isUpdatingRuntime) return;
|
|
||||||
setIsUpdatingRuntime(newMode);
|
|
||||||
setShowNodeInstallPrompt(false);
|
|
||||||
setDownloadClicked(false);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await updateSettings({ runtimeMode: newMode });
|
|
||||||
showSuccess("Runtime mode updated successfully.");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error updating runtime mode:", error);
|
|
||||||
showError(
|
|
||||||
error instanceof Error
|
|
||||||
? error.message
|
|
||||||
: "Failed to update runtime mode."
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
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.");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const currentRuntimeMode =
|
|
||||||
settings?.runtimeMode && settings.runtimeMode !== "unset"
|
|
||||||
? settings.runtimeMode
|
|
||||||
: "web-sandbox";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen p-8">
|
<div className="min-h-screen p-8">
|
||||||
<div className="max-w-5xl mx-auto">
|
<div className="max-w-5xl mx-auto">
|
||||||
@@ -178,251 +95,6 @@ export default function SettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Runtime Environment Section */}
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6">
|
|
||||||
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
|
|
||||||
Runtime Environment
|
|
||||||
</h2>
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4">
|
|
||||||
Choose how app code is executed. This affects performance,
|
|
||||||
security, and capabilities.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{settingsLoading ? (
|
|
||||||
<div className="flex items-center justify-center h-24">
|
|
||||||
{/* Inline SVG Spinner */}
|
|
||||||
<svg
|
|
||||||
className="animate-spin h-8 w-8 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>
|
|
||||||
) : settingsError ? (
|
|
||||||
<p className="text-red-500 text-center">
|
|
||||||
Error loading runtime settings: {settingsError.message}
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<Button
|
|
||||||
variant={
|
|
||||||
currentRuntimeMode === "web-sandbox" ? "default" : "outline"
|
|
||||||
}
|
|
||||||
className={`disabled:opacity-90 w-full justify-start p-4 h-auto text-left relative group ${
|
|
||||||
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"
|
|
||||||
? "opacity-50 cursor-not-allowed"
|
|
||||||
: "cursor-pointer"
|
|
||||||
}`}
|
|
||||||
onClick={() => handleRuntimeChange("web-sandbox")}
|
|
||||||
disabled={
|
|
||||||
isUpdatingRuntime !== null ||
|
|
||||||
currentRuntimeMode === "web-sandbox"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{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 className="bg-white dark:bg-gray-800 rounded-xl shadow-sm">
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm">
|
||||||
<ProviderSettingsGrid configuredProviders={[]} />
|
<ProviderSettingsGrid configuredProviders={[]} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ const validInvokeChannels = [
|
|||||||
"open-external-url",
|
"open-external-url",
|
||||||
"reset-all",
|
"reset-all",
|
||||||
"nodejs-status",
|
"nodejs-status",
|
||||||
|
"install-node",
|
||||||
"github:start-flow",
|
"github:start-flow",
|
||||||
"github:is-repo-available",
|
"github:is-repo-available",
|
||||||
"github:create-repo",
|
"github:create-repo",
|
||||||
|
|||||||
Reference in New Issue
Block a user