Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
B
bit-pm
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
燕伟桐
bit-pm
Commits
b2e3631a
提交
b2e3631a
authored
4月 18, 2025
作者:
Will Chen
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
Refactor chat input & properly update state for proposal
上级
db7ac39c
显示空白字符变更
内嵌
并排
正在显示
10 个修改的文件
包含
136 行增加
和
44 行删除
+136
-44
chatAtoms.ts
src/atoms/chatAtoms.ts
+1
-0
proposalAtoms.ts
src/atoms/proposalAtoms.ts
+4
-0
ChatInput.tsx
src/components/chat/ChatInput.tsx
+22
-13
HomeChatInput.tsx
src/components/chat/HomeChatInput.tsx
+85
-0
useProposal.ts
src/hooks/useProposal.ts
+12
-16
useStreamChat.ts
src/hooks/useStreamChat.ts
+1
-2
chat_stream_handlers.ts
src/ipc/handlers/chat_stream_handlers.ts
+1
-1
ipc_client.ts
src/ipc/ipc_client.ts
+1
-7
schemas.ts
src/lib/schemas.ts
+5
-0
home.tsx
src/pages/home.tsx
+4
-5
没有找到文件。
src/atoms/chatAtoms.ts
浏览文件 @
b2e3631a
...
...
@@ -11,6 +11,7 @@ export const selectedChatIdAtom = atom<number | null>(null);
export
const
isStreamingAtom
=
atom
<
boolean
>
(
false
);
export
const
chatInputValueAtom
=
atom
<
string
>
(
""
);
export
const
homeChatInputValueAtom
=
atom
<
string
>
(
""
);
// Atoms for chat list management
export
const
chatsAtom
=
atom
<
ChatSummary
[]
>
([]);
...
...
src/atoms/proposalAtoms.ts
0 → 100644
浏览文件 @
b2e3631a
import
{
atom
}
from
"jotai"
;
import
type
{
Proposal
,
ProposalResult
}
from
"@/lib/schemas"
;
export
const
proposalResultAtom
=
atom
<
ProposalResult
|
null
>
(
null
);
src/components/chat/ChatInput.tsx
浏览文件 @
b2e3631a
...
...
@@ -11,12 +11,12 @@ import {
Loader2
,
}
from
"lucide-react"
;
import
type
React
from
"react"
;
import
{
useEffect
,
useRef
,
useState
}
from
"react"
;
import
{
use
Callback
,
use
Effect
,
useRef
,
useState
}
from
"react"
;
import
{
ModelPicker
}
from
"@/components/ModelPicker"
;
import
{
useSettings
}
from
"@/hooks/useSettings"
;
import
{
IpcClient
}
from
"@/ipc/ipc_client"
;
import
{
chatInputValueAtom
}
from
"@/atoms/chatAtoms"
;
import
{
useAtom
}
from
"jotai"
;
import
{
chatInputValueAtom
,
chatMessagesAtom
}
from
"@/atoms/chatAtoms"
;
import
{
useAtom
,
useSetAtom
}
from
"jotai"
;
import
{
useStreamChat
}
from
"@/hooks/useStreamChat"
;
import
{
useChats
}
from
"@/hooks/useChats"
;
import
{
selectedAppIdAtom
}
from
"@/atoms/appAtoms"
;
...
...
@@ -26,7 +26,8 @@ import { Label } from "@/components/ui/label";
import
{
Switch
}
from
"@/components/ui/switch"
;
import
{
useProposal
}
from
"@/hooks/useProposal"
;
import
{
Proposal
}
from
"@/lib/schemas"
;
import
type
{
Message
}
from
"@/ipc/ipc_types"
;
import
{
isPreviewOpenAtom
}
from
"@/atoms/viewAtoms"
;
interface
ChatInputActionsProps
{
proposal
:
Proposal
;
onApprove
:
()
=>
void
;
...
...
@@ -36,12 +37,7 @@ interface ChatInputActionsProps {
isRejecting
:
boolean
;
// State for rejecting
}
interface
ChatInputProps
{
chatId
?:
number
;
onSubmit
?:
()
=>
void
;
}
export
function
ChatInput
({
chatId
,
onSubmit
}:
ChatInputProps
)
{
export
function
ChatInput
({
chatId
}:
{
chatId
?:
number
})
{
const
[
inputValue
,
setInputValue
]
=
useAtom
(
chatInputValueAtom
);
const
textareaRef
=
useRef
<
HTMLTextAreaElement
>
(
null
);
const
{
settings
,
updateSettings
,
isAnyProviderSetup
}
=
useSettings
();
...
...
@@ -51,6 +47,8 @@ export function ChatInput({ chatId, onSubmit }: ChatInputProps) {
const
[
showError
,
setShowError
]
=
useState
(
true
);
const
[
isApproving
,
setIsApproving
]
=
useState
(
false
);
// State for approving
const
[
isRejecting
,
setIsRejecting
]
=
useState
(
false
);
// State for rejecting
const
[
messages
,
setMessages
]
=
useAtom
<
Message
[]
>
(
chatMessagesAtom
);
const
setIsPreviewOpen
=
useSetAtom
(
isPreviewOpenAtom
);
// Use the hook to fetch the proposal
const
{
...
...
@@ -80,10 +78,19 @@ export function ChatInput({ chatId, onSubmit }: ChatInputProps) {
}
},
[
error
]);
const
fetchChatMessages
=
useCallback
(
async
()
=>
{
if
(
!
chatId
)
{
setMessages
([]);
return
;
}
const
chat
=
await
IpcClient
.
getInstance
().
getChat
(
chatId
);
setMessages
(
chat
.
messages
);
},
[
chatId
,
setMessages
]);
const
handleKeyPress
=
(
e
:
React
.
KeyboardEvent
)
=>
{
if
(
e
.
key
===
"Enter"
&&
!
e
.
shiftKey
)
{
e
.
preventDefault
();
submitHandler
();
handleSubmit
();
}
};
...
...
@@ -96,7 +103,6 @@ export function ChatInput({ chatId, onSubmit }: ChatInputProps) {
setInputValue
(
""
);
await
streamMessage
({
prompt
:
currentInput
,
chatId
});
};
const
submitHandler
=
onSubmit
?
onSubmit
:
handleSubmit
;
const
handleCancel
=
()
=>
{
if
(
chatId
)
{
...
...
@@ -133,7 +139,9 @@ export function ChatInput({ chatId, onSubmit }: ChatInputProps) {
setError
((
err
as
Error
)?.
message
||
"An error occurred while approving"
);
}
finally
{
setIsApproving
(
false
);
setIsPreviewOpen
(
true
);
refreshProposal
();
fetchChatMessages
();
}
};
...
...
@@ -162,6 +170,7 @@ export function ChatInput({ chatId, onSubmit }: ChatInputProps) {
}
finally
{
setIsRejecting
(
false
);
refreshProposal
();
fetchChatMessages
();
}
};
...
...
@@ -236,7 +245,7 @@ export function ChatInput({ chatId, onSubmit }: ChatInputProps) {
</
button
>
)
:
(
<
button
onClick=
{
submitHandler
}
onClick=
{
handleSubmit
}
disabled=
{
!
inputValue
.
trim
()
||
!
isAnyProviderSetup
()
}
className=
"px-2 py-2 mt-1 mr-2 hover:bg-(--background-darkest) text-(--sidebar-accent-fg) rounded-lg disabled:opacity-50"
>
...
...
src/components/chat/HomeChatInput.tsx
0 → 100644
浏览文件 @
b2e3631a
import
{
SendIcon
,
StopCircleIcon
,
X
}
from
"lucide-react"
;
import
type
React
from
"react"
;
import
{
useEffect
,
useRef
,
useState
}
from
"react"
;
import
{
ModelPicker
}
from
"@/components/ModelPicker"
;
import
{
useSettings
}
from
"@/hooks/useSettings"
;
import
{
homeChatInputValueAtom
}
from
"@/atoms/chatAtoms"
;
// Use a different atom for home input
import
{
useAtom
}
from
"jotai"
;
import
{
useStreamChat
}
from
"@/hooks/useStreamChat"
;
export
function
HomeChatInput
({
onSubmit
}:
{
onSubmit
:
()
=>
void
})
{
const
[
inputValue
,
setInputValue
]
=
useAtom
(
homeChatInputValueAtom
);
const
textareaRef
=
useRef
<
HTMLTextAreaElement
>
(
null
);
const
{
settings
,
updateSettings
,
isAnyProviderSetup
}
=
useSettings
();
const
{
streamMessage
,
isStreaming
,
setIsStreaming
}
=
useStreamChat
();
// eslint-disable-line @typescript-eslint/no-unused-vars
const
adjustHeight
=
()
=>
{
const
textarea
=
textareaRef
.
current
;
if
(
textarea
)
{
textarea
.
style
.
height
=
"0px"
;
const
scrollHeight
=
textarea
.
scrollHeight
;
textarea
.
style
.
height
=
`
${
scrollHeight
+
4
}
px`
;
}
};
useEffect
(()
=>
{
adjustHeight
();
},
[
inputValue
]);
const
handleKeyPress
=
(
e
:
React
.
KeyboardEvent
)
=>
{
if
(
e
.
key
===
"Enter"
&&
!
e
.
shiftKey
)
{
e
.
preventDefault
();
onSubmit
();
}
};
if
(
!
settings
)
{
return
null
;
// Or loading state
}
return
(
<>
<
div
className=
"p-4"
>
<
div
className=
"flex flex-col space-y-2 border border-border rounded-lg bg-(--background-lighter) shadow-sm"
>
<
div
className=
"flex items-start space-x-2 "
>
<
textarea
ref=
{
textareaRef
}
value=
{
inputValue
}
onChange=
{
(
e
)
=>
setInputValue
(
e
.
target
.
value
)
}
onKeyPress=
{
handleKeyPress
}
placeholder=
"Ask Dyad to build..."
className=
"flex-1 p-2 focus:outline-none overflow-y-auto min-h-[40px] max-h-[200px]"
style=
{
{
resize
:
"none"
}
}
disabled=
{
isStreaming
}
// Should ideally reflect if *any* stream is happening
/>
{
isStreaming
?
(
<
button
className=
"px-2 py-2 mt-1 mr-2 text-(--sidebar-accent-fg) rounded-lg opacity-50 cursor-not-allowed"
// Indicate disabled state
title=
"Cancel generation (unavailable here)"
>
<
StopCircleIcon
size=
{
20
}
/>
</
button
>
)
:
(
<
button
onClick=
{
onSubmit
}
disabled=
{
!
inputValue
.
trim
()
||
!
isAnyProviderSetup
()
}
className=
"px-2 py-2 mt-1 mr-2 hover:bg-(--background-darkest) text-(--sidebar-accent-fg) rounded-lg disabled:opacity-50"
title=
"Start new chat"
>
<
SendIcon
size=
{
20
}
/>
</
button
>
)
}
</
div
>
<
div
className=
"px-2 pb-2"
>
<
ModelPicker
selectedModel=
{
settings
.
selectedModel
}
onModelSelect=
{
(
model
)
=>
updateSettings
({
selectedModel
:
model
})
}
/>
</
div
>
</
div
>
</
div
>
</>
);
}
src/hooks/useProposal.ts
浏览文件 @
b2e3631a
import
{
useState
,
useEffect
,
useCallback
}
from
"react"
;
import
{
IpcClient
}
from
"@/ipc/ipc_client"
;
import
type
{
Proposal
}
from
"@/lib/schemas"
;
// Import Proposal type
// Define the structure returned by the IPC call
interface
ProposalResult
{
proposal
:
Proposal
;
messageId
:
number
;
}
import
type
{
Proposal
,
ProposalResult
}
from
"@/lib/schemas"
;
// Import Proposal type
import
{
proposalResultAtom
}
from
"@/atoms/proposalAtoms"
;
import
{
useAtom
}
from
"jotai"
;
export
function
useProposal
(
chatId
?:
number
|
undefined
)
{
const
[
proposal
Data
,
setProposalData
]
=
useState
<
ProposalResult
|
null
>
(
null
);
const
[
proposal
Result
,
setProposalResult
]
=
useAtom
(
proposalResultAtom
);
const
[
isLoading
,
setIsLoading
]
=
useState
<
boolean
>
(
false
);
const
[
error
,
setError
]
=
useState
<
string
|
null
>
(
null
);
const
fetchProposal
=
useCallback
(
async
(
innerChatId
?:
number
)
=>
{
chatId
=
chatId
??
innerChatId
;
console
.
log
(
"fetching proposal for chatId"
,
chatId
);
if
(
chatId
===
undefined
)
{
setProposal
Data
(
null
);
setProposal
Result
(
null
);
setIsLoading
(
false
);
setError
(
null
);
return
;
}
setIsLoading
(
true
);
setError
(
null
);
setProposal
Data
(
null
);
// Reset on new fetch
setProposal
Result
(
null
);
// Reset on new fetch
try
{
// Type assertion might be needed depending on how IpcClient is typed
const
result
=
(
await
IpcClient
.
getInstance
().
getProposal
(
...
...
@@ -32,14 +28,14 @@ export function useProposal(chatId?: number | undefined) {
))
as
ProposalResult
|
null
;
if
(
result
)
{
setProposal
Data
(
result
);
setProposal
Result
(
result
);
}
else
{
setProposal
Data
(
null
);
// Explicitly set to null if IPC returns null
setProposal
Result
(
null
);
// Explicitly set to null if IPC returns null
}
}
catch
(
err
:
any
)
{
console
.
error
(
"Error fetching proposal:"
,
err
);
setError
(
err
.
message
||
"Failed to fetch proposal"
);
setProposal
Data
(
null
);
// Clear proposal data on error
setProposal
Result
(
null
);
// Clear proposal data on error
}
finally
{
setIsLoading
(
false
);
}
...
...
@@ -64,8 +60,8 @@ export function useProposal(chatId?: number | undefined) {
);
return
{
proposal
:
proposal
Data
?.
proposal
??
null
,
messageId
:
proposal
Data
?.
messageId
,
proposal
:
proposal
Result
?.
proposal
??
null
,
messageId
:
proposal
Result
?.
messageId
,
isLoading
,
error
,
refreshProposal
,
// Expose the refresh function
...
...
src/hooks/useStreamChat.ts
浏览文件 @
b2e3631a
...
...
@@ -47,7 +47,6 @@ export function useStreamChat() {
}
setError
(
null
);
console
.
log
(
"streaming message - set messages"
,
prompt
);
setMessages
((
currentMessages
:
Message
[])
=>
{
if
(
redo
)
{
let
remainingMessages
=
currentMessages
.
slice
();
...
...
@@ -92,10 +91,10 @@ export function useStreamChat() {
if
(
response
.
updatedFiles
)
{
setIsPreviewOpen
(
true
);
}
refreshProposal
(
chatId
);
// Keep the same as below
setIsStreaming
(
false
);
refreshProposal
(
chatId
);
refreshChats
();
refreshApp
();
refreshVersions
();
...
...
src/ipc/handlers/chat_stream_handlers.ts
浏览文件 @
b2e3631a
...
...
@@ -285,13 +285,13 @@ export function registerChatStreamHandlers() {
chatId
:
req
.
chatId
,
updatedFiles
:
status
.
updatedFiles
??
false
,
}
satisfies
ChatResponseEnd
);
}
}
else
{
event
.
sender
.
send
(
"chat:response:end"
,
{
chatId
:
req
.
chatId
,
updatedFiles
:
false
,
}
satisfies
ChatResponseEnd
);
}
}
// Return the chat ID for backwards compatibility
return
req
.
chatId
;
...
...
src/ipc/ipc_client.ts
浏览文件 @
b2e3631a
...
...
@@ -17,15 +17,9 @@ import type {
Message
,
Version
,
}
from
"./ipc_types"
;
import
type
{
Proposal
}
from
"@/lib/schemas"
;
import
type
{
Proposal
,
ProposalResult
}
from
"@/lib/schemas"
;
import
{
showError
}
from
"@/lib/toast"
;
// Define the structure returned by getProposal
interface
ProposalResult
{
proposal
:
Proposal
;
messageId
:
number
;
}
export
interface
ChatStreamCallbacks
{
onUpdate
:
(
messages
:
Message
[])
=>
void
;
onEnd
:
(
response
:
ChatResponseEnd
)
=>
void
;
...
...
src/lib/schemas.ts
浏览文件 @
b2e3631a
...
...
@@ -117,3 +117,8 @@ export interface Proposal {
securityRisks
:
SecurityRisk
[];
filesChanged
:
FileChange
[];
}
export
interface
ProposalResult
{
proposal
:
Proposal
;
messageId
:
number
;
}
src/pages/home.tsx
浏览文件 @
b2e3631a
import
{
useNavigate
,
useSearch
}
from
"@tanstack/react-router"
;
import
{
useAtom
,
useSetAtom
}
from
"jotai"
;
import
{
c
hatInputValueAtom
}
from
"../atoms/chatAtoms"
;
import
{
homeC
hatInputValueAtom
}
from
"../atoms/chatAtoms"
;
import
{
selectedAppIdAtom
}
from
"@/atoms/appAtoms"
;
import
{
IpcClient
}
from
"@/ipc/ipc_client"
;
import
{
generateCuteAppName
}
from
"@/lib/utils"
;
import
{
useLoadApps
}
from
"@/hooks/useLoadApps"
;
import
{
useSettings
}
from
"@/hooks/useSettings"
;
import
{
SetupBanner
}
from
"@/components/SetupBanner"
;
import
{
ChatInput
}
from
"@/components/chat/ChatInput"
;
import
{
isPreviewOpenAtom
}
from
"@/atoms/viewAtoms"
;
import
{
useState
,
useEffect
}
from
"react"
;
import
{
useStreamChat
}
from
"@/hooks/useStreamChat"
;
import
{
HomeChatInput
}
from
"@/components/chat/HomeChatInput"
;
export
default
function
HomePage
()
{
const
[
inputValue
,
setInputValue
]
=
useAtom
(
c
hatInputValueAtom
);
const
[
inputValue
,
setInputValue
]
=
useAtom
(
homeC
hatInputValueAtom
);
const
navigate
=
useNavigate
();
const
search
=
useSearch
({
from
:
"/"
});
const
setSelectedAppId
=
useSetAtom
(
selectedAppIdAtom
);
...
...
@@ -87,7 +86,7 @@ export default function HomePage() {
<
SetupBanner
/>
<
div
className=
"w-full"
>
<
ChatInput
onSubmit=
{
handleSubmit
}
/>
<
Home
ChatInput
onSubmit=
{
handleSubmit
}
/>
<
div
className=
"flex flex-wrap gap-4 mt-4"
>
{
[
...
...
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论