clear runtime consent and settings

This commit is contained in:
Will Chen
2025-04-11 14:23:14 -07:00
parent 34f069960c
commit d3c1f8e34c
11 changed files with 740 additions and 14 deletions

261
package-lock.json generated
View File

@@ -21,6 +21,8 @@
"@radix-ui/react-popover": "^1.1.7", "@radix-ui/react-popover": "^1.1.7",
"@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-toggle": "^1.1.3",
"@radix-ui/react-toggle-group": "^1.1.3",
"@radix-ui/react-tooltip": "^1.1.8", "@radix-ui/react-tooltip": "^1.1.8",
"@rollup/plugin-commonjs": "^28.0.3", "@rollup/plugin-commonjs": "^28.0.3",
"@tailwindcss/typography": "^0.5.16", "@tailwindcss/typography": "^0.5.16",
@@ -5838,6 +5840,265 @@
} }
} }
}, },
"node_modules/@radix-ui/react-toggle": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.3.tgz",
"integrity": "sha512-Za5HHd9nvsiZ2t3EI/dVd4Bm/JydK+D22uHKk46fPtvuPxVCJBUo5mQybN+g5sZe35y7I6GDTTfdkVv5SnxlFg==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-primitive": "2.0.3",
"@radix-ui/react-use-controllable-state": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toggle-group": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.3.tgz",
"integrity": "sha512-khTzdGIxy8WurYUEUrapvj5aOev/tUA8TDEFi1D0Dn3yX+KR5AqjX0b7E5sL9ngRRpxDN2RRJdn5siasu5jtcg==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-primitive": "2.0.3",
"@radix-ui/react-roving-focus": "1.1.3",
"@radix-ui/react-toggle": "1.1.3",
"@radix-ui/react-use-controllable-state": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/primitive": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
"license": "MIT"
},
"node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-context": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-primitive": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.3.tgz",
"integrity": "sha512-Pf/t/GkndH7CQ8wE2hbkXA+WyZ83fhQQn5DDmwDiDo6AwN/fhaH8oqZ0jRjMrO2iaMhDi6P1HRx6AZwyMinY1g==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.2.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-slot": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz",
"integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
"integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-use-controllable-state": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.1.tgz",
"integrity": "sha512-YnEXIy8/ga01Y1PN0VfaNH//MhA91JlEGVBDxDzROqwrAtG5Yr2QGEPz8A/rJA3C7ZAHryOYGaUv8fLSW2H/mg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-callback-ref": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/primitive": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
"license": "MIT"
},
"node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-primitive": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.3.tgz",
"integrity": "sha512-Pf/t/GkndH7CQ8wE2hbkXA+WyZ83fhQQn5DDmwDiDo6AwN/fhaH8oqZ0jRjMrO2iaMhDi6P1HRx6AZwyMinY1g==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.2.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-slot": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz",
"integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
"integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-use-controllable-state": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.1.tgz",
"integrity": "sha512-YnEXIy8/ga01Y1PN0VfaNH//MhA91JlEGVBDxDzROqwrAtG5Yr2QGEPz8A/rJA3C7ZAHryOYGaUv8fLSW2H/mg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-callback-ref": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip": { "node_modules/@radix-ui/react-tooltip": {
"version": "1.1.8", "version": "1.1.8",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.8.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.8.tgz",

View File

@@ -70,6 +70,8 @@
"@radix-ui/react-popover": "^1.1.7", "@radix-ui/react-popover": "^1.1.7",
"@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-toggle": "^1.1.3",
"@radix-ui/react-toggle-group": "^1.1.3",
"@radix-ui/react-tooltip": "^1.1.8", "@radix-ui/react-tooltip": "^1.1.8",
"@rollup/plugin-commonjs": "^28.0.3", "@rollup/plugin-commonjs": "^28.0.3",
"@tailwindcss/typography": "^0.5.16", "@tailwindcss/typography": "^0.5.16",

View File

