Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
B
bit-pm
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
燕伟桐
bit-pm
Commits
9d1a0f7a
Unverified
提交
9d1a0f7a
authored
6月 04, 2025
作者:
Will Chen
提交者:
GitHub
6月 04, 2025
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
pro: show remaining credits (#329)
Fixes #265
上级
0f4e5322
隐藏空白字符变更
内嵌
并排
正在显示
8 个修改的文件
包含
182 行增加
和
21 行删除
+182
-21
TitleBar.tsx
src/app/TitleBar.tsx
+64
-20
useStreamChat.ts
src/hooks/useStreamChat.ts
+5
-1
useUserBudgetInfo.ts
src/hooks/useUserBudgetInfo.ts
+34
-0
pro_handlers.ts
src/ipc/handlers/pro_handlers.ts
+61
-0
ipc_client.ts
src/ipc/ipc_client.ts
+6
-0
ipc_host.ts
src/ipc/ipc_host.ts
+2
-0
ipc_types.ts
src/ipc/ipc_types.ts
+9
-0
preload.ts
src/preload.ts
+1
-0
没有找到文件。
src/app/TitleBar.tsx
浏览文件 @
9d1a0f7a
...
@@ -13,6 +13,13 @@ import { useEffect, useState } from "react";
...
@@ -13,6 +13,13 @@ import { useEffect, useState } from "react";
import
{
DyadProSuccessDialog
}
from
"@/components/DyadProSuccessDialog"
;
import
{
DyadProSuccessDialog
}
from
"@/components/DyadProSuccessDialog"
;
import
{
useTheme
}
from
"@/contexts/ThemeContext"
;
import
{
useTheme
}
from
"@/contexts/ThemeContext"
;
import
{
IpcClient
}
from
"@/ipc/ipc_client"
;
import
{
IpcClient
}
from
"@/ipc/ipc_client"
;
import
{
useUserBudgetInfo
}
from
"@/hooks/useUserBudgetInfo"
;
import
{
UserBudgetInfo
}
from
"@/ipc/ipc_types"
;
import
{
Tooltip
,
TooltipContent
,
TooltipTrigger
,
}
from
"@/components/ui/tooltip"
;
export
const
TitleBar
=
()
=>
{
export
const
TitleBar
=
()
=>
{
const
[
selectedAppId
]
=
useAtom
(
selectedAppIdAtom
);
const
[
selectedAppId
]
=
useAtom
(
selectedAppIdAtom
);
...
@@ -64,7 +71,7 @@ export const TitleBar = () => {
...
@@ -64,7 +71,7 @@ export const TitleBar = () => {
};
};
const
isDyadPro
=
!!
settings
?.
providerSettings
?.
auto
?.
apiKey
?.
value
;
const
isDyadPro
=
!!
settings
?.
providerSettings
?.
auto
?.
apiKey
?.
value
;
const
isDyadProEnabled
=
settings
?.
enableDyadPro
;
const
isDyadProEnabled
=
Boolean
(
settings
?.
enableDyadPro
)
;
return
(
return
(
<>
<>
...
@@ -82,25 +89,7 @@ export const TitleBar = () => {
...
@@ -82,25 +89,7 @@ export const TitleBar = () => {
>
>
{
displayText
}
{
displayText
}
</
Button
>
</
Button
>
{
isDyadPro
&&
(
{
isDyadPro
&&
<
DyadProButton
isDyadProEnabled=
{
isDyadProEnabled
}
/>
}
<
Button
data
-
testid=
"title-bar-dyad-pro-button"
onClick=
{
()
=>
{
navigate
({
to
:
providerSettingsRoute
.
id
,
params
:
{
provider
:
"auto"
},
});
}
}
variant=
"outline"
className=
{
cn
(
"ml-4 no-app-region-drag h-7 bg-indigo-600 text-white dark:bg-indigo-600 dark:text-white"
,
!
isDyadProEnabled
&&
"bg-zinc-600 dark:bg-zinc-600"
,
)
}
size=
"sm"
>
{
isDyadProEnabled
?
"Dyad Pro"
:
"Dyad Pro (disabled)"
}
</
Button
>
)
}
{
showWindowControls
&&
<
WindowsControls
/>
}
{
showWindowControls
&&
<
WindowsControls
/>
}
</
div
>
</
div
>
...
@@ -192,3 +181,58 @@ function WindowsControls() {
...
@@ -192,3 +181,58 @@ function WindowsControls() {
</
div
>
</
div
>
);
);
}
}
export
function
DyadProButton
({
isDyadProEnabled
,
}:
{
isDyadProEnabled
:
boolean
;
})
{
const
{
navigate
}
=
useRouter
();
const
{
userBudget
}
=
useUserBudgetInfo
();
return
(
<
Button
data
-
testid=
"title-bar-dyad-pro-button"
onClick=
{
()
=>
{
navigate
({
to
:
providerSettingsRoute
.
id
,
params
:
{
provider
:
"auto"
},
});
}
}
variant=
"outline"
className=
{
cn
(
"ml-4 no-app-region-drag h-7 bg-indigo-600 text-white dark:bg-indigo-600 dark:text-white"
,
!
isDyadProEnabled
&&
"bg-zinc-600 dark:bg-zinc-600"
,
)
}
size=
"sm"
>
{
isDyadProEnabled
?
"Dyad Pro"
:
"Dyad Pro (disabled)"
}
{
userBudget
&&
<
AICreditStatus
userBudget=
{
userBudget
}
/>
}
</
Button
>
);
}
export
function
AICreditStatus
({
userBudget
}:
{
userBudget
:
UserBudgetInfo
})
{
const
remaining
=
Math
.
round
(
userBudget
.
totalCredits
-
userBudget
.
usedCredits
,
);
return
(
<
Tooltip
>
<
TooltipTrigger
>
<
div
className=
"text-xs mt-0.5"
>
{
remaining
}
credits left
</
div
>
</
TooltipTrigger
>
<
TooltipContent
>
<
div
>
<
p
>
You have used
{
Math
.
round
(
userBudget
.
usedCredits
)
}
credits out of
{
" "
}
{
userBudget
.
totalCredits
}
.
</
p
>
<
p
>
Your budget resets on
{
" "
}
{
userBudget
.
budgetResetDate
.
toLocaleDateString
()
}
</
p
>
<
p
>
Note: there is a slight delay in updating the credit status.
</
p
>
</
div
>
</
TooltipContent
>
</
Tooltip
>
);
}
src/hooks/useStreamChat.ts
浏览文件 @
9d1a0f7a
...
@@ -19,6 +19,7 @@ import { useProposal } from "./useProposal";
...
@@ -19,6 +19,7 @@ import { useProposal } from "./useProposal";
import
{
useSearch
}
from
"@tanstack/react-router"
;
import
{
useSearch
}
from
"@tanstack/react-router"
;
import
{
useRunApp
}
from
"./useRunApp"
;
import
{
useRunApp
}
from
"./useRunApp"
;
import
{
useCountTokens
}
from
"./useCountTokens"
;
import
{
useCountTokens
}
from
"./useCountTokens"
;
import
{
useUserBudgetInfo
}
from
"./useUserBudgetInfo"
;
export
function
getRandomNumberId
()
{
export
function
getRandomNumberId
()
{
return
Math
.
floor
(
Math
.
random
()
*
1
_000_000_000_000_000
);
return
Math
.
floor
(
Math
.
random
()
*
1
_000_000_000_000_000
);
...
@@ -38,6 +39,7 @@ export function useStreamChat({
...
@@ -38,6 +39,7 @@ export function useStreamChat({
const
{
refreshVersions
}
=
useVersions
(
selectedAppId
);
const
{
refreshVersions
}
=
useVersions
(
selectedAppId
);
const
{
refreshAppIframe
}
=
useRunApp
();
const
{
refreshAppIframe
}
=
useRunApp
();
const
{
countTokens
}
=
useCountTokens
();
const
{
countTokens
}
=
useCountTokens
();
const
{
refetchUserBudget
}
=
useUserBudgetInfo
();
let
chatId
:
number
|
undefined
;
let
chatId
:
number
|
undefined
;
...
@@ -95,6 +97,8 @@ export function useStreamChat({
...
@@ -95,6 +97,8 @@ export function useStreamChat({
}
}
refreshProposal
(
chatId
);
refreshProposal
(
chatId
);
refetchUserBudget
();
// Keep the same as below
// Keep the same as below
setIsStreaming
(
false
);
setIsStreaming
(
false
);
refreshChats
();
refreshChats
();
...
@@ -120,7 +124,7 @@ export function useStreamChat({
...
@@ -120,7 +124,7 @@ export function useStreamChat({
setError
(
error
instanceof
Error
?
error
.
message
:
String
(
error
));
setError
(
error
instanceof
Error
?
error
.
message
:
String
(
error
));
}
}
},
},
[
setMessages
,
setIsStreaming
,
setIsPreviewOpen
],
[
setMessages
,
setIsStreaming
,
setIsPreviewOpen
,
refetchUserBudget
],
);
);
return
{
return
{
...
...
src/hooks/useUserBudgetInfo.ts
0 → 100644
浏览文件 @
9d1a0f7a
import
{
useQuery
}
from
"@tanstack/react-query"
;
import
{
IpcClient
}
from
"@/ipc/ipc_client"
;
import
type
{
UserBudgetInfo
}
from
"@/ipc/ipc_types"
;
const
FIVE_MINUTES_IN_MS
=
5
*
60
*
1000
;
export
function
useUserBudgetInfo
()
{
const
queryKey
=
[
"userBudgetInfo"
];
const
{
data
,
isLoading
,
error
,
isFetching
,
refetch
}
=
useQuery
<
UserBudgetInfo
|
null
,
Error
,
UserBudgetInfo
|
null
>
({
queryKey
:
queryKey
,
queryFn
:
async
()
=>
{
const
ipcClient
=
IpcClient
.
getInstance
();
return
ipcClient
.
getUserBudget
();
},
// This data is not critical and can be stale for a bit
staleTime
:
FIVE_MINUTES_IN_MS
,
// If an error occurs (e.g. API key not set), it returns null.
// We don't want react-query to retry automatically in such cases as it's not a transient network error.
retry
:
false
,
});
return
{
userBudget
:
data
,
isLoadingUserBudget
:
isLoading
,
userBudgetError
:
error
,
isFetchingUserBudget
:
isFetching
,
refetchUserBudget
:
refetch
,
};
}
src/ipc/handlers/pro_handlers.ts
0 → 100644
浏览文件 @
9d1a0f7a
import
fetch
from
"node-fetch"
;
// Electron main process might need node-fetch
import
log
from
"electron-log"
;
import
{
createLoggedHandler
}
from
"./safe_handle"
;
import
{
readSettings
}
from
"../../main/settings"
;
// Assuming settings are read this way
import
{
UserBudgetInfoSchema
}
from
"../ipc_types"
;
const
logger
=
log
.
scope
(
"pro_handlers"
);
const
handle
=
createLoggedHandler
(
logger
);
const
CONVERSION_RATIO
=
(
10
*
3
)
/
2
;
export
function
registerProHandlers
()
{
// This method should try to avoid throwing errors because this is auxiliary
// information and isn't critical to using the app
handle
(
"get-user-budget"
,
async
():
Promise
<
UserBudgetInfo
|
null
>
=>
{
logger
.
info
(
"Attempting to fetch user budget information."
);
const
settings
=
readSettings
();
const
apiKey
=
settings
.
providerSettings
?.
auto
?.
apiKey
?.
value
;
if
(
!
apiKey
)
{
logger
.
error
(
"LLM Gateway API key (Dyad Pro) is not configured."
);
return
null
;
}
const
url
=
"https://llm-gateway.dyad.sh/user/info"
;
const
headers
=
{
"Content-Type"
:
"application/json"
,
Authorization
:
`Bearer
${
apiKey
}
`
,
};
try
{
// Use native fetch if available, otherwise node-fetch will be used via import
const
response
=
await
fetch
(
url
,
{
method
:
"GET"
,
headers
:
headers
,
});
if
(
!
response
.
ok
)
{
const
errorBody
=
await
response
.
text
();
logger
.
error
(
`Failed to fetch user budget. Status:
${
response
.
status
}
. Body:
${
errorBody
}
`
,
);
return
null
;
}
const
data
=
await
response
.
json
();
const
userInfoData
=
data
[
"user_info"
];
logger
.
info
(
"Successfully fetched user budget information."
);
return
UserBudgetInfoSchema
.
parse
({
usedCredits
:
userInfoData
[
"spend"
]
*
CONVERSION_RATIO
,
totalCredits
:
userInfoData
[
"max_budget"
]
*
CONVERSION_RATIO
,
budgetResetDate
:
new
Date
(
userInfoData
[
"budget_reset_at"
]),
});
}
catch
(
error
:
any
)
{
logger
.
error
(
`Error fetching user budget:
${
error
.
message
}
`
,
error
);
return
null
;
}
});
}
src/ipc/ipc_client.ts
浏览文件 @
9d1a0f7a
...
@@ -30,6 +30,7 @@ import type {
...
@@ -30,6 +30,7 @@ import type {
ImportAppResult
,
ImportAppResult
,
ImportAppParams
,
ImportAppParams
,
RenameBranchParams
,
RenameBranchParams
,
UserBudgetInfo
,
}
from
"./ipc_types"
;
}
from
"./ipc_types"
;
import
type
{
ProposalResult
}
from
"@/lib/schemas"
;
import
type
{
ProposalResult
}
from
"@/lib/schemas"
;
import
{
showError
}
from
"@/lib/toast"
;
import
{
showError
}
from
"@/lib/toast"
;
...
@@ -825,4 +826,9 @@ export class IpcClient {
...
@@ -825,4 +826,9 @@ export class IpcClient {
async
clearSessionData
():
Promise
<
void
>
{
async
clearSessionData
():
Promise
<
void
>
{
return
this
.
ipcRenderer
.
invoke
(
"clear-session-data"
);
return
this
.
ipcRenderer
.
invoke
(
"clear-session-data"
);
}
}
// Method to get user budget information
public
async
getUserBudget
():
Promise
<
UserBudgetInfo
|
null
>
{
return
this
.
ipcRenderer
.
invoke
(
"get-user-budget"
);
}
}
}
src/ipc/ipc_host.ts
浏览文件 @
9d1a0f7a
...
@@ -18,6 +18,7 @@ import { registerLanguageModelHandlers } from "./handlers/language_model_handler
...
@@ -18,6 +18,7 @@ import { registerLanguageModelHandlers } from "./handlers/language_model_handler
import
{
registerReleaseNoteHandlers
}
from
"./handlers/release_note_handlers"
;
import
{
registerReleaseNoteHandlers
}
from
"./handlers/release_note_handlers"
;
import
{
registerImportHandlers
}
from
"./handlers/import_handlers"
;
import
{
registerImportHandlers
}
from
"./handlers/import_handlers"
;
import
{
registerSessionHandlers
}
from
"./handlers/session_handlers"
;
import
{
registerSessionHandlers
}
from
"./handlers/session_handlers"
;
import
{
registerProHandlers
}
from
"./handlers/pro_handlers"
;
export
function
registerIpcHandlers
()
{
export
function
registerIpcHandlers
()
{
// Register all IPC handlers by category
// Register all IPC handlers by category
...
@@ -41,4 +42,5 @@ export function registerIpcHandlers() {
...
@@ -41,4 +42,5 @@ export function registerIpcHandlers() {
registerReleaseNoteHandlers
();
registerReleaseNoteHandlers
();
registerImportHandlers
();
registerImportHandlers
();
registerSessionHandlers
();
registerSessionHandlers
();
registerProHandlers
();
}
}
src/ipc/ipc_types.ts
浏览文件 @
9d1a0f7a
import
{
z
}
from
"zod"
;
export
interface
AppOutput
{
export
interface
AppOutput
{
type
:
"stdout"
|
"stderr"
|
"info"
|
"client-error"
;
type
:
"stdout"
|
"stderr"
|
"info"
|
"client-error"
;
message
:
string
;
message
:
string
;
...
@@ -207,3 +209,10 @@ export interface RenameBranchParams {
...
@@ -207,3 +209,10 @@ export interface RenameBranchParams {
oldBranchName
:
string
;
oldBranchName
:
string
;
newBranchName
:
string
;
newBranchName
:
string
;
}
}
export
const
UserBudgetInfoSchema
=
z
.
object
({
usedCredits
:
z
.
number
(),
totalCredits
:
z
.
number
(),
budgetResetDate
:
z
.
date
(),
});
export
type
UserBudgetInfo
=
z
.
infer
<
typeof
UserBudgetInfoSchema
>
;
src/preload.ts
浏览文件 @
9d1a0f7a
...
@@ -75,6 +75,7 @@ const validInvokeChannels = [
...
@@ -75,6 +75,7 @@ const validInvokeChannels = [
"check-app-name"
,
"check-app-name"
,
"rename-branch"
,
"rename-branch"
,
"clear-session-data"
,
"clear-session-data"
,
"get-user-budget"
,
]
as
const
;
]
as
const
;
// Add valid receive channels
// Add valid receive channels
...
...
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论