From d3c1f8e34ca237be2393259b887907c83682c15d Mon Sep 17 00:00:00 2001 From: Will Chen Date: Fri, 11 Apr 2025 14:23:14 -0700 Subject: [PATCH] clear runtime consent and settings --- package-lock.json | 261 ++++++++++++++++++++++++++++ package.json | 2 + src/app/TitleBar.tsx | 22 ++- src/components/SetupRuntimeFlow.tsx | 125 +++++++++++++ src/components/ui/toggle-group.tsx | 71 ++++++++ src/components/ui/toggle.tsx | 47 +++++ src/ipc/ipc_types.ts | 1 - src/lib/schemas.ts | 4 + src/main/settings.ts | 1 + src/pages/home.tsx | 32 ++-- src/pages/settings.tsx | 188 ++++++++++++++++++++ 11 files changed, 740 insertions(+), 14 deletions(-) create mode 100644 src/components/SetupRuntimeFlow.tsx create mode 100644 src/components/ui/toggle-group.tsx create mode 100644 src/components/ui/toggle.tsx diff --git a/package-lock.json b/package-lock.json index b996653..1481a75 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,8 @@ "@radix-ui/react-popover": "^1.1.7", "@radix-ui/react-separator": "^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", "@rollup/plugin-commonjs": "^28.0.3", "@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": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.8.tgz", diff --git a/package.json b/package.json index 744d3dc..fae5c9b 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,8 @@ "@radix-ui/react-popover": "^1.1.7", "@radix-ui/react-separator": "^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", "@rollup/plugin-commonjs": "^28.0.3", "@tailwindcss/typography": "^0.5.16", diff --git a/src/app/TitleBar.tsx b/src/app/TitleBar.tsx index 13d5772..98e50de 100644 --- a/src/app/TitleBar.tsx +++ b/src/app/TitleBar.tsx @@ -2,11 +2,25 @@ import { useAtom } from "jotai"; import { selectedAppIdAtom } from "@/atoms/appAtoms"; import { useLoadApps } from "@/hooks/useLoadApps"; 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 = () => { const [selectedAppId] = useAtom(selectedAppIdAtom); const { apps } = useLoadApps(); const { navigate } = useRouter(); + const { settings } = useSettings(); // Get selected app name const selectedApp = apps.find((app) => app.id === selectedAppId); @@ -15,9 +29,11 @@ export const TitleBar = () => { : "(no app selected)"; return ( -
-
- {displayText} +
+
+
{displayText}
+
+ {formatRuntimeMode(settings?.runtimeMode)} runtime
Dyad
diff --git a/src/components/SetupRuntimeFlow.tsx b/src/components/SetupRuntimeFlow.tsx new file mode 100644 index 0000000..1703af3 --- /dev/null +++ b/src/components/SetupRuntimeFlow.tsx @@ -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; +} + +export function SetupRuntimeFlow({ onRuntimeSelected }: SetupRuntimeFlowProps) { + const [isLoading, setIsLoading] = useState(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 ( +
+

Welcome to Dyad

+

+ 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. +

+ +
+ + + +
+
+ ); +} diff --git a/src/components/ui/toggle-group.tsx b/src/components/ui/toggle-group.tsx new file mode 100644 index 0000000..5efb7ba --- /dev/null +++ b/src/components/ui/toggle-group.tsx @@ -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 +>({ + size: "default", + variant: "default", +}) + +function ToggleGroup({ + className, + variant, + size, + children, + ...props +}: React.ComponentProps & + VariantProps) { + return ( + + + {children} + + + ) +} + +function ToggleGroupItem({ + className, + children, + variant, + size, + ...props +}: React.ComponentProps & + VariantProps) { + const context = React.useContext(ToggleGroupContext) + + return ( + + {children} + + ) +} + +export { ToggleGroup, ToggleGroupItem } diff --git a/src/components/ui/toggle.tsx b/src/components/ui/toggle.tsx new file mode 100644 index 0000000..94ec8f5 --- /dev/null +++ b/src/components/ui/toggle.tsx @@ -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 & + VariantProps) { + return ( + + ) +} + +export { Toggle, toggleVariants } diff --git a/src/ipc/ipc_types.ts b/src/ipc/ipc_types.ts index a49cc82..f18ca0d 100644 --- a/src/ipc/ipc_types.ts +++ b/src/ipc/ipc_types.ts @@ -24,7 +24,6 @@ export interface ChatResponseEnd { export interface CreateAppParams { name: string; - path: string; } export interface CreateAppResult { diff --git a/src/lib/schemas.ts b/src/lib/schemas.ts index 11ecfe2..2c98310 100644 --- a/src/lib/schemas.ts +++ b/src/lib/schemas.ts @@ -61,12 +61,16 @@ export const ProviderSettingSchema = z.object({ */ export type ProviderSetting = z.infer; +export const RuntimeModeSchema = z.enum(["web-sandbox", "local-node", "unset"]); +export type RuntimeMode = z.infer; + /** * Zod schema for user settings */ export const UserSettingsSchema = z.object({ selectedModel: LargeLanguageModelSchema, providerSettings: z.record(z.string(), ProviderSettingSchema), + runtimeMode: RuntimeModeSchema, }); /** diff --git a/src/main/settings.ts b/src/main/settings.ts index 734e3df..d798925 100644 --- a/src/main/settings.ts +++ b/src/main/settings.ts @@ -9,6 +9,7 @@ const DEFAULT_SETTINGS: UserSettings = { provider: "auto", }, providerSettings: {}, + runtimeMode: "unset", }; const SETTINGS_FILE = "user-settings.json"; diff --git a/src/pages/home.tsx b/src/pages/home.tsx index 424a9a9..24421d6 100644 --- a/src/pages/home.tsx +++ b/src/pages/home.tsx @@ -1,7 +1,7 @@ import { useNavigate, useSearch } from "@tanstack/react-router"; import { useAtom, useSetAtom } from "jotai"; import { chatInputValueAtom } from "../atoms/chatAtoms"; -import { selectedAppIdAtom, appsListAtom } from "@/atoms/appAtoms"; +import { selectedAppIdAtom } from "@/atoms/appAtoms"; import { IpcClient } from "@/ipc/ipc_client"; import { generateCuteAppName } from "@/lib/utils"; import { useLoadApps } from "@/hooks/useLoadApps"; @@ -11,15 +11,16 @@ import { ChatInput } from "@/components/chat/ChatInput"; import { isPreviewOpenAtom } from "@/atoms/viewAtoms"; import { useState, useEffect } from "react"; import { useStreamChat } from "@/hooks/useStreamChat"; +import { SetupRuntimeFlow } from "@/components/SetupRuntimeFlow"; +import { RuntimeMode } from "@/lib/schemas"; export default function HomePage() { const [inputValue, setInputValue] = useAtom(chatInputValueAtom); const navigate = useNavigate(); const search = useSearch({ from: "/" }); - const [appsList] = useAtom(appsListAtom); const setSelectedAppId = useSetAtom(selectedAppIdAtom); const { refreshApps } = useLoadApps(); - const { isAnyProviderSetup } = useSettings(); + const { settings, isAnyProviderSetup, updateSettings } = useSettings(); const setIsPreviewOpen = useSetAtom(isPreviewOpenAtom); const [isLoading, setIsLoading] = useState(false); const { streamMessage } = useStreamChat(); @@ -34,6 +35,10 @@ export default function HomePage() { } }, [appId, navigate]); + const handleSetRuntimeMode = async (mode: RuntimeMode) => { + await updateSettings({ runtimeMode: mode }); + }; + const handleSubmit = async () => { if (!inputValue.trim()) return; @@ -42,33 +47,33 @@ export default function HomePage() { // Create the chat and navigate const result = await IpcClient.getInstance().createApp({ name: generateCuteAppName(), - path: "./apps/foo", }); - // Add a 2-second timeout *after* the streamMessage call - // This makes the loading UI feel less janky. + // Stream the message streamMessage({ prompt: inputValue, chatId: result.chatId }); await new Promise((resolve) => setTimeout(resolve, 2000)); setInputValue(""); setSelectedAppId(result.app.id); setIsPreviewOpen(false); - refreshApps(); + await refreshApps(); // Ensure refreshApps is awaited if it's async navigate({ to: "/chat", search: { id: result.chatId } }); } catch (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) { return (
+ {/* Loading Spinner */}
-
+

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 ; + } + + // Main Home Page Content (Rendered only if runtimeMode is set) return (

diff --git a/src/pages/settings.tsx b/src/pages/settings.tsx index a837ed3..568671c 100644 --- a/src/pages/settings.tsx +++ b/src/pages/settings.tsx @@ -4,11 +4,110 @@ import { ProviderSettingsGrid } from "@/components/ProviderSettings"; import ConfirmationDialog from "@/components/ConfirmationDialog"; import { IpcClient } from "@/ipc/ipc_client"; 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 ( + + ); +} export default function SettingsPage() { const { theme, setTheme } = useTheme(); + const { + settings, + updateSettings, + loading: settingsLoading, + error: settingsError, + } = useSettings(); const [isResetDialogOpen, setIsResetDialogOpen] = useState(false); const [isResetting, setIsResetting] = useState(false); + const [isUpdatingRuntime, setIsUpdatingRuntime] = useState(false); const handleResetEverything = async () => { 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 (
@@ -73,6 +195,72 @@ export default function SettingsPage() {
+ {/* Runtime Environment Section */} +
+

+ Runtime Environment +

+

+ Choose how app code is executed. This affects performance and + security. +

+ + {settingsLoading ? ( +
+ {/* Inline SVG Spinner */} + + + + +
+ ) : settingsError ? ( +

+ Error loading runtime settings: {settingsError.message} +

+ ) : ( +
+ handleRuntimeChange("web-sandbox")} + disabled={isUpdatingRuntime} + /> + handleRuntimeChange("local-node")} + disabled={isUpdatingRuntime} + /> +
+ )} +
+