@@ -2,11 +2,25 @@ import { useAtom } from "jotai";
import { selectedAppIdAtom } from "@/atoms/appAtoms"; import { selectedAppIdAtom } from "@/atoms/appAtoms";
import { useLoadApps } from "@/hooks/useLoadApps"; import { useLoadApps } from "@/hooks/useLoadApps";
import { useRouter } from "@tanstack/react-router"; import { useRouter } from "@tanstack/react-router";
import { useSettings } from "@/hooks/useSettings";
import { RuntimeMode } from "@/lib/schemas";
function formatRuntimeMode(runtimeMode: RuntimeMode | undefined) {
switch (runtimeMode) {
case "web-sandbox":
return "Sandbox";
case "local-node":
return "Local";
default:
return runtimeMode;
}
}
export const TitleBar = () => { export const TitleBar = () => {
const [selectedAppId] = useAtom(selectedAppIdAtom); const [selectedAppId] = useAtom(selectedAppIdAtom);
const { apps } = useLoadApps(); const { apps } = useLoadApps();
const { navigate } = useRouter(); const { navigate } = useRouter();
const { settings } = useSettings();
// Get selected app name // Get selected app name
const selectedApp = apps.find((app) => app.id === selectedAppId); const selectedApp = apps.find((app) => app.id === selectedAppId);
@@ -15,9 +29,11 @@ export const TitleBar = () => {
: "(no app selected)"; : "(no app selected)";
return ( return (
<div className="z-11 w-full h-8 bg-(--sidebar) absolute top-0 left-0 app-region-drag flex items-center"> <div className="@container z-11 w-full h-8 bg-(--sidebar) absolute top-0 left-0 app-region-drag flex items-center">
<div className="no-app-region-drag pl-24 text-sm font-medium"> <div className="pl-24"></div>
{displayText} <div className="hidden @md:block text-sm font-medium">{displayText}</div>
<div className="text-sm font-medium pl-4">
{formatRuntimeMode(settings?.runtimeMode)} runtime
</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

@@ -0,0 +1,125 @@
import { useState } 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";
interface SetupRuntimeFlowProps {
onRuntimeSelected: (mode: RuntimeMode) => Promise<void>;
}
export function SetupRuntimeFlow({ onRuntimeSelected }: SetupRuntimeFlowProps) {
const [isLoading, setIsLoading] = useState<RuntimeMode | null>(null);
const handleSelect = async (mode: RuntimeMode) => {
setIsLoading(mode);
try {
await onRuntimeSelected(mode);
// No need to setIsLoading(null) as the component will unmount 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
}
};
return (
<div className="flex flex-col items-center justify-center max-w-2xl m-auto p-8">
<h1 className="text-4xl font-bold mb-4 text-center">Welcome to Dyad</h1>
<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
want to run these apps. You can always change your mind later.
</p>
<div className="w-full space-y-4">
<Button
variant="outline"
className="relative bg-(--background-lightest) w-full justify-start p-6 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 Runtime</p>
<p 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>
Code will run inside a browser sandbox.
</div>
</p>
</div>
</Button>
<Button
variant="outline"
className="bg-(--background-lightest) w-full justify-start p-6 h-auto text-left relative"
onClick={() => handleSelect("local-node")}
disabled={!!isLoading}
>
{isLoading === "local-node" && (
<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">Local Node.js Runtime</p>
<div className="text-sm text-gray-500 dark:text-gray-400 mt-1">
<p>
Code will run using Node.js on your computer and have full
system access.
</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">
Warning: this will run AI-generated code directly on your
computer, which could put your system at risk.
</p>
</div>
</div>
</Button>
</div>
</div>
);
}

View File

@@ -0,0 +1,71 @@
import * as React from "react"
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
import { type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { toggleVariants } from "@/components/ui/toggle"
const ToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants>
>({
size: "default",
variant: "default",
})
function ToggleGroup({
className,
variant,
size,
children,
...props
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
VariantProps<typeof toggleVariants>) {
return (
<ToggleGroupPrimitive.Root
data-slot="toggle-group"
data-variant={variant}
data-size={size}
className={cn(
"group/toggle-group flex w-fit items-center rounded-md data-[variant=outline]:shadow-xs",
className
)}
{...props}
>
<ToggleGroupContext.Provider value={{ variant, size }}>
{children}
</ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
)
}
function ToggleGroupItem({
className,
children,
variant,
size,
...props
}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
VariantProps<typeof toggleVariants>) {
const context = React.useContext(ToggleGroupContext)
return (
<ToggleGroupPrimitive.Item
data-slot="toggle-group-item"
data-variant={context.variant || variant}
data-size={context.size || size}
className={cn(
toggleVariants({
variant: context.variant || variant,
size: context.size || size,
}),
"min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10 data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l",
className
)}
{...props}
>
{children}
</ToggleGroupPrimitive.Item>
)
}
export { ToggleGroup, ToggleGroupItem }

View File

@@ -0,0 +1,47 @@
"use client"
import * as React from "react"
import * as TogglePrimitive from "@radix-ui/react-toggle"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const toggleVariants = cva(
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
{
variants: {
variant: {
default: "bg-transparent",
outline:
"border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-9 px-2 min-w-9",
sm: "h-8 px-1.5 min-w-8",
lg: "h-10 px-2.5 min-w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Toggle({
className,
variant,
size,
...props
}: React.ComponentProps<typeof TogglePrimitive.Root> &
VariantProps<typeof toggleVariants>) {
return (
<TogglePrimitive.Root
data-slot="toggle"
className={cn(toggleVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Toggle, toggleVariants }

View File

@@ -24,7 +24,6 @@ export interface ChatResponseEnd {
export interface CreateAppParams { export interface CreateAppParams {
name: string; name: string;
path: string;
} }
export interface CreateAppResult { export interface CreateAppResult {

View File

@@ -61,12 +61,16 @@ export const ProviderSettingSchema = z.object({
*/ */
export type ProviderSetting = z.infer<typeof ProviderSettingSchema>; export type ProviderSetting = z.infer<typeof ProviderSettingSchema>;
export const RuntimeModeSchema = z.enum(["web-sandbox", "local-node", "unset"]);
export type RuntimeMode = z.infer<typeof RuntimeModeSchema>;
/** /**
* Zod schema for user settings * Zod schema for user settings
*/ */
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,
}); });
/** /**

View File

@@ -9,6 +9,7 @@ const DEFAULT_SETTINGS: UserSettings = {
provider: "auto", provider: "auto",
}, },
providerSettings: {}, providerSettings: {},
runtimeMode: "unset",
}; };
const SETTINGS_FILE = "user-settings.json"; const SETTINGS_FILE = "user-settings.json";

View File

@@ -1,7 +1,7 @@
import { useNavigate, useSearch } from "@tanstack/react-router"; import { useNavigate, useSearch } from "@tanstack/react-router";
import { useAtom, useSetAtom } from "jotai"; import { useAtom, useSetAtom } from "jotai";
import { chatInputValueAtom } from "../atoms/chatAtoms"; import { chatInputValueAtom } from "../atoms/chatAtoms";
import { selectedAppIdAtom, appsListAtom } from "@/atoms/appAtoms"; import { selectedAppIdAtom } from "@/atoms/appAtoms";
import { IpcClient } from "@/ipc/ipc_client"; import { IpcClient } from "@/ipc/ipc_client";
import { generateCuteAppName } from "@/lib/utils"; import { generateCuteAppName } from "@/lib/utils";
import { useLoadApps } from "@/hooks/useLoadApps"; import { useLoadApps } from "@/hooks/useLoadApps";
@@ -11,15 +11,16 @@ 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);
const navigate = useNavigate(); const navigate = useNavigate();
const search = useSearch({ from: "/" }); const search = useSearch({ from: "/" });
const [appsList] = useAtom(appsListAtom);
const setSelectedAppId = useSetAtom(selectedAppIdAtom); const setSelectedAppId = useSetAtom(selectedAppIdAtom);
const { refreshApps } = useLoadApps(); const { refreshApps } = useLoadApps();
const { isAnyProviderSetup } = useSettings(); const { settings, isAnyProviderSetup, updateSettings } = 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();
@@ -34,6 +35,10 @@ 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;
@@ -42,33 +47,33 @@ export default function HomePage() {
// Create the chat and navigate // Create the chat and navigate
const result = await IpcClient.getInstance().createApp({ const result = await IpcClient.getInstance().createApp({
name: generateCuteAppName(), name: generateCuteAppName(),
path: "./apps/foo",
}); });
// Add a 2-second timeout *after* the streamMessage call // Stream the message
// This makes the loading UI feel less janky.
streamMessage({ prompt: inputValue, chatId: result.chatId }); streamMessage({ prompt: inputValue, chatId: result.chatId });
await new Promise((resolve) => setTimeout(resolve, 2000)); await new Promise((resolve) => setTimeout(resolve, 2000));
setInputValue(""); setInputValue("");
setSelectedAppId(result.app.id); setSelectedAppId(result.app.id);
setIsPreviewOpen(false); setIsPreviewOpen(false);
refreshApps(); await refreshApps(); // Ensure refreshApps is awaited if it's async
navigate({ to: "/chat", search: { id: result.chatId } }); navigate({ to: "/chat", search: { id: result.chatId } });
} catch (error) { } catch (error) {
console.error("Failed to create chat:", error); console.error("Failed to create chat:", error);
setIsLoading(false); setIsLoading(false); // Ensure loading state is reset on error
} }
// No finally block needed for setIsLoading(false) here if navigation happens on success
}; };
// Loading overlay // Loading overlay for app creation
if (isLoading) { if (isLoading) {
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">
<div className="w-full flex flex-col items-center"> <div className="w-full flex flex-col items-center">
{/* Loading Spinner */}
<div className="relative w-24 h-24 mb-8"> <div className="relative w-24 h-24 mb-8">
<div className="absolute top-0 left-0 w-full h-full border-8 border-gray-200 dark:border-gray-700 rounded-full"></div> <div className="absolute top-0 left-0 w-full h-full border-8 border-gray-200 dark:border-gray-700 rounded-full"></div>
<div className="absolute top-0 left-0 w-full h-full border-8 border-t-(--primary) rounded-full animate-spin"></div> <div className="absolute top-0 left-0 w-full h-full border-8 border-t-primary rounded-full animate-spin"></div>
</div> </div>
<h2 className="text-2xl font-bold mb-2 text-gray-800 dark:text-gray-200"> <h2 className="text-2xl font-bold mb-2 text-gray-800 dark:text-gray-200">
Building your app Building your app
@@ -82,6 +87,13 @@ export default function HomePage() {
); );
} }
// Runtime Setup Flow
// Render this only if runtimeMode is not set in settings
if (settings?.runtimeMode === "unset") {
return <SetupRuntimeFlow onRuntimeSelected={handleSetRuntimeMode} />;
}
// 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">
<h1 className="text-6xl font-bold mb-12 bg-clip-text text-transparent bg-gradient-to-r from-gray-900 to-gray-600 dark:from-gray-100 dark:to-gray-400 tracking-tight"> <h1 className="text-6xl font-bold mb-12 bg-clip-text text-transparent bg-gradient-to-r from-gray-900 to-gray-600 dark:from-gray-100 dark:to-gray-400 tracking-tight">

View File

@@ -4,11 +4,110 @@ 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";
// Helper component for runtime option buttons
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();
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(false);
const handleResetEverything = async () => { const handleResetEverything = async () => {
setIsResetting(true); setIsResetting(true);
@@ -31,6 +130,29 @@ export default function SettingsPage() {
} }
}; };
const handleRuntimeChange = async (newMode: RuntimeMode) => {
if (newMode === settings?.runtimeMode || isUpdatingRuntime) return;
setIsUpdatingRuntime(true);
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(false);
}
};
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">
@@ -73,6 +195,72 @@ 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 and
security.
</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">
<RuntimeOptionButton
title="Sandboxed Runtime"
description="Code runs inside a browser sandbox. Safer, limited system access."
badge="Recommended for beginners"
isSelected={currentRuntimeMode === "web-sandbox"}
isLoading={
isUpdatingRuntime && currentRuntimeMode !== "web-sandbox"
}
onClick={() => handleRuntimeChange("web-sandbox")}
disabled={isUpdatingRuntime}
/>
<RuntimeOptionButton
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}
/>
</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>