Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
B
bit-pm
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
燕伟桐
bit-pm
Commits
6a4f5388
提交
6a4f5388
authored
4月 13, 2025
作者:
Will Chen
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
Update setup/settings flow for runtime
上级
3074634c
隐藏空白字符变更
内嵌
并排
正在显示
7 个修改的文件
包含
497 行增加
和
153 行删除
+497
-153
TitleBar.tsx
src/app/TitleBar.tsx
+3
-3
SetupRuntimeFlow.tsx
src/components/SetupRuntimeFlow.tsx
+166
-30
app_handlers.ts
src/ipc/handlers/app_handlers.ts
+52
-0
ipc_client.ts
src/ipc/ipc_client.ts
+17
-0
home.tsx
src/pages/home.tsx
+2
-6
settings.tsx
src/pages/settings.tsx
+256
-114
preload.ts
src/preload.ts
+1
-0
没有找到文件。
src/app/TitleBar.tsx
浏览文件 @
6a4f5388
...
@@ -8,9 +8,9 @@ import { RuntimeMode } from "@/lib/schemas";
...
@@ -8,9 +8,9 @@ import { RuntimeMode } from "@/lib/schemas";
function
formatRuntimeMode
(
runtimeMode
:
RuntimeMode
|
undefined
)
{
function
formatRuntimeMode
(
runtimeMode
:
RuntimeMode
|
undefined
)
{
switch
(
runtimeMode
)
{
switch
(
runtimeMode
)
{
case
"web-sandbox"
:
case
"web-sandbox"
:
return
"Sandbox"
;
return
"Sandbox
ed
"
;
case
"local-node"
:
case
"local-node"
:
return
"
Local
"
;
return
"
Full Access
"
;
default
:
default
:
return
runtimeMode
;
return
runtimeMode
;
}
}
...
@@ -33,7 +33,7 @@ export const TitleBar = () => {
...
@@ -33,7 +33,7 @@ export const TitleBar = () => {
<
div
className=
"pl-24"
></
div
>
<
div
className=
"pl-24"
></
div
>
<
div
className=
"hidden @md:block text-sm font-medium"
>
{
displayText
}
</
div
>
<
div
className=
"hidden @md:block text-sm font-medium"
>
{
displayText
}
</
div
>
<
div
className=
"text-sm font-medium pl-4"
>
<
div
className=
"text-sm font-medium pl-4"
>
{
formatRuntimeMode
(
settings
?.
runtimeMode
)
}
runtim
e
{
formatRuntimeMode
(
settings
?.
runtimeMode
)
}
mod
e
</
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
>
...
...
src/components/SetupRuntimeFlow.tsx
浏览文件 @
6a4f5388
import
{
useState
}
from
"react"
;
import
{
useState
,
useEffect
}
from
"react"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
IpcClient
}
from
"@/ipc/ipc_client"
;
import
{
IpcClient
}
from
"@/ipc/ipc_client"
;
import
{
useSettings
}
from
"@/hooks/useSettings"
;
// Assuming useSettings provides a refresh function
import
{
useSettings
}
from
"@/hooks/useSettings"
;
// Assuming useSettings provides a refresh function
import
{
RuntimeMode
}
from
"@/lib/schemas"
;
import
{
RuntimeMode
}
from
"@/lib/schemas"
;
import
{
ExternalLink
}
from
"lucide-react"
;
interface
SetupRuntimeFlowProps
{
interface
SetupRuntimeFlowProps
{
onRuntimeSelected
:
(
mode
:
RuntimeMode
)
=>
Promise
<
void
>
;
hideIntroText
?:
boolean
;
}
}
export
function
SetupRuntimeFlow
({
onRuntimeSelected
}:
SetupRuntimeFlowProps
)
{
export
function
SetupRuntimeFlow
({
hideIntroText
}:
SetupRuntimeFlowProps
)
{
const
[
isLoading
,
setIsLoading
]
=
useState
<
RuntimeMode
|
null
>
(
null
);
const
[
isLoading
,
setIsLoading
]
=
useState
<
RuntimeMode
|
"check"
|
null
>
(
null
);
const
[
showNodeInstallPrompt
,
setShowNodeInstallPrompt
]
=
useState
(
false
);
const
[
nodeVersion
,
setNodeVersion
]
=
useState
<
string
|
null
>
(
null
);
const
[
npmVersion
,
setNpmVersion
]
=
useState
<
string
|
null
>
(
null
);
const
[
downloadClicked
,
setDownloadClicked
]
=
useState
(
false
);
const
{
updateSettings
}
=
useSettings
();
// Pre-check Node.js status on component mount (optional but good UX)
useEffect
(()
=>
{
const
checkNode
=
async
()
=>
{
try
{
const
status
=
await
IpcClient
.
getInstance
().
getNodejsStatus
();
setNodeVersion
(
status
.
nodeVersion
);
setNpmVersion
(
status
.
npmVersion
);
}
catch
(
error
)
{
console
.
error
(
"Failed to check Node.js status:"
,
error
);
// Assume not installed if check fails
setNodeVersion
(
null
);
setNpmVersion
(
null
);
}
};
checkNode
();
},
[]);
const
handleSelect
=
async
(
mode
:
RuntimeMode
)
=>
{
const
handleSelect
=
async
(
mode
:
RuntimeMode
)
=>
{
if
(
isLoading
)
return
;
// Prevent double clicks
setIsLoading
(
mode
);
setIsLoading
(
mode
);
try
{
try
{
await
onRuntimeSelected
(
mode
);
await
updateSettings
({
runtimeMode
:
mode
}
);
//
No need to setIsLoading(null) as the component will unmount
on success
//
Component likely unmounts
on success
}
catch
(
error
)
{
}
catch
(
error
)
{
console
.
error
(
"Failed to set runtime mode:"
,
error
);
console
.
error
(
"Failed to set runtime mode:"
,
error
);
alert
(
alert
(
...
@@ -27,18 +54,45 @@ export function SetupRuntimeFlow({ onRuntimeSelected }: SetupRuntimeFlowProps) {
...
@@ -27,18 +54,45 @@ export function SetupRuntimeFlow({ onRuntimeSelected }: SetupRuntimeFlowProps) {
}
}
};
};
const
handleLocalNodeClick
=
async
()
=>
{
if
(
isLoading
)
return
;
setIsLoading
(
"check"
);
try
{
if
(
nodeVersion
&&
npmVersion
)
{
// Node and npm found, proceed directly
handleSelect
(
"local-node"
);
}
else
{
// Node or npm not found, show prompt
setShowNodeInstallPrompt
(
true
);
setIsLoading
(
null
);
}
}
catch
(
error
)
{
console
.
error
(
"Failed to check Node.js status on click:"
,
error
);
// Show prompt if check fails
setShowNodeInstallPrompt
(
true
);
setIsLoading
(
null
);
}
};
return
(
return
(
<
div
className=
"flex flex-col items-center justify-center max-w-2xl m-auto p-8"
>
<
div
className=
"flex flex-col items-center justify-center max-w-2xl m-auto p-6"
>
<
h1
className=
"text-4xl font-bold mb-4 text-center"
>
Welcome to Dyad
</
h1
>
{
!
hideIntroText
&&
(
<
p
className=
"text-lg text-gray-600 dark:text-gray-400 mb-10 text-center"
>
<>
You can start building apps with AI in a moment, but first pick how you
<
h1
className=
"text-4xl font-bold mb-2 text-center"
>
want to run these apps. You can always change your mind later.
Welcome to Dyad
</
p
>
</
h1
>
<
p
className=
"text-lg text-gray-600 dark:text-gray-400 mb-6 text-center"
>
Before you start building, choose how your apps will run.
<
br
/>
Don’t worry — you can change this later anytime.
</
p
>
</>
)
}
<
div
className=
"w-full space-y-4"
>
<
div
className=
"w-full space-y-4"
>
<
Button
<
Button
variant=
"outline"
variant=
"outline"
className=
"relative bg-(--background-lightest) w-full justify-start p-
6
h-auto text-left relative"
className=
"relative bg-(--background-lightest) w-full justify-start p-
4
h-auto text-left relative"
onClick=
{
()
=>
handleSelect
(
"web-sandbox"
)
}
onClick=
{
()
=>
handleSelect
(
"web-sandbox"
)
}
disabled=
{
!!
isLoading
}
disabled=
{
!!
isLoading
}
>
>
...
@@ -65,27 +119,29 @@ export function SetupRuntimeFlow({ onRuntimeSelected }: SetupRuntimeFlowProps) {
...
@@ -65,27 +119,29 @@ export function SetupRuntimeFlow({ onRuntimeSelected }: SetupRuntimeFlowProps) {
</
svg
>
</
svg
>
)
}
)
}
<
div
>
<
div
>
<
p
className=
"font-medium text-base"
>
Sandboxed
Runtim
e
</
p
>
<
p
className=
"font-medium text-base"
>
Sandboxed
Mod
e
</
p
>
<
p
className=
"text-sm text-gray-500 dark:text-gray-400 mt-1"
>
<
div
className=
"text-sm text-gray-500 dark:text-gray-400 mt-1"
>
<
div
>
<
div
>
<
span
className=
"absolute top-4 right-2 bg-blue-100 text-blue-800 text-xs font-medium px-2 py-0.5 rounded-full"
>
<
span
className=
"absolute top-4 right-2 bg-blue-100 text-blue-800 text-xs font-medium px-2 py-0.5 rounded-full"
>
Recommended for beginners
Recommended for beginners
</
span
>
</
span
>
Code will run inside a browser sandbox.
<
p
>
Apps run in a protected environment within your browser.
</
p
>
<
p
>
Does not support advanced apps.
</
p
>
</
div
>
</
div
>
</
p
>
</
div
>
</
div
>
</
div
>
</
Button
>
</
Button
>
<
Button
<
Button
variant=
"outline"
variant=
"outline"
className=
"bg-(--background-lightest) w-full justify-start p-6 h-auto text-left relative"
className=
"bg-(--background-lightest) w-full justify-start p-4 h-auto text-left relative flex flex-col items-start"
onClick=
{
()
=>
handleSelect
(
"local-node"
)
}
onClick=
{
handleLocalNodeClick
}
disabled=
{
!!
isLoading
}
disabled=
{
isLoading
===
"web-sandbox"
||
isLoading
===
"local-node"
}
style=
{
{
height
:
"auto"
}
}
// Ensure height adjusts
>
>
{
isLoading
===
"
local-node"
&&
(
{
isLoading
===
"
check"
||
isLoading
===
"local-node"
?
(
<
svg
<
svg
className=
"animate-spin h-5 w-5 mr-3 absolute right-4 top-
1/2 -translate-y-1/2"
className=
"animate-spin h-5 w-5 mr-3 absolute right-4 top-
6"
// Adjust position
xmlns=
"http://www.w3.org/2000/svg"
xmlns=
"http://www.w3.org/2000/svg"
fill=
"none"
fill=
"none"
viewBox=
"0 0 24 24"
viewBox=
"0 0 24 24"
...
@@ -104,17 +160,97 @@ export function SetupRuntimeFlow({ onRuntimeSelected }: SetupRuntimeFlowProps) {
...
@@ -104,17 +160,97 @@ export function SetupRuntimeFlow({ onRuntimeSelected }: SetupRuntimeFlowProps) {
d=
"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
d=
"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></
path
>
></
path
>
</
svg
>
</
svg
>
)
}
)
:
null
}
<
div
>
<
div
className=
"w-full"
>
<
p
className=
"font-medium text-base"
>
Local Node.js Runtim
e
</
p
>
<
p
className=
"font-medium text-base"
>
Full Access Mod
e
</
p
>
<
div
className=
"text-sm text-gray-500 dark:text-gray-400 mt-1"
>
<
div
className=
"text-sm text-gray-500 dark:text-gray-400 mt-1
w-full
"
>
<
p
>
<
p
>
Code will run using Node.js on your computer and have full
<
span
className=
"absolute top-4 right-2 bg-blue-100 text-blue-800 text-xs font-medium px-2 py-0.5 rounded-full"
>
system access.
Best for power users
</
span
>
<
p
>
Apps run directly on your computer with full access to your
system.
</
p
>
<
p
>
Supports advanced apps that require server-side capabilities.
</
p
>
</
p
>
</
p
>
<
p
className=
" mt-2 text-wrap wrap-break-word text-red-600 dark:text-red-400 bg-red-100 dark:bg-red-900 px-2 py-1 rounded-md"
>
{
showNodeInstallPrompt
&&
(
<
div
className=
"mt-4 p-4 border border-yellow-300 bg-yellow-50 dark:bg-yellow-900/30 rounded-md text-yellow-800 dark:text-yellow-200 w-full"
>
<
p
className=
"font-semibold"
>
Install Node.js
</
p
>
<
p
className=
"mt-1"
>
This mode requires Node.js to be installed.
</
p
>
{
downloadClicked
?
(
<
div
className=
"text-blue-400 cursor-pointer flex items-center"
onClick=
{
()
=>
{
IpcClient
.
getInstance
().
openExternalUrl
(
"https://nodejs.org/en/download#:~:text=Or%20get%20a%20prebuilt%20Node.js"
);
setDownloadClicked
(
true
);
}
}
>
Download Node.js
<
ExternalLink
className=
"w-3 h-3 ml-1"
/>
</
div
>
)
:
null
}
{
!
downloadClicked
?
(
<
Button
size=
"sm"
className=
" mt-3 w-full inline-flex items-center justify-center"
onClick=
{
()
=>
{
IpcClient
.
getInstance
().
openExternalUrl
(
"https://nodejs.org/en/download#:~:text=Or%20get%20a%20prebuilt%20Node.js"
);
setDownloadClicked
(
true
);
}
}
>
Download Node.js
<
ExternalLink
className=
"w-3 h-3 ml-1"
/>
</
Button
>
)
:
(
<
Button
size=
"sm"
className=
"mt-3 w-full"
onClick=
{
(
e
)
=>
{
e
.
stopPropagation
();
// Prevent outer button click
handleSelect
(
"local-node"
);
// Proceed with selection
}
}
disabled=
{
isLoading
===
"local-node"
}
// Disable while processing selection
>
{
isLoading
===
"local-node"
?
(
<
svg
className=
"animate-spin h-4 w-4 mr-2"
xmlns=
"http://www.w3.org/2000/svg"
fill=
"none"
viewBox=
"0 0 24 24"
>
<
circle
className=
"opacity-25"
cx=
"12"
cy=
"12"
r=
"10"
stroke=
"currentColor"
strokeWidth=
"4"
></
circle
>
<
path
className=
"opacity-75"
fill=
"currentColor"
d=
"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></
path
>
</
svg
>
)
:
null
}
Continue - I installed Node.js
</
Button
>
)
}
</
div
>
)
}
<
p
className=
"mt-4 text-wrap break-words text-red-600 dark:text-red-400 bg-red-100 dark:bg-red-900/50 px-2 py-1 rounded-md"
>
Warning: this will run AI-generated code directly on your
Warning: this will run AI-generated code directly on your
computer, which could put your
system
at risk.
computer, which could put your
computer
at risk.
</
p
>
</
p
>
</
div
>
</
div
>
</
div
>
</
div
>
...
...
src/ipc/handlers/app_handlers.ts
浏览文件 @
6a4f5388
...
@@ -209,7 +209,59 @@ async function executeAppLocalNode({
...
@@ -209,7 +209,59 @@ async function executeAppLocalNode({
});
});
}
}
function
checkCommandExists
(
command
:
string
):
Promise
<
string
|
null
>
{
return
new
Promise
((
resolve
)
=>
{
let
output
=
""
;
const
process
=
spawn
(
command
,
[
"--version"
],
{
shell
:
true
,
stdio
:
[
"ignore"
,
"pipe"
,
"pipe"
],
// ignore stdin, pipe stdout/stderr
});
process
.
stdout
?.
on
(
"data"
,
(
data
)
=>
{
output
+=
data
.
toString
();
});
process
.
stderr
?.
on
(
"data"
,
(
data
)
=>
{
// Log stderr but don't treat it as a failure unless the exit code is non-zero
console
.
warn
(
`Stderr from "
${
command
}
--version":
${
data
.
toString
().
trim
()}
`
);
});
process
.
on
(
"error"
,
(
error
)
=>
{
console
.
error
(
`Error executing command "
${
command
}
":`
,
error
.
message
);
resolve
(
null
);
// Command execution failed
});
process
.
on
(
"close"
,
(
code
)
=>
{
if
(
code
===
0
)
{
resolve
(
output
.
trim
());
// Command succeeded, return trimmed output
}
else
{
console
.
error
(
`Command "
${
command
}
--version" failed with code
${
code
}
`
);
resolve
(
null
);
// Command failed
}
});
});
}
export
function
registerAppHandlers
()
{
export
function
registerAppHandlers
()
{
ipcMain
.
handle
(
"nodejs-status"
,
async
():
Promise
<
{
nodeVersion
:
string
|
null
;
npmVersion
:
string
|
null
;
}
>
=>
{
// Run checks in parallel
const
[
nodeVersion
,
npmVersion
]
=
await
Promise
.
all
([
checkCommandExists
(
"node"
),
checkCommandExists
(
"npm"
),
]);
return
{
nodeVersion
,
npmVersion
};
}
);
ipcMain
.
handle
(
ipcMain
.
handle
(
"get-app-sandbox-config"
,
"get-app-sandbox-config"
,
async
(
_
,
{
appId
}:
{
appId
:
number
}):
Promise
<
SandboxConfig
>
=>
{
async
(
_
,
{
appId
}:
{
appId
:
number
}):
Promise
<
SandboxConfig
>
=>
{
...
...
src/ipc/ipc_client.ts
浏览文件 @
6a4f5388
...
@@ -488,4 +488,21 @@ export class IpcClient {
...
@@ -488,4 +488,21 @@ export class IpcClient {
throw
error
;
throw
error
;
}
}
}
}
// Check Node.js and npm status
public
async
getNodejsStatus
():
Promise
<
{
nodeVersion
:
string
|
null
;
npmVersion
:
string
|
null
;
}
>
{
try
{
const
result
=
await
this
.
ipcRenderer
.
invoke
(
"nodejs-status"
);
return
result
as
{
nodeVersion
:
string
|
null
;
npmVersion
:
string
|
null
;
};
}
catch
(
error
)
{
showError
(
error
);
throw
error
;
}
}
}
}
src/pages/home.tsx
浏览文件 @
6a4f5388
...
@@ -20,7 +20,7 @@ export default function HomePage() {
...
@@ -20,7 +20,7 @@ export default function HomePage() {
const
search
=
useSearch
({
from
:
"/"
});
const
search
=
useSearch
({
from
:
"/"
});
const
setSelectedAppId
=
useSetAtom
(
selectedAppIdAtom
);
const
setSelectedAppId
=
useSetAtom
(
selectedAppIdAtom
);
const
{
refreshApps
}
=
useLoadApps
();
const
{
refreshApps
}
=
useLoadApps
();
const
{
settings
,
isAnyProviderSetup
,
updateSettings
}
=
useSettings
();
const
{
settings
,
isAnyProviderSetup
}
=
useSettings
();
const
setIsPreviewOpen
=
useSetAtom
(
isPreviewOpenAtom
);
const
setIsPreviewOpen
=
useSetAtom
(
isPreviewOpenAtom
);
const
[
isLoading
,
setIsLoading
]
=
useState
(
false
);
const
[
isLoading
,
setIsLoading
]
=
useState
(
false
);
const
{
streamMessage
}
=
useStreamChat
();
const
{
streamMessage
}
=
useStreamChat
();
...
@@ -35,10 +35,6 @@ export default function HomePage() {
...
@@ -35,10 +35,6 @@ export default function HomePage() {
}
}
},
[
appId
,
navigate
]);
},
[
appId
,
navigate
]);
const
handleSetRuntimeMode
=
async
(
mode
:
RuntimeMode
)
=>
{
await
updateSettings
({
runtimeMode
:
mode
});
};
const
handleSubmit
=
async
()
=>
{
const
handleSubmit
=
async
()
=>
{
if
(
!
inputValue
.
trim
())
return
;
if
(
!
inputValue
.
trim
())
return
;
...
@@ -90,7 +86,7 @@ export default function HomePage() {
...
@@ -90,7 +86,7 @@ export default function HomePage() {
// Runtime Setup Flow
// Runtime Setup Flow
// Render this only if runtimeMode is not set in settings
// Render this only if runtimeMode is not set in settings
if
(
settings
?.
runtimeMode
===
"unset"
)
{
if
(
settings
?.
runtimeMode
===
"unset"
)
{
return
<
SetupRuntimeFlow
onRuntimeSelected=
{
handleSetRuntimeMode
}
/>;
return
<
SetupRuntimeFlow
/>;
}
}
// Main Home Page Content (Rendered only if runtimeMode is set)
// Main Home Page Content (Rendered only if runtimeMode is set)
...
...
src/pages/settings.tsx
浏览文件 @
6a4f5388
import
{
useState
}
from
"react"
;
import
{
useState
,
useEffect
}
from
"react"
;
import
{
useTheme
}
from
"../contexts/ThemeContext"
;
import
{
useTheme
}
from
"../contexts/ThemeContext"
;
import
{
ProviderSettingsGrid
}
from
"@/components/ProviderSettings"
;
import
{
ProviderSettingsGrid
}
from
"@/components/ProviderSettings"
;
import
ConfirmationDialog
from
"@/components/ConfirmationDialog"
;
import
ConfirmationDialog
from
"@/components/ConfirmationDialog"
;
...
@@ -6,96 +6,8 @@ import { IpcClient } from "@/ipc/ipc_client";
...
@@ -6,96 +6,8 @@ import { IpcClient } from "@/ipc/ipc_client";
import
{
showSuccess
,
showError
}
from
"@/lib/toast"
;
import
{
showSuccess
,
showError
}
from
"@/lib/toast"
;
import
{
useSettings
}
from
"@/hooks/useSettings"
;
import
{
useSettings
}
from
"@/hooks/useSettings"
;
import
{
RuntimeMode
}
from
"@/lib/schemas"
;
import
{
RuntimeMode
}
from
"@/lib/schemas"
;
import
{
Button
}
from
"@/components/ui/button"
;
// Helper component for runtime option buttons
import
{
ExternalLink
}
from
"lucide-react"
;
function
RuntimeOptionButton
({
title
,
description
,
badge
,
warning
,
isSelected
,
isLoading
,
onClick
,
disabled
,
}:
{
title
:
string
;
description
:
string
;
badge
?:
string
;
warning
?:
string
;
isSelected
:
boolean
;
isLoading
:
boolean
;
onClick
:
()
=>
void
;
disabled
:
boolean
;
})
{
return
(
<
button
onClick=
{
onClick
}
disabled=
{
disabled
||
isSelected
}
className=
{
`w-full justify-start p-6 h-auto text-left relative rounded-lg border transition-colors duration-150 group
${
isSelected
? "border-blue-500 dark:border-blue-400 bg-blue-50 dark:bg-gray-700 ring-2 ring-blue-300 dark:ring-blue-600"
: "bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/50"
}
${
disabled && !isSelected
? "opacity-50 cursor-not-allowed"
: "cursor-pointer"
}
`
}
>
{
isLoading
&&
(
<
div
className=
"absolute right-4 top-1/2 -translate-y-1/2"
>
{
/* Inline SVG Spinner */
}
<
svg
className=
"animate-spin h-5 w-5 text-gray-500 dark:text-gray-400"
xmlns=
"http://www.w3.org/2000/svg"
fill=
"none"
viewBox=
"0 0 24 24"
>
<
circle
className=
"opacity-25"
cx=
"12"
cy=
"12"
r=
"10"
stroke=
"currentColor"
strokeWidth=
"4"
></
circle
>
<
path
className=
"opacity-75"
fill=
"currentColor"
d=
"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></
path
>
</
svg
>
</
div
>
)
}
{
badge
&&
(
<
span
className=
"absolute top-2 right-2 bg-blue-100 text-blue-800 text-xs font-medium px-2 py-0.5 rounded-full group-hover:bg-blue-200 dark:group-hover:bg-blue-900"
>
{
badge
}
</
span
>
)
}
<
div
>
<
p
className=
{
`font-medium text-base ${
isSelected
? "text-blue-800 dark:text-blue-100"
: "text-gray-900 dark:text-white"
}`
}
>
{
title
}
</
p
>
<
p
className=
"text-sm text-gray-500 dark:text-gray-400 mt-1"
>
{
description
}
</
p
>
{
warning
&&
(
<
p
className=
"mt-2 text-wrap break-word text-red-600 dark:text-red-400 bg-red-100 dark:bg-red-900 px-2 py-1 rounded-md text-xs"
>
{
warning
}
</
p
>
)
}
</
div
>
</
button
>
);
}
export
default
function
SettingsPage
()
{
export
default
function
SettingsPage
()
{
const
{
theme
,
setTheme
}
=
useTheme
();
const
{
theme
,
setTheme
}
=
useTheme
();
...
@@ -107,7 +19,28 @@ export default function SettingsPage() {
...
@@ -107,7 +19,28 @@ export default function SettingsPage() {
}
=
useSettings
();
}
=
useSettings
();
const
[
isResetDialogOpen
,
setIsResetDialogOpen
]
=
useState
(
false
);
const
[
isResetDialogOpen
,
setIsResetDialogOpen
]
=
useState
(
false
);
const
[
isResetting
,
setIsResetting
]
=
useState
(
false
);
const
[
isResetting
,
setIsResetting
]
=
useState
(
false
);
const
[
isUpdatingRuntime
,
setIsUpdatingRuntime
]
=
useState
(
false
);
const
[
isUpdatingRuntime
,
setIsUpdatingRuntime
]
=
useState
<
RuntimeMode
|
"check"
|
null
>
(
null
);
const
[
showNodeInstallPrompt
,
setShowNodeInstallPrompt
]
=
useState
(
false
);
const
[
nodeVersion
,
setNodeVersion
]
=
useState
<
string
|
null
>
(
null
);
const
[
npmVersion
,
setNpmVersion
]
=
useState
<
string
|
null
>
(
null
);
const
[
downloadClicked
,
setDownloadClicked
]
=
useState
(
false
);
useEffect
(()
=>
{
const
checkNode
=
async
()
=>
{
try
{
const
status
=
await
IpcClient
.
getInstance
().
getNodejsStatus
();
setNodeVersion
(
status
.
nodeVersion
);
setNpmVersion
(
status
.
npmVersion
);
}
catch
(
error
)
{
console
.
error
(
"Failed to check Node.js status:"
,
error
);
setNodeVersion
(
null
);
setNpmVersion
(
null
);
}
};
checkNode
();
},
[]);
const
handleResetEverything
=
async
()
=>
{
const
handleResetEverything
=
async
()
=>
{
setIsResetting
(
true
);
setIsResetting
(
true
);
...
@@ -132,7 +65,10 @@ export default function SettingsPage() {
...
@@ -132,7 +65,10 @@ export default function SettingsPage() {
const
handleRuntimeChange
=
async
(
newMode
:
RuntimeMode
)
=>
{
const
handleRuntimeChange
=
async
(
newMode
:
RuntimeMode
)
=>
{
if
(
newMode
===
settings
?.
runtimeMode
||
isUpdatingRuntime
)
return
;
if
(
newMode
===
settings
?.
runtimeMode
||
isUpdatingRuntime
)
return
;
setIsUpdatingRuntime
(
true
);
setIsUpdatingRuntime
(
newMode
);
setShowNodeInstallPrompt
(
false
);
setDownloadClicked
(
false
);
try
{
try
{
await
updateSettings
({
runtimeMode
:
newMode
});
await
updateSettings
({
runtimeMode
:
newMode
});
showSuccess
(
"Runtime mode updated successfully."
);
showSuccess
(
"Runtime mode updated successfully."
);
...
@@ -144,7 +80,34 @@ export default function SettingsPage() {
...
@@ -144,7 +80,34 @@ export default function SettingsPage() {
:
"Failed to update runtime mode."
:
"Failed to update runtime mode."
);
);
}
finally
{
}
finally
{
setIsUpdatingRuntime
(
false
);
setIsUpdatingRuntime
(
null
);
}
};
const
handleLocalNodeClick
=
async
()
=>
{
if
(
isUpdatingRuntime
)
return
;
if
(
nodeVersion
&&
npmVersion
)
{
handleRuntimeChange
(
"local-node"
);
return
;
}
setIsUpdatingRuntime
(
"check"
);
try
{
const
status
=
await
IpcClient
.
getInstance
().
getNodejsStatus
();
setNodeVersion
(
status
.
nodeVersion
);
setNpmVersion
(
status
.
npmVersion
);
if
(
status
.
nodeVersion
&&
status
.
npmVersion
)
{
handleRuntimeChange
(
"local-node"
);
}
else
{
setShowNodeInstallPrompt
(
true
);
setIsUpdatingRuntime
(
null
);
}
}
catch
(
error
)
{
console
.
error
(
"Failed to check Node.js status on click:"
,
error
);
setShowNodeInstallPrompt
(
true
);
setIsUpdatingRuntime
(
null
);
showError
(
"Could not verify Node.js installation."
);
}
}
};
};
...
@@ -201,8 +164,8 @@ export default function SettingsPage() {
...
@@ -201,8 +164,8 @@ export default function SettingsPage() {
Runtime Environment
Runtime Environment
</
h2
>
</
h2
>
<
p
className=
"text-sm text-gray-500 dark:text-gray-400 mb-4"
>
<
p
className=
"text-sm text-gray-500 dark:text-gray-400 mb-4"
>
Choose how app code is executed. This affects performance
and
Choose how app code is executed. This affects performance
,
security.
security
, and capabilities
.
</
p
>
</
p
>
{
settingsLoading
?
(
{
settingsLoading
?
(
...
@@ -235,28 +198,207 @@ export default function SettingsPage() {
...
@@ -235,28 +198,207 @@ export default function SettingsPage() {
</
p
>
</
p
>
)
:
(
)
:
(
<
div
className=
"space-y-4"
>
<
div
className=
"space-y-4"
>
<
RuntimeOptionButton
<
Button
title=
"Sandboxed Runtime"
variant=
{
description=
"Code runs inside a browser sandbox. Safer, limited system access."
currentRuntimeMode
===
"web-sandbox"
?
"default"
:
"outline"
badge=
"Recommended for beginners"
isSelected=
{
currentRuntimeMode
===
"web-sandbox"
}
isLoading=
{
isUpdatingRuntime
&&
currentRuntimeMode
!==
"web-sandbox"
}
}
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"
)
}
onClick=
{
()
=>
handleRuntimeChange
(
"web-sandbox"
)
}
disabled=
{
isUpdatingRuntime
}
disabled=
{
/>
isUpdatingRuntime
!==
null
||
<
RuntimeOptionButton
currentRuntimeMode
===
"web-sandbox"
title=
"Local Node.js Runtime"
}
description=
"Code runs using Node.js on your computer. Full system access."
>
warning=
"Warning: Running AI-generated code directly on your computer can be risky. Only use code from trusted sources."
{
isUpdatingRuntime
===
"web-sandbox"
&&
(
isSelected=
{
currentRuntimeMode
===
"local-node"
}
<
svg
isLoading=
{
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"
isUpdatingRuntime && currentRuntimeMode !== "local-node"
? "opacity-50 cursor-not-allowed"
: "cursor-pointer"
}`
}
onClick=
{
handleLocalNodeClick
}
disabled=
{
isUpdatingRuntime
!==
null
||
currentRuntimeMode
===
"local-node"
}
}
onClick=
{
()
=>
handleRuntimeChange
(
"local-node"
)
}
>
disabled=
{
isUpdatingRuntime
}
{
(
isUpdatingRuntime
===
"check"
||
/>
isUpdatingRuntime
===
"local-node"
)
&&
(
<
svg
className=
"animate-spin h-5 w-5 absolute right-4 top-6 text-gray-500 dark:text-gray-400"
xmlns=
"http://www.w3.org/2000/svg"
fill=
"none"
viewBox=
"0 0 24 24"
>
<
circle
className=
"opacity-25"
cx=
"12"
cy=
"12"
r=
"10"
stroke=
"currentColor"
strokeWidth=
"4"
></
circle
>
<
path
className=
"opacity-75"
fill=
"currentColor"
d=
"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></
path
>
</
svg
>
)
}
<
span
className=
"absolute top-2 right-2 bg-blue-100 text-blue-800 text-xs font-medium px-2 py-0.5 rounded-full group-hover:bg-blue-200 dark:group-hover:bg-blue-900"
>
Best for power users
</
span
>
<
div
className=
"w-full"
>
<
p
className=
{
`font-medium text-base ${
currentRuntimeMode === "local-node"
? "text-blue-800 dark:text-blue-100"
: "text-gray-900 dark:text-white"
}`
}
>
Full Access Mode
</
p
>
<
div
className=
"text-sm text-gray-500 dark:text-gray-400 mt-1 w-full"
>
<
p
>
Apps run directly on your computer with full access to
your system.
</
p
>
<
p
>
Supports advanced apps that require server-side
capabilities.
</
p
>
{
showNodeInstallPrompt
&&
(
<
div
className=
"mt-4 p-4 border border-yellow-300 bg-yellow-50 dark:bg-yellow-900/30 rounded-md text-yellow-800 dark:text-yellow-200 w-full"
>
<
p
className=
"font-semibold"
>
Install Node.js
</
p
>
<
p
className=
"mt-1 text-xs"
>
This mode requires Node.js and npm to be installed
and accessible in your system's PATH.
</
p
>
{
!
downloadClicked
?
(
<
Button
variant=
"link"
size=
"sm"
className=
"p-0 h-auto mt-2 text-blue-600 dark:text-blue-400 hover:underline flex items-center"
onClick=
{
(
e
)
=>
{
e
.
stopPropagation
();
IpcClient
.
getInstance
().
openExternalUrl
(
"https://nodejs.org/en/download"
);
setDownloadClicked
(
true
);
}
}
>
Download Node.js
{
" "
}
<
ExternalLink
className=
"w-3 h-3 ml-1"
/>
</
Button
>
)
:
(
<
p
className=
"mt-2 text-xs text-gray-600 dark:text-gray-400"
>
Node.js download page opened.
</
p
>
)
}
<
Button
variant=
"default"
size=
"sm"
className=
"mt-3 w-full"
onClick=
{
(
e
)
=>
{
e
.
stopPropagation
();
handleRuntimeChange
(
"local-node"
);
}
}
disabled=
{
isUpdatingRuntime
===
"local-node"
}
>
{
isUpdatingRuntime
===
"local-node"
?
(
<
svg
className=
"animate-spin h-4 w-4 mr-2"
xmlns=
"http://www.w3.org/2000/svg"
fill=
"none"
viewBox=
"0 0 24 24"
>
<
circle
className=
"opacity-25"
cx=
"12"
cy=
"12"
r=
"10"
stroke=
"currentColor"
strokeWidth=
"4"
></
circle
>
<
path
className=
"opacity-75"
fill=
"currentColor"
d=
"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></
path
>
</
svg
>
)
:
null
}
Continue - I have installed Node.js
</
Button
>
</
div
>
)
}
<
p
className=
"mt-4 text-wrap break-words text-red-600 dark:text-red-400 bg-red-100 dark:bg-red-900/50 px-2 py-1 rounded-md text-xs"
>
Warning: This mode runs AI-generated code directly on
your computer, which can be risky. Only use code from
trusted sources or review it carefully.
</
p
>
</
div
>
</
div
>
</
Button
>
</
div
>
</
div
>
)
}
)
}
</
div
>
</
div
>
...
...
src/preload.ts
浏览文件 @
6a4f5388
...
@@ -31,6 +31,7 @@ const validInvokeChannels = [
...
@@ -31,6 +31,7 @@ const validInvokeChannels = [
"get-env-vars"
,
"get-env-vars"
,
"open-external-url"
,
"open-external-url"
,
"reset-all"
,
"reset-all"
,
"nodejs-status"
,
]
as
const
;
]
as
const
;
// Add valid receive channels
// Add valid receive channels
...
...
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论